argsbarg 4.1.0 → 5.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +74 -1
- package/README.md +91 -85
- package/docs/README.md +8 -8
- package/docs/ai-skills.md +9 -9
- package/docs/bundled-docs.md +5 -5
- package/docs/cli-program.md +13 -11
- package/docs/config-schema.md +36 -9
- package/docs/configure.md +177 -0
- package/docs/developing.md +7 -7
- package/docs/distribution-homebrew.md +104 -0
- package/docs/mcp.md +11 -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/nested.ts +1 -3
- package/index.d.ts +49 -81
- package/package.json +2 -2
- package/src/builtins/builtins.test.ts +84 -64
- package/src/builtins/completion-group.ts +17 -17
- package/src/builtins/configure-copy.ts +86 -0
- package/src/builtins/configure.ts +70 -0
- package/src/builtins/dispatch.ts +13 -9
- package/src/builtins/index.ts +1 -1
- package/src/builtins/mcp.ts +2 -2
- package/src/builtins/registry.ts +6 -4
- package/src/capabilities.ts +22 -15
- package/src/cli-tool/cli-smoke.test.ts +29 -0
- package/src/cli-tool/create.test.ts +141 -0
- package/src/cli-tool/create.ts +402 -0
- package/{examples/consumer-app/capabilities.test.ts → src/cli-tool/full-example-capabilities.test.ts} +16 -15
- package/src/cli-tool/main.ts +8 -0
- package/src/cli-tool/post-create.ts +111 -0
- package/src/cli-tool/program.ts +97 -0
- package/src/cli-tool/prompt.ts +28 -0
- package/src/cli-tool/run-create.ts +138 -0
- package/src/cli.ts +0 -2
- package/src/config/bootstrap.ts +27 -18
- package/src/config/file.test.ts +1 -1
- package/src/config/resolve.test.ts +167 -0
- package/src/config/resolve.ts +52 -8
- package/src/configure/configure.test.ts +148 -0
- package/src/configure/index.ts +284 -0
- package/src/configure/prompt.ts +40 -0
- package/src/docs/api-guide.test.ts +4 -5
- package/src/docs/builtin.ts +3 -5
- package/src/docs/docs.test.ts +6 -5
- package/src/docs/mcp-guide.ts +11 -12
- package/src/index.ts +5 -12
- package/src/install/binary-placement.test.ts +101 -0
- package/src/install/binary-placement.ts +47 -0
- package/src/install/install-validate.test.ts +5 -5
- package/src/install/normalize-uninstall.ts +11 -0
- package/src/install/normalize.ts +4 -19
- package/src/install/opts.ts +17 -0
- 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 +8 -10
- package/src/install/target-scope.ts +26 -36
- package/src/install/target-types.ts +0 -16
- package/src/install/targets/app.ts +19 -28
- package/src/install/targets/configure.ts +6 -2
- package/src/install/targets/index.ts +0 -3
- package/src/install/targets.test.ts +26 -44
- package/src/invoke.test.ts +1 -1
- package/src/mcp/env.test.ts +92 -0
- package/src/mcp/env.ts +15 -14
- package/src/mcp/tools.ts +1 -1
- package/src/mcp.integration.test.ts +4 -4
- package/src/parse.test.ts +13 -14
- package/src/prompt.ts +10 -0
- package/src/schema.ts +1 -1
- package/src/skill/hint.ts +2 -2
- package/src/types.ts +48 -22
- package/src/validate.ts +22 -28
- package/docs/install.md +0 -290
- 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/builtins/install.ts +0 -136
- package/src/install/app.ts +0 -94
- package/src/install/bootstrap.ts +0 -22
- package/src/install/completions.ts +0 -56
- package/src/install/index.ts +0 -415
- package/src/install/install.test.ts +0 -333
- 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,333 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
-
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import { writeAppConfigFile } from "../config/file.ts";
|
|
6
|
-
import type { CliProgram } from "../types.ts";
|
|
7
|
-
import { detectInstalledArtifacts } from "./detect-installed.ts";
|
|
8
|
-
import {
|
|
9
|
-
interactiveSelectionAssumesApp,
|
|
10
|
-
mergeInteractiveSelection,
|
|
11
|
-
parseInstallOpts,
|
|
12
|
-
runInstallMutation,
|
|
13
|
-
validateInstallOpts,
|
|
14
|
-
} from "./index.ts";
|
|
15
|
-
import { opencodeConfigDir } from "./mcp-opencode.ts";
|
|
16
|
-
import { resolveClaudeDesktopMcpPath, resolveInstallPaths } from "./paths.ts";
|
|
17
|
-
import { buildInstallPlan } from "./plan.ts";
|
|
18
|
-
import { printInstallStatus, writeInteractiveInstallIntro } from "./status.ts";
|
|
19
|
-
import { buildUninstallPlan } from "./uninstall.ts";
|
|
20
|
-
|
|
21
|
-
const fixture: CliProgram = {
|
|
22
|
-
key: "testapp",
|
|
23
|
-
version: "0.0.0",
|
|
24
|
-
description: "Test",
|
|
25
|
-
mcpServer: { enabled: true },
|
|
26
|
-
handler: () => {},
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
let home: string;
|
|
30
|
-
let prevHome: string | undefined;
|
|
31
|
-
|
|
32
|
-
let prevXdg: string | undefined;
|
|
33
|
-
|
|
34
|
-
beforeEach(() => {
|
|
35
|
-
home = mkdtempSync(join(tmpdir(), "argsbarg-install-"));
|
|
36
|
-
prevHome = process.env.HOME;
|
|
37
|
-
prevXdg = process.env.XDG_CONFIG_HOME;
|
|
38
|
-
process.env.HOME = home;
|
|
39
|
-
delete process.env.XDG_CONFIG_HOME;
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
afterEach(() => {
|
|
43
|
-
if (prevHome === undefined) delete process.env.HOME;
|
|
44
|
-
else process.env.HOME = prevHome;
|
|
45
|
-
if (prevXdg === undefined) delete process.env.XDG_CONFIG_HOME;
|
|
46
|
-
else process.env.XDG_CONFIG_HOME = prevXdg;
|
|
47
|
-
rmSync(home, { recursive: true, force: true });
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
describe("install paths", () => {
|
|
51
|
-
test("resolveInstallPaths uses ~/.local/bin for app", () => {
|
|
52
|
-
const paths = resolveInstallPaths(fixture);
|
|
53
|
-
expect(paths.appPath).toBe(join(home, ".local", "bin", "testapp"));
|
|
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"));
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
test("claude desktop mcp path on darwin", () => {
|
|
64
|
-
const prev = process.platform;
|
|
65
|
-
Object.defineProperty(process, "platform", { value: "darwin" });
|
|
66
|
-
try {
|
|
67
|
-
expect(resolveClaudeDesktopMcpPath(home)).toBe(
|
|
68
|
-
join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json"),
|
|
69
|
-
);
|
|
70
|
-
} finally {
|
|
71
|
-
Object.defineProperty(process, "platform", { value: prev });
|
|
72
|
-
}
|
|
73
|
-
});
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
describe("detect installed", () => {
|
|
77
|
-
test("detects app and completions", () => {
|
|
78
|
-
const paths = resolveInstallPaths(fixture);
|
|
79
|
-
mkdirSync(paths.appDir, { recursive: true });
|
|
80
|
-
writeFileSync(paths.appPath, "fake", "utf8");
|
|
81
|
-
mkdirSync(join(home, ".bash_completion.d"), { recursive: true });
|
|
82
|
-
writeFileSync(paths.bashCompletion, "# bash", "utf8");
|
|
83
|
-
|
|
84
|
-
const detected = detectInstalledArtifacts(paths, fixture);
|
|
85
|
-
expect(detected.app).toBe(true);
|
|
86
|
-
expect(detected.bashCompletion).toBe(true);
|
|
87
|
-
expect(detected.zshCompletion).toBe(false);
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
describe("install plan", () => {
|
|
92
|
-
test("buildInstallPlan --all includes app", () => {
|
|
93
|
-
const paths = resolveInstallPaths(fixture);
|
|
94
|
-
const plan = buildInstallPlan(fixture, paths, parseInstallOpts({ all: "1" }));
|
|
95
|
-
expect(plan.some((a) => a.kind === "app")).toBe(true);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
test("buildInstallPlan --mcp --yes also includes app", () => {
|
|
99
|
-
const paths = resolveInstallPaths(fixture);
|
|
100
|
-
const opts = parseInstallOpts({ mcp: "1", yes: "1" });
|
|
101
|
-
validateInstallOpts(opts);
|
|
102
|
-
const plan = buildInstallPlan(fixture, paths, opts);
|
|
103
|
-
expect(plan.some((a) => a.kind === "app")).toBe(true);
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
test("buildInstallPlan --mcp without yes omits app", () => {
|
|
107
|
-
const paths = resolveInstallPaths(fixture);
|
|
108
|
-
const plan = buildInstallPlan(fixture, paths, parseInstallOpts({ mcp: "1" }));
|
|
109
|
-
expect(plan.some((a) => a.kind === "app")).toBe(false);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
test("buildInstallPlan --mcp skips MCP when agentIntegration is skill", () => {
|
|
113
|
-
const skillApp: CliProgram = {
|
|
114
|
-
...fixture,
|
|
115
|
-
install: { agentIntegration: "skill" },
|
|
116
|
-
};
|
|
117
|
-
const paths = resolveInstallPaths(skillApp);
|
|
118
|
-
mkdirSync(join(home, ".claude"), { recursive: true });
|
|
119
|
-
mkdirSync(join(home, ".cursor"), { recursive: true });
|
|
120
|
-
const plan = buildInstallPlan(skillApp, paths, parseInstallOpts({ mcp: "1" }));
|
|
121
|
-
expect(plan.some((a) => a.kind.endsWith("-mcp"))).toBe(false);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
test("buildInstallPlan --mcp includes claude desktop when app data exists", () => {
|
|
125
|
-
const paths = resolveInstallPaths(fixture);
|
|
126
|
-
mkdirSync(join(home, ".claude"), { recursive: true });
|
|
127
|
-
mkdirSync(join(home, "Library", "Application Support", "Claude"), { recursive: true });
|
|
128
|
-
const plan = buildInstallPlan(fixture, paths, parseInstallOpts({ mcp: "1" }));
|
|
129
|
-
expect(plan.some((a) => a.kind === "claude-desktop-mcp")).toBe(process.platform === "darwin");
|
|
130
|
-
expect(plan.some((a) => a.kind === "claude-mcp")).toBe(true);
|
|
131
|
-
});
|
|
132
|
-
|
|
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
|
-
test("buildInstallPlan --mcp includes opencode when config dir exists", () => {
|
|
140
|
-
const paths = resolveInstallPaths(fixture);
|
|
141
|
-
mkdirSync(opencodeConfigDir(home), { recursive: true });
|
|
142
|
-
const plan = buildInstallPlan(fixture, paths, parseInstallOpts({ mcp: "1" }));
|
|
143
|
-
expect(plan.some((a) => a.kind === "opencode-mcp")).toBe(true);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
test("buildInstallPlan --mcp includes chatgpt when app data exists", () => {
|
|
147
|
-
const paths = resolveInstallPaths(fixture);
|
|
148
|
-
if (process.platform === "darwin") {
|
|
149
|
-
mkdirSync(join(home, "Library", "Application Support", "ChatGPT"), { recursive: true });
|
|
150
|
-
const plan = buildInstallPlan(fixture, paths, parseInstallOpts({ mcp: "1" }));
|
|
151
|
-
expect(plan.some((a) => a.kind === "chatgpt-desktop-mcp")).toBe(true);
|
|
152
|
-
}
|
|
153
|
-
});
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
describe("install status", () => {
|
|
157
|
-
test("printInstallStatus human output", () => {
|
|
158
|
-
const paths = resolveInstallPaths(fixture);
|
|
159
|
-
mkdirSync(paths.appDir, { recursive: true });
|
|
160
|
-
writeFileSync(paths.appPath, "fake", "utf8");
|
|
161
|
-
|
|
162
|
-
const chunks: string[] = [];
|
|
163
|
-
const orig = process.stdout.write;
|
|
164
|
-
process.stdout.write = ((s: string) => {
|
|
165
|
-
chunks.push(s);
|
|
166
|
-
return true;
|
|
167
|
-
}) as typeof process.stdout.write;
|
|
168
|
-
try {
|
|
169
|
-
printInstallStatus(fixture, {});
|
|
170
|
-
const out = chunks.join("");
|
|
171
|
-
expect(out).toContain("Installed artifacts for testapp");
|
|
172
|
-
expect(out).toContain("~/.local/bin/testapp");
|
|
173
|
-
} finally {
|
|
174
|
-
process.stdout.write = orig;
|
|
175
|
-
}
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
test("printInstallStatus json output", () => {
|
|
179
|
-
const paths = resolveInstallPaths(fixture);
|
|
180
|
-
mkdirSync(join(home, ".cursor"), { recursive: true });
|
|
181
|
-
writeFileSync(
|
|
182
|
-
paths.cursorMcpPath,
|
|
183
|
-
JSON.stringify({ mcpServers: { testapp: { command: "testapp", args: ["mcp"] } } }),
|
|
184
|
-
"utf8",
|
|
185
|
-
);
|
|
186
|
-
|
|
187
|
-
const chunks: string[] = [];
|
|
188
|
-
const orig = process.stdout.write;
|
|
189
|
-
process.stdout.write = ((s: string) => {
|
|
190
|
-
chunks.push(s);
|
|
191
|
-
return true;
|
|
192
|
-
}) as typeof process.stdout.write;
|
|
193
|
-
try {
|
|
194
|
-
printInstallStatus(fixture, { json: true });
|
|
195
|
-
const data = JSON.parse(chunks.join(""));
|
|
196
|
-
expect(data.cursorMcp).toContain("mcp.json");
|
|
197
|
-
} finally {
|
|
198
|
-
process.stdout.write = orig;
|
|
199
|
-
}
|
|
200
|
-
});
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
describe("interactive selection", () => {
|
|
204
|
-
test("mergeInteractiveSelection prepends 1 when app is assumed", () => {
|
|
205
|
-
expect(mergeInteractiveSelection([2, 3], 5, true)).toEqual([1, 2, 3]);
|
|
206
|
-
expect(mergeInteractiveSelection([1, 3], 5, true)).toEqual([1, 3]);
|
|
207
|
-
expect(mergeInteractiveSelection([2, 3], 5, false)).toEqual([2, 3]);
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
test("interactiveSelectionAssumesApp when install plan starts with app", () => {
|
|
211
|
-
const paths = resolveInstallPaths(fixture);
|
|
212
|
-
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
|
-
expect(interactiveSelectionAssumesApp(plan, false)).toBe(false);
|
|
222
|
-
});
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
describe("install output", () => {
|
|
226
|
-
test("writeInteractiveInstallIntro prints app Setup banner on stderr", () => {
|
|
227
|
-
const chunks: string[] = [];
|
|
228
|
-
const orig = process.stderr.write;
|
|
229
|
-
process.stderr.write = ((s: string | Uint8Array) => {
|
|
230
|
-
chunks.push(String(s));
|
|
231
|
-
return true;
|
|
232
|
-
}) as typeof process.stderr.write;
|
|
233
|
-
try {
|
|
234
|
-
writeInteractiveInstallIntro(fixture);
|
|
235
|
-
expect(chunks.join("")).toBe("\ntestapp Setup\n\n");
|
|
236
|
-
} finally {
|
|
237
|
-
process.stderr.write = orig;
|
|
238
|
-
}
|
|
239
|
-
});
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
describe("parseInstallOpts", () => {
|
|
243
|
-
test("json implies yes in validate path via cliInstall", () => {
|
|
244
|
-
const opts = parseInstallOpts({ json: "1", all: "1" });
|
|
245
|
-
expect(opts.json).toBe(true);
|
|
246
|
-
expect(opts.all).toBe(true);
|
|
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
|
-
|
|
256
|
-
test("uninstall allows --all", () => {
|
|
257
|
-
const opts = parseInstallOpts({ uninstall: "1", all: "1", yes: "1" });
|
|
258
|
-
expect(validateInstallOpts(opts)).toBeNull();
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
test("uninstall rejects --reinstall", () => {
|
|
262
|
-
const opts = parseInstallOpts({ uninstall: "1", reinstall: "1", all: "1" });
|
|
263
|
-
expect(validateInstallOpts(opts)).toContain("--reinstall");
|
|
264
|
-
});
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
describe("install mutation", () => {
|
|
268
|
-
test("uninstall --all with nothing installed succeeds", async () => {
|
|
269
|
-
const result = await runInstallMutation(fixture, { uninstall: "1", all: "1", yes: "1" });
|
|
270
|
-
expect(result.changed).toEqual([]);
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
test("install --skill skips cursor and claude when agent dirs missing", async () => {
|
|
274
|
-
const result = await runInstallMutation(fixture, { skill: "1", yes: "1", dry: "1" });
|
|
275
|
-
expect(
|
|
276
|
-
result.changed.every((p) => !p.includes("/.cursor/") && !p.includes("/.claude/skills/")),
|
|
277
|
-
).toBe(true);
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
test("uninstall --all removes detected app", async () => {
|
|
281
|
-
const paths = resolveInstallPaths(fixture);
|
|
282
|
-
mkdirSync(paths.appDir, { recursive: true });
|
|
283
|
-
writeFileSync(paths.appPath, "fake", "utf8");
|
|
284
|
-
|
|
285
|
-
const result = await runInstallMutation(fixture, { uninstall: "1", all: "1", yes: "1" });
|
|
286
|
-
expect(result.changed).toContain(paths.appPath);
|
|
287
|
-
expect(existsSync(paths.appPath)).toBe(false);
|
|
288
|
-
expect(existsSync(paths.appDir)).toBe(true);
|
|
289
|
-
});
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
describe("uninstall plan", () => {
|
|
293
|
-
test("buildUninstallPlan --all scopes like install --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
|
-
const program: CliProgram = {
|
|
304
|
-
...fixture,
|
|
305
|
-
appConfig: {
|
|
306
|
-
entries: { token: { description: "API token." } },
|
|
307
|
-
},
|
|
308
|
-
};
|
|
309
|
-
const paths = resolveInstallPaths(program);
|
|
310
|
-
mkdirSync(paths.appDir, { recursive: true });
|
|
311
|
-
writeFileSync(paths.appPath, "fake", "utf8");
|
|
312
|
-
writeAppConfigFile(program, { token: "secret" });
|
|
313
|
-
|
|
314
|
-
const plan = buildUninstallPlan(program, paths, parseInstallOpts({ uninstall: "1", all: "1" }));
|
|
315
|
-
const appIdx = plan.findIndex((a) => a.summary.startsWith("app:"));
|
|
316
|
-
const configureIdx = plan.findIndex((a) => a.summary.startsWith("app config:"));
|
|
317
|
-
expect(appIdx).toBeGreaterThanOrEqual(0);
|
|
318
|
-
expect(configureIdx).toBeGreaterThan(appIdx);
|
|
319
|
-
expect(configureIdx).toBe(plan.length - 1);
|
|
320
|
-
});
|
|
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
|
-
});
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
|
-
import type { CliProgram } from "../../types.ts";
|
|
3
|
-
import { installCompletions, uninstallCompletions } from "../completions.ts";
|
|
4
|
-
import { displayInstallPath, type InstallPaths } from "../paths.ts";
|
|
5
|
-
import { detectShells } from "../shell.ts";
|
|
6
|
-
import { InstallTarget } from "../target-base.ts";
|
|
7
|
-
import type {
|
|
8
|
-
DetectedSnapshot,
|
|
9
|
-
InstallAction,
|
|
10
|
-
InstalledArtifacts,
|
|
11
|
-
InstallStatus,
|
|
12
|
-
TargetPlanContext,
|
|
13
|
-
UninstallAction,
|
|
14
|
-
} from "../target-types.ts";
|
|
15
|
-
|
|
16
|
-
type ShellKind = "bash" | "zsh" | "fish";
|
|
17
|
-
|
|
18
|
-
interface ShellCompletionSpec {
|
|
19
|
-
shell: ShellKind;
|
|
20
|
-
label: string;
|
|
21
|
-
path: (paths: InstallPaths) => string;
|
|
22
|
-
detectedKey: "bashCompletion" | "zshCompletion" | "fishCompletion";
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const SHELLS: ShellCompletionSpec[] = [
|
|
26
|
-
{
|
|
27
|
-
shell: "bash",
|
|
28
|
-
label: "bash completion",
|
|
29
|
-
path: (p) => p.bashCompletion,
|
|
30
|
-
detectedKey: "bashCompletion",
|
|
31
|
-
},
|
|
32
|
-
{
|
|
33
|
-
shell: "zsh",
|
|
34
|
-
label: "zsh completion",
|
|
35
|
-
path: (p) => p.zshCompletion,
|
|
36
|
-
detectedKey: "zshCompletion",
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
shell: "fish",
|
|
40
|
-
label: "fish completion",
|
|
41
|
-
path: (p) => p.fishCompletion,
|
|
42
|
-
detectedKey: "fishCompletion",
|
|
43
|
-
},
|
|
44
|
-
];
|
|
45
|
-
|
|
46
|
-
/** Shell tab-completion scripts (bash / zsh / fish). */
|
|
47
|
-
class CompletionsInstallTarget extends InstallTarget {
|
|
48
|
-
readonly key = "completions" as const;
|
|
49
|
-
readonly actionKind = "completions" as const;
|
|
50
|
-
readonly category = "core" as const;
|
|
51
|
-
|
|
52
|
-
isAvailable(_root: CliProgram, _paths: InstallPaths): boolean {
|
|
53
|
-
return true;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
isDetected(paths: InstallPaths, _root: CliProgram): boolean {
|
|
57
|
-
return SHELLS.some((s) => existsSync(s.path(paths)));
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
applyDetected(paths: InstallPaths, _root: CliProgram, out: InstalledArtifacts): void {
|
|
61
|
-
for (const s of SHELLS) {
|
|
62
|
-
out[s.detectedKey] = existsSync(s.path(paths));
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
protected isDetectedFromSnapshot(detected: DetectedSnapshot): boolean {
|
|
67
|
-
return detected.bashCompletion || detected.zshCompletion || detected.fishCompletion;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
protected formatStatusLine(_paths: InstallPaths, _root: CliProgram): string {
|
|
71
|
-
return "";
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
protected assignStatusLine(_status: InstallStatus, _line: string): void {}
|
|
75
|
-
|
|
76
|
-
contributeStatus(
|
|
77
|
-
paths: InstallPaths,
|
|
78
|
-
_root: CliProgram,
|
|
79
|
-
detected: InstalledArtifacts,
|
|
80
|
-
status: InstallStatus,
|
|
81
|
-
): void {
|
|
82
|
-
if (detected.bashCompletion) {
|
|
83
|
-
status.bashCompletion = displayInstallPath(paths.bashCompletion);
|
|
84
|
-
}
|
|
85
|
-
if (detected.zshCompletion) {
|
|
86
|
-
status.zshCompletion = displayInstallPath(paths.zshCompletion);
|
|
87
|
-
}
|
|
88
|
-
if (detected.fishCompletion) {
|
|
89
|
-
status.fishCompletion = displayInstallPath(paths.fishCompletion);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
protected buildInstallActions(ctx: TargetPlanContext): InstallAction[] {
|
|
94
|
-
const shells = detectShells();
|
|
95
|
-
const actions: InstallAction[] = [];
|
|
96
|
-
const refresh = ctx.mode === "refresh";
|
|
97
|
-
|
|
98
|
-
for (const s of SHELLS) {
|
|
99
|
-
const onPath = shells[s.shell];
|
|
100
|
-
const path = s.path(ctx.paths);
|
|
101
|
-
const wasInstalled = ctx.detected[s.detectedKey];
|
|
102
|
-
if (!onPath || (refresh && !wasInstalled)) continue;
|
|
103
|
-
|
|
104
|
-
actions.push({
|
|
105
|
-
kind: "completions",
|
|
106
|
-
summary: `${s.label}: ${displayInstallPath(path)}`,
|
|
107
|
-
message: `Writing ${s.label} to ${displayInstallPath(path)}`,
|
|
108
|
-
run: () => {
|
|
109
|
-
const all = installCompletions(ctx.root, ctx.paths, ctx.dry);
|
|
110
|
-
return all.filter((p) => p === path);
|
|
111
|
-
},
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
return actions;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
protected buildUninstallActions(ctx: TargetPlanContext): UninstallAction[] {
|
|
118
|
-
const actions: UninstallAction[] = [];
|
|
119
|
-
for (const s of SHELLS) {
|
|
120
|
-
if (!ctx.detected[s.detectedKey]) continue;
|
|
121
|
-
const path = s.path(ctx.paths);
|
|
122
|
-
actions.push({
|
|
123
|
-
kind: "completions",
|
|
124
|
-
summary: `${s.label}: ${displayInstallPath(path)}`,
|
|
125
|
-
message: `Removing ${s.label} ${displayInstallPath(path)}`,
|
|
126
|
-
run: () => uninstallCompletions(ctx.paths, ctx.dry).filter((p) => p === path),
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
return actions;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
export const completionsTarget = new CompletionsInstallTarget();
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, expect, test } from "bun:test";
|
|
2
|
-
import {
|
|
3
|
-
chmodSync,
|
|
4
|
-
existsSync,
|
|
5
|
-
mkdirSync,
|
|
6
|
-
mkdtempSync,
|
|
7
|
-
readFileSync,
|
|
8
|
-
rmSync,
|
|
9
|
-
writeFileSync,
|
|
10
|
-
} from "node:fs";
|
|
11
|
-
import { tmpdir } from "node:os";
|
|
12
|
-
import { join } from "node:path";
|
|
13
|
-
import { cliPresentationRoot } from "../builtins/presentation.ts";
|
|
14
|
-
import { Cli } from "../index.ts";
|
|
15
|
-
import type { CliProgram, CliUpdateArtifact } from "../types.ts";
|
|
16
|
-
import { cliValidateProgram } from "../validate.ts";
|
|
17
|
-
import { parseInstallOpts, runInstallMutation } from "./index.ts";
|
|
18
|
-
|
|
19
|
-
let home: string;
|
|
20
|
-
let prevHome: string | undefined;
|
|
21
|
-
let prevExecPath: string;
|
|
22
|
-
|
|
23
|
-
beforeEach(() => {
|
|
24
|
-
home = mkdtempSync(join(tmpdir(), "argsbarg-update-"));
|
|
25
|
-
prevHome = process.env.HOME;
|
|
26
|
-
process.env.HOME = home;
|
|
27
|
-
prevExecPath = process.execPath;
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
afterEach(() => {
|
|
31
|
-
if (prevHome === undefined) delete process.env.HOME;
|
|
32
|
-
else process.env.HOME = prevHome;
|
|
33
|
-
process.execPath = prevExecPath;
|
|
34
|
-
rmSync(home, { recursive: true, force: true });
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
function fixtureWithUpdate(hook: () => Promise<CliUpdateArtifact>): CliProgram {
|
|
38
|
-
return {
|
|
39
|
-
key: "testapp",
|
|
40
|
-
version: "1.0.0",
|
|
41
|
-
description: "Test",
|
|
42
|
-
install: { updateGetLatest: hook },
|
|
43
|
-
commands: [{ key: "run", description: "Run", handler: () => {} }],
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
test("user update command is allowed when updateGetLatest is set", () => {
|
|
48
|
-
const root: CliProgram = {
|
|
49
|
-
...fixtureWithUpdate(async () => ({ path: process.execPath })),
|
|
50
|
-
commands: [{ key: "update", description: "Custom update", handler: () => {} }],
|
|
51
|
-
};
|
|
52
|
-
expect(() => cliValidateProgram(root)).not.toThrow();
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
test("presentation exposes install --update when hook is set", () => {
|
|
56
|
-
const root = fixtureWithUpdate(async () => ({ path: process.execPath }));
|
|
57
|
-
const presentation = cliPresentationRoot(root);
|
|
58
|
-
const install = presentation.commands.find((c) => c.key === "install");
|
|
59
|
-
expect(install?.options?.some((o) => o.name === "update")).toBe(true);
|
|
60
|
-
expect(presentation.commands.some((c) => c.key === "update")).toBe(false);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
test("parseInstallOpts treats --update separately from --reinstall", () => {
|
|
64
|
-
expect(parseInstallOpts({ update: "1" }).update).toBe(true);
|
|
65
|
-
expect(parseInstallOpts({ update: "1" }).reinstall).toBe(false);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
test("runInstallMutation honors --from for binary copy", async () => {
|
|
69
|
-
const root: CliProgram = {
|
|
70
|
-
key: "testapp",
|
|
71
|
-
version: "1.0.0",
|
|
72
|
-
description: "Test",
|
|
73
|
-
handler: () => {},
|
|
74
|
-
};
|
|
75
|
-
const source = join(home, "new-binary");
|
|
76
|
-
writeFileSync(source, "#!/bin/sh\necho hi\n", "utf8");
|
|
77
|
-
chmodSync(source, 0o755);
|
|
78
|
-
|
|
79
|
-
const bindir = join(home, ".local", "bin");
|
|
80
|
-
mkdirSync(bindir, { recursive: true });
|
|
81
|
-
writeFileSync(join(bindir, "testapp"), "old\n", "utf8");
|
|
82
|
-
|
|
83
|
-
const { changed } = await runInstallMutation(root, {
|
|
84
|
-
reinstall: "1",
|
|
85
|
-
yes: "1",
|
|
86
|
-
from: source,
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
const dest = join(home, ".local", "bin", "testapp");
|
|
90
|
-
expect(changed).toContain(dest);
|
|
91
|
-
expect(readFileSync(dest, "utf8")).toContain("echo hi");
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
test("Cli.invoke install --update uses hook and reinstalls", async () => {
|
|
95
|
-
const source = join(home, "new-binary");
|
|
96
|
-
writeFileSync(source, "#!/bin/sh\necho hi\n", "utf8");
|
|
97
|
-
chmodSync(source, 0o755);
|
|
98
|
-
|
|
99
|
-
const root = fixtureWithUpdate(async () => ({
|
|
100
|
-
path: source,
|
|
101
|
-
version: "2.0.0",
|
|
102
|
-
}));
|
|
103
|
-
|
|
104
|
-
const bindir = join(home, ".local", "bin");
|
|
105
|
-
mkdirSync(bindir, { recursive: true });
|
|
106
|
-
writeFileSync(join(bindir, "testapp"), "old\n", "utf8");
|
|
107
|
-
|
|
108
|
-
const result = await new Cli(root).invoke(["install", "--update"]);
|
|
109
|
-
expect(result.exitCode).toBe(0);
|
|
110
|
-
expect(result.stdout).toContain("Updated testapp 1.0.0 → 2.0.0");
|
|
111
|
-
expect(existsSync(join(home, ".local", "bin", "testapp"))).toBe(true);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
test("Cli.invoke install --update reports already current", async () => {
|
|
115
|
-
const root = fixtureWithUpdate(async () => ({
|
|
116
|
-
path: process.execPath,
|
|
117
|
-
version: "1.0.0",
|
|
118
|
-
}));
|
|
119
|
-
|
|
120
|
-
const result = await new Cli(root).invoke(["install", "--update"]);
|
|
121
|
-
expect(result.exitCode).toBe(0);
|
|
122
|
-
expect(result.stdout).toContain("Already at v1.0.0");
|
|
123
|
-
});
|
package/src/install/update.ts
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
|
-
import type { CliProgram } from "../types.ts";
|
|
3
|
-
import { runInstallMutation } from "./index.ts";
|
|
4
|
-
import { installErr } from "./status.ts";
|
|
5
|
-
|
|
6
|
-
/** Downloads the latest release and reinstalls installed artifacts (`myapp install --update`). */
|
|
7
|
-
export async function cliUpdate(root: CliProgram): Promise<never> {
|
|
8
|
-
const hook = root.install?.updateGetLatest;
|
|
9
|
-
if (!hook) {
|
|
10
|
-
installErr("Remote updates are not supported by this app.");
|
|
11
|
-
process.exit(1);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
let artifact: Awaited<ReturnType<typeof hook>>;
|
|
15
|
-
try {
|
|
16
|
-
artifact = await hook({ version: root.version });
|
|
17
|
-
} catch (err) {
|
|
18
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
19
|
-
installErr(message);
|
|
20
|
-
process.exit(1);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
if (!artifact.path || !existsSync(artifact.path)) {
|
|
24
|
-
installErr("Update download did not return a valid app binary.");
|
|
25
|
-
process.exit(1);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
if (artifact.version !== undefined && artifact.version === root.version) {
|
|
29
|
-
process.stdout.write(`Already at v${root.version}\n`);
|
|
30
|
-
await artifact.cleanup?.();
|
|
31
|
-
process.exit(0);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const currentVersion = root.version;
|
|
35
|
-
try {
|
|
36
|
-
await runInstallMutation(root, {
|
|
37
|
-
reinstall: "1",
|
|
38
|
-
yes: "1",
|
|
39
|
-
from: artifact.path,
|
|
40
|
-
});
|
|
41
|
-
} catch (err) {
|
|
42
|
-
await artifact.cleanup?.();
|
|
43
|
-
installErr(err instanceof Error ? err.message : String(err));
|
|
44
|
-
process.exit(1);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
await artifact.cleanup?.();
|
|
48
|
-
|
|
49
|
-
if (artifact.version !== undefined && artifact.version !== currentVersion) {
|
|
50
|
-
process.stdout.write(`Updated ${root.key} ${currentVersion} → ${artifact.version}\n`);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
process.exit(0);
|
|
54
|
-
}
|
|
File without changes
|
|
File without changes
|
/package/examples/{consumer-app → full-example}/scripts/schemagen/discover-schema-roots.test.ts
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|