From c619576835212198fb2f0ff84f53d84b9e47c782 Mon Sep 17 00:00:00 2001 From: REPPL Date: Wed, 12 Aug 2026 12:29:27 +0200 Subject: [PATCH 1/2] fix: print the one instruction in a form that runs in the state it describes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit hooks/bootstrap.sh's success notice and README.md both said to run `abcd ahoy install` once. The whole premise of that sentence is that `abcd` is not a name the shell can resolve — putting it there is what the command does — so the instruction failed with "command not found" for precisely the reader it was written for. On the first manual plugin install the consequence was not cosmetic: the agent reading the notice could not run the printed command, invented `cd && go run ./cmd/abcd ahoy install`, and told the user to run that instead — a source-build path needing a Go toolchain, reaching into the harness's plugin cache, and not the documented install at all. The script already holds the absolute path as $binary, so the notice prints it: the instruction becomes `"" ahoy install`, resolvable right now, by anyone, with no toolchain. The invocation is last on the line so it stays copy-pasteable, and the provenance parenthetical moves ahead of it rather than trailing the command. A2's contract is preserved — the success still leads the first word of the first stderr line, which is all the transcript renders. README.md carries the same invocation with the one part a committed file cannot know left as a placeholder the notice fills in. `${CLAUDE_PLUGIN_ROOT}` is deliberately NOT used there: it is bound for a hook and unset in the terminal the sentence is addressed to, which would reproduce the same defect in a new shape. CI holds both surfaces: every occurrence of `ahoy install` the notice or the README prints must be reached through a path rather than a bare name — an every-occurrence check, because a text can carry the runnable form and still leave the unrunnable one standing beside it. Reading it on a real plugin cache stays the manual install gate; the plugin root and the harness are not present in CI. Resolves iss-207. Assisted-by: Claude:claude-opus-5[1m] --- ...hat-resolves-the-no-binary-on-path-stat.md | 2 + CHANGELOG.md | 16 +++ README.md | 8 +- hooks/bootstrap.sh | 15 ++- internal/surface/cli/bootstrap_test.go | 97 +++++++++++++++++++ 5 files changed, 134 insertions(+), 4 deletions(-) rename .abcd/work/issues/{open => resolved}/iss-207-the-one-instruction-that-resolves-the-no-binary-on-path-stat.md (84%) diff --git a/.abcd/work/issues/open/iss-207-the-one-instruction-that-resolves-the-no-binary-on-path-stat.md b/.abcd/work/issues/resolved/iss-207-the-one-instruction-that-resolves-the-no-binary-on-path-stat.md similarity index 84% rename from .abcd/work/issues/open/iss-207-the-one-instruction-that-resolves-the-no-binary-on-path-stat.md rename to .abcd/work/issues/resolved/iss-207-the-one-instruction-that-resolves-the-no-binary-on-path-stat.md index 1ac95bb5..e3d3e90a 100644 --- a/.abcd/work/issues/open/iss-207-the-one-instruction-that-resolves-the-no-binary-on-path-stat.md +++ b/.abcd/work/issues/resolved/iss-207-the-one-instruction-that-resolves-the-no-binary-on-path-stat.md @@ -7,6 +7,8 @@ category: "documentation" source: "user-observation" found_during: "first manual plugin install test (2026-08-10)" found_at: "hooks/bootstrap.sh" +resolution: "The bootstrap notice and the README now name the plugin-root binary by path, so the one-time PATH setup instruction runs in the no-binary-on-PATH state it addresses; a bare abcd ahoy install on either surface is held by test." +impact: fix --- The one instruction that resolves the no-binary-on-PATH state cannot be run in that state. hooks/bootstrap.sh's success notice ends 'For the abcd command in your own terminal, run `abcd ahoy install` once.' and README.md repeats it: 'run `abcd ahoy install` once to put the plugin-root binary on your PATH.' But abcd is not on PATH — that is exactly the condition the sentence addresses — so the command as written fails with 'command not found'. Observed consequence on the first manual install (2026-08-10): the agent, unable to run the printed command, invented 'cd ~/.claude/plugins/marketplaces/abcd-marketplace && go run ./cmd/abcd ahoy install' and told the user to run that instead — a source-build instruction reaching into the harness's plugin cache, which is not the documented install path and requires a Go toolchain. Fix direction: print the absolute plugin-root path in the notice (the script already holds it as $binary) and give README the same concrete form, so the sentence is copy-pasteable in the state it describes. Related to iss-205, which is the same root confusion on the command surface. \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ed75fd6..b105d5ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -86,6 +86,22 @@ called out in a **Breaking** section. ### Fixed +- **The one instruction that resolves the no-binary-on-`PATH` state can now be run + in that state** (iss-207). The bootstrap's success notice and the README both + said to run `abcd ahoy install` once — a command whose whole premise is that + `abcd` is not a name the shell can resolve, so it failed with "command not + found" for precisely the reader it was written for. On the first manual install + the consequence was not cosmetic: the agent reading the notice could not run the + printed command, invented a `go run` incantation reaching into the harness's + plugin cache, and told the user to run that instead — a source-build path + needing a Go toolchain, and not the documented install at all. The notice now + prints the absolute plugin-root path the script already holds, with the + invocation last on the line so it stays copy-pasteable; the README carries the + same form with the one part a committed file cannot know left as a placeholder + the notice fills in. CI holds both surfaces — every `ahoy install` the notice or + the README prints must be reached through a path, not a bare name — while the + end-to-end reading of it on a real plugin cache remains the manual install gate. + - **The session-start hooks run after the bootstrap that provisions their binary, and a successful install reads as success** (iss-204, iss-208). The hook manifest listed the bootstrap and the two binary-backed commands as three diff --git a/README.md b/README.md index a6df01a9..8731f20d 100644 --- a/README.md +++ b/README.md @@ -233,8 +233,12 @@ and arm64) installs nothing and says why in plain language. A plugin root that already holds the binary costs one file test and no network. That covers the hooks. For the `abcd` command in your own terminal, keep the -[install](#install) above, or run `abcd ahoy install` once to put the -plugin-root binary on your `PATH`. For a stronger root of trust than +[install](#install) above, or put the plugin-root binary on your `PATH` by +running it once by its absolute path — `"/abcd" ahoy install`, +where `` is the directory named in the bootstrap's own success +notice, which prints the path in full. The path is absolute because `abcd` is +not on your `PATH` yet, which is what that one run fixes. For a stronger root +of trust than same-origin checksums, build from source — `go build ./cmd/abcd` — and place the binary in the plugin root and on your `PATH` yourself. A binary placed there by hand takes the same no-network fast path, so the `.binary-meta` diff --git a/hooks/bootstrap.sh b/hooks/bootstrap.sh index 5c9481a7..bccd130e 100755 --- a/hooks/bootstrap.sh +++ b/hooks/bootstrap.sh @@ -271,5 +271,16 @@ fi # The one place PATH setup is suggested; the symlink itself stays owned by ahoy. # This prints once per plugin root, because every later session takes the fast # path above. -notice "$(printf 'abcd bootstrap: installed the checksum-verified abcd binary (release %s) into the plugin root, so the abcd hooks are live for this session. For the abcd command in your own terminal, run `abcd ahoy install` once.%s' \ - "$release_tag" "$meta_note")" +# +# The instruction names the binary by the ABSOLUTE path this script already +# holds, and does so for a reason worth stating: the sentence is addressed to a +# reader for whom `abcd` is not a name the shell can resolve — that is the whole +# condition it exists to fix — so an instruction reading `abcd ahoy install` +# fails with "command not found" for everyone who needs it. On the first manual +# install the agent that read it invented a `go run` incantation into the +# harness's plugin cache instead (iss-207). $binary is resolvable right now, by +# anyone, with no toolchain. Keep the invocation LAST on the line so it stays +# copy-pasteable, and keep the success leading the first word: only the first +# line of a hook's stderr reaches the transcript. +notice "$(printf 'abcd bootstrap: installed the checksum-verified abcd binary (release %s) into the plugin root, so the abcd hooks are live for this session.%s For the abcd command in your own terminal, run this once — the path is absolute because abcd is not on your PATH yet, which is exactly what the command fixes: "%s" ahoy install' \ + "$release_tag" "$meta_note" "$binary")" diff --git a/internal/surface/cli/bootstrap_test.go b/internal/surface/cli/bootstrap_test.go index e6c64c9c..8009782b 100644 --- a/internal/surface/cli/bootstrap_test.go +++ b/internal/surface/cli/bootstrap_test.go @@ -411,6 +411,103 @@ func TestBootstrapInstallsVerifiedBinary(t *testing.T) { } } +// bootstrapRepoFile locates a committed file at the repository root from this +// test file's own on-disk position, the same way bootstrapScript does, so the +// assertions below read the bytes that actually ship. +func bootstrapRepoFile(t *testing.T, rel string) string { + t.Helper() + _, file, _, ok := runtime.Caller(0) + if !ok { + t.Fatal("runtime.Caller failed to locate the test source file") + } + path := filepath.Clean(filepath.Join(filepath.Dir(file), "..", "..", "..", rel)) + if _, err := os.Stat(path); err != nil { + t.Fatalf("the committed %s must exist: %v", rel, err) + } + return path +} + +// bootstrapReadmeInstruction is the shape the README's one-time PATH setup +// instruction has to take. The README cannot know the absolute plugin root at +// authoring time, so it carries the same INVOCATION with the one part it cannot +// know left as a placeholder the notice fills in — not a bare `abcd`, which is +// the name that does not resolve, and not `${CLAUDE_PLUGIN_ROOT}` either, which +// is set for a hook and unset in the terminal the sentence is addressed to. +const bootstrapReadmeInstruction = `"/abcd" ahoy install` + +// bootstrapEveryInvocationIsPathQualified reports each occurrence of `ahoy +// install` in body that is NOT reached through prefix. Splitting on the verb +// rather than searching for the good form is deliberate: a text may hold the +// runnable instruction AND still leave the unrunnable one standing beside it, +// and only an every-occurrence check sees the second one. +func bootstrapEveryInvocationIsPathQualified(body, prefix string) []string { + parts := strings.Split(body, "ahoy install") + var bad []string + for _, before := range parts[:len(parts)-1] { + if strings.HasSuffix(before, prefix) { + continue + } + if len(before) > 80 { + before = "…" + before[len(before)-80:] + } + bad = append(bad, before+"ahoy install") + } + return bad +} + +// TestBootstrapPrintsARunnableInstruction is iss-207: the ONE instruction that +// resolves the no-binary-on-PATH state could not be run in that state. The +// notice said "run `abcd ahoy install` once" while `abcd` is, by the notice's +// own premise, not a name the shell can resolve — so the sentence fails with +// "command not found" for exactly the reader it is written for. On the first +// manual install this was not a cosmetic failure: the agent reading it could not +// run the printed command, invented a `go run` incantation reaching into the +// harness's plugin cache, and told the user to run that instead — a +// source-build path needing a Go toolchain, which is not the documented install +// at all. +// +// The script already holds the absolute path as $binary, so the fix is to print +// it. Both surfaces are held here: the notice, whose path is concrete, and the +// README, whose corresponding sentence carries the same invocation with a +// placeholder for the one part a committed file cannot know. +func TestBootstrapPrintsARunnableInstruction(t *testing.T) { + t.Run("the notice names the absolute binary", func(t *testing.T) { + root := bootstrapRoot(t) + body := []byte("payload") + fx := bootstrapServer(t, body, bootstrapManifest(body)) + + out, code := runBootstrap(t, root, fx, "") + if code != 2 { + t.Fatalf("a verified download must exit 2 (a notice the user is shown), got %d (output %q)", code, out) + } + binary := filepath.Join(root, "abcd") + prefix := `"` + binary + `" ` + if !strings.Contains(out, prefix+"ahoy install") { + t.Errorf("the notice must print the absolute plugin-root binary the script already holds, so the instruction runs in the state it describes; want %q in output %q", prefix+"ahoy install", out) + } + for _, bad := range bootstrapEveryInvocationIsPathQualified(out, prefix) { + t.Errorf("the notice still prints an `ahoy install` a reader cannot run — `abcd` is not on PATH, which is the very state this sentence addresses: %q", bad) + } + // A2's contract, held here because this test rewrites the same sentence: + // the transcript renders only the first line of a hook's stderr, so the + // success has to lead it. + if got := firstLine(out); !strings.HasPrefix(got, "abcd bootstrap: installed") { + t.Errorf("the success must still lead the first visible line; first line = %q", got) + } + }) + + t.Run("the README carries the same concrete form", func(t *testing.T) { + body := mustReadFile(t, bootstrapRepoFile(t, "README.md")) + if !strings.Contains(body, bootstrapReadmeInstruction) { + t.Errorf("README.md must give the one-time PATH setup as %q; a bare `abcd ahoy install` cannot be run by the reader it is written for", bootstrapReadmeInstruction) + } + prefix := `"/abcd" ` + for _, bad := range bootstrapEveryInvocationIsPathQualified(body, prefix) { + t.Errorf("README.md still instructs an `ahoy install` that cannot be run before `abcd` is on PATH: %q", bad) + } + }) +} + // TestBootstrapRecordsTheRawPluginRootBasename is the observability half of the // commit-stamped-cache WARRANT. plugin_sha is gated to exactly forty lowercase // hex characters because itd-105 assumes the harness names each plugin cache From 1ea7ecd02c898ad8f7114dc09418e5f5cf48091f Mon Sep 17 00:00:00 2001 From: REPPL Date: Wed, 12 Aug 2026 13:04:07 +0200 Subject: [PATCH 2/2] fix: make the placeholder instantiable and the printed command shell-proof MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three review findings on the iss-207 change. The README defined `` solely by reference to the bootstrap's success notice — but that notice prints once per plugin root, because every later session takes the fast path, and nothing else in README.md or docs/ names the plugin root or its location. The reader who still has the notice on screen already has the whole command; the reader who does not could not instantiate the placeholder at all. It also pointed at the wrong thing: the notice prints the BINARY path, not the directory. The paragraph now says what `` is in host-agnostic terms — the directory the agent harness unpacked the plugin into, binary directly inside as `abcd` — notes that copying the command out of the notice is the short route, and sends a reader who cannot instantiate it to the install one-liner, which needs no plugin root. Naming the conventional cache path is not available here: docs-lint's harness/* rules block naming a specific harness in user-facing prose, which is the right trade. The same insertion had left a ragged re-wrap, a 13-character orphan line between two full-width ones. The paragraph is re-flowed and split at the change of subject. The printed command wrapped the path in DOUBLE quotes, which survives a space or an apostrophe and then leaves `$`, a backtick and a `"` live — a plugin root holding one of those would expand, substitute, or terminate the string on paste, on the one line whose entire job is to be pasted. It now uses the single-quoted form the repo already owns for exactly this (internal/core/ahoy's shSingleQuote, mirrored in POSIX sh), rewriting each embedded `'` as `'\''`. The README shows the same shape, so the two surfaces do not hand the reader two different spellings of one command. The new case does not compare strings: it installs into a plugin root whose name carries an apostrophe, a $HOME, double quotes and a backtick pair, then hands the command the notice printed to a real `sh` and requires it to run the binary at that path. Watched fail against the double-quoted form, where the pasted command expanded $HOME and executed the backticks. The one-line shape and A2's success-leads-the-first-line contract are unchanged and still asserted. Assisted-by: Claude:claude-opus-5[1m] --- CHANGELOG.md | 17 ++++--- README.md | 29 +++++++----- hooks/bootstrap.sh | 13 +++++- internal/surface/cli/bootstrap_test.go | 62 ++++++++++++++++++++++++-- 4 files changed, 99 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b105d5ff..6572159e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -95,12 +95,17 @@ called out in a **Breaking** section. printed command, invented a `go run` incantation reaching into the harness's plugin cache, and told the user to run that instead — a source-build path needing a Go toolchain, and not the documented install at all. The notice now - prints the absolute plugin-root path the script already holds, with the - invocation last on the line so it stays copy-pasteable; the README carries the - same form with the one part a committed file cannot know left as a placeholder - the notice fills in. CI holds both surfaces — every `ahoy install` the notice or - the README prints must be reached through a path, not a bare name — while the - end-to-end reading of it on a real plugin cache remains the manual install gate. + prints the absolute plugin-root path the script already holds, shell-quoted so + a plugin root containing a space, an apostrophe, a `$` or a backtick still + pastes as one word, and with the invocation last on the line so it stays + copy-pasteable to the end. The README carries the same form with the one part a + committed file cannot know left as a placeholder, says what that placeholder is + in host-agnostic terms, and points a reader who cannot instantiate it at the + install one-liner, which needs no plugin root. CI holds both surfaces — every + `ahoy install` either one prints must be reached through a path, not a bare + name, and the printed command is handed to a real shell against a hostile path + to prove it runs as pasted — while the end-to-end reading of it on a real + plugin cache remains the manual install gate. - **The session-start hooks run after the bootstrap that provisions their binary, and a successful install reads as success** (iss-204, iss-208). The hook diff --git a/README.md b/README.md index 8731f20d..edf361e9 100644 --- a/README.md +++ b/README.md @@ -234,17 +234,24 @@ already holds the binary costs one file test and no network. That covers the hooks. For the `abcd` command in your own terminal, keep the [install](#install) above, or put the plugin-root binary on your `PATH` by -running it once by its absolute path — `"/abcd" ahoy install`, -where `` is the directory named in the bootstrap's own success -notice, which prints the path in full. The path is absolute because `abcd` is -not on your `PATH` yet, which is what that one run fixes. For a stronger root -of trust than -same-origin checksums, build from source — `go build ./cmd/abcd` — and place -the binary in the plugin root and on your `PATH` yourself. A binary placed -there by hand takes the same no-network fast path, so the `.binary-meta` -provenance record beside it still describes whichever release the bootstrap -last fetched: delete that file so no version-skew notice is rendered from a -release the binary in place did not come from. +running it once by its absolute path — `'/abcd' ahoy install`. +The path is absolute because `abcd` is not on your `PATH` yet, which is what +that one run fixes. `` is the directory the agent harness unpacked +the abcd plugin into, with the binary sitting directly inside it as `abcd`; the +bootstrap's success notice prints that full binary path, so the shortest route +is to copy the command straight out of the notice. That notice appears once per +plugin root — later sessions take the fast path and stay silent — so if it has +scrolled away and you would rather not go looking for the directory, the +[install](#install) one-liner above needs no plugin root at all and gets you to +the same place. + +For a stronger root of trust than same-origin checksums, build from source — +`go build ./cmd/abcd` — and place the binary in the plugin root and on your +`PATH` yourself. A binary placed there by hand takes the same no-network fast +path, so the `.binary-meta` provenance record beside it still describes +whichever release the bootstrap last fetched: delete that file so no +version-skew notice is rendered from a release the binary in place did not come +from. ## Build diff --git a/hooks/bootstrap.sh b/hooks/bootstrap.sh index bccd130e..44f47602 100755 --- a/hooks/bootstrap.sh +++ b/hooks/bootstrap.sh @@ -282,5 +282,14 @@ fi # anyone, with no toolchain. Keep the invocation LAST on the line so it stays # copy-pasteable, and keep the success leading the first word: only the first # line of a hook's stderr reaches the transcript. -notice "$(printf 'abcd bootstrap: installed the checksum-verified abcd binary (release %s) into the plugin root, so the abcd hooks are live for this session.%s For the abcd command in your own terminal, run this once — the path is absolute because abcd is not on your PATH yet, which is exactly what the command fixes: "%s" ahoy install' \ - "$release_tag" "$meta_note" "$binary")" +# +# The path is wrapped in SINGLE quotes, the same form internal/core/ahoy's +# shSingleQuote produces, because this string is printed to be pasted into a +# shell and the plugin root is not a path this script chose. Double quotes carry +# a space or an apostrophe safely and then leave $, a backtick and a " live: a +# path holding one of those would expand, substitute, or terminate the string on +# paste. The sed rewrites each embedded ' as '\'' — close, escape, reopen — which +# is the one form that survives every byte a path can contain. +binary_quoted="'$(printf '%s' "$binary" | sed "s/'/'\\\\''/g")'" +notice "$(printf 'abcd bootstrap: installed the checksum-verified abcd binary (release %s) into the plugin root, so the abcd hooks are live for this session.%s For the abcd command in your own terminal, run this once — the path is absolute because abcd is not on your PATH yet, which is exactly what the command fixes: %s ahoy install' \ + "$release_tag" "$meta_note" "$binary_quoted")" diff --git a/internal/surface/cli/bootstrap_test.go b/internal/surface/cli/bootstrap_test.go index 8009782b..11b7128f 100644 --- a/internal/surface/cli/bootstrap_test.go +++ b/internal/surface/cli/bootstrap_test.go @@ -433,7 +433,19 @@ func bootstrapRepoFile(t *testing.T, rel string) string { // know left as a placeholder the notice fills in — not a bare `abcd`, which is // the name that does not resolve, and not `${CLAUDE_PLUGIN_ROOT}` either, which // is set for a hook and unset in the terminal the sentence is addressed to. -const bootstrapReadmeInstruction = `"/abcd" ahoy install` +// Single quotes, matching the bytes the notice prints, so the two surfaces do +// not show the reader two different shapes of the same command. +const bootstrapReadmeInstruction = `'/abcd' ahoy install` + +// bootstrapShSingleQuote mirrors internal/core/ahoy's shSingleQuote: the POSIX +// single-quote wrapping that survives every character a path can hold. It is +// restated here rather than imported (it is unexported, in another package) so +// this test states the bytes it EXPECTS independently of the code that produces +// them — a shared helper would agree with the script by construction and prove +// nothing about it. +func bootstrapShSingleQuote(s string) string { + return "'" + strings.ReplaceAll(s, "'", `'\''`) + "'" +} // bootstrapEveryInvocationIsPathQualified reports each occurrence of `ahoy // install` in body that is NOT reached through prefix. Splitting on the verb @@ -481,7 +493,7 @@ func TestBootstrapPrintsARunnableInstruction(t *testing.T) { t.Fatalf("a verified download must exit 2 (a notice the user is shown), got %d (output %q)", code, out) } binary := filepath.Join(root, "abcd") - prefix := `"` + binary + `" ` + prefix := bootstrapShSingleQuote(binary) + " " if !strings.Contains(out, prefix+"ahoy install") { t.Errorf("the notice must print the absolute plugin-root binary the script already holds, so the instruction runs in the state it describes; want %q in output %q", prefix+"ahoy install", out) } @@ -501,10 +513,54 @@ func TestBootstrapPrintsARunnableInstruction(t *testing.T) { if !strings.Contains(body, bootstrapReadmeInstruction) { t.Errorf("README.md must give the one-time PATH setup as %q; a bare `abcd ahoy install` cannot be run by the reader it is written for", bootstrapReadmeInstruction) } - prefix := `"/abcd" ` + prefix := `'/abcd' ` for _, bad := range bootstrapEveryInvocationIsPathQualified(body, prefix) { t.Errorf("README.md still instructs an `ahoy install` that cannot be run before `abcd` is on PATH: %q", bad) } + // The placeholder has to be instantiable from THIS file. The notice that + // prints the path in full prints once per plugin root (every later session + // takes the bootstrap's fast path), so a reader who meets this sentence on + // day five has no notice on screen — and the plugin root's location cannot + // be named here, because abcd's published prose stays host-agnostic and + // docs-lint blocks naming a specific harness. So the README owes two + // things: what the placeholder IS in terms the reader can act on, and a + // route that needs no plugin root at all. + if !strings.Contains(body, "directory the agent harness unpacked") { + t.Error("README.md must say what is in host-agnostic terms; a placeholder defined only as \"what the notice printed\" cannot be instantiated once the notice has scrolled away") + } + if !strings.Contains(body, "needs no plugin root") { + t.Error("README.md must point the reader who cannot instantiate at the install route that does not need one") + } + }) + + // The printed command is not merely path-qualified, it is QUOTED for a shell, + // and the double-quoted form this started as survives a space or an apostrophe + // while `$`, a backtick or a `"` inside the path still expand or terminate the + // string. The repo already owns the robust form (internal/core/ahoy's + // shSingleQuote), so the notice uses it. The proof is not a string comparison: + // the printed command is handed to a real `sh`, which either runs the binary + // at that path or does not. + t.Run("the printed command runs when pasted, whatever the path holds", func(t *testing.T) { + // Every metacharacter that defeats double quoting, plus the apostrophe + // that defeats naive single quoting, in one directory name. + root := bootstrapRootNamed(t, `it's $HOME "and" `+"`backticks`") + body := []byte("#!/bin/sh\nexit 0\n") + fx := bootstrapServer(t, body, bootstrapManifest(body)) + + out, code := runBootstrap(t, root, fx, "") + if code != 2 { + t.Fatalf("a verified download must exit 2 (a notice the user is shown), got %d (output %q)", code, out) + } + command := bootstrapShSingleQuote(filepath.Join(root, "abcd")) + " ahoy install" + if !strings.Contains(out, " "+command) { + t.Fatalf("the notice must print the shell-quoted command as its own word; want %q in output %q", command, out) + } + // The installed fixture binary is `exit 0`, so a shell that resolves the + // path runs it and exits 0. A path that broke out of its quoting instead + // yields "command not found", a syntax error, or an unrelated exit. + if err := exec.Command("sh", "-c", command).Run(); err != nil { + t.Errorf("the command the notice printed does not run when pasted into a shell: %v (command %q)", err, command) + } }) }