tines 0.0.82 → 0.0.84
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/dist/index.js +246 -24
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { existsSync as existsSync3, mkdirSync as mkdirSync4, readdirSync, readFileSync as
|
|
4
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync4, readdirSync, readFileSync as readFileSync5, statSync as statSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
5
5
|
import { hostname as hostname2 } from "node:os";
|
|
6
6
|
import { basename, dirname as dirname4, join as join4 } from "node:path";
|
|
7
7
|
import { createInterface } from "node:readline/promises";
|
|
@@ -40,7 +40,15 @@ function readBodyValue(value, stdin = processStdin) {
|
|
|
40
40
|
|
|
41
41
|
// src/daemon/daemon.ts
|
|
42
42
|
import { spawn as spawn2 } from "node:child_process";
|
|
43
|
-
import {
|
|
43
|
+
import {
|
|
44
|
+
createWriteStream,
|
|
45
|
+
mkdirSync as mkdirSync3,
|
|
46
|
+
readFileSync as readFileSync4,
|
|
47
|
+
rmSync,
|
|
48
|
+
statSync,
|
|
49
|
+
unlinkSync,
|
|
50
|
+
writeFileSync as writeFileSync2
|
|
51
|
+
} from "node:fs";
|
|
44
52
|
import { hostname, platform, arch } from "node:os";
|
|
45
53
|
import { dirname as dirname3, join as join3 } from "node:path";
|
|
46
54
|
|
|
@@ -108,6 +116,8 @@ var RUNNER_OFFLINE_FAIL_MS = 5 * 60 * 1e3;
|
|
|
108
116
|
var LAUNCH_STALL_MS = 5 * 60 * 1e3;
|
|
109
117
|
var RUN_KEY_SLACK_MS = 10 * 60 * 1e3;
|
|
110
118
|
var RUN_LOG_MAX_BYTES = 256 * 1024;
|
|
119
|
+
var RUN_LOG_RETENTION_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
120
|
+
var RUN_LOG_RAW_MAX_BYTES = 64 * 1024 * 1024;
|
|
111
121
|
var MODEL_PREDECESSORS = {
|
|
112
122
|
"claude-fable-5": ["claude-opus-5", "claude-opus-4-8", "claude-opus-4-7", "claude-opus-4-6"],
|
|
113
123
|
"claude-opus-5": ["claude-opus-4-8", "claude-opus-4-7", "claude-opus-4-6", "claude-opus-4-5", "claude-opus-4-1"],
|
|
@@ -497,6 +507,19 @@ function createApiClient(options) {
|
|
|
497
507
|
// Agent runs
|
|
498
508
|
listRuns: (filters = {}) => get(`/api/v1/runs${query(filters)}`),
|
|
499
509
|
getRun: (id) => get(`/api/v1/runs/${id}`),
|
|
510
|
+
/**
|
|
511
|
+
* The run's complete log (not the 256 KB tail `getRun` returns) as a
|
|
512
|
+
* streamed Response, so a multi-megabyte log never has to be held in
|
|
513
|
+
* memory. `raw` asks for the unrendered harness stream instead.
|
|
514
|
+
*/
|
|
515
|
+
getRunLogFull: (id, opts = {}) => raw("GET", `/api/v1/runs/${id}/log${opts.raw ? "?raw=1" : ""}`, {
|
|
516
|
+
headers: { accept: "text/plain" }
|
|
517
|
+
}),
|
|
518
|
+
/** Daemon-only: uploads the raw harness stream for a settled run. */
|
|
519
|
+
putRunLogRaw: (id, body) => raw("PUT", `/api/v1/runs/${id}/log/raw`, {
|
|
520
|
+
body,
|
|
521
|
+
headers: { "content-type": "application/x-ndjson", "content-length": String(body.byteLength) }
|
|
522
|
+
}),
|
|
500
523
|
cancelRun: (id) => request("POST", `/api/v1/runs/${id}/cancel`),
|
|
501
524
|
// Routing rules (one per exact scope; responses carry shadow hints)
|
|
502
525
|
listRoutingRules: () => get("/api/v1/routing-rules"),
|
|
@@ -550,6 +573,101 @@ var PLACEHOLDER_DESCRIPTIONS = {
|
|
|
550
573
|
};
|
|
551
574
|
var TEMPLATE_PLACEHOLDERS = Object.keys(PLACEHOLDER_DESCRIPTIONS).map((key) => ({ key, description: PLACEHOLDER_DESCRIPTIONS[key] }));
|
|
552
575
|
|
|
576
|
+
// src/daemon/claude-stream.ts
|
|
577
|
+
function clip(value, max) {
|
|
578
|
+
return value.length > max ? `${value.slice(0, max)}\u2026` : value;
|
|
579
|
+
}
|
|
580
|
+
function resultText(content) {
|
|
581
|
+
if (typeof content === "string") return content.trim();
|
|
582
|
+
if (Array.isArray(content)) {
|
|
583
|
+
return content.map((block) => typeof block?.text === "string" ? block.text : "").join("").trim();
|
|
584
|
+
}
|
|
585
|
+
return "";
|
|
586
|
+
}
|
|
587
|
+
function renderStreamEvent(event) {
|
|
588
|
+
switch (event.type) {
|
|
589
|
+
case "system":
|
|
590
|
+
if (event.subtype === "init") {
|
|
591
|
+
return [`[session] started${event.model ? ` (model ${event.model})` : ""}`];
|
|
592
|
+
}
|
|
593
|
+
return [];
|
|
594
|
+
case "assistant": {
|
|
595
|
+
const lines = [];
|
|
596
|
+
for (const block of event.message?.content ?? []) {
|
|
597
|
+
if (block.type === "text" && block.text?.trim()) {
|
|
598
|
+
lines.push(`[agent] ${clip(block.text.trim(), 2e3)}`);
|
|
599
|
+
} else if (block.type === "tool_use") {
|
|
600
|
+
const args = block.input === void 0 ? "" : JSON.stringify(block.input);
|
|
601
|
+
lines.push(`[tool] ${block.name ?? "tool"} ${clip(args, 300)}`.trimEnd());
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
return lines;
|
|
605
|
+
}
|
|
606
|
+
case "user": {
|
|
607
|
+
const lines = [];
|
|
608
|
+
for (const block of event.message?.content ?? []) {
|
|
609
|
+
if (block.type === "tool_result" && block.is_error) {
|
|
610
|
+
lines.push(`[tool] error: ${clip(resultText(block.content), 300)}`);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
return lines;
|
|
614
|
+
}
|
|
615
|
+
case "result": {
|
|
616
|
+
const parts = [];
|
|
617
|
+
if (typeof event.num_turns === "number") parts.push(`${event.num_turns} turns`);
|
|
618
|
+
if (typeof event.total_cost_usd === "number") parts.push(`$${event.total_cost_usd.toFixed(2)}`);
|
|
619
|
+
const detail = parts.length > 0 ? ` (${parts.join(", ")})` : "";
|
|
620
|
+
const lines = [`[session] result: ${event.subtype ?? "done"}${detail}`];
|
|
621
|
+
if (event.is_error && event.result) lines.push(`[error] ${clip(event.result, 2e3)}`);
|
|
622
|
+
return lines;
|
|
623
|
+
}
|
|
624
|
+
default:
|
|
625
|
+
return [];
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
var ClaudeStreamRenderer = class {
|
|
629
|
+
constructor(emit) {
|
|
630
|
+
this.emit = emit;
|
|
631
|
+
}
|
|
632
|
+
emit;
|
|
633
|
+
pending = "";
|
|
634
|
+
write(chunk) {
|
|
635
|
+
this.pending += chunk;
|
|
636
|
+
let nl = this.pending.indexOf("\n");
|
|
637
|
+
while (nl !== -1) {
|
|
638
|
+
this.line(this.pending.slice(0, nl));
|
|
639
|
+
this.pending = this.pending.slice(nl + 1);
|
|
640
|
+
nl = this.pending.indexOf("\n");
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
/** Renders any trailing partial line. Call once the harness has exited. */
|
|
644
|
+
finish() {
|
|
645
|
+
if (this.pending) {
|
|
646
|
+
this.line(this.pending);
|
|
647
|
+
this.pending = "";
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
line(raw) {
|
|
651
|
+
const trimmed = raw.trim();
|
|
652
|
+
if (!trimmed) return;
|
|
653
|
+
if (!trimmed.startsWith("{")) {
|
|
654
|
+
this.emit(`${raw}
|
|
655
|
+
`);
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
let event;
|
|
659
|
+
try {
|
|
660
|
+
event = JSON.parse(trimmed);
|
|
661
|
+
} catch {
|
|
662
|
+
this.emit(`${raw}
|
|
663
|
+
`);
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
for (const line of renderStreamEvent(event)) this.emit(`${line}
|
|
667
|
+
`);
|
|
668
|
+
}
|
|
669
|
+
};
|
|
670
|
+
|
|
553
671
|
// src/daemon/cli-refresh.ts
|
|
554
672
|
import { spawn } from "node:child_process";
|
|
555
673
|
import { existsSync, mkdirSync, readFileSync as readFileSync2 } from "node:fs";
|
|
@@ -571,7 +689,7 @@ function buildHarnessInvocation(spec, input) {
|
|
|
571
689
|
file: "sh",
|
|
572
690
|
args: [
|
|
573
691
|
"-c",
|
|
574
|
-
`claude -p${input.model ? ` --model ${shellQuote(input.model)}` : ""} < ${shellQuote(input.promptFile)}`
|
|
692
|
+
`claude -p --output-format stream-json --verbose${input.model ? ` --model ${shellQuote(input.model)}` : ""} < ${shellQuote(input.promptFile)}`
|
|
575
693
|
]
|
|
576
694
|
};
|
|
577
695
|
case "codex":
|
|
@@ -626,6 +744,7 @@ var RunTable = class {
|
|
|
626
744
|
async finishAndCleanup(run, status, error) {
|
|
627
745
|
if (run.settled) return this.cleanup(run);
|
|
628
746
|
run.settled = true;
|
|
747
|
+
run.drain?.();
|
|
629
748
|
await run.flush?.();
|
|
630
749
|
try {
|
|
631
750
|
await this.effects.finish(run, status, error);
|
|
@@ -661,10 +780,13 @@ var LogBatcher = class {
|
|
|
661
780
|
buffer = "";
|
|
662
781
|
timer = null;
|
|
663
782
|
sending = Promise.resolve();
|
|
783
|
+
seq = 0;
|
|
784
|
+
/** Chunks awaiting a successful send, oldest first. */
|
|
785
|
+
pending = [];
|
|
664
786
|
append(text2) {
|
|
665
787
|
if (!text2) return;
|
|
666
788
|
this.buffer += text2;
|
|
667
|
-
if (Buffer.byteLength(this.buffer, "utf8") >= (this.opts.maxBytes ??
|
|
789
|
+
if (Buffer.byteLength(this.buffer, "utf8") >= (this.opts.maxBytes ?? 32 * 1024)) {
|
|
668
790
|
void this.flush();
|
|
669
791
|
} else if (!this.timer) {
|
|
670
792
|
this.timer = setTimeout(() => void this.flush(), this.opts.intervalMs ?? 2e3);
|
|
@@ -677,13 +799,53 @@ var LogBatcher = class {
|
|
|
677
799
|
clearTimeout(this.timer);
|
|
678
800
|
this.timer = null;
|
|
679
801
|
}
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
this.
|
|
802
|
+
if (this.buffer) {
|
|
803
|
+
this.pending.push({ chunk: this.buffer, seq: ++this.seq });
|
|
804
|
+
this.buffer = "";
|
|
805
|
+
this.trimPending();
|
|
684
806
|
}
|
|
807
|
+
if (this.pending.length === 0) return this.sending;
|
|
808
|
+
this.sending = this.sending.then(() => this.drain());
|
|
685
809
|
return this.sending;
|
|
686
810
|
}
|
|
811
|
+
/**
|
|
812
|
+
* Sends queued chunks in order, stopping at the first failure so the
|
|
813
|
+
* survivors keep their place — and their seq, which is what lets the
|
|
814
|
+
* server recognise a resend of a chunk it already applied.
|
|
815
|
+
*/
|
|
816
|
+
async drain() {
|
|
817
|
+
while (this.pending.length > 0) {
|
|
818
|
+
const next = this.pending[0];
|
|
819
|
+
try {
|
|
820
|
+
await this.send(next.chunk, next.seq);
|
|
821
|
+
} catch (err) {
|
|
822
|
+
this.opts.onError?.(err);
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
this.pending.shift();
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* Bounds the retry backlog: a daemon that cannot reach the supervisor for
|
|
830
|
+
* a long time must not grow its heap without limit. The oldest chunks go
|
|
831
|
+
* first, with a marker so the gap is visible in the log rather than
|
|
832
|
+
* silent.
|
|
833
|
+
*/
|
|
834
|
+
trimPending() {
|
|
835
|
+
const cap = this.opts.maxPendingBytes ?? 1024 * 1024;
|
|
836
|
+
let held = this.pending.reduce((n, p) => n + Buffer.byteLength(p.chunk, "utf8"), 0);
|
|
837
|
+
if (held <= cap) return;
|
|
838
|
+
let lost = 0;
|
|
839
|
+
while (this.pending.length > 1 && held > cap) {
|
|
840
|
+
const dropped = this.pending.shift();
|
|
841
|
+
const bytes = Buffer.byteLength(dropped.chunk, "utf8");
|
|
842
|
+
held -= bytes;
|
|
843
|
+
lost += bytes;
|
|
844
|
+
}
|
|
845
|
+
const head = this.pending[0];
|
|
846
|
+
head.chunk = `[log] ${lost} bytes lost (supervisor unreachable)
|
|
847
|
+
${head.chunk}`;
|
|
848
|
+
}
|
|
687
849
|
};
|
|
688
850
|
var AMBIENT_CLI = { binDir: null, version: null, source: "ambient" };
|
|
689
851
|
var CliRefresher = class {
|
|
@@ -884,6 +1046,32 @@ function processStartTimeMs(pid) {
|
|
|
884
1046
|
|
|
885
1047
|
// src/daemon/daemon.ts
|
|
886
1048
|
var CLI_REFRESH_TTL_MS = 10 * 6e4;
|
|
1049
|
+
async function uploadRawLog(run) {
|
|
1050
|
+
const path2 = run.rawSpoolPath;
|
|
1051
|
+
if (!path2 || !run.rawUpload) return;
|
|
1052
|
+
run.rawSpoolPath = void 0;
|
|
1053
|
+
try {
|
|
1054
|
+
await new Promise((resolve) => run.rawSpool?.end(resolve) ?? resolve());
|
|
1055
|
+
const size = statSync(path2).size;
|
|
1056
|
+
if (size > 0) {
|
|
1057
|
+
let body = readFileSync4(path2);
|
|
1058
|
+
if (body.byteLength > RUN_LOG_RAW_MAX_BYTES) {
|
|
1059
|
+
const marker = Buffer.from(
|
|
1060
|
+
`{"type":"tines_truncated","dropped_bytes":${body.byteLength - RUN_LOG_RAW_MAX_BYTES}}
|
|
1061
|
+
`
|
|
1062
|
+
);
|
|
1063
|
+
body = Buffer.concat([marker, body.subarray(body.byteLength - RUN_LOG_RAW_MAX_BYTES + marker.byteLength)]);
|
|
1064
|
+
}
|
|
1065
|
+
await run.rawUpload(body);
|
|
1066
|
+
}
|
|
1067
|
+
} catch {
|
|
1068
|
+
} finally {
|
|
1069
|
+
try {
|
|
1070
|
+
unlinkSync(path2);
|
|
1071
|
+
} catch {
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
887
1075
|
var log = (message3) => console.log(`[${(/* @__PURE__ */ new Date()).toISOString().slice(11, 19)}] ${message3}`);
|
|
888
1076
|
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
889
1077
|
function killTree(pid, signal) {
|
|
@@ -943,7 +1131,9 @@ async function runDaemon(opts) {
|
|
|
943
1131
|
},
|
|
944
1132
|
release: (run) => {
|
|
945
1133
|
if (run.timeout) clearTimeout(run.timeout);
|
|
1134
|
+
run.renderer?.finish();
|
|
946
1135
|
rmSync(run.workspace, { recursive: true, force: true });
|
|
1136
|
+
void uploadRawLog(run);
|
|
947
1137
|
},
|
|
948
1138
|
persist: () => {
|
|
949
1139
|
const entries = table2.values().filter((run) => run.child?.pid !== void 0).map((run) => ({
|
|
@@ -999,10 +1189,11 @@ async function runDaemon(opts) {
|
|
|
999
1189
|
timedOut: false,
|
|
1000
1190
|
settled: false,
|
|
1001
1191
|
keyFingerprint: assignment.run_key.slice(0, 14),
|
|
1002
|
-
batcher: new LogBatcher(
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1192
|
+
batcher: new LogBatcher(
|
|
1193
|
+
(chunk, seq) => client2.appendRunLog(runId, { chunk, seq }).then(() => {
|
|
1194
|
+
}),
|
|
1195
|
+
{ onError: (err) => log(`log append for run ${runId} failed: ${message2(err)}`) }
|
|
1196
|
+
)
|
|
1006
1197
|
};
|
|
1007
1198
|
run.flush = () => run.batcher.flush();
|
|
1008
1199
|
table2.track(run);
|
|
@@ -1066,7 +1257,22 @@ async function runDaemon(opts) {
|
|
|
1066
1257
|
run.spawnedAt = Date.now();
|
|
1067
1258
|
table2.persist();
|
|
1068
1259
|
log(`run ${runId}: launched ${invocation.file} (pid ${child.pid})`);
|
|
1069
|
-
|
|
1260
|
+
if (opts.harness === "claude_code") {
|
|
1261
|
+
const renderer = new ClaudeStreamRenderer((line) => run.batcher.append(line));
|
|
1262
|
+
run.renderer = renderer;
|
|
1263
|
+
run.drain = () => renderer.finish();
|
|
1264
|
+
const spoolPath = join3(opts.configDir, "rawlogs", `${runId}.ndjson`);
|
|
1265
|
+
mkdirSync3(dirname3(spoolPath), { recursive: true });
|
|
1266
|
+
run.rawSpoolPath = spoolPath;
|
|
1267
|
+
run.rawSpool = createWriteStream(spoolPath);
|
|
1268
|
+
run.rawUpload = (body) => client2.putRunLogRaw(runId, body);
|
|
1269
|
+
child.stdout?.on("data", (data) => {
|
|
1270
|
+
run.rawSpool?.write(data);
|
|
1271
|
+
renderer.write(data.toString("utf8"));
|
|
1272
|
+
});
|
|
1273
|
+
} else {
|
|
1274
|
+
child.stdout?.on("data", (data) => run.batcher.append(data.toString("utf8")));
|
|
1275
|
+
}
|
|
1070
1276
|
child.stderr?.on("data", (data) => run.batcher.append(data.toString("utf8")));
|
|
1071
1277
|
run.timeout = setTimeout(
|
|
1072
1278
|
() => {
|
|
@@ -4643,14 +4849,14 @@ function readJsonBody(inline, file) {
|
|
|
4643
4849
|
if (file !== void 0 && file !== "-") {
|
|
4644
4850
|
let raw;
|
|
4645
4851
|
try {
|
|
4646
|
-
raw =
|
|
4852
|
+
raw = readFileSync5(file, "utf8");
|
|
4647
4853
|
} catch (err) {
|
|
4648
4854
|
die(`cannot read ${file}: ${err instanceof Error ? err.message : String(err)}`);
|
|
4649
4855
|
}
|
|
4650
4856
|
return parseJsonObject(raw, file);
|
|
4651
4857
|
}
|
|
4652
4858
|
if (file === "-" || !process.stdin.isTTY) {
|
|
4653
|
-
const raw =
|
|
4859
|
+
const raw = readFileSync5(0, "utf8");
|
|
4654
4860
|
if (raw.trim() === "") {
|
|
4655
4861
|
if (file === "-") die("no JSON on stdin");
|
|
4656
4862
|
return void 0;
|
|
@@ -4792,7 +4998,7 @@ function parseFileSpec(spec) {
|
|
|
4792
4998
|
}
|
|
4793
4999
|
const file = source.slice(1);
|
|
4794
5000
|
try {
|
|
4795
|
-
return { path: path2, content:
|
|
5001
|
+
return { path: path2, content: readFileSync5(file, "utf8") };
|
|
4796
5002
|
} catch (err) {
|
|
4797
5003
|
die(`cannot read ${file}: ${err instanceof Error ? err.message : String(err)}`);
|
|
4798
5004
|
}
|
|
@@ -4990,7 +5196,7 @@ warning: ${w}`);
|
|
|
4990
5196
|
function cliVersion() {
|
|
4991
5197
|
try {
|
|
4992
5198
|
const manifest = new URL("../package.json", import.meta.url);
|
|
4993
|
-
return JSON.parse(
|
|
5199
|
+
return JSON.parse(readFileSync5(manifest, "utf8")).version ?? "0.0.0-unknown";
|
|
4994
5200
|
} catch {
|
|
4995
5201
|
return "0.0.0-unknown";
|
|
4996
5202
|
}
|
|
@@ -5451,7 +5657,7 @@ function walkFolder(dir) {
|
|
|
5451
5657
|
const nextRel = rel ? `${rel}/${entry.name}` : entry.name;
|
|
5452
5658
|
if (entry.isDirectory()) walk(nextAbs, nextRel);
|
|
5453
5659
|
else if (entry.isFile()) {
|
|
5454
|
-
files.push({ path: nextRel, contentType: sniffContentType(entry.name), bytes:
|
|
5660
|
+
files.push({ path: nextRel, contentType: sniffContentType(entry.name), bytes: readFileSync5(nextAbs) });
|
|
5455
5661
|
}
|
|
5456
5662
|
}
|
|
5457
5663
|
};
|
|
@@ -5526,7 +5732,7 @@ withCommon(
|
|
|
5526
5732
|
const issue = await resolveIssue(api, ref);
|
|
5527
5733
|
let artifact;
|
|
5528
5734
|
if (opts.folder !== void 0) {
|
|
5529
|
-
if (!existsSync3(opts.folder) || !
|
|
5735
|
+
if (!existsSync3(opts.folder) || !statSync2(opts.folder).isDirectory()) {
|
|
5530
5736
|
die(`--folder needs a directory, got "${opts.folder}"`);
|
|
5531
5737
|
}
|
|
5532
5738
|
const files = walkFolder(opts.folder);
|
|
@@ -5538,7 +5744,7 @@ withCommon(
|
|
|
5538
5744
|
} else if (opts.file !== void 0) {
|
|
5539
5745
|
let bytes;
|
|
5540
5746
|
try {
|
|
5541
|
-
bytes =
|
|
5747
|
+
bytes = readFileSync5(opts.file);
|
|
5542
5748
|
} catch (err) {
|
|
5543
5749
|
die(`cannot read ${opts.file}: ${err instanceof Error ? err.message : String(err)}`);
|
|
5544
5750
|
}
|
|
@@ -5615,7 +5821,7 @@ withCommon(
|
|
|
5615
5821
|
if (opts.out === void 0) {
|
|
5616
5822
|
die(`artifact "${name2}" is a folder \u2014 pass --out <dir> to write its tree`);
|
|
5617
5823
|
}
|
|
5618
|
-
if (existsSync3(opts.out) && !
|
|
5824
|
+
if (existsSync3(opts.out) && !statSync2(opts.out).isDirectory()) {
|
|
5619
5825
|
die(`--out for a folder must be a directory, and "${opts.out}" is a file`);
|
|
5620
5826
|
}
|
|
5621
5827
|
const files = version.files ?? [];
|
|
@@ -5638,7 +5844,7 @@ withCommon(
|
|
|
5638
5844
|
const bytes = Buffer.from(content.bytes);
|
|
5639
5845
|
if (opts.out !== void 0) {
|
|
5640
5846
|
let target2 = opts.out;
|
|
5641
|
-
if (existsSync3(target2) &&
|
|
5847
|
+
if (existsSync3(target2) && statSync2(target2).isDirectory()) {
|
|
5642
5848
|
target2 = join4(target2, version.filename ?? name2);
|
|
5643
5849
|
}
|
|
5644
5850
|
writeFileSync3(target2, bytes);
|
|
@@ -6457,7 +6663,7 @@ withList(
|
|
|
6457
6663
|
});
|
|
6458
6664
|
});
|
|
6459
6665
|
withCommon(
|
|
6460
|
-
runsCmd.command("show <id>").description("Show a run; --logs prints the captured log tail").option("--logs", "print the log tail")
|
|
6666
|
+
runsCmd.command("show <id>").description("Show a run; --logs prints the captured log tail, --logs --full the whole log").option("--logs", "print the log tail").option("--full", "with --logs: print the complete log, not the 256 KB tail").option("--raw", "with --logs --full: print the unrendered harness stream instead")
|
|
6461
6667
|
).action(async (id, opts) => {
|
|
6462
6668
|
const api = client(opts);
|
|
6463
6669
|
const run = await api.getRun(id);
|
|
@@ -6486,8 +6692,24 @@ withCommon(
|
|
|
6486
6692
|
if (run.error) console.log(`error: ${run.error}`);
|
|
6487
6693
|
if (opts.logs) {
|
|
6488
6694
|
console.log("");
|
|
6695
|
+
if (opts.full || opts.raw) {
|
|
6696
|
+
const res = await api.getRunLogFull(id, { raw: opts.raw });
|
|
6697
|
+
const body = res.body;
|
|
6698
|
+
if (!body) return;
|
|
6699
|
+
const reader = body.getReader();
|
|
6700
|
+
const decoder = new TextDecoder();
|
|
6701
|
+
for (; ; ) {
|
|
6702
|
+
const { done, value } = await reader.read();
|
|
6703
|
+
if (done) break;
|
|
6704
|
+
if (value) process.stdout.write(decoder.decode(value, { stream: true }));
|
|
6705
|
+
}
|
|
6706
|
+
process.stdout.write(decoder.decode());
|
|
6707
|
+
return;
|
|
6708
|
+
}
|
|
6489
6709
|
if (run.log_bytes_dropped > 0) {
|
|
6490
|
-
console.log(
|
|
6710
|
+
console.log(
|
|
6711
|
+
`[${Math.round(run.log_bytes_dropped / 1024)} KB truncated from the head \u2014 ` + (run.log_expired ? "past its retention window; only this tail remains]" : `run \`tines runs show ${run.id} --logs --full\` for the complete ${Math.round(run.log_full_bytes / 1024)} KB log]`)
|
|
6712
|
+
);
|
|
6491
6713
|
}
|
|
6492
6714
|
console.log(run.log || "(no log output captured)");
|
|
6493
6715
|
}
|