@tablation/crew 0.53.2 → 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 +165 -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" });
|
|
@@ -12182,17 +12182,15 @@ function schedulerInstalled(plan, exists, crontab) {
|
|
|
12182
12182
|
}
|
|
12183
12183
|
|
|
12184
12184
|
// src/worktree-sweep.ts
|
|
12185
|
-
import { existsSync as existsSync19 } from "fs";
|
|
12186
|
-
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";
|
|
12187
12187
|
function planWorktreeSweep(target, tickets, contract) {
|
|
12188
12188
|
const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
|
|
12189
12189
|
const verified = contract.statuses.verified;
|
|
12190
12190
|
const byNumber = new Map(tickets.map((t) => [t.issue_id.replace(/^\D+/, ""), t]));
|
|
12191
12191
|
const actions = [];
|
|
12192
|
-
for (const w of worktrees(target.dir)) {
|
|
12193
|
-
const
|
|
12194
|
-
if (!m) continue;
|
|
12195
|
-
const ticket = byNumber.get(m[1]);
|
|
12192
|
+
for (const w of worktrees(target.dir).slice(1)) {
|
|
12193
|
+
const ticket = ticketForWorktree(w, byNumber, contract);
|
|
12196
12194
|
if (!ticket) continue;
|
|
12197
12195
|
const isVerified = ticket.status === verified;
|
|
12198
12196
|
if (!isVerified && !terminal.has(ticket.status)) continue;
|
|
@@ -12206,18 +12204,80 @@ function planWorktreeSweep(target, tickets, contract) {
|
|
|
12206
12204
|
}
|
|
12207
12205
|
return actions;
|
|
12208
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
|
+
}
|
|
12209
12226
|
function worktreeTicketNumbers(target) {
|
|
12210
12227
|
const nums = /* @__PURE__ */ new Set();
|
|
12211
|
-
for (const w of worktrees(target.dir)) {
|
|
12212
|
-
|
|
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));
|
|
12213
12234
|
if (m) nums.add(m[1]);
|
|
12214
12235
|
}
|
|
12236
|
+
for (const d of siblingWorktreeDirs(target)) nums.add(d.number);
|
|
12215
12237
|
return [...nums];
|
|
12216
12238
|
}
|
|
12217
|
-
|
|
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) {
|
|
12218
12270
|
let removed = 0;
|
|
12219
12271
|
let keptBranches = 0;
|
|
12220
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
|
+
}
|
|
12221
12281
|
if (!dryRun && existsSync19(a.path)) {
|
|
12222
12282
|
let dirty = false;
|
|
12223
12283
|
try {
|
|
@@ -12257,7 +12317,12 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin")
|
|
|
12257
12317
|
);
|
|
12258
12318
|
continue;
|
|
12259
12319
|
}
|
|
12260
|
-
|
|
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
|
+
}
|
|
12261
12326
|
if (!a.keepBranch && a.branch) {
|
|
12262
12327
|
gitOk(cwd, ["branch", "-D", a.branch]);
|
|
12263
12328
|
if (remoteConfigured(cwd, remote) && !deleteRemoteBranch(cwd, remote, a.branch)) {
|
|
@@ -12270,8 +12335,22 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin")
|
|
|
12270
12335
|
);
|
|
12271
12336
|
removed++;
|
|
12272
12337
|
}
|
|
12338
|
+
if (!dryRun && actions.length) gitOk(cwd, ["worktree", "prune"]);
|
|
12273
12339
|
return { removed, keptBranches };
|
|
12274
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
|
+
}
|
|
12275
12354
|
function remoteBranchTicketNumbers(cwd, remote, base) {
|
|
12276
12355
|
if (!remoteConfigured(cwd, remote)) return [];
|
|
12277
12356
|
const prefix = `refs/remotes/${remote}/`;
|
|
@@ -12297,13 +12376,18 @@ function deployedBranchStems(tickets, contract) {
|
|
|
12297
12376
|
const stems = /* @__PURE__ */ new Set();
|
|
12298
12377
|
for (const t of tickets) {
|
|
12299
12378
|
if (t.status !== contract.statuses.deployed) continue;
|
|
12300
|
-
const
|
|
12301
|
-
|
|
12302
|
-
|
|
12303
|
-
|
|
12304
|
-
|
|
12305
|
-
|
|
12306
|
-
|
|
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}`);
|
|
12307
12391
|
}
|
|
12308
12392
|
return stems;
|
|
12309
12393
|
}
|
|
@@ -12314,7 +12398,8 @@ function isTicketBranch(name, base, stems) {
|
|
|
12314
12398
|
}
|
|
12315
12399
|
function sweepCandidatesExist(cwd, base) {
|
|
12316
12400
|
const linked = worktrees(cwd).slice(1);
|
|
12317
|
-
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;
|
|
12318
12403
|
return localBranches(cwd).some((b) => b !== base && /-\d+(?:-.*)?$/.test(b));
|
|
12319
12404
|
}
|
|
12320
12405
|
function localBranchTicketNumbers(cwd, base) {
|
|
@@ -12364,14 +12449,14 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
|
|
|
12364
12449
|
}
|
|
12365
12450
|
|
|
12366
12451
|
// src/stream-sweep.ts
|
|
12367
|
-
import { existsSync as existsSync20, readdirSync as
|
|
12368
|
-
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";
|
|
12369
12454
|
var DAY_MS2 = 24 * 60 * 60 * 1e3;
|
|
12370
12455
|
function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
|
|
12371
|
-
const dir =
|
|
12456
|
+
const dir = join20(stateDir, "streams");
|
|
12372
12457
|
if (!existsSync20(dir)) return [];
|
|
12373
12458
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
12374
|
-
return
|
|
12459
|
+
return readdirSync6(dir).map((name) => join20(dir, name)).filter((path) => {
|
|
12375
12460
|
try {
|
|
12376
12461
|
return statSync7(path).mtimeMs < cutoff;
|
|
12377
12462
|
} catch {
|
|
@@ -12399,7 +12484,7 @@ function applyStreamSweep(paths, dryRun, log) {
|
|
|
12399
12484
|
function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
12400
12485
|
if (!existsSync20(stateDir)) return [];
|
|
12401
12486
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
12402
|
-
return
|
|
12487
|
+
return readdirSync6(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join20(stateDir, name)).filter((path) => {
|
|
12403
12488
|
try {
|
|
12404
12489
|
return statSync7(path).mtimeMs < cutoff;
|
|
12405
12490
|
} catch {
|
|
@@ -12409,24 +12494,24 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
|
12409
12494
|
}
|
|
12410
12495
|
|
|
12411
12496
|
// src/cli.ts
|
|
12412
|
-
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";
|
|
12413
12498
|
import { createInterface } from "readline/promises";
|
|
12414
12499
|
import { dirname as dirOf, resolve as resolvePath } from "path";
|
|
12415
12500
|
|
|
12416
12501
|
// src/ssh-keys.ts
|
|
12417
12502
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
12418
|
-
import { existsSync as existsSync21, mkdirSync as mkdirSync9, readFileSync as
|
|
12419
|
-
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";
|
|
12420
12505
|
var SshKeyError = class extends Error {
|
|
12421
12506
|
};
|
|
12422
12507
|
function shipSshKeyPathFor(stateDir) {
|
|
12423
|
-
return
|
|
12508
|
+
return join21(stateDir, "ssh", "id_ed25519");
|
|
12424
12509
|
}
|
|
12425
12510
|
function ensureShipSshKeypair(stateDir) {
|
|
12426
12511
|
const privateKeyPath = shipSshKeyPathFor(stateDir);
|
|
12427
12512
|
const publicKeyPath = `${privateKeyPath}.pub`;
|
|
12428
12513
|
if (!existsSync21(privateKeyPath)) {
|
|
12429
|
-
mkdirSync9(
|
|
12514
|
+
mkdirSync9(dirname12(privateKeyPath), { recursive: true });
|
|
12430
12515
|
try {
|
|
12431
12516
|
execFileSync3("ssh-keygen", [
|
|
12432
12517
|
"-t",
|
|
@@ -12449,7 +12534,7 @@ function ensureShipSshKeypair(stateDir) {
|
|
|
12449
12534
|
if (!existsSync21(publicKeyPath)) {
|
|
12450
12535
|
throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
|
|
12451
12536
|
}
|
|
12452
|
-
const publicKey =
|
|
12537
|
+
const publicKey = readFileSync15(publicKeyPath, "utf8").trim();
|
|
12453
12538
|
return { privateKeyPath, publicKeyPath, publicKey };
|
|
12454
12539
|
}
|
|
12455
12540
|
function sshKeygenAvailable() {
|
|
@@ -12665,9 +12750,9 @@ function syncAllPassengerCheckouts(cfg) {
|
|
|
12665
12750
|
|
|
12666
12751
|
// src/tunnel.ts
|
|
12667
12752
|
import { spawn as spawn3 } from "child_process";
|
|
12668
|
-
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";
|
|
12669
12754
|
import { randomInt } from "crypto";
|
|
12670
|
-
import { join as
|
|
12755
|
+
import { join as join22 } from "path";
|
|
12671
12756
|
var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
|
|
12672
12757
|
var defaultSpawn = (cmd, args) => spawn3(cmd, args, DEFAULT_SPAWN_OPTIONS);
|
|
12673
12758
|
var SLUG_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
@@ -12734,20 +12819,20 @@ function startTunnel(opts) {
|
|
|
12734
12819
|
};
|
|
12735
12820
|
}
|
|
12736
12821
|
function tunnelStatePath(stateDir, workspaceId) {
|
|
12737
|
-
return
|
|
12822
|
+
return join22(stateDir, "passengers", `${workspaceId}.json`);
|
|
12738
12823
|
}
|
|
12739
12824
|
function readPersistedTunnel(stateDir, workspaceId) {
|
|
12740
12825
|
const path = tunnelStatePath(stateDir, workspaceId);
|
|
12741
12826
|
if (!existsSync22(path)) return void 0;
|
|
12742
12827
|
try {
|
|
12743
|
-
return JSON.parse(
|
|
12828
|
+
return JSON.parse(readFileSync16(path, "utf8"));
|
|
12744
12829
|
} catch {
|
|
12745
12830
|
return void 0;
|
|
12746
12831
|
}
|
|
12747
12832
|
}
|
|
12748
12833
|
function writePersistedTunnel(stateDir, state) {
|
|
12749
12834
|
const path = tunnelStatePath(stateDir, state.workspaceId);
|
|
12750
|
-
mkdirSync10(
|
|
12835
|
+
mkdirSync10(join22(stateDir, "passengers"), { recursive: true });
|
|
12751
12836
|
writeFileSync11(path, `${JSON.stringify(state, null, 2)}
|
|
12752
12837
|
`);
|
|
12753
12838
|
}
|
|
@@ -12845,9 +12930,9 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
|
|
|
12845
12930
|
}
|
|
12846
12931
|
}
|
|
12847
12932
|
function readAllPersistedWorkspaceIds(stateDir) {
|
|
12848
|
-
const dir =
|
|
12933
|
+
const dir = join22(stateDir, "passengers");
|
|
12849
12934
|
if (!existsSync22(dir)) return [];
|
|
12850
|
-
return
|
|
12935
|
+
return readdirSync7(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
|
|
12851
12936
|
}
|
|
12852
12937
|
|
|
12853
12938
|
// src/mcp-endpoint-registry.ts
|
|
@@ -12967,8 +13052,8 @@ async function unregisterEndpoint(workspaceId, info, d) {
|
|
|
12967
13052
|
import { createServer as createHttpServer } from "http";
|
|
12968
13053
|
import { spawn as spawn4 } from "child_process";
|
|
12969
13054
|
import { randomBytes } from "crypto";
|
|
12970
|
-
import { existsSync as existsSync23, mkdirSync as mkdirSync11, readFileSync as
|
|
12971
|
-
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";
|
|
12972
13057
|
|
|
12973
13058
|
// src/secret-compare.ts
|
|
12974
13059
|
import { timingSafeEqual } from "crypto";
|
|
@@ -13057,20 +13142,20 @@ var defaultSpawn2 = (cmd, args, env) => {
|
|
|
13057
13142
|
return child;
|
|
13058
13143
|
};
|
|
13059
13144
|
function syncDaemonStatePath(stateDir, workspaceId) {
|
|
13060
|
-
return
|
|
13145
|
+
return join23(stateDir, "passengers", `${workspaceId}-sync.json`);
|
|
13061
13146
|
}
|
|
13062
13147
|
function readPersistedSyncDaemon(stateDir, workspaceId) {
|
|
13063
13148
|
const path = syncDaemonStatePath(stateDir, workspaceId);
|
|
13064
13149
|
if (!existsSync23(path)) return void 0;
|
|
13065
13150
|
try {
|
|
13066
|
-
return JSON.parse(
|
|
13151
|
+
return JSON.parse(readFileSync17(path, "utf8"));
|
|
13067
13152
|
} catch {
|
|
13068
13153
|
return void 0;
|
|
13069
13154
|
}
|
|
13070
13155
|
}
|
|
13071
13156
|
function writePersistedSyncDaemon(stateDir, state) {
|
|
13072
13157
|
const path = syncDaemonStatePath(stateDir, state.workspaceId);
|
|
13073
|
-
mkdirSync11(
|
|
13158
|
+
mkdirSync11(join23(stateDir, "passengers"), { recursive: true });
|
|
13074
13159
|
writeFileSync12(path, `${JSON.stringify(state, null, 2)}
|
|
13075
13160
|
`);
|
|
13076
13161
|
}
|
|
@@ -13091,10 +13176,10 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
|
|
|
13091
13176
|
}
|
|
13092
13177
|
}
|
|
13093
13178
|
function readAllPersistedSyncWorkspaceIds(stateDir) {
|
|
13094
|
-
const dir =
|
|
13179
|
+
const dir = join23(stateDir, "passengers");
|
|
13095
13180
|
if (!existsSync23(dir)) return [];
|
|
13096
13181
|
const suffix = "-sync.json";
|
|
13097
|
-
return
|
|
13182
|
+
return readdirSync8(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
|
|
13098
13183
|
}
|
|
13099
13184
|
function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
|
|
13100
13185
|
const alive = deps.isPidAlive ?? isPidAlive;
|
|
@@ -13149,12 +13234,12 @@ function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
|
|
|
13149
13234
|
}
|
|
13150
13235
|
|
|
13151
13236
|
// src/daemon.ts
|
|
13152
|
-
import { existsSync as existsSync25, mkdirSync as mkdirSync12, readFileSync as
|
|
13153
|
-
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";
|
|
13154
13239
|
|
|
13155
13240
|
// src/interrupted.ts
|
|
13156
|
-
import { existsSync as existsSync24, readdirSync as
|
|
13157
|
-
import { join as
|
|
13241
|
+
import { existsSync as existsSync24, readdirSync as readdirSync9, readFileSync as readFileSync18 } from "fs";
|
|
13242
|
+
import { join as join24 } from "path";
|
|
13158
13243
|
var INTERRUPTED_MARKER = "<!-- crew:interrupted -->";
|
|
13159
13244
|
function interruptedComment(b) {
|
|
13160
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.";
|
|
@@ -13174,19 +13259,19 @@ function runFinished(streamPath) {
|
|
|
13174
13259
|
const path = `${streamPath}.meta.json`;
|
|
13175
13260
|
if (!existsSync24(path)) return false;
|
|
13176
13261
|
try {
|
|
13177
|
-
return JSON.parse(
|
|
13262
|
+
return JSON.parse(readFileSync18(path, "utf8")).running !== true;
|
|
13178
13263
|
} catch {
|
|
13179
13264
|
return true;
|
|
13180
13265
|
}
|
|
13181
13266
|
}
|
|
13182
13267
|
function orphanedStreams(o) {
|
|
13183
|
-
const dir =
|
|
13268
|
+
const dir = join24(o.stateDir, "streams");
|
|
13184
13269
|
if (!existsSync24(dir)) return [];
|
|
13185
13270
|
const ownerAlive = o.isOwnerAlive ?? pidAlive;
|
|
13186
13271
|
const out = [];
|
|
13187
|
-
for (const name of
|
|
13272
|
+
for (const name of readdirSync9(dir)) {
|
|
13188
13273
|
if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
|
|
13189
|
-
const streamPath =
|
|
13274
|
+
const streamPath = join24(dir, name);
|
|
13190
13275
|
if (runFinished(streamPath)) continue;
|
|
13191
13276
|
const sidecar = readRunSidecar(streamPath);
|
|
13192
13277
|
if (!sidecar || sidecar.route !== o.routeLabel) continue;
|
|
@@ -13239,7 +13324,7 @@ async function reconcileRuns(o) {
|
|
|
13239
13324
|
// src/daemon.ts
|
|
13240
13325
|
var DEFAULT_FLOOR_MS = 5e3;
|
|
13241
13326
|
var DEFAULT_CEILING_MS = INTERVAL_SECONDS * 1e3;
|
|
13242
|
-
var defaultSleep = (ms2) => new Promise((
|
|
13327
|
+
var defaultSleep = (ms2) => new Promise((resolve6) => setTimeout(resolve6, ms2));
|
|
13243
13328
|
async function runDaemonLoop(o) {
|
|
13244
13329
|
const floor = o.floorMs ?? DEFAULT_FLOOR_MS;
|
|
13245
13330
|
const ceiling = o.ceilingMs ?? DEFAULT_CEILING_MS;
|
|
@@ -13285,8 +13370,8 @@ function latestMtime(path) {
|
|
|
13285
13370
|
if (stat.isFile()) return stat.mtimeMs;
|
|
13286
13371
|
if (!stat.isDirectory()) return 0;
|
|
13287
13372
|
let latest = stat.mtimeMs;
|
|
13288
|
-
for (const entry of
|
|
13289
|
-
latest = Math.max(latest, latestMtime(
|
|
13373
|
+
for (const entry of readdirSync10(path, { withFileTypes: true })) {
|
|
13374
|
+
latest = Math.max(latest, latestMtime(join25(path, entry.name)));
|
|
13290
13375
|
}
|
|
13291
13376
|
return latest;
|
|
13292
13377
|
}
|
|
@@ -13298,10 +13383,10 @@ function makeStaleChecker(paths, opts = {}) {
|
|
|
13298
13383
|
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync25(opts.reloadSentinel);
|
|
13299
13384
|
}
|
|
13300
13385
|
function reloadSentinelFor(stateDir) {
|
|
13301
|
-
return
|
|
13386
|
+
return join25(stateDir, "reload-requested");
|
|
13302
13387
|
}
|
|
13303
13388
|
function daemonStampFor(stateDir) {
|
|
13304
|
-
return
|
|
13389
|
+
return join25(stateDir, "daemon.started");
|
|
13305
13390
|
}
|
|
13306
13391
|
function noteDaemonStarted(stateDir, now = Date.now()) {
|
|
13307
13392
|
mkdirSync12(stateDir, { recursive: true });
|
|
@@ -13311,7 +13396,7 @@ function noteDaemonStarted(stateDir, now = Date.now()) {
|
|
|
13311
13396
|
}
|
|
13312
13397
|
function readDaemonStarted(stateDir) {
|
|
13313
13398
|
try {
|
|
13314
|
-
const v = JSON.parse(
|
|
13399
|
+
const v = JSON.parse(readFileSync19(daemonStampFor(stateDir), "utf8")).startedAt;
|
|
13315
13400
|
return typeof v === "number" ? v : null;
|
|
13316
13401
|
} catch {
|
|
13317
13402
|
return null;
|
|
@@ -13667,7 +13752,7 @@ async function buildEnvironment(route, ship, ticket) {
|
|
|
13667
13752
|
}
|
|
13668
13753
|
|
|
13669
13754
|
// src/cli.ts
|
|
13670
|
-
var CREW_HOME = isCompiledBinary(import.meta.url) ?
|
|
13755
|
+
var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname13(process.execPath) : resolve5(dirname13(fileURLToPath(import.meta.url)), "..");
|
|
13671
13756
|
var RELEASE_LOCK_TTL_MS = 20 * 60 * 1e3;
|
|
13672
13757
|
var CYCLE_SECONDS2 = 120;
|
|
13673
13758
|
var STALE_CYCLES = 3;
|
|
@@ -14185,7 +14270,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
14185
14270
|
}
|
|
14186
14271
|
if (host === "linux" && hasSystemd) {
|
|
14187
14272
|
const label = labelFor(CREW_HOME, "run");
|
|
14188
|
-
const staleTimer =
|
|
14273
|
+
const staleTimer = join26(homedir8(), ".config", "systemd", "user", `${label}.timer`);
|
|
14189
14274
|
if (existsSync26(staleTimer)) {
|
|
14190
14275
|
process.stdout.write(
|
|
14191
14276
|
`removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
|
|
@@ -14247,7 +14332,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
14247
14332
|
for (const a of plan.actions) {
|
|
14248
14333
|
if (a.kind === "enable") {
|
|
14249
14334
|
if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
|
|
14250
|
-
writeFileSync14(cfg.configFile, planEnable(
|
|
14335
|
+
writeFileSync14(cfg.configFile, planEnable(readFileSync20(cfg.configFile, "utf8"), a.route));
|
|
14251
14336
|
enabledSound.add(a.route);
|
|
14252
14337
|
changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
|
|
14253
14338
|
} else {
|
|
@@ -14336,9 +14421,10 @@ ${changed2.map((c) => ` - ${c}
|
|
|
14336
14421
|
]);
|
|
14337
14422
|
const scoped = ticketsByRepo(c, await tracker.ticketsByNumbers([...nums], truncated)).byRepo.get(target.name) ?? [];
|
|
14338
14423
|
const actions = planWorktreeSweep(target, scoped, tracker.contract);
|
|
14424
|
+
warnOrphanedWorktreeDirs(orphanedWorktreeDirs(target, scoped, tracker.contract), remit);
|
|
14339
14425
|
if (actions.length) {
|
|
14340
14426
|
remit.enter("worktree");
|
|
14341
|
-
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);
|
|
14342
14428
|
remit.emit(`swept ${r.removed} worktree(s), kept ${r.keptBranches} branch(es)`, { step: "worktree" });
|
|
14343
14429
|
}
|
|
14344
14430
|
const orphaned = planLocalBranchCleanup(target.dir, branch.base, scoped, tracker.contract);
|
|
@@ -15223,7 +15309,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
15223
15309
|
process.exit(1);
|
|
15224
15310
|
}
|
|
15225
15311
|
requireArmed("run the daemon loop");
|
|
15226
|
-
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")];
|
|
15227
15313
|
const configPaths = daemonConfigPaths(cfg);
|
|
15228
15314
|
watchPaths.push(...configPaths);
|
|
15229
15315
|
noteDaemonStarted(cfg.ship.stateDir);
|
|
@@ -15398,7 +15484,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
15398
15484
|
`);
|
|
15399
15485
|
if (!dryRun) {
|
|
15400
15486
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
15401
|
-
const raw = existsSync26(resolvedPath) ? JSON.parse(
|
|
15487
|
+
const raw = existsSync26(resolvedPath) ? JSON.parse(readFileSync20(resolvedPath, "utf8")) : {};
|
|
15402
15488
|
raw.agentPersonas = personaResult.agentPersonas;
|
|
15403
15489
|
if (skillResult) raw.agentSkills = skillResult.agentSkills;
|
|
15404
15490
|
if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
|
|
@@ -15468,7 +15554,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
15468
15554
|
`);
|
|
15469
15555
|
if (!dryRun) {
|
|
15470
15556
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
15471
|
-
const raw = existsSync26(resolvedPath) ? JSON.parse(
|
|
15557
|
+
const raw = existsSync26(resolvedPath) ? JSON.parse(readFileSync20(resolvedPath, "utf8")) : {};
|
|
15472
15558
|
raw.agentSkills = result.agentSkills;
|
|
15473
15559
|
mkdirSync13(dirOf(resolvedPath), { recursive: true });
|
|
15474
15560
|
writeFileSync14(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
@@ -15484,7 +15570,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
15484
15570
|
process.exit(2);
|
|
15485
15571
|
};
|
|
15486
15572
|
if (sub !== "add" && sub !== "list") usage2();
|
|
15487
|
-
const configText =
|
|
15573
|
+
const configText = readFileSync20(cfg.configFile, "utf8");
|
|
15488
15574
|
let routeName = positional[2];
|
|
15489
15575
|
let addPath = ".";
|
|
15490
15576
|
let addName;
|
|
@@ -15508,13 +15594,13 @@ ${changed2.map((c) => ` - ${c}
|
|
|
15508
15594
|
let resolvedRemotes;
|
|
15509
15595
|
if (existsSync26(resolvedFile)) {
|
|
15510
15596
|
try {
|
|
15511
|
-
resolvedRemotes = JSON.parse(
|
|
15597
|
+
resolvedRemotes = JSON.parse(readFileSync20(resolvedFile, "utf8"));
|
|
15512
15598
|
} catch {
|
|
15513
15599
|
}
|
|
15514
15600
|
}
|
|
15515
15601
|
const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
|
|
15516
15602
|
const text = configText;
|
|
15517
|
-
const base =
|
|
15603
|
+
const base = dirname13(cfg.configFile);
|
|
15518
15604
|
try {
|
|
15519
15605
|
if (sub === "list") {
|
|
15520
15606
|
const repos = configuredRepos(text, routeName, base);
|
|
@@ -15580,7 +15666,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
15580
15666
|
process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
|
|
15581
15667
|
`);
|
|
15582
15668
|
if (existsSync26(resolvedFile)) {
|
|
15583
|
-
const raw = JSON.parse(
|
|
15669
|
+
const raw = JSON.parse(readFileSync20(resolvedFile, "utf8"));
|
|
15584
15670
|
raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
|
|
15585
15671
|
raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
|
|
15586
15672
|
writeFileSync14(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
@@ -15837,7 +15923,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
15837
15923
|
const hadPreviousFile = existsSync26(resolvedPath);
|
|
15838
15924
|
if (hadPreviousFile) {
|
|
15839
15925
|
try {
|
|
15840
|
-
previous = JSON.parse(
|
|
15926
|
+
previous = JSON.parse(readFileSync20(resolvedPath, "utf8"));
|
|
15841
15927
|
} catch {
|
|
15842
15928
|
}
|
|
15843
15929
|
}
|
|
@@ -15900,7 +15986,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
15900
15986
|
process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
|
|
15901
15987
|
`);
|
|
15902
15988
|
} else {
|
|
15903
|
-
mkdirSync13(
|
|
15989
|
+
mkdirSync13(dirname13(resolvedPath), { recursive: true });
|
|
15904
15990
|
if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
|
|
15905
15991
|
const tmpPath = `${resolvedPath}.tmp${process.pid}`;
|
|
15906
15992
|
writeFileSync14(tmpPath, `${JSON.stringify(resolved, null, 2)}
|
|
@@ -15928,7 +16014,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
15928
16014
|
process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
|
|
15929
16015
|
`);
|
|
15930
16016
|
} else {
|
|
15931
|
-
mkdirSync13(
|
|
16017
|
+
mkdirSync13(dirname13(apiKeyFile), { recursive: true });
|
|
15932
16018
|
writeFileSync14(apiKeyFile, `${apiKeyVar}=${apiKey}
|
|
15933
16019
|
`, { mode: 384 });
|
|
15934
16020
|
process.stderr.write(`API key written to ${apiKeyFile}
|
|
@@ -15938,7 +16024,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
15938
16024
|
const keyPersisted = keychainBacked || apiKeyFile !== void 0;
|
|
15939
16025
|
const wizard = firstRun;
|
|
15940
16026
|
const writeFirstRun = wizard !== void 0 && !dryRun && keyPersisted;
|
|
15941
|
-
const routeDir = value("dir") ?? (writeFirstRun ?
|
|
16027
|
+
const routeDir = value("dir") ?? (writeFirstRun ? join26(DEFAULT_REPOS_BASE_PATH, found.workspaceSlug) : void 0);
|
|
15942
16028
|
const routeBlock = renderConnection(
|
|
15943
16029
|
found,
|
|
15944
16030
|
route2,
|
|
@@ -16130,6 +16216,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
16130
16216
|
await tracker.ticketsByNumbers(remoteBranchTicketNumbers(r.dir, r.config.branch.remote, r.config.branch.base), truncated)
|
|
16131
16217
|
).byRepo.get(r.name) ?? [];
|
|
16132
16218
|
const actions = planWorktreeSweep(r, wtTickets, tracker.contract);
|
|
16219
|
+
warnOrphanedWorktreeDirs(orphanedWorktreeDirs(r, wtTickets, tracker.contract), emit);
|
|
16133
16220
|
const stale = planRemoteBranchCleanup(r.dir, r.config.branch.remote, r.config.branch.base, brTickets, tracker.contract);
|
|
16134
16221
|
if (stale.length) {
|
|
16135
16222
|
anything = true;
|
|
@@ -16137,7 +16224,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
16137
16224
|
}
|
|
16138
16225
|
if (!actions.length) continue;
|
|
16139
16226
|
anything = true;
|
|
16140
|
-
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);
|
|
16141
16228
|
emit.emit(
|
|
16142
16229
|
`swept ${res.removed} worktree(s) for ${r.name}, kept ${res.keptBranches} branch(es)`,
|
|
16143
16230
|
{ step: "worktree" }
|
|
@@ -16530,7 +16617,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
16530
16617
|
}
|
|
16531
16618
|
case "log": {
|
|
16532
16619
|
try {
|
|
16533
|
-
const text =
|
|
16620
|
+
const text = readFileSync20(cfg.ship.logFile, "utf8").split("\n");
|
|
16534
16621
|
process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
|
|
16535
16622
|
`);
|
|
16536
16623
|
} catch {
|
|
@@ -16751,7 +16838,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
16751
16838
|
const unitPlan = planInstall(cfg.ship, CREW_HOME, host, detectSystemd(), "run");
|
|
16752
16839
|
const abandons = unitAbandonsAgents(unitPlan, (p) => {
|
|
16753
16840
|
try {
|
|
16754
|
-
return
|
|
16841
|
+
return readFileSync20(p, "utf8");
|
|
16755
16842
|
} catch {
|
|
16756
16843
|
return null;
|
|
16757
16844
|
}
|
package/package.json
CHANGED