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
@@ -0,0 +1,148 @@
1
+ import { afterEach, beforeEach, 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
+ import {
6
+ appConfigHasEntries,
7
+ parseConfigureOpts,
8
+ validateConfigureOpts,
9
+ } from "../configure/index.ts";
10
+ import { detectInstalledArtifacts } from "../install/detect-installed.ts";
11
+ import { parseInstallOpts } from "../install/opts.ts";
12
+ import { resolveClaudeDesktopMcpPath, resolveInstallPaths } from "../install/paths.ts";
13
+ import { buildInstallPlan, buildUpdatePlan } from "../install/plan.ts";
14
+ import { buildUninstallPlan } from "../install/uninstall.ts";
15
+ import type { CliProgram } from "../types.ts";
16
+
17
+ const fixture: CliProgram = {
18
+ key: "testapp",
19
+ version: "0.0.0",
20
+ description: "Test",
21
+ mcpServer: { enabled: true },
22
+ handler: () => {},
23
+ };
24
+
25
+ let home: string;
26
+ let prevHome: string | undefined;
27
+ let prevXdg: string | undefined;
28
+
29
+ beforeEach(() => {
30
+ home = mkdtempSync(join(tmpdir(), "argsbarg-configure-"));
31
+ prevHome = process.env.HOME;
32
+ prevXdg = process.env.XDG_CONFIG_HOME;
33
+ process.env.HOME = home;
34
+ delete process.env.XDG_CONFIG_HOME;
35
+ });
36
+
37
+ afterEach(() => {
38
+ if (prevHome === undefined) delete process.env.HOME;
39
+ else process.env.HOME = prevHome;
40
+ if (prevXdg === undefined) delete process.env.XDG_CONFIG_HOME;
41
+ else process.env.XDG_CONFIG_HOME = prevXdg;
42
+ rmSync(home, { recursive: true, force: true });
43
+ });
44
+
45
+ describe("configure opts", () => {
46
+ test("validate rejects multiple modes", () => {
47
+ const opts = parseConfigureOpts({ sync: "1", status: "1" });
48
+ expect(validateConfigureOpts(opts)).toContain("only one");
49
+ });
50
+
51
+ test("sync requires --yes", () => {
52
+ const opts = parseConfigureOpts({ sync: "1" });
53
+ expect(validateConfigureOpts(opts)).toContain("--yes");
54
+ });
55
+
56
+ test("remove-all requires --yes", () => {
57
+ const opts = parseConfigureOpts({ "remove-all": "1" });
58
+ expect(validateConfigureOpts(opts)).toContain("--yes");
59
+ });
60
+ });
61
+
62
+ describe("install paths", () => {
63
+ test("resolveInstallPaths includes skill and mcp paths", () => {
64
+ const paths = resolveInstallPaths(fixture);
65
+ expect(paths.cursorSkillDir).toContain(".cursor/skills");
66
+ expect(paths.cursorMcpPath).toContain("mcp.json");
67
+ });
68
+
69
+ test("claude desktop mcp path on darwin", () => {
70
+ const prev = process.platform;
71
+ Object.defineProperty(process, "platform", { value: "darwin" });
72
+ try {
73
+ expect(resolveClaudeDesktopMcpPath(home)).toBe(
74
+ join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json"),
75
+ );
76
+ } finally {
77
+ Object.defineProperty(process, "platform", { value: prev });
78
+ }
79
+ });
80
+ });
81
+
82
+ describe("detect installed", () => {
83
+ test("detects cursor mcp when configured", () => {
84
+ const paths = resolveInstallPaths(fixture);
85
+ mkdirSync(join(home, ".cursor"), { recursive: true });
86
+ writeFileSync(
87
+ paths.cursorMcpPath,
88
+ JSON.stringify({ mcpServers: { testapp: { command: "testapp", args: ["mcp"] } } }),
89
+ "utf8",
90
+ );
91
+
92
+ const detected = detectInstalledArtifacts(paths, fixture);
93
+ expect(detected.cursorMcp).toBe(true);
94
+ expect(detected.app).toBe(false);
95
+ });
96
+ });
97
+
98
+ describe("sync plan", () => {
99
+ test("buildUpdatePlan greenfield includes agent targets", () => {
100
+ const paths = resolveInstallPaths(fixture);
101
+ const plan = buildUpdatePlan(fixture, paths, parseInstallOpts({ reinstall: "1", yes: "1" }));
102
+ expect(plan.length).toBeGreaterThan(0);
103
+ expect(plan.some((a) => a.kind === "app")).toBe(false);
104
+ });
105
+
106
+ test("buildInstallPlan --all omits app self-install", () => {
107
+ const paths = resolveInstallPaths(fixture);
108
+ const plan = buildInstallPlan(fixture, paths, parseInstallOpts({ all: "1" }));
109
+ expect(plan.some((a) => a.kind === "app")).toBe(false);
110
+ expect(plan.some((a) => a.kind.endsWith("-mcp"))).toBe(true);
111
+ });
112
+
113
+ test("buildInstallPlan respects configure.agentIntegration skill", () => {
114
+ const skillApp: CliProgram = {
115
+ ...fixture,
116
+ configure: { agentIntegration: "skill" },
117
+ };
118
+ const paths = resolveInstallPaths(skillApp);
119
+ mkdirSync(join(home, ".claude"), { recursive: true });
120
+ mkdirSync(join(home, ".cursor"), { recursive: true });
121
+ const plan = buildInstallPlan(skillApp, paths, parseInstallOpts({ mcp: "1" }));
122
+ expect(plan.some((a) => a.kind.endsWith("-mcp"))).toBe(false);
123
+ });
124
+ });
125
+
126
+ describe("remove plan", () => {
127
+ test("buildUninstallPlan --all with nothing installed is empty", () => {
128
+ const paths = resolveInstallPaths(fixture);
129
+ const plan = buildUninstallPlan(fixture, paths, parseInstallOpts({ uninstall: "1", all: "1" }));
130
+ expect(plan.length).toBe(0);
131
+ });
132
+ });
133
+
134
+ describe("app config wizard", () => {
135
+ test("appConfigHasEntries when entries exist", () => {
136
+ expect(
137
+ appConfigHasEntries({
138
+ ...fixture,
139
+ appConfig: { entries: { token: { description: "API token." } } },
140
+ }),
141
+ ).toBe(true);
142
+ });
143
+
144
+ test("appConfigHasEntries false for empty entries or missing appConfig", () => {
145
+ expect(appConfigHasEntries({ ...fixture, appConfig: { entries: {} } })).toBe(false);
146
+ expect(appConfigHasEntries(fixture)).toBe(false);
147
+ });
148
+ });
@@ -0,0 +1,284 @@
1
+ import { resolveCapabilities } from "../capabilities.ts";
2
+ import { displayAppConfigPath, runConfigure } from "../config/bootstrap.ts";
3
+ import { resolveInstallPaths } from "../install/paths.ts";
4
+ import { buildInstallPlan, buildUpdatePlan } from "../install/plan.ts";
5
+ import {
6
+ installErr,
7
+ installInfo,
8
+ installOut,
9
+ printInstallStatus,
10
+ writeInteractiveInstallIntro,
11
+ } from "../install/status.ts";
12
+ import { resolveEffectiveInstallTargets } from "../install/target-effective.ts";
13
+ import { runTargetPreflight } from "../install/target-plan-build.ts";
14
+ import { INSTALL_TARGETS, installTargetForKey } from "../install/target-registry.ts";
15
+ import { buildDetectedSnapshot, buildTargetPlanContext } from "../install/target-scope.ts";
16
+ import type {
17
+ CliInstallArtifactKey,
18
+ InstallAction,
19
+ InstallActionKind,
20
+ InstallOpts,
21
+ TargetPlanContext,
22
+ UninstallAction,
23
+ } from "../install/target-types.ts";
24
+ import {
25
+ buildUninstallPlan,
26
+ skillDirFromUninstallSummary,
27
+ uninstallSkillDir,
28
+ } from "../install/uninstall.ts";
29
+ import { cliSkillInstall, skillTargetFromActionKind } from "../skill/install.ts";
30
+ import type { CliProgram } from "../types.ts";
31
+ import { artifactPromptLabel, promptTargetAction } from "./prompt.ts";
32
+
33
+ /** True when interactive `configure` should auto-run the app config wizard. */
34
+ export function appConfigHasEntries(program: CliProgram): boolean {
35
+ const entries = program.appConfig?.entries;
36
+ return !!entries && Object.keys(entries).length > 0;
37
+ }
38
+
39
+ export interface ConfigureOpts {
40
+ sync?: boolean;
41
+ removeAll?: boolean;
42
+ removeConfig?: boolean;
43
+ status?: boolean;
44
+ yes?: boolean;
45
+ dry?: boolean;
46
+ json?: boolean;
47
+ }
48
+
49
+ export function parseConfigureOpts(raw: Record<string, string>): ConfigureOpts {
50
+ const flag = (name: string) => raw[name] === "1";
51
+ return {
52
+ sync: flag("sync"),
53
+ removeAll: flag("remove-all"),
54
+ removeConfig: flag("remove-config"),
55
+ status: flag("status"),
56
+ yes: flag("yes"),
57
+ dry: flag("dry"),
58
+ json: flag("json"),
59
+ };
60
+ }
61
+
62
+ export function validateConfigureOpts(opts: ConfigureOpts): string | null {
63
+ const flags = [opts.sync, opts.removeAll, opts.removeConfig, opts.status].filter(Boolean);
64
+ if (flags.length > 1) {
65
+ return "Use only one of --sync, --remove-all, --remove-config, or --status.";
66
+ }
67
+ if (opts.json) {
68
+ opts.yes = true;
69
+ }
70
+ if ((opts.sync || opts.removeAll || opts.removeConfig) && !opts.yes) {
71
+ return "--yes is required with --sync, --remove-all, or --remove-config.";
72
+ }
73
+ return null;
74
+ }
75
+
76
+ function configureToInstallOpts(opts: ConfigureOpts): InstallOpts {
77
+ if (opts.status) {
78
+ return { status: true, yes: opts.yes, dry: opts.dry, json: opts.json };
79
+ }
80
+ if (opts.sync) {
81
+ return { reinstall: true, yes: true, dry: opts.dry, json: opts.json };
82
+ }
83
+ if (opts.removeAll) {
84
+ return { uninstall: true, all: true, yes: true, dry: opts.dry, json: opts.json };
85
+ }
86
+ if (opts.removeConfig) {
87
+ return { uninstall: true, configure: true, yes: true, dry: opts.dry, json: opts.json };
88
+ }
89
+ return { dry: opts.dry, json: opts.json };
90
+ }
91
+
92
+ function runSkillAction(root: CliProgram, kind: InstallActionKind, opts: InstallOpts): string[] {
93
+ const target = skillTargetFromActionKind(kind);
94
+ if (!target) return [];
95
+ return cliSkillInstall(root, target, {
96
+ global: true,
97
+ rimraf: true,
98
+ dry: opts.dry,
99
+ });
100
+ }
101
+
102
+ function executePlan(
103
+ root: CliProgram,
104
+ actions: Array<InstallAction | UninstallAction>,
105
+ opts: InstallOpts,
106
+ showProgress: boolean,
107
+ ): string[] {
108
+ const changed: string[] = [];
109
+ const paths = resolveInstallPaths(root);
110
+ for (const action of actions) {
111
+ if (showProgress) {
112
+ installInfo(action.message, opts);
113
+ }
114
+ if ("kind" in action && action.kind) {
115
+ const skillTarget = skillTargetFromActionKind(action.kind);
116
+ if (skillTarget) {
117
+ changed.push(...runSkillAction(root, action.kind as InstallActionKind, opts));
118
+ continue;
119
+ }
120
+ }
121
+ if (!("kind" in action) || !action.kind) {
122
+ const skillDir = skillDirFromUninstallSummary(action.summary, paths);
123
+ if (skillDir) {
124
+ changed.push(...uninstallSkillDir(skillDir, !!opts.dry));
125
+ continue;
126
+ }
127
+ }
128
+ changed.push(...action.run());
129
+ }
130
+ return changed;
131
+ }
132
+
133
+ function buildSingleTargetContext(
134
+ root: CliProgram,
135
+ paths: ReturnType<typeof resolveInstallPaths>,
136
+ detected: ReturnType<typeof buildDetectedSnapshot>,
137
+ key: CliInstallArtifactKey,
138
+ mode: "install" | "uninstall",
139
+ opts: InstallOpts,
140
+ ): TargetPlanContext {
141
+ const effective = resolveEffectiveInstallTargets(root.configure, root);
142
+ const base = buildTargetPlanContext(root, paths, opts, detected);
143
+ return {
144
+ ...base,
145
+ mode: mode === "install" ? "install-scoped" : "uninstall-scoped",
146
+ include: (k) => k === key && effective[k].enabled,
147
+ };
148
+ }
149
+
150
+ function actionsForTarget(
151
+ root: CliProgram,
152
+ paths: ReturnType<typeof resolveInstallPaths>,
153
+ key: CliInstallArtifactKey,
154
+ action: "install" | "uninstall",
155
+ opts: InstallOpts,
156
+ ): Array<InstallAction | UninstallAction> {
157
+ const detected = buildDetectedSnapshot(root, paths);
158
+ const target = installTargetForKey(key);
159
+ if (!target) return [];
160
+ const ctx = buildSingleTargetContext(root, paths, detected, key, action, opts);
161
+ if (action === "install") {
162
+ return target.planInstall(ctx);
163
+ }
164
+ return target.planUninstall(ctx);
165
+ }
166
+
167
+ async function runInteractiveConfigure(root: CliProgram, opts: ConfigureOpts): Promise<string[]> {
168
+ if (!process.stdin.isTTY) {
169
+ throw new Error("Interactive configure requires a TTY. Use flags such as --sync --yes.");
170
+ }
171
+
172
+ writeInteractiveInstallIntro(root);
173
+ const paths = resolveInstallPaths(root);
174
+ const detected = buildDetectedSnapshot(root, paths);
175
+ const effective = resolveEffectiveInstallTargets(root.configure, root);
176
+ const mutationOpts: InstallOpts = { dry: opts.dry, json: opts.json };
177
+ const changed: string[] = [];
178
+
179
+ for (const target of INSTALL_TARGETS) {
180
+ if (target.key === "app") continue;
181
+ if (!effective[target.key].enabled) continue;
182
+ if (target.key !== "configure" && !target.isAvailable(root, paths)) continue;
183
+ if (target.key === "configure" && !root.appConfig) continue;
184
+
185
+ if (target.key === "configure") {
186
+ if (!appConfigHasEntries(root)) continue;
187
+
188
+ const result = runConfigure(root, { context: "standalone", showHeading: false });
189
+ if (result.changed) {
190
+ installOut(`Wrote config: ${displayAppConfigPath(root)}`, mutationOpts);
191
+ changed.push(displayAppConfigPath(root));
192
+ }
193
+ continue;
194
+ }
195
+
196
+ const label = artifactPromptLabel(target.key);
197
+ const installed = target.detectedForSnapshot(detected);
198
+
199
+ const statusHint = installed ? "installed" : "not installed";
200
+ process.stderr.write(`\n${label} (${statusHint})\n`);
201
+ const choice = promptTargetAction(label, installed);
202
+ if (!choice || choice === "skip") continue;
203
+
204
+ const actions = actionsForTarget(root, paths, target.key, choice, mutationOpts);
205
+ if (actions.length === 0) continue;
206
+
207
+ const installActions = actions.filter(
208
+ (a): a is InstallAction =>
209
+ "kind" in a && typeof a.kind === "string" && a.kind.includes("-mcp"),
210
+ );
211
+ if (choice === "install" && resolveCapabilities(root).mcp && installActions.length > 0) {
212
+ runTargetPreflight(root, paths, mutationOpts, installActions);
213
+ }
214
+
215
+ changed.push(...executePlan(root, actions, mutationOpts, true));
216
+ }
217
+
218
+ return changed;
219
+ }
220
+
221
+ async function runAutomatedConfigure(root: CliProgram, opts: ConfigureOpts): Promise<string[]> {
222
+ const installOpts = configureToInstallOpts(opts);
223
+ const paths = resolveInstallPaths(root);
224
+
225
+ if (installOpts.status) {
226
+ printInstallStatus(root, installOpts);
227
+ return [];
228
+ }
229
+
230
+ let actions: Array<InstallAction | UninstallAction>;
231
+ if (installOpts.uninstall) {
232
+ actions = buildUninstallPlan(root, paths, installOpts);
233
+ } else if (installOpts.reinstall) {
234
+ actions = buildUpdatePlan(root, paths, installOpts);
235
+ } else {
236
+ actions = buildInstallPlan(root, paths, installOpts);
237
+ }
238
+
239
+ const installActions = actions.filter(
240
+ (a): a is InstallAction => "kind" in a && typeof a.kind === "string" && a.kind.includes("-mcp"),
241
+ );
242
+ if (!installOpts.uninstall && resolveCapabilities(root).mcp && installActions.length > 0) {
243
+ runTargetPreflight(root, paths, installOpts, installActions);
244
+ }
245
+
246
+ return executePlan(root, actions, installOpts, true);
247
+ }
248
+
249
+ /** Main configure command orchestrator. */
250
+ export async function cliConfigure(
251
+ root: CliProgram,
252
+ rawOpts: Record<string, string>,
253
+ ): Promise<never> {
254
+ const opts = parseConfigureOpts(rawOpts);
255
+ const err = validateConfigureOpts(opts);
256
+ if (err) {
257
+ installErr(err);
258
+ process.exit(1);
259
+ }
260
+
261
+ const isInteractive = !opts.sync && !opts.removeAll && !opts.removeConfig && !opts.status;
262
+
263
+ let changed: string[] = [];
264
+ try {
265
+ changed = isInteractive
266
+ ? await runInteractiveConfigure(root, opts)
267
+ : await runAutomatedConfigure(root, opts);
268
+ } catch (mutationErr) {
269
+ installErr(mutationErr instanceof Error ? mutationErr.message : String(mutationErr));
270
+ process.exit(1);
271
+ }
272
+
273
+ if (opts.json && !opts.status) {
274
+ process.stdout.write(`${JSON.stringify(changed, null, 2)}\n`);
275
+ process.exit(0);
276
+ }
277
+
278
+ if (!opts.status && changed.length > 0) {
279
+ const verb = opts.removeAll || opts.removeConfig ? "Removed" : opts.sync ? "Synced" : "Updated";
280
+ installOut(`${verb} ${changed.length} file(s).`, configureToInstallOpts(opts));
281
+ }
282
+
283
+ process.exit(0);
284
+ }
@@ -0,0 +1,40 @@
1
+ import { readSync } from "node:fs";
2
+ import type { CliInstallArtifactKey } from "../install/target-types.ts";
3
+
4
+ const LABELS: Record<CliInstallArtifactKey, string> = {
5
+ app: "App binary",
6
+ cursorSkill: "Cursor skill",
7
+ claudeSkill: "Claude skill",
8
+ codexSkill: "Codex skill",
9
+ opencodeSkill: "OpenCode skill",
10
+ openclawSkill: "OpenClaw skill",
11
+ cursorMcp: "Cursor MCP",
12
+ claudeCodeMcp: "Claude Code MCP",
13
+ claudeDesktopMcp: "Claude Desktop MCP",
14
+ opencodeMcp: "OpenCode MCP",
15
+ codexMcp: "Codex MCP",
16
+ openclawMcp: "OpenClaw MCP",
17
+ chatgptMcp: "ChatGPT desktop MCP",
18
+ configure: "App config",
19
+ };
20
+
21
+ export function artifactPromptLabel(key: CliInstallArtifactKey): string {
22
+ return LABELS[key];
23
+ }
24
+
25
+ export type TargetPromptAction = "install" | "skip" | "uninstall";
26
+
27
+ /** Prompt per target: Y/n when not installed, y/N when installed. */
28
+ export function promptTargetAction(label: string, installed: boolean): TargetPromptAction | null {
29
+ const hint = installed ? `${label} [y/N]: ` : `${label} [Y/n]: `;
30
+ process.stderr.write(hint);
31
+ const buf = Buffer.alloc(256);
32
+ const n = readSync(0, buf, { length: 256 });
33
+ const ans = buf.toString("utf8", 0, n).trim().toLowerCase();
34
+ if (installed) {
35
+ if (ans === "n") return "uninstall";
36
+ return "skip";
37
+ }
38
+ if (ans === "n") return "skip";
39
+ return "install";
40
+ }
@@ -71,21 +71,20 @@ test("generateApiGuide resolves program key in install notes", () => {
71
71
  };
72
72
  const md = generateApiGuide(fixture);
73
73
  expect(md).not.toContain("{argsbarg:program}");
74
- expect(md).toContain("myapp install --yes");
74
+ expect(md).toContain("brew install");
75
75
  expect(md).not.toContain("Upgrade to latest release");
76
76
  });
77
77
 
78
- test("generateApiGuide includes upgrade section when updateGetLatest is set", () => {
78
+ test("generateApiGuide mentions Homebrew upgrade", () => {
79
79
  const fixture: CliProgram = {
80
80
  key: "myapp",
81
81
  version: "1.0.0",
82
82
  description: "Demo app.",
83
- install: { updateGetLatest: async () => ({ path: process.execPath }) },
84
83
  commands: [{ key: "run", description: "Run.", handler: () => {} }],
85
84
  };
86
85
  const md = generateApiGuide(fixture);
87
- expect(md).toContain("Upgrade to latest release");
88
- expect(md).toContain("myapp install --update");
86
+ expect(md).toContain("brew upgrade");
87
+ expect(md).not.toContain("install --update");
89
88
  });
90
89
 
91
90
  test("generateApiGuide resolves {argsbarg:program} in consumer notes", () => {
@@ -1,3 +1,5 @@
1
+ import { docsSkillTopicDescription } from "../builtins/configure-copy.ts";
2
+ import { resolveCapabilities } from "../capabilities.ts";
1
3
  import {
2
4
  CliFallbackMode,
3
5
  type CliLeaf,
@@ -75,11 +77,7 @@ export function cliBuiltinDocsGroup(program: CliProgram): CliRouter {
75
77
  leaves.push(
76
78
  docsLeaf(program, "schema", "Print the full command tree as JSON."),
77
79
  docsLeaf(program, "api", "Print the full command reference as markdown."),
78
- docsLeaf(
79
- program,
80
- "skill",
81
- "Print a reference agent SKILL, use `install --skill` for optimized.",
82
- ),
80
+ docsLeaf(program, "skill", docsSkillTopicDescription(program, resolveCapabilities(program))),
83
81
  );
84
82
 
85
83
  return {
@@ -106,7 +106,7 @@ test("docs mcp when MCP enabled", async () => {
106
106
  expect(result.stdout).toContain("MCP server (myapp)");
107
107
  expect(result.stdout).toContain("myapp mcp");
108
108
  expect(result.stdout).toContain("claude_desktop_config.json");
109
- expect(result.stdout).toContain("install --mcp --yes");
109
+ expect(result.stdout).toContain("configure --sync --yes");
110
110
  });
111
111
 
112
112
  test("docs rejects unknown subcommand", async () => {
@@ -176,14 +176,14 @@ test("docs skill prints Cursor SKILL.md", async () => {
176
176
  expect(result.stdout).not.toContain("mcp.json");
177
177
  });
178
178
 
179
- test("docs skill help recommends install --skill", async () => {
179
+ test("docs skill help recommends configure", async () => {
180
180
  const presentation = cliPresentationRoot(docsFixture());
181
181
  const docsNode = presentation.commands.find((c) => c.key === "docs");
182
182
  expect(docsNode && "commands" in docsNode).toBe(true);
183
183
  if (docsNode && "commands" in docsNode) {
184
184
  const skill = docsNode.commands.find((c) => c.key === "skill");
185
185
  expect(skill?.description).toContain("reference agent SKILL");
186
- expect(skill?.description).toContain("install --skill");
186
+ expect(skill?.description).toContain("configure");
187
187
  expect(skill?.notes).toBeUndefined();
188
188
  expect(docsNode.notes).toContain("--save");
189
189
  expect(docsNode.notes).not.toContain("install --skill");
@@ -210,14 +210,15 @@ test("completions offer docs subcommands", () => {
210
210
  expect(bash).toContain("skill) echo");
211
211
  });
212
212
 
213
- test("generateMcpGuide includes schema URI and install targets", () => {
213
+ test("generateMcpGuide includes schema URI and configure sync", () => {
214
214
  const guide = generateMcpGuide(docsFixture(true));
215
215
  expect(guide).toContain("myapp://schema");
216
216
  expect(guide).toContain("~/.cursor/mcp.json");
217
217
  expect(guide).toContain("claude_desktop_config.json");
218
218
  expect(guide).toContain("## Installation");
219
219
  expect(guide).toContain("## Running directly");
220
- expect(guide).toContain("install --app");
220
+ expect(guide).toContain("configure --sync");
221
+ expect(guide).toContain("brew install myapp");
221
222
  expect(guide).toContain("OpenAI Codex");
222
223
  expect(guide).toContain("ChatGPT");
223
224
  });
@@ -105,13 +105,13 @@ export function generateMcpGuide(root: CliProgram): string {
105
105
  "",
106
106
  "## Installation",
107
107
  "",
108
- "### `install --mcp`",
108
+ "### `configure`",
109
109
  "",
110
110
  ];
111
111
 
112
- if (caps.install) {
112
+ if (caps.configure) {
113
113
  lines.push(
114
- `Install the CLI first so \`${root.key}\` is on your PATH (e.g. \`${root.key} install --app --yes\` or \`install --all --yes\`). Host configs reference the app by name.`,
114
+ `Install the CLI first so \`${root.key}\` is on your PATH (e.g. \`brew install ${root.key}\`). Host configs reference the app by name.`,
115
115
  "",
116
116
  );
117
117
  } else {
@@ -123,7 +123,7 @@ export function generateMcpGuide(root: CliProgram): string {
123
123
 
124
124
  lines.push(
125
125
  "```bash",
126
- `${root.key} install --mcp --yes`,
126
+ `${root.key} configure --sync --yes`,
127
127
  "```",
128
128
  "",
129
129
  "Merges the server entry below into host config when each host is present:",
@@ -167,18 +167,17 @@ export function generateMcpGuide(root: CliProgram): string {
167
167
  "",
168
168
  );
169
169
 
170
- if (mcp.shellEnv) {
171
- lines.push("## Environment", "");
172
- lines.push(
173
- "- **`shellEnv`** — captures login-shell environment at MCP startup (PATH, toolchain shims, exports).",
174
- );
175
- lines.push("");
176
- }
170
+ lines.push(
171
+ "## Environment",
172
+ "",
173
+ "- **`shellEnv`** — on by default; captures login-shell environment at MCP startup (PATH, toolchain shims, exports). Opt out with `shellEnv: false`.",
174
+ "",
175
+ );
177
176
 
178
177
  if (root.appConfig?.entries && Object.keys(root.appConfig.entries).length > 0) {
179
178
  lines.push("## Configuration", "");
180
179
  lines.push(
181
- `Configure before first use in Cursor or Claude Desktop (MCP hosts are non-interactive): \`${root.key} install --configure\`.`,
180
+ `Configure before first use in Cursor or Claude Desktop (MCP hosts are non-interactive): \`${root.key} configure\`.`,
182
181
  "",
183
182
  `Default config file: \`${displayAppConfigPath(root)}\` (flat JSON keys).`,
184
183
  "",
package/src/index.ts CHANGED
@@ -9,6 +9,7 @@ module layout.
9
9
 
10
10
  export { Cli, type CliInvokeKind, type CliInvokeResult } from "./cli.ts";
11
11
  export { cliErrWithHelp } from "./cli-errors.ts";
12
+ export { displayAppConfigPath, resolveAppConfigPath } from "./config/file.ts";
12
13
  export type { CliLeafInputs } from "./context.ts";
13
14
  export { CliContext } from "./context.ts";
14
15
  export {
@@ -26,24 +27,18 @@ export {
26
27
  shouldRunHeadlessWithYes,
27
28
  wantsExplicitJson,
28
29
  } from "./headless.ts";
29
- export type { GhReleaseUpdateConfig, GhVersionCheckConfig } from "./install/gh-release-update.ts";
30
- export {
31
- createGhFetchLatest,
32
- createGhVersionCheck,
33
- ghReleaseUpdateGetLatest,
34
- isAlreadyCurrent,
35
- parseReleaseTag,
36
- } from "./install/gh-release-update.ts";
37
30
  export type { McpBundlePaths, PackMcpBundleOpts } from "./mcp/bundle.ts";
38
31
  export { defaultMcpBundlePaths, generateMcpManifest, packMcpBundle } from "./mcp/bundle.ts";
39
32
  export type {
40
33
  CliAppConfig,
41
34
  CliAppConfigEntry,
35
+ CliAppConfigResolveContext,
36
+ CliAppConfigResolveFn,
37
+ CliConfigureConfig,
38
+ CliConfigureTargets,
42
39
  CliDocsConfig,
43
40
  CliDocsTopic,
44
41
  CliHandler,
45
- CliInstallConfig,
46
- CliInstallTargets,
47
42
  CliInvocation,
48
43
  CliMcpBundleConfig,
49
44
  CliMcpResource,
@@ -52,8 +47,6 @@ export type {
52
47
  CliOption,
53
48
  CliPositional,
54
49
  CliProgram,
55
- CliUpdateArtifact,
56
- CliUpdateGetLatest,
57
50
  InstallAgentIntegration,
58
51
  InstallTargetSpec,
59
52
  ResolvedInstallTarget,