@tablation/crew 0.53.2 → 0.53.4
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 +286 -136
- 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";
|
|
@@ -4075,7 +4075,7 @@ function detectSystemd() {
|
|
|
4075
4075
|
}
|
|
4076
4076
|
|
|
4077
4077
|
// src/ship-attention.ts
|
|
4078
|
-
import { statfsSync } from "fs";
|
|
4078
|
+
import { existsSync as existsSync11, statfsSync } from "fs";
|
|
4079
4079
|
|
|
4080
4080
|
// src/git.ts
|
|
4081
4081
|
import { execFileSync } from "child_process";
|
|
@@ -4765,6 +4765,44 @@ function recheckBaseStops(state, route) {
|
|
|
4765
4765
|
}
|
|
4766
4766
|
return cleared;
|
|
4767
4767
|
}
|
|
4768
|
+
var heldWorktreeKey = (repo, path) => `worktree_held:${repo}:${path}`;
|
|
4769
|
+
var HELD_WORKTREE_REMINDER_MS = 24 * 60 * 60 * 1e3;
|
|
4770
|
+
function noteHeldWorktree(state, h, nowMs = Date.now()) {
|
|
4771
|
+
const store = state.shipAttention();
|
|
4772
|
+
const previous = store.previous();
|
|
4773
|
+
const key = heldWorktreeKey(h.repo, h.path);
|
|
4774
|
+
const existing = previous.find((i) => i.key === key)?.held;
|
|
4775
|
+
const nowIso = new Date(nowMs).toISOString();
|
|
4776
|
+
const lastWarned = existing ? Date.parse(existing.lastWarnedAt) : NaN;
|
|
4777
|
+
const warn = !existing ? "first" : existing.reason !== h.reason ? "changed" : !(nowMs - lastWarned < HELD_WORKTREE_REMINDER_MS) ? "reminder" : null;
|
|
4778
|
+
const held = {
|
|
4779
|
+
...h,
|
|
4780
|
+
firstSeenAt: existing?.firstSeenAt ?? nowIso,
|
|
4781
|
+
lastWarnedAt: warn ? nowIso : existing.lastWarnedAt
|
|
4782
|
+
};
|
|
4783
|
+
const n = h.ticket.replace(/^\D+/, "");
|
|
4784
|
+
const item = {
|
|
4785
|
+
kind: "worktree_held",
|
|
4786
|
+
key,
|
|
4787
|
+
since: held.firstSeenAt,
|
|
4788
|
+
held,
|
|
4789
|
+
message: `${h.repo}: worktree ${h.path} for ${h.ticket} (${h.status}) is not swept: ${h.reason}. Fix by hand: \`crew drop ${n}\`, or commit/push or discard what is in it`
|
|
4790
|
+
};
|
|
4791
|
+
if (!existing || existing.reason !== h.reason || warn) store.persist(upsert(previous, item));
|
|
4792
|
+
return warn;
|
|
4793
|
+
}
|
|
4794
|
+
function reconcileHeldWorktrees(state, repo, log, stillRefused) {
|
|
4795
|
+
for (const i of state.shipAttention().previous()) {
|
|
4796
|
+
if (i.kind !== "worktree_held" || !i.held || i.held.repo !== repo) continue;
|
|
4797
|
+
const gone = !existsSync11(i.held.path);
|
|
4798
|
+
if (!gone && !(stillRefused && !stillRefused.has(i.held.path))) continue;
|
|
4799
|
+
clearShipAttention(state, i.key);
|
|
4800
|
+
log.emit(
|
|
4801
|
+
gone ? `worktree ${i.held.path} is gone; clearing the hold` : `worktree ${i.held.path} is no longer held; clearing the hold`,
|
|
4802
|
+
{ ticket: i.held.ticket, step: "worktree" }
|
|
4803
|
+
);
|
|
4804
|
+
}
|
|
4805
|
+
}
|
|
4768
4806
|
function openShipAttention(state, nowMs = Date.now()) {
|
|
4769
4807
|
const stale = staleRelease(state.releaseHeartbeat().at(), nowMs);
|
|
4770
4808
|
const stored = state.shipAttention().previous();
|
|
@@ -7187,7 +7225,7 @@ var alreadyReported = (ticket, g, events) => events.some((e) => e.record_id ===
|
|
|
7187
7225
|
|
|
7188
7226
|
// src/agent.ts
|
|
7189
7227
|
import { spawn as spawn2, spawnSync as spawnSync5 } from "child_process";
|
|
7190
|
-
import { readFileSync as readFileSync12, existsSync as
|
|
7228
|
+
import { readFileSync as readFileSync12, existsSync as existsSync14, statSync as statSync5, mkdirSync as mkdirSync6, writeFileSync as writeFileSync8, createWriteStream, openSync as openSync3, closeSync as closeSync3, readSync } from "fs";
|
|
7191
7229
|
import { StringDecoder } from "string_decoder";
|
|
7192
7230
|
import { finished } from "stream/promises";
|
|
7193
7231
|
import { join as join14, dirname as dirname6 } from "path";
|
|
@@ -7605,7 +7643,7 @@ function expandSkillMarkers(text, lookup, opts = {}) {
|
|
|
7605
7643
|
}
|
|
7606
7644
|
|
|
7607
7645
|
// src/skills.ts
|
|
7608
|
-
import { readFileSync as readFileSync11, readdirSync as readdirSync4, existsSync as
|
|
7646
|
+
import { readFileSync as readFileSync11, readdirSync as readdirSync4, existsSync as existsSync13 } from "fs";
|
|
7609
7647
|
import { join as join13 } from "path";
|
|
7610
7648
|
import { parse as parseYaml } from "yaml";
|
|
7611
7649
|
import { TablationClient as TablationClient3, StaleWriteError as StaleWriteError8, TablationApiError as TablationApiError2 } from "@tablation/client";
|
|
@@ -7622,7 +7660,7 @@ ${additions}`;
|
|
|
7622
7660
|
}
|
|
7623
7661
|
|
|
7624
7662
|
// src/agents.ts
|
|
7625
|
-
import { readFileSync as readFileSync10, existsSync as
|
|
7663
|
+
import { readFileSync as readFileSync10, existsSync as existsSync12 } from "fs";
|
|
7626
7664
|
import { join as join12 } from "path";
|
|
7627
7665
|
import { TablationClient as TablationClient2, StaleWriteError as StaleWriteError7, TablationApiError } from "@tablation/client";
|
|
7628
7666
|
var PERSONA_NAME = {
|
|
@@ -7638,10 +7676,10 @@ var AgentsSyncError = class extends Error {
|
|
|
7638
7676
|
};
|
|
7639
7677
|
function personaDefaultPrompt(promptsDir, role) {
|
|
7640
7678
|
const briefPath = join12(promptsDir, "personas", `lane-${role}.md`);
|
|
7641
|
-
if (!
|
|
7679
|
+
if (!existsSync12(briefPath)) throw new AgentsSyncError(`no brief at ${briefPath}`);
|
|
7642
7680
|
if (SELF_CONTAINED_ROLES.has(role)) return readFileSync10(briefPath, "utf8");
|
|
7643
7681
|
const commonPath = join12(promptsDir, "personas", "common.md");
|
|
7644
|
-
if (!
|
|
7682
|
+
if (!existsSync12(commonPath)) return readFileSync10(briefPath, "utf8");
|
|
7645
7683
|
return `${readFileSync10(commonPath, "utf8")}${readFileSync10(briefPath, "utf8")}`;
|
|
7646
7684
|
}
|
|
7647
7685
|
async function resolveAgentId(route, role, opts) {
|
|
@@ -8103,7 +8141,7 @@ function parseSkillFile(path) {
|
|
|
8103
8141
|
}
|
|
8104
8142
|
function listLocalSkills(promptsDir) {
|
|
8105
8143
|
const dir = join13(promptsDir, "skills");
|
|
8106
|
-
if (!
|
|
8144
|
+
if (!existsSync13(dir)) return [];
|
|
8107
8145
|
return readdirSync4(dir).filter((f) => f.endsWith(".md")).map((f) => parseSkillFile(join13(dir, f)));
|
|
8108
8146
|
}
|
|
8109
8147
|
async function syncSkills(route, opts) {
|
|
@@ -8356,17 +8394,17 @@ var AgentSpawnError = class extends AgentError {
|
|
|
8356
8394
|
};
|
|
8357
8395
|
function planAgentRun(o) {
|
|
8358
8396
|
const briefPath = join14(o.route.promptsDir, "personas", `lane-${o.role}.md`);
|
|
8359
|
-
if (!
|
|
8397
|
+
if (!existsSync14(briefPath)) {
|
|
8360
8398
|
throw new AgentError(`no brief at ${briefPath} \u2014 refusing to run an unscoped session`);
|
|
8361
8399
|
}
|
|
8362
8400
|
const commonPath = join14(o.route.promptsDir, "personas", "common.md");
|
|
8363
|
-
const usesCommon = !SELF_CONTAINED_ROLES.has(o.role) &&
|
|
8401
|
+
const usesCommon = !SELF_CONTAINED_ROLES.has(o.role) && existsSync14(commonPath);
|
|
8364
8402
|
const now = o.now ?? (() => Date.now());
|
|
8365
8403
|
const routeLabel = routeSlug(o.route.route);
|
|
8366
8404
|
const digestPath2 = join14(o.stateDir, `digest-${routeLabel}-${o.role}.md`);
|
|
8367
8405
|
let digest;
|
|
8368
8406
|
let ageSeconds;
|
|
8369
|
-
if (
|
|
8407
|
+
if (existsSync14(digestPath2)) {
|
|
8370
8408
|
ageSeconds = Math.floor((now() - statSync5(digestPath2).mtimeMs) / 1e3);
|
|
8371
8409
|
if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync12(digestPath2, "utf8");
|
|
8372
8410
|
}
|
|
@@ -8767,7 +8805,7 @@ function spawnAgent(plan, emit) {
|
|
|
8767
8805
|
for (const w of plan.warnings ?? []) emit.warn(w, { step: "agent", role: plan.role });
|
|
8768
8806
|
const eventsSink = openSink(plan.eventsPath);
|
|
8769
8807
|
const id = identityOf(plan, adapter.harness);
|
|
8770
|
-
return new Promise((
|
|
8808
|
+
return new Promise((resolve6, reject) => {
|
|
8771
8809
|
let streamFd;
|
|
8772
8810
|
try {
|
|
8773
8811
|
mkdirSync6(dirname6(plan.streamPath), { recursive: true });
|
|
@@ -8826,7 +8864,7 @@ function spawnAgent(plan, emit) {
|
|
|
8826
8864
|
await endSink(eventsSink);
|
|
8827
8865
|
if (spawnFailed) return;
|
|
8828
8866
|
const { ms: ms2 } = await finishRun(id, obs, emit, startedAt, { code: code ?? 1 }, plan.agentLog?.apiKey);
|
|
8829
|
-
|
|
8867
|
+
resolve6({ code: code ?? 1, ms: ms2 });
|
|
8830
8868
|
});
|
|
8831
8869
|
child.stdin?.on("error", (err) => {
|
|
8832
8870
|
emit.warn(`agent closed stdin before the prompt was delivered: ${err.message}`, { step: "agent" });
|
|
@@ -8869,12 +8907,12 @@ async function reattachRun(streamPath, sc, emit, o = {}) {
|
|
|
8869
8907
|
}
|
|
8870
8908
|
|
|
8871
8909
|
// src/run-worktree.ts
|
|
8872
|
-
import { existsSync as
|
|
8910
|
+
import { existsSync as existsSync16 } from "fs";
|
|
8873
8911
|
import { dirname as dirname7, join as join16 } from "path";
|
|
8874
8912
|
|
|
8875
8913
|
// src/ports.ts
|
|
8876
8914
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
8877
|
-
import { existsSync as
|
|
8915
|
+
import { existsSync as existsSync15 } from "fs";
|
|
8878
8916
|
import { join as join15 } from "path";
|
|
8879
8917
|
var run = (cmd, args) => {
|
|
8880
8918
|
try {
|
|
@@ -8920,7 +8958,7 @@ function ticketForPort(port, s = DEFAULT_PORTS) {
|
|
|
8920
8958
|
}
|
|
8921
8959
|
function worktreeExists(parent, prefix, n) {
|
|
8922
8960
|
for (const name of [`${prefix}${n}`, `${prefix}${String(n).padStart(3, "0")}`]) {
|
|
8923
|
-
if (
|
|
8961
|
+
if (existsSync15(join15(parent, name, ".git"))) return true;
|
|
8924
8962
|
}
|
|
8925
8963
|
return false;
|
|
8926
8964
|
}
|
|
@@ -8996,7 +9034,7 @@ function planRunWorktree(dir, cfg, t, role, opts = {}) {
|
|
|
8996
9034
|
const existing = worktreeForNumber(dir, num);
|
|
8997
9035
|
const located = locateBranchForTicket(dir, cfg, t, role);
|
|
8998
9036
|
const baseRef = hasRemote ? `${remote}/${base}` : base;
|
|
8999
|
-
if (existing && !existing.prunable &&
|
|
9037
|
+
if (existing && !existing.prunable && existsSync16(existing.path)) {
|
|
9000
9038
|
const branch = existing.branch;
|
|
9001
9039
|
if (status(existing.path).length > 0) {
|
|
9002
9040
|
return { action: opts.resumeInterrupted ? "resume-dirty" : "leave-dirty", path: existing.path, branch };
|
|
@@ -9061,7 +9099,7 @@ function worktreeHoldsUnpushed(path, remote) {
|
|
|
9061
9099
|
}
|
|
9062
9100
|
async function finishRunWorktree(dir, cfg, branch) {
|
|
9063
9101
|
const wt = worktrees(dir).find((w) => w.branch === branch && !w.prunable && w.path !== dir);
|
|
9064
|
-
if (!wt || !
|
|
9102
|
+
if (!wt || !existsSync16(wt.path)) return { kind: "none" };
|
|
9065
9103
|
if (cfg.hooks.handoff) return { kind: "kept-handoff", path: wt.path };
|
|
9066
9104
|
if (status(wt.path).length > 0) return { kind: "kept-dirty", path: wt.path };
|
|
9067
9105
|
if (remoteConfigured(dir, cfg.branch.remote) && worktreeHoldsUnpushed(wt.path, cfg.branch.remote)) {
|
|
@@ -9073,7 +9111,7 @@ async function finishRunWorktree(dir, cfg, branch) {
|
|
|
9073
9111
|
if (pids.length) await killGently(pids);
|
|
9074
9112
|
}
|
|
9075
9113
|
gitOk(dir, ["worktree", "remove", wt.path]);
|
|
9076
|
-
return
|
|
9114
|
+
return existsSync16(wt.path) ? { kind: "kept-dirty", path: wt.path } : { kind: "removed", path: wt.path };
|
|
9077
9115
|
}
|
|
9078
9116
|
|
|
9079
9117
|
// src/handoff-guard.ts
|
|
@@ -9333,12 +9371,12 @@ async function guardRunStrays(route, ship, tracker, role, workingId, since2, log
|
|
|
9333
9371
|
|
|
9334
9372
|
// src/connect-wizard.ts
|
|
9335
9373
|
import { delimiter, dirname as dirname8, join as join17 } from "path";
|
|
9336
|
-
import { accessSync as accessSync2, constants as constants2, existsSync as
|
|
9374
|
+
import { accessSync as accessSync2, constants as constants2, existsSync as existsSync17, mkdirSync as mkdirSync7, writeFileSync as writeFileSync9 } from "fs";
|
|
9337
9375
|
import { stringify } from "yaml";
|
|
9338
9376
|
var HOST_PASSENGERS_DOCS_URL = "https://crew.tablation.dev/host-passengers";
|
|
9339
9377
|
function shouldRunWizard(opts) {
|
|
9340
9378
|
if (!opts.isTTY) return false;
|
|
9341
|
-
const exists = opts.configExists ??
|
|
9379
|
+
const exists = opts.configExists ?? existsSync17;
|
|
9342
9380
|
return !configSearchPath(opts.crewHome).some((p) => exists(p));
|
|
9343
9381
|
}
|
|
9344
9382
|
function firstRunConfigPath() {
|
|
@@ -9450,7 +9488,7 @@ Wrote ${o.configPath}.
|
|
|
9450
9488
|
}
|
|
9451
9489
|
|
|
9452
9490
|
// src/release-run.ts
|
|
9453
|
-
import { readFileSync as readFileSync13, writeFileSync as writeFileSync10, existsSync as
|
|
9491
|
+
import { readFileSync as readFileSync13, writeFileSync as writeFileSync10, existsSync as existsSync18, copyFileSync, mkdirSync as mkdirSync8, mkdtempSync, rmSync as rmSync7 } from "fs";
|
|
9454
9492
|
import { dirname as dirname9, join as join18 } from "path";
|
|
9455
9493
|
import { tmpdir as tmpdir2 } from "os";
|
|
9456
9494
|
|
|
@@ -10141,7 +10179,7 @@ async function bump(o, size) {
|
|
|
10141
10179
|
}
|
|
10142
10180
|
const files = o.repo.release.versionFiles.map((f) => join18(o.cwd, f));
|
|
10143
10181
|
const first = files[0];
|
|
10144
|
-
if (!
|
|
10182
|
+
if (!existsSync18(first)) {
|
|
10145
10183
|
o.emit.error(`no ${o.repo.release.versionFiles[0]} to read a version from`);
|
|
10146
10184
|
return null;
|
|
10147
10185
|
}
|
|
@@ -10177,7 +10215,7 @@ function writeChangelog(o, version, entries) {
|
|
|
10177
10215
|
o.emit.emit(`would prepend ${entries.length} entr(ies) to ${file}`);
|
|
10178
10216
|
return;
|
|
10179
10217
|
}
|
|
10180
|
-
const existing =
|
|
10218
|
+
const existing = existsSync18(path) ? readFileSync13(path, "utf8") : "# Changelog\n";
|
|
10181
10219
|
writeFileSync10(path, insertChangelogSection(existing, section));
|
|
10182
10220
|
}
|
|
10183
10221
|
async function confirm(o, expected) {
|
|
@@ -10283,7 +10321,7 @@ function openReleaseCheckout(o) {
|
|
|
10283
10321
|
}
|
|
10284
10322
|
for (const f of o.repo.worktrees.copy) {
|
|
10285
10323
|
const src = join18(o.cwd, f);
|
|
10286
|
-
if (!
|
|
10324
|
+
if (!existsSync18(src)) continue;
|
|
10287
10325
|
mkdirSync8(dirname9(join18(dir, f)), { recursive: true });
|
|
10288
10326
|
copyFileSync(src, join18(dir, f));
|
|
10289
10327
|
}
|
|
@@ -10868,7 +10906,7 @@ function missingReleaserWarning(repo, release, ships, now = Date.now()) {
|
|
|
10868
10906
|
}
|
|
10869
10907
|
|
|
10870
10908
|
// src/watch.ts
|
|
10871
|
-
import { watch as watchFile, existsSync as
|
|
10909
|
+
import { watch as watchFile, existsSync as existsSync19, statSync as statSync6, openSync as openSync4, readSync as readSync2, closeSync as closeSync4 } from "fs";
|
|
10872
10910
|
var E = "\x1B[";
|
|
10873
10911
|
var dim = (s) => `${E}2m${s}${E}0m`;
|
|
10874
10912
|
var bold = (s) => `${E}1m${s}${E}0m`;
|
|
@@ -10969,7 +11007,7 @@ function renderHeaders(states, now = Date.now()) {
|
|
|
10969
11007
|
return states.map((s) => renderHeader(s, now)).join("");
|
|
10970
11008
|
}
|
|
10971
11009
|
function readFrom(file, from) {
|
|
10972
|
-
if (!
|
|
11010
|
+
if (!existsSync19(file)) return { events: [], offset: from };
|
|
10973
11011
|
const size = statSync6(file).size;
|
|
10974
11012
|
if (size < from) return readFrom(file, 0);
|
|
10975
11013
|
if (size === from) return { events: [], offset: from };
|
|
@@ -12182,17 +12220,15 @@ function schedulerInstalled(plan, exists, crontab) {
|
|
|
12182
12220
|
}
|
|
12183
12221
|
|
|
12184
12222
|
// src/worktree-sweep.ts
|
|
12185
|
-
import { existsSync as
|
|
12186
|
-
import { basename as basename8 } from "path";
|
|
12223
|
+
import { existsSync as existsSync20, readFileSync as readFileSync14, readdirSync as readdirSync5 } from "fs";
|
|
12224
|
+
import { basename as basename8, dirname as dirname11, join as join19, resolve as resolve4 } from "path";
|
|
12187
12225
|
function planWorktreeSweep(target, tickets, contract) {
|
|
12188
12226
|
const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
|
|
12189
12227
|
const verified = contract.statuses.verified;
|
|
12190
12228
|
const byNumber = new Map(tickets.map((t) => [t.issue_id.replace(/^\D+/, ""), t]));
|
|
12191
12229
|
const actions = [];
|
|
12192
|
-
for (const w of worktrees(target.dir)) {
|
|
12193
|
-
const
|
|
12194
|
-
if (!m) continue;
|
|
12195
|
-
const ticket = byNumber.get(m[1]);
|
|
12230
|
+
for (const w of worktrees(target.dir).slice(1)) {
|
|
12231
|
+
const ticket = ticketForWorktree(w, byNumber, contract);
|
|
12196
12232
|
if (!ticket) continue;
|
|
12197
12233
|
const isVerified = ticket.status === verified;
|
|
12198
12234
|
if (!isVerified && !terminal.has(ticket.status)) continue;
|
|
@@ -12206,37 +12242,108 @@ function planWorktreeSweep(target, tickets, contract) {
|
|
|
12206
12242
|
}
|
|
12207
12243
|
return actions;
|
|
12208
12244
|
}
|
|
12245
|
+
var DIR_NUMBER = /(?:^|-)(\d+)$/;
|
|
12246
|
+
var DIR_NUMBER_SUFFIXED = /(?:^|-)(\d+)(?:-[a-z0-9]{1,8})?$/;
|
|
12247
|
+
var BRANCH_STEM = /^(?:.*\/)?([^/]+?-\d+)(?:-.*)?$/;
|
|
12248
|
+
function ticketForWorktree(w, byNumber, contract) {
|
|
12249
|
+
if (w.branch) {
|
|
12250
|
+
const stem = BRANCH_STEM.exec(w.branch)?.[1];
|
|
12251
|
+
const num = stem && /-(\d+)$/.exec(stem)?.[1];
|
|
12252
|
+
const t = num ? byNumber.get(num) : void 0;
|
|
12253
|
+
if (t && ticketStems(t).has(stem)) return t;
|
|
12254
|
+
}
|
|
12255
|
+
const dir = basename8(w.path);
|
|
12256
|
+
const m = DIR_NUMBER.exec(dir);
|
|
12257
|
+
if (m && byNumber.has(m[1])) return byNumber.get(m[1]);
|
|
12258
|
+
if (!w.branch) {
|
|
12259
|
+
const t = byNumber.get(DIR_NUMBER_SUFFIXED.exec(dir)?.[1] ?? "");
|
|
12260
|
+
if (t && t.status === contract.statuses.deployed) return t;
|
|
12261
|
+
}
|
|
12262
|
+
return void 0;
|
|
12263
|
+
}
|
|
12209
12264
|
function worktreeTicketNumbers(target) {
|
|
12210
12265
|
const nums = /* @__PURE__ */ new Set();
|
|
12211
|
-
for (const w of worktrees(target.dir)) {
|
|
12212
|
-
|
|
12266
|
+
for (const w of worktrees(target.dir).slice(1)) {
|
|
12267
|
+
if (w.branch) {
|
|
12268
|
+
const n = /-(\d+)$/.exec(BRANCH_STEM.exec(w.branch)?.[1] ?? "")?.[1];
|
|
12269
|
+
if (n) nums.add(n);
|
|
12270
|
+
}
|
|
12271
|
+
const m = DIR_NUMBER_SUFFIXED.exec(basename8(w.path));
|
|
12213
12272
|
if (m) nums.add(m[1]);
|
|
12214
12273
|
}
|
|
12274
|
+
for (const d of siblingWorktreeDirs(target)) nums.add(d.number);
|
|
12215
12275
|
return [...nums];
|
|
12216
12276
|
}
|
|
12217
|
-
|
|
12277
|
+
function siblingWorktreeDirs(target) {
|
|
12278
|
+
const parent = dirname11(target.dir.replace(/[/\\]+$/, ""));
|
|
12279
|
+
const stem = `${basename8(target.dir.replace(/[/\\]+$/, ""))}-`;
|
|
12280
|
+
const registered = new Set(worktrees(target.dir).map((w) => resolve4(w.path)));
|
|
12281
|
+
let names;
|
|
12282
|
+
try {
|
|
12283
|
+
names = readdirSync5(parent);
|
|
12284
|
+
} catch {
|
|
12285
|
+
return [];
|
|
12286
|
+
}
|
|
12287
|
+
const out = [];
|
|
12288
|
+
for (const name of names) {
|
|
12289
|
+
if (!name.startsWith(stem)) continue;
|
|
12290
|
+
const m = DIR_NUMBER_SUFFIXED.exec(name);
|
|
12291
|
+
const path = join19(parent, name);
|
|
12292
|
+
if (m && !registered.has(resolve4(path))) out.push({ path, number: m[1] });
|
|
12293
|
+
}
|
|
12294
|
+
return out;
|
|
12295
|
+
}
|
|
12296
|
+
function orphanedWorktreeDirs(target, tickets, contract) {
|
|
12297
|
+
const deployed = new Set(
|
|
12298
|
+
tickets.filter((t) => t.status === contract.statuses.deployed).map((t) => t.issue_id.replace(/^\D+/, ""))
|
|
12299
|
+
);
|
|
12300
|
+
return siblingWorktreeDirs(target).filter((d) => deployed.has(d.number)).map((d) => d.path);
|
|
12301
|
+
}
|
|
12302
|
+
function warnOrphanedWorktreeDirs(paths, log, held) {
|
|
12303
|
+
for (const p of paths) {
|
|
12304
|
+
const number = DIR_NUMBER_SUFFIXED.exec(basename8(p))?.[1];
|
|
12305
|
+
const when = held && number ? noteHeldWorktree(held.state, { repo: held.repo, path: p, ticket: number, status: "deployed", reason: "orphaned directory" }) : "first";
|
|
12306
|
+
if (when) {
|
|
12307
|
+
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" });
|
|
12308
|
+
}
|
|
12309
|
+
}
|
|
12310
|
+
}
|
|
12311
|
+
async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin", base, held) {
|
|
12312
|
+
const refused = [];
|
|
12313
|
+
const refuse = (a, reason, detail) => {
|
|
12314
|
+
refused.push(a.path);
|
|
12315
|
+
const when = held ? noteHeldWorktree(held.state, { repo: held.repo, path: a.path, ticket: a.ticket.issue_id, status: a.ticket.status, reason }) : "first";
|
|
12316
|
+
if (when) {
|
|
12317
|
+
log.warn(
|
|
12318
|
+
`not removing worktree ${a.path} for ${a.ticket.issue_id} (${a.ticket.status}): ${detail}`,
|
|
12319
|
+
{ ticket: a.ticket.issue_id, step: "worktree" }
|
|
12320
|
+
);
|
|
12321
|
+
}
|
|
12322
|
+
};
|
|
12218
12323
|
let removed = 0;
|
|
12219
12324
|
let keptBranches = 0;
|
|
12220
12325
|
for (const a of actions) {
|
|
12221
|
-
if (!dryRun &&
|
|
12326
|
+
if (!dryRun && base && !a.keepBranch && existsSync20(a.path) && isHandBackMerge(a.path, base)) {
|
|
12327
|
+
if (gitOk(a.path, ["merge", "--abort"]) !== null) {
|
|
12328
|
+
log.emit(
|
|
12329
|
+
`abandoned the hand-back merge of ${base} in ${a.path}: ${a.ticket.issue_id} deployed without it`,
|
|
12330
|
+
{ ticket: a.ticket.issue_id, step: "worktree" }
|
|
12331
|
+
);
|
|
12332
|
+
}
|
|
12333
|
+
}
|
|
12334
|
+
if (!dryRun && existsSync20(a.path)) {
|
|
12222
12335
|
let dirty = false;
|
|
12223
12336
|
try {
|
|
12224
12337
|
dirty = status(a.path).length > 0;
|
|
12225
12338
|
} catch {
|
|
12226
12339
|
}
|
|
12227
12340
|
if (dirty) {
|
|
12228
|
-
|
|
12229
|
-
`not removing worktree ${a.path} for ${a.ticket.issue_id} (${a.ticket.status}): it has uncommitted changes`,
|
|
12230
|
-
{ ticket: a.ticket.issue_id, step: "worktree" }
|
|
12231
|
-
);
|
|
12341
|
+
refuse(a, "uncommitted changes", "it has uncommitted changes");
|
|
12232
12342
|
continue;
|
|
12233
12343
|
}
|
|
12234
12344
|
}
|
|
12235
|
-
if (!dryRun && a.requirePushed && a.branch &&
|
|
12236
|
-
|
|
12237
|
-
`not removing worktree ${a.path} for ${a.ticket.issue_id} (${a.ticket.status}): ${a.branch} has commits that are not on ${remote}`,
|
|
12238
|
-
{ ticket: a.ticket.issue_id, step: "worktree" }
|
|
12239
|
-
);
|
|
12345
|
+
if (!dryRun && a.requirePushed && a.branch && existsSync20(a.path) && remoteConfigured(cwd, remote) && worktreeHoldsUnpushed(a.path, remote)) {
|
|
12346
|
+
refuse(a, "unpushed commits", `${a.branch} has commits that are not on ${remote}`);
|
|
12240
12347
|
continue;
|
|
12241
12348
|
}
|
|
12242
12349
|
const n = Number(a.ticket.issue_id.replace(/^\D+/, ""));
|
|
@@ -12257,7 +12364,12 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin")
|
|
|
12257
12364
|
);
|
|
12258
12365
|
continue;
|
|
12259
12366
|
}
|
|
12260
|
-
|
|
12367
|
+
try {
|
|
12368
|
+
git(cwd, ["worktree", "remove", "--force", a.path]);
|
|
12369
|
+
} catch (e) {
|
|
12370
|
+
refuse(a, "removal failed", `could not remove it: ${e.message}`);
|
|
12371
|
+
continue;
|
|
12372
|
+
}
|
|
12261
12373
|
if (!a.keepBranch && a.branch) {
|
|
12262
12374
|
gitOk(cwd, ["branch", "-D", a.branch]);
|
|
12263
12375
|
if (remoteConfigured(cwd, remote) && !deleteRemoteBranch(cwd, remote, a.branch)) {
|
|
@@ -12270,7 +12382,21 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin")
|
|
|
12270
12382
|
);
|
|
12271
12383
|
removed++;
|
|
12272
12384
|
}
|
|
12273
|
-
|
|
12385
|
+
if (!dryRun && actions.length) gitOk(cwd, ["worktree", "prune"]);
|
|
12386
|
+
return { removed, keptBranches, refused };
|
|
12387
|
+
}
|
|
12388
|
+
function isHandBackMerge(path, base) {
|
|
12389
|
+
if (gitOk(path, ["rev-parse", "-q", "--verify", "MERGE_HEAD"]) === null) return false;
|
|
12390
|
+
const msgPath = gitOk(path, ["rev-parse", "--git-path", "MERGE_MSG"]);
|
|
12391
|
+
if (!msgPath) return false;
|
|
12392
|
+
let first;
|
|
12393
|
+
try {
|
|
12394
|
+
first = readFileSync14(resolve4(path, msgPath), "utf8").split("\n")[0] ?? "";
|
|
12395
|
+
} catch {
|
|
12396
|
+
return false;
|
|
12397
|
+
}
|
|
12398
|
+
const escaped = base.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
12399
|
+
return new RegExp(`^Merge (?:remote-tracking )?branch '(?:[^']+/)?${escaped}'`).test(first);
|
|
12274
12400
|
}
|
|
12275
12401
|
function remoteBranchTicketNumbers(cwd, remote, base) {
|
|
12276
12402
|
if (!remoteConfigured(cwd, remote)) return [];
|
|
@@ -12297,13 +12423,18 @@ function deployedBranchStems(tickets, contract) {
|
|
|
12297
12423
|
const stems = /* @__PURE__ */ new Set();
|
|
12298
12424
|
for (const t of tickets) {
|
|
12299
12425
|
if (t.status !== contract.statuses.deployed) continue;
|
|
12300
|
-
const
|
|
12301
|
-
|
|
12302
|
-
|
|
12303
|
-
|
|
12304
|
-
|
|
12305
|
-
|
|
12306
|
-
|
|
12426
|
+
for (const st of ticketStems(t)) stems.add(st);
|
|
12427
|
+
}
|
|
12428
|
+
return stems;
|
|
12429
|
+
}
|
|
12430
|
+
function ticketStems(t) {
|
|
12431
|
+
const stems = /* @__PURE__ */ new Set();
|
|
12432
|
+
const num = t.issue_id.replace(/^\D+/, "");
|
|
12433
|
+
const unpadded = String(Number.parseInt(num, 10));
|
|
12434
|
+
const prefixes = ["issue", t.project_issue_prefix?.toLowerCase()].filter((p) => !!p);
|
|
12435
|
+
for (const p of prefixes) {
|
|
12436
|
+
stems.add(`${p}-${num}`);
|
|
12437
|
+
stems.add(`${p}-${unpadded}`);
|
|
12307
12438
|
}
|
|
12308
12439
|
return stems;
|
|
12309
12440
|
}
|
|
@@ -12314,7 +12445,8 @@ function isTicketBranch(name, base, stems) {
|
|
|
12314
12445
|
}
|
|
12315
12446
|
function sweepCandidatesExist(cwd, base) {
|
|
12316
12447
|
const linked = worktrees(cwd).slice(1);
|
|
12317
|
-
if (linked.some((w) =>
|
|
12448
|
+
if (linked.some((w) => DIR_NUMBER_SUFFIXED.test(basename8(w.path)) || w.branch && BRANCH_STEM.test(w.branch))) return true;
|
|
12449
|
+
if (siblingWorktreeDirs({ name: "", dir: cwd }).length) return true;
|
|
12318
12450
|
return localBranches(cwd).some((b) => b !== base && /-\d+(?:-.*)?$/.test(b));
|
|
12319
12451
|
}
|
|
12320
12452
|
function localBranchTicketNumbers(cwd, base) {
|
|
@@ -12364,14 +12496,14 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
|
|
|
12364
12496
|
}
|
|
12365
12497
|
|
|
12366
12498
|
// src/stream-sweep.ts
|
|
12367
|
-
import { existsSync as
|
|
12368
|
-
import { join as
|
|
12499
|
+
import { existsSync as existsSync21, readdirSync as readdirSync6, rmSync as rmSync8, statSync as statSync7 } from "fs";
|
|
12500
|
+
import { join as join20 } from "path";
|
|
12369
12501
|
var DAY_MS2 = 24 * 60 * 60 * 1e3;
|
|
12370
12502
|
function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
|
|
12371
|
-
const dir =
|
|
12372
|
-
if (!
|
|
12503
|
+
const dir = join20(stateDir, "streams");
|
|
12504
|
+
if (!existsSync21(dir)) return [];
|
|
12373
12505
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
12374
|
-
return
|
|
12506
|
+
return readdirSync6(dir).map((name) => join20(dir, name)).filter((path) => {
|
|
12375
12507
|
try {
|
|
12376
12508
|
return statSync7(path).mtimeMs < cutoff;
|
|
12377
12509
|
} catch {
|
|
@@ -12397,9 +12529,9 @@ function applyStreamSweep(paths, dryRun, log) {
|
|
|
12397
12529
|
return removed;
|
|
12398
12530
|
}
|
|
12399
12531
|
function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
12400
|
-
if (!
|
|
12532
|
+
if (!existsSync21(stateDir)) return [];
|
|
12401
12533
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
12402
|
-
return
|
|
12534
|
+
return readdirSync6(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join20(stateDir, name)).filter((path) => {
|
|
12403
12535
|
try {
|
|
12404
12536
|
return statSync7(path).mtimeMs < cutoff;
|
|
12405
12537
|
} catch {
|
|
@@ -12409,24 +12541,24 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
|
12409
12541
|
}
|
|
12410
12542
|
|
|
12411
12543
|
// src/cli.ts
|
|
12412
|
-
import { readFileSync as
|
|
12544
|
+
import { readFileSync as readFileSync20, existsSync as existsSync27, mkdirSync as mkdirSync13, writeFileSync as writeFileSync14, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync2, statSync as statSync9 } from "fs";
|
|
12413
12545
|
import { createInterface } from "readline/promises";
|
|
12414
12546
|
import { dirname as dirOf, resolve as resolvePath } from "path";
|
|
12415
12547
|
|
|
12416
12548
|
// src/ssh-keys.ts
|
|
12417
12549
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
12418
|
-
import { existsSync as
|
|
12419
|
-
import { dirname as
|
|
12550
|
+
import { existsSync as existsSync22, mkdirSync as mkdirSync9, readFileSync as readFileSync15, chmodSync } from "fs";
|
|
12551
|
+
import { dirname as dirname12, join as join21 } from "path";
|
|
12420
12552
|
var SshKeyError = class extends Error {
|
|
12421
12553
|
};
|
|
12422
12554
|
function shipSshKeyPathFor(stateDir) {
|
|
12423
|
-
return
|
|
12555
|
+
return join21(stateDir, "ssh", "id_ed25519");
|
|
12424
12556
|
}
|
|
12425
12557
|
function ensureShipSshKeypair(stateDir) {
|
|
12426
12558
|
const privateKeyPath = shipSshKeyPathFor(stateDir);
|
|
12427
12559
|
const publicKeyPath = `${privateKeyPath}.pub`;
|
|
12428
|
-
if (!
|
|
12429
|
-
mkdirSync9(
|
|
12560
|
+
if (!existsSync22(privateKeyPath)) {
|
|
12561
|
+
mkdirSync9(dirname12(privateKeyPath), { recursive: true });
|
|
12430
12562
|
try {
|
|
12431
12563
|
execFileSync3("ssh-keygen", [
|
|
12432
12564
|
"-t",
|
|
@@ -12446,10 +12578,10 @@ function ensureShipSshKeypair(stateDir) {
|
|
|
12446
12578
|
}
|
|
12447
12579
|
chmodSync(privateKeyPath, 384);
|
|
12448
12580
|
}
|
|
12449
|
-
if (!
|
|
12581
|
+
if (!existsSync22(publicKeyPath)) {
|
|
12450
12582
|
throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
|
|
12451
12583
|
}
|
|
12452
|
-
const publicKey =
|
|
12584
|
+
const publicKey = readFileSync15(publicKeyPath, "utf8").trim();
|
|
12453
12585
|
return { privateKeyPath, publicKeyPath, publicKey };
|
|
12454
12586
|
}
|
|
12455
12587
|
function sshKeygenAvailable() {
|
|
@@ -12665,9 +12797,9 @@ function syncAllPassengerCheckouts(cfg) {
|
|
|
12665
12797
|
|
|
12666
12798
|
// src/tunnel.ts
|
|
12667
12799
|
import { spawn as spawn3 } from "child_process";
|
|
12668
|
-
import { existsSync as
|
|
12800
|
+
import { existsSync as existsSync23, mkdirSync as mkdirSync10, readFileSync as readFileSync16, writeFileSync as writeFileSync11, unlinkSync as unlinkSync3, readdirSync as readdirSync7 } from "fs";
|
|
12669
12801
|
import { randomInt } from "crypto";
|
|
12670
|
-
import { join as
|
|
12802
|
+
import { join as join22 } from "path";
|
|
12671
12803
|
var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
|
|
12672
12804
|
var defaultSpawn = (cmd, args) => spawn3(cmd, args, DEFAULT_SPAWN_OPTIONS);
|
|
12673
12805
|
var SLUG_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
@@ -12734,20 +12866,20 @@ function startTunnel(opts) {
|
|
|
12734
12866
|
};
|
|
12735
12867
|
}
|
|
12736
12868
|
function tunnelStatePath(stateDir, workspaceId) {
|
|
12737
|
-
return
|
|
12869
|
+
return join22(stateDir, "passengers", `${workspaceId}.json`);
|
|
12738
12870
|
}
|
|
12739
12871
|
function readPersistedTunnel(stateDir, workspaceId) {
|
|
12740
12872
|
const path = tunnelStatePath(stateDir, workspaceId);
|
|
12741
|
-
if (!
|
|
12873
|
+
if (!existsSync23(path)) return void 0;
|
|
12742
12874
|
try {
|
|
12743
|
-
return JSON.parse(
|
|
12875
|
+
return JSON.parse(readFileSync16(path, "utf8"));
|
|
12744
12876
|
} catch {
|
|
12745
12877
|
return void 0;
|
|
12746
12878
|
}
|
|
12747
12879
|
}
|
|
12748
12880
|
function writePersistedTunnel(stateDir, state) {
|
|
12749
12881
|
const path = tunnelStatePath(stateDir, state.workspaceId);
|
|
12750
|
-
mkdirSync10(
|
|
12882
|
+
mkdirSync10(join22(stateDir, "passengers"), { recursive: true });
|
|
12751
12883
|
writeFileSync11(path, `${JSON.stringify(state, null, 2)}
|
|
12752
12884
|
`);
|
|
12753
12885
|
}
|
|
@@ -12845,9 +12977,9 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
|
|
|
12845
12977
|
}
|
|
12846
12978
|
}
|
|
12847
12979
|
function readAllPersistedWorkspaceIds(stateDir) {
|
|
12848
|
-
const dir =
|
|
12849
|
-
if (!
|
|
12850
|
-
return
|
|
12980
|
+
const dir = join22(stateDir, "passengers");
|
|
12981
|
+
if (!existsSync23(dir)) return [];
|
|
12982
|
+
return readdirSync7(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
|
|
12851
12983
|
}
|
|
12852
12984
|
|
|
12853
12985
|
// src/mcp-endpoint-registry.ts
|
|
@@ -12967,8 +13099,8 @@ async function unregisterEndpoint(workspaceId, info, d) {
|
|
|
12967
13099
|
import { createServer as createHttpServer } from "http";
|
|
12968
13100
|
import { spawn as spawn4 } from "child_process";
|
|
12969
13101
|
import { randomBytes } from "crypto";
|
|
12970
|
-
import { existsSync as
|
|
12971
|
-
import { join as
|
|
13102
|
+
import { existsSync as existsSync24, mkdirSync as mkdirSync11, readFileSync as readFileSync17, writeFileSync as writeFileSync12, unlinkSync as unlinkSync4, readdirSync as readdirSync8 } from "fs";
|
|
13103
|
+
import { join as join23 } from "path";
|
|
12972
13104
|
|
|
12973
13105
|
// src/secret-compare.ts
|
|
12974
13106
|
import { timingSafeEqual } from "crypto";
|
|
@@ -13057,20 +13189,20 @@ var defaultSpawn2 = (cmd, args, env) => {
|
|
|
13057
13189
|
return child;
|
|
13058
13190
|
};
|
|
13059
13191
|
function syncDaemonStatePath(stateDir, workspaceId) {
|
|
13060
|
-
return
|
|
13192
|
+
return join23(stateDir, "passengers", `${workspaceId}-sync.json`);
|
|
13061
13193
|
}
|
|
13062
13194
|
function readPersistedSyncDaemon(stateDir, workspaceId) {
|
|
13063
13195
|
const path = syncDaemonStatePath(stateDir, workspaceId);
|
|
13064
|
-
if (!
|
|
13196
|
+
if (!existsSync24(path)) return void 0;
|
|
13065
13197
|
try {
|
|
13066
|
-
return JSON.parse(
|
|
13198
|
+
return JSON.parse(readFileSync17(path, "utf8"));
|
|
13067
13199
|
} catch {
|
|
13068
13200
|
return void 0;
|
|
13069
13201
|
}
|
|
13070
13202
|
}
|
|
13071
13203
|
function writePersistedSyncDaemon(stateDir, state) {
|
|
13072
13204
|
const path = syncDaemonStatePath(stateDir, state.workspaceId);
|
|
13073
|
-
mkdirSync11(
|
|
13205
|
+
mkdirSync11(join23(stateDir, "passengers"), { recursive: true });
|
|
13074
13206
|
writeFileSync12(path, `${JSON.stringify(state, null, 2)}
|
|
13075
13207
|
`);
|
|
13076
13208
|
}
|
|
@@ -13091,10 +13223,10 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
|
|
|
13091
13223
|
}
|
|
13092
13224
|
}
|
|
13093
13225
|
function readAllPersistedSyncWorkspaceIds(stateDir) {
|
|
13094
|
-
const dir =
|
|
13095
|
-
if (!
|
|
13226
|
+
const dir = join23(stateDir, "passengers");
|
|
13227
|
+
if (!existsSync24(dir)) return [];
|
|
13096
13228
|
const suffix = "-sync.json";
|
|
13097
|
-
return
|
|
13229
|
+
return readdirSync8(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
|
|
13098
13230
|
}
|
|
13099
13231
|
function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
|
|
13100
13232
|
const alive = deps.isPidAlive ?? isPidAlive;
|
|
@@ -13149,12 +13281,12 @@ function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
|
|
|
13149
13281
|
}
|
|
13150
13282
|
|
|
13151
13283
|
// src/daemon.ts
|
|
13152
|
-
import { existsSync as
|
|
13153
|
-
import { join as
|
|
13284
|
+
import { existsSync as existsSync26, mkdirSync as mkdirSync12, readFileSync as readFileSync19, readdirSync as readdirSync10, rmSync as rmSync9, statSync as statSync8, writeFileSync as writeFileSync13 } from "fs";
|
|
13285
|
+
import { join as join25 } from "path";
|
|
13154
13286
|
|
|
13155
13287
|
// src/interrupted.ts
|
|
13156
|
-
import { existsSync as
|
|
13157
|
-
import { join as
|
|
13288
|
+
import { existsSync as existsSync25, readdirSync as readdirSync9, readFileSync as readFileSync18 } from "fs";
|
|
13289
|
+
import { join as join24 } from "path";
|
|
13158
13290
|
var INTERRUPTED_MARKER = "<!-- crew:interrupted -->";
|
|
13159
13291
|
function interruptedComment(b) {
|
|
13160
13292
|
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.";
|
|
@@ -13172,21 +13304,21 @@ var pidAlive = (pid) => {
|
|
|
13172
13304
|
};
|
|
13173
13305
|
function runFinished(streamPath) {
|
|
13174
13306
|
const path = `${streamPath}.meta.json`;
|
|
13175
|
-
if (!
|
|
13307
|
+
if (!existsSync25(path)) return false;
|
|
13176
13308
|
try {
|
|
13177
|
-
return JSON.parse(
|
|
13309
|
+
return JSON.parse(readFileSync18(path, "utf8")).running !== true;
|
|
13178
13310
|
} catch {
|
|
13179
13311
|
return true;
|
|
13180
13312
|
}
|
|
13181
13313
|
}
|
|
13182
13314
|
function orphanedStreams(o) {
|
|
13183
|
-
const dir =
|
|
13184
|
-
if (!
|
|
13315
|
+
const dir = join24(o.stateDir, "streams");
|
|
13316
|
+
if (!existsSync25(dir)) return [];
|
|
13185
13317
|
const ownerAlive = o.isOwnerAlive ?? pidAlive;
|
|
13186
13318
|
const out = [];
|
|
13187
|
-
for (const name of
|
|
13319
|
+
for (const name of readdirSync9(dir)) {
|
|
13188
13320
|
if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
|
|
13189
|
-
const streamPath =
|
|
13321
|
+
const streamPath = join24(dir, name);
|
|
13190
13322
|
if (runFinished(streamPath)) continue;
|
|
13191
13323
|
const sidecar = readRunSidecar(streamPath);
|
|
13192
13324
|
if (!sidecar || sidecar.route !== o.routeLabel) continue;
|
|
@@ -13239,7 +13371,7 @@ async function reconcileRuns(o) {
|
|
|
13239
13371
|
// src/daemon.ts
|
|
13240
13372
|
var DEFAULT_FLOOR_MS = 5e3;
|
|
13241
13373
|
var DEFAULT_CEILING_MS = INTERVAL_SECONDS * 1e3;
|
|
13242
|
-
var defaultSleep = (ms2) => new Promise((
|
|
13374
|
+
var defaultSleep = (ms2) => new Promise((resolve6) => setTimeout(resolve6, ms2));
|
|
13243
13375
|
async function runDaemonLoop(o) {
|
|
13244
13376
|
const floor = o.floorMs ?? DEFAULT_FLOOR_MS;
|
|
13245
13377
|
const ceiling = o.ceilingMs ?? DEFAULT_CEILING_MS;
|
|
@@ -13285,8 +13417,8 @@ function latestMtime(path) {
|
|
|
13285
13417
|
if (stat.isFile()) return stat.mtimeMs;
|
|
13286
13418
|
if (!stat.isDirectory()) return 0;
|
|
13287
13419
|
let latest = stat.mtimeMs;
|
|
13288
|
-
for (const entry of
|
|
13289
|
-
latest = Math.max(latest, latestMtime(
|
|
13420
|
+
for (const entry of readdirSync10(path, { withFileTypes: true })) {
|
|
13421
|
+
latest = Math.max(latest, latestMtime(join25(path, entry.name)));
|
|
13290
13422
|
}
|
|
13291
13423
|
return latest;
|
|
13292
13424
|
}
|
|
@@ -13295,13 +13427,13 @@ function computeTreeSignature(paths) {
|
|
|
13295
13427
|
}
|
|
13296
13428
|
function makeStaleChecker(paths, opts = {}) {
|
|
13297
13429
|
const initial = computeTreeSignature(paths);
|
|
13298
|
-
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 &&
|
|
13430
|
+
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync26(opts.reloadSentinel);
|
|
13299
13431
|
}
|
|
13300
13432
|
function reloadSentinelFor(stateDir) {
|
|
13301
|
-
return
|
|
13433
|
+
return join25(stateDir, "reload-requested");
|
|
13302
13434
|
}
|
|
13303
13435
|
function daemonStampFor(stateDir) {
|
|
13304
|
-
return
|
|
13436
|
+
return join25(stateDir, "daemon.started");
|
|
13305
13437
|
}
|
|
13306
13438
|
function noteDaemonStarted(stateDir, now = Date.now()) {
|
|
13307
13439
|
mkdirSync12(stateDir, { recursive: true });
|
|
@@ -13311,7 +13443,7 @@ function noteDaemonStarted(stateDir, now = Date.now()) {
|
|
|
13311
13443
|
}
|
|
13312
13444
|
function readDaemonStarted(stateDir) {
|
|
13313
13445
|
try {
|
|
13314
|
-
const v = JSON.parse(
|
|
13446
|
+
const v = JSON.parse(readFileSync19(daemonStampFor(stateDir), "utf8")).startedAt;
|
|
13315
13447
|
return typeof v === "number" ? v : null;
|
|
13316
13448
|
} catch {
|
|
13317
13449
|
return null;
|
|
@@ -13667,7 +13799,7 @@ async function buildEnvironment(route, ship, ticket) {
|
|
|
13667
13799
|
}
|
|
13668
13800
|
|
|
13669
13801
|
// src/cli.ts
|
|
13670
|
-
var CREW_HOME = isCompiledBinary(import.meta.url) ?
|
|
13802
|
+
var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname13(process.execPath) : resolve5(dirname13(fileURLToPath(import.meta.url)), "..");
|
|
13671
13803
|
var RELEASE_LOCK_TTL_MS = 20 * 60 * 1e3;
|
|
13672
13804
|
var CYCLE_SECONDS2 = 120;
|
|
13673
13805
|
var STALE_CYCLES = 3;
|
|
@@ -14185,8 +14317,8 @@ if (command === "passenger-sync-daemon") {
|
|
|
14185
14317
|
}
|
|
14186
14318
|
if (host === "linux" && hasSystemd) {
|
|
14187
14319
|
const label = labelFor(CREW_HOME, "run");
|
|
14188
|
-
const staleTimer =
|
|
14189
|
-
if (
|
|
14320
|
+
const staleTimer = join26(homedir8(), ".config", "systemd", "user", `${label}.timer`);
|
|
14321
|
+
if (existsSync27(staleTimer)) {
|
|
14190
14322
|
process.stdout.write(
|
|
14191
14323
|
`removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
|
|
14192
14324
|
`
|
|
@@ -14212,7 +14344,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
14212
14344
|
const tracker = new Tracker(c, cfg.ship);
|
|
14213
14345
|
problems.push(...validateContract(tracker.contract).map((p) => `contract: ${p}`));
|
|
14214
14346
|
for (const r of await resolvedRepos(c)) {
|
|
14215
|
-
if (!
|
|
14347
|
+
if (!existsSync27(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
|
|
14216
14348
|
if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
|
|
14217
14349
|
problems.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
|
|
14218
14350
|
}
|
|
@@ -14230,7 +14362,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
14230
14362
|
}
|
|
14231
14363
|
async function doctorFix(o) {
|
|
14232
14364
|
const routes = await Promise.all(routesInScope(cfg.routes, named).map((c) => gatherHealth({ route: c.route, enabled: c.enabled, hasRepos: reposOf(c).length > 0 }, () => routeProblems(c))));
|
|
14233
|
-
const unitExists = (p) =>
|
|
14365
|
+
const unitExists = (p) => existsSync27(p);
|
|
14234
14366
|
const crontab = o.host === "linux" ? spawnSync6("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
|
|
14235
14367
|
const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
|
|
14236
14368
|
const plan = planFix({ routes, schedulerAbsent, host: o.host });
|
|
@@ -14247,7 +14379,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
14247
14379
|
for (const a of plan.actions) {
|
|
14248
14380
|
if (a.kind === "enable") {
|
|
14249
14381
|
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(
|
|
14382
|
+
writeFileSync14(cfg.configFile, planEnable(readFileSync20(cfg.configFile, "utf8"), a.route));
|
|
14251
14383
|
enabledSound.add(a.route);
|
|
14252
14384
|
changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
|
|
14253
14385
|
} else {
|
|
@@ -14327,6 +14459,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
14327
14459
|
}
|
|
14328
14460
|
async function sweepClosedTickets(c, target, branch, tracker, dryRun2, remit, idle = false) {
|
|
14329
14461
|
try {
|
|
14462
|
+
if (!dryRun2) reconcileHeldWorktrees(state, target.name, remit);
|
|
14330
14463
|
if (idle && !sweepCandidatesExist(target.dir, branch.base)) return;
|
|
14331
14464
|
const truncated = (limit) => remit.warn(`ticket lookup for the sweep hit its ${limit}-row limit; some tickets may be missed`, { step: "worktree" });
|
|
14332
14465
|
const nums = /* @__PURE__ */ new Set([
|
|
@@ -14336,11 +14469,14 @@ ${changed2.map((c) => ` - ${c}
|
|
|
14336
14469
|
]);
|
|
14337
14470
|
const scoped = ticketsByRepo(c, await tracker.ticketsByNumbers([...nums], truncated)).byRepo.get(target.name) ?? [];
|
|
14338
14471
|
const actions = planWorktreeSweep(target, scoped, tracker.contract);
|
|
14472
|
+
const orphanDirs = orphanedWorktreeDirs(target, scoped, tracker.contract);
|
|
14473
|
+
warnOrphanedWorktreeDirs(orphanDirs, remit, dryRun2 ? void 0 : { state, repo: target.name });
|
|
14339
14474
|
if (actions.length) {
|
|
14340
14475
|
remit.enter("worktree");
|
|
14341
|
-
const r = await applyWorktreeSweep(target.dir, actions, dryRun2, remit, branch.remote);
|
|
14476
|
+
const r = await applyWorktreeSweep(target.dir, actions, dryRun2, remit, branch.remote, branch.base, { state, repo: target.name });
|
|
14342
14477
|
remit.emit(`swept ${r.removed} worktree(s), kept ${r.keptBranches} branch(es)`, { step: "worktree" });
|
|
14343
|
-
|
|
14478
|
+
if (!dryRun2) reconcileHeldWorktrees(state, target.name, remit, /* @__PURE__ */ new Set([...r.refused, ...orphanDirs]));
|
|
14479
|
+
} else if (!dryRun2) reconcileHeldWorktrees(state, target.name, remit, new Set(orphanDirs));
|
|
14344
14480
|
const orphaned = planLocalBranchCleanup(target.dir, branch.base, scoped, tracker.contract);
|
|
14345
14481
|
if (orphaned.length) applyLocalBranchCleanup(target.dir, orphaned, dryRun2, remit);
|
|
14346
14482
|
const stale = planRemoteBranchCleanup(target.dir, branch.remote, branch.base, scoped, tracker.contract);
|
|
@@ -15223,7 +15359,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
15223
15359
|
process.exit(1);
|
|
15224
15360
|
}
|
|
15225
15361
|
requireArmed("run the daemon loop");
|
|
15226
|
-
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [
|
|
15362
|
+
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join26(CREW_HOME, "src"), join26(CREW_HOME, "dist"), join26(CREW_HOME, "bin")];
|
|
15227
15363
|
const configPaths = daemonConfigPaths(cfg);
|
|
15228
15364
|
watchPaths.push(...configPaths);
|
|
15229
15365
|
noteDaemonStarted(cfg.ship.stateDir);
|
|
@@ -15398,7 +15534,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
15398
15534
|
`);
|
|
15399
15535
|
if (!dryRun) {
|
|
15400
15536
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
15401
|
-
const raw =
|
|
15537
|
+
const raw = existsSync27(resolvedPath) ? JSON.parse(readFileSync20(resolvedPath, "utf8")) : {};
|
|
15402
15538
|
raw.agentPersonas = personaResult.agentPersonas;
|
|
15403
15539
|
if (skillResult) raw.agentSkills = skillResult.agentSkills;
|
|
15404
15540
|
if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
|
|
@@ -15468,7 +15604,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
15468
15604
|
`);
|
|
15469
15605
|
if (!dryRun) {
|
|
15470
15606
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
15471
|
-
const raw =
|
|
15607
|
+
const raw = existsSync27(resolvedPath) ? JSON.parse(readFileSync20(resolvedPath, "utf8")) : {};
|
|
15472
15608
|
raw.agentSkills = result.agentSkills;
|
|
15473
15609
|
mkdirSync13(dirOf(resolvedPath), { recursive: true });
|
|
15474
15610
|
writeFileSync14(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
@@ -15484,14 +15620,14 @@ ${changed2.map((c) => ` - ${c}
|
|
|
15484
15620
|
process.exit(2);
|
|
15485
15621
|
};
|
|
15486
15622
|
if (sub !== "add" && sub !== "list") usage2();
|
|
15487
|
-
const configText =
|
|
15623
|
+
const configText = readFileSync20(cfg.configFile, "utf8");
|
|
15488
15624
|
let routeName = positional[2];
|
|
15489
15625
|
let addPath = ".";
|
|
15490
15626
|
let addName;
|
|
15491
15627
|
if (sub === "list" && !routeName) usage2();
|
|
15492
15628
|
if (sub === "add") {
|
|
15493
15629
|
try {
|
|
15494
|
-
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) =>
|
|
15630
|
+
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync27(resolvePath(p.replace(/^~(?=\/|$)/, homedir8()))));
|
|
15495
15631
|
routeName = a.route;
|
|
15496
15632
|
addPath = a.path;
|
|
15497
15633
|
addName = a.name;
|
|
@@ -15506,15 +15642,15 @@ ${changed2.map((c) => ` - ${c}
|
|
|
15506
15642
|
}
|
|
15507
15643
|
const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
|
|
15508
15644
|
let resolvedRemotes;
|
|
15509
|
-
if (
|
|
15645
|
+
if (existsSync27(resolvedFile)) {
|
|
15510
15646
|
try {
|
|
15511
|
-
resolvedRemotes = JSON.parse(
|
|
15647
|
+
resolvedRemotes = JSON.parse(readFileSync20(resolvedFile, "utf8"));
|
|
15512
15648
|
} catch {
|
|
15513
15649
|
}
|
|
15514
15650
|
}
|
|
15515
15651
|
const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
|
|
15516
15652
|
const text = configText;
|
|
15517
|
-
const base =
|
|
15653
|
+
const base = dirname13(cfg.configFile);
|
|
15518
15654
|
try {
|
|
15519
15655
|
if (sub === "list") {
|
|
15520
15656
|
const repos = configuredRepos(text, routeName, base);
|
|
@@ -15523,14 +15659,14 @@ ${changed2.map((c) => ` - ${c}
|
|
|
15523
15659
|
for (const r of repos) {
|
|
15524
15660
|
const origin2 = originOf(r.dir);
|
|
15525
15661
|
const row = matchRepoName(origin2, resolvedRemotes);
|
|
15526
|
-
const note = !
|
|
15662
|
+
const note = !existsSync27(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
|
|
15527
15663
|
process.stdout.write(`${r.name} ${r.dir} ${note}
|
|
15528
15664
|
`);
|
|
15529
15665
|
}
|
|
15530
15666
|
break;
|
|
15531
15667
|
}
|
|
15532
15668
|
const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir8()));
|
|
15533
|
-
const top =
|
|
15669
|
+
const top = existsSync27(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
|
|
15534
15670
|
if (!top) throw new ReposError(`${target} is not a git checkout`);
|
|
15535
15671
|
const origin = originOf(top);
|
|
15536
15672
|
const plan = planRepoAdd({
|
|
@@ -15579,8 +15715,8 @@ ${changed2.map((c) => ` - ${c}
|
|
|
15579
15715
|
const created = await rowTracker.createRepoRow(rowPayload);
|
|
15580
15716
|
process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
|
|
15581
15717
|
`);
|
|
15582
|
-
if (
|
|
15583
|
-
const raw = JSON.parse(
|
|
15718
|
+
if (existsSync27(resolvedFile)) {
|
|
15719
|
+
const raw = JSON.parse(readFileSync20(resolvedFile, "utf8"));
|
|
15584
15720
|
raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
|
|
15585
15721
|
raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
|
|
15586
15722
|
writeFileSync14(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
@@ -15834,10 +15970,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
15834
15970
|
}
|
|
15835
15971
|
}
|
|
15836
15972
|
let previous;
|
|
15837
|
-
const hadPreviousFile =
|
|
15973
|
+
const hadPreviousFile = existsSync27(resolvedPath);
|
|
15838
15974
|
if (hadPreviousFile) {
|
|
15839
15975
|
try {
|
|
15840
|
-
previous = JSON.parse(
|
|
15976
|
+
previous = JSON.parse(readFileSync20(resolvedPath, "utf8"));
|
|
15841
15977
|
} catch {
|
|
15842
15978
|
}
|
|
15843
15979
|
}
|
|
@@ -15900,7 +16036,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
15900
16036
|
process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
|
|
15901
16037
|
`);
|
|
15902
16038
|
} else {
|
|
15903
|
-
mkdirSync13(
|
|
16039
|
+
mkdirSync13(dirname13(resolvedPath), { recursive: true });
|
|
15904
16040
|
if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
|
|
15905
16041
|
const tmpPath = `${resolvedPath}.tmp${process.pid}`;
|
|
15906
16042
|
writeFileSync14(tmpPath, `${JSON.stringify(resolved, null, 2)}
|
|
@@ -15928,7 +16064,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
15928
16064
|
process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
|
|
15929
16065
|
`);
|
|
15930
16066
|
} else {
|
|
15931
|
-
mkdirSync13(
|
|
16067
|
+
mkdirSync13(dirname13(apiKeyFile), { recursive: true });
|
|
15932
16068
|
writeFileSync14(apiKeyFile, `${apiKeyVar}=${apiKey}
|
|
15933
16069
|
`, { mode: 384 });
|
|
15934
16070
|
process.stderr.write(`API key written to ${apiKeyFile}
|
|
@@ -15938,7 +16074,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
15938
16074
|
const keyPersisted = keychainBacked || apiKeyFile !== void 0;
|
|
15939
16075
|
const wizard = firstRun;
|
|
15940
16076
|
const writeFirstRun = wizard !== void 0 && !dryRun && keyPersisted;
|
|
15941
|
-
const routeDir = value("dir") ?? (writeFirstRun ?
|
|
16077
|
+
const routeDir = value("dir") ?? (writeFirstRun ? join26(DEFAULT_REPOS_BASE_PATH, found.workspaceSlug) : void 0);
|
|
15942
16078
|
const routeBlock = renderConnection(
|
|
15943
16079
|
found,
|
|
15944
16080
|
route2,
|
|
@@ -16130,14 +16266,20 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
16130
16266
|
await tracker.ticketsByNumbers(remoteBranchTicketNumbers(r.dir, r.config.branch.remote, r.config.branch.base), truncated)
|
|
16131
16267
|
).byRepo.get(r.name) ?? [];
|
|
16132
16268
|
const actions = planWorktreeSweep(r, wtTickets, tracker.contract);
|
|
16269
|
+
const orphanDirs = orphanedWorktreeDirs(r, wtTickets, tracker.contract);
|
|
16270
|
+
warnOrphanedWorktreeDirs(orphanDirs, emit, dryRun ? void 0 : { state, repo: r.name });
|
|
16133
16271
|
const stale = planRemoteBranchCleanup(r.dir, r.config.branch.remote, r.config.branch.base, brTickets, tracker.contract);
|
|
16134
16272
|
if (stale.length) {
|
|
16135
16273
|
anything = true;
|
|
16136
16274
|
applyRemoteBranchCleanup(r.dir, r.config.branch.remote, stale, dryRun, emit);
|
|
16137
16275
|
}
|
|
16138
|
-
if (!actions.length)
|
|
16276
|
+
if (!actions.length) {
|
|
16277
|
+
if (!dryRun) reconcileHeldWorktrees(state, r.name, emit, new Set(orphanDirs));
|
|
16278
|
+
continue;
|
|
16279
|
+
}
|
|
16139
16280
|
anything = true;
|
|
16140
|
-
const res = await applyWorktreeSweep(r.dir, actions, dryRun, emit, r.config.branch.remote);
|
|
16281
|
+
const res = await applyWorktreeSweep(r.dir, actions, dryRun, emit, r.config.branch.remote, r.config.branch.base, { state, repo: r.name });
|
|
16282
|
+
if (!dryRun) reconcileHeldWorktrees(state, r.name, emit, /* @__PURE__ */ new Set([...res.refused, ...orphanDirs]));
|
|
16141
16283
|
emit.emit(
|
|
16142
16284
|
`swept ${res.removed} worktree(s) for ${r.name}, kept ${res.keptBranches} branch(es)`,
|
|
16143
16285
|
{ step: "worktree" }
|
|
@@ -16530,7 +16672,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
16530
16672
|
}
|
|
16531
16673
|
case "log": {
|
|
16532
16674
|
try {
|
|
16533
|
-
const text =
|
|
16675
|
+
const text = readFileSync20(cfg.ship.logFile, "utf8").split("\n");
|
|
16534
16676
|
process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
|
|
16535
16677
|
`);
|
|
16536
16678
|
} catch {
|
|
@@ -16641,6 +16783,14 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
16641
16783
|
// Additive (CREW-1379): `unsafe` means a ship must not cut a
|
|
16642
16784
|
// worktree or release from this base. `version` stays 1.
|
|
16643
16785
|
baseBranch: baseHealth(r),
|
|
16786
|
+
// Additive (CREW-2051): worktrees the sweep refuses to remove.
|
|
16787
|
+
heldWorktrees: openShipAttention(state).filter((i) => i.kind === "worktree_held" && i.held?.repo === r.name).map((i) => ({
|
|
16788
|
+
path: i.held.path,
|
|
16789
|
+
ticket: i.held.ticket,
|
|
16790
|
+
status: i.held.status,
|
|
16791
|
+
reason: i.held.reason,
|
|
16792
|
+
firstSeenAt: i.held.firstSeenAt
|
|
16793
|
+
})),
|
|
16644
16794
|
worktreePrefix: r.config.worktrees.prefix,
|
|
16645
16795
|
// A repo with no explicit `worktrees.prefix` no longer names its
|
|
16646
16796
|
// worktrees `<prefix><number>` (ISSUE-969) — the checkout's own
|
|
@@ -16740,7 +16890,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
16740
16890
|
);
|
|
16741
16891
|
try {
|
|
16742
16892
|
const evFile = eventFileFor(cfg.ship.stateDir);
|
|
16743
|
-
const evSize =
|
|
16893
|
+
const evSize = existsSync27(evFile) ? statSync9(evFile).size : 0;
|
|
16744
16894
|
process.stdout.write(
|
|
16745
16895
|
evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
|
|
16746
16896
|
` : "events log: ok\n"
|
|
@@ -16751,7 +16901,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
16751
16901
|
const unitPlan = planInstall(cfg.ship, CREW_HOME, host, detectSystemd(), "run");
|
|
16752
16902
|
const abandons = unitAbandonsAgents(unitPlan, (p) => {
|
|
16753
16903
|
try {
|
|
16754
|
-
return
|
|
16904
|
+
return readFileSync20(p, "utf8");
|
|
16755
16905
|
} catch {
|
|
16756
16906
|
return null;
|
|
16757
16907
|
}
|
package/package.json
CHANGED