@tablation/crew 0.60.0 → 0.61.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/dist/cli.js +441 -211
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2332,6 +2332,7 @@ function parseResolved(raw, m, where) {
|
|
|
2332
2332
|
...typeof raw.releaseSeat === "string" && raw.releaseSeat ? { releaseSeat: raw.releaseSeat } : {},
|
|
2333
2333
|
...typeof raw.releaseAgentId === "string" && raw.releaseAgentId ? { releaseAgentId: raw.releaseAgentId } : {},
|
|
2334
2334
|
...typeof raw.role === "string" && raw.role ? { role: raw.role } : {},
|
|
2335
|
+
...Array.isArray(raw.capabilityMisses) && raw.capabilityMisses.length > 0 ? { capabilityMisses: raw.capabilityMisses.filter((x) => typeof x === "string") } : {},
|
|
2335
2336
|
...Object.keys(agentPersonas).length > 0 ? { agentPersonas } : {},
|
|
2336
2337
|
...Object.keys(agentSkills).length > 0 ? { agentSkills } : {}
|
|
2337
2338
|
};
|
|
@@ -5182,10 +5183,11 @@ function doctorSelftestLines(stored, toolchains) {
|
|
|
5182
5183
|
}
|
|
5183
5184
|
|
|
5184
5185
|
// src/cli.ts
|
|
5185
|
-
import { resolve as resolve5, dirname as
|
|
5186
|
+
import { resolve as resolve5, dirname as dirname17, join as join30, basename as basename10 } from "path";
|
|
5186
5187
|
import { fileURLToPath } from "url";
|
|
5187
5188
|
import { hostname as hostname2, homedir as homedir8 } from "os";
|
|
5188
5189
|
import { spawnSync as spawnSync6 } from "child_process";
|
|
5190
|
+
import { parse as parseYaml2 } from "yaml";
|
|
5189
5191
|
|
|
5190
5192
|
// src/argv.ts
|
|
5191
5193
|
var VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
@@ -5760,6 +5762,103 @@ var State = class _State {
|
|
|
5760
5762
|
}
|
|
5761
5763
|
};
|
|
5762
5764
|
|
|
5765
|
+
// src/reset.ts
|
|
5766
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync7, readFileSync as readFileSync13, readdirSync as readdirSync4, rmSync as rmSync7, statSync as statSync5, writeFileSync as writeFileSync8 } from "fs";
|
|
5767
|
+
import { dirname as dirname6, join as join14 } from "path";
|
|
5768
|
+
function clonedCheckoutsFile(stateDir) {
|
|
5769
|
+
return join14(stateDir, "cloned-checkouts.json");
|
|
5770
|
+
}
|
|
5771
|
+
function readClonedCheckouts(stateDir) {
|
|
5772
|
+
try {
|
|
5773
|
+
const parsed = JSON.parse(readFileSync13(clonedCheckoutsFile(stateDir), "utf8"));
|
|
5774
|
+
return Array.isArray(parsed) ? parsed.filter((d) => typeof d === "string") : [];
|
|
5775
|
+
} catch {
|
|
5776
|
+
return [];
|
|
5777
|
+
}
|
|
5778
|
+
}
|
|
5779
|
+
function recordClonedCheckout(stateDir, dir) {
|
|
5780
|
+
const known = readClonedCheckouts(stateDir);
|
|
5781
|
+
if (known.includes(dir)) return;
|
|
5782
|
+
const file = clonedCheckoutsFile(stateDir);
|
|
5783
|
+
mkdirSync7(dirname6(file), { recursive: true });
|
|
5784
|
+
writeFileSync8(file, `${JSON.stringify([...known, dir], null, 2)}
|
|
5785
|
+
`);
|
|
5786
|
+
}
|
|
5787
|
+
var mark = (dryRun) => dryRun ? "would-remove" : "removed";
|
|
5788
|
+
async function runReset(o, fx) {
|
|
5789
|
+
const steps = [];
|
|
5790
|
+
for (const line of await fx.uninstallUnits(o.dryRun)) steps.push({ step: "scheduler", target: line, action: mark(o.dryRun) });
|
|
5791
|
+
const urls = [...new Set(o.routes.map((r) => r.baseUrl))];
|
|
5792
|
+
for (const url of urls) {
|
|
5793
|
+
for (const line of fx.removePair(url, o.dryRun)) steps.push({ step: "pair", target: line, action: mark(o.dryRun) });
|
|
5794
|
+
}
|
|
5795
|
+
for (const route of o.routes) {
|
|
5796
|
+
for (const r of await fx.revokeSeatKeys(route, o.dryRun)) {
|
|
5797
|
+
steps.push({ step: "seat-keys", target: `${route.route}: ${r.line}`, action: r.ok ? mark(o.dryRun) : "failed" });
|
|
5798
|
+
}
|
|
5799
|
+
}
|
|
5800
|
+
for (const route of [...o.routes, ...o.droppedRoutes ?? []]) {
|
|
5801
|
+
try {
|
|
5802
|
+
const cred = await fx.credential(route);
|
|
5803
|
+
if (!cred.stored) {
|
|
5804
|
+
steps.push({ step: "credential", target: route.route, action: "absent", detail: "no workspace credential in the keychain" });
|
|
5805
|
+
continue;
|
|
5806
|
+
}
|
|
5807
|
+
if (!o.dryRun) await cred.clear();
|
|
5808
|
+
steps.push({ step: "credential", target: route.route, action: mark(o.dryRun), detail: "workspace credential in the keychain" });
|
|
5809
|
+
} catch (e) {
|
|
5810
|
+
steps.push({ step: "credential", target: route.route, action: "failed", detail: e.message });
|
|
5811
|
+
}
|
|
5812
|
+
}
|
|
5813
|
+
if (o.removeCheckouts) {
|
|
5814
|
+
for (const dir of readClonedCheckouts(o.stateDir)) steps.push(removeCheckout(dir, o.dryRun, fx));
|
|
5815
|
+
}
|
|
5816
|
+
steps.push(...removeState(o.stateDir, o.dryRun));
|
|
5817
|
+
if (existsSync14(o.configFile)) {
|
|
5818
|
+
if (!o.dryRun) rmSync7(o.configFile, { force: true });
|
|
5819
|
+
steps.push({ step: "config", target: o.configFile, action: mark(o.dryRun) });
|
|
5820
|
+
} else {
|
|
5821
|
+
steps.push({ step: "config", target: o.configFile, action: "absent" });
|
|
5822
|
+
}
|
|
5823
|
+
return steps;
|
|
5824
|
+
}
|
|
5825
|
+
function removeCheckout(dir, dryRun, fx) {
|
|
5826
|
+
if (!existsSync14(dir)) return { step: "checkout", target: dir, action: "absent", detail: "already gone" };
|
|
5827
|
+
const { dirty, linkedWorktrees } = fx.inspectCheckout(dir);
|
|
5828
|
+
if (dirty.length) {
|
|
5829
|
+
return { step: "checkout", target: dir, action: "kept", detail: `has uncommitted changes (${dirty.length} path${dirty.length === 1 ? "" : "s"}: ${dirty.slice(0, 5).join(", ")}${dirty.length > 5 ? ", \u2026" : ""})` };
|
|
5830
|
+
}
|
|
5831
|
+
if (linkedWorktrees.length) {
|
|
5832
|
+
return { step: "checkout", target: dir, action: "kept", detail: `has linked worktrees (${linkedWorktrees.join(", ")}); remove them first` };
|
|
5833
|
+
}
|
|
5834
|
+
if (!dryRun) rmSync7(dir, { recursive: true, force: true });
|
|
5835
|
+
return { step: "checkout", target: dir, action: mark(dryRun) };
|
|
5836
|
+
}
|
|
5837
|
+
function removeState(stateDir, dryRun) {
|
|
5838
|
+
const out = [];
|
|
5839
|
+
const targets = [join14(stateDir, "resolved"), join14(stateDir, "keys"), clonedCheckoutsFile(stateDir), join14(stateDir, "reload-requested"), join14(stateDir, "daemon.started")];
|
|
5840
|
+
try {
|
|
5841
|
+
for (const name of readdirSync4(stateDir)) {
|
|
5842
|
+
if (name.startsWith(".") && statSync5(join14(stateDir, name)).isFile()) targets.push(join14(stateDir, name));
|
|
5843
|
+
}
|
|
5844
|
+
} catch {
|
|
5845
|
+
}
|
|
5846
|
+
for (const t of targets) {
|
|
5847
|
+
if (!existsSync14(t)) continue;
|
|
5848
|
+
if (!dryRun) rmSync7(t, { recursive: true, force: true });
|
|
5849
|
+
out.push({ step: "state", target: t, action: mark(dryRun) });
|
|
5850
|
+
}
|
|
5851
|
+
if (!out.length) out.push({ step: "state", target: stateDir, action: "absent", detail: "nothing to remove" });
|
|
5852
|
+
return out;
|
|
5853
|
+
}
|
|
5854
|
+
function describeResetStep(s) {
|
|
5855
|
+
const verb = { removed: "removed", "would-remove": "would remove", absent: "nothing to remove", kept: "KEPT", failed: "FAILED" }[s.action];
|
|
5856
|
+
return `${s.step}: ${verb}: ${s.target}${s.detail ? ` (${s.detail})` : ""}`;
|
|
5857
|
+
}
|
|
5858
|
+
function resetIncomplete(steps) {
|
|
5859
|
+
return steps.some((s) => s.action === "failed" || s.action === "kept");
|
|
5860
|
+
}
|
|
5861
|
+
|
|
5763
5862
|
// src/tracker.ts
|
|
5764
5863
|
import { hostname } from "os";
|
|
5765
5864
|
import { TablationClient as TablationClient3 } from "@tablation/client";
|
|
@@ -6154,24 +6253,24 @@ function recoveredFailureHeadlines(o, route) {
|
|
|
6154
6253
|
}
|
|
6155
6254
|
|
|
6156
6255
|
// src/ship-attention.ts
|
|
6157
|
-
import { existsSync as
|
|
6256
|
+
import { existsSync as existsSync16, statfsSync } from "fs";
|
|
6158
6257
|
|
|
6159
6258
|
// src/git.ts
|
|
6160
6259
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
6161
6260
|
import {
|
|
6162
|
-
existsSync as
|
|
6163
|
-
mkdirSync as
|
|
6261
|
+
existsSync as existsSync15,
|
|
6262
|
+
mkdirSync as mkdirSync8,
|
|
6164
6263
|
openSync as openSync3,
|
|
6165
6264
|
closeSync as closeSync3,
|
|
6166
6265
|
writeSync,
|
|
6167
|
-
statSync as
|
|
6266
|
+
statSync as statSync6,
|
|
6168
6267
|
unlinkSync as unlinkSync2,
|
|
6169
|
-
readFileSync as
|
|
6170
|
-
writeFileSync as
|
|
6171
|
-
readdirSync as
|
|
6172
|
-
rmSync as
|
|
6268
|
+
readFileSync as readFileSync14,
|
|
6269
|
+
writeFileSync as writeFileSync9,
|
|
6270
|
+
readdirSync as readdirSync5,
|
|
6271
|
+
rmSync as rmSync8
|
|
6173
6272
|
} from "fs";
|
|
6174
|
-
import { dirname as
|
|
6273
|
+
import { dirname as dirname7, basename as basename5, join as join15 } from "path";
|
|
6175
6274
|
var GitError = class extends Error {
|
|
6176
6275
|
};
|
|
6177
6276
|
function streamText(x) {
|
|
@@ -6537,8 +6636,8 @@ function recoverStrandedReleaseCheckouts(cwd) {
|
|
|
6537
6636
|
if (line.startsWith("worktree ")) path = line.slice("worktree ".length);
|
|
6538
6637
|
else if (line.startsWith("branch refs/heads/crew/release-") && path) {
|
|
6539
6638
|
gitOk(cwd, ["worktree", "remove", "--force", path]);
|
|
6540
|
-
const root =
|
|
6541
|
-
if (basename5(root).startsWith("crew-release-"))
|
|
6639
|
+
const root = dirname7(path);
|
|
6640
|
+
if (basename5(root).startsWith("crew-release-")) rmSync8(root, { recursive: true, force: true });
|
|
6542
6641
|
}
|
|
6543
6642
|
}
|
|
6544
6643
|
const stranded = (gitOk(cwd, ["for-each-ref", "--format=%(refname:short)", "refs/heads/crew/release-*"]) ?? "").split("\n").filter(Boolean);
|
|
@@ -6549,18 +6648,18 @@ function sweepStaleReleaseRoots(tmp, now = Date.now(), maxAgeMs = 60 * 60 * 1e3)
|
|
|
6549
6648
|
const removed = [];
|
|
6550
6649
|
let names;
|
|
6551
6650
|
try {
|
|
6552
|
-
names =
|
|
6651
|
+
names = readdirSync5(tmp);
|
|
6553
6652
|
} catch {
|
|
6554
6653
|
return removed;
|
|
6555
6654
|
}
|
|
6556
6655
|
for (const name of names) {
|
|
6557
6656
|
if (!name.startsWith("crew-release-")) continue;
|
|
6558
|
-
const root =
|
|
6657
|
+
const root = join15(tmp, name);
|
|
6559
6658
|
try {
|
|
6560
|
-
const st =
|
|
6659
|
+
const st = statSync6(root);
|
|
6561
6660
|
if (!st.isDirectory() || now - st.mtimeMs < maxAgeMs) continue;
|
|
6562
|
-
if (
|
|
6563
|
-
|
|
6661
|
+
if (existsSync15(join15(root, "checkout"))) continue;
|
|
6662
|
+
rmSync8(root, { recursive: true, force: true });
|
|
6564
6663
|
removed.push(name);
|
|
6565
6664
|
} catch {
|
|
6566
6665
|
}
|
|
@@ -6602,13 +6701,13 @@ function originUrlOf(dir, remoteName) {
|
|
|
6602
6701
|
}
|
|
6603
6702
|
}
|
|
6604
6703
|
function ensureRepoCheckout(dir, remote, opts = {}) {
|
|
6605
|
-
if (
|
|
6704
|
+
if (existsSync15(`${dir}/.git`)) return false;
|
|
6606
6705
|
if (!remote) {
|
|
6607
6706
|
throw new GitError(
|
|
6608
6707
|
`${dir} does not exist and this repo has no discovered remote to clone from \u2014 re-run \`crew connect\`, or check out the repository yourself and point \`repos:\` at it`
|
|
6609
6708
|
);
|
|
6610
6709
|
}
|
|
6611
|
-
|
|
6710
|
+
mkdirSync8(dirname7(dir), { recursive: true });
|
|
6612
6711
|
const url = cloneUrlFor(remote, opts);
|
|
6613
6712
|
try {
|
|
6614
6713
|
execFileSync2("git", ["clone", url, dir], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
@@ -6617,8 +6716,13 @@ function ensureRepoCheckout(dir, remote, opts = {}) {
|
|
|
6617
6716
|
const why = [err.stderr, err.stdout].map(streamText).filter(Boolean).join(" / ");
|
|
6618
6717
|
throw new GitError(`git clone ${url} ${dir}: ${why || err.message.trim()}`);
|
|
6619
6718
|
}
|
|
6719
|
+
cloneRecorder?.(dir);
|
|
6620
6720
|
return true;
|
|
6621
6721
|
}
|
|
6722
|
+
var cloneRecorder;
|
|
6723
|
+
function setCloneRecorder(fn) {
|
|
6724
|
+
cloneRecorder = fn;
|
|
6725
|
+
}
|
|
6622
6726
|
var CHECKOUT_LOCK_STALE_MS = 5 * 60 * 1e3;
|
|
6623
6727
|
function checkoutLockPath(dir) {
|
|
6624
6728
|
return `${dir}.sync.lock`;
|
|
@@ -6628,7 +6732,7 @@ function sleepSync(ms2) {
|
|
|
6628
6732
|
}
|
|
6629
6733
|
function acquireCheckoutLock(dir, timeoutMs = 3e4) {
|
|
6630
6734
|
const lockPath = checkoutLockPath(dir);
|
|
6631
|
-
|
|
6735
|
+
mkdirSync8(dirname7(lockPath), { recursive: true });
|
|
6632
6736
|
const deadline = Date.now() + timeoutMs;
|
|
6633
6737
|
for (; ; ) {
|
|
6634
6738
|
try {
|
|
@@ -6646,7 +6750,7 @@ function acquireCheckoutLock(dir, timeoutMs = 3e4) {
|
|
|
6646
6750
|
const err = e;
|
|
6647
6751
|
if (err.code !== "EEXIST") throw err;
|
|
6648
6752
|
try {
|
|
6649
|
-
if (Date.now() -
|
|
6753
|
+
if (Date.now() - statSync6(lockPath).mtimeMs > CHECKOUT_LOCK_STALE_MS) {
|
|
6650
6754
|
try {
|
|
6651
6755
|
unlinkSync2(lockPath);
|
|
6652
6756
|
} catch {
|
|
@@ -6665,17 +6769,17 @@ function acquireCheckoutLock(dir, timeoutMs = 3e4) {
|
|
|
6665
6769
|
function syncPassengerCheckout(dir, githubRemote, gitRemoteName, base, clone = {}) {
|
|
6666
6770
|
const release = acquireCheckoutLock(dir);
|
|
6667
6771
|
try {
|
|
6668
|
-
if (!
|
|
6772
|
+
if (!existsSync15(`${dir}/.git`) && githubRemote) {
|
|
6669
6773
|
const url = cloneUrlFor(githubRemote, clone);
|
|
6670
6774
|
const marker = cloneFailureMarker(dir);
|
|
6671
|
-
if (
|
|
6672
|
-
return { action: "clone-failed", detail:
|
|
6775
|
+
if (existsSync15(marker) && readFileSync14(marker, "utf8").split("\n")[0] === url) {
|
|
6776
|
+
return { action: "clone-failed", detail: readFileSync14(marker, "utf8").split("\n").slice(1).join("\n"), repeat: true };
|
|
6673
6777
|
}
|
|
6674
6778
|
try {
|
|
6675
6779
|
ensureRepoCheckout(dir, githubRemote, clone);
|
|
6676
6780
|
} catch (e) {
|
|
6677
6781
|
if (!(e instanceof GitError)) throw e;
|
|
6678
|
-
|
|
6782
|
+
writeFileSync9(marker, `${url}
|
|
6679
6783
|
${e.message}`);
|
|
6680
6784
|
return { action: "clone-failed", detail: e.message, repeat: false };
|
|
6681
6785
|
}
|
|
@@ -6873,7 +6977,7 @@ function noteHeldWorktree(state, h, nowMs = Date.now()) {
|
|
|
6873
6977
|
function reconcileHeldWorktrees(state, repo, log, stillRefused) {
|
|
6874
6978
|
for (const i of state.shipAttention().previous()) {
|
|
6875
6979
|
if (i.kind !== "worktree_held" || !i.held || i.held.repo !== repo) continue;
|
|
6876
|
-
const gone = !
|
|
6980
|
+
const gone = !existsSync16(i.held.path);
|
|
6877
6981
|
if (!gone && !(stillRefused && !stillRefused.has(i.held.path))) continue;
|
|
6878
6982
|
clearShipAttention(state, i.key);
|
|
6879
6983
|
log.emit(
|
|
@@ -8587,8 +8691,8 @@ function materializeBranchForTicket(dir, cfg, t, role, onProblem) {
|
|
|
8587
8691
|
}
|
|
8588
8692
|
|
|
8589
8693
|
// src/poll.ts
|
|
8590
|
-
import { writeFileSync as
|
|
8591
|
-
import { join as
|
|
8694
|
+
import { writeFileSync as writeFileSync10 } from "fs";
|
|
8695
|
+
import { join as join16 } from "path";
|
|
8592
8696
|
|
|
8593
8697
|
// src/stalled.ts
|
|
8594
8698
|
var STALLED_AFTER_CYCLES = 30;
|
|
@@ -8877,7 +8981,7 @@ async function decideCycle(o) {
|
|
|
8877
8981
|
}
|
|
8878
8982
|
return decision;
|
|
8879
8983
|
}
|
|
8880
|
-
var digestPath = (stateDir, route, role) =>
|
|
8984
|
+
var digestPath = (stateDir, route, role) => join16(stateDir, `digest-${route}-${role}.md`);
|
|
8881
8985
|
function writeDigest(o, d, role, stateDir) {
|
|
8882
8986
|
const me = o.route.resolved?.seats[role];
|
|
8883
8987
|
if (!me) return false;
|
|
@@ -8941,7 +9045,7 @@ function writeDigest(o, d, role, stateDir) {
|
|
|
8941
9045
|
};
|
|
8942
9046
|
const text = role === "qa" ? qaDigest(input) : buildingDigest(input);
|
|
8943
9047
|
const path = digestPath(stateDir, routeSlug(o.route.route), role);
|
|
8944
|
-
|
|
9048
|
+
writeFileSync10(path, text);
|
|
8945
9049
|
o.emit.emit(`queue digest written for ${role} (${Buffer.byteLength(text)} bytes)`, {
|
|
8946
9050
|
data: { role, bytes: Buffer.byteLength(text) }
|
|
8947
9051
|
});
|
|
@@ -9318,13 +9422,13 @@ async function applyStrandedVerified(writer, s, contract, operatorId, memberId,
|
|
|
9318
9422
|
}
|
|
9319
9423
|
|
|
9320
9424
|
// src/run-worktree.ts
|
|
9321
|
-
import { existsSync as
|
|
9322
|
-
import { dirname as
|
|
9425
|
+
import { existsSync as existsSync18 } from "fs";
|
|
9426
|
+
import { dirname as dirname8, join as join18 } from "path";
|
|
9323
9427
|
|
|
9324
9428
|
// src/ports.ts
|
|
9325
9429
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
9326
|
-
import { existsSync as
|
|
9327
|
-
import { join as
|
|
9430
|
+
import { existsSync as existsSync17 } from "fs";
|
|
9431
|
+
import { join as join17 } from "path";
|
|
9328
9432
|
var run = (cmd, args) => {
|
|
9329
9433
|
try {
|
|
9330
9434
|
return execFileSync3(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
@@ -9369,7 +9473,7 @@ function ticketForPort(port, s = DEFAULT_PORTS) {
|
|
|
9369
9473
|
}
|
|
9370
9474
|
function worktreeExists(parent, prefix, n) {
|
|
9371
9475
|
for (const name of [`${prefix}${n}`, `${prefix}${String(n).padStart(3, "0")}`]) {
|
|
9372
|
-
if (
|
|
9476
|
+
if (existsSync17(join17(parent, name, ".git"))) return true;
|
|
9373
9477
|
}
|
|
9374
9478
|
return false;
|
|
9375
9479
|
}
|
|
@@ -9445,7 +9549,7 @@ function planRunWorktree(dir, cfg, t, role, opts = {}) {
|
|
|
9445
9549
|
const existing = worktreeForNumber(dir, num);
|
|
9446
9550
|
const located = locateBranchForTicket(dir, cfg, t, role);
|
|
9447
9551
|
const baseRef = hasRemote ? `${remote}/${base}` : base;
|
|
9448
|
-
if (existing && !existing.prunable &&
|
|
9552
|
+
if (existing && !existing.prunable && existsSync18(existing.path)) {
|
|
9449
9553
|
const branch = existing.branch;
|
|
9450
9554
|
if (status(existing.path).length > 0) {
|
|
9451
9555
|
return { action: opts.resumeInterrupted ? "resume-dirty" : "leave-dirty", path: existing.path, branch };
|
|
@@ -9461,7 +9565,7 @@ function planRunWorktree(dir, cfg, t, role, opts = {}) {
|
|
|
9461
9565
|
if (countCommits(existing.path, `${baseRef}..HEAD`) > 0) return { action: "leave-unpushed", path: existing.path, branch };
|
|
9462
9566
|
return { action: "recut", path: existing.path, branch, base: baseRef };
|
|
9463
9567
|
}
|
|
9464
|
-
const path =
|
|
9568
|
+
const path = join18(dirname8(dir), worktreeForTicket(dir, cfg, t, role));
|
|
9465
9569
|
if (located?.where === "remote" || located && hasRemote && remoteBranchExists(dir, remote, located.branch)) {
|
|
9466
9570
|
return { action: "cut-remote", path, branch: located.branch };
|
|
9467
9571
|
}
|
|
@@ -9510,7 +9614,7 @@ function worktreeHoldsUnpushed(path, remote) {
|
|
|
9510
9614
|
}
|
|
9511
9615
|
async function finishRunWorktree(dir, cfg, branch) {
|
|
9512
9616
|
const wt = worktrees(dir).find((w) => w.branch === branch && !w.prunable && w.path !== dir);
|
|
9513
|
-
if (!wt || !
|
|
9617
|
+
if (!wt || !existsSync18(wt.path)) return { kind: "none" };
|
|
9514
9618
|
if (cfg.hooks.handoff) return { kind: "kept-handoff", path: wt.path };
|
|
9515
9619
|
if (status(wt.path).length > 0) return { kind: "kept-dirty", path: wt.path };
|
|
9516
9620
|
if (remoteConfigured(dir, cfg.branch.remote) && worktreeHoldsUnpushed(wt.path, cfg.branch.remote)) {
|
|
@@ -9522,7 +9626,7 @@ async function finishRunWorktree(dir, cfg, branch) {
|
|
|
9522
9626
|
if (pids.length) await killGently(pids);
|
|
9523
9627
|
}
|
|
9524
9628
|
gitOk(dir, ["worktree", "remove", wt.path]);
|
|
9525
|
-
return
|
|
9629
|
+
return existsSync18(wt.path) ? { kind: "kept-dirty", path: wt.path } : { kind: "removed", path: wt.path };
|
|
9526
9630
|
}
|
|
9527
9631
|
|
|
9528
9632
|
// src/handoff-guard.ts
|
|
@@ -9783,13 +9887,13 @@ async function guardRunStrays(route, ship, tracker, role, workingId, since2, log
|
|
|
9783
9887
|
}
|
|
9784
9888
|
|
|
9785
9889
|
// src/connect-wizard.ts
|
|
9786
|
-
import { delimiter, dirname as
|
|
9787
|
-
import { accessSync as accessSync2, constants as constants2, existsSync as
|
|
9890
|
+
import { delimiter, dirname as dirname9, join as join19 } from "path";
|
|
9891
|
+
import { accessSync as accessSync2, constants as constants2, existsSync as existsSync19, mkdirSync as mkdirSync9, writeFileSync as writeFileSync11 } from "fs";
|
|
9788
9892
|
import { stringify } from "yaml";
|
|
9789
9893
|
var HOST_PASSENGERS_DOCS_URL = "https://crew.tablation.dev/host-passengers";
|
|
9790
9894
|
function shouldRunWizard(opts) {
|
|
9791
9895
|
if (!opts.isTTY) return false;
|
|
9792
|
-
const exists = opts.configExists ??
|
|
9896
|
+
const exists = opts.configExists ?? existsSync19;
|
|
9793
9897
|
return !configSearchPath(opts.crewHome).some((p) => exists(p));
|
|
9794
9898
|
}
|
|
9795
9899
|
function firstRunConfigPath() {
|
|
@@ -9806,7 +9910,7 @@ function defaultIsExecutable(p) {
|
|
|
9806
9910
|
function findOnPath2(name, pathEnv, isExecutable = defaultIsExecutable) {
|
|
9807
9911
|
for (const dir of pathEnv.split(delimiter)) {
|
|
9808
9912
|
if (!dir) continue;
|
|
9809
|
-
const candidate =
|
|
9913
|
+
const candidate = join19(dir, name);
|
|
9810
9914
|
if (isExecutable(candidate)) return candidate;
|
|
9811
9915
|
}
|
|
9812
9916
|
return void 0;
|
|
@@ -9832,9 +9936,9 @@ async function runWizard(p, env) {
|
|
|
9832
9936
|
if (entered && isExec(entered)) agentBin = entered;
|
|
9833
9937
|
else p.say(entered ? `${entered} is not an executable file.` : "A path is required.");
|
|
9834
9938
|
}
|
|
9835
|
-
const nodeDir =
|
|
9939
|
+
const nodeDir = dirname9(env.nodePath);
|
|
9836
9940
|
const pnpmPath = findOnPath2("pnpm", env.pathEnv, isExec);
|
|
9837
|
-
const baseDirs = [nodeDir, pnpmPath ?
|
|
9941
|
+
const baseDirs = [nodeDir, pnpmPath ? dirname9(pnpmPath) : void 0];
|
|
9838
9942
|
p.say(
|
|
9839
9943
|
`
|
|
9840
9944
|
Host Passengers lets this machine run the agent sessions that other people's
|
|
@@ -9845,7 +9949,7 @@ Tablation workspaces ask for, in a container. More: ${HOST_PASSENGERS_DOCS_URL}`
|
|
|
9845
9949
|
p.say("Docker is not available here, so nothing will be hosted until it is. Your answer is recorded anyway.");
|
|
9846
9950
|
}
|
|
9847
9951
|
const dockerPath = hostPassengers ? findOnPath2("docker", env.pathEnv, isExec) : void 0;
|
|
9848
|
-
const extraPath = deriveExtraPath([...baseDirs, dockerPath ?
|
|
9952
|
+
const extraPath = deriveExtraPath([...baseDirs, dockerPath ? dirname9(dockerPath) : void 0]);
|
|
9849
9953
|
if (extraPath) {
|
|
9850
9954
|
p.say(`Hooks will get ${extraPath} on their PATH (where node, pnpm${dockerPath ? " and docker" : ""} live).`);
|
|
9851
9955
|
}
|
|
@@ -9867,9 +9971,9 @@ routes:
|
|
|
9867
9971
|
${routeBlock}`;
|
|
9868
9972
|
}
|
|
9869
9973
|
function writeNewConfig(path, text) {
|
|
9870
|
-
|
|
9974
|
+
mkdirSync9(dirname9(path), { recursive: true });
|
|
9871
9975
|
try {
|
|
9872
|
-
|
|
9976
|
+
writeFileSync11(path, text, { mode: 384, flag: "wx" });
|
|
9873
9977
|
return true;
|
|
9874
9978
|
} catch (e) {
|
|
9875
9979
|
if (e.code === "EEXIST") return false;
|
|
@@ -9902,7 +10006,7 @@ Wrote ${o.configPath}.
|
|
|
9902
10006
|
|
|
9903
10007
|
// src/connect-json.ts
|
|
9904
10008
|
import { accessSync as accessSync3, constants as constants3 } from "fs";
|
|
9905
|
-
import { dirname as
|
|
10009
|
+
import { dirname as dirname10 } from "path";
|
|
9906
10010
|
var CHOICE_EXIT_CODE = 3;
|
|
9907
10011
|
function createEmitter(json, write) {
|
|
9908
10012
|
return {
|
|
@@ -9950,7 +10054,7 @@ function flagWizardAnswers(f, env) {
|
|
|
9950
10054
|
if (f.agentBin && !isExec(f.agentBin)) return { error: `--agent-bin ${f.agentBin} is not an executable file` };
|
|
9951
10055
|
const pnpmPath = findOnPath2("pnpm", env.pathEnv, isExec);
|
|
9952
10056
|
const dockerPath = f.hostPassengers ? findOnPath2("docker", env.pathEnv, isExec) : void 0;
|
|
9953
|
-
const extraPath = f.extraPath ?? deriveExtraPath([
|
|
10057
|
+
const extraPath = f.extraPath ?? deriveExtraPath([dirname10(env.nodePath), pnpmPath ? dirname10(pnpmPath) : void 0, dockerPath ? dirname10(dockerPath) : void 0]);
|
|
9954
10058
|
return {
|
|
9955
10059
|
answers: {
|
|
9956
10060
|
name: f.shipName?.trim() || env.hostname,
|
|
@@ -9963,8 +10067,8 @@ function flagWizardAnswers(f, env) {
|
|
|
9963
10067
|
}
|
|
9964
10068
|
|
|
9965
10069
|
// src/release-run.ts
|
|
9966
|
-
import { readFileSync as
|
|
9967
|
-
import { dirname as
|
|
10070
|
+
import { readFileSync as readFileSync15, writeFileSync as writeFileSync12, existsSync as existsSync20, copyFileSync, mkdirSync as mkdirSync10, mkdtempSync as mkdtempSync2, rmSync as rmSync9 } from "fs";
|
|
10071
|
+
import { dirname as dirname11, join as join20 } from "path";
|
|
9968
10072
|
import { tmpdir as tmpdir3 } from "os";
|
|
9969
10073
|
|
|
9970
10074
|
// src/release.ts
|
|
@@ -10652,13 +10756,13 @@ async function bump(o, size) {
|
|
|
10652
10756
|
}
|
|
10653
10757
|
return printed;
|
|
10654
10758
|
}
|
|
10655
|
-
const files = o.repo.release.versionFiles.map((f) =>
|
|
10759
|
+
const files = o.repo.release.versionFiles.map((f) => join20(o.cwd, f));
|
|
10656
10760
|
const first = files[0];
|
|
10657
|
-
if (!
|
|
10761
|
+
if (!existsSync20(first)) {
|
|
10658
10762
|
o.emit.error(`no ${o.repo.release.versionFiles[0]} to read a version from`);
|
|
10659
10763
|
return null;
|
|
10660
10764
|
}
|
|
10661
|
-
const current = JSON.parse(
|
|
10765
|
+
const current = JSON.parse(readFileSync15(first, "utf8")).version;
|
|
10662
10766
|
if (!current) {
|
|
10663
10767
|
o.emit.error(`no version field in ${o.repo.release.versionFiles[0]}`);
|
|
10664
10768
|
return null;
|
|
@@ -10671,27 +10775,27 @@ async function bump(o, size) {
|
|
|
10671
10775
|
return version;
|
|
10672
10776
|
}
|
|
10673
10777
|
for (const f of files) {
|
|
10674
|
-
const src =
|
|
10778
|
+
const src = readFileSync15(f, "utf8");
|
|
10675
10779
|
const out = src.replace(/("version"\s*:\s*")[^"]*(")/, `$1${version}$2`);
|
|
10676
10780
|
if (out === src) {
|
|
10677
10781
|
o.emit.error(`no version field in ${f}`);
|
|
10678
10782
|
return null;
|
|
10679
10783
|
}
|
|
10680
|
-
|
|
10784
|
+
writeFileSync12(f, out);
|
|
10681
10785
|
}
|
|
10682
10786
|
return version;
|
|
10683
10787
|
}
|
|
10684
10788
|
function writeChangelog(o, version, entries) {
|
|
10685
10789
|
const file = o.repo.release.changelog;
|
|
10686
10790
|
if (!file || entries.length === 0) return;
|
|
10687
|
-
const path =
|
|
10791
|
+
const path = join20(o.cwd, file);
|
|
10688
10792
|
const section = renderChangelogSection(version, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), entries);
|
|
10689
10793
|
if (o.dryRun) {
|
|
10690
10794
|
o.emit.emit(`would prepend ${entries.length} entr(ies) to ${file}`);
|
|
10691
10795
|
return;
|
|
10692
10796
|
}
|
|
10693
|
-
const existing =
|
|
10694
|
-
|
|
10797
|
+
const existing = existsSync20(path) ? readFileSync15(path, "utf8") : "# Changelog\n";
|
|
10798
|
+
writeFileSync12(path, insertChangelogSection(existing, section));
|
|
10695
10799
|
}
|
|
10696
10800
|
async function confirm(o, expected) {
|
|
10697
10801
|
if (!o.repo.hooks.released) return void 0;
|
|
@@ -10774,7 +10878,7 @@ function closeReleaseCheckout(cwd, root, dir, branch, run2 = gitOk) {
|
|
|
10774
10878
|
run2(cwd, ["worktree", "remove", "--force", dir]);
|
|
10775
10879
|
} finally {
|
|
10776
10880
|
try {
|
|
10777
|
-
|
|
10881
|
+
rmSync9(root, { recursive: true, force: true });
|
|
10778
10882
|
} finally {
|
|
10779
10883
|
run2(cwd, ["worktree", "prune"]);
|
|
10780
10884
|
run2(cwd, ["branch", "-D", branch]);
|
|
@@ -10784,8 +10888,8 @@ function closeReleaseCheckout(cwd, root, dir, branch, run2 = gitOk) {
|
|
|
10784
10888
|
function openReleaseCheckout(o) {
|
|
10785
10889
|
const { remote, base } = o.repo.branch;
|
|
10786
10890
|
const branch = `crew/release-${Date.now()}`;
|
|
10787
|
-
const root = mkdtempSync2(
|
|
10788
|
-
const dir =
|
|
10891
|
+
const root = mkdtempSync2(join20(tmpdir3(), "crew-release-"));
|
|
10892
|
+
const dir = join20(root, "checkout");
|
|
10789
10893
|
o.emit.emit(`release checkout: ${dir}`, { step: "release" });
|
|
10790
10894
|
const close = () => closeReleaseCheckout(o.cwd, root, dir, branch);
|
|
10791
10895
|
try {
|
|
@@ -10795,10 +10899,10 @@ function openReleaseCheckout(o) {
|
|
|
10795
10899
|
throw e;
|
|
10796
10900
|
}
|
|
10797
10901
|
for (const f of o.repo.worktrees.copy) {
|
|
10798
|
-
const src =
|
|
10799
|
-
if (!
|
|
10800
|
-
|
|
10801
|
-
copyFileSync(src,
|
|
10902
|
+
const src = join20(o.cwd, f);
|
|
10903
|
+
if (!existsSync20(src)) continue;
|
|
10904
|
+
mkdirSync10(dirname11(join20(dir, f)), { recursive: true });
|
|
10905
|
+
copyFileSync(src, join20(dir, f));
|
|
10802
10906
|
}
|
|
10803
10907
|
return { dir, branch, close };
|
|
10804
10908
|
}
|
|
@@ -11466,7 +11570,7 @@ function missingReleaserWarning(repo, release, ships, now = Date.now()) {
|
|
|
11466
11570
|
}
|
|
11467
11571
|
|
|
11468
11572
|
// src/watch.ts
|
|
11469
|
-
import { watch as watchFile, existsSync as
|
|
11573
|
+
import { watch as watchFile, existsSync as existsSync21, statSync as statSync7, openSync as openSync4, readSync as readSync2, closeSync as closeSync4 } from "fs";
|
|
11470
11574
|
var E = "\x1B[";
|
|
11471
11575
|
var dim = (s) => `${E}2m${s}${E}0m`;
|
|
11472
11576
|
var bold = (s) => `${E}1m${s}${E}0m`;
|
|
@@ -11567,8 +11671,8 @@ function renderHeaders(states, now = Date.now()) {
|
|
|
11567
11671
|
return states.map((s) => renderHeader(s, now)).join("");
|
|
11568
11672
|
}
|
|
11569
11673
|
function readFrom(file, from) {
|
|
11570
|
-
if (!
|
|
11571
|
-
const size =
|
|
11674
|
+
if (!existsSync21(file)) return { events: [], offset: from };
|
|
11675
|
+
const size = statSync7(file).size;
|
|
11572
11676
|
if (size < from) return readFrom(file, 0);
|
|
11573
11677
|
if (size === from) return { events: [], offset: from };
|
|
11574
11678
|
const fd = openSync4(file, "r");
|
|
@@ -12518,9 +12622,54 @@ async function canMintAgentKeys(route, o = {}) {
|
|
|
12518
12622
|
}
|
|
12519
12623
|
}
|
|
12520
12624
|
|
|
12625
|
+
// src/capability-check.ts
|
|
12626
|
+
var WRITE_LEVELS = /* @__PURE__ */ new Set(["WRITE", "DELETE"]);
|
|
12627
|
+
async function canWriteTable(o, modelId) {
|
|
12628
|
+
try {
|
|
12629
|
+
const { access } = await get(o, `/data-models/${modelId}/access`);
|
|
12630
|
+
return access === void 0 ? void 0 : WRITE_LEVELS.has(access);
|
|
12631
|
+
} catch {
|
|
12632
|
+
return void 0;
|
|
12633
|
+
}
|
|
12634
|
+
}
|
|
12635
|
+
async function projectCapabilities(o, projectId) {
|
|
12636
|
+
if (!projectId) return void 0;
|
|
12637
|
+
try {
|
|
12638
|
+
return await get(o, `/projects/${projectId}/capabilities`);
|
|
12639
|
+
} catch {
|
|
12640
|
+
return void 0;
|
|
12641
|
+
}
|
|
12642
|
+
}
|
|
12643
|
+
async function checkCapabilities(o, found) {
|
|
12644
|
+
const admin = isAdminRole(found.role);
|
|
12645
|
+
const table2 = async (key, label, tableName2, modelId, askFor) => {
|
|
12646
|
+
if (!modelId) return { key, label, status: "skipped", note: `no ${tableName2} table in this project` };
|
|
12647
|
+
const ok = await canWriteTable(o, modelId);
|
|
12648
|
+
if (ok === void 0) return { key, label, status: "skipped", note: "could not read access" };
|
|
12649
|
+
return ok ? { key, label, status: "ok" } : { key, label, status: "missing", askFor };
|
|
12650
|
+
};
|
|
12651
|
+
const [crew, ships, repos, caps] = await Promise.all([
|
|
12652
|
+
table2("crew-table", "write the Crew table", "Crew", found.crewModelId ?? found.models.crew, "WRITE access on the Crew table"),
|
|
12653
|
+
table2("ships-table", "write the Ships table", "Ships", found.shipsModelId, "WRITE access on the Ships table"),
|
|
12654
|
+
table2("repos-table", "create Repos rows", "Repos", found.reposModelId, "WRITE access on the Repos table"),
|
|
12655
|
+
projectCapabilities(o, found.projectId)
|
|
12656
|
+
]);
|
|
12657
|
+
const mint = admin || caps?.MINT_AGENT_KEYS ? { key: "mint-agent-keys", label: "mint agent keys", status: "ok" } : caps ? { key: "mint-agent-keys", label: "mint agent keys", status: "missing", askFor: 'the "Mint agent keys" capability' } : { key: "mint-agent-keys", label: "mint agent keys", status: "skipped", note: "could not read capabilities" };
|
|
12658
|
+
const sync = admin ? { key: "sync-personas", label: "sync personas and skills", status: "ok" } : { key: "sync-personas", label: "sync personas and skills", status: "missing", askFor: "the workspace admin role (personas and skills are workspace-level tables)" };
|
|
12659
|
+
return [crew, ships, repos, mint, sync];
|
|
12660
|
+
}
|
|
12661
|
+
function missLine(i) {
|
|
12662
|
+
return `${i.label} \u2014 ask a workspace admin for ${i.askFor ?? "access"}`;
|
|
12663
|
+
}
|
|
12664
|
+
function describeCapability(i) {
|
|
12665
|
+
if (i.status === "ok") return `ok ${i.label}`;
|
|
12666
|
+
if (i.status === "skipped") return `skipped ${i.label} (${i.note ?? "not checked"})`;
|
|
12667
|
+
return `missing ${missLine(i)}`;
|
|
12668
|
+
}
|
|
12669
|
+
|
|
12521
12670
|
// src/agents-sync-run.ts
|
|
12522
|
-
import { existsSync as
|
|
12523
|
-
import { dirname as
|
|
12671
|
+
import { existsSync as existsSync22, mkdirSync as mkdirSync11, readFileSync as readFileSync16, writeFileSync as writeFileSync13 } from "fs";
|
|
12672
|
+
import { dirname as dirname12 } from "path";
|
|
12524
12673
|
async function runAgentsSync(target, o) {
|
|
12525
12674
|
const dryRun = o.dryRun ?? false;
|
|
12526
12675
|
const force = o.force ?? false;
|
|
@@ -12553,12 +12702,12 @@ async function runAgentsSync(target, o) {
|
|
|
12553
12702
|
for (const s of skillResult?.outcomes ?? []) o.say(describeSkillSyncOutcome(s));
|
|
12554
12703
|
if (!dryRun) {
|
|
12555
12704
|
const resolvedPath = resolvedPathFor(o.stateDir, target.route);
|
|
12556
|
-
const raw =
|
|
12705
|
+
const raw = existsSync22(resolvedPath) ? JSON.parse(readFileSync16(resolvedPath, "utf8")) : {};
|
|
12557
12706
|
raw.agentPersonas = personaResult.agentPersonas;
|
|
12558
12707
|
if (skillResult) raw.agentSkills = skillResult.agentSkills;
|
|
12559
12708
|
if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
|
|
12560
|
-
|
|
12561
|
-
|
|
12709
|
+
mkdirSync11(dirname12(resolvedPath), { recursive: true });
|
|
12710
|
+
writeFileSync13(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
12562
12711
|
`);
|
|
12563
12712
|
if (target.resolved) {
|
|
12564
12713
|
target.resolved.agentPersonas = personaResult.agentPersonas;
|
|
@@ -12665,7 +12814,7 @@ import { isMap as isMap2, parseDocument as parseDocument2, YAMLMap } from "yaml"
|
|
|
12665
12814
|
|
|
12666
12815
|
// src/repos-cmd.ts
|
|
12667
12816
|
import { homedir as homedir7 } from "os";
|
|
12668
|
-
import { basename as basename7, join as
|
|
12817
|
+
import { basename as basename7, join as join21, resolve as resolve3 } from "path";
|
|
12669
12818
|
import { isMap, isSeq, parseDocument, stringify as stringify2 } from "yaml";
|
|
12670
12819
|
var ReposError = class extends Error {
|
|
12671
12820
|
};
|
|
@@ -12863,7 +13012,7 @@ function planRepoClone(i) {
|
|
|
12863
13012
|
}
|
|
12864
13013
|
const ship = doc.get("ship", true);
|
|
12865
13014
|
const basePath = String(m.get("reposBasePath") ?? (isMap(ship) ? ship.get("reposBasePath") : void 0) ?? "~/Crew");
|
|
12866
|
-
const target = i.dir ? expandDir(i.dir, i.base) :
|
|
13015
|
+
const target = i.dir ? expandDir(i.dir, i.base) : join21(expandDir(basePath, i.base), i.route.split("/")[0], name);
|
|
12867
13016
|
const add = planRepoAdd({ text: i.text, base: i.base, route: i.route, path: target, origin: remote, resolved: i.resolved, name });
|
|
12868
13017
|
return { ...add, remote, target };
|
|
12869
13018
|
}
|
|
@@ -13006,7 +13155,7 @@ function existingRepoConfig(f) {
|
|
|
13006
13155
|
}
|
|
13007
13156
|
|
|
13008
13157
|
// src/sample-ticket.ts
|
|
13009
|
-
import { basename as basename8, dirname as
|
|
13158
|
+
import { basename as basename8, dirname as dirname14, join as join22 } from "path";
|
|
13010
13159
|
var SAMPLE_REPO = "crew-sample";
|
|
13011
13160
|
var SAMPLE_REMOTE = `local/${SAMPLE_REPO}`;
|
|
13012
13161
|
var SAMPLE_MARKER = "sample: true";
|
|
@@ -13029,8 +13178,8 @@ has something safe to work on: its test and build hooks always pass, it has no
|
|
|
13029
13178
|
remote, and \`crew sample-ticket --remove\` deletes it.
|
|
13030
13179
|
`;
|
|
13031
13180
|
function sampleDir(existing, reposBase, workspace) {
|
|
13032
|
-
if (existing.length > 0) return
|
|
13033
|
-
return
|
|
13181
|
+
if (existing.length > 0) return join22(dirname14(existing[0]), SAMPLE_REPO);
|
|
13182
|
+
return join22(reposBase, workspace, SAMPLE_REPO);
|
|
13034
13183
|
}
|
|
13035
13184
|
var SAMPLE_TITLE = "Sample: add a documented `greet` function with a test";
|
|
13036
13185
|
function sampleDescription() {
|
|
@@ -13111,8 +13260,8 @@ function schedulerInstalled(plan, exists, crontab) {
|
|
|
13111
13260
|
}
|
|
13112
13261
|
|
|
13113
13262
|
// src/worktree-sweep.ts
|
|
13114
|
-
import { existsSync as
|
|
13115
|
-
import { basename as basename9, dirname as
|
|
13263
|
+
import { existsSync as existsSync23, readFileSync as readFileSync17, readdirSync as readdirSync6 } from "fs";
|
|
13264
|
+
import { basename as basename9, dirname as dirname15, join as join23, resolve as resolve4 } from "path";
|
|
13116
13265
|
function planWorktreeSweep(target, tickets, contract) {
|
|
13117
13266
|
const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
|
|
13118
13267
|
const verified = contract.statuses.verified;
|
|
@@ -13166,12 +13315,12 @@ function worktreeTicketNumbers(target) {
|
|
|
13166
13315
|
return [...nums];
|
|
13167
13316
|
}
|
|
13168
13317
|
function siblingWorktreeDirs(target) {
|
|
13169
|
-
const parent =
|
|
13318
|
+
const parent = dirname15(target.dir.replace(/[/\\]+$/, ""));
|
|
13170
13319
|
const stem = `${basename9(target.dir.replace(/[/\\]+$/, ""))}-`;
|
|
13171
13320
|
const registered = new Set(worktrees(target.dir).map((w) => resolve4(w.path)));
|
|
13172
13321
|
let names;
|
|
13173
13322
|
try {
|
|
13174
|
-
names =
|
|
13323
|
+
names = readdirSync6(parent);
|
|
13175
13324
|
} catch {
|
|
13176
13325
|
return [];
|
|
13177
13326
|
}
|
|
@@ -13179,7 +13328,7 @@ function siblingWorktreeDirs(target) {
|
|
|
13179
13328
|
for (const name of names) {
|
|
13180
13329
|
if (!name.startsWith(stem)) continue;
|
|
13181
13330
|
const m = DIR_NUMBER_SUFFIXED.exec(name);
|
|
13182
|
-
const path =
|
|
13331
|
+
const path = join23(parent, name);
|
|
13183
13332
|
if (m && !registered.has(resolve4(path))) out.push({ path, number: m[1] });
|
|
13184
13333
|
}
|
|
13185
13334
|
return out;
|
|
@@ -13214,7 +13363,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
|
|
|
13214
13363
|
let removed = 0;
|
|
13215
13364
|
let keptBranches = 0;
|
|
13216
13365
|
for (const a of actions) {
|
|
13217
|
-
if (!dryRun && base && !a.keepBranch &&
|
|
13366
|
+
if (!dryRun && base && !a.keepBranch && existsSync23(a.path) && isHandBackMerge(a.path, base)) {
|
|
13218
13367
|
if (gitOk(a.path, ["merge", "--abort"]) !== null) {
|
|
13219
13368
|
log.emit(
|
|
13220
13369
|
`abandoned the hand-back merge of ${base} in ${a.path}: ${a.ticket.issue_id} deployed without it`,
|
|
@@ -13222,7 +13371,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
|
|
|
13222
13371
|
);
|
|
13223
13372
|
}
|
|
13224
13373
|
}
|
|
13225
|
-
if (!dryRun &&
|
|
13374
|
+
if (!dryRun && existsSync23(a.path)) {
|
|
13226
13375
|
let dirty = false;
|
|
13227
13376
|
try {
|
|
13228
13377
|
dirty = status(a.path).length > 0;
|
|
@@ -13233,7 +13382,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
|
|
|
13233
13382
|
continue;
|
|
13234
13383
|
}
|
|
13235
13384
|
}
|
|
13236
|
-
if (!dryRun && a.requirePushed && a.branch &&
|
|
13385
|
+
if (!dryRun && a.requirePushed && a.branch && existsSync23(a.path) && remoteConfigured(cwd, remote) && worktreeHoldsUnpushed(a.path, remote)) {
|
|
13237
13386
|
refuse(a, "unpushed commits", `${a.branch} has commits that are not on ${remote}`);
|
|
13238
13387
|
continue;
|
|
13239
13388
|
}
|
|
@@ -13282,7 +13431,7 @@ function isHandBackMerge(path, base) {
|
|
|
13282
13431
|
if (!msgPath) return false;
|
|
13283
13432
|
let first;
|
|
13284
13433
|
try {
|
|
13285
|
-
first =
|
|
13434
|
+
first = readFileSync17(resolve4(path, msgPath), "utf8").split("\n")[0] ?? "";
|
|
13286
13435
|
} catch {
|
|
13287
13436
|
return false;
|
|
13288
13437
|
}
|
|
@@ -13387,16 +13536,16 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
|
|
|
13387
13536
|
}
|
|
13388
13537
|
|
|
13389
13538
|
// src/stream-sweep.ts
|
|
13390
|
-
import { existsSync as
|
|
13391
|
-
import { join as
|
|
13539
|
+
import { existsSync as existsSync24, readdirSync as readdirSync7, rmSync as rmSync10, statSync as statSync8 } from "fs";
|
|
13540
|
+
import { join as join24 } from "path";
|
|
13392
13541
|
var DAY_MS2 = 24 * 60 * 60 * 1e3;
|
|
13393
13542
|
function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
|
|
13394
|
-
const dir =
|
|
13395
|
-
if (!
|
|
13543
|
+
const dir = join24(stateDir, "streams");
|
|
13544
|
+
if (!existsSync24(dir)) return [];
|
|
13396
13545
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
13397
|
-
return
|
|
13546
|
+
return readdirSync7(dir).map((name) => join24(dir, name)).filter((path) => {
|
|
13398
13547
|
try {
|
|
13399
|
-
return
|
|
13548
|
+
return statSync8(path).mtimeMs < cutoff;
|
|
13400
13549
|
} catch {
|
|
13401
13550
|
return false;
|
|
13402
13551
|
}
|
|
@@ -13410,7 +13559,7 @@ function applyStreamSweep(paths, dryRun, log) {
|
|
|
13410
13559
|
continue;
|
|
13411
13560
|
}
|
|
13412
13561
|
try {
|
|
13413
|
-
|
|
13562
|
+
rmSync10(path, { force: true });
|
|
13414
13563
|
removed++;
|
|
13415
13564
|
log.emit(`removed stream artifact ${path}`, { step: "worktree" });
|
|
13416
13565
|
} catch (e) {
|
|
@@ -13420,11 +13569,11 @@ function applyStreamSweep(paths, dryRun, log) {
|
|
|
13420
13569
|
return removed;
|
|
13421
13570
|
}
|
|
13422
13571
|
function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
13423
|
-
if (!
|
|
13572
|
+
if (!existsSync24(stateDir)) return [];
|
|
13424
13573
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
13425
|
-
return
|
|
13574
|
+
return readdirSync7(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join24(stateDir, name)).filter((path) => {
|
|
13426
13575
|
try {
|
|
13427
|
-
return
|
|
13576
|
+
return statSync8(path).mtimeMs < cutoff;
|
|
13428
13577
|
} catch {
|
|
13429
13578
|
return false;
|
|
13430
13579
|
}
|
|
@@ -13432,24 +13581,24 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
|
13432
13581
|
}
|
|
13433
13582
|
|
|
13434
13583
|
// src/cli.ts
|
|
13435
|
-
import { readFileSync as
|
|
13584
|
+
import { realpathSync as realpathSync2, readFileSync as readFileSync23, existsSync as existsSync30, mkdirSync as mkdirSync16, writeFileSync as writeFileSync17, rmSync as rmSync12, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync2, statSync as statSync10, readdirSync as readdirSync12 } from "fs";
|
|
13436
13585
|
import { createInterface } from "readline/promises";
|
|
13437
13586
|
import { dirname as dirOf, resolve as resolvePath } from "path";
|
|
13438
13587
|
|
|
13439
13588
|
// src/ssh-keys.ts
|
|
13440
13589
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
13441
|
-
import { existsSync as
|
|
13442
|
-
import { dirname as
|
|
13590
|
+
import { existsSync as existsSync25, mkdirSync as mkdirSync12, readFileSync as readFileSync18, chmodSync } from "fs";
|
|
13591
|
+
import { dirname as dirname16, join as join25 } from "path";
|
|
13443
13592
|
var SshKeyError = class extends Error {
|
|
13444
13593
|
};
|
|
13445
13594
|
function shipSshKeyPathFor(stateDir) {
|
|
13446
|
-
return
|
|
13595
|
+
return join25(stateDir, "ssh", "id_ed25519");
|
|
13447
13596
|
}
|
|
13448
13597
|
function ensureShipSshKeypair(stateDir) {
|
|
13449
13598
|
const privateKeyPath = shipSshKeyPathFor(stateDir);
|
|
13450
13599
|
const publicKeyPath = `${privateKeyPath}.pub`;
|
|
13451
|
-
if (!
|
|
13452
|
-
|
|
13600
|
+
if (!existsSync25(privateKeyPath)) {
|
|
13601
|
+
mkdirSync12(dirname16(privateKeyPath), { recursive: true });
|
|
13453
13602
|
try {
|
|
13454
13603
|
execFileSync4("ssh-keygen", [
|
|
13455
13604
|
"-t",
|
|
@@ -13469,10 +13618,10 @@ function ensureShipSshKeypair(stateDir) {
|
|
|
13469
13618
|
}
|
|
13470
13619
|
chmodSync(privateKeyPath, 384);
|
|
13471
13620
|
}
|
|
13472
|
-
if (!
|
|
13621
|
+
if (!existsSync25(publicKeyPath)) {
|
|
13473
13622
|
throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
|
|
13474
13623
|
}
|
|
13475
|
-
const publicKey =
|
|
13624
|
+
const publicKey = readFileSync18(publicKeyPath, "utf8").trim();
|
|
13476
13625
|
return { privateKeyPath, publicKeyPath, publicKey };
|
|
13477
13626
|
}
|
|
13478
13627
|
function sshKeygenAvailable() {
|
|
@@ -13688,9 +13837,9 @@ function syncAllPassengerCheckouts(cfg) {
|
|
|
13688
13837
|
|
|
13689
13838
|
// src/tunnel.ts
|
|
13690
13839
|
import { spawn as spawn4 } from "child_process";
|
|
13691
|
-
import { existsSync as
|
|
13840
|
+
import { existsSync as existsSync26, mkdirSync as mkdirSync13, readFileSync as readFileSync19, writeFileSync as writeFileSync14, unlinkSync as unlinkSync3, readdirSync as readdirSync8 } from "fs";
|
|
13692
13841
|
import { randomInt } from "crypto";
|
|
13693
|
-
import { join as
|
|
13842
|
+
import { join as join26 } from "path";
|
|
13694
13843
|
var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
|
|
13695
13844
|
var defaultSpawn = (cmd, args) => spawn4(cmd, args, DEFAULT_SPAWN_OPTIONS);
|
|
13696
13845
|
var SLUG_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
@@ -13757,21 +13906,21 @@ function startTunnel(opts) {
|
|
|
13757
13906
|
};
|
|
13758
13907
|
}
|
|
13759
13908
|
function tunnelStatePath(stateDir, workspaceId) {
|
|
13760
|
-
return
|
|
13909
|
+
return join26(stateDir, "passengers", `${workspaceId}.json`);
|
|
13761
13910
|
}
|
|
13762
13911
|
function readPersistedTunnel(stateDir, workspaceId) {
|
|
13763
13912
|
const path = tunnelStatePath(stateDir, workspaceId);
|
|
13764
|
-
if (!
|
|
13913
|
+
if (!existsSync26(path)) return void 0;
|
|
13765
13914
|
try {
|
|
13766
|
-
return JSON.parse(
|
|
13915
|
+
return JSON.parse(readFileSync19(path, "utf8"));
|
|
13767
13916
|
} catch {
|
|
13768
13917
|
return void 0;
|
|
13769
13918
|
}
|
|
13770
13919
|
}
|
|
13771
13920
|
function writePersistedTunnel(stateDir, state) {
|
|
13772
13921
|
const path = tunnelStatePath(stateDir, state.workspaceId);
|
|
13773
|
-
|
|
13774
|
-
|
|
13922
|
+
mkdirSync13(join26(stateDir, "passengers"), { recursive: true });
|
|
13923
|
+
writeFileSync14(path, `${JSON.stringify(state, null, 2)}
|
|
13775
13924
|
`);
|
|
13776
13925
|
}
|
|
13777
13926
|
function updatePersistedTunnel(stateDir, workspaceId, patch2) {
|
|
@@ -13868,9 +14017,9 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
|
|
|
13868
14017
|
}
|
|
13869
14018
|
}
|
|
13870
14019
|
function readAllPersistedWorkspaceIds(stateDir) {
|
|
13871
|
-
const dir =
|
|
13872
|
-
if (!
|
|
13873
|
-
return
|
|
14020
|
+
const dir = join26(stateDir, "passengers");
|
|
14021
|
+
if (!existsSync26(dir)) return [];
|
|
14022
|
+
return readdirSync8(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
|
|
13874
14023
|
}
|
|
13875
14024
|
|
|
13876
14025
|
// src/mcp-endpoint-registry.ts
|
|
@@ -13990,8 +14139,8 @@ async function unregisterEndpoint(workspaceId, info, d) {
|
|
|
13990
14139
|
import { createServer as createHttpServer } from "http";
|
|
13991
14140
|
import { spawn as spawn5 } from "child_process";
|
|
13992
14141
|
import { randomBytes } from "crypto";
|
|
13993
|
-
import { existsSync as
|
|
13994
|
-
import { join as
|
|
14142
|
+
import { existsSync as existsSync27, mkdirSync as mkdirSync14, readFileSync as readFileSync20, writeFileSync as writeFileSync15, unlinkSync as unlinkSync4, readdirSync as readdirSync9 } from "fs";
|
|
14143
|
+
import { join as join27 } from "path";
|
|
13995
14144
|
|
|
13996
14145
|
// src/secret-compare.ts
|
|
13997
14146
|
import { timingSafeEqual } from "crypto";
|
|
@@ -14080,21 +14229,21 @@ var defaultSpawn2 = (cmd, args, env) => {
|
|
|
14080
14229
|
return child;
|
|
14081
14230
|
};
|
|
14082
14231
|
function syncDaemonStatePath(stateDir, workspaceId) {
|
|
14083
|
-
return
|
|
14232
|
+
return join27(stateDir, "passengers", `${workspaceId}-sync.json`);
|
|
14084
14233
|
}
|
|
14085
14234
|
function readPersistedSyncDaemon(stateDir, workspaceId) {
|
|
14086
14235
|
const path = syncDaemonStatePath(stateDir, workspaceId);
|
|
14087
|
-
if (!
|
|
14236
|
+
if (!existsSync27(path)) return void 0;
|
|
14088
14237
|
try {
|
|
14089
|
-
return JSON.parse(
|
|
14238
|
+
return JSON.parse(readFileSync20(path, "utf8"));
|
|
14090
14239
|
} catch {
|
|
14091
14240
|
return void 0;
|
|
14092
14241
|
}
|
|
14093
14242
|
}
|
|
14094
14243
|
function writePersistedSyncDaemon(stateDir, state) {
|
|
14095
14244
|
const path = syncDaemonStatePath(stateDir, state.workspaceId);
|
|
14096
|
-
|
|
14097
|
-
|
|
14245
|
+
mkdirSync14(join27(stateDir, "passengers"), { recursive: true });
|
|
14246
|
+
writeFileSync15(path, `${JSON.stringify(state, null, 2)}
|
|
14098
14247
|
`);
|
|
14099
14248
|
}
|
|
14100
14249
|
function removePersistedSyncDaemon(stateDir, workspaceId) {
|
|
@@ -14114,10 +14263,10 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
|
|
|
14114
14263
|
}
|
|
14115
14264
|
}
|
|
14116
14265
|
function readAllPersistedSyncWorkspaceIds(stateDir) {
|
|
14117
|
-
const dir =
|
|
14118
|
-
if (!
|
|
14266
|
+
const dir = join27(stateDir, "passengers");
|
|
14267
|
+
if (!existsSync27(dir)) return [];
|
|
14119
14268
|
const suffix = "-sync.json";
|
|
14120
|
-
return
|
|
14269
|
+
return readdirSync9(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
|
|
14121
14270
|
}
|
|
14122
14271
|
function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
|
|
14123
14272
|
const alive = deps.isPidAlive ?? isPidAlive;
|
|
@@ -14172,12 +14321,12 @@ function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
|
|
|
14172
14321
|
}
|
|
14173
14322
|
|
|
14174
14323
|
// src/daemon.ts
|
|
14175
|
-
import { existsSync as
|
|
14176
|
-
import { join as
|
|
14324
|
+
import { existsSync as existsSync29, mkdirSync as mkdirSync15, readFileSync as readFileSync22, readdirSync as readdirSync11, rmSync as rmSync11, statSync as statSync9, writeFileSync as writeFileSync16 } from "fs";
|
|
14325
|
+
import { join as join29 } from "path";
|
|
14177
14326
|
|
|
14178
14327
|
// src/interrupted.ts
|
|
14179
|
-
import { existsSync as
|
|
14180
|
-
import { join as
|
|
14328
|
+
import { existsSync as existsSync28, readdirSync as readdirSync10, readFileSync as readFileSync21 } from "fs";
|
|
14329
|
+
import { join as join28 } from "path";
|
|
14181
14330
|
var INTERRUPTED_MARKER = calloutHeader("NOTE", "Interrupted");
|
|
14182
14331
|
function interruptedComment(b) {
|
|
14183
14332
|
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.";
|
|
@@ -14196,21 +14345,21 @@ var pidAlive = (pid) => {
|
|
|
14196
14345
|
};
|
|
14197
14346
|
function runFinished(streamPath) {
|
|
14198
14347
|
const path = `${streamPath}.meta.json`;
|
|
14199
|
-
if (!
|
|
14348
|
+
if (!existsSync28(path)) return false;
|
|
14200
14349
|
try {
|
|
14201
|
-
return JSON.parse(
|
|
14350
|
+
return JSON.parse(readFileSync21(path, "utf8")).running !== true;
|
|
14202
14351
|
} catch {
|
|
14203
14352
|
return true;
|
|
14204
14353
|
}
|
|
14205
14354
|
}
|
|
14206
14355
|
function orphanedStreams(o) {
|
|
14207
|
-
const dir =
|
|
14208
|
-
if (!
|
|
14356
|
+
const dir = join28(o.stateDir, "streams");
|
|
14357
|
+
if (!existsSync28(dir)) return [];
|
|
14209
14358
|
const ownerAlive = o.isOwnerAlive ?? pidAlive;
|
|
14210
14359
|
const out = [];
|
|
14211
|
-
for (const name of
|
|
14360
|
+
for (const name of readdirSync10(dir)) {
|
|
14212
14361
|
if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
|
|
14213
|
-
const streamPath =
|
|
14362
|
+
const streamPath = join28(dir, name);
|
|
14214
14363
|
if (runFinished(streamPath)) continue;
|
|
14215
14364
|
const sidecar = readRunSidecar(streamPath);
|
|
14216
14365
|
if (!sidecar || sidecar.route !== o.routeLabel) continue;
|
|
@@ -14302,15 +14451,15 @@ async function runDaemonLoop(o) {
|
|
|
14302
14451
|
function latestMtime(path) {
|
|
14303
14452
|
let stat;
|
|
14304
14453
|
try {
|
|
14305
|
-
stat =
|
|
14454
|
+
stat = statSync9(path);
|
|
14306
14455
|
} catch {
|
|
14307
14456
|
return 0;
|
|
14308
14457
|
}
|
|
14309
14458
|
if (stat.isFile()) return stat.mtimeMs;
|
|
14310
14459
|
if (!stat.isDirectory()) return 0;
|
|
14311
14460
|
let latest = stat.mtimeMs;
|
|
14312
|
-
for (const entry of
|
|
14313
|
-
latest = Math.max(latest, latestMtime(
|
|
14461
|
+
for (const entry of readdirSync11(path, { withFileTypes: true })) {
|
|
14462
|
+
latest = Math.max(latest, latestMtime(join29(path, entry.name)));
|
|
14314
14463
|
}
|
|
14315
14464
|
return latest;
|
|
14316
14465
|
}
|
|
@@ -14319,23 +14468,23 @@ function computeTreeSignature(paths) {
|
|
|
14319
14468
|
}
|
|
14320
14469
|
function makeStaleChecker(paths, opts = {}) {
|
|
14321
14470
|
const initial = computeTreeSignature(paths);
|
|
14322
|
-
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 &&
|
|
14471
|
+
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync29(opts.reloadSentinel);
|
|
14323
14472
|
}
|
|
14324
14473
|
function reloadSentinelFor(stateDir) {
|
|
14325
|
-
return
|
|
14474
|
+
return join29(stateDir, "reload-requested");
|
|
14326
14475
|
}
|
|
14327
14476
|
function daemonStampFor(stateDir) {
|
|
14328
|
-
return
|
|
14477
|
+
return join29(stateDir, "daemon.started");
|
|
14329
14478
|
}
|
|
14330
14479
|
function noteDaemonStarted(stateDir, now = Date.now()) {
|
|
14331
|
-
|
|
14332
|
-
|
|
14333
|
-
|
|
14480
|
+
mkdirSync15(stateDir, { recursive: true });
|
|
14481
|
+
rmSync11(reloadSentinelFor(stateDir), { force: true });
|
|
14482
|
+
writeFileSync16(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
|
|
14334
14483
|
`);
|
|
14335
14484
|
}
|
|
14336
14485
|
function readDaemonStarted(stateDir) {
|
|
14337
14486
|
try {
|
|
14338
|
-
const v = JSON.parse(
|
|
14487
|
+
const v = JSON.parse(readFileSync22(daemonStampFor(stateDir), "utf8")).startedAt;
|
|
14339
14488
|
return typeof v === "number" ? v : null;
|
|
14340
14489
|
} catch {
|
|
14341
14490
|
return null;
|
|
@@ -14344,7 +14493,7 @@ function readDaemonStarted(stateDir) {
|
|
|
14344
14493
|
function configChangedAfter(files, startedAt) {
|
|
14345
14494
|
for (const f of files) {
|
|
14346
14495
|
try {
|
|
14347
|
-
if (
|
|
14496
|
+
if (statSync9(f).mtimeMs > startedAt) return f;
|
|
14348
14497
|
} catch {
|
|
14349
14498
|
}
|
|
14350
14499
|
}
|
|
@@ -14691,7 +14840,7 @@ async function buildEnvironment(route, ship, ticket) {
|
|
|
14691
14840
|
}
|
|
14692
14841
|
|
|
14693
14842
|
// src/cli.ts
|
|
14694
|
-
var CREW_HOME = isCompiledBinary(import.meta.url) ?
|
|
14843
|
+
var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname17(process.execPath) : resolve5(dirname17(fileURLToPath(import.meta.url)), "..");
|
|
14695
14844
|
var RELEASE_LOCK_TTL_MS = 20 * 60 * 1e3;
|
|
14696
14845
|
var CYCLE_SECONDS2 = 120;
|
|
14697
14846
|
var STALE_CYCLES = 3;
|
|
@@ -14839,6 +14988,7 @@ function usage() {
|
|
|
14839
14988
|
crew inbox [--member NAME] your tickets across every workspace (or a colleague's)
|
|
14840
14989
|
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)
|
|
14841
14990
|
crew disconnect remove what connect installed for Pair (Claude MCP; Codex/Gemini instructions block and MCP entry per ship.pair)
|
|
14991
|
+
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)
|
|
14842
14992
|
crew agents sync [route] [--force] [--revoke-seat-keys] push crew's personas AND skill files into the workspace Agents/Agent Skills tables (--force overwrites even a diverged persona or skill; never one marked Customize prompt)
|
|
14843
14993
|
crew agents prompt R [route] print one persona's current prompt (e.g. R=pair, for a SessionStart hook; --harness H adds which CLI it serves)
|
|
14844
14994
|
crew skills sync [route] push only crew's skill files (e.g. grill-me) into the workspace Agent Skills table
|
|
@@ -15017,7 +15167,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
15017
15167
|
firstRun = { answers, configPath };
|
|
15018
15168
|
cfg = loadConfig(CREW_HOME, configPath, { text: renderShipBlock(answers), allowNoRoutes: true });
|
|
15019
15169
|
} else {
|
|
15020
|
-
cfg = loadConfig(CREW_HOME, void 0, command === "repos" ? { allowNoRoutes: true } : {});
|
|
15170
|
+
cfg = loadConfig(CREW_HOME, void 0, command === "repos" || command === "disconnect" && flag("reset") ? { allowNoRoutes: true } : {});
|
|
15021
15171
|
}
|
|
15022
15172
|
} catch (e) {
|
|
15023
15173
|
if (e instanceof ConfigError) {
|
|
@@ -15048,6 +15198,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
15048
15198
|
throw e;
|
|
15049
15199
|
}
|
|
15050
15200
|
const state = new State(cfg.ship.stateDir);
|
|
15201
|
+
setCloneRecorder((dir) => recordClonedCheckout(cfg.ship.stateDir, dir));
|
|
15051
15202
|
const dryRun = flag("dry-run");
|
|
15052
15203
|
const force = flag("force");
|
|
15053
15204
|
const EXCLUSIVE = { passengers: "passengers" };
|
|
@@ -15250,8 +15401,8 @@ if (command === "passenger-sync-daemon") {
|
|
|
15250
15401
|
}
|
|
15251
15402
|
if (host === "linux" && hasSystemd) {
|
|
15252
15403
|
const label = labelFor(CREW_HOME, "run");
|
|
15253
|
-
const staleTimer =
|
|
15254
|
-
if (
|
|
15404
|
+
const staleTimer = join30(homedir8(), ".config", "systemd", "user", `${label}.timer`);
|
|
15405
|
+
if (existsSync30(staleTimer)) {
|
|
15255
15406
|
process.stdout.write(
|
|
15256
15407
|
`removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
|
|
15257
15408
|
`
|
|
@@ -15277,7 +15428,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
15277
15428
|
const tracker = new Tracker(c, cfg.ship);
|
|
15278
15429
|
problems.push(...validateContract(tracker.contract).map((p) => `contract: ${p}`));
|
|
15279
15430
|
for (const r of await resolvedRepos(c)) {
|
|
15280
|
-
if (!
|
|
15431
|
+
if (!existsSync30(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
|
|
15281
15432
|
if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
|
|
15282
15433
|
problems.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
|
|
15283
15434
|
}
|
|
@@ -15295,7 +15446,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
15295
15446
|
}
|
|
15296
15447
|
async function doctorFix(o) {
|
|
15297
15448
|
const routes = await Promise.all(routesInScope(cfg.routes, named).map((c) => gatherHealth({ route: c.route, enabled: c.enabled, hasRepos: reposOf(c).length > 0 }, () => routeProblems(c))));
|
|
15298
|
-
const unitExists = (p) =>
|
|
15449
|
+
const unitExists = (p) => existsSync30(p);
|
|
15299
15450
|
const crontab = o.host === "linux" ? spawnSync6("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
|
|
15300
15451
|
const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
|
|
15301
15452
|
const plan = planFix({ routes, schedulerAbsent, host: o.host });
|
|
@@ -15312,7 +15463,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
15312
15463
|
for (const a of plan.actions) {
|
|
15313
15464
|
if (a.kind === "enable") {
|
|
15314
15465
|
if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
|
|
15315
|
-
|
|
15466
|
+
writeFileSync17(cfg.configFile, planEnable(readFileSync23(cfg.configFile, "utf8"), a.route));
|
|
15316
15467
|
enabledSound.add(a.route);
|
|
15317
15468
|
changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
|
|
15318
15469
|
} else {
|
|
@@ -16311,7 +16462,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16311
16462
|
process.exit(1);
|
|
16312
16463
|
}
|
|
16313
16464
|
requireArmed("run the daemon loop");
|
|
16314
|
-
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [
|
|
16465
|
+
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join30(CREW_HOME, "src"), join30(CREW_HOME, "dist"), join30(CREW_HOME, "bin")];
|
|
16315
16466
|
const configPaths = daemonConfigPaths(cfg);
|
|
16316
16467
|
watchPaths.push(...configPaths);
|
|
16317
16468
|
noteDaemonStarted(cfg.ship.stateDir);
|
|
@@ -16512,10 +16663,10 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16512
16663
|
`);
|
|
16513
16664
|
if (!dryRun) {
|
|
16514
16665
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
16515
|
-
const raw =
|
|
16666
|
+
const raw = existsSync30(resolvedPath) ? JSON.parse(readFileSync23(resolvedPath, "utf8")) : {};
|
|
16516
16667
|
raw.agentSkills = result.agentSkills;
|
|
16517
|
-
|
|
16518
|
-
|
|
16668
|
+
mkdirSync16(dirOf(resolvedPath), { recursive: true });
|
|
16669
|
+
writeFileSync17(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
16519
16670
|
`);
|
|
16520
16671
|
}
|
|
16521
16672
|
if (result.outcomes.some((o) => o.action === "diverged")) process.exitCode = 1;
|
|
@@ -16528,15 +16679,15 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16528
16679
|
process.exit(2);
|
|
16529
16680
|
};
|
|
16530
16681
|
if (sub !== "add" && sub !== "list" && sub !== "clone" && sub !== "hooks") usage2();
|
|
16531
|
-
const configText =
|
|
16682
|
+
const configText = readFileSync23(cfg.configFile, "utf8");
|
|
16532
16683
|
if (sub === "hooks") {
|
|
16533
16684
|
try {
|
|
16534
16685
|
const a = parseHooksArgs(positional.slice(2), routeNamesOf(configText));
|
|
16535
|
-
const dir = repoDirOf(configText,
|
|
16536
|
-
if (!
|
|
16537
|
-
const files = { exists: (r) =>
|
|
16686
|
+
const dir = repoDirOf(configText, dirname17(cfg.configFile), a.route, a.name);
|
|
16687
|
+
if (!existsSync30(dir)) throw new ReposError(`${dir} does not exist on disk`);
|
|
16688
|
+
const files = { exists: (r) => existsSync30(resolvePath(dir, r)), read: (r) => {
|
|
16538
16689
|
try {
|
|
16539
|
-
return
|
|
16690
|
+
return readFileSync23(resolvePath(dir, r), "utf8");
|
|
16540
16691
|
} catch {
|
|
16541
16692
|
return void 0;
|
|
16542
16693
|
}
|
|
@@ -16548,13 +16699,13 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16548
16699
|
if (write && suggestion.source === null) throw new ReposError(`nothing to write: ${suggestion.notes[0]}`);
|
|
16549
16700
|
if (write === "route") {
|
|
16550
16701
|
const plan = planRouteHooks(configText, a.route, a.name, suggestion.hooks);
|
|
16551
|
-
|
|
16702
|
+
writeFileSync17(cfg.configFile, plan.text);
|
|
16552
16703
|
written = { target: "route", file: cfg.configFile, hooks: plan.written, kept: plan.kept };
|
|
16553
16704
|
} else if (write === "repo") {
|
|
16554
16705
|
const have = existingRepoConfig(files);
|
|
16555
16706
|
if (have) throw new ReposError(`${resolvePath(dir, have)} already exists; not overwriting it`);
|
|
16556
16707
|
const file = resolvePath(dir, ".crew.yaml");
|
|
16557
|
-
|
|
16708
|
+
writeFileSync17(file, starterRepoConfig(suggestion.hooks), { flag: "wx" });
|
|
16558
16709
|
written = { target: "repo", file, hooks: Object.keys(suggestion.hooks), kept: [] };
|
|
16559
16710
|
}
|
|
16560
16711
|
if (flag("json")) {
|
|
@@ -16591,7 +16742,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16591
16742
|
if (sub === "list" && !routeName) usage2();
|
|
16592
16743
|
if (sub === "add") {
|
|
16593
16744
|
try {
|
|
16594
|
-
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) =>
|
|
16745
|
+
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync30(resolvePath(p.replace(/^~(?=\/|$)/, homedir8()))));
|
|
16595
16746
|
routeName = a.route;
|
|
16596
16747
|
addPath = a.path;
|
|
16597
16748
|
addName = a.name;
|
|
@@ -16620,15 +16771,15 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16620
16771
|
}
|
|
16621
16772
|
const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
|
|
16622
16773
|
let resolvedRemotes;
|
|
16623
|
-
if (
|
|
16774
|
+
if (existsSync30(resolvedFile)) {
|
|
16624
16775
|
try {
|
|
16625
|
-
resolvedRemotes = JSON.parse(
|
|
16776
|
+
resolvedRemotes = JSON.parse(readFileSync23(resolvedFile, "utf8"));
|
|
16626
16777
|
} catch {
|
|
16627
16778
|
}
|
|
16628
16779
|
}
|
|
16629
16780
|
const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
|
|
16630
16781
|
const text = configText;
|
|
16631
|
-
const base =
|
|
16782
|
+
const base = dirname17(cfg.configFile);
|
|
16632
16783
|
try {
|
|
16633
16784
|
if (sub === "list") {
|
|
16634
16785
|
const repos = configuredRepos(text, routeName, base);
|
|
@@ -16637,7 +16788,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16637
16788
|
for (const r of repos) {
|
|
16638
16789
|
const origin2 = originOf(r.dir);
|
|
16639
16790
|
const row = matchRepoName(origin2, resolvedRemotes);
|
|
16640
|
-
const note = !
|
|
16791
|
+
const note = !existsSync30(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
|
|
16641
16792
|
process.stdout.write(`${r.name} ${r.dir} ${note}
|
|
16642
16793
|
`);
|
|
16643
16794
|
}
|
|
@@ -16649,9 +16800,9 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16649
16800
|
` : `${human}
|
|
16650
16801
|
`);
|
|
16651
16802
|
const plan2 = planRepoClone({ text, base, route: routeName, name: addName, resolved: resolvedRemotes, dir: value("dir") });
|
|
16652
|
-
if (
|
|
16803
|
+
if (existsSync30(plan2.target) && (existsSync30(`${plan2.target}/.git`) || readdirSync12(plan2.target).length > 0)) {
|
|
16653
16804
|
throw new ReposError(
|
|
16654
|
-
|
|
16805
|
+
existsSync30(`${plan2.target}/.git`) ? `${plan2.target} is already a checkout \u2014 attach it with \`crew repos add ${routeName} ${plan2.name} ${plan2.target}\`` : `${plan2.target} exists and is not empty \u2014 pass --dir to clone somewhere else`
|
|
16655
16806
|
);
|
|
16656
16807
|
}
|
|
16657
16808
|
if (dryRun) {
|
|
@@ -16669,12 +16820,12 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16669
16820
|
throw new ReposError(`${e.message}${authHint}
|
|
16670
16821
|
crew.yaml was not changed`);
|
|
16671
16822
|
}
|
|
16672
|
-
|
|
16823
|
+
writeFileSync17(cfg.configFile, plan2.text);
|
|
16673
16824
|
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}`);
|
|
16674
16825
|
break;
|
|
16675
16826
|
}
|
|
16676
16827
|
const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir8()));
|
|
16677
|
-
const top =
|
|
16828
|
+
const top = existsSync30(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
|
|
16678
16829
|
if (!top) throw new ReposError(`${target} is not a git checkout`);
|
|
16679
16830
|
const origin = originOf(top);
|
|
16680
16831
|
const plan = planRepoAdd({
|
|
@@ -16723,15 +16874,15 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16723
16874
|
const created = await rowTracker.createRepoRow(rowPayload);
|
|
16724
16875
|
process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
|
|
16725
16876
|
`);
|
|
16726
|
-
if (
|
|
16727
|
-
const raw = JSON.parse(
|
|
16877
|
+
if (existsSync30(resolvedFile)) {
|
|
16878
|
+
const raw = JSON.parse(readFileSync23(resolvedFile, "utf8"));
|
|
16728
16879
|
raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
|
|
16729
16880
|
raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
|
|
16730
|
-
|
|
16881
|
+
writeFileSync17(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
16731
16882
|
`);
|
|
16732
16883
|
}
|
|
16733
16884
|
}
|
|
16734
|
-
|
|
16885
|
+
writeFileSync17(cfg.configFile, plan.text);
|
|
16735
16886
|
process.stdout.write(`added ${top} to ${routeName} as ${plan.mode === "dir" ? "dir" : `repos.${plan.name}`} in ${cfg.configFile}
|
|
16736
16887
|
`);
|
|
16737
16888
|
} catch (e) {
|
|
@@ -17016,6 +17167,16 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
17016
17167
|
}
|
|
17017
17168
|
const route2 = `${found.workspaceSlug}/${found.projectSlug ?? found.projectId}`;
|
|
17018
17169
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, route2);
|
|
17170
|
+
const capabilities = await checkCapabilities(authOpts, found);
|
|
17171
|
+
if (flag("json")) {
|
|
17172
|
+
process.stdout.write(`${JSON.stringify({ event: "capabilities", route: route2, items: capabilities })}
|
|
17173
|
+
`);
|
|
17174
|
+
} else {
|
|
17175
|
+
process.stderr.write("capabilities for this key:\n");
|
|
17176
|
+
for (const i of capabilities) process.stderr.write(` ${describeCapability(i)}
|
|
17177
|
+
`);
|
|
17178
|
+
}
|
|
17179
|
+
const capabilityMisses = capabilities.filter((i) => i.status === "missing").map(missLine);
|
|
17019
17180
|
if (found.locksModelId && !dryRun) {
|
|
17020
17181
|
const configured = cfg.routes.find((r) => r.route === route2);
|
|
17021
17182
|
const repoNames = [...Object.values(found.repoNames ?? {}), ...configured ? reposOf(configured).map((t) => t.name) : []].filter((n) => !!n);
|
|
@@ -17041,10 +17202,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
17041
17202
|
}
|
|
17042
17203
|
}
|
|
17043
17204
|
let previous;
|
|
17044
|
-
const hadPreviousFile =
|
|
17205
|
+
const hadPreviousFile = existsSync30(resolvedPath);
|
|
17045
17206
|
if (hadPreviousFile) {
|
|
17046
17207
|
try {
|
|
17047
|
-
previous = JSON.parse(
|
|
17208
|
+
previous = JSON.parse(readFileSync23(resolvedPath, "utf8"));
|
|
17048
17209
|
} catch {
|
|
17049
17210
|
}
|
|
17050
17211
|
}
|
|
@@ -17134,6 +17295,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
17134
17295
|
holds: found.holds.map((h) => ({ id: h.id, role: h.name })),
|
|
17135
17296
|
...found.releaseSeat ? { releaseSeat: found.releaseSeat } : {},
|
|
17136
17297
|
...found.role ?? previous?.role ? { role: found.role ?? previous?.role } : {},
|
|
17298
|
+
...capabilityMisses.length > 0 ? { capabilityMisses } : {},
|
|
17137
17299
|
...operator ? { operator } : {},
|
|
17138
17300
|
...contract ? { contract } : {},
|
|
17139
17301
|
...reviewedStatuses.length > 0 ? { reviewedStatuses } : {},
|
|
@@ -17152,10 +17314,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
17152
17314
|
process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
|
|
17153
17315
|
`);
|
|
17154
17316
|
} else {
|
|
17155
|
-
|
|
17317
|
+
mkdirSync16(dirname17(resolvedPath), { recursive: true });
|
|
17156
17318
|
if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
|
|
17157
17319
|
const tmpPath = `${resolvedPath}.tmp${process.pid}`;
|
|
17158
|
-
|
|
17320
|
+
writeFileSync17(tmpPath, `${JSON.stringify(resolved, null, 2)}
|
|
17159
17321
|
`);
|
|
17160
17322
|
renameSync2(tmpPath, resolvedPath);
|
|
17161
17323
|
process.stderr.write(`Resolved ids written to ${resolvedPath}
|
|
@@ -17190,8 +17352,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
17190
17352
|
process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
|
|
17191
17353
|
`);
|
|
17192
17354
|
} else {
|
|
17193
|
-
|
|
17194
|
-
|
|
17355
|
+
mkdirSync16(dirname17(apiKeyFile), { recursive: true });
|
|
17356
|
+
writeFileSync17(apiKeyFile, `${apiKeyVar}=${apiKey}
|
|
17195
17357
|
`, { mode: 384 });
|
|
17196
17358
|
process.stderr.write(`API key written to ${apiKeyFile}
|
|
17197
17359
|
`);
|
|
@@ -17200,7 +17362,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
17200
17362
|
const keyPersisted = keychainBacked || apiKeyFile !== void 0;
|
|
17201
17363
|
const wizard = firstRun;
|
|
17202
17364
|
const writeFirstRun = wizard !== void 0 && !dryRun && keyPersisted;
|
|
17203
|
-
const routeDir = value("dir") ?? (writeFirstRun ?
|
|
17365
|
+
const routeDir = value("dir") ?? (writeFirstRun ? join30(DEFAULT_REPOS_BASE_PATH, found.workspaceSlug) : void 0);
|
|
17204
17366
|
const routeBlock = renderConnection(
|
|
17205
17367
|
found,
|
|
17206
17368
|
route2,
|
|
@@ -17795,10 +17957,76 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
17795
17957
|
case "disconnect": {
|
|
17796
17958
|
const url = mcpUrlFor(value("base-url") ?? cfg.routes[0]?.baseUrl ?? DEFAULT_BASE_URL);
|
|
17797
17959
|
const disconnectHarnesses = /* @__PURE__ */ new Set([...shipPair(cfg.ship), ...shipToolchains(cfg.ship).map((t) => t.harness).filter(isPairHarness)]);
|
|
17798
|
-
|
|
17799
|
-
|
|
17960
|
+
const removePairEntries = (mcpUrl, dry) => [...disconnectHarnesses].map((h) => pairInstallerFor(h).removePair({ url: mcpUrl, bin: cfg.ship.agent.bin, dryRun: dry }));
|
|
17961
|
+
if (!flag("reset")) {
|
|
17962
|
+
for (const line of removePairEntries(url, dryRun)) process.stderr.write(`${line}
|
|
17800
17963
|
`);
|
|
17964
|
+
break;
|
|
17801
17965
|
}
|
|
17966
|
+
const host = hostPlatform();
|
|
17967
|
+
let resetStore;
|
|
17968
|
+
try {
|
|
17969
|
+
resetStore = getSessionStore3();
|
|
17970
|
+
} catch {
|
|
17971
|
+
resetStore = void 0;
|
|
17972
|
+
}
|
|
17973
|
+
const effects = {
|
|
17974
|
+
async uninstallUnits(dry) {
|
|
17975
|
+
const lines = [];
|
|
17976
|
+
if (host === "windows") return lines;
|
|
17977
|
+
const log = { emit: (m) => {
|
|
17978
|
+
lines.push(m);
|
|
17979
|
+
}, warn: (m) => {
|
|
17980
|
+
lines.push(m);
|
|
17981
|
+
} };
|
|
17982
|
+
for (const job of ["run", "release", "passengers"]) {
|
|
17983
|
+
await applyUninstall(planUninstall(cfg.ship, CREW_HOME, host, detectSystemd(), job), CREW_HOME, dry, log);
|
|
17984
|
+
}
|
|
17985
|
+
return lines;
|
|
17986
|
+
},
|
|
17987
|
+
removePair: removePairEntries,
|
|
17988
|
+
async revokeSeatKeys(r, dry) {
|
|
17989
|
+
const outcomes = await revokeSeatKeys(r, { userAgent: cfg.ship.userAgent, dryRun: dry });
|
|
17990
|
+
return outcomes.map((o) => ({ line: describeRevokeOutcome(o), ok: o.action !== "failed" }));
|
|
17991
|
+
},
|
|
17992
|
+
async credential(r) {
|
|
17993
|
+
if (!resetStore) return { stored: false, clear: async () => {
|
|
17994
|
+
} };
|
|
17995
|
+
const store = resetStore;
|
|
17996
|
+
const h = hostFromUrl3(r.baseUrl);
|
|
17997
|
+
const [slug] = splitRoute(r.route);
|
|
17998
|
+
const session = await store.get(h, slug);
|
|
17999
|
+
return { stored: Boolean(session?.apiKey), clear: () => store.clear(h, slug) };
|
|
18000
|
+
},
|
|
18001
|
+
inspectCheckout(dir) {
|
|
18002
|
+
const dirty = status(dir).map((c) => c.path);
|
|
18003
|
+
const linkedWorktrees = worktrees(dir).map((w) => w.path).filter((p) => realpathSync2(p) !== realpathSync2(dir));
|
|
18004
|
+
return { dirty, linkedWorktrees };
|
|
18005
|
+
}
|
|
18006
|
+
};
|
|
18007
|
+
const droppedRoutes = [];
|
|
18008
|
+
try {
|
|
18009
|
+
const rawCfg = parseYaml2(readFileSync23(cfg.configFile, "utf8"));
|
|
18010
|
+
for (const r of rawCfg?.routes ?? []) {
|
|
18011
|
+
if (typeof r?.route !== "string" || cfg.routes.some((x) => x.route === r.route)) continue;
|
|
18012
|
+
try {
|
|
18013
|
+
splitRoute(r.route);
|
|
18014
|
+
} catch {
|
|
18015
|
+
continue;
|
|
18016
|
+
}
|
|
18017
|
+
droppedRoutes.push({ route: r.route, baseUrl: typeof r.baseUrl === "string" ? r.baseUrl : rawCfg?.ship?.baseUrl ?? DEFAULT_BASE_URL });
|
|
18018
|
+
}
|
|
18019
|
+
} catch {
|
|
18020
|
+
}
|
|
18021
|
+
const steps = await runReset(
|
|
18022
|
+
{ routes: cfg.routes, droppedRoutes, stateDir: cfg.ship.stateDir, configFile: cfg.configFile, dryRun, removeCheckouts: flag("remove-checkouts") },
|
|
18023
|
+
effects
|
|
18024
|
+
);
|
|
18025
|
+
if (flag("json")) process.stdout.write(`${JSON.stringify({ dryRun, ok: !resetIncomplete(steps), steps })}
|
|
18026
|
+
`);
|
|
18027
|
+
else for (const s of steps) process.stdout.write(`${describeResetStep(s)}
|
|
18028
|
+
`);
|
|
18029
|
+
if (resetIncomplete(steps)) process.exitCode = 1;
|
|
17802
18030
|
break;
|
|
17803
18031
|
}
|
|
17804
18032
|
case "install": {
|
|
@@ -17820,8 +18048,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
17820
18048
|
process.stdout.write("crew reload: no daemon unit is running \u2014 nothing to reload (the next run reads crew.yaml fresh)\n");
|
|
17821
18049
|
break;
|
|
17822
18050
|
}
|
|
17823
|
-
|
|
17824
|
-
|
|
18051
|
+
mkdirSync16(cfg.ship.stateDir, { recursive: true });
|
|
18052
|
+
writeFileSync17(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
|
|
17825
18053
|
`);
|
|
17826
18054
|
process.stdout.write("crew reload: requested \u2014 the daemon finishes its in-flight pass, then relaunches with the new config\n");
|
|
17827
18055
|
if (flag("now") && control?.stopCommand && control.startCommand) {
|
|
@@ -17872,7 +18100,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
17872
18100
|
}
|
|
17873
18101
|
case "log": {
|
|
17874
18102
|
try {
|
|
17875
|
-
const text =
|
|
18103
|
+
const text = readFileSync23(cfg.ship.logFile, "utf8").split("\n");
|
|
17876
18104
|
process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
|
|
17877
18105
|
`);
|
|
17878
18106
|
} catch {
|
|
@@ -17895,8 +18123,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
17895
18123
|
};
|
|
17896
18124
|
if (!route.resolved) fail(`route ${route.route} is not connected \u2014 run \`crew connect\` first`);
|
|
17897
18125
|
const tracker = new Tracker(route, cfg.ship);
|
|
17898
|
-
const configText =
|
|
17899
|
-
const cfgBase =
|
|
18126
|
+
const configText = readFileSync23(cfg.configFile, "utf8");
|
|
18127
|
+
const cfgBase = dirname17(cfg.configFile);
|
|
17900
18128
|
const configured = configuredRepos(configText, route.route, cfgBase);
|
|
17901
18129
|
const dir = configured.find((r) => r.name === SAMPLE_REPO)?.dir ?? sampleDir(configured.map((r) => r.dir), route.reposBasePath, route.route.split("/")[0]);
|
|
17902
18130
|
const resolvedFile = resolvedPathFor(cfg.ship.stateDir, route.route);
|
|
@@ -17924,7 +18152,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
17924
18152
|
}
|
|
17925
18153
|
if (configured.some((r) => r.dir === dir)) {
|
|
17926
18154
|
try {
|
|
17927
|
-
|
|
18155
|
+
writeFileSync17(cfg.configFile, planRepoRemove(configText, cfgBase, route.route, dir).text);
|
|
17928
18156
|
} catch (e) {
|
|
17929
18157
|
if (e instanceof ReposError) fail(e.message);
|
|
17930
18158
|
throw e;
|
|
@@ -17932,29 +18160,29 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
17932
18160
|
}
|
|
17933
18161
|
if (row) {
|
|
17934
18162
|
await tracker.deleteRepoRow(row.id);
|
|
17935
|
-
if (
|
|
17936
|
-
const raw = JSON.parse(
|
|
18163
|
+
if (existsSync30(resolvedFile)) {
|
|
18164
|
+
const raw = JSON.parse(readFileSync23(resolvedFile, "utf8"));
|
|
17937
18165
|
if (raw.repoNames) delete raw.repoNames[row.id];
|
|
17938
18166
|
if (raw.repoRemotes) delete raw.repoRemotes[row.id];
|
|
17939
|
-
|
|
18167
|
+
writeFileSync17(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
17940
18168
|
`);
|
|
17941
18169
|
}
|
|
17942
18170
|
}
|
|
17943
|
-
if (
|
|
18171
|
+
if (existsSync30(join30(dir, ".git")) && basename10(dir) === SAMPLE_REPO) rmSync12(dir, { recursive: true, force: true });
|
|
17944
18172
|
say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
|
|
17945
18173
|
break;
|
|
17946
18174
|
}
|
|
17947
18175
|
const projectId = route.resolved.areaId ?? resolveRepoProject(await tracker.projectRows(), value("project"));
|
|
17948
|
-
const fresh = !
|
|
17949
|
-
if (
|
|
18176
|
+
const fresh = !existsSync30(join30(dir, ".git"));
|
|
18177
|
+
if (existsSync30(dir) && fresh && readdirSync12(dir).length > 0) fail(`${dir} exists and is not a git checkout \u2014 move it aside`);
|
|
17950
18178
|
if (dryRun) {
|
|
17951
18179
|
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`);
|
|
17952
18180
|
break;
|
|
17953
18181
|
}
|
|
17954
18182
|
if (fresh) {
|
|
17955
|
-
|
|
17956
|
-
|
|
17957
|
-
|
|
18183
|
+
mkdirSync16(dir, { recursive: true });
|
|
18184
|
+
writeFileSync17(join30(dir, "README.md"), SAMPLE_README);
|
|
18185
|
+
writeFileSync17(join30(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
|
|
17958
18186
|
const g = (...a) => {
|
|
17959
18187
|
const r = spawnSync6("git", a, { cwd: dir, encoding: "utf8" });
|
|
17960
18188
|
if (r.status !== 0) fail(`git ${a[0]} failed in ${dir}: ${(r.stderr || r.stdout).trim()}`);
|
|
@@ -17972,17 +18200,17 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
17972
18200
|
spec: { platform: "unix", releaseMode: "integrate", ciProvider: "none" }
|
|
17973
18201
|
});
|
|
17974
18202
|
repoId = (await tracker.createRepoRow(payload)).id;
|
|
17975
|
-
if (
|
|
17976
|
-
const raw = JSON.parse(
|
|
18203
|
+
if (existsSync30(resolvedFile)) {
|
|
18204
|
+
const raw = JSON.parse(readFileSync23(resolvedFile, "utf8"));
|
|
17977
18205
|
raw.repoNames = { ...raw.repoNames, [repoId]: SAMPLE_REPO };
|
|
17978
18206
|
raw.repoRemotes = { ...raw.repoRemotes, [repoId]: SAMPLE_REMOTE };
|
|
17979
|
-
|
|
18207
|
+
writeFileSync17(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
17980
18208
|
`);
|
|
17981
18209
|
}
|
|
17982
18210
|
}
|
|
17983
18211
|
if (!configured.some((r) => r.dir === dir)) {
|
|
17984
18212
|
try {
|
|
17985
|
-
|
|
18213
|
+
writeFileSync17(cfg.configFile, planRepoAdd({ text: configText, base: cfgBase, route: route.route, path: dir, name: SAMPLE_REPO }).text);
|
|
17986
18214
|
} catch (e) {
|
|
17987
18215
|
if (e instanceof ReposError) fail(e.message);
|
|
17988
18216
|
throw e;
|
|
@@ -18272,7 +18500,7 @@ watermark: ${state.watermark()}
|
|
|
18272
18500
|
}
|
|
18273
18501
|
for (const r of repos) {
|
|
18274
18502
|
const remote = r.config.branch.remote;
|
|
18275
|
-
if (
|
|
18503
|
+
if (existsSync30(r.dir)) {
|
|
18276
18504
|
pushAuth.push({
|
|
18277
18505
|
route: c.route,
|
|
18278
18506
|
repo: r.name,
|
|
@@ -18300,7 +18528,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
18300
18528
|
);
|
|
18301
18529
|
try {
|
|
18302
18530
|
const evFile = eventFileFor(cfg.ship.stateDir);
|
|
18303
|
-
const evSize =
|
|
18531
|
+
const evSize = existsSync30(evFile) ? statSync10(evFile).size : 0;
|
|
18304
18532
|
process.stdout.write(
|
|
18305
18533
|
evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
|
|
18306
18534
|
` : "events log: ok\n"
|
|
@@ -18311,7 +18539,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
18311
18539
|
const unitPlan = planInstall(cfg.ship, CREW_HOME, host, detectSystemd(), "run");
|
|
18312
18540
|
const abandons = unitAbandonsAgents(unitPlan, (p) => {
|
|
18313
18541
|
try {
|
|
18314
|
-
return
|
|
18542
|
+
return readFileSync23(p, "utf8");
|
|
18315
18543
|
} catch {
|
|
18316
18544
|
return null;
|
|
18317
18545
|
}
|
|
@@ -18327,7 +18555,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
18327
18555
|
const changed2 = configChangedAfter(daemonConfigPaths(cfg), daemonStartedAt);
|
|
18328
18556
|
if (changed2) {
|
|
18329
18557
|
process.stdout.write(
|
|
18330
|
-
`daemon config: WARN ${changed2} changed at ${new Date(
|
|
18558
|
+
`daemon config: WARN ${changed2} changed at ${new Date(statSync10(changed2).mtimeMs).toISOString()} but the daemon started at ${new Date(daemonStartedAt).toISOString()} \u2014 run \`crew reload\`
|
|
18331
18559
|
`
|
|
18332
18560
|
);
|
|
18333
18561
|
}
|
|
@@ -18387,6 +18615,8 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
18387
18615
|
const keyRole = route.resolved?.role ?? await fetchKeyRole(route, cfg.ship.userAgent);
|
|
18388
18616
|
const roleRoute = route.resolved && keyRole ? { ...route, resolved: { ...route.resolved, role: keyRole } } : route;
|
|
18389
18617
|
process.stdout.write(`api key role: ${keyRole ?? "unknown"}
|
|
18618
|
+
`);
|
|
18619
|
+
for (const m of route.resolved?.capabilityMisses ?? []) process.stdout.write(`capability: MISSING ${m} (re-run \`crew connect\` once granted)
|
|
18390
18620
|
`);
|
|
18391
18621
|
const canMint = await canMintAgentKeys(roleRoute, { userAgent: cfg.ship.userAgent });
|
|
18392
18622
|
process.stdout.write(`can mint agent keys: ${canMint === void 0 ? "unknown" : canMint ? "yes" : "no"}
|