argsbarg 3.0.0 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -1
- package/README.md +6 -6
- package/bun.lock +21 -0
- package/docs/ai-skills.md +8 -4
- package/docs/bundled-docs.md +91 -0
- package/docs/install.md +16 -4
- package/docs/mcp.md +4 -6
- package/examples/minimal.ts +6 -0
- package/examples/nested.ts +6 -0
- package/index.d.ts +46 -1
- package/package.json +4 -1
- package/src/builtins/completion-bash.ts +1 -8
- package/src/builtins/completion-fish.ts +0 -5
- package/src/builtins/completion-zsh.ts +1 -9
- package/src/builtins/dispatch.ts +40 -0
- package/src/builtins/export.ts +9 -0
- package/src/builtins/install.ts +9 -3
- package/src/builtins/presentation.ts +9 -0
- package/src/builtins/shell-helpers.ts +0 -2
- package/src/builtins/update.ts +14 -0
- package/src/capabilities.ts +12 -1
- package/src/docs/api-guide.test.ts +55 -0
- package/src/docs/api-guide.ts +129 -0
- package/src/docs/builtin.ts +61 -0
- package/src/docs/docs.test.ts +167 -0
- package/src/docs/mcp-guide.ts +118 -0
- package/src/docs/resolve.ts +119 -0
- package/src/help.ts +3 -7
- package/src/index.test.ts +40 -66
- package/src/index.ts +4 -0
- package/src/install/binary.ts +8 -3
- package/src/install/index.ts +55 -30
- package/src/install/plan.ts +5 -3
- package/src/install/update.test.ts +106 -0
- package/src/install/update.ts +55 -0
- package/src/invoke.ts +17 -15
- package/src/mcp/tools.ts +1 -1
- package/src/parse.ts +0 -27
- package/src/runtime.ts +12 -6
- package/src/schema.ts +7 -2
- package/src/skill/generate.ts +6 -39
- package/src/types.ts +47 -0
- package/src/validate.ts +53 -6
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { CliDocsConfig, CliProgram } from "../types.ts";
|
|
2
|
+
import { cliSchemaJson } from "../schema.ts";
|
|
3
|
+
import { generateSkillBundle } from "../skill/generate.ts";
|
|
4
|
+
import { generateApiGuide } from "./api-guide.ts";
|
|
5
|
+
import { generateMcpGuide } from "./mcp-guide.ts";
|
|
6
|
+
|
|
7
|
+
/** Built-in docs subcommand keys not allowed in `docs.topics`. */
|
|
8
|
+
export const DOCS_BUILTIN_TOPIC_KEYS = ["mcp", "all", "schema", "api", "skill"] as const;
|
|
9
|
+
|
|
10
|
+
export type DocsBuiltinTopicKey = (typeof DOCS_BUILTIN_TOPIC_KEYS)[number];
|
|
11
|
+
|
|
12
|
+
/** Default router description for the `docs` built-in. */
|
|
13
|
+
export const DOCS_ROUTER_DESCRIPTION = "Print bundled CLI documentation.";
|
|
14
|
+
|
|
15
|
+
/** Returns whether bundled docs are enabled on the program root. */
|
|
16
|
+
export function docsEnabled(program: CliProgram): boolean {
|
|
17
|
+
return program.docs?.enabled === true;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** User topic keys in declaration order. */
|
|
21
|
+
export function docsUserTopicKeys(docs: CliDocsConfig): string[] {
|
|
22
|
+
return Object.keys(docs.topics);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Subcommand used when argv is bare `myapp docs`. */
|
|
26
|
+
export function docsEffectiveDefaultTopic(docs: CliDocsConfig): string {
|
|
27
|
+
if (docs.defaultTopic !== undefined) {
|
|
28
|
+
return docs.defaultTopic;
|
|
29
|
+
}
|
|
30
|
+
const keys = docsUserTopicKeys(docs);
|
|
31
|
+
if (keys.length === 0) {
|
|
32
|
+
throw new Error("docs.topics must be non-empty");
|
|
33
|
+
}
|
|
34
|
+
return keys[0]!;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Whether MCP auto-guide topic is included. */
|
|
38
|
+
export function docsIncludesMcpTopic(program: CliProgram): boolean {
|
|
39
|
+
return docsEnabled(program) && program.mcpServer?.enabled === true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Leaf help description for a user topic. */
|
|
43
|
+
export function docsTopicDescription(key: string, custom?: string): string {
|
|
44
|
+
if (custom) {
|
|
45
|
+
return custom;
|
|
46
|
+
}
|
|
47
|
+
if (key === "readme") {
|
|
48
|
+
return "Print README (user guide).";
|
|
49
|
+
}
|
|
50
|
+
const label = key.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
51
|
+
return `Print ${label} documentation.`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Ordered keys for `docs all` (user topics, then auto `mcp` when enabled). */
|
|
55
|
+
export function docsPrintOrder(program: CliProgram): string[] {
|
|
56
|
+
const docs = program.docs!;
|
|
57
|
+
const order = docsUserTopicKeys(docs);
|
|
58
|
+
if (docsIncludesMcpTopic(program)) {
|
|
59
|
+
order.push("mcp");
|
|
60
|
+
}
|
|
61
|
+
return order;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Markdown body for one docs topic key. */
|
|
65
|
+
export function docsTopicText(program: CliProgram, topic: string): string {
|
|
66
|
+
const docs = program.docs!;
|
|
67
|
+
if (topic === "mcp") {
|
|
68
|
+
if (!docsIncludesMcpTopic(program)) {
|
|
69
|
+
throw new Error("Unknown docs topic 'mcp'.");
|
|
70
|
+
}
|
|
71
|
+
return generateMcpGuide(program);
|
|
72
|
+
}
|
|
73
|
+
const entry = docs.topics[topic];
|
|
74
|
+
if (!entry) {
|
|
75
|
+
throw new Error(`Unknown docs topic '${topic}'.`);
|
|
76
|
+
}
|
|
77
|
+
return entry.text;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** All bundled docs concatenated with horizontal rules. */
|
|
81
|
+
export function combineAllDocs(program: CliProgram): string {
|
|
82
|
+
return docsPrintOrder(program)
|
|
83
|
+
.map((key) => docsTopicText(program, key).trim())
|
|
84
|
+
.join("\n\n---\n\n");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Writes CLI schema JSON to stdout (`docs schema`). */
|
|
88
|
+
export function printDocsSchema(program: CliProgram): void {
|
|
89
|
+
process.stdout.write(cliSchemaJson(program));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Writes markdown API reference to stdout (`docs api`). */
|
|
93
|
+
export function printDocsApi(program: CliProgram): void {
|
|
94
|
+
process.stdout.write(generateApiGuide(program));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Writes generated Cursor SKILL.md to stdout (`docs skill`). */
|
|
98
|
+
export function printDocsSkill(program: CliProgram): void {
|
|
99
|
+
const bundle = generateSkillBundle(program, "cursor");
|
|
100
|
+
process.stdout.write(`${bundle.skillMd}\n`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Writes one docs topic (or `all`) to stdout. */
|
|
104
|
+
export function printDocsTopic(program: CliProgram, topic: string): void {
|
|
105
|
+
if (topic === "schema") {
|
|
106
|
+
printDocsSchema(program);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (topic === "api") {
|
|
110
|
+
printDocsApi(program);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (topic === "skill") {
|
|
114
|
+
printDocsSkill(program);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const content = topic === "all" ? combineAllDocs(program) : docsTopicText(program, topic);
|
|
118
|
+
process.stdout.write(`${content}\n`);
|
|
119
|
+
}
|
package/src/help.ts
CHANGED
|
@@ -346,16 +346,12 @@ function usageLines(
|
|
|
346
346
|
}
|
|
347
347
|
|
|
348
348
|
/** Table rows for named options, including synthetic built-in rows. */
|
|
349
|
-
function rowsForOptions(defs: CliOption[], color: boolean
|
|
349
|
+
function rowsForOptions(defs: CliOption[], color: boolean): HelpRow[] {
|
|
350
350
|
const rows: HelpRow[] = [];
|
|
351
351
|
const helpLabel = color
|
|
352
352
|
? style.aquaBold("--help, ") + style.greenBright("-h")
|
|
353
353
|
: "--help, -h";
|
|
354
354
|
rows.push({ label: helpLabel, description: "Show help for this command." });
|
|
355
|
-
if (isRoot) {
|
|
356
|
-
const schemaLabel = color ? style.aquaBold("--schema") : "--schema";
|
|
357
|
-
rows.push({ label: schemaLabel, description: "Print the full command tree as JSON." });
|
|
358
|
-
}
|
|
359
355
|
for (const o of defs) {
|
|
360
356
|
const desc = o.required ? "(required) " + o.description : o.description;
|
|
361
357
|
rows.push({ label: cliOptionLabel(o, color), description: desc });
|
|
@@ -401,7 +397,7 @@ export function cliHelpRender(schema: CliRouter, helpPath: string[], useStderr:
|
|
|
401
397
|
).join("\n"),
|
|
402
398
|
);
|
|
403
399
|
|
|
404
|
-
const optBox = renderTableBox("Options", rowsForOptions(schema.options ?? [], color
|
|
400
|
+
const optBox = renderTableBox("Options", rowsForOptions(schema.options ?? [], color), hw, color);
|
|
405
401
|
if (optBox.length > 0) {
|
|
406
402
|
lines.push("");
|
|
407
403
|
lines.push(optBox.join("\n"));
|
|
@@ -450,7 +446,7 @@ export function cliHelpRender(schema: CliRouter, helpPath: string[], useStderr:
|
|
|
450
446
|
).join("\n"),
|
|
451
447
|
);
|
|
452
448
|
|
|
453
|
-
const optBox = renderTableBox("Options", rowsForOptions(node.options ?? [], color
|
|
449
|
+
const optBox = renderTableBox("Options", rowsForOptions(node.options ?? [], color), hw, color);
|
|
454
450
|
if (optBox.length > 0) {
|
|
455
451
|
lines.push("");
|
|
456
452
|
lines.push(optBox.join("\n"));
|
package/src/index.test.ts
CHANGED
|
@@ -500,8 +500,8 @@ test("leaf completion help prints correctly", async () => {
|
|
|
500
500
|
expect(stderr.toString()).toBe("");
|
|
501
501
|
});
|
|
502
502
|
|
|
503
|
-
test("
|
|
504
|
-
const { stdout, stderr, exitCode } = await $`bun run examples/nested.ts
|
|
503
|
+
test("docs schema exports JSON for nested CLIs", async () => {
|
|
504
|
+
const { stdout, stderr, exitCode } = await $`bun run examples/nested.ts docs schema`.nothrow().quiet();
|
|
505
505
|
expect(exitCode).toBe(0);
|
|
506
506
|
expect(stderr.toString()).toBe("");
|
|
507
507
|
|
|
@@ -516,15 +516,20 @@ test("--schema exports JSON for nested CLIs", async () => {
|
|
|
516
516
|
expect(lookup.positionals[0].name).toBe("path");
|
|
517
517
|
});
|
|
518
518
|
|
|
519
|
-
test("
|
|
520
|
-
const { stdout, exitCode } = await $`bun run examples/minimal.ts
|
|
519
|
+
test("docs schema exports JSON for leaf roots", async () => {
|
|
520
|
+
const { stdout, exitCode } = await $`bun run examples/minimal.ts docs schema`.nothrow().quiet();
|
|
521
521
|
expect(exitCode).toBe(0);
|
|
522
522
|
|
|
523
523
|
const schema = JSON.parse(stdout.toString());
|
|
524
524
|
expect(schema.key).toBe("minimal.ts");
|
|
525
525
|
expect(schema.positionals[0].name).toBe("name");
|
|
526
526
|
expect(schema.options[0].name).toBe("verbose");
|
|
527
|
-
expect(schema.commands.map((c: { key: string }) => c.key)).toEqual([
|
|
527
|
+
expect(schema.commands.map((c: { key: string }) => c.key)).toEqual([
|
|
528
|
+
"completion",
|
|
529
|
+
"version",
|
|
530
|
+
"install",
|
|
531
|
+
"docs",
|
|
532
|
+
]);
|
|
528
533
|
});
|
|
529
534
|
|
|
530
535
|
test("version builtin prints program version", async () => {
|
|
@@ -540,10 +545,15 @@ test("leaf root help lists completion built-in", async () => {
|
|
|
540
545
|
expect(stdout.toString()).toContain("Generate the autocompletion script for shells.");
|
|
541
546
|
});
|
|
542
547
|
|
|
543
|
-
test("
|
|
544
|
-
const root= testProgram({
|
|
548
|
+
test("root --schema is no longer a flag", () => {
|
|
549
|
+
const root = testProgram({
|
|
545
550
|
key: "app",
|
|
551
|
+
version: "1.0.0",
|
|
546
552
|
description: "demo",
|
|
553
|
+
docs: {
|
|
554
|
+
enabled: true,
|
|
555
|
+
topics: { readme: { text: "# readme\n" } },
|
|
556
|
+
},
|
|
547
557
|
commands: [
|
|
548
558
|
{
|
|
549
559
|
key: "x",
|
|
@@ -553,8 +563,8 @@ test("parse recognizes --schema at the program root", () => {
|
|
|
553
563
|
],
|
|
554
564
|
});
|
|
555
565
|
cliValidateProgram(root);
|
|
556
|
-
const pr = parse(root, ["--schema"]);
|
|
557
|
-
expect(pr.kind).toBe(ParseKind.
|
|
566
|
+
const pr = parse(cliPresentationRoot(root), ["--schema"]);
|
|
567
|
+
expect(pr.kind).not.toBe(ParseKind.Ok);
|
|
558
568
|
});
|
|
559
569
|
|
|
560
570
|
test("cliSchemaJson omits handlers and completion built-ins", () => {
|
|
@@ -587,32 +597,15 @@ test("cliSchemaJson omits handlers and completion built-ins", () => {
|
|
|
587
597
|
expect(schema).not.toHaveProperty("handler");
|
|
588
598
|
});
|
|
589
599
|
|
|
590
|
-
test("
|
|
591
|
-
const root= testProgram({
|
|
592
|
-
key: "app",
|
|
593
|
-
description: "",
|
|
594
|
-
commands: [
|
|
595
|
-
{
|
|
596
|
-
key: "x",
|
|
597
|
-
description: "cmd",
|
|
598
|
-
options: [
|
|
599
|
-
{
|
|
600
|
-
name: "schema",
|
|
601
|
-
description: "",
|
|
602
|
-
kind: CliOptionKind.String,
|
|
603
|
-
},
|
|
604
|
-
],
|
|
605
|
-
handler: () => {},
|
|
606
|
-
},
|
|
607
|
-
],
|
|
608
|
-
});
|
|
609
|
-
expect(() => cliValidateProgram(root)).toThrow(/reserved for --schema/);
|
|
610
|
-
});
|
|
611
|
-
|
|
612
|
-
test("root help lists --schema built-in", () => {
|
|
613
|
-
const root= testProgram({
|
|
600
|
+
test("docs help lists schema, api, and skill subcommands", () => {
|
|
601
|
+
const root = testProgram({
|
|
614
602
|
key: "app",
|
|
603
|
+
version: "1.0.0",
|
|
615
604
|
description: "demo",
|
|
605
|
+
docs: {
|
|
606
|
+
enabled: true,
|
|
607
|
+
topics: { readme: { text: "# readme\n" } },
|
|
608
|
+
},
|
|
616
609
|
commands: [
|
|
617
610
|
{
|
|
618
611
|
key: "x",
|
|
@@ -621,14 +614,19 @@ test("root help lists --schema built-in", () => {
|
|
|
621
614
|
},
|
|
622
615
|
],
|
|
623
616
|
});
|
|
624
|
-
const help = cliHelpRender(cliPresentationRoot(root), [], false);
|
|
625
|
-
expect(help).toContain("
|
|
617
|
+
const help = cliHelpRender(cliPresentationRoot(root), ["docs"], false);
|
|
618
|
+
expect(help).toContain("schema");
|
|
626
619
|
expect(help).toContain("Print the full command tree as JSON.");
|
|
620
|
+
expect(help).toContain("api");
|
|
621
|
+
expect(help).toContain("markdown");
|
|
622
|
+
expect(help).toContain("skill");
|
|
623
|
+
expect(help).toContain("SKILL.md");
|
|
627
624
|
});
|
|
628
625
|
|
|
629
|
-
test("
|
|
630
|
-
const root= testProgram({
|
|
626
|
+
test("root help omits legacy --schema flag", () => {
|
|
627
|
+
const root = testProgram({
|
|
631
628
|
key: "app",
|
|
629
|
+
version: "1.0.0",
|
|
632
630
|
description: "demo",
|
|
633
631
|
commands: [
|
|
634
632
|
{
|
|
@@ -638,37 +636,10 @@ test("nested help omits --schema built-in", () => {
|
|
|
638
636
|
},
|
|
639
637
|
],
|
|
640
638
|
});
|
|
641
|
-
const help = cliHelpRender(cliPresentationRoot(root), [
|
|
639
|
+
const help = cliHelpRender(cliPresentationRoot(root), [], false);
|
|
642
640
|
expect(help).not.toContain("--schema");
|
|
643
641
|
});
|
|
644
642
|
|
|
645
|
-
test("completion scripts offer --schema at the program root only", () => {
|
|
646
|
-
const root= testProgram({
|
|
647
|
-
key: "myapp",
|
|
648
|
-
description: "",
|
|
649
|
-
commands: [
|
|
650
|
-
{
|
|
651
|
-
key: "stat",
|
|
652
|
-
description: "stats",
|
|
653
|
-
commands: [
|
|
654
|
-
{
|
|
655
|
-
key: "show",
|
|
656
|
-
description: "show",
|
|
657
|
-
handler: () => {},
|
|
658
|
-
},
|
|
659
|
-
],
|
|
660
|
-
},
|
|
661
|
-
],
|
|
662
|
-
});
|
|
663
|
-
|
|
664
|
-
const bash = completionBashScript(cliPresentationRoot(root));
|
|
665
|
-
expect(bash).toContain("A_myapp_0_opts+=('--schema')");
|
|
666
|
-
expect(bash).not.toContain("A_myapp_1_opts+=('--schema')");
|
|
667
|
-
|
|
668
|
-
const zsh = completionZshScript(cliPresentationRoot(root));
|
|
669
|
-
expect(zsh).toContain("'--schema:Print the full command tree as JSON.'");
|
|
670
|
-
});
|
|
671
|
-
|
|
672
643
|
const nestedMcpFixture = testProgram({
|
|
673
644
|
key: "nested.ts",
|
|
674
645
|
description: "Nested groups demo.",
|
|
@@ -1760,7 +1731,10 @@ test("generateSkillBundle includes frontmatter and command catalog", () => {
|
|
|
1760
1731
|
expect(bundle.dirName).toBe("nested_ts");
|
|
1761
1732
|
expect(bundle.skillMd).toMatch(/^---\nname: nested_ts\n/);
|
|
1762
1733
|
expect(bundle.skillMd).toContain("stat owner lookup");
|
|
1763
|
-
expect(bundle.skillMd).toContain("
|
|
1734
|
+
expect(bundle.skillMd).toContain("Invoke via shell:");
|
|
1735
|
+
expect(bundle.skillMd).not.toContain("mcp.json");
|
|
1736
|
+
expect(bundle.skillMd).not.toContain("Prefer MCP");
|
|
1737
|
+
expect(bundle.skillMd).not.toContain("tools/call");
|
|
1764
1738
|
expect(bundle.referenceMd).toContain("```json");
|
|
1765
1739
|
expect(() => JSON.parse(bundle.referenceMd.match(/```json\n([\s\S]*?)\n```/)![1]!)).not.toThrow();
|
|
1766
1740
|
});
|
package/src/index.ts
CHANGED
package/src/install/binary.ts
CHANGED
|
@@ -16,8 +16,13 @@ export interface BinaryInstallResult {
|
|
|
16
16
|
patchedZshRc: boolean;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
/** Copies
|
|
20
|
-
export function installBinary(
|
|
19
|
+
/** Copies a binary to the install path and patches rc files when shells are detected. */
|
|
20
|
+
export function installBinary(
|
|
21
|
+
root: CliProgram,
|
|
22
|
+
paths: InstallPaths,
|
|
23
|
+
dry: boolean,
|
|
24
|
+
sourcePath: string = process.execPath,
|
|
25
|
+
): BinaryInstallResult {
|
|
21
26
|
const changed: string[] = [];
|
|
22
27
|
const shells = detectShells();
|
|
23
28
|
let patchedBashRc = false;
|
|
@@ -25,7 +30,7 @@ export function installBinary(root: CliProgram, paths: InstallPaths, dry: boolea
|
|
|
25
30
|
|
|
26
31
|
if (!dry) {
|
|
27
32
|
mkdirSync(paths.bindir, { recursive: true });
|
|
28
|
-
copyFileSync(
|
|
33
|
+
copyFileSync(sourcePath, paths.binaryPath);
|
|
29
34
|
}
|
|
30
35
|
changed.push(paths.binaryPath);
|
|
31
36
|
|
package/src/install/index.ts
CHANGED
|
@@ -15,13 +15,15 @@ import { buildUninstallPlan, uninstallSkillDir, type UninstallAction } from "./u
|
|
|
15
15
|
|
|
16
16
|
function parseInstallOpts(raw: Record<string, string>): InstallOpts {
|
|
17
17
|
const flag = (name: string) => raw[name] === "1";
|
|
18
|
+
const reinstall = flag("reinstall") || flag("update");
|
|
18
19
|
return {
|
|
19
20
|
all: flag("all"),
|
|
20
21
|
bin: flag("bin"),
|
|
21
22
|
completions: flag("completions"),
|
|
22
23
|
skill: flag("skill"),
|
|
23
24
|
mcp: flag("mcp"),
|
|
24
|
-
|
|
25
|
+
reinstall,
|
|
26
|
+
from: raw.from,
|
|
25
27
|
status: flag("status"),
|
|
26
28
|
uninstall: flag("uninstall"),
|
|
27
29
|
yes: flag("yes"),
|
|
@@ -36,28 +38,32 @@ function validateOpts(opts: InstallOpts): string | null {
|
|
|
36
38
|
if (opts.quiet && opts.dry) {
|
|
37
39
|
return "--quiet cannot be combined with --dry.";
|
|
38
40
|
}
|
|
39
|
-
if (opts.quiet && !opts.yes && !opts.json && !opts.
|
|
40
|
-
return "--quiet requires --yes (or --json / --
|
|
41
|
+
if (opts.quiet && !opts.yes && !opts.json && !opts.reinstall) {
|
|
42
|
+
return "--quiet requires --yes (or --json / --reinstall).";
|
|
41
43
|
}
|
|
42
44
|
if (opts.json) {
|
|
43
45
|
opts.yes = true;
|
|
44
46
|
}
|
|
45
|
-
if (opts.
|
|
47
|
+
if (opts.reinstall) {
|
|
46
48
|
opts.bin = true;
|
|
47
49
|
opts.yes = true;
|
|
48
50
|
}
|
|
49
51
|
|
|
50
|
-
const mutationFlags =
|
|
52
|
+
const mutationFlags =
|
|
53
|
+
opts.all || opts.bin || opts.completions || opts.skill || opts.mcp || opts.reinstall || opts.uninstall;
|
|
51
54
|
if (opts.status && mutationFlags) {
|
|
52
|
-
return "--status is mutually exclusive with install/
|
|
55
|
+
return "--status is mutually exclusive with install/reinstall/uninstall targets.";
|
|
53
56
|
}
|
|
54
|
-
if (
|
|
55
|
-
|
|
57
|
+
if (
|
|
58
|
+
opts.reinstall &&
|
|
59
|
+
(opts.all || opts.completions || opts.skill || opts.mcp || opts.uninstall || opts.status)
|
|
60
|
+
) {
|
|
61
|
+
return "--reinstall cannot be combined with other target flags.";
|
|
56
62
|
}
|
|
57
|
-
if (opts.uninstall && (opts.all || opts.
|
|
58
|
-
return "--uninstall cannot be combined with --all, --
|
|
63
|
+
if (opts.uninstall && (opts.all || opts.reinstall || opts.status)) {
|
|
64
|
+
return "--uninstall cannot be combined with --all, --reinstall, or --status.";
|
|
59
65
|
}
|
|
60
|
-
if (!opts.status && !opts.
|
|
66
|
+
if (!opts.status && !opts.reinstall && !opts.uninstall) {
|
|
61
67
|
const hasTarget = opts.all || opts.bin || opts.completions || opts.skill || opts.mcp;
|
|
62
68
|
if (!hasTarget) {
|
|
63
69
|
return "Specify at least one target: --all, --bin, --completions, --skill, or --mcp.";
|
|
@@ -114,31 +120,31 @@ function executePlan(
|
|
|
114
120
|
return changed;
|
|
115
121
|
}
|
|
116
122
|
|
|
117
|
-
/**
|
|
118
|
-
export async function
|
|
123
|
+
/** Runs install/reinstall/uninstall mutations without exiting the process. */
|
|
124
|
+
export async function runInstallMutation(
|
|
125
|
+
root: CliProgram,
|
|
126
|
+
rawOpts: Record<string, string>,
|
|
127
|
+
): Promise<{ changed: string[]; opts: InstallOpts; paths: ReturnType<typeof resolveInstallPaths> }> {
|
|
119
128
|
const opts = parseInstallOpts(rawOpts);
|
|
120
129
|
const err = validateOpts(opts);
|
|
121
130
|
if (err) {
|
|
122
|
-
|
|
123
|
-
process.exit(1);
|
|
131
|
+
throw new Error(err);
|
|
124
132
|
}
|
|
125
133
|
|
|
126
134
|
const paths = resolveInstallPaths(root, opts);
|
|
127
135
|
|
|
128
136
|
if (opts.status) {
|
|
129
137
|
printInstallStatus(root, opts);
|
|
130
|
-
|
|
138
|
+
return { changed: [], opts, paths };
|
|
131
139
|
}
|
|
132
140
|
|
|
133
|
-
// MCP conflict checks before planning
|
|
134
141
|
if (!opts.uninstall && resolveCapabilities(root).mcp && (opts.all || opts.mcp)) {
|
|
135
142
|
const entry = expectedMcpEntry(root);
|
|
136
143
|
const yes = !!opts.yes;
|
|
137
144
|
for (const p of [paths.cursorMcpPath, paths.claudeMcpPath]) {
|
|
138
145
|
const conflict = checkMcpConflict(p, paths.mcpName, entry, yes);
|
|
139
146
|
if (conflict) {
|
|
140
|
-
|
|
141
|
-
process.exit(1);
|
|
147
|
+
throw new Error(conflict);
|
|
142
148
|
}
|
|
143
149
|
}
|
|
144
150
|
}
|
|
@@ -146,37 +152,52 @@ export async function cliInstall(root: CliProgram, rawOpts: Record<string, strin
|
|
|
146
152
|
let actions: Array<InstallAction | UninstallAction>;
|
|
147
153
|
if (opts.uninstall) {
|
|
148
154
|
actions = buildUninstallPlan(root, paths, opts);
|
|
149
|
-
} else if (opts.
|
|
155
|
+
} else if (opts.reinstall) {
|
|
150
156
|
actions = buildUpdatePlan(root, paths, opts);
|
|
151
157
|
} else {
|
|
152
158
|
actions = buildInstallPlan(root, paths, opts);
|
|
153
159
|
}
|
|
154
160
|
|
|
155
161
|
if (actions.length === 0) {
|
|
156
|
-
|
|
157
|
-
process.exit(1);
|
|
162
|
+
throw new Error("Nothing to do.");
|
|
158
163
|
}
|
|
159
164
|
|
|
160
165
|
if (!opts.quiet && !opts.json) {
|
|
161
|
-
installOut("About to " + (opts.uninstall ? "remove" : opts.
|
|
166
|
+
installOut("About to " + (opts.uninstall ? "remove" : opts.reinstall ? "reinstall" : "install") + ":", opts);
|
|
162
167
|
for (const a of actions) {
|
|
163
168
|
installOut(" - " + a.summary, opts);
|
|
164
169
|
}
|
|
165
170
|
}
|
|
166
171
|
|
|
167
|
-
const autoYes = opts.yes || opts.json || opts.
|
|
172
|
+
const autoYes = opts.yes || opts.json || opts.reinstall;
|
|
168
173
|
if (!autoYes) {
|
|
169
174
|
if (!process.stdin.isTTY) {
|
|
170
|
-
|
|
171
|
-
process.exit(1);
|
|
175
|
+
throw new Error("Refusing to proceed without --yes (stdin is not a TTY).");
|
|
172
176
|
}
|
|
173
177
|
if (!promptConfirm()) {
|
|
174
|
-
|
|
175
|
-
process.exit(1);
|
|
178
|
+
throw new Error("Aborted.");
|
|
176
179
|
}
|
|
177
180
|
}
|
|
178
181
|
|
|
179
182
|
const changed = executePlan(root, actions, opts);
|
|
183
|
+
return { changed, opts, paths };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Main install command orchestrator. */
|
|
187
|
+
export async function cliInstall(root: CliProgram, rawOpts: Record<string, string>): Promise<never> {
|
|
188
|
+
let result: Awaited<ReturnType<typeof runInstallMutation>>;
|
|
189
|
+
try {
|
|
190
|
+
result = await runInstallMutation(root, rawOpts);
|
|
191
|
+
} catch (err) {
|
|
192
|
+
installErr(err instanceof Error ? err.message : String(err));
|
|
193
|
+
process.exit(1);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const { changed, opts, paths } = result;
|
|
197
|
+
|
|
198
|
+
if (opts.status) {
|
|
199
|
+
process.exit(0);
|
|
200
|
+
}
|
|
180
201
|
|
|
181
202
|
if (opts.json) {
|
|
182
203
|
process.stdout.write(JSON.stringify(changed, null, 2) + "\n");
|
|
@@ -184,9 +205,13 @@ export async function cliInstall(root: CliProgram, rawOpts: Record<string, strin
|
|
|
184
205
|
}
|
|
185
206
|
|
|
186
207
|
if (!opts.quiet && changed.length > 0) {
|
|
187
|
-
const verb = opts.uninstall ? "Removed" : opts.
|
|
208
|
+
const verb = opts.uninstall ? "Removed" : opts.reinstall ? "Reinstalled" : "Installed";
|
|
188
209
|
installOut(`${verb} ${changed.length} file(s).`, opts);
|
|
189
|
-
if (
|
|
210
|
+
if (
|
|
211
|
+
!opts.uninstall &&
|
|
212
|
+
(opts.all || opts.bin) &&
|
|
213
|
+
changed.some((p) => p === paths.bashRc || p === paths.zshRc || p === paths.binaryPath)
|
|
214
|
+
) {
|
|
190
215
|
installOut("Open a new shell, or run: hash -r (bash) / rehash (zsh)", opts);
|
|
191
216
|
}
|
|
192
217
|
}
|
package/src/install/plan.ts
CHANGED
|
@@ -15,7 +15,8 @@ export interface InstallOpts {
|
|
|
15
15
|
completions?: boolean;
|
|
16
16
|
skill?: boolean;
|
|
17
17
|
mcp?: boolean;
|
|
18
|
-
|
|
18
|
+
reinstall?: boolean;
|
|
19
|
+
from?: string;
|
|
19
20
|
status?: boolean;
|
|
20
21
|
uninstall?: boolean;
|
|
21
22
|
yes?: boolean;
|
|
@@ -35,7 +36,7 @@ export interface InstallAction {
|
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
function wantsBin(opts: InstallOpts): boolean {
|
|
38
|
-
return !!(opts.all || opts.bin || opts.
|
|
39
|
+
return !!(opts.all || opts.bin || opts.reinstall);
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
function wantsCompletions(opts: InstallOpts): boolean {
|
|
@@ -56,11 +57,12 @@ export function buildInstallPlan(root: CliProgram, paths: InstallPaths, opts: In
|
|
|
56
57
|
const dry = !!opts.dry;
|
|
57
58
|
|
|
58
59
|
if (wantsBin(opts)) {
|
|
60
|
+
const sourcePath = opts.from ?? process.execPath;
|
|
59
61
|
actions.push({
|
|
60
62
|
kind: "binary",
|
|
61
63
|
summary: `binary: ${paths.binaryPath}`,
|
|
62
64
|
message: `Installing binary to ${paths.binaryPath}`,
|
|
63
|
-
run: () => installBinary(root, paths, dry).changedFiles,
|
|
65
|
+
run: () => installBinary(root, paths, dry, sourcePath).changedFiles,
|
|
64
66
|
});
|
|
65
67
|
}
|
|
66
68
|
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { expect, test, beforeEach, afterEach } from "bun:test";
|
|
2
|
+
import { chmodSync, existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { cliPresentationRoot } from "../builtins/presentation.ts";
|
|
6
|
+
import { cliInvoke } from "../index.ts";
|
|
7
|
+
import type { CliProgram, CliUpdateArtifact } from "../types.ts";
|
|
8
|
+
import { cliValidateProgram } from "../validate.ts";
|
|
9
|
+
import { parseInstallOpts, runInstallMutation } from "./index.ts";
|
|
10
|
+
|
|
11
|
+
let home: string;
|
|
12
|
+
let prevHome: string | undefined;
|
|
13
|
+
let prevExecPath: string;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
home = mkdtempSync(join(tmpdir(), "argsbarg-update-"));
|
|
17
|
+
prevHome = process.env.HOME;
|
|
18
|
+
process.env.HOME = home;
|
|
19
|
+
prevExecPath = process.execPath;
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
if (prevHome === undefined) delete process.env.HOME;
|
|
24
|
+
else process.env.HOME = prevHome;
|
|
25
|
+
process.execPath = prevExecPath;
|
|
26
|
+
rmSync(home, { recursive: true, force: true });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
function fixtureWithUpdate(hook: () => Promise<CliUpdateArtifact>): CliProgram {
|
|
30
|
+
return {
|
|
31
|
+
key: "testapp",
|
|
32
|
+
version: "1.0.0",
|
|
33
|
+
description: "Test",
|
|
34
|
+
install: { updateGetLatest: hook },
|
|
35
|
+
commands: [{ key: "run", description: "Run", handler: () => {} }],
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
test("update reserved when updateGetLatest is set", () => {
|
|
40
|
+
const root: CliProgram = {
|
|
41
|
+
...fixtureWithUpdate(async () => ({ path: process.execPath })),
|
|
42
|
+
commands: [{ key: "update", description: "conflict", handler: () => {} }],
|
|
43
|
+
};
|
|
44
|
+
expect(() => cliValidateProgram(root)).toThrow(/Reserved command name: update/);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("presentation includes update builtin when hook is set", () => {
|
|
48
|
+
const root = fixtureWithUpdate(async () => ({ path: process.execPath }));
|
|
49
|
+
const presentation = cliPresentationRoot(root);
|
|
50
|
+
expect(presentation.commands.some((c) => c.key === "update")).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("parseInstallOpts maps deprecated --update to reinstall", () => {
|
|
54
|
+
const opts = parseInstallOpts({ update: "1" });
|
|
55
|
+
expect(opts.reinstall).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("runInstallMutation honors --from for binary copy", async () => {
|
|
59
|
+
const root: CliProgram = {
|
|
60
|
+
key: "testapp",
|
|
61
|
+
version: "1.0.0",
|
|
62
|
+
description: "Test",
|
|
63
|
+
handler: () => {},
|
|
64
|
+
};
|
|
65
|
+
const source = join(home, "new-binary");
|
|
66
|
+
writeFileSync(source, "#!/bin/sh\necho hi\n", "utf8");
|
|
67
|
+
chmodSync(source, 0o755);
|
|
68
|
+
|
|
69
|
+
const { changed } = await runInstallMutation(root, {
|
|
70
|
+
reinstall: "1",
|
|
71
|
+
yes: "1",
|
|
72
|
+
quiet: "1",
|
|
73
|
+
from: source,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const dest = join(home, ".local", "bin", "testapp");
|
|
77
|
+
expect(changed).toContain(dest);
|
|
78
|
+
expect(readFileSync(dest, "utf8")).toContain("echo hi");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("cliInvoke update uses hook and reinstalls", async () => {
|
|
82
|
+
const source = join(home, "new-binary");
|
|
83
|
+
writeFileSync(source, "#!/bin/sh\necho hi\n", "utf8");
|
|
84
|
+
chmodSync(source, 0o755);
|
|
85
|
+
|
|
86
|
+
const root = fixtureWithUpdate(async () => ({
|
|
87
|
+
path: source,
|
|
88
|
+
version: "2.0.0",
|
|
89
|
+
}));
|
|
90
|
+
|
|
91
|
+
const result = await cliInvoke(root, ["update"]);
|
|
92
|
+
expect(result.exitCode).toBe(0);
|
|
93
|
+
expect(result.stdout).toContain("Updated testapp 1.0.0 → 2.0.0");
|
|
94
|
+
expect(existsSync(join(home, ".local", "bin", "testapp"))).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("cliInvoke update reports already current", async () => {
|
|
98
|
+
const root = fixtureWithUpdate(async () => ({
|
|
99
|
+
path: process.execPath,
|
|
100
|
+
version: "1.0.0",
|
|
101
|
+
}));
|
|
102
|
+
|
|
103
|
+
const result = await cliInvoke(root, ["update"]);
|
|
104
|
+
expect(result.exitCode).toBe(0);
|
|
105
|
+
expect(result.stdout).toContain("Already at v1.0.0");
|
|
106
|
+
});
|