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
@@ -0,0 +1,47 @@
1
+ import { accessSync, constants, realpathSync } from "node:fs";
2
+ import { delimiter, join } from "node:path";
3
+ import type { CliProgram } from "../types.ts";
4
+
5
+ /** Resolves a command name on PATH to its real path, if present. */
6
+ export function resolvePathCommand(key: string): string | undefined {
7
+ const pathEnv = process.env.PATH ?? "";
8
+ for (const dir of pathEnv.split(delimiter)) {
9
+ if (!dir) continue;
10
+ const candidate = join(dir, key);
11
+ try {
12
+ accessSync(candidate, constants.F_OK);
13
+ return realpathSync(candidate);
14
+ } catch {}
15
+ }
16
+
17
+ const found = Bun.which(key);
18
+ if (found === null) return undefined;
19
+ try {
20
+ return realpathSync(found);
21
+ } catch {
22
+ return found;
23
+ }
24
+ }
25
+
26
+ function realpathOrSelf(path: string): string {
27
+ try {
28
+ return realpathSync(path);
29
+ } catch {
30
+ return path;
31
+ }
32
+ }
33
+
34
+ /** True when PATH resolves to the running executable (e.g. Homebrew Cellar). */
35
+ export function isExternallyManagedBinary(
36
+ key: string,
37
+ execPath: string = process.execPath,
38
+ ): boolean {
39
+ const resolved = resolvePathCommand(key);
40
+ if (!resolved) return false;
41
+ return resolved === realpathOrSelf(execPath);
42
+ }
43
+
44
+ /** True when the app binary is available on PATH (e.g. Homebrew). */
45
+ export function isAppInstalled(program: CliProgram): boolean {
46
+ return isExternallyManagedBinary(program.key);
47
+ }
@@ -4,6 +4,7 @@ import { displayAppConfigPath, runInstallConfigure } from "../config/bootstrap.t
4
4
  import { cliSkillInstall, skillTargetFromActionKind } from "../skill/install.ts";
5
5
  import type { CliProgram } from "../types.ts";
6
6
  import { normalizeInstallRawOpts } from "./normalize.ts";
7
+ import { normalizeUninstallRawOpts } from "./normalize-uninstall.ts";
7
8
  import { resolveInstallPaths } from "./paths.ts";
8
9
  import { buildInstallPlan, buildUpdatePlan, type InstallAction, type InstallOpts } from "./plan.ts";
9
10
  import {
@@ -13,7 +14,6 @@ import {
13
14
  printInstallStatus,
14
15
  writeInteractiveInstallIntro,
15
16
  } from "./status.ts";
16
- import { resolveEffectiveInstallTargets } from "./target-effective.ts";
17
17
  import { runTargetPreflight } from "./target-plan-build.ts";
18
18
  import type { InstallActionKind } from "./target-types.ts";
19
19
  import {
@@ -22,19 +22,14 @@ import {
22
22
  type UninstallAction,
23
23
  uninstallSkillDir,
24
24
  } from "./uninstall.ts";
25
- import { cliUpdate } from "./update.ts";
26
25
 
27
26
  export function parseInstallOpts(raw: Record<string, string>): InstallOpts {
28
27
  const flag = (name: string) => raw[name] === "1";
29
28
  return {
30
29
  all: flag("all"),
31
- app: flag("app"),
32
- completions: flag("completions"),
33
30
  skill: flag("skill"),
34
31
  mcp: flag("mcp"),
35
32
  reinstall: flag("reinstall"),
36
- update: flag("update"),
37
- from: raw.from,
38
33
  status: flag("status"),
39
34
  uninstall: flag("uninstall"),
40
35
  configure: flag("configure"),
@@ -46,91 +41,55 @@ export function parseInstallOpts(raw: Record<string, string>): InstallOpts {
46
41
 
47
42
  export function validateInstallOpts(opts: InstallOpts): string | null {
48
43
  const configureOnlyInstall =
49
- opts.configure &&
50
- !opts.uninstall &&
51
- !opts.all &&
52
- !opts.app &&
53
- !opts.completions &&
54
- !opts.skill &&
55
- !opts.mcp &&
56
- !opts.reinstall &&
57
- !opts.update &&
58
- !opts.status;
44
+ opts.configure && !opts.all && !opts.skill && !opts.mcp && !opts.reinstall && !opts.status;
59
45
 
60
46
  if (configureOnlyInstall) {
61
47
  return null;
62
48
  }
63
49
 
50
+ if (opts.uninstall) {
51
+ return "Use `uninstall` instead of `install --uninstall`.";
52
+ }
53
+
64
54
  if (opts.json) {
65
55
  opts.yes = true;
66
56
  }
67
57
  if (opts.reinstall) {
68
58
  opts.yes = true;
69
59
  }
70
- if (opts.update) {
71
- opts.yes = true;
72
- }
73
60
 
74
- const mutationFlags =
75
- opts.all ||
76
- opts.app ||
77
- opts.completions ||
78
- opts.skill ||
79
- opts.mcp ||
80
- opts.reinstall ||
81
- opts.update ||
82
- opts.uninstall ||
83
- opts.configure;
61
+ const mutationFlags = opts.all || opts.skill || opts.mcp || opts.reinstall || opts.configure;
84
62
  if (opts.status && mutationFlags) {
85
- return "--status is mutually exclusive with install/reinstall/uninstall targets.";
86
- }
87
- if (
88
- opts.reinstall &&
89
- (opts.all ||
90
- opts.completions ||
91
- opts.skill ||
92
- opts.mcp ||
93
- opts.uninstall ||
94
- opts.status ||
95
- opts.update ||
96
- opts.configure)
97
- ) {
98
- return "--reinstall cannot be combined with other target flags.";
63
+ return "--status is mutually exclusive with install/reinstall targets.";
99
64
  }
100
- if (
101
- opts.update &&
102
- (opts.all ||
103
- opts.app ||
104
- opts.completions ||
105
- opts.skill ||
106
- opts.mcp ||
107
- opts.uninstall ||
108
- opts.status ||
109
- opts.reinstall ||
110
- opts.configure)
111
- ) {
112
- return "--update cannot be combined with other target flags.";
113
- }
114
- if (opts.uninstall && (opts.reinstall || opts.update || opts.status)) {
115
- return "--uninstall cannot be combined with --reinstall, --update, or --status.";
116
- }
117
- if (opts.uninstall) {
118
- const hasUninstallTarget =
119
- opts.all || opts.app || opts.completions || opts.skill || opts.mcp || opts.configure;
120
- if (!hasUninstallTarget) {
121
- return "Specify at least one target: --all, --app, --completions, --skill, --mcp, or --configure.";
122
- }
65
+ if (opts.reinstall && (opts.all || opts.skill || opts.mcp || opts.status || opts.configure)) {
66
+ return "--reinstall cannot be combined with other target flags.";
123
67
  }
124
- if (!opts.status && !opts.reinstall && !opts.update && !opts.uninstall) {
125
- const hasTarget =
126
- opts.all || opts.app || opts.completions || opts.skill || opts.mcp || opts.configure;
68
+ if (!opts.status && !opts.reinstall) {
69
+ const hasTarget = opts.all || opts.skill || opts.mcp || opts.configure;
127
70
  if (!hasTarget) {
128
- return "Specify at least one target: --all, --app, --completions, --skill, --mcp, or --configure.";
71
+ return "Specify at least one target: --all, --skill, --mcp, or --configure.";
129
72
  }
130
73
  }
131
74
  return null;
132
75
  }
133
76
 
77
+ export function validateUninstallOpts(opts: InstallOpts): string | null {
78
+ if (opts.json) {
79
+ opts.yes = true;
80
+ }
81
+
82
+ if (opts.status || opts.reinstall) {
83
+ return "uninstall does not support --status or --reinstall.";
84
+ }
85
+
86
+ const hasTarget = opts.all || opts.skill || opts.mcp || opts.configure;
87
+ if (!hasTarget) {
88
+ return "Specify at least one target: --all, --skill, --mcp, or --configure.";
89
+ }
90
+ return null;
91
+ }
92
+
134
93
  function parseSelectionIndices(input: string, max: number): number[] | null {
135
94
  const trimmed = input.trim();
136
95
  if (!trimmed) return null;
@@ -148,14 +107,12 @@ function parseSelectionIndices(input: string, max: number): number[] | null {
148
107
  return indices.sort((a, b) => a - b);
149
108
  }
150
109
 
151
- /** Interactive install: item 1 is the app when the plan leads with `app`. */
110
+ /** Interactive install: legacy helper (app no longer installed). */
152
111
  export function interactiveSelectionAssumesApp(
153
- actions: Array<InstallAction | UninstallAction>,
154
- uninstall: boolean,
112
+ _actions: Array<InstallAction | UninstallAction>,
113
+ _uninstall: boolean,
155
114
  ): boolean {
156
- if (uninstall) return false;
157
- const first = actions[0];
158
- return first !== undefined && "kind" in first && first.kind === "app";
115
+ return false;
159
116
  }
160
117
 
161
118
  export function mergeInteractiveSelection(
@@ -229,18 +186,7 @@ function executePlan(
229
186
  return changed;
230
187
  }
231
188
 
232
- function shouldRunConfigureWizardAfterInstall(root: CliProgram, opts: InstallOpts): boolean {
233
- if (opts.uninstall || opts.reinstall || opts.update || opts.status || !root.appConfig) {
234
- return false;
235
- }
236
- if (!opts.all && !opts.app && !opts.completions && !opts.skill && !opts.mcp) {
237
- return false;
238
- }
239
- const effective = resolveEffectiveInstallTargets(root.install, root);
240
- return effective.configure.includedInAll && effective.configure.enabled;
241
- }
242
-
243
- /** Runs install/reinstall/uninstall mutations without exiting the process. */
189
+ /** Runs install/reinstall mutations without exiting the process. */
244
190
  export async function runInstallMutation(
245
191
  root: CliProgram,
246
192
  rawOpts: Record<string, string>,
@@ -255,7 +201,35 @@ export async function runInstallMutation(
255
201
  if (err) {
256
202
  throw new Error(err);
257
203
  }
204
+ return runInstallMutationInternal(root, opts);
205
+ }
206
+
207
+ /** Runs uninstall mutations without exiting the process. */
208
+ export async function runUninstallMutation(
209
+ root: CliProgram,
210
+ rawOpts: Record<string, string>,
211
+ ): Promise<{
212
+ changed: string[];
213
+ opts: InstallOpts;
214
+ paths: ReturnType<typeof resolveInstallPaths>;
215
+ }> {
216
+ const normalized = normalizeUninstallRawOpts(rawOpts);
217
+ const opts = parseInstallOpts(normalized);
218
+ const err = validateUninstallOpts(opts);
219
+ if (err) {
220
+ throw new Error(err);
221
+ }
222
+ return runInstallMutationInternal(root, opts);
223
+ }
258
224
 
225
+ async function runInstallMutationInternal(
226
+ root: CliProgram,
227
+ opts: InstallOpts,
228
+ ): Promise<{
229
+ changed: string[];
230
+ opts: InstallOpts;
231
+ paths: ReturnType<typeof resolveInstallPaths>;
232
+ }> {
259
233
  const paths = resolveInstallPaths(root);
260
234
 
261
235
  if (opts.status) {
@@ -280,13 +254,16 @@ export async function runInstallMutation(
280
254
  }
281
255
 
282
256
  let selectedActions = actions;
283
- const autoYes = !!(opts.yes || opts.json || opts.reinstall || opts.update || opts.dry);
257
+ const autoYes = !!(opts.yes || opts.json || opts.reinstall || opts.dry);
284
258
 
285
259
  if (!autoYes && !opts.json && process.stdin.isTTY) {
286
260
  writeInteractiveInstallIntro(root);
287
261
  }
288
262
 
289
263
  if (actions.length === 0) {
264
+ if (!autoYes && !opts.json && process.stdin.isTTY && !opts.configure) {
265
+ installOut("Nothing to install for the selected targets.", opts);
266
+ }
290
267
  return { changed: [], opts, paths };
291
268
  }
292
269
 
@@ -322,17 +299,15 @@ export async function cliInstall(
322
299
  root: CliProgram,
323
300
  rawOpts: Record<string, string>,
324
301
  ): Promise<never> {
302
+ if (rawOpts.uninstall === "1") {
303
+ installErr(`Use \`${root.key} uninstall\` instead of \`${root.key} install --uninstall\`.`);
304
+ process.exit(1);
305
+ }
306
+
325
307
  const normalized = normalizeInstallRawOpts(rawOpts);
326
308
  const opts = parseInstallOpts(normalized);
327
309
 
328
- const configureOnly =
329
- opts.configure &&
330
- !opts.uninstall &&
331
- !opts.all &&
332
- !opts.app &&
333
- !opts.completions &&
334
- !opts.skill &&
335
- !opts.mcp;
310
+ const configureOnly = opts.configure && !opts.all && !opts.skill && !opts.mcp;
336
311
 
337
312
  if (configureOnly) {
338
313
  if (!root.appConfig) {
@@ -357,14 +332,6 @@ export async function cliInstall(
357
332
  process.exit(1);
358
333
  }
359
334
 
360
- if (opts.update) {
361
- if (!resolveCapabilities(root).update) {
362
- installErr("Remote updates are not supported by this app.");
363
- process.exit(1);
364
- }
365
- await cliUpdate(root);
366
- }
367
-
368
335
  let result: Awaited<ReturnType<typeof runInstallMutation>>;
369
336
  try {
370
337
  result = await runInstallMutation(root, normalized);
@@ -373,23 +340,12 @@ export async function cliInstall(
373
340
  process.exit(1);
374
341
  }
375
342
 
376
- const { changed, opts: mutationOpts, paths } = result;
343
+ const { changed, opts: mutationOpts } = result;
377
344
 
378
345
  if (mutationOpts.status) {
379
346
  process.exit(0);
380
347
  }
381
348
 
382
- if (
383
- shouldRunConfigureWizardAfterInstall(root, mutationOpts) &&
384
- !mutationOpts.uninstall &&
385
- !mutationOpts.dry
386
- ) {
387
- const configResult = runInstallConfigure(root, { context: "after-install" });
388
- if (configResult.changed) {
389
- installOut(`Wrote config: ${displayAppConfigPath(root)}`, mutationOpts);
390
- }
391
- }
392
-
393
349
  if (mutationOpts.json) {
394
350
  process.stdout.write(`${JSON.stringify(changed, null, 2)}\n`);
395
351
  process.exit(0);
@@ -402,14 +358,52 @@ export async function cliInstall(
402
358
  ? "Reinstalled"
403
359
  : "Installed";
404
360
  installOut(`${verb} ${changed.length} file(s).`, mutationOpts);
405
- if (
406
- !mutationOpts.uninstall &&
407
- (mutationOpts.all || mutationOpts.app) &&
408
- changed.some((p) => p === paths.bashRc || p === paths.zshRc || p === paths.appPath)
409
- ) {
410
- installOut("Open a new shell, or run: hash -r (bash) / rehash (zsh)", mutationOpts);
361
+ }
362
+
363
+ if (mutationOpts.configure && root.appConfig) {
364
+ const configResult = runInstallConfigure(root, {
365
+ context: changed.length > 0 ? "after-install" : "standalone",
366
+ });
367
+ if (configResult.changed) {
368
+ installOut(`Wrote config: ${displayAppConfigPath(root)}`, mutationOpts);
411
369
  }
412
370
  }
413
371
 
414
372
  process.exit(0);
415
373
  }
374
+
375
+ /** Main uninstall command orchestrator. */
376
+ export async function cliUninstall(
377
+ root: CliProgram,
378
+ rawOpts: Record<string, string>,
379
+ ): Promise<never> {
380
+ const normalized = normalizeUninstallRawOpts(rawOpts);
381
+ const opts = parseInstallOpts(normalized);
382
+
383
+ const err = validateUninstallOpts(opts);
384
+ if (err) {
385
+ installErr(err);
386
+ process.exit(1);
387
+ }
388
+
389
+ let result: Awaited<ReturnType<typeof runUninstallMutation>>;
390
+ try {
391
+ result = await runUninstallMutation(root, rawOpts);
392
+ } catch (mutationErr) {
393
+ installErr(mutationErr instanceof Error ? mutationErr.message : String(mutationErr));
394
+ process.exit(1);
395
+ }
396
+
397
+ const { changed, opts: mutationOpts } = result;
398
+
399
+ if (mutationOpts.json) {
400
+ process.stdout.write(`${JSON.stringify(changed, null, 2)}\n`);
401
+ process.exit(0);
402
+ }
403
+
404
+ if (changed.length > 0) {
405
+ installOut(`Removed ${changed.length} file(s).`, mutationOpts);
406
+ }
407
+
408
+ process.exit(0);
409
+ }