@tablation/crew 0.71.2 → 0.72.0
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/README.md +2 -0
- package/dist/cli.js +880 -296
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -108,6 +108,25 @@ async function settleRepos(items, nameOf, resolve6) {
|
|
|
108
108
|
}
|
|
109
109
|
return { settled, errors };
|
|
110
110
|
}
|
|
111
|
+
function pidRunning(pid) {
|
|
112
|
+
try {
|
|
113
|
+
process.kill(pid, 0);
|
|
114
|
+
return true;
|
|
115
|
+
} catch (e) {
|
|
116
|
+
return e.code !== "ESRCH";
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function staleOwnLockClaim(holder, shipName2, running = pidRunning) {
|
|
120
|
+
if (!holder) return false;
|
|
121
|
+
const i = holder.lastIndexOf(":");
|
|
122
|
+
if (i < 0 || holder.slice(0, i) !== shipName2) return false;
|
|
123
|
+
const pid = Number(holder.slice(i + 1));
|
|
124
|
+
return Number.isInteger(pid) && pid > 0 && !running(pid);
|
|
125
|
+
}
|
|
126
|
+
function formatStaleOwnLock(scope, holder) {
|
|
127
|
+
return `WARN lock ${scope} is held by ${holder}, but that process is gone \u2014 the next release cycle reclaims it
|
|
128
|
+
`;
|
|
129
|
+
}
|
|
111
130
|
|
|
112
131
|
// src/selftest.ts
|
|
113
132
|
import { spawn as spawn3 } from "child_process";
|
|
@@ -5569,7 +5588,7 @@ import { StaleWriteError } from "@tablation/client";
|
|
|
5569
5588
|
function releaseLockScope(route, repo) {
|
|
5570
5589
|
return `${route}/${repo}`;
|
|
5571
5590
|
}
|
|
5572
|
-
async function acquireBoardLock(client2, modelId, scope, holderLabel, ttlMs) {
|
|
5591
|
+
async function acquireBoardLock(client2, modelId, scope, holderLabel, ttlMs, log = {}) {
|
|
5573
5592
|
if (!modelId) return { ok: true, release: async () => {
|
|
5574
5593
|
}, exclusion: "off" };
|
|
5575
5594
|
const filters = JSON.stringify([{ columnName: "scope", operator: "EQ", value: scope }]);
|
|
@@ -5577,7 +5596,9 @@ async function acquireBoardLock(client2, modelId, scope, holderLabel, ttlMs) {
|
|
|
5577
5596
|
const row = rows[0];
|
|
5578
5597
|
if (!row) return { ok: false, reason: "missing" };
|
|
5579
5598
|
const age = Date.now() - new Date(row.updated_at).getTime();
|
|
5580
|
-
const
|
|
5599
|
+
const staleOwn = !!row.holder && row.holder !== holderLabel && isDeadOwnClaim(row.holder, holderLabel);
|
|
5600
|
+
const free = !row.holder || row.holder === holderLabel || age > ttlMs || staleOwn;
|
|
5601
|
+
if (staleOwn) log.warn?.(`reclaimed stale own claim ${row.holder} on ${scope} (process is gone)`);
|
|
5581
5602
|
if (!free) return { ok: false, heldBy: row.holder ?? void 0, reason: "held" };
|
|
5582
5603
|
let claimed;
|
|
5583
5604
|
try {
|
|
@@ -5591,12 +5612,34 @@ async function acquireBoardLock(client2, modelId, scope, holderLabel, ttlMs) {
|
|
|
5591
5612
|
exclusion: "taken",
|
|
5592
5613
|
release: async () => {
|
|
5593
5614
|
try {
|
|
5594
|
-
|
|
5595
|
-
|
|
5615
|
+
try {
|
|
5616
|
+
await client2.update(modelId, row.id, { holder: "" }, claimed.updated_at);
|
|
5617
|
+
} catch (e) {
|
|
5618
|
+
if (!(e instanceof StaleWriteError)) throw e;
|
|
5619
|
+
const [now] = await client2.list(modelId, { filters: scopeFilter(scope), limit: 1 });
|
|
5620
|
+
if (now?.holder === holderLabel) await client2.update(modelId, row.id, { holder: "" }, now.updated_at);
|
|
5621
|
+
}
|
|
5622
|
+
log.debug?.(`lock released for ${scope}`);
|
|
5623
|
+
} catch (e) {
|
|
5624
|
+
log.warn?.(`lock release failed for ${scope}: ${e.message}`);
|
|
5596
5625
|
}
|
|
5597
5626
|
}
|
|
5598
5627
|
};
|
|
5599
5628
|
}
|
|
5629
|
+
function isDeadOwnClaim(holder, mine) {
|
|
5630
|
+
const split = (l) => {
|
|
5631
|
+
const i = l.lastIndexOf(":");
|
|
5632
|
+
return i < 0 ? null : { ship: l.slice(0, i), pid: Number(l.slice(i + 1)) };
|
|
5633
|
+
};
|
|
5634
|
+
const h = split(holder), m = split(mine);
|
|
5635
|
+
if (!h || !m || h.ship !== m.ship || !Number.isInteger(h.pid) || h.pid <= 0) return false;
|
|
5636
|
+
try {
|
|
5637
|
+
process.kill(h.pid, 0);
|
|
5638
|
+
return false;
|
|
5639
|
+
} catch (e) {
|
|
5640
|
+
return e.code === "ESRCH";
|
|
5641
|
+
}
|
|
5642
|
+
}
|
|
5600
5643
|
async function lockRowState(client2, modelId, scope) {
|
|
5601
5644
|
const rows = await client2.list(modelId, { filters: scopeFilter(scope), limit: 1 });
|
|
5602
5645
|
const row = rows[0];
|
|
@@ -5877,6 +5920,12 @@ var Tracker = class {
|
|
|
5877
5920
|
return [];
|
|
5878
5921
|
}
|
|
5879
5922
|
}
|
|
5923
|
+
/** Renames a Ships row (CREW-2495). Throws on a refused write (a member key may not edit the table). */
|
|
5924
|
+
async renameShipRow(rowId, name) {
|
|
5925
|
+
const model = this.route.resolved?.shipsModelId;
|
|
5926
|
+
if (!model) throw new Error("this workspace has no Ships table");
|
|
5927
|
+
await this.client.records.update(model, rowId, { name });
|
|
5928
|
+
}
|
|
5880
5929
|
/**
|
|
5881
5930
|
* This ship's own row on the `Ships` table, matched by name — the same
|
|
5882
5931
|
* exact-string match `doctor` has always used (see the callsign comment
|
|
@@ -6068,8 +6117,8 @@ var Tracker = class {
|
|
|
6068
6117
|
* Locks table with no row for `scope` is `{ ok: false, reason: 'missing' }`
|
|
6069
6118
|
* (CREW-1384): `crew connect` provisions the row.
|
|
6070
6119
|
*/
|
|
6071
|
-
acquireBoardLock(scope, holderLabel, ttlMs) {
|
|
6072
|
-
return acquireBoardLock(this.client.records, this.route.resolved?.locksModelId, scope, holderLabel, ttlMs);
|
|
6120
|
+
acquireBoardLock(scope, holderLabel, ttlMs, log) {
|
|
6121
|
+
return acquireBoardLock(this.client.records, this.route.resolved?.locksModelId, scope, holderLabel, ttlMs, log);
|
|
6073
6122
|
}
|
|
6074
6123
|
/** The Locks row for `scope` — present? held by whom? Undefined when this workspace has no Locks table. */
|
|
6075
6124
|
async lockState(scope) {
|
|
@@ -8563,9 +8612,9 @@ function doctorSelftestLines(stored, toolchains) {
|
|
|
8563
8612
|
}
|
|
8564
8613
|
|
|
8565
8614
|
// src/cli.ts
|
|
8566
|
-
import { resolve as resolve5, dirname as
|
|
8615
|
+
import { resolve as resolve5, dirname as dirname20, join as join35, basename as basename11 } from "path";
|
|
8567
8616
|
import { fileURLToPath } from "url";
|
|
8568
|
-
import { hostname as hostname2, homedir as
|
|
8617
|
+
import { hostname as hostname2, homedir as homedir9 } from "os";
|
|
8569
8618
|
import { spawnSync as spawnSync6 } from "child_process";
|
|
8570
8619
|
import { parse as parseYaml2 } from "yaml";
|
|
8571
8620
|
|
|
@@ -8579,6 +8628,7 @@ var VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
|
8579
8628
|
"extra-path",
|
|
8580
8629
|
"harness",
|
|
8581
8630
|
"hook",
|
|
8631
|
+
"host",
|
|
8582
8632
|
"key",
|
|
8583
8633
|
"level",
|
|
8584
8634
|
"limit",
|
|
@@ -8641,6 +8691,9 @@ function versionRequest(argv2) {
|
|
|
8641
8691
|
if (rest.some((a) => a !== "--json")) return null;
|
|
8642
8692
|
return { json: rest.length > 0 };
|
|
8643
8693
|
}
|
|
8694
|
+
function helpRequest(argv2) {
|
|
8695
|
+
return argv2[0] === "--help" || argv2[0] === "-h" || argv2[0] === "help";
|
|
8696
|
+
}
|
|
8644
8697
|
|
|
8645
8698
|
// src/roster.ts
|
|
8646
8699
|
function memberByIdentity(roster, identityId) {
|
|
@@ -8736,7 +8789,7 @@ function rosterMarkdown(roster, meId) {
|
|
|
8736
8789
|
}
|
|
8737
8790
|
|
|
8738
8791
|
// src/connect-resume.ts
|
|
8739
|
-
import { mkdirSync as mkdirSync10, readFileSync as readFileSync16, writeFileSync as writeFileSync11 } from "fs";
|
|
8792
|
+
import { mkdirSync as mkdirSync10, readFileSync as readFileSync16, rmSync as rmSync10, writeFileSync as writeFileSync11 } from "fs";
|
|
8740
8793
|
import { dirname as dirname8, join as join19 } from "path";
|
|
8741
8794
|
function lastSessionFile(stateDir) {
|
|
8742
8795
|
return join19(stateDir, "connect-session.json");
|
|
@@ -8771,10 +8824,45 @@ async function resumeKeychainSession(o) {
|
|
|
8771
8824
|
return void 0;
|
|
8772
8825
|
}
|
|
8773
8826
|
}
|
|
8827
|
+
function rememberedSessions(stateDir) {
|
|
8828
|
+
return Object.entries(readPointers(stateDir)).map(([host, workspace]) => ({ host, workspace }));
|
|
8829
|
+
}
|
|
8830
|
+
function forgetSession(stateDir, host) {
|
|
8831
|
+
try {
|
|
8832
|
+
const { [host]: _gone, ...rest } = readPointers(stateDir);
|
|
8833
|
+
if (Object.keys(rest).length) writeFileSync11(lastSessionFile(stateDir), `${JSON.stringify(rest, null, 2)}
|
|
8834
|
+
`, { mode: 384 });
|
|
8835
|
+
else rmSync10(lastSessionFile(stateDir), { force: true });
|
|
8836
|
+
} catch {
|
|
8837
|
+
}
|
|
8838
|
+
}
|
|
8839
|
+
async function signOutSessions(o) {
|
|
8840
|
+
const targets = rememberedSessions(o.stateDir).filter((t) => !o.host || t.host === o.host);
|
|
8841
|
+
if (o.host && o.workspace && !targets.some((t) => t.host === o.host && t.workspace === o.workspace)) {
|
|
8842
|
+
targets.push({ host: o.host, workspace: o.workspace });
|
|
8843
|
+
}
|
|
8844
|
+
const out = [];
|
|
8845
|
+
for (const t of targets) {
|
|
8846
|
+
try {
|
|
8847
|
+
const stored = await o.store.get(t.host, t.workspace);
|
|
8848
|
+
if (stored?.apiKey) {
|
|
8849
|
+
if (!o.dryRun) await o.store.clear(t.host, t.workspace);
|
|
8850
|
+
out.push({ ...t, action: o.dryRun ? "would-remove" : "removed" });
|
|
8851
|
+
} else {
|
|
8852
|
+
out.push({ ...t, action: "absent", detail: "no keychain entry" });
|
|
8853
|
+
}
|
|
8854
|
+
if (!o.dryRun) forgetSession(o.stateDir, t.host);
|
|
8855
|
+
} catch (e) {
|
|
8856
|
+
out.push({ ...t, action: "failed", detail: e.message });
|
|
8857
|
+
}
|
|
8858
|
+
}
|
|
8859
|
+
return out;
|
|
8860
|
+
}
|
|
8774
8861
|
|
|
8775
8862
|
// src/reset.ts
|
|
8776
|
-
import { existsSync as existsSync20, mkdirSync as mkdirSync11, readFileSync as readFileSync17, readdirSync as readdirSync7, rmSync as
|
|
8863
|
+
import { existsSync as existsSync20, mkdirSync as mkdirSync11, readFileSync as readFileSync17, readdirSync as readdirSync7, rmSync as rmSync11, statSync as statSync6, writeFileSync as writeFileSync12 } from "fs";
|
|
8777
8864
|
import { dirname as dirname9, join as join20 } from "path";
|
|
8865
|
+
import { hostFromUrl as hostFromUrl2 } from "@tablation/client";
|
|
8778
8866
|
function clonedCheckoutsFile(stateDir) {
|
|
8779
8867
|
return join20(stateDir, "cloned-checkouts.json");
|
|
8780
8868
|
}
|
|
@@ -8820,12 +8908,31 @@ async function runReset(o, fx) {
|
|
|
8820
8908
|
steps.push({ step: "credential", target: route.route, action: "failed", detail: e.message });
|
|
8821
8909
|
}
|
|
8822
8910
|
}
|
|
8911
|
+
const covered = new Set([...o.routes, ...o.droppedRoutes ?? []].map((r) => `${hostFromUrl2(r.baseUrl)}/${splitRoute(r.route)[0]}`));
|
|
8912
|
+
try {
|
|
8913
|
+
for (const s of await fx.hostSessions(o.stateDir)) {
|
|
8914
|
+
const target = `${s.host}/${s.workspace}`;
|
|
8915
|
+
if (covered.has(target)) continue;
|
|
8916
|
+
try {
|
|
8917
|
+
if (!s.stored) {
|
|
8918
|
+
steps.push({ step: "credential", target, action: "absent", detail: "no host session in the keychain" });
|
|
8919
|
+
continue;
|
|
8920
|
+
}
|
|
8921
|
+
if (!o.dryRun) await s.clear();
|
|
8922
|
+
steps.push({ step: "credential", target, action: mark(o.dryRun), detail: "host session stored by a dry-run connect" });
|
|
8923
|
+
} catch (e) {
|
|
8924
|
+
steps.push({ step: "credential", target, action: "failed", detail: e.message });
|
|
8925
|
+
}
|
|
8926
|
+
}
|
|
8927
|
+
} catch (e) {
|
|
8928
|
+
steps.push({ step: "credential", target: "host sessions", action: "failed", detail: e.message });
|
|
8929
|
+
}
|
|
8823
8930
|
if (o.removeCheckouts) {
|
|
8824
8931
|
for (const dir of readClonedCheckouts(o.stateDir)) steps.push(removeCheckout(dir, o.dryRun, fx));
|
|
8825
8932
|
}
|
|
8826
8933
|
steps.push(...removeState(o.stateDir, o.dryRun));
|
|
8827
8934
|
if (existsSync20(o.configFile)) {
|
|
8828
|
-
if (!o.dryRun)
|
|
8935
|
+
if (!o.dryRun) rmSync11(o.configFile, { force: true });
|
|
8829
8936
|
steps.push({ step: "config", target: o.configFile, action: mark(o.dryRun) });
|
|
8830
8937
|
} else {
|
|
8831
8938
|
steps.push({ step: "config", target: o.configFile, action: "absent" });
|
|
@@ -8841,7 +8948,7 @@ function removeCheckout(dir, dryRun, fx) {
|
|
|
8841
8948
|
if (linkedWorktrees.length) {
|
|
8842
8949
|
return { step: "checkout", target: dir, action: "kept", detail: `has linked worktrees (${linkedWorktrees.join(", ")}); remove them first` };
|
|
8843
8950
|
}
|
|
8844
|
-
if (!dryRun)
|
|
8951
|
+
if (!dryRun) rmSync11(dir, { recursive: true, force: true });
|
|
8845
8952
|
return { step: "checkout", target: dir, action: mark(dryRun) };
|
|
8846
8953
|
}
|
|
8847
8954
|
function removeState(stateDir, dryRun) {
|
|
@@ -8855,7 +8962,7 @@ function removeState(stateDir, dryRun) {
|
|
|
8855
8962
|
}
|
|
8856
8963
|
for (const t of targets) {
|
|
8857
8964
|
if (!existsSync20(t)) continue;
|
|
8858
|
-
if (!dryRun)
|
|
8965
|
+
if (!dryRun) rmSync11(t, { recursive: true, force: true });
|
|
8859
8966
|
out.push({ step: "state", target: t, action: mark(dryRun) });
|
|
8860
8967
|
}
|
|
8861
8968
|
if (!out.length) out.push({ step: "state", target: stateDir, action: "absent", detail: "nothing to remove" });
|
|
@@ -10519,7 +10626,9 @@ Wrote ${o.configPath}.
|
|
|
10519
10626
|
|
|
10520
10627
|
// src/connect-json.ts
|
|
10521
10628
|
import { accessSync as accessSync3, constants as constants3 } from "fs";
|
|
10522
|
-
import {
|
|
10629
|
+
import { homedir as homedir7 } from "os";
|
|
10630
|
+
import { dirname as dirname11, join as join23 } from "path";
|
|
10631
|
+
import { delimiter as delimiter2 } from "path";
|
|
10523
10632
|
var CHOICE_EXIT_CODE = 3;
|
|
10524
10633
|
function createEmitter(json, write) {
|
|
10525
10634
|
return {
|
|
@@ -10553,6 +10662,23 @@ function usesFlagAnswers(f, isTTY) {
|
|
|
10553
10662
|
if (isTTY) return false;
|
|
10554
10663
|
return !!(f.shipName || f.agentBin || f.extraPath || f.hostPassengers);
|
|
10555
10664
|
}
|
|
10665
|
+
function agentBinSearchDirs(home, pathEnv, extraPath) {
|
|
10666
|
+
const dirs = [
|
|
10667
|
+
...pathEnv.split(delimiter2),
|
|
10668
|
+
...(extraPath ?? "").split(delimiter2),
|
|
10669
|
+
join23(home, ".local/bin"),
|
|
10670
|
+
join23(home, ".claude/local"),
|
|
10671
|
+
"/opt/homebrew/bin",
|
|
10672
|
+
"/usr/local/bin",
|
|
10673
|
+
join23(home, ".npm-global/bin"),
|
|
10674
|
+
join23(home, ".volta/bin")
|
|
10675
|
+
];
|
|
10676
|
+
return [...new Set(dirs.filter(Boolean))];
|
|
10677
|
+
}
|
|
10678
|
+
function findAgentBin(name, env, extraPath, isExec) {
|
|
10679
|
+
const searched = agentBinSearchDirs(env.home ?? homedir7(), env.pathEnv, extraPath);
|
|
10680
|
+
return { bin: findOnPath2(name, searched.join(delimiter2), isExec), searched };
|
|
10681
|
+
}
|
|
10556
10682
|
function flagWizardAnswers(f, env) {
|
|
10557
10683
|
const isExec = env.isExecutable ?? ((p) => {
|
|
10558
10684
|
try {
|
|
@@ -10562,9 +10688,9 @@ function flagWizardAnswers(f, env) {
|
|
|
10562
10688
|
return false;
|
|
10563
10689
|
}
|
|
10564
10690
|
});
|
|
10565
|
-
const agentBin = f.agentBin ?? findOnPath2("claude", env.pathEnv, isExec);
|
|
10566
|
-
if (!agentBin) return { error: 'no agent binary: "claude" is not on PATH \u2014 pass --agent-bin /absolute/path' };
|
|
10567
10691
|
if (f.agentBin && !isExec(f.agentBin)) return { error: `--agent-bin ${f.agentBin} is not an executable file` };
|
|
10692
|
+
const found = f.agentBin ? void 0 : findAgentBin("claude", env, f.extraPath, isExec);
|
|
10693
|
+
const agentBin = f.agentBin ?? found?.bin;
|
|
10568
10694
|
const pnpmPath = findOnPath2("pnpm", env.pathEnv, isExec);
|
|
10569
10695
|
const dockerPath = f.hostPassengers ? findOnPath2("docker", env.pathEnv, isExec) : void 0;
|
|
10570
10696
|
const extraPath = f.extraPath ?? deriveExtraPath([dirname11(env.nodePath), pnpmPath ? dirname11(pnpmPath) : void 0, dockerPath ? dirname11(dockerPath) : void 0]);
|
|
@@ -10572,16 +10698,17 @@ function flagWizardAnswers(f, env) {
|
|
|
10572
10698
|
answers: {
|
|
10573
10699
|
name: f.shipName?.trim() || env.hostname,
|
|
10574
10700
|
platform: env.platform,
|
|
10575
|
-
agentBin,
|
|
10701
|
+
agentBin: agentBin ?? "claude",
|
|
10576
10702
|
extraPath,
|
|
10577
10703
|
hostPassengers: f.hostPassengers
|
|
10578
|
-
}
|
|
10704
|
+
},
|
|
10705
|
+
...agentBin ? {} : { agentBinSearched: found?.searched ?? [] }
|
|
10579
10706
|
};
|
|
10580
10707
|
}
|
|
10581
10708
|
|
|
10582
10709
|
// src/release-run.ts
|
|
10583
|
-
import { readFileSync as readFileSync18, writeFileSync as writeFileSync15, existsSync as existsSync22, copyFileSync, mkdirSync as mkdirSync13, mkdtempSync as mkdtempSync2, rmSync as
|
|
10584
|
-
import { dirname as dirname12, join as
|
|
10710
|
+
import { readFileSync as readFileSync18, writeFileSync as writeFileSync15, existsSync as existsSync22, copyFileSync, mkdirSync as mkdirSync13, mkdtempSync as mkdtempSync2, rmSync as rmSync12 } from "fs";
|
|
10711
|
+
import { dirname as dirname12, join as join24 } from "path";
|
|
10585
10712
|
import { tmpdir as tmpdir3 } from "os";
|
|
10586
10713
|
|
|
10587
10714
|
// src/release.ts
|
|
@@ -11299,7 +11426,7 @@ async function bump(o, size) {
|
|
|
11299
11426
|
}
|
|
11300
11427
|
return printed;
|
|
11301
11428
|
}
|
|
11302
|
-
const files = o.repo.release.versionFiles.map((f) =>
|
|
11429
|
+
const files = o.repo.release.versionFiles.map((f) => join24(o.cwd, f));
|
|
11303
11430
|
const first = files[0];
|
|
11304
11431
|
if (!existsSync22(first)) {
|
|
11305
11432
|
o.emit.error(`no ${o.repo.release.versionFiles[0]} to read a version from`);
|
|
@@ -11331,7 +11458,7 @@ async function bump(o, size) {
|
|
|
11331
11458
|
function writeChangelog(o, version, entries) {
|
|
11332
11459
|
const file = o.repo.release.changelog;
|
|
11333
11460
|
if (!file || entries.length === 0) return;
|
|
11334
|
-
const path =
|
|
11461
|
+
const path = join24(o.cwd, file);
|
|
11335
11462
|
const section = renderChangelogSection(version, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), entries);
|
|
11336
11463
|
if (o.dryRun) {
|
|
11337
11464
|
o.emit.emit(`would prepend ${entries.length} entr(ies) to ${file}`);
|
|
@@ -11421,7 +11548,7 @@ function closeReleaseCheckout(cwd, root, dir, branch, run2 = gitOk) {
|
|
|
11421
11548
|
run2(cwd, ["worktree", "remove", "--force", dir]);
|
|
11422
11549
|
} finally {
|
|
11423
11550
|
try {
|
|
11424
|
-
|
|
11551
|
+
rmSync12(root, { recursive: true, force: true });
|
|
11425
11552
|
} finally {
|
|
11426
11553
|
run2(cwd, ["worktree", "prune"]);
|
|
11427
11554
|
run2(cwd, ["branch", "-D", branch]);
|
|
@@ -11431,8 +11558,8 @@ function closeReleaseCheckout(cwd, root, dir, branch, run2 = gitOk) {
|
|
|
11431
11558
|
function openReleaseCheckout(o) {
|
|
11432
11559
|
const { remote, base } = o.repo.branch;
|
|
11433
11560
|
const branch = `crew/release-${Date.now()}`;
|
|
11434
|
-
const root = mkdtempSync2(
|
|
11435
|
-
const dir =
|
|
11561
|
+
const root = mkdtempSync2(join24(tmpdir3(), "crew-release-"));
|
|
11562
|
+
const dir = join24(root, "checkout");
|
|
11436
11563
|
o.emit.emit(`release checkout: ${dir}`, { step: "release" });
|
|
11437
11564
|
const close = () => closeReleaseCheckout(o.cwd, root, dir, branch);
|
|
11438
11565
|
try {
|
|
@@ -11442,10 +11569,10 @@ function openReleaseCheckout(o) {
|
|
|
11442
11569
|
throw e;
|
|
11443
11570
|
}
|
|
11444
11571
|
for (const f of o.repo.worktrees.copy) {
|
|
11445
|
-
const src =
|
|
11572
|
+
const src = join24(o.cwd, f);
|
|
11446
11573
|
if (!existsSync22(src)) continue;
|
|
11447
|
-
mkdirSync13(dirname12(
|
|
11448
|
-
copyFileSync(src,
|
|
11574
|
+
mkdirSync13(dirname12(join24(dir, f)), { recursive: true });
|
|
11575
|
+
copyFileSync(src, join24(dir, f));
|
|
11449
11576
|
}
|
|
11450
11577
|
return { dir, branch, close };
|
|
11451
11578
|
}
|
|
@@ -12515,6 +12642,217 @@ function nextRoles(decision, ran) {
|
|
|
12515
12642
|
return decision.selection.pending.filter((r) => !ran.includes(r)).sort((a, b) => (decision.selection.ranks[a] ?? 0) - (decision.selection.ranks[b] ?? 0));
|
|
12516
12643
|
}
|
|
12517
12644
|
|
|
12645
|
+
// src/ship-name.ts
|
|
12646
|
+
import { existsSync as existsSync25, mkdirSync as mkdirSync15, readFileSync as readFileSync20, renameSync as renameSync2, writeFileSync as writeFileSync17 } from "fs";
|
|
12647
|
+
import { dirname as dirname14 } from "path";
|
|
12648
|
+
import { parseDocument } from "yaml";
|
|
12649
|
+
|
|
12650
|
+
// src/machine-id.ts
|
|
12651
|
+
import { execFileSync as execFileSync4 } from "child_process";
|
|
12652
|
+
import { createHash as createHash3 } from "crypto";
|
|
12653
|
+
import { existsSync as existsSync24, mkdirSync as mkdirSync14, readFileSync as readFileSync19, writeFileSync as writeFileSync16 } from "fs";
|
|
12654
|
+
import { dirname as dirname13, join as join25 } from "path";
|
|
12655
|
+
function machineIdPathFor(stateDir) {
|
|
12656
|
+
return join25(stateDir, "machine-id");
|
|
12657
|
+
}
|
|
12658
|
+
var sha256 = (s) => createHash3("sha256").update(s).digest("hex");
|
|
12659
|
+
function readRawMachineId(platform2 = process.platform) {
|
|
12660
|
+
try {
|
|
12661
|
+
if (platform2 === "darwin") {
|
|
12662
|
+
const out = execFileSync4("ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"], {
|
|
12663
|
+
encoding: "utf8",
|
|
12664
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
12665
|
+
});
|
|
12666
|
+
return /"IOPlatformUUID"\s*=\s*"([^"]+)"/.exec(out)?.[1];
|
|
12667
|
+
}
|
|
12668
|
+
for (const p of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
|
|
12669
|
+
if (existsSync24(p)) {
|
|
12670
|
+
const id = readFileSync19(p, "utf8").trim();
|
|
12671
|
+
if (id) return id;
|
|
12672
|
+
}
|
|
12673
|
+
}
|
|
12674
|
+
} catch {
|
|
12675
|
+
}
|
|
12676
|
+
return void 0;
|
|
12677
|
+
}
|
|
12678
|
+
function ensureMachineId(stateDir, opts = {}) {
|
|
12679
|
+
if (opts.override?.trim()) return sha256(opts.override.trim());
|
|
12680
|
+
const path = machineIdPathFor(stateDir);
|
|
12681
|
+
if (existsSync24(path)) {
|
|
12682
|
+
const cached = readFileSync19(path, "utf8").trim();
|
|
12683
|
+
if (/^[0-9a-f]{64}$/.test(cached)) return cached;
|
|
12684
|
+
}
|
|
12685
|
+
const raw = (opts.read ?? readRawMachineId)();
|
|
12686
|
+
if (!raw) return void 0;
|
|
12687
|
+
const hashed = sha256(raw);
|
|
12688
|
+
mkdirSync14(dirname13(path), { recursive: true });
|
|
12689
|
+
writeFileSync16(path, `${hashed}
|
|
12690
|
+
`, { mode: 384 });
|
|
12691
|
+
return hashed;
|
|
12692
|
+
}
|
|
12693
|
+
function matchShipRows(ships, shipName2, machineId) {
|
|
12694
|
+
const hasColumn = ships.some((r) => "machine_id" in r);
|
|
12695
|
+
const byId = hasColumn && machineId ? ships.filter((r) => r.machine_id === machineId) : [];
|
|
12696
|
+
if (byId.length > 0) return { rows: byId, by: "machine_id", hasColumn };
|
|
12697
|
+
return { rows: ships.filter((r) => (r.name ?? "").trim() === shipName2.trim()), by: "name", hasColumn };
|
|
12698
|
+
}
|
|
12699
|
+
|
|
12700
|
+
// src/ship-name.ts
|
|
12701
|
+
var ShipNameError = class extends Error {
|
|
12702
|
+
};
|
|
12703
|
+
function validateShipName(raw) {
|
|
12704
|
+
const name = raw.trim();
|
|
12705
|
+
if (!name) throw new ShipNameError("the ship name cannot be empty");
|
|
12706
|
+
if (/[\u0000-\u001f\u007f]/.test(name)) throw new ShipNameError("the ship name cannot contain control characters");
|
|
12707
|
+
return name;
|
|
12708
|
+
}
|
|
12709
|
+
function planNameSync(local, row, lastSynced) {
|
|
12710
|
+
if (local === row) return { drift: "none", action: "none" };
|
|
12711
|
+
const rowMoved = lastSynced === void 0 || row !== lastSynced;
|
|
12712
|
+
const localMoved = lastSynced === void 0 || local !== lastSynced;
|
|
12713
|
+
if (rowMoved && localMoved) return { drift: "both", action: "row-wins" };
|
|
12714
|
+
if (rowMoved) return { drift: "row", action: "adopt" };
|
|
12715
|
+
return { drift: "local", action: "push" };
|
|
12716
|
+
}
|
|
12717
|
+
function shipNameOfYaml(text) {
|
|
12718
|
+
const v = parseDocument(text).getIn(["ship", "name"]);
|
|
12719
|
+
return typeof v === "string" && v.trim() ? v.trim() : void 0;
|
|
12720
|
+
}
|
|
12721
|
+
function setShipNameInYaml(text, name) {
|
|
12722
|
+
const doc = parseDocument(text);
|
|
12723
|
+
doc.setIn(["ship", "name"], name);
|
|
12724
|
+
return doc.toString();
|
|
12725
|
+
}
|
|
12726
|
+
function pickShipRow(rows, machineId, byName = []) {
|
|
12727
|
+
const hasColumn = rows.some((r) => "machine_id" in r);
|
|
12728
|
+
if (!hasColumn || !machineId) return { kind: "unsupported" };
|
|
12729
|
+
const byId = rows.filter((r) => r.machine_id === machineId);
|
|
12730
|
+
if (byId.length === 1) return { kind: "row", row: byId[0], by: "machine_id" };
|
|
12731
|
+
if (byId.length > 1) return { kind: "ambiguous" };
|
|
12732
|
+
const names = new Set(byName.map((n) => n.trim()));
|
|
12733
|
+
const named = rows.filter((r) => !r.machine_id && names.has((r.name ?? "").trim()));
|
|
12734
|
+
if (named.length === 1) return { kind: "row", row: named[0], by: "name" };
|
|
12735
|
+
return { kind: named.length > 1 ? "ambiguous" : "none" };
|
|
12736
|
+
}
|
|
12737
|
+
async function syncRouteName(i) {
|
|
12738
|
+
const picked = pickShipRow(i.rows, i.machineId);
|
|
12739
|
+
if (picked.kind !== "row") {
|
|
12740
|
+
const status2 = picked.kind === "none" ? "no-row" : picked.kind;
|
|
12741
|
+
return { status: status2, drift: "none", action: "none", name: i.local };
|
|
12742
|
+
}
|
|
12743
|
+
const rowName = (picked.row.name ?? "").trim();
|
|
12744
|
+
if (!rowName) return { status: "no-row", drift: "none", action: "none", name: i.local };
|
|
12745
|
+
const { drift, action } = planNameSync(i.local, rowName, i.lastSynced);
|
|
12746
|
+
const base = { drift, rowId: picked.row.id, rowName };
|
|
12747
|
+
if (action === "push") {
|
|
12748
|
+
if (!i.rename) return { status: "ok", ...base, action, name: i.local };
|
|
12749
|
+
try {
|
|
12750
|
+
await i.rename(picked.row.id, i.local);
|
|
12751
|
+
} catch (e) {
|
|
12752
|
+
return { status: "refused", ...base, action: "none", name: i.local, error: e.message };
|
|
12753
|
+
}
|
|
12754
|
+
return { status: "ok", ...base, action, name: i.local };
|
|
12755
|
+
}
|
|
12756
|
+
if (action === "adopt" || action === "row-wins") return { status: "ok", ...base, action, name: rowName, from: i.local };
|
|
12757
|
+
return { status: "ok", ...base, action, name: i.local };
|
|
12758
|
+
}
|
|
12759
|
+
function readSyncedName(resolvedFile) {
|
|
12760
|
+
if (!existsSync25(resolvedFile)) return void 0;
|
|
12761
|
+
try {
|
|
12762
|
+
const v = JSON.parse(readFileSync20(resolvedFile, "utf8")).shipName;
|
|
12763
|
+
return typeof v === "string" ? v : void 0;
|
|
12764
|
+
} catch {
|
|
12765
|
+
return void 0;
|
|
12766
|
+
}
|
|
12767
|
+
}
|
|
12768
|
+
function writeSyncedName(resolvedFile, name) {
|
|
12769
|
+
if (!existsSync25(resolvedFile)) return;
|
|
12770
|
+
let raw;
|
|
12771
|
+
try {
|
|
12772
|
+
raw = JSON.parse(readFileSync20(resolvedFile, "utf8"));
|
|
12773
|
+
} catch {
|
|
12774
|
+
return;
|
|
12775
|
+
}
|
|
12776
|
+
if (raw.shipName === name) return;
|
|
12777
|
+
raw.shipName = name;
|
|
12778
|
+
mkdirSync15(dirname14(resolvedFile), { recursive: true });
|
|
12779
|
+
const tmp = `${resolvedFile}.tmp${process.pid}`;
|
|
12780
|
+
writeFileSync17(tmp, `${JSON.stringify(raw, null, 2)}
|
|
12781
|
+
`);
|
|
12782
|
+
renameSync2(tmp, resolvedFile);
|
|
12783
|
+
}
|
|
12784
|
+
async function reconcileShipName(i) {
|
|
12785
|
+
let local = i.ship.name;
|
|
12786
|
+
try {
|
|
12787
|
+
local = shipNameOfYaml(readFileSync20(i.configFile, "utf8")) ?? local;
|
|
12788
|
+
} catch {
|
|
12789
|
+
}
|
|
12790
|
+
const resolvedFile = resolvedPathFor(i.stateDir, i.route);
|
|
12791
|
+
const lastSynced = readSyncedName(resolvedFile);
|
|
12792
|
+
let rows = [];
|
|
12793
|
+
try {
|
|
12794
|
+
rows = await i.rows();
|
|
12795
|
+
} catch {
|
|
12796
|
+
}
|
|
12797
|
+
const result = await syncRouteName({
|
|
12798
|
+
local,
|
|
12799
|
+
lastSynced,
|
|
12800
|
+
rows,
|
|
12801
|
+
machineId: i.machineId,
|
|
12802
|
+
rename: i.readOnly ? void 0 : i.rename
|
|
12803
|
+
});
|
|
12804
|
+
if (i.readOnly || result.status === "unsupported") return result;
|
|
12805
|
+
if (result.status === "refused") {
|
|
12806
|
+
i.emit?.warn(`could not rename the Ships row to "${local}": ${result.error}`, { step: "poll", data: { event: "ship_rename_refused", route: i.route, to: local } });
|
|
12807
|
+
return result;
|
|
12808
|
+
}
|
|
12809
|
+
if (result.status !== "ok") return result;
|
|
12810
|
+
if (result.action === "adopt" || result.action === "row-wins") {
|
|
12811
|
+
const text = readFileSync20(i.configFile, "utf8");
|
|
12812
|
+
writeFileSync17(i.configFile, setShipNameInYaml(text, result.name));
|
|
12813
|
+
i.ship.name = result.name;
|
|
12814
|
+
if (result.action === "row-wins") {
|
|
12815
|
+
i.emit?.warn(
|
|
12816
|
+
`ship name changed in both crew.yaml ("${result.from}") and the Ships table ("${result.name}"); the Ships row wins`,
|
|
12817
|
+
{ step: "poll", data: { event: "ship_renamed", route: i.route, from: result.from, to: result.name, source: "table", conflict: true } }
|
|
12818
|
+
);
|
|
12819
|
+
} else {
|
|
12820
|
+
i.emit?.emit(`ship renamed to "${result.name}" in the Ships table; crew.yaml updated`, {
|
|
12821
|
+
step: "poll",
|
|
12822
|
+
data: { event: "ship_renamed", route: i.route, from: result.from, to: result.name, source: "table" }
|
|
12823
|
+
});
|
|
12824
|
+
}
|
|
12825
|
+
} else if (result.action === "push") {
|
|
12826
|
+
i.emit?.emit(`ship name "${result.name}" pushed to the Ships table`, {
|
|
12827
|
+
step: "poll",
|
|
12828
|
+
data: { event: "ship_renamed", route: i.route, from: result.rowName, to: result.name, source: "crew.yaml" }
|
|
12829
|
+
});
|
|
12830
|
+
}
|
|
12831
|
+
writeSyncedName(resolvedFile, result.name);
|
|
12832
|
+
return result;
|
|
12833
|
+
}
|
|
12834
|
+
async function reconcileRouteShipName(o) {
|
|
12835
|
+
const fallback = { status: "unsupported", drift: "none", action: "none", name: o.ship.name };
|
|
12836
|
+
if (!o.route.resolved?.shipsModelId) return fallback;
|
|
12837
|
+
try {
|
|
12838
|
+
const machineId = o.machineId ?? ensureMachineId(o.ship.stateDir);
|
|
12839
|
+
const tracker = new Tracker(o.route, o.ship);
|
|
12840
|
+
return await reconcileShipName({
|
|
12841
|
+
route: o.route.route,
|
|
12842
|
+
configFile: o.configFile,
|
|
12843
|
+
stateDir: o.ship.stateDir,
|
|
12844
|
+
ship: o.ship,
|
|
12845
|
+
machineId,
|
|
12846
|
+
rows: () => tracker.shipRows(),
|
|
12847
|
+
rename: (id, name) => tracker.renameShipRow(id, name),
|
|
12848
|
+
emit: o.emit,
|
|
12849
|
+
readOnly: o.readOnly
|
|
12850
|
+
});
|
|
12851
|
+
} catch {
|
|
12852
|
+
return fallback;
|
|
12853
|
+
}
|
|
12854
|
+
}
|
|
12855
|
+
|
|
12518
12856
|
// src/connect.ts
|
|
12519
12857
|
var ConnectHttpError = class extends Error {
|
|
12520
12858
|
status;
|
|
@@ -12592,7 +12930,22 @@ async function provisionReleaseLocks(o, locksModelId, route, repoNames) {
|
|
|
12592
12930
|
};
|
|
12593
12931
|
return ensureLockRows(client2, locksModelId, scopes);
|
|
12594
12932
|
}
|
|
12595
|
-
var
|
|
12933
|
+
var ISSUES_TEMPLATE_IDENTIFIERS = ["tablation.issues", "crew.issues"];
|
|
12934
|
+
async function resolveIssuesTemplateKeys(o) {
|
|
12935
|
+
const keys = new Set(ISSUES_TEMPLATE_IDENTIFIERS);
|
|
12936
|
+
await Promise.all(ISSUES_TEMPLATE_IDENTIFIERS.map(async (identifier) => {
|
|
12937
|
+
try {
|
|
12938
|
+
const t = await get(
|
|
12939
|
+
o,
|
|
12940
|
+
`/library-templates/by-identifier/${encodeURIComponent(identifier)}`
|
|
12941
|
+
);
|
|
12942
|
+
if (t.id) keys.add(t.id);
|
|
12943
|
+
if (t.identifier) keys.add(t.identifier);
|
|
12944
|
+
} catch {
|
|
12945
|
+
}
|
|
12946
|
+
}));
|
|
12947
|
+
return keys;
|
|
12948
|
+
}
|
|
12596
12949
|
async function listLibraryTemplates(o) {
|
|
12597
12950
|
return get(o, "/library-templates");
|
|
12598
12951
|
}
|
|
@@ -12633,6 +12986,7 @@ async function discover(o) {
|
|
|
12633
12986
|
models: {},
|
|
12634
12987
|
seats: {},
|
|
12635
12988
|
holds: [],
|
|
12989
|
+
candidates: [],
|
|
12636
12990
|
problems,
|
|
12637
12991
|
provisioning
|
|
12638
12992
|
};
|
|
@@ -12641,6 +12995,7 @@ async function discover(o) {
|
|
|
12641
12995
|
out.role = me.role;
|
|
12642
12996
|
out.meEmail = me.email;
|
|
12643
12997
|
out.meId = me.id;
|
|
12998
|
+
out.meIdentityId = me.identityId;
|
|
12644
12999
|
out.meName = me.name;
|
|
12645
13000
|
} catch {
|
|
12646
13001
|
}
|
|
@@ -12664,7 +13019,8 @@ async function discover(o) {
|
|
|
12664
13019
|
}
|
|
12665
13020
|
} else {
|
|
12666
13021
|
const projects = await get(o, `/projects?workspaceId=${ws.id}`);
|
|
12667
|
-
const
|
|
13022
|
+
const issuesKeys = await resolveIssuesTemplateKeys(o);
|
|
13023
|
+
const defaultCandidate = projects.find((p) => !!p.sourceTemplateId && issuesKeys.has(p.sourceTemplateId)) ?? projects.find((p) => eq(p.slug ?? "", "issues"));
|
|
12668
13024
|
if (defaultCandidate && (await missingTables(o, defaultCandidate.id)).length === 0) {
|
|
12669
13025
|
out.projectId = defaultCandidate.id;
|
|
12670
13026
|
out.projectSlug = defaultCandidate.slug;
|
|
@@ -12802,10 +13158,16 @@ async function discover(o) {
|
|
|
12802
13158
|
out.shipRow = { id: shipRowId, name: o.ship.name, action: "matched", matchedBy };
|
|
12803
13159
|
const patchBody = {};
|
|
12804
13160
|
if (matchedBy === "machine_id") {
|
|
12805
|
-
const
|
|
12806
|
-
|
|
12807
|
-
|
|
12808
|
-
|
|
13161
|
+
const rowName = (mine[0].name ?? "").trim();
|
|
13162
|
+
const route = `${out.workspaceSlug}/${out.projectSlug ?? out.projectId}`;
|
|
13163
|
+
const plan = planNameSync(o.ship.name.trim(), rowName, o.ship.lastSyncedName?.(route));
|
|
13164
|
+
if (plan.action === "push") {
|
|
13165
|
+
patchBody.name = o.ship.name.trim();
|
|
13166
|
+
provisioning.push(`renamed Ships row "${rowName}" to "${o.ship.name.trim()}" (crew.yaml changed)`);
|
|
13167
|
+
} else if (plan.action === "adopt" || plan.action === "row-wins") {
|
|
13168
|
+
out.shipRow.name = rowName;
|
|
13169
|
+
out.shipNameAdopted = rowName;
|
|
13170
|
+
provisioning.push(`matched Ships row "${rowName}" by machine id \u2014 this host's name becomes "${rowName}"${plan.action === "row-wins" ? " (crew.yaml also changed; the Ships row wins)" : ""}`);
|
|
12809
13171
|
}
|
|
12810
13172
|
patchBody.last_seen = (/* @__PURE__ */ new Date()).toISOString();
|
|
12811
13173
|
}
|
|
@@ -12856,13 +13218,32 @@ async function discover(o) {
|
|
|
12856
13218
|
o,
|
|
12857
13219
|
`/data-models/${out.models.crew}/records?limit=200`
|
|
12858
13220
|
);
|
|
12859
|
-
|
|
12860
|
-
|
|
13221
|
+
let seatShipId = shipRowId;
|
|
13222
|
+
if (!seatShipId && o.dryRun && o.ship && out.shipsModelId) {
|
|
13223
|
+
try {
|
|
13224
|
+
const shipRows = await get(
|
|
13225
|
+
o,
|
|
13226
|
+
`/data-models/${out.shipsModelId}/records?limit=200`
|
|
13227
|
+
);
|
|
13228
|
+
const byId = o.ship.machineId ? shipRows.filter((r) => r.machine_id === o.ship.machineId) : [];
|
|
13229
|
+
const mineRows = byId.length > 0 ? byId : shipRows.filter((r) => (r.name ?? "").trim() === o.ship.name.trim() && !r.machine_id);
|
|
13230
|
+
if (mineRows.length === 1) seatShipId = mineRows[0].id;
|
|
13231
|
+
} catch {
|
|
13232
|
+
}
|
|
13233
|
+
}
|
|
13234
|
+
const scoped = seatShipId ? rows.filter((r) => r.ship_id === seatShipId) : rows.filter((r) => !r.ship_id);
|
|
13235
|
+
const unclaimed = seatShipId ? rows.filter((r) => !r.ship_id) : [];
|
|
12861
13236
|
for (const role of ["dev", "design", "qa", "triage"]) {
|
|
12862
13237
|
const searchTerm = role === "dev" ? "develop" : role;
|
|
12863
13238
|
let id = seatFor(scoped, searchTerm);
|
|
12864
13239
|
let seatAction = "matched";
|
|
12865
|
-
if (!id && shipRowId) {
|
|
13240
|
+
if (!id && seatShipId && !shipRowId) {
|
|
13241
|
+
const legacyId = seatFor(unclaimed, searchTerm);
|
|
13242
|
+
if (legacyId) {
|
|
13243
|
+
id = legacyId;
|
|
13244
|
+
seatAction = "claimed";
|
|
13245
|
+
}
|
|
13246
|
+
} else if (!id && shipRowId) {
|
|
12866
13247
|
const legacyId = seatFor(unclaimed, searchTerm);
|
|
12867
13248
|
if (legacyId) {
|
|
12868
13249
|
await patch(o, `/data-models/${out.models.crew}/records/${legacyId}`, { ship_id: shipRowId });
|
|
@@ -12882,10 +13263,13 @@ async function discover(o) {
|
|
|
12882
13263
|
}
|
|
12883
13264
|
if (id) {
|
|
12884
13265
|
out.seats[role] = id;
|
|
12885
|
-
|
|
13266
|
+
const rowShip = rows.find((r) => r.id === id)?.ship_id ?? shipRowId ?? void 0;
|
|
13267
|
+
(out.seatRows ??= []).push({ role, id, action: seatAction, ...rowShip ? { shipId: rowShip } : {} });
|
|
13268
|
+
} else if (o.dryRun && o.ship && out.shipsModelId) {
|
|
13269
|
+
(out.seatRows ??= []).push({ role, id: "", action: "would-create" });
|
|
12886
13270
|
} else problems.push(`no Crew row looking like the ${role} seat \u2014 create one, or omit that role`);
|
|
12887
13271
|
}
|
|
12888
|
-
const pairId = seatFor(scoped, "pair") || (
|
|
13272
|
+
const pairId = seatFor(scoped, "pair") || (seatShipId ? seatFor(unclaimed, "pair") : void 0);
|
|
12889
13273
|
if (pairId) out.seats.pair = pairId;
|
|
12890
13274
|
if (shipRowId) {
|
|
12891
13275
|
const releaseId = seatFor(scoped, "release");
|
|
@@ -12903,25 +13287,56 @@ async function discover(o) {
|
|
|
12903
13287
|
const holdRows = rows.filter((r) => !seatIds.has(r.id));
|
|
12904
13288
|
out.holds = holdRows.map((r) => ({ id: r.id, name: r.name ?? "", email: r.email, user_id: r.user_id }));
|
|
12905
13289
|
out.crewModelId = out.models.crew;
|
|
13290
|
+
const people = holdRows.filter((r) => !r.agent_id && (!r.ship_id || !!r.user_id));
|
|
13291
|
+
const members = /* @__PURE__ */ new Map();
|
|
13292
|
+
try {
|
|
13293
|
+
const list = await get(
|
|
13294
|
+
o,
|
|
13295
|
+
`/workspaces/${out.workspaceId}/members`
|
|
13296
|
+
);
|
|
13297
|
+
for (const m of Array.isArray(list) ? list : []) {
|
|
13298
|
+
const key = m.id ?? m.userId;
|
|
13299
|
+
if (key) members.set(key, { identityId: m.identityId, email: m.email });
|
|
13300
|
+
}
|
|
13301
|
+
} catch {
|
|
13302
|
+
}
|
|
13303
|
+
for (const h of out.holds) {
|
|
13304
|
+
const e = h.user_id ? members.get(h.user_id)?.email : void 0;
|
|
13305
|
+
if (e) h.memberEmail = e;
|
|
13306
|
+
}
|
|
13307
|
+
out.candidates = people.map((r) => ({
|
|
13308
|
+
id: r.id,
|
|
13309
|
+
name: r.name ?? "",
|
|
13310
|
+
email: r.email,
|
|
13311
|
+
user_id: r.user_id,
|
|
13312
|
+
detail: r.email || (r.user_id ? members.get(r.user_id)?.email : void 0) || void 0
|
|
13313
|
+
}));
|
|
13314
|
+
const isMine = (r) => !!r.user_id && (r.user_id === out.meId || !!out.meIdentityId && members.get(r.user_id)?.identityId === out.meIdentityId);
|
|
13315
|
+
const emailOf = (r) => [r.email, r.user_id ? members.get(r.user_id)?.email : void 0].filter((e) => !!e);
|
|
12906
13316
|
if (!out.meId) {
|
|
12907
|
-
if (out.
|
|
13317
|
+
if (out.candidates.length === 0) problems.push("no non-seat Crew rows \u2014 which row is the operator?");
|
|
12908
13318
|
else {
|
|
12909
|
-
const byEmail = out.meEmail ?
|
|
13319
|
+
const byEmail = out.meEmail ? people.filter((r) => emailOf(r).some((e) => eq(e, out.meEmail))) : [];
|
|
12910
13320
|
if (byEmail.length === 1) out.operator = byEmail[0].id;
|
|
12911
|
-
else if (out.
|
|
13321
|
+
else if (out.candidates.length === 1) out.operator = out.candidates[0].id;
|
|
12912
13322
|
}
|
|
12913
13323
|
} else {
|
|
12914
|
-
const candidates =
|
|
12915
|
-
const mine =
|
|
12916
|
-
const byEmail = out.meEmail ? candidates.filter((r) =>
|
|
12917
|
-
const
|
|
13324
|
+
const candidates = people.filter((r) => !r.user_id || isMine(r));
|
|
13325
|
+
const mine = people.find(isMine);
|
|
13326
|
+
const byEmail = out.meEmail ? candidates.filter((r) => emailOf(r).some((e) => eq(e, out.meEmail))) : [];
|
|
13327
|
+
const unlinked = people.filter((r) => !r.user_id);
|
|
13328
|
+
const only = candidates.length === 1 && unlinked.length === 1 ? candidates[0] : void 0;
|
|
12918
13329
|
const pick = mine ?? (byEmail.length === 1 ? byEmail[0] : only);
|
|
12919
13330
|
if (pick) {
|
|
12920
13331
|
out.operator = pick.id;
|
|
12921
|
-
out.operatorAction = pick
|
|
12922
|
-
if (pick
|
|
12923
|
-
} else
|
|
12924
|
-
|
|
13332
|
+
out.operatorAction = isMine(pick) ? "matched" : "linked";
|
|
13333
|
+
if (!isMine(pick) && o.ship) await linkOperatorRow(o, out, pick);
|
|
13334
|
+
} else {
|
|
13335
|
+
out.operatorCompared = `signed in as ${out.meEmail ?? "unknown email"} (identity ${out.meIdentityId ?? out.meId}); ` + (people.length === 0 ? "no person rows in the Crew table" : people.map((r) => {
|
|
13336
|
+
const m = r.user_id ? members.get(r.user_id) : void 0;
|
|
13337
|
+
return `"${r.name || "unnamed row"}" <${r.email ?? "no email"}>` + (r.user_id ? ` linked to member ${m?.email ?? r.user_id}${m?.identityId ? ` (identity ${m.identityId})` : ""}` : " unlinked");
|
|
13338
|
+
}).join("; "));
|
|
13339
|
+
if (o.ship && (!o.interactive || candidates.length === 0)) await createOperatorRow(o, out);
|
|
12925
13340
|
}
|
|
12926
13341
|
}
|
|
12927
13342
|
}
|
|
@@ -12962,6 +13377,7 @@ async function createOperatorRow(o, found) {
|
|
|
12962
13377
|
found.operator = created.id;
|
|
12963
13378
|
found.operatorAction = "created";
|
|
12964
13379
|
found.holds.push({ id: created.id, name, email: found.meEmail, user_id: found.meId });
|
|
13380
|
+
found.candidates.push({ id: created.id, name, email: found.meEmail, user_id: found.meId, detail: found.meEmail });
|
|
12965
13381
|
found.provisioning.push(`created Crew row "${name}" for you`);
|
|
12966
13382
|
} catch (e) {
|
|
12967
13383
|
if (!(e instanceof ConnectHttpError) || e.status !== 403) throw e;
|
|
@@ -12991,14 +13407,14 @@ ${opts.hostPassengers ? " hostPassengers: true\n" : ""}${apiKeyFileLine}${api
|
|
|
12991
13407
|
// src/cli.ts
|
|
12992
13408
|
import {
|
|
12993
13409
|
getSessionStore as getSessionStore3,
|
|
12994
|
-
hostFromUrl as
|
|
13410
|
+
hostFromUrl as hostFromUrl4,
|
|
12995
13411
|
loginWithDeviceCode,
|
|
12996
13412
|
getOrCreateShipId,
|
|
12997
13413
|
DeviceLoginError
|
|
12998
13414
|
} from "@tablation/client";
|
|
12999
13415
|
|
|
13000
13416
|
// src/seat-keys.ts
|
|
13001
|
-
import { getSessionStore as getSessionStore2, hostFromUrl as
|
|
13417
|
+
import { getSessionStore as getSessionStore2, hostFromUrl as hostFromUrl3 } from "@tablation/client";
|
|
13002
13418
|
var SEAT_KEY_ROLES = ROLE_NAMES.filter((r) => r !== "pair");
|
|
13003
13419
|
function keyIsAdmin(route) {
|
|
13004
13420
|
const role = route.resolved?.role;
|
|
@@ -13022,7 +13438,7 @@ async function resolveReleaseKey(route, getStore = getSessionStore2) {
|
|
|
13022
13438
|
const agentId = route.resolved?.releaseAgentId;
|
|
13023
13439
|
if (!agentId) return ship(keyIsAdmin(route) ? "release seat has no linked Agents row (run `crew agents sync`)" : `release seat ${MEMBER_NO_ROW}`);
|
|
13024
13440
|
try {
|
|
13025
|
-
const session = await getStore().get(
|
|
13441
|
+
const session = await getStore().get(hostFromUrl3(route.baseUrl), slugFor(route, agentId));
|
|
13026
13442
|
if (session?.apiKey) return { kind: "agent", apiKey: session.apiKey, agentId };
|
|
13027
13443
|
return ship(keyIsAdmin(route) ? "no agent key stored for the release seat (run `crew agents sync`)" : MEMBER_NO_KEY);
|
|
13028
13444
|
} catch {
|
|
@@ -13035,7 +13451,7 @@ async function resolveSeatKey(route, role, getStore = getSessionStore2) {
|
|
|
13035
13451
|
const agentId = seatAgentId(route, role);
|
|
13036
13452
|
if (!agentId) return ship(keyIsAdmin(route) ? "seat has no linked Agents row (run `crew agents sync`)" : `seat ${MEMBER_NO_ROW}`);
|
|
13037
13453
|
try {
|
|
13038
|
-
const session = await getStore().get(
|
|
13454
|
+
const session = await getStore().get(hostFromUrl3(route.baseUrl), slugFor(route, agentId));
|
|
13039
13455
|
if (session?.apiKey) return { kind: "agent", apiKey: session.apiKey, agentId };
|
|
13040
13456
|
return ship(keyIsAdmin(route) ? "no agent key stored for this seat (run `crew agents sync`)" : MEMBER_NO_KEY);
|
|
13041
13457
|
} catch {
|
|
@@ -13068,7 +13484,7 @@ async function mintMissingSeatKeys(route, o) {
|
|
|
13068
13484
|
detail: describeStoreError(e)
|
|
13069
13485
|
}));
|
|
13070
13486
|
}
|
|
13071
|
-
const host =
|
|
13487
|
+
const host = hostFromUrl3(route.baseUrl);
|
|
13072
13488
|
const base = route.baseUrl.replace(/\/+$/, "");
|
|
13073
13489
|
if (!o.dryRun) {
|
|
13074
13490
|
const probeSlug = seatKeySlug(splitRoute(route.route)[0], "_probe");
|
|
@@ -13189,7 +13605,7 @@ async function revokeSeatKeys(route, o) {
|
|
|
13189
13605
|
} catch (e) {
|
|
13190
13606
|
return targets.map(({ role }) => ({ role, action: "failed", detail: describeStoreError(e) }));
|
|
13191
13607
|
}
|
|
13192
|
-
const host =
|
|
13608
|
+
const host = hostFromUrl3(route.baseUrl);
|
|
13193
13609
|
const base = route.baseUrl.replace(/\/+$/, "");
|
|
13194
13610
|
for (const { role, agentId } of targets) {
|
|
13195
13611
|
if (!agentId) {
|
|
@@ -13301,8 +13717,8 @@ function describeCapability(i) {
|
|
|
13301
13717
|
}
|
|
13302
13718
|
|
|
13303
13719
|
// src/agents-sync-run.ts
|
|
13304
|
-
import { existsSync as
|
|
13305
|
-
import { dirname as
|
|
13720
|
+
import { existsSync as existsSync26, mkdirSync as mkdirSync16, readFileSync as readFileSync21, writeFileSync as writeFileSync18 } from "fs";
|
|
13721
|
+
import { dirname as dirname15 } from "path";
|
|
13306
13722
|
async function runAgentsSync(target, o) {
|
|
13307
13723
|
const dryRun = o.dryRun ?? false;
|
|
13308
13724
|
const force = o.force ?? false;
|
|
@@ -13335,12 +13751,12 @@ async function runAgentsSync(target, o) {
|
|
|
13335
13751
|
for (const s of skillResult?.outcomes ?? []) o.say(describeSkillSyncOutcome(s));
|
|
13336
13752
|
if (!dryRun) {
|
|
13337
13753
|
const resolvedPath = resolvedPathFor(o.stateDir, target.route);
|
|
13338
|
-
const raw =
|
|
13754
|
+
const raw = existsSync26(resolvedPath) ? JSON.parse(readFileSync21(resolvedPath, "utf8")) : {};
|
|
13339
13755
|
raw.agentPersonas = personaResult.agentPersonas;
|
|
13340
13756
|
if (skillResult) raw.agentSkills = skillResult.agentSkills;
|
|
13341
13757
|
if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
|
|
13342
|
-
|
|
13343
|
-
|
|
13758
|
+
mkdirSync16(dirname15(resolvedPath), { recursive: true });
|
|
13759
|
+
writeFileSync18(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
13344
13760
|
`);
|
|
13345
13761
|
if (target.resolved) {
|
|
13346
13762
|
target.resolved.agentPersonas = personaResult.agentPersonas;
|
|
@@ -13355,7 +13771,7 @@ async function runAgentsSync(target, o) {
|
|
|
13355
13771
|
}
|
|
13356
13772
|
|
|
13357
13773
|
// src/logbook.ts
|
|
13358
|
-
import { createHash as
|
|
13774
|
+
import { createHash as createHash4 } from "crypto";
|
|
13359
13775
|
import { TablationClient as TablationClient4 } from "@tablation/client";
|
|
13360
13776
|
var LogbookError = class extends Error {
|
|
13361
13777
|
};
|
|
@@ -13428,7 +13844,7 @@ async function showLogEntry(route, entryId, opts = {}) {
|
|
|
13428
13844
|
};
|
|
13429
13845
|
}
|
|
13430
13846
|
const prompt = appendAdditions(match.prompt ?? "", match.prompt_additions);
|
|
13431
|
-
const actualSha =
|
|
13847
|
+
const actualSha = createHash4("sha256").update(prompt).digest("hex");
|
|
13432
13848
|
const exact = !!entry.prompt_sha && actualSha === entry.prompt_sha;
|
|
13433
13849
|
return {
|
|
13434
13850
|
entry,
|
|
@@ -13443,12 +13859,12 @@ async function showLogEntry(route, entryId, opts = {}) {
|
|
|
13443
13859
|
}
|
|
13444
13860
|
|
|
13445
13861
|
// src/repos-hooks.ts
|
|
13446
|
-
import { isMap as isMap2, parseDocument as
|
|
13862
|
+
import { isMap as isMap2, parseDocument as parseDocument3, YAMLMap } from "yaml";
|
|
13447
13863
|
|
|
13448
13864
|
// src/repos-cmd.ts
|
|
13449
|
-
import { homedir as
|
|
13450
|
-
import { basename as basename8, join as
|
|
13451
|
-
import { isMap, isSeq, parseDocument, stringify as stringify2 } from "yaml";
|
|
13865
|
+
import { homedir as homedir8 } from "os";
|
|
13866
|
+
import { basename as basename8, join as join26, resolve as resolve3 } from "path";
|
|
13867
|
+
import { isMap, isSeq, parseDocument as parseDocument2, stringify as stringify2 } from "yaml";
|
|
13452
13868
|
var ReposError = class extends Error {
|
|
13453
13869
|
};
|
|
13454
13870
|
function normalizeRemote(url) {
|
|
@@ -13472,7 +13888,7 @@ function matchRepoName(origin, resolved) {
|
|
|
13472
13888
|
return void 0;
|
|
13473
13889
|
}
|
|
13474
13890
|
function routeNamesOf(text) {
|
|
13475
|
-
const routes =
|
|
13891
|
+
const routes = parseDocument2(text).get("routes", true);
|
|
13476
13892
|
if (!isSeq(routes)) return [];
|
|
13477
13893
|
return routes.items.flatMap((i) => isMap(i) && typeof i.get("route") === "string" ? [i.get("route")] : []);
|
|
13478
13894
|
}
|
|
@@ -13493,7 +13909,7 @@ function parseAddArgs(args, routes, isDir) {
|
|
|
13493
13909
|
return { route, path: "." };
|
|
13494
13910
|
}
|
|
13495
13911
|
function expandDir(p, base) {
|
|
13496
|
-
const home = p === "~" ?
|
|
13912
|
+
const home = p === "~" ? homedir8() : p.startsWith("~/") ? `${homedir8()}/${p.slice(2)}` : p;
|
|
13497
13913
|
return resolve3(base, home);
|
|
13498
13914
|
}
|
|
13499
13915
|
function routeMapOf(doc, route) {
|
|
@@ -13507,7 +13923,7 @@ function routeMapOf(doc, route) {
|
|
|
13507
13923
|
throw new ReposError(`no route "${route}" in the config (have: ${have})`);
|
|
13508
13924
|
}
|
|
13509
13925
|
function configuredRepos(text, route, base) {
|
|
13510
|
-
const doc =
|
|
13926
|
+
const doc = parseDocument2(text);
|
|
13511
13927
|
const m = routeMapOf(doc, route);
|
|
13512
13928
|
const repos = m.get("repos");
|
|
13513
13929
|
if (isMap(repos)) {
|
|
@@ -13522,7 +13938,7 @@ function configuredRepos(text, route, base) {
|
|
|
13522
13938
|
return dir ? [{ name: basename8(String(dir)), dir: expandDir(String(dir), base) }] : [];
|
|
13523
13939
|
}
|
|
13524
13940
|
function routeSetsHooks(text, route, name) {
|
|
13525
|
-
const m = routeMapOf(
|
|
13941
|
+
const m = routeMapOf(parseDocument2(text), route);
|
|
13526
13942
|
const repos = m.get("repos", true);
|
|
13527
13943
|
let target = m;
|
|
13528
13944
|
if (isMap(repos)) target = repos.get(name, true);
|
|
@@ -13578,7 +13994,7 @@ function listRepoRows(i) {
|
|
|
13578
13994
|
return rows;
|
|
13579
13995
|
}
|
|
13580
13996
|
function planRepoAdd(i) {
|
|
13581
|
-
const doc =
|
|
13997
|
+
const doc = parseDocument2(i.text);
|
|
13582
13998
|
if (doc.errors.length > 0) throw new ReposError(`config is not valid YAML \u2014 ${doc.errors[0].message}`);
|
|
13583
13999
|
const m = routeMapOf(doc, i.route);
|
|
13584
14000
|
const existing = configuredRepos(i.text, i.route, i.base);
|
|
@@ -13615,7 +14031,7 @@ function planRepoAdd(i) {
|
|
|
13615
14031
|
return { text: String(doc), mode, name, block: renderBlock(m), warning, unmatched: !matched };
|
|
13616
14032
|
}
|
|
13617
14033
|
function planRepoRemove(text, base, route, path) {
|
|
13618
|
-
const doc =
|
|
14034
|
+
const doc = parseDocument2(text);
|
|
13619
14035
|
if (doc.errors.length > 0) throw new ReposError(`config is not valid YAML \u2014 ${doc.errors[0].message}`);
|
|
13620
14036
|
const m = routeMapOf(doc, route);
|
|
13621
14037
|
const target = expandDir(path, base);
|
|
@@ -13693,7 +14109,7 @@ function planRepoClone(i) {
|
|
|
13693
14109
|
`the Repos row "${name}" has no remote to clone from \u2014 attach an existing checkout with \`crew repos add ${i.route} ${name} <path>\`, or set the row's remote in the tracker and re-run \`crew connect\``
|
|
13694
14110
|
);
|
|
13695
14111
|
}
|
|
13696
|
-
const doc =
|
|
14112
|
+
const doc = parseDocument2(i.text);
|
|
13697
14113
|
const m = routeMapOf(doc, i.route);
|
|
13698
14114
|
const existing = configuredRepos(i.text, i.route, i.base);
|
|
13699
14115
|
if (existing.some((e) => e.name.toLowerCase() === name.toLowerCase())) {
|
|
@@ -13701,7 +14117,7 @@ function planRepoClone(i) {
|
|
|
13701
14117
|
}
|
|
13702
14118
|
const ship = doc.get("ship", true);
|
|
13703
14119
|
const basePath = String(m.get("reposBasePath") ?? (isMap(ship) ? ship.get("reposBasePath") : void 0) ?? "~/Crew");
|
|
13704
|
-
const target = i.dir ? expandDir(i.dir, i.base) :
|
|
14120
|
+
const target = i.dir ? expandDir(i.dir, i.base) : join26(expandDir(basePath, i.base), i.route.split("/")[0], name);
|
|
13705
14121
|
const add = planRepoAdd({ text: i.text, base: i.base, route: i.route, path: target, origin: remote, resolved: i.resolved, name });
|
|
13706
14122
|
return { ...add, remote, target };
|
|
13707
14123
|
}
|
|
@@ -13847,7 +14263,7 @@ function repoDirOf(text, base, route, name) {
|
|
|
13847
14263
|
return hit.dir;
|
|
13848
14264
|
}
|
|
13849
14265
|
function planRouteHooks(text, route, name, hooks) {
|
|
13850
|
-
const doc =
|
|
14266
|
+
const doc = parseDocument3(text);
|
|
13851
14267
|
if (doc.errors.length > 0) throw new ReposError(`config is not valid YAML \u2014 ${doc.errors[0].message}`);
|
|
13852
14268
|
const m = routeMapOf(doc, route);
|
|
13853
14269
|
const repos = m.get("repos", true);
|
|
@@ -13909,7 +14325,7 @@ function existingRepoConfig(f) {
|
|
|
13909
14325
|
}
|
|
13910
14326
|
|
|
13911
14327
|
// src/repos-hooks-agent.ts
|
|
13912
|
-
import { spawn as spawn4, execFileSync as
|
|
14328
|
+
import { spawn as spawn4, execFileSync as execFileSync5 } from "child_process";
|
|
13913
14329
|
import { realpathSync as realpathSync2 } from "fs";
|
|
13914
14330
|
import { isAbsolute as isAbsolute2, relative } from "path";
|
|
13915
14331
|
var AgentSuggestError = class extends Error {
|
|
@@ -14012,7 +14428,7 @@ function classifyFailure(o) {
|
|
|
14012
14428
|
}
|
|
14013
14429
|
function porcelain(dir) {
|
|
14014
14430
|
try {
|
|
14015
|
-
return
|
|
14431
|
+
return execFileSync5("git", ["status", "--porcelain", "--untracked-files=all"], { cwd: dir, encoding: "utf8" });
|
|
14016
14432
|
} catch {
|
|
14017
14433
|
return void 0;
|
|
14018
14434
|
}
|
|
@@ -14081,7 +14497,7 @@ ${run2.err}` });
|
|
|
14081
14497
|
}
|
|
14082
14498
|
|
|
14083
14499
|
// src/sample-ticket.ts
|
|
14084
|
-
import { basename as basename9, dirname as
|
|
14500
|
+
import { basename as basename9, dirname as dirname17, join as join27 } from "path";
|
|
14085
14501
|
var SAMPLE_REPO = "crew-sample";
|
|
14086
14502
|
var SAMPLE_REMOTE = `local/${SAMPLE_REPO}`;
|
|
14087
14503
|
var SAMPLE_MARKER = "sample: true";
|
|
@@ -14104,8 +14520,8 @@ has something safe to work on: its test and build hooks always pass, it has no
|
|
|
14104
14520
|
remote, and \`crew sample-ticket --remove\` deletes it.
|
|
14105
14521
|
`;
|
|
14106
14522
|
function sampleDir(existing, reposBase, workspace) {
|
|
14107
|
-
if (existing.length > 0) return
|
|
14108
|
-
return
|
|
14523
|
+
if (existing.length > 0) return join27(dirname17(existing[0]), SAMPLE_REPO);
|
|
14524
|
+
return join27(reposBase, workspace, SAMPLE_REPO);
|
|
14109
14525
|
}
|
|
14110
14526
|
var SAMPLE_TITLE = "Sample: add a documented `greet` function with a test";
|
|
14111
14527
|
function sampleDescription() {
|
|
@@ -14149,7 +14565,7 @@ function planSampleTicket(i) {
|
|
|
14149
14565
|
}
|
|
14150
14566
|
|
|
14151
14567
|
// src/doctor-fix.ts
|
|
14152
|
-
import { parseDocument as
|
|
14568
|
+
import { parseDocument as parseDocument4 } from "yaml";
|
|
14153
14569
|
var sound = (r) => r.hasRepos && r.problems.length === 0;
|
|
14154
14570
|
function planFix(f) {
|
|
14155
14571
|
const actions = [];
|
|
@@ -14176,7 +14592,7 @@ function routesInScope(routes, named) {
|
|
|
14176
14592
|
return named ? routes.filter((r) => r.route === named) : routes;
|
|
14177
14593
|
}
|
|
14178
14594
|
function planEnable(text, route) {
|
|
14179
|
-
const doc =
|
|
14595
|
+
const doc = parseDocument4(text);
|
|
14180
14596
|
routeMapOf(doc, route).set("enabled", true);
|
|
14181
14597
|
return String(doc);
|
|
14182
14598
|
}
|
|
@@ -14186,8 +14602,8 @@ function schedulerInstalled(plan, exists, crontab) {
|
|
|
14186
14602
|
}
|
|
14187
14603
|
|
|
14188
14604
|
// src/worktree-sweep.ts
|
|
14189
|
-
import { existsSync as
|
|
14190
|
-
import { basename as basename10, dirname as
|
|
14605
|
+
import { existsSync as existsSync27, readFileSync as readFileSync22, readdirSync as readdirSync8 } from "fs";
|
|
14606
|
+
import { basename as basename10, dirname as dirname18, join as join28, resolve as resolve4 } from "path";
|
|
14191
14607
|
function planWorktreeSweep(target, tickets, contract) {
|
|
14192
14608
|
const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
|
|
14193
14609
|
const verified = contract.statuses.verified;
|
|
@@ -14241,7 +14657,7 @@ function worktreeTicketNumbers(target) {
|
|
|
14241
14657
|
return [...nums];
|
|
14242
14658
|
}
|
|
14243
14659
|
function siblingWorktreeDirs(target) {
|
|
14244
|
-
const parent =
|
|
14660
|
+
const parent = dirname18(target.dir.replace(/[/\\]+$/, ""));
|
|
14245
14661
|
const stem = `${basename10(target.dir.replace(/[/\\]+$/, ""))}-`;
|
|
14246
14662
|
const registered = new Set(worktrees(target.dir).map((w) => resolve4(w.path)));
|
|
14247
14663
|
let names;
|
|
@@ -14254,7 +14670,7 @@ function siblingWorktreeDirs(target) {
|
|
|
14254
14670
|
for (const name of names) {
|
|
14255
14671
|
if (!name.startsWith(stem)) continue;
|
|
14256
14672
|
const m = DIR_NUMBER_SUFFIXED.exec(name);
|
|
14257
|
-
const path =
|
|
14673
|
+
const path = join28(parent, name);
|
|
14258
14674
|
if (m && !registered.has(resolve4(path))) out.push({ path, number: m[1] });
|
|
14259
14675
|
}
|
|
14260
14676
|
return out;
|
|
@@ -14289,7 +14705,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
|
|
|
14289
14705
|
let removed = 0;
|
|
14290
14706
|
let keptBranches = 0;
|
|
14291
14707
|
for (const a of actions) {
|
|
14292
|
-
if (!dryRun && base && !a.keepBranch &&
|
|
14708
|
+
if (!dryRun && base && !a.keepBranch && existsSync27(a.path) && isHandBackMerge(a.path, base)) {
|
|
14293
14709
|
if (gitOk(a.path, ["merge", "--abort"]) !== null) {
|
|
14294
14710
|
log.emit(
|
|
14295
14711
|
`abandoned the hand-back merge of ${base} in ${a.path}: ${a.ticket.issue_id} deployed without it`,
|
|
@@ -14297,7 +14713,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
|
|
|
14297
14713
|
);
|
|
14298
14714
|
}
|
|
14299
14715
|
}
|
|
14300
|
-
if (!dryRun &&
|
|
14716
|
+
if (!dryRun && existsSync27(a.path)) {
|
|
14301
14717
|
let dirty = false;
|
|
14302
14718
|
try {
|
|
14303
14719
|
dirty = status(a.path).length > 0;
|
|
@@ -14308,7 +14724,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
|
|
|
14308
14724
|
continue;
|
|
14309
14725
|
}
|
|
14310
14726
|
}
|
|
14311
|
-
if (!dryRun && a.requirePushed && a.branch &&
|
|
14727
|
+
if (!dryRun && a.requirePushed && a.branch && existsSync27(a.path) && remoteConfigured(cwd, remote) && worktreeHoldsUnpushed(a.path, remote)) {
|
|
14312
14728
|
refuse(a, "unpushed commits", `${a.branch} has commits that are not on ${remote}`);
|
|
14313
14729
|
continue;
|
|
14314
14730
|
}
|
|
@@ -14357,7 +14773,7 @@ function isHandBackMerge(path, base) {
|
|
|
14357
14773
|
if (!msgPath) return false;
|
|
14358
14774
|
let first;
|
|
14359
14775
|
try {
|
|
14360
|
-
first =
|
|
14776
|
+
first = readFileSync22(resolve4(path, msgPath), "utf8").split("\n")[0] ?? "";
|
|
14361
14777
|
} catch {
|
|
14362
14778
|
return false;
|
|
14363
14779
|
}
|
|
@@ -14462,14 +14878,14 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
|
|
|
14462
14878
|
}
|
|
14463
14879
|
|
|
14464
14880
|
// src/stream-sweep.ts
|
|
14465
|
-
import { existsSync as
|
|
14466
|
-
import { join as
|
|
14881
|
+
import { existsSync as existsSync28, readdirSync as readdirSync9, rmSync as rmSync13, statSync as statSync8 } from "fs";
|
|
14882
|
+
import { join as join29 } from "path";
|
|
14467
14883
|
var DAY_MS2 = 24 * 60 * 60 * 1e3;
|
|
14468
14884
|
function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
|
|
14469
|
-
const dir =
|
|
14470
|
-
if (!
|
|
14885
|
+
const dir = join29(stateDir, "streams");
|
|
14886
|
+
if (!existsSync28(dir)) return [];
|
|
14471
14887
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
14472
|
-
return readdirSync9(dir).map((name) =>
|
|
14888
|
+
return readdirSync9(dir).map((name) => join29(dir, name)).filter((path) => {
|
|
14473
14889
|
try {
|
|
14474
14890
|
return statSync8(path).mtimeMs < cutoff;
|
|
14475
14891
|
} catch {
|
|
@@ -14485,7 +14901,7 @@ function applyStreamSweep(paths, dryRun, log) {
|
|
|
14485
14901
|
continue;
|
|
14486
14902
|
}
|
|
14487
14903
|
try {
|
|
14488
|
-
|
|
14904
|
+
rmSync13(path, { force: true });
|
|
14489
14905
|
removed++;
|
|
14490
14906
|
log.emit(`removed stream artifact ${path}`, { step: "worktree" });
|
|
14491
14907
|
} catch (e) {
|
|
@@ -14495,9 +14911,9 @@ function applyStreamSweep(paths, dryRun, log) {
|
|
|
14495
14911
|
return removed;
|
|
14496
14912
|
}
|
|
14497
14913
|
function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
14498
|
-
if (!
|
|
14914
|
+
if (!existsSync28(stateDir)) return [];
|
|
14499
14915
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
14500
|
-
return readdirSync9(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) =>
|
|
14916
|
+
return readdirSync9(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join29(stateDir, name)).filter((path) => {
|
|
14501
14917
|
try {
|
|
14502
14918
|
return statSync8(path).mtimeMs < cutoff;
|
|
14503
14919
|
} catch {
|
|
@@ -14507,26 +14923,26 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
|
14507
14923
|
}
|
|
14508
14924
|
|
|
14509
14925
|
// src/cli.ts
|
|
14510
|
-
import { realpathSync as realpathSync3, readFileSync as
|
|
14926
|
+
import { realpathSync as realpathSync3, readFileSync as readFileSync28, existsSync as existsSync34, mkdirSync as mkdirSync21, writeFileSync as writeFileSync22, rmSync as rmSync15, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync3, statSync as statSync10, readdirSync as readdirSync14 } from "fs";
|
|
14511
14927
|
import { createInterface } from "readline/promises";
|
|
14512
14928
|
import { dirname as dirOf, resolve as resolvePath } from "path";
|
|
14513
14929
|
|
|
14514
14930
|
// src/ssh-keys.ts
|
|
14515
|
-
import { execFileSync as
|
|
14516
|
-
import { existsSync as
|
|
14517
|
-
import { dirname as
|
|
14931
|
+
import { execFileSync as execFileSync6 } from "child_process";
|
|
14932
|
+
import { existsSync as existsSync29, mkdirSync as mkdirSync17, readFileSync as readFileSync23, chmodSync } from "fs";
|
|
14933
|
+
import { dirname as dirname19, join as join30 } from "path";
|
|
14518
14934
|
var SshKeyError = class extends Error {
|
|
14519
14935
|
};
|
|
14520
14936
|
function shipSshKeyPathFor(stateDir) {
|
|
14521
|
-
return
|
|
14937
|
+
return join30(stateDir, "ssh", "id_ed25519");
|
|
14522
14938
|
}
|
|
14523
14939
|
function ensureShipSshKeypair(stateDir) {
|
|
14524
14940
|
const privateKeyPath = shipSshKeyPathFor(stateDir);
|
|
14525
14941
|
const publicKeyPath = `${privateKeyPath}.pub`;
|
|
14526
|
-
if (!
|
|
14527
|
-
|
|
14942
|
+
if (!existsSync29(privateKeyPath)) {
|
|
14943
|
+
mkdirSync17(dirname19(privateKeyPath), { recursive: true });
|
|
14528
14944
|
try {
|
|
14529
|
-
|
|
14945
|
+
execFileSync6("ssh-keygen", [
|
|
14530
14946
|
"-t",
|
|
14531
14947
|
"ed25519",
|
|
14532
14948
|
"-N",
|
|
@@ -14544,71 +14960,21 @@ function ensureShipSshKeypair(stateDir) {
|
|
|
14544
14960
|
}
|
|
14545
14961
|
chmodSync(privateKeyPath, 384);
|
|
14546
14962
|
}
|
|
14547
|
-
if (!
|
|
14963
|
+
if (!existsSync29(publicKeyPath)) {
|
|
14548
14964
|
throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
|
|
14549
14965
|
}
|
|
14550
|
-
const publicKey =
|
|
14966
|
+
const publicKey = readFileSync23(publicKeyPath, "utf8").trim();
|
|
14551
14967
|
return { privateKeyPath, publicKeyPath, publicKey };
|
|
14552
14968
|
}
|
|
14553
14969
|
function sshKeygenAvailable() {
|
|
14554
14970
|
try {
|
|
14555
|
-
|
|
14971
|
+
execFileSync6("ssh-keygen", ["-V"], { stdio: ["ignore", "ignore", "ignore"] });
|
|
14556
14972
|
return true;
|
|
14557
14973
|
} catch (e) {
|
|
14558
14974
|
return e.code !== "ENOENT";
|
|
14559
14975
|
}
|
|
14560
14976
|
}
|
|
14561
14977
|
|
|
14562
|
-
// src/machine-id.ts
|
|
14563
|
-
import { execFileSync as execFileSync6 } from "child_process";
|
|
14564
|
-
import { createHash as createHash4 } from "crypto";
|
|
14565
|
-
import { existsSync as existsSync28, mkdirSync as mkdirSync16, readFileSync as readFileSync22, writeFileSync as writeFileSync17 } from "fs";
|
|
14566
|
-
import { dirname as dirname18, join as join29 } from "path";
|
|
14567
|
-
function machineIdPathFor(stateDir) {
|
|
14568
|
-
return join29(stateDir, "machine-id");
|
|
14569
|
-
}
|
|
14570
|
-
var sha256 = (s) => createHash4("sha256").update(s).digest("hex");
|
|
14571
|
-
function readRawMachineId(platform2 = process.platform) {
|
|
14572
|
-
try {
|
|
14573
|
-
if (platform2 === "darwin") {
|
|
14574
|
-
const out = execFileSync6("ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"], {
|
|
14575
|
-
encoding: "utf8",
|
|
14576
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
14577
|
-
});
|
|
14578
|
-
return /"IOPlatformUUID"\s*=\s*"([^"]+)"/.exec(out)?.[1];
|
|
14579
|
-
}
|
|
14580
|
-
for (const p of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
|
|
14581
|
-
if (existsSync28(p)) {
|
|
14582
|
-
const id = readFileSync22(p, "utf8").trim();
|
|
14583
|
-
if (id) return id;
|
|
14584
|
-
}
|
|
14585
|
-
}
|
|
14586
|
-
} catch {
|
|
14587
|
-
}
|
|
14588
|
-
return void 0;
|
|
14589
|
-
}
|
|
14590
|
-
function ensureMachineId(stateDir, opts = {}) {
|
|
14591
|
-
if (opts.override?.trim()) return sha256(opts.override.trim());
|
|
14592
|
-
const path = machineIdPathFor(stateDir);
|
|
14593
|
-
if (existsSync28(path)) {
|
|
14594
|
-
const cached = readFileSync22(path, "utf8").trim();
|
|
14595
|
-
if (/^[0-9a-f]{64}$/.test(cached)) return cached;
|
|
14596
|
-
}
|
|
14597
|
-
const raw = (opts.read ?? readRawMachineId)();
|
|
14598
|
-
if (!raw) return void 0;
|
|
14599
|
-
const hashed = sha256(raw);
|
|
14600
|
-
mkdirSync16(dirname18(path), { recursive: true });
|
|
14601
|
-
writeFileSync17(path, `${hashed}
|
|
14602
|
-
`, { mode: 384 });
|
|
14603
|
-
return hashed;
|
|
14604
|
-
}
|
|
14605
|
-
function matchShipRows(ships, shipName2, machineId) {
|
|
14606
|
-
const hasColumn = ships.some((r) => "machine_id" in r);
|
|
14607
|
-
const byId = hasColumn && machineId ? ships.filter((r) => r.machine_id === machineId) : [];
|
|
14608
|
-
if (byId.length > 0) return { rows: byId, by: "machine_id", hasColumn };
|
|
14609
|
-
return { rows: ships.filter((r) => (r.name ?? "").trim() === shipName2.trim()), by: "name", hasColumn };
|
|
14610
|
-
}
|
|
14611
|
-
|
|
14612
14978
|
// src/passenger-containers.ts
|
|
14613
14979
|
import { execFileSync as execFileSync7 } from "child_process";
|
|
14614
14980
|
import { createHash as createHash5 } from "crypto";
|
|
@@ -14813,9 +15179,9 @@ function syncAllPassengerCheckouts(cfg) {
|
|
|
14813
15179
|
|
|
14814
15180
|
// src/tunnel.ts
|
|
14815
15181
|
import { spawn as spawn5 } from "child_process";
|
|
14816
|
-
import { existsSync as
|
|
15182
|
+
import { existsSync as existsSync30, mkdirSync as mkdirSync18, readFileSync as readFileSync24, writeFileSync as writeFileSync19, unlinkSync as unlinkSync3, readdirSync as readdirSync10 } from "fs";
|
|
14817
15183
|
import { randomInt } from "crypto";
|
|
14818
|
-
import { join as
|
|
15184
|
+
import { join as join31 } from "path";
|
|
14819
15185
|
var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
|
|
14820
15186
|
var defaultSpawn = (cmd, args) => spawn5(cmd, args, DEFAULT_SPAWN_OPTIONS);
|
|
14821
15187
|
var SLUG_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
@@ -14882,21 +15248,21 @@ function startTunnel(opts) {
|
|
|
14882
15248
|
};
|
|
14883
15249
|
}
|
|
14884
15250
|
function tunnelStatePath(stateDir, workspaceId) {
|
|
14885
|
-
return
|
|
15251
|
+
return join31(stateDir, "passengers", `${workspaceId}.json`);
|
|
14886
15252
|
}
|
|
14887
15253
|
function readPersistedTunnel(stateDir, workspaceId) {
|
|
14888
15254
|
const path = tunnelStatePath(stateDir, workspaceId);
|
|
14889
|
-
if (!
|
|
15255
|
+
if (!existsSync30(path)) return void 0;
|
|
14890
15256
|
try {
|
|
14891
|
-
return JSON.parse(
|
|
15257
|
+
return JSON.parse(readFileSync24(path, "utf8"));
|
|
14892
15258
|
} catch {
|
|
14893
15259
|
return void 0;
|
|
14894
15260
|
}
|
|
14895
15261
|
}
|
|
14896
15262
|
function writePersistedTunnel(stateDir, state) {
|
|
14897
15263
|
const path = tunnelStatePath(stateDir, state.workspaceId);
|
|
14898
|
-
|
|
14899
|
-
|
|
15264
|
+
mkdirSync18(join31(stateDir, "passengers"), { recursive: true });
|
|
15265
|
+
writeFileSync19(path, `${JSON.stringify(state, null, 2)}
|
|
14900
15266
|
`);
|
|
14901
15267
|
}
|
|
14902
15268
|
function updatePersistedTunnel(stateDir, workspaceId, patch2) {
|
|
@@ -14993,8 +15359,8 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
|
|
|
14993
15359
|
}
|
|
14994
15360
|
}
|
|
14995
15361
|
function readAllPersistedWorkspaceIds(stateDir) {
|
|
14996
|
-
const dir =
|
|
14997
|
-
if (!
|
|
15362
|
+
const dir = join31(stateDir, "passengers");
|
|
15363
|
+
if (!existsSync30(dir)) return [];
|
|
14998
15364
|
return readdirSync10(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
|
|
14999
15365
|
}
|
|
15000
15366
|
|
|
@@ -15115,8 +15481,8 @@ async function unregisterEndpoint(workspaceId, info, d) {
|
|
|
15115
15481
|
import { createServer as createHttpServer } from "http";
|
|
15116
15482
|
import { spawn as spawn6 } from "child_process";
|
|
15117
15483
|
import { randomBytes } from "crypto";
|
|
15118
|
-
import { existsSync as
|
|
15119
|
-
import { join as
|
|
15484
|
+
import { existsSync as existsSync31, mkdirSync as mkdirSync19, readFileSync as readFileSync25, writeFileSync as writeFileSync20, unlinkSync as unlinkSync4, readdirSync as readdirSync11 } from "fs";
|
|
15485
|
+
import { join as join32 } from "path";
|
|
15120
15486
|
|
|
15121
15487
|
// src/secret-compare.ts
|
|
15122
15488
|
import { timingSafeEqual } from "crypto";
|
|
@@ -15205,21 +15571,21 @@ var defaultSpawn2 = (cmd, args, env) => {
|
|
|
15205
15571
|
return child;
|
|
15206
15572
|
};
|
|
15207
15573
|
function syncDaemonStatePath(stateDir, workspaceId) {
|
|
15208
|
-
return
|
|
15574
|
+
return join32(stateDir, "passengers", `${workspaceId}-sync.json`);
|
|
15209
15575
|
}
|
|
15210
15576
|
function readPersistedSyncDaemon(stateDir, workspaceId) {
|
|
15211
15577
|
const path = syncDaemonStatePath(stateDir, workspaceId);
|
|
15212
|
-
if (!
|
|
15578
|
+
if (!existsSync31(path)) return void 0;
|
|
15213
15579
|
try {
|
|
15214
|
-
return JSON.parse(
|
|
15580
|
+
return JSON.parse(readFileSync25(path, "utf8"));
|
|
15215
15581
|
} catch {
|
|
15216
15582
|
return void 0;
|
|
15217
15583
|
}
|
|
15218
15584
|
}
|
|
15219
15585
|
function writePersistedSyncDaemon(stateDir, state) {
|
|
15220
15586
|
const path = syncDaemonStatePath(stateDir, state.workspaceId);
|
|
15221
|
-
|
|
15222
|
-
|
|
15587
|
+
mkdirSync19(join32(stateDir, "passengers"), { recursive: true });
|
|
15588
|
+
writeFileSync20(path, `${JSON.stringify(state, null, 2)}
|
|
15223
15589
|
`);
|
|
15224
15590
|
}
|
|
15225
15591
|
function removePersistedSyncDaemon(stateDir, workspaceId) {
|
|
@@ -15239,8 +15605,8 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
|
|
|
15239
15605
|
}
|
|
15240
15606
|
}
|
|
15241
15607
|
function readAllPersistedSyncWorkspaceIds(stateDir) {
|
|
15242
|
-
const dir =
|
|
15243
|
-
if (!
|
|
15608
|
+
const dir = join32(stateDir, "passengers");
|
|
15609
|
+
if (!existsSync31(dir)) return [];
|
|
15244
15610
|
const suffix = "-sync.json";
|
|
15245
15611
|
return readdirSync11(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
|
|
15246
15612
|
}
|
|
@@ -15328,12 +15694,12 @@ function describeCause(err) {
|
|
|
15328
15694
|
}
|
|
15329
15695
|
|
|
15330
15696
|
// src/daemon.ts
|
|
15331
|
-
import { existsSync as
|
|
15332
|
-
import { join as
|
|
15697
|
+
import { existsSync as existsSync33, mkdirSync as mkdirSync20, readFileSync as readFileSync27, readdirSync as readdirSync13, rmSync as rmSync14, statSync as statSync9, writeFileSync as writeFileSync21 } from "fs";
|
|
15698
|
+
import { join as join34 } from "path";
|
|
15333
15699
|
|
|
15334
15700
|
// src/interrupted.ts
|
|
15335
|
-
import { existsSync as
|
|
15336
|
-
import { join as
|
|
15701
|
+
import { existsSync as existsSync32, readdirSync as readdirSync12, readFileSync as readFileSync26 } from "fs";
|
|
15702
|
+
import { join as join33 } from "path";
|
|
15337
15703
|
var INTERRUPTED_MARKER = calloutHeader("NOTE", "Interrupted");
|
|
15338
15704
|
function interruptedComment(b) {
|
|
15339
15705
|
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.";
|
|
@@ -15352,21 +15718,21 @@ var pidAlive = (pid) => {
|
|
|
15352
15718
|
};
|
|
15353
15719
|
function runFinished(streamPath) {
|
|
15354
15720
|
const path = `${streamPath}.meta.json`;
|
|
15355
|
-
if (!
|
|
15721
|
+
if (!existsSync32(path)) return false;
|
|
15356
15722
|
try {
|
|
15357
|
-
return JSON.parse(
|
|
15723
|
+
return JSON.parse(readFileSync26(path, "utf8")).running !== true;
|
|
15358
15724
|
} catch {
|
|
15359
15725
|
return true;
|
|
15360
15726
|
}
|
|
15361
15727
|
}
|
|
15362
15728
|
function orphanedStreams(o) {
|
|
15363
|
-
const dir =
|
|
15364
|
-
if (!
|
|
15729
|
+
const dir = join33(o.stateDir, "streams");
|
|
15730
|
+
if (!existsSync32(dir)) return [];
|
|
15365
15731
|
const ownerAlive = o.isOwnerAlive ?? pidAlive;
|
|
15366
15732
|
const out = [];
|
|
15367
15733
|
for (const name of readdirSync12(dir)) {
|
|
15368
15734
|
if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
|
|
15369
|
-
const streamPath =
|
|
15735
|
+
const streamPath = join33(dir, name);
|
|
15370
15736
|
if (runFinished(streamPath)) continue;
|
|
15371
15737
|
const sidecar = readRunSidecar(streamPath);
|
|
15372
15738
|
if (!sidecar || sidecar.route !== o.routeLabel) continue;
|
|
@@ -15486,7 +15852,7 @@ function latestMtime(path) {
|
|
|
15486
15852
|
if (!stat.isDirectory()) return 0;
|
|
15487
15853
|
let latest = stat.mtimeMs;
|
|
15488
15854
|
for (const entry of readdirSync13(path, { withFileTypes: true })) {
|
|
15489
|
-
latest = Math.max(latest, latestMtime(
|
|
15855
|
+
latest = Math.max(latest, latestMtime(join34(path, entry.name)));
|
|
15490
15856
|
}
|
|
15491
15857
|
return latest;
|
|
15492
15858
|
}
|
|
@@ -15495,23 +15861,23 @@ function computeTreeSignature(paths) {
|
|
|
15495
15861
|
}
|
|
15496
15862
|
function makeStaleChecker(paths, opts = {}) {
|
|
15497
15863
|
const initial = computeTreeSignature(paths);
|
|
15498
|
-
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 &&
|
|
15864
|
+
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync33(opts.reloadSentinel);
|
|
15499
15865
|
}
|
|
15500
15866
|
function reloadSentinelFor(stateDir) {
|
|
15501
|
-
return
|
|
15867
|
+
return join34(stateDir, "reload-requested");
|
|
15502
15868
|
}
|
|
15503
15869
|
function daemonStampFor(stateDir) {
|
|
15504
|
-
return
|
|
15870
|
+
return join34(stateDir, "daemon.started");
|
|
15505
15871
|
}
|
|
15506
15872
|
function noteDaemonStarted(stateDir, now = Date.now()) {
|
|
15507
|
-
|
|
15508
|
-
|
|
15509
|
-
|
|
15873
|
+
mkdirSync20(stateDir, { recursive: true });
|
|
15874
|
+
rmSync14(reloadSentinelFor(stateDir), { force: true });
|
|
15875
|
+
writeFileSync21(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
|
|
15510
15876
|
`);
|
|
15511
15877
|
}
|
|
15512
15878
|
function readDaemonStarted(stateDir) {
|
|
15513
15879
|
try {
|
|
15514
|
-
const v = JSON.parse(
|
|
15880
|
+
const v = JSON.parse(readFileSync27(daemonStampFor(stateDir), "utf8")).startedAt;
|
|
15515
15881
|
return typeof v === "number" ? v : null;
|
|
15516
15882
|
} catch {
|
|
15517
15883
|
return null;
|
|
@@ -15574,6 +15940,7 @@ async function runOnePassUnguarded(o) {
|
|
|
15574
15940
|
const spawn7 = o.spawnRoleAgent ?? runRoleAgent;
|
|
15575
15941
|
const breaker = o.spawnBreaker ?? new SpawnBreaker();
|
|
15576
15942
|
const ctx = { route: o.route, ship: o.ship, state: o.state, emit, getSessionStore: o.getSessionStore };
|
|
15943
|
+
if (o.configFile) await reconcileRouteShipName({ route: o.route, ship: o.ship, configFile: o.configFile, emit });
|
|
15577
15944
|
try {
|
|
15578
15945
|
await (o.reconcile ?? reconcileForRoute)(ctx, o);
|
|
15579
15946
|
} catch (e) {
|
|
@@ -15896,7 +16263,7 @@ async function buildEnvironment(route, ship, ticket) {
|
|
|
15896
16263
|
}
|
|
15897
16264
|
|
|
15898
16265
|
// src/cli.ts
|
|
15899
|
-
var CREW_HOME = isCompiledBinary(import.meta.url) ?
|
|
16266
|
+
var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname20(process.execPath) : resolve5(dirname20(fileURLToPath(import.meta.url)), "..");
|
|
15900
16267
|
var RELEASE_LOCK_TTL_MS = 20 * 60 * 1e3;
|
|
15901
16268
|
var CYCLE_SECONDS2 = 120;
|
|
15902
16269
|
var STALE_CYCLES = 3;
|
|
@@ -15940,7 +16307,7 @@ async function pickOperator(holds2, me) {
|
|
|
15940
16307
|
const choices = holds2.filter((h) => !h.user_id || h.user_id === me.id);
|
|
15941
16308
|
const canCreate = !!me.id;
|
|
15942
16309
|
process.stderr.write("\nWhich of these Crew rows is you?\n");
|
|
15943
|
-
choices.forEach((h, i) => process.stderr.write(` ${i + 1}. ${h.name || h.
|
|
16310
|
+
choices.forEach((h, i) => process.stderr.write(` ${i + 1}. ${h.name || "unnamed row"}${h.detail ? ` <${h.detail}>` : ""}
|
|
15944
16311
|
`));
|
|
15945
16312
|
if (canCreate) process.stderr.write(` 0. Create a new Crew row for ${me.name ?? ""} ${me.email ?? ""}`.trimEnd() + "\n");
|
|
15946
16313
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
@@ -15974,10 +16341,11 @@ async function pickResolvedStatuses(unrecognized) {
|
|
|
15974
16341
|
}
|
|
15975
16342
|
async function offerTemplateInstall(authOpts, workspaceId, decided) {
|
|
15976
16343
|
const templates = await listLibraryTemplates(authOpts);
|
|
15977
|
-
const
|
|
16344
|
+
const keys = await resolveIssuesTemplateKeys(authOpts);
|
|
16345
|
+
const template = templates.find((t) => t.identifier === ISSUES_TEMPLATE_IDENTIFIERS[0]) ?? templates.find((t) => keys.has(t.identifier) || keys.has(t.id));
|
|
15978
16346
|
if (!template) {
|
|
15979
16347
|
process.stderr.write(`
|
|
15980
|
-
No template with identifier "${
|
|
16348
|
+
No template with identifier "${ISSUES_TEMPLATE_IDENTIFIERS[0]}" is published to the Library yet \u2014 nothing to offer to install.
|
|
15981
16349
|
`);
|
|
15982
16350
|
return void 0;
|
|
15983
16351
|
}
|
|
@@ -16015,8 +16383,8 @@ No project here has what a route needs. Install the "${template.name}" library t
|
|
|
16015
16383
|
`);
|
|
16016
16384
|
return installed.project.slug;
|
|
16017
16385
|
}
|
|
16018
|
-
function usage() {
|
|
16019
|
-
|
|
16386
|
+
function usage(problem, requested = false) {
|
|
16387
|
+
const text = `crew \u2014 a standing team of headless agents
|
|
16020
16388
|
|
|
16021
16389
|
crew poll [route] decide a cycle and report it; writes nothing
|
|
16022
16390
|
crew run [route] [--role R] run the winning role's session
|
|
@@ -16044,12 +16412,14 @@ function usage() {
|
|
|
16044
16412
|
resume with no argument clears only the whole-crew pause; a role or release pause needs its own resume
|
|
16045
16413
|
crew log [route] tail the log
|
|
16046
16414
|
crew inbox [--member NAME] your tickets across every workspace (or a colleague's)
|
|
16415
|
+
crew connect --sign-out [--host H] [--workspace W] [--json] clear the keychain session a dry-run connect stored (no crew.yaml needed; nothing to remove still exits 0)
|
|
16047
16416
|
crew connect resolve a workspace's ids into a crew.yaml block, then (admin key) sync personas/skills and mint seat keys (--no-agents-sync skips that; --no-mcp skips registering the Tablation MCP server with Claude Code; --mcp replace overwrites a different one; --dry-run works with no crew.yaml and writes none: no file, no rows, no seat keys \u2014 the device approval and workspace credential behave as in a real run; with no workspace named it first reuses the keychain session a previous dry-run stored for the host, falling back to device approval when that is missing or rejected, and the signed_in event's session field says which)
|
|
16048
16417
|
crew disconnect remove what connect installed for Pair (Claude MCP; Codex/Gemini instructions block and MCP entry per ship.pair)
|
|
16049
16418
|
crew disconnect --reset [--remove-checkouts] [--dry-run] [--json] remove everything connect and install wrote on this machine: scheduler units, Pair/MCP entries, seat keys, keychain credential, resolved state, then crew.yaml (--remove-checkouts also deletes checkouts crew cloned, never one with local changes)
|
|
16050
16419
|
crew agents sync [route] [--force] [--revoke-seat-keys] push crew's personas AND skill files into the workspace Agents/Agent Skills tables (--force overwrites even a diverged persona or skill; never one marked Customize prompt)
|
|
16051
16420
|
crew agents prompt R [route] print one persona's current prompt (e.g. R=pair, for a SessionStart hook; --harness H adds which CLI it serves)
|
|
16052
16421
|
crew skills sync [route] push only crew's skill files (e.g. grill-me) into the workspace Agent Skills table
|
|
16422
|
+
crew ship rename NAME [--json] rename this ship in crew.yaml and on every workspace's Ships row
|
|
16053
16423
|
crew repos add [ROUTE] [NAME] [PATH] attach a local checkout to a route in crew.yaml (--dry-run previews)
|
|
16054
16424
|
crew repos clone [ROUTE] NAME clone a Repos row's remote into the route directory and attach it (--dir PATH, --dry-run, --json)
|
|
16055
16425
|
crew repos hooks [ROUTE] NAME suggest test/build hooks for a checkout (--agent, --write route|repo, --recipes, --json)
|
|
@@ -16078,10 +16448,18 @@ Options:
|
|
|
16078
16448
|
--all (inbox) include closed tickets
|
|
16079
16449
|
|
|
16080
16450
|
In watch: space pauses the stream, q quits.
|
|
16081
|
-
|
|
16451
|
+
`;
|
|
16452
|
+
if (requested) {
|
|
16453
|
+
process.stdout.write(text);
|
|
16454
|
+
process.exit(0);
|
|
16455
|
+
}
|
|
16456
|
+
process.stderr.write(problem ? `crew: ${problem}
|
|
16457
|
+
|
|
16458
|
+
${text}` : text);
|
|
16082
16459
|
process.exit(2);
|
|
16083
16460
|
}
|
|
16084
16461
|
var argv = process.argv.slice(2);
|
|
16462
|
+
if (helpRequest(argv)) usage(void 0, true);
|
|
16085
16463
|
var versionReq = versionRequest(argv);
|
|
16086
16464
|
if (versionReq) {
|
|
16087
16465
|
const version = crewVersion(CREW_HOME);
|
|
@@ -16101,7 +16479,46 @@ var connectEvents = createEmitter(command === "connect" && flag("json"), (l) =>
|
|
|
16101
16479
|
process.stdout.write(l);
|
|
16102
16480
|
});
|
|
16103
16481
|
var connectChoices = parseChoices(argv);
|
|
16104
|
-
if (!command) usage();
|
|
16482
|
+
if (!command) usage(argv.length ? `unknown option "${argv[0]}"` : void 0);
|
|
16483
|
+
var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
|
|
16484
|
+
"passenger-sync-daemon",
|
|
16485
|
+
"agents",
|
|
16486
|
+
"connect",
|
|
16487
|
+
"daemon",
|
|
16488
|
+
"deploy",
|
|
16489
|
+
"disconnect",
|
|
16490
|
+
"doctor",
|
|
16491
|
+
"drop",
|
|
16492
|
+
"inbox",
|
|
16493
|
+
"install",
|
|
16494
|
+
"log",
|
|
16495
|
+
"logbook",
|
|
16496
|
+
"merge",
|
|
16497
|
+
"passengers",
|
|
16498
|
+
"pause",
|
|
16499
|
+
"poll",
|
|
16500
|
+
"ports",
|
|
16501
|
+
"projects",
|
|
16502
|
+
"reap",
|
|
16503
|
+
"release",
|
|
16504
|
+
"reload",
|
|
16505
|
+
"repos",
|
|
16506
|
+
"resume",
|
|
16507
|
+
"rotate-passenger-url",
|
|
16508
|
+
"run",
|
|
16509
|
+
"sample-ticket",
|
|
16510
|
+
"selftest",
|
|
16511
|
+
"ship",
|
|
16512
|
+
"skills",
|
|
16513
|
+
"status",
|
|
16514
|
+
"sync",
|
|
16515
|
+
"unassign",
|
|
16516
|
+
"uninstall",
|
|
16517
|
+
"watch",
|
|
16518
|
+
"whoami",
|
|
16519
|
+
"worktree"
|
|
16520
|
+
]);
|
|
16521
|
+
if (!KNOWN_COMMANDS.has(command)) usage(`unknown command "${command}"`);
|
|
16105
16522
|
function findSyncRoute(c, name) {
|
|
16106
16523
|
if (!name && c.routes.length > 1) {
|
|
16107
16524
|
throw new ConfigError(`this ship has ${c.routes.length} routes \u2014 name one: ${c.routes.map((r) => r.route).join(", ")}`);
|
|
@@ -16191,6 +16608,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
16191
16608
|
let cfg;
|
|
16192
16609
|
let firstRun;
|
|
16193
16610
|
let firstRunFromFlags = false;
|
|
16611
|
+
let agentBinSearched;
|
|
16194
16612
|
try {
|
|
16195
16613
|
const connectIsTTY = !!(process.stdin.isTTY && process.stdout.isTTY);
|
|
16196
16614
|
const wizardFlags = {
|
|
@@ -16200,7 +16618,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
16200
16618
|
hostPassengers: flag("host-passengers"),
|
|
16201
16619
|
noWizard: flag("no-wizard")
|
|
16202
16620
|
};
|
|
16203
|
-
if (command === "connect" && usesFlagAnswers(wizardFlags, connectIsTTY) && shouldRunWizard({ isTTY: true, crewHome: CREW_HOME })) {
|
|
16621
|
+
if (command === "connect" && !flag("sign-out") && usesFlagAnswers(wizardFlags, connectIsTTY) && shouldRunWizard({ isTTY: true, crewHome: CREW_HOME })) {
|
|
16204
16622
|
const flagged = flagWizardAnswers(wizardFlags, {
|
|
16205
16623
|
hostname: hostname2(),
|
|
16206
16624
|
platform: hostPlatform(),
|
|
@@ -16217,8 +16635,9 @@ if (command === "passenger-sync-daemon") {
|
|
|
16217
16635
|
const configPath = firstRunConfigPath();
|
|
16218
16636
|
firstRun = { answers: flagged.answers, configPath };
|
|
16219
16637
|
firstRunFromFlags = true;
|
|
16638
|
+
agentBinSearched = flagged.agentBinSearched;
|
|
16220
16639
|
cfg = loadConfig(CREW_HOME, configPath, { text: renderShipBlock(flagged.answers), allowNoRoutes: true });
|
|
16221
|
-
} else if (command === "connect" && shouldRunWizard({
|
|
16640
|
+
} else if (command === "connect" && !flag("sign-out") && shouldRunWizard({
|
|
16222
16641
|
isTTY: connectIsTTY,
|
|
16223
16642
|
crewHome: CREW_HOME
|
|
16224
16643
|
})) {
|
|
@@ -16239,7 +16658,13 @@ if (command === "passenger-sync-daemon") {
|
|
|
16239
16658
|
firstRun = { answers, configPath };
|
|
16240
16659
|
cfg = loadConfig(CREW_HOME, configPath, { text: renderShipBlock(answers), allowNoRoutes: true });
|
|
16241
16660
|
} else {
|
|
16242
|
-
|
|
16661
|
+
const toleratesNoRoutes = command === "repos" || command === "ship" || command === "disconnect" && flag("reset") || command === "connect" && flag("sign-out");
|
|
16662
|
+
const configless = command === "connect" && flag("sign-out") || command === "disconnect" && flag("reset");
|
|
16663
|
+
if (configless && !existsSync34(findConfigFile(CREW_HOME))) {
|
|
16664
|
+
cfg = loadConfig(CREW_HOME, firstRunConfigPath(), { text: "ship: {}\n", allowNoRoutes: true });
|
|
16665
|
+
} else {
|
|
16666
|
+
cfg = loadConfig(CREW_HOME, void 0, toleratesNoRoutes ? { allowNoRoutes: true } : {});
|
|
16667
|
+
}
|
|
16243
16668
|
}
|
|
16244
16669
|
} catch (e) {
|
|
16245
16670
|
if (e instanceof ConfigError) {
|
|
@@ -16257,7 +16682,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
16257
16682
|
const DAEMON_SUBCOMMANDS = /* @__PURE__ */ new Set(["start", "stop", "status", "restart"]);
|
|
16258
16683
|
const worktreeArgs = command === "worktree" ? splitWorktreeArgs(positional, cfg.routes.map((r) => r.route)) : void 0;
|
|
16259
16684
|
const named = worktreeArgs ? worktreeArgs.route : command === "daemon" && DAEMON_SUBCOMMANDS.has(positional[1] ?? "") ? positional[2] : positional[1];
|
|
16260
|
-
const fleetWide = command === "inbox" || command === "connect" || command === "disconnect" || command === "agents" || command === "skills" || command === "logbook" || command === "repos" || command === "projects" || command === "whoami" || releaseFleetWide || command === "pause" || command === "resume" || command === "reload" || FLEET_CAPABLE.has(command) && !named && cfg.routes.length > 1 || worktreeArgs !== void 0 && !named && cfg.routes.length > 1;
|
|
16685
|
+
const fleetWide = command === "inbox" || command === "connect" || command === "disconnect" || command === "agents" || command === "skills" || command === "logbook" || command === "repos" || command === "ship" || command === "projects" || command === "whoami" || releaseFleetWide || command === "pause" || command === "resume" || command === "reload" || FLEET_CAPABLE.has(command) && !named && cfg.routes.length > 1 || worktreeArgs !== void 0 && !named && cfg.routes.length > 1;
|
|
16261
16686
|
let route;
|
|
16262
16687
|
try {
|
|
16263
16688
|
route = fleetWide ? cfg.routes[0] : findRoute(cfg, named);
|
|
@@ -16473,8 +16898,8 @@ if (command === "passenger-sync-daemon") {
|
|
|
16473
16898
|
}
|
|
16474
16899
|
if (host === "linux" && hasSystemd) {
|
|
16475
16900
|
const label = labelFor(CREW_HOME, "run");
|
|
16476
|
-
const staleTimer =
|
|
16477
|
-
if (
|
|
16901
|
+
const staleTimer = join35(homedir9(), ".config", "systemd", "user", `${label}.timer`);
|
|
16902
|
+
if (existsSync34(staleTimer)) {
|
|
16478
16903
|
process.stdout.write(
|
|
16479
16904
|
`removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
|
|
16480
16905
|
`
|
|
@@ -16500,7 +16925,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
16500
16925
|
const tracker = new Tracker(c, cfg.ship);
|
|
16501
16926
|
problems.push(...validateContract(tracker.contract).map((p) => `contract: ${p}`));
|
|
16502
16927
|
for (const r of await resolvedRepos(c)) {
|
|
16503
|
-
if (!
|
|
16928
|
+
if (!existsSync34(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
|
|
16504
16929
|
if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
|
|
16505
16930
|
problems.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
|
|
16506
16931
|
}
|
|
@@ -16518,7 +16943,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
16518
16943
|
}
|
|
16519
16944
|
async function doctorFix(o) {
|
|
16520
16945
|
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))));
|
|
16521
|
-
const unitExists = (p) =>
|
|
16946
|
+
const unitExists = (p) => existsSync34(p);
|
|
16522
16947
|
const crontab = o.host === "linux" ? spawnSync6("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
|
|
16523
16948
|
const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
|
|
16524
16949
|
const plan = planFix({ routes, schedulerAbsent, host: o.host });
|
|
@@ -16535,7 +16960,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
16535
16960
|
for (const a of plan.actions) {
|
|
16536
16961
|
if (a.kind === "enable") {
|
|
16537
16962
|
if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
|
|
16538
|
-
|
|
16963
|
+
writeFileSync22(cfg.configFile, planEnable(readFileSync28(cfg.configFile, "utf8"), a.route));
|
|
16539
16964
|
enabledSound.add(a.route);
|
|
16540
16965
|
changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
|
|
16541
16966
|
} else {
|
|
@@ -16721,7 +17146,10 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16721
17146
|
}
|
|
16722
17147
|
if (!dryRun) {
|
|
16723
17148
|
const holderLabel = `${cfg.ship.name}:${process.pid}`;
|
|
16724
|
-
const got = await tracker.acquireBoardLock(scope, holderLabel, RELEASE_LOCK_TTL_MS
|
|
17149
|
+
const got = await tracker.acquireBoardLock(scope, holderLabel, RELEASE_LOCK_TTL_MS, {
|
|
17150
|
+
warn: (m) => remit.warn(m, { step: "release" }),
|
|
17151
|
+
debug: (m) => remit.emit(m, { step: "release" })
|
|
17152
|
+
});
|
|
16725
17153
|
if (!got.ok && got.reason === "missing") {
|
|
16726
17154
|
const detail = `no Locks row for ${scope} \u2014 run \`crew connect ${c.route}\` to provision it`;
|
|
16727
17155
|
remit.warn(`release for ${scope} refused: ${detail}`, { step: "release" });
|
|
@@ -17015,6 +17443,11 @@ ${changed2.map((c) => ` - ${c}
|
|
|
17015
17443
|
case "poll":
|
|
17016
17444
|
case "run": {
|
|
17017
17445
|
const skipInlineRelease = flag("no-release");
|
|
17446
|
+
if (!dryRun) {
|
|
17447
|
+
for (const r of fleetWide ? cfg.routes : [route]) {
|
|
17448
|
+
await reconcileRouteShipName({ route: r, ship: cfg.ship, configFile: cfg.configFile, emit: emit.forRoute(r.route) });
|
|
17449
|
+
}
|
|
17450
|
+
}
|
|
17018
17451
|
if (fleetWide) {
|
|
17019
17452
|
const fleet = await decideFleet({
|
|
17020
17453
|
routes: cfg.routes,
|
|
@@ -17536,7 +17969,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
17536
17969
|
process.exit(1);
|
|
17537
17970
|
}
|
|
17538
17971
|
requireArmed("run the daemon loop");
|
|
17539
|
-
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [
|
|
17972
|
+
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join35(CREW_HOME, "src"), join35(CREW_HOME, "dist"), join35(CREW_HOME, "bin")];
|
|
17540
17973
|
const configPaths = daemonConfigPaths(cfg);
|
|
17541
17974
|
watchPaths.push(...configPaths);
|
|
17542
17975
|
noteDaemonStarted(cfg.ship.stateDir);
|
|
@@ -17548,6 +17981,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
17548
17981
|
state,
|
|
17549
17982
|
maxConcurrentAgents: cfg.ship.maxConcurrentAgents,
|
|
17550
17983
|
spawnBreaker,
|
|
17984
|
+
configFile: cfg.configFile,
|
|
17551
17985
|
newEmitter: () => new Emitter({
|
|
17552
17986
|
route: route.route,
|
|
17553
17987
|
eventFile: eventFileFor(cfg.ship.stateDir),
|
|
@@ -17754,15 +18188,72 @@ ${changed2.map((c) => ` - ${c}
|
|
|
17754
18188
|
`);
|
|
17755
18189
|
if (!dryRun) {
|
|
17756
18190
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
17757
|
-
const raw =
|
|
18191
|
+
const raw = existsSync34(resolvedPath) ? JSON.parse(readFileSync28(resolvedPath, "utf8")) : {};
|
|
17758
18192
|
raw.agentSkills = result.agentSkills;
|
|
17759
|
-
|
|
17760
|
-
|
|
18193
|
+
mkdirSync21(dirOf(resolvedPath), { recursive: true });
|
|
18194
|
+
writeFileSync22(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
17761
18195
|
`);
|
|
17762
18196
|
}
|
|
17763
18197
|
if (result.outcomes.some((o) => o.action === "diverged")) process.exitCode = 1;
|
|
17764
18198
|
break;
|
|
17765
18199
|
}
|
|
18200
|
+
case "ship": {
|
|
18201
|
+
if (positional[1] !== "rename" || positional.length !== 3) {
|
|
18202
|
+
process.stderr.write("crew ship rename NAME [--json]\n");
|
|
18203
|
+
process.exit(2);
|
|
18204
|
+
}
|
|
18205
|
+
let newName;
|
|
18206
|
+
try {
|
|
18207
|
+
newName = validateShipName(positional[2]);
|
|
18208
|
+
} catch (e) {
|
|
18209
|
+
if (e instanceof ShipNameError) {
|
|
18210
|
+
process.stderr.write(`crew ship: ${e.message}
|
|
18211
|
+
`);
|
|
18212
|
+
process.exit(2);
|
|
18213
|
+
}
|
|
18214
|
+
throw e;
|
|
18215
|
+
}
|
|
18216
|
+
const oldName = cfg.ship.name;
|
|
18217
|
+
writeFileSync22(cfg.configFile, setShipNameInYaml(readFileSync28(cfg.configFile, "utf8"), newName));
|
|
18218
|
+
cfg.ship.name = newName;
|
|
18219
|
+
let renameMachineId;
|
|
18220
|
+
try {
|
|
18221
|
+
renameMachineId = ensureMachineId(cfg.ship.stateDir);
|
|
18222
|
+
} catch {
|
|
18223
|
+
}
|
|
18224
|
+
const outcomes = [];
|
|
18225
|
+
for (const r of cfg.routes) {
|
|
18226
|
+
if (!r.resolved?.shipsModelId) {
|
|
18227
|
+
outcomes.push({ route: r.route, status: "no-ships-table" });
|
|
18228
|
+
continue;
|
|
18229
|
+
}
|
|
18230
|
+
const resolvedFile = resolvedPathFor(cfg.ship.stateDir, r.route);
|
|
18231
|
+
const tracker = new Tracker(r, cfg.ship);
|
|
18232
|
+
const picked = pickShipRow(await tracker.shipRows(), renameMachineId, [oldName, readSyncedName(resolvedFile) ?? ""]);
|
|
18233
|
+
if (picked.kind !== "row") {
|
|
18234
|
+
outcomes.push({ route: r.route, status: picked.kind === "unsupported" ? "unsupported" : picked.kind === "none" ? "no-row" : "ambiguous" });
|
|
18235
|
+
continue;
|
|
18236
|
+
}
|
|
18237
|
+
try {
|
|
18238
|
+
await tracker.renameShipRow(picked.row.id, newName);
|
|
18239
|
+
writeSyncedName(resolvedFile, newName);
|
|
18240
|
+
outcomes.push({ route: r.route, status: "renamed", rowId: picked.row.id });
|
|
18241
|
+
} catch (e) {
|
|
18242
|
+
outcomes.push({ route: r.route, status: "refused", rowId: picked.row.id, error: e.message });
|
|
18243
|
+
}
|
|
18244
|
+
}
|
|
18245
|
+
if (flag("json")) {
|
|
18246
|
+
process.stdout.write(`${JSON.stringify({ name: newName, previous: oldName, configFile: cfg.configFile, routes: outcomes })}
|
|
18247
|
+
`);
|
|
18248
|
+
} else {
|
|
18249
|
+
process.stdout.write(`ship renamed "${oldName}" -> "${newName}" in ${cfg.configFile}
|
|
18250
|
+
`);
|
|
18251
|
+
for (const o of outcomes) process.stdout.write(` ${o.route}: ${o.status}${o.error ? ` (${o.error})` : ""}
|
|
18252
|
+
`);
|
|
18253
|
+
}
|
|
18254
|
+
if (outcomes.some((o) => o.status === "refused")) process.exitCode = 1;
|
|
18255
|
+
break;
|
|
18256
|
+
}
|
|
17766
18257
|
case "repos": {
|
|
17767
18258
|
const sub = positional[1];
|
|
17768
18259
|
const usage2 = () => {
|
|
@@ -17770,13 +18261,13 @@ ${changed2.map((c) => ` - ${c}
|
|
|
17770
18261
|
process.exit(2);
|
|
17771
18262
|
};
|
|
17772
18263
|
if (sub !== "add" && sub !== "list" && sub !== "clone" && sub !== "hooks") usage2();
|
|
17773
|
-
const configText =
|
|
18264
|
+
const configText = readFileSync28(cfg.configFile, "utf8");
|
|
17774
18265
|
if (sub === "hooks") {
|
|
17775
18266
|
try {
|
|
17776
18267
|
const a = parseHooksArgs(positional.slice(2), routeNamesOf(configText));
|
|
17777
|
-
const dir = repoDirOf(configText,
|
|
17778
|
-
if (!
|
|
17779
|
-
const files = { exists: (r) =>
|
|
18268
|
+
const dir = repoDirOf(configText, dirname20(cfg.configFile), a.route, a.name);
|
|
18269
|
+
if (!existsSync34(dir)) throw new ReposError(`${dir} does not exist on disk`);
|
|
18270
|
+
const files = { exists: (r) => existsSync34(resolvePath(dir, r)), list: (r) => {
|
|
17780
18271
|
try {
|
|
17781
18272
|
return readdirSync14(resolvePath(dir, r));
|
|
17782
18273
|
} catch {
|
|
@@ -17784,7 +18275,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
17784
18275
|
}
|
|
17785
18276
|
}, read: (r) => {
|
|
17786
18277
|
try {
|
|
17787
|
-
return
|
|
18278
|
+
return readFileSync28(resolvePath(dir, r), "utf8");
|
|
17788
18279
|
} catch {
|
|
17789
18280
|
return void 0;
|
|
17790
18281
|
}
|
|
@@ -17822,13 +18313,13 @@ ${changed2.map((c) => ` - ${c}
|
|
|
17822
18313
|
if (write && suggestion.source === null && recipeIds.length === 0 && hookSpecs.length === 0) throw new ReposError(`nothing to write: ${suggestion.notes[0]}`);
|
|
17823
18314
|
if (write === "route") {
|
|
17824
18315
|
const plan = planRouteHooks(configText, a.route, a.name, picked.hooks);
|
|
17825
|
-
|
|
18316
|
+
writeFileSync22(cfg.configFile, plan.text);
|
|
17826
18317
|
written = { target: "route", file: cfg.configFile, hooks: plan.written, kept: plan.kept };
|
|
17827
18318
|
} else if (write === "repo") {
|
|
17828
18319
|
const have = existingRepoConfig(files);
|
|
17829
18320
|
if (have) throw new ReposError(`${resolvePath(dir, have)} already exists; not overwriting it`);
|
|
17830
18321
|
const file = resolvePath(dir, ".crew.yaml");
|
|
17831
|
-
|
|
18322
|
+
writeFileSync22(file, starterRepoConfig(picked.hooks, picked.release), { flag: "wx" });
|
|
17832
18323
|
written = { target: "repo", file, hooks: Object.keys(picked.hooks), kept: [] };
|
|
17833
18324
|
}
|
|
17834
18325
|
if (flag("json")) {
|
|
@@ -17880,7 +18371,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
17880
18371
|
if (sub === "list" && !routeName) usage2();
|
|
17881
18372
|
if (sub === "add") {
|
|
17882
18373
|
try {
|
|
17883
|
-
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) =>
|
|
18374
|
+
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync34(resolvePath(p.replace(/^~(?=\/|$)/, homedir9()))));
|
|
17884
18375
|
routeName = a.route;
|
|
17885
18376
|
addPath = a.path;
|
|
17886
18377
|
addName = a.name;
|
|
@@ -17909,15 +18400,15 @@ ${changed2.map((c) => ` - ${c}
|
|
|
17909
18400
|
}
|
|
17910
18401
|
const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
|
|
17911
18402
|
let resolvedRemotes;
|
|
17912
|
-
if (
|
|
18403
|
+
if (existsSync34(resolvedFile)) {
|
|
17913
18404
|
try {
|
|
17914
|
-
resolvedRemotes = JSON.parse(
|
|
18405
|
+
resolvedRemotes = JSON.parse(readFileSync28(resolvedFile, "utf8"));
|
|
17915
18406
|
} catch {
|
|
17916
18407
|
}
|
|
17917
18408
|
}
|
|
17918
18409
|
const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
|
|
17919
18410
|
const text = configText;
|
|
17920
|
-
const base =
|
|
18411
|
+
const base = dirname20(cfg.configFile);
|
|
17921
18412
|
try {
|
|
17922
18413
|
if (sub === "list") {
|
|
17923
18414
|
if (flag("json")) {
|
|
@@ -17927,10 +18418,10 @@ ${changed2.map((c) => ` - ${c}
|
|
|
17927
18418
|
base,
|
|
17928
18419
|
resolved: resolvedRemotes,
|
|
17929
18420
|
originOf,
|
|
17930
|
-
exists: (d) =>
|
|
18421
|
+
exists: (d) => existsSync34(d),
|
|
17931
18422
|
repoHasHooks: (d) => REPO_CONFIG_FILENAMES.some((f) => {
|
|
17932
18423
|
try {
|
|
17933
|
-
return Object.keys(parseRepoConfig(
|
|
18424
|
+
return Object.keys(parseRepoConfig(readFileSync28(resolvePath(d, f), "utf8"), f).hooks).length > 0;
|
|
17934
18425
|
} catch {
|
|
17935
18426
|
return false;
|
|
17936
18427
|
}
|
|
@@ -17946,7 +18437,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
17946
18437
|
for (const r of repos) {
|
|
17947
18438
|
const origin2 = originOf(r.dir);
|
|
17948
18439
|
const row = matchRepoName(origin2, resolvedRemotes);
|
|
17949
|
-
const note = !
|
|
18440
|
+
const note = !existsSync34(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
|
|
17950
18441
|
process.stdout.write(`${r.name} ${r.dir} ${note}
|
|
17951
18442
|
`);
|
|
17952
18443
|
}
|
|
@@ -17958,9 +18449,9 @@ ${changed2.map((c) => ` - ${c}
|
|
|
17958
18449
|
` : `${human}
|
|
17959
18450
|
`);
|
|
17960
18451
|
const plan2 = planRepoClone({ text, base, route: routeName, name: addName, resolved: resolvedRemotes, dir: value("dir") });
|
|
17961
|
-
if (
|
|
18452
|
+
if (existsSync34(plan2.target) && (existsSync34(`${plan2.target}/.git`) || readdirSync14(plan2.target).length > 0)) {
|
|
17962
18453
|
throw new ReposError(
|
|
17963
|
-
|
|
18454
|
+
existsSync34(`${plan2.target}/.git`) ? `${plan2.target} is already a checkout \u2014 attach it with \`crew repos add ${routeName} ${plan2.name} ${plan2.target}\`` : `${plan2.target} exists and is not empty \u2014 pass --dir to clone somewhere else`
|
|
17964
18455
|
);
|
|
17965
18456
|
}
|
|
17966
18457
|
if (dryRun) {
|
|
@@ -17978,12 +18469,12 @@ ${changed2.map((c) => ` - ${c}
|
|
|
17978
18469
|
throw new ReposError(`${e.message}${authHint}
|
|
17979
18470
|
crew.yaml was not changed`);
|
|
17980
18471
|
}
|
|
17981
|
-
|
|
18472
|
+
writeFileSync22(cfg.configFile, plan2.text);
|
|
17982
18473
|
say("attached", { route: routeName, name: plan2.name, dir: plan2.target, mode: plan2.mode }, `cloned and attached ${plan2.target} to ${routeName} as ${plan2.mode === "dir" ? "dir" : `repos.${plan2.name}`} in ${cfg.configFile}`);
|
|
17983
18474
|
break;
|
|
17984
18475
|
}
|
|
17985
|
-
const target = resolvePath(addPath.replace(/^~(?=\/|$)/,
|
|
17986
|
-
const top =
|
|
18476
|
+
const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir9()));
|
|
18477
|
+
const top = existsSync34(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
|
|
17987
18478
|
if (!top) throw new ReposError(`${target} is not a git checkout`);
|
|
17988
18479
|
const origin = originOf(top);
|
|
17989
18480
|
const plan = planRepoAdd({
|
|
@@ -18032,15 +18523,15 @@ ${changed2.map((c) => ` - ${c}
|
|
|
18032
18523
|
const created = await rowTracker.createRepoRow(rowPayload);
|
|
18033
18524
|
process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
|
|
18034
18525
|
`);
|
|
18035
|
-
if (
|
|
18036
|
-
const raw = JSON.parse(
|
|
18526
|
+
if (existsSync34(resolvedFile)) {
|
|
18527
|
+
const raw = JSON.parse(readFileSync28(resolvedFile, "utf8"));
|
|
18037
18528
|
raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
|
|
18038
18529
|
raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
|
|
18039
|
-
|
|
18530
|
+
writeFileSync22(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
18040
18531
|
`);
|
|
18041
18532
|
}
|
|
18042
18533
|
}
|
|
18043
|
-
|
|
18534
|
+
writeFileSync22(cfg.configFile, plan.text);
|
|
18044
18535
|
process.stdout.write(`added ${top} to ${routeName} as ${plan.mode === "dir" ? "dir" : `repos.${plan.name}`} in ${cfg.configFile}
|
|
18045
18536
|
`);
|
|
18046
18537
|
} catch (e) {
|
|
@@ -18134,6 +18625,29 @@ ${reconstruction.prompt}
|
|
|
18134
18625
|
}
|
|
18135
18626
|
case "connect": {
|
|
18136
18627
|
const arg = positional[1];
|
|
18628
|
+
if (flag("sign-out")) {
|
|
18629
|
+
let signOutStore;
|
|
18630
|
+
try {
|
|
18631
|
+
signOutStore = getSessionStore3();
|
|
18632
|
+
} catch {
|
|
18633
|
+
signOutStore = void 0;
|
|
18634
|
+
}
|
|
18635
|
+
const outcomes = signOutStore ? await signOutSessions({
|
|
18636
|
+
store: signOutStore,
|
|
18637
|
+
stateDir: cfg.ship.stateDir,
|
|
18638
|
+
dryRun,
|
|
18639
|
+
host: value("host") ?? (value("base-url") ? hostFromUrl4(value("base-url")) : void 0),
|
|
18640
|
+
workspace: value("workspace")
|
|
18641
|
+
}) : [];
|
|
18642
|
+
const failed = outcomes.some((o) => o.action === "failed");
|
|
18643
|
+
if (flag("json")) process.stdout.write(`${JSON.stringify({ dryRun, ok: !failed, sessions: outcomes })}
|
|
18644
|
+
`);
|
|
18645
|
+
else if (!outcomes.length) process.stdout.write("nothing to sign out of\n");
|
|
18646
|
+
else for (const o of outcomes) process.stdout.write(`${o.action}: ${o.host}/${o.workspace}${o.detail ? ` (${o.detail})` : ""}
|
|
18647
|
+
`);
|
|
18648
|
+
if (failed) process.exitCode = 1;
|
|
18649
|
+
break;
|
|
18650
|
+
}
|
|
18137
18651
|
const canAsk = !!(process.stdin.isTTY && process.stdout.isTTY) && !connectEvents.json;
|
|
18138
18652
|
const stopForChoice = (key, message, options) => {
|
|
18139
18653
|
connectEvents.emit("choice", message, choiceData(key, options));
|
|
@@ -18151,6 +18665,7 @@ ${reconstruction.prompt}
|
|
|
18151
18665
|
const project = argProject ?? value("project") ?? connectChoices.project;
|
|
18152
18666
|
const baseUrl = value("base-url") ?? cfg.routes[0]?.baseUrl ?? DEFAULT_BASE_URL;
|
|
18153
18667
|
let apiKey = value("key") ?? process.env.CREW_CONNECT_KEY ?? cfg.routes.find((c) => c.apiKey)?.apiKey;
|
|
18668
|
+
const givenKeySource = value("key") ?? process.env.CREW_CONNECT_KEY ? "flag" : "config";
|
|
18154
18669
|
let store;
|
|
18155
18670
|
try {
|
|
18156
18671
|
store = getSessionStore3();
|
|
@@ -18160,11 +18675,11 @@ ${reconstruction.prompt}
|
|
|
18160
18675
|
let keychainBacked = false;
|
|
18161
18676
|
if (!apiKey && workspace && store) {
|
|
18162
18677
|
try {
|
|
18163
|
-
const existing = await store.get(
|
|
18678
|
+
const existing = await store.get(hostFromUrl4(baseUrl), workspace);
|
|
18164
18679
|
if (existing?.apiKey) {
|
|
18165
18680
|
apiKey = existing.apiKey;
|
|
18166
18681
|
keychainBacked = true;
|
|
18167
|
-
process.stderr.write(`Reusing the existing keychain session for ${
|
|
18682
|
+
process.stderr.write(`Reusing the existing keychain session for ${hostFromUrl4(baseUrl)}/${workspace}.
|
|
18168
18683
|
`);
|
|
18169
18684
|
}
|
|
18170
18685
|
} catch {
|
|
@@ -18175,7 +18690,7 @@ ${reconstruction.prompt}
|
|
|
18175
18690
|
const resumed = await resumeKeychainSession({
|
|
18176
18691
|
store,
|
|
18177
18692
|
stateDir: cfg.ship.stateDir,
|
|
18178
|
-
host:
|
|
18693
|
+
host: hostFromUrl4(baseUrl),
|
|
18179
18694
|
verify: async (k) => {
|
|
18180
18695
|
try {
|
|
18181
18696
|
await get({ baseUrl, apiKey: k, userAgent: cfg.ship.userAgent }, "/auth/me");
|
|
@@ -18190,7 +18705,7 @@ ${reconstruction.prompt}
|
|
|
18190
18705
|
workspace = resumed.workspace;
|
|
18191
18706
|
keychainBacked = true;
|
|
18192
18707
|
signInSource = "keychain";
|
|
18193
|
-
process.stderr.write(`Reusing the existing keychain session for ${
|
|
18708
|
+
process.stderr.write(`Reusing the existing keychain session for ${hostFromUrl4(baseUrl)}/${workspace}.
|
|
18194
18709
|
`);
|
|
18195
18710
|
}
|
|
18196
18711
|
}
|
|
@@ -18247,13 +18762,14 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
18247
18762
|
connectEvents.emit("signed_in", `signed in as ${result.identity.email}`, {
|
|
18248
18763
|
identity: { id: result.identity.id, email: result.identity.email },
|
|
18249
18764
|
workspaces: [{ slug: result.workspace.slug, name: result.workspace.name }],
|
|
18250
|
-
session: "device"
|
|
18765
|
+
session: "device",
|
|
18766
|
+
host: hostFromUrl4(baseUrl)
|
|
18251
18767
|
});
|
|
18252
18768
|
const deviceWorkspace = "path" in result.workspace && typeof result.workspace.path === "string" ? result.workspace.path : result.workspace.slug;
|
|
18253
18769
|
if (!workspace) workspace = deviceWorkspace;
|
|
18254
18770
|
if (store) {
|
|
18255
18771
|
try {
|
|
18256
|
-
await store.set(
|
|
18772
|
+
await store.set(hostFromUrl4(baseUrl), deviceWorkspace, {
|
|
18257
18773
|
apiKey: result.apiKey.key,
|
|
18258
18774
|
shipId: getOrCreateShipId(),
|
|
18259
18775
|
workspaceId: result.workspace.id,
|
|
@@ -18261,9 +18777,9 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
18261
18777
|
createdAt: result.apiKey.createdAt
|
|
18262
18778
|
});
|
|
18263
18779
|
keychainBacked = true;
|
|
18264
|
-
rememberSession(cfg.ship.stateDir,
|
|
18780
|
+
rememberSession(cfg.ship.stateDir, hostFromUrl4(baseUrl), deviceWorkspace);
|
|
18265
18781
|
process.stderr.write(
|
|
18266
|
-
`API key stored in the OS keychain for ${
|
|
18782
|
+
`API key stored in the OS keychain for ${hostFromUrl4(baseUrl)}/${deviceWorkspace}.
|
|
18267
18783
|
`
|
|
18268
18784
|
);
|
|
18269
18785
|
} catch (e) {
|
|
@@ -18299,7 +18815,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
18299
18815
|
platform: cfg.ship.platform,
|
|
18300
18816
|
machineId,
|
|
18301
18817
|
hostPassengers: firstRun?.answers.hostPassengers ?? existingRoute?.hostPassengers === true,
|
|
18302
|
-
sshPublicKey
|
|
18818
|
+
sshPublicKey,
|
|
18819
|
+
lastSyncedName: (r) => readSyncedName(resolvedPathFor(cfg.ship.stateDir, r))
|
|
18303
18820
|
};
|
|
18304
18821
|
const runConnect = async (ws, proj) => {
|
|
18305
18822
|
let found = await discover({ ...authOpts, workspace: ws, project: proj, area: value("area"), ship, dryRun, interactive: canAsk || connectEvents.json });
|
|
@@ -18308,7 +18825,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
18308
18825
|
connectEvents.emit("signed_in", `key belongs to ${found.meEmail ?? found.meId}`, {
|
|
18309
18826
|
identity: { id: found.meId, email: found.meEmail, name: found.meName },
|
|
18310
18827
|
workspaces: [{ slug: found.workspaceSlug, name: found.workspaceName, role: found.role }],
|
|
18311
|
-
session: signInSource
|
|
18828
|
+
session: signInSource,
|
|
18829
|
+
host: hostFromUrl4(baseUrl)
|
|
18312
18830
|
});
|
|
18313
18831
|
}
|
|
18314
18832
|
connectEvents.emit("workspace_resolved", `workspace ${found.workspaceSlug}`, {
|
|
@@ -18355,6 +18873,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
18355
18873
|
}
|
|
18356
18874
|
process.exit(2);
|
|
18357
18875
|
}
|
|
18876
|
+
if (found.shipNameAdopted && !dryRun) {
|
|
18877
|
+
writeFileSync22(cfg.configFile, setShipNameInYaml(readFileSync28(cfg.configFile, "utf8"), found.shipNameAdopted));
|
|
18878
|
+
cfg.ship.name = found.shipNameAdopted;
|
|
18879
|
+
}
|
|
18358
18880
|
if (found.shipRow) {
|
|
18359
18881
|
connectEvents.emit("ship_row", `Ships row "${found.shipRow.name}" ${found.shipRow.action}`, found.shipRow);
|
|
18360
18882
|
}
|
|
@@ -18383,8 +18905,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
18383
18905
|
}
|
|
18384
18906
|
}
|
|
18385
18907
|
let operator = found.operator;
|
|
18386
|
-
if (!operator && found.
|
|
18387
|
-
const picked = await pickOperator(found.
|
|
18908
|
+
if (!operator && found.candidates.length > 0 && canAsk) {
|
|
18909
|
+
const picked = await pickOperator(found.candidates, { id: found.meId, name: found.meName, email: found.meEmail });
|
|
18388
18910
|
if (picked === NEW_OPERATOR_ROW) {
|
|
18389
18911
|
await createOperatorRow({ ...authOpts, dryRun }, found);
|
|
18390
18912
|
operator = found.operator;
|
|
@@ -18395,15 +18917,15 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
18395
18917
|
}
|
|
18396
18918
|
}
|
|
18397
18919
|
let previous;
|
|
18398
|
-
const hadPreviousFile =
|
|
18920
|
+
const hadPreviousFile = existsSync34(resolvedPath);
|
|
18399
18921
|
if (hadPreviousFile) {
|
|
18400
18922
|
try {
|
|
18401
|
-
previous = JSON.parse(
|
|
18923
|
+
previous = JSON.parse(readFileSync28(resolvedPath, "utf8"));
|
|
18402
18924
|
} catch {
|
|
18403
18925
|
}
|
|
18404
18926
|
}
|
|
18405
18927
|
if (!operator) operator = previous?.operator;
|
|
18406
|
-
if (connectEvents.json && !operator && found.
|
|
18928
|
+
if (connectEvents.json && !operator && found.candidates.length > 0) {
|
|
18407
18929
|
const answer = connectChoices.operator;
|
|
18408
18930
|
if (answer === "new") {
|
|
18409
18931
|
await createOperatorRow({ ...authOpts, dryRun }, found);
|
|
@@ -18418,27 +18940,36 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
18418
18940
|
found.operatorAction = "linked";
|
|
18419
18941
|
}
|
|
18420
18942
|
} else {
|
|
18421
|
-
const mine = found.
|
|
18943
|
+
const mine = found.candidates.filter((h) => !h.user_id || h.user_id === found.meId);
|
|
18422
18944
|
stopForChoice("operator", "Which Crew row is you?", [
|
|
18423
|
-
...mine.map((h) => ({ value: h.id, label: h.name || h.id })),
|
|
18945
|
+
...mine.map((h) => ({ value: h.id, label: h.name || "unnamed row", ...h.detail ? { detail: h.detail } : { detail: h.id } })),
|
|
18424
18946
|
...found.meId ? [{ value: "new", label: `Create a new Crew row for ${found.meName ?? found.meEmail ?? "me"}` }] : []
|
|
18425
18947
|
]);
|
|
18426
18948
|
}
|
|
18427
18949
|
}
|
|
18950
|
+
if (!operator && found.operatorCompared) {
|
|
18951
|
+
process.stderr.write(`crew connect: no Crew row matched you \u2014 ${found.operatorCompared}
|
|
18952
|
+
`);
|
|
18953
|
+
connectEvents.emit("operator_row", "no Crew row matched you", { action: "unmatched", compared: found.operatorCompared });
|
|
18954
|
+
}
|
|
18428
18955
|
if (operator) {
|
|
18429
|
-
|
|
18956
|
+
const opRow = found.holds.find((h) => h.id === operator);
|
|
18957
|
+
const opEmail = opRow?.email || opRow?.memberEmail || (opRow?.user_id && opRow.user_id === found.meId ? found.meEmail : void 0);
|
|
18958
|
+
connectEvents.emit("operator_row", `operator ${opRow?.name ?? operator}`, {
|
|
18430
18959
|
id: operator,
|
|
18431
|
-
action: found.operatorAction ?? "previous"
|
|
18960
|
+
action: found.operatorAction ?? "previous",
|
|
18961
|
+
...opRow?.name ? { name: opRow.name } : {},
|
|
18962
|
+
...opEmail ? { email: opEmail } : {}
|
|
18432
18963
|
});
|
|
18433
18964
|
}
|
|
18434
18965
|
if (!operator && !hadPreviousFile) {
|
|
18435
18966
|
process.stderr.write(
|
|
18436
|
-
`crew connect ${route2}: cannot determine the operator \u2014 ${found.
|
|
18967
|
+
`crew connect ${route2}: cannot determine the operator \u2014 ${found.candidates.length} Crew row(s) could be it, none matching this key's own email, and there's no terminal to ask.
|
|
18437
18968
|
Re-run from a real terminal to pick one, or pass --key for a key whose own
|
|
18438
18969
|
identity email matches exactly one Crew row.
|
|
18439
18970
|
`
|
|
18440
18971
|
);
|
|
18441
|
-
stopWithProblem(`cannot determine the operator \u2014 ${found.
|
|
18972
|
+
stopWithProblem(`cannot determine the operator \u2014 ${found.candidates.length} Crew row(s) could be it`, "operator");
|
|
18442
18973
|
}
|
|
18443
18974
|
const alreadyReviewed = new Set(previous?.reviewedStatuses ?? []);
|
|
18444
18975
|
const stillUnrecognized = (found.unrecognizedStatuses ?? []).filter((s) => !alreadyReviewed.has(s.value));
|
|
@@ -18501,18 +19032,20 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
18501
19032
|
// nothing about its prompt actually changed.
|
|
18502
19033
|
...previous?.agentPersonas ? { agentPersonas: previous.agentPersonas } : {},
|
|
18503
19034
|
...previous?.agentSkills ? { agentSkills: previous.agentSkills } : {},
|
|
18504
|
-
...previous?.releaseAgentId ? { releaseAgentId: previous.releaseAgentId } : {}
|
|
19035
|
+
...previous?.releaseAgentId ? { releaseAgentId: previous.releaseAgentId } : {},
|
|
19036
|
+
// CREW-2495: the ship name this route last saw agree with its Ships row.
|
|
19037
|
+
...found.shipRow ? { shipName: found.shipRow.name } : previous?.shipName ? { shipName: previous.shipName } : {}
|
|
18505
19038
|
};
|
|
18506
19039
|
if (dryRun) {
|
|
18507
19040
|
process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
|
|
18508
19041
|
`);
|
|
18509
19042
|
} else {
|
|
18510
|
-
|
|
19043
|
+
mkdirSync21(dirname20(resolvedPath), { recursive: true });
|
|
18511
19044
|
if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
|
|
18512
19045
|
const tmpPath = `${resolvedPath}.tmp${process.pid}`;
|
|
18513
|
-
|
|
19046
|
+
writeFileSync22(tmpPath, `${JSON.stringify(resolved, null, 2)}
|
|
18514
19047
|
`);
|
|
18515
|
-
|
|
19048
|
+
renameSync3(tmpPath, resolvedPath);
|
|
18516
19049
|
process.stderr.write(`Resolved ids written to ${resolvedPath}
|
|
18517
19050
|
`);
|
|
18518
19051
|
}
|
|
@@ -18533,20 +19066,27 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
18533
19066
|
role: sr.role,
|
|
18534
19067
|
id: sr.id,
|
|
18535
19068
|
action: sr.action,
|
|
19069
|
+
...sr.shipId ? { ship_id: sr.shipId } : {},
|
|
18536
19070
|
key: minted?.action ?? "none",
|
|
18537
19071
|
...minted?.detail ? { detail: minted.detail } : {}
|
|
18538
19072
|
});
|
|
18539
19073
|
}
|
|
18540
19074
|
let apiKeyFile;
|
|
18541
19075
|
const apiKeyVar = "CREW_API_KEY";
|
|
19076
|
+
if (firstRunFromFlags && agentBinSearched && !dryRun) {
|
|
19077
|
+
const msg = `no agent binary: "claude" was not found on PATH or in ${agentBinSearched.join(", ")} \u2014 pass --agent-bin /absolute/path`;
|
|
19078
|
+
process.stderr.write(`crew connect: ${msg}
|
|
19079
|
+
`);
|
|
19080
|
+
stopWithProblem(msg, "wizard");
|
|
19081
|
+
}
|
|
18542
19082
|
if ((mintedFromDevice || firstRunFromFlags) && !keychainBacked) {
|
|
18543
19083
|
apiKeyFile = apiKeyPathFor(cfg.ship.stateDir, route2);
|
|
18544
19084
|
if (dryRun) {
|
|
18545
19085
|
process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
|
|
18546
19086
|
`);
|
|
18547
19087
|
} else {
|
|
18548
|
-
|
|
18549
|
-
|
|
19088
|
+
mkdirSync21(dirname20(apiKeyFile), { recursive: true });
|
|
19089
|
+
writeFileSync22(apiKeyFile, `${apiKeyVar}=${apiKey}
|
|
18550
19090
|
`, { mode: 384 });
|
|
18551
19091
|
process.stderr.write(`API key written to ${apiKeyFile}
|
|
18552
19092
|
`);
|
|
@@ -18555,7 +19095,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
18555
19095
|
const keyPersisted = keychainBacked || apiKeyFile !== void 0;
|
|
18556
19096
|
const wizard = firstRun;
|
|
18557
19097
|
const writeFirstRun = wizard !== void 0 && !dryRun && keyPersisted;
|
|
18558
|
-
const routeDir = value("dir") ?? (writeFirstRun ?
|
|
19098
|
+
const routeDir = value("dir") ?? (writeFirstRun ? join35(DEFAULT_REPOS_BASE_PATH, found.workspaceSlug) : void 0);
|
|
18559
19099
|
const routeBlock = renderConnection(
|
|
18560
19100
|
found,
|
|
18561
19101
|
route2,
|
|
@@ -18704,13 +19244,39 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
18704
19244
|
wroteConfig,
|
|
18705
19245
|
operator,
|
|
18706
19246
|
dryRun,
|
|
18707
|
-
unresolved: found.problems.length
|
|
19247
|
+
unresolved: found.problems.length,
|
|
19248
|
+
...firstRunFromFlags ? { agentBin: agentBinSearched ? null : firstRun?.answers.agentBin } : {},
|
|
19249
|
+
...agentBinSearched ? { agentBinNote: `no \`claude\` found on PATH or in ${agentBinSearched.join(", ")}; a real run needs --agent-bin` } : {}
|
|
18708
19250
|
});
|
|
18709
19251
|
};
|
|
18710
19252
|
try {
|
|
18711
19253
|
if (!workspace) {
|
|
18712
19254
|
const workspaces = await listWorkspaces(authOpts);
|
|
18713
|
-
|
|
19255
|
+
const configWorkspaces = [...new Set(cfg.routes.map((r) => splitRoute(r.route)[0]))];
|
|
19256
|
+
const defaulted = dryRun && givenKeySource === "config" && configWorkspaces.length === 1 ? workspaces.find((w) => w.slug === configWorkspaces[0]) : void 0;
|
|
19257
|
+
if (!signedInEmitted && (apiKey ?? "") !== "") {
|
|
19258
|
+
let me = {};
|
|
19259
|
+
try {
|
|
19260
|
+
me = await get(authOpts, "/auth/me");
|
|
19261
|
+
} catch {
|
|
19262
|
+
}
|
|
19263
|
+
if (me.id || me.email) {
|
|
19264
|
+
signedInEmitted = true;
|
|
19265
|
+
connectEvents.emit(
|
|
19266
|
+
"signed_in",
|
|
19267
|
+
`key belongs to ${me.email ?? me.id}${defaulted ? `; using ${defaulted.slug}, the workspace crew.yaml already names` : ""}`,
|
|
19268
|
+
{
|
|
19269
|
+
identity: { id: me.id, email: me.email, name: me.name },
|
|
19270
|
+
workspaces: workspaces.map((w) => ({ slug: w.slug, name: w.name, role: w.role })),
|
|
19271
|
+
session: givenKeySource,
|
|
19272
|
+
host: hostFromUrl4(baseUrl)
|
|
19273
|
+
}
|
|
19274
|
+
);
|
|
19275
|
+
}
|
|
19276
|
+
}
|
|
19277
|
+
if (defaulted) {
|
|
19278
|
+
await runConnect(defaulted.slug, project);
|
|
19279
|
+
} else if (workspaces.length === 0) {
|
|
18714
19280
|
process.stderr.write("crew connect: this key's identity has no workspace memberships.\n");
|
|
18715
19281
|
stopWithProblem("this key's identity has no workspace memberships", "workspace");
|
|
18716
19282
|
} else if (workspaces.length === 1) {
|
|
@@ -19186,11 +19752,20 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
19186
19752
|
if (!resetStore) return { stored: false, clear: async () => {
|
|
19187
19753
|
} };
|
|
19188
19754
|
const store = resetStore;
|
|
19189
|
-
const h =
|
|
19755
|
+
const h = hostFromUrl4(r.baseUrl);
|
|
19190
19756
|
const [slug] = splitRoute(r.route);
|
|
19191
19757
|
const session = await store.get(h, slug);
|
|
19192
19758
|
return { stored: Boolean(session?.apiKey), clear: () => store.clear(h, slug) };
|
|
19193
19759
|
},
|
|
19760
|
+
async hostSessions(stateDir) {
|
|
19761
|
+
const store = resetStore;
|
|
19762
|
+
return Promise.all(rememberedSessions(stateDir).map(async ({ host: h, workspace: w }) => {
|
|
19763
|
+
const session = store ? await store.get(h, w).catch(() => void 0) : void 0;
|
|
19764
|
+
return { host: h, workspace: w, stored: Boolean(session?.apiKey), clear: async () => {
|
|
19765
|
+
await store?.clear(h, w);
|
|
19766
|
+
} };
|
|
19767
|
+
}));
|
|
19768
|
+
},
|
|
19194
19769
|
inspectCheckout(dir) {
|
|
19195
19770
|
const dirty = status(dir).map((c) => c.path);
|
|
19196
19771
|
const linkedWorktrees = worktrees(dir).map((w) => w.path).filter((p) => realpathSync3(p) !== realpathSync3(dir));
|
|
@@ -19199,7 +19774,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
19199
19774
|
};
|
|
19200
19775
|
const droppedRoutes = [];
|
|
19201
19776
|
try {
|
|
19202
|
-
const rawCfg = parseYaml2(
|
|
19777
|
+
const rawCfg = parseYaml2(readFileSync28(cfg.configFile, "utf8"));
|
|
19203
19778
|
for (const r of rawCfg?.routes ?? []) {
|
|
19204
19779
|
if (typeof r?.route !== "string" || cfg.routes.some((x) => x.route === r.route)) continue;
|
|
19205
19780
|
try {
|
|
@@ -19241,8 +19816,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
19241
19816
|
process.stdout.write("crew reload: no daemon unit is running \u2014 nothing to reload (the next run reads crew.yaml fresh)\n");
|
|
19242
19817
|
break;
|
|
19243
19818
|
}
|
|
19244
|
-
|
|
19245
|
-
|
|
19819
|
+
mkdirSync21(cfg.ship.stateDir, { recursive: true });
|
|
19820
|
+
writeFileSync22(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
|
|
19246
19821
|
`);
|
|
19247
19822
|
process.stdout.write("crew reload: requested \u2014 the daemon finishes its in-flight pass, then relaunches with the new config\n");
|
|
19248
19823
|
if (flag("now") && control?.stopCommand && control.startCommand) {
|
|
@@ -19293,7 +19868,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
19293
19868
|
}
|
|
19294
19869
|
case "log": {
|
|
19295
19870
|
try {
|
|
19296
|
-
const text =
|
|
19871
|
+
const text = readFileSync28(cfg.ship.logFile, "utf8").split("\n");
|
|
19297
19872
|
process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
|
|
19298
19873
|
`);
|
|
19299
19874
|
} catch {
|
|
@@ -19316,8 +19891,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
19316
19891
|
};
|
|
19317
19892
|
if (!route.resolved) fail(`route ${route.route} is not connected \u2014 run \`crew connect\` first`);
|
|
19318
19893
|
const tracker = new Tracker(route, cfg.ship);
|
|
19319
|
-
const configText =
|
|
19320
|
-
const cfgBase =
|
|
19894
|
+
const configText = readFileSync28(cfg.configFile, "utf8");
|
|
19895
|
+
const cfgBase = dirname20(cfg.configFile);
|
|
19321
19896
|
const configured = configuredRepos(configText, route.route, cfgBase);
|
|
19322
19897
|
const dir = configured.find((r) => r.name === SAMPLE_REPO)?.dir ?? sampleDir(configured.map((r) => r.dir), route.reposBasePath, route.route.split("/")[0]);
|
|
19323
19898
|
const resolvedFile = resolvedPathFor(cfg.ship.stateDir, route.route);
|
|
@@ -19345,7 +19920,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
19345
19920
|
}
|
|
19346
19921
|
if (configured.some((r) => r.dir === dir)) {
|
|
19347
19922
|
try {
|
|
19348
|
-
|
|
19923
|
+
writeFileSync22(cfg.configFile, planRepoRemove(configText, cfgBase, route.route, dir).text);
|
|
19349
19924
|
} catch (e) {
|
|
19350
19925
|
if (e instanceof ReposError) fail(e.message);
|
|
19351
19926
|
throw e;
|
|
@@ -19353,29 +19928,29 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
19353
19928
|
}
|
|
19354
19929
|
if (row) {
|
|
19355
19930
|
await tracker.deleteRepoRow(row.id);
|
|
19356
|
-
if (
|
|
19357
|
-
const raw = JSON.parse(
|
|
19931
|
+
if (existsSync34(resolvedFile)) {
|
|
19932
|
+
const raw = JSON.parse(readFileSync28(resolvedFile, "utf8"));
|
|
19358
19933
|
if (raw.repoNames) delete raw.repoNames[row.id];
|
|
19359
19934
|
if (raw.repoRemotes) delete raw.repoRemotes[row.id];
|
|
19360
|
-
|
|
19935
|
+
writeFileSync22(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
19361
19936
|
`);
|
|
19362
19937
|
}
|
|
19363
19938
|
}
|
|
19364
|
-
if (
|
|
19939
|
+
if (existsSync34(join35(dir, ".git")) && basename11(dir) === SAMPLE_REPO) rmSync15(dir, { recursive: true, force: true });
|
|
19365
19940
|
say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
|
|
19366
19941
|
break;
|
|
19367
19942
|
}
|
|
19368
19943
|
const projectId = route.resolved.areaId ?? resolveRepoProject(await tracker.projectRows(), value("project"));
|
|
19369
|
-
const fresh = !
|
|
19370
|
-
if (
|
|
19944
|
+
const fresh = !existsSync34(join35(dir, ".git"));
|
|
19945
|
+
if (existsSync34(dir) && fresh && readdirSync14(dir).length > 0) fail(`${dir} exists and is not a git checkout \u2014 move it aside`);
|
|
19371
19946
|
if (dryRun) {
|
|
19372
19947
|
say("dry-run", { dir, newCheckout: fresh, repoRow: row?.id ?? null }, `would ${fresh ? "create" : "reuse"} ${dir}${row ? "" : ", create the Repos row"}, and file one ${tracker.contract.statuses.approved} ticket`);
|
|
19373
19948
|
break;
|
|
19374
19949
|
}
|
|
19375
19950
|
if (fresh) {
|
|
19376
|
-
|
|
19377
|
-
|
|
19378
|
-
|
|
19951
|
+
mkdirSync21(dir, { recursive: true });
|
|
19952
|
+
writeFileSync22(join35(dir, "README.md"), SAMPLE_README);
|
|
19953
|
+
writeFileSync22(join35(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
|
|
19379
19954
|
const g = (...a) => {
|
|
19380
19955
|
const r = spawnSync6("git", a, { cwd: dir, encoding: "utf8" });
|
|
19381
19956
|
if (r.status !== 0) fail(`git ${a[0]} failed in ${dir}: ${(r.stderr || r.stdout).trim()}`);
|
|
@@ -19393,17 +19968,17 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
19393
19968
|
spec: { platform: "unix", releaseMode: "integrate", ciProvider: "none" }
|
|
19394
19969
|
});
|
|
19395
19970
|
repoId = (await tracker.createRepoRow(payload)).id;
|
|
19396
|
-
if (
|
|
19397
|
-
const raw = JSON.parse(
|
|
19971
|
+
if (existsSync34(resolvedFile)) {
|
|
19972
|
+
const raw = JSON.parse(readFileSync28(resolvedFile, "utf8"));
|
|
19398
19973
|
raw.repoNames = { ...raw.repoNames, [repoId]: SAMPLE_REPO };
|
|
19399
19974
|
raw.repoRemotes = { ...raw.repoRemotes, [repoId]: SAMPLE_REMOTE };
|
|
19400
|
-
|
|
19975
|
+
writeFileSync22(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
19401
19976
|
`);
|
|
19402
19977
|
}
|
|
19403
19978
|
}
|
|
19404
19979
|
if (!configured.some((r) => r.dir === dir)) {
|
|
19405
19980
|
try {
|
|
19406
|
-
|
|
19981
|
+
writeFileSync22(cfg.configFile, planRepoAdd({ text: configText, base: cfgBase, route: route.route, path: dir, name: SAMPLE_REPO }).text);
|
|
19407
19982
|
} catch (e) {
|
|
19408
19983
|
if (e instanceof ReposError) fail(e.message);
|
|
19409
19984
|
throw e;
|
|
@@ -19524,6 +20099,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
19524
20099
|
const projects = projectModeLabels(await statusTracker.projectRowsWithModes().catch(() => []));
|
|
19525
20100
|
const crewMinVersion = await readCrewFloor(route, cfg.ship) ?? null;
|
|
19526
20101
|
if (flag("json")) {
|
|
20102
|
+
const nameSync = await reconcileRouteShipName({ route, ship: cfg.ship, configFile: cfg.configFile, readOnly: true });
|
|
19527
20103
|
const repos = await resolvedRepos(route);
|
|
19528
20104
|
const seatIds = route.resolved?.seats ?? {};
|
|
19529
20105
|
const crewRows = Object.keys(seatIds).length ? await statusTracker.crewRows() : [];
|
|
@@ -19567,7 +20143,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
19567
20143
|
eventFile: eventFileFor(cfg.ship.stateDir),
|
|
19568
20144
|
// Additive (CREW-1373): open ship-level items, including the
|
|
19569
20145
|
// derived `release_stale`. `version` stays 1.
|
|
19570
|
-
attention: openShipAttention(state)
|
|
20146
|
+
attention: openShipAttention(state),
|
|
20147
|
+
// Additive (CREW-2495): this ship's Ships-row name and whether it has drifted from `name`.
|
|
20148
|
+
rowName: nameSync.rowName ?? null,
|
|
20149
|
+
nameDrift: nameSync.drift
|
|
19571
20150
|
},
|
|
19572
20151
|
// Additive (CREW-1402): what the operator owes. `version` stays 1.
|
|
19573
20152
|
todo: todo.map((i) => ({ ticket: displayKey(i.ticket), title: i.ticket.title ?? null, reasons: i.reasons })),
|
|
@@ -19714,7 +20293,7 @@ watermark: ${state.watermark()}
|
|
|
19714
20293
|
}
|
|
19715
20294
|
for (const r of repos) {
|
|
19716
20295
|
const remote = r.config.branch.remote;
|
|
19717
|
-
if (
|
|
20296
|
+
if (existsSync34(r.dir)) {
|
|
19718
20297
|
pushAuth.push({
|
|
19719
20298
|
route: c.route,
|
|
19720
20299
|
repo: r.name,
|
|
@@ -19742,7 +20321,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
19742
20321
|
);
|
|
19743
20322
|
try {
|
|
19744
20323
|
const evFile = eventFileFor(cfg.ship.stateDir);
|
|
19745
|
-
const evSize =
|
|
20324
|
+
const evSize = existsSync34(evFile) ? statSync10(evFile).size : 0;
|
|
19746
20325
|
process.stdout.write(
|
|
19747
20326
|
evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
|
|
19748
20327
|
` : "events log: ok\n"
|
|
@@ -19753,7 +20332,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
19753
20332
|
const unitPlan = planInstall(cfg.ship, CREW_HOME, host, detectSystemd(), "run");
|
|
19754
20333
|
const abandons = unitAbandonsAgents(unitPlan, (p) => {
|
|
19755
20334
|
try {
|
|
19756
|
-
return
|
|
20335
|
+
return readFileSync28(p, "utf8");
|
|
19757
20336
|
} catch {
|
|
19758
20337
|
return null;
|
|
19759
20338
|
}
|
|
@@ -19825,6 +20404,11 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
19825
20404
|
if (warn) process.stdout.write(` WARNING ${warn}
|
|
19826
20405
|
`);
|
|
19827
20406
|
}
|
|
20407
|
+
const scope = releaseLockScope(route.route, r.name);
|
|
20408
|
+
const lock2 = await tracker.lockState(scope).catch(() => void 0);
|
|
20409
|
+
if (lock2?.holder && staleOwnLockClaim(lock2.holder, cfg.ship.name)) {
|
|
20410
|
+
process.stdout.write(` ${formatStaleOwnLock(scope, lock2.holder)}`);
|
|
20411
|
+
}
|
|
19828
20412
|
}
|
|
19829
20413
|
const missingAuthorship = await tracker.missingAuthorshipColumns();
|
|
19830
20414
|
process.stdout.write(
|
|
@@ -20145,7 +20729,7 @@ ${foreign.join("\n")}
|
|
|
20145
20729
|
break;
|
|
20146
20730
|
}
|
|
20147
20731
|
default:
|
|
20148
|
-
usage();
|
|
20732
|
+
usage(`unknown command "${command}"`);
|
|
20149
20733
|
}
|
|
20150
20734
|
lock?.release();
|
|
20151
20735
|
}
|