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