Skip to content

Commit b1a76d6

Browse files
authored
Merge pull request #6 from sahil87/260516-perf-drop-brew-from-shell-init-version
perf: drop brew probe from shell-init and version
2 parents 20ca522 + 47e7d48 commit b1a76d6

7 files changed

Lines changed: 65 additions & 47 deletions

File tree

docs/memory/cli/commands.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ Roster invariants:
7575
| `main.go` | Entry point, version variable, `translateExit`, `errSilent`, `errExitCode`. |
7676
| `root.go` | `newRootCmd()` — cobra root with three subcommands wired in. |
7777
| `tools.go` | `Tool` struct, `Roster`, `formulaPrefix`, `shellPlaceholder`. |
78-
| `brew.go` | Shared brew helpers used by every subcommand: `hasBrew`, `isInstalled`, `brewBinary`, `brewMissingHint`, `installBrewMissingHint`, `shllFormula`. See [update](update.md) for details. |
78+
| `brew.go` | Shared brew helpers used by the brew-coupled subcommands (`install`, `update`): `hasBrew`, `isInstalled`, `brewBinary`, `brewMissingHint`, `installBrewMissingHint`, `shllFormula`. `shell-init` and `version` are install-mechanism agnostic and do NOT consult brew — they detect runnable tools via `proc.ErrNotFound` from the sub-tool invocation itself. See [update](update.md) for details. |
7979
| `install.go` | `newInstallCmd()` + `runInstall`. See [install](install.md). |
8080
| `update.go` | `newUpdateCmd()` + `runUpdate`. See [update](update.md). |
8181
| `shell_init.go` | `newShellInitCmd()` + `runShellInit`. See [shell-init](shell-init.md). |

docs/memory/cli/shell-init.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
`shll shell-init <shell>` — emits a single concatenated shell-init blob composed from every installed roster tool with shell integration.
44

5-
Source: `src/cmd/shll/shell_init.go`. Uses the shared brew helpers in `src/cmd/shll/brew.go` and the `Roster` from `src/cmd/shll/tools.go`.
5+
Source: `src/cmd/shll/shell_init.go`. Uses the `Roster` from `src/cmd/shll/tools.go`. Does **not** call brew — install-state is detected via `proc.ErrNotFound` from the sub-tool invocation itself (i.e. "is the binary on PATH?"), which is install-mechanism agnostic (brew, from-source via `just install`, etc.).
66

77
## Usage
88

@@ -15,18 +15,18 @@ A single eval line replaces what would otherwise be N per-tool eval lines (today
1515

1616
## Behavior contract
1717

18-
`runShellInit(ctx, shell, stdout, stderr)` (`src/cmd/shll/shell_init.go:65`) is the implementation seam. The cobra `RunE` wrapper handles argument validation before delegating:
18+
`runShellInit(ctx, shell, stdout, stderr)` (`src/cmd/shll/shell_init.go:74`) is the implementation seam. The cobra `RunE` wrapper handles argument validation before delegating:
1919

2020
1. **Missing shell argument.** No positional → return `errExitCode{code: 2, msg: "shll shell-init: missing shell. Supported: zsh, bash"}`. Exit code: **2**. stdout: empty.
2121

2222
2. **Unsupported shell.** Argument is not in `supportedShells = []string{"zsh", "bash"}` → return `errExitCode{code: 2, msg: ...}`. Exit code: **2**. stdout: empty.
2323

2424
3. **Composition loop.** For each tool in `Roster` (in order):
2525
- Skip if `len(tool.ShellInit) == 0` (tool has no shell integration).
26-
- Skip silently if `!isInstalled(ctx, tool.Formula)` — Constitution V (graceful degradation).
27-
- Otherwise build argv via `substituteShell(tool.ShellInit, shell)` (replaces every `"<shell>"` token with the user-supplied shell name).
26+
- Build argv via `substituteShell(tool.ShellInit, shell)` (replaces every `"<shell>"` token with the user-supplied shell name).
2827
- Run `proc.Run(ctx, argv[0], argv[1:]...)` (capture transport).
29-
- On error: write `shll shell-init: <tool>: <err>` to stderr, set `anyFailed = true`, **and skip this tool's stdout** (eval-safety — failing tool's partial output never reaches stdout). Continue with the next tool.
28+
- On `proc.ErrNotFound` (binary not on PATH): skip silently — Constitution V (graceful degradation). Install-mechanism agnostic: any source-built or non-brew install of the tool participates as long as its binary is on PATH.
29+
- On any other error: write `shll shell-init: <tool>: <err>` to stderr, set `anyFailed = true`, **and skip this tool's stdout** (eval-safety — failing tool's partial output never reaches stdout). Continue with the next tool.
3030
- On success: write the captured stdout bytes verbatim to the output writer.
3131

3232
4. **Final exit.** If `anyFailed`, return `errSilent` (exit 1). Else return nil (exit 0).
@@ -51,7 +51,7 @@ Output is concatenated in `Roster` order. This is deterministic (Spec: Compositi
5151

5252
## Argv substitution
5353

54-
`substituteShell(argv, shell)` (`src/cmd/shll/shell_init.go:98`) replaces every literal `<shell>` token with `shell`, returning a fresh slice (does not mutate the roster):
54+
`substituteShell(argv, shell)` (`src/cmd/shll/shell_init.go:107`) replaces every literal `<shell>` token with `shell`, returning a fresh slice (does not mutate the roster):
5555

5656
| Tool | Roster argv | After substitution (zsh) |
5757
|------|-------------|--------------------------|
@@ -97,7 +97,7 @@ Covered scenarios:
9797
## Cross-references
9898

9999
- Roster definition and `<shell>` placeholder: [cli/commands](commands.md#hardcoded-tool-roster).
100-
- Subprocess wrapper conventions: [internal/proc](../internal/proc.md).
101-
- Brew detection (`isInstalled`): [cli/update](update.md#detection).
100+
- Subprocess wrapper conventions: [internal/proc](../internal/proc.md) — including `proc.ErrNotFound` semantics.
101+
- Brew detection (`isInstalled`) — used by `install` and `update` only, not here: [cli/update](update.md#detection).
102102
- Rc-file installer: [cli/shell-install](shell-install.md) — wraps `shll shell-init <shell>` in an `eval` line and writes it to the user's rc file (idempotent install / `--print` dry-run / `--uninstall` removal).
103-
- Constitution V (Graceful Degradation) — uninstalled tools omitted silently.
103+
- Constitution V (Graceful Degradation) — tools not on PATH are omitted silently.

docs/memory/cli/version.md

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,16 @@ idea not installed
3030
3. For each tool in `Roster` (in order), write `<tool.Name>\t<toolVersion(ctx, tool)>\n`.
3131
4. `w.Flush()` — propagates any write error up.
3232

33-
`toolVersion(ctx, tool)` (`src/cmd/shll/version.go:68`) is the per-tool resolver:
33+
`toolVersion(ctx, tool)` (`src/cmd/shll/version.go:73`) is the per-tool resolver:
3434

35-
1. If `!isInstalled(ctx, tool.Formula)` → return `notInstalledLabel = "not installed"`.
36-
2. Else create `subCtx, cancel := context.WithTimeout(ctx, versionTimeout)`. Defer cancel.
37-
3. Run `proc.Run(subCtx, tool.Name, "--version")` (capture transport).
38-
4. On any error (transport error, exit non-zero, deadline exceeded) → return `notInstalledLabel`.
39-
5. On success → return `normalizeVersion(string(out))`.
35+
1. Create `subCtx, cancel := context.WithTimeout(ctx, versionTimeout)`. Defer cancel.
36+
2. Run `proc.Run(subCtx, tool.Name, "--version")` (capture transport).
37+
3. On any error (`proc.ErrNotFound` for missing binary, exit non-zero, deadline exceeded, etc.) → return `notInstalledLabel = "not installed"`.
38+
4. On success → return `normalizeVersion(string(out))`.
4039

41-
`normalizeVersion(raw string) string` (`src/cmd/shll/version.go:93`) is the single point of normalization shared by the shll row and every roster row. It is purely shape-based — there is no per-tool branching — so independent upstream `--version` standardization (e.g., tu/rk/fab-kit cleaning up their own output in parallel) is absorbed without shll code changes.
40+
"Installed" is detected via `proc.ErrNotFound` (binary not on PATH) rather than a brew probe — install-mechanism agnostic, and saves ~400ms per tool (no Homebrew/Ruby startup tax).
41+
42+
`normalizeVersion(raw string) string` (`src/cmd/shll/version.go:95`) is the single point of normalization shared by the shll row and every roster row. It is purely shape-based — there is no per-tool branching — so independent upstream `--version` standardization (e.g., tu/rk/fab-kit cleaning up their own output in parallel) is absorbed without shll code changes.
4243

4344
The normalization pipeline runs in this order on the input:
4445

@@ -71,7 +72,7 @@ Properties (Design Decision #5):
7172
- 2s is generous (typical `--version` runs in well under 100ms).
7273
- Bounds worst-case `shll version` runtime to `len(Roster) * versionTimeout` ≈ 12 seconds even if every tool hangs.
7374
- A timeout is treated as "not installed" — we don't differentiate hung-but-installed from missing in the output. The user gets a usable table either way.
74-
- The deadline applies only to the `--version` invocation, not to the `brew list` probe. The probe runs against the parent ctx (typically unbounded for the CLI invocation).
75+
- The deadline applies only to the `--version` invocation. There is no separate install probe — installation is inferred from `proc.ErrNotFound` returned by the same `--version` call.
7576

7677
`TestVersion_TimeoutHandling` simulates the timeout path by having the fake runner return `context.DeadlineExceeded` immediately for the targeted tool (no real wall-clock wait), then asserts the row reads `not installed` and that the test's elapsed time stays under `versionTimeout`.
7778

@@ -109,6 +110,6 @@ Unit scenarios pinning the normalization contract (12 cases, all named `TestNorm
109110

110111
## Cross-references
111112

112-
- Subprocess wrapper conventions: [internal/proc](../internal/proc.md).
113+
- Subprocess wrapper conventions: [internal/proc](../internal/proc.md) — including `proc.ErrNotFound` semantics.
113114
- Roster definition: [cli/commands](commands.md#hardcoded-tool-roster).
114-
- Brew detection (`isInstalled`): [cli/update](update.md#detection).
115+
- Brew detection (`isInstalled`) — used by `install` and `update` only, not here: [cli/update](update.md#detection).

src/cmd/shll/shell_init.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package main
22

33
import (
44
"context"
5+
"errors"
56
"fmt"
67
"io"
78

@@ -64,6 +65,11 @@ func isSupportedShell(shell string) bool {
6465
// any sub-tool error output. Eval-safety only applies to stdout.
6566
// - exit code is non-zero if any sub-tool's shell-init failed.
6667
//
68+
// "Installed" here means "runnable on PATH" — we attempt the sub-tool's
69+
// shell-init and treat proc.ErrNotFound (binary missing) as graceful skip
70+
// per Constitution V. This is independent of the install mechanism (brew,
71+
// from-source, etc.), so source-built tools are not silently dropped.
72+
//
6773
// Order is roster order (deterministic — spec requirement).
6874
func runShellInit(ctx context.Context, shell string, stdout, stderr io.Writer) error {
6975
if ctx == nil {
@@ -74,13 +80,13 @@ func runShellInit(ctx context.Context, shell string, stdout, stderr io.Writer) e
7480
if len(tool.ShellInit) == 0 {
7581
continue
7682
}
77-
if !isInstalled(ctx, tool.Formula) {
78-
// Graceful degradation — uninstalled means no output, no error.
79-
continue
80-
}
8183
argv := substituteShell(tool.ShellInit, shell)
8284
out, err := proc.Run(ctx, argv[0], argv[1:]...)
8385
if err != nil {
86+
if errors.Is(err, proc.ErrNotFound) {
87+
// Graceful degradation — binary not on PATH means no output, no error.
88+
continue
89+
}
8490
fmt.Fprintf(stderr, "shll shell-init: %s: %v\n", tool.Name, err)
8591
anyFailed = true
8692
continue

src/cmd/shll/shell_init_test.go

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,24 @@ import (
1111
)
1212

1313
// shellInitFakeBuilder constructs a fakeRunner that simulates per-tool installation
14-
// state and shell-init outputs. installedFormulas selects which roster formulas
15-
// are present; outputs maps tool argv (joined by space) to stdout, missing entries
16-
// produce empty stdout success.
14+
// state and shell-init outputs.
15+
//
16+
// installedFormulas selects which roster formulas are "installed" — for a tool
17+
// whose formula is absent from this map, the fake returns proc.ErrNotFound from
18+
// its `<tool> shell-init <shell>` invocation, mirroring real exec.LookPath
19+
// behavior when the binary is missing from PATH. For installed tools, outputs
20+
// supplies the canned stdout (missing entries default to empty stdout success).
1721
func shellInitFake(installedFormulas map[string]bool, outputs map[string]string, errors map[string]error) *fakeRunner {
22+
// Map binary name → formula so the fake can resolve "is this tool's binary
23+
// on the simulated PATH?" from the Roster.
24+
formulaByName := map[string]string{}
25+
for _, t := range Roster {
26+
formulaByName[t.Name] = t.Formula
27+
}
1828
return &fakeRunner{respond: func(req proc.Request) proc.Result {
19-
if req.Name == brewBinary && len(req.Args) >= 4 && req.Args[0] == "list" {
20-
formula := req.Args[3]
21-
if installedFormulas[formula] {
22-
return proc.Result{Stdout: []byte(formula + " 1.0.0\n")}
23-
}
24-
return proc.Result{Err: stdErr("not installed")}
29+
// Simulate ErrNotFound for tools whose formula isn't installed.
30+
if formula, ok := formulaByName[req.Name]; ok && !installedFormulas[formula] {
31+
return proc.Result{Err: proc.ErrNotFound}
2532
}
2633
key := strings.Join(append([]string{req.Name}, req.Args...), " ")
2734
if e, ok := errors[key]; ok {

src/cmd/shll/version.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,15 @@ func runVersion(ctx context.Context, stdout io.Writer) error {
6262
}
6363

6464
// toolVersion returns the version string for a single roster tool, or
65-
// notInstalledLabel on any failure (binary missing, brew reports
66-
// not-installed, --version errors, or timeout). The returned string never
67-
// contains internal newlines — only the first non-empty line is reported.
65+
// notInstalledLabel on any failure (binary missing from PATH, --version
66+
// errors, or timeout). The returned string never contains internal newlines —
67+
// only the first non-empty line is reported.
68+
//
69+
// "Installed" here means "runnable on PATH" — proc.Run returns proc.ErrNotFound
70+
// when the binary is missing, and any other failure mode (non-zero exit,
71+
// timeout, etc.) falls under the same notInstalledLabel branch. This is
72+
// install-mechanism agnostic (brew, from-source, etc.).
6873
func toolVersion(ctx context.Context, tool Tool) string {
69-
if !isInstalled(ctx, tool.Formula) {
70-
return notInstalledLabel
71-
}
7274
subCtx, cancel := context.WithTimeout(ctx, versionTimeout)
7375
defer cancel()
7476
out, err := proc.Run(subCtx, tool.Name, "--version")

src/cmd/shll/version_test.go

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package main
33
import (
44
"bytes"
55
"context"
6-
"errors"
76
"strings"
87
"testing"
98
"time"
@@ -12,15 +11,18 @@ import (
1211
)
1312

1413
// versionFake constructs a fakeRunner that simulates per-tool installation and
15-
// version output.
14+
// version output. For a tool whose formula is absent from installedFormulas,
15+
// the fake returns proc.ErrNotFound from `<tool> --version`, mirroring real
16+
// exec.LookPath behavior when the binary is missing from PATH.
1617
func versionFake(installedFormulas map[string]bool, versions map[string]string) *fakeRunner {
18+
formulaByName := map[string]string{}
19+
for _, t := range Roster {
20+
formulaByName[t.Name] = t.Formula
21+
}
1722
return &fakeRunner{respond: func(req proc.Request) proc.Result {
18-
if req.Name == brewBinary && len(req.Args) >= 4 && req.Args[0] == "list" {
19-
formula := req.Args[3]
20-
if installedFormulas[formula] {
21-
return proc.Result{Stdout: []byte(formula + " 1.0.0\n")}
22-
}
23-
return proc.Result{Err: errors.New("not installed")}
23+
// Simulate ErrNotFound for tools whose formula isn't installed.
24+
if formula, ok := formulaByName[req.Name]; ok && !installedFormulas[formula] {
25+
return proc.Result{Err: proc.ErrNotFound}
2426
}
2527
// Match per-tool --version invocations: req.Name is the tool name,
2628
// args[0] is "--version".

0 commit comments

Comments
 (0)