argsbarg 4.1.0 → 5.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/CHANGELOG.md +74 -1
  2. package/README.md +91 -85
  3. package/docs/README.md +8 -8
  4. package/docs/ai-skills.md +9 -9
  5. package/docs/bundled-docs.md +5 -5
  6. package/docs/cli-program.md +13 -11
  7. package/docs/config-schema.md +36 -9
  8. package/docs/configure.md +177 -0
  9. package/docs/developing.md +7 -7
  10. package/docs/distribution-homebrew.md +104 -0
  11. package/docs/mcp.md +11 -12
  12. package/docs/output-schema.md +1 -1
  13. package/examples/full-example/Formula/.gitkeep +0 -0
  14. package/examples/full-example/README.md +98 -0
  15. package/examples/full-example/biome.json +22 -0
  16. package/examples/{consumer-app → full-example}/bun.lock +2 -0
  17. package/examples/full-example/justfile +134 -0
  18. package/examples/{consumer-app → full-example}/package.json +10 -3
  19. package/examples/{consumer-app → full-example}/schemas/generated/app-config.json +1 -1
  20. package/examples/{consumer-app → full-example}/schemas/generated/status.json +1 -1
  21. package/examples/full-example/scripts/create-identity.ts +11 -0
  22. package/examples/full-example/scripts/formula-shared.ts +73 -0
  23. package/examples/full-example/scripts/gen-dev-formula.ts +26 -0
  24. package/examples/full-example/scripts/print-identity.ts +27 -0
  25. package/examples/full-example/src/commands/echo/command.ts +21 -0
  26. package/examples/full-example/src/commands/status/command.test.ts +10 -0
  27. package/examples/full-example/src/commands/status/command.ts +36 -0
  28. package/examples/{consumer-app → full-example}/src/commands/status/types.ts +1 -1
  29. package/examples/full-example/src/index.ts +10 -0
  30. package/examples/full-example/src/program.ts +57 -0
  31. package/examples/{consumer-app → full-example}/src/types.ts +1 -1
  32. package/examples/nested.ts +1 -3
  33. package/index.d.ts +49 -81
  34. package/package.json +2 -2
  35. package/src/builtins/builtins.test.ts +84 -64
  36. package/src/builtins/completion-group.ts +17 -17
  37. package/src/builtins/configure-copy.ts +86 -0
  38. package/src/builtins/configure.ts +70 -0
  39. package/src/builtins/dispatch.ts +13 -9
  40. package/src/builtins/index.ts +1 -1
  41. package/src/builtins/mcp.ts +2 -2
  42. package/src/builtins/registry.ts +6 -4
  43. package/src/capabilities.ts +22 -15
  44. package/src/cli-tool/cli-smoke.test.ts +29 -0
  45. package/src/cli-tool/create.test.ts +141 -0
  46. package/src/cli-tool/create.ts +402 -0
  47. package/{examples/consumer-app/capabilities.test.ts → src/cli-tool/full-example-capabilities.test.ts} +16 -15
  48. package/src/cli-tool/main.ts +8 -0
  49. package/src/cli-tool/post-create.ts +111 -0
  50. package/src/cli-tool/program.ts +97 -0
  51. package/src/cli-tool/prompt.ts +28 -0
  52. package/src/cli-tool/run-create.ts +138 -0
  53. package/src/cli.ts +0 -2
  54. package/src/config/bootstrap.ts +27 -18
  55. package/src/config/file.test.ts +1 -1
  56. package/src/config/resolve.test.ts +167 -0
  57. package/src/config/resolve.ts +52 -8
  58. package/src/configure/configure.test.ts +148 -0
  59. package/src/configure/index.ts +284 -0
  60. package/src/configure/prompt.ts +40 -0
  61. package/src/docs/api-guide.test.ts +4 -5
  62. package/src/docs/builtin.ts +3 -5
  63. package/src/docs/docs.test.ts +6 -5
  64. package/src/docs/mcp-guide.ts +11 -12
  65. package/src/index.ts +5 -12
  66. package/src/install/binary-placement.test.ts +101 -0
  67. package/src/install/binary-placement.ts +47 -0
  68. package/src/install/install-validate.test.ts +5 -5
  69. package/src/install/normalize-uninstall.ts +11 -0
  70. package/src/install/normalize.ts +4 -19
  71. package/src/install/opts.ts +17 -0
  72. package/src/install/paths.ts +0 -22
  73. package/src/install/plan.ts +14 -6
  74. package/src/install/shell.ts +0 -14
  75. package/src/install/status.test.ts +6 -6
  76. package/src/install/status.ts +0 -6
  77. package/src/install/target-effective.ts +8 -10
  78. package/src/install/target-scope.ts +26 -36
  79. package/src/install/target-types.ts +0 -16
  80. package/src/install/targets/app.ts +19 -28
  81. package/src/install/targets/configure.ts +6 -2
  82. package/src/install/targets/index.ts +0 -3
  83. package/src/install/targets.test.ts +26 -44
  84. package/src/invoke.test.ts +1 -1
  85. package/src/mcp/env.test.ts +92 -0
  86. package/src/mcp/env.ts +15 -14
  87. package/src/mcp/tools.ts +1 -1
  88. package/src/mcp.integration.test.ts +4 -4
  89. package/src/parse.test.ts +13 -14
  90. package/src/prompt.ts +10 -0
  91. package/src/schema.ts +1 -1
  92. package/src/skill/hint.ts +2 -2
  93. package/src/types.ts +48 -22
  94. package/src/validate.ts +22 -28
  95. package/docs/install.md +0 -290
  96. package/docs/templates/cursor/rules/cli-program.mdc +0 -31
  97. package/examples/config-app/main.ts +0 -20
  98. package/examples/config-app/program.ts +0 -78
  99. package/examples/config-app/schema.ts +0 -37
  100. package/examples/config-app/types.ts +0 -19
  101. package/examples/consumer-app/README.md +0 -56
  102. package/examples/consumer-app/src/main.ts +0 -15
  103. package/examples/consumer-app/src/program.ts +0 -108
  104. package/src/builtins/install.ts +0 -136
  105. package/src/install/app.ts +0 -94
  106. package/src/install/bootstrap.ts +0 -22
  107. package/src/install/completions.ts +0 -56
  108. package/src/install/index.ts +0 -415
  109. package/src/install/install.test.ts +0 -333
  110. package/src/install/targets/completions.ts +0 -133
  111. package/src/install/update.test.ts +0 -123
  112. package/src/install/update.ts +0 -54
  113. /package/examples/{consumer-app → full-example}/schemas/configSchemas.ts +0 -0
  114. /package/examples/{consumer-app → full-example}/schemas/outputSchemas.ts +0 -0
  115. /package/examples/{consumer-app → full-example}/scripts/schemagen/discover-schema-roots.test.ts +0 -0
  116. /package/examples/{consumer-app → full-example}/scripts/schemagen/discover-schema-roots.ts +0 -0
  117. /package/examples/{consumer-app → full-example}/scripts/schemagen/naming.ts +0 -0
  118. /package/examples/{consumer-app → full-example}/scripts/schemagen.ts +0 -0
  119. /package/examples/{consumer-app → full-example}/tsconfig.json +0 -0
@@ -12,8 +12,6 @@ export type InstallPlanMode =
12
12
 
13
13
  export interface InstallScope {
14
14
  all?: boolean;
15
- app?: boolean;
16
- completions?: boolean;
17
15
  skill?: boolean;
18
16
  mcp?: boolean;
19
17
  configure?: boolean;
@@ -29,7 +27,6 @@ export type CliInstallArtifactKey =
29
27
  | "claudeSkill"
30
28
  | "codexMcp"
31
29
  | "codexSkill"
32
- | "completions"
33
30
  | "configure"
34
31
  | "cursorMcp"
35
32
  | "cursorSkill"
@@ -40,7 +37,6 @@ export type CliInstallArtifactKey =
40
37
 
41
38
  export type InstallActionKind =
42
39
  | "app"
43
- | "completions"
44
40
  | "cursor-skill"
45
41
  | "claude-skill"
46
42
  | "codex-skill"
@@ -73,9 +69,6 @@ export interface UninstallAction {
73
69
 
74
70
  export interface InstalledArtifacts {
75
71
  app: boolean;
76
- bashCompletion: boolean;
77
- zshCompletion: boolean;
78
- fishCompletion: boolean;
79
72
  cursorSkill: boolean;
80
73
  claudeSkill: boolean;
81
74
  codexSkill: boolean;
@@ -88,15 +81,10 @@ export interface InstalledArtifacts {
88
81
  codexMcp: boolean;
89
82
  openclawMcp: boolean;
90
83
  chatGptMcp: boolean;
91
- bashRcPath: boolean;
92
- zshRcFpath: boolean;
93
84
  }
94
85
 
95
86
  export interface InstallStatus {
96
87
  app?: string;
97
- bashCompletion?: string;
98
- zshCompletion?: string;
99
- fishCompletion?: string;
100
88
  cursorSkill?: string;
101
89
  claudeSkill?: string;
102
90
  codexSkill?: string;
@@ -117,13 +105,9 @@ export interface DetectedSnapshot extends InstalledArtifacts {
117
105
 
118
106
  export interface InstallOpts {
119
107
  all?: boolean;
120
- app?: boolean;
121
- completions?: boolean;
122
108
  skill?: boolean;
123
109
  mcp?: boolean;
124
110
  reinstall?: boolean;
125
- update?: boolean;
126
- from?: string;
127
111
  status?: boolean;
128
112
  uninstall?: boolean;
129
113
  configure?: boolean;
@@ -1,6 +1,5 @@
1
- import { existsSync } from "node:fs";
2
1
  import type { CliProgram } from "../../types.ts";
3
- import { installApp, uninstallApp } from "../app.ts";
2
+ import { isExternallyManagedBinary } from "../binary-placement.ts";
4
3
  import { displayInstallPath, type InstallPaths } from "../paths.ts";
5
4
  import { InstallTarget } from "../target-base.ts";
6
5
  import type {
@@ -12,57 +11,49 @@ import type {
12
11
  UninstallAction,
13
12
  } from "../target-types.ts";
14
13
 
15
- /** Installs the compiled app to ~/.local/bin/<key>. */
14
+ /** Reports app install location (Homebrew PATH or legacy ~/.local/bin). No self-install actions. */
16
15
  class AppInstallTarget extends InstallTarget {
17
16
  readonly key = "app" as const;
18
17
  readonly actionKind = "app" as const;
19
18
  readonly category = "core" as const;
20
19
 
20
+ defaultIncludedInAll(): boolean {
21
+ return false;
22
+ }
23
+
21
24
  isAvailable(_root: CliProgram, _paths: InstallPaths): boolean {
22
25
  return true;
23
26
  }
24
27
 
25
- isDetected(paths: InstallPaths, _root: CliProgram): boolean {
26
- return existsSync(paths.appPath);
28
+ isDetected(paths: InstallPaths, root: CliProgram): boolean {
29
+ return isExternallyManagedBinary(root.key) || false;
27
30
  }
28
31
 
29
- applyDetected(paths: InstallPaths, root: CliProgram, out: InstalledArtifacts): void {
30
- out.app = this.isDetected(paths, root);
32
+ applyDetected(_paths: InstallPaths, root: CliProgram, out: InstalledArtifacts): void {
33
+ out.app = isExternallyManagedBinary(root.key);
31
34
  }
32
35
 
33
36
  protected isDetectedFromSnapshot(detected: DetectedSnapshot): boolean {
34
37
  return detected.app;
35
38
  }
36
39
 
37
- protected formatStatusLine(paths: InstallPaths, _root: CliProgram): string {
38
- return displayInstallPath(paths.appPath);
40
+ protected formatStatusLine(_paths: InstallPaths, root: CliProgram): string {
41
+ if (isExternallyManagedBinary(root.key)) {
42
+ return "system (PATH)";
43
+ }
44
+ return "not installed (use Homebrew)";
39
45
  }
40
46
 
41
47
  protected assignStatusLine(status: InstallStatus, line: string): void {
42
48
  status.app = line;
43
49
  }
44
50
 
45
- protected buildInstallActions(ctx: TargetPlanContext): InstallAction[] {
46
- const sourcePath = ctx.opts.from ?? process.execPath;
47
- return [
48
- {
49
- kind: this.actionKind,
50
- summary: `app: ${displayInstallPath(ctx.paths.appPath)}`,
51
- message: `Installing app to ${displayInstallPath(ctx.paths.appPath)}`,
52
- run: () => installApp(ctx.root, ctx.paths, ctx.dry, sourcePath).changedFiles,
53
- },
54
- ];
51
+ protected buildInstallActions(_ctx: TargetPlanContext): InstallAction[] {
52
+ return [];
55
53
  }
56
54
 
57
- protected buildUninstallActions(ctx: TargetPlanContext): UninstallAction[] {
58
- return [
59
- {
60
- kind: this.actionKind,
61
- summary: `app: ${displayInstallPath(ctx.paths.appPath)}`,
62
- message: `Removing app ${displayInstallPath(ctx.paths.appPath)}`,
63
- run: () => uninstallApp(ctx.root, ctx.paths, ctx.dry),
64
- },
65
- ];
55
+ protected buildUninstallActions(_ctx: TargetPlanContext): UninstallAction[] {
56
+ return [];
66
57
  }
67
58
  }
68
59
 
@@ -1,5 +1,5 @@
1
1
  import { appConfigInstalled, displayAppConfigPath, uninstallAppConfig } from "../../config/file.ts";
2
- import type { CliProgram } from "../../types.ts";
2
+ import type { CliProgram, InstallAgentIntegration } from "../../types.ts";
3
3
  import type { InstallPaths } from "../paths.ts";
4
4
  import { InstallTarget } from "../target-base.ts";
5
5
  import type {
@@ -11,12 +11,16 @@ import type {
11
11
  UninstallAction,
12
12
  } from "../target-types.ts";
13
13
 
14
- /** App config file (install --configure / wizard). */
14
+ /** App config file (interactive configure / wizard). */
15
15
  class ConfigureInstallTarget extends InstallTarget {
16
16
  readonly key = "configure" as const;
17
17
  readonly actionKind = "configure" as const;
18
18
  readonly category = "core" as const;
19
19
 
20
+ defaultIncludedInAll(_integration: InstallAgentIntegration): boolean {
21
+ return false;
22
+ }
23
+
20
24
  isAvailable(root: CliProgram, _paths: InstallPaths): boolean {
21
25
  return root.appConfig !== undefined;
22
26
  }
@@ -6,7 +6,6 @@ import { claudeDesktopMcpTarget } from "./claude-desktop-mcp.ts";
6
6
  import { claudeSkillTarget } from "./claude-skill.ts";
7
7
  import { codexMcpTarget } from "./codex-mcp.ts";
8
8
  import { codexSkillTarget } from "./codex-skill.ts";
9
- import { completionsTarget } from "./completions.ts";
10
9
  import { configureTarget } from "./configure.ts";
11
10
  import { cursorMcpTarget } from "./cursor-mcp.ts";
12
11
  import { cursorSkillTarget } from "./cursor-skill.ts";
@@ -18,7 +17,6 @@ import { opencodeSkillTarget } from "./opencode-skill.ts";
18
17
  /** Ordered install targets (plan iteration order). */
19
18
  export const INSTALL_TARGETS: InstallTarget[] = [
20
19
  appTarget,
21
- completionsTarget,
22
20
  cursorSkillTarget,
23
21
  claudeSkillTarget,
24
22
  codexSkillTarget,
@@ -42,7 +40,6 @@ export {
42
40
  claudeSkillTarget,
43
41
  codexMcpTarget,
44
42
  codexSkillTarget,
45
- completionsTarget,
46
43
  configureTarget,
47
44
  cursorMcpTarget,
48
45
  cursorSkillTarget,
@@ -1,11 +1,7 @@
1
1
  import { describe, expect, test } from "bun:test";
2
- import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
2
  import type { CliProgram } from "../types.ts";
6
- import { maybeBootstrapInstallArgv } from "./bootstrap.ts";
7
3
  import { normalizeInstallRawOpts } from "./normalize.ts";
8
- import { resolveInstallPaths } from "./paths.ts";
4
+ import { normalizeUninstallRawOpts } from "./normalize-uninstall.ts";
9
5
  import { resolveAgentIntegration, resolveEffectiveInstallTargets } from "./target-effective.ts";
10
6
  import { isArtifactInScope } from "./target-scope.ts";
11
7
 
@@ -15,7 +11,7 @@ describe("normalizeInstallRawOpts", () => {
15
11
  });
16
12
 
17
13
  test("bare uninstall sets all", () => {
18
- expect(normalizeInstallRawOpts({ uninstall: "1" })).toEqual({
14
+ expect(normalizeUninstallRawOpts({})).toEqual({
19
15
  uninstall: "1",
20
16
  all: "1",
21
17
  });
@@ -37,11 +33,10 @@ describe("resolveAgentIntegration", () => {
37
33
  });
38
34
 
39
35
  describe("resolveEffectiveInstallTargets", () => {
40
- test("defaults app completions configure includedInAll", () => {
36
+ test("defaults app and configure not in --all", () => {
41
37
  const t = resolveEffectiveInstallTargets(undefined);
42
- expect(t.app.includedInAll).toBe(true);
43
- expect(t.completions.includedInAll).toBe(true);
44
- expect(t.configure.includedInAll).toBe(true);
38
+ expect(t.app.includedInAll).toBe(false);
39
+ expect(t.configure.includedInAll).toBe(false);
45
40
  });
46
41
 
47
42
  test("skill mode includes skills in --all not MCP pairs", () => {
@@ -80,10 +75,10 @@ describe("resolveEffectiveInstallTargets", () => {
80
75
  version: "1",
81
76
  description: "x",
82
77
  mcpServer: { enabled: true },
83
- install: { agentIntegration: "skill" },
78
+ configure: { agentIntegration: "skill" },
84
79
  handler: () => {},
85
80
  };
86
- const effective = resolveEffectiveInstallTargets(program.install, program);
81
+ const effective = resolveEffectiveInstallTargets(program.configure, program);
87
82
  const scope = { mcp: true };
88
83
  expect(isArtifactInScope("cursorMcp", scope, effective, "install-scoped", program)).toBe(false);
89
84
  expect(
@@ -99,38 +94,25 @@ describe("resolveEffectiveInstallTargets", () => {
99
94
  ),
100
95
  ).toBe(true);
101
96
  });
102
- });
103
-
104
- describe("maybeBootstrapInstallArgv", () => {
105
- const program: CliProgram = {
106
- key: "bootapp",
107
- version: "1.0.0",
108
- description: "Boot",
109
- handler: () => {},
110
- };
111
-
112
- test("rewrites empty argv when app missing and TTY", () => {
113
- const prev = process.stdin.isTTY;
114
- Object.defineProperty(process.stdin, "isTTY", { value: true, configurable: true });
115
- try {
116
- expect(maybeBootstrapInstallArgv([], program)).toEqual(["install"]);
117
- } finally {
118
- Object.defineProperty(process.stdin, "isTTY", { value: prev, configurable: true });
119
- }
120
- });
121
97
 
122
- test("does not rewrite when app exists", () => {
123
- const home = mkdtempSync(join(tmpdir(), "boot-"));
124
- const prevHome = process.env.HOME;
125
- process.env.HOME = home;
126
- const paths = resolveInstallPaths(program);
127
- try {
128
- mkdirSync(paths.appDir, { recursive: true });
129
- writeFileSync(paths.appPath, "x", "utf8");
130
- expect(maybeBootstrapInstallArgv([], program)).toEqual([]);
131
- } finally {
132
- process.env.HOME = prevHome;
133
- rmSync(home, { recursive: true, force: true });
134
- }
98
+ test("scoped --mcp --skill --configure includes skill and mcp not only configure", () => {
99
+ const program: CliProgram = {
100
+ key: "app",
101
+ version: "1",
102
+ description: "x",
103
+ mcpServer: { enabled: true },
104
+ configure: { agentIntegration: "both" },
105
+ handler: () => {},
106
+ };
107
+ const effective = resolveEffectiveInstallTargets(program.configure, program);
108
+ const scope = { mcp: true, skill: true, configure: true };
109
+ expect(isArtifactInScope("cursorSkill", scope, effective, "install-scoped", program)).toBe(
110
+ true,
111
+ );
112
+ expect(isArtifactInScope("cursorMcp", scope, effective, "install-scoped", program)).toBe(true);
113
+ expect(isArtifactInScope("configure", scope, effective, "install-scoped", program)).toBe(true);
114
+ expect(isArtifactInScope("claudeSkill", scope, effective, "install-scoped", program)).toBe(
115
+ true,
116
+ );
135
117
  });
136
118
  });
@@ -20,7 +20,7 @@ const program = {
20
20
  key: "t",
21
21
  description: "d",
22
22
  version: "0.0.0",
23
- install: { enabled: false },
23
+ configure: { enabled: false },
24
24
  handler: (ctx) => console.log(ctx.invocation),
25
25
  };
26
26
  await new Cli(program).run([]);
@@ -0,0 +1,92 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import { chmodSync, mkdtempSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { applyShellEnv, bootstrapMcpEnv } from "./env.ts";
6
+
7
+ const TEST_VAR = "ARGS_BARG_SHELL_ENV_TEST";
8
+
9
+ describe("mcp/env", () => {
10
+ test("applyShellEnv merges PATH and fills missing vars", () => {
11
+ const prevPath = process.env.PATH;
12
+ const prevTest = process.env[TEST_VAR];
13
+ delete process.env[TEST_VAR];
14
+ try {
15
+ process.env.PATH = "/host/bin";
16
+ applyShellEnv({
17
+ PATH: "/shell/bin:/host/bin",
18
+ [TEST_VAR]: "from-shell",
19
+ });
20
+ expect(process.env.PATH).toBe("/shell/bin:/host/bin");
21
+ expect(process.env[TEST_VAR]).toBe("from-shell");
22
+ } finally {
23
+ if (prevPath === undefined) delete process.env.PATH;
24
+ else process.env.PATH = prevPath;
25
+ if (prevTest === undefined) delete process.env[TEST_VAR];
26
+ else process.env[TEST_VAR] = prevTest;
27
+ }
28
+ });
29
+
30
+ test("applyShellEnv does not overwrite host vars except PATH merge", () => {
31
+ const prev = process.env.HOME;
32
+ process.env.HOME = "/host/home";
33
+ try {
34
+ applyShellEnv({ HOME: "/shell/home" });
35
+ expect(process.env.HOME).toBe("/host/home");
36
+ } finally {
37
+ if (prev === undefined) delete process.env.HOME;
38
+ else process.env.HOME = prev;
39
+ }
40
+ });
41
+ });
42
+
43
+ describe("bootstrapMcpEnv", () => {
44
+ let fakeShell: string;
45
+ let prevShell: string | undefined;
46
+ let prevTest: string | undefined;
47
+
48
+ beforeEach(() => {
49
+ const dir = mkdtempSync(join(tmpdir(), "argsbarg-shell-env-"));
50
+ fakeShell = join(dir, "fake-shell.sh");
51
+ writeFileSync(
52
+ fakeShell,
53
+ `#!/bin/sh
54
+ if [ "$1" = "-l" ] && [ "$2" = "-c" ] && [ "$3" = "env" ]; then
55
+ echo "${TEST_VAR}=from_fake_shell"
56
+ fi
57
+ `,
58
+ );
59
+ chmodSync(fakeShell, 0o755);
60
+ prevShell = process.env.SHELL;
61
+ prevTest = process.env[TEST_VAR];
62
+ delete process.env[TEST_VAR];
63
+ process.env.SHELL = fakeShell;
64
+ });
65
+
66
+ afterEach(() => {
67
+ if (prevShell === undefined) delete process.env.SHELL;
68
+ else process.env.SHELL = prevShell;
69
+ if (prevTest === undefined) delete process.env[TEST_VAR];
70
+ else process.env[TEST_VAR] = prevTest;
71
+ });
72
+
73
+ test("defaults on when shellEnv is undefined", () => {
74
+ bootstrapMcpEnv({});
75
+ expect(process.env[TEST_VAR]).toBe("from_fake_shell");
76
+ });
77
+
78
+ test("runs when shellEnv is true", () => {
79
+ bootstrapMcpEnv({ shellEnv: true });
80
+ expect(process.env[TEST_VAR]).toBe("from_fake_shell");
81
+ });
82
+
83
+ test("uses explicit shell path when shellEnv is a string", () => {
84
+ bootstrapMcpEnv({ shellEnv: fakeShell });
85
+ expect(process.env[TEST_VAR]).toBe("from_fake_shell");
86
+ });
87
+
88
+ test("skips capture when shellEnv is false", () => {
89
+ bootstrapMcpEnv({ shellEnv: false });
90
+ expect(process.env[TEST_VAR]).toBeUndefined();
91
+ });
92
+ });
package/src/mcp/env.ts CHANGED
@@ -39,21 +39,22 @@ export function applyShellEnv(env: Record<string, string>): void {
39
39
  }
40
40
  }
41
41
 
42
- /** Applies mcpServer shellEnv bootstrap. */
42
+ /** Applies mcpServer shellEnv bootstrap (default on; opt out with `shellEnv: false`). */
43
43
  export function bootstrapMcpEnv(config: { shellEnv?: boolean | string }): void {
44
+ if (config.shellEnv === false) {
45
+ return;
46
+ }
44
47
  const shellEnvCfg = config.shellEnv;
45
- if (shellEnvCfg) {
46
- const shell =
47
- typeof shellEnvCfg === "string"
48
- ? shellEnvCfg
49
- : (process.env.SHELL ?? (process.platform === "darwin" ? "/bin/zsh" : "/bin/bash"));
50
- const captured = captureShellEnv(shell);
51
- if (Object.keys(captured).length === 0) {
52
- process.stderr.write(
53
- `[argsbarg] shellEnv: failed to capture shell environment from ${shell}\n`,
54
- );
55
- } else {
56
- applyShellEnv(captured);
57
- }
48
+ const shell =
49
+ typeof shellEnvCfg === "string"
50
+ ? shellEnvCfg
51
+ : (process.env.SHELL ?? (process.platform === "darwin" ? "/bin/zsh" : "/bin/bash"));
52
+ const captured = captureShellEnv(shell);
53
+ if (Object.keys(captured).length === 0) {
54
+ process.stderr.write(
55
+ `[argsbarg] shellEnv: failed to capture shell environment from ${shell}\n`,
56
+ );
57
+ } else {
58
+ applyShellEnv(captured);
58
59
  }
59
60
  }
package/src/mcp/tools.ts CHANGED
@@ -224,7 +224,7 @@ export function collectMcpTools(root: CliProgram): McpToolDef[] {
224
224
  if (isCliLeaf(cmd)) {
225
225
  if (
226
226
  cmd.key === "completion" ||
227
- cmd.key === "install" ||
227
+ cmd.key === "configure" ||
228
228
  cmd.key === "mcp" ||
229
229
  cmd.key === "version"
230
230
  ) {
@@ -35,7 +35,7 @@ test("collectMcpTools lists user leaf commands only", () => {
35
35
  expect(names).toContain("stat_owner_lookup");
36
36
  expect(names).toContain("read");
37
37
  expect(names).not.toContain("hidden");
38
- expect(names).not.toContain("install");
38
+ expect(names).not.toContain("configure");
39
39
  expect(names).not.toContain("mcp");
40
40
  expect(names).not.toContain("completion");
41
41
  const lookup = tools.find((t) => t.name === "stat_owner_lookup")!;
@@ -242,19 +242,19 @@ test("mcpToolCallToArgv expands varargs positionals", () => {
242
242
  expect(argv).toEqual(["read", "a", "b"]);
243
243
  });
244
244
 
245
- test("reserved command name install is rejected", () => {
245
+ test("reserved command name configure is rejected", () => {
246
246
  const root = testProgram({
247
247
  key: "app",
248
248
  description: "",
249
249
  commands: [
250
250
  {
251
- key: "install",
251
+ key: "configure",
252
252
  description: "bad",
253
253
  handler: () => {},
254
254
  },
255
255
  ],
256
256
  });
257
- expect(() => cliValidateProgram(root)).toThrow(/Reserved command name: install/);
257
+ expect(() => cliValidateProgram(root)).toThrow(/Reserved command name: configure/);
258
258
  });
259
259
 
260
260
  test("top-level command name mcp is allowed without mcpServer", () => {
package/src/parse.test.ts CHANGED
@@ -495,7 +495,7 @@ test("leaf completion help prints correctly", async () => {
495
495
  const out = stdout.toString();
496
496
  expect(exitCode).toBe(0);
497
497
  expect(out).toContain("Show help for this command.");
498
- expect(out).toContain("Manual install:");
498
+ expect(out).toContain("Homebrew");
499
499
  expect(stderr.toString()).toBe("");
500
500
  });
501
501
 
@@ -526,9 +526,8 @@ test("docs schema exports JSON for leaf roots", async () => {
526
526
  expect(schema.positionals[0].name).toBe("name");
527
527
  expect(schema.options[0].name).toBe("verbose");
528
528
  expect(schema.commands.map((c: { key: string }) => c.key)).toEqual([
529
- "completion",
530
529
  "version",
531
- "install",
530
+ "configure",
532
531
  "docs",
533
532
  ]);
534
533
  });
@@ -539,11 +538,11 @@ test("version builtin prints program version", async () => {
539
538
  expect(stdout.toString().trim()).toMatch(/^\d+\.\d+\.\d+/);
540
539
  });
541
540
 
542
- test("leaf root help lists completion built-in", async () => {
541
+ test("leaf root help omits hidden completion built-in", async () => {
543
542
  const { stdout, exitCode } = await $`bun run examples/minimal.ts -h`.nothrow().quiet();
544
543
  expect(exitCode).toBe(0);
545
- expect(stdout.toString()).toContain("completion");
546
- expect(stdout.toString()).toContain("Generate the autocompletion script for shells.");
544
+ expect(stdout.toString()).not.toContain("completion");
545
+ expect(stdout.toString()).toContain("configure");
547
546
  });
548
547
 
549
548
  test("root --schema is no longer a flag", () => {
@@ -614,7 +613,7 @@ test("cliSchemaExport resolves program key in install notes", () => {
614
613
 
615
614
  const json = cliSchemaJson(root);
616
615
  expect(json).not.toContain("{argsbarg:program}");
617
- expect(json).toContain("myapp install --yes");
616
+ expect(json).toContain("brew install");
618
617
  });
619
618
 
620
619
  test("cliSchemaExport resolves {argsbarg:program} in consumer notes", () => {
@@ -1107,7 +1106,7 @@ test("mcpToolCallToArgv empty array varargs errors when required", () => {
1107
1106
 
1108
1107
  // ── Skills ────────────────────────────────────────────────────────────────────
1109
1108
 
1110
- test("install config on non-root node is rejected", () => {
1109
+ test("configure config on non-root node is rejected", () => {
1111
1110
  const root = {
1112
1111
  key: "app",
1113
1112
  version: "0.0.0",
@@ -1116,23 +1115,23 @@ test("install config on non-root node is rejected", () => {
1116
1115
  {
1117
1116
  key: "x",
1118
1117
  description: "",
1119
- install: { enabled: false },
1118
+ configure: { enabled: false },
1120
1119
  handler: () => {},
1121
1120
  },
1122
1121
  ],
1123
1122
  } as unknown as CliProgram;
1124
- expect(() => cliValidateProgram(root)).toThrow(/install is only supported on the program root/);
1123
+ expect(() => cliValidateProgram(root)).toThrow(/configure is only supported on the program root/);
1125
1124
  });
1126
1125
 
1127
- test("install.prefix is rejected", () => {
1126
+ test("configure.prefix is rejected", () => {
1128
1127
  const root = {
1129
1128
  key: "app",
1130
1129
  version: "0.0.0",
1131
1130
  description: "",
1132
- install: { prefix: "/opt/bin" },
1131
+ configure: { prefix: "/opt/bin" },
1133
1132
  handler: () => {},
1134
1133
  } as unknown as CliProgram;
1135
- expect(() => cliValidateProgram(root)).toThrow(/install\.prefix removed/);
1134
+ expect(() => cliValidateProgram(root)).toThrow(/configure\.prefix removed/);
1136
1135
  });
1137
1136
 
1138
1137
  test("generateSkillBundle includes frontmatter and compact command index", () => {
@@ -1183,7 +1182,7 @@ test("cliSkillInstall writes project Cursor skill files", () => {
1183
1182
  expect(readFileSync(join(skillDir, "reference.md"), "utf8")).toContain("CLI API reference");
1184
1183
  const skillText = readFileSync(join(skillDir, "SKILL.md"), "utf8");
1185
1184
  expect(skillText.startsWith("---\n")).toBe(true);
1186
- const hint = "<!-- Generated by nested.ts install --skill; do not edit. -->";
1185
+ const hint = "<!-- Generated by nested.ts configure; do not edit. -->";
1187
1186
  expect(skillText.indexOf(hint)).toBeGreaterThan(skillText.indexOf("---\n", 4));
1188
1187
  const refText = readFileSync(join(skillDir, "reference.md"), "utf8");
1189
1188
  expect(refText.startsWith(hint)).toBe(true);
package/src/prompt.ts ADDED
@@ -0,0 +1,10 @@
1
+ /** Shared terminal prompt helpers. */
2
+
3
+ import { readSync } from "node:fs";
4
+
5
+ /** Read one line from stdin (no masking). */
6
+ export function readPromptLine(): string {
7
+ const buf = Buffer.alloc(4096);
8
+ const n = readSync(0, buf, { length: 4096 });
9
+ return buf.toString("utf8", 0, n).replace(/\r?\n$/, "");
10
+ }
package/src/schema.ts CHANGED
@@ -13,7 +13,7 @@ import {
13
13
  leafOutputSchema,
14
14
  } from "./types.ts";
15
15
 
16
- const RESERVED = new Set(["completion", "install", "docs", "mcp", "version", "config"]);
16
+ const RESERVED = new Set(["completion", "configure", "docs", "mcp", "version", "config"]);
17
17
 
18
18
  function exportCommand(cmd: CliNode, root: CliProgram): CliSchemaExport | null {
19
19
  if (cmd.hidden) {
package/src/skill/hint.ts CHANGED
@@ -23,9 +23,9 @@ export function insertGeneratedHint(
23
23
  return `${hint}${content}`;
24
24
  }
25
25
 
26
- /** Hint for `install --skill` output files. */
26
+ /** Hint for `configure` skill output files. */
27
27
  export function skillInstallHint(program: CliProgram): string {
28
- return generatedFileHtmlComment(`${program.key} install --skill`);
28
+ return generatedFileHtmlComment(`${program.key} configure`);
29
29
  }
30
30
 
31
31
  /** Applies install hints to SKILL.md (after frontmatter) and reference.md. */