@tablation/crew 0.53.1 → 0.53.3
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/cli.js +189 -78
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -32,7 +32,7 @@ function doctorBaseReport(r, base, health) {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
// src/cli.ts
|
|
35
|
-
import { resolve as
|
|
35
|
+
import { resolve as resolve5, dirname as dirname13, join as join26, basename as basename9 } from "path";
|
|
36
36
|
import { fileURLToPath } from "url";
|
|
37
37
|
import { hostname as hostname2, homedir as homedir8 } from "os";
|
|
38
38
|
import { spawnSync as spawnSync6 } from "child_process";
|
|
@@ -8767,7 +8767,7 @@ function spawnAgent(plan, emit) {
|
|
|
8767
8767
|
for (const w of plan.warnings ?? []) emit.warn(w, { step: "agent", role: plan.role });
|
|
8768
8768
|
const eventsSink = openSink(plan.eventsPath);
|
|
8769
8769
|
const id = identityOf(plan, adapter.harness);
|
|
8770
|
-
return new Promise((
|
|
8770
|
+
return new Promise((resolve6, reject) => {
|
|
8771
8771
|
let streamFd;
|
|
8772
8772
|
try {
|
|
8773
8773
|
mkdirSync6(dirname6(plan.streamPath), { recursive: true });
|
|
@@ -8826,7 +8826,7 @@ function spawnAgent(plan, emit) {
|
|
|
8826
8826
|
await endSink(eventsSink);
|
|
8827
8827
|
if (spawnFailed) return;
|
|
8828
8828
|
const { ms: ms2 } = await finishRun(id, obs, emit, startedAt, { code: code ?? 1 }, plan.agentLog?.apiKey);
|
|
8829
|
-
|
|
8829
|
+
resolve6({ code: code ?? 1, ms: ms2 });
|
|
8830
8830
|
});
|
|
8831
8831
|
child.stdin?.on("error", (err) => {
|
|
8832
8832
|
emit.warn(`agent closed stdin before the prompt was delivered: ${err.message}`, { step: "agent" });
|
|
@@ -9134,6 +9134,17 @@ ${shipName2 ? `Ship: ${shipName2}
|
|
|
9134
9134
|
|
|
9135
9135
|
${v.reason[0].toUpperCase()}${v.reason.slice(1)}. Set back to \`in_progress\`, still held by this ship; push with a plain \`git push --set-upstream ${v.remote} <branch>\` (never \`--force\`) and set \`fixed\` again.`;
|
|
9136
9136
|
}
|
|
9137
|
+
async function takenOverSince(i, ticketRow) {
|
|
9138
|
+
if (!i.since || !i.writer.events) return false;
|
|
9139
|
+
const from = Date.parse(i.since);
|
|
9140
|
+
const evs = await i.writer.events("taken_over");
|
|
9141
|
+
return evs.some((e) => {
|
|
9142
|
+
if (e.record_id !== ticketRow) return false;
|
|
9143
|
+
if (Date.parse(e.occurred_at ?? e.created_at ?? "") < from) return false;
|
|
9144
|
+
const by = / by (.+?); /.exec(e.message ?? "")?.[1];
|
|
9145
|
+
return !!by && by !== i.shipName;
|
|
9146
|
+
});
|
|
9147
|
+
}
|
|
9137
9148
|
async function enforceHandoff(i) {
|
|
9138
9149
|
try {
|
|
9139
9150
|
const t = await i.writer.ticket(i.ticketId);
|
|
@@ -9159,6 +9170,18 @@ async function enforceHandoff(i) {
|
|
|
9159
9170
|
return { kind: "failed", why: v.reason };
|
|
9160
9171
|
}
|
|
9161
9172
|
const heldBy = i.hold ? t[i.hold.column] : void 0;
|
|
9173
|
+
if (i.hold && heldBy && heldBy !== i.hold.shipId) {
|
|
9174
|
+
i.log.warn(`${t.issue_id} is held by another ship now \u2014 this run's hand-off check is stale, left alone`, log);
|
|
9175
|
+
return { kind: "unverified", why: "ticket is held by another ship" };
|
|
9176
|
+
}
|
|
9177
|
+
if (await takenOverSince(i, t.id)) {
|
|
9178
|
+
i.log.warn(`${t.issue_id} was taken over by another ship during this run \u2014 left alone`, log);
|
|
9179
|
+
return { kind: "unverified", why: "ticket was taken over during this run" };
|
|
9180
|
+
}
|
|
9181
|
+
if (!v.local) {
|
|
9182
|
+
i.log.warn(`${t.issue_id} has no local branch on this ship, so it is not this ship's hand-off to bounce: ${v.reason}`, log);
|
|
9183
|
+
return { kind: "unverified", why: "no local branch on this ship" };
|
|
9184
|
+
}
|
|
9162
9185
|
await i.writer.updateTicket(t.id, {
|
|
9163
9186
|
status: i.contract.statuses.building,
|
|
9164
9187
|
...t.assignee_id ? {} : { assignee_id: i.seatId },
|
|
@@ -9198,6 +9221,7 @@ async function guardRunHandoff(route, ship, tracker, role, workingId, log, since
|
|
|
9198
9221
|
seatId,
|
|
9199
9222
|
shipName: ship.name,
|
|
9200
9223
|
hold: affinity ? { column: affinity.column, shipId: affinity.shipId } : void 0,
|
|
9224
|
+
since: since2,
|
|
9201
9225
|
log
|
|
9202
9226
|
});
|
|
9203
9227
|
if (outcome.kind === "pushed") await retireRunWorktree(tracker, target.dir, cfg, t, role, seatId, ship.name, log);
|
|
@@ -12158,17 +12182,15 @@ function schedulerInstalled(plan, exists, crontab) {
|
|
|
12158
12182
|
}
|
|
12159
12183
|
|
|
12160
12184
|
// src/worktree-sweep.ts
|
|
12161
|
-
import { existsSync as existsSync19 } from "fs";
|
|
12162
|
-
import { basename as basename8 } from "path";
|
|
12185
|
+
import { existsSync as existsSync19, readFileSync as readFileSync14, readdirSync as readdirSync5 } from "fs";
|
|
12186
|
+
import { basename as basename8, dirname as dirname11, join as join19, resolve as resolve4 } from "path";
|
|
12163
12187
|
function planWorktreeSweep(target, tickets, contract) {
|
|
12164
12188
|
const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
|
|
12165
12189
|
const verified = contract.statuses.verified;
|
|
12166
12190
|
const byNumber = new Map(tickets.map((t) => [t.issue_id.replace(/^\D+/, ""), t]));
|
|
12167
12191
|
const actions = [];
|
|
12168
|
-
for (const w of worktrees(target.dir)) {
|
|
12169
|
-
const
|
|
12170
|
-
if (!m) continue;
|
|
12171
|
-
const ticket = byNumber.get(m[1]);
|
|
12192
|
+
for (const w of worktrees(target.dir).slice(1)) {
|
|
12193
|
+
const ticket = ticketForWorktree(w, byNumber, contract);
|
|
12172
12194
|
if (!ticket) continue;
|
|
12173
12195
|
const isVerified = ticket.status === verified;
|
|
12174
12196
|
if (!isVerified && !terminal.has(ticket.status)) continue;
|
|
@@ -12182,18 +12204,80 @@ function planWorktreeSweep(target, tickets, contract) {
|
|
|
12182
12204
|
}
|
|
12183
12205
|
return actions;
|
|
12184
12206
|
}
|
|
12207
|
+
var DIR_NUMBER = /(?:^|-)(\d+)$/;
|
|
12208
|
+
var DIR_NUMBER_SUFFIXED = /(?:^|-)(\d+)(?:-[a-z0-9]{1,8})?$/;
|
|
12209
|
+
var BRANCH_STEM = /^(?:.*\/)?([^/]+?-\d+)(?:-.*)?$/;
|
|
12210
|
+
function ticketForWorktree(w, byNumber, contract) {
|
|
12211
|
+
if (w.branch) {
|
|
12212
|
+
const stem = BRANCH_STEM.exec(w.branch)?.[1];
|
|
12213
|
+
const num = stem && /-(\d+)$/.exec(stem)?.[1];
|
|
12214
|
+
const t = num ? byNumber.get(num) : void 0;
|
|
12215
|
+
if (t && ticketStems(t).has(stem)) return t;
|
|
12216
|
+
}
|
|
12217
|
+
const dir = basename8(w.path);
|
|
12218
|
+
const m = DIR_NUMBER.exec(dir);
|
|
12219
|
+
if (m && byNumber.has(m[1])) return byNumber.get(m[1]);
|
|
12220
|
+
if (!w.branch) {
|
|
12221
|
+
const t = byNumber.get(DIR_NUMBER_SUFFIXED.exec(dir)?.[1] ?? "");
|
|
12222
|
+
if (t && t.status === contract.statuses.deployed) return t;
|
|
12223
|
+
}
|
|
12224
|
+
return void 0;
|
|
12225
|
+
}
|
|
12185
12226
|
function worktreeTicketNumbers(target) {
|
|
12186
12227
|
const nums = /* @__PURE__ */ new Set();
|
|
12187
|
-
for (const w of worktrees(target.dir)) {
|
|
12188
|
-
|
|
12228
|
+
for (const w of worktrees(target.dir).slice(1)) {
|
|
12229
|
+
if (w.branch) {
|
|
12230
|
+
const n = /-(\d+)$/.exec(BRANCH_STEM.exec(w.branch)?.[1] ?? "")?.[1];
|
|
12231
|
+
if (n) nums.add(n);
|
|
12232
|
+
}
|
|
12233
|
+
const m = DIR_NUMBER_SUFFIXED.exec(basename8(w.path));
|
|
12189
12234
|
if (m) nums.add(m[1]);
|
|
12190
12235
|
}
|
|
12236
|
+
for (const d of siblingWorktreeDirs(target)) nums.add(d.number);
|
|
12191
12237
|
return [...nums];
|
|
12192
12238
|
}
|
|
12193
|
-
|
|
12239
|
+
function siblingWorktreeDirs(target) {
|
|
12240
|
+
const parent = dirname11(target.dir.replace(/[/\\]+$/, ""));
|
|
12241
|
+
const stem = `${basename8(target.dir.replace(/[/\\]+$/, ""))}-`;
|
|
12242
|
+
const registered = new Set(worktrees(target.dir).map((w) => resolve4(w.path)));
|
|
12243
|
+
let names;
|
|
12244
|
+
try {
|
|
12245
|
+
names = readdirSync5(parent);
|
|
12246
|
+
} catch {
|
|
12247
|
+
return [];
|
|
12248
|
+
}
|
|
12249
|
+
const out = [];
|
|
12250
|
+
for (const name of names) {
|
|
12251
|
+
if (!name.startsWith(stem)) continue;
|
|
12252
|
+
const m = DIR_NUMBER_SUFFIXED.exec(name);
|
|
12253
|
+
const path = join19(parent, name);
|
|
12254
|
+
if (m && !registered.has(resolve4(path))) out.push({ path, number: m[1] });
|
|
12255
|
+
}
|
|
12256
|
+
return out;
|
|
12257
|
+
}
|
|
12258
|
+
function orphanedWorktreeDirs(target, tickets, contract) {
|
|
12259
|
+
const deployed = new Set(
|
|
12260
|
+
tickets.filter((t) => t.status === contract.statuses.deployed).map((t) => t.issue_id.replace(/^\D+/, ""))
|
|
12261
|
+
);
|
|
12262
|
+
return siblingWorktreeDirs(target).filter((d) => deployed.has(d.number)).map((d) => d.path);
|
|
12263
|
+
}
|
|
12264
|
+
function warnOrphanedWorktreeDirs(paths, log) {
|
|
12265
|
+
for (const p of paths) {
|
|
12266
|
+
log.warn(`orphaned directory ${p}: its ticket is deployed but it is not a registered worktree; the sweep will not delete it \u2014 remove it by hand`, { step: "worktree" });
|
|
12267
|
+
}
|
|
12268
|
+
}
|
|
12269
|
+
async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin", base) {
|
|
12194
12270
|
let removed = 0;
|
|
12195
12271
|
let keptBranches = 0;
|
|
12196
12272
|
for (const a of actions) {
|
|
12273
|
+
if (!dryRun && base && !a.keepBranch && existsSync19(a.path) && isHandBackMerge(a.path, base)) {
|
|
12274
|
+
if (gitOk(a.path, ["merge", "--abort"]) !== null) {
|
|
12275
|
+
log.emit(
|
|
12276
|
+
`abandoned the hand-back merge of ${base} in ${a.path}: ${a.ticket.issue_id} deployed without it`,
|
|
12277
|
+
{ ticket: a.ticket.issue_id, step: "worktree" }
|
|
12278
|
+
);
|
|
12279
|
+
}
|
|
12280
|
+
}
|
|
12197
12281
|
if (!dryRun && existsSync19(a.path)) {
|
|
12198
12282
|
let dirty = false;
|
|
12199
12283
|
try {
|
|
@@ -12233,7 +12317,12 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin")
|
|
|
12233
12317
|
);
|
|
12234
12318
|
continue;
|
|
12235
12319
|
}
|
|
12236
|
-
|
|
12320
|
+
try {
|
|
12321
|
+
git(cwd, ["worktree", "remove", "--force", a.path]);
|
|
12322
|
+
} catch (e) {
|
|
12323
|
+
log.warn(`could not remove worktree ${a.path} for ${a.ticket.issue_id}: ${e.message}`, { ticket: a.ticket.issue_id, step: "worktree" });
|
|
12324
|
+
continue;
|
|
12325
|
+
}
|
|
12237
12326
|
if (!a.keepBranch && a.branch) {
|
|
12238
12327
|
gitOk(cwd, ["branch", "-D", a.branch]);
|
|
12239
12328
|
if (remoteConfigured(cwd, remote) && !deleteRemoteBranch(cwd, remote, a.branch)) {
|
|
@@ -12246,8 +12335,22 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin")
|
|
|
12246
12335
|
);
|
|
12247
12336
|
removed++;
|
|
12248
12337
|
}
|
|
12338
|
+
if (!dryRun && actions.length) gitOk(cwd, ["worktree", "prune"]);
|
|
12249
12339
|
return { removed, keptBranches };
|
|
12250
12340
|
}
|
|
12341
|
+
function isHandBackMerge(path, base) {
|
|
12342
|
+
if (gitOk(path, ["rev-parse", "-q", "--verify", "MERGE_HEAD"]) === null) return false;
|
|
12343
|
+
const msgPath = gitOk(path, ["rev-parse", "--git-path", "MERGE_MSG"]);
|
|
12344
|
+
if (!msgPath) return false;
|
|
12345
|
+
let first;
|
|
12346
|
+
try {
|
|
12347
|
+
first = readFileSync14(resolve4(path, msgPath), "utf8").split("\n")[0] ?? "";
|
|
12348
|
+
} catch {
|
|
12349
|
+
return false;
|
|
12350
|
+
}
|
|
12351
|
+
const escaped = base.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
12352
|
+
return new RegExp(`^Merge (?:remote-tracking )?branch '(?:[^']+/)?${escaped}'`).test(first);
|
|
12353
|
+
}
|
|
12251
12354
|
function remoteBranchTicketNumbers(cwd, remote, base) {
|
|
12252
12355
|
if (!remoteConfigured(cwd, remote)) return [];
|
|
12253
12356
|
const prefix = `refs/remotes/${remote}/`;
|
|
@@ -12273,13 +12376,18 @@ function deployedBranchStems(tickets, contract) {
|
|
|
12273
12376
|
const stems = /* @__PURE__ */ new Set();
|
|
12274
12377
|
for (const t of tickets) {
|
|
12275
12378
|
if (t.status !== contract.statuses.deployed) continue;
|
|
12276
|
-
const
|
|
12277
|
-
|
|
12278
|
-
|
|
12279
|
-
|
|
12280
|
-
|
|
12281
|
-
|
|
12282
|
-
|
|
12379
|
+
for (const st of ticketStems(t)) stems.add(st);
|
|
12380
|
+
}
|
|
12381
|
+
return stems;
|
|
12382
|
+
}
|
|
12383
|
+
function ticketStems(t) {
|
|
12384
|
+
const stems = /* @__PURE__ */ new Set();
|
|
12385
|
+
const num = t.issue_id.replace(/^\D+/, "");
|
|
12386
|
+
const unpadded = String(Number.parseInt(num, 10));
|
|
12387
|
+
const prefixes = ["issue", t.project_issue_prefix?.toLowerCase()].filter((p) => !!p);
|
|
12388
|
+
for (const p of prefixes) {
|
|
12389
|
+
stems.add(`${p}-${num}`);
|
|
12390
|
+
stems.add(`${p}-${unpadded}`);
|
|
12283
12391
|
}
|
|
12284
12392
|
return stems;
|
|
12285
12393
|
}
|
|
@@ -12290,7 +12398,8 @@ function isTicketBranch(name, base, stems) {
|
|
|
12290
12398
|
}
|
|
12291
12399
|
function sweepCandidatesExist(cwd, base) {
|
|
12292
12400
|
const linked = worktrees(cwd).slice(1);
|
|
12293
|
-
if (linked.some((w) =>
|
|
12401
|
+
if (linked.some((w) => DIR_NUMBER_SUFFIXED.test(basename8(w.path)) || w.branch && BRANCH_STEM.test(w.branch))) return true;
|
|
12402
|
+
if (siblingWorktreeDirs({ name: "", dir: cwd }).length) return true;
|
|
12294
12403
|
return localBranches(cwd).some((b) => b !== base && /-\d+(?:-.*)?$/.test(b));
|
|
12295
12404
|
}
|
|
12296
12405
|
function localBranchTicketNumbers(cwd, base) {
|
|
@@ -12340,14 +12449,14 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
|
|
|
12340
12449
|
}
|
|
12341
12450
|
|
|
12342
12451
|
// src/stream-sweep.ts
|
|
12343
|
-
import { existsSync as existsSync20, readdirSync as
|
|
12344
|
-
import { join as
|
|
12452
|
+
import { existsSync as existsSync20, readdirSync as readdirSync6, rmSync as rmSync8, statSync as statSync7 } from "fs";
|
|
12453
|
+
import { join as join20 } from "path";
|
|
12345
12454
|
var DAY_MS2 = 24 * 60 * 60 * 1e3;
|
|
12346
12455
|
function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
|
|
12347
|
-
const dir =
|
|
12456
|
+
const dir = join20(stateDir, "streams");
|
|
12348
12457
|
if (!existsSync20(dir)) return [];
|
|
12349
12458
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
12350
|
-
return
|
|
12459
|
+
return readdirSync6(dir).map((name) => join20(dir, name)).filter((path) => {
|
|
12351
12460
|
try {
|
|
12352
12461
|
return statSync7(path).mtimeMs < cutoff;
|
|
12353
12462
|
} catch {
|
|
@@ -12375,7 +12484,7 @@ function applyStreamSweep(paths, dryRun, log) {
|
|
|
12375
12484
|
function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
12376
12485
|
if (!existsSync20(stateDir)) return [];
|
|
12377
12486
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
12378
|
-
return
|
|
12487
|
+
return readdirSync6(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join20(stateDir, name)).filter((path) => {
|
|
12379
12488
|
try {
|
|
12380
12489
|
return statSync7(path).mtimeMs < cutoff;
|
|
12381
12490
|
} catch {
|
|
@@ -12385,24 +12494,24 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
|
12385
12494
|
}
|
|
12386
12495
|
|
|
12387
12496
|
// src/cli.ts
|
|
12388
|
-
import { readFileSync as
|
|
12497
|
+
import { readFileSync as readFileSync20, existsSync as existsSync26, mkdirSync as mkdirSync13, writeFileSync as writeFileSync14, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync2, statSync as statSync9 } from "fs";
|
|
12389
12498
|
import { createInterface } from "readline/promises";
|
|
12390
12499
|
import { dirname as dirOf, resolve as resolvePath } from "path";
|
|
12391
12500
|
|
|
12392
12501
|
// src/ssh-keys.ts
|
|
12393
12502
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
12394
|
-
import { existsSync as existsSync21, mkdirSync as mkdirSync9, readFileSync as
|
|
12395
|
-
import { dirname as
|
|
12503
|
+
import { existsSync as existsSync21, mkdirSync as mkdirSync9, readFileSync as readFileSync15, chmodSync } from "fs";
|
|
12504
|
+
import { dirname as dirname12, join as join21 } from "path";
|
|
12396
12505
|
var SshKeyError = class extends Error {
|
|
12397
12506
|
};
|
|
12398
12507
|
function shipSshKeyPathFor(stateDir) {
|
|
12399
|
-
return
|
|
12508
|
+
return join21(stateDir, "ssh", "id_ed25519");
|
|
12400
12509
|
}
|
|
12401
12510
|
function ensureShipSshKeypair(stateDir) {
|
|
12402
12511
|
const privateKeyPath = shipSshKeyPathFor(stateDir);
|
|
12403
12512
|
const publicKeyPath = `${privateKeyPath}.pub`;
|
|
12404
12513
|
if (!existsSync21(privateKeyPath)) {
|
|
12405
|
-
mkdirSync9(
|
|
12514
|
+
mkdirSync9(dirname12(privateKeyPath), { recursive: true });
|
|
12406
12515
|
try {
|
|
12407
12516
|
execFileSync3("ssh-keygen", [
|
|
12408
12517
|
"-t",
|
|
@@ -12425,7 +12534,7 @@ function ensureShipSshKeypair(stateDir) {
|
|
|
12425
12534
|
if (!existsSync21(publicKeyPath)) {
|
|
12426
12535
|
throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
|
|
12427
12536
|
}
|
|
12428
|
-
const publicKey =
|
|
12537
|
+
const publicKey = readFileSync15(publicKeyPath, "utf8").trim();
|
|
12429
12538
|
return { privateKeyPath, publicKeyPath, publicKey };
|
|
12430
12539
|
}
|
|
12431
12540
|
function sshKeygenAvailable() {
|
|
@@ -12641,9 +12750,9 @@ function syncAllPassengerCheckouts(cfg) {
|
|
|
12641
12750
|
|
|
12642
12751
|
// src/tunnel.ts
|
|
12643
12752
|
import { spawn as spawn3 } from "child_process";
|
|
12644
|
-
import { existsSync as existsSync22, mkdirSync as mkdirSync10, readFileSync as
|
|
12753
|
+
import { existsSync as existsSync22, mkdirSync as mkdirSync10, readFileSync as readFileSync16, writeFileSync as writeFileSync11, unlinkSync as unlinkSync3, readdirSync as readdirSync7 } from "fs";
|
|
12645
12754
|
import { randomInt } from "crypto";
|
|
12646
|
-
import { join as
|
|
12755
|
+
import { join as join22 } from "path";
|
|
12647
12756
|
var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
|
|
12648
12757
|
var defaultSpawn = (cmd, args) => spawn3(cmd, args, DEFAULT_SPAWN_OPTIONS);
|
|
12649
12758
|
var SLUG_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
@@ -12710,20 +12819,20 @@ function startTunnel(opts) {
|
|
|
12710
12819
|
};
|
|
12711
12820
|
}
|
|
12712
12821
|
function tunnelStatePath(stateDir, workspaceId) {
|
|
12713
|
-
return
|
|
12822
|
+
return join22(stateDir, "passengers", `${workspaceId}.json`);
|
|
12714
12823
|
}
|
|
12715
12824
|
function readPersistedTunnel(stateDir, workspaceId) {
|
|
12716
12825
|
const path = tunnelStatePath(stateDir, workspaceId);
|
|
12717
12826
|
if (!existsSync22(path)) return void 0;
|
|
12718
12827
|
try {
|
|
12719
|
-
return JSON.parse(
|
|
12828
|
+
return JSON.parse(readFileSync16(path, "utf8"));
|
|
12720
12829
|
} catch {
|
|
12721
12830
|
return void 0;
|
|
12722
12831
|
}
|
|
12723
12832
|
}
|
|
12724
12833
|
function writePersistedTunnel(stateDir, state) {
|
|
12725
12834
|
const path = tunnelStatePath(stateDir, state.workspaceId);
|
|
12726
|
-
mkdirSync10(
|
|
12835
|
+
mkdirSync10(join22(stateDir, "passengers"), { recursive: true });
|
|
12727
12836
|
writeFileSync11(path, `${JSON.stringify(state, null, 2)}
|
|
12728
12837
|
`);
|
|
12729
12838
|
}
|
|
@@ -12821,9 +12930,9 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
|
|
|
12821
12930
|
}
|
|
12822
12931
|
}
|
|
12823
12932
|
function readAllPersistedWorkspaceIds(stateDir) {
|
|
12824
|
-
const dir =
|
|
12933
|
+
const dir = join22(stateDir, "passengers");
|
|
12825
12934
|
if (!existsSync22(dir)) return [];
|
|
12826
|
-
return
|
|
12935
|
+
return readdirSync7(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
|
|
12827
12936
|
}
|
|
12828
12937
|
|
|
12829
12938
|
// src/mcp-endpoint-registry.ts
|
|
@@ -12943,8 +13052,8 @@ async function unregisterEndpoint(workspaceId, info, d) {
|
|
|
12943
13052
|
import { createServer as createHttpServer } from "http";
|
|
12944
13053
|
import { spawn as spawn4 } from "child_process";
|
|
12945
13054
|
import { randomBytes } from "crypto";
|
|
12946
|
-
import { existsSync as existsSync23, mkdirSync as mkdirSync11, readFileSync as
|
|
12947
|
-
import { join as
|
|
13055
|
+
import { existsSync as existsSync23, mkdirSync as mkdirSync11, readFileSync as readFileSync17, writeFileSync as writeFileSync12, unlinkSync as unlinkSync4, readdirSync as readdirSync8 } from "fs";
|
|
13056
|
+
import { join as join23 } from "path";
|
|
12948
13057
|
|
|
12949
13058
|
// src/secret-compare.ts
|
|
12950
13059
|
import { timingSafeEqual } from "crypto";
|
|
@@ -13033,20 +13142,20 @@ var defaultSpawn2 = (cmd, args, env) => {
|
|
|
13033
13142
|
return child;
|
|
13034
13143
|
};
|
|
13035
13144
|
function syncDaemonStatePath(stateDir, workspaceId) {
|
|
13036
|
-
return
|
|
13145
|
+
return join23(stateDir, "passengers", `${workspaceId}-sync.json`);
|
|
13037
13146
|
}
|
|
13038
13147
|
function readPersistedSyncDaemon(stateDir, workspaceId) {
|
|
13039
13148
|
const path = syncDaemonStatePath(stateDir, workspaceId);
|
|
13040
13149
|
if (!existsSync23(path)) return void 0;
|
|
13041
13150
|
try {
|
|
13042
|
-
return JSON.parse(
|
|
13151
|
+
return JSON.parse(readFileSync17(path, "utf8"));
|
|
13043
13152
|
} catch {
|
|
13044
13153
|
return void 0;
|
|
13045
13154
|
}
|
|
13046
13155
|
}
|
|
13047
13156
|
function writePersistedSyncDaemon(stateDir, state) {
|
|
13048
13157
|
const path = syncDaemonStatePath(stateDir, state.workspaceId);
|
|
13049
|
-
mkdirSync11(
|
|
13158
|
+
mkdirSync11(join23(stateDir, "passengers"), { recursive: true });
|
|
13050
13159
|
writeFileSync12(path, `${JSON.stringify(state, null, 2)}
|
|
13051
13160
|
`);
|
|
13052
13161
|
}
|
|
@@ -13067,10 +13176,10 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
|
|
|
13067
13176
|
}
|
|
13068
13177
|
}
|
|
13069
13178
|
function readAllPersistedSyncWorkspaceIds(stateDir) {
|
|
13070
|
-
const dir =
|
|
13179
|
+
const dir = join23(stateDir, "passengers");
|
|
13071
13180
|
if (!existsSync23(dir)) return [];
|
|
13072
13181
|
const suffix = "-sync.json";
|
|
13073
|
-
return
|
|
13182
|
+
return readdirSync8(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
|
|
13074
13183
|
}
|
|
13075
13184
|
function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
|
|
13076
13185
|
const alive = deps.isPidAlive ?? isPidAlive;
|
|
@@ -13125,12 +13234,12 @@ function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
|
|
|
13125
13234
|
}
|
|
13126
13235
|
|
|
13127
13236
|
// src/daemon.ts
|
|
13128
|
-
import { existsSync as existsSync25, mkdirSync as mkdirSync12, readFileSync as
|
|
13129
|
-
import { join as
|
|
13237
|
+
import { existsSync as existsSync25, mkdirSync as mkdirSync12, readFileSync as readFileSync19, readdirSync as readdirSync10, rmSync as rmSync9, statSync as statSync8, writeFileSync as writeFileSync13 } from "fs";
|
|
13238
|
+
import { join as join25 } from "path";
|
|
13130
13239
|
|
|
13131
13240
|
// src/interrupted.ts
|
|
13132
|
-
import { existsSync as existsSync24, readdirSync as
|
|
13133
|
-
import { join as
|
|
13241
|
+
import { existsSync as existsSync24, readdirSync as readdirSync9, readFileSync as readFileSync18 } from "fs";
|
|
13242
|
+
import { join as join24 } from "path";
|
|
13134
13243
|
var INTERRUPTED_MARKER = "<!-- crew:interrupted -->";
|
|
13135
13244
|
function interruptedComment(b) {
|
|
13136
13245
|
const tree = b.dirty === null ? "The worktree could not be checked." : b.dirty ? "The worktree has uncommitted changes \u2014 they are this seat's own, from that run." : "The worktree is clean.";
|
|
@@ -13150,19 +13259,19 @@ function runFinished(streamPath) {
|
|
|
13150
13259
|
const path = `${streamPath}.meta.json`;
|
|
13151
13260
|
if (!existsSync24(path)) return false;
|
|
13152
13261
|
try {
|
|
13153
|
-
return JSON.parse(
|
|
13262
|
+
return JSON.parse(readFileSync18(path, "utf8")).running !== true;
|
|
13154
13263
|
} catch {
|
|
13155
13264
|
return true;
|
|
13156
13265
|
}
|
|
13157
13266
|
}
|
|
13158
13267
|
function orphanedStreams(o) {
|
|
13159
|
-
const dir =
|
|
13268
|
+
const dir = join24(o.stateDir, "streams");
|
|
13160
13269
|
if (!existsSync24(dir)) return [];
|
|
13161
13270
|
const ownerAlive = o.isOwnerAlive ?? pidAlive;
|
|
13162
13271
|
const out = [];
|
|
13163
|
-
for (const name of
|
|
13272
|
+
for (const name of readdirSync9(dir)) {
|
|
13164
13273
|
if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
|
|
13165
|
-
const streamPath =
|
|
13274
|
+
const streamPath = join24(dir, name);
|
|
13166
13275
|
if (runFinished(streamPath)) continue;
|
|
13167
13276
|
const sidecar = readRunSidecar(streamPath);
|
|
13168
13277
|
if (!sidecar || sidecar.route !== o.routeLabel) continue;
|
|
@@ -13215,7 +13324,7 @@ async function reconcileRuns(o) {
|
|
|
13215
13324
|
// src/daemon.ts
|
|
13216
13325
|
var DEFAULT_FLOOR_MS = 5e3;
|
|
13217
13326
|
var DEFAULT_CEILING_MS = INTERVAL_SECONDS * 1e3;
|
|
13218
|
-
var defaultSleep = (ms2) => new Promise((
|
|
13327
|
+
var defaultSleep = (ms2) => new Promise((resolve6) => setTimeout(resolve6, ms2));
|
|
13219
13328
|
async function runDaemonLoop(o) {
|
|
13220
13329
|
const floor = o.floorMs ?? DEFAULT_FLOOR_MS;
|
|
13221
13330
|
const ceiling = o.ceilingMs ?? DEFAULT_CEILING_MS;
|
|
@@ -13261,8 +13370,8 @@ function latestMtime(path) {
|
|
|
13261
13370
|
if (stat.isFile()) return stat.mtimeMs;
|
|
13262
13371
|
if (!stat.isDirectory()) return 0;
|
|
13263
13372
|
let latest = stat.mtimeMs;
|
|
13264
|
-
for (const entry of
|
|
13265
|
-
latest = Math.max(latest, latestMtime(
|
|
13373
|
+
for (const entry of readdirSync10(path, { withFileTypes: true })) {
|
|
13374
|
+
latest = Math.max(latest, latestMtime(join25(path, entry.name)));
|
|
13266
13375
|
}
|
|
13267
13376
|
return latest;
|
|
13268
13377
|
}
|
|
@@ -13274,10 +13383,10 @@ function makeStaleChecker(paths, opts = {}) {
|
|
|
13274
13383
|
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync25(opts.reloadSentinel);
|
|
13275
13384
|
}
|
|
13276
13385
|
function reloadSentinelFor(stateDir) {
|
|
13277
|
-
return
|
|
13386
|
+
return join25(stateDir, "reload-requested");
|
|
13278
13387
|
}
|
|
13279
13388
|
function daemonStampFor(stateDir) {
|
|
13280
|
-
return
|
|
13389
|
+
return join25(stateDir, "daemon.started");
|
|
13281
13390
|
}
|
|
13282
13391
|
function noteDaemonStarted(stateDir, now = Date.now()) {
|
|
13283
13392
|
mkdirSync12(stateDir, { recursive: true });
|
|
@@ -13287,7 +13396,7 @@ function noteDaemonStarted(stateDir, now = Date.now()) {
|
|
|
13287
13396
|
}
|
|
13288
13397
|
function readDaemonStarted(stateDir) {
|
|
13289
13398
|
try {
|
|
13290
|
-
const v = JSON.parse(
|
|
13399
|
+
const v = JSON.parse(readFileSync19(daemonStampFor(stateDir), "utf8")).startedAt;
|
|
13291
13400
|
return typeof v === "number" ? v : null;
|
|
13292
13401
|
} catch {
|
|
13293
13402
|
return null;
|
|
@@ -13643,7 +13752,7 @@ async function buildEnvironment(route, ship, ticket) {
|
|
|
13643
13752
|
}
|
|
13644
13753
|
|
|
13645
13754
|
// src/cli.ts
|
|
13646
|
-
var CREW_HOME = isCompiledBinary(import.meta.url) ?
|
|
13755
|
+
var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname13(process.execPath) : resolve5(dirname13(fileURLToPath(import.meta.url)), "..");
|
|
13647
13756
|
var RELEASE_LOCK_TTL_MS = 20 * 60 * 1e3;
|
|
13648
13757
|
var CYCLE_SECONDS2 = 120;
|
|
13649
13758
|
var STALE_CYCLES = 3;
|
|
@@ -14161,7 +14270,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
14161
14270
|
}
|
|
14162
14271
|
if (host === "linux" && hasSystemd) {
|
|
14163
14272
|
const label = labelFor(CREW_HOME, "run");
|
|
14164
|
-
const staleTimer =
|
|
14273
|
+
const staleTimer = join26(homedir8(), ".config", "systemd", "user", `${label}.timer`);
|
|
14165
14274
|
if (existsSync26(staleTimer)) {
|
|
14166
14275
|
process.stdout.write(
|
|
14167
14276
|
`removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
|
|
@@ -14223,7 +14332,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
14223
14332
|
for (const a of plan.actions) {
|
|
14224
14333
|
if (a.kind === "enable") {
|
|
14225
14334
|
if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
|
|
14226
|
-
writeFileSync14(cfg.configFile, planEnable(
|
|
14335
|
+
writeFileSync14(cfg.configFile, planEnable(readFileSync20(cfg.configFile, "utf8"), a.route));
|
|
14227
14336
|
enabledSound.add(a.route);
|
|
14228
14337
|
changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
|
|
14229
14338
|
} else {
|
|
@@ -14312,9 +14421,10 @@ ${changed2.map((c) => ` - ${c}
|
|
|
14312
14421
|
]);
|
|
14313
14422
|
const scoped = ticketsByRepo(c, await tracker.ticketsByNumbers([...nums], truncated)).byRepo.get(target.name) ?? [];
|
|
14314
14423
|
const actions = planWorktreeSweep(target, scoped, tracker.contract);
|
|
14424
|
+
warnOrphanedWorktreeDirs(orphanedWorktreeDirs(target, scoped, tracker.contract), remit);
|
|
14315
14425
|
if (actions.length) {
|
|
14316
14426
|
remit.enter("worktree");
|
|
14317
|
-
const r = await applyWorktreeSweep(target.dir, actions, dryRun2, remit, branch.remote);
|
|
14427
|
+
const r = await applyWorktreeSweep(target.dir, actions, dryRun2, remit, branch.remote, branch.base);
|
|
14318
14428
|
remit.emit(`swept ${r.removed} worktree(s), kept ${r.keptBranches} branch(es)`, { step: "worktree" });
|
|
14319
14429
|
}
|
|
14320
14430
|
const orphaned = planLocalBranchCleanup(target.dir, branch.base, scoped, tracker.contract);
|
|
@@ -15199,7 +15309,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
15199
15309
|
process.exit(1);
|
|
15200
15310
|
}
|
|
15201
15311
|
requireArmed("run the daemon loop");
|
|
15202
|
-
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [
|
|
15312
|
+
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join26(CREW_HOME, "src"), join26(CREW_HOME, "dist"), join26(CREW_HOME, "bin")];
|
|
15203
15313
|
const configPaths = daemonConfigPaths(cfg);
|
|
15204
15314
|
watchPaths.push(...configPaths);
|
|
15205
15315
|
noteDaemonStarted(cfg.ship.stateDir);
|
|
@@ -15374,7 +15484,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
15374
15484
|
`);
|
|
15375
15485
|
if (!dryRun) {
|
|
15376
15486
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
15377
|
-
const raw = existsSync26(resolvedPath) ? JSON.parse(
|
|
15487
|
+
const raw = existsSync26(resolvedPath) ? JSON.parse(readFileSync20(resolvedPath, "utf8")) : {};
|
|
15378
15488
|
raw.agentPersonas = personaResult.agentPersonas;
|
|
15379
15489
|
if (skillResult) raw.agentSkills = skillResult.agentSkills;
|
|
15380
15490
|
if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
|
|
@@ -15444,7 +15554,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
15444
15554
|
`);
|
|
15445
15555
|
if (!dryRun) {
|
|
15446
15556
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
15447
|
-
const raw = existsSync26(resolvedPath) ? JSON.parse(
|
|
15557
|
+
const raw = existsSync26(resolvedPath) ? JSON.parse(readFileSync20(resolvedPath, "utf8")) : {};
|
|
15448
15558
|
raw.agentSkills = result.agentSkills;
|
|
15449
15559
|
mkdirSync13(dirOf(resolvedPath), { recursive: true });
|
|
15450
15560
|
writeFileSync14(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
@@ -15460,7 +15570,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
15460
15570
|
process.exit(2);
|
|
15461
15571
|
};
|
|
15462
15572
|
if (sub !== "add" && sub !== "list") usage2();
|
|
15463
|
-
const configText =
|
|
15573
|
+
const configText = readFileSync20(cfg.configFile, "utf8");
|
|
15464
15574
|
let routeName = positional[2];
|
|
15465
15575
|
let addPath = ".";
|
|
15466
15576
|
let addName;
|
|
@@ -15484,13 +15594,13 @@ ${changed2.map((c) => ` - ${c}
|
|
|
15484
15594
|
let resolvedRemotes;
|
|
15485
15595
|
if (existsSync26(resolvedFile)) {
|
|
15486
15596
|
try {
|
|
15487
|
-
resolvedRemotes = JSON.parse(
|
|
15597
|
+
resolvedRemotes = JSON.parse(readFileSync20(resolvedFile, "utf8"));
|
|
15488
15598
|
} catch {
|
|
15489
15599
|
}
|
|
15490
15600
|
}
|
|
15491
15601
|
const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
|
|
15492
15602
|
const text = configText;
|
|
15493
|
-
const base =
|
|
15603
|
+
const base = dirname13(cfg.configFile);
|
|
15494
15604
|
try {
|
|
15495
15605
|
if (sub === "list") {
|
|
15496
15606
|
const repos = configuredRepos(text, routeName, base);
|
|
@@ -15556,7 +15666,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
15556
15666
|
process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
|
|
15557
15667
|
`);
|
|
15558
15668
|
if (existsSync26(resolvedFile)) {
|
|
15559
|
-
const raw = JSON.parse(
|
|
15669
|
+
const raw = JSON.parse(readFileSync20(resolvedFile, "utf8"));
|
|
15560
15670
|
raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
|
|
15561
15671
|
raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
|
|
15562
15672
|
writeFileSync14(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
@@ -15813,7 +15923,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
15813
15923
|
const hadPreviousFile = existsSync26(resolvedPath);
|
|
15814
15924
|
if (hadPreviousFile) {
|
|
15815
15925
|
try {
|
|
15816
|
-
previous = JSON.parse(
|
|
15926
|
+
previous = JSON.parse(readFileSync20(resolvedPath, "utf8"));
|
|
15817
15927
|
} catch {
|
|
15818
15928
|
}
|
|
15819
15929
|
}
|
|
@@ -15876,7 +15986,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
15876
15986
|
process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
|
|
15877
15987
|
`);
|
|
15878
15988
|
} else {
|
|
15879
|
-
mkdirSync13(
|
|
15989
|
+
mkdirSync13(dirname13(resolvedPath), { recursive: true });
|
|
15880
15990
|
if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
|
|
15881
15991
|
const tmpPath = `${resolvedPath}.tmp${process.pid}`;
|
|
15882
15992
|
writeFileSync14(tmpPath, `${JSON.stringify(resolved, null, 2)}
|
|
@@ -15904,7 +16014,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
15904
16014
|
process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
|
|
15905
16015
|
`);
|
|
15906
16016
|
} else {
|
|
15907
|
-
mkdirSync13(
|
|
16017
|
+
mkdirSync13(dirname13(apiKeyFile), { recursive: true });
|
|
15908
16018
|
writeFileSync14(apiKeyFile, `${apiKeyVar}=${apiKey}
|
|
15909
16019
|
`, { mode: 384 });
|
|
15910
16020
|
process.stderr.write(`API key written to ${apiKeyFile}
|
|
@@ -15914,7 +16024,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
15914
16024
|
const keyPersisted = keychainBacked || apiKeyFile !== void 0;
|
|
15915
16025
|
const wizard = firstRun;
|
|
15916
16026
|
const writeFirstRun = wizard !== void 0 && !dryRun && keyPersisted;
|
|
15917
|
-
const routeDir = value("dir") ?? (writeFirstRun ?
|
|
16027
|
+
const routeDir = value("dir") ?? (writeFirstRun ? join26(DEFAULT_REPOS_BASE_PATH, found.workspaceSlug) : void 0);
|
|
15918
16028
|
const routeBlock = renderConnection(
|
|
15919
16029
|
found,
|
|
15920
16030
|
route2,
|
|
@@ -16106,6 +16216,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
16106
16216
|
await tracker.ticketsByNumbers(remoteBranchTicketNumbers(r.dir, r.config.branch.remote, r.config.branch.base), truncated)
|
|
16107
16217
|
).byRepo.get(r.name) ?? [];
|
|
16108
16218
|
const actions = planWorktreeSweep(r, wtTickets, tracker.contract);
|
|
16219
|
+
warnOrphanedWorktreeDirs(orphanedWorktreeDirs(r, wtTickets, tracker.contract), emit);
|
|
16109
16220
|
const stale = planRemoteBranchCleanup(r.dir, r.config.branch.remote, r.config.branch.base, brTickets, tracker.contract);
|
|
16110
16221
|
if (stale.length) {
|
|
16111
16222
|
anything = true;
|
|
@@ -16113,7 +16224,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
16113
16224
|
}
|
|
16114
16225
|
if (!actions.length) continue;
|
|
16115
16226
|
anything = true;
|
|
16116
|
-
const res = await applyWorktreeSweep(r.dir, actions, dryRun, emit, r.config.branch.remote);
|
|
16227
|
+
const res = await applyWorktreeSweep(r.dir, actions, dryRun, emit, r.config.branch.remote, r.config.branch.base);
|
|
16117
16228
|
emit.emit(
|
|
16118
16229
|
`swept ${res.removed} worktree(s) for ${r.name}, kept ${res.keptBranches} branch(es)`,
|
|
16119
16230
|
{ step: "worktree" }
|
|
@@ -16506,7 +16617,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
16506
16617
|
}
|
|
16507
16618
|
case "log": {
|
|
16508
16619
|
try {
|
|
16509
|
-
const text =
|
|
16620
|
+
const text = readFileSync20(cfg.ship.logFile, "utf8").split("\n");
|
|
16510
16621
|
process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
|
|
16511
16622
|
`);
|
|
16512
16623
|
} catch {
|
|
@@ -16727,7 +16838,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
16727
16838
|
const unitPlan = planInstall(cfg.ship, CREW_HOME, host, detectSystemd(), "run");
|
|
16728
16839
|
const abandons = unitAbandonsAgents(unitPlan, (p) => {
|
|
16729
16840
|
try {
|
|
16730
|
-
return
|
|
16841
|
+
return readFileSync20(p, "utf8");
|
|
16731
16842
|
} catch {
|
|
16732
16843
|
return null;
|
|
16733
16844
|
}
|
package/package.json
CHANGED