argsbarg 7.0.7 → 7.0.9
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 +20 -1
- package/README.md +1 -1
- package/docs/cli-program.md +22 -7
- package/docs/output-schema.md +1 -0
- package/index.d.ts +19 -6
- package/package.json +1 -1
- package/src/core/document-leaf.test.ts +208 -0
- package/src/core/json-leaf.test.ts +26 -0
- package/src/core/leaf-inputs.ts +70 -15
- package/src/core/parse.ts +7 -6
- package/src/core/types.ts +19 -7
- package/src/core/validate.ts +6 -5
- package/src/exports/cli.ts +1 -0
- package/src/help.test.ts +284 -1
- package/src/help.ts +412 -57
- package/src/http/openapi.ts +2 -2
- package/src/http/routes.ts +2 -2
- package/src/http/server.ts +9 -1
- package/src/index.ts +2 -0
- package/src/mcp/tools.ts +3 -3
- package/src/test/integration/http.test.ts +36 -0
package/src/core/parse.ts
CHANGED
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
type CliRouter,
|
|
20
20
|
isCliLeaf,
|
|
21
21
|
isCliRouter,
|
|
22
|
-
|
|
22
|
+
isDocumentLeaf,
|
|
23
23
|
} from "./types.ts";
|
|
24
24
|
|
|
25
25
|
// ── Parse Result ──────────────────────────────────────────────────────────────
|
|
@@ -292,9 +292,9 @@ export function collectOptionDefs(root: CliNode, path: string[]): CliOption[] {
|
|
|
292
292
|
return [...(node.options ?? [])];
|
|
293
293
|
}
|
|
294
294
|
|
|
295
|
-
/** Fills `args` for a json leaf from `startIdx` (0 or 1 JSON string positional). */
|
|
295
|
+
/** Fills `args` for a document / json leaf from `startIdx` (0 or 1 JSON or YAML string positional). */
|
|
296
296
|
function finishJsonLeaf(
|
|
297
|
-
|
|
297
|
+
node: CliLeaf,
|
|
298
298
|
startIdx: number,
|
|
299
299
|
argv: string[],
|
|
300
300
|
path: string[],
|
|
@@ -313,7 +313,8 @@ function finishJsonLeaf(
|
|
|
313
313
|
return errorResult("Unexpected extra arguments", path, [], pathParams);
|
|
314
314
|
}
|
|
315
315
|
if (tok.startsWith("-")) {
|
|
316
|
-
|
|
316
|
+
const kindLabel = node.kind === "document" ? "Document" : "JSON";
|
|
317
|
+
return errorResult(`${kindLabel} commands do not accept options: ${tok}`, path, [], pathParams);
|
|
317
318
|
}
|
|
318
319
|
args.push(tok);
|
|
319
320
|
idx += 1;
|
|
@@ -552,7 +553,7 @@ export function parse(root: CliNode, argv: string[]): ParseResult {
|
|
|
552
553
|
let node: CliNode | undefined;
|
|
553
554
|
|
|
554
555
|
if (isCliLeaf(root)) {
|
|
555
|
-
if (
|
|
556
|
+
if (isDocumentLeaf(root)) {
|
|
556
557
|
return finishJsonLeaf(root, i, argv, path, opts, pathParams);
|
|
557
558
|
}
|
|
558
559
|
return finishLeaf(root, i, argv, path, opts, root.options ?? [], forcePositionals, pathParams);
|
|
@@ -645,7 +646,7 @@ export function parse(root: CliNode, argv: string[]): ParseResult {
|
|
|
645
646
|
|
|
646
647
|
// Walk the command tree
|
|
647
648
|
while (true) {
|
|
648
|
-
if (isCliLeaf(current) &&
|
|
649
|
+
if (isCliLeaf(current) && isDocumentLeaf(current)) {
|
|
649
650
|
return finishJsonLeaf(current, i, argv, path, opts, pathParams);
|
|
650
651
|
}
|
|
651
652
|
|
package/src/core/types.ts
CHANGED
|
@@ -500,16 +500,17 @@ export interface CliNodeBase {
|
|
|
500
500
|
options?: CliOption[];
|
|
501
501
|
}
|
|
502
502
|
|
|
503
|
-
/** Leaf input mode: `json` =
|
|
504
|
-
export type CliLeafKind = "json";
|
|
503
|
+
/** Leaf input mode: `document` (or legacy `json`) = structured JSON or YAML document body (no CLI flags). */
|
|
504
|
+
export type CliLeafKind = "document" | "json";
|
|
505
505
|
|
|
506
506
|
/**
|
|
507
507
|
* A leaf command node with a handler and optional positionals.
|
|
508
508
|
*/
|
|
509
509
|
export type CliLeaf = CliNodeBase & {
|
|
510
510
|
/**
|
|
511
|
-
* When `"json"
|
|
512
|
-
* MCP/HTTP tool args = body). Requires `inputSchema`;
|
|
511
|
+
* When `"document"` (or legacy `"json"`), the leaf accepts a single JSON or YAML document
|
|
512
|
+
* (CLI positional or piped stdin; MCP/HTTP tool args = body). Requires `inputSchema`;
|
|
513
|
+
* forbids `options` and `positionals`.
|
|
513
514
|
*/
|
|
514
515
|
kind?: CliLeafKind;
|
|
515
516
|
/** Handler function for leaf commands. */
|
|
@@ -691,9 +692,20 @@ export function isCliLeaf(node: CliNode): node is CliLeaf {
|
|
|
691
692
|
return "handler" in node && typeof node.handler === "function";
|
|
692
693
|
}
|
|
693
694
|
|
|
694
|
-
/** True when the leaf accepts a
|
|
695
|
-
export function
|
|
696
|
-
|
|
695
|
+
/** True when the leaf accepts a structured JSON or YAML document body (no CLI flags). */
|
|
696
|
+
export function isDocumentLeaf(
|
|
697
|
+
/** Leaf command node to inspect. */
|
|
698
|
+
leaf: CliLeaf,
|
|
699
|
+
): boolean {
|
|
700
|
+
return leaf.kind === "document" || leaf.kind === "json";
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/** True when the leaf accepts a structured document body (backward-compatible alias for `isDocumentLeaf`). */
|
|
704
|
+
export function isJsonLeaf(
|
|
705
|
+
/** Leaf command node to inspect. */
|
|
706
|
+
leaf: CliLeaf,
|
|
707
|
+
): boolean {
|
|
708
|
+
return isDocumentLeaf(leaf);
|
|
697
709
|
}
|
|
698
710
|
|
|
699
711
|
/** True when the node is a router (has subcommands). */
|
package/src/core/validate.ts
CHANGED
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
CliValueFormat,
|
|
18
18
|
isCliLeaf,
|
|
19
19
|
isCliRouter,
|
|
20
|
-
|
|
20
|
+
isDocumentLeaf,
|
|
21
21
|
} from "./types.ts";
|
|
22
22
|
|
|
23
23
|
/** Validates `docs` configuration on the program root. */
|
|
@@ -264,15 +264,16 @@ function walkNode(node: CliNode, program: CliProgram, isRoot: boolean): void {
|
|
|
264
264
|
if (isRoot && node.mcpTool !== undefined) {
|
|
265
265
|
throw new CliSchemaValidationError("mcpTool is only supported on leaf commands");
|
|
266
266
|
}
|
|
267
|
-
if (
|
|
267
|
+
if (isDocumentLeaf(node)) {
|
|
268
|
+
const kindStr = `kind: "${node.kind ?? "document"}"`;
|
|
268
269
|
if (node.inputSchema === undefined) {
|
|
269
|
-
throw new CliSchemaValidationError(
|
|
270
|
+
throw new CliSchemaValidationError(`${kindStr} requires inputSchema on ${node.key}`);
|
|
270
271
|
}
|
|
271
272
|
if ((node.options ?? []).length > 0) {
|
|
272
|
-
throw new CliSchemaValidationError(
|
|
273
|
+
throw new CliSchemaValidationError(`${kindStr} forbids options on ${node.key}`);
|
|
273
274
|
}
|
|
274
275
|
if ((node.positionals ?? []).length > 0) {
|
|
275
|
-
throw new CliSchemaValidationError(
|
|
276
|
+
throw new CliSchemaValidationError(`${kindStr} forbids positionals on ${node.key}`);
|
|
276
277
|
}
|
|
277
278
|
}
|
|
278
279
|
const outputSchema = node.outputSchema;
|
package/src/exports/cli.ts
CHANGED
package/src/help.test.ts
CHANGED
|
@@ -5,7 +5,14 @@ Help rendering and label formatting tests.
|
|
|
5
5
|
import { describe, expect, test } from "bun:test";
|
|
6
6
|
import { cliPresentationRoot } from "./builtins/presentation.ts";
|
|
7
7
|
import { type CliOption, CliOptionKind, type CliPositional } from "./core/types.ts";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
CLI_NOTES_PROGRAM,
|
|
10
|
+
cliHelpRender,
|
|
11
|
+
cliOptionLabel,
|
|
12
|
+
cliPositionalLabel,
|
|
13
|
+
cliResolveNotes,
|
|
14
|
+
schemaToYamlLines,
|
|
15
|
+
} from "./help.ts";
|
|
9
16
|
import { testProgram } from "./test/fixtures.ts";
|
|
10
17
|
|
|
11
18
|
describe("cliOptionLabel", () => {
|
|
@@ -148,4 +155,280 @@ describe("cliHelpRender", () => {
|
|
|
148
155
|
expect(help).toContain("See `myapp docs readme` for the user guide.");
|
|
149
156
|
expect(help).not.toContain("docs skill");
|
|
150
157
|
});
|
|
158
|
+
|
|
159
|
+
/** Tests that non-TTY help strips box characters and renders plain text. */
|
|
160
|
+
test("non-TTY help strips boxes and renders clean plain text", () => {
|
|
161
|
+
const root = testProgram({
|
|
162
|
+
key: "myapp",
|
|
163
|
+
version: "1.0.0",
|
|
164
|
+
description: "Test application.",
|
|
165
|
+
commands: [
|
|
166
|
+
{
|
|
167
|
+
key: "status",
|
|
168
|
+
description: "Show status.",
|
|
169
|
+
options: [
|
|
170
|
+
{
|
|
171
|
+
name: "verbose",
|
|
172
|
+
shortName: "v",
|
|
173
|
+
description: "Verbose output.",
|
|
174
|
+
kind: CliOptionKind.Presence,
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
handler: () => {},
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
});
|
|
181
|
+
const help = cliHelpRender(cliPresentationRoot(root), ["status"], false, { isTTY: false });
|
|
182
|
+
expect(help).not.toContain("╭─");
|
|
183
|
+
expect(help).not.toContain("╰─");
|
|
184
|
+
expect(help).not.toContain("│");
|
|
185
|
+
expect(help).toContain("Usage:\n myapp status [OPTIONS]");
|
|
186
|
+
expect(help).toContain("Options:\n --help, -h Show help for this command.\n --verbose, -v Verbose output.");
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
/** Tests that TTY help renders rounded UTF-8 boxes. */
|
|
190
|
+
test("TTY help renders rounded UTF-8 boxes and omits output schema by default", () => {
|
|
191
|
+
const root = testProgram({
|
|
192
|
+
key: "myapp",
|
|
193
|
+
version: "1.0.0",
|
|
194
|
+
description: "Test application.",
|
|
195
|
+
commands: [
|
|
196
|
+
{
|
|
197
|
+
key: "status",
|
|
198
|
+
description: "Show status.",
|
|
199
|
+
outputSchema: {
|
|
200
|
+
type: "object",
|
|
201
|
+
properties: {
|
|
202
|
+
version: { type: "string", description: "App version." },
|
|
203
|
+
},
|
|
204
|
+
required: ["version"],
|
|
205
|
+
},
|
|
206
|
+
handler: () => {},
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
});
|
|
210
|
+
const help = cliHelpRender(cliPresentationRoot(root), ["status"], false, { isTTY: true });
|
|
211
|
+
expect(help).toContain("╭");
|
|
212
|
+
expect(help).toContain("╰");
|
|
213
|
+
expect(help).toContain("│");
|
|
214
|
+
expect(help).not.toContain("Output Schema");
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
/** Tests that non-TTY help automatically includes output schema in YAML by default. */
|
|
218
|
+
test("non-TTY help automatically includes output schema in YAML by default", () => {
|
|
219
|
+
const root = testProgram({
|
|
220
|
+
key: "myapp",
|
|
221
|
+
version: "1.0.0",
|
|
222
|
+
description: "Test application.",
|
|
223
|
+
commands: [
|
|
224
|
+
{
|
|
225
|
+
key: "status",
|
|
226
|
+
description: "Show status.",
|
|
227
|
+
outputSchema: {
|
|
228
|
+
type: "object",
|
|
229
|
+
properties: {
|
|
230
|
+
version: { type: "string", description: "App version." },
|
|
231
|
+
},
|
|
232
|
+
required: ["version"],
|
|
233
|
+
},
|
|
234
|
+
handler: () => {},
|
|
235
|
+
},
|
|
236
|
+
],
|
|
237
|
+
});
|
|
238
|
+
const help = cliHelpRender(cliPresentationRoot(root), ["status"], false, { isTTY: false });
|
|
239
|
+
expect(help).toContain("Output Schema (with --json):");
|
|
240
|
+
expect(help).toContain("# App version.");
|
|
241
|
+
expect(help).toContain("version: string");
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
/** Tests that json leaf commands render Output Schema (JSON) and Input Schema. */
|
|
245
|
+
test("json leaf renders Output Schema (JSON) and Input Schema in non-TTY mode", () => {
|
|
246
|
+
const root = testProgram({
|
|
247
|
+
key: "myapp",
|
|
248
|
+
version: "1.0.0",
|
|
249
|
+
description: "Test application.",
|
|
250
|
+
commands: [
|
|
251
|
+
{
|
|
252
|
+
key: "create",
|
|
253
|
+
kind: "json",
|
|
254
|
+
description: "Create resource.",
|
|
255
|
+
inputSchema: {
|
|
256
|
+
type: "object",
|
|
257
|
+
properties: {
|
|
258
|
+
name: { type: "string", description: "Resource name." },
|
|
259
|
+
},
|
|
260
|
+
required: ["name"],
|
|
261
|
+
},
|
|
262
|
+
outputSchema: {
|
|
263
|
+
type: "object",
|
|
264
|
+
properties: {
|
|
265
|
+
id: { type: "string", description: "Generated ID." },
|
|
266
|
+
},
|
|
267
|
+
required: ["id"],
|
|
268
|
+
},
|
|
269
|
+
handler: () => {},
|
|
270
|
+
},
|
|
271
|
+
],
|
|
272
|
+
});
|
|
273
|
+
const help = cliHelpRender(cliPresentationRoot(root), ["create"], false, { isTTY: false });
|
|
274
|
+
expect(help).toContain("Input Schema:");
|
|
275
|
+
expect(help).toContain("# Resource name.");
|
|
276
|
+
expect(help).toContain("name: string");
|
|
277
|
+
expect(help).toContain("Output Schema (JSON):");
|
|
278
|
+
expect(help).toContain("# Generated ID.");
|
|
279
|
+
expect(help).toContain("id: string");
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
/** Tests that document leaf commands render [DOCUMENT] usage and schema sections. */
|
|
283
|
+
test("document leaf renders [DOCUMENT] usage and schemas in non-TTY mode", () => {
|
|
284
|
+
const root = testProgram({
|
|
285
|
+
key: "myapp",
|
|
286
|
+
version: "1.0.0",
|
|
287
|
+
description: "Test application.",
|
|
288
|
+
commands: [
|
|
289
|
+
{
|
|
290
|
+
key: "deploy",
|
|
291
|
+
kind: "document",
|
|
292
|
+
description: "Deploy from document.",
|
|
293
|
+
inputSchema: {
|
|
294
|
+
type: "object",
|
|
295
|
+
properties: {
|
|
296
|
+
target: { type: "string", description: "Deployment target." },
|
|
297
|
+
},
|
|
298
|
+
required: ["target"],
|
|
299
|
+
},
|
|
300
|
+
outputSchema: {
|
|
301
|
+
type: "object",
|
|
302
|
+
properties: {
|
|
303
|
+
url: { type: "string", description: "Deployment URL." },
|
|
304
|
+
},
|
|
305
|
+
required: ["url"],
|
|
306
|
+
},
|
|
307
|
+
handler: () => {},
|
|
308
|
+
},
|
|
309
|
+
],
|
|
310
|
+
});
|
|
311
|
+
const help = cliHelpRender(cliPresentationRoot(root), ["deploy"], false, { isTTY: false });
|
|
312
|
+
expect(help).toContain("myapp deploy [DOCUMENT]");
|
|
313
|
+
expect(help).toContain("Pass a JSON or YAML document as an argument or pipe to stdin.");
|
|
314
|
+
expect(help).toContain("Input Schema:");
|
|
315
|
+
expect(help).toContain("# Deployment target.");
|
|
316
|
+
expect(help).toContain("target: string");
|
|
317
|
+
expect(help).toContain("Output Schema (JSON):");
|
|
318
|
+
expect(help).toContain("# Deployment URL.");
|
|
319
|
+
expect(help).toContain("url: string");
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
/** Tests for converting JSON Schema to human- and agent-friendly YAML lines. */
|
|
324
|
+
describe("schemaToYamlLines", () => {
|
|
325
|
+
/** Tests primitive properties with required and optional keys and comments. */
|
|
326
|
+
test("formats primitive properties with descriptions and optionality", () => {
|
|
327
|
+
const schema = {
|
|
328
|
+
type: "object",
|
|
329
|
+
properties: {
|
|
330
|
+
documentId: {
|
|
331
|
+
type: "string",
|
|
332
|
+
description: "Unique document identifier.",
|
|
333
|
+
},
|
|
334
|
+
index: {
|
|
335
|
+
type: "integer",
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
required: ["documentId"],
|
|
339
|
+
};
|
|
340
|
+
const lines = schemaToYamlLines(schema, 0);
|
|
341
|
+
expect(lines).toEqual(["# Unique document identifier.", "documentId: string", "index?: integer"]);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
/** Tests enums, string formats, and union types. */
|
|
345
|
+
test("formats enums, string formats, and union types", () => {
|
|
346
|
+
const schema = {
|
|
347
|
+
type: "object",
|
|
348
|
+
properties: {
|
|
349
|
+
format: {
|
|
350
|
+
type: "string",
|
|
351
|
+
enum: ["pdf", "html"],
|
|
352
|
+
},
|
|
353
|
+
createdAt: {
|
|
354
|
+
type: "string",
|
|
355
|
+
format: "date-time",
|
|
356
|
+
},
|
|
357
|
+
status: {
|
|
358
|
+
anyOf: [{ type: "string" }, { type: "number" }],
|
|
359
|
+
},
|
|
360
|
+
},
|
|
361
|
+
};
|
|
362
|
+
const lines = schemaToYamlLines(schema, 0);
|
|
363
|
+
expect(lines).toEqual(['format?: "pdf" | "html"', "createdAt?: string (date-time)", "status?: string | number"]);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
/** Tests nested objects and arrays of objects with definitions. */
|
|
367
|
+
test("formats nested objects and arrays of objects with definition resolution", () => {
|
|
368
|
+
const schema = {
|
|
369
|
+
type: "object",
|
|
370
|
+
properties: {
|
|
371
|
+
tab: {
|
|
372
|
+
type: "object",
|
|
373
|
+
description: "Active tab metadata.",
|
|
374
|
+
properties: {
|
|
375
|
+
tabId: { type: "string" },
|
|
376
|
+
title: { type: "string" },
|
|
377
|
+
},
|
|
378
|
+
required: ["tabId", "title"],
|
|
379
|
+
},
|
|
380
|
+
tabs: {
|
|
381
|
+
type: "array",
|
|
382
|
+
items: {
|
|
383
|
+
$ref: "#/definitions/TabItem",
|
|
384
|
+
},
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
definitions: {
|
|
388
|
+
TabItem: {
|
|
389
|
+
type: "object",
|
|
390
|
+
properties: {
|
|
391
|
+
tabId: { type: "string" },
|
|
392
|
+
title: { type: "string" },
|
|
393
|
+
index: { type: "integer" },
|
|
394
|
+
},
|
|
395
|
+
required: ["tabId", "title", "index"],
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
required: ["tab"],
|
|
399
|
+
};
|
|
400
|
+
const lines = schemaToYamlLines(schema, 0);
|
|
401
|
+
expect(lines).toEqual([
|
|
402
|
+
"# Active tab metadata.",
|
|
403
|
+
"tab:",
|
|
404
|
+
" tabId: string",
|
|
405
|
+
" title: string",
|
|
406
|
+
"tabs?:",
|
|
407
|
+
" - tabId: string",
|
|
408
|
+
" title: string",
|
|
409
|
+
" index: integer",
|
|
410
|
+
]);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
/** Tests recursive references handle cycles gracefully without infinite loop. */
|
|
414
|
+
test("handles recursive definition references without infinite loop", () => {
|
|
415
|
+
const schema = {
|
|
416
|
+
type: "object",
|
|
417
|
+
properties: {
|
|
418
|
+
name: { type: "string" },
|
|
419
|
+
parent: { $ref: "#/definitions/TreeNode" },
|
|
420
|
+
},
|
|
421
|
+
definitions: {
|
|
422
|
+
TreeNode: {
|
|
423
|
+
type: "object",
|
|
424
|
+
properties: {
|
|
425
|
+
name: { type: "string" },
|
|
426
|
+
parent: { $ref: "#/definitions/TreeNode" },
|
|
427
|
+
},
|
|
428
|
+
},
|
|
429
|
+
},
|
|
430
|
+
};
|
|
431
|
+
const lines = schemaToYamlLines(schema, 0);
|
|
432
|
+
expect(lines).toEqual(["name?: string", "parent?:", " name?: string", " parent?: TreeNode"]);
|
|
433
|
+
});
|
|
151
434
|
});
|