@tablation/crew 0.53.3 → 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 +140 -77
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -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
|
}
|
|
@@ -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,7 +12220,7 @@ function schedulerInstalled(plan, exists, crontab) {
|
|
|
12182
12220
|
}
|
|
12183
12221
|
|
|
12184
12222
|
// src/worktree-sweep.ts
|
|
12185
|
-
import { existsSync as
|
|
12223
|
+
import { existsSync as existsSync20, readFileSync as readFileSync14, readdirSync as readdirSync5 } from "fs";
|
|
12186
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));
|
|
@@ -12261,16 +12299,31 @@ function orphanedWorktreeDirs(target, tickets, contract) {
|
|
|
12261
12299
|
);
|
|
12262
12300
|
return siblingWorktreeDirs(target).filter((d) => deployed.has(d.number)).map((d) => d.path);
|
|
12263
12301
|
}
|
|
12264
|
-
function warnOrphanedWorktreeDirs(paths, log) {
|
|
12302
|
+
function warnOrphanedWorktreeDirs(paths, log, held) {
|
|
12265
12303
|
for (const p of paths) {
|
|
12266
|
-
|
|
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
|
+
}
|
|
12267
12309
|
}
|
|
12268
12310
|
}
|
|
12269
|
-
async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin", base) {
|
|
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
|
+
};
|
|
12270
12323
|
let removed = 0;
|
|
12271
12324
|
let keptBranches = 0;
|
|
12272
12325
|
for (const a of actions) {
|
|
12273
|
-
if (!dryRun && base && !a.keepBranch &&
|
|
12326
|
+
if (!dryRun && base && !a.keepBranch && existsSync20(a.path) && isHandBackMerge(a.path, base)) {
|
|
12274
12327
|
if (gitOk(a.path, ["merge", "--abort"]) !== null) {
|
|
12275
12328
|
log.emit(
|
|
12276
12329
|
`abandoned the hand-back merge of ${base} in ${a.path}: ${a.ticket.issue_id} deployed without it`,
|
|
@@ -12278,25 +12331,19 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
|
|
|
12278
12331
|
);
|
|
12279
12332
|
}
|
|
12280
12333
|
}
|
|
12281
|
-
if (!dryRun &&
|
|
12334
|
+
if (!dryRun && existsSync20(a.path)) {
|
|
12282
12335
|
let dirty = false;
|
|
12283
12336
|
try {
|
|
12284
12337
|
dirty = status(a.path).length > 0;
|
|
12285
12338
|
} catch {
|
|
12286
12339
|
}
|
|
12287
12340
|
if (dirty) {
|
|
12288
|
-
|
|
12289
|
-
`not removing worktree ${a.path} for ${a.ticket.issue_id} (${a.ticket.status}): it has uncommitted changes`,
|
|
12290
|
-
{ ticket: a.ticket.issue_id, step: "worktree" }
|
|
12291
|
-
);
|
|
12341
|
+
refuse(a, "uncommitted changes", "it has uncommitted changes");
|
|
12292
12342
|
continue;
|
|
12293
12343
|
}
|
|
12294
12344
|
}
|
|
12295
|
-
if (!dryRun && a.requirePushed && a.branch &&
|
|
12296
|
-
|
|
12297
|
-
`not removing worktree ${a.path} for ${a.ticket.issue_id} (${a.ticket.status}): ${a.branch} has commits that are not on ${remote}`,
|
|
12298
|
-
{ ticket: a.ticket.issue_id, step: "worktree" }
|
|
12299
|
-
);
|
|
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}`);
|
|
12300
12347
|
continue;
|
|
12301
12348
|
}
|
|
12302
12349
|
const n = Number(a.ticket.issue_id.replace(/^\D+/, ""));
|
|
@@ -12320,7 +12367,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
|
|
|
12320
12367
|
try {
|
|
12321
12368
|
git(cwd, ["worktree", "remove", "--force", a.path]);
|
|
12322
12369
|
} catch (e) {
|
|
12323
|
-
|
|
12370
|
+
refuse(a, "removal failed", `could not remove it: ${e.message}`);
|
|
12324
12371
|
continue;
|
|
12325
12372
|
}
|
|
12326
12373
|
if (!a.keepBranch && a.branch) {
|
|
@@ -12336,7 +12383,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
|
|
|
12336
12383
|
removed++;
|
|
12337
12384
|
}
|
|
12338
12385
|
if (!dryRun && actions.length) gitOk(cwd, ["worktree", "prune"]);
|
|
12339
|
-
return { removed, keptBranches };
|
|
12386
|
+
return { removed, keptBranches, refused };
|
|
12340
12387
|
}
|
|
12341
12388
|
function isHandBackMerge(path, base) {
|
|
12342
12389
|
if (gitOk(path, ["rev-parse", "-q", "--verify", "MERGE_HEAD"]) === null) return false;
|
|
@@ -12449,12 +12496,12 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
|
|
|
12449
12496
|
}
|
|
12450
12497
|
|
|
12451
12498
|
// src/stream-sweep.ts
|
|
12452
|
-
import { existsSync as
|
|
12499
|
+
import { existsSync as existsSync21, readdirSync as readdirSync6, rmSync as rmSync8, statSync as statSync7 } from "fs";
|
|
12453
12500
|
import { join as join20 } from "path";
|
|
12454
12501
|
var DAY_MS2 = 24 * 60 * 60 * 1e3;
|
|
12455
12502
|
function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
|
|
12456
12503
|
const dir = join20(stateDir, "streams");
|
|
12457
|
-
if (!
|
|
12504
|
+
if (!existsSync21(dir)) return [];
|
|
12458
12505
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
12459
12506
|
return readdirSync6(dir).map((name) => join20(dir, name)).filter((path) => {
|
|
12460
12507
|
try {
|
|
@@ -12482,7 +12529,7 @@ function applyStreamSweep(paths, dryRun, log) {
|
|
|
12482
12529
|
return removed;
|
|
12483
12530
|
}
|
|
12484
12531
|
function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
12485
|
-
if (!
|
|
12532
|
+
if (!existsSync21(stateDir)) return [];
|
|
12486
12533
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
12487
12534
|
return readdirSync6(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join20(stateDir, name)).filter((path) => {
|
|
12488
12535
|
try {
|
|
@@ -12494,13 +12541,13 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
|
12494
12541
|
}
|
|
12495
12542
|
|
|
12496
12543
|
// src/cli.ts
|
|
12497
|
-
import { readFileSync as readFileSync20, existsSync 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";
|
|
12498
12545
|
import { createInterface } from "readline/promises";
|
|
12499
12546
|
import { dirname as dirOf, resolve as resolvePath } from "path";
|
|
12500
12547
|
|
|
12501
12548
|
// src/ssh-keys.ts
|
|
12502
12549
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
12503
|
-
import { existsSync as
|
|
12550
|
+
import { existsSync as existsSync22, mkdirSync as mkdirSync9, readFileSync as readFileSync15, chmodSync } from "fs";
|
|
12504
12551
|
import { dirname as dirname12, join as join21 } from "path";
|
|
12505
12552
|
var SshKeyError = class extends Error {
|
|
12506
12553
|
};
|
|
@@ -12510,7 +12557,7 @@ function shipSshKeyPathFor(stateDir) {
|
|
|
12510
12557
|
function ensureShipSshKeypair(stateDir) {
|
|
12511
12558
|
const privateKeyPath = shipSshKeyPathFor(stateDir);
|
|
12512
12559
|
const publicKeyPath = `${privateKeyPath}.pub`;
|
|
12513
|
-
if (!
|
|
12560
|
+
if (!existsSync22(privateKeyPath)) {
|
|
12514
12561
|
mkdirSync9(dirname12(privateKeyPath), { recursive: true });
|
|
12515
12562
|
try {
|
|
12516
12563
|
execFileSync3("ssh-keygen", [
|
|
@@ -12531,7 +12578,7 @@ function ensureShipSshKeypair(stateDir) {
|
|
|
12531
12578
|
}
|
|
12532
12579
|
chmodSync(privateKeyPath, 384);
|
|
12533
12580
|
}
|
|
12534
|
-
if (!
|
|
12581
|
+
if (!existsSync22(publicKeyPath)) {
|
|
12535
12582
|
throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
|
|
12536
12583
|
}
|
|
12537
12584
|
const publicKey = readFileSync15(publicKeyPath, "utf8").trim();
|
|
@@ -12750,7 +12797,7 @@ function syncAllPassengerCheckouts(cfg) {
|
|
|
12750
12797
|
|
|
12751
12798
|
// src/tunnel.ts
|
|
12752
12799
|
import { spawn as spawn3 } from "child_process";
|
|
12753
|
-
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";
|
|
12754
12801
|
import { randomInt } from "crypto";
|
|
12755
12802
|
import { join as join22 } from "path";
|
|
12756
12803
|
var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
|
|
@@ -12823,7 +12870,7 @@ function tunnelStatePath(stateDir, workspaceId) {
|
|
|
12823
12870
|
}
|
|
12824
12871
|
function readPersistedTunnel(stateDir, workspaceId) {
|
|
12825
12872
|
const path = tunnelStatePath(stateDir, workspaceId);
|
|
12826
|
-
if (!
|
|
12873
|
+
if (!existsSync23(path)) return void 0;
|
|
12827
12874
|
try {
|
|
12828
12875
|
return JSON.parse(readFileSync16(path, "utf8"));
|
|
12829
12876
|
} catch {
|
|
@@ -12931,7 +12978,7 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
|
|
|
12931
12978
|
}
|
|
12932
12979
|
function readAllPersistedWorkspaceIds(stateDir) {
|
|
12933
12980
|
const dir = join22(stateDir, "passengers");
|
|
12934
|
-
if (!
|
|
12981
|
+
if (!existsSync23(dir)) return [];
|
|
12935
12982
|
return readdirSync7(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
|
|
12936
12983
|
}
|
|
12937
12984
|
|
|
@@ -13052,7 +13099,7 @@ async function unregisterEndpoint(workspaceId, info, d) {
|
|
|
13052
13099
|
import { createServer as createHttpServer } from "http";
|
|
13053
13100
|
import { spawn as spawn4 } from "child_process";
|
|
13054
13101
|
import { randomBytes } from "crypto";
|
|
13055
|
-
import { existsSync as
|
|
13102
|
+
import { existsSync as existsSync24, mkdirSync as mkdirSync11, readFileSync as readFileSync17, writeFileSync as writeFileSync12, unlinkSync as unlinkSync4, readdirSync as readdirSync8 } from "fs";
|
|
13056
13103
|
import { join as join23 } from "path";
|
|
13057
13104
|
|
|
13058
13105
|
// src/secret-compare.ts
|
|
@@ -13146,7 +13193,7 @@ function syncDaemonStatePath(stateDir, workspaceId) {
|
|
|
13146
13193
|
}
|
|
13147
13194
|
function readPersistedSyncDaemon(stateDir, workspaceId) {
|
|
13148
13195
|
const path = syncDaemonStatePath(stateDir, workspaceId);
|
|
13149
|
-
if (!
|
|
13196
|
+
if (!existsSync24(path)) return void 0;
|
|
13150
13197
|
try {
|
|
13151
13198
|
return JSON.parse(readFileSync17(path, "utf8"));
|
|
13152
13199
|
} catch {
|
|
@@ -13177,7 +13224,7 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
|
|
|
13177
13224
|
}
|
|
13178
13225
|
function readAllPersistedSyncWorkspaceIds(stateDir) {
|
|
13179
13226
|
const dir = join23(stateDir, "passengers");
|
|
13180
|
-
if (!
|
|
13227
|
+
if (!existsSync24(dir)) return [];
|
|
13181
13228
|
const suffix = "-sync.json";
|
|
13182
13229
|
return readdirSync8(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
|
|
13183
13230
|
}
|
|
@@ -13234,11 +13281,11 @@ function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
|
|
|
13234
13281
|
}
|
|
13235
13282
|
|
|
13236
13283
|
// src/daemon.ts
|
|
13237
|
-
import { existsSync 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";
|
|
13238
13285
|
import { join as join25 } from "path";
|
|
13239
13286
|
|
|
13240
13287
|
// src/interrupted.ts
|
|
13241
|
-
import { existsSync as
|
|
13288
|
+
import { existsSync as existsSync25, readdirSync as readdirSync9, readFileSync as readFileSync18 } from "fs";
|
|
13242
13289
|
import { join as join24 } from "path";
|
|
13243
13290
|
var INTERRUPTED_MARKER = "<!-- crew:interrupted -->";
|
|
13244
13291
|
function interruptedComment(b) {
|
|
@@ -13257,7 +13304,7 @@ var pidAlive = (pid) => {
|
|
|
13257
13304
|
};
|
|
13258
13305
|
function runFinished(streamPath) {
|
|
13259
13306
|
const path = `${streamPath}.meta.json`;
|
|
13260
|
-
if (!
|
|
13307
|
+
if (!existsSync25(path)) return false;
|
|
13261
13308
|
try {
|
|
13262
13309
|
return JSON.parse(readFileSync18(path, "utf8")).running !== true;
|
|
13263
13310
|
} catch {
|
|
@@ -13266,7 +13313,7 @@ function runFinished(streamPath) {
|
|
|
13266
13313
|
}
|
|
13267
13314
|
function orphanedStreams(o) {
|
|
13268
13315
|
const dir = join24(o.stateDir, "streams");
|
|
13269
|
-
if (!
|
|
13316
|
+
if (!existsSync25(dir)) return [];
|
|
13270
13317
|
const ownerAlive = o.isOwnerAlive ?? pidAlive;
|
|
13271
13318
|
const out = [];
|
|
13272
13319
|
for (const name of readdirSync9(dir)) {
|
|
@@ -13380,7 +13427,7 @@ function computeTreeSignature(paths) {
|
|
|
13380
13427
|
}
|
|
13381
13428
|
function makeStaleChecker(paths, opts = {}) {
|
|
13382
13429
|
const initial = computeTreeSignature(paths);
|
|
13383
|
-
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 &&
|
|
13430
|
+
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync26(opts.reloadSentinel);
|
|
13384
13431
|
}
|
|
13385
13432
|
function reloadSentinelFor(stateDir) {
|
|
13386
13433
|
return join25(stateDir, "reload-requested");
|
|
@@ -14271,7 +14318,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
14271
14318
|
if (host === "linux" && hasSystemd) {
|
|
14272
14319
|
const label = labelFor(CREW_HOME, "run");
|
|
14273
14320
|
const staleTimer = join26(homedir8(), ".config", "systemd", "user", `${label}.timer`);
|
|
14274
|
-
if (
|
|
14321
|
+
if (existsSync27(staleTimer)) {
|
|
14275
14322
|
process.stdout.write(
|
|
14276
14323
|
`removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
|
|
14277
14324
|
`
|
|
@@ -14297,7 +14344,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
14297
14344
|
const tracker = new Tracker(c, cfg.ship);
|
|
14298
14345
|
problems.push(...validateContract(tracker.contract).map((p) => `contract: ${p}`));
|
|
14299
14346
|
for (const r of await resolvedRepos(c)) {
|
|
14300
|
-
if (!
|
|
14347
|
+
if (!existsSync27(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
|
|
14301
14348
|
if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
|
|
14302
14349
|
problems.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
|
|
14303
14350
|
}
|
|
@@ -14315,7 +14362,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
14315
14362
|
}
|
|
14316
14363
|
async function doctorFix(o) {
|
|
14317
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))));
|
|
14318
|
-
const unitExists = (p) =>
|
|
14365
|
+
const unitExists = (p) => existsSync27(p);
|
|
14319
14366
|
const crontab = o.host === "linux" ? spawnSync6("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
|
|
14320
14367
|
const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
|
|
14321
14368
|
const plan = planFix({ routes, schedulerAbsent, host: o.host });
|
|
@@ -14412,6 +14459,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
14412
14459
|
}
|
|
14413
14460
|
async function sweepClosedTickets(c, target, branch, tracker, dryRun2, remit, idle = false) {
|
|
14414
14461
|
try {
|
|
14462
|
+
if (!dryRun2) reconcileHeldWorktrees(state, target.name, remit);
|
|
14415
14463
|
if (idle && !sweepCandidatesExist(target.dir, branch.base)) return;
|
|
14416
14464
|
const truncated = (limit) => remit.warn(`ticket lookup for the sweep hit its ${limit}-row limit; some tickets may be missed`, { step: "worktree" });
|
|
14417
14465
|
const nums = /* @__PURE__ */ new Set([
|
|
@@ -14421,12 +14469,14 @@ ${changed2.map((c) => ` - ${c}
|
|
|
14421
14469
|
]);
|
|
14422
14470
|
const scoped = ticketsByRepo(c, await tracker.ticketsByNumbers([...nums], truncated)).byRepo.get(target.name) ?? [];
|
|
14423
14471
|
const actions = planWorktreeSweep(target, scoped, tracker.contract);
|
|
14424
|
-
|
|
14472
|
+
const orphanDirs = orphanedWorktreeDirs(target, scoped, tracker.contract);
|
|
14473
|
+
warnOrphanedWorktreeDirs(orphanDirs, remit, dryRun2 ? void 0 : { state, repo: target.name });
|
|
14425
14474
|
if (actions.length) {
|
|
14426
14475
|
remit.enter("worktree");
|
|
14427
|
-
const r = await applyWorktreeSweep(target.dir, actions, dryRun2, remit, branch.remote, branch.base);
|
|
14476
|
+
const r = await applyWorktreeSweep(target.dir, actions, dryRun2, remit, branch.remote, branch.base, { state, repo: target.name });
|
|
14428
14477
|
remit.emit(`swept ${r.removed} worktree(s), kept ${r.keptBranches} branch(es)`, { step: "worktree" });
|
|
14429
|
-
|
|
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));
|
|
14430
14480
|
const orphaned = planLocalBranchCleanup(target.dir, branch.base, scoped, tracker.contract);
|
|
14431
14481
|
if (orphaned.length) applyLocalBranchCleanup(target.dir, orphaned, dryRun2, remit);
|
|
14432
14482
|
const stale = planRemoteBranchCleanup(target.dir, branch.remote, branch.base, scoped, tracker.contract);
|
|
@@ -15484,7 +15534,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
15484
15534
|
`);
|
|
15485
15535
|
if (!dryRun) {
|
|
15486
15536
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
15487
|
-
const raw =
|
|
15537
|
+
const raw = existsSync27(resolvedPath) ? JSON.parse(readFileSync20(resolvedPath, "utf8")) : {};
|
|
15488
15538
|
raw.agentPersonas = personaResult.agentPersonas;
|
|
15489
15539
|
if (skillResult) raw.agentSkills = skillResult.agentSkills;
|
|
15490
15540
|
if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
|
|
@@ -15554,7 +15604,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
15554
15604
|
`);
|
|
15555
15605
|
if (!dryRun) {
|
|
15556
15606
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
15557
|
-
const raw =
|
|
15607
|
+
const raw = existsSync27(resolvedPath) ? JSON.parse(readFileSync20(resolvedPath, "utf8")) : {};
|
|
15558
15608
|
raw.agentSkills = result.agentSkills;
|
|
15559
15609
|
mkdirSync13(dirOf(resolvedPath), { recursive: true });
|
|
15560
15610
|
writeFileSync14(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
@@ -15577,7 +15627,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
15577
15627
|
if (sub === "list" && !routeName) usage2();
|
|
15578
15628
|
if (sub === "add") {
|
|
15579
15629
|
try {
|
|
15580
|
-
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) =>
|
|
15630
|
+
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync27(resolvePath(p.replace(/^~(?=\/|$)/, homedir8()))));
|
|
15581
15631
|
routeName = a.route;
|
|
15582
15632
|
addPath = a.path;
|
|
15583
15633
|
addName = a.name;
|
|
@@ -15592,7 +15642,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
15592
15642
|
}
|
|
15593
15643
|
const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
|
|
15594
15644
|
let resolvedRemotes;
|
|
15595
|
-
if (
|
|
15645
|
+
if (existsSync27(resolvedFile)) {
|
|
15596
15646
|
try {
|
|
15597
15647
|
resolvedRemotes = JSON.parse(readFileSync20(resolvedFile, "utf8"));
|
|
15598
15648
|
} catch {
|
|
@@ -15609,14 +15659,14 @@ ${changed2.map((c) => ` - ${c}
|
|
|
15609
15659
|
for (const r of repos) {
|
|
15610
15660
|
const origin2 = originOf(r.dir);
|
|
15611
15661
|
const row = matchRepoName(origin2, resolvedRemotes);
|
|
15612
|
-
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)"}`;
|
|
15613
15663
|
process.stdout.write(`${r.name} ${r.dir} ${note}
|
|
15614
15664
|
`);
|
|
15615
15665
|
}
|
|
15616
15666
|
break;
|
|
15617
15667
|
}
|
|
15618
15668
|
const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir8()));
|
|
15619
|
-
const top =
|
|
15669
|
+
const top = existsSync27(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
|
|
15620
15670
|
if (!top) throw new ReposError(`${target} is not a git checkout`);
|
|
15621
15671
|
const origin = originOf(top);
|
|
15622
15672
|
const plan = planRepoAdd({
|
|
@@ -15665,7 +15715,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
15665
15715
|
const created = await rowTracker.createRepoRow(rowPayload);
|
|
15666
15716
|
process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
|
|
15667
15717
|
`);
|
|
15668
|
-
if (
|
|
15718
|
+
if (existsSync27(resolvedFile)) {
|
|
15669
15719
|
const raw = JSON.parse(readFileSync20(resolvedFile, "utf8"));
|
|
15670
15720
|
raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
|
|
15671
15721
|
raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
|
|
@@ -15920,7 +15970,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
15920
15970
|
}
|
|
15921
15971
|
}
|
|
15922
15972
|
let previous;
|
|
15923
|
-
const hadPreviousFile =
|
|
15973
|
+
const hadPreviousFile = existsSync27(resolvedPath);
|
|
15924
15974
|
if (hadPreviousFile) {
|
|
15925
15975
|
try {
|
|
15926
15976
|
previous = JSON.parse(readFileSync20(resolvedPath, "utf8"));
|
|
@@ -16216,15 +16266,20 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
16216
16266
|
await tracker.ticketsByNumbers(remoteBranchTicketNumbers(r.dir, r.config.branch.remote, r.config.branch.base), truncated)
|
|
16217
16267
|
).byRepo.get(r.name) ?? [];
|
|
16218
16268
|
const actions = planWorktreeSweep(r, wtTickets, tracker.contract);
|
|
16219
|
-
|
|
16269
|
+
const orphanDirs = orphanedWorktreeDirs(r, wtTickets, tracker.contract);
|
|
16270
|
+
warnOrphanedWorktreeDirs(orphanDirs, emit, dryRun ? void 0 : { state, repo: r.name });
|
|
16220
16271
|
const stale = planRemoteBranchCleanup(r.dir, r.config.branch.remote, r.config.branch.base, brTickets, tracker.contract);
|
|
16221
16272
|
if (stale.length) {
|
|
16222
16273
|
anything = true;
|
|
16223
16274
|
applyRemoteBranchCleanup(r.dir, r.config.branch.remote, stale, dryRun, emit);
|
|
16224
16275
|
}
|
|
16225
|
-
if (!actions.length)
|
|
16276
|
+
if (!actions.length) {
|
|
16277
|
+
if (!dryRun) reconcileHeldWorktrees(state, r.name, emit, new Set(orphanDirs));
|
|
16278
|
+
continue;
|
|
16279
|
+
}
|
|
16226
16280
|
anything = true;
|
|
16227
|
-
const res = await applyWorktreeSweep(r.dir, actions, dryRun, emit, r.config.branch.remote, r.config.branch.base);
|
|
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]));
|
|
16228
16283
|
emit.emit(
|
|
16229
16284
|
`swept ${res.removed} worktree(s) for ${r.name}, kept ${res.keptBranches} branch(es)`,
|
|
16230
16285
|
{ step: "worktree" }
|
|
@@ -16728,6 +16783,14 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
16728
16783
|
// Additive (CREW-1379): `unsafe` means a ship must not cut a
|
|
16729
16784
|
// worktree or release from this base. `version` stays 1.
|
|
16730
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
|
+
})),
|
|
16731
16794
|
worktreePrefix: r.config.worktrees.prefix,
|
|
16732
16795
|
// A repo with no explicit `worktrees.prefix` no longer names its
|
|
16733
16796
|
// worktrees `<prefix><number>` (ISSUE-969) — the checkout's own
|
|
@@ -16827,7 +16890,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
16827
16890
|
);
|
|
16828
16891
|
try {
|
|
16829
16892
|
const evFile = eventFileFor(cfg.ship.stateDir);
|
|
16830
|
-
const evSize =
|
|
16893
|
+
const evSize = existsSync27(evFile) ? statSync9(evFile).size : 0;
|
|
16831
16894
|
process.stdout.write(
|
|
16832
16895
|
evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
|
|
16833
16896
|
` : "events log: ok\n"
|
package/package.json
CHANGED