argsbarg 3.3.1 → 3.3.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.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [3.3.2] - 2026-06-21
11
+
12
+
13
+ ## [3.3.2] - 2026-06-21
14
+
15
+ ### Changed
16
+
17
+ - **`install --uninstall`** — symmetric with install: requires `--all` or scoped flags; `--uninstall --all` removes everything argsbarg installed; empty scope succeeds without error.
18
+
10
19
  ## [3.3.1] - 2026-06-21
11
20
 
12
21
  ### Added
@@ -243,7 +252,9 @@ const cli = { ... } satisfies CliProgram; // or : CliProgram
243
252
  - Migrate schemas: rename every `children` property to **`commands`**; move positional definitions to **`CliPositional`** objects on `positionals` and strip `positional` / `argMin` / `argMax` from flag definitions under `options` (flags only carry `name`, `description`, `kind`, and optional `shortName`).
244
253
  - Imports: use `CliPositional` where needed; replace `CliOptionDef` with `CliOption` or `CliPositional` as appropriate.
245
254
 
246
- [Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v3.3.1...HEAD
255
+ [Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v3.3.2...HEAD
256
+ [3.3.2]: https://github.com/bdombro/bun-argsbarg/releases/tag/v3.3.2
257
+ [3.3.2]: https://github.com/bdombro/bun-argsbarg/releases/tag/v3.3.2
247
258
  [3.3.1]: https://github.com/bdombro/bun-argsbarg/releases/tag/v3.3.1
248
259
  [3.3.0]: https://github.com/bdombro/bun-argsbarg/releases/tag/v3.3.0
249
260
  [3.2.0]: https://github.com/bdombro/bun-argsbarg/releases/tag/v3.2.0
@@ -52,12 +52,27 @@ ArgsBarg is **schema-first** — the program tree is the product. **Keep `CliPro
52
52
  | --- | --- |
53
53
  | Shared option objects (`DRY_RUN_OPTION`, `JSON_OPTION`) | Identical flag reused on many leaves |
54
54
  | Shared spreads (`...MCP_TOOL_MUTATOR`) | Same `mcpTool` metadata on a family of commands |
55
- | `somethingCommand()` factory | Entry file is large; handler/body is substantial (Ink page, headless dispatch) |
55
+ | `commands/<name>/command.tsx` module | Entry file is large; handler/body is substantial (Ink page, headless dispatch) |
56
56
  | `docs.topics` text imports | Compile-time markdown bundling — not schema shape |
57
57
 
58
58
  **Avoid extracting** thin indirection: a file that only re-exports `{ key, description, options }` with no logic, or splitting every leaf into its own module when the handler is a few lines. If extraction does not reduce duplication or file size materially, keep it inline.
59
59
 
60
- **`satisfies CliProgram`** on the root (or on extracted command factories) preserves type-checking whether inline or not.
60
+ When you extract a leaf or router, prefer a **plain exported object** not a zero-arg wrapper function:
61
+
62
+ ```typescript
63
+ // commands/reserve/command.tsx
64
+ export const reserveCommand = {
65
+ key: "reserve",
66
+ description: "Reserve a QA environment.",
67
+ options: [YES_OPTION, DRY_RUN_OPTION],
68
+ positionals: [/* … */],
69
+ handler: async (ctx) => { /* … */ },
70
+ } satisfies CliLeaf;
71
+ ```
72
+
73
+ Use a **parameterized factory** only when the schema truly depends on inputs (e.g. `createUpsertCommand(deps)` for tests or injected config). A `reserveCommand()` that returns a static literal adds indirection without benefit.
74
+
75
+ **`satisfies CliProgram`** on the root (or **`satisfies CliLeaf`** / router type on extracted modules) preserves type-checking whether inline or not.
61
76
 
62
77
  ## Descriptions
63
78
 
package/docs/install.md CHANGED
@@ -17,8 +17,8 @@ myapp update
17
17
  # See what is installed
18
18
  myapp install --status
19
19
 
20
- # Remove everything detected
21
- myapp install --uninstall --yes
20
+ # Remove everything installed with --all
21
+ myapp install --uninstall --all --yes
22
22
  ```
23
23
 
24
24
  ## What gets installed
@@ -33,7 +33,9 @@ myapp install --uninstall --yes
33
33
  | Claude skill | `--skill` | `~/.claude/skills/<dir>/` when `~/.claude` exists |
34
34
  | MCP config | `--mcp` | `~/.cursor/mcp.json` and `~/.claude.json` when MCP is enabled |
35
35
 
36
- `--all` expands to `--bin`, `--completions`, `--skill`, and `--mcp` (when `mcpServer.enabled` is `true`).
36
+ `--all` expands to `--bin`, `--completions`, `--skill`, and `--mcp` (when `mcpServer.enabled` is `true`) for both install and uninstall. Missing targets are skipped silently (no error if nothing is on disk or a shell/agent directory does not exist).
37
+
38
+ `install --uninstall` requires the same target flags as install (`--all`, `--bin`, etc.) — bare `--uninstall` alone is an error.
37
39
 
38
40
  Shells not on PATH are skipped silently (no warnings).
39
41
 
@@ -105,7 +107,7 @@ Environment:
105
107
  | `--reinstall` | Reinstall artifacts already on disk (implies `--bin` + `--yes`) |
106
108
  | `--from <path>` | Binary to copy with `--reinstall` (default: running executable) |
107
109
  | `--status` | Read-only inventory |
108
- | `--uninstall` | Remove detected artifacts (scope with `--bin`, `--completions`, `--skill`, `--mcp`) |
110
+ | `--uninstall` | Remove artifacts in scope (`--all`, `--bin`, `--completions`, `--skill`, `--mcp`); skips targets not installed |
109
111
 
110
112
  `--update` is accepted as a deprecated alias for `--reinstall`.
111
113
 
@@ -13,7 +13,7 @@ alwaysApply: false
13
13
  - Reserved root commands (do not declare): `completion`, `install`, `mcp`, `version`, `docs`, `update`
14
14
 
15
15
  ## Schema
16
- - **Inline** options, positionals, and handler schema; extract only for shared flags, `mcpTool` spreads, or large handlers (Ink/dispatch)
16
+ - **Inline** options, positionals, and handler schema; extract to `export const …Command = { … } satisfies CliLeaf` (or router type) when shared flags, `mcpTool` spreads, or large handlers justify a module — not zero-arg wrapper functions
17
17
  - Leaf descriptions: action-oriented, not UI jargon
18
18
  - Prefer option names `yes`, `dry-run`, `json` when semantics match; describe non-interactive use on the option
19
19
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "argsbarg",
3
- "version": "3.3.1",
3
+ "version": "3.3.2",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "bun": ">=1.3"
@@ -42,7 +42,7 @@ export function installBuiltinOptions(root: CliProgram): CliOption[] {
42
42
  },
43
43
  {
44
44
  name: "uninstall",
45
- description: "Remove installed artifacts (all detected, or limit with other flags).",
45
+ description: "Remove installed artifacts (use --all or scoped flags; skips targets not on disk).",
46
46
  kind: CliOptionKind.Presence,
47
47
  },
48
48
  {
@@ -96,8 +96,8 @@ export function cliBuiltinInstallCommand(root: CliProgram): CliLeaf {
96
96
  ` {app} update\n\n` +
97
97
  "See what is installed:\n" +
98
98
  ` {app} install --status\n\n` +
99
- "Remove everything:\n" +
100
- ` {app} install --uninstall --yes\n\n` +
99
+ "Remove everything installed with --all:\n" +
100
+ ` {app} install --uninstall --all --yes\n\n` +
101
101
  "Use --dry to preview changes, --json for machine-readable output.",
102
102
  options: installBuiltinOptions(root),
103
103
  handler: () => {},
@@ -1,5 +1,4 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
- import { CliProgram } from "../types.ts";
3
2
  import { InstallPaths } from "./paths.ts";
4
3
 
5
4
  export interface InstalledArtifacts {
@@ -13,7 +13,7 @@ import { resolveInstallPaths } from "./paths.ts";
13
13
  import { installErr, installInfo, installOut, printInstallStatus } from "./status.ts";
14
14
  import { buildUninstallPlan, uninstallSkillDir, type UninstallAction } from "./uninstall.ts";
15
15
 
16
- function parseInstallOpts(raw: Record<string, string>): InstallOpts {
16
+ export function parseInstallOpts(raw: Record<string, string>): InstallOpts {
17
17
  const flag = (name: string) => raw[name] === "1";
18
18
  const reinstall = flag("reinstall") || flag("update");
19
19
  return {
@@ -34,7 +34,7 @@ function parseInstallOpts(raw: Record<string, string>): InstallOpts {
34
34
  };
35
35
  }
36
36
 
37
- function validateOpts(opts: InstallOpts): string | null {
37
+ export function validateInstallOpts(opts: InstallOpts): string | null {
38
38
  if (opts.quiet && opts.dry) {
39
39
  return "--quiet cannot be combined with --dry.";
40
40
  }
@@ -60,10 +60,10 @@ function validateOpts(opts: InstallOpts): string | null {
60
60
  ) {
61
61
  return "--reinstall cannot be combined with other target flags.";
62
62
  }
63
- if (opts.uninstall && (opts.all || opts.reinstall || opts.status)) {
64
- return "--uninstall cannot be combined with --all, --reinstall, or --status.";
63
+ if (opts.uninstall && (opts.reinstall || opts.status)) {
64
+ return "--uninstall cannot be combined with --reinstall or --status.";
65
65
  }
66
- if (!opts.status && !opts.reinstall && !opts.uninstall) {
66
+ if (!opts.status && !opts.reinstall) {
67
67
  const hasTarget = opts.all || opts.bin || opts.completions || opts.skill || opts.mcp;
68
68
  if (!hasTarget) {
69
69
  return "Specify at least one target: --all, --bin, --completions, --skill, or --mcp.";
@@ -126,7 +126,7 @@ export async function runInstallMutation(
126
126
  rawOpts: Record<string, string>,
127
127
  ): Promise<{ changed: string[]; opts: InstallOpts; paths: ReturnType<typeof resolveInstallPaths> }> {
128
128
  const opts = parseInstallOpts(rawOpts);
129
- const err = validateOpts(opts);
129
+ const err = validateInstallOpts(opts);
130
130
  if (err) {
131
131
  throw new Error(err);
132
132
  }
@@ -159,7 +159,7 @@ export async function runInstallMutation(
159
159
  }
160
160
 
161
161
  if (actions.length === 0) {
162
- throw new Error("Nothing to do.");
162
+ return { changed: [], opts, paths };
163
163
  }
164
164
 
165
165
  if (!opts.quiet && !opts.json) {
@@ -218,5 +218,3 @@ export async function cliInstall(root: CliProgram, rawOpts: Record<string, strin
218
218
 
219
219
  process.exit(0);
220
220
  }
221
-
222
- export { parseInstallOpts };
@@ -6,8 +6,9 @@ import { CliProgram } from "../types.ts";
6
6
  import { detectInstalledArtifacts } from "./detect-installed.ts";
7
7
  import { resolveInstallPaths } from "./paths.ts";
8
8
  import { buildInstallPlan } from "./plan.ts";
9
+ import { buildUninstallPlan } from "./uninstall.ts";
9
10
  import { printInstallStatus } from "./status.ts";
10
- import { parseInstallOpts } from "./index.ts";
11
+ import { parseInstallOpts, runInstallMutation, validateInstallOpts } from "./index.ts";
11
12
 
12
13
  const fixture: CliProgram = {
13
14
  key: "testapp",
@@ -123,3 +124,65 @@ describe("parseInstallOpts", () => {
123
124
  expect(opts.all).toBe(true);
124
125
  });
125
126
  });
127
+
128
+ describe("validateInstallOpts", () => {
129
+ test("uninstall requires a target flag", () => {
130
+ const opts = parseInstallOpts({ uninstall: "1", yes: "1" });
131
+ expect(validateInstallOpts(opts)).toContain("Specify at least one target");
132
+ });
133
+
134
+ test("uninstall allows --all", () => {
135
+ const opts = parseInstallOpts({ uninstall: "1", all: "1", yes: "1" });
136
+ expect(validateInstallOpts(opts)).toBeNull();
137
+ });
138
+
139
+ test("uninstall rejects --reinstall", () => {
140
+ const opts = parseInstallOpts({ uninstall: "1", reinstall: "1", all: "1" });
141
+ expect(validateInstallOpts(opts)).toContain("--reinstall");
142
+ });
143
+ });
144
+
145
+ describe("install mutation", () => {
146
+ test("uninstall --all with nothing installed succeeds", async () => {
147
+ const result = await runInstallMutation(fixture, { uninstall: "1", all: "1", yes: "1" });
148
+ expect(result.changed).toEqual([]);
149
+ });
150
+
151
+ test("install --skill with no agent dirs succeeds", async () => {
152
+ const result = await runInstallMutation(fixture, { skill: "1", yes: "1", dry: "1" });
153
+ expect(result.changed).toEqual([]);
154
+ });
155
+
156
+ test("uninstall --all removes detected binary", async () => {
157
+ const paths = resolveInstallPaths(fixture, {});
158
+ mkdirSync(paths.bindir, { recursive: true });
159
+ writeFileSync(paths.binaryPath, "fake", "utf8");
160
+
161
+ const result = await runInstallMutation(fixture, { uninstall: "1", all: "1", yes: "1" });
162
+ expect(result.changed).toContain(paths.binaryPath);
163
+ expect(existsSync(paths.binaryPath)).toBe(false);
164
+ });
165
+ });
166
+
167
+ describe("uninstall plan", () => {
168
+ test("buildUninstallPlan --all scopes like install --all", () => {
169
+ const paths = resolveInstallPaths(fixture, {});
170
+ mkdirSync(paths.bindir, { recursive: true });
171
+ writeFileSync(paths.binaryPath, "fake", "utf8");
172
+
173
+ const plan = buildUninstallPlan(fixture, paths, parseInstallOpts({ uninstall: "1", all: "1" }));
174
+ expect(plan.some((a) => a.summary.startsWith("binary:"))).toBe(true);
175
+ });
176
+
177
+ test("buildUninstallPlan --bin ignores completions", () => {
178
+ const paths = resolveInstallPaths(fixture, {});
179
+ mkdirSync(paths.bindir, { recursive: true });
180
+ writeFileSync(paths.binaryPath, "fake", "utf8");
181
+ mkdirSync(join(home, ".bash_completion.d"), { recursive: true });
182
+ writeFileSync(paths.bashCompletion, "# bash", "utf8");
183
+
184
+ const plan = buildUninstallPlan(fixture, paths, parseInstallOpts({ uninstall: "1", bin: "1" }));
185
+ expect(plan).toHaveLength(1);
186
+ expect(plan[0]!.summary.startsWith("binary:")).toBe(true);
187
+ });
188
+ });
@@ -1,7 +1,6 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { dirname } from "node:path";
3
3
  import { CliProgram } from "../types.ts";
4
- import { InstallPaths } from "./paths.ts";
5
4
 
6
5
  export interface McpServerEntry {
7
6
  command: string;
@@ -35,19 +35,19 @@ export interface InstallAction {
35
35
  run: () => string[];
36
36
  }
37
37
 
38
- function wantsBin(opts: InstallOpts): boolean {
38
+ export function wantsInstallBin(opts: InstallOpts): boolean {
39
39
  return !!(opts.all || opts.bin || opts.reinstall);
40
40
  }
41
41
 
42
- function wantsCompletions(opts: InstallOpts): boolean {
42
+ export function wantsInstallCompletions(opts: InstallOpts): boolean {
43
43
  return !!(opts.all || opts.completions);
44
44
  }
45
45
 
46
- function wantsSkill(opts: InstallOpts): boolean {
46
+ export function wantsInstallSkill(opts: InstallOpts): boolean {
47
47
  return !!(opts.all || opts.skill);
48
48
  }
49
49
 
50
- function wantsMcp(opts: InstallOpts, root: CliProgram): boolean {
50
+ export function wantsInstallMcp(opts: InstallOpts, root: CliProgram): boolean {
51
51
  return !!(opts.mcp || opts.all) && resolveCapabilities(root).mcp;
52
52
  }
53
53
 
@@ -56,7 +56,7 @@ export function buildInstallPlan(root: CliProgram, paths: InstallPaths, opts: In
56
56
  const actions: InstallAction[] = [];
57
57
  const dry = !!opts.dry;
58
58
 
59
- if (wantsBin(opts)) {
59
+ if (wantsInstallBin(opts)) {
60
60
  const sourcePath = opts.from ?? process.execPath;
61
61
  actions.push({
62
62
  kind: "binary",
@@ -66,7 +66,7 @@ export function buildInstallPlan(root: CliProgram, paths: InstallPaths, opts: In
66
66
  });
67
67
  }
68
68
 
69
- if (wantsCompletions(opts)) {
69
+ if (wantsInstallCompletions(opts)) {
70
70
  const shells = detectShells();
71
71
  if (shells.bash) {
72
72
  actions.push({
@@ -103,7 +103,7 @@ export function buildInstallPlan(root: CliProgram, paths: InstallPaths, opts: In
103
103
  }
104
104
  }
105
105
 
106
- if (wantsSkill(opts)) {
106
+ if (wantsInstallSkill(opts)) {
107
107
  const home = userHome();
108
108
  if (existsSync(join(home, ".cursor"))) {
109
109
  actions.push({
@@ -123,7 +123,7 @@ export function buildInstallPlan(root: CliProgram, paths: InstallPaths, opts: In
123
123
  }
124
124
  }
125
125
 
126
- if (wantsMcp(opts, root)) {
126
+ if (wantsInstallMcp(opts, root)) {
127
127
  const entry = expectedMcpEntry(root);
128
128
  if (existsSync(join(userHome(), ".cursor"))) {
129
129
  actions.push({
@@ -1,13 +1,17 @@
1
1
  import { existsSync, rmSync } from "node:fs";
2
- import { join } from "node:path";
3
- import { resolveCapabilities } from "../capabilities.ts";
4
2
  import { CliProgram } from "../types.ts";
5
3
  import { uninstallBinary } from "./binary.ts";
6
4
  import { uninstallCompletions } from "./completions.ts";
7
5
  import { detectInstalledArtifacts } from "./detect-installed.ts";
8
6
  import { removeMcpConfig } from "./mcp-config.ts";
9
- import { InstallPaths, userHome } from "./paths.ts";
10
- import type { InstallOpts } from "./plan.ts";
7
+ import { InstallPaths } from "./paths.ts";
8
+ import {
9
+ wantsInstallBin,
10
+ wantsInstallCompletions,
11
+ wantsInstallMcp,
12
+ wantsInstallSkill,
13
+ type InstallOpts,
14
+ } from "./plan.ts";
11
15
 
12
16
  export interface UninstallAction {
13
17
  summary: string;
@@ -15,22 +19,17 @@ export interface UninstallAction {
15
19
  run: () => string[];
16
20
  }
17
21
 
18
- function scopeAll(opts: InstallOpts): boolean {
19
- return !opts.bin && !opts.completions && !opts.skill && !opts.mcp;
20
- }
21
-
22
- /** Builds uninstall actions from detected artifacts. */
22
+ /** Builds uninstall actions for scoped targets (--all mirrors install --all). */
23
23
  export function buildUninstallPlan(
24
24
  root: CliProgram,
25
25
  paths: InstallPaths,
26
26
  opts: InstallOpts,
27
27
  ): UninstallAction[] {
28
28
  const detected = detectInstalledArtifacts(paths);
29
- const all = scopeAll(opts);
30
29
  const dry = !!opts.dry;
31
30
  const actions: UninstallAction[] = [];
32
31
 
33
- if ((all || opts.bin) && detected.binary) {
32
+ if (wantsInstallBin(opts) && detected.binary) {
34
33
  actions.push({
35
34
  summary: `binary: ${paths.binaryPath}`,
36
35
  message: `Removing binary ${paths.binaryPath}`,
@@ -38,7 +37,10 @@ export function buildUninstallPlan(
38
37
  });
39
38
  }
40
39
 
41
- if ((all || opts.completions) && (detected.bashCompletion || detected.zshCompletion || detected.fishCompletion)) {
40
+ if (
41
+ wantsInstallCompletions(opts) &&
42
+ (detected.bashCompletion || detected.zshCompletion || detected.fishCompletion)
43
+ ) {
42
44
  if (detected.bashCompletion) {
43
45
  actions.push({
44
46
  summary: `bash completion: ${paths.bashCompletion}`,
@@ -62,7 +64,7 @@ export function buildUninstallPlan(
62
64
  }
63
65
  }
64
66
 
65
- if ((all || opts.skill) && detected.cursorSkill) {
67
+ if (wantsInstallSkill(opts) && detected.cursorSkill) {
66
68
  actions.push({
67
69
  summary: `cursor skill: ${paths.cursorSkillDir}/`,
68
70
  message: `Removing Cursor skill ${paths.cursorSkillDir}/`,
@@ -70,7 +72,7 @@ export function buildUninstallPlan(
70
72
  });
71
73
  }
72
74
 
73
- if ((all || opts.skill) && detected.claudeSkill) {
75
+ if (wantsInstallSkill(opts) && detected.claudeSkill) {
74
76
  actions.push({
75
77
  summary: `claude skill: ${paths.claudeSkillDir}/`,
76
78
  message: `Removing Claude Code skill ${paths.claudeSkillDir}/`,
@@ -78,7 +80,7 @@ export function buildUninstallPlan(
78
80
  });
79
81
  }
80
82
 
81
- if ((all || opts.mcp) && resolveCapabilities(root).mcp) {
83
+ if (wantsInstallMcp(opts, root)) {
82
84
  if (detected.cursorMcp) {
83
85
  actions.push({
84
86
  summary: `cursor mcp: ${paths.cursorMcpPath}`,