argsbarg 4.0.4 → 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 (147) hide show
  1. package/CHANGELOG.md +77 -1
  2. package/README.md +91 -85
  3. package/docs/README.md +6 -6
  4. package/docs/ai-skills.md +8 -5
  5. package/docs/bundled-docs.md +1 -1
  6. package/docs/cli-program.md +9 -7
  7. package/docs/config-schema.md +37 -13
  8. package/docs/developing.md +8 -8
  9. package/docs/distribution-homebrew.md +103 -0
  10. package/docs/install.md +143 -106
  11. package/docs/mcp.md +23 -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/mcp-test.ts +8 -24
  33. package/examples/nested.ts +1 -3
  34. package/index.d.ts +81 -65
  35. package/package.json +2 -2
  36. package/src/builtins/builtins.test.ts +37 -22
  37. package/src/builtins/completion-group.ts +17 -15
  38. package/src/builtins/config.test.ts +31 -25
  39. package/src/builtins/config.ts +4 -3
  40. package/src/builtins/dispatch.ts +25 -1
  41. package/src/builtins/install.ts +45 -82
  42. package/src/builtins/mcp.ts +1 -1
  43. package/src/builtins/registry.ts +2 -0
  44. package/src/builtins/uninstall.ts +80 -0
  45. package/src/capabilities.ts +5 -7
  46. package/src/cli-tool/cli-smoke.test.ts +19 -0
  47. package/src/cli-tool/create.test.ts +119 -0
  48. package/src/cli-tool/create.ts +380 -0
  49. package/{examples/consumer-app/capabilities.test.ts → src/cli-tool/full-example-capabilities.test.ts} +15 -14
  50. package/src/cli-tool/main.ts +8 -0
  51. package/src/cli-tool/post-create.ts +111 -0
  52. package/src/cli-tool/program.ts +82 -0
  53. package/src/cli-tool/prompt.ts +28 -0
  54. package/src/cli-tool/run-create.ts +149 -0
  55. package/src/config/bootstrap.ts +174 -66
  56. package/src/config/context.test.ts +22 -36
  57. package/src/config/context.ts +5 -4
  58. package/src/config/file.test.ts +66 -56
  59. package/src/config/file.ts +33 -25
  60. package/src/config/resolve.test.ts +192 -1
  61. package/src/config/resolve.ts +92 -13
  62. package/src/config.integration.test.ts +17 -10
  63. package/src/docs/api-guide.test.ts +4 -5
  64. package/src/docs/docs.test.ts +2 -1
  65. package/src/docs/mcp-guide.ts +7 -8
  66. package/src/hidden-mcpb.test.ts +41 -1
  67. package/src/index.ts +7 -10
  68. package/src/install/binary-placement.test.ts +101 -0
  69. package/src/install/binary-placement.ts +47 -0
  70. package/src/install/detect-installed.ts +2 -97
  71. package/src/install/index.ts +239 -168
  72. package/src/install/install-validate.test.ts +61 -0
  73. package/src/install/install.test.ts +170 -90
  74. package/src/install/mcp-openclaw.test.ts +40 -0
  75. package/src/install/mcp-openclaw.ts +106 -0
  76. package/src/install/normalize-uninstall.ts +11 -0
  77. package/src/install/normalize.ts +20 -0
  78. package/src/install/paths.ts +18 -26
  79. package/src/install/plan.ts +40 -261
  80. package/src/install/shell.ts +0 -14
  81. package/src/install/status.test.ts +85 -0
  82. package/src/install/status.ts +22 -15
  83. package/src/install/target-base.ts +93 -0
  84. package/src/install/target-detect.ts +20 -0
  85. package/src/install/target-effective.ts +129 -0
  86. package/src/install/target-mcp-cli.ts +149 -0
  87. package/src/install/target-mcp-json.ts +130 -0
  88. package/src/install/target-plan-build.ts +67 -0
  89. package/src/install/target-registry.ts +57 -0
  90. package/src/install/target-scope.ts +253 -0
  91. package/src/install/target-skill.ts +104 -0
  92. package/src/install/target-types.ts +129 -0
  93. package/src/install/targets/app.ts +60 -0
  94. package/src/install/targets/chatgpt-mcp.ts +12 -0
  95. package/src/install/targets/claude-code-mcp.ts +15 -0
  96. package/src/install/targets/claude-desktop-mcp.ts +12 -0
  97. package/src/install/targets/claude-skill.ts +16 -0
  98. package/src/install/targets/codex-mcp.ts +25 -0
  99. package/src/install/targets/codex-skill.ts +14 -0
  100. package/src/install/targets/configure.ts +63 -0
  101. package/src/install/targets/cursor-mcp.ts +15 -0
  102. package/src/install/targets/cursor-skill.ts +16 -0
  103. package/src/install/targets/index.ts +50 -0
  104. package/src/install/targets/openclaw-mcp.ts +25 -0
  105. package/src/install/targets/openclaw-skill.ts +17 -0
  106. package/src/install/targets/opencode-mcp.ts +101 -0
  107. package/src/install/targets/opencode-skill.ts +15 -0
  108. package/src/install/targets.test.ts +118 -0
  109. package/src/install/uninstall.ts +16 -152
  110. package/src/invoke.test.ts +7 -1
  111. package/src/mcp/bundle.ts +16 -4
  112. package/src/mcp/claude.test.ts +14 -1
  113. package/src/mcp/claude.ts +11 -4
  114. package/src/mcp/env.test.ts +92 -0
  115. package/src/mcp/env.ts +15 -14
  116. package/src/mcp/zip.test.ts +17 -0
  117. package/src/mcp/zip.ts +62 -9
  118. package/src/mcp.integration.test.ts +1 -1
  119. package/src/parse.test.ts +14 -2
  120. package/src/paths/host.ts +11 -11
  121. package/src/paths/remove-empty-dir.ts +13 -0
  122. package/src/prompt.ts +10 -0
  123. package/src/schema.ts +9 -1
  124. package/src/skill/generate.ts +18 -4
  125. package/src/skill/install.ts +33 -6
  126. package/src/skill/naming.ts +28 -0
  127. package/src/types.ts +86 -7
  128. package/src/validate.ts +73 -9
  129. package/docs/templates/cursor/rules/cli-program.mdc +0 -31
  130. package/examples/config-app/main.ts +0 -20
  131. package/examples/config-app/program.ts +0 -81
  132. package/examples/config-app/schema.ts +0 -37
  133. package/examples/config-app/types.ts +0 -19
  134. package/examples/consumer-app/README.md +0 -57
  135. package/examples/consumer-app/src/main.ts +0 -15
  136. package/examples/consumer-app/src/program.ts +0 -108
  137. package/src/install/binary.ts +0 -94
  138. package/src/install/completions.ts +0 -56
  139. package/src/install/update.test.ts +0 -108
  140. package/src/install/update.ts +0 -57
  141. /package/examples/{consumer-app → full-example}/schemas/configSchemas.ts +0 -0
  142. /package/examples/{consumer-app → full-example}/schemas/outputSchemas.ts +0 -0
  143. /package/examples/{consumer-app → full-example}/scripts/schemagen/discover-schema-roots.test.ts +0 -0
  144. /package/examples/{consumer-app → full-example}/scripts/schemagen/discover-schema-roots.ts +0 -0
  145. /package/examples/{consumer-app → full-example}/scripts/schemagen/naming.ts +0 -0
  146. /package/examples/{consumer-app → full-example}/scripts/schemagen.ts +0 -0
  147. /package/examples/{consumer-app → full-example}/tsconfig.json +0 -0
@@ -1,146 +1,153 @@
1
1
  import { readSync } from "node:fs";
2
2
  import { resolveCapabilities } from "../capabilities.ts";
3
3
  import { displayAppConfigPath, runInstallConfigure } from "../config/bootstrap.ts";
4
- import { cliSkillInstall } from "../skill/install.ts";
4
+ import { cliSkillInstall, skillTargetFromActionKind } from "../skill/install.ts";
5
5
  import type { CliProgram } from "../types.ts";
6
- import { checkCodexMcpConflict } from "./mcp-codex.ts";
7
- import { checkMcpConflict, expectedMcpEntry } from "./mcp-config.ts";
8
- import { checkOpenCodeMcpConflict, expectedOpenCodeMcpEntry } from "./mcp-opencode.ts";
9
- import { resolveInstallPaths, userHome } from "./paths.ts";
6
+ import { normalizeInstallRawOpts } from "./normalize.ts";
7
+ import { normalizeUninstallRawOpts } from "./normalize-uninstall.ts";
8
+ import { resolveInstallPaths } from "./paths.ts";
10
9
  import { buildInstallPlan, buildUpdatePlan, type InstallAction, type InstallOpts } from "./plan.ts";
11
- import { installErr, installInfo, installOut, printInstallStatus } from "./status.ts";
12
- import { buildUninstallPlan, type UninstallAction, uninstallSkillDir } from "./uninstall.ts";
13
- import { cliUpdate } from "./update.ts";
10
+ import {
11
+ installErr,
12
+ installInfo,
13
+ installOut,
14
+ printInstallStatus,
15
+ writeInteractiveInstallIntro,
16
+ } from "./status.ts";
17
+ import { runTargetPreflight } from "./target-plan-build.ts";
18
+ import type { InstallActionKind } from "./target-types.ts";
19
+ import {
20
+ buildUninstallPlan,
21
+ skillDirFromUninstallSummary,
22
+ type UninstallAction,
23
+ uninstallSkillDir,
24
+ } from "./uninstall.ts";
14
25
 
15
26
  export function parseInstallOpts(raw: Record<string, string>): InstallOpts {
16
27
  const flag = (name: string) => raw[name] === "1";
17
28
  return {
18
29
  all: flag("all"),
19
- bin: flag("bin"),
20
- completions: flag("completions"),
21
30
  skill: flag("skill"),
22
31
  mcp: flag("mcp"),
23
32
  reinstall: flag("reinstall"),
24
- update: flag("update"),
25
- from: raw.from,
26
33
  status: flag("status"),
27
34
  uninstall: flag("uninstall"),
28
35
  configure: flag("configure"),
29
- config: flag("config"),
30
36
  yes: flag("yes"),
31
37
  dry: flag("dry"),
32
38
  json: flag("json"),
33
- quiet: flag("quiet"),
34
- prefix: raw.prefix,
35
39
  };
36
40
  }
37
41
 
38
42
  export function validateInstallOpts(opts: InstallOpts): string | null {
39
- if (opts.configure) {
40
- if (
41
- opts.all ||
42
- opts.bin ||
43
- opts.completions ||
44
- opts.skill ||
45
- opts.mcp ||
46
- opts.config ||
47
- opts.reinstall ||
48
- opts.update ||
49
- opts.uninstall ||
50
- opts.status
51
- ) {
52
- return "--configure cannot be combined with other install flags.";
53
- }
43
+ const configureOnlyInstall =
44
+ opts.configure && !opts.all && !opts.skill && !opts.mcp && !opts.reinstall && !opts.status;
45
+
46
+ if (configureOnlyInstall) {
54
47
  return null;
55
48
  }
56
- if (opts.quiet && opts.dry) {
57
- return "--quiet cannot be combined with --dry.";
58
- }
59
- if (opts.quiet && !opts.yes && !opts.json && !opts.reinstall && !opts.update) {
60
- return "--quiet requires --yes (or --json / --reinstall / --update).";
49
+
50
+ if (opts.uninstall) {
51
+ return "Use `uninstall` instead of `install --uninstall`.";
61
52
  }
53
+
62
54
  if (opts.json) {
63
55
  opts.yes = true;
64
56
  }
65
57
  if (opts.reinstall) {
66
- opts.bin = true;
67
- opts.yes = true;
68
- }
69
- if (opts.update) {
70
58
  opts.yes = true;
71
59
  }
72
60
 
73
- const mutationFlags =
74
- opts.all ||
75
- opts.bin ||
76
- opts.completions ||
77
- opts.skill ||
78
- opts.mcp ||
79
- opts.reinstall ||
80
- opts.update ||
81
- opts.uninstall;
61
+ const mutationFlags = opts.all || opts.skill || opts.mcp || opts.reinstall || opts.configure;
82
62
  if (opts.status && mutationFlags) {
83
- return "--status is mutually exclusive with install/reinstall/uninstall targets.";
84
- }
85
- if (
86
- opts.reinstall &&
87
- (opts.all ||
88
- opts.completions ||
89
- opts.skill ||
90
- opts.mcp ||
91
- opts.uninstall ||
92
- opts.status ||
93
- opts.update)
94
- ) {
95
- return "--reinstall cannot be combined with other target flags.";
63
+ return "--status is mutually exclusive with install/reinstall targets.";
96
64
  }
97
- if (
98
- opts.update &&
99
- (opts.all ||
100
- opts.bin ||
101
- opts.completions ||
102
- opts.skill ||
103
- opts.mcp ||
104
- opts.uninstall ||
105
- opts.status ||
106
- opts.reinstall)
107
- ) {
108
- return "--update cannot be combined with other target flags.";
109
- }
110
- if (opts.uninstall && (opts.reinstall || opts.update || opts.status)) {
111
- return "--uninstall cannot be combined with --reinstall, --update, or --status.";
112
- }
113
- if (opts.uninstall) {
114
- const hasUninstallTarget =
115
- opts.all || opts.bin || opts.completions || opts.skill || opts.mcp || opts.config;
116
- if (!hasUninstallTarget) {
117
- return "Specify at least one target: --all, --bin, --completions, --skill, --mcp, or --config.";
118
- }
65
+ if (opts.reinstall && (opts.all || opts.skill || opts.mcp || opts.status || opts.configure)) {
66
+ return "--reinstall cannot be combined with other target flags.";
119
67
  }
120
- if (!opts.status && !opts.reinstall && !opts.update && !opts.uninstall) {
121
- const hasTarget =
122
- opts.all || opts.bin || opts.completions || opts.skill || opts.mcp || opts.config;
68
+ if (!opts.status && !opts.reinstall) {
69
+ const hasTarget = opts.all || opts.skill || opts.mcp || opts.configure;
123
70
  if (!hasTarget) {
124
- return "Specify at least one target: --all, --bin, --completions, --skill, --mcp, or --config.";
71
+ return "Specify at least one target: --all, --skill, --mcp, or --configure.";
125
72
  }
126
73
  }
127
74
  return null;
128
75
  }
129
76
 
130
- function promptConfirm(): boolean {
131
- process.stderr.write("Continue? [y/N] ");
77
+ export function validateUninstallOpts(opts: InstallOpts): string | null {
78
+ if (opts.json) {
79
+ opts.yes = true;
80
+ }
81
+
82
+ if (opts.status || opts.reinstall) {
83
+ return "uninstall does not support --status or --reinstall.";
84
+ }
85
+
86
+ const hasTarget = opts.all || opts.skill || opts.mcp || opts.configure;
87
+ if (!hasTarget) {
88
+ return "Specify at least one target: --all, --skill, --mcp, or --configure.";
89
+ }
90
+ return null;
91
+ }
92
+
93
+ function parseSelectionIndices(input: string, max: number): number[] | null {
94
+ const trimmed = input.trim();
95
+ if (!trimmed) return null;
96
+ const parts = trimmed.split(/[\s,]+/).filter(Boolean);
97
+ const indices: number[] = [];
98
+ for (const part of parts) {
99
+ const n = Number(part);
100
+ if (!Number.isInteger(n) || n < 1 || n > max) {
101
+ return null;
102
+ }
103
+ if (!indices.includes(n)) {
104
+ indices.push(n);
105
+ }
106
+ }
107
+ return indices.sort((a, b) => a - b);
108
+ }
109
+
110
+ /** Interactive install: legacy helper (app no longer installed). */
111
+ export function interactiveSelectionAssumesApp(
112
+ _actions: Array<InstallAction | UninstallAction>,
113
+ _uninstall: boolean,
114
+ ): boolean {
115
+ return false;
116
+ }
117
+
118
+ export function mergeInteractiveSelection(
119
+ indices: number[],
120
+ count: number,
121
+ alwaysIncludeFirst: boolean,
122
+ ): number[] {
123
+ if (!alwaysIncludeFirst || count === 0 || indices.includes(1)) {
124
+ return indices;
125
+ }
126
+ return [1, ...indices].sort((a, b) => a - b);
127
+ }
128
+
129
+ function promptSelectiveConfirm(count: number, alwaysIncludeFirst: boolean): number[] | null {
130
+ const hint = alwaysIncludeFirst
131
+ ? "Proceed [y/N], or numbers to include (e.g. 2,3): "
132
+ : "Proceed [y/N], or numbers to include (e.g. 1,3): ";
133
+ process.stderr.write(hint);
132
134
  const buf = Buffer.alloc(256);
133
135
  const n = readSync(0, buf, { length: 256 });
134
136
  const ans = buf.toString("utf8", 0, n).trim();
135
- return ans === "y" || ans === "Y";
137
+ if (ans === "y" || ans === "Y") {
138
+ return Array.from({ length: count }, (_, i) => i + 1);
139
+ }
140
+ if (ans === "n" || ans === "N" || ans === "") {
141
+ return null;
142
+ }
143
+ const parsed = parseSelectionIndices(ans, count);
144
+ if (!parsed) return null;
145
+ return mergeInteractiveSelection(parsed, count, alwaysIncludeFirst);
136
146
  }
137
147
 
138
- function runSkillAction(
139
- root: CliProgram,
140
- kind: "cursor-skill" | "claude-skill",
141
- opts: InstallOpts,
142
- ): string[] {
143
- const target = kind === "cursor-skill" ? "cursor" : "claude";
148
+ function runSkillAction(root: CliProgram, kind: InstallActionKind, opts: InstallOpts): string[] {
149
+ const target = skillTargetFromActionKind(kind);
150
+ if (!target) return [];
144
151
  return cliSkillInstall(root, target, {
145
152
  global: true,
146
153
  rimraf: true,
@@ -152,30 +159,34 @@ function executePlan(
152
159
  root: CliProgram,
153
160
  actions: Array<InstallAction | UninstallAction>,
154
161
  opts: InstallOpts,
162
+ showProgress: boolean,
155
163
  ): string[] {
156
164
  const changed: string[] = [];
165
+ const paths = resolveInstallPaths(root);
157
166
  for (const action of actions) {
158
- installInfo(action.message, opts);
159
- if ("kind" in action && (action.kind === "cursor-skill" || action.kind === "claude-skill")) {
160
- changed.push(...runSkillAction(root, action.kind, opts));
161
- continue;
167
+ if (showProgress) {
168
+ installInfo(action.message, opts);
162
169
  }
163
- if (!("kind" in action) && action.summary.startsWith("cursor skill")) {
164
- const paths = resolveInstallPaths(root, opts);
165
- changed.push(...uninstallSkillDir(paths.cursorSkillDir, !!opts.dry));
166
- continue;
170
+ if ("kind" in action && action.kind) {
171
+ const skillTarget = skillTargetFromActionKind(action.kind);
172
+ if (skillTarget) {
173
+ changed.push(...runSkillAction(root, action.kind as InstallActionKind, opts));
174
+ continue;
175
+ }
167
176
  }
168
- if (!("kind" in action) && action.summary.startsWith("claude skill")) {
169
- const paths = resolveInstallPaths(root, opts);
170
- changed.push(...uninstallSkillDir(paths.claudeSkillDir, !!opts.dry));
171
- continue;
177
+ if (!("kind" in action) || !action.kind) {
178
+ const skillDir = skillDirFromUninstallSummary(action.summary, paths);
179
+ if (skillDir) {
180
+ changed.push(...uninstallSkillDir(skillDir, !!opts.dry));
181
+ continue;
182
+ }
172
183
  }
173
184
  changed.push(...action.run());
174
185
  }
175
186
  return changed;
176
187
  }
177
188
 
178
- /** Runs install/reinstall/uninstall mutations without exiting the process. */
189
+ /** Runs install/reinstall mutations without exiting the process. */
179
190
  export async function runInstallMutation(
180
191
  root: CliProgram,
181
192
  rawOpts: Record<string, string>,
@@ -184,49 +195,48 @@ export async function runInstallMutation(
184
195
  opts: InstallOpts;
185
196
  paths: ReturnType<typeof resolveInstallPaths>;
186
197
  }> {
187
- const opts = parseInstallOpts(rawOpts);
198
+ const normalized = normalizeInstallRawOpts(rawOpts);
199
+ const opts = parseInstallOpts(normalized);
188
200
  const err = validateInstallOpts(opts);
189
201
  if (err) {
190
202
  throw new Error(err);
191
203
  }
204
+ return runInstallMutationInternal(root, opts);
205
+ }
192
206
 
193
- const paths = resolveInstallPaths(root, opts);
207
+ /** Runs uninstall mutations without exiting the process. */
208
+ export async function runUninstallMutation(
209
+ root: CliProgram,
210
+ rawOpts: Record<string, string>,
211
+ ): Promise<{
212
+ changed: string[];
213
+ opts: InstallOpts;
214
+ paths: ReturnType<typeof resolveInstallPaths>;
215
+ }> {
216
+ const normalized = normalizeUninstallRawOpts(rawOpts);
217
+ const opts = parseInstallOpts(normalized);
218
+ const err = validateUninstallOpts(opts);
219
+ if (err) {
220
+ throw new Error(err);
221
+ }
222
+ return runInstallMutationInternal(root, opts);
223
+ }
224
+
225
+ async function runInstallMutationInternal(
226
+ root: CliProgram,
227
+ opts: InstallOpts,
228
+ ): Promise<{
229
+ changed: string[];
230
+ opts: InstallOpts;
231
+ paths: ReturnType<typeof resolveInstallPaths>;
232
+ }> {
233
+ const paths = resolveInstallPaths(root);
194
234
 
195
235
  if (opts.status) {
196
236
  printInstallStatus(root, opts);
197
237
  return { changed: [], opts, paths };
198
238
  }
199
239
 
200
- if (!opts.uninstall && resolveCapabilities(root).mcp && (opts.all || opts.mcp)) {
201
- const entry = expectedMcpEntry(root);
202
- const openCodeEntry = expectedOpenCodeMcpEntry(root);
203
- const yes = !!opts.yes;
204
- for (const p of [
205
- paths.cursorMcpPath,
206
- paths.claudeMcpPath,
207
- paths.claudeDesktopMcpPath,
208
- paths.chatGptMcpPath,
209
- ]) {
210
- const conflict = checkMcpConflict(p, paths.mcpName, entry, yes);
211
- if (conflict) {
212
- throw new Error(conflict);
213
- }
214
- }
215
- const openCodeConflict = checkOpenCodeMcpConflict(
216
- paths.opencodeMcpPath,
217
- paths.mcpName,
218
- openCodeEntry,
219
- yes,
220
- );
221
- if (openCodeConflict) {
222
- throw new Error(openCodeConflict);
223
- }
224
- const codexConflict = checkCodexMcpConflict(userHome(), paths.mcpName, entry, yes);
225
- if (codexConflict) {
226
- throw new Error(codexConflict);
227
- }
228
- }
229
-
230
240
  let actions: Array<InstallAction | UninstallAction>;
231
241
  if (opts.uninstall) {
232
242
  actions = buildUninstallPlan(root, paths, opts);
@@ -236,31 +246,51 @@ export async function runInstallMutation(
236
246
  actions = buildInstallPlan(root, paths, opts);
237
247
  }
238
248
 
249
+ const installActions = actions.filter(
250
+ (a): a is InstallAction => "kind" in a && typeof a.kind === "string" && a.kind.includes("-mcp"),
251
+ );
252
+ if (!opts.uninstall && resolveCapabilities(root).mcp && installActions.length > 0) {
253
+ runTargetPreflight(root, paths, opts, installActions);
254
+ }
255
+
256
+ let selectedActions = actions;
257
+ const autoYes = !!(opts.yes || opts.json || opts.reinstall || opts.dry);
258
+
259
+ if (!autoYes && !opts.json && process.stdin.isTTY) {
260
+ writeInteractiveInstallIntro(root);
261
+ }
262
+
239
263
  if (actions.length === 0) {
264
+ if (!autoYes && !opts.json && process.stdin.isTTY && !opts.configure) {
265
+ installOut("Nothing to install for the selected targets.", opts);
266
+ }
240
267
  return { changed: [], opts, paths };
241
268
  }
242
269
 
243
- if (!opts.quiet && !opts.json) {
270
+ if (!autoYes) {
244
271
  installOut(
245
272
  `About to ${opts.uninstall ? "remove" : opts.reinstall ? "reinstall" : "install"}:`,
246
273
  opts,
247
274
  );
248
- for (const a of actions) {
249
- installOut(` - ${a.summary}`, opts);
275
+ for (let i = 0; i < actions.length; i++) {
276
+ installOut(` ${i + 1}. ${actions[i]?.summary}`, opts);
250
277
  }
251
278
  }
252
279
 
253
- const autoYes = opts.yes || opts.json || opts.reinstall;
254
280
  if (!autoYes) {
255
281
  if (!process.stdin.isTTY) {
256
282
  throw new Error("Refusing to proceed without --yes (stdin is not a TTY).");
257
283
  }
258
- if (!promptConfirm()) {
284
+ const assumeApp = interactiveSelectionAssumesApp(actions, !!opts.uninstall);
285
+ const selection = promptSelectiveConfirm(actions.length, assumeApp);
286
+ if (!selection) {
259
287
  throw new Error("Aborted.");
260
288
  }
289
+ process.stderr.write("Done.\n");
290
+ selectedActions = selection.map((i) => actions[i - 1]).filter(Boolean) as typeof actions;
261
291
  }
262
292
 
263
- const changed = executePlan(root, actions, opts);
293
+ const changed = executePlan(root, selectedActions, opts, autoYes);
264
294
  return { changed, opts, paths };
265
295
  }
266
296
 
@@ -269,14 +299,25 @@ export async function cliInstall(
269
299
  root: CliProgram,
270
300
  rawOpts: Record<string, string>,
271
301
  ): Promise<never> {
272
- const opts = parseInstallOpts(rawOpts);
302
+ if (rawOpts.uninstall === "1") {
303
+ installErr(`Use \`${root.key} uninstall\` instead of \`${root.key} install --uninstall\`.`);
304
+ process.exit(1);
305
+ }
306
+
307
+ const normalized = normalizeInstallRawOpts(rawOpts);
308
+ const opts = parseInstallOpts(normalized);
273
309
 
274
- if (opts.configure) {
310
+ const configureOnly = opts.configure && !opts.all && !opts.skill && !opts.mcp;
311
+
312
+ if (configureOnly) {
275
313
  if (!root.appConfig) {
276
- installErr("install --configure requires program.appConfig on the program root.");
314
+ installErr("This app does not support --configure.");
277
315
  process.exit(1);
278
316
  }
279
- const result = runInstallConfigure(root);
317
+ if (process.stdin.isTTY) {
318
+ writeInteractiveInstallIntro(root);
319
+ }
320
+ const result = runInstallConfigure(root, { context: "standalone" });
280
321
  if (result.changed) {
281
322
  installOut(`Wrote config: ${displayAppConfigPath(root)}`, opts);
282
323
  } else {
@@ -291,23 +332,15 @@ export async function cliInstall(
291
332
  process.exit(1);
292
333
  }
293
334
 
294
- if (opts.update) {
295
- if (!resolveCapabilities(root).update) {
296
- installErr("install --update requires install.updateGetLatest on the program root.");
297
- process.exit(1);
298
- }
299
- await cliUpdate(root);
300
- }
301
-
302
335
  let result: Awaited<ReturnType<typeof runInstallMutation>>;
303
336
  try {
304
- result = await runInstallMutation(root, rawOpts);
337
+ result = await runInstallMutation(root, normalized);
305
338
  } catch (err) {
306
339
  installErr(err instanceof Error ? err.message : String(err));
307
340
  process.exit(1);
308
341
  }
309
342
 
310
- const { changed, opts: mutationOpts, paths } = result;
343
+ const { changed, opts: mutationOpts } = result;
311
344
 
312
345
  if (mutationOpts.status) {
313
346
  process.exit(0);
@@ -318,21 +351,59 @@ export async function cliInstall(
318
351
  process.exit(0);
319
352
  }
320
353
 
321
- if (!mutationOpts.quiet && changed.length > 0) {
354
+ if (changed.length > 0) {
322
355
  const verb = mutationOpts.uninstall
323
356
  ? "Removed"
324
357
  : mutationOpts.reinstall
325
358
  ? "Reinstalled"
326
359
  : "Installed";
327
360
  installOut(`${verb} ${changed.length} file(s).`, mutationOpts);
328
- if (
329
- !mutationOpts.uninstall &&
330
- (mutationOpts.all || mutationOpts.bin) &&
331
- changed.some((p) => p === paths.bashRc || p === paths.zshRc || p === paths.binaryPath)
332
- ) {
333
- installOut("Open a new shell, or run: hash -r (bash) / rehash (zsh)", mutationOpts);
361
+ }
362
+
363
+ if (mutationOpts.configure && root.appConfig) {
364
+ const configResult = runInstallConfigure(root, {
365
+ context: changed.length > 0 ? "after-install" : "standalone",
366
+ });
367
+ if (configResult.changed) {
368
+ installOut(`Wrote config: ${displayAppConfigPath(root)}`, mutationOpts);
334
369
  }
335
370
  }
336
371
 
337
372
  process.exit(0);
338
373
  }
374
+
375
+ /** Main uninstall command orchestrator. */
376
+ export async function cliUninstall(
377
+ root: CliProgram,
378
+ rawOpts: Record<string, string>,
379
+ ): Promise<never> {
380
+ const normalized = normalizeUninstallRawOpts(rawOpts);
381
+ const opts = parseInstallOpts(normalized);
382
+
383
+ const err = validateUninstallOpts(opts);
384
+ if (err) {
385
+ installErr(err);
386
+ process.exit(1);
387
+ }
388
+
389
+ let result: Awaited<ReturnType<typeof runUninstallMutation>>;
390
+ try {
391
+ result = await runUninstallMutation(root, rawOpts);
392
+ } catch (mutationErr) {
393
+ installErr(mutationErr instanceof Error ? mutationErr.message : String(mutationErr));
394
+ process.exit(1);
395
+ }
396
+
397
+ const { changed, opts: mutationOpts } = result;
398
+
399
+ if (mutationOpts.json) {
400
+ process.stdout.write(`${JSON.stringify(changed, null, 2)}\n`);
401
+ process.exit(0);
402
+ }
403
+
404
+ if (changed.length > 0) {
405
+ installOut(`Removed ${changed.length} file(s).`, mutationOpts);
406
+ }
407
+
408
+ process.exit(0);
409
+ }
@@ -0,0 +1,61 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { CliProgram } from "../types.ts";
3
+ import { CliSchemaValidationError } from "../types.ts";
4
+ import { cliValidateProgram } from "../validate.ts";
5
+
6
+ const base: CliProgram = {
7
+ key: "app",
8
+ version: "1.0.0",
9
+ description: "Test",
10
+ handler: () => {},
11
+ };
12
+
13
+ describe("validateInstallConfig", () => {
14
+ test("accepts empty install config", () => {
15
+ expect(() => cliValidateProgram(base)).not.toThrow();
16
+ });
17
+
18
+ test("rejects removed allSkills shorthand", () => {
19
+ const program = {
20
+ ...base,
21
+ install: { targets: { allSkills: true } },
22
+ } as CliProgram;
23
+ expect(() => cliValidateProgram(program)).toThrow(CliSchemaValidationError);
24
+ expect(() => cliValidateProgram(program)).toThrow(/allSkills/);
25
+ });
26
+
27
+ test("rejects both sides of pair without both integration", () => {
28
+ const program: CliProgram = {
29
+ ...base,
30
+ mcpServer: { enabled: true },
31
+ install: {
32
+ agentIntegration: "mcp",
33
+ targets: { cursorMcp: true, cursorSkill: true },
34
+ },
35
+ };
36
+ expect(() => cliValidateProgram(program)).toThrow(/both/);
37
+ });
38
+
39
+ test("rejects explicit MCP target in skill integration mode", () => {
40
+ const program: CliProgram = {
41
+ ...base,
42
+ install: {
43
+ agentIntegration: "skill",
44
+ targets: { cursorMcp: true },
45
+ },
46
+ };
47
+ expect(() => cliValidateProgram(program)).toThrow(/cursorMcp/);
48
+ });
49
+
50
+ test("allows explicit pair targets with both integration", () => {
51
+ const program: CliProgram = {
52
+ ...base,
53
+ mcpServer: { enabled: true },
54
+ install: {
55
+ agentIntegration: "both",
56
+ targets: { cursorMcp: true, cursorSkill: true },
57
+ },
58
+ };
59
+ expect(() => cliValidateProgram(program)).not.toThrow();
60
+ });
61
+ });