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/src/core/parse.ts CHANGED
@@ -19,7 +19,7 @@ import {
19
19
  type CliRouter,
20
20
  isCliLeaf,
21
21
  isCliRouter,
22
- isJsonLeaf,
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
- _node: CliLeaf,
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
- return errorResult(`JSON commands do not accept options: ${tok}`, path, [], pathParams);
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 (isJsonLeaf(root)) {
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) && isJsonLeaf(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` = pure JSON body (no CLI flags). */
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"`, the leaf accepts a single JSON document (CLI positional or piped stdin;
512
- * MCP/HTTP tool args = body). Requires `inputSchema`; forbids `options` and `positionals`.
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 pure JSON body (no CLI flags). */
695
- export function isJsonLeaf(leaf: CliLeaf): boolean {
696
- return leaf.kind === "json";
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). */
@@ -17,7 +17,7 @@ import {
17
17
  CliValueFormat,
18
18
  isCliLeaf,
19
19
  isCliRouter,
20
- isJsonLeaf,
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 (isJsonLeaf(node)) {
267
+ if (isDocumentLeaf(node)) {
268
+ const kindStr = `kind: "${node.kind ?? "document"}"`;
268
269
  if (node.inputSchema === undefined) {
269
- throw new CliSchemaValidationError(`kind: "json" requires inputSchema on ${node.key}`);
270
+ throw new CliSchemaValidationError(`${kindStr} requires inputSchema on ${node.key}`);
270
271
  }
271
272
  if ((node.options ?? []).length > 0) {
272
- throw new CliSchemaValidationError(`kind: "json" forbids options on ${node.key}`);
273
+ throw new CliSchemaValidationError(`${kindStr} forbids options on ${node.key}`);
273
274
  }
274
275
  if ((node.positionals ?? []).length > 0) {
275
- throw new CliSchemaValidationError(`kind: "json" forbids positionals on ${node.key}`);
276
+ throw new CliSchemaValidationError(`${kindStr} forbids positionals on ${node.key}`);
276
277
  }
277
278
  }
278
279
  const outputSchema = node.outputSchema;
@@ -40,6 +40,7 @@ export {
40
40
  CliOptionKind,
41
41
  CliSchemaValidationError,
42
42
  CliValueFormat,
43
+ isDocumentLeaf,
43
44
  isJsonLeaf,
44
45
  } from "../core/types.ts";
45
46
  export { Cli, type CliInvokeKind, type CliInvokeResult } from "../runtime/cli.ts";
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 { CLI_NOTES_PROGRAM, cliHelpRender, cliOptionLabel, cliPositionalLabel, cliResolveNotes } from "./help.ts";
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
  });