argsbarg 4.1.0 → 4.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +48 -1
- package/README.md +90 -84
- package/docs/README.md +6 -6
- package/docs/bundled-docs.md +1 -1
- package/docs/cli-program.md +5 -3
- package/docs/config-schema.md +36 -9
- package/docs/developing.md +7 -7
- package/docs/distribution-homebrew.md +103 -0
- package/docs/install.md +105 -189
- package/docs/mcp.md +2 -3
- 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/nested.ts +1 -3
- package/index.d.ts +27 -66
- package/package.json +2 -2
- package/src/builtins/builtins.test.ts +22 -23
- package/src/builtins/completion-group.ts +17 -15
- package/src/builtins/dispatch.ts +25 -1
- package/src/builtins/install.ts +22 -52
- package/src/builtins/registry.ts +2 -0
- package/src/builtins/uninstall.ts +80 -0
- package/src/capabilities.ts +1 -3
- 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/cli.ts +0 -2
- package/src/config/bootstrap.ts +16 -9
- package/src/config/resolve.test.ts +167 -0
- package/src/config/resolve.ts +50 -6
- 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/index.ts +3 -10
- package/src/install/binary-placement.test.ts +101 -0
- package/src/install/binary-placement.ts +47 -0
- package/src/install/index.ts +117 -123
- package/src/install/install.test.ts +89 -105
- package/src/install/normalize-uninstall.ts +11 -0
- package/src/install/normalize.ts +4 -19
- package/src/install/paths.ts +0 -22
- package/src/install/plan.ts +14 -6
- package/src/install/shell.ts +0 -14
- package/src/install/status.test.ts +6 -6
- package/src/install/status.ts +0 -6
- package/src/install/target-effective.ts +0 -2
- package/src/install/target-scope.ts +15 -28
- package/src/install/target-types.ts +0 -16
- package/src/install/targets/app.ts +19 -28
- package/src/install/targets/configure.ts +5 -1
- package/src/install/targets/index.ts +0 -3
- package/src/install/targets.test.ts +24 -42
- package/src/mcp/env.test.ts +92 -0
- package/src/mcp/env.ts +15 -14
- package/src/parse.test.ts +3 -2
- package/src/prompt.ts +10 -0
- package/src/schema.ts +9 -1
- package/src/types.ts +27 -9
- package/src/validate.ts +5 -11
- 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 -78
- package/examples/config-app/schema.ts +0 -37
- package/examples/config-app/types.ts +0 -19
- package/examples/consumer-app/README.md +0 -56
- package/examples/consumer-app/src/main.ts +0 -15
- package/examples/consumer-app/src/program.ts +0 -108
- package/src/install/app.ts +0 -94
- package/src/install/bootstrap.ts +0 -22
- package/src/install/completions.ts +0 -56
- package/src/install/targets/completions.ts +0 -133
- package/src/install/update.test.ts +0 -123
- package/src/install/update.ts +0 -54
- /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
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
-
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { chmodSync, existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { writeAppConfigFile } from "../config/file.ts";
|
|
6
6
|
import type { CliProgram } from "../types.ts";
|
|
7
7
|
import { detectInstalledArtifacts } from "./detect-installed.ts";
|
|
8
8
|
import {
|
|
9
|
+
cliInstall,
|
|
9
10
|
interactiveSelectionAssumesApp,
|
|
10
11
|
mergeInteractiveSelection,
|
|
11
12
|
parseInstallOpts,
|
|
12
13
|
runInstallMutation,
|
|
14
|
+
runUninstallMutation,
|
|
13
15
|
validateInstallOpts,
|
|
16
|
+
validateUninstallOpts,
|
|
14
17
|
} from "./index.ts";
|
|
15
18
|
import { opencodeConfigDir } from "./mcp-opencode.ts";
|
|
19
|
+
import { normalizeUninstallRawOpts } from "./normalize-uninstall.ts";
|
|
16
20
|
import { resolveClaudeDesktopMcpPath, resolveInstallPaths } from "./paths.ts";
|
|
17
|
-
import { buildInstallPlan } from "./plan.ts";
|
|
21
|
+
import { buildInstallPlan, buildUpdatePlan } from "./plan.ts";
|
|
18
22
|
import { printInstallStatus, writeInteractiveInstallIntro } from "./status.ts";
|
|
19
23
|
import { buildUninstallPlan } from "./uninstall.ts";
|
|
20
24
|
|
|
@@ -28,7 +32,6 @@ const fixture: CliProgram = {
|
|
|
28
32
|
|
|
29
33
|
let home: string;
|
|
30
34
|
let prevHome: string | undefined;
|
|
31
|
-
|
|
32
35
|
let prevXdg: string | undefined;
|
|
33
36
|
|
|
34
37
|
beforeEach(() => {
|
|
@@ -48,16 +51,10 @@ afterEach(() => {
|
|
|
48
51
|
});
|
|
49
52
|
|
|
50
53
|
describe("install paths", () => {
|
|
51
|
-
test("resolveInstallPaths
|
|
54
|
+
test("resolveInstallPaths includes skill and mcp paths", () => {
|
|
52
55
|
const paths = resolveInstallPaths(fixture);
|
|
53
|
-
expect(paths.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
test("fish completion uses XDG_CONFIG_HOME", () => {
|
|
57
|
-
const xdg = join(home, "xdg");
|
|
58
|
-
process.env.XDG_CONFIG_HOME = xdg;
|
|
59
|
-
const paths = resolveInstallPaths(fixture);
|
|
60
|
-
expect(paths.fishCompletion).toBe(join(xdg, "fish", "completions", "testapp.fish"));
|
|
56
|
+
expect(paths.cursorSkillDir).toContain(".cursor/skills");
|
|
57
|
+
expect(paths.cursorMcpPath).toContain("mcp.json");
|
|
61
58
|
});
|
|
62
59
|
|
|
63
60
|
test("claude desktop mcp path on darwin", () => {
|
|
@@ -74,38 +71,39 @@ describe("install paths", () => {
|
|
|
74
71
|
});
|
|
75
72
|
|
|
76
73
|
describe("detect installed", () => {
|
|
77
|
-
test("detects
|
|
74
|
+
test("detects cursor mcp when configured", () => {
|
|
78
75
|
const paths = resolveInstallPaths(fixture);
|
|
79
|
-
mkdirSync(
|
|
80
|
-
writeFileSync(
|
|
81
|
-
|
|
82
|
-
|
|
76
|
+
mkdirSync(join(home, ".cursor"), { recursive: true });
|
|
77
|
+
writeFileSync(
|
|
78
|
+
paths.cursorMcpPath,
|
|
79
|
+
JSON.stringify({ mcpServers: { testapp: { command: "testapp", args: ["mcp"] } } }),
|
|
80
|
+
"utf8",
|
|
81
|
+
);
|
|
83
82
|
|
|
84
83
|
const detected = detectInstalledArtifacts(paths, fixture);
|
|
85
|
-
expect(detected.
|
|
86
|
-
expect(detected.
|
|
87
|
-
expect(detected.zshCompletion).toBe(false);
|
|
84
|
+
expect(detected.cursorMcp).toBe(true);
|
|
85
|
+
expect(detected.app).toBe(false);
|
|
88
86
|
});
|
|
89
87
|
});
|
|
90
88
|
|
|
91
89
|
describe("install plan", () => {
|
|
92
|
-
test("buildInstallPlan --all
|
|
90
|
+
test("buildInstallPlan --all omits app self-install", () => {
|
|
93
91
|
const paths = resolveInstallPaths(fixture);
|
|
94
92
|
const plan = buildInstallPlan(fixture, paths, parseInstallOpts({ all: "1" }));
|
|
95
|
-
expect(plan.some((a) => a.kind === "app")).toBe(
|
|
93
|
+
expect(plan.some((a) => a.kind === "app")).toBe(false);
|
|
94
|
+
expect(plan.some((a) => a.kind.endsWith("-mcp"))).toBe(true);
|
|
96
95
|
});
|
|
97
96
|
|
|
98
|
-
test("buildInstallPlan --mcp
|
|
97
|
+
test("buildInstallPlan --mcp without yes omits implicit app", () => {
|
|
99
98
|
const paths = resolveInstallPaths(fixture);
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
const plan = buildInstallPlan(fixture, paths, opts);
|
|
103
|
-
expect(plan.some((a) => a.kind === "app")).toBe(true);
|
|
99
|
+
const plan = buildInstallPlan(fixture, paths, parseInstallOpts({ mcp: "1" }));
|
|
100
|
+
expect(plan.some((a) => a.kind === "app")).toBe(false);
|
|
104
101
|
});
|
|
105
102
|
|
|
106
|
-
test("
|
|
103
|
+
test("buildUpdatePlan greenfield falls back to install-all", () => {
|
|
107
104
|
const paths = resolveInstallPaths(fixture);
|
|
108
|
-
const plan =
|
|
105
|
+
const plan = buildUpdatePlan(fixture, paths, parseInstallOpts({ reinstall: "1", yes: "1" }));
|
|
106
|
+
expect(plan.length).toBeGreaterThan(0);
|
|
109
107
|
expect(plan.some((a) => a.kind === "app")).toBe(false);
|
|
110
108
|
});
|
|
111
109
|
|
|
@@ -130,12 +128,6 @@ describe("install plan", () => {
|
|
|
130
128
|
expect(plan.some((a) => a.kind === "claude-mcp")).toBe(true);
|
|
131
129
|
});
|
|
132
130
|
|
|
133
|
-
test("buildInstallPlan --mcp skips claude desktop without app data", () => {
|
|
134
|
-
const paths = resolveInstallPaths(fixture);
|
|
135
|
-
const plan = buildInstallPlan(fixture, paths, parseInstallOpts({ mcp: "1" }));
|
|
136
|
-
expect(plan.some((a) => a.kind === "claude-desktop-mcp")).toBe(false);
|
|
137
|
-
});
|
|
138
|
-
|
|
139
131
|
test("buildInstallPlan --mcp includes opencode when config dir exists", () => {
|
|
140
132
|
const paths = resolveInstallPaths(fixture);
|
|
141
133
|
mkdirSync(opencodeConfigDir(home), { recursive: true });
|
|
@@ -143,21 +135,37 @@ describe("install plan", () => {
|
|
|
143
135
|
expect(plan.some((a) => a.kind === "opencode-mcp")).toBe(true);
|
|
144
136
|
});
|
|
145
137
|
|
|
146
|
-
test("buildInstallPlan --mcp
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
138
|
+
test("buildInstallPlan --mcp --skill --configure is not empty when agents exist", () => {
|
|
139
|
+
const program: CliProgram = {
|
|
140
|
+
...fixture,
|
|
141
|
+
install: { agentIntegration: "both" },
|
|
142
|
+
appConfig: {
|
|
143
|
+
entries: { token: { description: "API token." } },
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
const paths = resolveInstallPaths(program);
|
|
147
|
+
mkdirSync(join(home, ".cursor"), { recursive: true });
|
|
148
|
+
mkdirSync(join(home, ".claude"), { recursive: true });
|
|
149
|
+
const plan = buildInstallPlan(
|
|
150
|
+
program,
|
|
151
|
+
paths,
|
|
152
|
+
parseInstallOpts({ mcp: "1", skill: "1", configure: "1" }),
|
|
153
|
+
);
|
|
154
|
+
expect(plan.length).toBeGreaterThan(0);
|
|
155
|
+
expect(plan.some((a) => a.kind.endsWith("-mcp"))).toBe(true);
|
|
156
|
+
expect(plan.some((a) => a.kind.endsWith("-skill"))).toBe(true);
|
|
153
157
|
});
|
|
154
158
|
});
|
|
155
159
|
|
|
156
160
|
describe("install status", () => {
|
|
157
|
-
test("printInstallStatus human output", () => {
|
|
161
|
+
test("printInstallStatus human output with MCP", () => {
|
|
158
162
|
const paths = resolveInstallPaths(fixture);
|
|
159
|
-
mkdirSync(
|
|
160
|
-
writeFileSync(
|
|
163
|
+
mkdirSync(join(home, ".cursor"), { recursive: true });
|
|
164
|
+
writeFileSync(
|
|
165
|
+
paths.cursorMcpPath,
|
|
166
|
+
JSON.stringify({ mcpServers: { testapp: { command: "testapp", args: ["mcp"] } } }),
|
|
167
|
+
"utf8",
|
|
168
|
+
);
|
|
161
169
|
|
|
162
170
|
const chunks: string[] = [];
|
|
163
171
|
const orig = process.stdout.write;
|
|
@@ -169,7 +177,7 @@ describe("install status", () => {
|
|
|
169
177
|
printInstallStatus(fixture, {});
|
|
170
178
|
const out = chunks.join("");
|
|
171
179
|
expect(out).toContain("Installed artifacts for testapp");
|
|
172
|
-
expect(out).toContain("
|
|
180
|
+
expect(out).toContain("cursor mcp");
|
|
173
181
|
} finally {
|
|
174
182
|
process.stdout.write = orig;
|
|
175
183
|
}
|
|
@@ -207,17 +215,9 @@ describe("interactive selection", () => {
|
|
|
207
215
|
expect(mergeInteractiveSelection([2, 3], 5, false)).toEqual([2, 3]);
|
|
208
216
|
});
|
|
209
217
|
|
|
210
|
-
test("interactiveSelectionAssumesApp
|
|
218
|
+
test("interactiveSelectionAssumesApp is always false", () => {
|
|
211
219
|
const paths = resolveInstallPaths(fixture);
|
|
212
220
|
const plan = buildInstallPlan(fixture, paths, parseInstallOpts({ all: "1" }));
|
|
213
|
-
expect(interactiveSelectionAssumesApp(plan, false)).toBe(true);
|
|
214
|
-
expect(interactiveSelectionAssumesApp(plan, true)).toBe(false);
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
test("interactiveSelectionAssumesApp false when app not in plan", () => {
|
|
218
|
-
const paths = resolveInstallPaths(fixture);
|
|
219
|
-
const plan = buildInstallPlan(fixture, paths, parseInstallOpts({ completions: "1" }));
|
|
220
|
-
expect(plan.some((a) => a.kind === "app")).toBe(false);
|
|
221
221
|
expect(interactiveSelectionAssumesApp(plan, false)).toBe(false);
|
|
222
222
|
});
|
|
223
223
|
});
|
|
@@ -239,34 +239,34 @@ describe("install output", () => {
|
|
|
239
239
|
});
|
|
240
240
|
});
|
|
241
241
|
|
|
242
|
-
describe("
|
|
243
|
-
test("
|
|
244
|
-
const opts = parseInstallOpts({
|
|
245
|
-
expect(opts
|
|
242
|
+
describe("validateUninstallOpts", () => {
|
|
243
|
+
test("bare uninstall normalizes to --all", () => {
|
|
244
|
+
const opts = parseInstallOpts(normalizeUninstallRawOpts({ yes: "1" }));
|
|
245
|
+
expect(validateUninstallOpts(opts)).toBeNull();
|
|
246
246
|
expect(opts.all).toBe(true);
|
|
247
247
|
});
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
describe("validateInstallOpts", () => {
|
|
251
|
-
test("uninstall requires a target flag", () => {
|
|
252
|
-
const opts = parseInstallOpts({ uninstall: "1", yes: "1" });
|
|
253
|
-
expect(validateInstallOpts(opts)).toContain("Specify at least one target");
|
|
254
|
-
});
|
|
255
248
|
|
|
256
249
|
test("uninstall allows --all", () => {
|
|
257
250
|
const opts = parseInstallOpts({ uninstall: "1", all: "1", yes: "1" });
|
|
258
|
-
expect(
|
|
251
|
+
expect(validateUninstallOpts(opts)).toBeNull();
|
|
259
252
|
});
|
|
260
253
|
|
|
261
254
|
test("uninstall rejects --reinstall", () => {
|
|
262
255
|
const opts = parseInstallOpts({ uninstall: "1", reinstall: "1", all: "1" });
|
|
263
|
-
expect(
|
|
256
|
+
expect(validateUninstallOpts(opts)).toContain("--reinstall");
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
describe("validateInstallOpts", () => {
|
|
261
|
+
test("install --uninstall is rejected", () => {
|
|
262
|
+
const opts = parseInstallOpts({ uninstall: "1", all: "1", yes: "1" });
|
|
263
|
+
expect(validateInstallOpts(opts)).toContain("uninstall");
|
|
264
264
|
});
|
|
265
265
|
});
|
|
266
266
|
|
|
267
267
|
describe("install mutation", () => {
|
|
268
268
|
test("uninstall --all with nothing installed succeeds", async () => {
|
|
269
|
-
const result = await
|
|
269
|
+
const result = await runUninstallMutation(fixture, { all: "1", yes: "1" });
|
|
270
270
|
expect(result.changed).toEqual([]);
|
|
271
271
|
});
|
|
272
272
|
|
|
@@ -277,29 +277,30 @@ describe("install mutation", () => {
|
|
|
277
277
|
).toBe(true);
|
|
278
278
|
});
|
|
279
279
|
|
|
280
|
-
test("
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
280
|
+
test("install --all --yes dry-run succeeds when stdin is not a TTY", async () => {
|
|
281
|
+
const program: CliProgram = {
|
|
282
|
+
...fixture,
|
|
283
|
+
appConfig: {
|
|
284
|
+
entries: { token: { description: "API token." } },
|
|
285
|
+
},
|
|
286
|
+
};
|
|
287
|
+
const origIsTTY = process.stdin.isTTY;
|
|
288
|
+
Object.defineProperty(process.stdin, "isTTY", { value: false, configurable: true });
|
|
289
|
+
const exitCode = await new Promise<number>((resolve) => {
|
|
290
|
+
const origExit = process.exit;
|
|
291
|
+
process.exit = ((code?: number) => {
|
|
292
|
+
process.exit = origExit;
|
|
293
|
+
resolve(code ?? 0);
|
|
294
|
+
}) as typeof process.exit;
|
|
295
|
+
void cliInstall(program, { all: "1", yes: "1", dry: "1" });
|
|
296
|
+
});
|
|
297
|
+
Object.defineProperty(process.stdin, "isTTY", { value: origIsTTY, configurable: true });
|
|
298
|
+
expect(exitCode).toBe(0);
|
|
289
299
|
});
|
|
290
300
|
});
|
|
291
301
|
|
|
292
302
|
describe("uninstall plan", () => {
|
|
293
|
-
test("buildUninstallPlan --all
|
|
294
|
-
const paths = resolveInstallPaths(fixture);
|
|
295
|
-
mkdirSync(paths.appDir, { recursive: true });
|
|
296
|
-
writeFileSync(paths.appPath, "fake", "utf8");
|
|
297
|
-
|
|
298
|
-
const plan = buildUninstallPlan(fixture, paths, parseInstallOpts({ uninstall: "1", all: "1" }));
|
|
299
|
-
expect(plan.some((a) => a.summary.startsWith("app:"))).toBe(true);
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
test("buildUninstallPlan --all runs configure last", () => {
|
|
303
|
+
test("buildUninstallPlan --all runs configure last when config exists", () => {
|
|
303
304
|
const program: CliProgram = {
|
|
304
305
|
...fixture,
|
|
305
306
|
appConfig: {
|
|
@@ -307,27 +308,10 @@ describe("uninstall plan", () => {
|
|
|
307
308
|
},
|
|
308
309
|
};
|
|
309
310
|
const paths = resolveInstallPaths(program);
|
|
310
|
-
mkdirSync(paths.appDir, { recursive: true });
|
|
311
|
-
writeFileSync(paths.appPath, "fake", "utf8");
|
|
312
311
|
writeAppConfigFile(program, { token: "secret" });
|
|
313
312
|
|
|
314
313
|
const plan = buildUninstallPlan(program, paths, parseInstallOpts({ uninstall: "1", all: "1" }));
|
|
315
|
-
const appIdx = plan.findIndex((a) => a.summary.startsWith("app:"));
|
|
316
314
|
const configureIdx = plan.findIndex((a) => a.summary.startsWith("app config:"));
|
|
317
|
-
expect(appIdx).toBeGreaterThanOrEqual(0);
|
|
318
|
-
expect(configureIdx).toBeGreaterThan(appIdx);
|
|
319
315
|
expect(configureIdx).toBe(plan.length - 1);
|
|
320
316
|
});
|
|
321
|
-
|
|
322
|
-
test("buildUninstallPlan --app ignores completions", () => {
|
|
323
|
-
const paths = resolveInstallPaths(fixture);
|
|
324
|
-
mkdirSync(paths.appDir, { recursive: true });
|
|
325
|
-
writeFileSync(paths.appPath, "fake", "utf8");
|
|
326
|
-
mkdirSync(join(home, ".bash_completion.d"), { recursive: true });
|
|
327
|
-
writeFileSync(paths.bashCompletion, "# bash", "utf8");
|
|
328
|
-
|
|
329
|
-
const plan = buildUninstallPlan(fixture, paths, parseInstallOpts({ uninstall: "1", app: "1" }));
|
|
330
|
-
expect(plan).toHaveLength(1);
|
|
331
|
-
expect(plan[0]?.summary.startsWith("app:")).toBe(true);
|
|
332
|
-
});
|
|
333
317
|
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Normalizes bare `uninstall` to `--all`. */
|
|
2
|
+
export function normalizeUninstallRawOpts(raw: Record<string, string>): Record<string, string> {
|
|
3
|
+
const flag = (name: string) => raw[name] === "1";
|
|
4
|
+
const out: Record<string, string> = { ...raw, uninstall: "1" };
|
|
5
|
+
|
|
6
|
+
const hasUninstallTarget = flag("all") || flag("skill") || flag("mcp") || flag("configure");
|
|
7
|
+
if (!hasUninstallTarget) {
|
|
8
|
+
out.all = "1";
|
|
9
|
+
}
|
|
10
|
+
return out;
|
|
11
|
+
}
|
package/src/install/normalize.ts
CHANGED
|
@@ -1,30 +1,15 @@
|
|
|
1
|
-
/** Normalizes bare `install`
|
|
1
|
+
/** Normalizes bare `install` to `--all`. */
|
|
2
2
|
export function normalizeInstallRawOpts(raw: Record<string, string>): Record<string, string> {
|
|
3
3
|
const flag = (name: string) => raw[name] === "1";
|
|
4
4
|
const out = { ...raw };
|
|
5
5
|
|
|
6
|
-
if (flag("status") || flag("reinstall")
|
|
6
|
+
if (flag("status") || flag("reinstall")) {
|
|
7
7
|
return out;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
const hasInstallTarget =
|
|
11
|
-
flag("all") || flag("app") || flag("completions") || flag("skill") || flag("mcp");
|
|
12
|
-
const hasUninstallTarget =
|
|
13
|
-
flag("all") ||
|
|
14
|
-
flag("app") ||
|
|
15
|
-
flag("completions") ||
|
|
16
|
-
flag("skill") ||
|
|
17
|
-
flag("mcp") ||
|
|
18
|
-
flag("configure");
|
|
10
|
+
const hasInstallTarget = flag("all") || flag("skill") || flag("mcp");
|
|
19
11
|
|
|
20
|
-
if (flag("configure") && !
|
|
21
|
-
return out;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
if (flag("uninstall")) {
|
|
25
|
-
if (!hasUninstallTarget) {
|
|
26
|
-
out.all = "1";
|
|
27
|
-
}
|
|
12
|
+
if (flag("configure") && !hasInstallTarget) {
|
|
28
13
|
return out;
|
|
29
14
|
}
|
|
30
15
|
|
package/src/install/paths.ts
CHANGED
|
@@ -8,11 +8,6 @@ import { resolveOpenclawConfigPath } from "./mcp-openclaw.ts";
|
|
|
8
8
|
import { resolveOpenCodeConfigPathForInstall } from "./mcp-opencode.ts";
|
|
9
9
|
|
|
10
10
|
export interface InstallPaths {
|
|
11
|
-
appDir: string;
|
|
12
|
-
appPath: string;
|
|
13
|
-
bashCompletion: string;
|
|
14
|
-
zshCompletion: string;
|
|
15
|
-
fishCompletion: string;
|
|
16
11
|
cursorSkillDir: string;
|
|
17
12
|
claudeSkillDir: string;
|
|
18
13
|
codexSkillDir: string;
|
|
@@ -25,8 +20,6 @@ export interface InstallPaths {
|
|
|
25
20
|
chatGptMcpPath: string;
|
|
26
21
|
codexConfigPath: string;
|
|
27
22
|
openclawConfigPath: string;
|
|
28
|
-
bashRc: string;
|
|
29
|
-
zshRc: string;
|
|
30
23
|
mcpName: string;
|
|
31
24
|
skillDirName: string;
|
|
32
25
|
}
|
|
@@ -38,11 +31,6 @@ export function displayInstallPath(path: string): string {
|
|
|
38
31
|
return displayHomePath(path);
|
|
39
32
|
}
|
|
40
33
|
|
|
41
|
-
/** App install directory (`~/.local/bin`). */
|
|
42
|
-
export function resolveAppDir(): string {
|
|
43
|
-
return join(userHome(), ".local", "bin");
|
|
44
|
-
}
|
|
45
|
-
|
|
46
34
|
/** Resolves Claude Desktop `claude_desktop_config.json` for the current OS. */
|
|
47
35
|
export function resolveClaudeDesktopMcpPath(home: string): string {
|
|
48
36
|
if (process.platform === "darwin") {
|
|
@@ -80,22 +68,14 @@ export function chatGptDesktopPresent(_home: string, configPath: string): boolea
|
|
|
80
68
|
/** Resolves all install artifact paths for a program root. */
|
|
81
69
|
export function resolveInstallPaths(root: CliProgram): InstallPaths {
|
|
82
70
|
const home = userHome();
|
|
83
|
-
const appDir = resolveAppDir();
|
|
84
|
-
const key = root.key;
|
|
85
71
|
const skillDirName = sanitizeToolSegment(root.key);
|
|
86
72
|
const codexSlug = skillDirNameForTarget(root.key, "codex");
|
|
87
73
|
const opencodeSlug = skillDirNameForTarget(root.key, "opencode");
|
|
88
74
|
const openclawSlug = skillDirNameForTarget(root.key, "openclaw");
|
|
89
|
-
const xdgConfig = xdgConfigHome(home);
|
|
90
75
|
const claudeDesktopMcpPath = resolveClaudeDesktopMcpPath(home);
|
|
91
76
|
const chatGptMcpPath = resolveChatGptMcpPath(home);
|
|
92
77
|
|
|
93
78
|
return {
|
|
94
|
-
appDir,
|
|
95
|
-
appPath: join(appDir, key),
|
|
96
|
-
bashCompletion: join(home, ".bash_completion.d", key),
|
|
97
|
-
zshCompletion: join(home, ".zsh", "completions", `_${key}`),
|
|
98
|
-
fishCompletion: join(xdgConfig, "fish", "completions", `${key}.fish`),
|
|
99
79
|
cursorSkillDir: join(home, ".cursor", "skills", skillDirName),
|
|
100
80
|
claudeSkillDir: join(home, ".claude", "skills", skillDirName),
|
|
101
81
|
codexSkillDir: join(home, ".codex", "skills", codexSlug),
|
|
@@ -108,8 +88,6 @@ export function resolveInstallPaths(root: CliProgram): InstallPaths {
|
|
|
108
88
|
chatGptMcpPath,
|
|
109
89
|
codexConfigPath: join(home, ".codex", "config.toml"),
|
|
110
90
|
openclawConfigPath: resolveOpenclawConfigPath(home),
|
|
111
|
-
bashRc: join(home, ".bashrc"),
|
|
112
|
-
zshRc: join(home, ".zshrc"),
|
|
113
91
|
mcpName: mcpServerId(root),
|
|
114
92
|
skillDirName,
|
|
115
93
|
};
|
package/src/install/plan.ts
CHANGED
|
@@ -15,26 +15,34 @@ export function buildInstallPlan(
|
|
|
15
15
|
return buildInstallPlanFromTargets(root, paths, opts);
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
/** Builds update/reinstall actions
|
|
18
|
+
/** Builds update/reinstall actions; greenfield fallback when nothing detected. */
|
|
19
19
|
export function buildUpdatePlan(
|
|
20
20
|
root: CliProgram,
|
|
21
21
|
paths: InstallPaths,
|
|
22
22
|
opts: InstallOpts,
|
|
23
23
|
): InstallAction[] {
|
|
24
|
-
|
|
24
|
+
const refresh = buildInstallPlanFromTargets(root, paths, {
|
|
25
|
+
...opts,
|
|
26
|
+
reinstall: true,
|
|
27
|
+
all: true,
|
|
28
|
+
});
|
|
29
|
+
if (refresh.length > 0) {
|
|
30
|
+
return refresh;
|
|
31
|
+
}
|
|
32
|
+
return buildInstallPlanFromTargets(root, paths, { ...opts, reinstall: false, all: true });
|
|
25
33
|
}
|
|
26
34
|
|
|
27
35
|
/** @deprecated Use resolveInstallPlanMode + shouldIncludeArtifact. */
|
|
28
|
-
export function wantsInstallApp(
|
|
29
|
-
return
|
|
36
|
+
export function wantsInstallApp(_opts: InstallOpts): boolean {
|
|
37
|
+
return false;
|
|
30
38
|
}
|
|
31
39
|
|
|
32
40
|
/** @deprecated Use {@link wantsInstallApp}. */
|
|
33
41
|
export const wantsInstallBin = wantsInstallApp;
|
|
34
42
|
|
|
35
43
|
/** @deprecated Use resolveInstallPlanMode + shouldIncludeArtifact. */
|
|
36
|
-
export function wantsInstallCompletions(
|
|
37
|
-
return
|
|
44
|
+
export function wantsInstallCompletions(_opts: InstallOpts): boolean {
|
|
45
|
+
return false;
|
|
38
46
|
}
|
|
39
47
|
|
|
40
48
|
/** @deprecated Use resolveInstallPlanMode + shouldIncludeArtifact. */
|
package/src/install/shell.ts
CHANGED
|
@@ -21,20 +21,6 @@ export function rcMarkerEnd(appKey: string, tag: string): string {
|
|
|
21
21
|
return `# end ${appKey}:${tag}`;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
/** Returns rc snippet block for PATH, or null if already present. */
|
|
25
|
-
export function buildPathRcBlock(appKey: string, appDir: string): string {
|
|
26
|
-
const start = rcMarkerStart(appKey, "path");
|
|
27
|
-
const end = rcMarkerEnd(appKey, "path");
|
|
28
|
-
return [start, `export PATH="${appDir}:$PATH"`, end].join("\n");
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** Returns rc snippet block for zsh fpath, or null if already present. */
|
|
32
|
-
export function buildZshFpathRcBlock(appKey: string, completionsDir: string): string {
|
|
33
|
-
const start = rcMarkerStart(appKey, "fpath");
|
|
34
|
-
const end = rcMarkerEnd(appKey, "fpath");
|
|
35
|
-
return [start, `fpath=(${completionsDir} $fpath)`, end].join("\n");
|
|
36
|
-
}
|
|
37
|
-
|
|
38
24
|
/** Removes a marker-delimited block from rc file content. */
|
|
39
25
|
export function removeRcBlock(content: string, appKey: string, tag: string): string {
|
|
40
26
|
const start = rcMarkerStart(appKey, tag);
|
|
@@ -34,9 +34,9 @@ describe("resolveInstallTargetPreview", () => {
|
|
|
34
34
|
const paths = resolveInstallPaths(program);
|
|
35
35
|
const preview = resolveInstallTargetPreview(program, paths);
|
|
36
36
|
expect(preview.agentIntegration).toBe("mcp");
|
|
37
|
-
expect(preview.all).toContain("app");
|
|
38
|
-
expect(preview.all).
|
|
39
|
-
expect(preview.
|
|
37
|
+
expect(preview.all).not.toContain("app");
|
|
38
|
+
expect(preview.all.length).toBeGreaterThan(0);
|
|
39
|
+
expect(preview.all.every((k) => k.endsWith("Mcp") || k === "configure")).toBe(true);
|
|
40
40
|
});
|
|
41
41
|
|
|
42
42
|
test("shell app previews skill keys", () => {
|
|
@@ -49,7 +49,8 @@ describe("resolveInstallTargetPreview", () => {
|
|
|
49
49
|
const paths = resolveInstallPaths(program);
|
|
50
50
|
const preview = resolveInstallTargetPreview(program, paths);
|
|
51
51
|
expect(preview.agentIntegration).toBe("skill");
|
|
52
|
-
expect(preview.all).toContain("app");
|
|
52
|
+
expect(preview.all).not.toContain("app");
|
|
53
|
+
expect(preview.all.length).toBeGreaterThan(0);
|
|
53
54
|
expect(preview.mcp).toEqual([]);
|
|
54
55
|
});
|
|
55
56
|
});
|
|
@@ -76,8 +77,7 @@ describe("printInstallStatus json", () => {
|
|
|
76
77
|
effective: { all: string[]; mcp: string[]; skill: string[] };
|
|
77
78
|
};
|
|
78
79
|
expect(parsed.agentIntegration).toBe("mcp");
|
|
79
|
-
expect(parsed.effective.all).
|
|
80
|
-
expect(Array.isArray(parsed.effective.skill)).toBe(true);
|
|
80
|
+
expect(parsed.effective.all.length).toBeGreaterThan(0);
|
|
81
81
|
} finally {
|
|
82
82
|
process.stdout.write = orig;
|
|
83
83
|
}
|
package/src/install/status.ts
CHANGED
|
@@ -42,9 +42,6 @@ export function printInstallStatus(root: CliProgram, opts: InstallOpts): void {
|
|
|
42
42
|
},
|
|
43
43
|
};
|
|
44
44
|
if (status.app) json.app = status.app;
|
|
45
|
-
if (status.bashCompletion) json.bashCompletion = status.bashCompletion;
|
|
46
|
-
if (status.zshCompletion) json.zshCompletion = status.zshCompletion;
|
|
47
|
-
if (status.fishCompletion) json.fishCompletion = status.fishCompletion;
|
|
48
45
|
if (status.cursorSkill) json.cursorSkill = status.cursorSkill;
|
|
49
46
|
if (status.claudeSkill) json.claudeSkill = status.claudeSkill;
|
|
50
47
|
if (status.cursorMcp) json.cursorMcp = status.cursorMcp;
|
|
@@ -60,9 +57,6 @@ export function printInstallStatus(root: CliProgram, opts: InstallOpts): void {
|
|
|
60
57
|
installOut(`Installed artifacts for ${root.key}:`, opts);
|
|
61
58
|
const lines: [string, string | undefined][] = [
|
|
62
59
|
["app", status.app],
|
|
63
|
-
["bash completion", status.bashCompletion],
|
|
64
|
-
["zsh completion", status.zshCompletion],
|
|
65
|
-
["fish completion", status.fishCompletion],
|
|
66
60
|
["cursor skill", status.cursorSkill],
|
|
67
61
|
["claude skill", status.claudeSkill],
|
|
68
62
|
["cursor mcp", status.cursorMcp],
|
|
@@ -107,8 +107,6 @@ export interface InstallTargetPreview {
|
|
|
107
107
|
/** Derives plan mode from install CLI flags. */
|
|
108
108
|
export function resolveInstallPlanMode(opts: {
|
|
109
109
|
all?: boolean;
|
|
110
|
-
app?: boolean;
|
|
111
|
-
completions?: boolean;
|
|
112
110
|
skill?: boolean;
|
|
113
111
|
mcp?: boolean;
|
|
114
112
|
configure?: boolean;
|
|
@@ -30,9 +30,6 @@ export { mcpCategoryEnabled } from "./target-effective.ts";
|
|
|
30
30
|
function emptyInstalledArtifacts(): InstalledArtifacts {
|
|
31
31
|
return {
|
|
32
32
|
app: false,
|
|
33
|
-
bashCompletion: false,
|
|
34
|
-
zshCompletion: false,
|
|
35
|
-
fishCompletion: false,
|
|
36
33
|
cursorSkill: false,
|
|
37
34
|
claudeSkill: false,
|
|
38
35
|
codexSkill: false,
|
|
@@ -45,8 +42,6 @@ function emptyInstalledArtifacts(): InstalledArtifacts {
|
|
|
45
42
|
codexMcp: false,
|
|
46
43
|
openclawMcp: false,
|
|
47
44
|
chatGptMcp: false,
|
|
48
|
-
bashRcPath: false,
|
|
49
|
-
zshRcFpath: false,
|
|
50
45
|
};
|
|
51
46
|
}
|
|
52
47
|
|
|
@@ -62,18 +57,10 @@ export function detectInstalledArtifacts(
|
|
|
62
57
|
return out;
|
|
63
58
|
}
|
|
64
59
|
|
|
65
|
-
|
|
66
|
-
return !!(opts.yes || opts.json || opts.reinstall || opts.update || opts.dry);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** Scope flags; non-interactive `install --mcp` implicitly includes app. */
|
|
60
|
+
/** Scope flags for install/uninstall. */
|
|
70
61
|
export function resolveInstallScope(opts: InstallOpts): InstallScope {
|
|
71
|
-
const assumeApp =
|
|
72
|
-
!opts.uninstall && opts.mcp && !opts.all && !opts.app && isNonInteractiveInstall(opts);
|
|
73
62
|
return {
|
|
74
63
|
all: opts.all,
|
|
75
|
-
app: opts.app || assumeApp,
|
|
76
|
-
completions: opts.completions,
|
|
77
64
|
skill: opts.skill,
|
|
78
65
|
mcp: opts.mcp,
|
|
79
66
|
configure: opts.configure,
|
|
@@ -133,22 +120,20 @@ export function isArtifactInScope(
|
|
|
133
120
|
return t.enabled && t.includedInAll;
|
|
134
121
|
}
|
|
135
122
|
|
|
136
|
-
const scoped =
|
|
137
|
-
scope.app || scope.completions || scope.skill || scope.mcp || scope.configure || false;
|
|
138
|
-
|
|
139
123
|
if (mode === "uninstall-scoped" || mode === "install-scoped") {
|
|
140
124
|
if (!effective[key].enabled) return false;
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if (scope.completions) return key === "completions";
|
|
125
|
+
|
|
126
|
+
let inScope = false;
|
|
144
127
|
if (scope.skill) {
|
|
145
|
-
|
|
128
|
+
inScope = inScope || agentCategoryInScope(key, SKILL_KEYS, "skill", effective, targets, root);
|
|
146
129
|
}
|
|
147
130
|
if (scope.mcp) {
|
|
148
|
-
|
|
131
|
+
inScope = inScope || agentCategoryInScope(key, MCP_KEYS, "mcp", effective, targets, root);
|
|
149
132
|
}
|
|
150
|
-
if (
|
|
151
|
-
|
|
133
|
+
if (scope.configure && key === "configure") {
|
|
134
|
+
inScope = true;
|
|
135
|
+
}
|
|
136
|
+
return inScope;
|
|
152
137
|
}
|
|
153
138
|
|
|
154
139
|
return false;
|
|
@@ -174,7 +159,11 @@ export function shouldIncludeArtifact(
|
|
|
174
159
|
return false;
|
|
175
160
|
}
|
|
176
161
|
if (mode === "refresh" && detected) {
|
|
177
|
-
if (
|
|
162
|
+
if (key === "app") {
|
|
163
|
+
// app is status-only; refresh skills/MCP only
|
|
164
|
+
} else if (!detected[key]) {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
178
167
|
}
|
|
179
168
|
if ((mode === "uninstall-all" || mode === "uninstall-scoped") && detected) {
|
|
180
169
|
const det = detected[key];
|
|
@@ -194,6 +183,7 @@ export function shouldIncludeArtifact(
|
|
|
194
183
|
return isArtifactInScope(key, scope, effective, mode, root, targets);
|
|
195
184
|
}
|
|
196
185
|
if (mode === "refresh") {
|
|
186
|
+
if (key === "app") return false;
|
|
197
187
|
return effective[key].enabled;
|
|
198
188
|
}
|
|
199
189
|
return isArtifactInScope(key, scope, effective, mode, root, targets);
|
|
@@ -235,9 +225,6 @@ export function detectedForArtifact(
|
|
|
235
225
|
detected: DetectedSnapshot,
|
|
236
226
|
): boolean {
|
|
237
227
|
const target = installTargetForKey(key);
|
|
238
|
-
if (key === "completions") {
|
|
239
|
-
return detected.bashCompletion || detected.zshCompletion || detected.fishCompletion;
|
|
240
|
-
}
|
|
241
228
|
if (key === "configure") {
|
|
242
229
|
return detected.appConfig ?? false;
|
|
243
230
|
}
|