stitchkit 0.118.1 → 0.119.0
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 +66 -0
- package/dist/browser/http.d.ts.map +1 -1
- package/dist/contract/client-types.d.ts +6 -3
- package/dist/contract/client-types.d.ts.map +1 -1
- package/dist/contract/define-validation.d.ts.map +1 -1
- package/dist/contract/define.d.ts +18 -12
- package/dist/contract/define.d.ts.map +1 -1
- package/dist/contract/endpoint-capabilities.d.ts +28 -0
- package/dist/contract/endpoint-capabilities.d.ts.map +1 -0
- package/dist/contract/factory.d.ts +49 -19
- package/dist/contract/factory.d.ts.map +1 -1
- package/dist/contract/tool-view.d.ts +1 -1
- package/dist/contract/tool-view.d.ts.map +1 -1
- package/dist/contract/transports.d.ts +5 -0
- package/dist/contract/transports.d.ts.map +1 -0
- package/dist/entrypoints/agent-runtime/coding-tools.js +6 -5
- package/dist/entrypoints/agent-runtime/harness.js +6 -5
- package/dist/entrypoints/agent-runtime/sandbox.js +6 -5
- package/dist/entrypoints/agent-runtime/sqlite/bun.js +9 -8
- package/dist/entrypoints/agent-runtime/sqlite/node.js +9 -8
- package/dist/entrypoints/agent-runtime/testing.js +9 -8
- package/dist/entrypoints/agent-runtime.js +14 -13
- package/dist/entrypoints/application/diagnostic-journal.js +3 -3
- package/dist/entrypoints/application/directory-inbox.js +4 -4
- package/dist/entrypoints/application/opentelemetry.js +1 -1
- package/dist/entrypoints/application.js +5 -5
- package/dist/entrypoints/cli/publish.js +4 -4
- package/dist/entrypoints/cli.js +8 -7
- package/dist/entrypoints/contract.d.ts +1 -1
- package/dist/entrypoints/contract.d.ts.map +1 -1
- package/dist/entrypoints/contract.js +6 -4
- package/dist/entrypoints/files.js +3 -3
- package/dist/entrypoints/geo/contract.d.ts +7 -0
- package/dist/entrypoints/geo/contract.d.ts.map +1 -0
- package/dist/entrypoints/geo/contract.js +6 -0
- package/dist/entrypoints/geo.js +60 -23
- package/dist/entrypoints/http.js +2 -2
- package/dist/entrypoints/index.js +8 -6
- package/dist/entrypoints/live.js +1 -1
- package/dist/entrypoints/node.js +7 -6
- package/dist/entrypoints/primitives.js +1 -1
- package/dist/entrypoints/process/owner-loss.js +1 -1
- package/dist/entrypoints/process.js +3 -3
- package/dist/entrypoints/react.js +1 -1
- package/dist/entrypoints/remote.js +4 -3
- package/dist/entrypoints/server.js +11 -10
- package/dist/entrypoints/telegram.js +2 -2
- package/dist/entrypoints/testing.js +7 -6
- package/dist/entrypoints/tools/connections.js +3 -2
- package/dist/entrypoints/tools/contract.js +1 -1
- package/dist/entrypoints/tools/invoker.js +5 -4
- package/dist/entrypoints/tools/mcp.js +7 -6
- package/dist/entrypoints/tools.js +17 -15
- package/dist/entrypoints/tracking/contract.js +5 -3
- package/dist/entrypoints/tracking/performance.js +3 -3
- package/dist/entrypoints/tracking.d.ts +1 -1
- package/dist/entrypoints/tracking.d.ts.map +1 -1
- package/dist/entrypoints/tracking.js +63 -83
- package/dist/entrypoints/xlsx.js +1 -1
- package/dist/geo/index.d.ts +2 -1
- package/dist/geo/index.d.ts.map +1 -1
- package/dist/geo/record.d.ts +8 -0
- package/dist/geo/record.d.ts.map +1 -0
- package/dist/geo/resolver.d.ts +1 -3
- package/dist/geo/resolver.d.ts.map +1 -1
- package/dist/geo/schema.d.ts +44 -0
- package/dist/geo/schema.d.ts.map +1 -0
- package/dist/geo/types.d.ts +2 -13
- package/dist/geo/types.d.ts.map +1 -1
- package/dist/{index-n5sv4z0w.js → index-1k18qzqa.js} +13 -15
- package/dist/{index-tsngj2b5.js → index-2dfvv1jg.js} +206 -206
- package/dist/{index-d9hq291n.js → index-4jpykzvw.js} +70 -81
- package/dist/{index-ghtpfz4z.js → index-57gqaq8b.js} +2 -2
- package/dist/index-69t14s4a.js +39 -0
- package/dist/{index-6p3frkv5.js → index-6a69xzrx.js} +4 -4
- package/dist/{index-dky0dcmf.js → index-72r9k0av.js} +6 -2
- package/dist/{index-vv1sch8a.js → index-8vzh5wpv.js} +13 -8
- package/dist/{index-xx1j310k.js → index-8yxgcyt6.js} +171 -169
- package/dist/{index-p6apg0jg.js → index-9frf01t9.js} +16 -9
- package/dist/{index-q0th8q0z.js → index-9mjrnqxs.js} +1 -3
- package/dist/{index-ega3yz4c.js → index-aqwg2dm5.js} +1 -1
- package/dist/{index-073nw9qr.js → index-ba7are04.js} +524 -522
- package/dist/{index-095ya5hm.js → index-bbvc59gw.js} +13 -11
- package/dist/{index-txg1npmg.js → index-cyxp8rbf.js} +1 -1
- package/dist/index-d3xdyjt6.js +38 -0
- package/dist/{index-n8z3rvzw.js → index-f4njz9j0.js} +3 -5
- package/dist/{index-90p34rts.js → index-fcqj44mb.js} +3 -1
- package/dist/{index-86nbfysb.js → index-ff39f9dr.js} +3 -3
- package/dist/{index-75ns2ybj.js → index-fqd4yqwd.js} +1 -1
- package/dist/{index-nqxrm176.js → index-g444vfz2.js} +1 -1
- package/dist/{index-vwhrnp67.js → index-kz9734jc.js} +3 -3
- package/dist/{index-dr3t5n1v.js → index-nn9qvkww.js} +46 -46
- package/dist/{index-qezhmp51.js → index-nz06qw5a.js} +3 -1
- package/dist/{index-1bx1xv1k.js → index-pfvde2fz.js} +5 -2
- package/dist/{index-s80awwsw.js → index-qch1vcaa.js} +3 -1
- package/dist/{index-cycp3r3e.js → index-rxcgp4b9.js} +2 -2
- package/dist/{index-w7jq3fz9.js → index-s0qjs6ee.js} +1198 -1196
- package/dist/{index-p0rf7sq9.js → index-s20s2mad.js} +209 -209
- package/dist/{index-v6vb5qx8.js → index-s3yxw6kt.js} +1 -1
- package/dist/{index-cqmqyjax.js → index-sa99p98w.js} +1 -1
- package/dist/{index-0gxvns6s.js → index-sba3e6m4.js} +1 -0
- package/dist/{index-f24bdt13.js → index-v1swr26p.js} +3 -1
- package/dist/{index-rhzkndr5.js → index-x4p3kw1p.js} +533 -530
- package/dist/{index-hdjmdwsj.js → index-x8d9fdeg.js} +513 -507
- package/dist/{index-htxh46ej.js → index-ycqhyars.js} +2 -2
- package/dist/{index-9bwcthxw.js → index-yj6saztf.js} +31 -13
- package/dist/{index-tdpmfks8.js → index-ynrf908v.js} +1 -1
- package/dist/{index-f6m4c545.js → index-zrdcmg6j.js} +4 -6
- package/dist/process/bun-child.d.ts +3 -2
- package/dist/process/bun-child.d.ts.map +1 -1
- package/dist/server/contract-method.d.ts.map +1 -1
- package/dist/tools/internal/surface-projector.d.ts.map +1 -1
- package/dist/tools/operations/async-operation-canonical-types.d.ts +1 -1
- package/dist/tracking/attribution.d.ts +21 -25
- package/dist/tracking/attribution.d.ts.map +1 -1
- package/dist/tracking/client-visit.d.ts +5 -7
- package/dist/tracking/client-visit.d.ts.map +1 -1
- package/dist/tracking/client.d.ts +2 -2
- package/dist/tracking/client.d.ts.map +1 -1
- package/dist/tracking/contract.d.ts +1 -1
- package/dist/tracking/host.d.ts +0 -1
- package/dist/tracking/host.d.ts.map +1 -1
- package/dist/tracking/privacy.d.ts +6 -0
- package/dist/tracking/privacy.d.ts.map +1 -1
- package/dist/tracking/schemas.d.ts +2 -2
- package/dist/tracking/schemas.d.ts.map +1 -1
- package/llms/contract.txt +46 -24
- package/llms/geo-contract.txt +23 -0
- package/llms/geo.txt +40 -4
- package/llms/stitchkit.txt +4 -3
- package/llms/tools.2.txt +2 -2
- package/llms/tools.3.txt +1 -1
- package/llms/tracking-contract.txt +1 -1
- package/llms/tracking.txt +27 -5
- package/llms/{upgrading-0.105.0-0.118.0.txt → upgrading-0.106.0-0.119.0.txt} +121 -410
- package/llms/upgrading-0.37.0-0.53.0.txt +969 -0
- package/llms/{upgrading-0.44.0-0.62.0.txt → upgrading-0.55.0-0.68.0.txt} +509 -528
- package/llms/{upgrading-0.63.0-0.83.0.txt → upgrading-0.69.0-0.95.0.txt} +506 -509
- package/llms/{upgrading-0.84.0-0.104.0.txt → upgrading-0.96.0-0.105.0.txt} +410 -506
- package/llms/upgrading.txt +5 -5
- package/llms-full.txt +247 -38
- package/llms.txt +10 -9
- package/package.json +23 -19
- package/llms/upgrading-0.37.0-0.44.0.txt +0 -444
|
@@ -14,15 +14,15 @@ import {
|
|
|
14
14
|
objectShapeKeys,
|
|
15
15
|
assertUniqueToolName,
|
|
16
16
|
buildToolPresentationSchema
|
|
17
|
-
} from "./index-
|
|
17
|
+
} from "./index-x4p3kw1p.js";
|
|
18
18
|
import {
|
|
19
19
|
toolResultFromError,
|
|
20
20
|
toolErrorFromResult
|
|
21
|
-
} from "./index-
|
|
21
|
+
} from "./index-s3yxw6kt.js";
|
|
22
22
|
import {
|
|
23
23
|
createToolRunner,
|
|
24
24
|
formatToolError
|
|
25
|
-
} from "./index-
|
|
25
|
+
} from "./index-ycqhyars.js";
|
|
26
26
|
import {
|
|
27
27
|
writeContainedFile
|
|
28
28
|
} from "./index-q968hy6y.js";
|
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
} from "./index-arjmb6de.js";
|
|
34
34
|
import {
|
|
35
35
|
collectToolSurface
|
|
36
|
-
} from "./index-
|
|
36
|
+
} from "./index-ff39f9dr.js";
|
|
37
37
|
import {
|
|
38
38
|
fetchGuarded,
|
|
39
39
|
readCapped
|
|
@@ -59,6 +59,78 @@ import {
|
|
|
59
59
|
isRecord
|
|
60
60
|
} from "./index-9jfdm5tg.js";
|
|
61
61
|
|
|
62
|
+
// src/tools/operations/wait-core.ts
|
|
63
|
+
var DEFAULT_BACKOFF = [2, 3, 5, 5, 8, 10];
|
|
64
|
+
var DEFAULT_TIMEOUT = 600;
|
|
65
|
+
|
|
66
|
+
class WaitTimeoutError extends Error {
|
|
67
|
+
constructor() {
|
|
68
|
+
super("wait deadline elapsed before the first snapshot");
|
|
69
|
+
this.name = "WaitTimeoutError";
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async function pollUntil(params) {
|
|
73
|
+
const backoff = params.backoff?.length ? params.backoff : DEFAULT_BACKOFF;
|
|
74
|
+
const lastBackoff = backoff[backoff.length - 1] ?? 5;
|
|
75
|
+
const timeoutSec = params.timeoutSec ?? DEFAULT_TIMEOUT;
|
|
76
|
+
const pause = params.sleepFn ?? sleep;
|
|
77
|
+
const now = params.nowFn ?? (() => performance.now());
|
|
78
|
+
const startedAt = now();
|
|
79
|
+
const deadline = startedAt + timeoutSec * 1000;
|
|
80
|
+
let last;
|
|
81
|
+
for (let attempt = 0;; attempt++) {
|
|
82
|
+
params.signal?.throwIfAborted();
|
|
83
|
+
const remainingBeforePoll = Math.max(0, deadline - now());
|
|
84
|
+
if (remainingBeforePoll === 0 && last) {
|
|
85
|
+
return { state: last.state, timedOut: true };
|
|
86
|
+
}
|
|
87
|
+
const deadlineController = new AbortController;
|
|
88
|
+
const onCallerAbort = () => deadlineController.abort(params.signal?.reason);
|
|
89
|
+
params.signal?.addEventListener("abort", onCallerAbort, { once: true });
|
|
90
|
+
const deadlineTimer = setTimeout(() => deadlineController.abort(new WaitTimeoutError), remainingBeforePoll);
|
|
91
|
+
deadlineTimer.unref?.();
|
|
92
|
+
let state;
|
|
93
|
+
try {
|
|
94
|
+
state = await params.poll(deadlineController.signal);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
if (params.signal?.aborted)
|
|
97
|
+
throw params.signal.reason ?? error;
|
|
98
|
+
if (deadlineController.signal.aborted && now() >= deadline) {
|
|
99
|
+
if (last)
|
|
100
|
+
return { state: last.state, timedOut: true };
|
|
101
|
+
throw new WaitTimeoutError;
|
|
102
|
+
}
|
|
103
|
+
throw error;
|
|
104
|
+
} finally {
|
|
105
|
+
clearTimeout(deadlineTimer);
|
|
106
|
+
params.signal?.removeEventListener("abort", onCallerAbort);
|
|
107
|
+
}
|
|
108
|
+
params.signal?.throwIfAborted();
|
|
109
|
+
last = { state };
|
|
110
|
+
if (params.done(state))
|
|
111
|
+
return { state, timedOut: false };
|
|
112
|
+
const elapsedMs = now() - startedAt;
|
|
113
|
+
const elapsedSec = elapsedMs / 1000;
|
|
114
|
+
params.onTick?.(attempt + 1, elapsedSec);
|
|
115
|
+
if (elapsedSec >= timeoutSec)
|
|
116
|
+
return { state, timedOut: true };
|
|
117
|
+
const waitSec = backoff[Math.min(attempt, backoff.length - 1)] ?? lastBackoff;
|
|
118
|
+
await pause(Math.min(waitSec * 1000, Math.max(0, deadline - now())), params.signal);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function runWaitOperation(params) {
|
|
122
|
+
return pollUntil({
|
|
123
|
+
poll: (signal) => params.poll(params.input, signal),
|
|
124
|
+
done: params.done,
|
|
125
|
+
backoff: params.backoff,
|
|
126
|
+
timeoutSec: params.timeoutSec,
|
|
127
|
+
sleepFn: params.sleepFn,
|
|
128
|
+
onTick: params.onTick,
|
|
129
|
+
signal: params.signal,
|
|
130
|
+
nowFn: params.nowFn
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
62
134
|
// src/tools/cli/command.ts
|
|
63
135
|
function defineCliCommand2(definition) {
|
|
64
136
|
return definition;
|
|
@@ -201,13 +273,15 @@ function emitPresentationFailure(result, failure, writers, opts) {
|
|
|
201
273
|
}, writers, { json: opts.json, toolName: opts.toolName, exitCodes: opts.exitCodes });
|
|
202
274
|
}
|
|
203
275
|
|
|
276
|
+
// src/tools/cli/args-fields.ts
|
|
277
|
+
import { z } from "zod";
|
|
278
|
+
|
|
204
279
|
// src/tools/cli/argument-error.ts
|
|
205
280
|
class CliArgumentError2 extends Error {
|
|
206
281
|
name = "CliArgumentError";
|
|
207
282
|
}
|
|
208
283
|
|
|
209
284
|
// src/tools/cli/args-fields.ts
|
|
210
|
-
import { z } from "zod";
|
|
211
285
|
function appendCliOptionValue(raw, name, value, info) {
|
|
212
286
|
const existing = raw.get(name);
|
|
213
287
|
const repeatable = info?.kind === "array" || !name.includes(".") && info === undefined;
|
|
@@ -724,1287 +798,978 @@ function managedCliDescriptor(tool) {
|
|
|
724
798
|
};
|
|
725
799
|
}
|
|
726
800
|
|
|
727
|
-
// src/tools/cli/
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
function
|
|
732
|
-
|
|
801
|
+
// src/tools/cli/io.ts
|
|
802
|
+
import { writeSync } from "node:fs";
|
|
803
|
+
|
|
804
|
+
// src/tools/cli/stdin.ts
|
|
805
|
+
function readPipedStdin(input = process.stdin) {
|
|
806
|
+
if (input.isTTY || input.readableEnded)
|
|
807
|
+
return Promise.resolve(null);
|
|
808
|
+
if (input.destroyed)
|
|
809
|
+
return Promise.reject(new Error("stdin closed before EOF"));
|
|
810
|
+
return new Promise((resolve, reject) => {
|
|
811
|
+
const chunks = [];
|
|
812
|
+
const cleanup = () => {
|
|
813
|
+
clearTimeout(timer);
|
|
814
|
+
input.pause();
|
|
815
|
+
input.off("data", onData);
|
|
816
|
+
input.off("end", onEnd);
|
|
817
|
+
input.off("error", onError);
|
|
818
|
+
input.off("close", onClose);
|
|
819
|
+
};
|
|
820
|
+
const onData = (chunk) => {
|
|
821
|
+
if (chunk.length === 0)
|
|
822
|
+
return;
|
|
823
|
+
clearTimeout(timer);
|
|
824
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
825
|
+
};
|
|
826
|
+
const onEnd = () => {
|
|
827
|
+
cleanup();
|
|
828
|
+
const text = Buffer.concat(chunks).toString("utf8").trim();
|
|
829
|
+
resolve(text.length > 0 ? text : null);
|
|
830
|
+
};
|
|
831
|
+
const onError = (error) => {
|
|
832
|
+
cleanup();
|
|
833
|
+
reject(error);
|
|
834
|
+
};
|
|
835
|
+
const onClose = () => onError(new Error("stdin closed before EOF"));
|
|
836
|
+
const timer = setTimeout(() => {
|
|
837
|
+
cleanup();
|
|
838
|
+
resolve(null);
|
|
839
|
+
}, 250);
|
|
840
|
+
input.on("end", onEnd);
|
|
841
|
+
input.on("error", onError);
|
|
842
|
+
input.on("close", onClose);
|
|
843
|
+
input.on("data", onData);
|
|
844
|
+
input.resume();
|
|
845
|
+
});
|
|
733
846
|
}
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
847
|
+
|
|
848
|
+
// src/tools/cli/io.ts
|
|
849
|
+
function createCliIo(config) {
|
|
850
|
+
let pendingWrite;
|
|
851
|
+
let writeFailed = false;
|
|
852
|
+
const writeFd = (fd, text) => {
|
|
853
|
+
const bytes = Buffer.from(text, "utf8");
|
|
854
|
+
let offset = 0;
|
|
855
|
+
while (offset < bytes.length) {
|
|
856
|
+
try {
|
|
857
|
+
offset += writeSync(fd, bytes, offset, bytes.length - offset);
|
|
858
|
+
} catch (error) {
|
|
859
|
+
if (isRecord(error) && error.code === "EAGAIN") {
|
|
860
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 1);
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
const stream = fd === 1 ? process.stdout : process.stderr;
|
|
864
|
+
const rest = bytes.subarray(offset);
|
|
865
|
+
pendingWrite = new Promise((resolve) => {
|
|
866
|
+
stream.write(rest, (failure) => {
|
|
867
|
+
if (failure)
|
|
868
|
+
writeFailed = true;
|
|
869
|
+
resolve();
|
|
870
|
+
});
|
|
871
|
+
});
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
748
875
|
};
|
|
876
|
+
const stdout = config.stdout ?? ((text) => writeFd(1, text));
|
|
877
|
+
const stderr = config.stderr ?? ((text) => writeFd(2, text));
|
|
878
|
+
const exit = config.exit ?? ((code) => {
|
|
879
|
+
if (pendingWrite === undefined)
|
|
880
|
+
process.exit(code);
|
|
881
|
+
else
|
|
882
|
+
pendingWrite.then(() => process.exit(writeFailed ? 1 : code));
|
|
883
|
+
});
|
|
884
|
+
const readStdin = config.stdin ?? readPipedStdin;
|
|
885
|
+
return { stdout, stderr, exit, readStdin };
|
|
749
886
|
}
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
887
|
+
|
|
888
|
+
// src/tools/cli/download.ts
|
|
889
|
+
import { basename, resolve } from "node:path";
|
|
890
|
+
|
|
891
|
+
// src/internal/write-download.ts
|
|
892
|
+
import path from "node:path";
|
|
893
|
+
async function writeDownload(root, name, data) {
|
|
894
|
+
if (!isSafeEntryName(name) || path.basename(name) !== name) {
|
|
895
|
+
throw new Error("download name escapes the output dir");
|
|
757
896
|
}
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
897
|
+
const parent = { handle: root, basename: name };
|
|
898
|
+
const metadata = await containedEntryMetadata(parent);
|
|
899
|
+
await writeContainedFile({
|
|
900
|
+
parent,
|
|
901
|
+
content: data,
|
|
902
|
+
replace: true,
|
|
903
|
+
...metadata?.isFile() && { mode: metadata.mode }
|
|
904
|
+
});
|
|
762
905
|
}
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
906
|
+
|
|
907
|
+
// src/tools/cli/download.ts
|
|
908
|
+
var DEFAULT_DOWNLOAD_MAX_BYTES = 100 * 1024 * 1024;
|
|
909
|
+
async function downloadResults(files, dir, stderr, quiet, allowPrivate, maxBytes, timeoutMs) {
|
|
910
|
+
const root = resolve(dir);
|
|
911
|
+
if (files.length === 0)
|
|
912
|
+
return true;
|
|
913
|
+
let directory;
|
|
914
|
+
try {
|
|
915
|
+
directory = await openPinnedDirectory(root, { create: true, readable: false });
|
|
916
|
+
} catch (error) {
|
|
917
|
+
stderr(`failed to prepare download directory: ${error instanceof Error ? error.message : String(error)}
|
|
918
|
+
`);
|
|
919
|
+
return false;
|
|
774
920
|
}
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
version: false,
|
|
796
|
-
...helpFilter !== undefined && { helpFilter }
|
|
797
|
-
};
|
|
798
|
-
}
|
|
799
|
-
if (token === "--version" || token === "version") {
|
|
800
|
-
return { commandArgv: [], topLevelHelp: false, version: true };
|
|
801
|
-
}
|
|
802
|
-
if (!token.startsWith("-") || token === "-") {
|
|
803
|
-
return {
|
|
804
|
-
command: token,
|
|
805
|
-
commandArgv: [...globals, ...argv.slice(index + 1)],
|
|
806
|
-
topLevelHelp: false,
|
|
807
|
-
version: false
|
|
808
|
-
};
|
|
809
|
-
}
|
|
810
|
-
if (!token.startsWith("--")) {
|
|
811
|
-
return {
|
|
812
|
-
command: defaultCommand,
|
|
813
|
-
commandArgv: [...globals, ...argv.slice(index)],
|
|
814
|
-
topLevelHelp: false,
|
|
815
|
-
version: false
|
|
816
|
-
};
|
|
817
|
-
}
|
|
818
|
-
const option = classifyLongOptionToken(token);
|
|
819
|
-
if (!option) {
|
|
820
|
-
return {
|
|
821
|
-
command: defaultCommand,
|
|
822
|
-
commandArgv: [...globals, ...argv.slice(index)],
|
|
823
|
-
topLevelHelp: false,
|
|
824
|
-
version: false
|
|
825
|
-
};
|
|
826
|
-
}
|
|
827
|
-
if (option.globalKind === "boolean") {
|
|
828
|
-
globals.push(token);
|
|
829
|
-
index += 1;
|
|
830
|
-
const next = argv[index];
|
|
831
|
-
if (option.value === undefined && next !== undefined && separateBoolValue(next) !== undefined) {
|
|
832
|
-
globals.push(next);
|
|
833
|
-
index += 1;
|
|
921
|
+
let succeeded = true;
|
|
922
|
+
try {
|
|
923
|
+
for (const file of files) {
|
|
924
|
+
try {
|
|
925
|
+
const res = await fetchGuarded(new URL(file.url), allowPrivate, { timeoutMs });
|
|
926
|
+
if (!res.ok)
|
|
927
|
+
throw new Error(`HTTP ${res.status}`);
|
|
928
|
+
const buffer = await readCapped(res, maxBytes);
|
|
929
|
+
if (!buffer)
|
|
930
|
+
throw new Error(`file exceeds the ${maxBytes}-byte cap`);
|
|
931
|
+
const name = basename(file.name);
|
|
932
|
+
await writeDownload(directory, name, buffer);
|
|
933
|
+
if (!quiet)
|
|
934
|
+
stderr(`saved ${resolve(root, name)} (${(buffer.length / 1024).toFixed(0)}KB)
|
|
935
|
+
`);
|
|
936
|
+
} catch (err) {
|
|
937
|
+
succeeded = false;
|
|
938
|
+
const reason = errnoOf(err) ?? (err instanceof Error ? err.message : String(err));
|
|
939
|
+
stderr(`failed to download ${file.url}: ${reason}
|
|
940
|
+
`);
|
|
834
941
|
}
|
|
835
|
-
continue;
|
|
836
942
|
}
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
commandArgv: [],
|
|
845
|
-
topLevelHelp: true,
|
|
846
|
-
version: false,
|
|
847
|
-
...helpFilter !== undefined && { helpFilter }
|
|
848
|
-
};
|
|
849
|
-
}
|
|
850
|
-
if (value === "--version" || value === "version") {
|
|
851
|
-
return { commandArgv: [], topLevelHelp: false, version: true };
|
|
852
|
-
}
|
|
853
|
-
if (value !== undefined)
|
|
854
|
-
globals.push(value);
|
|
855
|
-
index += value === undefined ? 1 : 2;
|
|
856
|
-
} else {
|
|
857
|
-
index += 1;
|
|
858
|
-
}
|
|
859
|
-
continue;
|
|
943
|
+
} finally {
|
|
944
|
+
try {
|
|
945
|
+
await directory.close();
|
|
946
|
+
} catch (error) {
|
|
947
|
+
succeeded = false;
|
|
948
|
+
stderr(`failed to close download directory: ${error instanceof Error ? error.message : String(error)}
|
|
949
|
+
`);
|
|
860
950
|
}
|
|
861
|
-
return {
|
|
862
|
-
command: defaultCommand,
|
|
863
|
-
commandArgv: [...globals, ...argv.slice(index)],
|
|
864
|
-
topLevelHelp: false,
|
|
865
|
-
version: false
|
|
866
|
-
};
|
|
867
951
|
}
|
|
868
|
-
return
|
|
869
|
-
command: defaultCommand,
|
|
870
|
-
commandArgv: globals,
|
|
871
|
-
topLevelHelp: false,
|
|
872
|
-
version: false
|
|
873
|
-
};
|
|
952
|
+
return succeeded;
|
|
874
953
|
}
|
|
875
954
|
|
|
876
|
-
// src/tools/cli/
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
955
|
+
// src/tools/cli/help.ts
|
|
956
|
+
var GLOBAL_OPTIONS = [
|
|
957
|
+
["--json", "Emit compact success/error JSON records (for scripts)"],
|
|
958
|
+
["--wait", "Block-poll an async result to a terminal state"],
|
|
959
|
+
["--wait-timeout <s>", "Override the --wait timeout in seconds"],
|
|
960
|
+
["--output-dir <dir>", "Download result media into a directory"],
|
|
961
|
+
["--quiet", "Suppress non-essential stderr output"],
|
|
962
|
+
["--dry-run", "Print the resolved call without executing it"],
|
|
963
|
+
["--help, -h", "Show help for a command"],
|
|
964
|
+
["--help <text>", "List only the commands matching a substring"],
|
|
965
|
+
["--count-by <field>", "Count records per distinct value of a field"],
|
|
966
|
+
["--sum <field>", "Total a numeric field, optionally grouped by --by"],
|
|
967
|
+
["--sort <field>", "Order the records by a field, largest first"],
|
|
968
|
+
["--ascending", "Flip --sort to smallest first"],
|
|
969
|
+
["--top <n>", "Keep the n leading entries of the view asked for"],
|
|
970
|
+
["--table <a,b>", "Render the named fields as an aligned table"]
|
|
971
|
+
];
|
|
972
|
+
function typeLabel(schema) {
|
|
973
|
+
if (Array.isArray(schema.enum))
|
|
974
|
+
return schema.enum.join("|");
|
|
975
|
+
if (schema.type === "array")
|
|
976
|
+
return "value";
|
|
977
|
+
if (typeof schema.type === "string")
|
|
978
|
+
return schema.type;
|
|
979
|
+
return "value";
|
|
898
980
|
}
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
const
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
let ascending = false;
|
|
915
|
-
const positionals = [];
|
|
916
|
-
const pushFlag = (name, value, json = false) => {
|
|
917
|
-
appendCliFieldValue(flags, jsonFields, name, value, fieldFor(name), json);
|
|
918
|
-
};
|
|
919
|
-
let optionsEnded = false;
|
|
920
|
-
for (let i = 0;i < argv.length; i++) {
|
|
921
|
-
const tok = argv[i];
|
|
922
|
-
if (tok === undefined)
|
|
923
|
-
continue;
|
|
924
|
-
if (!optionsEnded && tok === "--") {
|
|
925
|
-
optionsEnded = true;
|
|
981
|
+
function constraintLabel(schema) {
|
|
982
|
+
const limits = [];
|
|
983
|
+
const bounds = [
|
|
984
|
+
["minimum", ">="],
|
|
985
|
+
["exclusiveMinimum", ">"],
|
|
986
|
+
["maximum", "<="],
|
|
987
|
+
["exclusiveMaximum", "<"],
|
|
988
|
+
["minLength", "length >="],
|
|
989
|
+
["maxLength", "length <="],
|
|
990
|
+
["minItems", "items >="],
|
|
991
|
+
["maxItems", "items <="]
|
|
992
|
+
];
|
|
993
|
+
for (const [keyword, label] of bounds) {
|
|
994
|
+
const bound = schema[keyword];
|
|
995
|
+
if (typeof bound !== "number")
|
|
926
996
|
continue;
|
|
927
|
-
|
|
928
|
-
if (optionsEnded || !looksLikeOption(tok)) {
|
|
929
|
-
positionals.push(tok);
|
|
997
|
+
if (schema.type === "integer" && Math.abs(bound) === Number.MAX_SAFE_INTEGER)
|
|
930
998
|
continue;
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
|
|
999
|
+
limits.push(`${label}${bound}`);
|
|
1000
|
+
}
|
|
1001
|
+
const compositions = [
|
|
1002
|
+
["anyOf", "any of", " | "],
|
|
1003
|
+
["oneOf", "one of", " | "],
|
|
1004
|
+
["allOf", "all of", " & "]
|
|
1005
|
+
];
|
|
1006
|
+
for (const [keyword, label, separator] of compositions) {
|
|
1007
|
+
const members = schema[keyword];
|
|
1008
|
+
if (!Array.isArray(members))
|
|
934
1009
|
continue;
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
const
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
throw new CliArgumentError2(`Unknown option "${tok}"`);
|
|
942
|
-
const inline = match?.[2];
|
|
943
|
-
const info = fieldFor(field);
|
|
944
|
-
if (info?.kind === "boolean") {
|
|
945
|
-
const separate = inline === undefined ? separateBoolValue(argv[i + 1]) : undefined;
|
|
946
|
-
if (separate !== undefined)
|
|
947
|
-
i++;
|
|
948
|
-
pushFlag(field, inline ?? String(separate ?? true));
|
|
949
|
-
if (inline !== undefined)
|
|
950
|
-
parseReservedBool(field, inline);
|
|
951
|
-
literalBooleanFlags.add(field);
|
|
952
|
-
continue;
|
|
953
|
-
}
|
|
954
|
-
let value = inline;
|
|
955
|
-
if (value === undefined) {
|
|
956
|
-
const next = argv[i + 1];
|
|
957
|
-
if (next !== undefined && !isOptionToken(next)) {
|
|
958
|
-
value = next;
|
|
959
|
-
i++;
|
|
960
|
-
} else {
|
|
961
|
-
throw new CliArgumentError2(`--${field} requires a value`);
|
|
962
|
-
}
|
|
963
|
-
}
|
|
964
|
-
pushFlag(field, value);
|
|
1010
|
+
const branches = members.map((member) => {
|
|
1011
|
+
const constraints = isRecord(member) ? constraintLabel(member) : "";
|
|
1012
|
+
const type = isRecord(member) ? typeLabel(member) : member === false ? "no value" : "any value";
|
|
1013
|
+
return { constraints, text: `${type}${constraints}` };
|
|
1014
|
+
});
|
|
1015
|
+
if (branches.every((branch) => branch.constraints === ""))
|
|
965
1016
|
continue;
|
|
966
|
-
}
|
|
967
|
-
const option = classifyLongOptionToken(tok);
|
|
968
|
-
if (!option)
|
|
969
|
-
throw new CliArgumentError2(`Unknown option "${tok}"`);
|
|
970
|
-
const { name, json } = cliFieldOption(option, fields);
|
|
971
|
-
let { value } = option;
|
|
972
|
-
if (name.length === 0)
|
|
973
|
-
throw new CliArgumentError2("Invalid empty option name");
|
|
974
|
-
if (json && RESERVED_CLI_OPTIONS.has(name)) {
|
|
975
|
-
throw new CliArgumentError2(`--${name} is a framework option and does not accept :=JSON`);
|
|
976
|
-
}
|
|
977
|
-
if (option.globalKind === "boolean") {
|
|
978
|
-
const separate = value === undefined ? separateBoolValue(argv[i + 1]) : undefined;
|
|
979
|
-
if (separate !== undefined)
|
|
980
|
-
i++;
|
|
981
|
-
if (name !== "help") {
|
|
982
|
-
appendCliOptionValue(frameworkFlags, name, value ?? String(separate ?? true), {
|
|
983
|
-
kind: "boolean"
|
|
984
|
-
});
|
|
985
|
-
}
|
|
986
|
-
const enabled = value === undefined ? separate ?? true : parseReservedBool(name, value);
|
|
987
|
-
if (name === "dry-run")
|
|
988
|
-
options.dryRun = enabled;
|
|
989
|
-
else if (name === "json")
|
|
990
|
-
options.json = enabled;
|
|
991
|
-
else if (name === "wait")
|
|
992
|
-
options.wait = enabled;
|
|
993
|
-
else if (name === "quiet")
|
|
994
|
-
options.quiet = enabled;
|
|
995
|
-
else if (name === "ascending")
|
|
996
|
-
ascending = enabled;
|
|
997
|
-
else
|
|
998
|
-
options.help = options.help || enabled;
|
|
999
|
-
continue;
|
|
1000
|
-
}
|
|
1001
|
-
if (option.globalKind === "value") {
|
|
1002
|
-
const next = argv[i + 1];
|
|
1003
|
-
value = value ?? next;
|
|
1004
|
-
if (value === undefined || !option.inline && isOptionToken(value)) {
|
|
1005
|
-
throw new CliArgumentError2(`--${name} requires a value`);
|
|
1006
|
-
}
|
|
1007
|
-
if (!option.inline)
|
|
1008
|
-
i++;
|
|
1009
|
-
appendCliOptionValue(frameworkFlags, name, value, { kind: "string" });
|
|
1010
|
-
if (name === "wait-timeout") {
|
|
1011
|
-
const timeout = Number(value);
|
|
1012
|
-
if (!Number.isFinite(timeout) || timeout <= 0) {
|
|
1013
|
-
throw new CliArgumentError2("--wait-timeout must be a positive number");
|
|
1014
|
-
}
|
|
1015
|
-
options.waitTimeout = timeout;
|
|
1016
|
-
} else if (VIEW_OPTIONS.has(name)) {
|
|
1017
|
-
viewFlags.set(name, value);
|
|
1018
|
-
} else {
|
|
1019
|
-
options.outputDir = value;
|
|
1020
|
-
}
|
|
1021
|
-
continue;
|
|
1022
|
-
}
|
|
1023
|
-
if (value === undefined && name.startsWith("no-") && fieldFor(name.slice(3))?.kind === "boolean") {
|
|
1024
|
-
pushFlag(name.slice(3), "false");
|
|
1025
|
-
literalBooleanFlags.add(name.slice(3));
|
|
1026
|
-
continue;
|
|
1027
|
-
}
|
|
1028
|
-
const unsafeSegment = name.split(".").find(isUnsafeKey);
|
|
1029
|
-
if (unsafeSegment !== undefined) {
|
|
1030
|
-
throw new CliArgumentError2(`Unsafe option name "--${name}"`);
|
|
1031
|
-
}
|
|
1032
|
-
const info = fieldFor(name);
|
|
1033
|
-
const rootName = name.split(".")[0] ?? name;
|
|
1034
|
-
if (!fields.has(rootName) && !config.allowUnknown) {
|
|
1035
|
-
throw new CliArgumentError2(`Unknown option "--${name}"`);
|
|
1036
|
-
}
|
|
1037
|
-
if (value === undefined) {
|
|
1038
|
-
if (info?.kind === "boolean") {
|
|
1039
|
-
const separate = separateBoolValue(argv[i + 1]);
|
|
1040
|
-
if (separate !== undefined)
|
|
1041
|
-
i++;
|
|
1042
|
-
pushFlag(name, String(separate ?? true));
|
|
1043
|
-
literalBooleanFlags.add(name);
|
|
1044
|
-
continue;
|
|
1045
|
-
}
|
|
1046
|
-
const next = argv[i + 1];
|
|
1047
|
-
if (next !== undefined && !isOptionToken(next)) {
|
|
1048
|
-
value = next;
|
|
1049
|
-
i++;
|
|
1050
|
-
} else {
|
|
1051
|
-
throw new CliArgumentError2(`--${name} requires a value`);
|
|
1052
|
-
}
|
|
1053
|
-
}
|
|
1054
|
-
pushFlag(name, value, json);
|
|
1017
|
+
limits.push(`${label}: ${branches.map((branch) => branch.text).join(separator)}`);
|
|
1055
1018
|
}
|
|
1056
|
-
return {
|
|
1057
|
-
options,
|
|
1058
|
-
flags,
|
|
1059
|
-
jsonFields,
|
|
1060
|
-
literalBooleanFlags,
|
|
1061
|
-
viewFlags,
|
|
1062
|
-
ascending,
|
|
1063
|
-
positionals
|
|
1064
|
-
};
|
|
1019
|
+
return limits.length === 0 ? "" : ` [${limits.join(", ")}]`;
|
|
1065
1020
|
}
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1021
|
+
function fieldTypeLabel(schema) {
|
|
1022
|
+
return `<${typeLabel(schema)}>${constraintLabel(schema)}`;
|
|
1023
|
+
}
|
|
1024
|
+
function nullableField(schema) {
|
|
1025
|
+
if (schema.type === "null")
|
|
1026
|
+
return true;
|
|
1027
|
+
if (Array.isArray(schema.type) && schema.type.includes("null"))
|
|
1028
|
+
return true;
|
|
1029
|
+
return Array.isArray(schema.anyOf) && schema.anyOf.some((member) => isRecord(member) && nullableField(member));
|
|
1030
|
+
}
|
|
1031
|
+
function jsonValueHelp(fields) {
|
|
1032
|
+
const example = fields.find((field) => nullableField(field.schema))?.name;
|
|
1033
|
+
return [
|
|
1034
|
+
"Assign a whole JSON value with --field:=JSON; the declared schema still validates it.",
|
|
1035
|
+
...example === undefined ? [] : [`JSON null: --${example}:=null. The literal string stays --${example} null.`]
|
|
1036
|
+
];
|
|
1037
|
+
}
|
|
1038
|
+
function padRight(text, width) {
|
|
1039
|
+
return text.length >= width ? text : text + " ".repeat(width - text.length);
|
|
1040
|
+
}
|
|
1041
|
+
function summarize(desc) {
|
|
1042
|
+
const firstLine = desc.split(`
|
|
1043
|
+
`)[0]?.trim() ?? "";
|
|
1044
|
+
const firstSentence = firstLine.split(". ")[0]?.trim() ?? firstLine;
|
|
1045
|
+
const max = 72;
|
|
1046
|
+
return firstSentence.length > max ? `${firstSentence.slice(0, max - 1).trimEnd()}…` : firstSentence;
|
|
1047
|
+
}
|
|
1048
|
+
function applicationOptionLines(fields) {
|
|
1049
|
+
if (fields.length === 0)
|
|
1050
|
+
return [];
|
|
1051
|
+
const labels = new Map(fields.map((field) => [field.name, `--${field.name} ${fieldTypeLabel(field.schema)}`]));
|
|
1052
|
+
const width = Math.max(...fields.map((field) => labels.get(field.name)?.length ?? 0));
|
|
1053
|
+
return [
|
|
1054
|
+
"",
|
|
1055
|
+
"Application options:",
|
|
1056
|
+
...fields.map((field) => ` ${padRight(labels.get(field.name) ?? `--${field.name}`, width)} ${field.description ?? ""}`.trimEnd())
|
|
1057
|
+
];
|
|
1058
|
+
}
|
|
1059
|
+
function filterCommands(commands, filter) {
|
|
1060
|
+
const needle = filter.toLowerCase();
|
|
1061
|
+
return new Map([...commands].filter(([command, descriptor]) => command.toLowerCase().includes(needle) || descriptor.description.toLowerCase().includes(needle)));
|
|
1062
|
+
}
|
|
1063
|
+
function renderTopHelp(input) {
|
|
1064
|
+
const { name, version, defaultCommand } = input;
|
|
1065
|
+
const commands = input.filter === undefined ? input.commands : filterCommands(input.commands, input.filter);
|
|
1066
|
+
const lines = [
|
|
1067
|
+
`${name} ${version}`,
|
|
1068
|
+
"",
|
|
1069
|
+
`Usage: ${name} ${defaultCommand ? "[command]" : "<command>"} [args] [--flags]`,
|
|
1070
|
+
"",
|
|
1071
|
+
input.filter === undefined ? "Commands:" : `Commands matching "${input.filter}" (${commands.size} of ${input.commands.size}):`
|
|
1072
|
+
];
|
|
1073
|
+
const width = Math.max(0, ...[...commands.keys()].map((key) => key.length));
|
|
1074
|
+
for (const [command, descriptor] of commands) {
|
|
1075
|
+
lines.push(` ${padRight(command, width)} ${summarize(descriptor.description)}${command === defaultCommand ? " (default)" : ""}${descriptor.unavailableReason !== undefined ? " [unavailable]" : ""}`);
|
|
1076
|
+
if (descriptor.unavailableReason !== undefined) {
|
|
1077
|
+
lines.push(` Unavailable: ${descriptor.unavailableReason}`);
|
|
1072
1078
|
}
|
|
1073
|
-
return;
|
|
1074
1079
|
}
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
const table = flags.get("table");
|
|
1078
|
-
const by = flags.get("by");
|
|
1079
|
-
const rawTop = flags.get("top");
|
|
1080
|
-
const named = [
|
|
1081
|
-
["--count-by", countBy],
|
|
1082
|
-
["--sum", sum]
|
|
1083
|
-
].filter(([, value]) => value !== undefined);
|
|
1084
|
-
if (named.length > 1) {
|
|
1085
|
-
throw new CliArgumentError2(`${named.map(([flag]) => flag).join(" and ")} ask for different shapes — pass one`);
|
|
1080
|
+
if (input.unavailable !== undefined) {
|
|
1081
|
+
lines.push("", `Managed commands are unavailable: ${input.unavailable}`);
|
|
1086
1082
|
}
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
}
|
|
1083
|
+
if (input.filter !== undefined) {
|
|
1084
|
+
lines.push("", `Run "${name} <command> --help" for command-specific flags.`);
|
|
1085
|
+
return `${lines.join(`
|
|
1086
|
+
`)}
|
|
1087
|
+
`;
|
|
1093
1088
|
}
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1089
|
+
lines.push("", "Global options:");
|
|
1090
|
+
const optWidth = Math.max(...GLOBAL_OPTIONS.map(([flag]) => flag.length));
|
|
1091
|
+
for (const [flag, desc] of GLOBAL_OPTIONS)
|
|
1092
|
+
lines.push(` ${padRight(flag, optWidth)} ${desc}`);
|
|
1093
|
+
lines.push(...applicationOptionLines(input.applicationOptions));
|
|
1094
|
+
lines.push("", ...jsonValueHelp(input.applicationOptions));
|
|
1095
|
+
lines.push("", `Run "${name} <command> --help" for command-specific flags.`);
|
|
1096
|
+
return `${lines.join(`
|
|
1097
|
+
`)}
|
|
1098
|
+
`;
|
|
1099
|
+
}
|
|
1100
|
+
function renderCommandHelp(name, command, descriptor, applicationOptions = []) {
|
|
1101
|
+
const fields = jsonSchemaFields(descriptor.presentationSchema);
|
|
1102
|
+
const fieldsByName = new Map(fields.map((field) => [field.name, field]));
|
|
1103
|
+
const kinds = describeSchemaFields(descriptor.argumentSchema);
|
|
1104
|
+
const positionals = [];
|
|
1105
|
+
const positionalNames = descriptor.positionals ?? [...kinds].filter(([, info]) => info.kind !== "boolean").map(([fieldName]) => fieldName);
|
|
1106
|
+
for (const fieldName of positionalNames) {
|
|
1107
|
+
const field = fieldsByName.get(fieldName);
|
|
1108
|
+
if (field)
|
|
1109
|
+
positionals.push(field);
|
|
1110
|
+
}
|
|
1111
|
+
const variadicTail = descriptor.positionals !== undefined && kinds.get(descriptor.positionals[descriptor.positionals.length - 1] ?? "")?.kind === "array" ? descriptor.positionals[descriptor.positionals.length - 1] : undefined;
|
|
1112
|
+
const positionalSyntax = new Map(positionals.map((field) => {
|
|
1113
|
+
const label = field.name === variadicTail ? `${field.name}...` : field.name;
|
|
1114
|
+
return [field.name, field.required ? `<${label}>` : `[${label}]`];
|
|
1115
|
+
}));
|
|
1116
|
+
const usage = [
|
|
1117
|
+
`Usage: ${name} ${command}`,
|
|
1118
|
+
...positionals.map((field) => positionalSyntax.get(field.name) ?? field.name),
|
|
1119
|
+
"[--flags]"
|
|
1120
|
+
].join(" ");
|
|
1121
|
+
const lines = [descriptor.description, "", usage, ""];
|
|
1122
|
+
if (descriptor.unavailableReason !== undefined) {
|
|
1123
|
+
lines.push(`Unavailable: ${descriptor.unavailableReason}`, "");
|
|
1124
|
+
}
|
|
1125
|
+
if (fields.length > 0) {
|
|
1126
|
+
lines.push("Arguments:");
|
|
1127
|
+
const labels = new Map(fields.map((field) => {
|
|
1128
|
+
const positional = positionalSyntax.get(field.name);
|
|
1129
|
+
const alias = descriptor.aliases.get(field.name);
|
|
1130
|
+
const option = alias ? `-${alias}, --${field.name}` : `--${field.name}`;
|
|
1131
|
+
return [field.name, positional ? `${positional} | ${option}` : option];
|
|
1132
|
+
}));
|
|
1133
|
+
const width = Math.max(...fields.map((field) => labels.get(field.name)?.length ?? 0));
|
|
1134
|
+
for (const f of fields) {
|
|
1135
|
+
const req = f.required ? " (required)" : "";
|
|
1136
|
+
const desc = f.description ? ` — ${f.description}` : "";
|
|
1137
|
+
const label = labels.get(f.name) ?? `--${f.name}`;
|
|
1138
|
+
lines.push(` ${padRight(label, width)} ${fieldTypeLabel(f.schema)}${req}${desc}`);
|
|
1098
1139
|
}
|
|
1099
|
-
|
|
1100
|
-
|
|
1140
|
+
const flag = [...kinds].find(([, info]) => info.kind === "boolean")?.[0];
|
|
1141
|
+
if (flag !== undefined) {
|
|
1142
|
+
lines.push("", `A boolean flag is on alone or takes true/false: --${flag}, --${flag} false, --no-${flag}`);
|
|
1101
1143
|
}
|
|
1102
|
-
|
|
1103
|
-
|
|
1144
|
+
const arrays = [...kinds].filter(([, info]) => info.kind === "array");
|
|
1145
|
+
for (const [field] of arrays) {
|
|
1146
|
+
lines.push("", `Repeat --${field} <value> for each element, or assign the whole array with --${field}:=JSON.`);
|
|
1104
1147
|
}
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
fields = table.split(",").map((field) => field.trim()).filter(Boolean);
|
|
1108
|
-
if (fields.length === 0)
|
|
1109
|
-
throw new CliArgumentError2("--table needs at least one field");
|
|
1148
|
+
if (arrays.length > 0 && descriptor.positionals === undefined) {
|
|
1149
|
+
lines.push("Array options cannot be combined with automatic positional arguments; name other fields with flags.");
|
|
1110
1150
|
}
|
|
1111
|
-
|
|
1112
|
-
kind: "records",
|
|
1113
|
-
...sort !== undefined && { sort },
|
|
1114
|
-
...ascending && { ascending },
|
|
1115
|
-
...top !== undefined && { top },
|
|
1116
|
-
...fields && { table: fields }
|
|
1117
|
-
};
|
|
1118
|
-
}
|
|
1119
|
-
if (ascending) {
|
|
1120
|
-
throw new CliArgumentError2("--ascending orders a record view; pass --sort with it");
|
|
1151
|
+
lines.push("", ...jsonValueHelp(fields), "");
|
|
1121
1152
|
}
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1153
|
+
const applicationLines = applicationOptionLines(applicationOptions);
|
|
1154
|
+
if (applicationLines.length > 0)
|
|
1155
|
+
lines.push(...applicationLines.slice(1), "");
|
|
1156
|
+
return `${lines.join(`
|
|
1157
|
+
`)}
|
|
1158
|
+
`;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
// src/tools/cli/args-route.ts
|
|
1162
|
+
function isOptionToken(token) {
|
|
1163
|
+
return token.startsWith("--") || /^-[A-Za-z](?:=|$)/.test(token);
|
|
1164
|
+
}
|
|
1165
|
+
function looksLikeOption(token) {
|
|
1166
|
+
return token.startsWith("--") || /^-[A-Za-z]/.test(token);
|
|
1167
|
+
}
|
|
1168
|
+
var BOOL_OPTIONS = new Set(["json", "wait", "quiet", "dry-run", "help", "ascending"]);
|
|
1169
|
+
var VIEW_OPTIONS = new Set(["count-by", "sum", "by", "top", "table", "sort"]);
|
|
1170
|
+
var VALUE_OPTIONS = new Set(["wait-timeout", "output-dir", ...VIEW_OPTIONS]);
|
|
1171
|
+
var RESERVED_CLI_OPTIONS = new Set([...BOOL_OPTIONS, ...VALUE_OPTIONS]);
|
|
1172
|
+
function classifyLongOptionToken(token) {
|
|
1173
|
+
if (!token.startsWith("--") || token === "--")
|
|
1174
|
+
return;
|
|
1175
|
+
const equals = token.indexOf("=");
|
|
1176
|
+
const name = equals >= 0 ? token.slice(2, equals) : token.slice(2);
|
|
1177
|
+
return {
|
|
1178
|
+
name,
|
|
1179
|
+
value: equals >= 0 ? token.slice(equals + 1) : undefined,
|
|
1180
|
+
inline: equals >= 0,
|
|
1181
|
+
globalKind: BOOL_OPTIONS.has(name) ? "boolean" : VALUE_OPTIONS.has(name) ? "value" : undefined
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
1184
|
+
var HELP_TOKENS = new Set(["--help", "-h", "help"]);
|
|
1185
|
+
function helpFilterFrom(token, rest) {
|
|
1186
|
+
if (token?.startsWith("--help=")) {
|
|
1187
|
+
const value = token.slice("--help=".length).trim();
|
|
1188
|
+
if (value.length === 0 || isReservedBoolWord(value))
|
|
1189
|
+
return;
|
|
1190
|
+
return value;
|
|
1191
|
+
}
|
|
1192
|
+
if (token === undefined || !HELP_TOKENS.has(token))
|
|
1193
|
+
return;
|
|
1194
|
+
const next = rest[0];
|
|
1195
|
+
return next !== undefined && !next.startsWith("-") ? next : undefined;
|
|
1196
|
+
}
|
|
1197
|
+
function routeCliArgv2(argv, defaultCommand) {
|
|
1198
|
+
if (defaultCommand === undefined) {
|
|
1199
|
+
const [command, ...commandArgv] = argv;
|
|
1200
|
+
const helpFilter = helpFilterFrom(command, commandArgv);
|
|
1126
1201
|
return {
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1202
|
+
command,
|
|
1203
|
+
commandArgv,
|
|
1204
|
+
topLevelHelp: false,
|
|
1205
|
+
version: false,
|
|
1206
|
+
...helpFilter !== undefined && { helpFilter }
|
|
1131
1207
|
};
|
|
1132
1208
|
}
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1209
|
+
const globals = [];
|
|
1210
|
+
let index = 0;
|
|
1211
|
+
while (index < argv.length) {
|
|
1212
|
+
const token = argv[index];
|
|
1213
|
+
if (token === undefined)
|
|
1214
|
+
break;
|
|
1215
|
+
if (token === "--") {
|
|
1216
|
+
return {
|
|
1217
|
+
commandArgv: [],
|
|
1218
|
+
topLevelHelp: false,
|
|
1219
|
+
version: false,
|
|
1220
|
+
error: 'A command is required before "--"'
|
|
1221
|
+
};
|
|
1142
1222
|
}
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
const arrayFlag = [...flags.keys()].find((key) => fieldFor(key)?.kind === "array");
|
|
1153
|
-
if (arrayFlag !== undefined) {
|
|
1154
|
-
throw new CliArgumentError2(`--${arrayFlag} cannot be combined with automatic positional arguments. Repeat --${arrayFlag} for each element or use --${arrayFlag}:=JSON; name other fields with flags, or declare a positional policy.`);
|
|
1223
|
+
const inlineHelpFilter = token.startsWith("--help=") && !isReservedBoolWord(token.slice("--help=".length));
|
|
1224
|
+
if (token === "--help" || token === "-h" || inlineHelpFilter) {
|
|
1225
|
+
const helpFilter = helpFilterFrom(token, argv.slice(index + 1));
|
|
1226
|
+
return {
|
|
1227
|
+
commandArgv: [],
|
|
1228
|
+
topLevelHelp: true,
|
|
1229
|
+
version: false,
|
|
1230
|
+
...helpFilter !== undefined && { helpFilter }
|
|
1231
|
+
};
|
|
1155
1232
|
}
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
const fillable = declaredPositionals === undefined ? [...fields.entries()].filter(([, info]) => info.kind !== "boolean").map(([name]) => name) : [...declaredPositionals];
|
|
1159
|
-
const variadicTail = declaredPositionals !== undefined && fields.get(fillable[fillable.length - 1] ?? "")?.kind === "array" ? fillable[fillable.length - 1] : undefined;
|
|
1160
|
-
let pi = 0;
|
|
1161
|
-
for (const key of fillable) {
|
|
1162
|
-
if (pi >= positionals.length)
|
|
1163
|
-
break;
|
|
1164
|
-
if (key === variadicTail) {
|
|
1165
|
-
if (flags.has(key)) {
|
|
1166
|
-
throw new CliArgumentError2(`--${key} conflicts with the positional values for "${key}" — pass one form, not both`);
|
|
1167
|
-
}
|
|
1168
|
-
toolArgs[key] = coerceField(fields.get(key), positionals.slice(pi), `<${key}>`);
|
|
1169
|
-
pi = positionals.length;
|
|
1170
|
-
break;
|
|
1233
|
+
if (token === "--version" || token === "version") {
|
|
1234
|
+
return { commandArgv: [], topLevelHelp: false, version: true };
|
|
1171
1235
|
}
|
|
1172
|
-
if (
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1236
|
+
if (!token.startsWith("-") || token === "-") {
|
|
1237
|
+
return {
|
|
1238
|
+
command: token,
|
|
1239
|
+
commandArgv: [...globals, ...argv.slice(index + 1)],
|
|
1240
|
+
topLevelHelp: false,
|
|
1241
|
+
version: false
|
|
1242
|
+
};
|
|
1177
1243
|
}
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
const exactLiteralBoolean = literalBooleanFlags.has(key) && fields.has(key);
|
|
1186
|
-
if (dot > 0 && !exactLiteralBoolean) {
|
|
1187
|
-
dottedRoots.set(key.slice(0, dot), key);
|
|
1244
|
+
if (!token.startsWith("--")) {
|
|
1245
|
+
return {
|
|
1246
|
+
command: defaultCommand,
|
|
1247
|
+
commandArgv: [...globals, ...argv.slice(index)],
|
|
1248
|
+
topLevelHelp: false,
|
|
1249
|
+
version: false
|
|
1250
|
+
};
|
|
1188
1251
|
}
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1252
|
+
const option = classifyLongOptionToken(token);
|
|
1253
|
+
if (!option) {
|
|
1254
|
+
return {
|
|
1255
|
+
command: defaultCommand,
|
|
1256
|
+
commandArgv: [...globals, ...argv.slice(index)],
|
|
1257
|
+
topLevelHelp: false,
|
|
1258
|
+
version: false
|
|
1259
|
+
};
|
|
1193
1260
|
}
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1261
|
+
if (option.globalKind === "boolean") {
|
|
1262
|
+
globals.push(token);
|
|
1263
|
+
index += 1;
|
|
1264
|
+
const next = argv[index];
|
|
1265
|
+
if (option.value === undefined && next !== undefined && separateBoolValue(next) !== undefined) {
|
|
1266
|
+
globals.push(next);
|
|
1267
|
+
index += 1;
|
|
1199
1268
|
}
|
|
1269
|
+
continue;
|
|
1200
1270
|
}
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1271
|
+
if (option.globalKind === "value") {
|
|
1272
|
+
globals.push(token);
|
|
1273
|
+
if (!option.inline) {
|
|
1274
|
+
const value = argv[index + 1];
|
|
1275
|
+
if (value === "--help" || value === "-h") {
|
|
1276
|
+
const helpFilter = helpFilterFrom(value, argv.slice(index + 2));
|
|
1277
|
+
return {
|
|
1278
|
+
commandArgv: [],
|
|
1279
|
+
topLevelHelp: true,
|
|
1280
|
+
version: false,
|
|
1281
|
+
...helpFilter !== undefined && { helpFilter }
|
|
1282
|
+
};
|
|
1283
|
+
}
|
|
1284
|
+
if (value === "--version" || value === "version") {
|
|
1285
|
+
return { commandArgv: [], topLevelHelp: false, version: true };
|
|
1286
|
+
}
|
|
1287
|
+
if (value !== undefined)
|
|
1288
|
+
globals.push(value);
|
|
1289
|
+
index += value === undefined ? 1 : 2;
|
|
1290
|
+
} else {
|
|
1291
|
+
index += 1;
|
|
1292
|
+
}
|
|
1209
1293
|
continue;
|
|
1210
1294
|
}
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
for (const name of config.knownFields ?? []) {
|
|
1218
|
-
if (!fields.has(name))
|
|
1219
|
-
fields.set(name, { kind: "other" });
|
|
1295
|
+
return {
|
|
1296
|
+
command: defaultCommand,
|
|
1297
|
+
commandArgv: [...globals, ...argv.slice(index)],
|
|
1298
|
+
topLevelHelp: false,
|
|
1299
|
+
version: false
|
|
1300
|
+
};
|
|
1220
1301
|
}
|
|
1221
|
-
const fieldFor = (name) => fields.get(name) ?? (name.includes(".") ? describeSchemaPath(schema, name.split(".")) : undefined);
|
|
1222
|
-
const tokens = readCliTokens(argv, fields, fieldFor, config);
|
|
1223
|
-
const { options } = tokens;
|
|
1224
|
-
const view = resolveCliView(tokens.viewFlags, tokens.ascending);
|
|
1225
|
-
if (view)
|
|
1226
|
-
options.view = view;
|
|
1227
1302
|
return {
|
|
1228
|
-
|
|
1229
|
-
|
|
1303
|
+
command: defaultCommand,
|
|
1304
|
+
commandArgv: globals,
|
|
1305
|
+
topLevelHelp: false,
|
|
1306
|
+
version: false
|
|
1230
1307
|
};
|
|
1231
1308
|
}
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1309
|
+
|
|
1310
|
+
// src/tools/cli/command-shape.ts
|
|
1311
|
+
function commandShapeIssue(name, descriptor, exists, passthroughField, applicationGlobals = new Set) {
|
|
1312
|
+
assertUniqueToolName(name, exists, "CLI command");
|
|
1313
|
+
if (name === "help" || name === "version") {
|
|
1314
|
+
throw new Error(`[stitchkit] CLI command "${name}" is reserved`);
|
|
1315
|
+
}
|
|
1316
|
+
if (passthroughField !== undefined && descriptor.aliases.has(passthroughField)) {
|
|
1317
|
+
throw new Error(`[stitchkit] CLI command "${name}" cannot alias passthrough field "${passthroughField}"`);
|
|
1318
|
+
}
|
|
1319
|
+
const fieldNames = jsonSchemaFields(descriptor.presentationSchema).map((field) => field.name);
|
|
1320
|
+
const conflicting = fieldNames.filter((field) => RESERVED_CLI_OPTIONS.has(field));
|
|
1321
|
+
if (conflicting.length > 0) {
|
|
1322
|
+
return `[stitchkit] CLI command "${name}" declares reserved option field(s): ${conflicting.join(", ")}`;
|
|
1323
|
+
}
|
|
1324
|
+
const shadowed = fieldNames.filter((field) => applicationGlobals.has(field));
|
|
1325
|
+
if (shadowed.length > 0) {
|
|
1326
|
+
return `[stitchkit] CLI command "${name}" declares field(s) shadowed by application global options: ${shadowed.join(", ")}`;
|
|
1327
|
+
}
|
|
1236
1328
|
}
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
const
|
|
1241
|
-
|
|
1242
|
-
const
|
|
1329
|
+
|
|
1330
|
+
// src/tools/cli/managed-surface.ts
|
|
1331
|
+
async function buildManagedSurface(session, typedGlobals, forExecution) {
|
|
1332
|
+
const { config, nativeHelp, applicationGlobalNames } = session;
|
|
1333
|
+
let authPromise;
|
|
1334
|
+
const resolveIdentity = () => {
|
|
1335
|
+
authPromise ??= (async () => config.resolveAuth ? await config.resolveAuth(typedGlobals) : await config.auth)();
|
|
1336
|
+
return authPromise;
|
|
1337
|
+
};
|
|
1338
|
+
const dynamicSurface = typeof config.services === "function" || typeof config.runtimeTools === "function";
|
|
1339
|
+
let auth;
|
|
1340
|
+
if (dynamicSurface || forExecution) {
|
|
1341
|
+
try {
|
|
1342
|
+
auth = await resolveIdentity();
|
|
1343
|
+
} catch (error) {
|
|
1344
|
+
const failure = toolResultFromError(error);
|
|
1345
|
+
const normalized = toolErrorFromResult(failure);
|
|
1346
|
+
return {
|
|
1347
|
+
resolved: false,
|
|
1348
|
+
failure,
|
|
1349
|
+
reason: `${normalized.code}: ${normalized.message}`,
|
|
1350
|
+
help: new Map(nativeHelp)
|
|
1351
|
+
};
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
const { tools, help } = buildCliSurface(config, auth, nativeHelp, applicationGlobalNames, commandShapeIssue);
|
|
1355
|
+
return { resolved: true, auth, help, tools };
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
// src/tools/cli/args.ts
|
|
1359
|
+
import { z as z2 } from "zod";
|
|
1360
|
+
|
|
1361
|
+
// src/tools/cli/args-json.ts
|
|
1362
|
+
function cliFieldOption(option, fields) {
|
|
1363
|
+
const json = option.inline && option.name.endsWith(":") && !fields.has(option.name);
|
|
1364
|
+
const name = json ? option.name.slice(0, -1) : option.name;
|
|
1365
|
+
return { name, json };
|
|
1366
|
+
}
|
|
1367
|
+
function appendCliFieldValue(raw, jsonFields, name, value, info, json) {
|
|
1368
|
+
appendCliOptionValue(raw, name, value, json || jsonFields.has(name) ? { kind: "other" } : info);
|
|
1369
|
+
if (json)
|
|
1370
|
+
jsonFields.add(name);
|
|
1371
|
+
}
|
|
1372
|
+
function coerceCliFieldValue(info, values, label, json) {
|
|
1373
|
+
if (!json)
|
|
1374
|
+
return coerceField(info, values, label);
|
|
1375
|
+
try {
|
|
1376
|
+
return safeJsonParse(values[0] ?? "");
|
|
1377
|
+
} catch (cause) {
|
|
1378
|
+
throw new CliArgumentError2(`${label} expects valid JSON`, { cause });
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
// src/tools/cli/args-tokens.ts
|
|
1383
|
+
function readCliTokens(argv, fields, fieldFor, config) {
|
|
1384
|
+
const options = {
|
|
1385
|
+
json: false,
|
|
1386
|
+
wait: false,
|
|
1387
|
+
quiet: false,
|
|
1388
|
+
dryRun: false,
|
|
1389
|
+
help: false
|
|
1390
|
+
};
|
|
1391
|
+
const flags = new Map;
|
|
1243
1392
|
const jsonFields = new Set;
|
|
1244
|
-
|
|
1393
|
+
const literalBooleanFlags = new Set;
|
|
1394
|
+
const frameworkFlags = new Map;
|
|
1395
|
+
const viewFlags = new Map;
|
|
1396
|
+
let ascending = false;
|
|
1397
|
+
const positionals = [];
|
|
1398
|
+
const pushFlag = (name, value, json = false) => {
|
|
1399
|
+
appendCliFieldValue(flags, jsonFields, name, value, fieldFor(name), json);
|
|
1400
|
+
};
|
|
1401
|
+
let optionsEnded = false;
|
|
1245
1402
|
for (let i = 0;i < argv.length; i++) {
|
|
1246
|
-
const
|
|
1247
|
-
if (
|
|
1403
|
+
const tok = argv[i];
|
|
1404
|
+
if (tok === undefined)
|
|
1248
1405
|
continue;
|
|
1249
|
-
if (!
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1406
|
+
if (!optionsEnded && tok === "--") {
|
|
1407
|
+
optionsEnded = true;
|
|
1408
|
+
continue;
|
|
1409
|
+
}
|
|
1410
|
+
if (optionsEnded || !looksLikeOption(tok)) {
|
|
1411
|
+
positionals.push(tok);
|
|
1412
|
+
continue;
|
|
1413
|
+
}
|
|
1414
|
+
if (tok === "-h") {
|
|
1415
|
+
options.help = true;
|
|
1416
|
+
continue;
|
|
1417
|
+
}
|
|
1418
|
+
if (!tok.startsWith("--")) {
|
|
1419
|
+
const match = /^-([A-Za-z])(?:=(.*))?$/.exec(tok);
|
|
1420
|
+
const alias = match?.[1];
|
|
1421
|
+
const field = alias === undefined ? undefined : config.optionAliases?.get(alias);
|
|
1422
|
+
if (!alias || !field)
|
|
1423
|
+
throw new CliArgumentError2(`Unknown option "${tok}"`);
|
|
1424
|
+
const inline = match?.[2];
|
|
1425
|
+
const info = fieldFor(field);
|
|
1426
|
+
if (info?.kind === "boolean") {
|
|
1427
|
+
const separate = inline === undefined ? separateBoolValue(argv[i + 1]) : undefined;
|
|
1428
|
+
if (separate !== undefined)
|
|
1429
|
+
i++;
|
|
1430
|
+
pushFlag(field, inline ?? String(separate ?? true));
|
|
1431
|
+
if (inline !== undefined)
|
|
1432
|
+
parseReservedBool(field, inline);
|
|
1433
|
+
literalBooleanFlags.add(field);
|
|
1434
|
+
continue;
|
|
1435
|
+
}
|
|
1436
|
+
let value = inline;
|
|
1437
|
+
if (value === undefined) {
|
|
1438
|
+
const next = argv[i + 1];
|
|
1439
|
+
if (next !== undefined && !isOptionToken(next)) {
|
|
1440
|
+
value = next;
|
|
1441
|
+
i++;
|
|
1442
|
+
} else {
|
|
1443
|
+
throw new CliArgumentError2(`--${field} requires a value`);
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
pushFlag(field, value);
|
|
1256
1447
|
continue;
|
|
1257
1448
|
}
|
|
1449
|
+
const option = classifyLongOptionToken(tok);
|
|
1450
|
+
if (!option)
|
|
1451
|
+
throw new CliArgumentError2(`Unknown option "${tok}"`);
|
|
1452
|
+
const { name, json } = cliFieldOption(option, fields);
|
|
1258
1453
|
let { value } = option;
|
|
1259
|
-
if (
|
|
1260
|
-
|
|
1454
|
+
if (name.length === 0)
|
|
1455
|
+
throw new CliArgumentError2("Invalid empty option name");
|
|
1456
|
+
if (json && RESERVED_CLI_OPTIONS.has(name)) {
|
|
1457
|
+
throw new CliArgumentError2(`--${name} is a framework option and does not accept :=JSON`);
|
|
1458
|
+
}
|
|
1459
|
+
if (option.globalKind === "boolean") {
|
|
1460
|
+
const separate = value === undefined ? separateBoolValue(argv[i + 1]) : undefined;
|
|
1261
1461
|
if (separate !== undefined)
|
|
1262
1462
|
i++;
|
|
1263
|
-
|
|
1264
|
-
|
|
1463
|
+
if (name !== "help") {
|
|
1464
|
+
appendCliOptionValue(frameworkFlags, name, value ?? String(separate ?? true), {
|
|
1465
|
+
kind: "boolean"
|
|
1466
|
+
});
|
|
1467
|
+
}
|
|
1468
|
+
const enabled = value === undefined ? separate ?? true : parseReservedBool(name, value);
|
|
1469
|
+
if (name === "dry-run")
|
|
1470
|
+
options.dryRun = enabled;
|
|
1471
|
+
else if (name === "json")
|
|
1472
|
+
options.json = enabled;
|
|
1473
|
+
else if (name === "wait")
|
|
1474
|
+
options.wait = enabled;
|
|
1475
|
+
else if (name === "quiet")
|
|
1476
|
+
options.quiet = enabled;
|
|
1477
|
+
else if (name === "ascending")
|
|
1478
|
+
ascending = enabled;
|
|
1479
|
+
else
|
|
1480
|
+
options.help = options.help || enabled;
|
|
1481
|
+
continue;
|
|
1482
|
+
}
|
|
1483
|
+
if (option.globalKind === "value") {
|
|
1484
|
+
const next = argv[i + 1];
|
|
1485
|
+
value = value ?? next;
|
|
1486
|
+
if (value === undefined || !option.inline && isOptionToken(value)) {
|
|
1487
|
+
throw new CliArgumentError2(`--${name} requires a value`);
|
|
1488
|
+
}
|
|
1489
|
+
if (!option.inline)
|
|
1490
|
+
i++;
|
|
1491
|
+
appendCliOptionValue(frameworkFlags, name, value, { kind: "string" });
|
|
1492
|
+
if (name === "wait-timeout") {
|
|
1493
|
+
const timeout = Number(value);
|
|
1494
|
+
if (!Number.isFinite(timeout) || timeout <= 0) {
|
|
1495
|
+
throw new CliArgumentError2("--wait-timeout must be a positive number");
|
|
1496
|
+
}
|
|
1497
|
+
options.waitTimeout = timeout;
|
|
1498
|
+
} else if (VIEW_OPTIONS.has(name)) {
|
|
1499
|
+
viewFlags.set(name, value);
|
|
1500
|
+
} else {
|
|
1501
|
+
options.outputDir = value;
|
|
1502
|
+
}
|
|
1503
|
+
continue;
|
|
1504
|
+
}
|
|
1505
|
+
if (value === undefined && name.startsWith("no-") && fieldFor(name.slice(3))?.kind === "boolean") {
|
|
1506
|
+
pushFlag(name.slice(3), "false");
|
|
1507
|
+
literalBooleanFlags.add(name.slice(3));
|
|
1508
|
+
continue;
|
|
1509
|
+
}
|
|
1510
|
+
const unsafeSegment = name.split(".").find(isUnsafeKey);
|
|
1511
|
+
if (unsafeSegment !== undefined) {
|
|
1512
|
+
throw new CliArgumentError2(`Unsafe option name "--${name}"`);
|
|
1513
|
+
}
|
|
1514
|
+
const info = fieldFor(name);
|
|
1515
|
+
const rootName = name.split(".")[0] ?? name;
|
|
1516
|
+
if (!fields.has(rootName) && !config.allowUnknown) {
|
|
1517
|
+
throw new CliArgumentError2(`Unknown option "--${name}"`);
|
|
1518
|
+
}
|
|
1519
|
+
if (value === undefined) {
|
|
1520
|
+
if (info?.kind === "boolean") {
|
|
1521
|
+
const separate = separateBoolValue(argv[i + 1]);
|
|
1522
|
+
if (separate !== undefined)
|
|
1523
|
+
i++;
|
|
1524
|
+
pushFlag(name, String(separate ?? true));
|
|
1525
|
+
literalBooleanFlags.add(name);
|
|
1526
|
+
continue;
|
|
1527
|
+
}
|
|
1265
1528
|
const next = argv[i + 1];
|
|
1266
1529
|
if (next !== undefined && !isOptionToken(next)) {
|
|
1267
1530
|
value = next;
|
|
1268
1531
|
i++;
|
|
1269
1532
|
} else {
|
|
1270
|
-
throw new CliArgumentError2(`--${
|
|
1533
|
+
throw new CliArgumentError2(`--${name} requires a value`);
|
|
1271
1534
|
}
|
|
1272
1535
|
}
|
|
1273
|
-
|
|
1274
|
-
}
|
|
1275
|
-
const args = {};
|
|
1276
|
-
for (const [name, values] of raw) {
|
|
1277
|
-
args[name] = coerceCliFieldValue(fields.get(name), values, `--${name}`, jsonFields.has(name));
|
|
1278
|
-
}
|
|
1279
|
-
let coerced;
|
|
1280
|
-
try {
|
|
1281
|
-
coerced = coerceJsonArgs2(args, schema);
|
|
1282
|
-
} catch (error) {
|
|
1283
|
-
if (error instanceof z2.ZodError)
|
|
1284
|
-
refuseGlobalOption(error.issues[0]);
|
|
1285
|
-
throw error;
|
|
1536
|
+
pushFlag(name, value, json);
|
|
1286
1537
|
}
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1538
|
+
return {
|
|
1539
|
+
options,
|
|
1540
|
+
flags,
|
|
1541
|
+
jsonFields,
|
|
1542
|
+
literalBooleanFlags,
|
|
1543
|
+
viewFlags,
|
|
1544
|
+
ascending,
|
|
1545
|
+
positionals
|
|
1546
|
+
};
|
|
1291
1547
|
}
|
|
1292
1548
|
|
|
1293
|
-
// src/tools/cli/view.ts
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
if (isRecord(data)) {
|
|
1299
|
-
const arrays = Object.entries(data).filter(([, value]) => Array.isArray(value));
|
|
1300
|
-
const only = arrays[0];
|
|
1301
|
-
if (arrays.length === 1 && only && Array.isArray(only[1]))
|
|
1302
|
-
return only[1];
|
|
1303
|
-
if (arrays.length > 1) {
|
|
1304
|
-
throw new CliArgumentError2(`an aggregate needs one collection, but the result carries several: ${arrays.map(([key]) => key).join(", ")} — name the shape you want with jq instead`);
|
|
1549
|
+
// src/tools/cli/args-view.ts
|
|
1550
|
+
function resolveCliView(flags, ascending) {
|
|
1551
|
+
if (flags.size === 0) {
|
|
1552
|
+
if (ascending) {
|
|
1553
|
+
throw new CliArgumentError2("--ascending orders a record view; pass --sort with it");
|
|
1305
1554
|
}
|
|
1555
|
+
return;
|
|
1306
1556
|
}
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
}
|
|
1319
|
-
return current;
|
|
1320
|
-
}
|
|
1321
|
-
function fieldPath(field) {
|
|
1322
|
-
const path = field.split(".");
|
|
1323
|
-
if (path.some((segment) => segment.length === 0 || isUnsafeKey(segment))) {
|
|
1324
|
-
throw new CliArgumentError2(`"${field}" is not a readable field path`);
|
|
1325
|
-
}
|
|
1326
|
-
return path;
|
|
1327
|
-
}
|
|
1328
|
-
function availableFields(records) {
|
|
1329
|
-
const keys = new Set;
|
|
1330
|
-
for (const record of records) {
|
|
1331
|
-
if (isRecord(record))
|
|
1332
|
-
for (const key of Object.keys(record))
|
|
1333
|
-
keys.add(key);
|
|
1334
|
-
}
|
|
1335
|
-
return [...keys].sort();
|
|
1336
|
-
}
|
|
1337
|
-
function assertFieldPresent(records, field) {
|
|
1338
|
-
const path = fieldPath(field);
|
|
1339
|
-
const present = records.some((record) => readField(record, path) !== undefined);
|
|
1340
|
-
if (records.length > 0 && !present) {
|
|
1341
|
-
const available = availableFields(records);
|
|
1342
|
-
throw new CliArgumentError2(`no record carries the field "${field}"${available.length > 0 ? ` — available: ${available.join(", ")}` : ""}`);
|
|
1343
|
-
}
|
|
1344
|
-
return path;
|
|
1345
|
-
}
|
|
1346
|
-
function groupKey(value) {
|
|
1347
|
-
if (value === undefined)
|
|
1348
|
-
return ABSENT;
|
|
1349
|
-
if (value === null)
|
|
1350
|
-
return "null";
|
|
1351
|
-
if (value === "null" || value === ABSENT)
|
|
1352
|
-
return JSON.stringify(value);
|
|
1353
|
-
if (typeof value === "object")
|
|
1354
|
-
return JSON.stringify(value);
|
|
1355
|
-
return String(value);
|
|
1356
|
-
}
|
|
1357
|
-
function ordered(totals, top) {
|
|
1358
|
-
const entries = [...totals].sort(([leftKey, left], [rightKey, right]) => right === left ? leftKey.localeCompare(rightKey) : right - left);
|
|
1359
|
-
const sliced = top === undefined ? entries : entries.slice(0, top);
|
|
1360
|
-
return {
|
|
1361
|
-
kind: "json",
|
|
1362
|
-
data: Object.fromEntries(sliced),
|
|
1363
|
-
order: sliced.map(([key]) => key)
|
|
1364
|
-
};
|
|
1365
|
-
}
|
|
1366
|
-
function stringifyGroups(data, order, compact) {
|
|
1367
|
-
if (!isRecord(data) || order.length === 0)
|
|
1368
|
-
return JSON.stringify(data);
|
|
1369
|
-
const lines = order.map((key) => `${JSON.stringify(key)}:${compact ? "" : " "}${JSON.stringify(data[key])}`);
|
|
1370
|
-
return compact ? `{${lines.join(",")}}` : `{
|
|
1371
|
-
${lines.join(`,
|
|
1372
|
-
`)}
|
|
1373
|
-
}`;
|
|
1374
|
-
}
|
|
1375
|
-
function numericValue(value, field) {
|
|
1376
|
-
if (isAbsent(value))
|
|
1377
|
-
return;
|
|
1378
|
-
if (typeof value === "number" && Number.isFinite(value))
|
|
1379
|
-
return value;
|
|
1380
|
-
if (typeof value === "bigint")
|
|
1381
|
-
return Number(value);
|
|
1382
|
-
throw new CliArgumentError2(`--sum ${field}: "${groupKey(value)}" is not a number`);
|
|
1383
|
-
}
|
|
1384
|
-
function renderTable(records, columns) {
|
|
1385
|
-
const fields = columns.map(({ field }) => field);
|
|
1386
|
-
const cell = (value) => {
|
|
1387
|
-
if (value === undefined)
|
|
1388
|
-
return "";
|
|
1389
|
-
if (value === null)
|
|
1390
|
-
return "null";
|
|
1391
|
-
if (typeof value === "object")
|
|
1392
|
-
return JSON.stringify(value);
|
|
1393
|
-
return String(value);
|
|
1394
|
-
};
|
|
1395
|
-
const rows = records.map((record) => columns.map(({ path }) => cell(readField(record, path))));
|
|
1396
|
-
const widths = fields.map((field, column) => Math.max(field.length, ...rows.map((row) => row[column]?.length ?? 0)));
|
|
1397
|
-
const line = (cells) => cells.map((value, column) => value.padEnd(widths[column] ?? value.length)).join(" ").trimEnd();
|
|
1398
|
-
return `${[line(fields), line(widths.map((width) => "-".repeat(width))), ...rows.map(line)].join(`
|
|
1399
|
-
`)}
|
|
1400
|
-
`;
|
|
1401
|
-
}
|
|
1402
|
-
function compareFieldValues(left, right) {
|
|
1403
|
-
if (isAbsent(left))
|
|
1404
|
-
return isAbsent(right) ? 0 : 1;
|
|
1405
|
-
if (isAbsent(right))
|
|
1406
|
-
return -1;
|
|
1407
|
-
if (typeof left === "number" && typeof right === "number")
|
|
1408
|
-
return left - right;
|
|
1409
|
-
if (typeof left === "bigint" || typeof right === "bigint") {
|
|
1410
|
-
return Number(left) - Number(right);
|
|
1411
|
-
}
|
|
1412
|
-
return groupKey(left).localeCompare(groupKey(right));
|
|
1413
|
-
}
|
|
1414
|
-
function renderRecords(records, view) {
|
|
1415
|
-
let selected = [...records];
|
|
1416
|
-
if (view.sort !== undefined) {
|
|
1417
|
-
const path = assertFieldPresent(records, view.sort);
|
|
1418
|
-
const direction = view.ascending ? 1 : -1;
|
|
1419
|
-
selected.sort((left, right) => {
|
|
1420
|
-
const leftValue = readField(left, path);
|
|
1421
|
-
const rightValue = readField(right, path);
|
|
1422
|
-
const order = compareFieldValues(leftValue, rightValue);
|
|
1423
|
-
return isAbsent(leftValue) || isAbsent(rightValue) ? order : order * direction;
|
|
1424
|
-
});
|
|
1557
|
+
const countBy = flags.get("count-by");
|
|
1558
|
+
const sum = flags.get("sum");
|
|
1559
|
+
const table = flags.get("table");
|
|
1560
|
+
const by = flags.get("by");
|
|
1561
|
+
const rawTop = flags.get("top");
|
|
1562
|
+
const named = [
|
|
1563
|
+
["--count-by", countBy],
|
|
1564
|
+
["--sum", sum]
|
|
1565
|
+
].filter(([, value]) => value !== undefined);
|
|
1566
|
+
if (named.length > 1) {
|
|
1567
|
+
throw new CliArgumentError2(`${named.map(([flag]) => flag).join(" and ")} ask for different shapes — pass one`);
|
|
1425
1568
|
}
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
selected = selected.slice(0, view.top);
|
|
1432
|
-
return columns ? { kind: "text", text: renderTable(selected, columns) } : { kind: "json", data: selected };
|
|
1433
|
-
}
|
|
1434
|
-
function refuseEnvelopeFields(data, view) {
|
|
1435
|
-
if (!isRecord(data))
|
|
1436
|
-
return;
|
|
1437
|
-
const fields = view.kind === "records" ? [...view.sort === undefined ? [] : [view.sort], ...view.table ?? []] : [view.field, ...view.kind === "sum" && view.by !== undefined ? [view.by] : []];
|
|
1438
|
-
for (const field of fields) {
|
|
1439
|
-
const first = field.split(".")[0] ?? "";
|
|
1440
|
-
if (Object.hasOwn(data, first) && !Array.isArray(data[first])) {
|
|
1441
|
-
throw new CliArgumentError2(`"${field}" belongs to the result object, not to its empty collection`);
|
|
1569
|
+
let top;
|
|
1570
|
+
if (rawTop !== undefined) {
|
|
1571
|
+
top = Number(rawTop);
|
|
1572
|
+
if (!Number.isInteger(top) || top <= 0) {
|
|
1573
|
+
throw new CliArgumentError2("--top must be a positive whole number");
|
|
1442
1574
|
}
|
|
1443
1575
|
}
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
refuseEnvelopeFields(data, view);
|
|
1449
|
-
if (view.kind === "records")
|
|
1450
|
-
return renderRecords(records, view);
|
|
1451
|
-
if (view.kind === "count") {
|
|
1452
|
-
const path = assertFieldPresent(records, view.field);
|
|
1453
|
-
const totals = new Map;
|
|
1454
|
-
for (const record of records) {
|
|
1455
|
-
const key = groupKey(readField(record, path));
|
|
1456
|
-
totals.set(key, (totals.get(key) ?? 0) + 1);
|
|
1576
|
+
const sort = flags.get("sort");
|
|
1577
|
+
if (table !== undefined || sort !== undefined) {
|
|
1578
|
+
if (countBy !== undefined || sum !== undefined) {
|
|
1579
|
+
throw new CliArgumentError2(`${table !== undefined ? "--table" : "--sort"} lists records; --count-by and --sum aggregate them — pass one`);
|
|
1457
1580
|
}
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
const path = assertFieldPresent(records, view.field);
|
|
1461
|
-
if (view.by === undefined) {
|
|
1462
|
-
let total = 0;
|
|
1463
|
-
for (const record of records)
|
|
1464
|
-
total += numericValue(readField(record, path), view.field) ?? 0;
|
|
1465
|
-
return { kind: "json", data: total };
|
|
1466
|
-
}
|
|
1467
|
-
const byPath = assertFieldPresent(records, view.by);
|
|
1468
|
-
const totals = new Map;
|
|
1469
|
-
for (const record of records) {
|
|
1470
|
-
const key = groupKey(readField(record, byPath));
|
|
1471
|
-
totals.set(key, (totals.get(key) ?? 0) + (numericValue(readField(record, path), view.field) ?? 0));
|
|
1472
|
-
}
|
|
1473
|
-
return ordered(totals, view.top);
|
|
1474
|
-
}
|
|
1475
|
-
|
|
1476
|
-
// src/tools/operations/wait-core.ts
|
|
1477
|
-
var DEFAULT_BACKOFF = [2, 3, 5, 5, 8, 10];
|
|
1478
|
-
var DEFAULT_TIMEOUT = 600;
|
|
1479
|
-
|
|
1480
|
-
class WaitTimeoutError extends Error {
|
|
1481
|
-
constructor() {
|
|
1482
|
-
super("wait deadline elapsed before the first snapshot");
|
|
1483
|
-
this.name = "WaitTimeoutError";
|
|
1484
|
-
}
|
|
1485
|
-
}
|
|
1486
|
-
async function pollUntil(params) {
|
|
1487
|
-
const backoff = params.backoff?.length ? params.backoff : DEFAULT_BACKOFF;
|
|
1488
|
-
const lastBackoff = backoff[backoff.length - 1] ?? 5;
|
|
1489
|
-
const timeoutSec = params.timeoutSec ?? DEFAULT_TIMEOUT;
|
|
1490
|
-
const pause = params.sleepFn ?? sleep;
|
|
1491
|
-
const now = params.nowFn ?? (() => performance.now());
|
|
1492
|
-
const startedAt = now();
|
|
1493
|
-
const deadline = startedAt + timeoutSec * 1000;
|
|
1494
|
-
let last;
|
|
1495
|
-
for (let attempt = 0;; attempt++) {
|
|
1496
|
-
params.signal?.throwIfAborted();
|
|
1497
|
-
const remainingBeforePoll = Math.max(0, deadline - now());
|
|
1498
|
-
if (remainingBeforePoll === 0 && last) {
|
|
1499
|
-
return { state: last.state, timedOut: true };
|
|
1581
|
+
if (by !== undefined) {
|
|
1582
|
+
throw new CliArgumentError2("--by groups a view; a record view orders with --sort");
|
|
1500
1583
|
}
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
params.signal?.addEventListener("abort", onCallerAbort, { once: true });
|
|
1504
|
-
const deadlineTimer = setTimeout(() => deadlineController.abort(new WaitTimeoutError), remainingBeforePoll);
|
|
1505
|
-
deadlineTimer.unref?.();
|
|
1506
|
-
let state;
|
|
1507
|
-
try {
|
|
1508
|
-
state = await params.poll(deadlineController.signal);
|
|
1509
|
-
} catch (error) {
|
|
1510
|
-
if (params.signal?.aborted)
|
|
1511
|
-
throw params.signal.reason ?? error;
|
|
1512
|
-
if (deadlineController.signal.aborted && now() >= deadline) {
|
|
1513
|
-
if (last)
|
|
1514
|
-
return { state: last.state, timedOut: true };
|
|
1515
|
-
throw new WaitTimeoutError;
|
|
1516
|
-
}
|
|
1517
|
-
throw error;
|
|
1518
|
-
} finally {
|
|
1519
|
-
clearTimeout(deadlineTimer);
|
|
1520
|
-
params.signal?.removeEventListener("abort", onCallerAbort);
|
|
1584
|
+
if (top !== undefined && sort === undefined) {
|
|
1585
|
+
throw new CliArgumentError2("--top needs --sort here: unordered records have no n largest");
|
|
1521
1586
|
}
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
const elapsedSec = elapsedMs / 1000;
|
|
1528
|
-
params.onTick?.(attempt + 1, elapsedSec);
|
|
1529
|
-
if (elapsedSec >= timeoutSec)
|
|
1530
|
-
return { state, timedOut: true };
|
|
1531
|
-
const waitSec = backoff[Math.min(attempt, backoff.length - 1)] ?? lastBackoff;
|
|
1532
|
-
await pause(Math.min(waitSec * 1000, Math.max(0, deadline - now())), params.signal);
|
|
1533
|
-
}
|
|
1534
|
-
}
|
|
1535
|
-
function runWaitOperation(params) {
|
|
1536
|
-
return pollUntil({
|
|
1537
|
-
poll: (signal) => params.poll(params.input, signal),
|
|
1538
|
-
done: params.done,
|
|
1539
|
-
backoff: params.backoff,
|
|
1540
|
-
timeoutSec: params.timeoutSec,
|
|
1541
|
-
sleepFn: params.sleepFn,
|
|
1542
|
-
onTick: params.onTick,
|
|
1543
|
-
signal: params.signal,
|
|
1544
|
-
nowFn: params.nowFn
|
|
1545
|
-
});
|
|
1546
|
-
}
|
|
1547
|
-
|
|
1548
|
-
// src/tools/cli/wait.ts
|
|
1549
|
-
var DEFAULT_TIMEOUT2 = 600;
|
|
1550
|
-
function waitFailure(tool, result) {
|
|
1551
|
-
return {
|
|
1552
|
-
ok: false,
|
|
1553
|
-
code: "WAIT_FAILED",
|
|
1554
|
-
details: {
|
|
1555
|
-
message: `"${tool}" reached a terminal failed state`,
|
|
1556
|
-
result
|
|
1557
|
-
}
|
|
1558
|
-
};
|
|
1559
|
-
}
|
|
1560
|
-
async function pollUntilDone2(params) {
|
|
1561
|
-
const { initial, wait, call } = params;
|
|
1562
|
-
if (!initial.ok)
|
|
1563
|
-
return initial;
|
|
1564
|
-
if (wait.failed?.(initial.data))
|
|
1565
|
-
return waitFailure(wait.tool, initial.data);
|
|
1566
|
-
if (wait.done(initial.data))
|
|
1567
|
-
return initial;
|
|
1568
|
-
const pollArgs = wait.poll(initial.data);
|
|
1569
|
-
if (!pollArgs)
|
|
1570
|
-
return initial;
|
|
1571
|
-
const timeoutSec = params.timeoutSec ?? wait.timeout ?? DEFAULT_TIMEOUT2;
|
|
1572
|
-
let failedState;
|
|
1573
|
-
const { state, timedOut } = await pollUntil({
|
|
1574
|
-
poll: () => call(wait.tool, pollArgs),
|
|
1575
|
-
done: (result) => {
|
|
1576
|
-
if (!result.ok)
|
|
1577
|
-
return true;
|
|
1578
|
-
if (wait.failed?.(result.data)) {
|
|
1579
|
-
failedState = result;
|
|
1580
|
-
return true;
|
|
1581
|
-
}
|
|
1582
|
-
return wait.done(result.data);
|
|
1583
|
-
},
|
|
1584
|
-
backoff: wait.backoff,
|
|
1585
|
-
timeoutSec,
|
|
1586
|
-
sleepFn: params.sleepFn,
|
|
1587
|
-
onTick: params.onTick,
|
|
1588
|
-
signal: params.signal
|
|
1589
|
-
});
|
|
1590
|
-
if (timedOut) {
|
|
1591
|
-
return {
|
|
1592
|
-
ok: false,
|
|
1593
|
-
code: "TIMEOUT",
|
|
1594
|
-
details: { message: `Timed out after ${timeoutSec}s waiting for "${wait.tool}"` }
|
|
1595
|
-
};
|
|
1596
|
-
}
|
|
1597
|
-
if (state === failedState && state.ok)
|
|
1598
|
-
return waitFailure(wait.tool, state.data);
|
|
1599
|
-
return state;
|
|
1600
|
-
}
|
|
1601
|
-
|
|
1602
|
-
// src/tools/cli/io.ts
|
|
1603
|
-
import { writeSync } from "node:fs";
|
|
1604
|
-
|
|
1605
|
-
// src/tools/cli/stdin.ts
|
|
1606
|
-
function readPipedStdin(input = process.stdin) {
|
|
1607
|
-
if (input.isTTY || input.readableEnded)
|
|
1608
|
-
return Promise.resolve(null);
|
|
1609
|
-
if (input.destroyed)
|
|
1610
|
-
return Promise.reject(new Error("stdin closed before EOF"));
|
|
1611
|
-
return new Promise((resolve, reject) => {
|
|
1612
|
-
const chunks = [];
|
|
1613
|
-
const cleanup = () => {
|
|
1614
|
-
clearTimeout(timer);
|
|
1615
|
-
input.pause();
|
|
1616
|
-
input.off("data", onData);
|
|
1617
|
-
input.off("end", onEnd);
|
|
1618
|
-
input.off("error", onError);
|
|
1619
|
-
input.off("close", onClose);
|
|
1620
|
-
};
|
|
1621
|
-
const onData = (chunk) => {
|
|
1622
|
-
if (chunk.length === 0)
|
|
1623
|
-
return;
|
|
1624
|
-
clearTimeout(timer);
|
|
1625
|
-
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
1626
|
-
};
|
|
1627
|
-
const onEnd = () => {
|
|
1628
|
-
cleanup();
|
|
1629
|
-
const text = Buffer.concat(chunks).toString("utf8").trim();
|
|
1630
|
-
resolve(text.length > 0 ? text : null);
|
|
1631
|
-
};
|
|
1632
|
-
const onError = (error) => {
|
|
1633
|
-
cleanup();
|
|
1634
|
-
reject(error);
|
|
1635
|
-
};
|
|
1636
|
-
const onClose = () => onError(new Error("stdin closed before EOF"));
|
|
1637
|
-
const timer = setTimeout(() => {
|
|
1638
|
-
cleanup();
|
|
1639
|
-
resolve(null);
|
|
1640
|
-
}, 250);
|
|
1641
|
-
input.on("end", onEnd);
|
|
1642
|
-
input.on("error", onError);
|
|
1643
|
-
input.on("close", onClose);
|
|
1644
|
-
input.on("data", onData);
|
|
1645
|
-
input.resume();
|
|
1646
|
-
});
|
|
1647
|
-
}
|
|
1648
|
-
|
|
1649
|
-
// src/tools/cli/io.ts
|
|
1650
|
-
function createCliIo(config) {
|
|
1651
|
-
let pendingWrite;
|
|
1652
|
-
let writeFailed = false;
|
|
1653
|
-
const writeFd = (fd, text) => {
|
|
1654
|
-
const bytes = Buffer.from(text, "utf8");
|
|
1655
|
-
let offset = 0;
|
|
1656
|
-
while (offset < bytes.length) {
|
|
1657
|
-
try {
|
|
1658
|
-
offset += writeSync(fd, bytes, offset, bytes.length - offset);
|
|
1659
|
-
} catch (error) {
|
|
1660
|
-
if (isRecord(error) && error.code === "EAGAIN") {
|
|
1661
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 1);
|
|
1662
|
-
continue;
|
|
1663
|
-
}
|
|
1664
|
-
const stream = fd === 1 ? process.stdout : process.stderr;
|
|
1665
|
-
const rest = bytes.subarray(offset);
|
|
1666
|
-
pendingWrite = new Promise((resolve) => {
|
|
1667
|
-
stream.write(rest, (failure) => {
|
|
1668
|
-
if (failure)
|
|
1669
|
-
writeFailed = true;
|
|
1670
|
-
resolve();
|
|
1671
|
-
});
|
|
1672
|
-
});
|
|
1673
|
-
return;
|
|
1674
|
-
}
|
|
1675
|
-
}
|
|
1676
|
-
};
|
|
1677
|
-
const stdout = config.stdout ?? ((text) => writeFd(1, text));
|
|
1678
|
-
const stderr = config.stderr ?? ((text) => writeFd(2, text));
|
|
1679
|
-
const exit = config.exit ?? ((code) => {
|
|
1680
|
-
if (pendingWrite === undefined)
|
|
1681
|
-
process.exit(code);
|
|
1682
|
-
else
|
|
1683
|
-
pendingWrite.then(() => process.exit(writeFailed ? 1 : code));
|
|
1684
|
-
});
|
|
1685
|
-
const readStdin = config.stdin ?? readPipedStdin;
|
|
1686
|
-
return { stdout, stderr, exit, readStdin };
|
|
1687
|
-
}
|
|
1688
|
-
|
|
1689
|
-
// src/tools/cli/download.ts
|
|
1690
|
-
import { basename, resolve } from "node:path";
|
|
1691
|
-
|
|
1692
|
-
// src/internal/write-download.ts
|
|
1693
|
-
import path from "node:path";
|
|
1694
|
-
async function writeDownload(root, name, data) {
|
|
1695
|
-
if (!isSafeEntryName(name) || path.basename(name) !== name) {
|
|
1696
|
-
throw new Error("download name escapes the output dir");
|
|
1697
|
-
}
|
|
1698
|
-
const parent = { handle: root, basename: name };
|
|
1699
|
-
const metadata = await containedEntryMetadata(parent);
|
|
1700
|
-
await writeContainedFile({
|
|
1701
|
-
parent,
|
|
1702
|
-
content: data,
|
|
1703
|
-
replace: true,
|
|
1704
|
-
...metadata?.isFile() && { mode: metadata.mode }
|
|
1705
|
-
});
|
|
1706
|
-
}
|
|
1707
|
-
|
|
1708
|
-
// src/tools/cli/download.ts
|
|
1709
|
-
var DEFAULT_DOWNLOAD_MAX_BYTES = 100 * 1024 * 1024;
|
|
1710
|
-
async function downloadResults(files, dir, stderr, quiet, allowPrivate, maxBytes, timeoutMs) {
|
|
1711
|
-
const root = resolve(dir);
|
|
1712
|
-
if (files.length === 0)
|
|
1713
|
-
return true;
|
|
1714
|
-
let directory;
|
|
1715
|
-
try {
|
|
1716
|
-
directory = await openPinnedDirectory(root, { create: true, readable: false });
|
|
1717
|
-
} catch (error) {
|
|
1718
|
-
stderr(`failed to prepare download directory: ${error instanceof Error ? error.message : String(error)}
|
|
1719
|
-
`);
|
|
1720
|
-
return false;
|
|
1721
|
-
}
|
|
1722
|
-
let succeeded = true;
|
|
1723
|
-
try {
|
|
1724
|
-
for (const file of files) {
|
|
1725
|
-
try {
|
|
1726
|
-
const res = await fetchGuarded(new URL(file.url), allowPrivate, { timeoutMs });
|
|
1727
|
-
if (!res.ok)
|
|
1728
|
-
throw new Error(`HTTP ${res.status}`);
|
|
1729
|
-
const buffer = await readCapped(res, maxBytes);
|
|
1730
|
-
if (!buffer)
|
|
1731
|
-
throw new Error(`file exceeds the ${maxBytes}-byte cap`);
|
|
1732
|
-
const name = basename(file.name);
|
|
1733
|
-
await writeDownload(directory, name, buffer);
|
|
1734
|
-
if (!quiet)
|
|
1735
|
-
stderr(`saved ${resolve(root, name)} (${(buffer.length / 1024).toFixed(0)}KB)
|
|
1736
|
-
`);
|
|
1737
|
-
} catch (err) {
|
|
1738
|
-
succeeded = false;
|
|
1739
|
-
const reason = errnoOf(err) ?? (err instanceof Error ? err.message : String(err));
|
|
1740
|
-
stderr(`failed to download ${file.url}: ${reason}
|
|
1741
|
-
`);
|
|
1742
|
-
}
|
|
1743
|
-
}
|
|
1744
|
-
} finally {
|
|
1745
|
-
try {
|
|
1746
|
-
await directory.close();
|
|
1747
|
-
} catch (error) {
|
|
1748
|
-
succeeded = false;
|
|
1749
|
-
stderr(`failed to close download directory: ${error instanceof Error ? error.message : String(error)}
|
|
1750
|
-
`);
|
|
1751
|
-
}
|
|
1752
|
-
}
|
|
1753
|
-
return succeeded;
|
|
1754
|
-
}
|
|
1755
|
-
|
|
1756
|
-
// src/tools/cli/help.ts
|
|
1757
|
-
var GLOBAL_OPTIONS = [
|
|
1758
|
-
["--json", "Emit compact success/error JSON records (for scripts)"],
|
|
1759
|
-
["--wait", "Block-poll an async result to a terminal state"],
|
|
1760
|
-
["--wait-timeout <s>", "Override the --wait timeout in seconds"],
|
|
1761
|
-
["--output-dir <dir>", "Download result media into a directory"],
|
|
1762
|
-
["--quiet", "Suppress non-essential stderr output"],
|
|
1763
|
-
["--dry-run", "Print the resolved call without executing it"],
|
|
1764
|
-
["--help, -h", "Show help for a command"],
|
|
1765
|
-
["--help <text>", "List only the commands matching a substring"],
|
|
1766
|
-
["--count-by <field>", "Count records per distinct value of a field"],
|
|
1767
|
-
["--sum <field>", "Total a numeric field, optionally grouped by --by"],
|
|
1768
|
-
["--sort <field>", "Order the records by a field, largest first"],
|
|
1769
|
-
["--ascending", "Flip --sort to smallest first"],
|
|
1770
|
-
["--top <n>", "Keep the n leading entries of the view asked for"],
|
|
1771
|
-
["--table <a,b>", "Render the named fields as an aligned table"]
|
|
1772
|
-
];
|
|
1773
|
-
function typeLabel(schema) {
|
|
1774
|
-
if (Array.isArray(schema.enum))
|
|
1775
|
-
return schema.enum.join("|");
|
|
1776
|
-
if (schema.type === "array")
|
|
1777
|
-
return "value";
|
|
1778
|
-
if (typeof schema.type === "string")
|
|
1779
|
-
return schema.type;
|
|
1780
|
-
return "value";
|
|
1781
|
-
}
|
|
1782
|
-
function constraintLabel(schema) {
|
|
1783
|
-
const limits = [];
|
|
1784
|
-
const bounds = [
|
|
1785
|
-
["minimum", ">="],
|
|
1786
|
-
["exclusiveMinimum", ">"],
|
|
1787
|
-
["maximum", "<="],
|
|
1788
|
-
["exclusiveMaximum", "<"],
|
|
1789
|
-
["minLength", "length >="],
|
|
1790
|
-
["maxLength", "length <="],
|
|
1791
|
-
["minItems", "items >="],
|
|
1792
|
-
["maxItems", "items <="]
|
|
1793
|
-
];
|
|
1794
|
-
for (const [keyword, label] of bounds) {
|
|
1795
|
-
const bound = schema[keyword];
|
|
1796
|
-
if (typeof bound !== "number")
|
|
1797
|
-
continue;
|
|
1798
|
-
if (schema.type === "integer" && Math.abs(bound) === Number.MAX_SAFE_INTEGER)
|
|
1799
|
-
continue;
|
|
1800
|
-
limits.push(`${label}${bound}`);
|
|
1801
|
-
}
|
|
1802
|
-
const compositions = [
|
|
1803
|
-
["anyOf", "any of", " | "],
|
|
1804
|
-
["oneOf", "one of", " | "],
|
|
1805
|
-
["allOf", "all of", " & "]
|
|
1806
|
-
];
|
|
1807
|
-
for (const [keyword, label, separator] of compositions) {
|
|
1808
|
-
const members = schema[keyword];
|
|
1809
|
-
if (!Array.isArray(members))
|
|
1810
|
-
continue;
|
|
1811
|
-
const branches = members.map((member) => {
|
|
1812
|
-
const constraints = isRecord(member) ? constraintLabel(member) : "";
|
|
1813
|
-
const type = isRecord(member) ? typeLabel(member) : member === false ? "no value" : "any value";
|
|
1814
|
-
return { constraints, text: `${type}${constraints}` };
|
|
1815
|
-
});
|
|
1816
|
-
if (branches.every((branch) => branch.constraints === ""))
|
|
1817
|
-
continue;
|
|
1818
|
-
limits.push(`${label}: ${branches.map((branch) => branch.text).join(separator)}`);
|
|
1819
|
-
}
|
|
1820
|
-
return limits.length === 0 ? "" : ` [${limits.join(", ")}]`;
|
|
1821
|
-
}
|
|
1822
|
-
function fieldTypeLabel(schema) {
|
|
1823
|
-
return `<${typeLabel(schema)}>${constraintLabel(schema)}`;
|
|
1824
|
-
}
|
|
1825
|
-
function nullableField(schema) {
|
|
1826
|
-
if (schema.type === "null")
|
|
1827
|
-
return true;
|
|
1828
|
-
if (Array.isArray(schema.type) && schema.type.includes("null"))
|
|
1829
|
-
return true;
|
|
1830
|
-
return Array.isArray(schema.anyOf) && schema.anyOf.some((member) => isRecord(member) && nullableField(member));
|
|
1831
|
-
}
|
|
1832
|
-
function jsonValueHelp(fields) {
|
|
1833
|
-
const example = fields.find((field) => nullableField(field.schema))?.name;
|
|
1834
|
-
return [
|
|
1835
|
-
"Assign a whole JSON value with --field:=JSON; the declared schema still validates it.",
|
|
1836
|
-
...example === undefined ? [] : [`JSON null: --${example}:=null. The literal string stays --${example} null.`]
|
|
1837
|
-
];
|
|
1838
|
-
}
|
|
1839
|
-
function padRight(text, width) {
|
|
1840
|
-
return text.length >= width ? text : text + " ".repeat(width - text.length);
|
|
1841
|
-
}
|
|
1842
|
-
function summarize(desc) {
|
|
1843
|
-
const firstLine = desc.split(`
|
|
1844
|
-
`)[0]?.trim() ?? "";
|
|
1845
|
-
const firstSentence = firstLine.split(". ")[0]?.trim() ?? firstLine;
|
|
1846
|
-
const max = 72;
|
|
1847
|
-
return firstSentence.length > max ? `${firstSentence.slice(0, max - 1).trimEnd()}…` : firstSentence;
|
|
1848
|
-
}
|
|
1849
|
-
function applicationOptionLines(fields) {
|
|
1850
|
-
if (fields.length === 0)
|
|
1851
|
-
return [];
|
|
1852
|
-
const labels = new Map(fields.map((field) => [field.name, `--${field.name} ${fieldTypeLabel(field.schema)}`]));
|
|
1853
|
-
const width = Math.max(...fields.map((field) => labels.get(field.name)?.length ?? 0));
|
|
1854
|
-
return [
|
|
1855
|
-
"",
|
|
1856
|
-
"Application options:",
|
|
1857
|
-
...fields.map((field) => ` ${padRight(labels.get(field.name) ?? `--${field.name}`, width)} ${field.description ?? ""}`.trimEnd())
|
|
1858
|
-
];
|
|
1859
|
-
}
|
|
1860
|
-
function filterCommands(commands, filter) {
|
|
1861
|
-
const needle = filter.toLowerCase();
|
|
1862
|
-
return new Map([...commands].filter(([command, descriptor]) => command.toLowerCase().includes(needle) || descriptor.description.toLowerCase().includes(needle)));
|
|
1863
|
-
}
|
|
1864
|
-
function renderTopHelp(input) {
|
|
1865
|
-
const { name, version, defaultCommand } = input;
|
|
1866
|
-
const commands = input.filter === undefined ? input.commands : filterCommands(input.commands, input.filter);
|
|
1867
|
-
const lines = [
|
|
1868
|
-
`${name} ${version}`,
|
|
1869
|
-
"",
|
|
1870
|
-
`Usage: ${name} ${defaultCommand ? "[command]" : "<command>"} [args] [--flags]`,
|
|
1871
|
-
"",
|
|
1872
|
-
input.filter === undefined ? "Commands:" : `Commands matching "${input.filter}" (${commands.size} of ${input.commands.size}):`
|
|
1873
|
-
];
|
|
1874
|
-
const width = Math.max(0, ...[...commands.keys()].map((key) => key.length));
|
|
1875
|
-
for (const [command, descriptor] of commands) {
|
|
1876
|
-
lines.push(` ${padRight(command, width)} ${summarize(descriptor.description)}${command === defaultCommand ? " (default)" : ""}${descriptor.unavailableReason !== undefined ? " [unavailable]" : ""}`);
|
|
1877
|
-
if (descriptor.unavailableReason !== undefined) {
|
|
1878
|
-
lines.push(` Unavailable: ${descriptor.unavailableReason}`);
|
|
1587
|
+
let fields;
|
|
1588
|
+
if (table !== undefined) {
|
|
1589
|
+
fields = table.split(",").map((field) => field.trim()).filter(Boolean);
|
|
1590
|
+
if (fields.length === 0)
|
|
1591
|
+
throw new CliArgumentError2("--table needs at least one field");
|
|
1879
1592
|
}
|
|
1593
|
+
return {
|
|
1594
|
+
kind: "records",
|
|
1595
|
+
...sort !== undefined && { sort },
|
|
1596
|
+
...ascending && { ascending },
|
|
1597
|
+
...top !== undefined && { top },
|
|
1598
|
+
...fields && { table: fields }
|
|
1599
|
+
};
|
|
1880
1600
|
}
|
|
1881
|
-
if (
|
|
1882
|
-
|
|
1883
|
-
}
|
|
1884
|
-
if (input.filter !== undefined) {
|
|
1885
|
-
lines.push("", `Run "${name} <command> --help" for command-specific flags.`);
|
|
1886
|
-
return `${lines.join(`
|
|
1887
|
-
`)}
|
|
1888
|
-
`;
|
|
1601
|
+
if (ascending) {
|
|
1602
|
+
throw new CliArgumentError2("--ascending orders a record view; pass --sort with it");
|
|
1889
1603
|
}
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
}
|
|
1901
|
-
function renderCommandHelp(name, command, descriptor, applicationOptions = []) {
|
|
1902
|
-
const fields = jsonSchemaFields(descriptor.presentationSchema);
|
|
1903
|
-
const fieldsByName = new Map(fields.map((field) => [field.name, field]));
|
|
1904
|
-
const kinds = describeSchemaFields(descriptor.argumentSchema);
|
|
1905
|
-
const positionals = [];
|
|
1906
|
-
const positionalNames = descriptor.positionals ?? [...kinds].filter(([, info]) => info.kind !== "boolean").map(([fieldName]) => fieldName);
|
|
1907
|
-
for (const fieldName of positionalNames) {
|
|
1908
|
-
const field = fieldsByName.get(fieldName);
|
|
1909
|
-
if (field)
|
|
1910
|
-
positionals.push(field);
|
|
1604
|
+
if (sum !== undefined) {
|
|
1605
|
+
if (top !== undefined && by === undefined) {
|
|
1606
|
+
throw new CliArgumentError2("--top needs --by: a single sum has nothing to rank");
|
|
1607
|
+
}
|
|
1608
|
+
return {
|
|
1609
|
+
kind: "sum",
|
|
1610
|
+
field: sum,
|
|
1611
|
+
...by !== undefined && { by },
|
|
1612
|
+
...top !== undefined && { top }
|
|
1613
|
+
};
|
|
1911
1614
|
}
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
const usage = [
|
|
1918
|
-
`Usage: ${name} ${command}`,
|
|
1919
|
-
...positionals.map((field) => positionalSyntax.get(field.name) ?? field.name),
|
|
1920
|
-
"[--flags]"
|
|
1921
|
-
].join(" ");
|
|
1922
|
-
const lines = [descriptor.description, "", usage, ""];
|
|
1923
|
-
if (descriptor.unavailableReason !== undefined) {
|
|
1924
|
-
lines.push(`Unavailable: ${descriptor.unavailableReason}`, "");
|
|
1615
|
+
if (countBy !== undefined) {
|
|
1616
|
+
if (by !== undefined) {
|
|
1617
|
+
throw new CliArgumentError2("--count-by already names the grouping field; drop --by");
|
|
1618
|
+
}
|
|
1619
|
+
return { kind: "count", field: countBy, ...top !== undefined && { top } };
|
|
1925
1620
|
}
|
|
1926
|
-
if (
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
const positional = positionalSyntax.get(field.name);
|
|
1930
|
-
const alias = descriptor.aliases.get(field.name);
|
|
1931
|
-
const option = alias ? `-${alias}, --${field.name}` : `--${field.name}`;
|
|
1932
|
-
return [field.name, positional ? `${positional} | ${option}` : option];
|
|
1933
|
-
}));
|
|
1934
|
-
const width = Math.max(...fields.map((field) => labels.get(field.name)?.length ?? 0));
|
|
1935
|
-
for (const f of fields) {
|
|
1936
|
-
const req = f.required ? " (required)" : "";
|
|
1937
|
-
const desc = f.description ? ` — ${f.description}` : "";
|
|
1938
|
-
const label = labels.get(f.name) ?? `--${f.name}`;
|
|
1939
|
-
lines.push(` ${padRight(label, width)} ${fieldTypeLabel(f.schema)}${req}${desc}`);
|
|
1621
|
+
if (by !== undefined) {
|
|
1622
|
+
if (top === undefined) {
|
|
1623
|
+
throw new CliArgumentError2("--by groups a view; pass --count-by, --sum or --top with it");
|
|
1940
1624
|
}
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1625
|
+
return { kind: "count", field: by, top };
|
|
1626
|
+
}
|
|
1627
|
+
throw new CliArgumentError2("--top needs --sort, --by, --count-by or --sum to rank");
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
// src/tools/cli/args.ts
|
|
1631
|
+
function buildToolArgs(tokens, fields, fieldFor, declaredPositionals, allowUnknown) {
|
|
1632
|
+
const { flags, jsonFields, literalBooleanFlags, positionals } = tokens;
|
|
1633
|
+
if (declaredPositionals === undefined && positionals.length > 0) {
|
|
1634
|
+
const arrayFlag = [...flags.keys()].find((key) => fieldFor(key)?.kind === "array");
|
|
1635
|
+
if (arrayFlag !== undefined) {
|
|
1636
|
+
throw new CliArgumentError2(`--${arrayFlag} cannot be combined with automatic positional arguments. Repeat --${arrayFlag} for each element or use --${arrayFlag}:=JSON; name other fields with flags, or declare a positional policy.`);
|
|
1944
1637
|
}
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1638
|
+
}
|
|
1639
|
+
const toolArgs = {};
|
|
1640
|
+
const fillable = declaredPositionals === undefined ? [...fields.entries()].filter(([, info]) => info.kind !== "boolean").map(([name]) => name) : [...declaredPositionals];
|
|
1641
|
+
const variadicTail = declaredPositionals !== undefined && fields.get(fillable[fillable.length - 1] ?? "")?.kind === "array" ? fillable[fillable.length - 1] : undefined;
|
|
1642
|
+
let pi = 0;
|
|
1643
|
+
for (const key of fillable) {
|
|
1644
|
+
if (pi >= positionals.length)
|
|
1645
|
+
break;
|
|
1646
|
+
if (key === variadicTail) {
|
|
1647
|
+
if (flags.has(key)) {
|
|
1648
|
+
throw new CliArgumentError2(`--${key} conflicts with the positional values for "${key}" — pass one form, not both`);
|
|
1649
|
+
}
|
|
1650
|
+
toolArgs[key] = coerceField(fields.get(key), positionals.slice(pi), `<${key}>`);
|
|
1651
|
+
pi = positionals.length;
|
|
1652
|
+
break;
|
|
1948
1653
|
}
|
|
1949
|
-
if (
|
|
1950
|
-
|
|
1654
|
+
if (flags.has(key))
|
|
1655
|
+
continue;
|
|
1656
|
+
const value = positionals[pi++];
|
|
1657
|
+
if (value !== undefined) {
|
|
1658
|
+
toolArgs[key] = coerceField(fields.get(key), [value], `<${key}>`);
|
|
1951
1659
|
}
|
|
1952
|
-
lines.push("", ...jsonValueHelp(fields), "");
|
|
1953
1660
|
}
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
lines.push(...applicationLines.slice(1), "");
|
|
1957
|
-
return `${lines.join(`
|
|
1958
|
-
`)}
|
|
1959
|
-
`;
|
|
1960
|
-
}
|
|
1961
|
-
|
|
1962
|
-
// src/tools/cli/command-shape.ts
|
|
1963
|
-
function commandShapeIssue(name, descriptor, exists, passthroughField, applicationGlobals = new Set) {
|
|
1964
|
-
assertUniqueToolName(name, exists, "CLI command");
|
|
1965
|
-
if (name === "help" || name === "version") {
|
|
1966
|
-
throw new Error(`[stitchkit] CLI command "${name}" is reserved`);
|
|
1661
|
+
if (pi < positionals.length) {
|
|
1662
|
+
throw new CliArgumentError2(`Unexpected positional argument "${positionals[pi]}"`);
|
|
1967
1663
|
}
|
|
1968
|
-
|
|
1969
|
-
|
|
1664
|
+
const dottedRoots = new Map;
|
|
1665
|
+
for (const key of flags.keys()) {
|
|
1666
|
+
const dot = key.indexOf(".");
|
|
1667
|
+
const exactLiteralBoolean = literalBooleanFlags.has(key) && fields.has(key);
|
|
1668
|
+
if (dot > 0 && !exactLiteralBoolean) {
|
|
1669
|
+
dottedRoots.set(key.slice(0, dot), key);
|
|
1670
|
+
}
|
|
1970
1671
|
}
|
|
1971
|
-
const
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1672
|
+
for (const [root, dotted] of dottedRoots) {
|
|
1673
|
+
if (flags.has(root)) {
|
|
1674
|
+
throw new CliArgumentError2(`--${root} conflicts with --${dotted} — pass one form, not both`);
|
|
1675
|
+
}
|
|
1975
1676
|
}
|
|
1976
|
-
const
|
|
1977
|
-
|
|
1978
|
-
|
|
1677
|
+
for (const key of jsonFields) {
|
|
1678
|
+
for (const other of flags.keys()) {
|
|
1679
|
+
if (other.startsWith(`${key}.`) || key.startsWith(`${other}.`)) {
|
|
1680
|
+
throw new CliArgumentError2(`--${key} conflicts with --${other} — pass one form, not both`);
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
for (const [key, values] of flags) {
|
|
1685
|
+
const info = fieldFor(key);
|
|
1686
|
+
const exactLiteralBoolean = literalBooleanFlags.has(key) && fields.has(key);
|
|
1687
|
+
if (key.includes(".") && !exactLiteralBoolean) {
|
|
1688
|
+
const root = key.split(".")[0] ?? key;
|
|
1689
|
+
const value = !jsonFields.has(key) && info === undefined && allowUnknown && !fields.has(root) ? looseCoerce(values[0] ?? "") : coerceCliFieldValue(info, values, `--${key}`, jsonFields.has(key));
|
|
1690
|
+
setNested(toolArgs, key.split("."), value);
|
|
1691
|
+
continue;
|
|
1692
|
+
}
|
|
1693
|
+
toolArgs[key] = coerceCliFieldValue(info, values, `--${key}`, jsonFields.has(key));
|
|
1979
1694
|
}
|
|
1695
|
+
return toolArgs;
|
|
1980
1696
|
}
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1697
|
+
function parseCliArgs2(argv, schema, config = {}) {
|
|
1698
|
+
const fields = describeSchemaFields(schema);
|
|
1699
|
+
for (const name of config.knownFields ?? []) {
|
|
1700
|
+
if (!fields.has(name))
|
|
1701
|
+
fields.set(name, { kind: "other" });
|
|
1702
|
+
}
|
|
1703
|
+
const fieldFor = (name) => fields.get(name) ?? (name.includes(".") ? describeSchemaPath(schema, name.split(".")) : undefined);
|
|
1704
|
+
const tokens = readCliTokens(argv, fields, fieldFor, config);
|
|
1705
|
+
const { options } = tokens;
|
|
1706
|
+
const view = resolveCliView(tokens.viewFlags, tokens.ascending);
|
|
1707
|
+
if (view)
|
|
1708
|
+
options.view = view;
|
|
1709
|
+
return {
|
|
1710
|
+
toolArgs: buildToolArgs(tokens, fields, fieldFor, config.positionals, config.allowUnknown === true),
|
|
1711
|
+
options
|
|
1989
1712
|
};
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
1713
|
+
}
|
|
1714
|
+
function refuseGlobalOption(issue) {
|
|
1715
|
+
const field = issue?.path[0];
|
|
1716
|
+
const where = typeof field === "string" ? `--${field}` : "application option";
|
|
1717
|
+
throw new CliArgumentError2(`${where}: ${issue?.message ?? "invalid value"}`);
|
|
1718
|
+
}
|
|
1719
|
+
function extractCliGlobalOptions2(argv, schema) {
|
|
1720
|
+
if (schema === undefined)
|
|
1721
|
+
return { argv: [...argv], globals: {} };
|
|
1722
|
+
const fields = describeSchemaFields(schema);
|
|
1723
|
+
const rest = [];
|
|
1724
|
+
const raw = new Map;
|
|
1725
|
+
const jsonFields = new Set;
|
|
1726
|
+
let ended = false;
|
|
1727
|
+
for (let i = 0;i < argv.length; i++) {
|
|
1728
|
+
const token = argv[i];
|
|
1729
|
+
if (token === undefined)
|
|
1730
|
+
continue;
|
|
1731
|
+
if (!ended && token === "--")
|
|
1732
|
+
ended = true;
|
|
1733
|
+
const option = ended ? undefined : classifyLongOptionToken(token);
|
|
1734
|
+
const field = option ? cliFieldOption(option, fields) : undefined;
|
|
1735
|
+
const info = field && option?.globalKind === undefined ? fields.get(field.name) : undefined;
|
|
1736
|
+
if (!option || !field || !info) {
|
|
1737
|
+
rest.push(token);
|
|
1738
|
+
continue;
|
|
1739
|
+
}
|
|
1740
|
+
let { value } = option;
|
|
1741
|
+
if (value === undefined && info.kind === "boolean") {
|
|
1742
|
+
const separate = separateBoolValue(argv[i + 1]);
|
|
1743
|
+
if (separate !== undefined)
|
|
1744
|
+
i++;
|
|
1745
|
+
value = String(separate ?? true);
|
|
1746
|
+
} else if (value === undefined) {
|
|
1747
|
+
const next = argv[i + 1];
|
|
1748
|
+
if (next !== undefined && !isOptionToken(next)) {
|
|
1749
|
+
value = next;
|
|
1750
|
+
i++;
|
|
1751
|
+
} else {
|
|
1752
|
+
throw new CliArgumentError2(`--${option.name} requires a value`);
|
|
1753
|
+
}
|
|
2004
1754
|
}
|
|
1755
|
+
appendCliFieldValue(raw, jsonFields, field.name, value, info, field.json);
|
|
2005
1756
|
}
|
|
2006
|
-
const
|
|
2007
|
-
|
|
1757
|
+
const args = {};
|
|
1758
|
+
for (const [name, values] of raw) {
|
|
1759
|
+
args[name] = coerceCliFieldValue(fields.get(name), values, `--${name}`, jsonFields.has(name));
|
|
1760
|
+
}
|
|
1761
|
+
let coerced;
|
|
1762
|
+
try {
|
|
1763
|
+
coerced = coerceJsonArgs2(args, schema);
|
|
1764
|
+
} catch (error) {
|
|
1765
|
+
if (error instanceof z2.ZodError)
|
|
1766
|
+
refuseGlobalOption(error.issues[0]);
|
|
1767
|
+
throw error;
|
|
1768
|
+
}
|
|
1769
|
+
const parsed = schema.safeParse(coerced);
|
|
1770
|
+
if (!parsed.success)
|
|
1771
|
+
refuseGlobalOption(parsed.error.issues[0]);
|
|
1772
|
+
return { argv: rest, globals: parsed.data };
|
|
2008
1773
|
}
|
|
2009
1774
|
|
|
2010
1775
|
// src/tools/cli/prepare.ts
|
|
@@ -2202,6 +1967,243 @@ function unavailableCommandJson(argv) {
|
|
|
2202
1967
|
return false;
|
|
2203
1968
|
}
|
|
2204
1969
|
|
|
1970
|
+
// src/tools/cli/view.ts
|
|
1971
|
+
var ABSENT = "(absent)";
|
|
1972
|
+
function selectRecords(data) {
|
|
1973
|
+
if (Array.isArray(data))
|
|
1974
|
+
return data;
|
|
1975
|
+
if (isRecord(data)) {
|
|
1976
|
+
const arrays = Object.entries(data).filter(([, value]) => Array.isArray(value));
|
|
1977
|
+
const only = arrays[0];
|
|
1978
|
+
if (arrays.length === 1 && only && Array.isArray(only[1]))
|
|
1979
|
+
return only[1];
|
|
1980
|
+
if (arrays.length > 1) {
|
|
1981
|
+
throw new CliArgumentError2(`an aggregate needs one collection, but the result carries several: ${arrays.map(([key]) => key).join(", ")} — name the shape you want with jq instead`);
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
throw new CliArgumentError2("an aggregate needs a collection; this result is not one");
|
|
1985
|
+
}
|
|
1986
|
+
function isAbsent(value) {
|
|
1987
|
+
return value === undefined || value === null;
|
|
1988
|
+
}
|
|
1989
|
+
function readField(record, path) {
|
|
1990
|
+
let current = record;
|
|
1991
|
+
for (const segment of path) {
|
|
1992
|
+
if (!isRecord(current) || !Object.hasOwn(current, segment))
|
|
1993
|
+
return;
|
|
1994
|
+
current = current[segment];
|
|
1995
|
+
}
|
|
1996
|
+
return current;
|
|
1997
|
+
}
|
|
1998
|
+
function fieldPath(field) {
|
|
1999
|
+
const path = field.split(".");
|
|
2000
|
+
if (path.some((segment) => segment.length === 0 || isUnsafeKey(segment))) {
|
|
2001
|
+
throw new CliArgumentError2(`"${field}" is not a readable field path`);
|
|
2002
|
+
}
|
|
2003
|
+
return path;
|
|
2004
|
+
}
|
|
2005
|
+
function availableFields(records) {
|
|
2006
|
+
const keys = new Set;
|
|
2007
|
+
for (const record of records) {
|
|
2008
|
+
if (isRecord(record))
|
|
2009
|
+
for (const key of Object.keys(record))
|
|
2010
|
+
keys.add(key);
|
|
2011
|
+
}
|
|
2012
|
+
return [...keys].sort();
|
|
2013
|
+
}
|
|
2014
|
+
function assertFieldPresent(records, field) {
|
|
2015
|
+
const path = fieldPath(field);
|
|
2016
|
+
const present = records.some((record) => readField(record, path) !== undefined);
|
|
2017
|
+
if (records.length > 0 && !present) {
|
|
2018
|
+
const available = availableFields(records);
|
|
2019
|
+
throw new CliArgumentError2(`no record carries the field "${field}"${available.length > 0 ? ` — available: ${available.join(", ")}` : ""}`);
|
|
2020
|
+
}
|
|
2021
|
+
return path;
|
|
2022
|
+
}
|
|
2023
|
+
function groupKey(value) {
|
|
2024
|
+
if (value === undefined)
|
|
2025
|
+
return ABSENT;
|
|
2026
|
+
if (value === null)
|
|
2027
|
+
return "null";
|
|
2028
|
+
if (value === "null" || value === ABSENT)
|
|
2029
|
+
return JSON.stringify(value);
|
|
2030
|
+
if (typeof value === "object")
|
|
2031
|
+
return JSON.stringify(value);
|
|
2032
|
+
return String(value);
|
|
2033
|
+
}
|
|
2034
|
+
function ordered(totals, top) {
|
|
2035
|
+
const entries = [...totals].sort(([leftKey, left], [rightKey, right]) => right === left ? leftKey.localeCompare(rightKey) : right - left);
|
|
2036
|
+
const sliced = top === undefined ? entries : entries.slice(0, top);
|
|
2037
|
+
return {
|
|
2038
|
+
kind: "json",
|
|
2039
|
+
data: Object.fromEntries(sliced),
|
|
2040
|
+
order: sliced.map(([key]) => key)
|
|
2041
|
+
};
|
|
2042
|
+
}
|
|
2043
|
+
function stringifyGroups(data, order, compact) {
|
|
2044
|
+
if (!isRecord(data) || order.length === 0)
|
|
2045
|
+
return JSON.stringify(data);
|
|
2046
|
+
const lines = order.map((key) => `${JSON.stringify(key)}:${compact ? "" : " "}${JSON.stringify(data[key])}`);
|
|
2047
|
+
return compact ? `{${lines.join(",")}}` : `{
|
|
2048
|
+
${lines.join(`,
|
|
2049
|
+
`)}
|
|
2050
|
+
}`;
|
|
2051
|
+
}
|
|
2052
|
+
function numericValue(value, field) {
|
|
2053
|
+
if (isAbsent(value))
|
|
2054
|
+
return;
|
|
2055
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
2056
|
+
return value;
|
|
2057
|
+
if (typeof value === "bigint")
|
|
2058
|
+
return Number(value);
|
|
2059
|
+
throw new CliArgumentError2(`--sum ${field}: "${groupKey(value)}" is not a number`);
|
|
2060
|
+
}
|
|
2061
|
+
function renderTable(records, columns) {
|
|
2062
|
+
const fields = columns.map(({ field }) => field);
|
|
2063
|
+
const cell = (value) => {
|
|
2064
|
+
if (value === undefined)
|
|
2065
|
+
return "";
|
|
2066
|
+
if (value === null)
|
|
2067
|
+
return "null";
|
|
2068
|
+
if (typeof value === "object")
|
|
2069
|
+
return JSON.stringify(value);
|
|
2070
|
+
return String(value);
|
|
2071
|
+
};
|
|
2072
|
+
const rows = records.map((record) => columns.map(({ path }) => cell(readField(record, path))));
|
|
2073
|
+
const widths = fields.map((field, column) => Math.max(field.length, ...rows.map((row) => row[column]?.length ?? 0)));
|
|
2074
|
+
const line = (cells) => cells.map((value, column) => value.padEnd(widths[column] ?? value.length)).join(" ").trimEnd();
|
|
2075
|
+
return `${[line(fields), line(widths.map((width) => "-".repeat(width))), ...rows.map(line)].join(`
|
|
2076
|
+
`)}
|
|
2077
|
+
`;
|
|
2078
|
+
}
|
|
2079
|
+
function compareFieldValues(left, right) {
|
|
2080
|
+
if (isAbsent(left))
|
|
2081
|
+
return isAbsent(right) ? 0 : 1;
|
|
2082
|
+
if (isAbsent(right))
|
|
2083
|
+
return -1;
|
|
2084
|
+
if (typeof left === "number" && typeof right === "number")
|
|
2085
|
+
return left - right;
|
|
2086
|
+
if (typeof left === "bigint" || typeof right === "bigint") {
|
|
2087
|
+
return Number(left) - Number(right);
|
|
2088
|
+
}
|
|
2089
|
+
return groupKey(left).localeCompare(groupKey(right));
|
|
2090
|
+
}
|
|
2091
|
+
function renderRecords(records, view) {
|
|
2092
|
+
let selected = [...records];
|
|
2093
|
+
if (view.sort !== undefined) {
|
|
2094
|
+
const path = assertFieldPresent(records, view.sort);
|
|
2095
|
+
const direction = view.ascending ? 1 : -1;
|
|
2096
|
+
selected.sort((left, right) => {
|
|
2097
|
+
const leftValue = readField(left, path);
|
|
2098
|
+
const rightValue = readField(right, path);
|
|
2099
|
+
const order = compareFieldValues(leftValue, rightValue);
|
|
2100
|
+
return isAbsent(leftValue) || isAbsent(rightValue) ? order : order * direction;
|
|
2101
|
+
});
|
|
2102
|
+
}
|
|
2103
|
+
const columns = view.table?.map((field) => ({
|
|
2104
|
+
field,
|
|
2105
|
+
path: assertFieldPresent(records, field)
|
|
2106
|
+
}));
|
|
2107
|
+
if (view.top !== undefined)
|
|
2108
|
+
selected = selected.slice(0, view.top);
|
|
2109
|
+
return columns ? { kind: "text", text: renderTable(selected, columns) } : { kind: "json", data: selected };
|
|
2110
|
+
}
|
|
2111
|
+
function refuseEnvelopeFields(data, view) {
|
|
2112
|
+
if (!isRecord(data))
|
|
2113
|
+
return;
|
|
2114
|
+
const fields = view.kind === "records" ? [...view.sort === undefined ? [] : [view.sort], ...view.table ?? []] : [view.field, ...view.kind === "sum" && view.by !== undefined ? [view.by] : []];
|
|
2115
|
+
for (const field of fields) {
|
|
2116
|
+
const first = field.split(".")[0] ?? "";
|
|
2117
|
+
if (Object.hasOwn(data, first) && !Array.isArray(data[first])) {
|
|
2118
|
+
throw new CliArgumentError2(`"${field}" belongs to the result object, not to its empty collection`);
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
function renderCliView2(data, view) {
|
|
2123
|
+
const records = selectRecords(data);
|
|
2124
|
+
if (records.length === 0)
|
|
2125
|
+
refuseEnvelopeFields(data, view);
|
|
2126
|
+
if (view.kind === "records")
|
|
2127
|
+
return renderRecords(records, view);
|
|
2128
|
+
if (view.kind === "count") {
|
|
2129
|
+
const path = assertFieldPresent(records, view.field);
|
|
2130
|
+
const totals = new Map;
|
|
2131
|
+
for (const record of records) {
|
|
2132
|
+
const key = groupKey(readField(record, path));
|
|
2133
|
+
totals.set(key, (totals.get(key) ?? 0) + 1);
|
|
2134
|
+
}
|
|
2135
|
+
return ordered(totals, view.top);
|
|
2136
|
+
}
|
|
2137
|
+
const path = assertFieldPresent(records, view.field);
|
|
2138
|
+
if (view.by === undefined) {
|
|
2139
|
+
let total = 0;
|
|
2140
|
+
for (const record of records)
|
|
2141
|
+
total += numericValue(readField(record, path), view.field) ?? 0;
|
|
2142
|
+
return { kind: "json", data: total };
|
|
2143
|
+
}
|
|
2144
|
+
const byPath = assertFieldPresent(records, view.by);
|
|
2145
|
+
const totals = new Map;
|
|
2146
|
+
for (const record of records) {
|
|
2147
|
+
const key = groupKey(readField(record, byPath));
|
|
2148
|
+
totals.set(key, (totals.get(key) ?? 0) + (numericValue(readField(record, path), view.field) ?? 0));
|
|
2149
|
+
}
|
|
2150
|
+
return ordered(totals, view.top);
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
// src/tools/cli/wait.ts
|
|
2154
|
+
var DEFAULT_TIMEOUT2 = 600;
|
|
2155
|
+
function waitFailure(tool, result) {
|
|
2156
|
+
return {
|
|
2157
|
+
ok: false,
|
|
2158
|
+
code: "WAIT_FAILED",
|
|
2159
|
+
details: {
|
|
2160
|
+
message: `"${tool}" reached a terminal failed state`,
|
|
2161
|
+
result
|
|
2162
|
+
}
|
|
2163
|
+
};
|
|
2164
|
+
}
|
|
2165
|
+
async function pollUntilDone2(params) {
|
|
2166
|
+
const { initial, wait, call } = params;
|
|
2167
|
+
if (!initial.ok)
|
|
2168
|
+
return initial;
|
|
2169
|
+
if (wait.failed?.(initial.data))
|
|
2170
|
+
return waitFailure(wait.tool, initial.data);
|
|
2171
|
+
if (wait.done(initial.data))
|
|
2172
|
+
return initial;
|
|
2173
|
+
const pollArgs = wait.poll(initial.data);
|
|
2174
|
+
if (!pollArgs)
|
|
2175
|
+
return initial;
|
|
2176
|
+
const timeoutSec = params.timeoutSec ?? wait.timeout ?? DEFAULT_TIMEOUT2;
|
|
2177
|
+
let failedState;
|
|
2178
|
+
const { state, timedOut } = await pollUntil({
|
|
2179
|
+
poll: () => call(wait.tool, pollArgs),
|
|
2180
|
+
done: (result) => {
|
|
2181
|
+
if (!result.ok)
|
|
2182
|
+
return true;
|
|
2183
|
+
if (wait.failed?.(result.data)) {
|
|
2184
|
+
failedState = result;
|
|
2185
|
+
return true;
|
|
2186
|
+
}
|
|
2187
|
+
return wait.done(result.data);
|
|
2188
|
+
},
|
|
2189
|
+
backoff: wait.backoff,
|
|
2190
|
+
timeoutSec,
|
|
2191
|
+
sleepFn: params.sleepFn,
|
|
2192
|
+
onTick: params.onTick,
|
|
2193
|
+
signal: params.signal
|
|
2194
|
+
});
|
|
2195
|
+
if (timedOut) {
|
|
2196
|
+
return {
|
|
2197
|
+
ok: false,
|
|
2198
|
+
code: "TIMEOUT",
|
|
2199
|
+
details: { message: `Timed out after ${timeoutSec}s waiting for "${wait.tool}"` }
|
|
2200
|
+
};
|
|
2201
|
+
}
|
|
2202
|
+
if (state === failedState && state.ok)
|
|
2203
|
+
return waitFailure(wait.tool, state.data);
|
|
2204
|
+
return state;
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2205
2207
|
// src/tools/cli/run-managed.ts
|
|
2206
2208
|
async function runManagedInvocation(session, route) {
|
|
2207
2209
|
const { config } = session;
|