@tablation/crew 0.59.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 +9 -0
- package/dist/cli.js +703 -275
- 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([
|
|
@@ -5211,7 +5213,8 @@ var VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
|
5211
5213
|
"tail",
|
|
5212
5214
|
"ticket",
|
|
5213
5215
|
"workspace",
|
|
5214
|
-
"workspace-id"
|
|
5216
|
+
"workspace-id",
|
|
5217
|
+
"write"
|
|
5215
5218
|
]);
|
|
5216
5219
|
function positionalArgs(argv2) {
|
|
5217
5220
|
const out = [];
|
|
@@ -5759,6 +5762,103 @@ var State = class _State {
|
|
|
5759
5762
|
}
|
|
5760
5763
|
};
|
|
5761
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
|
+
|
|
5762
5862
|
// src/tracker.ts
|
|
5763
5863
|
import { hostname } from "os";
|
|
5764
5864
|
import { TablationClient as TablationClient3 } from "@tablation/client";
|
|
@@ -6153,24 +6253,24 @@ function recoveredFailureHeadlines(o, route) {
|
|
|
6153
6253
|
}
|
|
6154
6254
|
|
|
6155
6255
|
// src/ship-attention.ts
|
|
6156
|
-
import { existsSync as
|
|
6256
|
+
import { existsSync as existsSync16, statfsSync } from "fs";
|
|
6157
6257
|
|
|
6158
6258
|
// src/git.ts
|
|
6159
6259
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
6160
6260
|
import {
|
|
6161
|
-
existsSync as
|
|
6162
|
-
mkdirSync as
|
|
6261
|
+
existsSync as existsSync15,
|
|
6262
|
+
mkdirSync as mkdirSync8,
|
|
6163
6263
|
openSync as openSync3,
|
|
6164
6264
|
closeSync as closeSync3,
|
|
6165
6265
|
writeSync,
|
|
6166
|
-
statSync as
|
|
6266
|
+
statSync as statSync6,
|
|
6167
6267
|
unlinkSync as unlinkSync2,
|
|
6168
|
-
readFileSync as
|
|
6169
|
-
writeFileSync as
|
|
6170
|
-
readdirSync as
|
|
6171
|
-
rmSync as
|
|
6268
|
+
readFileSync as readFileSync14,
|
|
6269
|
+
writeFileSync as writeFileSync9,
|
|
6270
|
+
readdirSync as readdirSync5,
|
|
6271
|
+
rmSync as rmSync8
|
|
6172
6272
|
} from "fs";
|
|
6173
|
-
import { dirname as
|
|
6273
|
+
import { dirname as dirname7, basename as basename5, join as join15 } from "path";
|
|
6174
6274
|
var GitError = class extends Error {
|
|
6175
6275
|
};
|
|
6176
6276
|
function streamText(x) {
|
|
@@ -6536,8 +6636,8 @@ function recoverStrandedReleaseCheckouts(cwd) {
|
|
|
6536
6636
|
if (line.startsWith("worktree ")) path = line.slice("worktree ".length);
|
|
6537
6637
|
else if (line.startsWith("branch refs/heads/crew/release-") && path) {
|
|
6538
6638
|
gitOk(cwd, ["worktree", "remove", "--force", path]);
|
|
6539
|
-
const root =
|
|
6540
|
-
if (basename5(root).startsWith("crew-release-"))
|
|
6639
|
+
const root = dirname7(path);
|
|
6640
|
+
if (basename5(root).startsWith("crew-release-")) rmSync8(root, { recursive: true, force: true });
|
|
6541
6641
|
}
|
|
6542
6642
|
}
|
|
6543
6643
|
const stranded = (gitOk(cwd, ["for-each-ref", "--format=%(refname:short)", "refs/heads/crew/release-*"]) ?? "").split("\n").filter(Boolean);
|
|
@@ -6548,18 +6648,18 @@ function sweepStaleReleaseRoots(tmp, now = Date.now(), maxAgeMs = 60 * 60 * 1e3)
|
|
|
6548
6648
|
const removed = [];
|
|
6549
6649
|
let names;
|
|
6550
6650
|
try {
|
|
6551
|
-
names =
|
|
6651
|
+
names = readdirSync5(tmp);
|
|
6552
6652
|
} catch {
|
|
6553
6653
|
return removed;
|
|
6554
6654
|
}
|
|
6555
6655
|
for (const name of names) {
|
|
6556
6656
|
if (!name.startsWith("crew-release-")) continue;
|
|
6557
|
-
const root =
|
|
6657
|
+
const root = join15(tmp, name);
|
|
6558
6658
|
try {
|
|
6559
|
-
const st =
|
|
6659
|
+
const st = statSync6(root);
|
|
6560
6660
|
if (!st.isDirectory() || now - st.mtimeMs < maxAgeMs) continue;
|
|
6561
|
-
if (
|
|
6562
|
-
|
|
6661
|
+
if (existsSync15(join15(root, "checkout"))) continue;
|
|
6662
|
+
rmSync8(root, { recursive: true, force: true });
|
|
6563
6663
|
removed.push(name);
|
|
6564
6664
|
} catch {
|
|
6565
6665
|
}
|
|
@@ -6601,13 +6701,13 @@ function originUrlOf(dir, remoteName) {
|
|
|
6601
6701
|
}
|
|
6602
6702
|
}
|
|
6603
6703
|
function ensureRepoCheckout(dir, remote, opts = {}) {
|
|
6604
|
-
if (
|
|
6704
|
+
if (existsSync15(`${dir}/.git`)) return false;
|
|
6605
6705
|
if (!remote) {
|
|
6606
6706
|
throw new GitError(
|
|
6607
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`
|
|
6608
6708
|
);
|
|
6609
6709
|
}
|
|
6610
|
-
|
|
6710
|
+
mkdirSync8(dirname7(dir), { recursive: true });
|
|
6611
6711
|
const url = cloneUrlFor(remote, opts);
|
|
6612
6712
|
try {
|
|
6613
6713
|
execFileSync2("git", ["clone", url, dir], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
@@ -6616,8 +6716,13 @@ function ensureRepoCheckout(dir, remote, opts = {}) {
|
|
|
6616
6716
|
const why = [err.stderr, err.stdout].map(streamText).filter(Boolean).join(" / ");
|
|
6617
6717
|
throw new GitError(`git clone ${url} ${dir}: ${why || err.message.trim()}`);
|
|
6618
6718
|
}
|
|
6719
|
+
cloneRecorder?.(dir);
|
|
6619
6720
|
return true;
|
|
6620
6721
|
}
|
|
6722
|
+
var cloneRecorder;
|
|
6723
|
+
function setCloneRecorder(fn) {
|
|
6724
|
+
cloneRecorder = fn;
|
|
6725
|
+
}
|
|
6621
6726
|
var CHECKOUT_LOCK_STALE_MS = 5 * 60 * 1e3;
|
|
6622
6727
|
function checkoutLockPath(dir) {
|
|
6623
6728
|
return `${dir}.sync.lock`;
|
|
@@ -6627,7 +6732,7 @@ function sleepSync(ms2) {
|
|
|
6627
6732
|
}
|
|
6628
6733
|
function acquireCheckoutLock(dir, timeoutMs = 3e4) {
|
|
6629
6734
|
const lockPath = checkoutLockPath(dir);
|
|
6630
|
-
|
|
6735
|
+
mkdirSync8(dirname7(lockPath), { recursive: true });
|
|
6631
6736
|
const deadline = Date.now() + timeoutMs;
|
|
6632
6737
|
for (; ; ) {
|
|
6633
6738
|
try {
|
|
@@ -6645,7 +6750,7 @@ function acquireCheckoutLock(dir, timeoutMs = 3e4) {
|
|
|
6645
6750
|
const err = e;
|
|
6646
6751
|
if (err.code !== "EEXIST") throw err;
|
|
6647
6752
|
try {
|
|
6648
|
-
if (Date.now() -
|
|
6753
|
+
if (Date.now() - statSync6(lockPath).mtimeMs > CHECKOUT_LOCK_STALE_MS) {
|
|
6649
6754
|
try {
|
|
6650
6755
|
unlinkSync2(lockPath);
|
|
6651
6756
|
} catch {
|
|
@@ -6664,17 +6769,17 @@ function acquireCheckoutLock(dir, timeoutMs = 3e4) {
|
|
|
6664
6769
|
function syncPassengerCheckout(dir, githubRemote, gitRemoteName, base, clone = {}) {
|
|
6665
6770
|
const release = acquireCheckoutLock(dir);
|
|
6666
6771
|
try {
|
|
6667
|
-
if (!
|
|
6772
|
+
if (!existsSync15(`${dir}/.git`) && githubRemote) {
|
|
6668
6773
|
const url = cloneUrlFor(githubRemote, clone);
|
|
6669
6774
|
const marker = cloneFailureMarker(dir);
|
|
6670
|
-
if (
|
|
6671
|
-
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 };
|
|
6672
6777
|
}
|
|
6673
6778
|
try {
|
|
6674
6779
|
ensureRepoCheckout(dir, githubRemote, clone);
|
|
6675
6780
|
} catch (e) {
|
|
6676
6781
|
if (!(e instanceof GitError)) throw e;
|
|
6677
|
-
|
|
6782
|
+
writeFileSync9(marker, `${url}
|
|
6678
6783
|
${e.message}`);
|
|
6679
6784
|
return { action: "clone-failed", detail: e.message, repeat: false };
|
|
6680
6785
|
}
|
|
@@ -6872,7 +6977,7 @@ function noteHeldWorktree(state, h, nowMs = Date.now()) {
|
|
|
6872
6977
|
function reconcileHeldWorktrees(state, repo, log, stillRefused) {
|
|
6873
6978
|
for (const i of state.shipAttention().previous()) {
|
|
6874
6979
|
if (i.kind !== "worktree_held" || !i.held || i.held.repo !== repo) continue;
|
|
6875
|
-
const gone = !
|
|
6980
|
+
const gone = !existsSync16(i.held.path);
|
|
6876
6981
|
if (!gone && !(stillRefused && !stillRefused.has(i.held.path))) continue;
|
|
6877
6982
|
clearShipAttention(state, i.key);
|
|
6878
6983
|
log.emit(
|
|
@@ -8586,8 +8691,8 @@ function materializeBranchForTicket(dir, cfg, t, role, onProblem) {
|
|
|
8586
8691
|
}
|
|
8587
8692
|
|
|
8588
8693
|
// src/poll.ts
|
|
8589
|
-
import { writeFileSync as
|
|
8590
|
-
import { join as
|
|
8694
|
+
import { writeFileSync as writeFileSync10 } from "fs";
|
|
8695
|
+
import { join as join16 } from "path";
|
|
8591
8696
|
|
|
8592
8697
|
// src/stalled.ts
|
|
8593
8698
|
var STALLED_AFTER_CYCLES = 30;
|
|
@@ -8876,7 +8981,7 @@ async function decideCycle(o) {
|
|
|
8876
8981
|
}
|
|
8877
8982
|
return decision;
|
|
8878
8983
|
}
|
|
8879
|
-
var digestPath = (stateDir, route, role) =>
|
|
8984
|
+
var digestPath = (stateDir, route, role) => join16(stateDir, `digest-${route}-${role}.md`);
|
|
8880
8985
|
function writeDigest(o, d, role, stateDir) {
|
|
8881
8986
|
const me = o.route.resolved?.seats[role];
|
|
8882
8987
|
if (!me) return false;
|
|
@@ -8940,7 +9045,7 @@ function writeDigest(o, d, role, stateDir) {
|
|
|
8940
9045
|
};
|
|
8941
9046
|
const text = role === "qa" ? qaDigest(input) : buildingDigest(input);
|
|
8942
9047
|
const path = digestPath(stateDir, routeSlug(o.route.route), role);
|
|
8943
|
-
|
|
9048
|
+
writeFileSync10(path, text);
|
|
8944
9049
|
o.emit.emit(`queue digest written for ${role} (${Buffer.byteLength(text)} bytes)`, {
|
|
8945
9050
|
data: { role, bytes: Buffer.byteLength(text) }
|
|
8946
9051
|
});
|
|
@@ -9317,13 +9422,13 @@ async function applyStrandedVerified(writer, s, contract, operatorId, memberId,
|
|
|
9317
9422
|
}
|
|
9318
9423
|
|
|
9319
9424
|
// src/run-worktree.ts
|
|
9320
|
-
import { existsSync as
|
|
9321
|
-
import { dirname as
|
|
9425
|
+
import { existsSync as existsSync18 } from "fs";
|
|
9426
|
+
import { dirname as dirname8, join as join18 } from "path";
|
|
9322
9427
|
|
|
9323
9428
|
// src/ports.ts
|
|
9324
9429
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
9325
|
-
import { existsSync as
|
|
9326
|
-
import { join as
|
|
9430
|
+
import { existsSync as existsSync17 } from "fs";
|
|
9431
|
+
import { join as join17 } from "path";
|
|
9327
9432
|
var run = (cmd, args) => {
|
|
9328
9433
|
try {
|
|
9329
9434
|
return execFileSync3(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
@@ -9368,7 +9473,7 @@ function ticketForPort(port, s = DEFAULT_PORTS) {
|
|
|
9368
9473
|
}
|
|
9369
9474
|
function worktreeExists(parent, prefix, n) {
|
|
9370
9475
|
for (const name of [`${prefix}${n}`, `${prefix}${String(n).padStart(3, "0")}`]) {
|
|
9371
|
-
if (
|
|
9476
|
+
if (existsSync17(join17(parent, name, ".git"))) return true;
|
|
9372
9477
|
}
|
|
9373
9478
|
return false;
|
|
9374
9479
|
}
|
|
@@ -9444,7 +9549,7 @@ function planRunWorktree(dir, cfg, t, role, opts = {}) {
|
|
|
9444
9549
|
const existing = worktreeForNumber(dir, num);
|
|
9445
9550
|
const located = locateBranchForTicket(dir, cfg, t, role);
|
|
9446
9551
|
const baseRef = hasRemote ? `${remote}/${base}` : base;
|
|
9447
|
-
if (existing && !existing.prunable &&
|
|
9552
|
+
if (existing && !existing.prunable && existsSync18(existing.path)) {
|
|
9448
9553
|
const branch = existing.branch;
|
|
9449
9554
|
if (status(existing.path).length > 0) {
|
|
9450
9555
|
return { action: opts.resumeInterrupted ? "resume-dirty" : "leave-dirty", path: existing.path, branch };
|
|
@@ -9460,7 +9565,7 @@ function planRunWorktree(dir, cfg, t, role, opts = {}) {
|
|
|
9460
9565
|
if (countCommits(existing.path, `${baseRef}..HEAD`) > 0) return { action: "leave-unpushed", path: existing.path, branch };
|
|
9461
9566
|
return { action: "recut", path: existing.path, branch, base: baseRef };
|
|
9462
9567
|
}
|
|
9463
|
-
const path =
|
|
9568
|
+
const path = join18(dirname8(dir), worktreeForTicket(dir, cfg, t, role));
|
|
9464
9569
|
if (located?.where === "remote" || located && hasRemote && remoteBranchExists(dir, remote, located.branch)) {
|
|
9465
9570
|
return { action: "cut-remote", path, branch: located.branch };
|
|
9466
9571
|
}
|
|
@@ -9509,7 +9614,7 @@ function worktreeHoldsUnpushed(path, remote) {
|
|
|
9509
9614
|
}
|
|
9510
9615
|
async function finishRunWorktree(dir, cfg, branch) {
|
|
9511
9616
|
const wt = worktrees(dir).find((w) => w.branch === branch && !w.prunable && w.path !== dir);
|
|
9512
|
-
if (!wt || !
|
|
9617
|
+
if (!wt || !existsSync18(wt.path)) return { kind: "none" };
|
|
9513
9618
|
if (cfg.hooks.handoff) return { kind: "kept-handoff", path: wt.path };
|
|
9514
9619
|
if (status(wt.path).length > 0) return { kind: "kept-dirty", path: wt.path };
|
|
9515
9620
|
if (remoteConfigured(dir, cfg.branch.remote) && worktreeHoldsUnpushed(wt.path, cfg.branch.remote)) {
|
|
@@ -9521,7 +9626,7 @@ async function finishRunWorktree(dir, cfg, branch) {
|
|
|
9521
9626
|
if (pids.length) await killGently(pids);
|
|
9522
9627
|
}
|
|
9523
9628
|
gitOk(dir, ["worktree", "remove", wt.path]);
|
|
9524
|
-
return
|
|
9629
|
+
return existsSync18(wt.path) ? { kind: "kept-dirty", path: wt.path } : { kind: "removed", path: wt.path };
|
|
9525
9630
|
}
|
|
9526
9631
|
|
|
9527
9632
|
// src/handoff-guard.ts
|
|
@@ -9782,13 +9887,13 @@ async function guardRunStrays(route, ship, tracker, role, workingId, since2, log
|
|
|
9782
9887
|
}
|
|
9783
9888
|
|
|
9784
9889
|
// src/connect-wizard.ts
|
|
9785
|
-
import { delimiter, dirname as
|
|
9786
|
-
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";
|
|
9787
9892
|
import { stringify } from "yaml";
|
|
9788
9893
|
var HOST_PASSENGERS_DOCS_URL = "https://crew.tablation.dev/host-passengers";
|
|
9789
9894
|
function shouldRunWizard(opts) {
|
|
9790
9895
|
if (!opts.isTTY) return false;
|
|
9791
|
-
const exists = opts.configExists ??
|
|
9896
|
+
const exists = opts.configExists ?? existsSync19;
|
|
9792
9897
|
return !configSearchPath(opts.crewHome).some((p) => exists(p));
|
|
9793
9898
|
}
|
|
9794
9899
|
function firstRunConfigPath() {
|
|
@@ -9805,7 +9910,7 @@ function defaultIsExecutable(p) {
|
|
|
9805
9910
|
function findOnPath2(name, pathEnv, isExecutable = defaultIsExecutable) {
|
|
9806
9911
|
for (const dir of pathEnv.split(delimiter)) {
|
|
9807
9912
|
if (!dir) continue;
|
|
9808
|
-
const candidate =
|
|
9913
|
+
const candidate = join19(dir, name);
|
|
9809
9914
|
if (isExecutable(candidate)) return candidate;
|
|
9810
9915
|
}
|
|
9811
9916
|
return void 0;
|
|
@@ -9831,9 +9936,9 @@ async function runWizard(p, env) {
|
|
|
9831
9936
|
if (entered && isExec(entered)) agentBin = entered;
|
|
9832
9937
|
else p.say(entered ? `${entered} is not an executable file.` : "A path is required.");
|
|
9833
9938
|
}
|
|
9834
|
-
const nodeDir =
|
|
9939
|
+
const nodeDir = dirname9(env.nodePath);
|
|
9835
9940
|
const pnpmPath = findOnPath2("pnpm", env.pathEnv, isExec);
|
|
9836
|
-
const baseDirs = [nodeDir, pnpmPath ?
|
|
9941
|
+
const baseDirs = [nodeDir, pnpmPath ? dirname9(pnpmPath) : void 0];
|
|
9837
9942
|
p.say(
|
|
9838
9943
|
`
|
|
9839
9944
|
Host Passengers lets this machine run the agent sessions that other people's
|
|
@@ -9844,7 +9949,7 @@ Tablation workspaces ask for, in a container. More: ${HOST_PASSENGERS_DOCS_URL}`
|
|
|
9844
9949
|
p.say("Docker is not available here, so nothing will be hosted until it is. Your answer is recorded anyway.");
|
|
9845
9950
|
}
|
|
9846
9951
|
const dockerPath = hostPassengers ? findOnPath2("docker", env.pathEnv, isExec) : void 0;
|
|
9847
|
-
const extraPath = deriveExtraPath([...baseDirs, dockerPath ?
|
|
9952
|
+
const extraPath = deriveExtraPath([...baseDirs, dockerPath ? dirname9(dockerPath) : void 0]);
|
|
9848
9953
|
if (extraPath) {
|
|
9849
9954
|
p.say(`Hooks will get ${extraPath} on their PATH (where node, pnpm${dockerPath ? " and docker" : ""} live).`);
|
|
9850
9955
|
}
|
|
@@ -9866,9 +9971,9 @@ routes:
|
|
|
9866
9971
|
${routeBlock}`;
|
|
9867
9972
|
}
|
|
9868
9973
|
function writeNewConfig(path, text) {
|
|
9869
|
-
|
|
9974
|
+
mkdirSync9(dirname9(path), { recursive: true });
|
|
9870
9975
|
try {
|
|
9871
|
-
|
|
9976
|
+
writeFileSync11(path, text, { mode: 384, flag: "wx" });
|
|
9872
9977
|
return true;
|
|
9873
9978
|
} catch (e) {
|
|
9874
9979
|
if (e.code === "EEXIST") return false;
|
|
@@ -9901,7 +10006,7 @@ Wrote ${o.configPath}.
|
|
|
9901
10006
|
|
|
9902
10007
|
// src/connect-json.ts
|
|
9903
10008
|
import { accessSync as accessSync3, constants as constants3 } from "fs";
|
|
9904
|
-
import { dirname as
|
|
10009
|
+
import { dirname as dirname10 } from "path";
|
|
9905
10010
|
var CHOICE_EXIT_CODE = 3;
|
|
9906
10011
|
function createEmitter(json, write) {
|
|
9907
10012
|
return {
|
|
@@ -9949,7 +10054,7 @@ function flagWizardAnswers(f, env) {
|
|
|
9949
10054
|
if (f.agentBin && !isExec(f.agentBin)) return { error: `--agent-bin ${f.agentBin} is not an executable file` };
|
|
9950
10055
|
const pnpmPath = findOnPath2("pnpm", env.pathEnv, isExec);
|
|
9951
10056
|
const dockerPath = f.hostPassengers ? findOnPath2("docker", env.pathEnv, isExec) : void 0;
|
|
9952
|
-
const extraPath = f.extraPath ?? deriveExtraPath([
|
|
10057
|
+
const extraPath = f.extraPath ?? deriveExtraPath([dirname10(env.nodePath), pnpmPath ? dirname10(pnpmPath) : void 0, dockerPath ? dirname10(dockerPath) : void 0]);
|
|
9953
10058
|
return {
|
|
9954
10059
|
answers: {
|
|
9955
10060
|
name: f.shipName?.trim() || env.hostname,
|
|
@@ -9962,8 +10067,8 @@ function flagWizardAnswers(f, env) {
|
|
|
9962
10067
|
}
|
|
9963
10068
|
|
|
9964
10069
|
// src/release-run.ts
|
|
9965
|
-
import { readFileSync as
|
|
9966
|
-
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";
|
|
9967
10072
|
import { tmpdir as tmpdir3 } from "os";
|
|
9968
10073
|
|
|
9969
10074
|
// src/release.ts
|
|
@@ -10651,13 +10756,13 @@ async function bump(o, size) {
|
|
|
10651
10756
|
}
|
|
10652
10757
|
return printed;
|
|
10653
10758
|
}
|
|
10654
|
-
const files = o.repo.release.versionFiles.map((f) =>
|
|
10759
|
+
const files = o.repo.release.versionFiles.map((f) => join20(o.cwd, f));
|
|
10655
10760
|
const first = files[0];
|
|
10656
|
-
if (!
|
|
10761
|
+
if (!existsSync20(first)) {
|
|
10657
10762
|
o.emit.error(`no ${o.repo.release.versionFiles[0]} to read a version from`);
|
|
10658
10763
|
return null;
|
|
10659
10764
|
}
|
|
10660
|
-
const current = JSON.parse(
|
|
10765
|
+
const current = JSON.parse(readFileSync15(first, "utf8")).version;
|
|
10661
10766
|
if (!current) {
|
|
10662
10767
|
o.emit.error(`no version field in ${o.repo.release.versionFiles[0]}`);
|
|
10663
10768
|
return null;
|
|
@@ -10670,27 +10775,27 @@ async function bump(o, size) {
|
|
|
10670
10775
|
return version;
|
|
10671
10776
|
}
|
|
10672
10777
|
for (const f of files) {
|
|
10673
|
-
const src =
|
|
10778
|
+
const src = readFileSync15(f, "utf8");
|
|
10674
10779
|
const out = src.replace(/("version"\s*:\s*")[^"]*(")/, `$1${version}$2`);
|
|
10675
10780
|
if (out === src) {
|
|
10676
10781
|
o.emit.error(`no version field in ${f}`);
|
|
10677
10782
|
return null;
|
|
10678
10783
|
}
|
|
10679
|
-
|
|
10784
|
+
writeFileSync12(f, out);
|
|
10680
10785
|
}
|
|
10681
10786
|
return version;
|
|
10682
10787
|
}
|
|
10683
10788
|
function writeChangelog(o, version, entries) {
|
|
10684
10789
|
const file = o.repo.release.changelog;
|
|
10685
10790
|
if (!file || entries.length === 0) return;
|
|
10686
|
-
const path =
|
|
10791
|
+
const path = join20(o.cwd, file);
|
|
10687
10792
|
const section = renderChangelogSection(version, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), entries);
|
|
10688
10793
|
if (o.dryRun) {
|
|
10689
10794
|
o.emit.emit(`would prepend ${entries.length} entr(ies) to ${file}`);
|
|
10690
10795
|
return;
|
|
10691
10796
|
}
|
|
10692
|
-
const existing =
|
|
10693
|
-
|
|
10797
|
+
const existing = existsSync20(path) ? readFileSync15(path, "utf8") : "# Changelog\n";
|
|
10798
|
+
writeFileSync12(path, insertChangelogSection(existing, section));
|
|
10694
10799
|
}
|
|
10695
10800
|
async function confirm(o, expected) {
|
|
10696
10801
|
if (!o.repo.hooks.released) return void 0;
|
|
@@ -10773,7 +10878,7 @@ function closeReleaseCheckout(cwd, root, dir, branch, run2 = gitOk) {
|
|
|
10773
10878
|
run2(cwd, ["worktree", "remove", "--force", dir]);
|
|
10774
10879
|
} finally {
|
|
10775
10880
|
try {
|
|
10776
|
-
|
|
10881
|
+
rmSync9(root, { recursive: true, force: true });
|
|
10777
10882
|
} finally {
|
|
10778
10883
|
run2(cwd, ["worktree", "prune"]);
|
|
10779
10884
|
run2(cwd, ["branch", "-D", branch]);
|
|
@@ -10783,8 +10888,8 @@ function closeReleaseCheckout(cwd, root, dir, branch, run2 = gitOk) {
|
|
|
10783
10888
|
function openReleaseCheckout(o) {
|
|
10784
10889
|
const { remote, base } = o.repo.branch;
|
|
10785
10890
|
const branch = `crew/release-${Date.now()}`;
|
|
10786
|
-
const root = mkdtempSync2(
|
|
10787
|
-
const dir =
|
|
10891
|
+
const root = mkdtempSync2(join20(tmpdir3(), "crew-release-"));
|
|
10892
|
+
const dir = join20(root, "checkout");
|
|
10788
10893
|
o.emit.emit(`release checkout: ${dir}`, { step: "release" });
|
|
10789
10894
|
const close = () => closeReleaseCheckout(o.cwd, root, dir, branch);
|
|
10790
10895
|
try {
|
|
@@ -10794,10 +10899,10 @@ function openReleaseCheckout(o) {
|
|
|
10794
10899
|
throw e;
|
|
10795
10900
|
}
|
|
10796
10901
|
for (const f of o.repo.worktrees.copy) {
|
|
10797
|
-
const src =
|
|
10798
|
-
if (!
|
|
10799
|
-
|
|
10800
|
-
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));
|
|
10801
10906
|
}
|
|
10802
10907
|
return { dir, branch, close };
|
|
10803
10908
|
}
|
|
@@ -11465,7 +11570,7 @@ function missingReleaserWarning(repo, release, ships, now = Date.now()) {
|
|
|
11465
11570
|
}
|
|
11466
11571
|
|
|
11467
11572
|
// src/watch.ts
|
|
11468
|
-
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";
|
|
11469
11574
|
var E = "\x1B[";
|
|
11470
11575
|
var dim = (s) => `${E}2m${s}${E}0m`;
|
|
11471
11576
|
var bold = (s) => `${E}1m${s}${E}0m`;
|
|
@@ -11566,8 +11671,8 @@ function renderHeaders(states, now = Date.now()) {
|
|
|
11566
11671
|
return states.map((s) => renderHeader(s, now)).join("");
|
|
11567
11672
|
}
|
|
11568
11673
|
function readFrom(file, from) {
|
|
11569
|
-
if (!
|
|
11570
|
-
const size =
|
|
11674
|
+
if (!existsSync21(file)) return { events: [], offset: from };
|
|
11675
|
+
const size = statSync7(file).size;
|
|
11571
11676
|
if (size < from) return readFrom(file, 0);
|
|
11572
11677
|
if (size === from) return { events: [], offset: from };
|
|
11573
11678
|
const fd = openSync4(file, "r");
|
|
@@ -12517,9 +12622,54 @@ async function canMintAgentKeys(route, o = {}) {
|
|
|
12517
12622
|
}
|
|
12518
12623
|
}
|
|
12519
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
|
+
|
|
12520
12670
|
// src/agents-sync-run.ts
|
|
12521
|
-
import { existsSync as
|
|
12522
|
-
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";
|
|
12523
12673
|
async function runAgentsSync(target, o) {
|
|
12524
12674
|
const dryRun = o.dryRun ?? false;
|
|
12525
12675
|
const force = o.force ?? false;
|
|
@@ -12552,12 +12702,12 @@ async function runAgentsSync(target, o) {
|
|
|
12552
12702
|
for (const s of skillResult?.outcomes ?? []) o.say(describeSkillSyncOutcome(s));
|
|
12553
12703
|
if (!dryRun) {
|
|
12554
12704
|
const resolvedPath = resolvedPathFor(o.stateDir, target.route);
|
|
12555
|
-
const raw =
|
|
12705
|
+
const raw = existsSync22(resolvedPath) ? JSON.parse(readFileSync16(resolvedPath, "utf8")) : {};
|
|
12556
12706
|
raw.agentPersonas = personaResult.agentPersonas;
|
|
12557
12707
|
if (skillResult) raw.agentSkills = skillResult.agentSkills;
|
|
12558
12708
|
if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
|
|
12559
|
-
|
|
12560
|
-
|
|
12709
|
+
mkdirSync11(dirname12(resolvedPath), { recursive: true });
|
|
12710
|
+
writeFileSync13(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
12561
12711
|
`);
|
|
12562
12712
|
if (target.resolved) {
|
|
12563
12713
|
target.resolved.agentPersonas = personaResult.agentPersonas;
|
|
@@ -12659,77 +12809,12 @@ async function showLogEntry(route, entryId, opts = {}) {
|
|
|
12659
12809
|
};
|
|
12660
12810
|
}
|
|
12661
12811
|
|
|
12662
|
-
// src/
|
|
12663
|
-
import {
|
|
12664
|
-
var SAMPLE_REPO = "crew-sample";
|
|
12665
|
-
var SAMPLE_REMOTE = `local/${SAMPLE_REPO}`;
|
|
12666
|
-
var SAMPLE_MARKER = "sample: true";
|
|
12667
|
-
var SAMPLE_CREW_YAML = `version: 1
|
|
12668
|
-
platform: unix
|
|
12669
|
-
branch:
|
|
12670
|
-
base: main
|
|
12671
|
-
hooks:
|
|
12672
|
-
test: "true"
|
|
12673
|
-
build: "true"
|
|
12674
|
-
release:
|
|
12675
|
-
mode: integrate
|
|
12676
|
-
versioning: none
|
|
12677
|
-
changelog: false
|
|
12678
|
-
`;
|
|
12679
|
-
var SAMPLE_README = `# crew-sample
|
|
12680
|
-
|
|
12681
|
-
A throwaway repository created by \`crew sample-ticket\`. It exists so the crew
|
|
12682
|
-
has something safe to work on: its test and build hooks always pass, it has no
|
|
12683
|
-
remote, and \`crew sample-ticket --remove\` deletes it.
|
|
12684
|
-
`;
|
|
12685
|
-
function sampleDir(existing, reposBase, workspace) {
|
|
12686
|
-
if (existing.length > 0) return join20(dirname12(existing[0]), SAMPLE_REPO);
|
|
12687
|
-
return join20(reposBase, workspace, SAMPLE_REPO);
|
|
12688
|
-
}
|
|
12689
|
-
var SAMPLE_TITLE = "Sample: add a documented `greet` function with a test";
|
|
12690
|
-
function sampleDescription() {
|
|
12691
|
-
return [
|
|
12692
|
-
SAMPLE_MARKER,
|
|
12693
|
-
"",
|
|
12694
|
-
"> This is a sample ticket filed by `crew sample-ticket` on a throwaway repository. Nothing here is real work.",
|
|
12695
|
-
"",
|
|
12696
|
-
"## What",
|
|
12697
|
-
"",
|
|
12698
|
-
"Add `greet.js` exporting `greet(name)`, which returns `Hello, <name>!` (and `Hello, world!` for no name),",
|
|
12699
|
-
"with a JSDoc comment, and `greet.test.js` covering both cases using `node:test`.",
|
|
12700
|
-
"",
|
|
12701
|
-
"## Acceptance",
|
|
12702
|
-
"",
|
|
12703
|
-
'- `greet("Ada")` returns `Hello, Ada!`; `greet()` returns `Hello, world!`.',
|
|
12704
|
-
"- The test file runs with `node --test` and passes.",
|
|
12705
|
-
"- Add a line to the README saying the function exists.",
|
|
12706
|
-
"",
|
|
12707
|
-
"## Notes",
|
|
12708
|
-
"",
|
|
12709
|
-
"The repo has no remote: leave the branch local. The hooks in `.crew.yaml` always pass, so QA is checking the",
|
|
12710
|
-
"diff against the acceptance list above."
|
|
12711
|
-
].join("\n");
|
|
12712
|
-
}
|
|
12713
|
-
function isSampleTicket(description) {
|
|
12714
|
-
return (description ?? "").trimStart().startsWith(SAMPLE_MARKER);
|
|
12715
|
-
}
|
|
12716
|
-
function planSampleTicket(i) {
|
|
12717
|
-
return {
|
|
12718
|
-
title: SAMPLE_TITLE,
|
|
12719
|
-
description: sampleDescription(),
|
|
12720
|
-
report_type: "feature",
|
|
12721
|
-
severity: "s4",
|
|
12722
|
-
reporter_name: "crew sample-ticket",
|
|
12723
|
-
status: i.approvedStatus,
|
|
12724
|
-
project_id: i.projectId,
|
|
12725
|
-
repo_id: i.repoId,
|
|
12726
|
-
...i.filedBy ? { filed_by_id: i.filedBy } : {}
|
|
12727
|
-
};
|
|
12728
|
-
}
|
|
12812
|
+
// src/repos-hooks.ts
|
|
12813
|
+
import { isMap as isMap2, parseDocument as parseDocument2, YAMLMap } from "yaml";
|
|
12729
12814
|
|
|
12730
12815
|
// src/repos-cmd.ts
|
|
12731
12816
|
import { homedir as homedir7 } from "os";
|
|
12732
|
-
import { basename as
|
|
12817
|
+
import { basename as basename7, join as join21, resolve as resolve3 } from "path";
|
|
12733
12818
|
import { isMap, isSeq, parseDocument, stringify as stringify2 } from "yaml";
|
|
12734
12819
|
var ReposError = class extends Error {
|
|
12735
12820
|
};
|
|
@@ -12801,7 +12886,7 @@ function configuredRepos(text, route, base) {
|
|
|
12801
12886
|
});
|
|
12802
12887
|
}
|
|
12803
12888
|
const dir = m.get("dir");
|
|
12804
|
-
return dir ? [{ name:
|
|
12889
|
+
return dir ? [{ name: basename7(String(dir)), dir: expandDir(String(dir), base) }] : [];
|
|
12805
12890
|
}
|
|
12806
12891
|
function planRepoAdd(i) {
|
|
12807
12892
|
const doc = parseDocument(i.text);
|
|
@@ -12813,7 +12898,7 @@ function planRepoAdd(i) {
|
|
|
12813
12898
|
throw new ReposError(`${target} is already configured on route ${i.route}`);
|
|
12814
12899
|
}
|
|
12815
12900
|
const matched = matchRepoName(i.origin, i.resolved);
|
|
12816
|
-
const name = matched ?? i.name ?? normalizeRemote(i.origin)?.split("/").pop() ??
|
|
12901
|
+
const name = matched ?? i.name ?? normalizeRemote(i.origin)?.split("/").pop() ?? basename7(target);
|
|
12817
12902
|
let warning;
|
|
12818
12903
|
if (!matched) {
|
|
12819
12904
|
const known = Object.values(i.resolved?.repoNames ?? {});
|
|
@@ -12932,8 +13017,213 @@ function planRepoClone(i) {
|
|
|
12932
13017
|
return { ...add, remote, target };
|
|
12933
13018
|
}
|
|
12934
13019
|
|
|
13020
|
+
// src/repos-hooks.ts
|
|
13021
|
+
function packageManager(f) {
|
|
13022
|
+
if (f.exists("pnpm-lock.yaml")) return "pnpm";
|
|
13023
|
+
if (f.exists("yarn.lock")) return "yarn";
|
|
13024
|
+
if (f.exists("bun.lock") || f.exists("bun.lockb")) return "bun";
|
|
13025
|
+
return "npm";
|
|
13026
|
+
}
|
|
13027
|
+
function fromPackageJson(f) {
|
|
13028
|
+
const text = f.read("package.json");
|
|
13029
|
+
if (text === void 0) return void 0;
|
|
13030
|
+
let pkg;
|
|
13031
|
+
try {
|
|
13032
|
+
pkg = JSON.parse(text);
|
|
13033
|
+
} catch {
|
|
13034
|
+
return void 0;
|
|
13035
|
+
}
|
|
13036
|
+
if (!pkg || typeof pkg !== "object") return void 0;
|
|
13037
|
+
const pm = packageManager(f);
|
|
13038
|
+
const scripts = pkg.scripts && typeof pkg.scripts === "object" ? pkg.scripts : {};
|
|
13039
|
+
const out = {};
|
|
13040
|
+
if (typeof scripts.test === "string") out.test = `${pm} test`;
|
|
13041
|
+
if (typeof scripts.build === "string") out.build = pm === "yarn" ? "yarn build" : `${pm} run build`;
|
|
13042
|
+
if (typeof pkg.version === "string") {
|
|
13043
|
+
out.version = `node -p "require('./package.json').version"`;
|
|
13044
|
+
out.bump = `npm version "$CREW_BUMP" --no-git-tag-version | sed 's/^v//'`;
|
|
13045
|
+
}
|
|
13046
|
+
return out;
|
|
13047
|
+
}
|
|
13048
|
+
function fromPackageSwift(f) {
|
|
13049
|
+
return f.exists("Package.swift") ? { test: "swift test", build: "swift build" } : void 0;
|
|
13050
|
+
}
|
|
13051
|
+
function fromPyproject(f) {
|
|
13052
|
+
return f.exists("pyproject.toml") ? { test: "pytest" } : void 0;
|
|
13053
|
+
}
|
|
13054
|
+
function fromMakefile(f) {
|
|
13055
|
+
const text = f.read("Makefile") ?? f.read("makefile") ?? f.read("GNUmakefile");
|
|
13056
|
+
if (text === void 0) return void 0;
|
|
13057
|
+
const out = {};
|
|
13058
|
+
for (const target of ["test", "build"]) {
|
|
13059
|
+
if (new RegExp(`^${target}\\s*:`, "m").test(text)) out[target] = `make ${target}`;
|
|
13060
|
+
}
|
|
13061
|
+
return out;
|
|
13062
|
+
}
|
|
13063
|
+
var DETECTORS = [
|
|
13064
|
+
{ source: "package.json", detect: fromPackageJson },
|
|
13065
|
+
{ source: "Package.swift", detect: fromPackageSwift },
|
|
13066
|
+
{ source: "pyproject.toml", detect: fromPyproject },
|
|
13067
|
+
{ source: "Makefile", detect: fromMakefile }
|
|
13068
|
+
];
|
|
13069
|
+
function suggestHooks(f) {
|
|
13070
|
+
const found = DETECTORS.flatMap((d) => {
|
|
13071
|
+
const hooks = d.detect(f);
|
|
13072
|
+
return hooks && Object.keys(hooks).length > 0 ? [{ source: d.source, hooks }] : [];
|
|
13073
|
+
});
|
|
13074
|
+
if (found.length === 0) {
|
|
13075
|
+
return { source: null, hooks: {}, notes: ["no package.json scripts, Package.swift, pyproject.toml or Makefile test/build targets found \u2014 nothing to propose"] };
|
|
13076
|
+
}
|
|
13077
|
+
const [first, ...others] = found;
|
|
13078
|
+
const notes = others.length > 0 ? [`also found ${others.map((o) => o.source).join(", ")}; proposing from ${first.source} only`] : [];
|
|
13079
|
+
return { source: first.source, hooks: first.hooks, notes };
|
|
13080
|
+
}
|
|
13081
|
+
function parseHooksArgs(args, routes) {
|
|
13082
|
+
const rest = [...args];
|
|
13083
|
+
let route;
|
|
13084
|
+
if (rest.length > 1 && routes.includes(rest[0])) route = rest.shift();
|
|
13085
|
+
if (!route) {
|
|
13086
|
+
if (routes.length === 1) route = routes[0];
|
|
13087
|
+
else throw new ReposError(routes.length === 0 ? "the config has no routes \u2014 run `crew connect` first" : `several routes are configured; name one: ${routes.join(", ")}`);
|
|
13088
|
+
}
|
|
13089
|
+
if (rest.length !== 1) throw new ReposError("expected: crew repos hooks [route] <name> [--suggest] [--write route|repo] [--json]");
|
|
13090
|
+
return { route, name: rest[0] };
|
|
13091
|
+
}
|
|
13092
|
+
function repoDirOf(text, base, route, name) {
|
|
13093
|
+
const repos = configuredRepos(text, route, base);
|
|
13094
|
+
const hit = repos.find((r) => r.name === name);
|
|
13095
|
+
if (!hit) {
|
|
13096
|
+
throw new ReposError(`route ${route} has no repo named "${name}" (have: ${repos.map((r) => r.name).join(", ") || "none"})`);
|
|
13097
|
+
}
|
|
13098
|
+
return hit.dir;
|
|
13099
|
+
}
|
|
13100
|
+
function planRouteHooks(text, route, name, hooks) {
|
|
13101
|
+
const doc = parseDocument2(text);
|
|
13102
|
+
if (doc.errors.length > 0) throw new ReposError(`config is not valid YAML \u2014 ${doc.errors[0].message}`);
|
|
13103
|
+
const m = routeMapOf(doc, route);
|
|
13104
|
+
const repos = m.get("repos", true);
|
|
13105
|
+
let target = m;
|
|
13106
|
+
if (isMap2(repos)) {
|
|
13107
|
+
const entry = repos.get(name, true);
|
|
13108
|
+
if (isMap2(entry)) target = entry;
|
|
13109
|
+
else {
|
|
13110
|
+
const created = new YAMLMap();
|
|
13111
|
+
created.set("dir", String(repos.get(name)));
|
|
13112
|
+
repos.set(name, created);
|
|
13113
|
+
target = created;
|
|
13114
|
+
}
|
|
13115
|
+
}
|
|
13116
|
+
const current = target.get("hooks", true);
|
|
13117
|
+
let hooksMap;
|
|
13118
|
+
if (isMap2(current)) hooksMap = current;
|
|
13119
|
+
else {
|
|
13120
|
+
hooksMap = new YAMLMap();
|
|
13121
|
+
target.set("hooks", hooksMap);
|
|
13122
|
+
}
|
|
13123
|
+
const written = [];
|
|
13124
|
+
const kept = [];
|
|
13125
|
+
for (const [k, v] of Object.entries(hooks)) {
|
|
13126
|
+
if (v === void 0) continue;
|
|
13127
|
+
if (hooksMap.has(k)) kept.push(k);
|
|
13128
|
+
else {
|
|
13129
|
+
hooksMap.set(k, v);
|
|
13130
|
+
written.push(k);
|
|
13131
|
+
}
|
|
13132
|
+
}
|
|
13133
|
+
return { text: String(doc), written, kept };
|
|
13134
|
+
}
|
|
13135
|
+
function starterRepoConfig(hooks) {
|
|
13136
|
+
const lines = [
|
|
13137
|
+
"# Starter .crew.yaml proposed by `crew repos hooks --write repo`.",
|
|
13138
|
+
"# Review the hooks, then commit this file; it is the durable home for them",
|
|
13139
|
+
"# (docs/REPO_SPEC.md). A route-level copy in crew.yaml is shadowed by it.",
|
|
13140
|
+
`version: ${SPEC_VERSION}`,
|
|
13141
|
+
"platform: any",
|
|
13142
|
+
"",
|
|
13143
|
+
"hooks:"
|
|
13144
|
+
];
|
|
13145
|
+
const entries = Object.entries(hooks).filter(([, v]) => v !== void 0);
|
|
13146
|
+
if (entries.length === 0) lines.push(' # test: "..."', ' # build: "..."');
|
|
13147
|
+
for (const [k, v] of entries) lines.push(` ${k}: ${JSON.stringify(v)}`);
|
|
13148
|
+
lines.push("", "release:", " # integrate: merging to the base branch is the release; nothing is deployed.", " mode: integrate", "");
|
|
13149
|
+
const text = lines.join("\n");
|
|
13150
|
+
parseRepoConfig(text, ".crew.yaml");
|
|
13151
|
+
return text;
|
|
13152
|
+
}
|
|
13153
|
+
function existingRepoConfig(f) {
|
|
13154
|
+
return REPO_CONFIG_FILENAMES.find((n) => f.exists(n));
|
|
13155
|
+
}
|
|
13156
|
+
|
|
13157
|
+
// src/sample-ticket.ts
|
|
13158
|
+
import { basename as basename8, dirname as dirname14, join as join22 } from "path";
|
|
13159
|
+
var SAMPLE_REPO = "crew-sample";
|
|
13160
|
+
var SAMPLE_REMOTE = `local/${SAMPLE_REPO}`;
|
|
13161
|
+
var SAMPLE_MARKER = "sample: true";
|
|
13162
|
+
var SAMPLE_CREW_YAML = `version: 1
|
|
13163
|
+
platform: unix
|
|
13164
|
+
branch:
|
|
13165
|
+
base: main
|
|
13166
|
+
hooks:
|
|
13167
|
+
test: "true"
|
|
13168
|
+
build: "true"
|
|
13169
|
+
release:
|
|
13170
|
+
mode: integrate
|
|
13171
|
+
versioning: none
|
|
13172
|
+
changelog: false
|
|
13173
|
+
`;
|
|
13174
|
+
var SAMPLE_README = `# crew-sample
|
|
13175
|
+
|
|
13176
|
+
A throwaway repository created by \`crew sample-ticket\`. It exists so the crew
|
|
13177
|
+
has something safe to work on: its test and build hooks always pass, it has no
|
|
13178
|
+
remote, and \`crew sample-ticket --remove\` deletes it.
|
|
13179
|
+
`;
|
|
13180
|
+
function sampleDir(existing, reposBase, workspace) {
|
|
13181
|
+
if (existing.length > 0) return join22(dirname14(existing[0]), SAMPLE_REPO);
|
|
13182
|
+
return join22(reposBase, workspace, SAMPLE_REPO);
|
|
13183
|
+
}
|
|
13184
|
+
var SAMPLE_TITLE = "Sample: add a documented `greet` function with a test";
|
|
13185
|
+
function sampleDescription() {
|
|
13186
|
+
return [
|
|
13187
|
+
SAMPLE_MARKER,
|
|
13188
|
+
"",
|
|
13189
|
+
"> This is a sample ticket filed by `crew sample-ticket` on a throwaway repository. Nothing here is real work.",
|
|
13190
|
+
"",
|
|
13191
|
+
"## What",
|
|
13192
|
+
"",
|
|
13193
|
+
"Add `greet.js` exporting `greet(name)`, which returns `Hello, <name>!` (and `Hello, world!` for no name),",
|
|
13194
|
+
"with a JSDoc comment, and `greet.test.js` covering both cases using `node:test`.",
|
|
13195
|
+
"",
|
|
13196
|
+
"## Acceptance",
|
|
13197
|
+
"",
|
|
13198
|
+
'- `greet("Ada")` returns `Hello, Ada!`; `greet()` returns `Hello, world!`.',
|
|
13199
|
+
"- The test file runs with `node --test` and passes.",
|
|
13200
|
+
"- Add a line to the README saying the function exists.",
|
|
13201
|
+
"",
|
|
13202
|
+
"## Notes",
|
|
13203
|
+
"",
|
|
13204
|
+
"The repo has no remote: leave the branch local. The hooks in `.crew.yaml` always pass, so QA is checking the",
|
|
13205
|
+
"diff against the acceptance list above."
|
|
13206
|
+
].join("\n");
|
|
13207
|
+
}
|
|
13208
|
+
function isSampleTicket(description) {
|
|
13209
|
+
return (description ?? "").trimStart().startsWith(SAMPLE_MARKER);
|
|
13210
|
+
}
|
|
13211
|
+
function planSampleTicket(i) {
|
|
13212
|
+
return {
|
|
13213
|
+
title: SAMPLE_TITLE,
|
|
13214
|
+
description: sampleDescription(),
|
|
13215
|
+
report_type: "feature",
|
|
13216
|
+
severity: "s4",
|
|
13217
|
+
reporter_name: "crew sample-ticket",
|
|
13218
|
+
status: i.approvedStatus,
|
|
13219
|
+
project_id: i.projectId,
|
|
13220
|
+
repo_id: i.repoId,
|
|
13221
|
+
...i.filedBy ? { filed_by_id: i.filedBy } : {}
|
|
13222
|
+
};
|
|
13223
|
+
}
|
|
13224
|
+
|
|
12935
13225
|
// src/doctor-fix.ts
|
|
12936
|
-
import { parseDocument as
|
|
13226
|
+
import { parseDocument as parseDocument3 } from "yaml";
|
|
12937
13227
|
var sound = (r) => r.hasRepos && r.problems.length === 0;
|
|
12938
13228
|
function planFix(f) {
|
|
12939
13229
|
const actions = [];
|
|
@@ -12960,7 +13250,7 @@ function routesInScope(routes, named) {
|
|
|
12960
13250
|
return named ? routes.filter((r) => r.route === named) : routes;
|
|
12961
13251
|
}
|
|
12962
13252
|
function planEnable(text, route) {
|
|
12963
|
-
const doc =
|
|
13253
|
+
const doc = parseDocument3(text);
|
|
12964
13254
|
routeMapOf(doc, route).set("enabled", true);
|
|
12965
13255
|
return String(doc);
|
|
12966
13256
|
}
|
|
@@ -12970,8 +13260,8 @@ function schedulerInstalled(plan, exists, crontab) {
|
|
|
12970
13260
|
}
|
|
12971
13261
|
|
|
12972
13262
|
// src/worktree-sweep.ts
|
|
12973
|
-
import { existsSync as
|
|
12974
|
-
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";
|
|
12975
13265
|
function planWorktreeSweep(target, tickets, contract) {
|
|
12976
13266
|
const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
|
|
12977
13267
|
const verified = contract.statuses.verified;
|
|
@@ -13025,12 +13315,12 @@ function worktreeTicketNumbers(target) {
|
|
|
13025
13315
|
return [...nums];
|
|
13026
13316
|
}
|
|
13027
13317
|
function siblingWorktreeDirs(target) {
|
|
13028
|
-
const parent =
|
|
13318
|
+
const parent = dirname15(target.dir.replace(/[/\\]+$/, ""));
|
|
13029
13319
|
const stem = `${basename9(target.dir.replace(/[/\\]+$/, ""))}-`;
|
|
13030
13320
|
const registered = new Set(worktrees(target.dir).map((w) => resolve4(w.path)));
|
|
13031
13321
|
let names;
|
|
13032
13322
|
try {
|
|
13033
|
-
names =
|
|
13323
|
+
names = readdirSync6(parent);
|
|
13034
13324
|
} catch {
|
|
13035
13325
|
return [];
|
|
13036
13326
|
}
|
|
@@ -13038,7 +13328,7 @@ function siblingWorktreeDirs(target) {
|
|
|
13038
13328
|
for (const name of names) {
|
|
13039
13329
|
if (!name.startsWith(stem)) continue;
|
|
13040
13330
|
const m = DIR_NUMBER_SUFFIXED.exec(name);
|
|
13041
|
-
const path =
|
|
13331
|
+
const path = join23(parent, name);
|
|
13042
13332
|
if (m && !registered.has(resolve4(path))) out.push({ path, number: m[1] });
|
|
13043
13333
|
}
|
|
13044
13334
|
return out;
|
|
@@ -13073,7 +13363,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
|
|
|
13073
13363
|
let removed = 0;
|
|
13074
13364
|
let keptBranches = 0;
|
|
13075
13365
|
for (const a of actions) {
|
|
13076
|
-
if (!dryRun && base && !a.keepBranch &&
|
|
13366
|
+
if (!dryRun && base && !a.keepBranch && existsSync23(a.path) && isHandBackMerge(a.path, base)) {
|
|
13077
13367
|
if (gitOk(a.path, ["merge", "--abort"]) !== null) {
|
|
13078
13368
|
log.emit(
|
|
13079
13369
|
`abandoned the hand-back merge of ${base} in ${a.path}: ${a.ticket.issue_id} deployed without it`,
|
|
@@ -13081,7 +13371,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
|
|
|
13081
13371
|
);
|
|
13082
13372
|
}
|
|
13083
13373
|
}
|
|
13084
|
-
if (!dryRun &&
|
|
13374
|
+
if (!dryRun && existsSync23(a.path)) {
|
|
13085
13375
|
let dirty = false;
|
|
13086
13376
|
try {
|
|
13087
13377
|
dirty = status(a.path).length > 0;
|
|
@@ -13092,7 +13382,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
|
|
|
13092
13382
|
continue;
|
|
13093
13383
|
}
|
|
13094
13384
|
}
|
|
13095
|
-
if (!dryRun && a.requirePushed && a.branch &&
|
|
13385
|
+
if (!dryRun && a.requirePushed && a.branch && existsSync23(a.path) && remoteConfigured(cwd, remote) && worktreeHoldsUnpushed(a.path, remote)) {
|
|
13096
13386
|
refuse(a, "unpushed commits", `${a.branch} has commits that are not on ${remote}`);
|
|
13097
13387
|
continue;
|
|
13098
13388
|
}
|
|
@@ -13141,7 +13431,7 @@ function isHandBackMerge(path, base) {
|
|
|
13141
13431
|
if (!msgPath) return false;
|
|
13142
13432
|
let first;
|
|
13143
13433
|
try {
|
|
13144
|
-
first =
|
|
13434
|
+
first = readFileSync17(resolve4(path, msgPath), "utf8").split("\n")[0] ?? "";
|
|
13145
13435
|
} catch {
|
|
13146
13436
|
return false;
|
|
13147
13437
|
}
|
|
@@ -13246,16 +13536,16 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
|
|
|
13246
13536
|
}
|
|
13247
13537
|
|
|
13248
13538
|
// src/stream-sweep.ts
|
|
13249
|
-
import { existsSync as
|
|
13250
|
-
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";
|
|
13251
13541
|
var DAY_MS2 = 24 * 60 * 60 * 1e3;
|
|
13252
13542
|
function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
|
|
13253
|
-
const dir =
|
|
13254
|
-
if (!
|
|
13543
|
+
const dir = join24(stateDir, "streams");
|
|
13544
|
+
if (!existsSync24(dir)) return [];
|
|
13255
13545
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
13256
|
-
return
|
|
13546
|
+
return readdirSync7(dir).map((name) => join24(dir, name)).filter((path) => {
|
|
13257
13547
|
try {
|
|
13258
|
-
return
|
|
13548
|
+
return statSync8(path).mtimeMs < cutoff;
|
|
13259
13549
|
} catch {
|
|
13260
13550
|
return false;
|
|
13261
13551
|
}
|
|
@@ -13269,7 +13559,7 @@ function applyStreamSweep(paths, dryRun, log) {
|
|
|
13269
13559
|
continue;
|
|
13270
13560
|
}
|
|
13271
13561
|
try {
|
|
13272
|
-
|
|
13562
|
+
rmSync10(path, { force: true });
|
|
13273
13563
|
removed++;
|
|
13274
13564
|
log.emit(`removed stream artifact ${path}`, { step: "worktree" });
|
|
13275
13565
|
} catch (e) {
|
|
@@ -13279,11 +13569,11 @@ function applyStreamSweep(paths, dryRun, log) {
|
|
|
13279
13569
|
return removed;
|
|
13280
13570
|
}
|
|
13281
13571
|
function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
13282
|
-
if (!
|
|
13572
|
+
if (!existsSync24(stateDir)) return [];
|
|
13283
13573
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
13284
|
-
return
|
|
13574
|
+
return readdirSync7(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join24(stateDir, name)).filter((path) => {
|
|
13285
13575
|
try {
|
|
13286
|
-
return
|
|
13576
|
+
return statSync8(path).mtimeMs < cutoff;
|
|
13287
13577
|
} catch {
|
|
13288
13578
|
return false;
|
|
13289
13579
|
}
|
|
@@ -13291,24 +13581,24 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
|
13291
13581
|
}
|
|
13292
13582
|
|
|
13293
13583
|
// src/cli.ts
|
|
13294
|
-
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";
|
|
13295
13585
|
import { createInterface } from "readline/promises";
|
|
13296
13586
|
import { dirname as dirOf, resolve as resolvePath } from "path";
|
|
13297
13587
|
|
|
13298
13588
|
// src/ssh-keys.ts
|
|
13299
13589
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
13300
|
-
import { existsSync as
|
|
13301
|
-
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";
|
|
13302
13592
|
var SshKeyError = class extends Error {
|
|
13303
13593
|
};
|
|
13304
13594
|
function shipSshKeyPathFor(stateDir) {
|
|
13305
|
-
return
|
|
13595
|
+
return join25(stateDir, "ssh", "id_ed25519");
|
|
13306
13596
|
}
|
|
13307
13597
|
function ensureShipSshKeypair(stateDir) {
|
|
13308
13598
|
const privateKeyPath = shipSshKeyPathFor(stateDir);
|
|
13309
13599
|
const publicKeyPath = `${privateKeyPath}.pub`;
|
|
13310
|
-
if (!
|
|
13311
|
-
|
|
13600
|
+
if (!existsSync25(privateKeyPath)) {
|
|
13601
|
+
mkdirSync12(dirname16(privateKeyPath), { recursive: true });
|
|
13312
13602
|
try {
|
|
13313
13603
|
execFileSync4("ssh-keygen", [
|
|
13314
13604
|
"-t",
|
|
@@ -13328,10 +13618,10 @@ function ensureShipSshKeypair(stateDir) {
|
|
|
13328
13618
|
}
|
|
13329
13619
|
chmodSync(privateKeyPath, 384);
|
|
13330
13620
|
}
|
|
13331
|
-
if (!
|
|
13621
|
+
if (!existsSync25(publicKeyPath)) {
|
|
13332
13622
|
throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
|
|
13333
13623
|
}
|
|
13334
|
-
const publicKey =
|
|
13624
|
+
const publicKey = readFileSync18(publicKeyPath, "utf8").trim();
|
|
13335
13625
|
return { privateKeyPath, publicKeyPath, publicKey };
|
|
13336
13626
|
}
|
|
13337
13627
|
function sshKeygenAvailable() {
|
|
@@ -13547,9 +13837,9 @@ function syncAllPassengerCheckouts(cfg) {
|
|
|
13547
13837
|
|
|
13548
13838
|
// src/tunnel.ts
|
|
13549
13839
|
import { spawn as spawn4 } from "child_process";
|
|
13550
|
-
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";
|
|
13551
13841
|
import { randomInt } from "crypto";
|
|
13552
|
-
import { join as
|
|
13842
|
+
import { join as join26 } from "path";
|
|
13553
13843
|
var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
|
|
13554
13844
|
var defaultSpawn = (cmd, args) => spawn4(cmd, args, DEFAULT_SPAWN_OPTIONS);
|
|
13555
13845
|
var SLUG_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
@@ -13616,21 +13906,21 @@ function startTunnel(opts) {
|
|
|
13616
13906
|
};
|
|
13617
13907
|
}
|
|
13618
13908
|
function tunnelStatePath(stateDir, workspaceId) {
|
|
13619
|
-
return
|
|
13909
|
+
return join26(stateDir, "passengers", `${workspaceId}.json`);
|
|
13620
13910
|
}
|
|
13621
13911
|
function readPersistedTunnel(stateDir, workspaceId) {
|
|
13622
13912
|
const path = tunnelStatePath(stateDir, workspaceId);
|
|
13623
|
-
if (!
|
|
13913
|
+
if (!existsSync26(path)) return void 0;
|
|
13624
13914
|
try {
|
|
13625
|
-
return JSON.parse(
|
|
13915
|
+
return JSON.parse(readFileSync19(path, "utf8"));
|
|
13626
13916
|
} catch {
|
|
13627
13917
|
return void 0;
|
|
13628
13918
|
}
|
|
13629
13919
|
}
|
|
13630
13920
|
function writePersistedTunnel(stateDir, state) {
|
|
13631
13921
|
const path = tunnelStatePath(stateDir, state.workspaceId);
|
|
13632
|
-
|
|
13633
|
-
|
|
13922
|
+
mkdirSync13(join26(stateDir, "passengers"), { recursive: true });
|
|
13923
|
+
writeFileSync14(path, `${JSON.stringify(state, null, 2)}
|
|
13634
13924
|
`);
|
|
13635
13925
|
}
|
|
13636
13926
|
function updatePersistedTunnel(stateDir, workspaceId, patch2) {
|
|
@@ -13727,9 +14017,9 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
|
|
|
13727
14017
|
}
|
|
13728
14018
|
}
|
|
13729
14019
|
function readAllPersistedWorkspaceIds(stateDir) {
|
|
13730
|
-
const dir =
|
|
13731
|
-
if (!
|
|
13732
|
-
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));
|
|
13733
14023
|
}
|
|
13734
14024
|
|
|
13735
14025
|
// src/mcp-endpoint-registry.ts
|
|
@@ -13849,8 +14139,8 @@ async function unregisterEndpoint(workspaceId, info, d) {
|
|
|
13849
14139
|
import { createServer as createHttpServer } from "http";
|
|
13850
14140
|
import { spawn as spawn5 } from "child_process";
|
|
13851
14141
|
import { randomBytes } from "crypto";
|
|
13852
|
-
import { existsSync as
|
|
13853
|
-
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";
|
|
13854
14144
|
|
|
13855
14145
|
// src/secret-compare.ts
|
|
13856
14146
|
import { timingSafeEqual } from "crypto";
|
|
@@ -13939,21 +14229,21 @@ var defaultSpawn2 = (cmd, args, env) => {
|
|
|
13939
14229
|
return child;
|
|
13940
14230
|
};
|
|
13941
14231
|
function syncDaemonStatePath(stateDir, workspaceId) {
|
|
13942
|
-
return
|
|
14232
|
+
return join27(stateDir, "passengers", `${workspaceId}-sync.json`);
|
|
13943
14233
|
}
|
|
13944
14234
|
function readPersistedSyncDaemon(stateDir, workspaceId) {
|
|
13945
14235
|
const path = syncDaemonStatePath(stateDir, workspaceId);
|
|
13946
|
-
if (!
|
|
14236
|
+
if (!existsSync27(path)) return void 0;
|
|
13947
14237
|
try {
|
|
13948
|
-
return JSON.parse(
|
|
14238
|
+
return JSON.parse(readFileSync20(path, "utf8"));
|
|
13949
14239
|
} catch {
|
|
13950
14240
|
return void 0;
|
|
13951
14241
|
}
|
|
13952
14242
|
}
|
|
13953
14243
|
function writePersistedSyncDaemon(stateDir, state) {
|
|
13954
14244
|
const path = syncDaemonStatePath(stateDir, state.workspaceId);
|
|
13955
|
-
|
|
13956
|
-
|
|
14245
|
+
mkdirSync14(join27(stateDir, "passengers"), { recursive: true });
|
|
14246
|
+
writeFileSync15(path, `${JSON.stringify(state, null, 2)}
|
|
13957
14247
|
`);
|
|
13958
14248
|
}
|
|
13959
14249
|
function removePersistedSyncDaemon(stateDir, workspaceId) {
|
|
@@ -13973,10 +14263,10 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
|
|
|
13973
14263
|
}
|
|
13974
14264
|
}
|
|
13975
14265
|
function readAllPersistedSyncWorkspaceIds(stateDir) {
|
|
13976
|
-
const dir =
|
|
13977
|
-
if (!
|
|
14266
|
+
const dir = join27(stateDir, "passengers");
|
|
14267
|
+
if (!existsSync27(dir)) return [];
|
|
13978
14268
|
const suffix = "-sync.json";
|
|
13979
|
-
return
|
|
14269
|
+
return readdirSync9(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
|
|
13980
14270
|
}
|
|
13981
14271
|
function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
|
|
13982
14272
|
const alive = deps.isPidAlive ?? isPidAlive;
|
|
@@ -14031,12 +14321,12 @@ function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
|
|
|
14031
14321
|
}
|
|
14032
14322
|
|
|
14033
14323
|
// src/daemon.ts
|
|
14034
|
-
import { existsSync as
|
|
14035
|
-
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";
|
|
14036
14326
|
|
|
14037
14327
|
// src/interrupted.ts
|
|
14038
|
-
import { existsSync as
|
|
14039
|
-
import { join as
|
|
14328
|
+
import { existsSync as existsSync28, readdirSync as readdirSync10, readFileSync as readFileSync21 } from "fs";
|
|
14329
|
+
import { join as join28 } from "path";
|
|
14040
14330
|
var INTERRUPTED_MARKER = calloutHeader("NOTE", "Interrupted");
|
|
14041
14331
|
function interruptedComment(b) {
|
|
14042
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.";
|
|
@@ -14055,21 +14345,21 @@ var pidAlive = (pid) => {
|
|
|
14055
14345
|
};
|
|
14056
14346
|
function runFinished(streamPath) {
|
|
14057
14347
|
const path = `${streamPath}.meta.json`;
|
|
14058
|
-
if (!
|
|
14348
|
+
if (!existsSync28(path)) return false;
|
|
14059
14349
|
try {
|
|
14060
|
-
return JSON.parse(
|
|
14350
|
+
return JSON.parse(readFileSync21(path, "utf8")).running !== true;
|
|
14061
14351
|
} catch {
|
|
14062
14352
|
return true;
|
|
14063
14353
|
}
|
|
14064
14354
|
}
|
|
14065
14355
|
function orphanedStreams(o) {
|
|
14066
|
-
const dir =
|
|
14067
|
-
if (!
|
|
14356
|
+
const dir = join28(o.stateDir, "streams");
|
|
14357
|
+
if (!existsSync28(dir)) return [];
|
|
14068
14358
|
const ownerAlive = o.isOwnerAlive ?? pidAlive;
|
|
14069
14359
|
const out = [];
|
|
14070
|
-
for (const name of
|
|
14360
|
+
for (const name of readdirSync10(dir)) {
|
|
14071
14361
|
if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
|
|
14072
|
-
const streamPath =
|
|
14362
|
+
const streamPath = join28(dir, name);
|
|
14073
14363
|
if (runFinished(streamPath)) continue;
|
|
14074
14364
|
const sidecar = readRunSidecar(streamPath);
|
|
14075
14365
|
if (!sidecar || sidecar.route !== o.routeLabel) continue;
|
|
@@ -14161,15 +14451,15 @@ async function runDaemonLoop(o) {
|
|
|
14161
14451
|
function latestMtime(path) {
|
|
14162
14452
|
let stat;
|
|
14163
14453
|
try {
|
|
14164
|
-
stat =
|
|
14454
|
+
stat = statSync9(path);
|
|
14165
14455
|
} catch {
|
|
14166
14456
|
return 0;
|
|
14167
14457
|
}
|
|
14168
14458
|
if (stat.isFile()) return stat.mtimeMs;
|
|
14169
14459
|
if (!stat.isDirectory()) return 0;
|
|
14170
14460
|
let latest = stat.mtimeMs;
|
|
14171
|
-
for (const entry of
|
|
14172
|
-
latest = Math.max(latest, latestMtime(
|
|
14461
|
+
for (const entry of readdirSync11(path, { withFileTypes: true })) {
|
|
14462
|
+
latest = Math.max(latest, latestMtime(join29(path, entry.name)));
|
|
14173
14463
|
}
|
|
14174
14464
|
return latest;
|
|
14175
14465
|
}
|
|
@@ -14178,23 +14468,23 @@ function computeTreeSignature(paths) {
|
|
|
14178
14468
|
}
|
|
14179
14469
|
function makeStaleChecker(paths, opts = {}) {
|
|
14180
14470
|
const initial = computeTreeSignature(paths);
|
|
14181
|
-
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 &&
|
|
14471
|
+
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync29(opts.reloadSentinel);
|
|
14182
14472
|
}
|
|
14183
14473
|
function reloadSentinelFor(stateDir) {
|
|
14184
|
-
return
|
|
14474
|
+
return join29(stateDir, "reload-requested");
|
|
14185
14475
|
}
|
|
14186
14476
|
function daemonStampFor(stateDir) {
|
|
14187
|
-
return
|
|
14477
|
+
return join29(stateDir, "daemon.started");
|
|
14188
14478
|
}
|
|
14189
14479
|
function noteDaemonStarted(stateDir, now = Date.now()) {
|
|
14190
|
-
|
|
14191
|
-
|
|
14192
|
-
|
|
14480
|
+
mkdirSync15(stateDir, { recursive: true });
|
|
14481
|
+
rmSync11(reloadSentinelFor(stateDir), { force: true });
|
|
14482
|
+
writeFileSync16(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
|
|
14193
14483
|
`);
|
|
14194
14484
|
}
|
|
14195
14485
|
function readDaemonStarted(stateDir) {
|
|
14196
14486
|
try {
|
|
14197
|
-
const v = JSON.parse(
|
|
14487
|
+
const v = JSON.parse(readFileSync22(daemonStampFor(stateDir), "utf8")).startedAt;
|
|
14198
14488
|
return typeof v === "number" ? v : null;
|
|
14199
14489
|
} catch {
|
|
14200
14490
|
return null;
|
|
@@ -14203,7 +14493,7 @@ function readDaemonStarted(stateDir) {
|
|
|
14203
14493
|
function configChangedAfter(files, startedAt) {
|
|
14204
14494
|
for (const f of files) {
|
|
14205
14495
|
try {
|
|
14206
|
-
if (
|
|
14496
|
+
if (statSync9(f).mtimeMs > startedAt) return f;
|
|
14207
14497
|
} catch {
|
|
14208
14498
|
}
|
|
14209
14499
|
}
|
|
@@ -14550,7 +14840,7 @@ async function buildEnvironment(route, ship, ticket) {
|
|
|
14550
14840
|
}
|
|
14551
14841
|
|
|
14552
14842
|
// src/cli.ts
|
|
14553
|
-
var CREW_HOME = isCompiledBinary(import.meta.url) ?
|
|
14843
|
+
var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname17(process.execPath) : resolve5(dirname17(fileURLToPath(import.meta.url)), "..");
|
|
14554
14844
|
var RELEASE_LOCK_TTL_MS = 20 * 60 * 1e3;
|
|
14555
14845
|
var CYCLE_SECONDS2 = 120;
|
|
14556
14846
|
var STALE_CYCLES = 3;
|
|
@@ -14698,11 +14988,13 @@ function usage() {
|
|
|
14698
14988
|
crew inbox [--member NAME] your tickets across every workspace (or a colleague's)
|
|
14699
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)
|
|
14700
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)
|
|
14701
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)
|
|
14702
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)
|
|
14703
14994
|
crew skills sync [route] push only crew's skill files (e.g. grill-me) into the workspace Agent Skills table
|
|
14704
14995
|
crew repos add [ROUTE] [NAME] [PATH] attach a local checkout to a route in crew.yaml (--dry-run previews)
|
|
14705
14996
|
crew repos clone [ROUTE] NAME clone a Repos row's remote into the route directory and attach it (--dir PATH, --dry-run, --json)
|
|
14997
|
+
crew repos hooks [ROUTE] NAME suggest test/build hooks for a checkout (--write route|repo, --json)
|
|
14706
14998
|
crew repos list ROUTE the checkouts a route has, with the tracker's Repos row each matches
|
|
14707
14999
|
crew logbook list [route] recent Agent Log entries, filterable by --role/--ticket
|
|
14708
15000
|
crew logbook show [route] ID [--prompt] one entry; --prompt reconstructs and verifies its prompt
|
|
@@ -14875,7 +15167,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
14875
15167
|
firstRun = { answers, configPath };
|
|
14876
15168
|
cfg = loadConfig(CREW_HOME, configPath, { text: renderShipBlock(answers), allowNoRoutes: true });
|
|
14877
15169
|
} else {
|
|
14878
|
-
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 } : {});
|
|
14879
15171
|
}
|
|
14880
15172
|
} catch (e) {
|
|
14881
15173
|
if (e instanceof ConfigError) {
|
|
@@ -14906,6 +15198,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
14906
15198
|
throw e;
|
|
14907
15199
|
}
|
|
14908
15200
|
const state = new State(cfg.ship.stateDir);
|
|
15201
|
+
setCloneRecorder((dir) => recordClonedCheckout(cfg.ship.stateDir, dir));
|
|
14909
15202
|
const dryRun = flag("dry-run");
|
|
14910
15203
|
const force = flag("force");
|
|
14911
15204
|
const EXCLUSIVE = { passengers: "passengers" };
|
|
@@ -15108,8 +15401,8 @@ if (command === "passenger-sync-daemon") {
|
|
|
15108
15401
|
}
|
|
15109
15402
|
if (host === "linux" && hasSystemd) {
|
|
15110
15403
|
const label = labelFor(CREW_HOME, "run");
|
|
15111
|
-
const staleTimer =
|
|
15112
|
-
if (
|
|
15404
|
+
const staleTimer = join30(homedir8(), ".config", "systemd", "user", `${label}.timer`);
|
|
15405
|
+
if (existsSync30(staleTimer)) {
|
|
15113
15406
|
process.stdout.write(
|
|
15114
15407
|
`removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
|
|
15115
15408
|
`
|
|
@@ -15135,7 +15428,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
15135
15428
|
const tracker = new Tracker(c, cfg.ship);
|
|
15136
15429
|
problems.push(...validateContract(tracker.contract).map((p) => `contract: ${p}`));
|
|
15137
15430
|
for (const r of await resolvedRepos(c)) {
|
|
15138
|
-
if (!
|
|
15431
|
+
if (!existsSync30(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
|
|
15139
15432
|
if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
|
|
15140
15433
|
problems.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
|
|
15141
15434
|
}
|
|
@@ -15153,7 +15446,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
15153
15446
|
}
|
|
15154
15447
|
async function doctorFix(o) {
|
|
15155
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))));
|
|
15156
|
-
const unitExists = (p) =>
|
|
15449
|
+
const unitExists = (p) => existsSync30(p);
|
|
15157
15450
|
const crontab = o.host === "linux" ? spawnSync6("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
|
|
15158
15451
|
const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
|
|
15159
15452
|
const plan = planFix({ routes, schedulerAbsent, host: o.host });
|
|
@@ -15170,7 +15463,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
15170
15463
|
for (const a of plan.actions) {
|
|
15171
15464
|
if (a.kind === "enable") {
|
|
15172
15465
|
if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
|
|
15173
|
-
|
|
15466
|
+
writeFileSync17(cfg.configFile, planEnable(readFileSync23(cfg.configFile, "utf8"), a.route));
|
|
15174
15467
|
enabledSound.add(a.route);
|
|
15175
15468
|
changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
|
|
15176
15469
|
} else {
|
|
@@ -16169,7 +16462,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16169
16462
|
process.exit(1);
|
|
16170
16463
|
}
|
|
16171
16464
|
requireArmed("run the daemon loop");
|
|
16172
|
-
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")];
|
|
16173
16466
|
const configPaths = daemonConfigPaths(cfg);
|
|
16174
16467
|
watchPaths.push(...configPaths);
|
|
16175
16468
|
noteDaemonStarted(cfg.ship.stateDir);
|
|
@@ -16370,10 +16663,10 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16370
16663
|
`);
|
|
16371
16664
|
if (!dryRun) {
|
|
16372
16665
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
16373
|
-
const raw =
|
|
16666
|
+
const raw = existsSync30(resolvedPath) ? JSON.parse(readFileSync23(resolvedPath, "utf8")) : {};
|
|
16374
16667
|
raw.agentSkills = result.agentSkills;
|
|
16375
|
-
|
|
16376
|
-
|
|
16668
|
+
mkdirSync16(dirOf(resolvedPath), { recursive: true });
|
|
16669
|
+
writeFileSync17(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
16377
16670
|
`);
|
|
16378
16671
|
}
|
|
16379
16672
|
if (result.outcomes.some((o) => o.action === "diverged")) process.exitCode = 1;
|
|
@@ -16382,18 +16675,74 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16382
16675
|
case "repos": {
|
|
16383
16676
|
const sub = positional[1];
|
|
16384
16677
|
const usage2 = () => {
|
|
16385
|
-
process.stderr.write("crew repos add [workspace/project] [name] [path] [--name N] [--project P] [--dry-run] [--no-create]\ncrew repos clone [workspace/project] <name> [--dir PATH] [--dry-run] [--json]\ncrew repos list <workspace/project>\n");
|
|
16678
|
+
process.stderr.write("crew repos add [workspace/project] [name] [path] [--name N] [--project P] [--dry-run] [--no-create]\ncrew repos clone [workspace/project] <name> [--dir PATH] [--dry-run] [--json]\ncrew repos list <workspace/project>\ncrew repos hooks [workspace/project] <name> [--suggest] [--write route|repo] [--json]\n");
|
|
16386
16679
|
process.exit(2);
|
|
16387
16680
|
};
|
|
16388
|
-
if (sub !== "add" && sub !== "list" && sub !== "clone") usage2();
|
|
16389
|
-
const configText =
|
|
16681
|
+
if (sub !== "add" && sub !== "list" && sub !== "clone" && sub !== "hooks") usage2();
|
|
16682
|
+
const configText = readFileSync23(cfg.configFile, "utf8");
|
|
16683
|
+
if (sub === "hooks") {
|
|
16684
|
+
try {
|
|
16685
|
+
const a = parseHooksArgs(positional.slice(2), routeNamesOf(configText));
|
|
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) => {
|
|
16689
|
+
try {
|
|
16690
|
+
return readFileSync23(resolvePath(dir, r), "utf8");
|
|
16691
|
+
} catch {
|
|
16692
|
+
return void 0;
|
|
16693
|
+
}
|
|
16694
|
+
} };
|
|
16695
|
+
const suggestion = suggestHooks(files);
|
|
16696
|
+
const write = value("write");
|
|
16697
|
+
if (write !== void 0 && write !== "route" && write !== "repo") throw new ReposError(`--write takes "route" or "repo" (got "${write}")`);
|
|
16698
|
+
let written;
|
|
16699
|
+
if (write && suggestion.source === null) throw new ReposError(`nothing to write: ${suggestion.notes[0]}`);
|
|
16700
|
+
if (write === "route") {
|
|
16701
|
+
const plan = planRouteHooks(configText, a.route, a.name, suggestion.hooks);
|
|
16702
|
+
writeFileSync17(cfg.configFile, plan.text);
|
|
16703
|
+
written = { target: "route", file: cfg.configFile, hooks: plan.written, kept: plan.kept };
|
|
16704
|
+
} else if (write === "repo") {
|
|
16705
|
+
const have = existingRepoConfig(files);
|
|
16706
|
+
if (have) throw new ReposError(`${resolvePath(dir, have)} already exists; not overwriting it`);
|
|
16707
|
+
const file = resolvePath(dir, ".crew.yaml");
|
|
16708
|
+
writeFileSync17(file, starterRepoConfig(suggestion.hooks), { flag: "wx" });
|
|
16709
|
+
written = { target: "repo", file, hooks: Object.keys(suggestion.hooks), kept: [] };
|
|
16710
|
+
}
|
|
16711
|
+
if (flag("json")) {
|
|
16712
|
+
process.stdout.write(`${JSON.stringify({ route: a.route, repo: a.name, dir, ...suggestion, written: written ?? null })}
|
|
16713
|
+
`);
|
|
16714
|
+
} else {
|
|
16715
|
+
if (suggestion.source) process.stdout.write(`${a.name}: proposed from ${suggestion.source}
|
|
16716
|
+
`);
|
|
16717
|
+
for (const [k, v] of Object.entries(suggestion.hooks)) process.stdout.write(` ${k}: ${v}
|
|
16718
|
+
`);
|
|
16719
|
+
for (const n of suggestion.notes) process.stdout.write(`note: ${n}
|
|
16720
|
+
`);
|
|
16721
|
+
if (written) {
|
|
16722
|
+
process.stdout.write(`wrote ${written.hooks.join(", ") || "nothing new"} to ${written.file}
|
|
16723
|
+
`);
|
|
16724
|
+
if (written.kept.length) process.stdout.write(`kept existing route hooks: ${written.kept.join(", ")}
|
|
16725
|
+
`);
|
|
16726
|
+
if (written.target === "repo") process.stdout.write("review it and commit it to the repository\n");
|
|
16727
|
+
}
|
|
16728
|
+
}
|
|
16729
|
+
} catch (e) {
|
|
16730
|
+
if (e instanceof ReposError) {
|
|
16731
|
+
process.stderr.write(`crew repos: ${e.message}
|
|
16732
|
+
`);
|
|
16733
|
+
process.exit(2);
|
|
16734
|
+
}
|
|
16735
|
+
throw e;
|
|
16736
|
+
}
|
|
16737
|
+
break;
|
|
16738
|
+
}
|
|
16390
16739
|
let routeName = positional[2];
|
|
16391
16740
|
let addPath = ".";
|
|
16392
16741
|
let addName;
|
|
16393
16742
|
if (sub === "list" && !routeName) usage2();
|
|
16394
16743
|
if (sub === "add") {
|
|
16395
16744
|
try {
|
|
16396
|
-
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) =>
|
|
16745
|
+
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync30(resolvePath(p.replace(/^~(?=\/|$)/, homedir8()))));
|
|
16397
16746
|
routeName = a.route;
|
|
16398
16747
|
addPath = a.path;
|
|
16399
16748
|
addName = a.name;
|
|
@@ -16422,15 +16771,15 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16422
16771
|
}
|
|
16423
16772
|
const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
|
|
16424
16773
|
let resolvedRemotes;
|
|
16425
|
-
if (
|
|
16774
|
+
if (existsSync30(resolvedFile)) {
|
|
16426
16775
|
try {
|
|
16427
|
-
resolvedRemotes = JSON.parse(
|
|
16776
|
+
resolvedRemotes = JSON.parse(readFileSync23(resolvedFile, "utf8"));
|
|
16428
16777
|
} catch {
|
|
16429
16778
|
}
|
|
16430
16779
|
}
|
|
16431
16780
|
const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
|
|
16432
16781
|
const text = configText;
|
|
16433
|
-
const base =
|
|
16782
|
+
const base = dirname17(cfg.configFile);
|
|
16434
16783
|
try {
|
|
16435
16784
|
if (sub === "list") {
|
|
16436
16785
|
const repos = configuredRepos(text, routeName, base);
|
|
@@ -16439,7 +16788,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16439
16788
|
for (const r of repos) {
|
|
16440
16789
|
const origin2 = originOf(r.dir);
|
|
16441
16790
|
const row = matchRepoName(origin2, resolvedRemotes);
|
|
16442
|
-
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)"}`;
|
|
16443
16792
|
process.stdout.write(`${r.name} ${r.dir} ${note}
|
|
16444
16793
|
`);
|
|
16445
16794
|
}
|
|
@@ -16451,9 +16800,9 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16451
16800
|
` : `${human}
|
|
16452
16801
|
`);
|
|
16453
16802
|
const plan2 = planRepoClone({ text, base, route: routeName, name: addName, resolved: resolvedRemotes, dir: value("dir") });
|
|
16454
|
-
if (
|
|
16803
|
+
if (existsSync30(plan2.target) && (existsSync30(`${plan2.target}/.git`) || readdirSync12(plan2.target).length > 0)) {
|
|
16455
16804
|
throw new ReposError(
|
|
16456
|
-
|
|
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`
|
|
16457
16806
|
);
|
|
16458
16807
|
}
|
|
16459
16808
|
if (dryRun) {
|
|
@@ -16471,12 +16820,12 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16471
16820
|
throw new ReposError(`${e.message}${authHint}
|
|
16472
16821
|
crew.yaml was not changed`);
|
|
16473
16822
|
}
|
|
16474
|
-
|
|
16823
|
+
writeFileSync17(cfg.configFile, plan2.text);
|
|
16475
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}`);
|
|
16476
16825
|
break;
|
|
16477
16826
|
}
|
|
16478
16827
|
const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir8()));
|
|
16479
|
-
const top =
|
|
16828
|
+
const top = existsSync30(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
|
|
16480
16829
|
if (!top) throw new ReposError(`${target} is not a git checkout`);
|
|
16481
16830
|
const origin = originOf(top);
|
|
16482
16831
|
const plan = planRepoAdd({
|
|
@@ -16525,15 +16874,15 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16525
16874
|
const created = await rowTracker.createRepoRow(rowPayload);
|
|
16526
16875
|
process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
|
|
16527
16876
|
`);
|
|
16528
|
-
if (
|
|
16529
|
-
const raw = JSON.parse(
|
|
16877
|
+
if (existsSync30(resolvedFile)) {
|
|
16878
|
+
const raw = JSON.parse(readFileSync23(resolvedFile, "utf8"));
|
|
16530
16879
|
raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
|
|
16531
16880
|
raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
|
|
16532
|
-
|
|
16881
|
+
writeFileSync17(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
16533
16882
|
`);
|
|
16534
16883
|
}
|
|
16535
16884
|
}
|
|
16536
|
-
|
|
16885
|
+
writeFileSync17(cfg.configFile, plan.text);
|
|
16537
16886
|
process.stdout.write(`added ${top} to ${routeName} as ${plan.mode === "dir" ? "dir" : `repos.${plan.name}`} in ${cfg.configFile}
|
|
16538
16887
|
`);
|
|
16539
16888
|
} catch (e) {
|
|
@@ -16818,6 +17167,16 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
16818
17167
|
}
|
|
16819
17168
|
const route2 = `${found.workspaceSlug}/${found.projectSlug ?? found.projectId}`;
|
|
16820
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);
|
|
16821
17180
|
if (found.locksModelId && !dryRun) {
|
|
16822
17181
|
const configured = cfg.routes.find((r) => r.route === route2);
|
|
16823
17182
|
const repoNames = [...Object.values(found.repoNames ?? {}), ...configured ? reposOf(configured).map((t) => t.name) : []].filter((n) => !!n);
|
|
@@ -16843,10 +17202,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
16843
17202
|
}
|
|
16844
17203
|
}
|
|
16845
17204
|
let previous;
|
|
16846
|
-
const hadPreviousFile =
|
|
17205
|
+
const hadPreviousFile = existsSync30(resolvedPath);
|
|
16847
17206
|
if (hadPreviousFile) {
|
|
16848
17207
|
try {
|
|
16849
|
-
previous = JSON.parse(
|
|
17208
|
+
previous = JSON.parse(readFileSync23(resolvedPath, "utf8"));
|
|
16850
17209
|
} catch {
|
|
16851
17210
|
}
|
|
16852
17211
|
}
|
|
@@ -16936,6 +17295,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
16936
17295
|
holds: found.holds.map((h) => ({ id: h.id, role: h.name })),
|
|
16937
17296
|
...found.releaseSeat ? { releaseSeat: found.releaseSeat } : {},
|
|
16938
17297
|
...found.role ?? previous?.role ? { role: found.role ?? previous?.role } : {},
|
|
17298
|
+
...capabilityMisses.length > 0 ? { capabilityMisses } : {},
|
|
16939
17299
|
...operator ? { operator } : {},
|
|
16940
17300
|
...contract ? { contract } : {},
|
|
16941
17301
|
...reviewedStatuses.length > 0 ? { reviewedStatuses } : {},
|
|
@@ -16954,10 +17314,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
16954
17314
|
process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
|
|
16955
17315
|
`);
|
|
16956
17316
|
} else {
|
|
16957
|
-
|
|
17317
|
+
mkdirSync16(dirname17(resolvedPath), { recursive: true });
|
|
16958
17318
|
if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
|
|
16959
17319
|
const tmpPath = `${resolvedPath}.tmp${process.pid}`;
|
|
16960
|
-
|
|
17320
|
+
writeFileSync17(tmpPath, `${JSON.stringify(resolved, null, 2)}
|
|
16961
17321
|
`);
|
|
16962
17322
|
renameSync2(tmpPath, resolvedPath);
|
|
16963
17323
|
process.stderr.write(`Resolved ids written to ${resolvedPath}
|
|
@@ -16992,8 +17352,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
16992
17352
|
process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
|
|
16993
17353
|
`);
|
|
16994
17354
|
} else {
|
|
16995
|
-
|
|
16996
|
-
|
|
17355
|
+
mkdirSync16(dirname17(apiKeyFile), { recursive: true });
|
|
17356
|
+
writeFileSync17(apiKeyFile, `${apiKeyVar}=${apiKey}
|
|
16997
17357
|
`, { mode: 384 });
|
|
16998
17358
|
process.stderr.write(`API key written to ${apiKeyFile}
|
|
16999
17359
|
`);
|
|
@@ -17002,7 +17362,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
17002
17362
|
const keyPersisted = keychainBacked || apiKeyFile !== void 0;
|
|
17003
17363
|
const wizard = firstRun;
|
|
17004
17364
|
const writeFirstRun = wizard !== void 0 && !dryRun && keyPersisted;
|
|
17005
|
-
const routeDir = value("dir") ?? (writeFirstRun ?
|
|
17365
|
+
const routeDir = value("dir") ?? (writeFirstRun ? join30(DEFAULT_REPOS_BASE_PATH, found.workspaceSlug) : void 0);
|
|
17006
17366
|
const routeBlock = renderConnection(
|
|
17007
17367
|
found,
|
|
17008
17368
|
route2,
|
|
@@ -17597,10 +17957,76 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
17597
17957
|
case "disconnect": {
|
|
17598
17958
|
const url = mcpUrlFor(value("base-url") ?? cfg.routes[0]?.baseUrl ?? DEFAULT_BASE_URL);
|
|
17599
17959
|
const disconnectHarnesses = /* @__PURE__ */ new Set([...shipPair(cfg.ship), ...shipToolchains(cfg.ship).map((t) => t.harness).filter(isPairHarness)]);
|
|
17600
|
-
|
|
17601
|
-
|
|
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}
|
|
17602
17963
|
`);
|
|
17964
|
+
break;
|
|
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 {
|
|
17603
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;
|
|
17604
18030
|
break;
|
|
17605
18031
|
}
|
|
17606
18032
|
case "install": {
|
|
@@ -17622,8 +18048,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
17622
18048
|
process.stdout.write("crew reload: no daemon unit is running \u2014 nothing to reload (the next run reads crew.yaml fresh)\n");
|
|
17623
18049
|
break;
|
|
17624
18050
|
}
|
|
17625
|
-
|
|
17626
|
-
|
|
18051
|
+
mkdirSync16(cfg.ship.stateDir, { recursive: true });
|
|
18052
|
+
writeFileSync17(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
|
|
17627
18053
|
`);
|
|
17628
18054
|
process.stdout.write("crew reload: requested \u2014 the daemon finishes its in-flight pass, then relaunches with the new config\n");
|
|
17629
18055
|
if (flag("now") && control?.stopCommand && control.startCommand) {
|
|
@@ -17674,7 +18100,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
17674
18100
|
}
|
|
17675
18101
|
case "log": {
|
|
17676
18102
|
try {
|
|
17677
|
-
const text =
|
|
18103
|
+
const text = readFileSync23(cfg.ship.logFile, "utf8").split("\n");
|
|
17678
18104
|
process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
|
|
17679
18105
|
`);
|
|
17680
18106
|
} catch {
|
|
@@ -17697,8 +18123,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
17697
18123
|
};
|
|
17698
18124
|
if (!route.resolved) fail(`route ${route.route} is not connected \u2014 run \`crew connect\` first`);
|
|
17699
18125
|
const tracker = new Tracker(route, cfg.ship);
|
|
17700
|
-
const configText =
|
|
17701
|
-
const cfgBase =
|
|
18126
|
+
const configText = readFileSync23(cfg.configFile, "utf8");
|
|
18127
|
+
const cfgBase = dirname17(cfg.configFile);
|
|
17702
18128
|
const configured = configuredRepos(configText, route.route, cfgBase);
|
|
17703
18129
|
const dir = configured.find((r) => r.name === SAMPLE_REPO)?.dir ?? sampleDir(configured.map((r) => r.dir), route.reposBasePath, route.route.split("/")[0]);
|
|
17704
18130
|
const resolvedFile = resolvedPathFor(cfg.ship.stateDir, route.route);
|
|
@@ -17726,7 +18152,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
17726
18152
|
}
|
|
17727
18153
|
if (configured.some((r) => r.dir === dir)) {
|
|
17728
18154
|
try {
|
|
17729
|
-
|
|
18155
|
+
writeFileSync17(cfg.configFile, planRepoRemove(configText, cfgBase, route.route, dir).text);
|
|
17730
18156
|
} catch (e) {
|
|
17731
18157
|
if (e instanceof ReposError) fail(e.message);
|
|
17732
18158
|
throw e;
|
|
@@ -17734,29 +18160,29 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
17734
18160
|
}
|
|
17735
18161
|
if (row) {
|
|
17736
18162
|
await tracker.deleteRepoRow(row.id);
|
|
17737
|
-
if (
|
|
17738
|
-
const raw = JSON.parse(
|
|
18163
|
+
if (existsSync30(resolvedFile)) {
|
|
18164
|
+
const raw = JSON.parse(readFileSync23(resolvedFile, "utf8"));
|
|
17739
18165
|
if (raw.repoNames) delete raw.repoNames[row.id];
|
|
17740
18166
|
if (raw.repoRemotes) delete raw.repoRemotes[row.id];
|
|
17741
|
-
|
|
18167
|
+
writeFileSync17(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
17742
18168
|
`);
|
|
17743
18169
|
}
|
|
17744
18170
|
}
|
|
17745
|
-
if (
|
|
18171
|
+
if (existsSync30(join30(dir, ".git")) && basename10(dir) === SAMPLE_REPO) rmSync12(dir, { recursive: true, force: true });
|
|
17746
18172
|
say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
|
|
17747
18173
|
break;
|
|
17748
18174
|
}
|
|
17749
18175
|
const projectId = route.resolved.areaId ?? resolveRepoProject(await tracker.projectRows(), value("project"));
|
|
17750
|
-
const fresh = !
|
|
17751
|
-
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`);
|
|
17752
18178
|
if (dryRun) {
|
|
17753
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`);
|
|
17754
18180
|
break;
|
|
17755
18181
|
}
|
|
17756
18182
|
if (fresh) {
|
|
17757
|
-
|
|
17758
|
-
|
|
17759
|
-
|
|
18183
|
+
mkdirSync16(dir, { recursive: true });
|
|
18184
|
+
writeFileSync17(join30(dir, "README.md"), SAMPLE_README);
|
|
18185
|
+
writeFileSync17(join30(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
|
|
17760
18186
|
const g = (...a) => {
|
|
17761
18187
|
const r = spawnSync6("git", a, { cwd: dir, encoding: "utf8" });
|
|
17762
18188
|
if (r.status !== 0) fail(`git ${a[0]} failed in ${dir}: ${(r.stderr || r.stdout).trim()}`);
|
|
@@ -17774,17 +18200,17 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
17774
18200
|
spec: { platform: "unix", releaseMode: "integrate", ciProvider: "none" }
|
|
17775
18201
|
});
|
|
17776
18202
|
repoId = (await tracker.createRepoRow(payload)).id;
|
|
17777
|
-
if (
|
|
17778
|
-
const raw = JSON.parse(
|
|
18203
|
+
if (existsSync30(resolvedFile)) {
|
|
18204
|
+
const raw = JSON.parse(readFileSync23(resolvedFile, "utf8"));
|
|
17779
18205
|
raw.repoNames = { ...raw.repoNames, [repoId]: SAMPLE_REPO };
|
|
17780
18206
|
raw.repoRemotes = { ...raw.repoRemotes, [repoId]: SAMPLE_REMOTE };
|
|
17781
|
-
|
|
18207
|
+
writeFileSync17(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
17782
18208
|
`);
|
|
17783
18209
|
}
|
|
17784
18210
|
}
|
|
17785
18211
|
if (!configured.some((r) => r.dir === dir)) {
|
|
17786
18212
|
try {
|
|
17787
|
-
|
|
18213
|
+
writeFileSync17(cfg.configFile, planRepoAdd({ text: configText, base: cfgBase, route: route.route, path: dir, name: SAMPLE_REPO }).text);
|
|
17788
18214
|
} catch (e) {
|
|
17789
18215
|
if (e instanceof ReposError) fail(e.message);
|
|
17790
18216
|
throw e;
|
|
@@ -18074,7 +18500,7 @@ watermark: ${state.watermark()}
|
|
|
18074
18500
|
}
|
|
18075
18501
|
for (const r of repos) {
|
|
18076
18502
|
const remote = r.config.branch.remote;
|
|
18077
|
-
if (
|
|
18503
|
+
if (existsSync30(r.dir)) {
|
|
18078
18504
|
pushAuth.push({
|
|
18079
18505
|
route: c.route,
|
|
18080
18506
|
repo: r.name,
|
|
@@ -18102,7 +18528,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
18102
18528
|
);
|
|
18103
18529
|
try {
|
|
18104
18530
|
const evFile = eventFileFor(cfg.ship.stateDir);
|
|
18105
|
-
const evSize =
|
|
18531
|
+
const evSize = existsSync30(evFile) ? statSync10(evFile).size : 0;
|
|
18106
18532
|
process.stdout.write(
|
|
18107
18533
|
evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
|
|
18108
18534
|
` : "events log: ok\n"
|
|
@@ -18113,7 +18539,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
18113
18539
|
const unitPlan = planInstall(cfg.ship, CREW_HOME, host, detectSystemd(), "run");
|
|
18114
18540
|
const abandons = unitAbandonsAgents(unitPlan, (p) => {
|
|
18115
18541
|
try {
|
|
18116
|
-
return
|
|
18542
|
+
return readFileSync23(p, "utf8");
|
|
18117
18543
|
} catch {
|
|
18118
18544
|
return null;
|
|
18119
18545
|
}
|
|
@@ -18129,7 +18555,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
18129
18555
|
const changed2 = configChangedAfter(daemonConfigPaths(cfg), daemonStartedAt);
|
|
18130
18556
|
if (changed2) {
|
|
18131
18557
|
process.stdout.write(
|
|
18132
|
-
`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\`
|
|
18133
18559
|
`
|
|
18134
18560
|
);
|
|
18135
18561
|
}
|
|
@@ -18189,6 +18615,8 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
18189
18615
|
const keyRole = route.resolved?.role ?? await fetchKeyRole(route, cfg.ship.userAgent);
|
|
18190
18616
|
const roleRoute = route.resolved && keyRole ? { ...route, resolved: { ...route.resolved, role: keyRole } } : route;
|
|
18191
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)
|
|
18192
18620
|
`);
|
|
18193
18621
|
const canMint = await canMintAgentKeys(roleRoute, { userAgent: cfg.ship.userAgent });
|
|
18194
18622
|
process.stdout.write(`can mint agent keys: ${canMint === void 0 ? "unknown" : canMint ? "yes" : "no"}
|