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/help.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/*
|
|
2
|
-
This module renders CLI help with wrapping, boxes, tables, and TTY color
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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 (
|
|
354
|
-
out.push(`${fullPath} ${
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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(
|
|
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
|
|
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
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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
|
|
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(
|
|
763
|
+
lines.push(subBox.join("\n"));
|
|
450
764
|
}
|
|
451
|
-
|
|
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
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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 (
|
|
493
|
-
const
|
|
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
|
|
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
|
|
506
|
-
|
|
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
|
|
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`;
|
package/src/http/openapi.ts
CHANGED
|
@@ -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,
|
|
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:
|
|
256
|
+
required: isDocumentLeaf(route.leaf),
|
|
257
257
|
content: {
|
|
258
258
|
[JSON_CONTENT_TYPE]: {
|
|
259
259
|
schema: dereferenceJsonSchema(buildInputSchema(program, route)),
|
package/src/http/routes.ts
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
type CliNode,
|
|
9
9
|
type CliProgram,
|
|
10
10
|
isCliLeaf,
|
|
11
|
-
|
|
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 (
|
|
255
|
+
if (isDocumentLeaf(leaf)) {
|
|
256
256
|
return argv;
|
|
257
257
|
}
|
|
258
258
|
|
package/src/http/server.ts
CHANGED
|
@@ -175,7 +175,15 @@ export async function handleApiRequest(
|
|
|
175
175
|
}
|
|
176
176
|
body = parsed as Record<string, unknown>;
|
|
177
177
|
} catch {
|
|
178
|
-
|
|
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";
|