argsbarg 1.4.0 → 1.4.2
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/mcp_v1.2_invocation_and_extensions_a4f82c1e.plan.md +647 -0
- package/.cursor/plans/v1.3_parser_ergonomics_b3e91f02.plan.md +455 -0
- package/CHANGELOG.md +28 -1
- package/README.md +11 -7
- package/docs/mcp.md +96 -7
- package/examples/mcp-test.ts +66 -0
- package/index.d.ts +111 -33
- package/package.json +1 -1
- package/src/completion.ts +55 -1
- package/src/context.ts +42 -1
- package/src/help.ts +12 -2
- package/src/index.test.ts +648 -6
- package/src/index.ts +12 -1
- package/src/invoke.ts +1 -1
- package/src/mcp/env.ts +99 -0
- package/src/mcp/server.ts +34 -22
- package/src/mcp/tools.ts +59 -6
- package/src/mcp.ts +4 -0
- package/src/parse.ts +77 -10
- package/src/runtime.ts +1 -1
- package/src/types.ts +66 -11
- package/src/validate.ts +54 -16
package/src/index.ts
CHANGED
|
@@ -7,8 +7,19 @@ It gives consumers one stable import path without forcing them to know the inter
|
|
|
7
7
|
module layout.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
export { cliInvoke } from "./invoke.ts";
|
|
11
|
+
export type { CliInvokeKind, CliInvokeResult } from "./invoke.ts";
|
|
10
12
|
export { CliContext } from "./context.ts";
|
|
11
13
|
export { cliErrWithHelp, cliRun } from "./runtime";
|
|
12
14
|
export { CliFallbackMode, CliOptionKind, CliSchemaValidationError } from "./types.ts";
|
|
13
|
-
export type {
|
|
15
|
+
export type {
|
|
16
|
+
CliCommand,
|
|
17
|
+
CliHandler,
|
|
18
|
+
CliInvocation,
|
|
19
|
+
CliMcpResource,
|
|
20
|
+
CliMcpServerConfig,
|
|
21
|
+
CliMcpToolConfig,
|
|
22
|
+
CliOption,
|
|
23
|
+
CliPositional,
|
|
24
|
+
} from "./types.ts";
|
|
14
25
|
export { isInteractiveTty } from "./utils.ts";
|
package/src/invoke.ts
CHANGED
|
@@ -108,7 +108,7 @@ export async function cliInvoke(root: CliCommand, argv: string[]): Promise<CliIn
|
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
const handler = current.handler;
|
|
111
|
-
const ctx = new CliContext(root.key, pr.path, pr.args, pr.opts, root);
|
|
111
|
+
const ctx = new CliContext(root.key, pr.path, pr.args, pr.opts, root, "mcp");
|
|
112
112
|
|
|
113
113
|
let stdout = "";
|
|
114
114
|
let stderr = "";
|
package/src/mcp/env.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/*
|
|
2
|
+
This module bootstraps process.env for MCP servers from login shell and .env files.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { readFileSync } from "node:fs";
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
|
|
8
|
+
/** Parses `env` stdout from a login shell into a key/value map. */
|
|
9
|
+
export function captureShellEnv(shell: string): Record<string, string> {
|
|
10
|
+
const result = spawnSync(shell, ["-l", "-c", "env"], {
|
|
11
|
+
encoding: "utf8",
|
|
12
|
+
timeout: 5000,
|
|
13
|
+
});
|
|
14
|
+
if (result.error || result.status !== 0) {
|
|
15
|
+
return {};
|
|
16
|
+
}
|
|
17
|
+
const env: Record<string, string> = {};
|
|
18
|
+
for (const line of result.stdout.split("\n")) {
|
|
19
|
+
const eq = line.indexOf("=");
|
|
20
|
+
if (eq > 0) {
|
|
21
|
+
env[line.slice(0, eq)] = line.slice(eq + 1);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return env;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Merges captured shell env into process.env (PATH merged; host wins for other keys). */
|
|
28
|
+
export function applyShellEnv(env: Record<string, string>): void {
|
|
29
|
+
for (const [key, val] of Object.entries(env)) {
|
|
30
|
+
if (key === "PATH") {
|
|
31
|
+
const existing = process.env.PATH ?? "";
|
|
32
|
+
const existingParts = new Set(existing.split(":"));
|
|
33
|
+
const shellOnly = val.split(":").filter((p) => p.length > 0 && !existingParts.has(p));
|
|
34
|
+
if (shellOnly.length > 0) {
|
|
35
|
+
process.env.PATH = [...shellOnly, existing].join(":");
|
|
36
|
+
}
|
|
37
|
+
} else if (process.env[key] === undefined) {
|
|
38
|
+
process.env[key] = val;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Loads a .env file into process.env (always overwrites). Warns on stderr if missing. */
|
|
44
|
+
export function loadEnvFile(envFile: string): void {
|
|
45
|
+
const resolved = envFile.startsWith("~")
|
|
46
|
+
? envFile.replace("~", process.env.HOME ?? "")
|
|
47
|
+
: envFile;
|
|
48
|
+
let text: string;
|
|
49
|
+
try {
|
|
50
|
+
text = readFileSync(resolved, "utf8");
|
|
51
|
+
} catch {
|
|
52
|
+
process.stderr.write(`[argsbarg] envFile not found: ${envFile}\n`);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
for (const line of text.split("\n")) {
|
|
56
|
+
const trimmed = line.trim();
|
|
57
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const eq = trimmed.indexOf("=");
|
|
61
|
+
if (eq < 1) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const key = trimmed.slice(0, eq).trim();
|
|
65
|
+
let val = trimmed.slice(eq + 1).trim();
|
|
66
|
+
if (
|
|
67
|
+
(val.startsWith('"') && val.endsWith('"')) ||
|
|
68
|
+
(val.startsWith("'") && val.endsWith("'"))
|
|
69
|
+
) {
|
|
70
|
+
val = val.slice(1, -1);
|
|
71
|
+
}
|
|
72
|
+
if (key) {
|
|
73
|
+
process.env[key] = val;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Applies mcpServer shellEnv and envFile bootstrap in order. */
|
|
79
|
+
export function bootstrapMcpEnv(config: {
|
|
80
|
+
shellEnv?: boolean | string;
|
|
81
|
+
envFile?: string;
|
|
82
|
+
}): void {
|
|
83
|
+
const shellEnvCfg = config.shellEnv;
|
|
84
|
+
if (shellEnvCfg) {
|
|
85
|
+
const shell =
|
|
86
|
+
typeof shellEnvCfg === "string"
|
|
87
|
+
? shellEnvCfg
|
|
88
|
+
: (process.env.SHELL ?? (process.platform === "darwin" ? "/bin/zsh" : "/bin/bash"));
|
|
89
|
+
const captured = captureShellEnv(shell);
|
|
90
|
+
if (Object.keys(captured).length === 0) {
|
|
91
|
+
process.stderr.write(`[argsbarg] shellEnv: failed to capture shell environment from ${shell}\n`);
|
|
92
|
+
} else {
|
|
93
|
+
applyShellEnv(captured);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (config.envFile) {
|
|
97
|
+
loadEnvFile(config.envFile);
|
|
98
|
+
}
|
|
99
|
+
}
|
package/src/mcp/server.ts
CHANGED
|
@@ -4,13 +4,12 @@ resources, and ping. Responses are newline-delimited JSON on stdout only.
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { cliInvoke } from "../invoke.ts";
|
|
7
|
-
import { cliSchemaJson } from "../schema.ts";
|
|
8
7
|
import { CliCommand } from "../types.ts";
|
|
9
8
|
import { buildToolCallSuccess } from "./result.ts";
|
|
10
9
|
import {
|
|
10
|
+
allMcpResources,
|
|
11
11
|
collectMcpTools,
|
|
12
12
|
mcpToolCallToArgv,
|
|
13
|
-
resolveMcpSchemaUri,
|
|
14
13
|
resolveMcpServerInfo,
|
|
15
14
|
} from "./tools.ts";
|
|
16
15
|
|
|
@@ -118,6 +117,18 @@ async function handleRequestLine(root: CliCommand, line: string): Promise<void>
|
|
|
118
117
|
writeError(id, -32602, `Unknown tool: ${name}`);
|
|
119
118
|
return;
|
|
120
119
|
}
|
|
120
|
+
const missingEnv = (tool.leaf.mcpTool?.requiresEnv ?? []).filter((k) => !process.env[k]);
|
|
121
|
+
if (missingEnv.length > 0) {
|
|
122
|
+
writeResponse({
|
|
123
|
+
jsonrpc: "2.0",
|
|
124
|
+
id,
|
|
125
|
+
result: {
|
|
126
|
+
content: [{ type: "text", text: `Missing required env: ${missingEnv.join(", ")}` }],
|
|
127
|
+
isError: true,
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
121
132
|
const argvResult = mcpToolCallToArgv(root, tool, (rawArgs ?? {}) as Record<string, unknown>);
|
|
122
133
|
if ("error" in argvResult) {
|
|
123
134
|
writeResponse({
|
|
@@ -152,21 +163,13 @@ async function handleRequestLine(root: CliCommand, line: string): Promise<void>
|
|
|
152
163
|
}
|
|
153
164
|
|
|
154
165
|
if (method === "resources/list") {
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
uri,
|
|
163
|
-
name: "cli-schema",
|
|
164
|
-
description: "Full CLI command tree (same as --schema).",
|
|
165
|
-
mimeType: "application/json",
|
|
166
|
-
},
|
|
167
|
-
],
|
|
168
|
-
},
|
|
169
|
-
});
|
|
166
|
+
const resources = allMcpResources(root).map((r) => ({
|
|
167
|
+
uri: r.uri,
|
|
168
|
+
name: r.name,
|
|
169
|
+
description: r.description,
|
|
170
|
+
mimeType: r.mimeType,
|
|
171
|
+
}));
|
|
172
|
+
writeResponse({ jsonrpc: "2.0", id, result: { resources } });
|
|
170
173
|
return;
|
|
171
174
|
}
|
|
172
175
|
|
|
@@ -176,20 +179,29 @@ async function handleRequestLine(root: CliCommand, line: string): Promise<void>
|
|
|
176
179
|
writeError(id, -32602, "Invalid params: uri required");
|
|
177
180
|
return;
|
|
178
181
|
}
|
|
179
|
-
const
|
|
180
|
-
|
|
182
|
+
const all = allMcpResources(root);
|
|
183
|
+
const found = all.find((r) => r.uri === uri);
|
|
184
|
+
if (!found) {
|
|
181
185
|
writeError(id, -32602, `Unknown resource: ${uri}`);
|
|
182
186
|
return;
|
|
183
187
|
}
|
|
188
|
+
let text: string;
|
|
189
|
+
try {
|
|
190
|
+
text = found.load();
|
|
191
|
+
} catch (err) {
|
|
192
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
193
|
+
writeError(id, -32603, `Resource load failed: ${message}`);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
184
196
|
writeResponse({
|
|
185
197
|
jsonrpc: "2.0",
|
|
186
198
|
id,
|
|
187
199
|
result: {
|
|
188
200
|
contents: [
|
|
189
201
|
{
|
|
190
|
-
uri:
|
|
191
|
-
mimeType:
|
|
192
|
-
text
|
|
202
|
+
uri: found.uri,
|
|
203
|
+
mimeType: found.mimeType,
|
|
204
|
+
text,
|
|
193
205
|
},
|
|
194
206
|
],
|
|
195
207
|
},
|
package/src/mcp/tools.ts
CHANGED
|
@@ -6,6 +6,7 @@ flat JSON tool arguments into argv for cliInvoke.
|
|
|
6
6
|
import { readFileSync } from "node:fs";
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
import { collectOptionDefs } from "../parse.ts";
|
|
9
|
+
import { cliSchemaJson } from "../schema.ts";
|
|
9
10
|
import { CliCommand, CliOption, CliOptionKind, CliPositional } from "../types.ts";
|
|
10
11
|
|
|
11
12
|
/** Default URI for the CLI schema MCP resource. */
|
|
@@ -54,6 +55,8 @@ function optionProperty(opt: CliOption): Record<string, unknown> {
|
|
|
54
55
|
return { type: "string", ...base };
|
|
55
56
|
case CliOptionKind.Number:
|
|
56
57
|
return { type: "number", ...base };
|
|
58
|
+
case CliOptionKind.Enum:
|
|
59
|
+
return { type: "string", enum: opt.choices, ...base };
|
|
57
60
|
}
|
|
58
61
|
}
|
|
59
62
|
|
|
@@ -98,6 +101,48 @@ function buildInputSchema(root: CliCommand, path: string[], leaf: CliCommand): R
|
|
|
98
101
|
return schema;
|
|
99
102
|
}
|
|
100
103
|
|
|
104
|
+
/** Resolves MCP tool description with optional override and requiresEnv suffix. */
|
|
105
|
+
function resolveToolDescription(root: CliCommand, path: string[], leaf: CliCommand): string {
|
|
106
|
+
if (leaf.mcpTool?.description) {
|
|
107
|
+
return leaf.mcpTool.description;
|
|
108
|
+
}
|
|
109
|
+
let desc = mcpToolDescription(path, root.key, leaf.description);
|
|
110
|
+
const env = leaf.mcpTool?.requiresEnv;
|
|
111
|
+
if (env && env.length > 0) {
|
|
112
|
+
desc += ` [requires env: ${env.join(", ")}]`;
|
|
113
|
+
}
|
|
114
|
+
return desc;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** One resolved MCP resource (built-in or user-defined). */
|
|
118
|
+
export interface McpResourceEntry {
|
|
119
|
+
uri: string;
|
|
120
|
+
name: string;
|
|
121
|
+
description?: string;
|
|
122
|
+
mimeType: string;
|
|
123
|
+
load: () => string;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Returns built-in schema resource plus user mcpServer.resources. */
|
|
127
|
+
export function allMcpResources(root: CliCommand): McpResourceEntry[] {
|
|
128
|
+
const schemaUri = resolveMcpSchemaUri(root);
|
|
129
|
+
const builtIn: McpResourceEntry = {
|
|
130
|
+
uri: schemaUri,
|
|
131
|
+
name: "cli-schema",
|
|
132
|
+
description: "Full CLI command tree (same as --schema).",
|
|
133
|
+
mimeType: "application/json",
|
|
134
|
+
load: () => cliSchemaJson(root),
|
|
135
|
+
};
|
|
136
|
+
const user = (root.mcpServer?.resources ?? []).map((r) => ({
|
|
137
|
+
uri: r.uri,
|
|
138
|
+
name: r.name,
|
|
139
|
+
description: r.description,
|
|
140
|
+
mimeType: r.mimeType ?? "text/plain",
|
|
141
|
+
load: r.load,
|
|
142
|
+
}));
|
|
143
|
+
return [builtIn, ...user];
|
|
144
|
+
}
|
|
145
|
+
|
|
101
146
|
/** Recursively collects MCP tool definitions from user leaf commands. */
|
|
102
147
|
export function collectMcpTools(root: CliCommand): McpToolDef[] {
|
|
103
148
|
const out: McpToolDef[] = [];
|
|
@@ -113,7 +158,7 @@ export function collectMcpTools(root: CliCommand): McpToolDef[] {
|
|
|
113
158
|
}
|
|
114
159
|
out.push({
|
|
115
160
|
name: mcpToolName(root, path),
|
|
116
|
-
description:
|
|
161
|
+
description: resolveToolDescription(root, path, cmd),
|
|
117
162
|
path,
|
|
118
163
|
leaf: cmd,
|
|
119
164
|
inputSchema: buildInputSchema(root, path, cmd),
|
|
@@ -187,12 +232,20 @@ export function mcpToolCallToArgv(
|
|
|
187
232
|
const { argMin = 1, argMax = 1 } = p;
|
|
188
233
|
|
|
189
234
|
if (argMax === 0) {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
235
|
+
const raw = args[p.name];
|
|
236
|
+
let items: string[];
|
|
237
|
+
if (Array.isArray(raw)) {
|
|
238
|
+
items = raw.map(String);
|
|
239
|
+
} else if (typeof raw === "string") {
|
|
240
|
+
items = raw.includes(",")
|
|
241
|
+
? raw.split(",").map((s) => s.trim()).filter(Boolean)
|
|
242
|
+
: raw.trim()
|
|
243
|
+
? [raw.trim()]
|
|
244
|
+
: [];
|
|
245
|
+
} else {
|
|
246
|
+
items = [];
|
|
195
247
|
}
|
|
248
|
+
argv.push(...items);
|
|
196
249
|
continue;
|
|
197
250
|
}
|
|
198
251
|
|
package/src/mcp.ts
CHANGED
|
@@ -3,6 +3,7 @@ This module starts the ArgsBarg MCP stdio server for opt-in program roots.
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { mcpServeStdioLoop } from "./mcp/server.ts";
|
|
6
|
+
import { bootstrapMcpEnv } from "./mcp/env.ts";
|
|
6
7
|
import { CliCommand } from "./types.ts";
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -11,6 +12,9 @@ import { CliCommand } from "./types.ts";
|
|
|
11
12
|
*/
|
|
12
13
|
export async function cliMcpServeStdio(root: CliCommand): Promise<never> {
|
|
13
14
|
try {
|
|
15
|
+
if (root.mcpServer) {
|
|
16
|
+
bootstrapMcpEnv(root.mcpServer);
|
|
17
|
+
}
|
|
14
18
|
await mcpServeStdioLoop(root);
|
|
15
19
|
process.exit(0);
|
|
16
20
|
} catch (err) {
|
package/src/parse.ts
CHANGED
|
@@ -231,11 +231,6 @@ export function collectOptionDefs(root: CliCommand, path: string[]): CliOption[]
|
|
|
231
231
|
return defs;
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
-
/** True when every positional slot has bounded arity (no `argMax: 0` varargs tail). */
|
|
235
|
-
function allowsTrailingOptions(positionals: CliCommand["positionals"]): boolean {
|
|
236
|
-
return (positionals ?? []).every((p) => (p.argMax ?? 1) !== 0);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
234
|
/** Fills `args` for a leaf from `startIdx` according to `node.positionals`. */
|
|
240
235
|
function finishLeaf(
|
|
241
236
|
node: CliCommand,
|
|
@@ -244,7 +239,7 @@ function finishLeaf(
|
|
|
244
239
|
path: string[],
|
|
245
240
|
opts: Record<string, string>,
|
|
246
241
|
optionDefs: CliOption[],
|
|
247
|
-
|
|
242
|
+
forcePositionalsIn: boolean,
|
|
248
243
|
): ParseResult {
|
|
249
244
|
/** Builds a parse error for positional consumption failures. */
|
|
250
245
|
function errorResult(msg: string): ParseResult {
|
|
@@ -263,6 +258,7 @@ function finishLeaf(
|
|
|
263
258
|
|
|
264
259
|
let idx = startIdx;
|
|
265
260
|
const args: string[] = [];
|
|
261
|
+
let forcePositionals = forcePositionalsIn;
|
|
266
262
|
|
|
267
263
|
for (const p of node.positionals ?? []) {
|
|
268
264
|
const { argMin = 1, argMax = 1 } = p;
|
|
@@ -288,9 +284,37 @@ function finishLeaf(
|
|
|
288
284
|
let count = 0;
|
|
289
285
|
if (argMax === 0) {
|
|
290
286
|
while (idx < argv.length) {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
287
|
+
const tok = argv[idx];
|
|
288
|
+
|
|
289
|
+
if (!forcePositionals && tok === "--") {
|
|
290
|
+
forcePositionals = true;
|
|
291
|
+
idx++;
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (!forcePositionals && isHelpTok(tok)) {
|
|
296
|
+
return helpResult(path, true);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (!forcePositionals && tok.startsWith("-")) {
|
|
300
|
+
// MUST be false — lenient mode swallows unknown flags as positionals silently
|
|
301
|
+
const tailRep = consumeOptions(optionDefs, false, argv, idx, opts);
|
|
302
|
+
if (tailRep.report.err) {
|
|
303
|
+
return errorResult(tailRep.report.err);
|
|
304
|
+
}
|
|
305
|
+
if (tailRep.report.sawDoubleDash) {
|
|
306
|
+
forcePositionals = true;
|
|
307
|
+
}
|
|
308
|
+
if (tailRep.nextIndex > idx) {
|
|
309
|
+
idx = tailRep.nextIndex;
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
return errorResult(`Unexpected option token: ${tok}`);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
args.push(tok);
|
|
316
|
+
idx++;
|
|
317
|
+
count++;
|
|
294
318
|
}
|
|
295
319
|
} else {
|
|
296
320
|
while (count < argMax && idx < argv.length) {
|
|
@@ -305,7 +329,7 @@ function finishLeaf(
|
|
|
305
329
|
}
|
|
306
330
|
|
|
307
331
|
if (idx < argv.length) {
|
|
308
|
-
if (forcePositionals
|
|
332
|
+
if (forcePositionals) {
|
|
309
333
|
return errorResult("Unexpected extra arguments");
|
|
310
334
|
}
|
|
311
335
|
|
|
@@ -483,6 +507,19 @@ export function parse(root: CliCommand, argv: string[]): ParseResult {
|
|
|
483
507
|
|
|
484
508
|
if (i >= argv.length) {
|
|
485
509
|
if ((current.commands ?? []).length > 0) {
|
|
510
|
+
const fb = current.fallbackCommand;
|
|
511
|
+
const fm = current.fallbackMode ?? CliFallbackMode.MissingOnly;
|
|
512
|
+
if (
|
|
513
|
+
fb !== undefined &&
|
|
514
|
+
(fm === CliFallbackMode.MissingOnly || fm === CliFallbackMode.MissingOrUnknown)
|
|
515
|
+
) {
|
|
516
|
+
const fbNode = findChild(current.commands ?? [], fb);
|
|
517
|
+
if (fbNode) {
|
|
518
|
+
path.push(fb);
|
|
519
|
+
current = fbNode;
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
486
523
|
return helpResult(path, false);
|
|
487
524
|
}
|
|
488
525
|
return finishLeaf(current, i, argv, path, opts, collectOptionDefs(root, path), forcePositionals);
|
|
@@ -513,6 +550,21 @@ export function parse(root: CliCommand, argv: string[]): ParseResult {
|
|
|
513
550
|
}
|
|
514
551
|
|
|
515
552
|
if ((current.commands ?? []).length > 0) {
|
|
553
|
+
const fb = current.fallbackCommand;
|
|
554
|
+
const fm = current.fallbackMode ?? CliFallbackMode.MissingOnly;
|
|
555
|
+
const canRouteUnknown =
|
|
556
|
+
fb !== undefined &&
|
|
557
|
+
(fm === CliFallbackMode.MissingOrUnknown || fm === CliFallbackMode.UnknownOnly);
|
|
558
|
+
|
|
559
|
+
if (canRouteUnknown) {
|
|
560
|
+
const fbNode = findChild(current.commands ?? [], fb!);
|
|
561
|
+
if (fbNode) {
|
|
562
|
+
path.push(fb!);
|
|
563
|
+
current = fbNode;
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
516
568
|
return {
|
|
517
569
|
kind: ParseKind.Error,
|
|
518
570
|
path,
|
|
@@ -601,6 +653,21 @@ export function postParseValidate(root: CliCommand, pr: ParseResult): ParseResul
|
|
|
601
653
|
};
|
|
602
654
|
}
|
|
603
655
|
}
|
|
656
|
+
if (d.kind === CliOptionKind.Enum) {
|
|
657
|
+
const choices = d.choices ?? [];
|
|
658
|
+
if (!choices.includes(v)) {
|
|
659
|
+
return {
|
|
660
|
+
kind: ParseKind.Error,
|
|
661
|
+
path: pr.path,
|
|
662
|
+
opts: {},
|
|
663
|
+
args: [],
|
|
664
|
+
helpExplicit: false,
|
|
665
|
+
helpPath: [],
|
|
666
|
+
errorMsg: `Option --${k}: '${v}' is not one of: ${choices.join(", ")}`,
|
|
667
|
+
errorHelpPath: pr.path,
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
}
|
|
604
671
|
}
|
|
605
672
|
|
|
606
673
|
return pr;
|
package/src/runtime.ts
CHANGED
|
@@ -136,7 +136,7 @@ export async function cliRun(root: CliCommand, argv: string[] = process.argv.sli
|
|
|
136
136
|
process.exit(1);
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
const ctx = new CliContext(parseRoot.key, pr.path, pr.args, pr.opts, parseRoot);
|
|
139
|
+
const ctx = new CliContext(parseRoot.key, pr.path, pr.args, pr.opts, parseRoot, "cli");
|
|
140
140
|
try {
|
|
141
141
|
await Promise.resolve(current.handler(ctx));
|
|
142
142
|
process.exit(0);
|
package/src/types.ts
CHANGED
|
@@ -9,7 +9,12 @@ It gives the package one shared model for both library users and internal module
|
|
|
9
9
|
import type { CliContext } from "./context.ts";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
12
|
+
* How a leaf handler was dispatched.
|
|
13
|
+
*/
|
|
14
|
+
export type CliInvocation = "cli" | "mcp";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Option kinds: presence (boolean flag), string (free-form text), number (strict double), or enum (fixed choices).
|
|
13
18
|
*/
|
|
14
19
|
export enum CliOptionKind {
|
|
15
20
|
/** Boolean flag: no value token (may be implicit `"1"` when set). */
|
|
@@ -18,24 +23,25 @@ export enum CliOptionKind {
|
|
|
18
23
|
String = "string",
|
|
19
24
|
/** Strict floating-point value (parsed at validation time). */
|
|
20
25
|
Number = "number",
|
|
26
|
+
/** Fixed set of allowed string values. Requires non-empty `choices` on the option. */
|
|
27
|
+
Enum = "enum",
|
|
21
28
|
}
|
|
22
29
|
|
|
23
30
|
/**
|
|
24
|
-
* When fallbackCommand is used for missing or unknown
|
|
25
|
-
* 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.
|
|
26
32
|
*/
|
|
27
33
|
export enum CliFallbackMode {
|
|
28
34
|
/**
|
|
29
|
-
* If argv has no
|
|
35
|
+
* If argv has no next subcommand, route to `fallbackCommand`; if the token is unknown, error.
|
|
30
36
|
*/
|
|
31
37
|
MissingOnly = "missingOnly",
|
|
32
38
|
/**
|
|
33
|
-
* If argv has no
|
|
39
|
+
* If argv has no next subcommand or the token is not a known child, route to `fallbackCommand`.
|
|
34
40
|
*/
|
|
35
41
|
MissingOrUnknown = "missingOrUnknown",
|
|
36
42
|
/**
|
|
37
|
-
* If the
|
|
38
|
-
* 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).
|
|
39
45
|
*/
|
|
40
46
|
UnknownOnly = "unknownOnly",
|
|
41
47
|
}
|
|
@@ -54,6 +60,11 @@ export interface CliOption {
|
|
|
54
60
|
shortName?: string;
|
|
55
61
|
/** Whether this option must be provided. Cannot be used with Presence kind. */
|
|
56
62
|
required?: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Allowed values. Required when kind === Enum; ignored otherwise.
|
|
65
|
+
* Must be a non-empty array of distinct non-empty strings.
|
|
66
|
+
*/
|
|
67
|
+
choices?: string[];
|
|
57
68
|
}
|
|
58
69
|
|
|
59
70
|
/**
|
|
@@ -88,6 +99,39 @@ export interface CliMcpServerConfig {
|
|
|
88
99
|
version?: string;
|
|
89
100
|
/** Resource URI for schema export (default: `"argsbarg://schema"`). */
|
|
90
101
|
schemaResourceUri?: string;
|
|
102
|
+
/**
|
|
103
|
+
* Capture the user's login shell environment at MCP server start and merge it
|
|
104
|
+
* into process.env. Solves missing PATH, nvm/rbenv shims, Homebrew binaries,
|
|
105
|
+
* and shell exports that MCP hosts (e.g. Cursor) don't inherit.
|
|
106
|
+
*/
|
|
107
|
+
shellEnv?: boolean | string;
|
|
108
|
+
/**
|
|
109
|
+
* Path to a .env file loaded into process.env at MCP server start, after shellEnv.
|
|
110
|
+
* Supports `~` expansion. Warns on stderr if the file does not exist.
|
|
111
|
+
* Always overwrites — envFile is authoritative for its keys.
|
|
112
|
+
*/
|
|
113
|
+
envFile?: string;
|
|
114
|
+
/**
|
|
115
|
+
* Custom MCP resources exposed alongside the built-in argsbarg://schema resource.
|
|
116
|
+
* URIs must be unique and must not equal schemaResourceUri.
|
|
117
|
+
*/
|
|
118
|
+
resources?: CliMcpResource[];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* A custom MCP resource exposed under resources/list and resources/read.
|
|
123
|
+
*/
|
|
124
|
+
export interface CliMcpResource {
|
|
125
|
+
/** Resource URI (must be unique; must not equal schemaResourceUri). */
|
|
126
|
+
uri: string;
|
|
127
|
+
/** Short display name for resources/list. */
|
|
128
|
+
name: string;
|
|
129
|
+
/** Optional human description for resources/list. */
|
|
130
|
+
description?: string;
|
|
131
|
+
/** MIME type (default: "text/plain"). */
|
|
132
|
+
mimeType?: string;
|
|
133
|
+
/** Called at resources/read time; must return the resource body. */
|
|
134
|
+
load: () => string;
|
|
91
135
|
}
|
|
92
136
|
|
|
93
137
|
/**
|
|
@@ -96,6 +140,17 @@ export interface CliMcpServerConfig {
|
|
|
96
140
|
export interface CliMcpToolConfig {
|
|
97
141
|
/** When `false`, omit from `tools/list` (default: exposed). */
|
|
98
142
|
enabled?: boolean;
|
|
143
|
+
/**
|
|
144
|
+
* Override the generated MCP tool description.
|
|
145
|
+
* Default: auto-generated from command path and description.
|
|
146
|
+
*/
|
|
147
|
+
description?: string;
|
|
148
|
+
/**
|
|
149
|
+
* Environment variable names required at runtime.
|
|
150
|
+
* Appended to auto-generated MCP tool descriptions; enforced at tools/call time.
|
|
151
|
+
* Empty string counts as absent.
|
|
152
|
+
*/
|
|
153
|
+
requiresEnv?: string[];
|
|
99
154
|
}
|
|
100
155
|
|
|
101
156
|
/**
|
|
@@ -130,17 +185,17 @@ export type CliCommand =
|
|
|
130
185
|
positionals?: CliPositional[];
|
|
131
186
|
/** Nested subcommands (empty for leaf commands). */
|
|
132
187
|
commands?: never;
|
|
133
|
-
/** Default
|
|
188
|
+
/** Default subcommand (routing commands only). */
|
|
134
189
|
fallbackCommand?: never;
|
|
135
|
-
/** How fallbackCommand is applied (routing commands only). */
|
|
190
|
+
/** How fallbackCommand is applied at this routing node (routing commands only). */
|
|
136
191
|
fallbackMode?: never;
|
|
137
192
|
})
|
|
138
193
|
| (CliCommandBase & {
|
|
139
194
|
/** Nested subcommands. */
|
|
140
195
|
commands: CliCommand[];
|
|
141
|
-
/** Default
|
|
196
|
+
/** Default subcommand when argv omits a command or uses an unknown token at this routing node. */
|
|
142
197
|
fallbackCommand?: string;
|
|
143
|
-
/** How fallbackCommand is applied. */
|
|
198
|
+
/** How fallbackCommand is applied at this routing node (not root-only). */
|
|
144
199
|
fallbackMode?: CliFallbackMode;
|
|
145
200
|
/** Handler function (leaf commands only). */
|
|
146
201
|
handler?: never;
|