@tablation/crew 0.71.1 → 0.71.13
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 +480 -131
- 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];
|
|
@@ -6068,8 +6111,8 @@ var Tracker = class {
|
|
|
6068
6111
|
* Locks table with no row for `scope` is `{ ok: false, reason: 'missing' }`
|
|
6069
6112
|
* (CREW-1384): `crew connect` provisions the row.
|
|
6070
6113
|
*/
|
|
6071
|
-
acquireBoardLock(scope, holderLabel, ttlMs) {
|
|
6072
|
-
return acquireBoardLock(this.client.records, this.route.resolved?.locksModelId, scope, holderLabel, ttlMs);
|
|
6114
|
+
acquireBoardLock(scope, holderLabel, ttlMs, log) {
|
|
6115
|
+
return acquireBoardLock(this.client.records, this.route.resolved?.locksModelId, scope, holderLabel, ttlMs, log);
|
|
6073
6116
|
}
|
|
6074
6117
|
/** The Locks row for `scope` — present? held by whom? Undefined when this workspace has no Locks table. */
|
|
6075
6118
|
async lockState(scope) {
|
|
@@ -8563,9 +8606,9 @@ function doctorSelftestLines(stored, toolchains) {
|
|
|
8563
8606
|
}
|
|
8564
8607
|
|
|
8565
8608
|
// src/cli.ts
|
|
8566
|
-
import { resolve as resolve5, dirname as dirname19, join as
|
|
8609
|
+
import { resolve as resolve5, dirname as dirname19, join as join35, basename as basename11 } from "path";
|
|
8567
8610
|
import { fileURLToPath } from "url";
|
|
8568
|
-
import { hostname as hostname2, homedir as
|
|
8611
|
+
import { hostname as hostname2, homedir as homedir9 } from "os";
|
|
8569
8612
|
import { spawnSync as spawnSync6 } from "child_process";
|
|
8570
8613
|
import { parse as parseYaml2 } from "yaml";
|
|
8571
8614
|
|
|
@@ -8579,6 +8622,7 @@ var VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
|
8579
8622
|
"extra-path",
|
|
8580
8623
|
"harness",
|
|
8581
8624
|
"hook",
|
|
8625
|
+
"host",
|
|
8582
8626
|
"key",
|
|
8583
8627
|
"level",
|
|
8584
8628
|
"limit",
|
|
@@ -8635,6 +8679,15 @@ function splitConnectArg(arg) {
|
|
|
8635
8679
|
const i = arg.indexOf("/");
|
|
8636
8680
|
return [arg.slice(0, i), arg.slice(i + 1)];
|
|
8637
8681
|
}
|
|
8682
|
+
function versionRequest(argv2) {
|
|
8683
|
+
if (argv2[0] !== "--version" && argv2[0] !== "version") return null;
|
|
8684
|
+
const rest = argv2.slice(1);
|
|
8685
|
+
if (rest.some((a) => a !== "--json")) return null;
|
|
8686
|
+
return { json: rest.length > 0 };
|
|
8687
|
+
}
|
|
8688
|
+
function helpRequest(argv2) {
|
|
8689
|
+
return argv2[0] === "--help" || argv2[0] === "-h" || argv2[0] === "help";
|
|
8690
|
+
}
|
|
8638
8691
|
|
|
8639
8692
|
// src/roster.ts
|
|
8640
8693
|
function memberByIdentity(roster, identityId) {
|
|
@@ -8730,7 +8783,7 @@ function rosterMarkdown(roster, meId) {
|
|
|
8730
8783
|
}
|
|
8731
8784
|
|
|
8732
8785
|
// src/connect-resume.ts
|
|
8733
|
-
import { mkdirSync as mkdirSync10, readFileSync as readFileSync16, writeFileSync as writeFileSync11 } from "fs";
|
|
8786
|
+
import { mkdirSync as mkdirSync10, readFileSync as readFileSync16, rmSync as rmSync10, writeFileSync as writeFileSync11 } from "fs";
|
|
8734
8787
|
import { dirname as dirname8, join as join19 } from "path";
|
|
8735
8788
|
function lastSessionFile(stateDir) {
|
|
8736
8789
|
return join19(stateDir, "connect-session.json");
|
|
@@ -8765,10 +8818,45 @@ async function resumeKeychainSession(o) {
|
|
|
8765
8818
|
return void 0;
|
|
8766
8819
|
}
|
|
8767
8820
|
}
|
|
8821
|
+
function rememberedSessions(stateDir) {
|
|
8822
|
+
return Object.entries(readPointers(stateDir)).map(([host, workspace]) => ({ host, workspace }));
|
|
8823
|
+
}
|
|
8824
|
+
function forgetSession(stateDir, host) {
|
|
8825
|
+
try {
|
|
8826
|
+
const { [host]: _gone, ...rest } = readPointers(stateDir);
|
|
8827
|
+
if (Object.keys(rest).length) writeFileSync11(lastSessionFile(stateDir), `${JSON.stringify(rest, null, 2)}
|
|
8828
|
+
`, { mode: 384 });
|
|
8829
|
+
else rmSync10(lastSessionFile(stateDir), { force: true });
|
|
8830
|
+
} catch {
|
|
8831
|
+
}
|
|
8832
|
+
}
|
|
8833
|
+
async function signOutSessions(o) {
|
|
8834
|
+
const targets = rememberedSessions(o.stateDir).filter((t) => !o.host || t.host === o.host);
|
|
8835
|
+
if (o.host && o.workspace && !targets.some((t) => t.host === o.host && t.workspace === o.workspace)) {
|
|
8836
|
+
targets.push({ host: o.host, workspace: o.workspace });
|
|
8837
|
+
}
|
|
8838
|
+
const out = [];
|
|
8839
|
+
for (const t of targets) {
|
|
8840
|
+
try {
|
|
8841
|
+
const stored = await o.store.get(t.host, t.workspace);
|
|
8842
|
+
if (stored?.apiKey) {
|
|
8843
|
+
if (!o.dryRun) await o.store.clear(t.host, t.workspace);
|
|
8844
|
+
out.push({ ...t, action: o.dryRun ? "would-remove" : "removed" });
|
|
8845
|
+
} else {
|
|
8846
|
+
out.push({ ...t, action: "absent", detail: "no keychain entry" });
|
|
8847
|
+
}
|
|
8848
|
+
if (!o.dryRun) forgetSession(o.stateDir, t.host);
|
|
8849
|
+
} catch (e) {
|
|
8850
|
+
out.push({ ...t, action: "failed", detail: e.message });
|
|
8851
|
+
}
|
|
8852
|
+
}
|
|
8853
|
+
return out;
|
|
8854
|
+
}
|
|
8768
8855
|
|
|
8769
8856
|
// src/reset.ts
|
|
8770
|
-
import { existsSync as existsSync20, mkdirSync as mkdirSync11, readFileSync as readFileSync17, readdirSync as readdirSync7, rmSync as
|
|
8857
|
+
import { existsSync as existsSync20, mkdirSync as mkdirSync11, readFileSync as readFileSync17, readdirSync as readdirSync7, rmSync as rmSync11, statSync as statSync6, writeFileSync as writeFileSync12 } from "fs";
|
|
8771
8858
|
import { dirname as dirname9, join as join20 } from "path";
|
|
8859
|
+
import { hostFromUrl as hostFromUrl2 } from "@tablation/client";
|
|
8772
8860
|
function clonedCheckoutsFile(stateDir) {
|
|
8773
8861
|
return join20(stateDir, "cloned-checkouts.json");
|
|
8774
8862
|
}
|
|
@@ -8814,12 +8902,31 @@ async function runReset(o, fx) {
|
|
|
8814
8902
|
steps.push({ step: "credential", target: route.route, action: "failed", detail: e.message });
|
|
8815
8903
|
}
|
|
8816
8904
|
}
|
|
8905
|
+
const covered = new Set([...o.routes, ...o.droppedRoutes ?? []].map((r) => `${hostFromUrl2(r.baseUrl)}/${splitRoute(r.route)[0]}`));
|
|
8906
|
+
try {
|
|
8907
|
+
for (const s of await fx.hostSessions(o.stateDir)) {
|
|
8908
|
+
const target = `${s.host}/${s.workspace}`;
|
|
8909
|
+
if (covered.has(target)) continue;
|
|
8910
|
+
try {
|
|
8911
|
+
if (!s.stored) {
|
|
8912
|
+
steps.push({ step: "credential", target, action: "absent", detail: "no host session in the keychain" });
|
|
8913
|
+
continue;
|
|
8914
|
+
}
|
|
8915
|
+
if (!o.dryRun) await s.clear();
|
|
8916
|
+
steps.push({ step: "credential", target, action: mark(o.dryRun), detail: "host session stored by a dry-run connect" });
|
|
8917
|
+
} catch (e) {
|
|
8918
|
+
steps.push({ step: "credential", target, action: "failed", detail: e.message });
|
|
8919
|
+
}
|
|
8920
|
+
}
|
|
8921
|
+
} catch (e) {
|
|
8922
|
+
steps.push({ step: "credential", target: "host sessions", action: "failed", detail: e.message });
|
|
8923
|
+
}
|
|
8817
8924
|
if (o.removeCheckouts) {
|
|
8818
8925
|
for (const dir of readClonedCheckouts(o.stateDir)) steps.push(removeCheckout(dir, o.dryRun, fx));
|
|
8819
8926
|
}
|
|
8820
8927
|
steps.push(...removeState(o.stateDir, o.dryRun));
|
|
8821
8928
|
if (existsSync20(o.configFile)) {
|
|
8822
|
-
if (!o.dryRun)
|
|
8929
|
+
if (!o.dryRun) rmSync11(o.configFile, { force: true });
|
|
8823
8930
|
steps.push({ step: "config", target: o.configFile, action: mark(o.dryRun) });
|
|
8824
8931
|
} else {
|
|
8825
8932
|
steps.push({ step: "config", target: o.configFile, action: "absent" });
|
|
@@ -8835,7 +8942,7 @@ function removeCheckout(dir, dryRun, fx) {
|
|
|
8835
8942
|
if (linkedWorktrees.length) {
|
|
8836
8943
|
return { step: "checkout", target: dir, action: "kept", detail: `has linked worktrees (${linkedWorktrees.join(", ")}); remove them first` };
|
|
8837
8944
|
}
|
|
8838
|
-
if (!dryRun)
|
|
8945
|
+
if (!dryRun) rmSync11(dir, { recursive: true, force: true });
|
|
8839
8946
|
return { step: "checkout", target: dir, action: mark(dryRun) };
|
|
8840
8947
|
}
|
|
8841
8948
|
function removeState(stateDir, dryRun) {
|
|
@@ -8849,7 +8956,7 @@ function removeState(stateDir, dryRun) {
|
|
|
8849
8956
|
}
|
|
8850
8957
|
for (const t of targets) {
|
|
8851
8958
|
if (!existsSync20(t)) continue;
|
|
8852
|
-
if (!dryRun)
|
|
8959
|
+
if (!dryRun) rmSync11(t, { recursive: true, force: true });
|
|
8853
8960
|
out.push({ step: "state", target: t, action: mark(dryRun) });
|
|
8854
8961
|
}
|
|
8855
8962
|
if (!out.length) out.push({ step: "state", target: stateDir, action: "absent", detail: "nothing to remove" });
|
|
@@ -10513,7 +10620,9 @@ Wrote ${o.configPath}.
|
|
|
10513
10620
|
|
|
10514
10621
|
// src/connect-json.ts
|
|
10515
10622
|
import { accessSync as accessSync3, constants as constants3 } from "fs";
|
|
10516
|
-
import {
|
|
10623
|
+
import { homedir as homedir7 } from "os";
|
|
10624
|
+
import { dirname as dirname11, join as join23 } from "path";
|
|
10625
|
+
import { delimiter as delimiter2 } from "path";
|
|
10517
10626
|
var CHOICE_EXIT_CODE = 3;
|
|
10518
10627
|
function createEmitter(json, write) {
|
|
10519
10628
|
return {
|
|
@@ -10547,6 +10656,23 @@ function usesFlagAnswers(f, isTTY) {
|
|
|
10547
10656
|
if (isTTY) return false;
|
|
10548
10657
|
return !!(f.shipName || f.agentBin || f.extraPath || f.hostPassengers);
|
|
10549
10658
|
}
|
|
10659
|
+
function agentBinSearchDirs(home, pathEnv, extraPath) {
|
|
10660
|
+
const dirs = [
|
|
10661
|
+
...pathEnv.split(delimiter2),
|
|
10662
|
+
...(extraPath ?? "").split(delimiter2),
|
|
10663
|
+
join23(home, ".local/bin"),
|
|
10664
|
+
join23(home, ".claude/local"),
|
|
10665
|
+
"/opt/homebrew/bin",
|
|
10666
|
+
"/usr/local/bin",
|
|
10667
|
+
join23(home, ".npm-global/bin"),
|
|
10668
|
+
join23(home, ".volta/bin")
|
|
10669
|
+
];
|
|
10670
|
+
return [...new Set(dirs.filter(Boolean))];
|
|
10671
|
+
}
|
|
10672
|
+
function findAgentBin(name, env, extraPath, isExec) {
|
|
10673
|
+
const searched = agentBinSearchDirs(env.home ?? homedir7(), env.pathEnv, extraPath);
|
|
10674
|
+
return { bin: findOnPath2(name, searched.join(delimiter2), isExec), searched };
|
|
10675
|
+
}
|
|
10550
10676
|
function flagWizardAnswers(f, env) {
|
|
10551
10677
|
const isExec = env.isExecutable ?? ((p) => {
|
|
10552
10678
|
try {
|
|
@@ -10556,9 +10682,9 @@ function flagWizardAnswers(f, env) {
|
|
|
10556
10682
|
return false;
|
|
10557
10683
|
}
|
|
10558
10684
|
});
|
|
10559
|
-
const agentBin = f.agentBin ?? findOnPath2("claude", env.pathEnv, isExec);
|
|
10560
|
-
if (!agentBin) return { error: 'no agent binary: "claude" is not on PATH \u2014 pass --agent-bin /absolute/path' };
|
|
10561
10685
|
if (f.agentBin && !isExec(f.agentBin)) return { error: `--agent-bin ${f.agentBin} is not an executable file` };
|
|
10686
|
+
const found = f.agentBin ? void 0 : findAgentBin("claude", env, f.extraPath, isExec);
|
|
10687
|
+
const agentBin = f.agentBin ?? found?.bin;
|
|
10562
10688
|
const pnpmPath = findOnPath2("pnpm", env.pathEnv, isExec);
|
|
10563
10689
|
const dockerPath = f.hostPassengers ? findOnPath2("docker", env.pathEnv, isExec) : void 0;
|
|
10564
10690
|
const extraPath = f.extraPath ?? deriveExtraPath([dirname11(env.nodePath), pnpmPath ? dirname11(pnpmPath) : void 0, dockerPath ? dirname11(dockerPath) : void 0]);
|
|
@@ -10566,16 +10692,17 @@ function flagWizardAnswers(f, env) {
|
|
|
10566
10692
|
answers: {
|
|
10567
10693
|
name: f.shipName?.trim() || env.hostname,
|
|
10568
10694
|
platform: env.platform,
|
|
10569
|
-
agentBin,
|
|
10695
|
+
agentBin: agentBin ?? "claude",
|
|
10570
10696
|
extraPath,
|
|
10571
10697
|
hostPassengers: f.hostPassengers
|
|
10572
|
-
}
|
|
10698
|
+
},
|
|
10699
|
+
...agentBin ? {} : { agentBinSearched: found?.searched ?? [] }
|
|
10573
10700
|
};
|
|
10574
10701
|
}
|
|
10575
10702
|
|
|
10576
10703
|
// src/release-run.ts
|
|
10577
|
-
import { readFileSync as readFileSync18, writeFileSync as writeFileSync15, existsSync as existsSync22, copyFileSync, mkdirSync as mkdirSync13, mkdtempSync as mkdtempSync2, rmSync as
|
|
10578
|
-
import { dirname as dirname12, join as
|
|
10704
|
+
import { readFileSync as readFileSync18, writeFileSync as writeFileSync15, existsSync as existsSync22, copyFileSync, mkdirSync as mkdirSync13, mkdtempSync as mkdtempSync2, rmSync as rmSync12 } from "fs";
|
|
10705
|
+
import { dirname as dirname12, join as join24 } from "path";
|
|
10579
10706
|
import { tmpdir as tmpdir3 } from "os";
|
|
10580
10707
|
|
|
10581
10708
|
// src/release.ts
|
|
@@ -11293,7 +11420,7 @@ async function bump(o, size) {
|
|
|
11293
11420
|
}
|
|
11294
11421
|
return printed;
|
|
11295
11422
|
}
|
|
11296
|
-
const files = o.repo.release.versionFiles.map((f) =>
|
|
11423
|
+
const files = o.repo.release.versionFiles.map((f) => join24(o.cwd, f));
|
|
11297
11424
|
const first = files[0];
|
|
11298
11425
|
if (!existsSync22(first)) {
|
|
11299
11426
|
o.emit.error(`no ${o.repo.release.versionFiles[0]} to read a version from`);
|
|
@@ -11325,7 +11452,7 @@ async function bump(o, size) {
|
|
|
11325
11452
|
function writeChangelog(o, version, entries) {
|
|
11326
11453
|
const file = o.repo.release.changelog;
|
|
11327
11454
|
if (!file || entries.length === 0) return;
|
|
11328
|
-
const path =
|
|
11455
|
+
const path = join24(o.cwd, file);
|
|
11329
11456
|
const section = renderChangelogSection(version, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), entries);
|
|
11330
11457
|
if (o.dryRun) {
|
|
11331
11458
|
o.emit.emit(`would prepend ${entries.length} entr(ies) to ${file}`);
|
|
@@ -11415,7 +11542,7 @@ function closeReleaseCheckout(cwd, root, dir, branch, run2 = gitOk) {
|
|
|
11415
11542
|
run2(cwd, ["worktree", "remove", "--force", dir]);
|
|
11416
11543
|
} finally {
|
|
11417
11544
|
try {
|
|
11418
|
-
|
|
11545
|
+
rmSync12(root, { recursive: true, force: true });
|
|
11419
11546
|
} finally {
|
|
11420
11547
|
run2(cwd, ["worktree", "prune"]);
|
|
11421
11548
|
run2(cwd, ["branch", "-D", branch]);
|
|
@@ -11425,8 +11552,8 @@ function closeReleaseCheckout(cwd, root, dir, branch, run2 = gitOk) {
|
|
|
11425
11552
|
function openReleaseCheckout(o) {
|
|
11426
11553
|
const { remote, base } = o.repo.branch;
|
|
11427
11554
|
const branch = `crew/release-${Date.now()}`;
|
|
11428
|
-
const root = mkdtempSync2(
|
|
11429
|
-
const dir =
|
|
11555
|
+
const root = mkdtempSync2(join24(tmpdir3(), "crew-release-"));
|
|
11556
|
+
const dir = join24(root, "checkout");
|
|
11430
11557
|
o.emit.emit(`release checkout: ${dir}`, { step: "release" });
|
|
11431
11558
|
const close = () => closeReleaseCheckout(o.cwd, root, dir, branch);
|
|
11432
11559
|
try {
|
|
@@ -11436,10 +11563,10 @@ function openReleaseCheckout(o) {
|
|
|
11436
11563
|
throw e;
|
|
11437
11564
|
}
|
|
11438
11565
|
for (const f of o.repo.worktrees.copy) {
|
|
11439
|
-
const src =
|
|
11566
|
+
const src = join24(o.cwd, f);
|
|
11440
11567
|
if (!existsSync22(src)) continue;
|
|
11441
|
-
mkdirSync13(dirname12(
|
|
11442
|
-
copyFileSync(src,
|
|
11568
|
+
mkdirSync13(dirname12(join24(dir, f)), { recursive: true });
|
|
11569
|
+
copyFileSync(src, join24(dir, f));
|
|
11443
11570
|
}
|
|
11444
11571
|
return { dir, branch, close };
|
|
11445
11572
|
}
|
|
@@ -12586,7 +12713,22 @@ async function provisionReleaseLocks(o, locksModelId, route, repoNames) {
|
|
|
12586
12713
|
};
|
|
12587
12714
|
return ensureLockRows(client2, locksModelId, scopes);
|
|
12588
12715
|
}
|
|
12589
|
-
var
|
|
12716
|
+
var ISSUES_TEMPLATE_IDENTIFIERS = ["tablation.issues", "crew.issues"];
|
|
12717
|
+
async function resolveIssuesTemplateKeys(o) {
|
|
12718
|
+
const keys = new Set(ISSUES_TEMPLATE_IDENTIFIERS);
|
|
12719
|
+
await Promise.all(ISSUES_TEMPLATE_IDENTIFIERS.map(async (identifier) => {
|
|
12720
|
+
try {
|
|
12721
|
+
const t = await get(
|
|
12722
|
+
o,
|
|
12723
|
+
`/library-templates/by-identifier/${encodeURIComponent(identifier)}`
|
|
12724
|
+
);
|
|
12725
|
+
if (t.id) keys.add(t.id);
|
|
12726
|
+
if (t.identifier) keys.add(t.identifier);
|
|
12727
|
+
} catch {
|
|
12728
|
+
}
|
|
12729
|
+
}));
|
|
12730
|
+
return keys;
|
|
12731
|
+
}
|
|
12590
12732
|
async function listLibraryTemplates(o) {
|
|
12591
12733
|
return get(o, "/library-templates");
|
|
12592
12734
|
}
|
|
@@ -12627,6 +12769,7 @@ async function discover(o) {
|
|
|
12627
12769
|
models: {},
|
|
12628
12770
|
seats: {},
|
|
12629
12771
|
holds: [],
|
|
12772
|
+
candidates: [],
|
|
12630
12773
|
problems,
|
|
12631
12774
|
provisioning
|
|
12632
12775
|
};
|
|
@@ -12635,6 +12778,7 @@ async function discover(o) {
|
|
|
12635
12778
|
out.role = me.role;
|
|
12636
12779
|
out.meEmail = me.email;
|
|
12637
12780
|
out.meId = me.id;
|
|
12781
|
+
out.meIdentityId = me.identityId;
|
|
12638
12782
|
out.meName = me.name;
|
|
12639
12783
|
} catch {
|
|
12640
12784
|
}
|
|
@@ -12658,7 +12802,8 @@ async function discover(o) {
|
|
|
12658
12802
|
}
|
|
12659
12803
|
} else {
|
|
12660
12804
|
const projects = await get(o, `/projects?workspaceId=${ws.id}`);
|
|
12661
|
-
const
|
|
12805
|
+
const issuesKeys = await resolveIssuesTemplateKeys(o);
|
|
12806
|
+
const defaultCandidate = projects.find((p) => !!p.sourceTemplateId && issuesKeys.has(p.sourceTemplateId)) ?? projects.find((p) => eq(p.slug ?? "", "issues"));
|
|
12662
12807
|
if (defaultCandidate && (await missingTables(o, defaultCandidate.id)).length === 0) {
|
|
12663
12808
|
out.projectId = defaultCandidate.id;
|
|
12664
12809
|
out.projectSlug = defaultCandidate.slug;
|
|
@@ -12850,13 +12995,32 @@ async function discover(o) {
|
|
|
12850
12995
|
o,
|
|
12851
12996
|
`/data-models/${out.models.crew}/records?limit=200`
|
|
12852
12997
|
);
|
|
12853
|
-
|
|
12854
|
-
|
|
12998
|
+
let seatShipId = shipRowId;
|
|
12999
|
+
if (!seatShipId && o.dryRun && o.ship && out.shipsModelId) {
|
|
13000
|
+
try {
|
|
13001
|
+
const shipRows = await get(
|
|
13002
|
+
o,
|
|
13003
|
+
`/data-models/${out.shipsModelId}/records?limit=200`
|
|
13004
|
+
);
|
|
13005
|
+
const byId = o.ship.machineId ? shipRows.filter((r) => r.machine_id === o.ship.machineId) : [];
|
|
13006
|
+
const mineRows = byId.length > 0 ? byId : shipRows.filter((r) => (r.name ?? "").trim() === o.ship.name.trim() && !r.machine_id);
|
|
13007
|
+
if (mineRows.length === 1) seatShipId = mineRows[0].id;
|
|
13008
|
+
} catch {
|
|
13009
|
+
}
|
|
13010
|
+
}
|
|
13011
|
+
const scoped = seatShipId ? rows.filter((r) => r.ship_id === seatShipId) : rows.filter((r) => !r.ship_id);
|
|
13012
|
+
const unclaimed = seatShipId ? rows.filter((r) => !r.ship_id) : [];
|
|
12855
13013
|
for (const role of ["dev", "design", "qa", "triage"]) {
|
|
12856
13014
|
const searchTerm = role === "dev" ? "develop" : role;
|
|
12857
13015
|
let id = seatFor(scoped, searchTerm);
|
|
12858
13016
|
let seatAction = "matched";
|
|
12859
|
-
if (!id && shipRowId) {
|
|
13017
|
+
if (!id && seatShipId && !shipRowId) {
|
|
13018
|
+
const legacyId = seatFor(unclaimed, searchTerm);
|
|
13019
|
+
if (legacyId) {
|
|
13020
|
+
id = legacyId;
|
|
13021
|
+
seatAction = "claimed";
|
|
13022
|
+
}
|
|
13023
|
+
} else if (!id && shipRowId) {
|
|
12860
13024
|
const legacyId = seatFor(unclaimed, searchTerm);
|
|
12861
13025
|
if (legacyId) {
|
|
12862
13026
|
await patch(o, `/data-models/${out.models.crew}/records/${legacyId}`, { ship_id: shipRowId });
|
|
@@ -12876,10 +13040,13 @@ async function discover(o) {
|
|
|
12876
13040
|
}
|
|
12877
13041
|
if (id) {
|
|
12878
13042
|
out.seats[role] = id;
|
|
12879
|
-
|
|
13043
|
+
const rowShip = rows.find((r) => r.id === id)?.ship_id ?? shipRowId ?? void 0;
|
|
13044
|
+
(out.seatRows ??= []).push({ role, id, action: seatAction, ...rowShip ? { shipId: rowShip } : {} });
|
|
13045
|
+
} else if (o.dryRun && o.ship && out.shipsModelId) {
|
|
13046
|
+
(out.seatRows ??= []).push({ role, id: "", action: "would-create" });
|
|
12880
13047
|
} else problems.push(`no Crew row looking like the ${role} seat \u2014 create one, or omit that role`);
|
|
12881
13048
|
}
|
|
12882
|
-
const pairId = seatFor(scoped, "pair") || (
|
|
13049
|
+
const pairId = seatFor(scoped, "pair") || (seatShipId ? seatFor(unclaimed, "pair") : void 0);
|
|
12883
13050
|
if (pairId) out.seats.pair = pairId;
|
|
12884
13051
|
if (shipRowId) {
|
|
12885
13052
|
const releaseId = seatFor(scoped, "release");
|
|
@@ -12897,25 +13064,56 @@ async function discover(o) {
|
|
|
12897
13064
|
const holdRows = rows.filter((r) => !seatIds.has(r.id));
|
|
12898
13065
|
out.holds = holdRows.map((r) => ({ id: r.id, name: r.name ?? "", email: r.email, user_id: r.user_id }));
|
|
12899
13066
|
out.crewModelId = out.models.crew;
|
|
13067
|
+
const people = holdRows.filter((r) => !r.agent_id && (!r.ship_id || !!r.user_id));
|
|
13068
|
+
const members = /* @__PURE__ */ new Map();
|
|
13069
|
+
try {
|
|
13070
|
+
const list = await get(
|
|
13071
|
+
o,
|
|
13072
|
+
`/workspaces/${out.workspaceId}/members`
|
|
13073
|
+
);
|
|
13074
|
+
for (const m of Array.isArray(list) ? list : []) {
|
|
13075
|
+
const key = m.id ?? m.userId;
|
|
13076
|
+
if (key) members.set(key, { identityId: m.identityId, email: m.email });
|
|
13077
|
+
}
|
|
13078
|
+
} catch {
|
|
13079
|
+
}
|
|
13080
|
+
for (const h of out.holds) {
|
|
13081
|
+
const e = h.user_id ? members.get(h.user_id)?.email : void 0;
|
|
13082
|
+
if (e) h.memberEmail = e;
|
|
13083
|
+
}
|
|
13084
|
+
out.candidates = people.map((r) => ({
|
|
13085
|
+
id: r.id,
|
|
13086
|
+
name: r.name ?? "",
|
|
13087
|
+
email: r.email,
|
|
13088
|
+
user_id: r.user_id,
|
|
13089
|
+
detail: r.email || (r.user_id ? members.get(r.user_id)?.email : void 0) || void 0
|
|
13090
|
+
}));
|
|
13091
|
+
const isMine = (r) => !!r.user_id && (r.user_id === out.meId || !!out.meIdentityId && members.get(r.user_id)?.identityId === out.meIdentityId);
|
|
13092
|
+
const emailOf = (r) => [r.email, r.user_id ? members.get(r.user_id)?.email : void 0].filter((e) => !!e);
|
|
12900
13093
|
if (!out.meId) {
|
|
12901
|
-
if (out.
|
|
13094
|
+
if (out.candidates.length === 0) problems.push("no non-seat Crew rows \u2014 which row is the operator?");
|
|
12902
13095
|
else {
|
|
12903
|
-
const byEmail = out.meEmail ?
|
|
13096
|
+
const byEmail = out.meEmail ? people.filter((r) => emailOf(r).some((e) => eq(e, out.meEmail))) : [];
|
|
12904
13097
|
if (byEmail.length === 1) out.operator = byEmail[0].id;
|
|
12905
|
-
else if (out.
|
|
13098
|
+
else if (out.candidates.length === 1) out.operator = out.candidates[0].id;
|
|
12906
13099
|
}
|
|
12907
13100
|
} else {
|
|
12908
|
-
const candidates =
|
|
12909
|
-
const mine =
|
|
12910
|
-
const byEmail = out.meEmail ? candidates.filter((r) =>
|
|
12911
|
-
const
|
|
13101
|
+
const candidates = people.filter((r) => !r.user_id || isMine(r));
|
|
13102
|
+
const mine = people.find(isMine);
|
|
13103
|
+
const byEmail = out.meEmail ? candidates.filter((r) => emailOf(r).some((e) => eq(e, out.meEmail))) : [];
|
|
13104
|
+
const unlinked = people.filter((r) => !r.user_id);
|
|
13105
|
+
const only = candidates.length === 1 && unlinked.length === 1 ? candidates[0] : void 0;
|
|
12912
13106
|
const pick = mine ?? (byEmail.length === 1 ? byEmail[0] : only);
|
|
12913
13107
|
if (pick) {
|
|
12914
13108
|
out.operator = pick.id;
|
|
12915
|
-
out.operatorAction = pick
|
|
12916
|
-
if (pick
|
|
12917
|
-
} else
|
|
12918
|
-
|
|
13109
|
+
out.operatorAction = isMine(pick) ? "matched" : "linked";
|
|
13110
|
+
if (!isMine(pick) && o.ship) await linkOperatorRow(o, out, pick);
|
|
13111
|
+
} else {
|
|
13112
|
+
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) => {
|
|
13113
|
+
const m = r.user_id ? members.get(r.user_id) : void 0;
|
|
13114
|
+
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");
|
|
13115
|
+
}).join("; "));
|
|
13116
|
+
if (o.ship && (!o.interactive || candidates.length === 0)) await createOperatorRow(o, out);
|
|
12919
13117
|
}
|
|
12920
13118
|
}
|
|
12921
13119
|
}
|
|
@@ -12956,6 +13154,7 @@ async function createOperatorRow(o, found) {
|
|
|
12956
13154
|
found.operator = created.id;
|
|
12957
13155
|
found.operatorAction = "created";
|
|
12958
13156
|
found.holds.push({ id: created.id, name, email: found.meEmail, user_id: found.meId });
|
|
13157
|
+
found.candidates.push({ id: created.id, name, email: found.meEmail, user_id: found.meId, detail: found.meEmail });
|
|
12959
13158
|
found.provisioning.push(`created Crew row "${name}" for you`);
|
|
12960
13159
|
} catch (e) {
|
|
12961
13160
|
if (!(e instanceof ConnectHttpError) || e.status !== 403) throw e;
|
|
@@ -12985,14 +13184,14 @@ ${opts.hostPassengers ? " hostPassengers: true\n" : ""}${apiKeyFileLine}${api
|
|
|
12985
13184
|
// src/cli.ts
|
|
12986
13185
|
import {
|
|
12987
13186
|
getSessionStore as getSessionStore3,
|
|
12988
|
-
hostFromUrl as
|
|
13187
|
+
hostFromUrl as hostFromUrl4,
|
|
12989
13188
|
loginWithDeviceCode,
|
|
12990
13189
|
getOrCreateShipId,
|
|
12991
13190
|
DeviceLoginError
|
|
12992
13191
|
} from "@tablation/client";
|
|
12993
13192
|
|
|
12994
13193
|
// src/seat-keys.ts
|
|
12995
|
-
import { getSessionStore as getSessionStore2, hostFromUrl as
|
|
13194
|
+
import { getSessionStore as getSessionStore2, hostFromUrl as hostFromUrl3 } from "@tablation/client";
|
|
12996
13195
|
var SEAT_KEY_ROLES = ROLE_NAMES.filter((r) => r !== "pair");
|
|
12997
13196
|
function keyIsAdmin(route) {
|
|
12998
13197
|
const role = route.resolved?.role;
|
|
@@ -13016,7 +13215,7 @@ async function resolveReleaseKey(route, getStore = getSessionStore2) {
|
|
|
13016
13215
|
const agentId = route.resolved?.releaseAgentId;
|
|
13017
13216
|
if (!agentId) return ship(keyIsAdmin(route) ? "release seat has no linked Agents row (run `crew agents sync`)" : `release seat ${MEMBER_NO_ROW}`);
|
|
13018
13217
|
try {
|
|
13019
|
-
const session = await getStore().get(
|
|
13218
|
+
const session = await getStore().get(hostFromUrl3(route.baseUrl), slugFor(route, agentId));
|
|
13020
13219
|
if (session?.apiKey) return { kind: "agent", apiKey: session.apiKey, agentId };
|
|
13021
13220
|
return ship(keyIsAdmin(route) ? "no agent key stored for the release seat (run `crew agents sync`)" : MEMBER_NO_KEY);
|
|
13022
13221
|
} catch {
|
|
@@ -13029,7 +13228,7 @@ async function resolveSeatKey(route, role, getStore = getSessionStore2) {
|
|
|
13029
13228
|
const agentId = seatAgentId(route, role);
|
|
13030
13229
|
if (!agentId) return ship(keyIsAdmin(route) ? "seat has no linked Agents row (run `crew agents sync`)" : `seat ${MEMBER_NO_ROW}`);
|
|
13031
13230
|
try {
|
|
13032
|
-
const session = await getStore().get(
|
|
13231
|
+
const session = await getStore().get(hostFromUrl3(route.baseUrl), slugFor(route, agentId));
|
|
13033
13232
|
if (session?.apiKey) return { kind: "agent", apiKey: session.apiKey, agentId };
|
|
13034
13233
|
return ship(keyIsAdmin(route) ? "no agent key stored for this seat (run `crew agents sync`)" : MEMBER_NO_KEY);
|
|
13035
13234
|
} catch {
|
|
@@ -13062,7 +13261,7 @@ async function mintMissingSeatKeys(route, o) {
|
|
|
13062
13261
|
detail: describeStoreError(e)
|
|
13063
13262
|
}));
|
|
13064
13263
|
}
|
|
13065
|
-
const host =
|
|
13264
|
+
const host = hostFromUrl3(route.baseUrl);
|
|
13066
13265
|
const base = route.baseUrl.replace(/\/+$/, "");
|
|
13067
13266
|
if (!o.dryRun) {
|
|
13068
13267
|
const probeSlug = seatKeySlug(splitRoute(route.route)[0], "_probe");
|
|
@@ -13183,7 +13382,7 @@ async function revokeSeatKeys(route, o) {
|
|
|
13183
13382
|
} catch (e) {
|
|
13184
13383
|
return targets.map(({ role }) => ({ role, action: "failed", detail: describeStoreError(e) }));
|
|
13185
13384
|
}
|
|
13186
|
-
const host =
|
|
13385
|
+
const host = hostFromUrl3(route.baseUrl);
|
|
13187
13386
|
const base = route.baseUrl.replace(/\/+$/, "");
|
|
13188
13387
|
for (const { role, agentId } of targets) {
|
|
13189
13388
|
if (!agentId) {
|
|
@@ -13440,8 +13639,8 @@ async function showLogEntry(route, entryId, opts = {}) {
|
|
|
13440
13639
|
import { isMap as isMap2, parseDocument as parseDocument2, YAMLMap } from "yaml";
|
|
13441
13640
|
|
|
13442
13641
|
// src/repos-cmd.ts
|
|
13443
|
-
import { homedir as
|
|
13444
|
-
import { basename as basename8, join as
|
|
13642
|
+
import { homedir as homedir8 } from "os";
|
|
13643
|
+
import { basename as basename8, join as join25, resolve as resolve3 } from "path";
|
|
13445
13644
|
import { isMap, isSeq, parseDocument, stringify as stringify2 } from "yaml";
|
|
13446
13645
|
var ReposError = class extends Error {
|
|
13447
13646
|
};
|
|
@@ -13487,7 +13686,7 @@ function parseAddArgs(args, routes, isDir) {
|
|
|
13487
13686
|
return { route, path: "." };
|
|
13488
13687
|
}
|
|
13489
13688
|
function expandDir(p, base) {
|
|
13490
|
-
const home = p === "~" ?
|
|
13689
|
+
const home = p === "~" ? homedir8() : p.startsWith("~/") ? `${homedir8()}/${p.slice(2)}` : p;
|
|
13491
13690
|
return resolve3(base, home);
|
|
13492
13691
|
}
|
|
13493
13692
|
function routeMapOf(doc, route) {
|
|
@@ -13695,7 +13894,7 @@ function planRepoClone(i) {
|
|
|
13695
13894
|
}
|
|
13696
13895
|
const ship = doc.get("ship", true);
|
|
13697
13896
|
const basePath = String(m.get("reposBasePath") ?? (isMap(ship) ? ship.get("reposBasePath") : void 0) ?? "~/Crew");
|
|
13698
|
-
const target = i.dir ? expandDir(i.dir, i.base) :
|
|
13897
|
+
const target = i.dir ? expandDir(i.dir, i.base) : join25(expandDir(basePath, i.base), i.route.split("/")[0], name);
|
|
13699
13898
|
const add = planRepoAdd({ text: i.text, base: i.base, route: i.route, path: target, origin: remote, resolved: i.resolved, name });
|
|
13700
13899
|
return { ...add, remote, target };
|
|
13701
13900
|
}
|
|
@@ -14075,7 +14274,7 @@ ${run2.err}` });
|
|
|
14075
14274
|
}
|
|
14076
14275
|
|
|
14077
14276
|
// src/sample-ticket.ts
|
|
14078
|
-
import { basename as basename9, dirname as dirname15, join as
|
|
14277
|
+
import { basename as basename9, dirname as dirname15, join as join26 } from "path";
|
|
14079
14278
|
var SAMPLE_REPO = "crew-sample";
|
|
14080
14279
|
var SAMPLE_REMOTE = `local/${SAMPLE_REPO}`;
|
|
14081
14280
|
var SAMPLE_MARKER = "sample: true";
|
|
@@ -14098,8 +14297,8 @@ has something safe to work on: its test and build hooks always pass, it has no
|
|
|
14098
14297
|
remote, and \`crew sample-ticket --remove\` deletes it.
|
|
14099
14298
|
`;
|
|
14100
14299
|
function sampleDir(existing, reposBase, workspace) {
|
|
14101
|
-
if (existing.length > 0) return
|
|
14102
|
-
return
|
|
14300
|
+
if (existing.length > 0) return join26(dirname15(existing[0]), SAMPLE_REPO);
|
|
14301
|
+
return join26(reposBase, workspace, SAMPLE_REPO);
|
|
14103
14302
|
}
|
|
14104
14303
|
var SAMPLE_TITLE = "Sample: add a documented `greet` function with a test";
|
|
14105
14304
|
function sampleDescription() {
|
|
@@ -14181,7 +14380,7 @@ function schedulerInstalled(plan, exists, crontab) {
|
|
|
14181
14380
|
|
|
14182
14381
|
// src/worktree-sweep.ts
|
|
14183
14382
|
import { existsSync as existsSync25, readFileSync as readFileSync20, readdirSync as readdirSync8 } from "fs";
|
|
14184
|
-
import { basename as basename10, dirname as dirname16, join as
|
|
14383
|
+
import { basename as basename10, dirname as dirname16, join as join27, resolve as resolve4 } from "path";
|
|
14185
14384
|
function planWorktreeSweep(target, tickets, contract) {
|
|
14186
14385
|
const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
|
|
14187
14386
|
const verified = contract.statuses.verified;
|
|
@@ -14248,7 +14447,7 @@ function siblingWorktreeDirs(target) {
|
|
|
14248
14447
|
for (const name of names) {
|
|
14249
14448
|
if (!name.startsWith(stem)) continue;
|
|
14250
14449
|
const m = DIR_NUMBER_SUFFIXED.exec(name);
|
|
14251
|
-
const path =
|
|
14450
|
+
const path = join27(parent, name);
|
|
14252
14451
|
if (m && !registered.has(resolve4(path))) out.push({ path, number: m[1] });
|
|
14253
14452
|
}
|
|
14254
14453
|
return out;
|
|
@@ -14456,14 +14655,14 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
|
|
|
14456
14655
|
}
|
|
14457
14656
|
|
|
14458
14657
|
// src/stream-sweep.ts
|
|
14459
|
-
import { existsSync as existsSync26, readdirSync as readdirSync9, rmSync as
|
|
14460
|
-
import { join as
|
|
14658
|
+
import { existsSync as existsSync26, readdirSync as readdirSync9, rmSync as rmSync13, statSync as statSync8 } from "fs";
|
|
14659
|
+
import { join as join28 } from "path";
|
|
14461
14660
|
var DAY_MS2 = 24 * 60 * 60 * 1e3;
|
|
14462
14661
|
function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
|
|
14463
|
-
const dir =
|
|
14662
|
+
const dir = join28(stateDir, "streams");
|
|
14464
14663
|
if (!existsSync26(dir)) return [];
|
|
14465
14664
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
14466
|
-
return readdirSync9(dir).map((name) =>
|
|
14665
|
+
return readdirSync9(dir).map((name) => join28(dir, name)).filter((path) => {
|
|
14467
14666
|
try {
|
|
14468
14667
|
return statSync8(path).mtimeMs < cutoff;
|
|
14469
14668
|
} catch {
|
|
@@ -14479,7 +14678,7 @@ function applyStreamSweep(paths, dryRun, log) {
|
|
|
14479
14678
|
continue;
|
|
14480
14679
|
}
|
|
14481
14680
|
try {
|
|
14482
|
-
|
|
14681
|
+
rmSync13(path, { force: true });
|
|
14483
14682
|
removed++;
|
|
14484
14683
|
log.emit(`removed stream artifact ${path}`, { step: "worktree" });
|
|
14485
14684
|
} catch (e) {
|
|
@@ -14491,7 +14690,7 @@ function applyStreamSweep(paths, dryRun, log) {
|
|
|
14491
14690
|
function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
14492
14691
|
if (!existsSync26(stateDir)) return [];
|
|
14493
14692
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
14494
|
-
return readdirSync9(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) =>
|
|
14693
|
+
return readdirSync9(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join28(stateDir, name)).filter((path) => {
|
|
14495
14694
|
try {
|
|
14496
14695
|
return statSync8(path).mtimeMs < cutoff;
|
|
14497
14696
|
} catch {
|
|
@@ -14501,18 +14700,18 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
|
14501
14700
|
}
|
|
14502
14701
|
|
|
14503
14702
|
// src/cli.ts
|
|
14504
|
-
import { realpathSync as realpathSync3, readFileSync as readFileSync27, existsSync as existsSync33, mkdirSync as mkdirSync20, writeFileSync as writeFileSync21, rmSync as
|
|
14703
|
+
import { realpathSync as realpathSync3, readFileSync as readFileSync27, existsSync as existsSync33, mkdirSync as mkdirSync20, writeFileSync as writeFileSync21, rmSync as rmSync15, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync2, statSync as statSync10, readdirSync as readdirSync14 } from "fs";
|
|
14505
14704
|
import { createInterface } from "readline/promises";
|
|
14506
14705
|
import { dirname as dirOf, resolve as resolvePath } from "path";
|
|
14507
14706
|
|
|
14508
14707
|
// src/ssh-keys.ts
|
|
14509
14708
|
import { execFileSync as execFileSync5 } from "child_process";
|
|
14510
14709
|
import { existsSync as existsSync27, mkdirSync as mkdirSync15, readFileSync as readFileSync21, chmodSync } from "fs";
|
|
14511
|
-
import { dirname as dirname17, join as
|
|
14710
|
+
import { dirname as dirname17, join as join29 } from "path";
|
|
14512
14711
|
var SshKeyError = class extends Error {
|
|
14513
14712
|
};
|
|
14514
14713
|
function shipSshKeyPathFor(stateDir) {
|
|
14515
|
-
return
|
|
14714
|
+
return join29(stateDir, "ssh", "id_ed25519");
|
|
14516
14715
|
}
|
|
14517
14716
|
function ensureShipSshKeypair(stateDir) {
|
|
14518
14717
|
const privateKeyPath = shipSshKeyPathFor(stateDir);
|
|
@@ -14557,9 +14756,9 @@ function sshKeygenAvailable() {
|
|
|
14557
14756
|
import { execFileSync as execFileSync6 } from "child_process";
|
|
14558
14757
|
import { createHash as createHash4 } from "crypto";
|
|
14559
14758
|
import { existsSync as existsSync28, mkdirSync as mkdirSync16, readFileSync as readFileSync22, writeFileSync as writeFileSync17 } from "fs";
|
|
14560
|
-
import { dirname as dirname18, join as
|
|
14759
|
+
import { dirname as dirname18, join as join30 } from "path";
|
|
14561
14760
|
function machineIdPathFor(stateDir) {
|
|
14562
|
-
return
|
|
14761
|
+
return join30(stateDir, "machine-id");
|
|
14563
14762
|
}
|
|
14564
14763
|
var sha256 = (s) => createHash4("sha256").update(s).digest("hex");
|
|
14565
14764
|
function readRawMachineId(platform2 = process.platform) {
|
|
@@ -14809,7 +15008,7 @@ function syncAllPassengerCheckouts(cfg) {
|
|
|
14809
15008
|
import { spawn as spawn5 } from "child_process";
|
|
14810
15009
|
import { existsSync as existsSync29, mkdirSync as mkdirSync17, readFileSync as readFileSync23, writeFileSync as writeFileSync18, unlinkSync as unlinkSync3, readdirSync as readdirSync10 } from "fs";
|
|
14811
15010
|
import { randomInt } from "crypto";
|
|
14812
|
-
import { join as
|
|
15011
|
+
import { join as join31 } from "path";
|
|
14813
15012
|
var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
|
|
14814
15013
|
var defaultSpawn = (cmd, args) => spawn5(cmd, args, DEFAULT_SPAWN_OPTIONS);
|
|
14815
15014
|
var SLUG_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
@@ -14876,7 +15075,7 @@ function startTunnel(opts) {
|
|
|
14876
15075
|
};
|
|
14877
15076
|
}
|
|
14878
15077
|
function tunnelStatePath(stateDir, workspaceId) {
|
|
14879
|
-
return
|
|
15078
|
+
return join31(stateDir, "passengers", `${workspaceId}.json`);
|
|
14880
15079
|
}
|
|
14881
15080
|
function readPersistedTunnel(stateDir, workspaceId) {
|
|
14882
15081
|
const path = tunnelStatePath(stateDir, workspaceId);
|
|
@@ -14889,7 +15088,7 @@ function readPersistedTunnel(stateDir, workspaceId) {
|
|
|
14889
15088
|
}
|
|
14890
15089
|
function writePersistedTunnel(stateDir, state) {
|
|
14891
15090
|
const path = tunnelStatePath(stateDir, state.workspaceId);
|
|
14892
|
-
mkdirSync17(
|
|
15091
|
+
mkdirSync17(join31(stateDir, "passengers"), { recursive: true });
|
|
14893
15092
|
writeFileSync18(path, `${JSON.stringify(state, null, 2)}
|
|
14894
15093
|
`);
|
|
14895
15094
|
}
|
|
@@ -14987,7 +15186,7 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
|
|
|
14987
15186
|
}
|
|
14988
15187
|
}
|
|
14989
15188
|
function readAllPersistedWorkspaceIds(stateDir) {
|
|
14990
|
-
const dir =
|
|
15189
|
+
const dir = join31(stateDir, "passengers");
|
|
14991
15190
|
if (!existsSync29(dir)) return [];
|
|
14992
15191
|
return readdirSync10(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
|
|
14993
15192
|
}
|
|
@@ -15110,7 +15309,7 @@ import { createServer as createHttpServer } from "http";
|
|
|
15110
15309
|
import { spawn as spawn6 } from "child_process";
|
|
15111
15310
|
import { randomBytes } from "crypto";
|
|
15112
15311
|
import { existsSync as existsSync30, mkdirSync as mkdirSync18, readFileSync as readFileSync24, writeFileSync as writeFileSync19, unlinkSync as unlinkSync4, readdirSync as readdirSync11 } from "fs";
|
|
15113
|
-
import { join as
|
|
15312
|
+
import { join as join32 } from "path";
|
|
15114
15313
|
|
|
15115
15314
|
// src/secret-compare.ts
|
|
15116
15315
|
import { timingSafeEqual } from "crypto";
|
|
@@ -15199,7 +15398,7 @@ var defaultSpawn2 = (cmd, args, env) => {
|
|
|
15199
15398
|
return child;
|
|
15200
15399
|
};
|
|
15201
15400
|
function syncDaemonStatePath(stateDir, workspaceId) {
|
|
15202
|
-
return
|
|
15401
|
+
return join32(stateDir, "passengers", `${workspaceId}-sync.json`);
|
|
15203
15402
|
}
|
|
15204
15403
|
function readPersistedSyncDaemon(stateDir, workspaceId) {
|
|
15205
15404
|
const path = syncDaemonStatePath(stateDir, workspaceId);
|
|
@@ -15212,7 +15411,7 @@ function readPersistedSyncDaemon(stateDir, workspaceId) {
|
|
|
15212
15411
|
}
|
|
15213
15412
|
function writePersistedSyncDaemon(stateDir, state) {
|
|
15214
15413
|
const path = syncDaemonStatePath(stateDir, state.workspaceId);
|
|
15215
|
-
mkdirSync18(
|
|
15414
|
+
mkdirSync18(join32(stateDir, "passengers"), { recursive: true });
|
|
15216
15415
|
writeFileSync19(path, `${JSON.stringify(state, null, 2)}
|
|
15217
15416
|
`);
|
|
15218
15417
|
}
|
|
@@ -15233,7 +15432,7 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
|
|
|
15233
15432
|
}
|
|
15234
15433
|
}
|
|
15235
15434
|
function readAllPersistedSyncWorkspaceIds(stateDir) {
|
|
15236
|
-
const dir =
|
|
15435
|
+
const dir = join32(stateDir, "passengers");
|
|
15237
15436
|
if (!existsSync30(dir)) return [];
|
|
15238
15437
|
const suffix = "-sync.json";
|
|
15239
15438
|
return readdirSync11(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
|
|
@@ -15322,12 +15521,12 @@ function describeCause(err) {
|
|
|
15322
15521
|
}
|
|
15323
15522
|
|
|
15324
15523
|
// src/daemon.ts
|
|
15325
|
-
import { existsSync as existsSync32, mkdirSync as mkdirSync19, readFileSync as readFileSync26, readdirSync as readdirSync13, rmSync as
|
|
15326
|
-
import { join as
|
|
15524
|
+
import { existsSync as existsSync32, mkdirSync as mkdirSync19, readFileSync as readFileSync26, readdirSync as readdirSync13, rmSync as rmSync14, statSync as statSync9, writeFileSync as writeFileSync20 } from "fs";
|
|
15525
|
+
import { join as join34 } from "path";
|
|
15327
15526
|
|
|
15328
15527
|
// src/interrupted.ts
|
|
15329
15528
|
import { existsSync as existsSync31, readdirSync as readdirSync12, readFileSync as readFileSync25 } from "fs";
|
|
15330
|
-
import { join as
|
|
15529
|
+
import { join as join33 } from "path";
|
|
15331
15530
|
var INTERRUPTED_MARKER = calloutHeader("NOTE", "Interrupted");
|
|
15332
15531
|
function interruptedComment(b) {
|
|
15333
15532
|
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.";
|
|
@@ -15354,13 +15553,13 @@ function runFinished(streamPath) {
|
|
|
15354
15553
|
}
|
|
15355
15554
|
}
|
|
15356
15555
|
function orphanedStreams(o) {
|
|
15357
|
-
const dir =
|
|
15556
|
+
const dir = join33(o.stateDir, "streams");
|
|
15358
15557
|
if (!existsSync31(dir)) return [];
|
|
15359
15558
|
const ownerAlive = o.isOwnerAlive ?? pidAlive;
|
|
15360
15559
|
const out = [];
|
|
15361
15560
|
for (const name of readdirSync12(dir)) {
|
|
15362
15561
|
if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
|
|
15363
|
-
const streamPath =
|
|
15562
|
+
const streamPath = join33(dir, name);
|
|
15364
15563
|
if (runFinished(streamPath)) continue;
|
|
15365
15564
|
const sidecar = readRunSidecar(streamPath);
|
|
15366
15565
|
if (!sidecar || sidecar.route !== o.routeLabel) continue;
|
|
@@ -15480,7 +15679,7 @@ function latestMtime(path) {
|
|
|
15480
15679
|
if (!stat.isDirectory()) return 0;
|
|
15481
15680
|
let latest = stat.mtimeMs;
|
|
15482
15681
|
for (const entry of readdirSync13(path, { withFileTypes: true })) {
|
|
15483
|
-
latest = Math.max(latest, latestMtime(
|
|
15682
|
+
latest = Math.max(latest, latestMtime(join34(path, entry.name)));
|
|
15484
15683
|
}
|
|
15485
15684
|
return latest;
|
|
15486
15685
|
}
|
|
@@ -15492,14 +15691,14 @@ function makeStaleChecker(paths, opts = {}) {
|
|
|
15492
15691
|
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync32(opts.reloadSentinel);
|
|
15493
15692
|
}
|
|
15494
15693
|
function reloadSentinelFor(stateDir) {
|
|
15495
|
-
return
|
|
15694
|
+
return join34(stateDir, "reload-requested");
|
|
15496
15695
|
}
|
|
15497
15696
|
function daemonStampFor(stateDir) {
|
|
15498
|
-
return
|
|
15697
|
+
return join34(stateDir, "daemon.started");
|
|
15499
15698
|
}
|
|
15500
15699
|
function noteDaemonStarted(stateDir, now = Date.now()) {
|
|
15501
15700
|
mkdirSync19(stateDir, { recursive: true });
|
|
15502
|
-
|
|
15701
|
+
rmSync14(reloadSentinelFor(stateDir), { force: true });
|
|
15503
15702
|
writeFileSync20(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
|
|
15504
15703
|
`);
|
|
15505
15704
|
}
|
|
@@ -15934,7 +16133,7 @@ async function pickOperator(holds2, me) {
|
|
|
15934
16133
|
const choices = holds2.filter((h) => !h.user_id || h.user_id === me.id);
|
|
15935
16134
|
const canCreate = !!me.id;
|
|
15936
16135
|
process.stderr.write("\nWhich of these Crew rows is you?\n");
|
|
15937
|
-
choices.forEach((h, i) => process.stderr.write(` ${i + 1}. ${h.name || h.
|
|
16136
|
+
choices.forEach((h, i) => process.stderr.write(` ${i + 1}. ${h.name || "unnamed row"}${h.detail ? ` <${h.detail}>` : ""}
|
|
15938
16137
|
`));
|
|
15939
16138
|
if (canCreate) process.stderr.write(` 0. Create a new Crew row for ${me.name ?? ""} ${me.email ?? ""}`.trimEnd() + "\n");
|
|
15940
16139
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
@@ -15968,10 +16167,11 @@ async function pickResolvedStatuses(unrecognized) {
|
|
|
15968
16167
|
}
|
|
15969
16168
|
async function offerTemplateInstall(authOpts, workspaceId, decided) {
|
|
15970
16169
|
const templates = await listLibraryTemplates(authOpts);
|
|
15971
|
-
const
|
|
16170
|
+
const keys = await resolveIssuesTemplateKeys(authOpts);
|
|
16171
|
+
const template = templates.find((t) => t.identifier === ISSUES_TEMPLATE_IDENTIFIERS[0]) ?? templates.find((t) => keys.has(t.identifier) || keys.has(t.id));
|
|
15972
16172
|
if (!template) {
|
|
15973
16173
|
process.stderr.write(`
|
|
15974
|
-
No template with identifier "${
|
|
16174
|
+
No template with identifier "${ISSUES_TEMPLATE_IDENTIFIERS[0]}" is published to the Library yet \u2014 nothing to offer to install.
|
|
15975
16175
|
`);
|
|
15976
16176
|
return void 0;
|
|
15977
16177
|
}
|
|
@@ -16009,8 +16209,8 @@ No project here has what a route needs. Install the "${template.name}" library t
|
|
|
16009
16209
|
`);
|
|
16010
16210
|
return installed.project.slug;
|
|
16011
16211
|
}
|
|
16012
|
-
function usage() {
|
|
16013
|
-
|
|
16212
|
+
function usage(problem, requested = false) {
|
|
16213
|
+
const text = `crew \u2014 a standing team of headless agents
|
|
16014
16214
|
|
|
16015
16215
|
crew poll [route] decide a cycle and report it; writes nothing
|
|
16016
16216
|
crew run [route] [--role R] run the winning role's session
|
|
@@ -16018,6 +16218,7 @@ function usage() {
|
|
|
16018
16218
|
crew merge [route] merge verified branches and stop
|
|
16019
16219
|
crew deploy [route] release now, even with nothing new to merge
|
|
16020
16220
|
crew watch [route] live view of what the crew is doing
|
|
16221
|
+
crew --version [--json] print the crew version on one line (also: crew version); --json adds home and compiled
|
|
16021
16222
|
crew status [route] [--json] paused/running state; --json for a machine reader
|
|
16022
16223
|
crew projects [--route R] [--json] the Projects rows each route can read (id, name, prefix, work mode)
|
|
16023
16224
|
crew whoami [route] [--json] the signed-in identity, key role, operator Crew row and credential source per route
|
|
@@ -16037,6 +16238,7 @@ function usage() {
|
|
|
16037
16238
|
resume with no argument clears only the whole-crew pause; a role or release pause needs its own resume
|
|
16038
16239
|
crew log [route] tail the log
|
|
16039
16240
|
crew inbox [--member NAME] your tickets across every workspace (or a colleague's)
|
|
16241
|
+
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)
|
|
16040
16242
|
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)
|
|
16041
16243
|
crew disconnect remove what connect installed for Pair (Claude MCP; Codex/Gemini instructions block and MCP entry per ship.pair)
|
|
16042
16244
|
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)
|
|
@@ -16071,10 +16273,26 @@ Options:
|
|
|
16071
16273
|
--all (inbox) include closed tickets
|
|
16072
16274
|
|
|
16073
16275
|
In watch: space pauses the stream, q quits.
|
|
16074
|
-
|
|
16276
|
+
`;
|
|
16277
|
+
if (requested) {
|
|
16278
|
+
process.stdout.write(text);
|
|
16279
|
+
process.exit(0);
|
|
16280
|
+
}
|
|
16281
|
+
process.stderr.write(problem ? `crew: ${problem}
|
|
16282
|
+
|
|
16283
|
+
${text}` : text);
|
|
16075
16284
|
process.exit(2);
|
|
16076
16285
|
}
|
|
16077
16286
|
var argv = process.argv.slice(2);
|
|
16287
|
+
if (helpRequest(argv)) usage(void 0, true);
|
|
16288
|
+
var versionReq = versionRequest(argv);
|
|
16289
|
+
if (versionReq) {
|
|
16290
|
+
const version = crewVersion(CREW_HOME);
|
|
16291
|
+
process.stdout.write(versionReq.json ? `${JSON.stringify({ version, home: CREW_HOME, compiled: isCompiledBinary(import.meta.url) })}
|
|
16292
|
+
` : `${version}
|
|
16293
|
+
`);
|
|
16294
|
+
process.exit(0);
|
|
16295
|
+
}
|
|
16078
16296
|
var flag = (name) => argv.includes(`--${name}`);
|
|
16079
16297
|
var value = (name) => {
|
|
16080
16298
|
const i = argv.indexOf(`--${name}`);
|
|
@@ -16086,7 +16304,45 @@ var connectEvents = createEmitter(command === "connect" && flag("json"), (l) =>
|
|
|
16086
16304
|
process.stdout.write(l);
|
|
16087
16305
|
});
|
|
16088
16306
|
var connectChoices = parseChoices(argv);
|
|
16089
|
-
if (!command) usage();
|
|
16307
|
+
if (!command) usage(argv.length ? `unknown option "${argv[0]}"` : void 0);
|
|
16308
|
+
var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
|
|
16309
|
+
"passenger-sync-daemon",
|
|
16310
|
+
"agents",
|
|
16311
|
+
"connect",
|
|
16312
|
+
"daemon",
|
|
16313
|
+
"deploy",
|
|
16314
|
+
"disconnect",
|
|
16315
|
+
"doctor",
|
|
16316
|
+
"drop",
|
|
16317
|
+
"inbox",
|
|
16318
|
+
"install",
|
|
16319
|
+
"log",
|
|
16320
|
+
"logbook",
|
|
16321
|
+
"merge",
|
|
16322
|
+
"passengers",
|
|
16323
|
+
"pause",
|
|
16324
|
+
"poll",
|
|
16325
|
+
"ports",
|
|
16326
|
+
"projects",
|
|
16327
|
+
"reap",
|
|
16328
|
+
"release",
|
|
16329
|
+
"reload",
|
|
16330
|
+
"repos",
|
|
16331
|
+
"resume",
|
|
16332
|
+
"rotate-passenger-url",
|
|
16333
|
+
"run",
|
|
16334
|
+
"sample-ticket",
|
|
16335
|
+
"selftest",
|
|
16336
|
+
"skills",
|
|
16337
|
+
"status",
|
|
16338
|
+
"sync",
|
|
16339
|
+
"unassign",
|
|
16340
|
+
"uninstall",
|
|
16341
|
+
"watch",
|
|
16342
|
+
"whoami",
|
|
16343
|
+
"worktree"
|
|
16344
|
+
]);
|
|
16345
|
+
if (!KNOWN_COMMANDS.has(command)) usage(`unknown command "${command}"`);
|
|
16090
16346
|
function findSyncRoute(c, name) {
|
|
16091
16347
|
if (!name && c.routes.length > 1) {
|
|
16092
16348
|
throw new ConfigError(`this ship has ${c.routes.length} routes \u2014 name one: ${c.routes.map((r) => r.route).join(", ")}`);
|
|
@@ -16176,6 +16432,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
16176
16432
|
let cfg;
|
|
16177
16433
|
let firstRun;
|
|
16178
16434
|
let firstRunFromFlags = false;
|
|
16435
|
+
let agentBinSearched;
|
|
16179
16436
|
try {
|
|
16180
16437
|
const connectIsTTY = !!(process.stdin.isTTY && process.stdout.isTTY);
|
|
16181
16438
|
const wizardFlags = {
|
|
@@ -16185,7 +16442,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
16185
16442
|
hostPassengers: flag("host-passengers"),
|
|
16186
16443
|
noWizard: flag("no-wizard")
|
|
16187
16444
|
};
|
|
16188
|
-
if (command === "connect" && usesFlagAnswers(wizardFlags, connectIsTTY) && shouldRunWizard({ isTTY: true, crewHome: CREW_HOME })) {
|
|
16445
|
+
if (command === "connect" && !flag("sign-out") && usesFlagAnswers(wizardFlags, connectIsTTY) && shouldRunWizard({ isTTY: true, crewHome: CREW_HOME })) {
|
|
16189
16446
|
const flagged = flagWizardAnswers(wizardFlags, {
|
|
16190
16447
|
hostname: hostname2(),
|
|
16191
16448
|
platform: hostPlatform(),
|
|
@@ -16202,8 +16459,9 @@ if (command === "passenger-sync-daemon") {
|
|
|
16202
16459
|
const configPath = firstRunConfigPath();
|
|
16203
16460
|
firstRun = { answers: flagged.answers, configPath };
|
|
16204
16461
|
firstRunFromFlags = true;
|
|
16462
|
+
agentBinSearched = flagged.agentBinSearched;
|
|
16205
16463
|
cfg = loadConfig(CREW_HOME, configPath, { text: renderShipBlock(flagged.answers), allowNoRoutes: true });
|
|
16206
|
-
} else if (command === "connect" && shouldRunWizard({
|
|
16464
|
+
} else if (command === "connect" && !flag("sign-out") && shouldRunWizard({
|
|
16207
16465
|
isTTY: connectIsTTY,
|
|
16208
16466
|
crewHome: CREW_HOME
|
|
16209
16467
|
})) {
|
|
@@ -16224,7 +16482,13 @@ if (command === "passenger-sync-daemon") {
|
|
|
16224
16482
|
firstRun = { answers, configPath };
|
|
16225
16483
|
cfg = loadConfig(CREW_HOME, configPath, { text: renderShipBlock(answers), allowNoRoutes: true });
|
|
16226
16484
|
} else {
|
|
16227
|
-
|
|
16485
|
+
const toleratesNoRoutes = command === "repos" || command === "disconnect" && flag("reset") || command === "connect" && flag("sign-out");
|
|
16486
|
+
const configless = command === "connect" && flag("sign-out") || command === "disconnect" && flag("reset");
|
|
16487
|
+
if (configless && !existsSync33(findConfigFile(CREW_HOME))) {
|
|
16488
|
+
cfg = loadConfig(CREW_HOME, firstRunConfigPath(), { text: "ship: {}\n", allowNoRoutes: true });
|
|
16489
|
+
} else {
|
|
16490
|
+
cfg = loadConfig(CREW_HOME, void 0, toleratesNoRoutes ? { allowNoRoutes: true } : {});
|
|
16491
|
+
}
|
|
16228
16492
|
}
|
|
16229
16493
|
} catch (e) {
|
|
16230
16494
|
if (e instanceof ConfigError) {
|
|
@@ -16458,7 +16722,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
16458
16722
|
}
|
|
16459
16723
|
if (host === "linux" && hasSystemd) {
|
|
16460
16724
|
const label = labelFor(CREW_HOME, "run");
|
|
16461
|
-
const staleTimer =
|
|
16725
|
+
const staleTimer = join35(homedir9(), ".config", "systemd", "user", `${label}.timer`);
|
|
16462
16726
|
if (existsSync33(staleTimer)) {
|
|
16463
16727
|
process.stdout.write(
|
|
16464
16728
|
`removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
|
|
@@ -16706,7 +16970,10 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16706
16970
|
}
|
|
16707
16971
|
if (!dryRun) {
|
|
16708
16972
|
const holderLabel = `${cfg.ship.name}:${process.pid}`;
|
|
16709
|
-
const got = await tracker.acquireBoardLock(scope, holderLabel, RELEASE_LOCK_TTL_MS
|
|
16973
|
+
const got = await tracker.acquireBoardLock(scope, holderLabel, RELEASE_LOCK_TTL_MS, {
|
|
16974
|
+
warn: (m) => remit.warn(m, { step: "release" }),
|
|
16975
|
+
debug: (m) => remit.emit(m, { step: "release" })
|
|
16976
|
+
});
|
|
16710
16977
|
if (!got.ok && got.reason === "missing") {
|
|
16711
16978
|
const detail = `no Locks row for ${scope} \u2014 run \`crew connect ${c.route}\` to provision it`;
|
|
16712
16979
|
remit.warn(`release for ${scope} refused: ${detail}`, { step: "release" });
|
|
@@ -17521,7 +17788,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
17521
17788
|
process.exit(1);
|
|
17522
17789
|
}
|
|
17523
17790
|
requireArmed("run the daemon loop");
|
|
17524
|
-
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [
|
|
17791
|
+
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join35(CREW_HOME, "src"), join35(CREW_HOME, "dist"), join35(CREW_HOME, "bin")];
|
|
17525
17792
|
const configPaths = daemonConfigPaths(cfg);
|
|
17526
17793
|
watchPaths.push(...configPaths);
|
|
17527
17794
|
noteDaemonStarted(cfg.ship.stateDir);
|
|
@@ -17865,7 +18132,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
17865
18132
|
if (sub === "list" && !routeName) usage2();
|
|
17866
18133
|
if (sub === "add") {
|
|
17867
18134
|
try {
|
|
17868
|
-
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync33(resolvePath(p.replace(/^~(?=\/|$)/,
|
|
18135
|
+
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync33(resolvePath(p.replace(/^~(?=\/|$)/, homedir9()))));
|
|
17869
18136
|
routeName = a.route;
|
|
17870
18137
|
addPath = a.path;
|
|
17871
18138
|
addName = a.name;
|
|
@@ -17967,7 +18234,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
17967
18234
|
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}`);
|
|
17968
18235
|
break;
|
|
17969
18236
|
}
|
|
17970
|
-
const target = resolvePath(addPath.replace(/^~(?=\/|$)/,
|
|
18237
|
+
const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir9()));
|
|
17971
18238
|
const top = existsSync33(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
|
|
17972
18239
|
if (!top) throw new ReposError(`${target} is not a git checkout`);
|
|
17973
18240
|
const origin = originOf(top);
|
|
@@ -18119,6 +18386,29 @@ ${reconstruction.prompt}
|
|
|
18119
18386
|
}
|
|
18120
18387
|
case "connect": {
|
|
18121
18388
|
const arg = positional[1];
|
|
18389
|
+
if (flag("sign-out")) {
|
|
18390
|
+
let signOutStore;
|
|
18391
|
+
try {
|
|
18392
|
+
signOutStore = getSessionStore3();
|
|
18393
|
+
} catch {
|
|
18394
|
+
signOutStore = void 0;
|
|
18395
|
+
}
|
|
18396
|
+
const outcomes = signOutStore ? await signOutSessions({
|
|
18397
|
+
store: signOutStore,
|
|
18398
|
+
stateDir: cfg.ship.stateDir,
|
|
18399
|
+
dryRun,
|
|
18400
|
+
host: value("host") ?? (value("base-url") ? hostFromUrl4(value("base-url")) : void 0),
|
|
18401
|
+
workspace: value("workspace")
|
|
18402
|
+
}) : [];
|
|
18403
|
+
const failed = outcomes.some((o) => o.action === "failed");
|
|
18404
|
+
if (flag("json")) process.stdout.write(`${JSON.stringify({ dryRun, ok: !failed, sessions: outcomes })}
|
|
18405
|
+
`);
|
|
18406
|
+
else if (!outcomes.length) process.stdout.write("nothing to sign out of\n");
|
|
18407
|
+
else for (const o of outcomes) process.stdout.write(`${o.action}: ${o.host}/${o.workspace}${o.detail ? ` (${o.detail})` : ""}
|
|
18408
|
+
`);
|
|
18409
|
+
if (failed) process.exitCode = 1;
|
|
18410
|
+
break;
|
|
18411
|
+
}
|
|
18122
18412
|
const canAsk = !!(process.stdin.isTTY && process.stdout.isTTY) && !connectEvents.json;
|
|
18123
18413
|
const stopForChoice = (key, message, options) => {
|
|
18124
18414
|
connectEvents.emit("choice", message, choiceData(key, options));
|
|
@@ -18136,6 +18426,7 @@ ${reconstruction.prompt}
|
|
|
18136
18426
|
const project = argProject ?? value("project") ?? connectChoices.project;
|
|
18137
18427
|
const baseUrl = value("base-url") ?? cfg.routes[0]?.baseUrl ?? DEFAULT_BASE_URL;
|
|
18138
18428
|
let apiKey = value("key") ?? process.env.CREW_CONNECT_KEY ?? cfg.routes.find((c) => c.apiKey)?.apiKey;
|
|
18429
|
+
const givenKeySource = value("key") ?? process.env.CREW_CONNECT_KEY ? "flag" : "config";
|
|
18139
18430
|
let store;
|
|
18140
18431
|
try {
|
|
18141
18432
|
store = getSessionStore3();
|
|
@@ -18145,11 +18436,11 @@ ${reconstruction.prompt}
|
|
|
18145
18436
|
let keychainBacked = false;
|
|
18146
18437
|
if (!apiKey && workspace && store) {
|
|
18147
18438
|
try {
|
|
18148
|
-
const existing = await store.get(
|
|
18439
|
+
const existing = await store.get(hostFromUrl4(baseUrl), workspace);
|
|
18149
18440
|
if (existing?.apiKey) {
|
|
18150
18441
|
apiKey = existing.apiKey;
|
|
18151
18442
|
keychainBacked = true;
|
|
18152
|
-
process.stderr.write(`Reusing the existing keychain session for ${
|
|
18443
|
+
process.stderr.write(`Reusing the existing keychain session for ${hostFromUrl4(baseUrl)}/${workspace}.
|
|
18153
18444
|
`);
|
|
18154
18445
|
}
|
|
18155
18446
|
} catch {
|
|
@@ -18160,7 +18451,7 @@ ${reconstruction.prompt}
|
|
|
18160
18451
|
const resumed = await resumeKeychainSession({
|
|
18161
18452
|
store,
|
|
18162
18453
|
stateDir: cfg.ship.stateDir,
|
|
18163
|
-
host:
|
|
18454
|
+
host: hostFromUrl4(baseUrl),
|
|
18164
18455
|
verify: async (k) => {
|
|
18165
18456
|
try {
|
|
18166
18457
|
await get({ baseUrl, apiKey: k, userAgent: cfg.ship.userAgent }, "/auth/me");
|
|
@@ -18175,7 +18466,7 @@ ${reconstruction.prompt}
|
|
|
18175
18466
|
workspace = resumed.workspace;
|
|
18176
18467
|
keychainBacked = true;
|
|
18177
18468
|
signInSource = "keychain";
|
|
18178
|
-
process.stderr.write(`Reusing the existing keychain session for ${
|
|
18469
|
+
process.stderr.write(`Reusing the existing keychain session for ${hostFromUrl4(baseUrl)}/${workspace}.
|
|
18179
18470
|
`);
|
|
18180
18471
|
}
|
|
18181
18472
|
}
|
|
@@ -18232,13 +18523,14 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
18232
18523
|
connectEvents.emit("signed_in", `signed in as ${result.identity.email}`, {
|
|
18233
18524
|
identity: { id: result.identity.id, email: result.identity.email },
|
|
18234
18525
|
workspaces: [{ slug: result.workspace.slug, name: result.workspace.name }],
|
|
18235
|
-
session: "device"
|
|
18526
|
+
session: "device",
|
|
18527
|
+
host: hostFromUrl4(baseUrl)
|
|
18236
18528
|
});
|
|
18237
18529
|
const deviceWorkspace = "path" in result.workspace && typeof result.workspace.path === "string" ? result.workspace.path : result.workspace.slug;
|
|
18238
18530
|
if (!workspace) workspace = deviceWorkspace;
|
|
18239
18531
|
if (store) {
|
|
18240
18532
|
try {
|
|
18241
|
-
await store.set(
|
|
18533
|
+
await store.set(hostFromUrl4(baseUrl), deviceWorkspace, {
|
|
18242
18534
|
apiKey: result.apiKey.key,
|
|
18243
18535
|
shipId: getOrCreateShipId(),
|
|
18244
18536
|
workspaceId: result.workspace.id,
|
|
@@ -18246,9 +18538,9 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
18246
18538
|
createdAt: result.apiKey.createdAt
|
|
18247
18539
|
});
|
|
18248
18540
|
keychainBacked = true;
|
|
18249
|
-
rememberSession(cfg.ship.stateDir,
|
|
18541
|
+
rememberSession(cfg.ship.stateDir, hostFromUrl4(baseUrl), deviceWorkspace);
|
|
18250
18542
|
process.stderr.write(
|
|
18251
|
-
`API key stored in the OS keychain for ${
|
|
18543
|
+
`API key stored in the OS keychain for ${hostFromUrl4(baseUrl)}/${deviceWorkspace}.
|
|
18252
18544
|
`
|
|
18253
18545
|
);
|
|
18254
18546
|
} catch (e) {
|
|
@@ -18293,7 +18585,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
18293
18585
|
connectEvents.emit("signed_in", `key belongs to ${found.meEmail ?? found.meId}`, {
|
|
18294
18586
|
identity: { id: found.meId, email: found.meEmail, name: found.meName },
|
|
18295
18587
|
workspaces: [{ slug: found.workspaceSlug, name: found.workspaceName, role: found.role }],
|
|
18296
|
-
session: signInSource
|
|
18588
|
+
session: signInSource,
|
|
18589
|
+
host: hostFromUrl4(baseUrl)
|
|
18297
18590
|
});
|
|
18298
18591
|
}
|
|
18299
18592
|
connectEvents.emit("workspace_resolved", `workspace ${found.workspaceSlug}`, {
|
|
@@ -18368,8 +18661,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
18368
18661
|
}
|
|
18369
18662
|
}
|
|
18370
18663
|
let operator = found.operator;
|
|
18371
|
-
if (!operator && found.
|
|
18372
|
-
const picked = await pickOperator(found.
|
|
18664
|
+
if (!operator && found.candidates.length > 0 && canAsk) {
|
|
18665
|
+
const picked = await pickOperator(found.candidates, { id: found.meId, name: found.meName, email: found.meEmail });
|
|
18373
18666
|
if (picked === NEW_OPERATOR_ROW) {
|
|
18374
18667
|
await createOperatorRow({ ...authOpts, dryRun }, found);
|
|
18375
18668
|
operator = found.operator;
|
|
@@ -18388,7 +18681,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
18388
18681
|
}
|
|
18389
18682
|
}
|
|
18390
18683
|
if (!operator) operator = previous?.operator;
|
|
18391
|
-
if (connectEvents.json && !operator && found.
|
|
18684
|
+
if (connectEvents.json && !operator && found.candidates.length > 0) {
|
|
18392
18685
|
const answer = connectChoices.operator;
|
|
18393
18686
|
if (answer === "new") {
|
|
18394
18687
|
await createOperatorRow({ ...authOpts, dryRun }, found);
|
|
@@ -18403,27 +18696,36 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
18403
18696
|
found.operatorAction = "linked";
|
|
18404
18697
|
}
|
|
18405
18698
|
} else {
|
|
18406
|
-
const mine = found.
|
|
18699
|
+
const mine = found.candidates.filter((h) => !h.user_id || h.user_id === found.meId);
|
|
18407
18700
|
stopForChoice("operator", "Which Crew row is you?", [
|
|
18408
|
-
...mine.map((h) => ({ value: h.id, label: h.name || h.id })),
|
|
18701
|
+
...mine.map((h) => ({ value: h.id, label: h.name || "unnamed row", ...h.detail ? { detail: h.detail } : { detail: h.id } })),
|
|
18409
18702
|
...found.meId ? [{ value: "new", label: `Create a new Crew row for ${found.meName ?? found.meEmail ?? "me"}` }] : []
|
|
18410
18703
|
]);
|
|
18411
18704
|
}
|
|
18412
18705
|
}
|
|
18706
|
+
if (!operator && found.operatorCompared) {
|
|
18707
|
+
process.stderr.write(`crew connect: no Crew row matched you \u2014 ${found.operatorCompared}
|
|
18708
|
+
`);
|
|
18709
|
+
connectEvents.emit("operator_row", "no Crew row matched you", { action: "unmatched", compared: found.operatorCompared });
|
|
18710
|
+
}
|
|
18413
18711
|
if (operator) {
|
|
18414
|
-
|
|
18712
|
+
const opRow = found.holds.find((h) => h.id === operator);
|
|
18713
|
+
const opEmail = opRow?.email || opRow?.memberEmail || (opRow?.user_id && opRow.user_id === found.meId ? found.meEmail : void 0);
|
|
18714
|
+
connectEvents.emit("operator_row", `operator ${opRow?.name ?? operator}`, {
|
|
18415
18715
|
id: operator,
|
|
18416
|
-
action: found.operatorAction ?? "previous"
|
|
18716
|
+
action: found.operatorAction ?? "previous",
|
|
18717
|
+
...opRow?.name ? { name: opRow.name } : {},
|
|
18718
|
+
...opEmail ? { email: opEmail } : {}
|
|
18417
18719
|
});
|
|
18418
18720
|
}
|
|
18419
18721
|
if (!operator && !hadPreviousFile) {
|
|
18420
18722
|
process.stderr.write(
|
|
18421
|
-
`crew connect ${route2}: cannot determine the operator \u2014 ${found.
|
|
18723
|
+
`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.
|
|
18422
18724
|
Re-run from a real terminal to pick one, or pass --key for a key whose own
|
|
18423
18725
|
identity email matches exactly one Crew row.
|
|
18424
18726
|
`
|
|
18425
18727
|
);
|
|
18426
|
-
stopWithProblem(`cannot determine the operator \u2014 ${found.
|
|
18728
|
+
stopWithProblem(`cannot determine the operator \u2014 ${found.candidates.length} Crew row(s) could be it`, "operator");
|
|
18427
18729
|
}
|
|
18428
18730
|
const alreadyReviewed = new Set(previous?.reviewedStatuses ?? []);
|
|
18429
18731
|
const stillUnrecognized = (found.unrecognizedStatuses ?? []).filter((s) => !alreadyReviewed.has(s.value));
|
|
@@ -18518,12 +18820,19 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
18518
18820
|
role: sr.role,
|
|
18519
18821
|
id: sr.id,
|
|
18520
18822
|
action: sr.action,
|
|
18823
|
+
...sr.shipId ? { ship_id: sr.shipId } : {},
|
|
18521
18824
|
key: minted?.action ?? "none",
|
|
18522
18825
|
...minted?.detail ? { detail: minted.detail } : {}
|
|
18523
18826
|
});
|
|
18524
18827
|
}
|
|
18525
18828
|
let apiKeyFile;
|
|
18526
18829
|
const apiKeyVar = "CREW_API_KEY";
|
|
18830
|
+
if (firstRunFromFlags && agentBinSearched && !dryRun) {
|
|
18831
|
+
const msg = `no agent binary: "claude" was not found on PATH or in ${agentBinSearched.join(", ")} \u2014 pass --agent-bin /absolute/path`;
|
|
18832
|
+
process.stderr.write(`crew connect: ${msg}
|
|
18833
|
+
`);
|
|
18834
|
+
stopWithProblem(msg, "wizard");
|
|
18835
|
+
}
|
|
18527
18836
|
if ((mintedFromDevice || firstRunFromFlags) && !keychainBacked) {
|
|
18528
18837
|
apiKeyFile = apiKeyPathFor(cfg.ship.stateDir, route2);
|
|
18529
18838
|
if (dryRun) {
|
|
@@ -18540,7 +18849,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
18540
18849
|
const keyPersisted = keychainBacked || apiKeyFile !== void 0;
|
|
18541
18850
|
const wizard = firstRun;
|
|
18542
18851
|
const writeFirstRun = wizard !== void 0 && !dryRun && keyPersisted;
|
|
18543
|
-
const routeDir = value("dir") ?? (writeFirstRun ?
|
|
18852
|
+
const routeDir = value("dir") ?? (writeFirstRun ? join35(DEFAULT_REPOS_BASE_PATH, found.workspaceSlug) : void 0);
|
|
18544
18853
|
const routeBlock = renderConnection(
|
|
18545
18854
|
found,
|
|
18546
18855
|
route2,
|
|
@@ -18689,13 +18998,39 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
18689
18998
|
wroteConfig,
|
|
18690
18999
|
operator,
|
|
18691
19000
|
dryRun,
|
|
18692
|
-
unresolved: found.problems.length
|
|
19001
|
+
unresolved: found.problems.length,
|
|
19002
|
+
...firstRunFromFlags ? { agentBin: agentBinSearched ? null : firstRun?.answers.agentBin } : {},
|
|
19003
|
+
...agentBinSearched ? { agentBinNote: `no \`claude\` found on PATH or in ${agentBinSearched.join(", ")}; a real run needs --agent-bin` } : {}
|
|
18693
19004
|
});
|
|
18694
19005
|
};
|
|
18695
19006
|
try {
|
|
18696
19007
|
if (!workspace) {
|
|
18697
19008
|
const workspaces = await listWorkspaces(authOpts);
|
|
18698
|
-
|
|
19009
|
+
const configWorkspaces = [...new Set(cfg.routes.map((r) => splitRoute(r.route)[0]))];
|
|
19010
|
+
const defaulted = dryRun && givenKeySource === "config" && configWorkspaces.length === 1 ? workspaces.find((w) => w.slug === configWorkspaces[0]) : void 0;
|
|
19011
|
+
if (!signedInEmitted && (apiKey ?? "") !== "") {
|
|
19012
|
+
let me = {};
|
|
19013
|
+
try {
|
|
19014
|
+
me = await get(authOpts, "/auth/me");
|
|
19015
|
+
} catch {
|
|
19016
|
+
}
|
|
19017
|
+
if (me.id || me.email) {
|
|
19018
|
+
signedInEmitted = true;
|
|
19019
|
+
connectEvents.emit(
|
|
19020
|
+
"signed_in",
|
|
19021
|
+
`key belongs to ${me.email ?? me.id}${defaulted ? `; using ${defaulted.slug}, the workspace crew.yaml already names` : ""}`,
|
|
19022
|
+
{
|
|
19023
|
+
identity: { id: me.id, email: me.email, name: me.name },
|
|
19024
|
+
workspaces: workspaces.map((w) => ({ slug: w.slug, name: w.name, role: w.role })),
|
|
19025
|
+
session: givenKeySource,
|
|
19026
|
+
host: hostFromUrl4(baseUrl)
|
|
19027
|
+
}
|
|
19028
|
+
);
|
|
19029
|
+
}
|
|
19030
|
+
}
|
|
19031
|
+
if (defaulted) {
|
|
19032
|
+
await runConnect(defaulted.slug, project);
|
|
19033
|
+
} else if (workspaces.length === 0) {
|
|
18699
19034
|
process.stderr.write("crew connect: this key's identity has no workspace memberships.\n");
|
|
18700
19035
|
stopWithProblem("this key's identity has no workspace memberships", "workspace");
|
|
18701
19036
|
} else if (workspaces.length === 1) {
|
|
@@ -19171,11 +19506,20 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
19171
19506
|
if (!resetStore) return { stored: false, clear: async () => {
|
|
19172
19507
|
} };
|
|
19173
19508
|
const store = resetStore;
|
|
19174
|
-
const h =
|
|
19509
|
+
const h = hostFromUrl4(r.baseUrl);
|
|
19175
19510
|
const [slug] = splitRoute(r.route);
|
|
19176
19511
|
const session = await store.get(h, slug);
|
|
19177
19512
|
return { stored: Boolean(session?.apiKey), clear: () => store.clear(h, slug) };
|
|
19178
19513
|
},
|
|
19514
|
+
async hostSessions(stateDir) {
|
|
19515
|
+
const store = resetStore;
|
|
19516
|
+
return Promise.all(rememberedSessions(stateDir).map(async ({ host: h, workspace: w }) => {
|
|
19517
|
+
const session = store ? await store.get(h, w).catch(() => void 0) : void 0;
|
|
19518
|
+
return { host: h, workspace: w, stored: Boolean(session?.apiKey), clear: async () => {
|
|
19519
|
+
await store?.clear(h, w);
|
|
19520
|
+
} };
|
|
19521
|
+
}));
|
|
19522
|
+
},
|
|
19179
19523
|
inspectCheckout(dir) {
|
|
19180
19524
|
const dirty = status(dir).map((c) => c.path);
|
|
19181
19525
|
const linkedWorktrees = worktrees(dir).map((w) => w.path).filter((p) => realpathSync3(p) !== realpathSync3(dir));
|
|
@@ -19346,12 +19690,12 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
19346
19690
|
`);
|
|
19347
19691
|
}
|
|
19348
19692
|
}
|
|
19349
|
-
if (existsSync33(
|
|
19693
|
+
if (existsSync33(join35(dir, ".git")) && basename11(dir) === SAMPLE_REPO) rmSync15(dir, { recursive: true, force: true });
|
|
19350
19694
|
say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
|
|
19351
19695
|
break;
|
|
19352
19696
|
}
|
|
19353
19697
|
const projectId = route.resolved.areaId ?? resolveRepoProject(await tracker.projectRows(), value("project"));
|
|
19354
|
-
const fresh = !existsSync33(
|
|
19698
|
+
const fresh = !existsSync33(join35(dir, ".git"));
|
|
19355
19699
|
if (existsSync33(dir) && fresh && readdirSync14(dir).length > 0) fail(`${dir} exists and is not a git checkout \u2014 move it aside`);
|
|
19356
19700
|
if (dryRun) {
|
|
19357
19701
|
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`);
|
|
@@ -19359,8 +19703,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
19359
19703
|
}
|
|
19360
19704
|
if (fresh) {
|
|
19361
19705
|
mkdirSync20(dir, { recursive: true });
|
|
19362
|
-
writeFileSync21(
|
|
19363
|
-
writeFileSync21(
|
|
19706
|
+
writeFileSync21(join35(dir, "README.md"), SAMPLE_README);
|
|
19707
|
+
writeFileSync21(join35(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
|
|
19364
19708
|
const g = (...a) => {
|
|
19365
19709
|
const r = spawnSync6("git", a, { cwd: dir, encoding: "utf8" });
|
|
19366
19710
|
if (r.status !== 0) fail(`git ${a[0]} failed in ${dir}: ${(r.stderr || r.stdout).trim()}`);
|
|
@@ -19810,6 +20154,11 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
19810
20154
|
if (warn) process.stdout.write(` WARNING ${warn}
|
|
19811
20155
|
`);
|
|
19812
20156
|
}
|
|
20157
|
+
const scope = releaseLockScope(route.route, r.name);
|
|
20158
|
+
const lock2 = await tracker.lockState(scope).catch(() => void 0);
|
|
20159
|
+
if (lock2?.holder && staleOwnLockClaim(lock2.holder, cfg.ship.name)) {
|
|
20160
|
+
process.stdout.write(` ${formatStaleOwnLock(scope, lock2.holder)}`);
|
|
20161
|
+
}
|
|
19813
20162
|
}
|
|
19814
20163
|
const missingAuthorship = await tracker.missingAuthorshipColumns();
|
|
19815
20164
|
process.stdout.write(
|
|
@@ -20130,7 +20479,7 @@ ${foreign.join("\n")}
|
|
|
20130
20479
|
break;
|
|
20131
20480
|
}
|
|
20132
20481
|
default:
|
|
20133
|
-
usage();
|
|
20482
|
+
usage(`unknown command "${command}"`);
|
|
20134
20483
|
}
|
|
20135
20484
|
lock?.release();
|
|
20136
20485
|
}
|