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
@@ -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 {
@@ -17,6 +17,10 @@ class ConfigureInstallTarget extends InstallTarget {
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", () => {
@@ -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
+ install: { agentIntegration: "both" },
105
+ handler: () => {},
106
+ };
107
+ const effective = resolveEffectiveInstallTargets(program.install, 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
  });
@@ -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/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
 
@@ -529,6 +529,7 @@ test("docs schema exports JSON for leaf roots", async () => {
529
529
  "completion",
530
530
  "version",
531
531
  "install",
532
+ "uninstall",
532
533
  "docs",
533
534
  ]);
534
535
  });
@@ -614,7 +615,7 @@ test("cliSchemaExport resolves program key in install notes", () => {
614
615
 
615
616
  const json = cliSchemaJson(root);
616
617
  expect(json).not.toContain("{argsbarg:program}");
617
- expect(json).toContain("myapp install --yes");
618
+ expect(json).toContain("brew install");
618
619
  });
619
620
 
620
621
  test("cliSchemaExport resolves {argsbarg:program} in consumer notes", () => {
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,15 @@ 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([
17
+ "completion",
18
+ "install",
19
+ "uninstall",
20
+ "docs",
21
+ "mcp",
22
+ "version",
23
+ "config",
24
+ ]);
17
25
 
18
26
  function exportCommand(cmd: CliNode, root: CliProgram): CliSchemaExport | null {
19
27
  if (cmd.hidden) {
package/src/types.ts CHANGED
@@ -202,6 +202,26 @@ export interface CliUpdateArtifact {
202
202
  /** Fetches the latest release binary for `install --update`. */
203
203
  export type CliUpdateGetLatest = (ctx: { version: string }) => Promise<CliUpdateArtifact>;
204
204
 
205
+ /** Context passed to {@link CliAppConfigEntry.resolve} for one config key. */
206
+ export interface CliAppConfigResolveContext {
207
+ /** Schema key being resolved. */
208
+ key: string;
209
+ /** Entry metadata for this key. */
210
+ entry: CliAppConfigEntry;
211
+ /** Program root (read-only). */
212
+ program: CliProgram;
213
+ /** Raw value from the config file, if any. */
214
+ fileValue: unknown;
215
+ /** Non-empty host env string when `entry.env` is set; otherwise `undefined`. */
216
+ envValue: string | undefined;
217
+ }
218
+
219
+ /**
220
+ * Optional fallback resolver for one config key (e.g. `gh auth token` when `GH_TOKEN` is unset).
221
+ * Return `undefined` to continue resolution (env, then default).
222
+ */
223
+ export type CliAppConfigResolveFn = (ctx: CliAppConfigResolveContext) => unknown;
224
+
205
225
  /**
206
226
  * Metadata overlay for one key in {@link CliAppConfig.entries}.
207
227
  * Types and validation come from {@link CliAppConfig.jsonSchema} when set; otherwise all values are strings.
@@ -222,6 +242,11 @@ export interface CliAppConfigEntry {
222
242
  sensitive?: boolean;
223
243
  /** When set: non-empty `process.env[env]` overrides file; value exported after resolve. */
224
244
  env?: string;
245
+ /**
246
+ * Optional fallback after file when env is empty.
247
+ * Return `undefined` to fall back to `env` (if set) and schema defaults.
248
+ */
249
+ resolve?: CliAppConfigResolveFn;
225
250
  }
226
251
 
227
252
  /**
@@ -248,11 +273,6 @@ export interface CliInstallConfig {
248
273
  agentIntegration?: InstallAgentIntegration;
249
274
  /** Per-artifact gates for full install/uninstall. See {@link resolveEffectiveInstallTargets}. */
250
275
  targets?: CliInstallTargets;
251
- /**
252
- * When set, enables `install --update` on the program root.
253
- * Should download or locate the latest release binary and return its path.
254
- */
255
- updateGetLatest?: CliUpdateGetLatest;
256
276
  }
257
277
 
258
278
  /** Agent integration mode for install — MCP vs shell skill per host. */
@@ -275,7 +295,7 @@ export interface ResolvedInstallTarget {
275
295
 
276
296
  /** Per-artifact gates for full install/uninstall. See {@link resolveEffectiveInstallTargets}. */
277
297
  export interface CliInstallTargets {
278
- /** Copy app to `~/.local/bin/<key>`. Default includedInAll true (opt-out). */
298
+ /** App binary status only (Homebrew PATH); no self-install. */
279
299
  app?: InstallTargetSpec;
280
300
  /** ChatGPT desktop MCP. Default false. */
281
301
  chatgptMcp?: InstallTargetSpec;
@@ -289,9 +309,7 @@ export interface CliInstallTargets {
289
309
  codexMcp?: InstallTargetSpec;
290
310
  /** Codex skill. Default false. */
291
311
  codexSkill?: InstallTargetSpec;
292
- /** Shell completions for detected shells. Default includedInAll true (opt-out). */
293
- completions?: InstallTargetSpec;
294
- /** App config: wizard on install, file removal on uninstall. Default includedInAll true. */
312
+ /** App config: wizard via install --configure only. Default not in --all. */
295
313
  configure?: InstallTargetSpec;
296
314
  /** Cursor MCP. Default false. */
297
315
  cursorMcp?: InstallTargetSpec;
package/src/validate.ts CHANGED
@@ -81,6 +81,11 @@ function validateConfigBlock(appConfigBlock: import("./types.ts").CliAppConfig):
81
81
  }
82
82
  envNames.add(entry.env);
83
83
  }
84
+ if (entry.resolve !== undefined && typeof entry.resolve !== "function") {
85
+ throw new CliSchemaValidationError(
86
+ `program.appConfig.entries['${key}'].resolve must be a function when set`,
87
+ );
88
+ }
84
89
  }
85
90
 
86
91
  const jsonSchema = appConfigBlock.jsonSchema;
@@ -197,17 +202,6 @@ export function cliValidateProgram(program: CliProgram): void {
197
202
  validateConfigBlock(program.appConfig);
198
203
  }
199
204
 
200
- if (program.install?.updateGetLatest !== undefined) {
201
- if (program.install.enabled === false) {
202
- throw new CliSchemaValidationError(
203
- "install.updateGetLatest requires install to be enabled (omit install.enabled: false)",
204
- );
205
- }
206
- if (typeof program.install.updateGetLatest !== "function") {
207
- throw new CliSchemaValidationError("install.updateGetLatest must be a function");
208
- }
209
- }
210
-
211
205
  if (program.install !== undefined) {
212
206
  validateInstallConfig(program);
213
207
  }
@@ -1,31 +0,0 @@
1
- ---
2
- description: Argsbarg schema — read framework docs before editing CLI commands
3
- globs: "src/**/commands/**/*.{ts,tsx},src/index.{ts,tsx}"
4
- alwaysApply: false
5
- ---
6
-
7
- When adding or changing argsbarg schema, leaf handlers, or MCP exposure:
8
-
9
- 1. **Read** `node_modules/argsbarg/docs/cli-program.md` (required — authoritative guide).
10
- 2. MCP tools, varargs, `inputSchema` → also `node_modules/argsbarg/docs/mcp.md`.
11
- 3. JSON stdout / `outputSchema` guide and codegen → `node_modules/argsbarg/docs/output-schema.md`.
12
- 4. App config / `program.appConfig` guide and codegen → `node_modules/argsbarg/docs/config-schema.md`.
13
- 5. `install`, `install.targets`, completions, skills → `node_modules/argsbarg/docs/install.md`.
14
- 6. Bundled `docs` built-in → `node_modules/argsbarg/docs/bundled-docs.md`.
15
- 7. **Examples** (shipped under `node_modules/argsbarg/examples/`):
16
- - Concepts / minimal config → `examples/config-app/`
17
- - **Copy template** (all builtins, schemagen, `outputSchema`) → `examples/consumer-app/`
18
-
19
- **Hard rules** (details and examples are in the docs above — do not contradict them):
20
-
21
- - Reserved root commands: `completion`, `install`, `mcp`, `version`, `docs`, `update`, `config`.
22
- - `satisfies CliProgram` / `CliLeaf`; action-oriented `description` on root, commands, options, and positionals.
23
- - Omit `mcpTool` unless genuinely CLI-only (`enabled: false`) or an irreducible wire limit — fix schema and headless handlers first.
24
- - Interactive leaves: one headless path for MCP, non-TTY CLI, and `--yes` / `--dry-run` / `--json` (`shouldRunHeadless*`, `requireYesInNonTty`); not raw `isTTY`.
25
- - String options: `format` / `default` / `pattern` per `cli-program.md`; `readLeafInputs()` for multi-flag leaves.
26
- - Varargs (`argMax: 0`): CLI space-separated; MCP JSON array only — no comma-splitting positionals.
27
- - Multi-surface leaves (Ink + headless + MCP): one **`read*Flags(ctx)`** per command (or shared family helper + extensions); one **`resolve*Input(flags)`** for cross-field rules — handler reads ctx once, all paths share the struct.
28
- - JSON stdout: `outputSchema` on the leaf from generated constants in `outputSchemas.ts` — see `output-schema.md`; mark roots with **`JSON payload`** JSDoc in `src/**/types.ts`; do not hand-edit `src/schemas/generated/` or `outputSchemas.ts`.
29
- - App config: `program.appConfig.jsonSchema` from generated `configSchemas.ts` — see `config-schema.md`; mark roots with **`Config schema`** JSDoc; prefer the layout in `node_modules/argsbarg/examples/consumer-app/` when adding new schema roots.
30
-
31
- **App-specific conventions:** replace this line with a `**<your-app> conventions:**` section (bullets only). Keep it at the bottom of this file — `just consumer-dev` / `just consumers-sync` in the argsbarg repo refresh the template above and preserve this block. Do not duplicate `cli-program.md` here; link paths and patterns only. For a second rule file (e.g. `.cursor/argsbarg.mdc`), that is fine too.
@@ -1,20 +0,0 @@
1
- #!/usr/bin/env bun
2
- /*
3
- Multi-file consumer example for program.appConfig.
4
-
5
- Files:
6
- types.ts — AppConfig interface (Config schema JSDoc marker for schemagen)
7
- schema.ts — APP_CONFIG_JSON_SCHEMA (inline; production apps generate this)
8
- program.ts — CliProgram with appConfig block and commands using ctx.appConfig
9
-
10
- Try:
11
- CONFIG_APP_API_TOKEN=dev bun ./examples/config-app/main.ts show --json
12
- CONFIG_APP_API_TOKEN=dev bun ./examples/config-app/main.ts config get
13
- CONFIG_APP_API_TOKEN=dev bun ./examples/config-app/main.ts ping
14
- */
15
-
16
- import { Cli } from "../../src/index.ts";
17
- import { program } from "./program.ts";
18
-
19
- const cli = new Cli(program);
20
- await cli.run();