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/help.ts CHANGED
@@ -1,13 +1,14 @@
1
1
  /*
2
- This module renders CLI help with wrapping, boxes, tables, and TTY color.
3
- It formats both explicit help and error output, using the terminal width to keep the
4
- layout readable and aligned with the current display.
2
+ This module renders CLI help with wrapping, boxes, tables, and TTY color for interactive
3
+ terminal sessions, as well as clean unboxed plain text with in-band YAML input and output
4
+ schemas for non-TTY agent discovery and piping.
5
5
 
6
6
  It keeps help formatting shared across help and error paths so users see one consistent
7
7
  style no matter how help is reached.
8
8
  */
9
9
 
10
10
  import {
11
+ type CliLeaf,
11
12
  type CliNode,
12
13
  type CliOption,
13
14
  CliOptionKind,
@@ -15,7 +16,7 @@ import {
15
16
  type CliRouter,
16
17
  isCliLeaf,
17
18
  isCliRouter,
18
- isJsonLeaf,
19
+ isDocumentLeaf,
19
20
  } from "./core/types.ts";
20
21
  import { visibleOptions, visibleSubcommands } from "./runtime/exposure.ts";
21
22
 
@@ -73,9 +74,9 @@ function getHelpWidth(): number {
73
74
  return Math.max(40, process.stdout.columns || 80);
74
75
  }
75
76
 
76
- /** True when stdout is a TTY (used to decide on color). */
77
- function isStdoutTTY(): boolean {
78
- return !!process.stdout.isTTY;
77
+ /** True when stdout/stderr is a TTY (used to decide on boxes and color). */
78
+ function isOutputTTY(useStderr: boolean): boolean {
79
+ return useStderr ? !!process.stderr.isTTY : !!process.stdout.isTTY;
79
80
  }
80
81
 
81
82
  // ── Width Helpers ─────────────────────────────────────────────────────────────
@@ -321,6 +322,52 @@ function renderTableBox(title: string, rows: HelpRow[], hw: number, color: boole
321
322
  return out;
322
323
  }
323
324
 
325
+ /** Renders a plain-text section with a header and 2-space indented lines (non-TTY). */
326
+ function renderPlainSection(
327
+ /** Section header title (e.g. "Usage" or "Notes"). */
328
+ title: string,
329
+ /** Content lines to indent under the header. */
330
+ lines: string[],
331
+ ): string[] {
332
+ if (lines.length === 0) return [];
333
+ const out: string[] = [`${title}:`];
334
+ for (const line of lines) {
335
+ out.push(line.length > 0 ? ` ${line}` : "");
336
+ }
337
+ return out;
338
+ }
339
+
340
+ /** Renders a plain-text two-column table without box borders (non-TTY). */
341
+ function renderPlainTable(
342
+ /** Section header title (e.g. "Options" or "Subcommands"). */
343
+ title: string,
344
+ /** Rows with label and description to format. */
345
+ rows: HelpRow[],
346
+ /** Available terminal width. */
347
+ hw: number,
348
+ ): string[] {
349
+ if (rows.length === 0) return [];
350
+ let labelWidth = 0;
351
+ for (const row of rows) {
352
+ labelWidth = Math.max(labelWidth, visibleWidth(row.label));
353
+ }
354
+ const descWidth = Math.max(20, hw - labelWidth - 4);
355
+ const out: string[] = [`${title}:`];
356
+ for (const row of rows) {
357
+ const wrapped = wrapText(row.description, descWidth);
358
+ const paddedLabel = padVisible(row.label, labelWidth);
359
+ if (wrapped.length === 0 || wrapped[0].length === 0) {
360
+ out.push(` ${row.label}`);
361
+ } else {
362
+ out.push(` ${paddedLabel} ${wrapped[0]}`);
363
+ for (let idx = 1; idx < wrapped.length; idx++) {
364
+ out.push(` ${spaces(labelWidth)} ${wrapped[idx]}`);
365
+ }
366
+ }
367
+ }
368
+ return out;
369
+ }
370
+
324
371
  // ── Usage & Rows ──────────────────────────────────────────────────────────────
325
372
 
326
373
  /** Builds one or two usage line strings (OPTIONS / COMMAND / ARGS) for the help header. */
@@ -329,8 +376,9 @@ function usageLines(
329
376
  helpPath: string[],
330
377
  hasCommands: boolean,
331
378
  hasArgs: boolean,
332
- jsonLeaf: boolean,
379
+ documentLeaf: boolean,
333
380
  color: boolean,
381
+ leafKind?: string,
334
382
  ): string[] {
335
383
  let fullPath = appName;
336
384
  for (const seg of helpPath) {
@@ -339,7 +387,8 @@ function usageLines(
339
387
  const usageOpts = color ? style.aquaBold("[OPTIONS]") : "[OPTIONS]";
340
388
  const usageCmd = color ? style.aquaBold("COMMAND") : "COMMAND";
341
389
  const usageArgs = color ? style.aquaBold("[ARGS]...") : "[ARGS]...";
342
- const usageJson = color ? style.aquaBold("[JSON]") : "[JSON]";
390
+ const docTag = leafKind === "document" ? "[DOCUMENT]" : "[JSON]";
391
+ const usageDoc = color ? style.aquaBold(docTag) : docTag;
343
392
 
344
393
  const out: string[] = [];
345
394
  if (helpPath.length === 0) {
@@ -350,8 +399,8 @@ function usageLines(
350
399
  }
351
400
  return out;
352
401
  }
353
- if (jsonLeaf) {
354
- out.push(`${fullPath} ${usageJson}`);
402
+ if (documentLeaf) {
403
+ out.push(`${fullPath} ${usageDoc}`);
355
404
  return out;
356
405
  }
357
406
  out.push(`${fullPath} ${usageOpts}${hasArgs ? ` ${usageArgs}` : ""}`);
@@ -361,10 +410,11 @@ function usageLines(
361
410
  return out;
362
411
  }
363
412
 
364
- /** Table rows for `kind: "json"` leaf input (schema properties + stdin hint). */
365
- function rowsForJsonInput(inputSchema: Record<string, unknown> | undefined): HelpRow[] {
366
- const hint = "Pass a JSON document as an argument or pipe to stdin.";
367
- const rows: HelpRow[] = [{ label: "JSON", description: hint }];
413
+ /** Table rows for `kind: "document"` / `kind: "json"` leaf input (schema properties + stdin hint). */
414
+ function rowsForJsonInput(inputSchema: Record<string, unknown> | undefined, kind?: string): HelpRow[] {
415
+ const hint = "Pass a JSON or YAML document as an argument or pipe to stdin.";
416
+ const label = kind === "document" ? "DOCUMENT" : "JSON";
417
+ const rows: HelpRow[] = [{ label, description: hint }];
368
418
  const props = inputSchema?.properties;
369
419
  if (!props || typeof props !== "object" || Array.isArray(props)) {
370
420
  return rows;
@@ -404,24 +454,287 @@ function rowsForSubcommands(cmds: CliNode[]): HelpRow[] {
404
454
  .map((c) => ({ label: c.key, description: c.description }));
405
455
  }
406
456
 
457
+ // ── Schema YAML Formatting ───────────────────────────────────────────────────
458
+
459
+ /**
460
+ * Resolves a JSON Schema $ref pointer from definitions or $defs.
461
+ */
462
+ function resolveRef(
463
+ /** Reference URI string (e.g. `#/definitions/Foo` or `#/$defs/Foo`). */
464
+ ref: string,
465
+ /** Definitions dictionary from the root schema. */
466
+ defs: Record<string, unknown>,
467
+ ): Record<string, unknown> | null {
468
+ const name = ref.replace(/^#\/(definitions|\$defs)\//, "");
469
+ const target = defs[name];
470
+ if (typeof target === "object" && target !== null) {
471
+ return target as Record<string, unknown>;
472
+ }
473
+ return null;
474
+ }
475
+
476
+ /**
477
+ * Formats a single-line type representation from a JSON Schema fragment.
478
+ * Returns null if the type is complex and requires multiline YAML formatting.
479
+ */
480
+ function formatType(
481
+ /** Schema fragment to inspect. */
482
+ schema: Record<string, unknown>,
483
+ /** Schema definitions for reference lookup. */
484
+ defs: Record<string, unknown>,
485
+ /** Ancestor reference names visited in the current descent. */
486
+ seen: Set<string>,
487
+ ): string | null {
488
+ if (typeof schema.$ref === "string") {
489
+ const name = schema.$ref.replace(/^#\/(definitions|\$defs)\//, "");
490
+ if (seen.has(name)) {
491
+ return name;
492
+ }
493
+ seen.add(name);
494
+ const resolved = resolveRef(schema.$ref, defs);
495
+ const res = resolved ? formatType(resolved, defs, seen) : name;
496
+ seen.delete(name);
497
+ return res;
498
+ }
499
+
500
+ if (Array.isArray(schema.enum) && schema.enum.length > 0) {
501
+ return schema.enum.map((v) => (typeof v === "string" ? JSON.stringify(v) : String(v))).join(" | ");
502
+ }
503
+
504
+ const union = (schema.anyOf ?? schema.oneOf) as unknown[];
505
+ if (Array.isArray(union) && union.length > 0) {
506
+ const parts: string[] = [];
507
+ let allSimple = true;
508
+ for (const variant of union) {
509
+ if (typeof variant === "object" && variant !== null) {
510
+ const formatted = formatType(variant as Record<string, unknown>, defs, seen);
511
+ if (formatted !== null) {
512
+ parts.push(formatted);
513
+ } else {
514
+ allSimple = false;
515
+ break;
516
+ }
517
+ }
518
+ }
519
+ if (allSimple && parts.length > 0) {
520
+ return parts.join(" | ");
521
+ }
522
+ }
523
+
524
+ if (Array.isArray(schema.type)) {
525
+ return schema.type.join(" | ");
526
+ }
527
+
528
+ if (schema.type === "string") {
529
+ if (typeof schema.format === "string") {
530
+ return `string (${schema.format})`;
531
+ }
532
+ return "string";
533
+ }
534
+
535
+ if (schema.type === "number") return "number";
536
+ if (schema.type === "integer") return "integer";
537
+ if (schema.type === "boolean") return "boolean";
538
+ if (schema.type === "null") return "null";
539
+
540
+ if (schema.type === "array" && schema.items && typeof schema.items === "object") {
541
+ const itemType = formatType(schema.items as Record<string, unknown>, defs, seen);
542
+ if (itemType !== null) {
543
+ if (itemType.includes(" | ")) {
544
+ return `(${itemType})[]`;
545
+ }
546
+ return `${itemType}[]`;
547
+ }
548
+ return null;
549
+ }
550
+
551
+ if (schema.type === "object" || schema.properties !== undefined) {
552
+ if (schema.properties && typeof schema.properties === "object" && Object.keys(schema.properties).length > 0) {
553
+ return null;
554
+ }
555
+ if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
556
+ const valType = formatType(schema.additionalProperties as Record<string, unknown>, defs, seen) ?? "object";
557
+ return `{ [key: string]: ${valType} }`;
558
+ }
559
+ return "object";
560
+ }
561
+
562
+ return null;
563
+ }
564
+
565
+ /**
566
+ * Formats a JSON Schema node into multiline YAML lines with JSDoc comments.
567
+ */
568
+ function formatSchemaLines(
569
+ /** Schema object to format. */
570
+ schema: Record<string, unknown>,
571
+ /** Schema definitions dictionary. */
572
+ defs: Record<string, unknown>,
573
+ /** Indentation level in spaces. */
574
+ indent: number,
575
+ /** Ancestor reference names visited in the current descent. */
576
+ seen: Set<string>,
577
+ ): string[] {
578
+ if (typeof schema.$ref === "string") {
579
+ const name = schema.$ref.replace(/^#\/(definitions|\$defs)\//, "");
580
+ if (seen.has(name)) {
581
+ return [`${spaces(indent)}${name}`];
582
+ }
583
+ seen.add(name);
584
+ const resolved = resolveRef(schema.$ref, defs);
585
+ const res = resolved ? formatSchemaLines(resolved, defs, indent, seen) : [`${spaces(indent)}${name}`];
586
+ seen.delete(name);
587
+ return res;
588
+ }
589
+
590
+ if (schema.type === "object" || schema.properties !== undefined) {
591
+ const props = (schema.properties as Record<string, Record<string, unknown>>) ?? {};
592
+ const required = new Set(Array.isArray(schema.required) ? schema.required.map((k) => String(k)) : []);
593
+ const propEntries = Object.entries(props);
594
+ if (propEntries.length === 0) {
595
+ const simple = formatType(schema, defs, seen);
596
+ return simple ? [`${spaces(indent)}${simple}`] : [`${spaces(indent)}{}`];
597
+ }
598
+
599
+ const lines: string[] = [];
600
+ for (const [key, prop] of propEntries) {
601
+ if (typeof prop !== "object" || prop === null) continue;
602
+ const desc = typeof prop.description === "string" ? prop.description.trim() : "";
603
+ if (desc.length > 0) {
604
+ for (const dLine of desc.split("\n")) {
605
+ lines.push(`${spaces(indent)}# ${dLine.trim()}`);
606
+ }
607
+ }
608
+ const isReq = required.has(key);
609
+ const keyStr = isReq ? key : `${key}?`;
610
+ const simple = formatType(prop, defs, seen);
611
+ if (simple !== null) {
612
+ lines.push(`${spaces(indent)}${keyStr}: ${simple}`);
613
+ } else {
614
+ if (prop.type === "array" && prop.items && typeof prop.items === "object") {
615
+ lines.push(`${spaces(indent)}${keyStr}:`);
616
+ const itemSchema = prop.items as Record<string, unknown>;
617
+ const itemLines = formatSchemaLines(itemSchema, defs, 0, seen);
618
+ if (itemLines.length > 0) {
619
+ lines.push(`${spaces(indent + 2)}- ${itemLines[0]}`);
620
+ for (let i = 1; i < itemLines.length; i++) {
621
+ lines.push(`${spaces(indent + 4)}${itemLines[i]}`);
622
+ }
623
+ } else {
624
+ lines.push(`${spaces(indent + 2)}- {}`);
625
+ }
626
+ } else {
627
+ lines.push(`${spaces(indent)}${keyStr}:`);
628
+ const childLines = formatSchemaLines(prop, defs, indent + 2, seen);
629
+ lines.push(...childLines);
630
+ }
631
+ }
632
+ }
633
+ return lines;
634
+ }
635
+
636
+ if (schema.type === "array") {
637
+ if (schema.items && typeof schema.items === "object") {
638
+ const itemSchema = schema.items as Record<string, unknown>;
639
+ const simple = formatType(itemSchema, defs, seen);
640
+ if (simple !== null) {
641
+ return [`${spaces(indent)}- ${simple}`];
642
+ }
643
+ const itemLines = formatSchemaLines(itemSchema, defs, 0, seen);
644
+ if (itemLines.length > 0) {
645
+ const out: string[] = [`${spaces(indent)}- ${itemLines[0]}`];
646
+ for (let i = 1; i < itemLines.length; i++) {
647
+ out.push(`${spaces(indent + 2)}${itemLines[i]}`);
648
+ }
649
+ return out;
650
+ }
651
+ return [`${spaces(indent)}- {}`];
652
+ }
653
+ return [`${spaces(indent)}array`];
654
+ }
655
+
656
+ const fallback = formatType(schema, defs, seen);
657
+ return fallback ? [`${spaces(indent)}${fallback}`] : [`${spaces(indent)}object`];
658
+ }
659
+
660
+ /**
661
+ * Converts a JSON Schema object into human-readable, agent-friendly YAML lines.
662
+ */
663
+ export function schemaToYamlLines(
664
+ /** JSON Schema object to format. */
665
+ schema: Record<string, unknown>,
666
+ /** Starting indentation column in spaces (default 0). */
667
+ indent = 0,
668
+ ): string[] {
669
+ const defs = ((schema.definitions ?? schema.$defs) as Record<string, unknown>) ?? {};
670
+ const lines: string[] = [];
671
+ if (typeof schema.description === "string" && schema.description.trim().length > 0) {
672
+ for (const dLine of schema.description.trim().split("\n")) {
673
+ lines.push(`${spaces(indent)}# ${dLine.trim()}`);
674
+ }
675
+ }
676
+ lines.push(...formatSchemaLines(schema, defs, indent, new Set<string>()));
677
+ return lines;
678
+ }
679
+
407
680
  // ── Main Help Render ──────────────────────────────────────────────────────────
408
681
 
409
- function appendNotesBox(lines: string[], notes: string | undefined, appKey: string, hw: number, color: boolean): void {
682
+ /**
683
+ * Optional rendering options for CLI help output.
684
+ */
685
+ export interface CliHelpRenderOptions {
686
+ /** Override TTY detection for testing or headless environments. */
687
+ isTTY?: boolean;
688
+ /** Force schema display in TTY mode (always included by default in non-TTY). */
689
+ showSchema?: boolean;
690
+ }
691
+
692
+ /** Appends notes section to lines, using boxes in TTY mode and clean indentation in non-TTY mode. */
693
+ function appendNotesBox(
694
+ /** Accumulator lines for help output. */
695
+ lines: string[],
696
+ /** Raw notes text from schema. */
697
+ notes: string | undefined,
698
+ /** Program key for placeholder resolution. */
699
+ appKey: string,
700
+ /** Available terminal width. */
701
+ hw: number,
702
+ /** Whether ANSI color styling is enabled. */
703
+ color: boolean,
704
+ /** Whether output is targeting a TTY terminal. */
705
+ isTTY: boolean,
706
+ ): void {
410
707
  if ((notes ?? "").length === 0) {
411
708
  return;
412
709
  }
413
710
  const resolved = cliResolveNotes(notes ?? "", appKey);
414
711
  lines.push("");
415
- lines.push(renderTextBox("Notes", wrapText(resolved, hw - 4), hw, color).join("\n"));
712
+ if (isTTY) {
713
+ lines.push(renderTextBox("Notes", wrapText(resolved, hw - 4), hw, color).join("\n"));
714
+ } else {
715
+ lines.push(renderPlainSection("Notes", wrapText(resolved, hw - 4)).join("\n"));
716
+ }
416
717
  }
417
718
 
418
719
  /**
419
720
  * Renders full help for the app root or a nested command, following `helpPath` from the root key.
420
- * `useStderr` is reserved for call-site consistency; width and color use stdout TTY.
721
+ * In TTY mode, renders rounded UTF-8 boxes with ANSI color.
722
+ * In non-TTY mode, strips boxes and borders, and renders full untruncated YAML schemas by default.
421
723
  */
422
- export function cliHelpRender(schema: CliRouter, helpPath: string[], _useStderr: boolean): string {
724
+ export function cliHelpRender(
725
+ /** Root command presentation schema. */
726
+ schema: CliRouter,
727
+ /** Segment path to the target command node. */
728
+ helpPath: string[],
729
+ /** Whether output will be directed to stderr. */
730
+ useStderr: boolean,
731
+ /** Optional rendering overrides. */
732
+ opts?: CliHelpRenderOptions,
733
+ ): string {
423
734
  const hw = getHelpWidth();
424
- const color = isStdoutTTY();
735
+ const isTTY = opts?.isTTY ?? isOutputTTY(useStderr);
736
+ const color = isTTY;
737
+ const showSchema = opts?.showSchema ?? !isTTY;
425
738
 
426
739
  if (helpPath.length === 0) {
427
740
  const lines: string[] = [];
@@ -430,25 +743,43 @@ export function cliHelpRender(schema: CliRouter, helpPath: string[], _useStderr:
430
743
  lines.push(color ? style.white(schema.description) : schema.description);
431
744
  lines.push("");
432
745
  }
433
- lines.push(
434
- renderTextBox(
435
- "Usage",
436
- usageLines(schema.key, helpPath, (schema.commands ?? []).length > 0, false, false, color),
437
- hw,
438
- color,
439
- ).join("\n"),
440
- );
746
+ const usage = usageLines(schema.key, helpPath, (schema.commands ?? []).length > 0, false, false, color);
747
+ if (isTTY) {
748
+ lines.push(renderTextBox("Usage", usage, hw, color).join("\n"));
749
+ } else {
750
+ lines.push(renderPlainSection("Usage", usage).join("\n"));
751
+ }
441
752
 
442
- const optBox = renderTableBox("Options", rowsForOptions(visibleOptions(schema.options), color), hw, color);
753
+ const optRows = rowsForOptions(visibleOptions(schema.options), color);
754
+ const optBox = isTTY ? renderTableBox("Options", optRows, hw, color) : renderPlainTable("Options", optRows, hw);
443
755
  if (optBox.length > 0) {
444
756
  lines.push("");
445
757
  lines.push(optBox.join("\n"));
446
758
  }
447
759
  if ((schema.commands ?? []).length > 0) {
760
+ const subRows = rowsForSubcommands(schema.commands ?? []);
761
+ const subBox = isTTY ? renderTableBox("Commands", subRows, hw, color) : renderPlainTable("Commands", subRows, hw);
448
762
  lines.push("");
449
- lines.push(renderTableBox("Commands", rowsForSubcommands(schema.commands ?? []), hw, color).join("\n"));
763
+ lines.push(subBox.join("\n"));
450
764
  }
451
- appendNotesBox(lines, schema.notes, schema.key, hw, color);
765
+
766
+ if (isCliLeaf(schema as unknown as CliNode) && showSchema) {
767
+ const leaf = schema as unknown as CliLeaf;
768
+ if (leaf.outputSchema !== undefined) {
769
+ const title = isDocumentLeaf(leaf) ? "Output Schema (JSON)" : "Output Schema (with --json)";
770
+ const yamlLines = schemaToYamlLines(leaf.outputSchema, 0);
771
+ if (yamlLines.length > 0) {
772
+ lines.push("");
773
+ if (isTTY) {
774
+ lines.push(renderTextBox(title, yamlLines, hw, color).join("\n"));
775
+ } else {
776
+ lines.push(renderPlainSection(title, yamlLines).join("\n"));
777
+ }
778
+ }
779
+ }
780
+ }
781
+
782
+ appendNotesBox(lines, schema.notes, schema.key, hw, color, isTTY);
452
783
  return `${lines.join("\n")}\n\n`;
453
784
  }
454
785
 
@@ -472,42 +803,50 @@ export function cliHelpRender(schema: CliRouter, helpPath: string[], _useStderr:
472
803
  lines.push(color ? style.white(node.description) : node.description);
473
804
  lines.push("");
474
805
  }
475
- const nodeIsJsonLeaf = isCliLeaf(node) && isJsonLeaf(node);
476
- lines.push(
477
- renderTextBox(
478
- "Usage",
479
- usageLines(
480
- schema.key,
481
- helpPath,
482
- isCliRouter(node) && node.commands.length > 0,
483
- isCliLeaf(node) && (node.positionals ?? []).length > 0,
484
- nodeIsJsonLeaf,
485
- color,
486
- ),
487
- hw,
488
- color,
489
- ).join("\n"),
806
+ const nodeIsDocumentLeaf = isCliLeaf(node) && isDocumentLeaf(node);
807
+ const usage = usageLines(
808
+ schema.key,
809
+ helpPath,
810
+ isCliRouter(node) && node.commands.length > 0,
811
+ isCliLeaf(node) && (node.positionals ?? []).length > 0,
812
+ nodeIsDocumentLeaf,
813
+ color,
814
+ isCliLeaf(node) ? node.kind : undefined,
490
815
  );
816
+ if (isTTY) {
817
+ lines.push(renderTextBox("Usage", usage, hw, color).join("\n"));
818
+ } else {
819
+ lines.push(renderPlainSection("Usage", usage).join("\n"));
820
+ }
491
821
 
492
- if (nodeIsJsonLeaf && isCliLeaf(node)) {
493
- const inputBox = renderTableBox("Input", rowsForJsonInput(node.inputSchema), hw, color);
822
+ if (nodeIsDocumentLeaf && isCliLeaf(node)) {
823
+ const inputRows = rowsForJsonInput(node.inputSchema, node.kind);
824
+ const inputBox = isTTY ? renderTableBox("Input", inputRows, hw, color) : renderPlainTable("Input", inputRows, hw);
494
825
  if (inputBox.length > 0) {
495
826
  lines.push("");
496
827
  lines.push(inputBox.join("\n"));
497
828
  }
829
+ if (showSchema && node.inputSchema !== undefined) {
830
+ const yamlLines = schemaToYamlLines(node.inputSchema, 0);
831
+ if (yamlLines.length > 0) {
832
+ lines.push("");
833
+ if (isTTY) {
834
+ lines.push(renderTextBox("Input Schema", yamlLines, hw, color).join("\n"));
835
+ } else {
836
+ lines.push(renderPlainSection("Input Schema", yamlLines).join("\n"));
837
+ }
838
+ }
839
+ }
498
840
  } else {
499
- const optBox = renderTableBox("Options", rowsForOptions(visibleOptions(node.options), color), hw, color);
841
+ const optRows = rowsForOptions(visibleOptions(node.options), color);
842
+ const optBox = isTTY ? renderTableBox("Options", optRows, hw, color) : renderPlainTable("Options", optRows, hw);
500
843
  if (optBox.length > 0) {
501
844
  lines.push("");
502
845
  lines.push(optBox.join("\n"));
503
846
  }
504
847
 
505
- const posBox = renderTableBox(
506
- "Arguments",
507
- rowsForPositionals(isCliLeaf(node) ? (node.positionals ?? []) : [], color),
508
- hw,
509
- color,
510
- );
848
+ const posRows = rowsForPositionals(isCliLeaf(node) ? (node.positionals ?? []) : [], color);
849
+ const posBox = isTTY ? renderTableBox("Arguments", posRows, hw, color) : renderPlainTable("Arguments", posRows, hw);
511
850
  if (posBox.length > 0) {
512
851
  lines.push("");
513
852
  lines.push(posBox.join("\n"));
@@ -515,14 +854,30 @@ export function cliHelpRender(schema: CliRouter, helpPath: string[], _useStderr:
515
854
  }
516
855
 
517
856
  const subcmds = isCliRouter(node) ? node.commands : [];
518
- const subBox = renderTableBox("Subcommands", rowsForSubcommands(subcmds), hw, color);
857
+ const subRows = rowsForSubcommands(subcmds);
858
+ const subBox = isTTY
859
+ ? renderTableBox("Subcommands", subRows, hw, color)
860
+ : renderPlainTable("Subcommands", subRows, hw);
519
861
  if (subBox.length > 0) {
520
862
  lines.push("");
521
863
  lines.push(subBox.join("\n"));
522
864
  }
523
865
 
866
+ if (isCliLeaf(node) && node.outputSchema !== undefined && showSchema) {
867
+ const title = nodeIsDocumentLeaf ? "Output Schema (JSON)" : "Output Schema (with --json)";
868
+ const yamlLines = schemaToYamlLines(node.outputSchema, 0);
869
+ if (yamlLines.length > 0) {
870
+ lines.push("");
871
+ if (isTTY) {
872
+ lines.push(renderTextBox(title, yamlLines, hw, color).join("\n"));
873
+ } else {
874
+ lines.push(renderPlainSection(title, yamlLines).join("\n"));
875
+ }
876
+ }
877
+ }
878
+
524
879
  if ((node.notes ?? "").length > 0) {
525
- appendNotesBox(lines, node.notes, schema.key, hw, color);
880
+ appendNotesBox(lines, node.notes, schema.key, hw, color, isTTY);
526
881
  }
527
882
 
528
883
  return `${lines.join("\n")}\n\n`;
@@ -3,7 +3,7 @@ Hand-built OpenAPI 3.1 document from exposed HTTP REST routes.
3
3
  */
4
4
 
5
5
  import type { CliHttpMethod, CliNode, CliProgram } from "../core/types.ts";
6
- import { CliOptionKind, isCliLeaf, isJsonLeaf } from "../core/types.ts";
6
+ import { CliOptionKind, isCliLeaf, isDocumentLeaf } from "../core/types.ts";
7
7
  import { leafWireOptions } from "../mcp/tools.ts";
8
8
  import { collectHttpRoutes, defaultSuccessStatus } from "./routes.ts";
9
9
  import { dereferenceJsonSchema } from "./schema-deref.ts";
@@ -253,7 +253,7 @@ export function generateOpenApi(program: CliProgram): Record<string, unknown> {
253
253
  ];
254
254
  } else {
255
255
  op.requestBody = {
256
- required: isJsonLeaf(route.leaf),
256
+ required: isDocumentLeaf(route.leaf),
257
257
  content: {
258
258
  [JSON_CONTENT_TYPE]: {
259
259
  schema: dereferenceJsonSchema(buildInputSchema(program, route)),
@@ -8,7 +8,7 @@ import {
8
8
  type CliNode,
9
9
  type CliProgram,
10
10
  isCliLeaf,
11
- isJsonLeaf,
11
+ isDocumentLeaf,
12
12
  CliOptionKind as OptKind,
13
13
  } from "../core/types.ts";
14
14
  import { formatMcpOptionValue, leafHasYesOption, leafWireOptions } from "../mcp/tools.ts";
@@ -252,7 +252,7 @@ export function httpRequestToArgv(
252
252
  }
253
253
 
254
254
  const leaf = route.leaf;
255
- if (isJsonLeaf(leaf)) {
255
+ if (isDocumentLeaf(leaf)) {
256
256
  return argv;
257
257
  }
258
258
 
@@ -175,7 +175,15 @@ export async function handleApiRequest(
175
175
  }
176
176
  body = parsed as Record<string, unknown>;
177
177
  } catch {
178
- return finish(apiErrorResponse(400, { error: "Invalid JSON body" }));
178
+ try {
179
+ const parsed = Bun.YAML.parse(rawBody);
180
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
181
+ return finish(apiErrorResponse(400, { error: "Request body must be a JSON object" }));
182
+ }
183
+ body = parsed as Record<string, unknown>;
184
+ } catch {
185
+ return finish(apiErrorResponse(400, { error: "Invalid JSON body" }));
186
+ }
179
187
  }
180
188
  }
181
189
  }
package/src/index.ts CHANGED
@@ -18,6 +18,7 @@ export {
18
18
  } from "./core/formats.ts";
19
19
  export {
20
20
  LeafInputError,
21
+ parseDocumentText,
21
22
  preloadPipableJson,
22
23
  readJsonOptionValue,
23
24
  } from "./core/leaf-inputs.ts";
@@ -68,6 +69,7 @@ export {
68
69
  CliOptionKind,
69
70
  CliSchemaValidationError,
70
71
  CliValueFormat,
72
+ isDocumentLeaf,
71
73
  isJsonLeaf,
72
74
  } from "./core/types.ts";
73
75
  export type { HeadlessContext } from "./headless/routing.ts";