argsbarg 4.1.1 → 5.0.2

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 (82) hide show
  1. package/CHANGELOG.md +34 -1
  2. package/README.md +5 -5
  3. package/docs/README.md +3 -3
  4. package/docs/ai-skills.md +9 -9
  5. package/docs/bundled-docs.md +4 -4
  6. package/docs/cli-program.md +8 -8
  7. package/docs/config-schema.md +2 -2
  8. package/docs/configure.md +177 -0
  9. package/docs/developing.md +1 -1
  10. package/docs/distribution-homebrew.md +12 -9
  11. package/docs/mcp.md +9 -9
  12. package/examples/full-example/README.md +3 -3
  13. package/examples/full-example/justfile +6 -6
  14. package/examples/full-example/scripts/formula-shared.ts +3 -2
  15. package/examples/full-example/src/program.ts +3 -3
  16. package/examples/nested.ts +1 -1
  17. package/index.d.ts +23 -16
  18. package/package.json +1 -1
  19. package/src/builtins/builtins.test.ts +105 -61
  20. package/src/builtins/completion-group.ts +4 -6
  21. package/src/builtins/config.test.ts +8 -0
  22. package/src/builtins/configure-copy.ts +86 -0
  23. package/src/builtins/configure.ts +70 -0
  24. package/src/builtins/dispatch.ts +13 -33
  25. package/src/builtins/index.ts +1 -1
  26. package/src/builtins/mcp.ts +2 -2
  27. package/src/builtins/registry.ts +6 -6
  28. package/src/capabilities.ts +22 -13
  29. package/src/cli-tool/cli-smoke.test.ts +21 -3
  30. package/src/cli-tool/create.test.ts +36 -1
  31. package/src/cli-tool/create.ts +26 -4
  32. package/src/cli-tool/full-example-capabilities.test.ts +10 -2
  33. package/src/cli-tool/main.ts +0 -0
  34. package/src/cli-tool/program.ts +20 -5
  35. package/src/cli-tool/run-create.ts +8 -19
  36. package/src/config/bootstrap.ts +11 -9
  37. package/src/config/context.test.ts +8 -0
  38. package/src/config/file.test.ts +13 -1
  39. package/src/config/resolve.test.ts +18 -0
  40. package/src/config/resolve.ts +2 -2
  41. package/src/config/validate.test.ts +11 -0
  42. package/src/config.integration.test.ts +5 -0
  43. package/src/configure/configure.test.ts +170 -0
  44. package/src/configure/index.ts +298 -0
  45. package/src/configure/prompt.ts +47 -0
  46. package/src/docs/api-guide.test.ts +10 -0
  47. package/src/docs/builtin.ts +3 -5
  48. package/src/docs/docs.test.ts +32 -5
  49. package/src/docs/mcp-guide.ts +4 -4
  50. package/src/docs/mcp-resources.test.ts +10 -0
  51. package/src/formats.test.ts +9 -0
  52. package/src/headless.test.ts +10 -0
  53. package/src/hidden-mcpb.test.ts +22 -0
  54. package/src/index.ts +2 -2
  55. package/src/install/binary-placement.test.ts +14 -0
  56. package/src/install/gh-release-update.test.ts +9 -0
  57. package/src/install/install-validate.test.ts +15 -5
  58. package/src/install/mcp-codex.test.ts +7 -0
  59. package/src/install/mcp-openclaw.test.ts +6 -0
  60. package/src/install/mcp-opencode.test.ts +10 -0
  61. package/src/install/opts.ts +17 -0
  62. package/src/install/status.test.ts +9 -0
  63. package/src/install/target-effective.ts +8 -8
  64. package/src/install/target-scope.ts +11 -8
  65. package/src/install/targets/configure.ts +1 -1
  66. package/src/install/targets.test.ts +23 -4
  67. package/src/invoke.test.ts +15 -1
  68. package/src/mcp/claude.test.ts +10 -0
  69. package/src/mcp/env.test.ts +12 -0
  70. package/src/mcp/tools.ts +1 -1
  71. package/src/mcp/zip.test.ts +5 -0
  72. package/src/mcp.integration.test.ts +43 -4
  73. package/src/parse.test.ts +82 -13
  74. package/src/schema.ts +1 -9
  75. package/src/skill/hint.ts +2 -2
  76. package/src/types.ts +22 -14
  77. package/src/validate.ts +17 -17
  78. package/docs/install.md +0 -206
  79. package/src/builtins/install.ts +0 -106
  80. package/src/builtins/uninstall.ts +0 -80
  81. package/src/install/index.ts +0 -409
  82. package/src/install/install.test.ts +0 -317
@@ -1,7 +1,7 @@
1
1
  import { capabilityDeniedMessage, resolveCapabilities } from "../capabilities.ts";
2
2
  import { Cli } from "../cli.ts";
3
+ import { cliConfigure } from "../configure/index.ts";
3
4
  import { cliBuiltinDocsGroupIfEnabled } from "../docs/builtin.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";
@@ -12,10 +12,9 @@ import { completionFishScript } from "./completion-fish.ts";
12
12
  import { cliBuiltinCompletionGroup as completionGroup } from "./completion-group.ts";
13
13
  import { completionZshScript } from "./completion-zsh.ts";
14
14
  import { cliBuiltinConfigGroupIfEnabled } from "./config.ts";
15
- import { cliBuiltinInstallCommand } from "./install.ts";
15
+ import { cliBuiltinConfigureCommand } from "./configure.ts";
16
16
  import { cliBuiltinMcpCommand } from "./mcp.ts";
17
17
  import { cliPresentationRoot } from "./presentation.ts";
18
- import { cliBuiltinUninstallCommand } from "./uninstall.ts";
19
18
  import { cliBuiltinVersionCommand } from "./version.ts";
20
19
 
21
20
  export interface DispatchBuiltinOpts {
@@ -45,6 +44,10 @@ export async function dispatchBuiltin(
45
44
  const caps = resolveCapabilities(program);
46
45
 
47
46
  if (pr.path[0] === "completion") {
47
+ if (!caps.completion) {
48
+ process.stderr.write(capabilityDeniedMessage("completion"));
49
+ process.exit(1);
50
+ }
48
51
  const schemaForCompletion = completionSchema(program, opts);
49
52
  if (pr.path[1] === "bash") {
50
53
  process.stdout.write(completionBashScript(schemaForCompletion));
@@ -93,28 +96,16 @@ export async function dispatchBuiltin(
93
96
  process.exit(1);
94
97
  }
95
98
 
96
- if (pr.path[0] === "install") {
97
- if (!caps.install) {
98
- process.stderr.write(capabilityDeniedMessage("install"));
99
- process.exit(1);
100
- }
101
- if (pr.path.length !== 1) {
102
- process.stderr.write(`Unknown subcommand: install ${pr.path.slice(1).join(" ")}\n`);
103
- process.exit(1);
104
- }
105
- await cliInstall(program, pr.opts);
106
- }
107
-
108
- if (pr.path[0] === "uninstall") {
109
- if (!caps.install) {
110
- process.stderr.write(capabilityDeniedMessage("install"));
99
+ if (pr.path[0] === "configure") {
100
+ if (!caps.configure) {
101
+ process.stderr.write(capabilityDeniedMessage("configure"));
111
102
  process.exit(1);
112
103
  }
113
104
  if (pr.path.length !== 1) {
114
- process.stderr.write(`Unknown subcommand: uninstall ${pr.path.slice(1).join(" ")}\n`);
105
+ process.stderr.write(`Unknown subcommand: configure ${pr.path.slice(1).join(" ")}\n`);
115
106
  process.exit(1);
116
107
  }
117
- await cliUninstall(program, pr.opts);
108
+ await cliConfigure(program, pr.opts);
118
109
  }
119
110
  }
120
111
 
@@ -141,23 +132,12 @@ export function builtinInterceptRoot(
141
132
  };
142
133
  }
143
134
 
144
- if (first === "install" && caps.install) {
145
- return {
146
- parseRoot: {
147
- key: program.key,
148
- description: program.description,
149
- commands: [cliBuiltinInstallCommand(program)],
150
- },
151
- isLeafCompletionIntercept: false,
152
- };
153
- }
154
-
155
- if (first === "uninstall" && caps.install) {
135
+ if (first === "configure" && caps.configure) {
156
136
  return {
157
137
  parseRoot: {
158
138
  key: program.key,
159
139
  description: program.description,
160
- commands: [cliBuiltinUninstallCommand(program)],
140
+ commands: [cliBuiltinConfigureCommand(program)],
161
141
  },
162
142
  isLeafCompletionIntercept: false,
163
143
  };
@@ -2,9 +2,9 @@ export { completionBashScript } from "./completion-bash.ts";
2
2
  export { completionFishScript } from "./completion-fish.ts";
3
3
  export { cliBuiltinCompletionGroup } from "./completion-group.ts";
4
4
  export { completionZshScript } from "./completion-zsh.ts";
5
+ export { cliBuiltinConfigureCommand, configureBuiltinOptions } from "./configure.ts";
5
6
  export { builtinInterceptRoot, dispatchBuiltin } from "./dispatch.ts";
6
7
  export { type CliSchemaExport, exportPresentationBuiltins } from "./export.ts";
7
- export { cliBuiltinInstallCommand, installBuiltinOptions } from "./install.ts";
8
8
  export { cliBuiltinMcpCommand } from "./mcp.ts";
9
9
  export {
10
10
  cliParseRoot,
@@ -12,8 +12,8 @@ export function cliBuiltinMcpCommand(program: CliProgram): CliRouter {
12
12
  " args: mcp",
13
13
  "",
14
14
  ];
15
- if (caps.install) {
16
- lines.push("Or:", "", " {argsbarg:program} install --mcp --yes", "");
15
+ if (caps.configure) {
16
+ lines.push("Or:", "", " {argsbarg:program} configure", "");
17
17
  }
18
18
  if (docsEnabled(program)) {
19
19
  lines.push("Full setup guide: {argsbarg:program} docs mcp");
@@ -3,9 +3,8 @@ import { cliBuiltinDocsGroupIfEnabled } from "../docs/builtin.ts";
3
3
  import type { CliNode, CliProgram } from "../types.ts";
4
4
  import { cliBuiltinCompletionGroup } from "./completion-group.ts";
5
5
  import { cliBuiltinConfigGroupIfEnabled } from "./config.ts";
6
- import { cliBuiltinInstallCommand } from "./install.ts";
6
+ import { cliBuiltinConfigureCommand } from "./configure.ts";
7
7
  import { cliBuiltinMcpCommand } from "./mcp.ts";
8
- import { cliBuiltinUninstallCommand } from "./uninstall.ts";
9
8
  import { cliBuiltinVersionCommand } from "./version.ts";
10
9
 
11
10
  type BuiltinFactory = (program: CliProgram) => CliNode | null;
@@ -27,11 +26,12 @@ function pushBuiltin(
27
26
  /** Capability-gated built-in command nodes in stable order (parse, help, export). */
28
27
  export function resolveBuiltins(program: CliProgram, caps: CliCapabilities): CliNode[] {
29
28
  const builtins: CliNode[] = [];
30
- pushBuiltin(builtins, program, (p) => cliBuiltinCompletionGroup(p));
29
+ if (caps.completion) {
30
+ pushBuiltin(builtins, program, (p) => cliBuiltinCompletionGroup(p));
31
+ }
31
32
  pushBuiltin(builtins, program, () => cliBuiltinVersionCommand());
32
- if (caps.install) {
33
- pushBuiltin(builtins, program, (p) => cliBuiltinInstallCommand(p));
34
- pushBuiltin(builtins, program, (p) => cliBuiltinUninstallCommand(p));
33
+ if (caps.configure) {
34
+ pushBuiltin(builtins, program, (p) => cliBuiltinConfigureCommand(p));
35
35
  }
36
36
  pushBuiltin(builtins, program, (p) => cliBuiltinDocsGroupIfEnabled(p) ?? null);
37
37
  if (caps.mcp) {
@@ -8,20 +8,20 @@ import type { CliProgram } from "./types.ts";
8
8
 
9
9
  /** Platform builtins derived from program config and runtime. */
10
10
  export interface CliCapabilities {
11
- completion: true;
11
+ completion: boolean;
12
12
  mcp: boolean;
13
- install: boolean;
13
+ configure: boolean;
14
14
  docs: boolean;
15
15
  configCommands: boolean;
16
16
  }
17
17
 
18
18
  /** Resolves which capabilities are enabled for a program. */
19
19
  export function resolveCapabilities(program: CliProgram): CliCapabilities {
20
- const install = program.install?.enabled !== false;
20
+ const configure = program.configure?.enabled !== false;
21
21
  return {
22
- completion: true,
22
+ completion: program.completion?.enabled !== false,
23
23
  mcp: program.mcpServer?.enabled === true,
24
- install,
24
+ configure,
25
25
  docs: program.docs?.enabled === true,
26
26
  configCommands: configCommandsEnabled(program),
27
27
  };
@@ -29,9 +29,12 @@ export function resolveCapabilities(program: CliProgram): CliCapabilities {
29
29
 
30
30
  /** Reserved top-level command names for the given capabilities. */
31
31
  export function reservedCommandNames(caps: CliCapabilities): string[] {
32
- const names = ["completion", "version"];
33
- if (caps.install) {
34
- names.push("install", "uninstall");
32
+ const names = ["version"];
33
+ if (caps.completion) {
34
+ names.unshift("completion");
35
+ }
36
+ if (caps.configure) {
37
+ names.push("configure");
35
38
  }
36
39
  if (caps.docs) {
37
40
  names.push("docs");
@@ -57,15 +60,17 @@ export function skipsRequiredAppConfigExit(path: string[], caps: CliCapabilities
57
60
  return false;
58
61
  }
59
62
 
60
- export type CapabilityFeature = "mcp" | "install" | "docs" | "config";
63
+ export type CapabilityFeature = "mcp" | "configure" | "docs" | "config" | "completion";
61
64
 
62
65
  /** Stderr message when a disabled built-in is invoked from the CLI. */
63
66
  export function capabilityDeniedMessage(feature: CapabilityFeature): string {
64
67
  switch (feature) {
68
+ case "completion":
69
+ return "Shell completion is not available for this app.\n";
65
70
  case "mcp":
66
71
  return "MCP is not available for this app.\n";
67
- case "install":
68
- return "Install is not available for this app.\n";
72
+ case "configure":
73
+ return "Configure is not available for this app.\n";
69
74
  case "docs":
70
75
  return "Documentation commands are not available for this app.\n";
71
76
  case "config":
@@ -79,12 +84,16 @@ export function assertBuiltinAllowed(argv: string[], caps: CliCapabilities): voi
79
84
  return;
80
85
  }
81
86
  const first = argv[0];
87
+ if (first === "completion" && !caps.completion) {
88
+ process.stderr.write(capabilityDeniedMessage("completion"));
89
+ process.exit(1);
90
+ }
82
91
  if (first === "mcp" && !caps.mcp) {
83
92
  process.stderr.write(capabilityDeniedMessage("mcp"));
84
93
  process.exit(1);
85
94
  }
86
- if (first === "install" && !caps.install) {
87
- process.stderr.write(capabilityDeniedMessage("install"));
95
+ if (first === "configure" && !caps.configure) {
96
+ process.stderr.write(capabilityDeniedMessage("configure"));
88
97
  process.exit(1);
89
98
  }
90
99
  if (first === "docs" && !caps.docs) {
@@ -1,19 +1,37 @@
1
+ /*
2
+ Tests for cli-tool/cli-smoke module behavior.
3
+ */
4
+
1
5
  import { describe, expect, test } from "bun:test";
2
6
  import { spawnSync } from "node:child_process";
3
7
  import { join } from "node:path";
4
8
 
5
9
  const main = join(import.meta.dir, "main.ts");
6
10
 
11
+ /** Tests for argsbarg cli-tool. */
7
12
  describe("argsbarg cli-tool", () => {
13
+ /** Tests that version subcommand prints version. */
8
14
  test("version subcommand prints version", () => {
9
15
  const proc = spawnSync("bun", [main, "version"], { encoding: "utf8" });
10
16
  expect(proc.status).toBe(0);
11
17
  expect(proc.stdout.trim().length).toBeGreaterThan(0);
12
18
  });
13
19
 
14
- test("completion subcommand emits bash script", () => {
15
- const proc = spawnSync("bun", [main, "completion", "bash"], { encoding: "utf8" });
20
+ /** Tests that help lists create and version only (no install, completion, mcp). */
21
+ test("help lists create and version only (no install, completion, mcp)", () => {
22
+ const proc = spawnSync("bun", [main, "--help"], { encoding: "utf8" });
16
23
  expect(proc.status).toBe(0);
17
- expect(proc.stdout).toContain("complete");
24
+ expect(proc.stdout).toContain("create");
25
+ expect(proc.stdout).toContain("version");
26
+ expect(proc.stdout).not.toContain("configure");
27
+ expect(proc.stdout).not.toContain("completion");
28
+ expect(proc.stdout).not.toContain("mcp");
29
+ });
30
+
31
+ /** Completion subcommand is disabled. */
32
+ test("completion subcommand is disabled", () => {
33
+ const proc = spawnSync("bun", [main, "completion", "bash"], { encoding: "utf8" });
34
+ expect(proc.status).toBe(1);
35
+ expect(proc.stderr).toContain("Shell completion is not available");
18
36
  });
19
37
  });
@@ -1,3 +1,7 @@
1
+ /*
2
+ Tests for cli-tool/create module behavior.
3
+ */
4
+
1
5
  import { describe, expect, test } from "bun:test";
2
6
  import { mkdtempSync, rmSync } from "node:fs";
3
7
  import { tmpdir } from "node:os";
@@ -8,10 +12,13 @@ import {
8
12
  diffCreate,
9
13
  diffCreateDetails,
10
14
  renderCreateTree,
15
+ resolveCreateOptions,
11
16
  substituteTemplateContent,
12
17
  } from "./create.ts";
13
18
 
19
+ /** Tests for argsbarg create. */
14
20
  describe("argsbarg create", () => {
21
+ /** Tests that substitutes {key} tokens. */
15
22
  test("substitutes {key} tokens", () => {
16
23
  const out = substituteTemplateContent(
17
24
  "key={key} class={className} env={envPrefix}_API_TOKEN tap={tap} org={tapOrg}",
@@ -37,10 +44,35 @@ describe("argsbarg create", () => {
37
44
  expect(out).not.toContain("{key}");
38
45
  });
39
46
 
47
+ /** Tests that classNameFromKey. */
40
48
  test("classNameFromKey", () => {
41
49
  expect(classNameFromKey("sqsp-i18n")).toBe("SqspI18n");
50
+ expect(classNameFromKey("at1")).toBe("At1");
51
+ expect(classNameFromKey("1password")).toBe("App1password");
52
+ });
53
+
54
+ /** ResolveCreateOptions derives identity defaults from key. */
55
+ test("resolveCreateOptions derives identity defaults from key", () => {
56
+ expect(resolveCreateOptions({ key: "1password", releaseRepo: "org/1password" }).className).toBe(
57
+ "App1password",
58
+ );
59
+ expect(
60
+ resolveCreateOptions({ key: "my-cli", className: "Custom", releaseRepo: "org/my-cli" })
61
+ .className,
62
+ ).toBe("Custom");
63
+ const opts = resolveCreateOptions({ key: "at1", releaseRepo: "bdombro/at1" });
64
+ expect(opts.tap).toBe("bdombro/at1");
65
+ expect(opts.releaseRepo).toBe("bdombro/at1");
66
+ expect(opts.homepage).toBe("https://github.com/bdombro/at1");
67
+ expect(opts.desc).toBe("At1 CLI");
68
+ });
69
+
70
+ /** ResolveCreateOptions requires release repo. */
71
+ test("resolveCreateOptions requires release repo", () => {
72
+ expect(() => resolveCreateOptions({ key: "at1" })).toThrow(/release repo/i);
42
73
  });
43
74
 
75
+ /** Tests that renderCreateTree includes justfile and create-identity. */
44
76
  test("renderCreateTree includes justfile and create-identity", () => {
45
77
  const tree = renderCreateTree({
46
78
  key: "testapp",
@@ -66,6 +98,7 @@ describe("argsbarg create", () => {
66
98
  expect(formula).toContain("create-identity.ts");
67
99
  });
68
100
 
101
+ /** Tests that --check detects drift. */
69
102
  test("--check detects drift", () => {
70
103
  const dir = mkdtempSync(join(tmpdir(), "argsbarg-create-"));
71
104
  try {
@@ -89,6 +122,7 @@ describe("argsbarg create", () => {
89
122
  }
90
123
  });
91
124
 
125
+ /** Tests that --check infers options from create-identity.ts. */
92
126
  test("--check infers options from create-identity.ts", () => {
93
127
  const dir = mkdtempSync(join(tmpdir(), "argsbarg-create-"));
94
128
  try {
@@ -112,8 +146,9 @@ describe("argsbarg create", () => {
112
146
  }
113
147
  });
114
148
 
149
+ /** Tests that --diff captures drift details. */
115
150
  test("--diff captures drift details", () => {
116
- const drifts = diffCreateDetails("/nonexistent", { key: "x" });
151
+ const drifts = diffCreateDetails("/nonexistent", { key: "x", releaseRepo: "org/x" });
117
152
  expect(drifts.length).toBeGreaterThan(0);
118
153
  });
119
154
  });
@@ -44,11 +44,20 @@ export function keyToEnvPrefix(key: string): string {
44
44
  .toUpperCase();
45
45
  }
46
46
 
47
+ /** PascalCase Homebrew formula class from CLI key; prefix `App` when Ruby constant rules require it. */
47
48
  export function classNameFromKey(key: string): string {
48
- return key
49
+ const name = key
49
50
  .split(/[-_]/)
51
+ .filter((s) => s.length > 0)
50
52
  .map((s) => s.charAt(0).toUpperCase() + s.slice(1))
51
53
  .join("");
54
+ if (name.length === 0) {
55
+ throw new Error(`Invalid CLI key: ${key}`);
56
+ }
57
+ if (!/^[A-Z]/.test(name)) {
58
+ return `App${name}`;
59
+ }
60
+ return name;
52
61
  }
53
62
 
54
63
  function tapLibraryParts(tap: string): { org: string; repo: string } {
@@ -131,6 +140,12 @@ export function inferCreateOptions(
131
140
  };
132
141
  }
133
142
 
143
+ function assertReleaseRepoFormat(releaseRepo: string): void {
144
+ if (!/^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/.test(releaseRepo)) {
145
+ throw new Error(`Invalid release repo (expected org/repo): ${releaseRepo}`);
146
+ }
147
+ }
148
+
134
149
  export function resolveCreateOptions(
135
150
  partial: Partial<CreateOptions>,
136
151
  baseDir?: string,
@@ -139,13 +154,20 @@ export function resolveCreateOptions(
139
154
  const tmpl = templateIdentity();
140
155
  const key = merged.key ?? tmpl.key ?? "full-example";
141
156
  const className = merged.className ?? classNameFromKey(key);
157
+ const releaseRepo = merged.releaseRepo;
158
+ if (!releaseRepo) {
159
+ throw new Error(
160
+ "GitHub release repo (org/repo) is required. Pass --release-repo or use the interactive wizard.",
161
+ );
162
+ }
163
+ assertReleaseRepoFormat(releaseRepo);
142
164
  const devTemplate = merged.devTemplate ?? (baseDir ? isDevTemplateDir(baseDir) : false);
143
165
  return {
144
166
  key,
145
167
  className,
146
- tap: merged.tap ?? `local/${key}`,
147
- homepage: merged.homepage ?? `https://github.com/${merged.releaseRepo ?? `example/${key}`}`,
148
- releaseRepo: merged.releaseRepo ?? `example/${key}`,
168
+ tap: merged.tap ?? releaseRepo,
169
+ homepage: merged.homepage ?? `https://github.com/${releaseRepo}`,
170
+ releaseRepo,
149
171
  desc: merged.desc ?? `${className} CLI`,
150
172
  force: merged.force ?? false,
151
173
  dryRun: merged.dryRun ?? false,
@@ -1,3 +1,7 @@
1
+ /*
2
+ Tests for cli-tool/full-example-capabilities module behavior.
3
+ */
4
+
1
5
  import { describe, expect, test } from "bun:test";
2
6
  import { readFileSync } from "node:fs";
3
7
  import { join } from "node:path";
@@ -22,7 +26,7 @@ const sinkProgram = {
22
26
  topics: { readme: { text: "# readme\n" } },
23
27
  },
24
28
  mcpServer: { enabled: true },
25
- install: {},
29
+ configure: {},
26
30
  commands: [
27
31
  {
28
32
  key: "status",
@@ -45,7 +49,9 @@ const sinkProgram = {
45
49
  ],
46
50
  } satisfies CliProgram;
47
51
 
52
+ /** Tests for full-example template. */
48
53
  describe("full-example template", () => {
54
+ /** Tests that program source enables every builtin flag. */
49
55
  test("program source enables every builtin flag", () => {
50
56
  expect(programSource).toContain("mcpServer: {");
51
57
  expect(programSource).toContain("enabled: true");
@@ -53,16 +59,18 @@ describe("full-example template", () => {
53
59
  expect(programSource).toContain("appConfig:");
54
60
  });
55
61
 
62
+ /** Status command defines outputSchema. */
56
63
  test("status command defines outputSchema", () => {
57
64
  const statusSource = readFileSync(join(exampleRoot, "src/commands/status/command.ts"), "utf8");
58
65
  expect(statusSource).toContain("outputSchema:");
59
66
  });
60
67
 
68
+ /** ResolveCapabilities matches full sink shape. */
61
69
  test("resolveCapabilities matches full sink shape", () => {
62
70
  expect(resolveCapabilities(sinkProgram)).toEqual({
63
71
  completion: true,
64
72
  mcp: true,
65
- install: true,
73
+ configure: true,
66
74
  docs: true,
67
75
  configCommands: true,
68
76
  });
File without changes
@@ -10,6 +10,8 @@ export const program = {
10
10
  key: "argsbarg",
11
11
  version: pkg.version,
12
12
  description: "Argsbarg developer tools — bootstrap CLIs from the full-example template.",
13
+ completion: { enabled: false },
14
+ configure: { enabled: false },
13
15
  commands: [
14
16
  {
15
17
  key: "create",
@@ -18,17 +20,30 @@ export const program = {
18
20
  { name: "key", description: "CLI binary name.", kind: CliOptionKind.String },
19
21
  {
20
22
  name: "class-name",
21
- description: "Homebrew formula class name.",
23
+ description: "Homebrew formula Ruby class (default: derived from --key).",
24
+ kind: CliOptionKind.String,
25
+ },
26
+ {
27
+ name: "tap",
28
+ description: "Homebrew tap (default: same as --release-repo).",
29
+ kind: CliOptionKind.String,
30
+ },
31
+ {
32
+ name: "homepage",
33
+ description: "Formula homepage (default: https://github.com/<release-repo>).",
22
34
  kind: CliOptionKind.String,
23
35
  },
24
- { name: "tap", description: "Homebrew tap (org/repo).", kind: CliOptionKind.String },
25
- { name: "homepage", description: "Formula homepage URL.", kind: CliOptionKind.String },
26
36
  {
27
37
  name: "release-repo",
28
- description: "GitHub org/repo for release binary.",
38
+ description: "GitHub org/repo for releases (required in non-interactive mode).",
39
+ kind: CliOptionKind.String,
40
+ },
41
+ {
42
+ name: "desc",
43
+ description:
44
+ "App + formula description (default: <ClassName> CLI; stored in create-identity.ts).",
29
45
  kind: CliOptionKind.String,
30
46
  },
31
- { name: "desc", description: "Formula description.", kind: CliOptionKind.String },
32
47
  { name: "force", description: "Overwrite existing files.", kind: CliOptionKind.Presence },
33
48
  {
34
49
  name: "dry-run",
@@ -5,7 +5,6 @@ import { relative, resolve } from "node:path";
5
5
  import {
6
6
  applyCreate,
7
7
  type CreateOptions,
8
- classNameFromKey,
9
8
  diffCreateDetails,
10
9
  parseCreateArgv,
11
10
  printCreateDiffs,
@@ -27,36 +26,23 @@ function collectInteractiveOptions(
27
26
  const targetDir = promptOptional("Target directory", dir) ?? dir;
28
27
  const baseDir = resolve(process.cwd(), targetDir);
29
28
  const key = promptRequired("CLI key (binary name)", partial.key);
30
- const className =
31
- promptOptional("Formula class name", partial.className ?? classNameFromKey(key)) ??
32
- classNameFromKey(key);
33
- const releaseRepo =
34
- promptOptional("GitHub release repo (org/repo)", partial.releaseRepo ?? `example/${key}`) ??
35
- `example/${key}`;
36
- const homepage =
37
- promptOptional("Homepage URL", partial.homepage ?? `https://github.com/${releaseRepo}`) ??
38
- `https://github.com/${releaseRepo}`;
39
- const tap =
40
- promptOptional("Homebrew tap (org/repo)", partial.tap ?? `local/${key}`) ?? `local/${key}`;
41
- const desc =
42
- promptOptional("Formula description", partial.desc ?? `${className} CLI`) ?? `${className} CLI`;
29
+ const releaseRepo = promptRequired("GitHub release repo (org/repo)", partial.releaseRepo);
43
30
 
44
31
  const opts = resolveCreateOptions(
45
32
  {
46
33
  ...partial,
47
34
  key,
48
- className,
49
- tap,
50
- homepage,
51
35
  releaseRepo,
52
- desc,
53
36
  force: partial.force ?? false,
54
37
  },
55
38
  baseDir,
56
39
  );
57
40
 
58
41
  process.stderr.write(`\nTarget: ${baseDir}\n`);
59
- process.stderr.write(`Key: ${opts.key} Class: ${opts.className} Tap: ${opts.tap}\n\n`);
42
+ process.stderr.write(
43
+ `Key: ${opts.key} Class: ${opts.className} Tap: ${opts.tap} Release: ${opts.releaseRepo}\n`,
44
+ );
45
+ process.stderr.write(`Homepage: ${opts.homepage}\n\n`);
60
46
  const tree = renderCreateTree(opts);
61
47
  process.stderr.write(`Files (${tree.size}):\n`);
62
48
  for (const rel of [...tree.keys()].sort()) {
@@ -104,6 +90,9 @@ export async function runCreate(input: Partial<CreateOptions> & { dir?: string }
104
90
  if (!partial.key) {
105
91
  throw new Error("--key is required in non-interactive mode.");
106
92
  }
93
+ if (!partial.releaseRepo) {
94
+ throw new Error("--release-repo is required in non-interactive mode.");
95
+ }
107
96
  opts = resolveCreateOptions(partial, baseDir);
108
97
  return runCreateApply(baseDir, opts, partial.dryRun ?? false);
109
98
  } catch (err) {
@@ -176,10 +176,12 @@ function promptConfigKey(
176
176
  return { value: undefined, userTyped: false };
177
177
  }
178
178
 
179
- /** Options for the interactive `install --configure` wizard. */
179
+ /** Options for the interactive configure wizard (app config target). */
180
180
  export interface RunInstallConfigureOpts {
181
181
  /** Where the wizard was started from (standalone command vs right after install). */
182
182
  context?: "standalone" | "after-install";
183
+ /** When false, omit the "Configuration Setup" banner (e.g. inside interactive `configure`). */
184
+ showHeading?: boolean;
183
185
  }
184
186
 
185
187
  /** Print the "Configuration Setup" heading before the configure prompts. */
@@ -225,13 +227,13 @@ function promptMissingRequired(program: CliProgram): Record<string, unknown> {
225
227
  return updates;
226
228
  }
227
229
 
228
- /** Run the full interactive configure wizard (`install --configure`). */
229
- export function runInstallConfigure(
230
+ /** Run the full interactive app config wizard (`configure`). */
231
+ export function runConfigure(
230
232
  program: CliProgram,
231
- _opts: RunInstallConfigureOpts = {},
233
+ opts: RunInstallConfigureOpts = {},
232
234
  ): { path: string; changed: boolean } {
233
235
  if (!program.appConfig) {
234
- throw new Error("install --configure requires program.appConfig on the program root.");
236
+ throw new Error("configure requires program.appConfig on the program root.");
235
237
  }
236
238
  if (!process.stdin.isTTY) {
237
239
  const { resolved } = bootstrapAppConfig(program, { validateFile: false });
@@ -239,7 +241,7 @@ export function runInstallConfigure(
239
241
  if (missing.length > 0) {
240
242
  process.stderr.write(`${formatMissingConfigMessage(program, missing)}\n`);
241
243
  } else {
242
- process.stderr.write("install --configure requires an interactive terminal.\n");
244
+ process.stderr.write("configure requires an interactive terminal.\n");
243
245
  }
244
246
  process.exit(1);
245
247
  }
@@ -253,7 +255,7 @@ export function runInstallConfigure(
253
255
  const next: Record<string, unknown> = { ...existing };
254
256
  let changed = false;
255
257
 
256
- if (shouldShowConfigureSetupHeading(program)) {
258
+ if (opts.showHeading !== false && shouldShowConfigureSetupHeading(program)) {
257
259
  writeConfigureSetupHeading();
258
260
  }
259
261
 
@@ -286,7 +288,7 @@ export function runInstallConfigure(
286
288
  return { path, changed: false };
287
289
  }
288
290
 
289
- /** Summary for `install --status`: config path, whether the file exists, and which required settings are set (never their values). */
291
+ /** Summary for `configure --status`: config path, whether the file exists, and which required settings are set (never their values). */
290
292
  export function appConfigStatus(program: CliProgram):
291
293
  | {
292
294
  path: string;
@@ -343,7 +345,7 @@ export function ensureAppConfig(
343
345
 
344
346
  if (opts.interactive && process.stdin.isTTY) {
345
347
  if (opts.configure) {
346
- runInstallConfigure(program, { context: "standalone" });
348
+ runConfigure(program, { context: "standalone" });
347
349
  fileData = readAppConfigFileRaw(resolveAppConfigPath(program));
348
350
  resolved = resolveAppConfig(program, fileData, hostEnv);
349
351
  exportConfigToEnv(program, resolved, hostEnv);
@@ -1,3 +1,7 @@
1
+ /*
2
+ Tests for config/context module behavior.
3
+ */
4
+
1
5
  import { describe, expect, test } from "bun:test";
2
6
  import { mkdtempSync, rmSync } from "node:fs";
3
7
  import { tmpdir } from "node:os";
@@ -20,7 +24,9 @@ const program: CliProgram = {
20
24
  handler: () => {},
21
25
  };
22
26
 
27
+ /** Tests for config/context. */
23
28
  describe("config/context", () => {
29
+ /** Tests that AppConfigSnapshot get, require, read, set. */
24
30
  test("AppConfigSnapshot get, require, read, set", () => {
25
31
  const dir = mkdtempSync(join(tmpdir(), "ctx-test-"));
26
32
  const prevHome = process.env.HOME;
@@ -50,6 +56,7 @@ describe("config/context", () => {
50
56
  }
51
57
  });
52
58
 
59
+ /** Tests that EmptyAppConfigSnapshot when program.appConfig unset. */
53
60
  test("EmptyAppConfigSnapshot when program.appConfig unset", () => {
54
61
  const programWithoutConfig: CliProgram = {
55
62
  key: "x",
@@ -65,6 +72,7 @@ describe("config/context", () => {
65
72
  expect(empty.dir).toBe(dirname(empty.path));
66
73
  });
67
74
 
75
+ /** Tests that AppConfigSnapshot path uses OS default from program key. */
68
76
  test("AppConfigSnapshot path uses OS default from program key", () => {
69
77
  const ctx = createAppConfigSnapshot(program, {}, {});
70
78
  expect(ctx.path).toContain("ctx_test");