argsbarg 1.4.1 → 1.4.3
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/.cursor/plans/v1.3_parser_ergonomics_b3e91f02.plan.md +455 -0
- package/.private/scratch.md +2 -1
- package/CHANGELOG.md +27 -1
- package/README.md +20 -6
- package/docs/ai-skills.md +75 -0
- package/docs/mcp.md +12 -10
- package/index.d.ts +26 -12
- package/package.json +1 -1
- package/src/ai.ts +7 -0
- package/src/completion.ts +50 -9
- package/src/context.ts +38 -0
- package/src/index.test.ts +458 -12
- package/src/mcp/tools.ts +14 -6
- package/src/parse.ts +62 -10
- package/src/runtime.ts +42 -23
- package/src/skill/generate.ts +183 -0
- package/src/skill/install.ts +45 -0
- package/src/types.ts +23 -12
- package/src/validate.ts +21 -15
package/src/runtime.ts
CHANGED
|
@@ -7,9 +7,10 @@ It keeps execution flow out of the public barrel so the exported API stays small
|
|
|
7
7
|
the runtime responsibilities remain easy to reason about.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import { cliBuiltinAiGroup, cliBuiltinCompletionGroup, cliPresentationRoot, completionBashScript, completionZshScript } from "./completion.ts";
|
|
11
11
|
import { CliContext } from "./context.ts";
|
|
12
12
|
import { cliHelpRender } from "./help.ts";
|
|
13
|
+
import { cliSkillInstall } from "./skill/install.ts";
|
|
13
14
|
import { cliMcpServeStdio } from "./mcp.ts";
|
|
14
15
|
import { parse, postParseValidate, ParseKind } from "./parse.ts";
|
|
15
16
|
import { cliSchemaJson } from "./schema.ts";
|
|
@@ -44,29 +45,27 @@ export async function cliRun(root: CliCommand, argv: string[] = process.argv.sli
|
|
|
44
45
|
process.exit(1);
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (root.handler && argv.length >= 1 && argv[0] === "mcp" && !root.mcpServer) {
|
|
51
|
-
process.stderr.write("Unknown command: mcp\n");
|
|
48
|
+
if (argv.length >= 2 && argv[0] === "ai" && argv[1] === "mcp" && !root.mcpServer) {
|
|
49
|
+
process.stderr.write("MCP is not enabled. Set mcpServer on the program root.\n");
|
|
52
50
|
process.exit(1);
|
|
53
51
|
}
|
|
54
52
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
53
|
+
let parseRoot = root;
|
|
54
|
+
let isLeafCompletionIntercept = false;
|
|
55
|
+
|
|
56
|
+
if (root.handler && argv.length >= 1 && argv[0] === "ai") {
|
|
59
57
|
parseRoot = {
|
|
60
58
|
key: root.key,
|
|
61
59
|
description: root.description,
|
|
62
|
-
commands: [
|
|
63
|
-
} as
|
|
64
|
-
} else if (root.handler && argv.length >= 1 && argv[0] === "
|
|
60
|
+
commands: [cliBuiltinAiGroup(root)],
|
|
61
|
+
} as CliCommand;
|
|
62
|
+
} else if (root.handler && argv.length >= 1 && argv[0] === "completion") {
|
|
63
|
+
isLeafCompletionIntercept = true;
|
|
65
64
|
parseRoot = {
|
|
66
65
|
key: root.key,
|
|
67
66
|
description: root.description,
|
|
68
|
-
commands: [
|
|
69
|
-
} as
|
|
67
|
+
commands: [cliBuiltinCompletionGroup(root.key)],
|
|
68
|
+
} as any;
|
|
70
69
|
} else {
|
|
71
70
|
parseRoot = cliRootMergedWithBuiltins(root);
|
|
72
71
|
}
|
|
@@ -109,16 +108,36 @@ export async function cliRun(root: CliCommand, argv: string[] = process.argv.sli
|
|
|
109
108
|
}
|
|
110
109
|
}
|
|
111
110
|
|
|
112
|
-
if (pr.path[0] === "
|
|
113
|
-
if (
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
111
|
+
if (pr.path[0] === "ai") {
|
|
112
|
+
if (pr.path[1] === "mcp") {
|
|
113
|
+
if (!root.mcpServer) {
|
|
114
|
+
process.stderr.write("MCP is not enabled. Set mcpServer on the program root.\n");
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
if (pr.path.length !== 2) {
|
|
118
|
+
process.stderr.write("Unknown subcommand: ai " + pr.path.slice(1).join(" ") + "\n");
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
await cliMcpServeStdio(root);
|
|
122
|
+
} else if (pr.path[1] === "skill" && (pr.path[2] === "cursor" || pr.path[2] === "claude")) {
|
|
123
|
+
if (root.aiSkill?.enabled === false) {
|
|
124
|
+
process.stderr.write("AI skills are disabled. Remove aiSkill.enabled: false from the program root.\n");
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
if (pr.path.length !== 3) {
|
|
128
|
+
process.stderr.write("Unknown subcommand: ai " + pr.path.slice(1).join(" ") + "\n");
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
const msg = cliSkillInstall(root, pr.path[2], {
|
|
132
|
+
global: pr.opts.global === "1",
|
|
133
|
+
force: pr.opts.force === "1",
|
|
134
|
+
});
|
|
135
|
+
process.stderr.write(msg + "\n");
|
|
136
|
+
process.exit(0);
|
|
137
|
+
} else {
|
|
138
|
+
process.stderr.write("Unknown subcommand: ai " + pr.path.slice(1).join(" ") + "\n");
|
|
119
139
|
process.exit(1);
|
|
120
140
|
}
|
|
121
|
-
await cliMcpServeStdio(root);
|
|
122
141
|
}
|
|
123
142
|
|
|
124
143
|
let current = parseRoot;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/*
|
|
2
|
+
This module generates Agent Skills content (SKILL.md + reference.md) from a CLI schema.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { collectOptionDefs } from "../parse.ts";
|
|
6
|
+
import { cliSchemaJson } from "../schema.ts";
|
|
7
|
+
import { collectMcpTools, sanitizeToolSegment } from "../mcp/tools.ts";
|
|
8
|
+
import { CliCommand, CliOptionKind } from "../types.ts";
|
|
9
|
+
|
|
10
|
+
export type SkillTarget = "cursor" | "claude";
|
|
11
|
+
|
|
12
|
+
export interface SkillBundle {
|
|
13
|
+
dirName: string;
|
|
14
|
+
skillMd: string;
|
|
15
|
+
referenceMd: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Truncates text to maxLen with ellipsis. */
|
|
19
|
+
function truncate(text: string, maxLen: number): string {
|
|
20
|
+
if (text.length <= maxLen) return text;
|
|
21
|
+
return text.slice(0, maxLen - 1) + "…";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Builds third-person skill description for YAML frontmatter. */
|
|
25
|
+
function skillDescription(root: CliCommand): string {
|
|
26
|
+
const tools = collectMcpTools(root);
|
|
27
|
+
const paths = tools.map((t) => (t.path.length > 0 ? t.path.join(" ") : root.key));
|
|
28
|
+
const sample = paths.slice(0, 5).join(", ");
|
|
29
|
+
const more = paths.length > 5 ? `, and ${paths.length - 5} more` : "";
|
|
30
|
+
const desc = `Operates the ${root.key} CLI (${sample}${more}). Use when the user mentions ${root.key}${paths.length > 0 ? `, ${paths.slice(0, 3).join(", ")}` : ""}, or related tasks.`;
|
|
31
|
+
return truncate(desc, 1024);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Formats one command line for the catalog section. */
|
|
35
|
+
function formatCommandEntry(root: CliCommand, tool: ReturnType<typeof collectMcpTools>[number]): string {
|
|
36
|
+
const cliPath = tool.path.length > 0 ? `${root.key} ${tool.path.join(" ")}` : root.key;
|
|
37
|
+
let line = `- **\`${cliPath}\`** — ${tool.description}`;
|
|
38
|
+
const opts = collectOptionDefs(root, tool.path);
|
|
39
|
+
const flags = opts.filter((o) => o.kind === CliOptionKind.Presence).map((o) => `--${o.name}`);
|
|
40
|
+
if (flags.length > 0) {
|
|
41
|
+
line += ` (flags: ${flags.join(", ")})`;
|
|
42
|
+
}
|
|
43
|
+
const enums = opts.filter((o) => o.kind === CliOptionKind.Enum && o.choices?.length);
|
|
44
|
+
for (const e of enums) {
|
|
45
|
+
line += ` (\`--${e.name}\`: ${e.choices!.join(" | ")})`;
|
|
46
|
+
}
|
|
47
|
+
const varargs = (tool.leaf.positionals ?? []).filter((p) => (p.argMax ?? 1) === 0);
|
|
48
|
+
if (varargs.length > 0) {
|
|
49
|
+
line += ` (varargs: ${varargs.map((p) => p.name).join(", ")})`;
|
|
50
|
+
}
|
|
51
|
+
return line;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Builds SKILL.md body for the given target. */
|
|
55
|
+
function buildSkillMd(root: CliCommand, target: SkillTarget, dirName: string): string {
|
|
56
|
+
const name = root.aiSkill?.name ?? sanitizeToolSegment(root.key);
|
|
57
|
+
const description = skillDescription(root);
|
|
58
|
+
const tools = collectMcpTools(root);
|
|
59
|
+
|
|
60
|
+
const lines: string[] = [
|
|
61
|
+
"---",
|
|
62
|
+
`name: ${name}`,
|
|
63
|
+
`description: ${description}`,
|
|
64
|
+
"---",
|
|
65
|
+
"",
|
|
66
|
+
`# ${root.key}`,
|
|
67
|
+
"",
|
|
68
|
+
root.description,
|
|
69
|
+
"",
|
|
70
|
+
"## When to use",
|
|
71
|
+
"",
|
|
72
|
+
`Use this skill when working with **${root.key}** — shell commands, automation, or agent tool calls for this application.`,
|
|
73
|
+
"",
|
|
74
|
+
"## Execution",
|
|
75
|
+
"",
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
if (root.mcpServer !== undefined) {
|
|
79
|
+
lines.push(
|
|
80
|
+
"**Prefer MCP** when a host has the server connected:",
|
|
81
|
+
"",
|
|
82
|
+
"```bash",
|
|
83
|
+
`${root.key} ai mcp`,
|
|
84
|
+
"```",
|
|
85
|
+
"",
|
|
86
|
+
"Example Cursor `mcp.json` entry:",
|
|
87
|
+
"",
|
|
88
|
+
"```json",
|
|
89
|
+
JSON.stringify(
|
|
90
|
+
{
|
|
91
|
+
mcpServers: {
|
|
92
|
+
[root.mcpServer.name ?? root.key]: {
|
|
93
|
+
command: root.key,
|
|
94
|
+
args: ["ai", "mcp"],
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
null,
|
|
99
|
+
2,
|
|
100
|
+
),
|
|
101
|
+
"```",
|
|
102
|
+
"",
|
|
103
|
+
"When MCP tools are available, use `tools/call` with flat JSON arguments. Read the schema resource for full shapes.",
|
|
104
|
+
"",
|
|
105
|
+
"Otherwise invoke via shell:",
|
|
106
|
+
"",
|
|
107
|
+
);
|
|
108
|
+
} else {
|
|
109
|
+
lines.push("Invoke via shell:", "");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
lines.push("```bash", `${root.key} <subcommand> [options] [args]`, "```", "", "## Commands", "");
|
|
113
|
+
|
|
114
|
+
if (tools.length === 0) {
|
|
115
|
+
lines.push("(No leaf commands in schema.)", "");
|
|
116
|
+
} else {
|
|
117
|
+
for (const tool of tools) {
|
|
118
|
+
lines.push(formatCommandEntry(root, tool));
|
|
119
|
+
}
|
|
120
|
+
lines.push("");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
lines.push(
|
|
124
|
+
"## Pitfalls",
|
|
125
|
+
"",
|
|
126
|
+
"- Use `--` before tokens that look like flags when they are positional arguments.",
|
|
127
|
+
"- Under MCP (`ctx.invocation === \"mcp\"`), child processes must not inherit stdout — use piped stdout.",
|
|
128
|
+
"- Required environment variables are listed per command in descriptions (`requires env`).",
|
|
129
|
+
"",
|
|
130
|
+
"## Reference",
|
|
131
|
+
"",
|
|
132
|
+
"See `reference.md` in this skill directory for the full `--schema` JSON export.",
|
|
133
|
+
"",
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
if (target === "cursor") {
|
|
137
|
+
lines.push(
|
|
138
|
+
"## Cursor install location",
|
|
139
|
+
"",
|
|
140
|
+
`- Project: \`.cursor/skills/${dirName}/\``,
|
|
141
|
+
`- Global: \`~/.cursor/skills/${dirName}/\``,
|
|
142
|
+
"",
|
|
143
|
+
"Do not install under `~/.cursor/skills-cursor/` (reserved for Cursor built-ins).",
|
|
144
|
+
"",
|
|
145
|
+
);
|
|
146
|
+
} else {
|
|
147
|
+
lines.push(
|
|
148
|
+
"## Claude Code",
|
|
149
|
+
"",
|
|
150
|
+
`- Invoke with \`/${dirName}\` or let Claude auto-match from the description.`,
|
|
151
|
+
`- Project skills: \`.claude/skills/${dirName}/\``,
|
|
152
|
+
`- Global skills: \`~/.claude/skills/${dirName}/\``,
|
|
153
|
+
`- Bundled files in this directory are available via \`\${CLAUDE_SKILL_DIR}\` when the skill runs.`,
|
|
154
|
+
"",
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return lines.join("\n");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Builds reference.md with pretty-printed schema JSON. */
|
|
162
|
+
function buildReferenceMd(root: CliCommand): string {
|
|
163
|
+
return [
|
|
164
|
+
`# ${root.key} — CLI reference`,
|
|
165
|
+
"",
|
|
166
|
+
"Generated from the program `--schema` export. Handlers and runtime-only nodes are omitted.",
|
|
167
|
+
"",
|
|
168
|
+
"```json",
|
|
169
|
+
cliSchemaJson(root).trimEnd(),
|
|
170
|
+
"```",
|
|
171
|
+
"",
|
|
172
|
+
].join("\n");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Generates SKILL.md and reference.md for Cursor or Claude Code. */
|
|
176
|
+
export function generateSkillBundle(root: CliCommand, target: SkillTarget): SkillBundle {
|
|
177
|
+
const dirName = root.aiSkill?.name ?? sanitizeToolSegment(root.key);
|
|
178
|
+
return {
|
|
179
|
+
dirName,
|
|
180
|
+
skillMd: buildSkillMd(root, target, dirName),
|
|
181
|
+
referenceMd: buildReferenceMd(root),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/*
|
|
2
|
+
This module installs generated Agent Skills to Cursor or Claude Code skill directories.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { CliCommand } from "../types.ts";
|
|
9
|
+
import { generateSkillBundle, type SkillTarget } from "./generate.ts";
|
|
10
|
+
|
|
11
|
+
export interface SkillInstallOpts {
|
|
12
|
+
global?: boolean;
|
|
13
|
+
force?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Resolves the user home directory (`$HOME` when set). */
|
|
17
|
+
function userHome(): string {
|
|
18
|
+
return process.env.HOME ?? homedir();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Resolves the install directory for a skill target. */
|
|
22
|
+
function resolveSkillDir(target: SkillTarget, dirName: string, global: boolean): string {
|
|
23
|
+
const base = global
|
|
24
|
+
? join(userHome(), target === "cursor" ? ".cursor" : ".claude", "skills")
|
|
25
|
+
: join(process.cwd(), target === "cursor" ? ".cursor" : ".claude", "skills");
|
|
26
|
+
return join(base, dirName);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Writes SKILL.md and reference.md to the target skills directory. */
|
|
30
|
+
export function cliSkillInstall(root: CliCommand, target: SkillTarget, opts: SkillInstallOpts): string {
|
|
31
|
+
const bundle = generateSkillBundle(root, target);
|
|
32
|
+
const dir = resolveSkillDir(target, bundle.dirName, opts.global ?? false);
|
|
33
|
+
|
|
34
|
+
if (existsSync(dir) && !opts.force) {
|
|
35
|
+
process.stderr.write(`Skill directory already exists: ${dir}\nUse --force to overwrite.\n`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
mkdirSync(dir, { recursive: true });
|
|
40
|
+
writeFileSync(join(dir, "SKILL.md"), bundle.skillMd, "utf8");
|
|
41
|
+
writeFileSync(join(dir, "reference.md"), bundle.referenceMd, "utf8");
|
|
42
|
+
|
|
43
|
+
const label = target === "cursor" ? "Cursor" : "Claude Code";
|
|
44
|
+
return `Installed ${label} skill to ${dir}/`;
|
|
45
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -28,21 +28,20 @@ export enum CliOptionKind {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
|
-
* When fallbackCommand is used for missing or unknown
|
|
32
|
-
* Only the program root may set a non-default mode or a non-nil fallbackCommand.
|
|
31
|
+
* When `fallbackCommand` is used for missing or unknown subcommand tokens at a routing node.
|
|
33
32
|
*/
|
|
34
33
|
export enum CliFallbackMode {
|
|
35
34
|
/**
|
|
36
|
-
* If argv has no
|
|
35
|
+
* If argv has no next subcommand, route to `fallbackCommand`; if the token is unknown, error.
|
|
37
36
|
*/
|
|
38
37
|
MissingOnly = "missingOnly",
|
|
39
38
|
/**
|
|
40
|
-
* If argv has no
|
|
39
|
+
* If argv has no next subcommand or the token is not a known child, route to `fallbackCommand`.
|
|
41
40
|
*/
|
|
42
41
|
MissingOrUnknown = "missingOrUnknown",
|
|
43
42
|
/**
|
|
44
|
-
* If the
|
|
45
|
-
* When the
|
|
43
|
+
* If the next token is present but not a known child, route to `fallbackCommand`.
|
|
44
|
+
* When the subcommand token is missing (exhausted argv), do not use fallback (implicit scoped help).
|
|
46
45
|
*/
|
|
47
46
|
UnknownOnly = "unknownOnly",
|
|
48
47
|
}
|
|
@@ -91,7 +90,7 @@ export interface CliPositional {
|
|
|
91
90
|
}
|
|
92
91
|
|
|
93
92
|
/**
|
|
94
|
-
* Root-only. Enables `myapp mcp` and MCP stdio server metadata.
|
|
93
|
+
* Root-only. Enables `myapp ai mcp` and MCP stdio server metadata.
|
|
95
94
|
*/
|
|
96
95
|
export interface CliMcpServerConfig {
|
|
97
96
|
/** `initialize` serverInfo.name (default: root `key`). */
|
|
@@ -154,6 +153,16 @@ export interface CliMcpToolConfig {
|
|
|
154
153
|
requiresEnv?: string[];
|
|
155
154
|
}
|
|
156
155
|
|
|
156
|
+
/**
|
|
157
|
+
* Root-only. Opt out of `ai skill` install commands with `{ enabled: false }`.
|
|
158
|
+
*/
|
|
159
|
+
export interface CliAiSkillConfig {
|
|
160
|
+
/** When `false`, disable `ai skill *` install commands (default: enabled). */
|
|
161
|
+
enabled?: boolean;
|
|
162
|
+
/** Skill directory name (default: sanitized root `key`). */
|
|
163
|
+
name?: string;
|
|
164
|
+
}
|
|
165
|
+
|
|
157
166
|
/**
|
|
158
167
|
* Base properties shared by all command nodes.
|
|
159
168
|
*/
|
|
@@ -166,8 +175,10 @@ export interface CliCommandBase {
|
|
|
166
175
|
notes?: string;
|
|
167
176
|
/** Global or command-level flags/options. */
|
|
168
177
|
options?: CliOption[];
|
|
169
|
-
/** Root-only. When set, enables the `mcp` built-in subcommand. */
|
|
178
|
+
/** Root-only. When set, enables the `ai mcp` built-in subcommand. */
|
|
170
179
|
mcpServer?: CliMcpServerConfig;
|
|
180
|
+
/** Root-only. Opt out of `ai skill` install with `{ enabled: false }`. */
|
|
181
|
+
aiSkill?: CliAiSkillConfig;
|
|
171
182
|
/** Leaf-only. Per-tool MCP exposure and metadata. */
|
|
172
183
|
mcpTool?: CliMcpToolConfig;
|
|
173
184
|
}
|
|
@@ -186,17 +197,17 @@ export type CliCommand =
|
|
|
186
197
|
positionals?: CliPositional[];
|
|
187
198
|
/** Nested subcommands (empty for leaf commands). */
|
|
188
199
|
commands?: never;
|
|
189
|
-
/** Default
|
|
200
|
+
/** Default subcommand (routing commands only). */
|
|
190
201
|
fallbackCommand?: never;
|
|
191
|
-
/** How fallbackCommand is applied (routing commands only). */
|
|
202
|
+
/** How fallbackCommand is applied at this routing node (routing commands only). */
|
|
192
203
|
fallbackMode?: never;
|
|
193
204
|
})
|
|
194
205
|
| (CliCommandBase & {
|
|
195
206
|
/** Nested subcommands. */
|
|
196
207
|
commands: CliCommand[];
|
|
197
|
-
/** Default
|
|
208
|
+
/** Default subcommand when argv omits a command or uses an unknown token at this routing node. */
|
|
198
209
|
fallbackCommand?: string;
|
|
199
|
-
/** How fallbackCommand is applied. */
|
|
210
|
+
/** How fallbackCommand is applied at this routing node (not root-only). */
|
|
200
211
|
fallbackMode?: CliFallbackMode;
|
|
201
212
|
/** Handler function (leaf commands only). */
|
|
202
213
|
handler?: never;
|
package/src/validate.ts
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
} from "./types.ts";
|
|
15
15
|
import { MCP_SCHEMA_URI_DEFAULT } from "./mcp/tools.ts";
|
|
16
16
|
|
|
17
|
-
const reservedCommandNames = ["completion", "
|
|
17
|
+
const reservedCommandNames = ["completion", "ai"];
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Validates the static CliCommand tree against ArgBarg rules.
|
|
@@ -35,25 +35,15 @@ export function cliValidateRoot(root: CliCommand): void {
|
|
|
35
35
|
|
|
36
36
|
/** Recursively validates a command node: handlers vs children, options, and positionals. */
|
|
37
37
|
function walkCommand(cmd: CliCommand, isRoot: boolean = false): void {
|
|
38
|
-
|
|
39
|
-
if (!isRoot && cmd.fallbackCommand !== undefined) {
|
|
40
|
-
throw new CliSchemaValidationError(
|
|
41
|
-
"Fallback is only supported on the program root (not on " + cmd.key + ")",
|
|
42
|
-
);
|
|
43
|
-
}
|
|
44
|
-
if (
|
|
45
|
-
!isRoot &&
|
|
46
|
-
cmd.fallbackMode !== undefined &&
|
|
47
|
-
cmd.fallbackMode !== CliFallbackMode.MissingOnly
|
|
48
|
-
) {
|
|
38
|
+
if (!isRoot && cmd.mcpServer !== undefined) {
|
|
49
39
|
throw new CliSchemaValidationError(
|
|
50
|
-
"
|
|
40
|
+
"mcpServer is only supported on the program root (not on " + cmd.key + ")",
|
|
51
41
|
);
|
|
52
42
|
}
|
|
53
43
|
|
|
54
|
-
if (!isRoot && cmd.
|
|
44
|
+
if (!isRoot && cmd.aiSkill !== undefined) {
|
|
55
45
|
throw new CliSchemaValidationError(
|
|
56
|
-
"
|
|
46
|
+
"aiSkill is only supported on the program root (not on " + cmd.key + ")",
|
|
57
47
|
);
|
|
58
48
|
}
|
|
59
49
|
|
|
@@ -89,6 +79,22 @@ function walkCommand(cmd: CliCommand, isRoot: boolean = false): void {
|
|
|
89
79
|
seenNames.add(child.key);
|
|
90
80
|
}
|
|
91
81
|
|
|
82
|
+
if (cmd.fallbackMode !== undefined && cmd.fallbackCommand === undefined) {
|
|
83
|
+
throw new CliSchemaValidationError(
|
|
84
|
+
`fallbackMode requires fallbackCommand on '${cmd.key}'`,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (cmd.fallbackCommand !== undefined) {
|
|
89
|
+
const children = cmd.commands ?? [];
|
|
90
|
+
const valid = children.find((c) => c.key === cmd.fallbackCommand);
|
|
91
|
+
if (!valid) {
|
|
92
|
+
throw new CliSchemaValidationError(
|
|
93
|
+
`fallbackCommand '${cmd.fallbackCommand}' is not a child of '${cmd.key}'`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
92
98
|
// Validate options (short name uniqueness, reserved -h, required presence)
|
|
93
99
|
const seenShorts = new Set<string>();
|
|
94
100
|
for (const opt of cmd.options ?? []) {
|