argsbarg 3.3.6 → 3.3.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [3.3.8] - 2026-06-21
11
+
12
+ ### Changed
13
+
14
+ - **`install --update`** — downloads the latest release and reinstalls installed artifacts when `install.updateGetLatest` is set. Replaces the top-level `update` command and the old `--update` alias for `--reinstall`.
15
+
16
+ ### Removed
17
+
18
+ - **`update` built-in** — use `myapp install --update` instead.
19
+
20
+ ## [3.3.7] - 2026-06-21
21
+
22
+ ### Changed
23
+
24
+ - **`docs mcp`** — intro copy is user-facing (`exposes an MCP server with features similar to the CLI`) instead of describing argsbarg internals.
25
+
10
26
  ## [3.3.6] - 2026-06-21
11
27
 
12
28
  ### Added
@@ -282,7 +298,9 @@ const cli = { ... } satisfies CliProgram; // or : CliProgram
282
298
  - Migrate schemas: rename every `children` property to **`commands`**; move positional definitions to **`CliPositional`** objects on `positionals` and strip `positional` / `argMin` / `argMax` from flag definitions under `options` (flags only carry `name`, `description`, `kind`, and optional `shortName`).
283
299
  - Imports: use `CliPositional` where needed; replace `CliOptionDef` with `CliOption` or `CliPositional` as appropriate.
284
300
 
285
- [Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v3.3.6...HEAD
301
+ [Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v3.3.8...HEAD
302
+ [3.3.8]: https://github.com/bdombro/bun-argsbarg/releases/tag/v3.3.8
303
+ [3.3.7]: https://github.com/bdombro/bun-argsbarg/releases/tag/v3.3.7
286
304
  [3.3.6]: https://github.com/bdombro/bun-argsbarg/releases/tag/v3.3.6
287
305
  [3.3.5]: https://github.com/bdombro/bun-argsbarg/releases/tag/v3.3.5
288
306
  [3.3.4]: https://github.com/bdombro/bun-argsbarg/releases/tag/v3.3.4
package/README.md CHANGED
@@ -122,7 +122,7 @@ myapp install --all --yes
122
122
 
123
123
  This copies the binary to `~/.local/bin`, installs shell completions (bash/zsh/fish when each shell is on PATH), writes Cursor/Claude skills when agent directories exist, and merges MCP server entries into Cursor and Claude config files.
124
124
 
125
- See **[docs/install.md](docs/install.md)** for `--reinstall`, `update`, `--status`, `--uninstall`, and flags.
125
+ See **[docs/install.md](docs/install.md)** for `--reinstall`, `install --update`, `--status`, `--uninstall`, and flags.
126
126
 
127
127
 
128
128
  ### Shell completions
@@ -187,7 +187,7 @@ Add `CliPositional` entries to the command’s `positionals` list (separate from
187
187
 
188
188
  ### Capabilities (built-ins)
189
189
 
190
- `completion`, `version`, `install`, `update`, and `mcp` are not part of your schema — they are injected at runtime from program-level config (`mcpServer`, `install`, `docs`). Reserved command names: `completion` and `version` always; `install` unless `install.enabled: false`; `update` when `install.updateGetLatest` is set; `mcp` when `mcpServer.enabled` is `true`; `docs` when `docs.enabled` is `true`.
190
+ `completion`, `version`, `install`, and `mcp` are not part of your schema — they are injected at runtime from program-level config (`mcpServer`, `install`, `docs`). Reserved command names: `completion` and `version` always; `install` unless `install.enabled: false`; `mcp` when `mcpServer.enabled` is `true`; `docs` when `docs.enabled` is `true`. When `install.updateGetLatest` is set, `install --update` is available (not a separate command).
191
191
 
192
192
 
193
193
 
@@ -196,7 +196,7 @@ Basic synchronous handlers do not need this structure — only commands with an
196
196
 
197
197
  ## Reserved names
198
198
 
199
- Do not declare user commands named `completion`, `install`, `mcp`, `version`, `docs`, or `update` at the root — ArgsBarg injects these when configured.
199
+ Do not declare user commands named `completion`, `install`, `mcp`, `version`, or `docs` at the root — ArgsBarg injects these when configured.
200
200
 
201
201
  ## Cursor rule for consumer repos
202
202
 
package/docs/install.md CHANGED
@@ -12,7 +12,7 @@ myapp install --all --yes
12
12
  myapp install --reinstall
13
13
 
14
14
  # Download latest release (when install.updateGetLatest is configured)
15
- myapp update
15
+ myapp install --update
16
16
 
17
17
  # See what is installed
18
18
  myapp install --status
@@ -54,7 +54,7 @@ install: {
54
54
  }
55
55
  ```
56
56
 
57
- When `updateGetLatest` is set, ArgsBarg also registers the **`update`** built-in (`myapp update`).
57
+ When `updateGetLatest` is set, ArgsBarg adds **`install --update`** (download latest release and reinstall installed artifacts).
58
58
 
59
59
  ### GitHub releases (`ghReleaseUpdateGetLatest`)
60
60
 
@@ -105,12 +105,11 @@ Environment:
105
105
  | `--quiet` | Suppress summaries and per-step messages (requires `--yes`) |
106
106
  | `--prefix <dir>` | Override binary install directory |
107
107
  | `--reinstall` | Reinstall artifacts already on disk (implies `--bin` + `--yes`) |
108
+ | `--update` | Download latest release and reinstall installed artifacts (requires `install.updateGetLatest`; implies `--yes`) |
108
109
  | `--from <path>` | Binary to copy with `--reinstall` (default: running executable) |
109
110
  | `--status` | Read-only inventory |
110
111
  | `--uninstall` | Remove artifacts in scope (`--all`, `--bin`, `--completions`, `--skill`, `--mcp`); skips targets not installed |
111
112
 
112
- `--update` is accepted as a deprecated alias for `--reinstall`.
113
-
114
113
  ## MCP merge behavior
115
114
 
116
115
  When `--mcp` runs, entries are merged into `mcpServers[<sanitized-key>]` with:
package/index.d.ts CHANGED
@@ -175,7 +175,7 @@ export interface CliUpdateArtifact {
175
175
  /** Called after reinstall completes (e.g. remove a temp download directory). */
176
176
  cleanup?: () => void | Promise<void>;
177
177
  }
178
- /** Fetches the latest release binary for the `update` built-in. */
178
+ /** Fetches the latest release binary for `install --update`. */
179
179
  export type CliUpdateGetLatest = (ctx: {
180
180
  version: string;
181
181
  }) => Promise<CliUpdateArtifact>;
@@ -185,7 +185,7 @@ export interface CliInstallConfig {
185
185
  /** Default bin directory (default: `~/.local/bin`). Overridden by `INSTALL_PREFIX` env and `--prefix`. */
186
186
  prefix?: string;
187
187
  /**
188
- * When set, enables the `update` built-in (`myapp update`).
188
+ * When set, enables `install --update` on the program root.
189
189
  * Should download or locate the latest release binary and return its path.
190
190
  */
191
191
  updateGetLatest?: CliUpdateGetLatest;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "argsbarg",
3
- "version": "3.3.6",
3
+ "version": "3.3.8",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "bun": ">=1.3"
@@ -6,14 +6,12 @@ import { completionFishScript } from "./completion-fish.ts";
6
6
  import { completionZshScript } from "./completion-zsh.ts";
7
7
  import { cliBuiltinInstallCommand } from "./install.ts";
8
8
  import { cliBuiltinMcpCommand } from "./mcp.ts";
9
- import { cliBuiltinUpdateCommand } from "./update.ts";
10
9
  import { cliBuiltinVersionCommand } from "./version.ts";
11
10
  import { cliBuiltinCompletionGroup as completionGroup } from "./completion-group.ts";
12
11
  import { cliPresentationRoot } from "./presentation.ts";
13
12
  import { cliBuiltinDocsGroupIfEnabled } from "../docs/builtin.ts";
14
13
  import { cliMcpServeStdio } from "../mcp.ts";
15
14
  import { cliInstall } from "../install/index.ts";
16
- import { cliUpdate } from "../install/update.ts";
17
15
  import type { ParseResult } from "../parse.ts";
18
16
  import { ParseKind } from "../parse.ts";
19
17
 
@@ -82,20 +80,6 @@ export async function dispatchBuiltin(
82
80
  process.exit(0);
83
81
  }
84
82
 
85
- if (pr.path[0] === "update") {
86
- if (!caps.update) {
87
- process.stderr.write(
88
- "update is not enabled. Set install.updateGetLatest on the program root.\n",
89
- );
90
- process.exit(1);
91
- }
92
- if (pr.path.length !== 1) {
93
- process.stderr.write("Unknown subcommand: update " + pr.path.slice(1).join(" ") + "\n");
94
- process.exit(1);
95
- }
96
- await cliUpdate(program);
97
- }
98
-
99
83
  if (pr.path[0] === "install") {
100
84
  if (!caps.install) {
101
85
  process.stderr.write("install is disabled. Remove install.enabled: false from the program root.\n");
@@ -143,17 +127,6 @@ export function builtinInterceptRoot(
143
127
  };
144
128
  }
145
129
 
146
- if (first === "update" && caps.update) {
147
- return {
148
- parseRoot: {
149
- key: program.key,
150
- description: program.description,
151
- commands: [cliBuiltinUpdateCommand(program)],
152
- },
153
- isLeafCompletionIntercept: false,
154
- };
155
- }
156
-
157
130
  if (first === "mcp" && caps.mcp) {
158
131
  return {
159
132
  parseRoot: {
@@ -3,7 +3,6 @@ import type { CliFallbackMode, CliOption, CliPositional, CliProgram } from "../t
3
3
  import { cliBuiltinCompletionGroup } from "./completion-group.ts";
4
4
  import { cliBuiltinInstallCommand } from "./install.ts";
5
5
  import { cliBuiltinMcpCommand } from "./mcp.ts";
6
- import { cliBuiltinUpdateCommand } from "./update.ts";
7
6
  import { cliBuiltinVersionCommand } from "./version.ts";
8
7
  import { cliBuiltinDocsGroupIfEnabled } from "../docs/builtin.ts";
9
8
 
@@ -52,9 +51,6 @@ export function exportPresentationBuiltins(program: CliProgram, caps?: CliCapabi
52
51
  if (resolved.install) {
53
52
  builtins.push(exportBuiltinNode(cliBuiltinInstallCommand(program)));
54
53
  }
55
- if (resolved.update) {
56
- builtins.push(exportBuiltinNode(cliBuiltinUpdateCommand(program)));
57
- }
58
54
  const docsGroup = cliBuiltinDocsGroupIfEnabled(program);
59
55
  if (docsGroup) {
60
56
  builtins.push(exportBuiltinNode(docsGroup));
@@ -80,6 +80,15 @@ export function installBuiltinOptions(root: CliProgram): CliOption[] {
80
80
  });
81
81
  }
82
82
 
83
+ if (resolveCapabilities(root).update) {
84
+ const statusIdx = opts.findIndex((o) => o.name === "status");
85
+ opts.splice(statusIdx, 0, {
86
+ name: "update",
87
+ description: "Download and install the latest release.",
88
+ kind: CliOptionKind.Presence,
89
+ });
90
+ }
91
+
83
92
  return opts;
84
93
  }
85
94
 
@@ -94,7 +103,7 @@ export function cliBuiltinInstallCommand(root: CliProgram): CliLeaf {
94
103
  ` ${app} install --all --yes\n\n` +
95
104
  "Refresh after upgrading:\n" +
96
105
  ` ${app} install --reinstall\n` +
97
- ` ${app} update\n\n` +
106
+ ` ${app} install --update\n\n` +
98
107
  "See what is installed:\n" +
99
108
  ` ${app} install --status\n\n` +
100
109
  "Remove everything installed with --all:\n" +
@@ -5,7 +5,6 @@ import { isCliLeaf, isCliRouter } from "../types.ts";
5
5
  import { cliBuiltinCompletionGroup } from "./completion-group.ts";
6
6
  import { cliBuiltinInstallCommand } from "./install.ts";
7
7
  import { cliBuiltinMcpCommand } from "./mcp.ts";
8
- import { cliBuiltinUpdateCommand } from "./update.ts";
9
8
  import { cliBuiltinVersionCommand } from "./version.ts";
10
9
  import { cliBuiltinDocsGroupIfEnabled } from "../docs/builtin.ts";
11
10
 
@@ -18,9 +17,6 @@ export function presentationBuiltins(program: CliProgram, caps: CliCapabilities)
18
17
  if (caps.install) {
19
18
  builtins.push(cliBuiltinInstallCommand(program));
20
19
  }
21
- if (caps.update) {
22
- builtins.push(cliBuiltinUpdateCommand(program));
23
- }
24
20
  const docsGroup = cliBuiltinDocsGroupIfEnabled(program);
25
21
  if (docsGroup) {
26
22
  builtins.push(docsGroup);
@@ -32,9 +32,6 @@ export function reservedCommandNames(caps: CliCapabilities): string[] {
32
32
  if (caps.install) {
33
33
  names.push("install");
34
34
  }
35
- if (caps.update) {
36
- names.push("update");
37
- }
38
35
  if (caps.docs) {
39
36
  names.push("docs");
40
37
  }
@@ -29,7 +29,7 @@ export function generateMcpGuide(root: CliProgram): string {
29
29
  const lines: string[] = [
30
30
  `# MCP server (${root.key})`,
31
31
  "",
32
- `${root.key} exposes an MCP server via argsbarg. Each exposed leaf command becomes an MCP tool.`,
32
+ `${root.key} exposes an MCP server with features similar to the CLI.`,
33
33
  "",
34
34
  "## Quick start",
35
35
  "",
@@ -101,7 +101,7 @@ export function createGhVersionCheck(config: GhVersionCheckConfig): {
101
101
  if (cached === null || isAlreadyCurrent(config.currentVersion, cached.latest)) {
102
102
  return null;
103
103
  }
104
- return `Update available: v${cached.latest} (you have v${config.currentVersion}). Run \`${config.commandName} update\``;
104
+ return `Update available: v${cached.latest} (you have v${config.currentVersion}). Run \`${config.commandName} install --update\``;
105
105
  },
106
106
 
107
107
  refreshIfStale(): void {
@@ -12,17 +12,18 @@ import {
12
12
  import { resolveInstallPaths } from "./paths.ts";
13
13
  import { installErr, installInfo, installOut, printInstallStatus } from "./status.ts";
14
14
  import { buildUninstallPlan, uninstallSkillDir, type UninstallAction } from "./uninstall.ts";
15
+ import { cliUpdate } from "./update.ts";
15
16
 
16
17
  export function parseInstallOpts(raw: Record<string, string>): InstallOpts {
17
18
  const flag = (name: string) => raw[name] === "1";
18
- const reinstall = flag("reinstall") || flag("update");
19
19
  return {
20
20
  all: flag("all"),
21
21
  bin: flag("bin"),
22
22
  completions: flag("completions"),
23
23
  skill: flag("skill"),
24
24
  mcp: flag("mcp"),
25
- reinstall,
25
+ reinstall: flag("reinstall"),
26
+ update: flag("update"),
26
27
  from: raw.from,
27
28
  status: flag("status"),
28
29
  uninstall: flag("uninstall"),
@@ -38,8 +39,8 @@ export function validateInstallOpts(opts: InstallOpts): string | null {
38
39
  if (opts.quiet && opts.dry) {
39
40
  return "--quiet cannot be combined with --dry.";
40
41
  }
41
- if (opts.quiet && !opts.yes && !opts.json && !opts.reinstall) {
42
- return "--quiet requires --yes (or --json / --reinstall).";
42
+ if (opts.quiet && !opts.yes && !opts.json && !opts.reinstall && !opts.update) {
43
+ return "--quiet requires --yes (or --json / --reinstall / --update).";
43
44
  }
44
45
  if (opts.json) {
45
46
  opts.yes = true;
@@ -48,22 +49,31 @@ export function validateInstallOpts(opts: InstallOpts): string | null {
48
49
  opts.bin = true;
49
50
  opts.yes = true;
50
51
  }
52
+ if (opts.update) {
53
+ opts.yes = true;
54
+ }
51
55
 
52
56
  const mutationFlags =
53
- opts.all || opts.bin || opts.completions || opts.skill || opts.mcp || opts.reinstall || opts.uninstall;
57
+ opts.all || opts.bin || opts.completions || opts.skill || opts.mcp || opts.reinstall || opts.update || opts.uninstall;
54
58
  if (opts.status && mutationFlags) {
55
59
  return "--status is mutually exclusive with install/reinstall/uninstall targets.";
56
60
  }
57
61
  if (
58
62
  opts.reinstall &&
59
- (opts.all || opts.completions || opts.skill || opts.mcp || opts.uninstall || opts.status)
63
+ (opts.all || opts.completions || opts.skill || opts.mcp || opts.uninstall || opts.status || opts.update)
60
64
  ) {
61
65
  return "--reinstall cannot be combined with other target flags.";
62
66
  }
63
- if (opts.uninstall && (opts.reinstall || opts.status)) {
64
- return "--uninstall cannot be combined with --reinstall or --status.";
67
+ if (
68
+ opts.update &&
69
+ (opts.all || opts.bin || opts.completions || opts.skill || opts.mcp || opts.uninstall || opts.status || opts.reinstall)
70
+ ) {
71
+ return "--update cannot be combined with other target flags.";
72
+ }
73
+ if (opts.uninstall && (opts.reinstall || opts.update || opts.status)) {
74
+ return "--uninstall cannot be combined with --reinstall, --update, or --status.";
65
75
  }
66
- if (!opts.status && !opts.reinstall) {
76
+ if (!opts.status && !opts.reinstall && !opts.update) {
67
77
  const hasTarget = opts.all || opts.bin || opts.completions || opts.skill || opts.mcp;
68
78
  if (!hasTarget) {
69
79
  return "Specify at least one target: --all, --bin, --completions, --skill, or --mcp.";
@@ -185,6 +195,21 @@ export async function runInstallMutation(
185
195
 
186
196
  /** Main install command orchestrator. */
187
197
  export async function cliInstall(root: CliProgram, rawOpts: Record<string, string>): Promise<never> {
198
+ const opts = parseInstallOpts(rawOpts);
199
+ const err = validateInstallOpts(opts);
200
+ if (err) {
201
+ installErr(err);
202
+ process.exit(1);
203
+ }
204
+
205
+ if (opts.update) {
206
+ if (!resolveCapabilities(root).update) {
207
+ installErr("install --update requires install.updateGetLatest on the program root.");
208
+ process.exit(1);
209
+ }
210
+ await cliUpdate(root);
211
+ }
212
+
188
213
  let result: Awaited<ReturnType<typeof runInstallMutation>>;
189
214
  try {
190
215
  result = await runInstallMutation(root, rawOpts);
@@ -193,26 +218,26 @@ export async function cliInstall(root: CliProgram, rawOpts: Record<string, strin
193
218
  process.exit(1);
194
219
  }
195
220
 
196
- const { changed, opts, paths } = result;
221
+ const { changed, opts: mutationOpts, paths } = result;
197
222
 
198
- if (opts.status) {
223
+ if (mutationOpts.status) {
199
224
  process.exit(0);
200
225
  }
201
226
 
202
- if (opts.json) {
227
+ if (mutationOpts.json) {
203
228
  process.stdout.write(JSON.stringify(changed, null, 2) + "\n");
204
229
  process.exit(0);
205
230
  }
206
231
 
207
- if (!opts.quiet && changed.length > 0) {
208
- const verb = opts.uninstall ? "Removed" : opts.reinstall ? "Reinstalled" : "Installed";
209
- installOut(`${verb} ${changed.length} file(s).`, opts);
232
+ if (!mutationOpts.quiet && changed.length > 0) {
233
+ const verb = mutationOpts.uninstall ? "Removed" : mutationOpts.reinstall ? "Reinstalled" : "Installed";
234
+ installOut(`${verb} ${changed.length} file(s).`, mutationOpts);
210
235
  if (
211
- !opts.uninstall &&
212
- (opts.all || opts.bin) &&
236
+ !mutationOpts.uninstall &&
237
+ (mutationOpts.all || mutationOpts.bin) &&
213
238
  changed.some((p) => p === paths.bashRc || p === paths.zshRc || p === paths.binaryPath)
214
239
  ) {
215
- installOut("Open a new shell, or run: hash -r (bash) / rehash (zsh)", opts);
240
+ installOut("Open a new shell, or run: hash -r (bash) / rehash (zsh)", mutationOpts);
216
241
  }
217
242
  }
218
243
 
@@ -16,6 +16,7 @@ export interface InstallOpts {
16
16
  skill?: boolean;
17
17
  mcp?: boolean;
18
18
  reinstall?: boolean;
19
+ update?: boolean;
19
20
  from?: string;
20
21
  status?: boolean;
21
22
  uninstall?: boolean;
@@ -36,23 +36,25 @@ function fixtureWithUpdate(hook: () => Promise<CliUpdateArtifact>): CliProgram {
36
36
  };
37
37
  }
38
38
 
39
- test("update reserved when updateGetLatest is set", () => {
39
+ test("user update command is allowed when updateGetLatest is set", () => {
40
40
  const root: CliProgram = {
41
41
  ...fixtureWithUpdate(async () => ({ path: process.execPath })),
42
- commands: [{ key: "update", description: "conflict", handler: () => {} }],
42
+ commands: [{ key: "update", description: "Custom update", handler: () => {} }],
43
43
  };
44
- expect(() => cliValidateProgram(root)).toThrow(/Reserved command name: update/);
44
+ expect(() => cliValidateProgram(root)).not.toThrow();
45
45
  });
46
46
 
47
- test("presentation includes update builtin when hook is set", () => {
47
+ test("presentation exposes install --update when hook is set", () => {
48
48
  const root = fixtureWithUpdate(async () => ({ path: process.execPath }));
49
49
  const presentation = cliPresentationRoot(root);
50
- expect(presentation.commands.some((c) => c.key === "update")).toBe(true);
50
+ const install = presentation.commands.find((c) => c.key === "install");
51
+ expect(install?.options?.some((o) => o.name === "update")).toBe(true);
52
+ expect(presentation.commands.some((c) => c.key === "update")).toBe(false);
51
53
  });
52
54
 
53
- test("parseInstallOpts maps deprecated --update to reinstall", () => {
54
- const opts = parseInstallOpts({ update: "1" });
55
- expect(opts.reinstall).toBe(true);
55
+ test("parseInstallOpts treats --update separately from --reinstall", () => {
56
+ expect(parseInstallOpts({ update: "1" }).update).toBe(true);
57
+ expect(parseInstallOpts({ update: "1" }).reinstall).toBe(false);
56
58
  });
57
59
 
58
60
  test("runInstallMutation honors --from for binary copy", async () => {
@@ -78,7 +80,7 @@ test("runInstallMutation honors --from for binary copy", async () => {
78
80
  expect(readFileSync(dest, "utf8")).toContain("echo hi");
79
81
  });
80
82
 
81
- test("cliInvoke update uses hook and reinstalls", async () => {
83
+ test("cliInvoke install --update uses hook and reinstalls", async () => {
82
84
  const source = join(home, "new-binary");
83
85
  writeFileSync(source, "#!/bin/sh\necho hi\n", "utf8");
84
86
  chmodSync(source, 0o755);
@@ -88,19 +90,19 @@ test("cliInvoke update uses hook and reinstalls", async () => {
88
90
  version: "2.0.0",
89
91
  }));
90
92
 
91
- const result = await cliInvoke(root, ["update"]);
93
+ const result = await cliInvoke(root, ["install", "--update"]);
92
94
  expect(result.exitCode).toBe(0);
93
95
  expect(result.stdout).toContain("Updated testapp 1.0.0 → 2.0.0");
94
96
  expect(existsSync(join(home, ".local", "bin", "testapp"))).toBe(true);
95
97
  });
96
98
 
97
- test("cliInvoke update reports already current", async () => {
99
+ test("cliInvoke install --update reports already current", async () => {
98
100
  const root = fixtureWithUpdate(async () => ({
99
101
  path: process.execPath,
100
102
  version: "1.0.0",
101
103
  }));
102
104
 
103
- const result = await cliInvoke(root, ["update"]);
105
+ const result = await cliInvoke(root, ["install", "--update"]);
104
106
  expect(result.exitCode).toBe(0);
105
107
  expect(result.stdout).toContain("Already at v1.0.0");
106
108
  });
@@ -3,7 +3,7 @@ import type { CliProgram } from "../types.ts";
3
3
  import { runInstallMutation } from "./index.ts";
4
4
  import { installErr } from "./status.ts";
5
5
 
6
- /** Downloads the latest release and reinstalls installed artifacts (`myapp update`). */
6
+ /** Downloads the latest release and reinstalls installed artifacts (`myapp install --update`). */
7
7
  export async function cliUpdate(root: CliProgram): Promise<never> {
8
8
  const hook = root.install?.updateGetLatest;
9
9
  if (!hook) {
package/src/invoke.ts CHANGED
@@ -5,10 +5,10 @@ process.exit so MCP tool calls can run handlers repeatedly.
5
5
  */
6
6
 
7
7
  import { CliContext } from "./context.ts";
8
- import { builtinInterceptRoot } from "./builtins/dispatch.ts";
8
+ import { builtinInterceptRoot, dispatchBuiltin } from "./builtins/dispatch.ts";
9
9
  import { cliPresentationRoot } from "./builtins/presentation.ts";
10
10
  import { parse, postParseValidate, ParseKind } from "./parse.ts";
11
- import { type CliNode, type CliProgram, isCliLeaf, isCliRouter } from "./types.ts";
11
+ import { type CliNode, type CliProgram, type CliRouter, isCliLeaf, isCliRouter } from "./types.ts";
12
12
  import { format } from "node:util";
13
13
 
14
14
  /** Outcome of a non-exiting CLI invocation. */
@@ -52,10 +52,17 @@ function findChild(cmds: CliNode[], name: string): CliNode | undefined {
52
52
  */
53
53
  export async function cliInvoke(root: CliProgram, argv: string[]): Promise<CliInvokeResult> {
54
54
  let parseRoot: CliNode = root;
55
+ let completionParseRoot: CliRouter = cliPresentationRoot(root);
56
+ let isLeafCompletionIntercept = false;
57
+
55
58
  if (isCliLeaf(root)) {
56
59
  const intercept = builtinInterceptRoot(root, argv);
57
- if (intercept.parseRoot !== root) {
60
+ if (intercept.isLeafCompletionIntercept || intercept.parseRoot !== root) {
58
61
  parseRoot = intercept.parseRoot;
62
+ completionParseRoot = isCliRouter(intercept.parseRoot)
63
+ ? intercept.parseRoot
64
+ : cliPresentationRoot(root);
65
+ isLeafCompletionIntercept = intercept.isLeafCompletionIntercept;
59
66
  }
60
67
  } else {
61
68
  parseRoot = cliPresentationRoot(root);
@@ -165,6 +172,10 @@ export async function cliInvoke(root: CliProgram, argv: string[]): Promise<CliIn
165
172
  };
166
173
 
167
174
  try {
175
+ if (pr.kind === ParseKind.Ok) {
176
+ await dispatchBuiltin(root, pr, { isLeafCompletionIntercept, parseRoot: completionParseRoot });
177
+ }
178
+
168
179
  await Promise.resolve(handler(ctx));
169
180
  return { kind: "ok", exitCode: 0, stdout, stderr };
170
181
  } catch (err) {
package/src/runtime.ts CHANGED
@@ -40,13 +40,6 @@ export async function cliRun(program: CliProgram, argv: string[] = process.argv.
40
40
  process.exit(1);
41
41
  }
42
42
 
43
- if (argv.length >= 1 && argv[0] === "update" && !caps.update) {
44
- process.stderr.write(
45
- "update is not enabled. Set install.updateGetLatest on the program root.\n",
46
- );
47
- process.exit(1);
48
- }
49
-
50
43
  if (argv.length >= 1 && argv[0] === "docs" && !caps.docs) {
51
44
  process.stderr.write("docs is not enabled. Set docs: { enabled: true } on the program root.\n");
52
45
  process.exit(1);
package/src/schema.ts CHANGED
@@ -6,7 +6,7 @@ import { type CliNode, type CliProgram, isCliLeaf, isCliRouter } from "./types.t
6
6
  import { exportPresentationBuiltins, type CliSchemaExport } from "./builtins/export.ts";
7
7
  import { cliResolveNotes } from "./help.ts";
8
8
 
9
- const RESERVED = new Set(["completion", "install", "docs", "mcp", "version", "update"]);
9
+ const RESERVED = new Set(["completion", "install", "docs", "mcp", "version"]);
10
10
 
11
11
  function exportCommand(cmd: CliNode, root: CliProgram): CliSchemaExport {
12
12
  const out: CliSchemaExport = {
package/src/types.ts CHANGED
@@ -162,7 +162,7 @@ export interface CliUpdateArtifact {
162
162
  cleanup?: () => void | Promise<void>;
163
163
  }
164
164
 
165
- /** Fetches the latest release binary for the `update` built-in. */
165
+ /** Fetches the latest release binary for `install --update`. */
166
166
  export type CliUpdateGetLatest = (ctx: { version: string }) => Promise<CliUpdateArtifact>;
167
167
 
168
168
  export interface CliInstallConfig {
@@ -171,7 +171,7 @@ export interface CliInstallConfig {
171
171
  /** Default bin directory (default: `~/.local/bin`). Overridden by `INSTALL_PREFIX` env and `--prefix`. */
172
172
  prefix?: string;
173
173
  /**
174
- * When set, enables the `update` built-in (`myapp update`).
174
+ * When set, enables `install --update` on the program root.
175
175
  * Should download or locate the latest release binary and return its path.
176
176
  */
177
177
  updateGetLatest?: CliUpdateGetLatest;
@@ -1,14 +0,0 @@
1
- import type { CliLeaf, CliProgram } from "../types.ts";
2
- import { cliUpdate } from "../install/update.ts";
3
-
4
- /** Built-in `update` command (enabled when `install.updateGetLatest` is set). */
5
- export function cliBuiltinUpdateCommand(program: CliProgram): CliLeaf {
6
- return {
7
- key: "update",
8
- description: "Download and install the latest release.",
9
- mcpTool: { enabled: false },
10
- handler: async () => {
11
- await cliUpdate(program);
12
- },
13
- };
14
- }