argsbarg 4.1.0 → 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 (103) hide show
  1. package/CHANGELOG.md +48 -1
  2. package/README.md +90 -84
  3. package/docs/README.md +6 -6
  4. package/docs/bundled-docs.md +1 -1
  5. package/docs/cli-program.md +5 -3
  6. package/docs/config-schema.md +36 -9
  7. package/docs/developing.md +7 -7
  8. package/docs/distribution-homebrew.md +103 -0
  9. package/docs/install.md +105 -189
  10. package/docs/mcp.md +2 -3
  11. package/docs/output-schema.md +1 -1
  12. package/examples/full-example/Formula/.gitkeep +0 -0
  13. package/examples/full-example/README.md +98 -0
  14. package/examples/full-example/biome.json +22 -0
  15. package/examples/{consumer-app → full-example}/bun.lock +2 -0
  16. package/examples/full-example/justfile +134 -0
  17. package/examples/{consumer-app → full-example}/package.json +10 -3
  18. package/examples/{consumer-app → full-example}/schemas/generated/app-config.json +1 -1
  19. package/examples/{consumer-app → full-example}/schemas/generated/status.json +1 -1
  20. package/examples/full-example/scripts/create-identity.ts +11 -0
  21. package/examples/full-example/scripts/formula-shared.ts +73 -0
  22. package/examples/full-example/scripts/gen-dev-formula.ts +26 -0
  23. package/examples/full-example/scripts/print-identity.ts +27 -0
  24. package/examples/full-example/src/commands/echo/command.ts +21 -0
  25. package/examples/full-example/src/commands/status/command.test.ts +10 -0
  26. package/examples/full-example/src/commands/status/command.ts +36 -0
  27. package/examples/{consumer-app → full-example}/src/commands/status/types.ts +1 -1
  28. package/examples/full-example/src/index.ts +10 -0
  29. package/examples/full-example/src/program.ts +57 -0
  30. package/examples/{consumer-app → full-example}/src/types.ts +1 -1
  31. package/examples/nested.ts +1 -3
  32. package/index.d.ts +27 -66
  33. package/package.json +2 -2
  34. package/src/builtins/builtins.test.ts +22 -23
  35. package/src/builtins/completion-group.ts +17 -15
  36. package/src/builtins/dispatch.ts +25 -1
  37. package/src/builtins/install.ts +22 -52
  38. package/src/builtins/registry.ts +2 -0
  39. package/src/builtins/uninstall.ts +80 -0
  40. package/src/capabilities.ts +1 -3
  41. package/src/cli-tool/cli-smoke.test.ts +19 -0
  42. package/src/cli-tool/create.test.ts +119 -0
  43. package/src/cli-tool/create.ts +380 -0
  44. package/{examples/consumer-app/capabilities.test.ts → src/cli-tool/full-example-capabilities.test.ts} +15 -14
  45. package/src/cli-tool/main.ts +8 -0
  46. package/src/cli-tool/post-create.ts +111 -0
  47. package/src/cli-tool/program.ts +82 -0
  48. package/src/cli-tool/prompt.ts +28 -0
  49. package/src/cli-tool/run-create.ts +149 -0
  50. package/src/cli.ts +0 -2
  51. package/src/config/bootstrap.ts +16 -9
  52. package/src/config/resolve.test.ts +167 -0
  53. package/src/config/resolve.ts +50 -6
  54. package/src/docs/api-guide.test.ts +4 -5
  55. package/src/docs/docs.test.ts +2 -1
  56. package/src/docs/mcp-guide.ts +7 -8
  57. package/src/index.ts +3 -10
  58. package/src/install/binary-placement.test.ts +101 -0
  59. package/src/install/binary-placement.ts +47 -0
  60. package/src/install/index.ts +117 -123
  61. package/src/install/install.test.ts +89 -105
  62. package/src/install/normalize-uninstall.ts +11 -0
  63. package/src/install/normalize.ts +4 -19
  64. package/src/install/paths.ts +0 -22
  65. package/src/install/plan.ts +14 -6
  66. package/src/install/shell.ts +0 -14
  67. package/src/install/status.test.ts +6 -6
  68. package/src/install/status.ts +0 -6
  69. package/src/install/target-effective.ts +0 -2
  70. package/src/install/target-scope.ts +15 -28
  71. package/src/install/target-types.ts +0 -16
  72. package/src/install/targets/app.ts +19 -28
  73. package/src/install/targets/configure.ts +5 -1
  74. package/src/install/targets/index.ts +0 -3
  75. package/src/install/targets.test.ts +24 -42
  76. package/src/mcp/env.test.ts +92 -0
  77. package/src/mcp/env.ts +15 -14
  78. package/src/parse.test.ts +3 -2
  79. package/src/prompt.ts +10 -0
  80. package/src/schema.ts +9 -1
  81. package/src/types.ts +27 -9
  82. package/src/validate.ts +5 -11
  83. package/docs/templates/cursor/rules/cli-program.mdc +0 -31
  84. package/examples/config-app/main.ts +0 -20
  85. package/examples/config-app/program.ts +0 -78
  86. package/examples/config-app/schema.ts +0 -37
  87. package/examples/config-app/types.ts +0 -19
  88. package/examples/consumer-app/README.md +0 -56
  89. package/examples/consumer-app/src/main.ts +0 -15
  90. package/examples/consumer-app/src/program.ts +0 -108
  91. package/src/install/app.ts +0 -94
  92. package/src/install/bootstrap.ts +0 -22
  93. package/src/install/completions.ts +0 -56
  94. package/src/install/targets/completions.ts +0 -133
  95. package/src/install/update.test.ts +0 -123
  96. package/src/install/update.ts +0 -54
  97. /package/examples/{consumer-app → full-example}/schemas/configSchemas.ts +0 -0
  98. /package/examples/{consumer-app → full-example}/schemas/outputSchemas.ts +0 -0
  99. /package/examples/{consumer-app → full-example}/scripts/schemagen/discover-schema-roots.test.ts +0 -0
  100. /package/examples/{consumer-app → full-example}/scripts/schemagen/discover-schema-roots.ts +0 -0
  101. /package/examples/{consumer-app → full-example}/scripts/schemagen/naming.ts +0 -0
  102. /package/examples/{consumer-app → full-example}/scripts/schemagen.ts +0 -0
  103. /package/examples/{consumer-app → full-example}/tsconfig.json +0 -0
@@ -6,6 +6,7 @@ import { completionBashScript, completionFishScript, completionZshScript } from
6
6
  import { cliBuiltinInstallCommand, installBuiltinOptions } from "./install.ts";
7
7
  import { cliBuiltinMcpCommand } from "./mcp.ts";
8
8
  import { cliParseRoot, cliPresentationRoot } from "./presentation.ts";
9
+ import { cliBuiltinUninstallCommand, uninstallBuiltinOptions } from "./uninstall.ts";
9
10
 
10
11
  const fixture: CliProgram = {
11
12
  key: "myapp",
@@ -22,22 +23,32 @@ const fixture: CliProgram = {
22
23
  };
23
24
 
24
25
  describe("builtins help copy", () => {
25
- test("install command includes description and option text", () => {
26
+ test("install command includes Homebrew-oriented description", () => {
26
27
  const install = cliBuiltinInstallCommand(fixture);
27
- expect(install.description).toContain("shell completions");
28
- expect(install.notes).toContain("install --all");
28
+ expect(install.description).toContain("agent skills");
29
+ expect(install.notes).toContain("brew install");
29
30
  const names = installBuiltinOptions(fixture).map((o) => o.name);
30
31
  expect(names).toContain("all");
31
32
  expect(names).toContain("mcp");
33
+ expect(names).not.toContain("app");
34
+ expect(names).not.toContain("completions");
35
+ expect(names).not.toContain("update");
32
36
  expect(names.indexOf("all")).toBeLessThan(names.indexOf("mcp"));
33
37
  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"));
38
+ expect(names).not.toContain("uninstall");
37
39
  const yesOpt = installBuiltinOptions(fixture).find((o) => o.name === "yes");
38
40
  expect(yesOpt?.shortName).toBe("y");
39
41
  });
40
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
+
41
52
  test("install -y parses as --yes", () => {
42
53
  const root = cliParseRoot(fixture);
43
54
  const pr = postParseValidate(root, parse(root, ["install", "-y"]));
@@ -53,23 +64,10 @@ describe("builtins help copy", () => {
53
64
  expect(names).not.toContain("mcp");
54
65
  });
55
66
 
56
- test("install omits --update when updateGetLatest unset", () => {
67
+ test("install notes mention brew upgrade", () => {
57
68
  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
- });
62
-
63
- test("install notes include upgrade section when updateGetLatest is set", () => {
64
- const withUpdate: CliProgram = {
65
- ...fixture,
66
- install: { updateGetLatest: async () => ({ path: process.execPath }) },
67
- };
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"));
69
+ expect(install.notes).toContain("brew upgrade");
70
+ expect(install.notes).toContain("install --configure");
73
71
  });
74
72
 
75
73
  test("mcp builtin description is user-facing", () => {
@@ -90,6 +88,7 @@ describe("presentation root", () => {
90
88
  const keys = root.commands?.map((c) => c.key) ?? [];
91
89
  expect(keys).toContain("mcp");
92
90
  expect(keys).toContain("install");
91
+ expect(keys).toContain("uninstall");
93
92
  });
94
93
 
95
94
  test("omits install when install.enabled is false", () => {
@@ -159,6 +158,6 @@ describe("schema export builtins", () => {
159
158
  test("exportPresentationBuiltins includes install options", () => {
160
159
  const builtins = exportPresentationBuiltins(fixture);
161
160
  const install = builtins.find((b) => b.key === "install");
162
- expect(install?.options?.find((o) => o.name === "all")?.description).toContain("app");
161
+ expect(install?.options?.find((o) => o.name === "all")?.description).toContain("agent");
163
162
  });
164
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,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: {
@@ -6,19 +6,7 @@ export function installBuiltinOptions(root: CliProgram): CliOption[] {
6
6
  const opts: CliOption[] = [
7
7
  {
8
8
  name: "all",
9
- description:
10
- "Install the default set (app, shell completions, and configuration when supported).",
11
- kind: CliOptionKind.Presence,
12
- },
13
- {
14
- name: "app",
15
- description: "Copy this app to your install directory (default ~/.local/bin).",
16
- kind: CliOptionKind.Presence,
17
- },
18
- {
19
- name: "completions",
20
- description:
21
- "Install bash, zsh, and fish tab-completion scripts (each skipped silently if that shell is not on PATH).",
9
+ description: "Install agent skills and MCP config (default artifact set for this app).",
22
10
  kind: CliOptionKind.Presence,
23
11
  },
24
12
  {
@@ -41,8 +29,7 @@ export function installBuiltinOptions(root: CliProgram): CliOption[] {
41
29
  if (root.appConfig) {
42
30
  opts.push({
43
31
  name: "configure",
44
- description:
45
- "Run the configuration wizard (install), or remove the config file (--uninstall).",
32
+ description: "Run the interactive configuration wizard.",
46
33
  kind: CliOptionKind.Presence,
47
34
  });
48
35
  }
@@ -55,31 +42,10 @@ export function installBuiltinOptions(root: CliProgram): CliOption[] {
55
42
  },
56
43
  {
57
44
  name: "reinstall",
58
- description: "Refresh everything already installed for this app.",
59
- kind: CliOptionKind.Presence,
60
- },
61
- );
62
-
63
- if (resolveCapabilities(root).update) {
64
- opts.push({
65
- name: "update",
66
- description: "Download the latest release and refresh installed files.",
67
- kind: CliOptionKind.Presence,
68
- });
69
- }
70
-
71
- opts.push(
72
- {
73
- name: "uninstall",
74
45
  description:
75
- "Remove installed files (--all removes everything; use individual flags to remove one category).",
46
+ "Refresh installed agent artifacts (skills, MCP). Used by Homebrew post_install.",
76
47
  kind: CliOptionKind.Presence,
77
48
  },
78
- {
79
- name: "from",
80
- description: "App executable to copy (default: running executable). Used with --reinstall.",
81
- kind: CliOptionKind.String,
82
- },
83
49
  {
84
50
  name: "yes",
85
51
  description: "Skip the confirmation prompt.",
@@ -93,7 +59,7 @@ export function installBuiltinOptions(root: CliProgram): CliOption[] {
93
59
  },
94
60
  {
95
61
  name: "json",
96
- description: "Print changed paths (install/update/uninstall) or status JSON on stdout.",
62
+ description: "Print changed paths (install/reinstall) or status JSON on stdout.",
97
63
  kind: CliOptionKind.Presence,
98
64
  },
99
65
  );
@@ -105,30 +71,34 @@ export function installBuiltinOptions(root: CliProgram): CliOption[] {
105
71
  export function cliBuiltinInstallCommand(root: CliProgram): CliLeaf {
106
72
  const app = root.key;
107
73
  const notesLines = [
108
- "First-time setup:",
109
- ` ${app} install --yes`,
74
+ "Install the binary via Homebrew (tap-from-repo), then refresh agent artifacts:",
75
+ ` brew tap <org>/<repo>`,
76
+ ` brew install <tap>/${app}`,
110
77
  "",
111
- "Refresh after upgrading:",
112
- ` ${app} install --reinstall`,
113
- ];
114
- if (resolveCapabilities(root).update) {
115
- notesLines.push("", "Upgrade to latest release:", ` ${app} install --update`);
116
- }
117
- notesLines.push(
78
+ "Homebrew post_install runs:",
79
+ ` ${app} install --reinstall --yes`,
80
+ "",
81
+ "Configure separately (interactive):",
82
+ ` ${app} install --configure`,
83
+ "",
84
+ "Upgrade:",
85
+ ` brew upgrade ${app}`,
86
+ "",
87
+ "Shell completions are installed by Homebrew during brew install.",
88
+ "See: https://docs.brew.sh/Shell-Completion",
118
89
  "",
119
90
  "See what is installed:",
120
91
  ` ${app} install --status`,
121
92
  "",
122
- "Remove everything installed with --all:",
123
- ` ${app} install --uninstall --all --yes`,
93
+ "Remove agent artifacts:",
94
+ ` ${app} uninstall --yes`,
124
95
  "",
125
96
  "Use --dry to preview changes without writing files.",
126
97
  "Use --json for machine-readable output.",
127
- );
98
+ ];
128
99
  return {
129
100
  key: "install",
130
- description:
131
- "Install this app, shell completions, agent skills, and MCP config on your machine.",
101
+ description: "Install agent skills and MCP config for this app (binary via Homebrew).",
132
102
  options: installBuiltinOptions(root),
133
103
  notes: notesLines.join("\n"),
134
104
  handler: () => {},
@@ -5,6 +5,7 @@ import { cliBuiltinCompletionGroup } from "./completion-group.ts";
5
5
  import { cliBuiltinConfigGroupIfEnabled } from "./config.ts";
6
6
  import { cliBuiltinInstallCommand } from "./install.ts";
7
7
  import { cliBuiltinMcpCommand } from "./mcp.ts";
8
+ import { cliBuiltinUninstallCommand } from "./uninstall.ts";
8
9
  import { cliBuiltinVersionCommand } from "./version.ts";
9
10
 
10
11
  type BuiltinFactory = (program: CliProgram) => CliNode | null;
@@ -30,6 +31,7 @@ export function resolveBuiltins(program: CliProgram, caps: CliCapabilities): Cli
30
31
  pushBuiltin(builtins, program, () => cliBuiltinVersionCommand());
31
32
  if (caps.install) {
32
33
  pushBuiltin(builtins, program, (p) => cliBuiltinInstallCommand(p));
34
+ pushBuiltin(builtins, program, (p) => cliBuiltinUninstallCommand(p));
33
35
  }
34
36
  pushBuiltin(builtins, program, (p) => cliBuiltinDocsGroupIfEnabled(p) ?? null);
35
37
  if (caps.mcp) {
@@ -0,0 +1,80 @@
1
+ import { resolveCapabilities } from "../capabilities.ts";
2
+ import { type CliLeaf, type CliOption, CliOptionKind, type CliProgram } from "../types.ts";
3
+
4
+ /** Uninstall command options (dynamic: `--mcp` only when MCP is enabled). */
5
+ export function uninstallBuiltinOptions(root: CliProgram): CliOption[] {
6
+ const opts: CliOption[] = [
7
+ {
8
+ name: "all",
9
+ description: "Remove all detected agent artifacts (default when no scope flags are given).",
10
+ kind: CliOptionKind.Presence,
11
+ },
12
+ {
13
+ name: "skill",
14
+ description: "Remove agent skills only.",
15
+ kind: CliOptionKind.Presence,
16
+ },
17
+ ];
18
+
19
+ if (resolveCapabilities(root).mcp) {
20
+ opts.push({
21
+ name: "mcp",
22
+ description: "Remove MCP server configuration only.",
23
+ kind: CliOptionKind.Presence,
24
+ });
25
+ }
26
+
27
+ if (root.appConfig) {
28
+ opts.push({
29
+ name: "configure",
30
+ description: "Remove the app config file only.",
31
+ kind: CliOptionKind.Presence,
32
+ });
33
+ }
34
+
35
+ opts.push(
36
+ {
37
+ name: "yes",
38
+ description: "Skip the confirmation prompt.",
39
+ kind: CliOptionKind.Presence,
40
+ shortName: "y",
41
+ },
42
+ {
43
+ name: "dry",
44
+ description: "Show what would change without writing files.",
45
+ kind: CliOptionKind.Presence,
46
+ },
47
+ {
48
+ name: "json",
49
+ description: "Print changed paths on stdout.",
50
+ kind: CliOptionKind.Presence,
51
+ },
52
+ );
53
+
54
+ return opts;
55
+ }
56
+
57
+ /** Builds the `uninstall` built-in command. */
58
+ export function cliBuiltinUninstallCommand(root: CliProgram): CliLeaf {
59
+ const app = root.key;
60
+ const notesLines = [
61
+ "Remove agent artifacts installed by this app (skills, MCP, config).",
62
+ "Run before `brew uninstall` while the CLI is still on PATH:",
63
+ "",
64
+ ` ${app} uninstall --yes`,
65
+ ` brew uninstall <tap>/${app}`,
66
+ "",
67
+ "Remove app config only:",
68
+ ` ${app} uninstall --configure --yes`,
69
+ "",
70
+ "Use --dry to preview changes without writing files.",
71
+ "Use --json for machine-readable output.",
72
+ ];
73
+ return {
74
+ key: "uninstall",
75
+ description: "Remove agent skills, MCP config, and app config for this app.",
76
+ options: uninstallBuiltinOptions(root),
77
+ notes: notesLines.join("\n"),
78
+ handler: () => {},
79
+ };
80
+ }
@@ -12,7 +12,6 @@ export interface CliCapabilities {
12
12
  mcp: boolean;
13
13
  install: boolean;
14
14
  docs: boolean;
15
- update: boolean;
16
15
  configCommands: boolean;
17
16
  }
18
17
 
@@ -24,7 +23,6 @@ export function resolveCapabilities(program: CliProgram): CliCapabilities {
24
23
  mcp: program.mcpServer?.enabled === true,
25
24
  install,
26
25
  docs: program.docs?.enabled === true,
27
- update: install && typeof program.install?.updateGetLatest === "function",
28
26
  configCommands: configCommandsEnabled(program),
29
27
  };
30
28
  }
@@ -33,7 +31,7 @@ export function resolveCapabilities(program: CliProgram): CliCapabilities {
33
31
  export function reservedCommandNames(caps: CliCapabilities): string[] {
34
32
  const names = ["completion", "version"];
35
33
  if (caps.install) {
36
- names.push("install");
34
+ names.push("install", "uninstall");
37
35
  }
38
36
  if (caps.docs) {
39
37
  names.push("docs");
@@ -0,0 +1,19 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { spawnSync } from "node:child_process";
3
+ import { join } from "node:path";
4
+
5
+ const main = join(import.meta.dir, "main.ts");
6
+
7
+ describe("argsbarg cli-tool", () => {
8
+ test("version subcommand prints version", () => {
9
+ const proc = spawnSync("bun", [main, "version"], { encoding: "utf8" });
10
+ expect(proc.status).toBe(0);
11
+ expect(proc.stdout.trim().length).toBeGreaterThan(0);
12
+ });
13
+
14
+ test("completion subcommand emits bash script", () => {
15
+ const proc = spawnSync("bun", [main, "completion", "bash"], { encoding: "utf8" });
16
+ expect(proc.status).toBe(0);
17
+ expect(proc.stdout).toContain("complete");
18
+ });
19
+ });
@@ -0,0 +1,119 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { mkdtempSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import {
6
+ applyCreate,
7
+ classNameFromKey,
8
+ diffCreate,
9
+ diffCreateDetails,
10
+ renderCreateTree,
11
+ substituteTemplateContent,
12
+ } from "./create.ts";
13
+
14
+ describe("argsbarg create", () => {
15
+ test("substitutes {key} tokens", () => {
16
+ const out = substituteTemplateContent(
17
+ "key={key} class={className} env={envPrefix}_API_TOKEN tap={tap} org={tapOrg}",
18
+ {
19
+ key: "my-cli",
20
+ className: "MyCli",
21
+ tap: "org/my-cli",
22
+ homepage: "https://github.com/org/my-cli",
23
+ releaseRepo: "org/my-cli",
24
+ desc: "My CLI",
25
+ force: false,
26
+ dryRun: false,
27
+ check: false,
28
+ diff: false,
29
+ yes: false,
30
+ devTemplate: false,
31
+ },
32
+ );
33
+ expect(out).toContain("my-cli");
34
+ expect(out).toContain("MyCli");
35
+ expect(out).toContain("MY_CLI_API_TOKEN");
36
+ expect(out).toContain("org/my-cli");
37
+ expect(out).not.toContain("{key}");
38
+ });
39
+
40
+ test("classNameFromKey", () => {
41
+ expect(classNameFromKey("sqsp-i18n")).toBe("SqspI18n");
42
+ });
43
+
44
+ test("renderCreateTree includes justfile and create-identity", () => {
45
+ const tree = renderCreateTree({
46
+ key: "testapp",
47
+ className: "Testapp",
48
+ tap: "local/testapp",
49
+ homepage: "https://example.com",
50
+ releaseRepo: "example/testapp",
51
+ desc: "Test",
52
+ force: false,
53
+ dryRun: false,
54
+ check: false,
55
+ diff: false,
56
+ yes: false,
57
+ devTemplate: false,
58
+ });
59
+ expect(tree.has("justfile")).toBe(true);
60
+ expect(tree.has("biome.json")).toBe(true);
61
+ expect(tree.has(".cursor/rules/code.mdc")).toBe(true);
62
+ expect(tree.has("scripts/create-identity.ts")).toBe(true);
63
+ const identity = tree.get("scripts/create-identity.ts");
64
+ expect(identity).toContain('key: "testapp"');
65
+ const formula = tree.get("scripts/formula-shared.ts");
66
+ expect(formula).toContain("create-identity.ts");
67
+ });
68
+
69
+ test("--check detects drift", () => {
70
+ const dir = mkdtempSync(join(tmpdir(), "argsbarg-create-"));
71
+ try {
72
+ applyCreate(dir, {
73
+ key: "testapp",
74
+ className: "Testapp",
75
+ tap: "local/testapp",
76
+ homepage: "https://example.com",
77
+ releaseRepo: "example/testapp",
78
+ desc: "Test",
79
+ force: true,
80
+ dryRun: false,
81
+ check: false,
82
+ diff: false,
83
+ yes: false,
84
+ devTemplate: false,
85
+ });
86
+ expect(diffCreate(dir, { key: "testapp", className: "Testapp" })).toEqual([]);
87
+ } finally {
88
+ rmSync(dir, { recursive: true, force: true });
89
+ }
90
+ });
91
+
92
+ test("--check infers options from create-identity.ts", () => {
93
+ const dir = mkdtempSync(join(tmpdir(), "argsbarg-create-"));
94
+ try {
95
+ applyCreate(dir, {
96
+ key: "testapp",
97
+ className: "Testapp",
98
+ tap: "local/testapp",
99
+ homepage: "https://example.com",
100
+ releaseRepo: "example/testapp",
101
+ desc: "Test",
102
+ force: true,
103
+ dryRun: false,
104
+ check: false,
105
+ diff: false,
106
+ yes: false,
107
+ devTemplate: false,
108
+ });
109
+ expect(diffCreate(dir, { check: true })).toEqual([]);
110
+ } finally {
111
+ rmSync(dir, { recursive: true, force: true });
112
+ }
113
+ });
114
+
115
+ test("--diff captures drift details", () => {
116
+ const drifts = diffCreateDetails("/nonexistent", { key: "x" });
117
+ expect(drifts.length).toBeGreaterThan(0);
118
+ });
119
+ });