@tablation/crew 0.71.2 → 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 +465 -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",
|
|
@@ -8641,6 +8685,9 @@ function versionRequest(argv2) {
|
|
|
8641
8685
|
if (rest.some((a) => a !== "--json")) return null;
|
|
8642
8686
|
return { json: rest.length > 0 };
|
|
8643
8687
|
}
|
|
8688
|
+
function helpRequest(argv2) {
|
|
8689
|
+
return argv2[0] === "--help" || argv2[0] === "-h" || argv2[0] === "help";
|
|
8690
|
+
}
|
|
8644
8691
|
|
|
8645
8692
|
// src/roster.ts
|
|
8646
8693
|
function memberByIdentity(roster, identityId) {
|
|
@@ -8736,7 +8783,7 @@ function rosterMarkdown(roster, meId) {
|
|
|
8736
8783
|
}
|
|
8737
8784
|
|
|
8738
8785
|
// src/connect-resume.ts
|
|
8739
|
-
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";
|
|
8740
8787
|
import { dirname as dirname8, join as join19 } from "path";
|
|
8741
8788
|
function lastSessionFile(stateDir) {
|
|
8742
8789
|
return join19(stateDir, "connect-session.json");
|
|
@@ -8771,10 +8818,45 @@ async function resumeKeychainSession(o) {
|
|
|
8771
8818
|
return void 0;
|
|
8772
8819
|
}
|
|
8773
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
|
+
}
|
|
8774
8855
|
|
|
8775
8856
|
// src/reset.ts
|
|
8776
|
-
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";
|
|
8777
8858
|
import { dirname as dirname9, join as join20 } from "path";
|
|
8859
|
+
import { hostFromUrl as hostFromUrl2 } from "@tablation/client";
|
|
8778
8860
|
function clonedCheckoutsFile(stateDir) {
|
|
8779
8861
|
return join20(stateDir, "cloned-checkouts.json");
|
|
8780
8862
|
}
|
|
@@ -8820,12 +8902,31 @@ async function runReset(o, fx) {
|
|
|
8820
8902
|
steps.push({ step: "credential", target: route.route, action: "failed", detail: e.message });
|
|
8821
8903
|
}
|
|
8822
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
|
+
}
|
|
8823
8924
|
if (o.removeCheckouts) {
|
|
8824
8925
|
for (const dir of readClonedCheckouts(o.stateDir)) steps.push(removeCheckout(dir, o.dryRun, fx));
|
|
8825
8926
|
}
|
|
8826
8927
|
steps.push(...removeState(o.stateDir, o.dryRun));
|
|
8827
8928
|
if (existsSync20(o.configFile)) {
|
|
8828
|
-
if (!o.dryRun)
|
|
8929
|
+
if (!o.dryRun) rmSync11(o.configFile, { force: true });
|
|
8829
8930
|
steps.push({ step: "config", target: o.configFile, action: mark(o.dryRun) });
|
|
8830
8931
|
} else {
|
|
8831
8932
|
steps.push({ step: "config", target: o.configFile, action: "absent" });
|
|
@@ -8841,7 +8942,7 @@ function removeCheckout(dir, dryRun, fx) {
|
|
|
8841
8942
|
if (linkedWorktrees.length) {
|
|
8842
8943
|
return { step: "checkout", target: dir, action: "kept", detail: `has linked worktrees (${linkedWorktrees.join(", ")}); remove them first` };
|
|
8843
8944
|
}
|
|
8844
|
-
if (!dryRun)
|
|
8945
|
+
if (!dryRun) rmSync11(dir, { recursive: true, force: true });
|
|
8845
8946
|
return { step: "checkout", target: dir, action: mark(dryRun) };
|
|
8846
8947
|
}
|
|
8847
8948
|
function removeState(stateDir, dryRun) {
|
|
@@ -8855,7 +8956,7 @@ function removeState(stateDir, dryRun) {
|
|
|
8855
8956
|
}
|
|
8856
8957
|
for (const t of targets) {
|
|
8857
8958
|
if (!existsSync20(t)) continue;
|
|
8858
|
-
if (!dryRun)
|
|
8959
|
+
if (!dryRun) rmSync11(t, { recursive: true, force: true });
|
|
8859
8960
|
out.push({ step: "state", target: t, action: mark(dryRun) });
|
|
8860
8961
|
}
|
|
8861
8962
|
if (!out.length) out.push({ step: "state", target: stateDir, action: "absent", detail: "nothing to remove" });
|
|
@@ -10519,7 +10620,9 @@ Wrote ${o.configPath}.
|
|
|
10519
10620
|
|
|
10520
10621
|
// src/connect-json.ts
|
|
10521
10622
|
import { accessSync as accessSync3, constants as constants3 } from "fs";
|
|
10522
|
-
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";
|
|
10523
10626
|
var CHOICE_EXIT_CODE = 3;
|
|
10524
10627
|
function createEmitter(json, write) {
|
|
10525
10628
|
return {
|
|
@@ -10553,6 +10656,23 @@ function usesFlagAnswers(f, isTTY) {
|
|
|
10553
10656
|
if (isTTY) return false;
|
|
10554
10657
|
return !!(f.shipName || f.agentBin || f.extraPath || f.hostPassengers);
|
|
10555
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
|
+
}
|
|
10556
10676
|
function flagWizardAnswers(f, env) {
|
|
10557
10677
|
const isExec = env.isExecutable ?? ((p) => {
|
|
10558
10678
|
try {
|
|
@@ -10562,9 +10682,9 @@ function flagWizardAnswers(f, env) {
|
|
|
10562
10682
|
return false;
|
|
10563
10683
|
}
|
|
10564
10684
|
});
|
|
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
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;
|
|
10568
10688
|
const pnpmPath = findOnPath2("pnpm", env.pathEnv, isExec);
|
|
10569
10689
|
const dockerPath = f.hostPassengers ? findOnPath2("docker", env.pathEnv, isExec) : void 0;
|
|
10570
10690
|
const extraPath = f.extraPath ?? deriveExtraPath([dirname11(env.nodePath), pnpmPath ? dirname11(pnpmPath) : void 0, dockerPath ? dirname11(dockerPath) : void 0]);
|
|
@@ -10572,16 +10692,17 @@ function flagWizardAnswers(f, env) {
|
|
|
10572
10692
|
answers: {
|
|
10573
10693
|
name: f.shipName?.trim() || env.hostname,
|
|
10574
10694
|
platform: env.platform,
|
|
10575
|
-
agentBin,
|
|
10695
|
+
agentBin: agentBin ?? "claude",
|
|
10576
10696
|
extraPath,
|
|
10577
10697
|
hostPassengers: f.hostPassengers
|
|
10578
|
-
}
|
|
10698
|
+
},
|
|
10699
|
+
...agentBin ? {} : { agentBinSearched: found?.searched ?? [] }
|
|
10579
10700
|
};
|
|
10580
10701
|
}
|
|
10581
10702
|
|
|
10582
10703
|
// 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
|
|
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";
|
|
10585
10706
|
import { tmpdir as tmpdir3 } from "os";
|
|
10586
10707
|
|
|
10587
10708
|
// src/release.ts
|
|
@@ -11299,7 +11420,7 @@ async function bump(o, size) {
|
|
|
11299
11420
|
}
|
|
11300
11421
|
return printed;
|
|
11301
11422
|
}
|
|
11302
|
-
const files = o.repo.release.versionFiles.map((f) =>
|
|
11423
|
+
const files = o.repo.release.versionFiles.map((f) => join24(o.cwd, f));
|
|
11303
11424
|
const first = files[0];
|
|
11304
11425
|
if (!existsSync22(first)) {
|
|
11305
11426
|
o.emit.error(`no ${o.repo.release.versionFiles[0]} to read a version from`);
|
|
@@ -11331,7 +11452,7 @@ async function bump(o, size) {
|
|
|
11331
11452
|
function writeChangelog(o, version, entries) {
|
|
11332
11453
|
const file = o.repo.release.changelog;
|
|
11333
11454
|
if (!file || entries.length === 0) return;
|
|
11334
|
-
const path =
|
|
11455
|
+
const path = join24(o.cwd, file);
|
|
11335
11456
|
const section = renderChangelogSection(version, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), entries);
|
|
11336
11457
|
if (o.dryRun) {
|
|
11337
11458
|
o.emit.emit(`would prepend ${entries.length} entr(ies) to ${file}`);
|
|
@@ -11421,7 +11542,7 @@ function closeReleaseCheckout(cwd, root, dir, branch, run2 = gitOk) {
|
|
|
11421
11542
|
run2(cwd, ["worktree", "remove", "--force", dir]);
|
|
11422
11543
|
} finally {
|
|
11423
11544
|
try {
|
|
11424
|
-
|
|
11545
|
+
rmSync12(root, { recursive: true, force: true });
|
|
11425
11546
|
} finally {
|
|
11426
11547
|
run2(cwd, ["worktree", "prune"]);
|
|
11427
11548
|
run2(cwd, ["branch", "-D", branch]);
|
|
@@ -11431,8 +11552,8 @@ function closeReleaseCheckout(cwd, root, dir, branch, run2 = gitOk) {
|
|
|
11431
11552
|
function openReleaseCheckout(o) {
|
|
11432
11553
|
const { remote, base } = o.repo.branch;
|
|
11433
11554
|
const branch = `crew/release-${Date.now()}`;
|
|
11434
|
-
const root = mkdtempSync2(
|
|
11435
|
-
const dir =
|
|
11555
|
+
const root = mkdtempSync2(join24(tmpdir3(), "crew-release-"));
|
|
11556
|
+
const dir = join24(root, "checkout");
|
|
11436
11557
|
o.emit.emit(`release checkout: ${dir}`, { step: "release" });
|
|
11437
11558
|
const close = () => closeReleaseCheckout(o.cwd, root, dir, branch);
|
|
11438
11559
|
try {
|
|
@@ -11442,10 +11563,10 @@ function openReleaseCheckout(o) {
|
|
|
11442
11563
|
throw e;
|
|
11443
11564
|
}
|
|
11444
11565
|
for (const f of o.repo.worktrees.copy) {
|
|
11445
|
-
const src =
|
|
11566
|
+
const src = join24(o.cwd, f);
|
|
11446
11567
|
if (!existsSync22(src)) continue;
|
|
11447
|
-
mkdirSync13(dirname12(
|
|
11448
|
-
copyFileSync(src,
|
|
11568
|
+
mkdirSync13(dirname12(join24(dir, f)), { recursive: true });
|
|
11569
|
+
copyFileSync(src, join24(dir, f));
|
|
11449
11570
|
}
|
|
11450
11571
|
return { dir, branch, close };
|
|
11451
11572
|
}
|
|
@@ -12592,7 +12713,22 @@ async function provisionReleaseLocks(o, locksModelId, route, repoNames) {
|
|
|
12592
12713
|
};
|
|
12593
12714
|
return ensureLockRows(client2, locksModelId, scopes);
|
|
12594
12715
|
}
|
|
12595
|
-
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
|
+
}
|
|
12596
12732
|
async function listLibraryTemplates(o) {
|
|
12597
12733
|
return get(o, "/library-templates");
|
|
12598
12734
|
}
|
|
@@ -12633,6 +12769,7 @@ async function discover(o) {
|
|
|
12633
12769
|
models: {},
|
|
12634
12770
|
seats: {},
|
|
12635
12771
|
holds: [],
|
|
12772
|
+
candidates: [],
|
|
12636
12773
|
problems,
|
|
12637
12774
|
provisioning
|
|
12638
12775
|
};
|
|
@@ -12641,6 +12778,7 @@ async function discover(o) {
|
|
|
12641
12778
|
out.role = me.role;
|
|
12642
12779
|
out.meEmail = me.email;
|
|
12643
12780
|
out.meId = me.id;
|
|
12781
|
+
out.meIdentityId = me.identityId;
|
|
12644
12782
|
out.meName = me.name;
|
|
12645
12783
|
} catch {
|
|
12646
12784
|
}
|
|
@@ -12664,7 +12802,8 @@ async function discover(o) {
|
|
|
12664
12802
|
}
|
|
12665
12803
|
} else {
|
|
12666
12804
|
const projects = await get(o, `/projects?workspaceId=${ws.id}`);
|
|
12667
|
-
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"));
|
|
12668
12807
|
if (defaultCandidate && (await missingTables(o, defaultCandidate.id)).length === 0) {
|
|
12669
12808
|
out.projectId = defaultCandidate.id;
|
|
12670
12809
|
out.projectSlug = defaultCandidate.slug;
|
|
@@ -12856,13 +12995,32 @@ async function discover(o) {
|
|
|
12856
12995
|
o,
|
|
12857
12996
|
`/data-models/${out.models.crew}/records?limit=200`
|
|
12858
12997
|
);
|
|
12859
|
-
|
|
12860
|
-
|
|
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) : [];
|
|
12861
13013
|
for (const role of ["dev", "design", "qa", "triage"]) {
|
|
12862
13014
|
const searchTerm = role === "dev" ? "develop" : role;
|
|
12863
13015
|
let id = seatFor(scoped, searchTerm);
|
|
12864
13016
|
let seatAction = "matched";
|
|
12865
|
-
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) {
|
|
12866
13024
|
const legacyId = seatFor(unclaimed, searchTerm);
|
|
12867
13025
|
if (legacyId) {
|
|
12868
13026
|
await patch(o, `/data-models/${out.models.crew}/records/${legacyId}`, { ship_id: shipRowId });
|
|
@@ -12882,10 +13040,13 @@ async function discover(o) {
|
|
|
12882
13040
|
}
|
|
12883
13041
|
if (id) {
|
|
12884
13042
|
out.seats[role] = id;
|
|
12885
|
-
|
|
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" });
|
|
12886
13047
|
} else problems.push(`no Crew row looking like the ${role} seat \u2014 create one, or omit that role`);
|
|
12887
13048
|
}
|
|
12888
|
-
const pairId = seatFor(scoped, "pair") || (
|
|
13049
|
+
const pairId = seatFor(scoped, "pair") || (seatShipId ? seatFor(unclaimed, "pair") : void 0);
|
|
12889
13050
|
if (pairId) out.seats.pair = pairId;
|
|
12890
13051
|
if (shipRowId) {
|
|
12891
13052
|
const releaseId = seatFor(scoped, "release");
|
|
@@ -12903,25 +13064,56 @@ async function discover(o) {
|
|
|
12903
13064
|
const holdRows = rows.filter((r) => !seatIds.has(r.id));
|
|
12904
13065
|
out.holds = holdRows.map((r) => ({ id: r.id, name: r.name ?? "", email: r.email, user_id: r.user_id }));
|
|
12905
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);
|
|
12906
13093
|
if (!out.meId) {
|
|
12907
|
-
if (out.
|
|
13094
|
+
if (out.candidates.length === 0) problems.push("no non-seat Crew rows \u2014 which row is the operator?");
|
|
12908
13095
|
else {
|
|
12909
|
-
const byEmail = out.meEmail ?
|
|
13096
|
+
const byEmail = out.meEmail ? people.filter((r) => emailOf(r).some((e) => eq(e, out.meEmail))) : [];
|
|
12910
13097
|
if (byEmail.length === 1) out.operator = byEmail[0].id;
|
|
12911
|
-
else if (out.
|
|
13098
|
+
else if (out.candidates.length === 1) out.operator = out.candidates[0].id;
|
|
12912
13099
|
}
|
|
12913
13100
|
} else {
|
|
12914
|
-
const candidates =
|
|
12915
|
-
const mine =
|
|
12916
|
-
const byEmail = out.meEmail ? candidates.filter((r) =>
|
|
12917
|
-
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;
|
|
12918
13106
|
const pick = mine ?? (byEmail.length === 1 ? byEmail[0] : only);
|
|
12919
13107
|
if (pick) {
|
|
12920
13108
|
out.operator = pick.id;
|
|
12921
|
-
out.operatorAction = pick
|
|
12922
|
-
if (pick
|
|
12923
|
-
} else
|
|
12924
|
-
|
|
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);
|
|
12925
13117
|
}
|
|
12926
13118
|
}
|
|
12927
13119
|
}
|
|
@@ -12962,6 +13154,7 @@ async function createOperatorRow(o, found) {
|
|
|
12962
13154
|
found.operator = created.id;
|
|
12963
13155
|
found.operatorAction = "created";
|
|
12964
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 });
|
|
12965
13158
|
found.provisioning.push(`created Crew row "${name}" for you`);
|
|
12966
13159
|
} catch (e) {
|
|
12967
13160
|
if (!(e instanceof ConnectHttpError) || e.status !== 403) throw e;
|
|
@@ -12991,14 +13184,14 @@ ${opts.hostPassengers ? " hostPassengers: true\n" : ""}${apiKeyFileLine}${api
|
|
|
12991
13184
|
// src/cli.ts
|
|
12992
13185
|
import {
|
|
12993
13186
|
getSessionStore as getSessionStore3,
|
|
12994
|
-
hostFromUrl as
|
|
13187
|
+
hostFromUrl as hostFromUrl4,
|
|
12995
13188
|
loginWithDeviceCode,
|
|
12996
13189
|
getOrCreateShipId,
|
|
12997
13190
|
DeviceLoginError
|
|
12998
13191
|
} from "@tablation/client";
|
|
12999
13192
|
|
|
13000
13193
|
// src/seat-keys.ts
|
|
13001
|
-
import { getSessionStore as getSessionStore2, hostFromUrl as
|
|
13194
|
+
import { getSessionStore as getSessionStore2, hostFromUrl as hostFromUrl3 } from "@tablation/client";
|
|
13002
13195
|
var SEAT_KEY_ROLES = ROLE_NAMES.filter((r) => r !== "pair");
|
|
13003
13196
|
function keyIsAdmin(route) {
|
|
13004
13197
|
const role = route.resolved?.role;
|
|
@@ -13022,7 +13215,7 @@ async function resolveReleaseKey(route, getStore = getSessionStore2) {
|
|
|
13022
13215
|
const agentId = route.resolved?.releaseAgentId;
|
|
13023
13216
|
if (!agentId) return ship(keyIsAdmin(route) ? "release seat has no linked Agents row (run `crew agents sync`)" : `release seat ${MEMBER_NO_ROW}`);
|
|
13024
13217
|
try {
|
|
13025
|
-
const session = await getStore().get(
|
|
13218
|
+
const session = await getStore().get(hostFromUrl3(route.baseUrl), slugFor(route, agentId));
|
|
13026
13219
|
if (session?.apiKey) return { kind: "agent", apiKey: session.apiKey, agentId };
|
|
13027
13220
|
return ship(keyIsAdmin(route) ? "no agent key stored for the release seat (run `crew agents sync`)" : MEMBER_NO_KEY);
|
|
13028
13221
|
} catch {
|
|
@@ -13035,7 +13228,7 @@ async function resolveSeatKey(route, role, getStore = getSessionStore2) {
|
|
|
13035
13228
|
const agentId = seatAgentId(route, role);
|
|
13036
13229
|
if (!agentId) return ship(keyIsAdmin(route) ? "seat has no linked Agents row (run `crew agents sync`)" : `seat ${MEMBER_NO_ROW}`);
|
|
13037
13230
|
try {
|
|
13038
|
-
const session = await getStore().get(
|
|
13231
|
+
const session = await getStore().get(hostFromUrl3(route.baseUrl), slugFor(route, agentId));
|
|
13039
13232
|
if (session?.apiKey) return { kind: "agent", apiKey: session.apiKey, agentId };
|
|
13040
13233
|
return ship(keyIsAdmin(route) ? "no agent key stored for this seat (run `crew agents sync`)" : MEMBER_NO_KEY);
|
|
13041
13234
|
} catch {
|
|
@@ -13068,7 +13261,7 @@ async function mintMissingSeatKeys(route, o) {
|
|
|
13068
13261
|
detail: describeStoreError(e)
|
|
13069
13262
|
}));
|
|
13070
13263
|
}
|
|
13071
|
-
const host =
|
|
13264
|
+
const host = hostFromUrl3(route.baseUrl);
|
|
13072
13265
|
const base = route.baseUrl.replace(/\/+$/, "");
|
|
13073
13266
|
if (!o.dryRun) {
|
|
13074
13267
|
const probeSlug = seatKeySlug(splitRoute(route.route)[0], "_probe");
|
|
@@ -13189,7 +13382,7 @@ async function revokeSeatKeys(route, o) {
|
|
|
13189
13382
|
} catch (e) {
|
|
13190
13383
|
return targets.map(({ role }) => ({ role, action: "failed", detail: describeStoreError(e) }));
|
|
13191
13384
|
}
|
|
13192
|
-
const host =
|
|
13385
|
+
const host = hostFromUrl3(route.baseUrl);
|
|
13193
13386
|
const base = route.baseUrl.replace(/\/+$/, "");
|
|
13194
13387
|
for (const { role, agentId } of targets) {
|
|
13195
13388
|
if (!agentId) {
|
|
@@ -13446,8 +13639,8 @@ async function showLogEntry(route, entryId, opts = {}) {
|
|
|
13446
13639
|
import { isMap as isMap2, parseDocument as parseDocument2, YAMLMap } from "yaml";
|
|
13447
13640
|
|
|
13448
13641
|
// src/repos-cmd.ts
|
|
13449
|
-
import { homedir as
|
|
13450
|
-
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";
|
|
13451
13644
|
import { isMap, isSeq, parseDocument, stringify as stringify2 } from "yaml";
|
|
13452
13645
|
var ReposError = class extends Error {
|
|
13453
13646
|
};
|
|
@@ -13493,7 +13686,7 @@ function parseAddArgs(args, routes, isDir) {
|
|
|
13493
13686
|
return { route, path: "." };
|
|
13494
13687
|
}
|
|
13495
13688
|
function expandDir(p, base) {
|
|
13496
|
-
const home = p === "~" ?
|
|
13689
|
+
const home = p === "~" ? homedir8() : p.startsWith("~/") ? `${homedir8()}/${p.slice(2)}` : p;
|
|
13497
13690
|
return resolve3(base, home);
|
|
13498
13691
|
}
|
|
13499
13692
|
function routeMapOf(doc, route) {
|
|
@@ -13701,7 +13894,7 @@ function planRepoClone(i) {
|
|
|
13701
13894
|
}
|
|
13702
13895
|
const ship = doc.get("ship", true);
|
|
13703
13896
|
const basePath = String(m.get("reposBasePath") ?? (isMap(ship) ? ship.get("reposBasePath") : void 0) ?? "~/Crew");
|
|
13704
|
-
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);
|
|
13705
13898
|
const add = planRepoAdd({ text: i.text, base: i.base, route: i.route, path: target, origin: remote, resolved: i.resolved, name });
|
|
13706
13899
|
return { ...add, remote, target };
|
|
13707
13900
|
}
|
|
@@ -14081,7 +14274,7 @@ ${run2.err}` });
|
|
|
14081
14274
|
}
|
|
14082
14275
|
|
|
14083
14276
|
// src/sample-ticket.ts
|
|
14084
|
-
import { basename as basename9, dirname as dirname15, join as
|
|
14277
|
+
import { basename as basename9, dirname as dirname15, join as join26 } from "path";
|
|
14085
14278
|
var SAMPLE_REPO = "crew-sample";
|
|
14086
14279
|
var SAMPLE_REMOTE = `local/${SAMPLE_REPO}`;
|
|
14087
14280
|
var SAMPLE_MARKER = "sample: true";
|
|
@@ -14104,8 +14297,8 @@ has something safe to work on: its test and build hooks always pass, it has no
|
|
|
14104
14297
|
remote, and \`crew sample-ticket --remove\` deletes it.
|
|
14105
14298
|
`;
|
|
14106
14299
|
function sampleDir(existing, reposBase, workspace) {
|
|
14107
|
-
if (existing.length > 0) return
|
|
14108
|
-
return
|
|
14300
|
+
if (existing.length > 0) return join26(dirname15(existing[0]), SAMPLE_REPO);
|
|
14301
|
+
return join26(reposBase, workspace, SAMPLE_REPO);
|
|
14109
14302
|
}
|
|
14110
14303
|
var SAMPLE_TITLE = "Sample: add a documented `greet` function with a test";
|
|
14111
14304
|
function sampleDescription() {
|
|
@@ -14187,7 +14380,7 @@ function schedulerInstalled(plan, exists, crontab) {
|
|
|
14187
14380
|
|
|
14188
14381
|
// src/worktree-sweep.ts
|
|
14189
14382
|
import { existsSync as existsSync25, readFileSync as readFileSync20, readdirSync as readdirSync8 } from "fs";
|
|
14190
|
-
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";
|
|
14191
14384
|
function planWorktreeSweep(target, tickets, contract) {
|
|
14192
14385
|
const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
|
|
14193
14386
|
const verified = contract.statuses.verified;
|
|
@@ -14254,7 +14447,7 @@ function siblingWorktreeDirs(target) {
|
|
|
14254
14447
|
for (const name of names) {
|
|
14255
14448
|
if (!name.startsWith(stem)) continue;
|
|
14256
14449
|
const m = DIR_NUMBER_SUFFIXED.exec(name);
|
|
14257
|
-
const path =
|
|
14450
|
+
const path = join27(parent, name);
|
|
14258
14451
|
if (m && !registered.has(resolve4(path))) out.push({ path, number: m[1] });
|
|
14259
14452
|
}
|
|
14260
14453
|
return out;
|
|
@@ -14462,14 +14655,14 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
|
|
|
14462
14655
|
}
|
|
14463
14656
|
|
|
14464
14657
|
// src/stream-sweep.ts
|
|
14465
|
-
import { existsSync as existsSync26, readdirSync as readdirSync9, rmSync as
|
|
14466
|
-
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";
|
|
14467
14660
|
var DAY_MS2 = 24 * 60 * 60 * 1e3;
|
|
14468
14661
|
function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
|
|
14469
|
-
const dir =
|
|
14662
|
+
const dir = join28(stateDir, "streams");
|
|
14470
14663
|
if (!existsSync26(dir)) return [];
|
|
14471
14664
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
14472
|
-
return readdirSync9(dir).map((name) =>
|
|
14665
|
+
return readdirSync9(dir).map((name) => join28(dir, name)).filter((path) => {
|
|
14473
14666
|
try {
|
|
14474
14667
|
return statSync8(path).mtimeMs < cutoff;
|
|
14475
14668
|
} catch {
|
|
@@ -14485,7 +14678,7 @@ function applyStreamSweep(paths, dryRun, log) {
|
|
|
14485
14678
|
continue;
|
|
14486
14679
|
}
|
|
14487
14680
|
try {
|
|
14488
|
-
|
|
14681
|
+
rmSync13(path, { force: true });
|
|
14489
14682
|
removed++;
|
|
14490
14683
|
log.emit(`removed stream artifact ${path}`, { step: "worktree" });
|
|
14491
14684
|
} catch (e) {
|
|
@@ -14497,7 +14690,7 @@ function applyStreamSweep(paths, dryRun, log) {
|
|
|
14497
14690
|
function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
14498
14691
|
if (!existsSync26(stateDir)) return [];
|
|
14499
14692
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
14500
|
-
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) => {
|
|
14501
14694
|
try {
|
|
14502
14695
|
return statSync8(path).mtimeMs < cutoff;
|
|
14503
14696
|
} catch {
|
|
@@ -14507,18 +14700,18 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
|
14507
14700
|
}
|
|
14508
14701
|
|
|
14509
14702
|
// src/cli.ts
|
|
14510
|
-
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";
|
|
14511
14704
|
import { createInterface } from "readline/promises";
|
|
14512
14705
|
import { dirname as dirOf, resolve as resolvePath } from "path";
|
|
14513
14706
|
|
|
14514
14707
|
// src/ssh-keys.ts
|
|
14515
14708
|
import { execFileSync as execFileSync5 } from "child_process";
|
|
14516
14709
|
import { existsSync as existsSync27, mkdirSync as mkdirSync15, readFileSync as readFileSync21, chmodSync } from "fs";
|
|
14517
|
-
import { dirname as dirname17, join as
|
|
14710
|
+
import { dirname as dirname17, join as join29 } from "path";
|
|
14518
14711
|
var SshKeyError = class extends Error {
|
|
14519
14712
|
};
|
|
14520
14713
|
function shipSshKeyPathFor(stateDir) {
|
|
14521
|
-
return
|
|
14714
|
+
return join29(stateDir, "ssh", "id_ed25519");
|
|
14522
14715
|
}
|
|
14523
14716
|
function ensureShipSshKeypair(stateDir) {
|
|
14524
14717
|
const privateKeyPath = shipSshKeyPathFor(stateDir);
|
|
@@ -14563,9 +14756,9 @@ function sshKeygenAvailable() {
|
|
|
14563
14756
|
import { execFileSync as execFileSync6 } from "child_process";
|
|
14564
14757
|
import { createHash as createHash4 } from "crypto";
|
|
14565
14758
|
import { existsSync as existsSync28, mkdirSync as mkdirSync16, readFileSync as readFileSync22, writeFileSync as writeFileSync17 } from "fs";
|
|
14566
|
-
import { dirname as dirname18, join as
|
|
14759
|
+
import { dirname as dirname18, join as join30 } from "path";
|
|
14567
14760
|
function machineIdPathFor(stateDir) {
|
|
14568
|
-
return
|
|
14761
|
+
return join30(stateDir, "machine-id");
|
|
14569
14762
|
}
|
|
14570
14763
|
var sha256 = (s) => createHash4("sha256").update(s).digest("hex");
|
|
14571
14764
|
function readRawMachineId(platform2 = process.platform) {
|
|
@@ -14815,7 +15008,7 @@ function syncAllPassengerCheckouts(cfg) {
|
|
|
14815
15008
|
import { spawn as spawn5 } from "child_process";
|
|
14816
15009
|
import { existsSync as existsSync29, mkdirSync as mkdirSync17, readFileSync as readFileSync23, writeFileSync as writeFileSync18, unlinkSync as unlinkSync3, readdirSync as readdirSync10 } from "fs";
|
|
14817
15010
|
import { randomInt } from "crypto";
|
|
14818
|
-
import { join as
|
|
15011
|
+
import { join as join31 } from "path";
|
|
14819
15012
|
var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
|
|
14820
15013
|
var defaultSpawn = (cmd, args) => spawn5(cmd, args, DEFAULT_SPAWN_OPTIONS);
|
|
14821
15014
|
var SLUG_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
@@ -14882,7 +15075,7 @@ function startTunnel(opts) {
|
|
|
14882
15075
|
};
|
|
14883
15076
|
}
|
|
14884
15077
|
function tunnelStatePath(stateDir, workspaceId) {
|
|
14885
|
-
return
|
|
15078
|
+
return join31(stateDir, "passengers", `${workspaceId}.json`);
|
|
14886
15079
|
}
|
|
14887
15080
|
function readPersistedTunnel(stateDir, workspaceId) {
|
|
14888
15081
|
const path = tunnelStatePath(stateDir, workspaceId);
|
|
@@ -14895,7 +15088,7 @@ function readPersistedTunnel(stateDir, workspaceId) {
|
|
|
14895
15088
|
}
|
|
14896
15089
|
function writePersistedTunnel(stateDir, state) {
|
|
14897
15090
|
const path = tunnelStatePath(stateDir, state.workspaceId);
|
|
14898
|
-
mkdirSync17(
|
|
15091
|
+
mkdirSync17(join31(stateDir, "passengers"), { recursive: true });
|
|
14899
15092
|
writeFileSync18(path, `${JSON.stringify(state, null, 2)}
|
|
14900
15093
|
`);
|
|
14901
15094
|
}
|
|
@@ -14993,7 +15186,7 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
|
|
|
14993
15186
|
}
|
|
14994
15187
|
}
|
|
14995
15188
|
function readAllPersistedWorkspaceIds(stateDir) {
|
|
14996
|
-
const dir =
|
|
15189
|
+
const dir = join31(stateDir, "passengers");
|
|
14997
15190
|
if (!existsSync29(dir)) return [];
|
|
14998
15191
|
return readdirSync10(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
|
|
14999
15192
|
}
|
|
@@ -15116,7 +15309,7 @@ import { createServer as createHttpServer } from "http";
|
|
|
15116
15309
|
import { spawn as spawn6 } from "child_process";
|
|
15117
15310
|
import { randomBytes } from "crypto";
|
|
15118
15311
|
import { existsSync as existsSync30, mkdirSync as mkdirSync18, readFileSync as readFileSync24, writeFileSync as writeFileSync19, unlinkSync as unlinkSync4, readdirSync as readdirSync11 } from "fs";
|
|
15119
|
-
import { join as
|
|
15312
|
+
import { join as join32 } from "path";
|
|
15120
15313
|
|
|
15121
15314
|
// src/secret-compare.ts
|
|
15122
15315
|
import { timingSafeEqual } from "crypto";
|
|
@@ -15205,7 +15398,7 @@ var defaultSpawn2 = (cmd, args, env) => {
|
|
|
15205
15398
|
return child;
|
|
15206
15399
|
};
|
|
15207
15400
|
function syncDaemonStatePath(stateDir, workspaceId) {
|
|
15208
|
-
return
|
|
15401
|
+
return join32(stateDir, "passengers", `${workspaceId}-sync.json`);
|
|
15209
15402
|
}
|
|
15210
15403
|
function readPersistedSyncDaemon(stateDir, workspaceId) {
|
|
15211
15404
|
const path = syncDaemonStatePath(stateDir, workspaceId);
|
|
@@ -15218,7 +15411,7 @@ function readPersistedSyncDaemon(stateDir, workspaceId) {
|
|
|
15218
15411
|
}
|
|
15219
15412
|
function writePersistedSyncDaemon(stateDir, state) {
|
|
15220
15413
|
const path = syncDaemonStatePath(stateDir, state.workspaceId);
|
|
15221
|
-
mkdirSync18(
|
|
15414
|
+
mkdirSync18(join32(stateDir, "passengers"), { recursive: true });
|
|
15222
15415
|
writeFileSync19(path, `${JSON.stringify(state, null, 2)}
|
|
15223
15416
|
`);
|
|
15224
15417
|
}
|
|
@@ -15239,7 +15432,7 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
|
|
|
15239
15432
|
}
|
|
15240
15433
|
}
|
|
15241
15434
|
function readAllPersistedSyncWorkspaceIds(stateDir) {
|
|
15242
|
-
const dir =
|
|
15435
|
+
const dir = join32(stateDir, "passengers");
|
|
15243
15436
|
if (!existsSync30(dir)) return [];
|
|
15244
15437
|
const suffix = "-sync.json";
|
|
15245
15438
|
return readdirSync11(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
|
|
@@ -15328,12 +15521,12 @@ function describeCause(err) {
|
|
|
15328
15521
|
}
|
|
15329
15522
|
|
|
15330
15523
|
// src/daemon.ts
|
|
15331
|
-
import { existsSync as existsSync32, mkdirSync as mkdirSync19, readFileSync as readFileSync26, readdirSync as readdirSync13, rmSync as
|
|
15332
|
-
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";
|
|
15333
15526
|
|
|
15334
15527
|
// src/interrupted.ts
|
|
15335
15528
|
import { existsSync as existsSync31, readdirSync as readdirSync12, readFileSync as readFileSync25 } from "fs";
|
|
15336
|
-
import { join as
|
|
15529
|
+
import { join as join33 } from "path";
|
|
15337
15530
|
var INTERRUPTED_MARKER = calloutHeader("NOTE", "Interrupted");
|
|
15338
15531
|
function interruptedComment(b) {
|
|
15339
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.";
|
|
@@ -15360,13 +15553,13 @@ function runFinished(streamPath) {
|
|
|
15360
15553
|
}
|
|
15361
15554
|
}
|
|
15362
15555
|
function orphanedStreams(o) {
|
|
15363
|
-
const dir =
|
|
15556
|
+
const dir = join33(o.stateDir, "streams");
|
|
15364
15557
|
if (!existsSync31(dir)) return [];
|
|
15365
15558
|
const ownerAlive = o.isOwnerAlive ?? pidAlive;
|
|
15366
15559
|
const out = [];
|
|
15367
15560
|
for (const name of readdirSync12(dir)) {
|
|
15368
15561
|
if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
|
|
15369
|
-
const streamPath =
|
|
15562
|
+
const streamPath = join33(dir, name);
|
|
15370
15563
|
if (runFinished(streamPath)) continue;
|
|
15371
15564
|
const sidecar = readRunSidecar(streamPath);
|
|
15372
15565
|
if (!sidecar || sidecar.route !== o.routeLabel) continue;
|
|
@@ -15486,7 +15679,7 @@ function latestMtime(path) {
|
|
|
15486
15679
|
if (!stat.isDirectory()) return 0;
|
|
15487
15680
|
let latest = stat.mtimeMs;
|
|
15488
15681
|
for (const entry of readdirSync13(path, { withFileTypes: true })) {
|
|
15489
|
-
latest = Math.max(latest, latestMtime(
|
|
15682
|
+
latest = Math.max(latest, latestMtime(join34(path, entry.name)));
|
|
15490
15683
|
}
|
|
15491
15684
|
return latest;
|
|
15492
15685
|
}
|
|
@@ -15498,14 +15691,14 @@ function makeStaleChecker(paths, opts = {}) {
|
|
|
15498
15691
|
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync32(opts.reloadSentinel);
|
|
15499
15692
|
}
|
|
15500
15693
|
function reloadSentinelFor(stateDir) {
|
|
15501
|
-
return
|
|
15694
|
+
return join34(stateDir, "reload-requested");
|
|
15502
15695
|
}
|
|
15503
15696
|
function daemonStampFor(stateDir) {
|
|
15504
|
-
return
|
|
15697
|
+
return join34(stateDir, "daemon.started");
|
|
15505
15698
|
}
|
|
15506
15699
|
function noteDaemonStarted(stateDir, now = Date.now()) {
|
|
15507
15700
|
mkdirSync19(stateDir, { recursive: true });
|
|
15508
|
-
|
|
15701
|
+
rmSync14(reloadSentinelFor(stateDir), { force: true });
|
|
15509
15702
|
writeFileSync20(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
|
|
15510
15703
|
`);
|
|
15511
15704
|
}
|
|
@@ -15940,7 +16133,7 @@ async function pickOperator(holds2, me) {
|
|
|
15940
16133
|
const choices = holds2.filter((h) => !h.user_id || h.user_id === me.id);
|
|
15941
16134
|
const canCreate = !!me.id;
|
|
15942
16135
|
process.stderr.write("\nWhich of these Crew rows is you?\n");
|
|
15943
|
-
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}>` : ""}
|
|
15944
16137
|
`));
|
|
15945
16138
|
if (canCreate) process.stderr.write(` 0. Create a new Crew row for ${me.name ?? ""} ${me.email ?? ""}`.trimEnd() + "\n");
|
|
15946
16139
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
@@ -15974,10 +16167,11 @@ async function pickResolvedStatuses(unrecognized) {
|
|
|
15974
16167
|
}
|
|
15975
16168
|
async function offerTemplateInstall(authOpts, workspaceId, decided) {
|
|
15976
16169
|
const templates = await listLibraryTemplates(authOpts);
|
|
15977
|
-
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));
|
|
15978
16172
|
if (!template) {
|
|
15979
16173
|
process.stderr.write(`
|
|
15980
|
-
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.
|
|
15981
16175
|
`);
|
|
15982
16176
|
return void 0;
|
|
15983
16177
|
}
|
|
@@ -16015,8 +16209,8 @@ No project here has what a route needs. Install the "${template.name}" library t
|
|
|
16015
16209
|
`);
|
|
16016
16210
|
return installed.project.slug;
|
|
16017
16211
|
}
|
|
16018
|
-
function usage() {
|
|
16019
|
-
|
|
16212
|
+
function usage(problem, requested = false) {
|
|
16213
|
+
const text = `crew \u2014 a standing team of headless agents
|
|
16020
16214
|
|
|
16021
16215
|
crew poll [route] decide a cycle and report it; writes nothing
|
|
16022
16216
|
crew run [route] [--role R] run the winning role's session
|
|
@@ -16044,6 +16238,7 @@ function usage() {
|
|
|
16044
16238
|
resume with no argument clears only the whole-crew pause; a role or release pause needs its own resume
|
|
16045
16239
|
crew log [route] tail the log
|
|
16046
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)
|
|
16047
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)
|
|
16048
16243
|
crew disconnect remove what connect installed for Pair (Claude MCP; Codex/Gemini instructions block and MCP entry per ship.pair)
|
|
16049
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)
|
|
@@ -16078,10 +16273,18 @@ Options:
|
|
|
16078
16273
|
--all (inbox) include closed tickets
|
|
16079
16274
|
|
|
16080
16275
|
In watch: space pauses the stream, q quits.
|
|
16081
|
-
|
|
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);
|
|
16082
16284
|
process.exit(2);
|
|
16083
16285
|
}
|
|
16084
16286
|
var argv = process.argv.slice(2);
|
|
16287
|
+
if (helpRequest(argv)) usage(void 0, true);
|
|
16085
16288
|
var versionReq = versionRequest(argv);
|
|
16086
16289
|
if (versionReq) {
|
|
16087
16290
|
const version = crewVersion(CREW_HOME);
|
|
@@ -16101,7 +16304,45 @@ var connectEvents = createEmitter(command === "connect" && flag("json"), (l) =>
|
|
|
16101
16304
|
process.stdout.write(l);
|
|
16102
16305
|
});
|
|
16103
16306
|
var connectChoices = parseChoices(argv);
|
|
16104
|
-
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}"`);
|
|
16105
16346
|
function findSyncRoute(c, name) {
|
|
16106
16347
|
if (!name && c.routes.length > 1) {
|
|
16107
16348
|
throw new ConfigError(`this ship has ${c.routes.length} routes \u2014 name one: ${c.routes.map((r) => r.route).join(", ")}`);
|
|
@@ -16191,6 +16432,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
16191
16432
|
let cfg;
|
|
16192
16433
|
let firstRun;
|
|
16193
16434
|
let firstRunFromFlags = false;
|
|
16435
|
+
let agentBinSearched;
|
|
16194
16436
|
try {
|
|
16195
16437
|
const connectIsTTY = !!(process.stdin.isTTY && process.stdout.isTTY);
|
|
16196
16438
|
const wizardFlags = {
|
|
@@ -16200,7 +16442,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
16200
16442
|
hostPassengers: flag("host-passengers"),
|
|
16201
16443
|
noWizard: flag("no-wizard")
|
|
16202
16444
|
};
|
|
16203
|
-
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 })) {
|
|
16204
16446
|
const flagged = flagWizardAnswers(wizardFlags, {
|
|
16205
16447
|
hostname: hostname2(),
|
|
16206
16448
|
platform: hostPlatform(),
|
|
@@ -16217,8 +16459,9 @@ if (command === "passenger-sync-daemon") {
|
|
|
16217
16459
|
const configPath = firstRunConfigPath();
|
|
16218
16460
|
firstRun = { answers: flagged.answers, configPath };
|
|
16219
16461
|
firstRunFromFlags = true;
|
|
16462
|
+
agentBinSearched = flagged.agentBinSearched;
|
|
16220
16463
|
cfg = loadConfig(CREW_HOME, configPath, { text: renderShipBlock(flagged.answers), allowNoRoutes: true });
|
|
16221
|
-
} else if (command === "connect" && shouldRunWizard({
|
|
16464
|
+
} else if (command === "connect" && !flag("sign-out") && shouldRunWizard({
|
|
16222
16465
|
isTTY: connectIsTTY,
|
|
16223
16466
|
crewHome: CREW_HOME
|
|
16224
16467
|
})) {
|
|
@@ -16239,7 +16482,13 @@ if (command === "passenger-sync-daemon") {
|
|
|
16239
16482
|
firstRun = { answers, configPath };
|
|
16240
16483
|
cfg = loadConfig(CREW_HOME, configPath, { text: renderShipBlock(answers), allowNoRoutes: true });
|
|
16241
16484
|
} else {
|
|
16242
|
-
|
|
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
|
+
}
|
|
16243
16492
|
}
|
|
16244
16493
|
} catch (e) {
|
|
16245
16494
|
if (e instanceof ConfigError) {
|
|
@@ -16473,7 +16722,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
16473
16722
|
}
|
|
16474
16723
|
if (host === "linux" && hasSystemd) {
|
|
16475
16724
|
const label = labelFor(CREW_HOME, "run");
|
|
16476
|
-
const staleTimer =
|
|
16725
|
+
const staleTimer = join35(homedir9(), ".config", "systemd", "user", `${label}.timer`);
|
|
16477
16726
|
if (existsSync33(staleTimer)) {
|
|
16478
16727
|
process.stdout.write(
|
|
16479
16728
|
`removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
|
|
@@ -16721,7 +16970,10 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16721
16970
|
}
|
|
16722
16971
|
if (!dryRun) {
|
|
16723
16972
|
const holderLabel = `${cfg.ship.name}:${process.pid}`;
|
|
16724
|
-
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
|
+
});
|
|
16725
16977
|
if (!got.ok && got.reason === "missing") {
|
|
16726
16978
|
const detail = `no Locks row for ${scope} \u2014 run \`crew connect ${c.route}\` to provision it`;
|
|
16727
16979
|
remit.warn(`release for ${scope} refused: ${detail}`, { step: "release" });
|
|
@@ -17536,7 +17788,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
17536
17788
|
process.exit(1);
|
|
17537
17789
|
}
|
|
17538
17790
|
requireArmed("run the daemon loop");
|
|
17539
|
-
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")];
|
|
17540
17792
|
const configPaths = daemonConfigPaths(cfg);
|
|
17541
17793
|
watchPaths.push(...configPaths);
|
|
17542
17794
|
noteDaemonStarted(cfg.ship.stateDir);
|
|
@@ -17880,7 +18132,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
17880
18132
|
if (sub === "list" && !routeName) usage2();
|
|
17881
18133
|
if (sub === "add") {
|
|
17882
18134
|
try {
|
|
17883
|
-
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()))));
|
|
17884
18136
|
routeName = a.route;
|
|
17885
18137
|
addPath = a.path;
|
|
17886
18138
|
addName = a.name;
|
|
@@ -17982,7 +18234,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
17982
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}`);
|
|
17983
18235
|
break;
|
|
17984
18236
|
}
|
|
17985
|
-
const target = resolvePath(addPath.replace(/^~(?=\/|$)/,
|
|
18237
|
+
const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir9()));
|
|
17986
18238
|
const top = existsSync33(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
|
|
17987
18239
|
if (!top) throw new ReposError(`${target} is not a git checkout`);
|
|
17988
18240
|
const origin = originOf(top);
|
|
@@ -18134,6 +18386,29 @@ ${reconstruction.prompt}
|
|
|
18134
18386
|
}
|
|
18135
18387
|
case "connect": {
|
|
18136
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
|
+
}
|
|
18137
18412
|
const canAsk = !!(process.stdin.isTTY && process.stdout.isTTY) && !connectEvents.json;
|
|
18138
18413
|
const stopForChoice = (key, message, options) => {
|
|
18139
18414
|
connectEvents.emit("choice", message, choiceData(key, options));
|
|
@@ -18151,6 +18426,7 @@ ${reconstruction.prompt}
|
|
|
18151
18426
|
const project = argProject ?? value("project") ?? connectChoices.project;
|
|
18152
18427
|
const baseUrl = value("base-url") ?? cfg.routes[0]?.baseUrl ?? DEFAULT_BASE_URL;
|
|
18153
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";
|
|
18154
18430
|
let store;
|
|
18155
18431
|
try {
|
|
18156
18432
|
store = getSessionStore3();
|
|
@@ -18160,11 +18436,11 @@ ${reconstruction.prompt}
|
|
|
18160
18436
|
let keychainBacked = false;
|
|
18161
18437
|
if (!apiKey && workspace && store) {
|
|
18162
18438
|
try {
|
|
18163
|
-
const existing = await store.get(
|
|
18439
|
+
const existing = await store.get(hostFromUrl4(baseUrl), workspace);
|
|
18164
18440
|
if (existing?.apiKey) {
|
|
18165
18441
|
apiKey = existing.apiKey;
|
|
18166
18442
|
keychainBacked = true;
|
|
18167
|
-
process.stderr.write(`Reusing the existing keychain session for ${
|
|
18443
|
+
process.stderr.write(`Reusing the existing keychain session for ${hostFromUrl4(baseUrl)}/${workspace}.
|
|
18168
18444
|
`);
|
|
18169
18445
|
}
|
|
18170
18446
|
} catch {
|
|
@@ -18175,7 +18451,7 @@ ${reconstruction.prompt}
|
|
|
18175
18451
|
const resumed = await resumeKeychainSession({
|
|
18176
18452
|
store,
|
|
18177
18453
|
stateDir: cfg.ship.stateDir,
|
|
18178
|
-
host:
|
|
18454
|
+
host: hostFromUrl4(baseUrl),
|
|
18179
18455
|
verify: async (k) => {
|
|
18180
18456
|
try {
|
|
18181
18457
|
await get({ baseUrl, apiKey: k, userAgent: cfg.ship.userAgent }, "/auth/me");
|
|
@@ -18190,7 +18466,7 @@ ${reconstruction.prompt}
|
|
|
18190
18466
|
workspace = resumed.workspace;
|
|
18191
18467
|
keychainBacked = true;
|
|
18192
18468
|
signInSource = "keychain";
|
|
18193
|
-
process.stderr.write(`Reusing the existing keychain session for ${
|
|
18469
|
+
process.stderr.write(`Reusing the existing keychain session for ${hostFromUrl4(baseUrl)}/${workspace}.
|
|
18194
18470
|
`);
|
|
18195
18471
|
}
|
|
18196
18472
|
}
|
|
@@ -18247,13 +18523,14 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
18247
18523
|
connectEvents.emit("signed_in", `signed in as ${result.identity.email}`, {
|
|
18248
18524
|
identity: { id: result.identity.id, email: result.identity.email },
|
|
18249
18525
|
workspaces: [{ slug: result.workspace.slug, name: result.workspace.name }],
|
|
18250
|
-
session: "device"
|
|
18526
|
+
session: "device",
|
|
18527
|
+
host: hostFromUrl4(baseUrl)
|
|
18251
18528
|
});
|
|
18252
18529
|
const deviceWorkspace = "path" in result.workspace && typeof result.workspace.path === "string" ? result.workspace.path : result.workspace.slug;
|
|
18253
18530
|
if (!workspace) workspace = deviceWorkspace;
|
|
18254
18531
|
if (store) {
|
|
18255
18532
|
try {
|
|
18256
|
-
await store.set(
|
|
18533
|
+
await store.set(hostFromUrl4(baseUrl), deviceWorkspace, {
|
|
18257
18534
|
apiKey: result.apiKey.key,
|
|
18258
18535
|
shipId: getOrCreateShipId(),
|
|
18259
18536
|
workspaceId: result.workspace.id,
|
|
@@ -18261,9 +18538,9 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
18261
18538
|
createdAt: result.apiKey.createdAt
|
|
18262
18539
|
});
|
|
18263
18540
|
keychainBacked = true;
|
|
18264
|
-
rememberSession(cfg.ship.stateDir,
|
|
18541
|
+
rememberSession(cfg.ship.stateDir, hostFromUrl4(baseUrl), deviceWorkspace);
|
|
18265
18542
|
process.stderr.write(
|
|
18266
|
-
`API key stored in the OS keychain for ${
|
|
18543
|
+
`API key stored in the OS keychain for ${hostFromUrl4(baseUrl)}/${deviceWorkspace}.
|
|
18267
18544
|
`
|
|
18268
18545
|
);
|
|
18269
18546
|
} catch (e) {
|
|
@@ -18308,7 +18585,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
18308
18585
|
connectEvents.emit("signed_in", `key belongs to ${found.meEmail ?? found.meId}`, {
|
|
18309
18586
|
identity: { id: found.meId, email: found.meEmail, name: found.meName },
|
|
18310
18587
|
workspaces: [{ slug: found.workspaceSlug, name: found.workspaceName, role: found.role }],
|
|
18311
|
-
session: signInSource
|
|
18588
|
+
session: signInSource,
|
|
18589
|
+
host: hostFromUrl4(baseUrl)
|
|
18312
18590
|
});
|
|
18313
18591
|
}
|
|
18314
18592
|
connectEvents.emit("workspace_resolved", `workspace ${found.workspaceSlug}`, {
|
|
@@ -18383,8 +18661,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
18383
18661
|
}
|
|
18384
18662
|
}
|
|
18385
18663
|
let operator = found.operator;
|
|
18386
|
-
if (!operator && found.
|
|
18387
|
-
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 });
|
|
18388
18666
|
if (picked === NEW_OPERATOR_ROW) {
|
|
18389
18667
|
await createOperatorRow({ ...authOpts, dryRun }, found);
|
|
18390
18668
|
operator = found.operator;
|
|
@@ -18403,7 +18681,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
18403
18681
|
}
|
|
18404
18682
|
}
|
|
18405
18683
|
if (!operator) operator = previous?.operator;
|
|
18406
|
-
if (connectEvents.json && !operator && found.
|
|
18684
|
+
if (connectEvents.json && !operator && found.candidates.length > 0) {
|
|
18407
18685
|
const answer = connectChoices.operator;
|
|
18408
18686
|
if (answer === "new") {
|
|
18409
18687
|
await createOperatorRow({ ...authOpts, dryRun }, found);
|
|
@@ -18418,27 +18696,36 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
18418
18696
|
found.operatorAction = "linked";
|
|
18419
18697
|
}
|
|
18420
18698
|
} else {
|
|
18421
|
-
const mine = found.
|
|
18699
|
+
const mine = found.candidates.filter((h) => !h.user_id || h.user_id === found.meId);
|
|
18422
18700
|
stopForChoice("operator", "Which Crew row is you?", [
|
|
18423
|
-
...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 } })),
|
|
18424
18702
|
...found.meId ? [{ value: "new", label: `Create a new Crew row for ${found.meName ?? found.meEmail ?? "me"}` }] : []
|
|
18425
18703
|
]);
|
|
18426
18704
|
}
|
|
18427
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
|
+
}
|
|
18428
18711
|
if (operator) {
|
|
18429
|
-
|
|
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}`, {
|
|
18430
18715
|
id: operator,
|
|
18431
|
-
action: found.operatorAction ?? "previous"
|
|
18716
|
+
action: found.operatorAction ?? "previous",
|
|
18717
|
+
...opRow?.name ? { name: opRow.name } : {},
|
|
18718
|
+
...opEmail ? { email: opEmail } : {}
|
|
18432
18719
|
});
|
|
18433
18720
|
}
|
|
18434
18721
|
if (!operator && !hadPreviousFile) {
|
|
18435
18722
|
process.stderr.write(
|
|
18436
|
-
`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.
|
|
18437
18724
|
Re-run from a real terminal to pick one, or pass --key for a key whose own
|
|
18438
18725
|
identity email matches exactly one Crew row.
|
|
18439
18726
|
`
|
|
18440
18727
|
);
|
|
18441
|
-
stopWithProblem(`cannot determine the operator \u2014 ${found.
|
|
18728
|
+
stopWithProblem(`cannot determine the operator \u2014 ${found.candidates.length} Crew row(s) could be it`, "operator");
|
|
18442
18729
|
}
|
|
18443
18730
|
const alreadyReviewed = new Set(previous?.reviewedStatuses ?? []);
|
|
18444
18731
|
const stillUnrecognized = (found.unrecognizedStatuses ?? []).filter((s) => !alreadyReviewed.has(s.value));
|
|
@@ -18533,12 +18820,19 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
18533
18820
|
role: sr.role,
|
|
18534
18821
|
id: sr.id,
|
|
18535
18822
|
action: sr.action,
|
|
18823
|
+
...sr.shipId ? { ship_id: sr.shipId } : {},
|
|
18536
18824
|
key: minted?.action ?? "none",
|
|
18537
18825
|
...minted?.detail ? { detail: minted.detail } : {}
|
|
18538
18826
|
});
|
|
18539
18827
|
}
|
|
18540
18828
|
let apiKeyFile;
|
|
18541
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
|
+
}
|
|
18542
18836
|
if ((mintedFromDevice || firstRunFromFlags) && !keychainBacked) {
|
|
18543
18837
|
apiKeyFile = apiKeyPathFor(cfg.ship.stateDir, route2);
|
|
18544
18838
|
if (dryRun) {
|
|
@@ -18555,7 +18849,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
18555
18849
|
const keyPersisted = keychainBacked || apiKeyFile !== void 0;
|
|
18556
18850
|
const wizard = firstRun;
|
|
18557
18851
|
const writeFirstRun = wizard !== void 0 && !dryRun && keyPersisted;
|
|
18558
|
-
const routeDir = value("dir") ?? (writeFirstRun ?
|
|
18852
|
+
const routeDir = value("dir") ?? (writeFirstRun ? join35(DEFAULT_REPOS_BASE_PATH, found.workspaceSlug) : void 0);
|
|
18559
18853
|
const routeBlock = renderConnection(
|
|
18560
18854
|
found,
|
|
18561
18855
|
route2,
|
|
@@ -18704,13 +18998,39 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
18704
18998
|
wroteConfig,
|
|
18705
18999
|
operator,
|
|
18706
19000
|
dryRun,
|
|
18707
|
-
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` } : {}
|
|
18708
19004
|
});
|
|
18709
19005
|
};
|
|
18710
19006
|
try {
|
|
18711
19007
|
if (!workspace) {
|
|
18712
19008
|
const workspaces = await listWorkspaces(authOpts);
|
|
18713
|
-
|
|
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) {
|
|
18714
19034
|
process.stderr.write("crew connect: this key's identity has no workspace memberships.\n");
|
|
18715
19035
|
stopWithProblem("this key's identity has no workspace memberships", "workspace");
|
|
18716
19036
|
} else if (workspaces.length === 1) {
|
|
@@ -19186,11 +19506,20 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
19186
19506
|
if (!resetStore) return { stored: false, clear: async () => {
|
|
19187
19507
|
} };
|
|
19188
19508
|
const store = resetStore;
|
|
19189
|
-
const h =
|
|
19509
|
+
const h = hostFromUrl4(r.baseUrl);
|
|
19190
19510
|
const [slug] = splitRoute(r.route);
|
|
19191
19511
|
const session = await store.get(h, slug);
|
|
19192
19512
|
return { stored: Boolean(session?.apiKey), clear: () => store.clear(h, slug) };
|
|
19193
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
|
+
},
|
|
19194
19523
|
inspectCheckout(dir) {
|
|
19195
19524
|
const dirty = status(dir).map((c) => c.path);
|
|
19196
19525
|
const linkedWorktrees = worktrees(dir).map((w) => w.path).filter((p) => realpathSync3(p) !== realpathSync3(dir));
|
|
@@ -19361,12 +19690,12 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
19361
19690
|
`);
|
|
19362
19691
|
}
|
|
19363
19692
|
}
|
|
19364
|
-
if (existsSync33(
|
|
19693
|
+
if (existsSync33(join35(dir, ".git")) && basename11(dir) === SAMPLE_REPO) rmSync15(dir, { recursive: true, force: true });
|
|
19365
19694
|
say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
|
|
19366
19695
|
break;
|
|
19367
19696
|
}
|
|
19368
19697
|
const projectId = route.resolved.areaId ?? resolveRepoProject(await tracker.projectRows(), value("project"));
|
|
19369
|
-
const fresh = !existsSync33(
|
|
19698
|
+
const fresh = !existsSync33(join35(dir, ".git"));
|
|
19370
19699
|
if (existsSync33(dir) && fresh && readdirSync14(dir).length > 0) fail(`${dir} exists and is not a git checkout \u2014 move it aside`);
|
|
19371
19700
|
if (dryRun) {
|
|
19372
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`);
|
|
@@ -19374,8 +19703,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
19374
19703
|
}
|
|
19375
19704
|
if (fresh) {
|
|
19376
19705
|
mkdirSync20(dir, { recursive: true });
|
|
19377
|
-
writeFileSync21(
|
|
19378
|
-
writeFileSync21(
|
|
19706
|
+
writeFileSync21(join35(dir, "README.md"), SAMPLE_README);
|
|
19707
|
+
writeFileSync21(join35(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
|
|
19379
19708
|
const g = (...a) => {
|
|
19380
19709
|
const r = spawnSync6("git", a, { cwd: dir, encoding: "utf8" });
|
|
19381
19710
|
if (r.status !== 0) fail(`git ${a[0]} failed in ${dir}: ${(r.stderr || r.stdout).trim()}`);
|
|
@@ -19825,6 +20154,11 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
19825
20154
|
if (warn) process.stdout.write(` WARNING ${warn}
|
|
19826
20155
|
`);
|
|
19827
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
|
+
}
|
|
19828
20162
|
}
|
|
19829
20163
|
const missingAuthorship = await tracker.missingAuthorshipColumns();
|
|
19830
20164
|
process.stdout.write(
|
|
@@ -20145,7 +20479,7 @@ ${foreign.join("\n")}
|
|
|
20145
20479
|
break;
|
|
20146
20480
|
}
|
|
20147
20481
|
default:
|
|
20148
|
-
usage();
|
|
20482
|
+
usage(`unknown command "${command}"`);
|
|
20149
20483
|
}
|
|
20150
20484
|
lock?.release();
|
|
20151
20485
|
}
|