argsbarg 3.1.0 → 3.3.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 +30 -1
- package/README.md +4 -6
- package/docs/ai-skills.md +1 -1
- package/docs/bundled-docs.md +18 -4
- package/docs/install.md +51 -4
- package/docs/mcp.md +4 -6
- package/examples/minimal.ts +6 -0
- package/examples/nested.ts +3 -0
- package/index.d.ts +93 -1
- package/package.json +1 -1
- package/plan.md +10 -185
- 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 +27 -0
- package/src/builtins/export.ts +4 -0
- package/src/builtins/install.ts +9 -3
- package/src/builtins/presentation.ts +4 -0
- package/src/builtins/shell-helpers.ts +0 -2
- package/src/builtins/update.ts +14 -0
- package/src/capabilities.ts +7 -1
- package/src/docs/api-guide.test.ts +55 -0
- package/src/docs/api-guide.ts +129 -0
- package/src/docs/builtin.ts +3 -0
- package/src/docs/docs.test.ts +47 -1
- package/src/docs/mcp-guide.ts +3 -3
- package/src/docs/resolve.ts +32 -1
- package/src/headless.test.ts +86 -0
- package/src/headless.ts +86 -0
- package/src/help.ts +3 -7
- package/src/index.test.ts +36 -65
- package/src/index.ts +20 -0
- package/src/install/binary.ts +8 -3
- package/src/install/gh-release-update.test.ts +22 -0
- package/src/install/gh-release-update.ts +229 -0
- 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 +1 -11
- package/src/mcp/tools.ts +1 -1
- package/src/parse.ts +0 -27
- package/src/runtime.ts +7 -6
- package/src/schema.ts +7 -2
- package/src/skill/generate.ts +2 -2
- package/src/types.ts +17 -0
- package/src/validate.ts +11 -6
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
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
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 update`). */
|
|
7
|
+
export async function cliUpdate(root: CliProgram): Promise<never> {
|
|
8
|
+
const hook = root.install?.updateGetLatest;
|
|
9
|
+
if (!hook) {
|
|
10
|
+
installErr("update is not configured. Set install.updateGetLatest on the program root.");
|
|
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(`updateGetLatest returned missing binary: ${JSON.stringify(artifact.path)}`);
|
|
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
|
+
quiet: "1",
|
|
40
|
+
from: artifact.path,
|
|
41
|
+
});
|
|
42
|
+
} catch (err) {
|
|
43
|
+
await artifact.cleanup?.();
|
|
44
|
+
installErr(err instanceof Error ? err.message : String(err));
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
await artifact.cleanup?.();
|
|
49
|
+
|
|
50
|
+
if (artifact.version !== undefined && artifact.version !== currentVersion) {
|
|
51
|
+
process.stdout.write(`Updated ${root.key} ${currentVersion} → ${artifact.version}\n`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
process.exit(0);
|
|
55
|
+
}
|
package/src/invoke.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { type CliNode, type CliProgram, isCliLeaf, isCliRouter } from "./types.t
|
|
|
12
12
|
import { format } from "node:util";
|
|
13
13
|
|
|
14
14
|
/** Outcome of a non-exiting CLI invocation. */
|
|
15
|
-
export type CliInvokeKind = "ok" | "help" | "
|
|
15
|
+
export type CliInvokeKind = "ok" | "help" | "error";
|
|
16
16
|
|
|
17
17
|
/** Result of cliInvoke: captured output and exit metadata without process.exit. */
|
|
18
18
|
export interface CliInvokeResult {
|
|
@@ -74,16 +74,6 @@ export async function cliInvoke(root: CliProgram, argv: string[]): Promise<CliIn
|
|
|
74
74
|
};
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
if (pr.kind === ParseKind.Schema) {
|
|
78
|
-
return {
|
|
79
|
-
kind: "schema",
|
|
80
|
-
exitCode: 1,
|
|
81
|
-
stdout: "",
|
|
82
|
-
stderr: "",
|
|
83
|
-
errorMsg: "Schema export is not available via MCP tool calls.",
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
|
|
87
77
|
if (pr.kind === ParseKind.Error) {
|
|
88
78
|
return {
|
|
89
79
|
kind: "error",
|
package/src/mcp/tools.ts
CHANGED
|
@@ -134,7 +134,7 @@ export function allMcpResources(root: CliProgram): McpResourceEntry[] {
|
|
|
134
134
|
const builtIn: McpResourceEntry = {
|
|
135
135
|
uri: schemaUri,
|
|
136
136
|
name: "cli-schema",
|
|
137
|
-
description: "Full CLI command tree (same as
|
|
137
|
+
description: "Full CLI command tree (same as docs schema).",
|
|
138
138
|
mimeType: "application/json",
|
|
139
139
|
load: () => cliSchemaJson(root),
|
|
140
140
|
};
|
package/src/parse.ts
CHANGED
|
@@ -29,8 +29,6 @@ export enum ParseKind {
|
|
|
29
29
|
Ok = "ok",
|
|
30
30
|
/** User requested help (explicit or implicit). */
|
|
31
31
|
Help = "help",
|
|
32
|
-
/** User requested machine-readable schema export (`--schema`). */
|
|
33
|
-
Schema = "schema",
|
|
34
32
|
/** User error (unknown command, bad option, etc.). */
|
|
35
33
|
Error = "error",
|
|
36
34
|
}
|
|
@@ -59,18 +57,12 @@ export interface ParseResult {
|
|
|
59
57
|
|
|
60
58
|
const helpShort = "-h";
|
|
61
59
|
const helpLong = "--help";
|
|
62
|
-
const schemaLong = "--schema";
|
|
63
60
|
|
|
64
61
|
/** Returns true if the argv token is `-h` or `--help`. */
|
|
65
62
|
function isHelpTok(tok: string): boolean {
|
|
66
63
|
return tok === helpShort || tok === helpLong;
|
|
67
64
|
}
|
|
68
65
|
|
|
69
|
-
/** Returns true if the argv token is `--schema`. */
|
|
70
|
-
function isSchemaTok(tok: string): boolean {
|
|
71
|
-
return tok === schemaLong;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
66
|
/** Looks up a subcommand or routing node by `key`. */
|
|
75
67
|
function findChild(cmds: CliNode[], name: string): CliNode | undefined {
|
|
76
68
|
return cmds.find((c) => c.key === name);
|
|
@@ -195,7 +187,6 @@ function consumeOptions(
|
|
|
195
187
|
const tok = argv[idx];
|
|
196
188
|
|
|
197
189
|
if (isHelpTok(tok)) break;
|
|
198
|
-
if (isSchemaTok(tok)) break;
|
|
199
190
|
if (!tok.startsWith("-")) break;
|
|
200
191
|
|
|
201
192
|
if (tok === "--") {
|
|
@@ -371,20 +362,6 @@ function helpResult(p: string[], explicit: boolean): ParseResult {
|
|
|
371
362
|
};
|
|
372
363
|
}
|
|
373
364
|
|
|
374
|
-
/** Builds a schema-export result for the program root. */
|
|
375
|
-
function schemaResult(): ParseResult {
|
|
376
|
-
return {
|
|
377
|
-
kind: ParseKind.Schema,
|
|
378
|
-
path: [],
|
|
379
|
-
opts: {},
|
|
380
|
-
args: [],
|
|
381
|
-
helpExplicit: false,
|
|
382
|
-
helpPath: [],
|
|
383
|
-
errorMsg: "",
|
|
384
|
-
errorHelpPath: [],
|
|
385
|
-
};
|
|
386
|
-
}
|
|
387
|
-
|
|
388
365
|
/**
|
|
389
366
|
* Parses `argv` against the program root, routing into subcommands and filling `opts` / `args`.
|
|
390
367
|
*/
|
|
@@ -420,10 +397,6 @@ export function parse(root: CliNode, argv: string[]): ParseResult {
|
|
|
420
397
|
return helpResult([], true);
|
|
421
398
|
}
|
|
422
399
|
|
|
423
|
-
if (i < argv.length && !forcePositionals && isSchemaTok(argv[i])) {
|
|
424
|
-
return schemaResult();
|
|
425
|
-
}
|
|
426
|
-
|
|
427
400
|
// Determine which subcommand to route to
|
|
428
401
|
let cmdName: string;
|
|
429
402
|
let node: CliNode | undefined;
|
package/src/runtime.ts
CHANGED
|
@@ -10,7 +10,6 @@ import { type CliNode, type CliProgram, isCliLeaf, isCliRouter } from "./types.t
|
|
|
10
10
|
import { CliContext } from "./context.ts";
|
|
11
11
|
import { cliHelpRender } from "./help.ts";
|
|
12
12
|
import { parse, postParseValidate, ParseKind } from "./parse.ts";
|
|
13
|
-
import { cliSchemaJson } from "./schema.ts";
|
|
14
13
|
import { cliValidateProgram } from "./validate.ts";
|
|
15
14
|
|
|
16
15
|
function cliRootMergedWithBuiltins(program: CliProgram): CliRouter {
|
|
@@ -41,6 +40,13 @@ export async function cliRun(program: CliProgram, argv: string[] = process.argv.
|
|
|
41
40
|
process.exit(1);
|
|
42
41
|
}
|
|
43
42
|
|
|
43
|
+
if (argv.length >= 1 && argv[0] === "update" && !caps.update) {
|
|
44
|
+
process.stderr.write(
|
|
45
|
+
"update is not enabled. Set install.updateGetLatest on the program root.\n",
|
|
46
|
+
);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
|
|
44
50
|
if (argv.length >= 1 && argv[0] === "docs" && !caps.docs) {
|
|
45
51
|
process.stderr.write("docs is not enabled. Set docs: { enabled: true } on the program root.\n");
|
|
46
52
|
process.exit(1);
|
|
@@ -73,11 +79,6 @@ export async function cliRun(program: CliProgram, argv: string[] = process.argv.
|
|
|
73
79
|
process.exit(pr.helpExplicit ? 0 : 1);
|
|
74
80
|
}
|
|
75
81
|
|
|
76
|
-
if (pr.kind === ParseKind.Schema) {
|
|
77
|
-
process.stdout.write(cliSchemaJson(program));
|
|
78
|
-
process.exit(0);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
82
|
if (pr.kind === "error") {
|
|
82
83
|
const color = process.stderr.isTTY;
|
|
83
84
|
const msg = color ? `\u001B[31m${pr.errorMsg}\u001B[0m` : pr.errorMsg;
|
package/src/schema.ts
CHANGED
|
@@ -5,7 +5,7 @@ This module serializes the CLI schema tree to JSON for machine-readable introspe
|
|
|
5
5
|
import { type CliNode, type CliProgram, isCliLeaf, isCliRouter } from "./types.ts";
|
|
6
6
|
import { exportPresentationBuiltins, type CliSchemaExport } from "./builtins/export.ts";
|
|
7
7
|
|
|
8
|
-
const RESERVED = new Set(["completion", "install", "docs", "mcp", "version"]);
|
|
8
|
+
const RESERVED = new Set(["completion", "install", "docs", "mcp", "version", "update"]);
|
|
9
9
|
|
|
10
10
|
function exportCommand(cmd: CliNode): CliSchemaExport {
|
|
11
11
|
const out: CliSchemaExport = {
|
|
@@ -44,8 +44,13 @@ function exportCommand(cmd: CliNode): CliSchemaExport {
|
|
|
44
44
|
return out;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
/** Returns the JSON-safe command tree (handlers omitted). */
|
|
48
|
+
export function cliSchemaExport(root: CliProgram): CliSchemaExport {
|
|
49
|
+
return exportCommand(root);
|
|
50
|
+
}
|
|
51
|
+
|
|
47
52
|
export function cliSchemaJson(root: CliProgram): string {
|
|
48
|
-
return JSON.stringify(
|
|
53
|
+
return JSON.stringify(cliSchemaExport(root), null, 2) + "\n";
|
|
49
54
|
}
|
|
50
55
|
|
|
51
56
|
export type { CliSchemaExport };
|
package/src/skill/generate.ts
CHANGED
|
@@ -96,7 +96,7 @@ function buildSkillMd(root: CliProgram, target: SkillTarget, dirName: string): s
|
|
|
96
96
|
"",
|
|
97
97
|
"## Reference",
|
|
98
98
|
"",
|
|
99
|
-
"See `reference.md` in this skill directory for the full
|
|
99
|
+
"See `reference.md` in this skill directory for the full `docs schema` JSON export.",
|
|
100
100
|
"",
|
|
101
101
|
);
|
|
102
102
|
|
|
@@ -130,7 +130,7 @@ function buildReferenceMd(root: CliProgram): string {
|
|
|
130
130
|
return [
|
|
131
131
|
`# ${root.key} — CLI reference`,
|
|
132
132
|
"",
|
|
133
|
-
"Generated from the program
|
|
133
|
+
"Generated from the program `docs schema` export. Handlers and runtime-only nodes are omitted.",
|
|
134
134
|
"",
|
|
135
135
|
"```json",
|
|
136
136
|
cliSchemaJson(root).trimEnd(),
|
package/src/types.ts
CHANGED
|
@@ -153,11 +153,28 @@ export interface CliMcpToolConfig {
|
|
|
153
153
|
/**
|
|
154
154
|
* Opt-out and defaults for the `install` built-in (program root only).
|
|
155
155
|
*/
|
|
156
|
+
export interface CliUpdateArtifact {
|
|
157
|
+
/** Path to an executable binary to copy into the install location. */
|
|
158
|
+
path: string;
|
|
159
|
+
/** Release version of `path` (used for already-current checks and success messages). */
|
|
160
|
+
version?: string;
|
|
161
|
+
/** Called after reinstall completes (e.g. remove a temp download directory). */
|
|
162
|
+
cleanup?: () => void | Promise<void>;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Fetches the latest release binary for the `update` built-in. */
|
|
166
|
+
export type CliUpdateGetLatest = (ctx: { version: string }) => Promise<CliUpdateArtifact>;
|
|
167
|
+
|
|
156
168
|
export interface CliInstallConfig {
|
|
157
169
|
/** When `false`, hide/disable `install` (default: enabled). */
|
|
158
170
|
enabled?: boolean;
|
|
159
171
|
/** Default bin directory (default: `~/.local/bin`). Overridden by `INSTALL_PREFIX` env and `--prefix`. */
|
|
160
172
|
prefix?: string;
|
|
173
|
+
/**
|
|
174
|
+
* When set, enables the `update` built-in (`myapp update`).
|
|
175
|
+
* Should download or locate the latest release binary and return its path.
|
|
176
|
+
*/
|
|
177
|
+
updateGetLatest?: CliUpdateGetLatest;
|
|
161
178
|
}
|
|
162
179
|
|
|
163
180
|
/**
|
package/src/validate.ts
CHANGED
|
@@ -63,6 +63,17 @@ export function cliValidateProgram(program: CliProgram): void {
|
|
|
63
63
|
validateDocsConfig(program.docs);
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
if (program.install?.updateGetLatest !== undefined) {
|
|
67
|
+
if (program.install.enabled === false) {
|
|
68
|
+
throw new CliSchemaValidationError(
|
|
69
|
+
"install.updateGetLatest requires install to be enabled (omit install.enabled: false)",
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
if (typeof program.install.updateGetLatest !== "function") {
|
|
73
|
+
throw new CliSchemaValidationError("install.updateGetLatest must be a function");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
66
77
|
const caps = resolveCapabilities(program);
|
|
67
78
|
const reserved = reservedCommandNames(caps);
|
|
68
79
|
|
|
@@ -166,12 +177,6 @@ function validateOptions(scopeKey: string, options: import("./types.ts").CliOpti
|
|
|
166
177
|
);
|
|
167
178
|
}
|
|
168
179
|
|
|
169
|
-
if (opt.name === "schema") {
|
|
170
|
-
throw new CliSchemaValidationError(
|
|
171
|
-
`Option name "schema" is reserved for --schema: ${scopeKey}/${opt.name}`,
|
|
172
|
-
);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
180
|
if (opt.shortName !== undefined) {
|
|
176
181
|
if (opt.shortName === "h") {
|
|
177
182
|
throw new CliSchemaValidationError(
|