stitchkit 0.90.2 → 0.90.4
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 +129 -0
- package/dist/agent-runtime-coding-tools.js +3 -3
- package/dist/agent-runtime-harness.js +4 -4
- package/dist/agent-runtime-sandbox.js +3 -3
- package/dist/agent-runtime.js +7 -7
- package/dist/cli.d.ts +14 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +359 -11
- package/dist/{index-gbqjt8jz.js → index-19ryv24q.js} +3 -3
- package/dist/{index-2hrfpw2c.js → index-32vjke6q.js} +2 -2
- package/dist/{index-f475yj00.js → index-6atvfjc1.js} +1 -1
- package/dist/{index-79hb1wyh.js → index-7t0wq6j5.js} +3 -3
- package/dist/{index-j7q0xj6d.js → index-8crm1srv.js} +1 -1
- package/dist/{index-kc6h6hg0.js → index-8z9we758.js} +1 -1
- package/dist/{index-r159gjwy.js → index-fenaekmk.js} +94 -8
- package/dist/{index-wb15909q.js → index-kzxpsf8y.js} +3 -3
- package/dist/{index-44ht2790.js → index-vcnfwrtr.js} +1 -1
- package/dist/{index-8dph3pw6.js → index-y47z614h.js} +405 -35
- package/dist/testing.js +1 -1
- package/dist/tool-invoker.js +4 -4
- package/dist/tools/cli-args.d.ts +49 -0
- package/dist/tools/cli-args.d.ts.map +1 -1
- package/dist/tools/cli-command.d.ts +8 -1
- package/dist/tools/cli-command.d.ts.map +1 -1
- package/dist/tools/cli-installer.d.ts +29 -0
- package/dist/tools/cli-installer.d.ts.map +1 -0
- package/dist/tools/cli-manifest.d.ts +83 -0
- package/dist/tools/cli-manifest.d.ts.map +1 -0
- package/dist/tools/cli-policy.d.ts +6 -1
- package/dist/tools/cli-policy.d.ts.map +1 -1
- package/dist/tools/cli-profile.d.ts +40 -0
- package/dist/tools/cli-profile.d.ts.map +1 -0
- package/dist/tools/cli-update.d.ts +64 -0
- package/dist/tools/cli-update.d.ts.map +1 -0
- package/dist/tools/cli-view.d.ts +12 -0
- package/dist/tools/cli-view.d.ts.map +1 -0
- package/dist/tools/cli.d.ts +20 -5
- package/dist/tools/cli.d.ts.map +1 -1
- package/dist/tools/connections/index.d.ts +2 -1
- package/dist/tools/connections/index.d.ts.map +1 -1
- package/dist/tools/connections/index.js +37 -10
- package/dist/tools/connections/mcp.d.ts +2 -1
- package/dist/tools/connections/mcp.d.ts.map +1 -1
- package/dist/tools/connections/mount.d.ts.map +1 -1
- package/dist/tools/connections/openapi.d.ts +2 -1
- package/dist/tools/connections/openapi.d.ts.map +1 -1
- package/dist/tools/connections/runtime.d.ts +17 -0
- package/dist/tools/connections/runtime.d.ts.map +1 -1
- package/dist/tools/connections/types.d.ts +18 -1
- package/dist/tools/connections/types.d.ts.map +1 -1
- package/dist/tools/json-schema-dialect.d.ts +17 -0
- package/dist/tools/json-schema-dialect.d.ts.map +1 -0
- package/dist/tools/presentation.d.ts +8 -1
- package/dist/tools/presentation.d.ts.map +1 -1
- package/dist/tools.js +8 -8
- package/llms-full.txt +293 -7
- package/package.json +1 -1
|
@@ -3,14 +3,15 @@ import {
|
|
|
3
3
|
} from "./index-sbdmyz75.js";
|
|
4
4
|
import {
|
|
5
5
|
collectToolSurface
|
|
6
|
-
} from "./index-
|
|
6
|
+
} from "./index-kzxpsf8y.js";
|
|
7
7
|
import {
|
|
8
8
|
createToolRunner,
|
|
9
9
|
formatToolError
|
|
10
|
-
} from "./index-
|
|
10
|
+
} from "./index-32vjke6q.js";
|
|
11
11
|
import {
|
|
12
|
+
toolErrorFromResult,
|
|
12
13
|
toolResultFromError
|
|
13
|
-
} from "./index-
|
|
14
|
+
} from "./index-vcnfwrtr.js";
|
|
14
15
|
import {
|
|
15
16
|
formatZodError,
|
|
16
17
|
validateDeclaredOutput
|
|
@@ -26,7 +27,7 @@ import {
|
|
|
26
27
|
assertUniqueToolName,
|
|
27
28
|
buildToolPresentationSchema,
|
|
28
29
|
objectShapeKeys
|
|
29
|
-
} from "./index-
|
|
30
|
+
} from "./index-fenaekmk.js";
|
|
30
31
|
import {
|
|
31
32
|
jsonSchemaFields
|
|
32
33
|
} from "./index-cby4ar3v.js";
|
|
@@ -38,7 +39,8 @@ import {
|
|
|
38
39
|
import { z } from "zod";
|
|
39
40
|
var NUMERIC_VALUE = /^-(\d+\.?\d*|\.\d+)$/;
|
|
40
41
|
var BOOL_OPTIONS = new Set(["json", "wait", "quiet", "dry-run", "help"]);
|
|
41
|
-
var
|
|
42
|
+
var VIEW_OPTIONS = new Set(["count-by", "sum", "by", "top", "table"]);
|
|
43
|
+
var VALUE_OPTIONS = new Set(["wait-timeout", "output-dir", ...VIEW_OPTIONS]);
|
|
42
44
|
var RESERVED_CLI_OPTIONS = new Set([...BOOL_OPTIONS, ...VALUE_OPTIONS]);
|
|
43
45
|
function classifyLongOptionToken(token) {
|
|
44
46
|
if (!token.startsWith("--") || token === "--")
|
|
@@ -314,6 +316,7 @@ function parseCliArgs(argv, schema, config = {}) {
|
|
|
314
316
|
}
|
|
315
317
|
const flags = new Map;
|
|
316
318
|
const boolFlags = new Map;
|
|
319
|
+
const viewFlags = new Map;
|
|
317
320
|
const positionals = [];
|
|
318
321
|
const pushFlag = (name, value) => {
|
|
319
322
|
const existing = flags.get(name);
|
|
@@ -399,6 +402,10 @@ function parseCliArgs(argv, schema, config = {}) {
|
|
|
399
402
|
throw new CliArgumentError("--wait-timeout must be a positive number");
|
|
400
403
|
}
|
|
401
404
|
options.waitTimeout = timeout;
|
|
405
|
+
} else if (VIEW_OPTIONS.has(name)) {
|
|
406
|
+
if (viewFlags.has(name))
|
|
407
|
+
throw new CliArgumentError(`--${name} was given twice`);
|
|
408
|
+
viewFlags.set(name, value);
|
|
402
409
|
} else {
|
|
403
410
|
options.outputDir = value;
|
|
404
411
|
}
|
|
@@ -432,12 +439,24 @@ function parseCliArgs(argv, schema, config = {}) {
|
|
|
432
439
|
}
|
|
433
440
|
pushFlag(name, value);
|
|
434
441
|
}
|
|
442
|
+
const view = resolveCliView(viewFlags);
|
|
443
|
+
if (view)
|
|
444
|
+
options.view = view;
|
|
435
445
|
const toolArgs = {};
|
|
436
446
|
const fillable = config.positionals === undefined ? [...fields.entries()].filter(([, info]) => info.kind !== "boolean").map(([name]) => name) : [...config.positionals];
|
|
447
|
+
const variadicTail = config.positionals !== undefined && fields.get(fillable[fillable.length - 1] ?? "")?.kind === "array" ? fillable[fillable.length - 1] : undefined;
|
|
437
448
|
let pi = 0;
|
|
438
449
|
for (const key of fillable) {
|
|
439
450
|
if (pi >= positionals.length)
|
|
440
451
|
break;
|
|
452
|
+
if (key === variadicTail) {
|
|
453
|
+
if (flags.has(key)) {
|
|
454
|
+
throw new CliArgumentError(`--${key} conflicts with the positional values for "${key}" — pass one form, not both`);
|
|
455
|
+
}
|
|
456
|
+
toolArgs[key] = coerceField(fields.get(key), positionals.slice(pi));
|
|
457
|
+
pi = positionals.length;
|
|
458
|
+
break;
|
|
459
|
+
}
|
|
441
460
|
if (flags.has(key))
|
|
442
461
|
continue;
|
|
443
462
|
const value = positionals[pi++];
|
|
@@ -477,6 +496,112 @@ function parseCliArgs(argv, schema, config = {}) {
|
|
|
477
496
|
}
|
|
478
497
|
return { toolArgs, options };
|
|
479
498
|
}
|
|
499
|
+
function extractCliGlobalOptions(argv, schema) {
|
|
500
|
+
if (schema === undefined)
|
|
501
|
+
return { argv: [...argv], globals: {} };
|
|
502
|
+
const fields = describeSchemaFields(schema);
|
|
503
|
+
const rest = [];
|
|
504
|
+
const raw = new Map;
|
|
505
|
+
let ended = false;
|
|
506
|
+
for (let i = 0;i < argv.length; i++) {
|
|
507
|
+
const token = argv[i];
|
|
508
|
+
if (token === undefined)
|
|
509
|
+
continue;
|
|
510
|
+
if (!ended && token === "--")
|
|
511
|
+
ended = true;
|
|
512
|
+
const option = ended ? undefined : classifyLongOptionToken(token);
|
|
513
|
+
const info = option && option.globalKind === undefined ? fields.get(option.name) : undefined;
|
|
514
|
+
if (!option || !info) {
|
|
515
|
+
rest.push(token);
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
let { value } = option;
|
|
519
|
+
if (value === undefined && info.kind === "boolean") {
|
|
520
|
+
value = "true";
|
|
521
|
+
} else if (value === undefined) {
|
|
522
|
+
const next = argv[i + 1];
|
|
523
|
+
if (next !== undefined && (!next.startsWith("-") || NUMERIC_VALUE.test(next))) {
|
|
524
|
+
value = next;
|
|
525
|
+
i++;
|
|
526
|
+
} else {
|
|
527
|
+
throw new CliArgumentError(`--${option.name} requires a value`);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
const existing = raw.get(option.name);
|
|
531
|
+
if (existing)
|
|
532
|
+
existing.push(value);
|
|
533
|
+
else
|
|
534
|
+
raw.set(option.name, [value]);
|
|
535
|
+
}
|
|
536
|
+
const args = {};
|
|
537
|
+
for (const [name, values] of raw)
|
|
538
|
+
args[name] = coerceField(fields.get(name), values);
|
|
539
|
+
const parsed = schema.safeParse(coerceJsonArgs(args, schema));
|
|
540
|
+
if (!parsed.success) {
|
|
541
|
+
const issue = parsed.error.issues[0];
|
|
542
|
+
const field = issue?.path[0];
|
|
543
|
+
const where = typeof field === "string" ? `--${field}` : "application option";
|
|
544
|
+
throw new CliArgumentError(`${where}: ${issue?.message ?? "invalid value"}`);
|
|
545
|
+
}
|
|
546
|
+
return { argv: rest, globals: parsed.data };
|
|
547
|
+
}
|
|
548
|
+
function resolveCliView(flags) {
|
|
549
|
+
if (flags.size === 0)
|
|
550
|
+
return;
|
|
551
|
+
const countBy = flags.get("count-by");
|
|
552
|
+
const sum = flags.get("sum");
|
|
553
|
+
const table = flags.get("table");
|
|
554
|
+
const by = flags.get("by");
|
|
555
|
+
const rawTop = flags.get("top");
|
|
556
|
+
const named = [
|
|
557
|
+
["--count-by", countBy],
|
|
558
|
+
["--sum", sum],
|
|
559
|
+
["--table", table]
|
|
560
|
+
].filter(([, value]) => value !== undefined);
|
|
561
|
+
if (named.length > 1) {
|
|
562
|
+
throw new CliArgumentError(`${named.map(([flag]) => flag).join(" and ")} ask for different shapes — pass one`);
|
|
563
|
+
}
|
|
564
|
+
let top;
|
|
565
|
+
if (rawTop !== undefined) {
|
|
566
|
+
top = Number(rawTop);
|
|
567
|
+
if (!Number.isInteger(top) || top <= 0) {
|
|
568
|
+
throw new CliArgumentError("--top must be a positive whole number");
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
if (table !== undefined) {
|
|
572
|
+
if (by !== undefined || top !== undefined) {
|
|
573
|
+
throw new CliArgumentError("--table lists records; it takes neither --by nor --top");
|
|
574
|
+
}
|
|
575
|
+
const fields = table.split(",").map((field) => field.trim()).filter(Boolean);
|
|
576
|
+
if (fields.length === 0)
|
|
577
|
+
throw new CliArgumentError("--table needs at least one field");
|
|
578
|
+
return { kind: "table", fields };
|
|
579
|
+
}
|
|
580
|
+
if (sum !== undefined) {
|
|
581
|
+
if (top !== undefined && by === undefined) {
|
|
582
|
+
throw new CliArgumentError("--top needs --by: a single sum has nothing to rank");
|
|
583
|
+
}
|
|
584
|
+
return {
|
|
585
|
+
kind: "sum",
|
|
586
|
+
field: sum,
|
|
587
|
+
...by !== undefined && { by },
|
|
588
|
+
...top !== undefined && { top }
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
if (countBy !== undefined) {
|
|
592
|
+
if (by !== undefined) {
|
|
593
|
+
throw new CliArgumentError("--count-by already names the grouping field; drop --by");
|
|
594
|
+
}
|
|
595
|
+
return { kind: "count", field: countBy, ...top !== undefined && { top } };
|
|
596
|
+
}
|
|
597
|
+
if (by !== undefined) {
|
|
598
|
+
if (top === undefined) {
|
|
599
|
+
throw new CliArgumentError("--by groups a view; pass --count-by, --sum or --top with it");
|
|
600
|
+
}
|
|
601
|
+
return { kind: "count", field: by, top };
|
|
602
|
+
}
|
|
603
|
+
throw new CliArgumentError("--top needs --by, --count-by or --sum to rank");
|
|
604
|
+
}
|
|
480
605
|
|
|
481
606
|
// src/tools/cli-command.ts
|
|
482
607
|
function defineCliCommand(definition) {
|
|
@@ -509,7 +634,7 @@ function cliCommandPresentationSchema(definition) {
|
|
|
509
634
|
unrepresentable: "any"
|
|
510
635
|
});
|
|
511
636
|
}
|
|
512
|
-
async function executeCliCommand(definition, rawArgs, options, writers, coerceJson) {
|
|
637
|
+
async function executeCliCommand(definition, rawArgs, options, writers, coerceJson, globals = {}) {
|
|
513
638
|
let parsed;
|
|
514
639
|
try {
|
|
515
640
|
parsed = definition.input.safeParse(coerceJson ? coerceJsonArgs(rawArgs, definition.input) : rawArgs);
|
|
@@ -527,6 +652,7 @@ async function executeCliCommand(definition, rawArgs, options, writers, coerceJs
|
|
|
527
652
|
const data = await definition.handler({
|
|
528
653
|
input: parsed.data,
|
|
529
654
|
options,
|
|
655
|
+
globals,
|
|
530
656
|
...writers
|
|
531
657
|
});
|
|
532
658
|
const checked = validateDeclaredOutput(definition.output, data);
|
|
@@ -573,6 +699,126 @@ function emitResult(result, writers, opts) {
|
|
|
573
699
|
return codes[result.code] ?? 1;
|
|
574
700
|
}
|
|
575
701
|
|
|
702
|
+
// src/tools/cli-view.ts
|
|
703
|
+
var ABSENT = "(absent)";
|
|
704
|
+
function selectRecords(data) {
|
|
705
|
+
if (Array.isArray(data))
|
|
706
|
+
return data;
|
|
707
|
+
if (isRecord(data)) {
|
|
708
|
+
const arrays = Object.entries(data).filter(([, value]) => Array.isArray(value));
|
|
709
|
+
const only = arrays[0];
|
|
710
|
+
if (arrays.length === 1 && only && Array.isArray(only[1]))
|
|
711
|
+
return only[1];
|
|
712
|
+
if (arrays.length > 1) {
|
|
713
|
+
throw new CliArgumentError(`an aggregate needs one collection, but the result carries several: ${arrays.map(([key]) => key).join(", ")} — name the shape you want with jq instead`);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
throw new CliArgumentError("an aggregate needs a collection; this result is not one");
|
|
717
|
+
}
|
|
718
|
+
function readField(record, path) {
|
|
719
|
+
let current = record;
|
|
720
|
+
for (const segment of path) {
|
|
721
|
+
if (!isRecord(current))
|
|
722
|
+
return;
|
|
723
|
+
current = current[segment];
|
|
724
|
+
}
|
|
725
|
+
return current;
|
|
726
|
+
}
|
|
727
|
+
function fieldPath(field) {
|
|
728
|
+
const path = field.split(".");
|
|
729
|
+
if (path.some((segment) => segment.length === 0 || isUnsafeKey(segment))) {
|
|
730
|
+
throw new CliArgumentError(`"${field}" is not a readable field path`);
|
|
731
|
+
}
|
|
732
|
+
return path;
|
|
733
|
+
}
|
|
734
|
+
function availableFields(records) {
|
|
735
|
+
const keys = new Set;
|
|
736
|
+
for (const record of records) {
|
|
737
|
+
if (isRecord(record))
|
|
738
|
+
for (const key of Object.keys(record))
|
|
739
|
+
keys.add(key);
|
|
740
|
+
}
|
|
741
|
+
return [...keys].sort();
|
|
742
|
+
}
|
|
743
|
+
function assertFieldPresent(records, field) {
|
|
744
|
+
const path = fieldPath(field);
|
|
745
|
+
const present = records.some((record) => readField(record, path) !== undefined);
|
|
746
|
+
if (!present) {
|
|
747
|
+
const available = availableFields(records);
|
|
748
|
+
throw new CliArgumentError(`no record carries the field "${field}"${available.length > 0 ? ` — available: ${available.join(", ")}` : ""}`);
|
|
749
|
+
}
|
|
750
|
+
return path;
|
|
751
|
+
}
|
|
752
|
+
function groupKey(value) {
|
|
753
|
+
if (value === undefined)
|
|
754
|
+
return ABSENT;
|
|
755
|
+
if (value === null)
|
|
756
|
+
return "null";
|
|
757
|
+
if (typeof value === "object")
|
|
758
|
+
return JSON.stringify(value);
|
|
759
|
+
return String(value);
|
|
760
|
+
}
|
|
761
|
+
function ordered(totals, top) {
|
|
762
|
+
const entries = [...totals].sort(([leftKey, left], [rightKey, right]) => right === left ? leftKey.localeCompare(rightKey) : right - left);
|
|
763
|
+
const sliced = top === undefined ? entries : entries.slice(0, top);
|
|
764
|
+
return Object.fromEntries(sliced);
|
|
765
|
+
}
|
|
766
|
+
function numericValue(value, field) {
|
|
767
|
+
if (value === undefined || value === null)
|
|
768
|
+
return;
|
|
769
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
770
|
+
return value;
|
|
771
|
+
if (typeof value === "bigint")
|
|
772
|
+
return Number(value);
|
|
773
|
+
throw new CliArgumentError(`--sum ${field}: "${groupKey(value)}" is not a number`);
|
|
774
|
+
}
|
|
775
|
+
function renderTable(records, fields) {
|
|
776
|
+
const paths = fields.map((field) => assertFieldPresent(records, field));
|
|
777
|
+
const cell = (value) => {
|
|
778
|
+
if (value === undefined)
|
|
779
|
+
return "";
|
|
780
|
+
if (value === null)
|
|
781
|
+
return "null";
|
|
782
|
+
if (typeof value === "object")
|
|
783
|
+
return JSON.stringify(value);
|
|
784
|
+
return String(value);
|
|
785
|
+
};
|
|
786
|
+
const rows = records.map((record) => paths.map((path) => cell(readField(record, path))));
|
|
787
|
+
const widths = fields.map((field, column) => Math.max(field.length, ...rows.map((row) => row[column]?.length ?? 0)));
|
|
788
|
+
const line = (cells) => cells.map((value, column) => value.padEnd(widths[column] ?? value.length)).join(" ").trimEnd();
|
|
789
|
+
return `${[line(fields), line(widths.map((width) => "-".repeat(width))), ...rows.map(line)].join(`
|
|
790
|
+
`)}
|
|
791
|
+
`;
|
|
792
|
+
}
|
|
793
|
+
function renderCliView(data, view) {
|
|
794
|
+
const records = selectRecords(data);
|
|
795
|
+
if (view.kind === "table")
|
|
796
|
+
return { kind: "text", text: renderTable(records, view.fields) };
|
|
797
|
+
if (view.kind === "count") {
|
|
798
|
+
const path2 = assertFieldPresent(records, view.field);
|
|
799
|
+
const totals2 = new Map;
|
|
800
|
+
for (const record of records) {
|
|
801
|
+
const key = groupKey(readField(record, path2));
|
|
802
|
+
totals2.set(key, (totals2.get(key) ?? 0) + 1);
|
|
803
|
+
}
|
|
804
|
+
return { kind: "json", data: ordered(totals2, view.top) };
|
|
805
|
+
}
|
|
806
|
+
const path = assertFieldPresent(records, view.field);
|
|
807
|
+
if (view.by === undefined) {
|
|
808
|
+
let total = 0;
|
|
809
|
+
for (const record of records)
|
|
810
|
+
total += numericValue(readField(record, path), view.field) ?? 0;
|
|
811
|
+
return { kind: "json", data: total };
|
|
812
|
+
}
|
|
813
|
+
const byPath = assertFieldPresent(records, view.by);
|
|
814
|
+
const totals = new Map;
|
|
815
|
+
for (const record of records) {
|
|
816
|
+
const key = groupKey(readField(record, byPath));
|
|
817
|
+
totals.set(key, (totals.get(key) ?? 0) + (numericValue(readField(record, path), view.field) ?? 0));
|
|
818
|
+
}
|
|
819
|
+
return { kind: "json", data: ordered(totals, view.top) };
|
|
820
|
+
}
|
|
821
|
+
|
|
576
822
|
// src/tools/wait-core.ts
|
|
577
823
|
var DEFAULT_BACKOFF = [2, 3, 5, 5, 8, 10];
|
|
578
824
|
var DEFAULT_TIMEOUT = 600;
|
|
@@ -1121,7 +1367,11 @@ var GLOBAL_OPTIONS = [
|
|
|
1121
1367
|
["--output-dir <dir>", "Download result media into a directory"],
|
|
1122
1368
|
["--quiet", "Suppress non-essential stderr output"],
|
|
1123
1369
|
["--dry-run", "Print the resolved call without executing it"],
|
|
1124
|
-
["--help, -h", "Show help for a command"]
|
|
1370
|
+
["--help, -h", "Show help for a command"],
|
|
1371
|
+
["--count-by <field>", "Count records per distinct value of a field"],
|
|
1372
|
+
["--sum <field>", "Total a numeric field, optionally grouped by --by"],
|
|
1373
|
+
["--top <n> --by <f>", "Keep only the n largest groups"],
|
|
1374
|
+
["--table <a,b>", "Render the named fields as an aligned table"]
|
|
1125
1375
|
];
|
|
1126
1376
|
async function readPipedStdin() {
|
|
1127
1377
|
if (process.stdin.isTTY)
|
|
@@ -1190,7 +1440,19 @@ function collectPassthrough(toolArgs, field, knownKeys) {
|
|
|
1190
1440
|
const base = passthroughBase(toolArgs[field]);
|
|
1191
1441
|
toolArgs[field] = base ? { ...base, ...bag } : bag;
|
|
1192
1442
|
}
|
|
1193
|
-
function
|
|
1443
|
+
function applicationOptionLines(fields) {
|
|
1444
|
+
if (fields.length === 0)
|
|
1445
|
+
return [];
|
|
1446
|
+
const labels = new Map(fields.map((field) => [field.name, `--${field.name} <${typeLabel(field.schema)}>`]));
|
|
1447
|
+
const width = Math.max(...fields.map((field) => labels.get(field.name)?.length ?? 0));
|
|
1448
|
+
return [
|
|
1449
|
+
"",
|
|
1450
|
+
"Application options:",
|
|
1451
|
+
...fields.map((field) => ` ${padRight(labels.get(field.name) ?? `--${field.name}`, width)} ${field.description ?? ""}`.trimEnd())
|
|
1452
|
+
];
|
|
1453
|
+
}
|
|
1454
|
+
function renderTopHelp(input) {
|
|
1455
|
+
const { name, version, commands, defaultCommand } = input;
|
|
1194
1456
|
const lines = [
|
|
1195
1457
|
`${name} ${version}`,
|
|
1196
1458
|
"",
|
|
@@ -1202,29 +1464,35 @@ function renderTopHelp(name, version, commands, defaultCommand) {
|
|
|
1202
1464
|
for (const [command, descriptor] of commands) {
|
|
1203
1465
|
lines.push(` ${padRight(command, width)} ${summarize(descriptor.description)}${command === defaultCommand ? " (default)" : ""}`);
|
|
1204
1466
|
}
|
|
1467
|
+
if (input.unavailable !== undefined) {
|
|
1468
|
+
lines.push("", `Managed commands are unavailable: ${input.unavailable}`);
|
|
1469
|
+
}
|
|
1205
1470
|
lines.push("", "Global options:");
|
|
1206
1471
|
const optWidth = Math.max(...GLOBAL_OPTIONS.map(([flag]) => flag.length));
|
|
1207
1472
|
for (const [flag, desc] of GLOBAL_OPTIONS)
|
|
1208
1473
|
lines.push(` ${padRight(flag, optWidth)} ${desc}`);
|
|
1474
|
+
lines.push(...applicationOptionLines(input.applicationOptions));
|
|
1209
1475
|
lines.push("", `Run "${name} <command> --help" for command-specific flags.`);
|
|
1210
1476
|
return `${lines.join(`
|
|
1211
1477
|
`)}
|
|
1212
1478
|
`;
|
|
1213
1479
|
}
|
|
1214
|
-
function renderCommandHelp(name, command, descriptor) {
|
|
1480
|
+
function renderCommandHelp(name, command, descriptor, applicationOptions = []) {
|
|
1215
1481
|
const fields = jsonSchemaFields(descriptor.presentationSchema);
|
|
1216
1482
|
const fieldsByName = new Map(fields.map((field) => [field.name, field]));
|
|
1483
|
+
const kinds = describeSchemaFields(descriptor.argumentSchema);
|
|
1217
1484
|
const positionals = [];
|
|
1218
|
-
const positionalNames = descriptor.positionals ?? [...
|
|
1485
|
+
const positionalNames = descriptor.positionals ?? [...kinds].filter(([, info]) => info.kind !== "boolean").map(([fieldName]) => fieldName);
|
|
1219
1486
|
for (const fieldName of positionalNames) {
|
|
1220
1487
|
const field = fieldsByName.get(fieldName);
|
|
1221
1488
|
if (field)
|
|
1222
1489
|
positionals.push(field);
|
|
1223
1490
|
}
|
|
1224
|
-
const
|
|
1225
|
-
|
|
1226
|
-
field.
|
|
1227
|
-
|
|
1491
|
+
const variadicTail = descriptor.positionals !== undefined && kinds.get(descriptor.positionals[descriptor.positionals.length - 1] ?? "")?.kind === "array" ? descriptor.positionals[descriptor.positionals.length - 1] : undefined;
|
|
1492
|
+
const positionalSyntax = new Map(positionals.map((field) => {
|
|
1493
|
+
const label = field.name === variadicTail ? `${field.name}...` : field.name;
|
|
1494
|
+
return [field.name, field.required ? `<${label}>` : `[${label}]`];
|
|
1495
|
+
}));
|
|
1228
1496
|
const usage = [
|
|
1229
1497
|
`Usage: ${name} ${command}`,
|
|
1230
1498
|
...positionals.map((field) => positionalSyntax.get(field.name) ?? field.name),
|
|
@@ -1248,6 +1516,9 @@ function renderCommandHelp(name, command, descriptor) {
|
|
|
1248
1516
|
}
|
|
1249
1517
|
lines.push("");
|
|
1250
1518
|
}
|
|
1519
|
+
const applicationLines = applicationOptionLines(applicationOptions);
|
|
1520
|
+
if (applicationLines.length > 0)
|
|
1521
|
+
lines.push(...applicationLines.slice(1), "");
|
|
1251
1522
|
return `${lines.join(`
|
|
1252
1523
|
`)}
|
|
1253
1524
|
`;
|
|
@@ -1266,15 +1537,20 @@ function nativeDescriptor(definition) {
|
|
|
1266
1537
|
presentationSchema: cliCommandPresentationSchema(definition)
|
|
1267
1538
|
};
|
|
1268
1539
|
}
|
|
1269
|
-
function assertCommandShape(name, descriptor, exists, passthroughField) {
|
|
1540
|
+
function assertCommandShape(name, descriptor, exists, passthroughField, applicationGlobals = new Set) {
|
|
1270
1541
|
assertUniqueToolName(name, exists, "CLI command");
|
|
1271
1542
|
if (name === "help" || name === "version") {
|
|
1272
1543
|
throw new Error(`[stitchkit] CLI command "${name}" is reserved`);
|
|
1273
1544
|
}
|
|
1274
|
-
const
|
|
1545
|
+
const fieldNames = jsonSchemaFields(descriptor.presentationSchema).map((field) => field.name);
|
|
1546
|
+
const conflicting = fieldNames.filter((field) => RESERVED_CLI_OPTIONS.has(field));
|
|
1275
1547
|
if (conflicting.length > 0) {
|
|
1276
1548
|
throw new Error(`[stitchkit] CLI command "${name}" declares reserved option field(s): ${conflicting.join(", ")}`);
|
|
1277
1549
|
}
|
|
1550
|
+
const shadowed = fieldNames.filter((field) => applicationGlobals.has(field));
|
|
1551
|
+
if (shadowed.length > 0) {
|
|
1552
|
+
throw new Error(`[stitchkit] CLI command "${name}" declares field(s) shadowed by application global options: ${shadowed.join(", ")}`);
|
|
1553
|
+
}
|
|
1278
1554
|
if (passthroughField !== undefined && descriptor.aliases.has(passthroughField)) {
|
|
1279
1555
|
throw new Error(`[stitchkit] CLI command "${name}" cannot alias passthrough field "${passthroughField}"`);
|
|
1280
1556
|
}
|
|
@@ -1338,30 +1614,78 @@ async function downloadResults(files, dir, stderr, quiet, allowPrivate, maxBytes
|
|
|
1338
1614
|
return succeeded;
|
|
1339
1615
|
}
|
|
1340
1616
|
async function createCli(config) {
|
|
1617
|
+
let pendingWrite;
|
|
1618
|
+
let writeFailed = false;
|
|
1341
1619
|
const writeFd = (fd, text) => {
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1620
|
+
const bytes = Buffer.from(text, "utf8");
|
|
1621
|
+
let offset = 0;
|
|
1622
|
+
while (offset < bytes.length) {
|
|
1623
|
+
try {
|
|
1624
|
+
offset += writeSync(fd, bytes, offset, bytes.length - offset);
|
|
1625
|
+
} catch (error) {
|
|
1626
|
+
if (isRecord(error) && error.code === "EAGAIN") {
|
|
1627
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 1);
|
|
1628
|
+
continue;
|
|
1629
|
+
}
|
|
1630
|
+
const stream = fd === 1 ? process.stdout : process.stderr;
|
|
1631
|
+
const rest = bytes.subarray(offset);
|
|
1632
|
+
pendingWrite = new Promise((resolve2) => {
|
|
1633
|
+
stream.write(rest, (failure) => {
|
|
1634
|
+
if (failure)
|
|
1635
|
+
writeFailed = true;
|
|
1636
|
+
resolve2();
|
|
1637
|
+
});
|
|
1638
|
+
});
|
|
1639
|
+
return;
|
|
1640
|
+
}
|
|
1346
1641
|
}
|
|
1347
1642
|
};
|
|
1348
1643
|
const stdout = config.stdout ?? ((text) => writeFd(1, text));
|
|
1349
1644
|
const stderr = config.stderr ?? ((text) => writeFd(2, text));
|
|
1350
|
-
const exit = config.exit ?? ((code) =>
|
|
1645
|
+
const exit = config.exit ?? ((code) => {
|
|
1646
|
+
if (pendingWrite === undefined)
|
|
1647
|
+
process.exit(code);
|
|
1648
|
+
else
|
|
1649
|
+
pendingWrite.then(() => process.exit(writeFailed ? 1 : code));
|
|
1650
|
+
});
|
|
1351
1651
|
const argv = config.argv ?? process.argv.slice(2);
|
|
1352
1652
|
const readStdin = config.stdin ?? readPipedStdin;
|
|
1353
1653
|
if (config.auth !== undefined && config.resolveAuth !== undefined) {
|
|
1354
1654
|
throw new Error("[stitchkit] createCli: use either auth or resolveAuth, not both");
|
|
1355
1655
|
}
|
|
1656
|
+
const applicationOptions = config.globalOptions ? jsonSchemaFields(buildToolPresentationSchema({
|
|
1657
|
+
inputSchema: config.globalOptions,
|
|
1658
|
+
unrepresentable: "any"
|
|
1659
|
+
})) : [];
|
|
1660
|
+
const applicationGlobalNames = new Set(applicationOptions.map((field) => field.name));
|
|
1661
|
+
for (const name of applicationGlobalNames) {
|
|
1662
|
+
if (RESERVED_CLI_OPTIONS.has(name)) {
|
|
1663
|
+
throw new Error(`[stitchkit] CLI global option "--${name}" is reserved by the framework`);
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1356
1666
|
const nativeCommands = new Map;
|
|
1357
1667
|
const nativeHelp = new Map;
|
|
1358
1668
|
for (const definition of config.commands ?? []) {
|
|
1359
1669
|
const descriptor2 = applyCliPresentationPolicy(definition.name, nativeDescriptor(definition), config);
|
|
1360
|
-
assertCommandShape(definition.name, descriptor2, nativeCommands.has(definition.name), config.passthrough?.[definition.name]);
|
|
1670
|
+
assertCommandShape(definition.name, descriptor2, nativeCommands.has(definition.name), config.passthrough?.[definition.name], applicationGlobalNames);
|
|
1361
1671
|
nativeCommands.set(definition.name, definition);
|
|
1362
1672
|
nativeHelp.set(definition.name, descriptor2);
|
|
1363
1673
|
}
|
|
1364
|
-
|
|
1674
|
+
let globals;
|
|
1675
|
+
let routableArgv;
|
|
1676
|
+
try {
|
|
1677
|
+
const lifted = extractCliGlobalOptions(argv, config.globalOptions);
|
|
1678
|
+
globals = lifted.globals;
|
|
1679
|
+
routableArgv = lifted.argv;
|
|
1680
|
+
} catch (error) {
|
|
1681
|
+
if (!(error instanceof CliArgumentError))
|
|
1682
|
+
throw error;
|
|
1683
|
+
stderr(`${error.message}
|
|
1684
|
+
`);
|
|
1685
|
+
return exit(2);
|
|
1686
|
+
}
|
|
1687
|
+
const typedGlobals = globals;
|
|
1688
|
+
const route = routeCliArgv(routableArgv, config.defaultCommand);
|
|
1365
1689
|
if (route.error) {
|
|
1366
1690
|
stderr(`${route.error}
|
|
1367
1691
|
`);
|
|
@@ -1381,7 +1705,7 @@ async function createCli(config) {
|
|
|
1381
1705
|
if (!descriptor2)
|
|
1382
1706
|
throw new Error("[stitchkit] native CLI descriptor invariant failed");
|
|
1383
1707
|
if (helpRequested) {
|
|
1384
|
-
stdout(renderCommandHelp(config.name, command, descriptor2));
|
|
1708
|
+
stdout(renderCommandHelp(config.name, command, descriptor2, applicationOptions));
|
|
1385
1709
|
return exit(0);
|
|
1386
1710
|
}
|
|
1387
1711
|
const prepared2 = await prepareInvocation(command, commandArgv, descriptor2, config, readStdin);
|
|
@@ -1392,7 +1716,7 @@ async function createCli(config) {
|
|
|
1392
1716
|
}
|
|
1393
1717
|
const { toolArgs: toolArgs2, options: options2 } = prepared2;
|
|
1394
1718
|
if (options2.help) {
|
|
1395
|
-
stdout(renderCommandHelp(config.name, command, descriptor2));
|
|
1719
|
+
stdout(renderCommandHelp(config.name, command, descriptor2, applicationOptions));
|
|
1396
1720
|
return exit(0);
|
|
1397
1721
|
}
|
|
1398
1722
|
if (options2.wait) {
|
|
@@ -1415,7 +1739,7 @@ async function createCli(config) {
|
|
|
1415
1739
|
`);
|
|
1416
1740
|
return exit(0);
|
|
1417
1741
|
}
|
|
1418
|
-
const result2 = await executeCliCommand(native, toolArgs2, options2, { stdout, stderr }, config.coerceJsonArgs ?? true);
|
|
1742
|
+
const result2 = await executeCliCommand(native, toolArgs2, options2, { stdout, stderr }, config.coerceJsonArgs ?? true, globals);
|
|
1419
1743
|
const emission = prepareCliCommandEmission(native, result2, options2);
|
|
1420
1744
|
let emittedExitCode;
|
|
1421
1745
|
if (emission.result.ok && emission.presentation !== undefined) {
|
|
@@ -1433,12 +1757,26 @@ async function createCli(config) {
|
|
|
1433
1757
|
}
|
|
1434
1758
|
let authPromise;
|
|
1435
1759
|
const resolveIdentity = () => {
|
|
1436
|
-
authPromise ??=
|
|
1760
|
+
authPromise ??= (async () => config.resolveAuth ? await config.resolveAuth(typedGlobals) : await config.auth)();
|
|
1437
1761
|
return authPromise;
|
|
1438
1762
|
};
|
|
1439
1763
|
const dynamicSurface = typeof config.services === "function" || typeof config.runtimeTools === "function";
|
|
1440
1764
|
const buildManagedSurface = async (forExecution) => {
|
|
1441
|
-
|
|
1765
|
+
let auth;
|
|
1766
|
+
if (dynamicSurface || forExecution) {
|
|
1767
|
+
try {
|
|
1768
|
+
auth = await resolveIdentity();
|
|
1769
|
+
} catch (error) {
|
|
1770
|
+
const failure = toolResultFromError(error);
|
|
1771
|
+
const normalized = toolErrorFromResult(failure);
|
|
1772
|
+
return {
|
|
1773
|
+
resolved: false,
|
|
1774
|
+
failure,
|
|
1775
|
+
reason: `${normalized.code}: ${normalized.message}`,
|
|
1776
|
+
help: new Map(nativeHelp)
|
|
1777
|
+
};
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1442
1780
|
const services = typeof config.services === "function" ? config.services(auth) : config.services ?? [];
|
|
1443
1781
|
const runtimeTools = typeof config.runtimeTools === "function" ? config.runtimeTools(auth) : config.runtimeTools ?? [];
|
|
1444
1782
|
const tools = new Map;
|
|
@@ -1448,19 +1786,34 @@ async function createCli(config) {
|
|
|
1448
1786
|
transport: "CLI"
|
|
1449
1787
|
})) {
|
|
1450
1788
|
const descriptor2 = applyCliPresentationPolicy(mountable.name, managedDescriptor(mountable), config);
|
|
1451
|
-
assertCommandShape(mountable.name, descriptor2, help.has(mountable.name), config.passthrough?.[mountable.name]);
|
|
1789
|
+
assertCommandShape(mountable.name, descriptor2, help.has(mountable.name), config.passthrough?.[mountable.name], applicationGlobalNames);
|
|
1452
1790
|
tools.set(mountable.name, mountable);
|
|
1453
1791
|
help.set(mountable.name, descriptor2);
|
|
1454
1792
|
}
|
|
1455
1793
|
assertCliPoliciesResolved(help, config);
|
|
1456
|
-
return { auth, help, tools };
|
|
1794
|
+
return { resolved: true, auth, help, tools };
|
|
1457
1795
|
};
|
|
1458
1796
|
const topLevelHelp = route.topLevelHelp || command === undefined || command === "help" || command === "--help" || command === "-h";
|
|
1459
1797
|
const managed = await buildManagedSurface(!topLevelHelp && !helpRequested);
|
|
1460
1798
|
if (topLevelHelp) {
|
|
1461
|
-
stdout(renderTopHelp(
|
|
1799
|
+
stdout(renderTopHelp({
|
|
1800
|
+
name: config.name,
|
|
1801
|
+
version: config.version,
|
|
1802
|
+
commands: managed.help,
|
|
1803
|
+
defaultCommand: config.defaultCommand,
|
|
1804
|
+
applicationOptions,
|
|
1805
|
+
...managed.resolved ? {} : { unavailable: managed.reason }
|
|
1806
|
+
}));
|
|
1462
1807
|
return exit(0);
|
|
1463
1808
|
}
|
|
1809
|
+
if (!managed.resolved) {
|
|
1810
|
+
return exit(emitResult(managed.failure, { stdout, stderr }, {
|
|
1811
|
+
json: beforeSeparator.includes("--json"),
|
|
1812
|
+
toolName: command ?? config.name,
|
|
1813
|
+
errorHint: config.errorHint,
|
|
1814
|
+
exitCodes: { ...DEFAULT_EXIT_CODES, ...config.exitCodes }
|
|
1815
|
+
}));
|
|
1816
|
+
}
|
|
1464
1817
|
const tool = managed.tools.get(command);
|
|
1465
1818
|
if (!tool) {
|
|
1466
1819
|
stderr(`Unknown command "${command}". Run "${config.name} --help" for the command list.
|
|
@@ -1471,7 +1824,7 @@ async function createCli(config) {
|
|
|
1471
1824
|
if (!descriptor)
|
|
1472
1825
|
throw new Error("[stitchkit] managed CLI descriptor invariant failed");
|
|
1473
1826
|
if (helpRequested) {
|
|
1474
|
-
stdout(renderCommandHelp(config.name, command, descriptor));
|
|
1827
|
+
stdout(renderCommandHelp(config.name, command, descriptor, applicationOptions));
|
|
1475
1828
|
return exit(0);
|
|
1476
1829
|
}
|
|
1477
1830
|
const prepared = await prepareInvocation(command, commandArgv, descriptor, config, readStdin);
|
|
@@ -1497,7 +1850,7 @@ async function createCli(config) {
|
|
|
1497
1850
|
return exit(2);
|
|
1498
1851
|
}
|
|
1499
1852
|
if (options.help) {
|
|
1500
|
-
stdout(renderCommandHelp(config.name, command, descriptor));
|
|
1853
|
+
stdout(renderCommandHelp(config.name, command, descriptor, applicationOptions));
|
|
1501
1854
|
return exit(0);
|
|
1502
1855
|
}
|
|
1503
1856
|
if (options.dryRun) {
|
|
@@ -1507,7 +1860,7 @@ async function createCli(config) {
|
|
|
1507
1860
|
}
|
|
1508
1861
|
const runTool = createToolRunner({
|
|
1509
1862
|
source: "cli",
|
|
1510
|
-
context: { ...config.context?.(managed.auth), signal: config.signal },
|
|
1863
|
+
context: { ...config.context?.(managed.auth, typedGlobals), signal: config.signal },
|
|
1511
1864
|
hooks: config.hooks,
|
|
1512
1865
|
lifecycle: config.lifecycle,
|
|
1513
1866
|
errorHint: config.errorHint,
|
|
@@ -1545,6 +1898,23 @@ async function createCli(config) {
|
|
|
1545
1898
|
if (options.outputDir && result.ok && config.download) {
|
|
1546
1899
|
downloadsOk = await downloadResults(config.download(result.data), options.outputDir, stderr, options.quiet, config.allowPrivateDownloadHosts ?? false, config.maxDownloadBytes ?? DEFAULT_DOWNLOAD_MAX_BYTES, config.downloadTimeoutMs);
|
|
1547
1900
|
}
|
|
1901
|
+
if (options.view && result.ok) {
|
|
1902
|
+
let viewed;
|
|
1903
|
+
try {
|
|
1904
|
+
viewed = renderCliView(result.data, options.view);
|
|
1905
|
+
} catch (error) {
|
|
1906
|
+
if (!(error instanceof CliArgumentError))
|
|
1907
|
+
throw error;
|
|
1908
|
+
stderr(`${error.message}
|
|
1909
|
+
`);
|
|
1910
|
+
return exit(2);
|
|
1911
|
+
}
|
|
1912
|
+
if (viewed.kind === "text") {
|
|
1913
|
+
stdout(viewed.text);
|
|
1914
|
+
return exit(downloadsOk ? 0 : 1);
|
|
1915
|
+
}
|
|
1916
|
+
result = { ...result, data: viewed.data };
|
|
1917
|
+
}
|
|
1548
1918
|
const exitCode = emitResult(result, { stdout, stderr }, {
|
|
1549
1919
|
json: options.json,
|
|
1550
1920
|
toolName: command,
|
|
@@ -1554,4 +1924,4 @@ async function createCli(config) {
|
|
|
1554
1924
|
return exit(downloadsOk ? exitCode : 1);
|
|
1555
1925
|
}
|
|
1556
1926
|
|
|
1557
|
-
export { WaitTimeoutError, runWaitOperation, fetchGuarded, fetchPinnedDocument, readCapped, parseCliArgs, defineCliCommand, DEFAULT_EXIT_CODES, emitResult, pollUntilDone, createCli };
|
|
1927
|
+
export { WaitTimeoutError, runWaitOperation, fetchGuarded, fetchPinnedDocument, readCapped, routeCliArgv, CliArgumentError, parseCliArgs, extractCliGlobalOptions, defineCliCommand, DEFAULT_EXIT_CODES, emitResult, renderCliView, pollUntilDone, createCli };
|