argsbarg 4.1.0 → 5.0.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 (119) hide show
  1. package/CHANGELOG.md +74 -1
  2. package/README.md +91 -85
  3. package/docs/README.md +8 -8
  4. package/docs/ai-skills.md +9 -9
  5. package/docs/bundled-docs.md +5 -5
  6. package/docs/cli-program.md +13 -11
  7. package/docs/config-schema.md +36 -9
  8. package/docs/configure.md +177 -0
  9. package/docs/developing.md +7 -7
  10. package/docs/distribution-homebrew.md +104 -0
  11. package/docs/mcp.md +11 -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/nested.ts +1 -3
  33. package/index.d.ts +49 -81
  34. package/package.json +2 -2
  35. package/src/builtins/builtins.test.ts +84 -64
  36. package/src/builtins/completion-group.ts +17 -17
  37. package/src/builtins/configure-copy.ts +86 -0
  38. package/src/builtins/configure.ts +70 -0
  39. package/src/builtins/dispatch.ts +13 -9
  40. package/src/builtins/index.ts +1 -1
  41. package/src/builtins/mcp.ts +2 -2
  42. package/src/builtins/registry.ts +6 -4
  43. package/src/capabilities.ts +22 -15
  44. package/src/cli-tool/cli-smoke.test.ts +29 -0
  45. package/src/cli-tool/create.test.ts +141 -0
  46. package/src/cli-tool/create.ts +402 -0
  47. package/{examples/consumer-app/capabilities.test.ts → src/cli-tool/full-example-capabilities.test.ts} +16 -15
  48. package/src/cli-tool/main.ts +8 -0
  49. package/src/cli-tool/post-create.ts +111 -0
  50. package/src/cli-tool/program.ts +97 -0
  51. package/src/cli-tool/prompt.ts +28 -0
  52. package/src/cli-tool/run-create.ts +138 -0
  53. package/src/cli.ts +0 -2
  54. package/src/config/bootstrap.ts +27 -18
  55. package/src/config/file.test.ts +1 -1
  56. package/src/config/resolve.test.ts +167 -0
  57. package/src/config/resolve.ts +52 -8
  58. package/src/configure/configure.test.ts +148 -0
  59. package/src/configure/index.ts +284 -0
  60. package/src/configure/prompt.ts +40 -0
  61. package/src/docs/api-guide.test.ts +4 -5
  62. package/src/docs/builtin.ts +3 -5
  63. package/src/docs/docs.test.ts +6 -5
  64. package/src/docs/mcp-guide.ts +11 -12
  65. package/src/index.ts +5 -12
  66. package/src/install/binary-placement.test.ts +101 -0
  67. package/src/install/binary-placement.ts +47 -0
  68. package/src/install/install-validate.test.ts +5 -5
  69. package/src/install/normalize-uninstall.ts +11 -0
  70. package/src/install/normalize.ts +4 -19
  71. package/src/install/opts.ts +17 -0
  72. package/src/install/paths.ts +0 -22
  73. package/src/install/plan.ts +14 -6
  74. package/src/install/shell.ts +0 -14
  75. package/src/install/status.test.ts +6 -6
  76. package/src/install/status.ts +0 -6
  77. package/src/install/target-effective.ts +8 -10
  78. package/src/install/target-scope.ts +26 -36
  79. package/src/install/target-types.ts +0 -16
  80. package/src/install/targets/app.ts +19 -28
  81. package/src/install/targets/configure.ts +6 -2
  82. package/src/install/targets/index.ts +0 -3
  83. package/src/install/targets.test.ts +26 -44
  84. package/src/invoke.test.ts +1 -1
  85. package/src/mcp/env.test.ts +92 -0
  86. package/src/mcp/env.ts +15 -14
  87. package/src/mcp/tools.ts +1 -1
  88. package/src/mcp.integration.test.ts +4 -4
  89. package/src/parse.test.ts +13 -14
  90. package/src/prompt.ts +10 -0
  91. package/src/schema.ts +1 -1
  92. package/src/skill/hint.ts +2 -2
  93. package/src/types.ts +48 -22
  94. package/src/validate.ts +22 -28
  95. package/docs/install.md +0 -290
  96. package/docs/templates/cursor/rules/cli-program.mdc +0 -31
  97. package/examples/config-app/main.ts +0 -20
  98. package/examples/config-app/program.ts +0 -78
  99. package/examples/config-app/schema.ts +0 -37
  100. package/examples/config-app/types.ts +0 -19
  101. package/examples/consumer-app/README.md +0 -56
  102. package/examples/consumer-app/src/main.ts +0 -15
  103. package/examples/consumer-app/src/program.ts +0 -108
  104. package/src/builtins/install.ts +0 -136
  105. package/src/install/app.ts +0 -94
  106. package/src/install/bootstrap.ts +0 -22
  107. package/src/install/completions.ts +0 -56
  108. package/src/install/index.ts +0 -415
  109. package/src/install/install.test.ts +0 -333
  110. package/src/install/targets/completions.ts +0 -133
  111. package/src/install/update.test.ts +0 -123
  112. package/src/install/update.ts +0 -54
  113. /package/examples/{consumer-app → full-example}/schemas/configSchemas.ts +0 -0
  114. /package/examples/{consumer-app → full-example}/schemas/outputSchemas.ts +0 -0
  115. /package/examples/{consumer-app → full-example}/scripts/schemagen/discover-schema-roots.test.ts +0 -0
  116. /package/examples/{consumer-app → full-example}/scripts/schemagen/discover-schema-roots.ts +0 -0
  117. /package/examples/{consumer-app → full-example}/scripts/schemagen/naming.ts +0 -0
  118. /package/examples/{consumer-app → full-example}/scripts/schemagen.ts +0 -0
  119. /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;
@@ -245,21 +249,24 @@ export interface CliMcpToolConfig {
245
249
  */
246
250
  outputSchema?: Record<string, unknown>;
247
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
+ }
248
265
  /**
249
- * 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).
250
268
  */
251
- export interface CliUpdateArtifact {
252
- /** Path to an executable binary to copy into the install location. */
253
- path: string;
254
- /** Release version of `path` (used for already-current checks and success messages). */
255
- version?: string;
256
- /** Called after reinstall completes (e.g. remove a temp download directory). */
257
- cleanup?: () => void | Promise<void>;
258
- }
259
- /** Fetches the latest release binary for `install --update`. */
260
- export type CliUpdateGetLatest = (ctx: {
261
- version: string;
262
- }) => Promise<CliUpdateArtifact>;
269
+ export type CliAppConfigResolveFn = (ctx: CliAppConfigResolveContext) => unknown;
263
270
  /**
264
271
  * Metadata overlay for one key in {@link CliAppConfig.entries}.
265
272
  * Types and validation come from {@link CliAppConfig.jsonSchema} when set; otherwise all values are strings.
@@ -280,6 +287,11 @@ export interface CliAppConfigEntry {
280
287
  sensitive?: boolean;
281
288
  /** When set: non-empty `process.env[env]` overrides file; value exported after resolve. */
282
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;
283
295
  }
284
296
  /**
285
297
  * App configuration block on the program root ({@link CliProgram.appConfig}).
@@ -295,23 +307,23 @@ export interface CliAppConfig {
295
307
  /** Per-key metadata; keys must match `jsonSchema.properties` when `jsonSchema` is set. */
296
308
  entries: Record<string, CliAppConfigEntry>;
297
309
  }
298
- export interface CliInstallConfig {
299
- /** When `false`, hide/disable `install` (default: enabled). */
310
+ /** Opt-out for the `completion` built-in (default: enabled). */
311
+ export interface CliCompletionConfig {
312
+ /** When `false`, hide/disable `completion` (default: enabled). */
313
+ enabled?: boolean;
314
+ }
315
+ export interface CliConfigureConfig {
316
+ /** When `false`, hide/disable `configure` (default: enabled). */
300
317
  enabled?: boolean;
301
318
  /**
302
- * Default agent integration for full install (`install --all`).
303
- * - `'mcp'` when `mcpServer.enabled` (default): MCP targets in `--all`; paired skills excluded.
304
- * - `'skill'` when MCP is off (default): skill targets in `--all`; paired MCP excluded.
305
- * - `'both'`: install MCP and skill for the same host when both are available.
319
+ * Default agent integration for sync (`configure --sync`).
320
+ * - `'mcp'` when `mcpServer.enabled` (default): MCP targets in sync; paired skills excluded.
321
+ * - `'skill'` when MCP is off (default): skill targets in sync; paired MCP excluded.
322
+ * - `'both'`: sync MCP and skill for the same host when both are available.
306
323
  */
307
324
  agentIntegration?: InstallAgentIntegration;
308
- /** Per-artifact gates for full install/uninstall. See {@link resolveEffectiveInstallTargets}. */
309
- targets?: CliInstallTargets;
310
- /**
311
- * When set, enables `install --update` on the program root.
312
- * Should download or locate the latest release binary and return its path.
313
- */
314
- updateGetLatest?: CliUpdateGetLatest;
325
+ /** Per-artifact gates for configure sync and interactive wizard. See {@link resolveEffectiveInstallTargets}. */
326
+ targets?: CliConfigureTargets;
315
327
  }
316
328
  /** Agent integration mode for install — MCP vs shell skill per host. */
317
329
  export type InstallAgentIntegration = "mcp" | "skill" | "both";
@@ -319,16 +331,16 @@ export type InstallAgentIntegration = "mcp" | "skill" | "both";
319
331
  export type InstallTargetSpec = boolean | {
320
332
  /** When false, artifact is never installed (even with scoped CLI flags). Default true. */
321
333
  enabled?: boolean;
322
- /** When true, included in bare `install` / `install --all`. Default varies by key. */
334
+ /** When true, included in `configure --sync`. Default varies by key. */
323
335
  includedInAll?: boolean;
324
336
  };
325
337
  export interface ResolvedInstallTarget {
326
338
  enabled: boolean;
327
339
  includedInAll: boolean;
328
340
  }
329
- /** Per-artifact gates for full install/uninstall. See {@link resolveEffectiveInstallTargets}. */
330
- export interface CliInstallTargets {
331
- /** Copy app to `~/.local/bin/<key>`. Default includedInAll true (opt-out). */
341
+ /** Per-artifact gates for configure. See {@link resolveEffectiveInstallTargets}. */
342
+ export interface CliConfigureTargets {
343
+ /** App binary status only (Homebrew PATH); no self-install. */
332
344
  app?: InstallTargetSpec;
333
345
  /** ChatGPT desktop MCP. Default false. */
334
346
  chatgptMcp?: InstallTargetSpec;
@@ -342,9 +354,7 @@ export interface CliInstallTargets {
342
354
  codexMcp?: InstallTargetSpec;
343
355
  /** Codex skill. Default false. */
344
356
  codexSkill?: InstallTargetSpec;
345
- /** Shell completions for detected shells. Default includedInAll true (opt-out). */
346
- completions?: InstallTargetSpec;
347
- /** App config: wizard on install, file removal on uninstall. Default includedInAll true. */
357
+ /** App config: interactive wizard step in `configure`. Default not in sync. */
348
358
  configure?: InstallTargetSpec;
349
359
  /** Cursor MCP. Default false. */
350
360
  cursorMcp?: InstallTargetSpec;
@@ -442,8 +452,10 @@ export type CliProgram = CliNode & {
442
452
  appConfig?: CliAppConfig;
443
453
  /** When set with `enabled: true`, enables the `mcp` built-in subcommand. */
444
454
  mcpServer?: CliMcpServerConfig;
445
- /** Opt-out and defaults for `install`. */
446
- install?: CliInstallConfig;
455
+ /** Opt-out and defaults for `configure`. */
456
+ configure?: CliConfigureConfig;
457
+ /** Opt-out for shell completion generation (`completion bash|zsh|fish`). */
458
+ completion?: CliCompletionConfig;
447
459
  /** When set with `enabled: true`, enables the `docs` built-in command group. */
448
460
  docs?: CliDocsConfig;
449
461
  };
@@ -461,11 +473,10 @@ export declare class CliSchemaValidationError extends Error {
461
473
  }
462
474
  /** Platform builtins derived from program config and runtime. */
463
475
  export interface CliCapabilities {
464
- completion: true;
476
+ completion: boolean;
465
477
  mcp: boolean;
466
- install: boolean;
478
+ configure: boolean;
467
479
  docs: boolean;
468
- update: boolean;
469
480
  configCommands: boolean;
470
481
  }
471
482
  /** JSON-safe command node (no handlers). */
@@ -547,49 +558,6 @@ export declare function shouldRunHeadlessWithYes(ctx: HeadlessContext, opts: {
547
558
  export declare function requireYesInNonTty(yes: boolean, hint: string, dryRun?: boolean, interactive?: boolean): void;
548
559
  /** Prefixes a success message when running in dry-run mode. */
549
560
  export declare function formatDryRunMessage(message: string, dryRun: boolean): string;
550
- /** Config for {@link ghReleaseUpdateGetLatest}. */
551
- export interface GhReleaseUpdateConfig {
552
- /** GitHub `owner/repo` slug. */
553
- repo: string;
554
- /** Release asset filename (e.g. `myapp`). */
555
- asset: string;
556
- /** Temp directory name prefix for downloads. */
557
- tempPrefix: string;
558
- /** Path to the on-disk version-check cache JSON file. */
559
- cachePath: string;
560
- /** Optional hint when `gh auth` fails or no releases exist. */
561
- repoEnvHint?: string;
562
- }
563
- /** Config for {@link createGhVersionCheck}. */
564
- export interface GhVersionCheckConfig {
565
- /** Installed semver string. */
566
- currentVersion: string;
567
- /** CLI command name for update notices (e.g. `qa`). */
568
- commandName: string;
569
- /** Path to the on-disk version-check cache JSON file. */
570
- cachePath: string;
571
- /** Cache TTL in milliseconds (default 24h). */
572
- ttlMs?: number;
573
- /** When true, skip background refresh (e.g. test subprocess). */
574
- skipRefresh?: () => boolean;
575
- /** When true, skip refresh because `gh` is unavailable. */
576
- ghAvailable?: () => boolean;
577
- /** Fetches latest release version via `gh`. */
578
- fetchLatest: () => Promise<string>;
579
- }
580
- /** Returns whether the installed version matches the latest release. */
581
- export declare function isAlreadyCurrent(current: string, latest: string): boolean;
582
- /** Strips a leading `v` from a release tag. */
583
- export declare function parseReleaseTag(tag: string): string;
584
- /** Builds a `CliUpdateGetLatest` hook that downloads a release via `gh`. */
585
- export declare function ghReleaseUpdateGetLatest(config: GhReleaseUpdateConfig): CliUpdateGetLatest;
586
- /** Version-check cache helpers for summary notices and background refresh. */
587
- export declare function createGhVersionCheck(config: GhVersionCheckConfig): {
588
- getUpdateNotice: () => string | null;
589
- refreshIfStale: () => void;
590
- };
591
- /** Shared `gh release view` fetcher for hooks and version-check refresh. */
592
- export declare function createGhFetchLatest(config: Pick<GhReleaseUpdateConfig, "repo" | "repoEnvHint">): () => Promise<string>;
593
561
  /** Resolved paths for `mcp bundle`. */
594
562
  export interface McpBundlePaths {
595
563
  binaryPath: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "argsbarg",
3
- "version": "4.1.0",
3
+ "version": "5.0.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,9 +1,12 @@
1
1
  import { describe, expect, test } from "bun:test";
2
+ import { resolveCapabilities } from "../capabilities.ts";
3
+ import { cliBuiltinDocsGroup } from "../docs/builtin.ts";
2
4
  import { ParseKind, parse, postParseValidate } from "../parse.ts";
3
5
  import type { CliProgram } from "../types.ts";
6
+ import { cliBuiltinConfigureCommand, configureBuiltinOptions } from "./configure.ts";
7
+ import { configureCommandDescription, configureSyncOptionDescription } from "./configure-copy.ts";
4
8
  import { exportPresentationBuiltins } from "./export.ts";
5
9
  import { completionBashScript, completionFishScript, completionZshScript } from "./index.ts";
6
- import { cliBuiltinInstallCommand, installBuiltinOptions } from "./install.ts";
7
10
  import { cliBuiltinMcpCommand } from "./mcp.ts";
8
11
  import { cliParseRoot, cliPresentationRoot } from "./presentation.ts";
9
12
 
@@ -21,55 +24,61 @@ const fixture: CliProgram = {
21
24
  ],
22
25
  };
23
26
 
27
+ const noMcp: CliProgram = {
28
+ key: "skillonly",
29
+ version: "0.0.0",
30
+ description: "Skills only.",
31
+ commands: [{ key: "ping", description: "Ping.", handler: () => {} }],
32
+ };
33
+
24
34
  describe("builtins help copy", () => {
25
- test("install command includes description and option text", () => {
26
- const install = cliBuiltinInstallCommand(fixture);
27
- expect(install.description).toContain("shell completions");
28
- expect(install.notes).toContain("install --all");
29
- const names = installBuiltinOptions(fixture).map((o) => o.name);
30
- expect(names).toContain("all");
31
- expect(names).toContain("mcp");
32
- expect(names.indexOf("all")).toBeLessThan(names.indexOf("mcp"));
33
- expect(names.indexOf("mcp")).toBeLessThan(names.indexOf("status"));
34
- expect(names.indexOf("status")).toBeLessThan(names.indexOf("uninstall"));
35
- expect(names.indexOf("uninstall")).toBeLessThan(names.indexOf("from"));
36
- expect(names.indexOf("from")).toBeLessThan(names.indexOf("yes"));
37
- const yesOpt = installBuiltinOptions(fixture).find((o) => o.name === "yes");
35
+ test("configure command includes Homebrew-oriented description", () => {
36
+ const configure = cliBuiltinConfigureCommand(fixture);
37
+ expect(configure.description).toContain("agent skills");
38
+ expect(configure.description).toContain("MCP config");
39
+ expect(configure.notes).toContain("brew install");
40
+ const names = configureBuiltinOptions(fixture).map((o) => o.name);
41
+ expect(names).toContain("sync");
42
+ expect(names).toContain("remove-all");
43
+ expect(names).toContain("status");
44
+ const yesOpt = configureBuiltinOptions(fixture).find((o) => o.name === "yes");
38
45
  expect(yesOpt?.shortName).toBe("y");
39
46
  });
40
47
 
41
- test("install -y parses as --yes", () => {
42
- const root = cliParseRoot(fixture);
43
- const pr = postParseValidate(root, parse(root, ["install", "-y"]));
44
- expect(pr.kind).toBe(ParseKind.Ok);
45
- if (pr.kind === ParseKind.Ok) {
46
- expect(pr.opts.yes).toBe("1");
47
- }
48
- });
49
-
50
- test("install omits --mcp option when mcpServer unset", () => {
51
- const noMcp: CliProgram = { key: "x", version: "0.0.0", description: "x", handler: () => {} };
52
- const names = installBuiltinOptions(noMcp).map((o) => o.name);
53
- expect(names).not.toContain("mcp");
48
+ test("configure copy omits MCP when mcpServer unset", () => {
49
+ const caps = resolveCapabilities(noMcp);
50
+ expect(configureCommandDescription(noMcp, caps)).toBe(
51
+ "Set up agent skills for this app (binary via Homebrew).",
52
+ );
53
+ expect(configureCommandDescription(noMcp, caps)).not.toContain("MCP");
54
+ expect(configureSyncOptionDescription(noMcp, caps)).not.toContain("MCP");
55
+ const configure = cliBuiltinConfigureCommand(noMcp);
56
+ expect(configure.description).not.toContain("MCP");
54
57
  });
55
58
 
56
- test("install omits --update when updateGetLatest unset", () => {
57
- const install = cliBuiltinInstallCommand(fixture);
58
- expect(installBuiltinOptions(fixture).map((o) => o.name)).not.toContain("update");
59
- expect(install.notes).not.toContain("Upgrade to latest release");
60
- expect(install.notes).toContain("Refresh after upgrading");
61
- });
59
+ test("configure notes mention brew upgrade and interactive configure", () => {
60
+ const configure = cliBuiltinConfigureCommand(fixture);
61
+ expect(configure.notes).toContain("brew upgrade");
62
+ expect(configure.notes).toContain("configure --sync --yes");
63
+ expect(configure.notes).toContain(`${fixture.key} configure`);
62
64
 
63
- test("install notes include upgrade section when updateGetLatest is set", () => {
64
- const withUpdate: CliProgram = {
65
+ const withConfig: CliProgram = {
65
66
  ...fixture,
66
- install: { updateGetLatest: async () => ({ path: process.execPath }) },
67
+ appConfig: {
68
+ entries: { token: { description: "Token.", env: "TOKEN" } },
69
+ },
67
70
  };
68
- const install = cliBuiltinInstallCommand(withUpdate);
69
- const notes = install.notes ?? "";
70
- expect(installBuiltinOptions(withUpdate).map((o) => o.name)).toContain("update");
71
- expect(notes).toContain("Upgrade to latest release");
72
- expect(notes.indexOf("install --reinstall")).toBeLessThan(notes.indexOf("install --update"));
71
+ expect(configureBuiltinOptions(withConfig).map((o) => o.name)).toContain("remove-config");
72
+ });
73
+
74
+ test("configure -y parses as --yes", () => {
75
+ const root = cliParseRoot(fixture);
76
+ const pr = postParseValidate(root, parse(root, ["configure", "-y", "--sync"]));
77
+ expect(pr.kind).toBe(ParseKind.Ok);
78
+ if (pr.kind === ParseKind.Ok) {
79
+ expect(pr.opts.yes).toBe("1");
80
+ expect(pr.opts.sync).toBe("1");
81
+ }
73
82
  });
74
83
 
75
84
  test("mcp builtin description is user-facing", () => {
@@ -79,38 +88,31 @@ describe("builtins help copy", () => {
79
88
  };
80
89
  const mcp = cliBuiltinMcpCommand(withDocs);
81
90
  expect(mcp.description).toContain("MCP server");
82
- expect(mcp.notes).toContain("install --mcp --yes");
91
+ expect(mcp.notes).toContain("configure");
83
92
  expect(mcp.notes).toContain("docs mcp");
84
93
  });
85
94
  });
86
95
 
87
96
  describe("presentation root", () => {
88
- test("includes mcp and install when enabled", () => {
97
+ test("includes mcp and configure when enabled", () => {
89
98
  const root = cliPresentationRoot(fixture);
90
99
  const keys = root.commands?.map((c) => c.key) ?? [];
91
100
  expect(keys).toContain("mcp");
92
- expect(keys).toContain("install");
101
+ expect(keys).toContain("configure");
102
+ expect(keys).not.toContain("completion");
103
+ expect(keys).not.toContain("install");
93
104
  });
94
105
 
95
- test("omits install when install.enabled is false", () => {
96
- const disabled: CliProgram = { ...fixture, install: { enabled: false } };
106
+ test("omits configure when configure.enabled is false", () => {
107
+ const disabled: CliProgram = { ...fixture, configure: { enabled: false } };
97
108
  const root = cliPresentationRoot(disabled);
98
- expect(root.commands?.map((c) => c.key)).not.toContain("install");
109
+ expect(root.commands?.map((c) => c.key)).not.toContain("configure");
99
110
  });
111
+
100
112
  test("includes version builtin", () => {
101
113
  const root = cliPresentationRoot(fixture);
102
114
  expect(root.commands?.map((c) => c.key)).toContain("version");
103
115
  });
104
-
105
- test("root notes include agent hint when docs enabled", () => {
106
- const withDocs: CliProgram = {
107
- ...fixture,
108
- docs: { enabled: true, topics: { readme: { text: "# r\n" } } },
109
- };
110
- const root = cliPresentationRoot(withDocs);
111
- expect(root.notes).toContain("For AI agents: `myapp docs skill`.");
112
- expect(root.notes).not.toContain("install --skill");
113
- });
114
116
  });
115
117
 
116
118
  describe("completion emitters", () => {
@@ -119,14 +121,14 @@ describe("completion emitters", () => {
119
121
  const fish = completionFishScript(schema);
120
122
  expect(fish).toContain("complete -c myapp");
121
123
  expect(fish).toContain("hello");
122
- expect(fish).toContain("install");
124
+ expect(fish).toContain("configure");
123
125
  });
124
126
 
125
- test("bash script includes install flags", () => {
127
+ test("bash script includes configure flags", () => {
126
128
  const schema = cliPresentationRoot(fixture);
127
129
  const bash = completionBashScript(schema);
128
- expect(bash).toContain("--all");
129
- expect(bash).toContain("install");
130
+ expect(bash).toContain("hello");
131
+ expect(bash).toContain("--sync");
130
132
  });
131
133
 
132
134
  test("zsh script registers compdef", () => {
@@ -154,11 +156,29 @@ describe("schema export builtins", () => {
154
156
  };
155
157
  const builtins = exportPresentationBuiltins(withConfig);
156
158
  expect(builtins.map((b) => b.key)).toContain("config");
159
+ expect(builtins.map((b) => b.key)).toContain("configure");
157
160
  });
158
161
 
159
- test("exportPresentationBuiltins includes install options", () => {
162
+ test("exportPresentationBuiltins omits hidden completion", () => {
160
163
  const builtins = exportPresentationBuiltins(fixture);
161
- const install = builtins.find((b) => b.key === "install");
162
- expect(install?.options?.find((o) => o.name === "all")?.description).toContain("app");
164
+ expect(builtins.map((b) => b.key)).not.toContain("completion");
165
+ });
166
+ });
167
+
168
+ describe("docs skill topic copy", () => {
169
+ test("mentions configure when configure is enabled", () => {
170
+ const withDocs: CliProgram = {
171
+ ...noMcp,
172
+ docs: { enabled: true, topics: { readme: { text: "# r\n" } } },
173
+ };
174
+ const skill = cliBuiltinDocsGroup(withDocs).commands.find((c) => c.key === "skill");
175
+ expect(skill?.description).toContain("configure");
176
+
177
+ const configureOff: CliProgram = {
178
+ ...withDocs,
179
+ configure: { enabled: false },
180
+ };
181
+ const skillOff = cliBuiltinDocsGroup(configureOff).commands.find((c) => c.key === "skill");
182
+ expect(skillOff?.description).not.toContain("configure");
163
183
  });
164
184
  });
@@ -1,23 +1,22 @@
1
- import { resolveCapabilities } from "../capabilities.ts";
2
- import type { CliProgram, CliRouter } from "../types.ts";
1
+ import type { CliProgram } from "../types.ts";
3
2
 
4
3
  /**
5
4
  * Builds the static `completion` / `bash` / `zsh` / `fish` command subtree (merged into the program root at runtime).
6
5
  */
7
- export function cliBuiltinCompletionGroup(program: CliProgram): CliRouter {
6
+ export function cliBuiltinCompletionGroup(program: CliProgram): import("../types.ts").CliRouter {
8
7
  const appName = program.key;
9
- const caps = resolveCapabilities(program);
10
- const router: CliRouter = {
8
+ const router: import("../types.ts").CliRouter = {
11
9
  key: "completion",
10
+ hidden: true,
12
11
  description: "Generate the autocompletion script for shells.",
13
12
  commands: [
14
13
  {
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,16 @@ 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`;
49
- }
47
+ router.notes =
48
+ "Completions are installed by Homebrew during formula install.\n\n" +
49
+ "See: https://docs.brew.sh/Shell-Completion";
50
50
  return router;
51
51
  }
@@ -0,0 +1,86 @@
1
+ /** Capability-aware labels for configure builtin and docs copy. */
2
+
3
+ import type { CliCapabilities } from "../capabilities.ts";
4
+ import type { CliProgram } from "../types.ts";
5
+
6
+ type Kind = "skills" | "mcp" | "config";
7
+
8
+ const LABEL: Record<Kind, { prose: string; short: string }> = {
9
+ skills: { prose: "agent skills", short: "skills" },
10
+ mcp: { prose: "MCP config", short: "MCP" },
11
+ config: { prose: "app config", short: "config" },
12
+ };
13
+
14
+ function enabledKinds(program: CliProgram, caps: CliCapabilities): Kind[] {
15
+ const kinds: Kind[] = ["skills"];
16
+ if (caps.mcp) kinds.push("mcp");
17
+ if (program.appConfig) kinds.push("config");
18
+ return kinds;
19
+ }
20
+
21
+ function joinEnglish(items: string[]): string {
22
+ if (items.length === 0) return "agent artifacts";
23
+ if (items.length === 1) return items[0]!;
24
+ if (items.length === 2) return `${items[0]} and ${items[1]}`;
25
+ return `${items.slice(0, -1).join(", ")}, and ${items[items.length - 1]}`;
26
+ }
27
+
28
+ function prose(program: CliProgram, caps: CliCapabilities): string {
29
+ return joinEnglish(enabledKinds(program, caps).map((k) => LABEL[k].prose));
30
+ }
31
+
32
+ function short(program: CliProgram, caps: CliCapabilities): string {
33
+ return joinEnglish(enabledKinds(program, caps).map((k) => LABEL[k].short));
34
+ }
35
+
36
+ export function configureCommandDescription(program: CliProgram, caps: CliCapabilities): string {
37
+ return `Set up ${prose(program, caps)} for this app (binary via Homebrew).`;
38
+ }
39
+
40
+ export function configureSyncOptionDescription(program: CliProgram, caps: CliCapabilities): string {
41
+ return `Refresh installed ${short(program, caps)}. Used by Homebrew post_install.`;
42
+ }
43
+
44
+ export function docsSkillTopicDescription(program: CliProgram, caps: CliCapabilities): string {
45
+ if (caps.configure) {
46
+ return "Print a reference agent SKILL; run `configure` to install an optimized copy.";
47
+ }
48
+ return "Print a reference agent SKILL for AI agents.";
49
+ }
50
+
51
+ export function configureCommandNotes(program: CliProgram, caps: CliCapabilities): string {
52
+ const app = program.key;
53
+ const lines = [
54
+ "Install the binary via Homebrew (tap-from-repo), then set up agent artifacts:",
55
+ ` brew tap <org>/<repo>`,
56
+ ` brew install <tap>/${app}`,
57
+ "",
58
+ "Homebrew post_install runs:",
59
+ ` ${app} configure --sync --yes`,
60
+ "",
61
+ "Interactive setup (per target):",
62
+ ` ${app} configure`,
63
+ "",
64
+ "Upgrade:",
65
+ ` brew upgrade ${app}`,
66
+ "",
67
+ "Shell completions are installed by Homebrew during brew install.",
68
+ "See: https://docs.brew.sh/Shell-Completion",
69
+ "",
70
+ "See what is installed:",
71
+ ` ${app} configure --status`,
72
+ "",
73
+ "Remove agent artifacts before brew uninstall:",
74
+ ` ${app} configure --remove-all --yes`,
75
+ ` brew uninstall <tap>/${app}`,
76
+ "",
77
+ ];
78
+ if (program.appConfig) {
79
+ lines.push("Remove app config only:", ` ${app} configure --remove-config --yes`, "");
80
+ }
81
+ lines.push(
82
+ "Use --dry to preview changes without writing files.",
83
+ "Use --json for machine-readable output.",
84
+ );
85
+ return lines.join("\n");
86
+ }
@@ -0,0 +1,70 @@
1
+ import { resolveCapabilities } from "../capabilities.ts";
2
+ import { type CliLeaf, type CliOption, CliOptionKind, type CliProgram } from "../types.ts";
3
+ import {
4
+ configureCommandDescription,
5
+ configureCommandNotes,
6
+ configureSyncOptionDescription,
7
+ } from "./configure-copy.ts";
8
+
9
+ /** Configure command options. */
10
+ export function configureBuiltinOptions(root: CliProgram): CliOption[] {
11
+ const caps = resolveCapabilities(root);
12
+ const opts: CliOption[] = [
13
+ {
14
+ name: "sync",
15
+ description: configureSyncOptionDescription(root, caps),
16
+ kind: CliOptionKind.Presence,
17
+ },
18
+ {
19
+ name: "remove-all",
20
+ description: "Remove all detected agent artifacts (skills and MCP).",
21
+ kind: CliOptionKind.Presence,
22
+ },
23
+ ];
24
+
25
+ if (root.appConfig) {
26
+ opts.push({
27
+ name: "remove-config",
28
+ description: "Remove the app config file only.",
29
+ kind: CliOptionKind.Presence,
30
+ });
31
+ }
32
+
33
+ opts.push(
34
+ {
35
+ name: "status",
36
+ description: "Print what is currently installed (read-only).",
37
+ kind: CliOptionKind.Presence,
38
+ },
39
+ {
40
+ name: "yes",
41
+ description: "Skip confirmation (required for --sync, --remove-all, --remove-config).",
42
+ kind: CliOptionKind.Presence,
43
+ shortName: "y",
44
+ },
45
+ {
46
+ name: "dry",
47
+ description: "Show what would change without writing files.",
48
+ kind: CliOptionKind.Presence,
49
+ },
50
+ {
51
+ name: "json",
52
+ description: "Print changed paths or status JSON on stdout.",
53
+ kind: CliOptionKind.Presence,
54
+ },
55
+ );
56
+
57
+ return opts;
58
+ }
59
+
60
+ /** Builds the `configure` built-in command. */
61
+ export function cliBuiltinConfigureCommand(root: CliProgram): CliLeaf {
62
+ const caps = resolveCapabilities(root);
63
+ return {
64
+ key: "configure",
65
+ description: configureCommandDescription(root, caps),
66
+ options: configureBuiltinOptions(root),
67
+ notes: configureCommandNotes(root, caps),
68
+ handler: () => {},
69
+ };
70
+ }