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.
- package/CHANGELOG.md +77 -1
- package/README.md +91 -85
- package/docs/README.md +6 -6
- package/docs/ai-skills.md +8 -5
- package/docs/bundled-docs.md +1 -1
- package/docs/cli-program.md +9 -7
- package/docs/config-schema.md +37 -13
- package/docs/developing.md +8 -8
- package/docs/distribution-homebrew.md +103 -0
- package/docs/install.md +143 -106
- package/docs/mcp.md +23 -12
- package/docs/output-schema.md +1 -1
- package/examples/full-example/Formula/.gitkeep +0 -0
- package/examples/full-example/README.md +98 -0
- package/examples/full-example/biome.json +22 -0
- package/examples/{consumer-app → full-example}/bun.lock +2 -0
- package/examples/full-example/justfile +134 -0
- package/examples/{consumer-app → full-example}/package.json +10 -3
- package/examples/{consumer-app → full-example}/schemas/generated/app-config.json +1 -1
- package/examples/{consumer-app → full-example}/schemas/generated/status.json +1 -1
- package/examples/full-example/scripts/create-identity.ts +11 -0
- package/examples/full-example/scripts/formula-shared.ts +73 -0
- package/examples/full-example/scripts/gen-dev-formula.ts +26 -0
- package/examples/full-example/scripts/print-identity.ts +27 -0
- package/examples/full-example/src/commands/echo/command.ts +21 -0
- package/examples/full-example/src/commands/status/command.test.ts +10 -0
- package/examples/full-example/src/commands/status/command.ts +36 -0
- package/examples/{consumer-app → full-example}/src/commands/status/types.ts +1 -1
- package/examples/full-example/src/index.ts +10 -0
- package/examples/full-example/src/program.ts +57 -0
- package/examples/{consumer-app → full-example}/src/types.ts +1 -1
- package/examples/mcp-test.ts +8 -24
- package/examples/nested.ts +1 -3
- package/index.d.ts +81 -65
- package/package.json +2 -2
- package/src/builtins/builtins.test.ts +37 -22
- package/src/builtins/completion-group.ts +17 -15
- package/src/builtins/config.test.ts +31 -25
- package/src/builtins/config.ts +4 -3
- package/src/builtins/dispatch.ts +25 -1
- package/src/builtins/install.ts +45 -82
- package/src/builtins/mcp.ts +1 -1
- package/src/builtins/registry.ts +2 -0
- package/src/builtins/uninstall.ts +80 -0
- package/src/capabilities.ts +5 -7
- package/src/cli-tool/cli-smoke.test.ts +19 -0
- package/src/cli-tool/create.test.ts +119 -0
- package/src/cli-tool/create.ts +380 -0
- package/{examples/consumer-app/capabilities.test.ts → src/cli-tool/full-example-capabilities.test.ts} +15 -14
- package/src/cli-tool/main.ts +8 -0
- package/src/cli-tool/post-create.ts +111 -0
- package/src/cli-tool/program.ts +82 -0
- package/src/cli-tool/prompt.ts +28 -0
- package/src/cli-tool/run-create.ts +149 -0
- package/src/config/bootstrap.ts +174 -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 +192 -1
- package/src/config/resolve.ts +92 -13
- package/src/config.integration.test.ts +17 -10
- package/src/docs/api-guide.test.ts +4 -5
- package/src/docs/docs.test.ts +2 -1
- package/src/docs/mcp-guide.ts +7 -8
- package/src/hidden-mcpb.test.ts +41 -1
- package/src/index.ts +7 -10
- package/src/install/binary-placement.test.ts +101 -0
- package/src/install/binary-placement.ts +47 -0
- package/src/install/detect-installed.ts +2 -97
- package/src/install/index.ts +239 -168
- package/src/install/install-validate.test.ts +61 -0
- package/src/install/install.test.ts +170 -90
- package/src/install/mcp-openclaw.test.ts +40 -0
- package/src/install/mcp-openclaw.ts +106 -0
- package/src/install/normalize-uninstall.ts +11 -0
- package/src/install/normalize.ts +20 -0
- package/src/install/paths.ts +18 -26
- package/src/install/plan.ts +40 -261
- package/src/install/shell.ts +0 -14
- package/src/install/status.test.ts +85 -0
- package/src/install/status.ts +22 -15
- package/src/install/target-base.ts +93 -0
- package/src/install/target-detect.ts +20 -0
- package/src/install/target-effective.ts +129 -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 +253 -0
- package/src/install/target-skill.ts +104 -0
- package/src/install/target-types.ts +129 -0
- package/src/install/targets/app.ts +60 -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/configure.ts +63 -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 +50 -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 +118 -0
- package/src/install/uninstall.ts +16 -152
- package/src/invoke.test.ts +7 -1
- package/src/mcp/bundle.ts +16 -4
- package/src/mcp/claude.test.ts +14 -1
- package/src/mcp/claude.ts +11 -4
- package/src/mcp/env.test.ts +92 -0
- package/src/mcp/env.ts +15 -14
- package/src/mcp/zip.test.ts +17 -0
- package/src/mcp/zip.ts +62 -9
- package/src/mcp.integration.test.ts +1 -1
- package/src/parse.test.ts +14 -2
- package/src/paths/host.ts +11 -11
- package/src/paths/remove-empty-dir.ts +13 -0
- package/src/prompt.ts +10 -0
- package/src/schema.ts +9 -1
- package/src/skill/generate.ts +18 -4
- package/src/skill/install.ts +33 -6
- package/src/skill/naming.ts +28 -0
- package/src/types.ts +86 -7
- package/src/validate.ts +73 -9
- package/docs/templates/cursor/rules/cli-program.mdc +0 -31
- package/examples/config-app/main.ts +0 -20
- package/examples/config-app/program.ts +0 -81
- package/examples/config-app/schema.ts +0 -37
- package/examples/config-app/types.ts +0 -19
- package/examples/consumer-app/README.md +0 -57
- package/examples/consumer-app/src/main.ts +0 -15
- package/examples/consumer-app/src/program.ts +0 -108
- package/src/install/binary.ts +0 -94
- package/src/install/completions.ts +0 -56
- package/src/install/update.test.ts +0 -108
- package/src/install/update.ts +0 -57
- /package/examples/{consumer-app → full-example}/schemas/configSchemas.ts +0 -0
- /package/examples/{consumer-app → full-example}/schemas/outputSchemas.ts +0 -0
- /package/examples/{consumer-app → full-example}/scripts/schemagen/discover-schema-roots.test.ts +0 -0
- /package/examples/{consumer-app → full-example}/scripts/schemagen/discover-schema-roots.ts +0 -0
- /package/examples/{consumer-app → full-example}/scripts/schemagen/naming.ts +0 -0
- /package/examples/{consumer-app → full-example}/scripts/schemagen.ts +0 -0
- /package/examples/{consumer-app → full-example}/tsconfig.json +0 -0
package/src/install/index.ts
CHANGED
|
@@ -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 {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
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 {
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
57
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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, --
|
|
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
|
|
131
|
-
|
|
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
|
-
|
|
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
|
-
|
|
140
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
changed.push(...runSkillAction(root, action.kind, opts));
|
|
161
|
-
continue;
|
|
167
|
+
if (showProgress) {
|
|
168
|
+
installInfo(action.message, opts);
|
|
162
169
|
}
|
|
163
|
-
if (
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
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)
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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 (!
|
|
270
|
+
if (!autoYes) {
|
|
244
271
|
installOut(
|
|
245
272
|
`About to ${opts.uninstall ? "remove" : opts.reinstall ? "reinstall" : "install"}:`,
|
|
246
273
|
opts,
|
|
247
274
|
);
|
|
248
|
-
for (
|
|
249
|
-
installOut(`
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
310
|
+
const configureOnly = opts.configure && !opts.all && !opts.skill && !opts.mcp;
|
|
311
|
+
|
|
312
|
+
if (configureOnly) {
|
|
275
313
|
if (!root.appConfig) {
|
|
276
|
-
installErr("
|
|
314
|
+
installErr("This app does not support --configure.");
|
|
277
315
|
process.exit(1);
|
|
278
316
|
}
|
|
279
|
-
|
|
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,
|
|
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
|
|
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 (
|
|
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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
+
});
|