argsbarg 4.0.4 → 4.1.1

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.
Files changed (147) hide show
  1. package/CHANGELOG.md +77 -1
  2. package/README.md +91 -85
  3. package/docs/README.md +6 -6
  4. package/docs/ai-skills.md +8 -5
  5. package/docs/bundled-docs.md +1 -1
  6. package/docs/cli-program.md +9 -7
  7. package/docs/config-schema.md +37 -13
  8. package/docs/developing.md +8 -8
  9. package/docs/distribution-homebrew.md +103 -0
  10. package/docs/install.md +143 -106
  11. package/docs/mcp.md +23 -12
  12. package/docs/output-schema.md +1 -1
  13. package/examples/full-example/Formula/.gitkeep +0 -0
  14. package/examples/full-example/README.md +98 -0
  15. package/examples/full-example/biome.json +22 -0
  16. package/examples/{consumer-app → full-example}/bun.lock +2 -0
  17. package/examples/full-example/justfile +134 -0
  18. package/examples/{consumer-app → full-example}/package.json +10 -3
  19. package/examples/{consumer-app → full-example}/schemas/generated/app-config.json +1 -1
  20. package/examples/{consumer-app → full-example}/schemas/generated/status.json +1 -1
  21. package/examples/full-example/scripts/create-identity.ts +11 -0
  22. package/examples/full-example/scripts/formula-shared.ts +73 -0
  23. package/examples/full-example/scripts/gen-dev-formula.ts +26 -0
  24. package/examples/full-example/scripts/print-identity.ts +27 -0
  25. package/examples/full-example/src/commands/echo/command.ts +21 -0
  26. package/examples/full-example/src/commands/status/command.test.ts +10 -0
  27. package/examples/full-example/src/commands/status/command.ts +36 -0
  28. package/examples/{consumer-app → full-example}/src/commands/status/types.ts +1 -1
  29. package/examples/full-example/src/index.ts +10 -0
  30. package/examples/full-example/src/program.ts +57 -0
  31. package/examples/{consumer-app → full-example}/src/types.ts +1 -1
  32. package/examples/mcp-test.ts +8 -24
  33. package/examples/nested.ts +1 -3
  34. package/index.d.ts +81 -65
  35. package/package.json +2 -2
  36. package/src/builtins/builtins.test.ts +37 -22
  37. package/src/builtins/completion-group.ts +17 -15
  38. package/src/builtins/config.test.ts +31 -25
  39. package/src/builtins/config.ts +4 -3
  40. package/src/builtins/dispatch.ts +25 -1
  41. package/src/builtins/install.ts +45 -82
  42. package/src/builtins/mcp.ts +1 -1
  43. package/src/builtins/registry.ts +2 -0
  44. package/src/builtins/uninstall.ts +80 -0
  45. package/src/capabilities.ts +5 -7
  46. package/src/cli-tool/cli-smoke.test.ts +19 -0
  47. package/src/cli-tool/create.test.ts +119 -0
  48. package/src/cli-tool/create.ts +380 -0
  49. package/{examples/consumer-app/capabilities.test.ts → src/cli-tool/full-example-capabilities.test.ts} +15 -14
  50. package/src/cli-tool/main.ts +8 -0
  51. package/src/cli-tool/post-create.ts +111 -0
  52. package/src/cli-tool/program.ts +82 -0
  53. package/src/cli-tool/prompt.ts +28 -0
  54. package/src/cli-tool/run-create.ts +149 -0
  55. package/src/config/bootstrap.ts +174 -66
  56. package/src/config/context.test.ts +22 -36
  57. package/src/config/context.ts +5 -4
  58. package/src/config/file.test.ts +66 -56
  59. package/src/config/file.ts +33 -25
  60. package/src/config/resolve.test.ts +192 -1
  61. package/src/config/resolve.ts +92 -13
  62. package/src/config.integration.test.ts +17 -10
  63. package/src/docs/api-guide.test.ts +4 -5
  64. package/src/docs/docs.test.ts +2 -1
  65. package/src/docs/mcp-guide.ts +7 -8
  66. package/src/hidden-mcpb.test.ts +41 -1
  67. package/src/index.ts +7 -10
  68. package/src/install/binary-placement.test.ts +101 -0
  69. package/src/install/binary-placement.ts +47 -0
  70. package/src/install/detect-installed.ts +2 -97
  71. package/src/install/index.ts +239 -168
  72. package/src/install/install-validate.test.ts +61 -0
  73. package/src/install/install.test.ts +170 -90
  74. package/src/install/mcp-openclaw.test.ts +40 -0
  75. package/src/install/mcp-openclaw.ts +106 -0
  76. package/src/install/normalize-uninstall.ts +11 -0
  77. package/src/install/normalize.ts +20 -0
  78. package/src/install/paths.ts +18 -26
  79. package/src/install/plan.ts +40 -261
  80. package/src/install/shell.ts +0 -14
  81. package/src/install/status.test.ts +85 -0
  82. package/src/install/status.ts +22 -15
  83. package/src/install/target-base.ts +93 -0
  84. package/src/install/target-detect.ts +20 -0
  85. package/src/install/target-effective.ts +129 -0
  86. package/src/install/target-mcp-cli.ts +149 -0
  87. package/src/install/target-mcp-json.ts +130 -0
  88. package/src/install/target-plan-build.ts +67 -0
  89. package/src/install/target-registry.ts +57 -0
  90. package/src/install/target-scope.ts +253 -0
  91. package/src/install/target-skill.ts +104 -0
  92. package/src/install/target-types.ts +129 -0
  93. package/src/install/targets/app.ts +60 -0
  94. package/src/install/targets/chatgpt-mcp.ts +12 -0
  95. package/src/install/targets/claude-code-mcp.ts +15 -0
  96. package/src/install/targets/claude-desktop-mcp.ts +12 -0
  97. package/src/install/targets/claude-skill.ts +16 -0
  98. package/src/install/targets/codex-mcp.ts +25 -0
  99. package/src/install/targets/codex-skill.ts +14 -0
  100. package/src/install/targets/configure.ts +63 -0
  101. package/src/install/targets/cursor-mcp.ts +15 -0
  102. package/src/install/targets/cursor-skill.ts +16 -0
  103. package/src/install/targets/index.ts +50 -0
  104. package/src/install/targets/openclaw-mcp.ts +25 -0
  105. package/src/install/targets/openclaw-skill.ts +17 -0
  106. package/src/install/targets/opencode-mcp.ts +101 -0
  107. package/src/install/targets/opencode-skill.ts +15 -0
  108. package/src/install/targets.test.ts +118 -0
  109. package/src/install/uninstall.ts +16 -152
  110. package/src/invoke.test.ts +7 -1
  111. package/src/mcp/bundle.ts +16 -4
  112. package/src/mcp/claude.test.ts +14 -1
  113. package/src/mcp/claude.ts +11 -4
  114. package/src/mcp/env.test.ts +92 -0
  115. package/src/mcp/env.ts +15 -14
  116. package/src/mcp/zip.test.ts +17 -0
  117. package/src/mcp/zip.ts +62 -9
  118. package/src/mcp.integration.test.ts +1 -1
  119. package/src/parse.test.ts +14 -2
  120. package/src/paths/host.ts +11 -11
  121. package/src/paths/remove-empty-dir.ts +13 -0
  122. package/src/prompt.ts +10 -0
  123. package/src/schema.ts +9 -1
  124. package/src/skill/generate.ts +18 -4
  125. package/src/skill/install.ts +33 -6
  126. package/src/skill/naming.ts +28 -0
  127. package/src/types.ts +86 -7
  128. package/src/validate.ts +73 -9
  129. package/docs/templates/cursor/rules/cli-program.mdc +0 -31
  130. package/examples/config-app/main.ts +0 -20
  131. package/examples/config-app/program.ts +0 -81
  132. package/examples/config-app/schema.ts +0 -37
  133. package/examples/config-app/types.ts +0 -19
  134. package/examples/consumer-app/README.md +0 -57
  135. package/examples/consumer-app/src/main.ts +0 -15
  136. package/examples/consumer-app/src/program.ts +0 -108
  137. package/src/install/binary.ts +0 -94
  138. package/src/install/completions.ts +0 -56
  139. package/src/install/update.test.ts +0 -108
  140. package/src/install/update.ts +0 -57
  141. /package/examples/{consumer-app → full-example}/schemas/configSchemas.ts +0 -0
  142. /package/examples/{consumer-app → full-example}/schemas/outputSchemas.ts +0 -0
  143. /package/examples/{consumer-app → full-example}/scripts/schemagen/discover-schema-roots.test.ts +0 -0
  144. /package/examples/{consumer-app → full-example}/scripts/schemagen/discover-schema-roots.ts +0 -0
  145. /package/examples/{consumer-app → full-example}/scripts/schemagen/naming.ts +0 -0
  146. /package/examples/{consumer-app → full-example}/scripts/schemagen.ts +0 -0
  147. /package/examples/{consumer-app → full-example}/tsconfig.json +0 -0
package/index.d.ts CHANGED
@@ -1,5 +1,9 @@
1
1
  // Generated by dts-bundle-generator v9.5.1
2
2
 
3
+ /** Resolved absolute path to the app JSON config file (`~/.local/lib/<key>/config.json`). */
4
+ export declare function resolveAppConfigPath(program: CliProgram): string;
5
+ /** Human-readable config path for error messages (`~/…` when under home). */
6
+ export declare function displayAppConfigPath(program: CliProgram): string;
3
7
  export type ResolvedConfig = Record<string, unknown>;
4
8
  declare class EmptyAppConfigSnapshot {
5
9
  private readonly program;
@@ -22,7 +26,7 @@ declare class AppConfigSnapshot {
22
26
  require(key: string): unknown;
23
27
  set(key: string, value: unknown): void;
24
28
  read(): ResolvedConfig;
25
- /** Resolved absolute path to the app JSON config file (honors `program.appConfig.path` or OS default). */
29
+ /** Resolved absolute path to the app JSON config file (`~/.local/lib/<key>/config.json`). */
26
30
  get path(): string;
27
31
  /** Resolved absolute directory containing the config file. */
28
32
  get dir(): string;
@@ -194,6 +198,10 @@ export interface CliMcpBundleConfig {
194
198
  export interface CliMcpServerConfig {
195
199
  /** When `true`, enables the `mcp` built-in and MCP stdio server. */
196
200
  enabled: boolean;
201
+ /** When `true`, `mcp bundle` writes `dist/<key>.mcpb` for Claude Desktop. Default false. */
202
+ mcpd?: boolean;
203
+ /** When `true`, `mcp bundle` also writes `dist/claude-plugin/<name>.zip`. Default false. */
204
+ claudePlugin?: boolean;
197
205
  /** Resource URI for schema export (default: `<sanitized root key>://schema`). */
198
206
  schemaResourceUri?: string;
199
207
  /**
@@ -241,21 +249,24 @@ export interface CliMcpToolConfig {
241
249
  */
242
250
  outputSchema?: Record<string, unknown>;
243
251
  }
252
+ /** Context passed to {@link CliAppConfigEntry.resolve} for one config key. */
253
+ export interface CliAppConfigResolveContext {
254
+ /** Schema key being resolved. */
255
+ key: string;
256
+ /** Entry metadata for this key. */
257
+ entry: CliAppConfigEntry;
258
+ /** Program root (read-only). */
259
+ program: CliProgram;
260
+ /** Raw value from the config file, if any. */
261
+ fileValue: unknown;
262
+ /** Non-empty host env string when `entry.env` is set; otherwise `undefined`. */
263
+ envValue: string | undefined;
264
+ }
244
265
  /**
245
- * Opt-out and defaults for the `install` built-in (program root only).
266
+ * Optional fallback resolver for one config key (e.g. `gh auth token` when `GH_TOKEN` is unset).
267
+ * Return `undefined` to continue resolution (env, then default).
246
268
  */
247
- export interface CliUpdateArtifact {
248
- /** Path to an executable binary to copy into the install location. */
249
- path: string;
250
- /** Release version of `path` (used for already-current checks and success messages). */
251
- version?: string;
252
- /** Called after reinstall completes (e.g. remove a temp download directory). */
253
- cleanup?: () => void | Promise<void>;
254
- }
255
- /** Fetches the latest release binary for `install --update`. */
256
- export type CliUpdateGetLatest = (ctx: {
257
- version: string;
258
- }) => Promise<CliUpdateArtifact>;
269
+ export type CliAppConfigResolveFn = (ctx: CliAppConfigResolveContext) => unknown;
259
270
  /**
260
271
  * Metadata overlay for one key in {@link CliAppConfig.entries}.
261
272
  * Types and validation come from {@link CliAppConfig.jsonSchema} when set; otherwise all values are strings.
@@ -276,13 +287,16 @@ export interface CliAppConfigEntry {
276
287
  sensitive?: boolean;
277
288
  /** When set: non-empty `process.env[env]` overrides file; value exported after resolve. */
278
289
  env?: string;
290
+ /**
291
+ * Optional fallback after file when env is empty.
292
+ * Return `undefined` to fall back to `env` (if set) and schema defaults.
293
+ */
294
+ resolve?: CliAppConfigResolveFn;
279
295
  }
280
296
  /**
281
297
  * App configuration block on the program root ({@link CliProgram.appConfig}).
282
298
  */
283
299
  export interface CliAppConfig {
284
- /** Default: `~/.config/<sanitized-key>/config` (or `%APPDATA%/<key>/config` on Windows). */
285
- path?: string;
286
300
  /** Built-in `config get` / `config set`. Default: enabled when `appConfig` is set. */
287
301
  commands?: boolean | {
288
302
  enabled?: boolean;
@@ -296,13 +310,59 @@ export interface CliAppConfig {
296
310
  export interface CliInstallConfig {
297
311
  /** When `false`, hide/disable `install` (default: enabled). */
298
312
  enabled?: boolean;
299
- /** Default bin directory (default: `~/.local/bin`). Overridden by `INSTALL_PREFIX` env and `--prefix`. */
300
- prefix?: string;
301
313
  /**
302
- * When set, enables `install --update` on the program root.
303
- * Should download or locate the latest release binary and return its path.
314
+ * Default agent integration for full install (`install --all`).
315
+ * - `'mcp'` when `mcpServer.enabled` (default): MCP targets in `--all`; paired skills excluded.
316
+ * - `'skill'` when MCP is off (default): skill targets in `--all`; paired MCP excluded.
317
+ * - `'both'`: install MCP and skill for the same host when both are available.
304
318
  */
305
- updateGetLatest?: CliUpdateGetLatest;
319
+ agentIntegration?: InstallAgentIntegration;
320
+ /** Per-artifact gates for full install/uninstall. See {@link resolveEffectiveInstallTargets}. */
321
+ targets?: CliInstallTargets;
322
+ }
323
+ /** Agent integration mode for install — MCP vs shell skill per host. */
324
+ export type InstallAgentIntegration = "mcp" | "skill" | "both";
325
+ /** Boolean or structured gate for one install artifact. */
326
+ export type InstallTargetSpec = boolean | {
327
+ /** When false, artifact is never installed (even with scoped CLI flags). Default true. */
328
+ enabled?: boolean;
329
+ /** When true, included in bare `install` / `install --all`. Default varies by key. */
330
+ includedInAll?: boolean;
331
+ };
332
+ export interface ResolvedInstallTarget {
333
+ enabled: boolean;
334
+ includedInAll: boolean;
335
+ }
336
+ /** Per-artifact gates for full install/uninstall. See {@link resolveEffectiveInstallTargets}. */
337
+ export interface CliInstallTargets {
338
+ /** App binary status only (Homebrew PATH); no self-install. */
339
+ app?: InstallTargetSpec;
340
+ /** ChatGPT desktop MCP. Default false. */
341
+ chatgptMcp?: InstallTargetSpec;
342
+ /** Claude Code MCP (`~/.claude.json`). Default false. */
343
+ claudeCodeMcp?: InstallTargetSpec;
344
+ /** Claude Desktop MCP. Default false. */
345
+ claudeDesktopMcp?: InstallTargetSpec;
346
+ /** Claude Code skill. Default false. */
347
+ claudeSkill?: InstallTargetSpec;
348
+ /** Codex MCP (`codex mcp add`). Default false. */
349
+ codexMcp?: InstallTargetSpec;
350
+ /** Codex skill. Default false. */
351
+ codexSkill?: InstallTargetSpec;
352
+ /** App config: wizard via install --configure only. Default not in --all. */
353
+ configure?: InstallTargetSpec;
354
+ /** Cursor MCP. Default false. */
355
+ cursorMcp?: InstallTargetSpec;
356
+ /** Cursor skill. Default false. */
357
+ cursorSkill?: InstallTargetSpec;
358
+ /** OpenClaw MCP. Default false. */
359
+ openclawMcp?: InstallTargetSpec;
360
+ /** OpenClaw skill. Default false. */
361
+ openclawSkill?: InstallTargetSpec;
362
+ /** OpenCode MCP. Default false. */
363
+ opencodeMcp?: InstallTargetSpec;
364
+ /** OpenCode skill. Default false. */
365
+ opencodeSkill?: InstallTargetSpec;
306
366
  }
307
367
  /**
308
368
  * One bundled documentation topic for the `docs` built-in (program root only).
@@ -410,7 +470,6 @@ export interface CliCapabilities {
410
470
  mcp: boolean;
411
471
  install: boolean;
412
472
  docs: boolean;
413
- update: boolean;
414
473
  configCommands: boolean;
415
474
  }
416
475
  /** JSON-safe command node (no handlers). */
@@ -492,49 +551,6 @@ export declare function shouldRunHeadlessWithYes(ctx: HeadlessContext, opts: {
492
551
  export declare function requireYesInNonTty(yes: boolean, hint: string, dryRun?: boolean, interactive?: boolean): void;
493
552
  /** Prefixes a success message when running in dry-run mode. */
494
553
  export declare function formatDryRunMessage(message: string, dryRun: boolean): string;
495
- /** Config for {@link ghReleaseUpdateGetLatest}. */
496
- export interface GhReleaseUpdateConfig {
497
- /** GitHub `owner/repo` slug. */
498
- repo: string;
499
- /** Release asset filename (e.g. `myapp`). */
500
- asset: string;
501
- /** Temp directory name prefix for downloads. */
502
- tempPrefix: string;
503
- /** Path to the on-disk version-check cache JSON file. */
504
- cachePath: string;
505
- /** Optional hint when `gh auth` fails or no releases exist. */
506
- repoEnvHint?: string;
507
- }
508
- /** Config for {@link createGhVersionCheck}. */
509
- export interface GhVersionCheckConfig {
510
- /** Installed semver string. */
511
- currentVersion: string;
512
- /** CLI command name for update notices (e.g. `qa`). */
513
- commandName: string;
514
- /** Path to the on-disk version-check cache JSON file. */
515
- cachePath: string;
516
- /** Cache TTL in milliseconds (default 24h). */
517
- ttlMs?: number;
518
- /** When true, skip background refresh (e.g. test subprocess). */
519
- skipRefresh?: () => boolean;
520
- /** When true, skip refresh because `gh` is unavailable. */
521
- ghAvailable?: () => boolean;
522
- /** Fetches latest release version via `gh`. */
523
- fetchLatest: () => Promise<string>;
524
- }
525
- /** Returns whether the installed version matches the latest release. */
526
- export declare function isAlreadyCurrent(current: string, latest: string): boolean;
527
- /** Strips a leading `v` from a release tag. */
528
- export declare function parseReleaseTag(tag: string): string;
529
- /** Builds a `CliUpdateGetLatest` hook that downloads a release via `gh`. */
530
- export declare function ghReleaseUpdateGetLatest(config: GhReleaseUpdateConfig): CliUpdateGetLatest;
531
- /** Version-check cache helpers for summary notices and background refresh. */
532
- export declare function createGhVersionCheck(config: GhVersionCheckConfig): {
533
- getUpdateNotice: () => string | null;
534
- refreshIfStale: () => void;
535
- };
536
- /** Shared `gh release view` fetcher for hooks and version-check refresh. */
537
- export declare function createGhFetchLatest(config: Pick<GhReleaseUpdateConfig, "repo" | "repoEnvHint">): () => Promise<string>;
538
554
  /** Resolved paths for `mcp bundle`. */
539
555
  export interface McpBundlePaths {
540
556
  binaryPath: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "argsbarg",
3
- "version": "4.0.4",
3
+ "version": "4.1.1",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "bun": ">=1.3"
@@ -12,7 +12,7 @@
12
12
  "module": "./src/index.ts",
13
13
  "types": "./index.d.ts",
14
14
  "bin": {
15
- "argsbarg": "src/index.ts"
15
+ "argsbarg": "src/cli-tool/main.ts"
16
16
  },
17
17
  "exports": {
18
18
  ".": {
@@ -1,10 +1,12 @@
1
1
  import { describe, expect, test } from "bun:test";
2
+ import { ParseKind, parse, postParseValidate } from "../parse.ts";
2
3
  import type { CliProgram } from "../types.ts";
3
4
  import { exportPresentationBuiltins } from "./export.ts";
4
5
  import { completionBashScript, completionFishScript, completionZshScript } from "./index.ts";
5
6
  import { cliBuiltinInstallCommand, installBuiltinOptions } from "./install.ts";
6
7
  import { cliBuiltinMcpCommand } from "./mcp.ts";
7
- import { cliPresentationRoot } from "./presentation.ts";
8
+ import { cliParseRoot, cliPresentationRoot } from "./presentation.ts";
9
+ import { cliBuiltinUninstallCommand, uninstallBuiltinOptions } from "./uninstall.ts";
8
10
 
9
11
  const fixture: CliProgram = {
10
12
  key: "myapp",
@@ -21,14 +23,39 @@ const fixture: CliProgram = {
21
23
  };
22
24
 
23
25
  describe("builtins help copy", () => {
24
- test("install command includes description and option text", () => {
26
+ test("install command includes Homebrew-oriented description", () => {
25
27
  const install = cliBuiltinInstallCommand(fixture);
26
- expect(install.description).toContain("Install the binary");
27
- expect(install.notes).toContain("install --all");
28
+ expect(install.description).toContain("agent skills");
29
+ expect(install.notes).toContain("brew install");
28
30
  const names = installBuiltinOptions(fixture).map((o) => o.name);
29
31
  expect(names).toContain("all");
30
32
  expect(names).toContain("mcp");
31
- expect(names).toContain("prefix");
33
+ expect(names).not.toContain("app");
34
+ expect(names).not.toContain("completions");
35
+ expect(names).not.toContain("update");
36
+ expect(names.indexOf("all")).toBeLessThan(names.indexOf("mcp"));
37
+ expect(names.indexOf("mcp")).toBeLessThan(names.indexOf("status"));
38
+ expect(names).not.toContain("uninstall");
39
+ const yesOpt = installBuiltinOptions(fixture).find((o) => o.name === "yes");
40
+ expect(yesOpt?.shortName).toBe("y");
41
+ });
42
+
43
+ test("uninstall command includes removal guidance", () => {
44
+ const uninstall = cliBuiltinUninstallCommand(fixture);
45
+ expect(uninstall.notes).toContain("brew uninstall");
46
+ const names = uninstallBuiltinOptions(fixture).map((o) => o.name);
47
+ expect(names).toContain("all");
48
+ expect(names).not.toContain("status");
49
+ expect(names).not.toContain("reinstall");
50
+ });
51
+
52
+ test("install -y parses as --yes", () => {
53
+ const root = cliParseRoot(fixture);
54
+ const pr = postParseValidate(root, parse(root, ["install", "-y"]));
55
+ expect(pr.kind).toBe(ParseKind.Ok);
56
+ if (pr.kind === ParseKind.Ok) {
57
+ expect(pr.opts.yes).toBe("1");
58
+ }
32
59
  });
33
60
 
34
61
  test("install omits --mcp option when mcpServer unset", () => {
@@ -37,23 +64,10 @@ describe("builtins help copy", () => {
37
64
  expect(names).not.toContain("mcp");
38
65
  });
39
66
 
40
- test("install omits --update when updateGetLatest unset", () => {
67
+ test("install notes mention brew upgrade", () => {
41
68
  const install = cliBuiltinInstallCommand(fixture);
42
- expect(installBuiltinOptions(fixture).map((o) => o.name)).not.toContain("update");
43
- expect(install.notes).not.toContain("Upgrade to latest release");
44
- expect(install.notes).toContain("Refresh after upgrading");
45
- });
46
-
47
- test("install notes include upgrade section when updateGetLatest is set", () => {
48
- const withUpdate: CliProgram = {
49
- ...fixture,
50
- install: { updateGetLatest: async () => ({ path: process.execPath }) },
51
- };
52
- const install = cliBuiltinInstallCommand(withUpdate);
53
- const notes = install.notes ?? "";
54
- expect(installBuiltinOptions(withUpdate).map((o) => o.name)).toContain("update");
55
- expect(notes).toContain("Upgrade to latest release");
56
- expect(notes.indexOf("install --reinstall")).toBeLessThan(notes.indexOf("install --update"));
69
+ expect(install.notes).toContain("brew upgrade");
70
+ expect(install.notes).toContain("install --configure");
57
71
  });
58
72
 
59
73
  test("mcp builtin description is user-facing", () => {
@@ -74,6 +88,7 @@ describe("presentation root", () => {
74
88
  const keys = root.commands?.map((c) => c.key) ?? [];
75
89
  expect(keys).toContain("mcp");
76
90
  expect(keys).toContain("install");
91
+ expect(keys).toContain("uninstall");
77
92
  });
78
93
 
79
94
  test("omits install when install.enabled is false", () => {
@@ -143,6 +158,6 @@ describe("schema export builtins", () => {
143
158
  test("exportPresentationBuiltins includes install options", () => {
144
159
  const builtins = exportPresentationBuiltins(fixture);
145
160
  const install = builtins.find((b) => b.key === "install");
146
- expect(install?.options?.find((o) => o.name === "all")?.description).toContain("binary");
161
+ expect(install?.options?.find((o) => o.name === "all")?.description).toContain("agent");
147
162
  });
148
163
  });
@@ -1,13 +1,12 @@
1
1
  import { resolveCapabilities } from "../capabilities.ts";
2
- import type { CliProgram, CliRouter } from "../types.ts";
2
+ import type { CliProgram } from "../types.ts";
3
3
 
4
4
  /**
5
5
  * Builds the static `completion` / `bash` / `zsh` / `fish` command subtree (merged into the program root at runtime).
6
6
  */
7
- export function cliBuiltinCompletionGroup(program: CliProgram): CliRouter {
7
+ export function cliBuiltinCompletionGroup(program: CliProgram): import("../types.ts").CliRouter {
8
8
  const appName = program.key;
9
- const caps = resolveCapabilities(program);
10
- const router: CliRouter = {
9
+ const router: import("../types.ts").CliRouter = {
11
10
  key: "completion",
12
11
  description: "Generate the autocompletion script for shells.",
13
12
  commands: [
@@ -15,9 +14,9 @@ export function cliBuiltinCompletionGroup(program: CliProgram): CliRouter {
15
14
  key: "bash",
16
15
  description: "Print a bash tab-completion script.",
17
16
  notes:
18
- "Manual install:\n\n" +
19
- ` ${appName} completion bash > ~/.bash_completion.d/${appName}\n` +
20
- ` echo 'source ~/.bash_completion.d/${appName}' >> ~/.bashrc\n\n` +
17
+ "Homebrew installs completions during `brew install` via generate_completions_from_executable.\n\n" +
18
+ "Ensure your shell loads Homebrew completions:\n" +
19
+ " https://docs.brew.sh/Shell-Completion\n\n" +
21
20
  "Try this session only:\n\n" +
22
21
  ` source <(${appName} completion bash)`,
23
22
  handler: () => {},
@@ -26,9 +25,9 @@ export function cliBuiltinCompletionGroup(program: CliProgram): CliRouter {
26
25
  key: "zsh",
27
26
  description: "Print a zsh tab-completion script.",
28
27
  notes:
29
- "Manual install:\n\n" +
30
- ` ${appName} completion zsh > ~/.zsh/completions/_${appName}\n\n` +
31
- "Ensure ~/.zsh/completions is on your fpath, then restart zsh.\n\n" +
28
+ "Homebrew installs completions to $(brew --prefix)/share/zsh/site-functions.\n\n" +
29
+ "Ensure brew shellenv + compinit are configured:\n" +
30
+ " https://docs.brew.sh/Shell-Completion\n\n" +
32
31
  "Try this session only:\n\n" +
33
32
  ` eval "$(${appName} completion zsh)"`,
34
33
  handler: () => {},
@@ -37,15 +36,18 @@ export function cliBuiltinCompletionGroup(program: CliProgram): CliRouter {
37
36
  key: "fish",
38
37
  description: "Print a fish tab-completion script.",
39
38
  notes:
40
- "Manual install:\n\n" +
41
- ` ${appName} completion fish > ~/.config/fish/completions/${appName}.fish\n\n` +
42
- "Fish loads completions from that directory automatically.",
39
+ "Homebrew installs completions to $(brew --prefix)/share/fish/vendor_completions.d.\n\n" +
40
+ "See: https://docs.brew.sh/Shell-Completion\n\n" +
41
+ "Try this session only:\n\n" +
42
+ ` ${appName} completion fish | source`,
43
43
  handler: () => {},
44
44
  },
45
45
  ],
46
46
  };
47
- if (caps.install) {
48
- router.notes = `Install for all shells:\n\n ${appName} install --completions --yes`;
47
+ if (resolveCapabilities(program).install) {
48
+ router.notes =
49
+ "Completions are installed by Homebrew during formula install.\n\n" +
50
+ "See: https://docs.brew.sh/Shell-Completion";
49
51
  }
50
52
  return router;
51
53
  }
@@ -1,16 +1,16 @@
1
1
  import { describe, expect, test } from "bun:test";
2
- import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
- import { join } from "node:path";
4
+ import { dirname, join } from "node:path";
5
+ import { resolveAppConfigPath, writeAppConfigFile } from "../config/file.ts";
5
6
  import { Cli, type CliProgram } from "../index.ts";
6
7
 
7
- function configFixture(configPath: string): CliProgram {
8
+ function configFixture(): CliProgram {
8
9
  return {
9
10
  key: "cfg-app",
10
11
  version: "1.0.0",
11
12
  description: "Config builtin test.",
12
13
  appConfig: {
13
- path: configPath,
14
14
  entries: {
15
15
  apiToken: { description: "Token.", env: "API_TOKEN", sensitive: true },
16
16
  port: { description: "Port.", required: false },
@@ -23,15 +23,21 @@ function configFixture(configPath: string): CliProgram {
23
23
  describe("builtins/config", () => {
24
24
  test("config get redacts sensitive values", async () => {
25
25
  const dir = mkdtempSync(join(tmpdir(), "cfg-builtin-"));
26
- const configPath = join(dir, "config");
27
- writeFileSync(configPath, `${JSON.stringify({ apiToken: "secret" })}\n`);
26
+ const prevHome = process.env.HOME;
27
+ process.env.HOME = dir;
28
28
  const prev = process.env.API_TOKEN;
29
29
  delete process.env.API_TOKEN;
30
30
  try {
31
- const result = await new Cli(configFixture(configPath)).invoke(["config", "get", "apiToken"]);
31
+ const program = configFixture();
32
+ const configPath = resolveAppConfigPath(program);
33
+ mkdirSync(dirname(configPath), { recursive: true });
34
+ writeFileSync(configPath, `${JSON.stringify({ apiToken: "secret" })}\n`);
35
+ const result = await new Cli(program).invoke(["config", "get", "apiToken"]);
32
36
  expect(result.exitCode).toBe(0);
33
37
  expect(result.stdout.trim()).toBe("REDACTED");
34
38
  } finally {
39
+ if (prevHome === undefined) delete process.env.HOME;
40
+ else process.env.HOME = prevHome;
35
41
  if (prev !== undefined) process.env.API_TOKEN = prev;
36
42
  rmSync(dir, { recursive: true, force: true });
37
43
  }
@@ -39,20 +45,21 @@ describe("builtins/config", () => {
39
45
 
40
46
  test("config get --json redacts sensitive as { set: true }", async () => {
41
47
  const dir = mkdtempSync(join(tmpdir(), "cfg-builtin-"));
42
- const configPath = join(dir, "config");
43
- writeFileSync(configPath, `${JSON.stringify({ apiToken: "secret" })}\n`);
48
+ const prevHome = process.env.HOME;
49
+ process.env.HOME = dir;
44
50
  const prev = process.env.API_TOKEN;
45
51
  delete process.env.API_TOKEN;
46
52
  try {
47
- const result = await new Cli(configFixture(configPath)).invoke([
48
- "config",
49
- "get",
50
- "apiToken",
51
- "--json",
52
- ]);
53
+ const program = configFixture();
54
+ const configPath = resolveAppConfigPath(program);
55
+ mkdirSync(dirname(configPath), { recursive: true });
56
+ writeFileSync(configPath, `${JSON.stringify({ apiToken: "secret" })}\n`);
57
+ const result = await new Cli(program).invoke(["config", "get", "apiToken", "--json"]);
53
58
  expect(result.exitCode).toBe(0);
54
- expect(JSON.parse(result.stdout.trim())).toEqual({ set: true });
59
+ expect(JSON.parse(result.stdout)).toEqual({ set: true });
55
60
  } finally {
61
+ if (prevHome === undefined) delete process.env.HOME;
62
+ else process.env.HOME = prevHome;
56
63
  if (prev !== undefined) process.env.API_TOKEN = prev;
57
64
  rmSync(dir, { recursive: true, force: true });
58
65
  }
@@ -60,21 +67,20 @@ describe("builtins/config", () => {
60
67
 
61
68
  test("config set writes and resolves without required exit", async () => {
62
69
  const dir = mkdtempSync(join(tmpdir(), "cfg-builtin-"));
63
- const configPath = join(dir, "config");
64
- writeFileSync(configPath, `${JSON.stringify({ apiToken: "present" })}\n`);
70
+ const prevHome = process.env.HOME;
71
+ process.env.HOME = dir;
65
72
  const prev = process.env.API_TOKEN;
66
73
  delete process.env.API_TOKEN;
67
74
  try {
68
- const result = await new Cli(configFixture(configPath)).invoke([
69
- "config",
70
- "set",
71
- "port",
72
- "9090",
73
- ]);
75
+ const program = configFixture();
76
+ writeAppConfigFile(program, { apiToken: "seed" });
77
+ const result = await new Cli(program).invoke(["config", "set", "port", "9090"]);
74
78
  expect(result.exitCode).toBe(0);
75
- const get = await new Cli(configFixture(configPath)).invoke(["config", "get", "port"]);
79
+ const get = await new Cli(program).invoke(["config", "get", "port"]);
76
80
  expect(get.stdout.trim()).toBe("9090");
77
81
  } finally {
82
+ if (prevHome === undefined) delete process.env.HOME;
83
+ else process.env.HOME = prevHome;
78
84
  if (prev !== undefined) process.env.API_TOKEN = prev;
79
85
  rmSync(dir, { recursive: true, force: true });
80
86
  }
@@ -5,7 +5,7 @@ Built-in `config get` / `config set` commands.
5
5
  import { bootstrapAppConfig } from "../config/bootstrap.ts";
6
6
  import { configEntrySensitive, defaultConfigEntryTitle } from "../config/entry.ts";
7
7
  import { writeAppConfigFile } from "../config/file.ts";
8
- import { exportConfigToEnv, resolveAppConfig } from "../config/resolve.ts";
8
+ import { captureMappedHostEnv, exportConfigToEnv, resolveAppConfig } from "../config/resolve.ts";
9
9
  import { configPropertySchema, effectiveJsonSchema } from "../config/schema.ts";
10
10
  import { parseConfigSetValue } from "../config/validate.ts";
11
11
  import type { CliLeaf, CliOption, CliProgram, CliRouter } from "../types.ts";
@@ -128,11 +128,12 @@ function configSetRun(program: CliProgram, key: string, rawValue: string, useJso
128
128
  process.exit(1);
129
129
  }
130
130
 
131
+ const hostEnv = captureMappedHostEnv(program);
131
132
  const { fileData } = bootstrapAppConfig(program, { validateFile: true });
132
133
  const next = { ...fileData, [key]: parsed };
133
134
  writeAppConfigFile(program, next);
134
- const resolved = resolveAppConfig(program, next);
135
- exportConfigToEnv(program, resolved);
135
+ const resolved = resolveAppConfig(program, next, hostEnv);
136
+ exportConfigToEnv(program, resolved, hostEnv);
136
137
  }
137
138
 
138
139
  function configGetLeaf(program: CliProgram): CliLeaf {
@@ -1,7 +1,7 @@
1
1
  import { capabilityDeniedMessage, resolveCapabilities } from "../capabilities.ts";
2
2
  import { Cli } from "../cli.ts";
3
3
  import { cliBuiltinDocsGroupIfEnabled } from "../docs/builtin.ts";
4
- import { cliInstall } from "../install/index.ts";
4
+ import { cliInstall, cliUninstall } from "../install/index.ts";
5
5
  import { runMcpBundle } from "../mcp/bundle.ts";
6
6
  import type { ParseResult } from "../parse.ts";
7
7
  import { ParseKind } from "../parse.ts";
@@ -15,6 +15,7 @@ import { cliBuiltinConfigGroupIfEnabled } from "./config.ts";
15
15
  import { cliBuiltinInstallCommand } from "./install.ts";
16
16
  import { cliBuiltinMcpCommand } from "./mcp.ts";
17
17
  import { cliPresentationRoot } from "./presentation.ts";
18
+ import { cliBuiltinUninstallCommand } from "./uninstall.ts";
18
19
  import { cliBuiltinVersionCommand } from "./version.ts";
19
20
 
20
21
  export interface DispatchBuiltinOpts {
@@ -103,6 +104,18 @@ export async function dispatchBuiltin(
103
104
  }
104
105
  await cliInstall(program, pr.opts);
105
106
  }
107
+
108
+ if (pr.path[0] === "uninstall") {
109
+ if (!caps.install) {
110
+ process.stderr.write(capabilityDeniedMessage("install"));
111
+ process.exit(1);
112
+ }
113
+ if (pr.path.length !== 1) {
114
+ process.stderr.write(`Unknown subcommand: uninstall ${pr.path.slice(1).join(" ")}\n`);
115
+ process.exit(1);
116
+ }
117
+ await cliUninstall(program, pr.opts);
118
+ }
106
119
  }
107
120
 
108
121
  /** Built-in intercept roots for leaf programs. */
@@ -139,6 +152,17 @@ export function builtinInterceptRoot(
139
152
  };
140
153
  }
141
154
 
155
+ if (first === "uninstall" && caps.install) {
156
+ return {
157
+ parseRoot: {
158
+ key: program.key,
159
+ description: program.description,
160
+ commands: [cliBuiltinUninstallCommand(program)],
161
+ },
162
+ isLeafCompletionIntercept: false,
163
+ };
164
+ }
165
+
142
166
  if (first === "mcp" && caps.mcp) {
143
167
  return {
144
168
  parseRoot: {