tines 0.0.89 → 0.0.90
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 +258 -19
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4078,6 +4078,36 @@ function runRow(run) {
|
|
|
4078
4078
|
timestamp(run.created_at)
|
|
4079
4079
|
];
|
|
4080
4080
|
}
|
|
4081
|
+
function byteSize(bytes) {
|
|
4082
|
+
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
4083
|
+
let value = Math.max(0, bytes);
|
|
4084
|
+
let unit = 0;
|
|
4085
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
4086
|
+
value /= 1024;
|
|
4087
|
+
unit += 1;
|
|
4088
|
+
}
|
|
4089
|
+
const rounded = unit <= 1 ? Math.round(value) : Math.round(value * 10) / 10;
|
|
4090
|
+
return `${unit <= 1 ? rounded : rounded.toFixed(1)} ${units[unit]}`;
|
|
4091
|
+
}
|
|
4092
|
+
function ageLabel(isoTimestamp, now = Date.now()) {
|
|
4093
|
+
const then = Date.parse(isoTimestamp);
|
|
4094
|
+
if (!Number.isFinite(then)) return "\u2014";
|
|
4095
|
+
const seconds = Math.max(0, Math.round((now - then) / 1e3));
|
|
4096
|
+
if (seconds < 60) return `${seconds}s`;
|
|
4097
|
+
if (seconds < 3600) return `${Math.floor(seconds / 60)}m`;
|
|
4098
|
+
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h`;
|
|
4099
|
+
return `${Math.floor(seconds / 86400)}d`;
|
|
4100
|
+
}
|
|
4101
|
+
function keptWorkspaceRow(kept, sizeBytes, now = Date.now()) {
|
|
4102
|
+
return [
|
|
4103
|
+
kept.run_id,
|
|
4104
|
+
kept.issue_ref ?? "\u2014",
|
|
4105
|
+
kept.status,
|
|
4106
|
+
ageLabel(kept.kept_at, now),
|
|
4107
|
+
byteSize(sizeBytes),
|
|
4108
|
+
kept.path
|
|
4109
|
+
];
|
|
4110
|
+
}
|
|
4081
4111
|
function formatTable(rows) {
|
|
4082
4112
|
if (rows.length === 0) return "";
|
|
4083
4113
|
const widths = rows[0].map((_, i) => Math.max(...rows.map((r) => r[i].length)));
|
|
@@ -5501,10 +5531,11 @@ import { hostname as hostname2 } from "node:os";
|
|
|
5501
5531
|
import { spawn as spawn2 } from "node:child_process";
|
|
5502
5532
|
import {
|
|
5503
5533
|
createWriteStream,
|
|
5534
|
+
existsSync as existsSync4,
|
|
5504
5535
|
mkdirSync as mkdirSync4,
|
|
5505
5536
|
readFileSync as readFileSync6,
|
|
5506
|
-
rmSync,
|
|
5507
|
-
statSync as
|
|
5537
|
+
rmSync as rmSync2,
|
|
5538
|
+
statSync as statSync3,
|
|
5508
5539
|
unlinkSync,
|
|
5509
5540
|
writeFileSync as writeFileSync3
|
|
5510
5541
|
} from "node:fs";
|
|
@@ -5614,6 +5645,10 @@ import { dirname as dirname2, join as join2 } from "node:path";
|
|
|
5614
5645
|
// src/daemon/support.ts
|
|
5615
5646
|
import { delimiter } from "node:path";
|
|
5616
5647
|
var HARNESS_KINDS = ["claude_code", "codex", "custom"];
|
|
5648
|
+
var KEEP_WORKSPACES_MODES = ["never", "failed", "always"];
|
|
5649
|
+
function keepWorkspace(mode, outcome) {
|
|
5650
|
+
return mode === "always" || mode === "failed" && outcome === "failed";
|
|
5651
|
+
}
|
|
5617
5652
|
function shellQuote(value) {
|
|
5618
5653
|
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
5619
5654
|
}
|
|
@@ -5642,11 +5677,14 @@ function buildHarnessInvocation(spec, input) {
|
|
|
5642
5677
|
}
|
|
5643
5678
|
}
|
|
5644
5679
|
var RunTable = class {
|
|
5645
|
-
constructor(effects) {
|
|
5680
|
+
constructor(effects, opts = {}) {
|
|
5646
5681
|
this.effects = effects;
|
|
5682
|
+
this.keep = opts.keep ?? (() => false);
|
|
5647
5683
|
}
|
|
5648
5684
|
effects;
|
|
5649
5685
|
runs = /* @__PURE__ */ new Map();
|
|
5686
|
+
/** The daemon's keep decision, as data: `keepWorkspace` bound to its mode. */
|
|
5687
|
+
keep;
|
|
5650
5688
|
get size() {
|
|
5651
5689
|
return this.runs.size;
|
|
5652
5690
|
}
|
|
@@ -5667,11 +5705,19 @@ var RunTable = class {
|
|
|
5667
5705
|
persist() {
|
|
5668
5706
|
this.effects.persist();
|
|
5669
5707
|
}
|
|
5670
|
-
/**
|
|
5671
|
-
|
|
5708
|
+
/**
|
|
5709
|
+
* Removes the run and releases its local traces. Safe to call twice.
|
|
5710
|
+
*
|
|
5711
|
+
* The outcome defaults to `failed` because every route here that is not an
|
|
5712
|
+
* explicit completed finish is a failure: a supervisor cancel (which
|
|
5713
|
+
* reaches cleanup with no status at all), a clone failure on an
|
|
5714
|
+
* already-settled run, a daemon shutdown. Guessing `failed` also errs the
|
|
5715
|
+
* safe way — it keeps a directory rather than destroying evidence.
|
|
5716
|
+
*/
|
|
5717
|
+
cleanup(run, outcome = "failed") {
|
|
5672
5718
|
this.runs.delete(run.runId);
|
|
5673
5719
|
this.effects.persist();
|
|
5674
|
-
this.effects.release(run);
|
|
5720
|
+
this.effects.release(run, { keep: this.keep(outcome), outcome });
|
|
5675
5721
|
}
|
|
5676
5722
|
/**
|
|
5677
5723
|
* Ends a run: flush logs, finish-report, clean up. On a run someone
|
|
@@ -5682,7 +5728,9 @@ var RunTable = class {
|
|
|
5682
5728
|
async finishAndCleanup(run, status, error) {
|
|
5683
5729
|
if (run.settled) return this.cleanup(run);
|
|
5684
5730
|
run.settled = true;
|
|
5731
|
+
run.endNote ??= error;
|
|
5685
5732
|
run.drain?.();
|
|
5733
|
+
if (this.keep(status)) this.effects.noteKept?.(run);
|
|
5686
5734
|
await run.flush?.();
|
|
5687
5735
|
try {
|
|
5688
5736
|
await this.effects.finish(run, status, error);
|
|
@@ -5692,7 +5740,7 @@ var RunTable = class {
|
|
|
5692
5740
|
`finish report for run ${run.runId} not accepted: ${err instanceof Error ? err.message : String(err)}`
|
|
5693
5741
|
);
|
|
5694
5742
|
}
|
|
5695
|
-
this.cleanup(run);
|
|
5743
|
+
this.cleanup(run, status);
|
|
5696
5744
|
}
|
|
5697
5745
|
/**
|
|
5698
5746
|
* Marks a supervisor-settled run (`cancels`): kill, do NOT finish-report.
|
|
@@ -5910,7 +5958,15 @@ function message(err) {
|
|
|
5910
5958
|
}
|
|
5911
5959
|
|
|
5912
5960
|
// src/daemon/store.ts
|
|
5913
|
-
import {
|
|
5961
|
+
import {
|
|
5962
|
+
existsSync as existsSync3,
|
|
5963
|
+
mkdirSync as mkdirSync3,
|
|
5964
|
+
readdirSync as readdirSync2,
|
|
5965
|
+
readFileSync as readFileSync5,
|
|
5966
|
+
rmSync,
|
|
5967
|
+
statSync as statSync2,
|
|
5968
|
+
writeFileSync as writeFileSync2
|
|
5969
|
+
} from "node:fs";
|
|
5914
5970
|
import { homedir } from "node:os";
|
|
5915
5971
|
import { dirname as dirname3, join as join3 } from "node:path";
|
|
5916
5972
|
function defaultConfigDir() {
|
|
@@ -5981,6 +6037,80 @@ function processStartTimeMs(pid) {
|
|
|
5981
6037
|
return null;
|
|
5982
6038
|
}
|
|
5983
6039
|
}
|
|
6040
|
+
function workspacesDir(configDir) {
|
|
6041
|
+
return join3(configDir, "workspaces");
|
|
6042
|
+
}
|
|
6043
|
+
function keptMarkerPath(workspace) {
|
|
6044
|
+
return join3(workspace, "kept.json");
|
|
6045
|
+
}
|
|
6046
|
+
function writeKeptMarker(workspace, marker) {
|
|
6047
|
+
writeJsonFile(keptMarkerPath(workspace), marker);
|
|
6048
|
+
}
|
|
6049
|
+
function readKeptMarker(workspace) {
|
|
6050
|
+
const marker = readJsonFile(keptMarkerPath(workspace));
|
|
6051
|
+
if (!marker || typeof marker.run_id !== "string" || typeof marker.kept_at !== "string") {
|
|
6052
|
+
return null;
|
|
6053
|
+
}
|
|
6054
|
+
return marker;
|
|
6055
|
+
}
|
|
6056
|
+
function listKeptWorkspaces(configDir) {
|
|
6057
|
+
const root = workspacesDir(configDir);
|
|
6058
|
+
let names;
|
|
6059
|
+
try {
|
|
6060
|
+
names = readdirSync2(root, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
6061
|
+
} catch {
|
|
6062
|
+
return [];
|
|
6063
|
+
}
|
|
6064
|
+
const kept = [];
|
|
6065
|
+
for (const name2 of names) {
|
|
6066
|
+
const path2 = join3(root, name2);
|
|
6067
|
+
const marker = readKeptMarker(path2);
|
|
6068
|
+
if (marker) kept.push({ ...marker, path: path2 });
|
|
6069
|
+
}
|
|
6070
|
+
return kept.sort((a, b) => Date.parse(b.kept_at) - Date.parse(a.kept_at));
|
|
6071
|
+
}
|
|
6072
|
+
function pruneKeptWorkspaces(configDir, opts) {
|
|
6073
|
+
const kept = listKeptWorkspaces(configDir);
|
|
6074
|
+
const now = (opts.now ?? Date.now)();
|
|
6075
|
+
const doomed = [];
|
|
6076
|
+
const survivors = [];
|
|
6077
|
+
for (const entry of kept) {
|
|
6078
|
+
const age = now - Date.parse(entry.kept_at);
|
|
6079
|
+
const expired = opts.maxAgeMs !== void 0 && Number.isFinite(age) && age > opts.maxAgeMs;
|
|
6080
|
+
if (opts.all || expired) doomed.push(entry);
|
|
6081
|
+
else survivors.push(entry);
|
|
6082
|
+
}
|
|
6083
|
+
if (!opts.all && opts.maxCount !== void 0) doomed.push(...survivors.slice(opts.maxCount));
|
|
6084
|
+
const removed = [];
|
|
6085
|
+
for (const entry of doomed) {
|
|
6086
|
+
try {
|
|
6087
|
+
rmSync(entry.path, { recursive: true, force: true });
|
|
6088
|
+
removed.push(entry);
|
|
6089
|
+
} catch {
|
|
6090
|
+
}
|
|
6091
|
+
}
|
|
6092
|
+
return removed;
|
|
6093
|
+
}
|
|
6094
|
+
function directorySizeBytes(path2) {
|
|
6095
|
+
let total = 0;
|
|
6096
|
+
let entries;
|
|
6097
|
+
try {
|
|
6098
|
+
entries = readdirSync2(path2, { withFileTypes: true });
|
|
6099
|
+
} catch {
|
|
6100
|
+
return 0;
|
|
6101
|
+
}
|
|
6102
|
+
for (const entry of entries) {
|
|
6103
|
+
const child = join3(path2, entry.name);
|
|
6104
|
+
if (entry.isDirectory()) total += directorySizeBytes(child);
|
|
6105
|
+
else if (entry.isFile()) {
|
|
6106
|
+
try {
|
|
6107
|
+
total += statSync2(child).size;
|
|
6108
|
+
} catch {
|
|
6109
|
+
}
|
|
6110
|
+
}
|
|
6111
|
+
}
|
|
6112
|
+
return total;
|
|
6113
|
+
}
|
|
5984
6114
|
|
|
5985
6115
|
// src/daemon/daemon.ts
|
|
5986
6116
|
var CLI_REFRESH_TTL_MS = 10 * 6e4;
|
|
@@ -5990,7 +6120,7 @@ async function uploadRawLog(run) {
|
|
|
5990
6120
|
run.rawSpoolPath = void 0;
|
|
5991
6121
|
try {
|
|
5992
6122
|
await new Promise((resolve) => run.rawSpool?.end(resolve) ?? resolve());
|
|
5993
|
-
const size =
|
|
6123
|
+
const size = statSync3(path2).size;
|
|
5994
6124
|
if (size > 0) {
|
|
5995
6125
|
let body = readFileSync6(path2);
|
|
5996
6126
|
if (body.byteLength > RUN_LOG_RAW_MAX_BYTES) {
|
|
@@ -6032,6 +6162,34 @@ function pidAlive(pid) {
|
|
|
6032
6162
|
}
|
|
6033
6163
|
async function runDaemon(opts) {
|
|
6034
6164
|
mkdirSync4(opts.configDir, { recursive: true });
|
|
6165
|
+
mkdirSync4(workspacesDir(opts.configDir), { recursive: true, mode: 448 });
|
|
6166
|
+
const sweepKeptWorkspaces = () => {
|
|
6167
|
+
const removed = pruneKeptWorkspaces(opts.configDir, {
|
|
6168
|
+
maxAgeMs: opts.keepWorkspacesForHours * 36e5,
|
|
6169
|
+
maxCount: opts.keepWorkspacesMax
|
|
6170
|
+
});
|
|
6171
|
+
if (removed.length > 0) {
|
|
6172
|
+
log(
|
|
6173
|
+
`pruned ${removed.length} kept workspace(s) past retention (${opts.keepWorkspacesForHours}h, max ${opts.keepWorkspacesMax})`
|
|
6174
|
+
);
|
|
6175
|
+
}
|
|
6176
|
+
};
|
|
6177
|
+
sweepKeptWorkspaces();
|
|
6178
|
+
const settleWorkspace = (workspace, keep, marker) => {
|
|
6179
|
+
if (!keep) {
|
|
6180
|
+
rmSync2(workspace, { recursive: true, force: true });
|
|
6181
|
+
return;
|
|
6182
|
+
}
|
|
6183
|
+
if (!existsSync4(workspace)) return;
|
|
6184
|
+
try {
|
|
6185
|
+
writeKeptMarker(workspace, { ...marker, kept_at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
6186
|
+
log(`run ${marker.run_id}: workspace kept at ${workspace}`);
|
|
6187
|
+
} catch (err) {
|
|
6188
|
+
log(
|
|
6189
|
+
`run ${marker.run_id}: workspace kept at ${workspace}, but kept.json could not be written (${message2(err)})`
|
|
6190
|
+
);
|
|
6191
|
+
}
|
|
6192
|
+
};
|
|
6035
6193
|
let creds = loadRunnerCredentials(opts.configDir, opts.url, opts.name);
|
|
6036
6194
|
if (creds) {
|
|
6037
6195
|
log(`reconnecting as runner "${opts.name}" (${creds.runner_id}) \u2014 token from ${opts.configDir}`);
|
|
@@ -6067,24 +6225,33 @@ async function runDaemon(opts) {
|
|
|
6067
6225
|
finish: async (run, status, error) => {
|
|
6068
6226
|
await client2.finishRun(run.runId, { status, ...error ? { error } : {} });
|
|
6069
6227
|
},
|
|
6070
|
-
release: (run) => {
|
|
6228
|
+
release: (run, { keep, outcome }) => {
|
|
6071
6229
|
if (run.timeout) clearTimeout(run.timeout);
|
|
6072
6230
|
run.renderer?.finish();
|
|
6073
|
-
|
|
6231
|
+
settleWorkspace(run.workspace, keep, {
|
|
6232
|
+
run_id: run.runId,
|
|
6233
|
+
...run.issueLabel ? { issue_ref: run.issueLabel } : {},
|
|
6234
|
+
status: outcome,
|
|
6235
|
+
...run.endNote ? { error: run.endNote } : {}
|
|
6236
|
+
});
|
|
6074
6237
|
void uploadRawLog(run);
|
|
6238
|
+
sweepKeptWorkspaces();
|
|
6075
6239
|
},
|
|
6240
|
+
noteKept: (run) => run.batcher.append(`workspace kept at ${run.workspace}
|
|
6241
|
+
`),
|
|
6076
6242
|
persist: () => {
|
|
6077
6243
|
const entries = table2.values().filter((run) => run.child?.pid !== void 0).map((run) => ({
|
|
6078
6244
|
run_id: run.runId,
|
|
6079
6245
|
pid: run.child.pid,
|
|
6080
6246
|
workspace: run.workspace,
|
|
6081
6247
|
key_fingerprint: run.keyFingerprint,
|
|
6082
|
-
started_at: run.spawnedAt
|
|
6248
|
+
started_at: run.spawnedAt,
|
|
6249
|
+
...run.issueLabel ? { issue_ref: run.issueLabel } : {}
|
|
6083
6250
|
}));
|
|
6084
6251
|
saveDaemonState(statePath, entries);
|
|
6085
6252
|
},
|
|
6086
6253
|
log
|
|
6087
|
-
});
|
|
6254
|
+
}, { keep: (outcome) => keepWorkspace(opts.keepWorkspaces, outcome) });
|
|
6088
6255
|
for (const orphan of loadDaemonState(statePath)) {
|
|
6089
6256
|
if (pidAlive(orphan.pid)) {
|
|
6090
6257
|
const processStart = processStartTimeMs(orphan.pid);
|
|
@@ -6103,12 +6270,18 @@ async function runDaemon(opts) {
|
|
|
6103
6270
|
});
|
|
6104
6271
|
} catch {
|
|
6105
6272
|
}
|
|
6106
|
-
|
|
6273
|
+
settleWorkspace(orphan.workspace, keepWorkspace(opts.keepWorkspaces, "failed"), {
|
|
6274
|
+
run_id: orphan.run_id,
|
|
6275
|
+
...orphan.issue_ref ? { issue_ref: orphan.issue_ref } : {},
|
|
6276
|
+
status: "failed",
|
|
6277
|
+
error: "daemon restarted; orphaned harness killed"
|
|
6278
|
+
});
|
|
6107
6279
|
}
|
|
6108
6280
|
saveDaemonState(statePath, []);
|
|
6109
6281
|
const killWithoutFinish = (runId) => {
|
|
6110
6282
|
const run = table2.markCanceled(runId);
|
|
6111
6283
|
if (!run) return;
|
|
6284
|
+
run.endNote ??= "canceled by supervisor";
|
|
6112
6285
|
log(`supervisor canceled run ${runId}; killing without finish-reporting`);
|
|
6113
6286
|
if (run.child?.pid) {
|
|
6114
6287
|
const pid = run.child.pid;
|
|
@@ -6119,10 +6292,12 @@ async function runDaemon(opts) {
|
|
|
6119
6292
|
const launch = async (assignment) => {
|
|
6120
6293
|
const runId = assignment.run.id;
|
|
6121
6294
|
if (table2.has(runId)) return;
|
|
6122
|
-
const workspace = join4(opts.configDir,
|
|
6295
|
+
const workspace = join4(workspacesDir(opts.configDir), runId);
|
|
6296
|
+
const issueLabel = assignment.run.issue_ref ? `${assignment.run.issue_ref.project_name}/${assignment.run.issue_ref.number}` : void 0;
|
|
6123
6297
|
const run = {
|
|
6124
6298
|
runId,
|
|
6125
6299
|
workspace,
|
|
6300
|
+
...issueLabel ? { issueLabel } : {},
|
|
6126
6301
|
canceled: false,
|
|
6127
6302
|
timedOut: false,
|
|
6128
6303
|
settled: false,
|
|
@@ -6135,9 +6310,9 @@ async function runDaemon(opts) {
|
|
|
6135
6310
|
};
|
|
6136
6311
|
run.flush = () => run.batcher.flush();
|
|
6137
6312
|
table2.track(run);
|
|
6138
|
-
log(`run ${runId} assigned (issue ${
|
|
6313
|
+
log(`run ${runId} assigned (issue ${issueLabel ?? assignment.run.issue_id}); materializing workspace`);
|
|
6139
6314
|
try {
|
|
6140
|
-
|
|
6315
|
+
rmSync2(workspace, { recursive: true, force: true });
|
|
6141
6316
|
mkdirSync4(workspace, { recursive: true });
|
|
6142
6317
|
writeFileSync3(join4(workspace, "prompt.md"), `${assignment.prompt}
|
|
6143
6318
|
`);
|
|
@@ -6284,7 +6459,12 @@ async function runDaemon(opts) {
|
|
|
6284
6459
|
if (err instanceof ApiError && err.status === 401) {
|
|
6285
6460
|
for (const run of table2.values()) {
|
|
6286
6461
|
if (run.child?.pid) killTree(run.child.pid, "SIGKILL");
|
|
6287
|
-
|
|
6462
|
+
settleWorkspace(run.workspace, keepWorkspace(opts.keepWorkspaces, "failed"), {
|
|
6463
|
+
run_id: run.runId,
|
|
6464
|
+
...run.issueLabel ? { issue_ref: run.issueLabel } : {},
|
|
6465
|
+
status: "failed",
|
|
6466
|
+
error: "daemon token rejected"
|
|
6467
|
+
});
|
|
6288
6468
|
}
|
|
6289
6469
|
saveDaemonState(statePath, []);
|
|
6290
6470
|
clearRunnerCredentials(opts.configDir, opts.url, opts.name);
|
|
@@ -6522,6 +6702,20 @@ function register6(program3) {
|
|
|
6522
6702
|
runnerCmd.command("daemon").description("Run the local runner daemon: register/reconnect, poll for assigned runs, execute them").option("--name <name>", "runner name, unique per user (default: this hostname)").option("--harness <harness>", "claude-code | codex | custom", "claude-code").option("--command <template>", "custom harness command template ({prompt_file}, {workspace}, {model})").option("--max-concurrent <n>", "maximum simultaneous runs", (v) => Number.parseInt(v, 10), 1).option("--poll-interval <seconds>", "seconds between polls", (v) => Number.parseInt(v, 10), 15).option(
|
|
6523
6703
|
"--no-cli-refresh",
|
|
6524
6704
|
"do not install/refresh the agent-facing tines CLI from npm (harnesses use the ambient PATH)"
|
|
6705
|
+
).option(
|
|
6706
|
+
"--keep-workspaces <mode>",
|
|
6707
|
+
"keep settled runs' workspaces for debugging: never | failed | always",
|
|
6708
|
+
"never"
|
|
6709
|
+
).option(
|
|
6710
|
+
"--keep-workspaces-for <hours>",
|
|
6711
|
+
"delete kept workspaces older than this",
|
|
6712
|
+
(v) => Number(v),
|
|
6713
|
+
72
|
|
6714
|
+
).option(
|
|
6715
|
+
"--keep-workspaces-max <n>",
|
|
6716
|
+
"keep at most this many workspaces (oldest removed first)",
|
|
6717
|
+
(v) => Number.parseInt(v, 10),
|
|
6718
|
+
20
|
|
6525
6719
|
)
|
|
6526
6720
|
).action(
|
|
6527
6721
|
async (opts) => {
|
|
@@ -6539,6 +6733,16 @@ function register6(program3) {
|
|
|
6539
6733
|
if (!Number.isInteger(opts.pollInterval) || opts.pollInterval < 1) {
|
|
6540
6734
|
die("--poll-interval must be a positive number of seconds");
|
|
6541
6735
|
}
|
|
6736
|
+
const keepWorkspaces = opts.keepWorkspaces;
|
|
6737
|
+
if (!KEEP_WORKSPACES_MODES.includes(keepWorkspaces)) {
|
|
6738
|
+
die(`--keep-workspaces must be ${KEEP_WORKSPACES_MODES.join(", ")}, got "${opts.keepWorkspaces}"`);
|
|
6739
|
+
}
|
|
6740
|
+
if (!Number.isFinite(opts.keepWorkspacesFor) || opts.keepWorkspacesFor <= 0) {
|
|
6741
|
+
die("--keep-workspaces-for must be a positive number of hours");
|
|
6742
|
+
}
|
|
6743
|
+
if (!Number.isInteger(opts.keepWorkspacesMax) || opts.keepWorkspacesMax < 1) {
|
|
6744
|
+
die("--keep-workspaces-max must be a positive integer");
|
|
6745
|
+
}
|
|
6542
6746
|
await runDaemon({
|
|
6543
6747
|
url: resolveUrl(opts).replace(/\/+$/, ""),
|
|
6544
6748
|
apiKey: resolveApiKey(opts),
|
|
@@ -6548,10 +6752,45 @@ function register6(program3) {
|
|
|
6548
6752
|
maxConcurrent: opts.maxConcurrent,
|
|
6549
6753
|
pollIntervalMs: opts.pollInterval * 1e3,
|
|
6550
6754
|
configDir: defaultConfigDir(),
|
|
6551
|
-
cliRefresh: opts.cliRefresh
|
|
6755
|
+
cliRefresh: opts.cliRefresh,
|
|
6756
|
+
keepWorkspaces,
|
|
6757
|
+
keepWorkspacesForHours: opts.keepWorkspacesFor,
|
|
6758
|
+
keepWorkspacesMax: opts.keepWorkspacesMax
|
|
6552
6759
|
});
|
|
6553
6760
|
}
|
|
6554
6761
|
);
|
|
6762
|
+
const workspacesCmd = runnerCmd.command("workspaces").description("List run workspaces the daemon kept for debugging (--keep-workspaces)").option("--json", "print JSON instead of a table").action((opts) => {
|
|
6763
|
+
const configDir = defaultConfigDir();
|
|
6764
|
+
const kept = listKeptWorkspaces(configDir);
|
|
6765
|
+
const sized = kept.map((k) => ({ ...k, size_bytes: directorySizeBytes(k.path) }));
|
|
6766
|
+
if (opts.json) return printJson({ items: sized });
|
|
6767
|
+
if (sized.length === 0) {
|
|
6768
|
+
console.log(`no kept workspaces in ${workspacesDir(configDir)}`);
|
|
6769
|
+
return console.log(
|
|
6770
|
+
"the daemon keeps them only with --keep-workspaces failed (or always)."
|
|
6771
|
+
);
|
|
6772
|
+
}
|
|
6773
|
+
table([
|
|
6774
|
+
["RUN", "ISSUE", "STATUS", "AGE", "SIZE", "PATH"],
|
|
6775
|
+
...sized.map((k) => keptWorkspaceRow(k, k.size_bytes))
|
|
6776
|
+
]);
|
|
6777
|
+
});
|
|
6778
|
+
workspacesCmd.command("prune").description("Delete kept workspaces (never touches a live run's workspace)").option("--all", "delete every kept workspace").option("--older-than <hours>", "delete kept workspaces older than this", (v) => Number(v)).option("--json", "print JSON instead of a table").action((opts) => {
|
|
6779
|
+
if (opts.all === void 0 && opts.olderThan === void 0) {
|
|
6780
|
+
die("pass --all or --older-than <hours>");
|
|
6781
|
+
}
|
|
6782
|
+
if (opts.all && opts.olderThan !== void 0) die("--all cannot be combined with --older-than");
|
|
6783
|
+
if (opts.olderThan !== void 0 && (!Number.isFinite(opts.olderThan) || opts.olderThan < 0)) {
|
|
6784
|
+
die("--older-than must be a non-negative number of hours");
|
|
6785
|
+
}
|
|
6786
|
+
const removed = pruneKeptWorkspaces(defaultConfigDir(), {
|
|
6787
|
+
all: opts.all,
|
|
6788
|
+
...opts.olderThan !== void 0 ? { maxAgeMs: opts.olderThan * 36e5 } : {}
|
|
6789
|
+
});
|
|
6790
|
+
if (opts.json) return printJson({ items: removed });
|
|
6791
|
+
for (const entry of removed) console.log(`removed ${entry.path}`);
|
|
6792
|
+
console.log(`pruned ${removed.length} kept workspace${removed.length === 1 ? "" : "s"}`);
|
|
6793
|
+
});
|
|
6555
6794
|
const runsCmd = program3.command("runs").description("Agent runs: attempts at issues by runners");
|
|
6556
6795
|
withList(
|
|
6557
6796
|
runsCmd.command("list").description("List runs, newest first").option("-i, --issue <ref>", "filter to one issue (<project>/<number>)").option("-r, --runner <name>", "filter by runner name").option("--active", "only runs holding a claim (assigned/launching/running)")
|