@evantahler/mcpx 0.19.2 → 0.20.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/.claude/skills/mcpx.md +5 -0
- package/.cursor/rules/mcpx.mdc +5 -0
- package/README.md +32 -1
- package/package.json +1 -1
- package/src/commands/exec.ts +46 -16
- package/src/lib/input.ts +170 -0
package/.claude/skills/mcpx.md
CHANGED
|
@@ -47,6 +47,7 @@ This shows parameters, types, required fields, and the full JSON Schema.
|
|
|
47
47
|
```bash
|
|
48
48
|
mcpx exec <tool> '<json args>' # server auto-resolved if unambiguous
|
|
49
49
|
mcpx exec <server> <tool> '<json args>' # explicit server (required if tool name exists on multiple servers)
|
|
50
|
+
mcpx exec <server> <tool> -- --field value # shell-flag args (typed via the tool's input schema)
|
|
50
51
|
mcpx exec <server> <tool> -f params.json
|
|
51
52
|
```
|
|
52
53
|
|
|
@@ -78,6 +79,9 @@ mcpx exec Slack_SendMessage '{"channel":"#general","message":"hello"}'
|
|
|
78
79
|
# Or explicitly specify the server
|
|
79
80
|
mcpx exec arcade Slack_SendMessage '{"channel":"#general","message":"hello"}'
|
|
80
81
|
|
|
82
|
+
# Shell-flag form (anything after `--` is parsed against the tool's input schema)
|
|
83
|
+
mcpx exec arcade Slack_SendMessage -- --channel "#general" --message "hello"
|
|
84
|
+
|
|
81
85
|
# Chain commands — search repos and read the first result
|
|
82
86
|
mcpx exec github search_repositories '{"query":"mcp"}' \
|
|
83
87
|
| jq -r '.content[0].text | fromjson | .items[0].full_name' \
|
|
@@ -143,6 +147,7 @@ mcpx deauth <server> # remove stored auth
|
|
|
143
147
|
| `mcpx exec <server>` | List tools for a server |
|
|
144
148
|
| `mcpx exec <tool> '<json>'` | Execute tool (server auto-resolved) |
|
|
145
149
|
| `mcpx exec <server> <tool> '<json>'` | Execute tool (explicit server) |
|
|
150
|
+
| `mcpx exec <server> <tool> -- --k=v` | Execute with shell-flag args (typed via schema) |
|
|
146
151
|
| `mcpx exec <server> <tool> -f file` | Execute with args from file |
|
|
147
152
|
| `mcpx search "<query>"` | Search tools (keyword + semantic) |
|
|
148
153
|
| `mcpx search -k "<pattern>"` | Keyword/glob search only |
|
package/.cursor/rules/mcpx.mdc
CHANGED
|
@@ -27,6 +27,7 @@ This shows parameters, types, required fields, and the full JSON Schema.
|
|
|
27
27
|
```bash
|
|
28
28
|
mcpx exec <tool> '<json args>' # server auto-resolved if unambiguous
|
|
29
29
|
mcpx exec <server> <tool> '<json args>' # explicit server (required if tool name exists on multiple servers)
|
|
30
|
+
mcpx exec <server> <tool> -- --field value # shell-flag args (typed via the tool's input schema)
|
|
30
31
|
mcpx exec <server> <tool> -f params.json
|
|
31
32
|
```
|
|
32
33
|
|
|
@@ -58,6 +59,9 @@ mcpx exec Slack_SendMessage '{"channel":"#general","message":"hello"}'
|
|
|
58
59
|
# Or explicitly specify the server
|
|
59
60
|
mcpx exec arcade Slack_SendMessage '{"channel":"#general","message":"hello"}'
|
|
60
61
|
|
|
62
|
+
# Shell-flag form (anything after `--` is parsed against the tool's input schema)
|
|
63
|
+
mcpx exec arcade Slack_SendMessage -- --channel "#general" --message "hello"
|
|
64
|
+
|
|
61
65
|
# Chain commands — search repos and read the first result
|
|
62
66
|
mcpx exec github search_repositories '{"query":"mcp"}' \
|
|
63
67
|
| jq -r '.content[0].text | fromjson | .items[0].full_name' \
|
|
@@ -139,6 +143,7 @@ mcpx deauth <server> # remove stored auth
|
|
|
139
143
|
| `mcpx exec <server>` | List tools for a server |
|
|
140
144
|
| `mcpx exec <tool> '<json>'` | Execute tool (server auto-resolved) |
|
|
141
145
|
| `mcpx exec <server> <tool> '<json>'` | Execute tool (explicit server) |
|
|
146
|
+
| `mcpx exec <server> <tool> -- --k=v` | Execute with shell-flag args (typed via schema) |
|
|
142
147
|
| `mcpx exec <server> <tool> -f file` | Execute with args from file |
|
|
143
148
|
| `mcpx search "<query>"` | Search tools (keyword + semantic) |
|
|
144
149
|
| `mcpx search -k "<pattern>"` | Keyword/glob search only |
|
package/README.md
CHANGED
|
@@ -45,11 +45,15 @@ mcpx info github
|
|
|
45
45
|
# Inspect a specific tool
|
|
46
46
|
mcpx info github search_repositories
|
|
47
47
|
|
|
48
|
-
# Execute a tool
|
|
48
|
+
# Execute a tool (JSON args)
|
|
49
49
|
mcpx exec github search_repositories '{"query": "mcp server"}'
|
|
50
50
|
|
|
51
|
+
# Execute a tool with shell-style flags (anything after `--` is parsed against the tool's input schema)
|
|
52
|
+
mcpx exec github search_repositories -- --query "mcp server"
|
|
53
|
+
|
|
51
54
|
# Execute a tool without specifying the server (auto-resolved)
|
|
52
55
|
mcpx exec search_repositories '{"query": "mcp server"}'
|
|
56
|
+
mcpx exec search_repositories -- --query "mcp server"
|
|
53
57
|
|
|
54
58
|
# Search tools — combines keyword and semantic matching
|
|
55
59
|
mcpx search "post a ticket to linear"
|
|
@@ -80,6 +84,7 @@ mcpx search -n 5 "manage pull requests"
|
|
|
80
84
|
| `mcpx index -i` | Show index status |
|
|
81
85
|
| `mcpx exec <server> <tool> [json]` | Validate inputs locally, then execute tool |
|
|
82
86
|
| `mcpx exec <tool> [json]` | Execute tool (server auto-resolved if unambiguous) |
|
|
87
|
+
| `mcpx exec <server> <tool> -- --k=v` | Shell-flag args (typed via the tool's input schema) |
|
|
83
88
|
| `mcpx exec <server> <tool> -f file` | Read tool args from a JSON file |
|
|
84
89
|
| `mcpx exec <server>` | List available tools for a server |
|
|
85
90
|
| `mcpx auth <server>` | Authenticate with an HTTP MCP server (OAuth) |
|
|
@@ -505,6 +510,31 @@ Validation covers:
|
|
|
505
510
|
|
|
506
511
|
If a tool's `inputSchema` is unavailable (some servers don't provide one), execution proceeds without local validation.
|
|
507
512
|
|
|
513
|
+
### Shell-flag args
|
|
514
|
+
|
|
515
|
+
Anything after a `--` separator is parsed as shell flags using the tool's input schema for type coercion. This is handy for interactive use — you don't need to remember JSON quoting rules.
|
|
516
|
+
|
|
517
|
+
```bash
|
|
518
|
+
# JSON form
|
|
519
|
+
mcpx exec github create_issue '{"owner":"evantahler","repo":"mcpx","title":"bug"}'
|
|
520
|
+
|
|
521
|
+
# Equivalent shell-flag form
|
|
522
|
+
mcpx exec github create_issue -- --owner evantahler --repo mcpx --title bug
|
|
523
|
+
|
|
524
|
+
# --field=value also works
|
|
525
|
+
mcpx exec github create_issue -- --owner=evantahler --repo=mcpx --title=bug
|
|
526
|
+
|
|
527
|
+
# Booleans
|
|
528
|
+
mcpx exec my-server flagit -- --enabled # true
|
|
529
|
+
mcpx exec my-server flagit -- --no-enabled # false
|
|
530
|
+
|
|
531
|
+
# Arrays — repeatable flag or comma-split
|
|
532
|
+
mcpx exec my-server tag -- --label bug --label todo
|
|
533
|
+
mcpx exec my-server tag -- --label bug,todo
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
Type coercion follows the field's `type` in the input schema (`string`, `integer`, `number`, `boolean`, `array`). Nested objects must use the JSON form. Combining `--` shell flags with inline JSON args, `--file`, or stdin is rejected.
|
|
537
|
+
|
|
508
538
|
## Shell Output & Piping
|
|
509
539
|
|
|
510
540
|
Output is human-friendly by default, JSON when piped:
|
|
@@ -635,6 +665,7 @@ To discover tools:
|
|
|
635
665
|
To execute tools:
|
|
636
666
|
mcpx exec <tool> '<json args>' # server auto-resolved
|
|
637
667
|
mcpx exec <server> <tool> '<json args>' # explicit server
|
|
668
|
+
mcpx exec <server> <tool> -- --k=v # shell-flag args (typed via schema)
|
|
638
669
|
mcpx exec <server> <tool> -f params.json
|
|
639
670
|
|
|
640
671
|
Always search before executing — don't assume tool names.
|
package/package.json
CHANGED
package/src/commands/exec.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { Command } from "commander";
|
|
|
2
2
|
import type { ServerManager } from "../client/manager.ts";
|
|
3
3
|
import { DEFAULTS } from "../constants.ts";
|
|
4
4
|
import { getContext } from "../context.ts";
|
|
5
|
-
import { parseJsonArgs, readStdin } from "../lib/input.ts";
|
|
5
|
+
import { parseJsonArgs, parseShellArgs, readStdin } from "../lib/input.ts";
|
|
6
6
|
import {
|
|
7
7
|
formatCallResult,
|
|
8
8
|
formatError,
|
|
@@ -15,17 +15,25 @@ import { validateToolInput } from "../validation/schema.ts";
|
|
|
15
15
|
|
|
16
16
|
type ResolvedArgs =
|
|
17
17
|
| { mode: "list-tools"; server: string }
|
|
18
|
-
| {
|
|
18
|
+
| {
|
|
19
|
+
mode: "call-tool";
|
|
20
|
+
server: string;
|
|
21
|
+
tool: string;
|
|
22
|
+
rest: string[];
|
|
23
|
+
};
|
|
19
24
|
|
|
20
25
|
/**
|
|
21
26
|
* Resolve the positional args into either list-tools or call-tool mode.
|
|
22
27
|
* Supports both `exec <server> <tool> [args]` and `exec <tool> [args]`.
|
|
28
|
+
*
|
|
29
|
+
* `rest` is whatever positional tokens remain after `<server> <tool>`. It may contain
|
|
30
|
+
* a single inline JSON string, or shell-flag tokens (after `--`), or be empty.
|
|
23
31
|
*/
|
|
24
32
|
async function resolveExecArgs(
|
|
25
33
|
manager: ServerManager,
|
|
26
34
|
first: string,
|
|
27
35
|
second: string | undefined,
|
|
28
|
-
|
|
36
|
+
rest: string[],
|
|
29
37
|
): Promise<ResolvedArgs> {
|
|
30
38
|
const serverNames = manager.getServerNames();
|
|
31
39
|
const isServer = serverNames.includes(first);
|
|
@@ -60,10 +68,10 @@ async function resolveExecArgs(
|
|
|
60
68
|
}
|
|
61
69
|
}
|
|
62
70
|
|
|
63
|
-
return { mode: "call-tool", server: first, tool: second,
|
|
71
|
+
return { mode: "call-tool", server: first, tool: second, rest };
|
|
64
72
|
}
|
|
65
73
|
|
|
66
|
-
// Not a server name — treat first as a tool name
|
|
74
|
+
// Not a server name — treat first as a tool name; `second` is the start of `rest`.
|
|
67
75
|
const toolName = first;
|
|
68
76
|
const { tools } = await manager.getAllTools();
|
|
69
77
|
const matches = tools.filter((t) => t.tool.name === toolName);
|
|
@@ -79,28 +87,32 @@ async function resolveExecArgs(
|
|
|
79
87
|
);
|
|
80
88
|
}
|
|
81
89
|
|
|
82
|
-
|
|
90
|
+
const fullRest = second === undefined ? rest : [second, ...rest];
|
|
91
|
+
return { mode: "call-tool", server: matches[0]!.server, tool: toolName, rest: fullRest };
|
|
83
92
|
}
|
|
84
93
|
|
|
85
94
|
export function registerExecCommand(program: Command) {
|
|
86
95
|
program
|
|
87
|
-
.command("exec <
|
|
88
|
-
.description(
|
|
96
|
+
.command("exec <server> [tool] [args...]")
|
|
97
|
+
.description(
|
|
98
|
+
"execute a tool. server may be omitted if the tool name is unambiguous: `mcpx exec <tool> [args...]`. " +
|
|
99
|
+
"args may be a single JSON object string, or shell flags after `--` (e.g. `-- --field value`).",
|
|
100
|
+
)
|
|
89
101
|
.option("-f, --file <path>", "read JSON args from a file")
|
|
90
102
|
.option("--no-wait", "return task handle immediately without waiting for completion")
|
|
91
103
|
.option("--ttl <ms>", "task TTL in milliseconds", String(DEFAULTS.TASK_TTL_MS))
|
|
92
104
|
.action(
|
|
93
105
|
async (
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
106
|
+
serverOrTool: string,
|
|
107
|
+
toolOrFirstArg: string | undefined,
|
|
108
|
+
trailing: string[],
|
|
97
109
|
options: { file?: string; wait: boolean; ttl: string },
|
|
98
110
|
) => {
|
|
99
111
|
const { manager, formatOptions } = await getContext(program);
|
|
100
112
|
|
|
101
113
|
let resolved: ResolvedArgs;
|
|
102
114
|
try {
|
|
103
|
-
resolved = await resolveExecArgs(manager,
|
|
115
|
+
resolved = await resolveExecArgs(manager, serverOrTool, toolOrFirstArg, trailing);
|
|
104
116
|
} catch (err) {
|
|
105
117
|
console.error(formatError(String(err), formatOptions));
|
|
106
118
|
await manager.close();
|
|
@@ -120,15 +132,32 @@ export function registerExecCommand(program: Command) {
|
|
|
120
132
|
return;
|
|
121
133
|
}
|
|
122
134
|
|
|
123
|
-
const { server, tool,
|
|
135
|
+
const { server, tool, rest } = resolved;
|
|
124
136
|
|
|
125
137
|
try {
|
|
126
|
-
//
|
|
138
|
+
// Classify the trailing positional tokens. If the first one starts with `--`
|
|
139
|
+
// it's the shell-flag form; otherwise, treat a single token as inline JSON.
|
|
140
|
+
const isShellFlagForm = rest.length > 0 && rest[0]!.startsWith("--");
|
|
141
|
+
const argsStr = !isShellFlagForm && rest.length === 1 ? rest[0] : undefined;
|
|
142
|
+
const shellTokens = isShellFlagForm ? rest : [];
|
|
143
|
+
|
|
144
|
+
// More than one positional token without `--` flag prefix is ambiguous.
|
|
145
|
+
if (!isShellFlagForm && rest.length > 1) {
|
|
146
|
+
throw new Error("Cannot mix inline JSON args with shell flags — use one form");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Conflict checks
|
|
127
150
|
if (options.file && argsStr) {
|
|
128
151
|
throw new Error("Cannot specify both --file and inline JSON args");
|
|
129
152
|
}
|
|
153
|
+
if (shellTokens.length > 0 && options.file) {
|
|
154
|
+
throw new Error("Cannot mix `--` shell flags with --file");
|
|
155
|
+
}
|
|
130
156
|
|
|
131
|
-
//
|
|
157
|
+
// Fetch the tool schema once, up front, so shell-flag parsing can use it for type coercion.
|
|
158
|
+
const toolSchema = await manager.getToolSchema(server, tool);
|
|
159
|
+
|
|
160
|
+
// Parse args from: --file > inline JSON positional > shell flags after `--` > stdin > empty
|
|
132
161
|
let args: Record<string, unknown> = {};
|
|
133
162
|
|
|
134
163
|
if (options.file) {
|
|
@@ -140,6 +169,8 @@ export function registerExecCommand(program: Command) {
|
|
|
140
169
|
args = parseJsonArgs(content);
|
|
141
170
|
} else if (argsStr) {
|
|
142
171
|
args = parseJsonArgs(argsStr);
|
|
172
|
+
} else if (shellTokens.length > 0) {
|
|
173
|
+
args = parseShellArgs(shellTokens, toolSchema?.inputSchema);
|
|
143
174
|
} else if (!process.stdin.isTTY) {
|
|
144
175
|
// Read from stdin
|
|
145
176
|
const stdin = await readStdin();
|
|
@@ -149,7 +180,6 @@ export function registerExecCommand(program: Command) {
|
|
|
149
180
|
}
|
|
150
181
|
|
|
151
182
|
// Validate args against tool inputSchema before calling
|
|
152
|
-
const toolSchema = await manager.getToolSchema(server, tool);
|
|
153
183
|
if (toolSchema) {
|
|
154
184
|
const validation = validateToolInput(server, toolSchema, args);
|
|
155
185
|
if (!validation.valid) {
|
package/src/lib/input.ts
CHANGED
|
@@ -31,3 +31,173 @@ export async function readStdin(): Promise<string> {
|
|
|
31
31
|
}
|
|
32
32
|
return chunks.join("");
|
|
33
33
|
}
|
|
34
|
+
|
|
35
|
+
type SchemaProperty = {
|
|
36
|
+
type?: string | string[];
|
|
37
|
+
items?: { type?: string | string[] };
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Parse shell-style flag tokens into an arguments object, using a JSON Schema for type
|
|
42
|
+
* coercion. Supports:
|
|
43
|
+
* --key value, --key=value
|
|
44
|
+
* --key (boolean true; only when schema says boolean or schema is unknown)
|
|
45
|
+
* --no-key (boolean false; only when `key` is a known boolean field)
|
|
46
|
+
* repeated flags or comma-separated values for arrays
|
|
47
|
+
*
|
|
48
|
+
* Coerces values to integer/number/boolean according to the field's `type` in the
|
|
49
|
+
* schema. For unknown fields (or empty schema), values are left as strings — Ajv
|
|
50
|
+
* will surface the unknown-field error during validation.
|
|
51
|
+
*/
|
|
52
|
+
export function parseShellArgs(
|
|
53
|
+
tokens: string[],
|
|
54
|
+
inputSchema: Record<string, unknown> | undefined,
|
|
55
|
+
): Record<string, unknown> {
|
|
56
|
+
const properties = (inputSchema?.properties ?? {}) as Record<string, SchemaProperty>;
|
|
57
|
+
const result: Record<string, unknown> = {};
|
|
58
|
+
const seen = new Set<string>();
|
|
59
|
+
|
|
60
|
+
function getType(key: string): string | undefined {
|
|
61
|
+
const prop = properties[key];
|
|
62
|
+
if (!prop) return undefined;
|
|
63
|
+
const t = prop.type;
|
|
64
|
+
return Array.isArray(t) ? t[0] : t;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function getItemType(key: string): string | undefined {
|
|
68
|
+
const t = properties[key]?.items?.type;
|
|
69
|
+
return Array.isArray(t) ? t[0] : t;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function coerceScalar(key: string, raw: string, type: string | undefined): unknown {
|
|
73
|
+
switch (type) {
|
|
74
|
+
case "string":
|
|
75
|
+
return raw;
|
|
76
|
+
case "integer": {
|
|
77
|
+
const n = Number.parseInt(raw, 10);
|
|
78
|
+
if (Number.isNaN(n) || !/^-?\d+$/.test(raw.trim())) {
|
|
79
|
+
throw new Error(`--${key}: expected integer, got "${raw}"`);
|
|
80
|
+
}
|
|
81
|
+
return n;
|
|
82
|
+
}
|
|
83
|
+
case "number": {
|
|
84
|
+
const n = Number.parseFloat(raw);
|
|
85
|
+
if (Number.isNaN(n)) {
|
|
86
|
+
throw new Error(`--${key}: expected number, got "${raw}"`);
|
|
87
|
+
}
|
|
88
|
+
return n;
|
|
89
|
+
}
|
|
90
|
+
case "boolean": {
|
|
91
|
+
const lower = raw.toLowerCase();
|
|
92
|
+
if (lower === "true" || lower === "1" || lower === "") return true;
|
|
93
|
+
if (lower === "false" || lower === "0") return false;
|
|
94
|
+
throw new Error(`--${key}: expected boolean, got "${raw}"`);
|
|
95
|
+
}
|
|
96
|
+
case "object":
|
|
97
|
+
throw new Error(`--${key}: nested objects are not supported as shell flags — use JSON form`);
|
|
98
|
+
default:
|
|
99
|
+
return raw;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function assign(key: string, rawValue: string | undefined, isBooleanFlag: boolean): void {
|
|
104
|
+
const type = getType(key);
|
|
105
|
+
|
|
106
|
+
if (type === "array") {
|
|
107
|
+
const itemType = getItemType(key);
|
|
108
|
+
const pieces =
|
|
109
|
+
rawValue === undefined
|
|
110
|
+
? [""]
|
|
111
|
+
: rawValue
|
|
112
|
+
.split(",")
|
|
113
|
+
.map((s) => s.trim())
|
|
114
|
+
.filter((s) => s.length > 0);
|
|
115
|
+
const coerced = pieces.map((p) => coerceScalar(key, p, itemType));
|
|
116
|
+
const existing = result[key];
|
|
117
|
+
if (Array.isArray(existing)) {
|
|
118
|
+
existing.push(...coerced);
|
|
119
|
+
} else {
|
|
120
|
+
result[key] = coerced;
|
|
121
|
+
}
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (seen.has(key)) {
|
|
126
|
+
throw new Error(`--${key}: specified more than once (use comma-separated values for array fields)`);
|
|
127
|
+
}
|
|
128
|
+
seen.add(key);
|
|
129
|
+
|
|
130
|
+
if (isBooleanFlag) {
|
|
131
|
+
result[key] = true;
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (rawValue === undefined) {
|
|
136
|
+
throw new Error(`--${key}: expected value`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
result[key] = coerceScalar(key, rawValue, type);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
143
|
+
const token = tokens[i] ?? "";
|
|
144
|
+
|
|
145
|
+
if (!token.startsWith("--")) {
|
|
146
|
+
throw new Error(`unexpected positional argument "${token}" — use --field=value form`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const body = token.slice(2);
|
|
150
|
+
if (body.length === 0) {
|
|
151
|
+
throw new Error('unexpected bare "--" separator');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const eqIdx = body.indexOf("=");
|
|
155
|
+
let key: string;
|
|
156
|
+
let inlineValue: string | undefined;
|
|
157
|
+
if (eqIdx === -1) {
|
|
158
|
+
key = body;
|
|
159
|
+
inlineValue = undefined;
|
|
160
|
+
} else {
|
|
161
|
+
key = body.slice(0, eqIdx);
|
|
162
|
+
inlineValue = body.slice(eqIdx + 1);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// --no-key form: only treat as negation when `key` (without "no-") is a known boolean
|
|
166
|
+
if (key.startsWith("no-") && inlineValue === undefined) {
|
|
167
|
+
const bareKey = key.slice(3);
|
|
168
|
+
if (getType(bareKey) === "boolean") {
|
|
169
|
+
if (seen.has(bareKey)) {
|
|
170
|
+
throw new Error(`--${bareKey}: specified more than once`);
|
|
171
|
+
}
|
|
172
|
+
seen.add(bareKey);
|
|
173
|
+
result[bareKey] = false;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const type = getType(key);
|
|
179
|
+
const isBooleanField = type === "boolean";
|
|
180
|
+
|
|
181
|
+
if (inlineValue !== undefined) {
|
|
182
|
+
assign(key, inlineValue, false);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// No inline value — peek at next token. Booleans without a value mean "true".
|
|
187
|
+
const next = tokens[i + 1];
|
|
188
|
+
const nextLooksLikeFlag = next?.startsWith("--") === true;
|
|
189
|
+
if (isBooleanField && (next === undefined || nextLooksLikeFlag)) {
|
|
190
|
+
assign(key, undefined, true);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (next === undefined || nextLooksLikeFlag) {
|
|
195
|
+
throw new Error(`--${key}: expected value`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
i++;
|
|
199
|
+
assign(key, next, false);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return result;
|
|
203
|
+
}
|