@tablation/crew 0.63.1 → 0.64.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 +1 -1
- package/dist/cli.js +515 -183
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -5183,7 +5183,7 @@ function doctorSelftestLines(stored, toolchains) {
|
|
|
5183
5183
|
}
|
|
5184
5184
|
|
|
5185
5185
|
// src/cli.ts
|
|
5186
|
-
import { resolve as resolve5, dirname as
|
|
5186
|
+
import { resolve as resolve5, dirname as dirname19, join as join32, basename as basename10 } from "path";
|
|
5187
5187
|
import { fileURLToPath } from "url";
|
|
5188
5188
|
import { hostname as hostname2, homedir as homedir8 } from "os";
|
|
5189
5189
|
import { spawnSync as spawnSync6 } from "child_process";
|
|
@@ -5209,6 +5209,7 @@ var VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
|
5209
5209
|
"repo",
|
|
5210
5210
|
"role",
|
|
5211
5211
|
"route",
|
|
5212
|
+
"recipes",
|
|
5212
5213
|
"ship-name",
|
|
5213
5214
|
"tail",
|
|
5214
5215
|
"ticket",
|
|
@@ -5762,15 +5763,52 @@ var State = class _State {
|
|
|
5762
5763
|
}
|
|
5763
5764
|
};
|
|
5764
5765
|
|
|
5765
|
-
// src/
|
|
5766
|
-
import {
|
|
5766
|
+
// src/connect-resume.ts
|
|
5767
|
+
import { mkdirSync as mkdirSync7, readFileSync as readFileSync13, writeFileSync as writeFileSync8 } from "fs";
|
|
5767
5768
|
import { dirname as dirname6, join as join14 } from "path";
|
|
5769
|
+
function lastSessionFile(stateDir) {
|
|
5770
|
+
return join14(stateDir, "connect-session.json");
|
|
5771
|
+
}
|
|
5772
|
+
function readPointers(stateDir) {
|
|
5773
|
+
try {
|
|
5774
|
+
const parsed = JSON.parse(readFileSync13(lastSessionFile(stateDir), "utf8"));
|
|
5775
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
5776
|
+
return Object.fromEntries(Object.entries(parsed).filter((e) => typeof e[1] === "string"));
|
|
5777
|
+
} catch {
|
|
5778
|
+
return {};
|
|
5779
|
+
}
|
|
5780
|
+
}
|
|
5781
|
+
function rememberSession(stateDir, host, workspaceSlug) {
|
|
5782
|
+
try {
|
|
5783
|
+
const file = lastSessionFile(stateDir);
|
|
5784
|
+
mkdirSync7(dirname6(file), { recursive: true });
|
|
5785
|
+
writeFileSync8(file, `${JSON.stringify({ ...readPointers(stateDir), [host]: workspaceSlug }, null, 2)}
|
|
5786
|
+
`, { mode: 384 });
|
|
5787
|
+
} catch {
|
|
5788
|
+
}
|
|
5789
|
+
}
|
|
5790
|
+
async function resumeKeychainSession(o) {
|
|
5791
|
+
const workspace = readPointers(o.stateDir)[o.host];
|
|
5792
|
+
if (!workspace) return void 0;
|
|
5793
|
+
try {
|
|
5794
|
+
const stored = await o.store.get(o.host, workspace);
|
|
5795
|
+
if (!stored?.apiKey) return void 0;
|
|
5796
|
+
if (!await o.verify(stored.apiKey)) return void 0;
|
|
5797
|
+
return { workspace, apiKey: stored.apiKey };
|
|
5798
|
+
} catch {
|
|
5799
|
+
return void 0;
|
|
5800
|
+
}
|
|
5801
|
+
}
|
|
5802
|
+
|
|
5803
|
+
// src/reset.ts
|
|
5804
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync8, readFileSync as readFileSync14, readdirSync as readdirSync4, rmSync as rmSync7, statSync as statSync5, writeFileSync as writeFileSync9 } from "fs";
|
|
5805
|
+
import { dirname as dirname7, join as join15 } from "path";
|
|
5768
5806
|
function clonedCheckoutsFile(stateDir) {
|
|
5769
|
-
return
|
|
5807
|
+
return join15(stateDir, "cloned-checkouts.json");
|
|
5770
5808
|
}
|
|
5771
5809
|
function readClonedCheckouts(stateDir) {
|
|
5772
5810
|
try {
|
|
5773
|
-
const parsed = JSON.parse(
|
|
5811
|
+
const parsed = JSON.parse(readFileSync14(clonedCheckoutsFile(stateDir), "utf8"));
|
|
5774
5812
|
return Array.isArray(parsed) ? parsed.filter((d) => typeof d === "string") : [];
|
|
5775
5813
|
} catch {
|
|
5776
5814
|
return [];
|
|
@@ -5780,8 +5818,8 @@ function recordClonedCheckout(stateDir, dir) {
|
|
|
5780
5818
|
const known = readClonedCheckouts(stateDir);
|
|
5781
5819
|
if (known.includes(dir)) return;
|
|
5782
5820
|
const file = clonedCheckoutsFile(stateDir);
|
|
5783
|
-
|
|
5784
|
-
|
|
5821
|
+
mkdirSync8(dirname7(file), { recursive: true });
|
|
5822
|
+
writeFileSync9(file, `${JSON.stringify([...known, dir], null, 2)}
|
|
5785
5823
|
`);
|
|
5786
5824
|
}
|
|
5787
5825
|
var mark = (dryRun) => dryRun ? "would-remove" : "removed";
|
|
@@ -5836,10 +5874,10 @@ function removeCheckout(dir, dryRun, fx) {
|
|
|
5836
5874
|
}
|
|
5837
5875
|
function removeState(stateDir, dryRun) {
|
|
5838
5876
|
const out = [];
|
|
5839
|
-
const targets = [
|
|
5877
|
+
const targets = [join15(stateDir, "resolved"), join15(stateDir, "keys"), clonedCheckoutsFile(stateDir), join15(stateDir, "connect-session.json"), join15(stateDir, "reload-requested"), join15(stateDir, "daemon.started")];
|
|
5840
5878
|
try {
|
|
5841
5879
|
for (const name of readdirSync4(stateDir)) {
|
|
5842
|
-
if (name.startsWith(".") && statSync5(
|
|
5880
|
+
if (name.startsWith(".") && statSync5(join15(stateDir, name)).isFile()) targets.push(join15(stateDir, name));
|
|
5843
5881
|
}
|
|
5844
5882
|
} catch {
|
|
5845
5883
|
}
|
|
@@ -6113,18 +6151,18 @@ import { existsSync as existsSync16, statfsSync } from "fs";
|
|
|
6113
6151
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
6114
6152
|
import {
|
|
6115
6153
|
existsSync as existsSync15,
|
|
6116
|
-
mkdirSync as
|
|
6154
|
+
mkdirSync as mkdirSync9,
|
|
6117
6155
|
openSync as openSync3,
|
|
6118
6156
|
closeSync as closeSync3,
|
|
6119
6157
|
writeSync,
|
|
6120
6158
|
statSync as statSync6,
|
|
6121
6159
|
unlinkSync as unlinkSync2,
|
|
6122
|
-
readFileSync as
|
|
6123
|
-
writeFileSync as
|
|
6160
|
+
readFileSync as readFileSync15,
|
|
6161
|
+
writeFileSync as writeFileSync10,
|
|
6124
6162
|
readdirSync as readdirSync5,
|
|
6125
6163
|
rmSync as rmSync8
|
|
6126
6164
|
} from "fs";
|
|
6127
|
-
import { dirname as
|
|
6165
|
+
import { dirname as dirname8, basename as basename5, join as join16 } from "path";
|
|
6128
6166
|
var GitError = class extends Error {
|
|
6129
6167
|
};
|
|
6130
6168
|
function streamText(x) {
|
|
@@ -6490,7 +6528,7 @@ function recoverStrandedReleaseCheckouts(cwd) {
|
|
|
6490
6528
|
if (line.startsWith("worktree ")) path = line.slice("worktree ".length);
|
|
6491
6529
|
else if (line.startsWith("branch refs/heads/crew/release-") && path) {
|
|
6492
6530
|
gitOk(cwd, ["worktree", "remove", "--force", path]);
|
|
6493
|
-
const root =
|
|
6531
|
+
const root = dirname8(path);
|
|
6494
6532
|
if (basename5(root).startsWith("crew-release-")) rmSync8(root, { recursive: true, force: true });
|
|
6495
6533
|
}
|
|
6496
6534
|
}
|
|
@@ -6508,11 +6546,11 @@ function sweepStaleReleaseRoots(tmp, now = Date.now(), maxAgeMs = 60 * 60 * 1e3)
|
|
|
6508
6546
|
}
|
|
6509
6547
|
for (const name of names) {
|
|
6510
6548
|
if (!name.startsWith("crew-release-")) continue;
|
|
6511
|
-
const root =
|
|
6549
|
+
const root = join16(tmp, name);
|
|
6512
6550
|
try {
|
|
6513
6551
|
const st = statSync6(root);
|
|
6514
6552
|
if (!st.isDirectory() || now - st.mtimeMs < maxAgeMs) continue;
|
|
6515
|
-
if (existsSync15(
|
|
6553
|
+
if (existsSync15(join16(root, "checkout"))) continue;
|
|
6516
6554
|
rmSync8(root, { recursive: true, force: true });
|
|
6517
6555
|
removed.push(name);
|
|
6518
6556
|
} catch {
|
|
@@ -6561,7 +6599,7 @@ function ensureRepoCheckout(dir, remote, opts = {}) {
|
|
|
6561
6599
|
`${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`
|
|
6562
6600
|
);
|
|
6563
6601
|
}
|
|
6564
|
-
|
|
6602
|
+
mkdirSync9(dirname8(dir), { recursive: true });
|
|
6565
6603
|
const url = cloneUrlFor(remote, opts);
|
|
6566
6604
|
try {
|
|
6567
6605
|
execFileSync2("git", ["clone", url, dir], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
@@ -6586,7 +6624,7 @@ function sleepSync(ms2) {
|
|
|
6586
6624
|
}
|
|
6587
6625
|
function acquireCheckoutLock(dir, timeoutMs = 3e4) {
|
|
6588
6626
|
const lockPath = checkoutLockPath(dir);
|
|
6589
|
-
|
|
6627
|
+
mkdirSync9(dirname8(lockPath), { recursive: true });
|
|
6590
6628
|
const deadline = Date.now() + timeoutMs;
|
|
6591
6629
|
for (; ; ) {
|
|
6592
6630
|
try {
|
|
@@ -6626,14 +6664,14 @@ function syncPassengerCheckout(dir, githubRemote, gitRemoteName, base, clone = {
|
|
|
6626
6664
|
if (!existsSync15(`${dir}/.git`) && githubRemote) {
|
|
6627
6665
|
const url = cloneUrlFor(githubRemote, clone);
|
|
6628
6666
|
const marker = cloneFailureMarker(dir);
|
|
6629
|
-
if (existsSync15(marker) &&
|
|
6630
|
-
return { action: "clone-failed", detail:
|
|
6667
|
+
if (existsSync15(marker) && readFileSync15(marker, "utf8").split("\n")[0] === url) {
|
|
6668
|
+
return { action: "clone-failed", detail: readFileSync15(marker, "utf8").split("\n").slice(1).join("\n"), repeat: true };
|
|
6631
6669
|
}
|
|
6632
6670
|
try {
|
|
6633
6671
|
ensureRepoCheckout(dir, githubRemote, clone);
|
|
6634
6672
|
} catch (e) {
|
|
6635
6673
|
if (!(e instanceof GitError)) throw e;
|
|
6636
|
-
|
|
6674
|
+
writeFileSync10(marker, `${url}
|
|
6637
6675
|
${e.message}`);
|
|
6638
6676
|
return { action: "clone-failed", detail: e.message, repeat: false };
|
|
6639
6677
|
}
|
|
@@ -8762,8 +8800,8 @@ function materializeBranchForTicket(dir, cfg, t, role, onProblem) {
|
|
|
8762
8800
|
}
|
|
8763
8801
|
|
|
8764
8802
|
// src/poll.ts
|
|
8765
|
-
import { writeFileSync as
|
|
8766
|
-
import { join as
|
|
8803
|
+
import { writeFileSync as writeFileSync11 } from "fs";
|
|
8804
|
+
import { join as join17 } from "path";
|
|
8767
8805
|
|
|
8768
8806
|
// src/stalled.ts
|
|
8769
8807
|
var STALLED_AFTER_CYCLES = 30;
|
|
@@ -9054,7 +9092,7 @@ async function decideCycle(o) {
|
|
|
9054
9092
|
}
|
|
9055
9093
|
return decision;
|
|
9056
9094
|
}
|
|
9057
|
-
var digestPath = (stateDir, route, role) =>
|
|
9095
|
+
var digestPath = (stateDir, route, role) => join17(stateDir, `digest-${route}-${role}.md`);
|
|
9058
9096
|
function writeDigest(o, d, role, stateDir) {
|
|
9059
9097
|
const me = o.route.resolved?.seats[role];
|
|
9060
9098
|
if (!me) return false;
|
|
@@ -9118,7 +9156,7 @@ function writeDigest(o, d, role, stateDir) {
|
|
|
9118
9156
|
};
|
|
9119
9157
|
const text = role === "qa" ? qaDigest(input) : buildingDigest(input);
|
|
9120
9158
|
const path = digestPath(stateDir, routeSlug(o.route.route), role);
|
|
9121
|
-
|
|
9159
|
+
writeFileSync11(path, text);
|
|
9122
9160
|
o.emit.emit(`queue digest written for ${role} (${Buffer.byteLength(text)} bytes)`, {
|
|
9123
9161
|
data: { role, bytes: Buffer.byteLength(text) }
|
|
9124
9162
|
});
|
|
@@ -9496,12 +9534,12 @@ async function applyStrandedVerified(writer, s, contract, operatorId, memberId,
|
|
|
9496
9534
|
|
|
9497
9535
|
// src/run-worktree.ts
|
|
9498
9536
|
import { existsSync as existsSync18 } from "fs";
|
|
9499
|
-
import { dirname as
|
|
9537
|
+
import { dirname as dirname9, join as join19 } from "path";
|
|
9500
9538
|
|
|
9501
9539
|
// src/ports.ts
|
|
9502
9540
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
9503
9541
|
import { existsSync as existsSync17 } from "fs";
|
|
9504
|
-
import { join as
|
|
9542
|
+
import { join as join18 } from "path";
|
|
9505
9543
|
var run = (cmd, args) => {
|
|
9506
9544
|
try {
|
|
9507
9545
|
return execFileSync3(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
@@ -9546,7 +9584,7 @@ function ticketForPort(port, s = DEFAULT_PORTS) {
|
|
|
9546
9584
|
}
|
|
9547
9585
|
function worktreeExists(parent, prefix, n) {
|
|
9548
9586
|
for (const name of [`${prefix}${n}`, `${prefix}${String(n).padStart(3, "0")}`]) {
|
|
9549
|
-
if (existsSync17(
|
|
9587
|
+
if (existsSync17(join18(parent, name, ".git"))) return true;
|
|
9550
9588
|
}
|
|
9551
9589
|
return false;
|
|
9552
9590
|
}
|
|
@@ -9638,7 +9676,7 @@ function planRunWorktree(dir, cfg, t, role, opts = {}) {
|
|
|
9638
9676
|
if (countCommits(existing.path, `${baseRef}..HEAD`) > 0) return { action: "leave-unpushed", path: existing.path, branch };
|
|
9639
9677
|
return { action: "recut", path: existing.path, branch, base: baseRef };
|
|
9640
9678
|
}
|
|
9641
|
-
const path =
|
|
9679
|
+
const path = join19(dirname9(dir), worktreeForTicket(dir, cfg, t, role));
|
|
9642
9680
|
if (located?.where === "remote" || located && hasRemote && remoteBranchExists(dir, remote, located.branch)) {
|
|
9643
9681
|
return { action: "cut-remote", path, branch: located.branch };
|
|
9644
9682
|
}
|
|
@@ -9960,8 +9998,8 @@ async function guardRunStrays(route, ship, tracker, role, workingId, since2, log
|
|
|
9960
9998
|
}
|
|
9961
9999
|
|
|
9962
10000
|
// src/connect-wizard.ts
|
|
9963
|
-
import { delimiter, dirname as
|
|
9964
|
-
import { accessSync as accessSync2, constants as constants2, existsSync as existsSync19, mkdirSync as
|
|
10001
|
+
import { delimiter, dirname as dirname10, join as join20 } from "path";
|
|
10002
|
+
import { accessSync as accessSync2, constants as constants2, existsSync as existsSync19, mkdirSync as mkdirSync10, writeFileSync as writeFileSync12 } from "fs";
|
|
9965
10003
|
import { stringify } from "yaml";
|
|
9966
10004
|
var HOST_PASSENGERS_DOCS_URL = "https://crew.tablation.dev/host-passengers";
|
|
9967
10005
|
function shouldRunWizard(opts) {
|
|
@@ -9983,7 +10021,7 @@ function defaultIsExecutable(p) {
|
|
|
9983
10021
|
function findOnPath2(name, pathEnv, isExecutable = defaultIsExecutable) {
|
|
9984
10022
|
for (const dir of pathEnv.split(delimiter)) {
|
|
9985
10023
|
if (!dir) continue;
|
|
9986
|
-
const candidate =
|
|
10024
|
+
const candidate = join20(dir, name);
|
|
9987
10025
|
if (isExecutable(candidate)) return candidate;
|
|
9988
10026
|
}
|
|
9989
10027
|
return void 0;
|
|
@@ -10009,9 +10047,9 @@ async function runWizard(p, env) {
|
|
|
10009
10047
|
if (entered && isExec(entered)) agentBin = entered;
|
|
10010
10048
|
else p.say(entered ? `${entered} is not an executable file.` : "A path is required.");
|
|
10011
10049
|
}
|
|
10012
|
-
const nodeDir =
|
|
10050
|
+
const nodeDir = dirname10(env.nodePath);
|
|
10013
10051
|
const pnpmPath = findOnPath2("pnpm", env.pathEnv, isExec);
|
|
10014
|
-
const baseDirs = [nodeDir, pnpmPath ?
|
|
10052
|
+
const baseDirs = [nodeDir, pnpmPath ? dirname10(pnpmPath) : void 0];
|
|
10015
10053
|
p.say(
|
|
10016
10054
|
`
|
|
10017
10055
|
Host Passengers lets this machine run the agent sessions that other people's
|
|
@@ -10022,7 +10060,7 @@ Tablation workspaces ask for, in a container. More: ${HOST_PASSENGERS_DOCS_URL}`
|
|
|
10022
10060
|
p.say("Docker is not available here, so nothing will be hosted until it is. Your answer is recorded anyway.");
|
|
10023
10061
|
}
|
|
10024
10062
|
const dockerPath = hostPassengers ? findOnPath2("docker", env.pathEnv, isExec) : void 0;
|
|
10025
|
-
const extraPath = deriveExtraPath([...baseDirs, dockerPath ?
|
|
10063
|
+
const extraPath = deriveExtraPath([...baseDirs, dockerPath ? dirname10(dockerPath) : void 0]);
|
|
10026
10064
|
if (extraPath) {
|
|
10027
10065
|
p.say(`Hooks will get ${extraPath} on their PATH (where node, pnpm${dockerPath ? " and docker" : ""} live).`);
|
|
10028
10066
|
}
|
|
@@ -10044,9 +10082,9 @@ routes:
|
|
|
10044
10082
|
${routeBlock}`;
|
|
10045
10083
|
}
|
|
10046
10084
|
function writeNewConfig(path, text) {
|
|
10047
|
-
|
|
10085
|
+
mkdirSync10(dirname10(path), { recursive: true });
|
|
10048
10086
|
try {
|
|
10049
|
-
|
|
10087
|
+
writeFileSync12(path, text, { mode: 384, flag: "wx" });
|
|
10050
10088
|
return true;
|
|
10051
10089
|
} catch (e) {
|
|
10052
10090
|
if (e.code === "EEXIST") return false;
|
|
@@ -10079,7 +10117,7 @@ Wrote ${o.configPath}.
|
|
|
10079
10117
|
|
|
10080
10118
|
// src/connect-json.ts
|
|
10081
10119
|
import { accessSync as accessSync3, constants as constants3 } from "fs";
|
|
10082
|
-
import { dirname as
|
|
10120
|
+
import { dirname as dirname11 } from "path";
|
|
10083
10121
|
var CHOICE_EXIT_CODE = 3;
|
|
10084
10122
|
function createEmitter(json, write) {
|
|
10085
10123
|
return {
|
|
@@ -10127,7 +10165,7 @@ function flagWizardAnswers(f, env) {
|
|
|
10127
10165
|
if (f.agentBin && !isExec(f.agentBin)) return { error: `--agent-bin ${f.agentBin} is not an executable file` };
|
|
10128
10166
|
const pnpmPath = findOnPath2("pnpm", env.pathEnv, isExec);
|
|
10129
10167
|
const dockerPath = f.hostPassengers ? findOnPath2("docker", env.pathEnv, isExec) : void 0;
|
|
10130
|
-
const extraPath = f.extraPath ?? deriveExtraPath([
|
|
10168
|
+
const extraPath = f.extraPath ?? deriveExtraPath([dirname11(env.nodePath), pnpmPath ? dirname11(pnpmPath) : void 0, dockerPath ? dirname11(dockerPath) : void 0]);
|
|
10131
10169
|
return {
|
|
10132
10170
|
answers: {
|
|
10133
10171
|
name: f.shipName?.trim() || env.hostname,
|
|
@@ -10140,8 +10178,8 @@ function flagWizardAnswers(f, env) {
|
|
|
10140
10178
|
}
|
|
10141
10179
|
|
|
10142
10180
|
// src/release-run.ts
|
|
10143
|
-
import { readFileSync as
|
|
10144
|
-
import { dirname as
|
|
10181
|
+
import { readFileSync as readFileSync16, writeFileSync as writeFileSync13, existsSync as existsSync20, copyFileSync, mkdirSync as mkdirSync11, mkdtempSync as mkdtempSync2, rmSync as rmSync9 } from "fs";
|
|
10182
|
+
import { dirname as dirname12, join as join21 } from "path";
|
|
10145
10183
|
import { tmpdir as tmpdir3 } from "os";
|
|
10146
10184
|
|
|
10147
10185
|
// src/release.ts
|
|
@@ -10829,13 +10867,13 @@ async function bump(o, size) {
|
|
|
10829
10867
|
}
|
|
10830
10868
|
return printed;
|
|
10831
10869
|
}
|
|
10832
|
-
const files = o.repo.release.versionFiles.map((f) =>
|
|
10870
|
+
const files = o.repo.release.versionFiles.map((f) => join21(o.cwd, f));
|
|
10833
10871
|
const first = files[0];
|
|
10834
10872
|
if (!existsSync20(first)) {
|
|
10835
10873
|
o.emit.error(`no ${o.repo.release.versionFiles[0]} to read a version from`);
|
|
10836
10874
|
return null;
|
|
10837
10875
|
}
|
|
10838
|
-
const current = JSON.parse(
|
|
10876
|
+
const current = JSON.parse(readFileSync16(first, "utf8")).version;
|
|
10839
10877
|
if (!current) {
|
|
10840
10878
|
o.emit.error(`no version field in ${o.repo.release.versionFiles[0]}`);
|
|
10841
10879
|
return null;
|
|
@@ -10848,27 +10886,27 @@ async function bump(o, size) {
|
|
|
10848
10886
|
return version;
|
|
10849
10887
|
}
|
|
10850
10888
|
for (const f of files) {
|
|
10851
|
-
const src =
|
|
10889
|
+
const src = readFileSync16(f, "utf8");
|
|
10852
10890
|
const out = src.replace(/("version"\s*:\s*")[^"]*(")/, `$1${version}$2`);
|
|
10853
10891
|
if (out === src) {
|
|
10854
10892
|
o.emit.error(`no version field in ${f}`);
|
|
10855
10893
|
return null;
|
|
10856
10894
|
}
|
|
10857
|
-
|
|
10895
|
+
writeFileSync13(f, out);
|
|
10858
10896
|
}
|
|
10859
10897
|
return version;
|
|
10860
10898
|
}
|
|
10861
10899
|
function writeChangelog(o, version, entries) {
|
|
10862
10900
|
const file = o.repo.release.changelog;
|
|
10863
10901
|
if (!file || entries.length === 0) return;
|
|
10864
|
-
const path =
|
|
10902
|
+
const path = join21(o.cwd, file);
|
|
10865
10903
|
const section = renderChangelogSection(version, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), entries);
|
|
10866
10904
|
if (o.dryRun) {
|
|
10867
10905
|
o.emit.emit(`would prepend ${entries.length} entr(ies) to ${file}`);
|
|
10868
10906
|
return;
|
|
10869
10907
|
}
|
|
10870
|
-
const existing = existsSync20(path) ?
|
|
10871
|
-
|
|
10908
|
+
const existing = existsSync20(path) ? readFileSync16(path, "utf8") : "# Changelog\n";
|
|
10909
|
+
writeFileSync13(path, insertChangelogSection(existing, section));
|
|
10872
10910
|
}
|
|
10873
10911
|
async function confirm(o, expected) {
|
|
10874
10912
|
if (!o.repo.hooks.released) return void 0;
|
|
@@ -10961,8 +10999,8 @@ function closeReleaseCheckout(cwd, root, dir, branch, run2 = gitOk) {
|
|
|
10961
10999
|
function openReleaseCheckout(o) {
|
|
10962
11000
|
const { remote, base } = o.repo.branch;
|
|
10963
11001
|
const branch = `crew/release-${Date.now()}`;
|
|
10964
|
-
const root = mkdtempSync2(
|
|
10965
|
-
const dir =
|
|
11002
|
+
const root = mkdtempSync2(join21(tmpdir3(), "crew-release-"));
|
|
11003
|
+
const dir = join21(root, "checkout");
|
|
10966
11004
|
o.emit.emit(`release checkout: ${dir}`, { step: "release" });
|
|
10967
11005
|
const close = () => closeReleaseCheckout(o.cwd, root, dir, branch);
|
|
10968
11006
|
try {
|
|
@@ -10972,10 +11010,10 @@ function openReleaseCheckout(o) {
|
|
|
10972
11010
|
throw e;
|
|
10973
11011
|
}
|
|
10974
11012
|
for (const f of o.repo.worktrees.copy) {
|
|
10975
|
-
const src =
|
|
11013
|
+
const src = join21(o.cwd, f);
|
|
10976
11014
|
if (!existsSync20(src)) continue;
|
|
10977
|
-
|
|
10978
|
-
copyFileSync(src,
|
|
11015
|
+
mkdirSync11(dirname12(join21(dir, f)), { recursive: true });
|
|
11016
|
+
copyFileSync(src, join21(dir, f));
|
|
10979
11017
|
}
|
|
10980
11018
|
return { dir, branch, close };
|
|
10981
11019
|
}
|
|
@@ -12769,8 +12807,8 @@ function describeCapability(i) {
|
|
|
12769
12807
|
}
|
|
12770
12808
|
|
|
12771
12809
|
// src/agents-sync-run.ts
|
|
12772
|
-
import { existsSync as existsSync22, mkdirSync as
|
|
12773
|
-
import { dirname as
|
|
12810
|
+
import { existsSync as existsSync22, mkdirSync as mkdirSync12, readFileSync as readFileSync17, writeFileSync as writeFileSync14 } from "fs";
|
|
12811
|
+
import { dirname as dirname13 } from "path";
|
|
12774
12812
|
async function runAgentsSync(target, o) {
|
|
12775
12813
|
const dryRun = o.dryRun ?? false;
|
|
12776
12814
|
const force = o.force ?? false;
|
|
@@ -12803,12 +12841,12 @@ async function runAgentsSync(target, o) {
|
|
|
12803
12841
|
for (const s of skillResult?.outcomes ?? []) o.say(describeSkillSyncOutcome(s));
|
|
12804
12842
|
if (!dryRun) {
|
|
12805
12843
|
const resolvedPath = resolvedPathFor(o.stateDir, target.route);
|
|
12806
|
-
const raw = existsSync22(resolvedPath) ? JSON.parse(
|
|
12844
|
+
const raw = existsSync22(resolvedPath) ? JSON.parse(readFileSync17(resolvedPath, "utf8")) : {};
|
|
12807
12845
|
raw.agentPersonas = personaResult.agentPersonas;
|
|
12808
12846
|
if (skillResult) raw.agentSkills = skillResult.agentSkills;
|
|
12809
12847
|
if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
|
|
12810
|
-
|
|
12811
|
-
|
|
12848
|
+
mkdirSync12(dirname13(resolvedPath), { recursive: true });
|
|
12849
|
+
writeFileSync14(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
12812
12850
|
`);
|
|
12813
12851
|
if (target.resolved) {
|
|
12814
12852
|
target.resolved.agentPersonas = personaResult.agentPersonas;
|
|
@@ -12915,7 +12953,7 @@ import { isMap as isMap2, parseDocument as parseDocument2, YAMLMap } from "yaml"
|
|
|
12915
12953
|
|
|
12916
12954
|
// src/repos-cmd.ts
|
|
12917
12955
|
import { homedir as homedir7 } from "os";
|
|
12918
|
-
import { basename as basename7, join as
|
|
12956
|
+
import { basename as basename7, join as join22, resolve as resolve3 } from "path";
|
|
12919
12957
|
import { isMap, isSeq, parseDocument, stringify as stringify2 } from "yaml";
|
|
12920
12958
|
var ReposError = class extends Error {
|
|
12921
12959
|
};
|
|
@@ -13113,7 +13151,7 @@ function planRepoClone(i) {
|
|
|
13113
13151
|
}
|
|
13114
13152
|
const ship = doc.get("ship", true);
|
|
13115
13153
|
const basePath = String(m.get("reposBasePath") ?? (isMap(ship) ? ship.get("reposBasePath") : void 0) ?? "~/Crew");
|
|
13116
|
-
const target = i.dir ? expandDir(i.dir, i.base) :
|
|
13154
|
+
const target = i.dir ? expandDir(i.dir, i.base) : join22(expandDir(basePath, i.base), i.route.split("/")[0], name);
|
|
13117
13155
|
const add = planRepoAdd({ text: i.text, base: i.base, route: i.route, path: target, origin: remote, resolved: i.resolved, name });
|
|
13118
13156
|
return { ...add, remote, target };
|
|
13119
13157
|
}
|
|
@@ -13167,17 +13205,62 @@ var DETECTORS = [
|
|
|
13167
13205
|
{ source: "pyproject.toml", detect: fromPyproject },
|
|
13168
13206
|
{ source: "Makefile", detect: fromMakefile }
|
|
13169
13207
|
];
|
|
13208
|
+
var WORKFLOW_RE = /\.ya?ml$/;
|
|
13209
|
+
function detectRecipes(f) {
|
|
13210
|
+
const workflows = (f.list?.(".github/workflows") ?? []).filter((n) => WORKFLOW_RE.test(n)).sort();
|
|
13211
|
+
if (workflows.length === 0) return [];
|
|
13212
|
+
const evidence = workflows.map((n) => `.github/workflows/${n}`);
|
|
13213
|
+
return [
|
|
13214
|
+
{
|
|
13215
|
+
id: "github-actions-release",
|
|
13216
|
+
title: "Releases run in GitHub Actions",
|
|
13217
|
+
description: "CI publishes the release after the crew merges; the crew watches `gh release view` for the live tag.",
|
|
13218
|
+
evidence,
|
|
13219
|
+
// `ltrimstr` because `verify.match: version` compares the bare version, not `v1.2.3`.
|
|
13220
|
+
hooks: { released: `gh release view --json tagName -q '.tagName | ltrimstr("v")'` },
|
|
13221
|
+
release: { mode: "ci_auto", ci: "github", verifyMatch: "version" },
|
|
13222
|
+
writable: true,
|
|
13223
|
+
defaultOn: true
|
|
13224
|
+
},
|
|
13225
|
+
{
|
|
13226
|
+
id: "github-actions-ci-status",
|
|
13227
|
+
title: "Check CI status before hand-off",
|
|
13228
|
+
description: "Ask GitHub whether the latest run on the branch passed before a ticket is handed to QA. The repo spec has no hook for this yet, so it is reported but cannot be written.",
|
|
13229
|
+
evidence,
|
|
13230
|
+
hooks: {},
|
|
13231
|
+
check: `gh run list --branch "$CREW_BRANCH" --limit 1 --json conclusion -q '.[0].conclusion == "success"'`,
|
|
13232
|
+
writable: false,
|
|
13233
|
+
defaultOn: false
|
|
13234
|
+
}
|
|
13235
|
+
];
|
|
13236
|
+
}
|
|
13170
13237
|
function suggestHooks(f) {
|
|
13238
|
+
const recipes = detectRecipes(f);
|
|
13171
13239
|
const found = DETECTORS.flatMap((d) => {
|
|
13172
13240
|
const hooks = d.detect(f);
|
|
13173
13241
|
return hooks && Object.keys(hooks).length > 0 ? [{ source: d.source, hooks }] : [];
|
|
13174
13242
|
});
|
|
13175
13243
|
if (found.length === 0) {
|
|
13176
|
-
return { source: null, hooks: {}, notes: ["no package.json scripts, Package.swift, pyproject.toml or Makefile test/build targets found \u2014 nothing to propose"] };
|
|
13244
|
+
return { source: null, hooks: {}, notes: ["no package.json scripts, Package.swift, pyproject.toml or Makefile test/build targets found \u2014 nothing to propose"], recipes };
|
|
13177
13245
|
}
|
|
13178
13246
|
const [first, ...others] = found;
|
|
13179
13247
|
const notes = others.length > 0 ? [`also found ${others.map((o) => o.source).join(", ")}; proposing from ${first.source} only`] : [];
|
|
13180
|
-
return { source: first.source, hooks: first.hooks, notes };
|
|
13248
|
+
return { source: first.source, hooks: first.hooks, notes, recipes };
|
|
13249
|
+
}
|
|
13250
|
+
function selectRecipes(s, ids) {
|
|
13251
|
+
const chosen = ids.map((id) => {
|
|
13252
|
+
const r = s.recipes.find((x) => x.id === id);
|
|
13253
|
+
if (!r) throw new ReposError(`no recipe "${id}" here (have: ${s.recipes.map((x) => x.id).join(", ") || "none"})`);
|
|
13254
|
+
if (!r.writable) throw new ReposError(`recipe "${id}" cannot be written: ${r.description}`);
|
|
13255
|
+
return r;
|
|
13256
|
+
});
|
|
13257
|
+
let hooks = { ...s.hooks };
|
|
13258
|
+
let release;
|
|
13259
|
+
for (const r of chosen) {
|
|
13260
|
+
hooks = { ...r.hooks, ...hooks };
|
|
13261
|
+
release ??= r.release;
|
|
13262
|
+
}
|
|
13263
|
+
return { hooks, ...release ? { release } : {} };
|
|
13181
13264
|
}
|
|
13182
13265
|
function parseHooksArgs(args, routes) {
|
|
13183
13266
|
const rest = [...args];
|
|
@@ -13187,7 +13270,7 @@ function parseHooksArgs(args, routes) {
|
|
|
13187
13270
|
if (routes.length === 1) route = routes[0];
|
|
13188
13271
|
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(", ")}`);
|
|
13189
13272
|
}
|
|
13190
|
-
if (rest.length !== 1) throw new ReposError("expected: crew repos hooks [route] <name> [--suggest] [--write route|repo] [--json]");
|
|
13273
|
+
if (rest.length !== 1) throw new ReposError("expected: crew repos hooks [route] <name> [--suggest] [--agent] [--write route|repo] [--recipes ID,ID] [--json]");
|
|
13191
13274
|
return { route, name: rest[0] };
|
|
13192
13275
|
}
|
|
13193
13276
|
function repoDirOf(text, base, route, name) {
|
|
@@ -13233,7 +13316,7 @@ function planRouteHooks(text, route, name, hooks) {
|
|
|
13233
13316
|
}
|
|
13234
13317
|
return { text: String(doc), written, kept };
|
|
13235
13318
|
}
|
|
13236
|
-
function starterRepoConfig(hooks) {
|
|
13319
|
+
function starterRepoConfig(hooks, release) {
|
|
13237
13320
|
const lines = [
|
|
13238
13321
|
"# Starter .crew.yaml proposed by `crew repos hooks --write repo`.",
|
|
13239
13322
|
"# Review the hooks, then commit this file; it is the durable home for them",
|
|
@@ -13246,7 +13329,12 @@ function starterRepoConfig(hooks) {
|
|
|
13246
13329
|
const entries = Object.entries(hooks).filter(([, v]) => v !== void 0);
|
|
13247
13330
|
if (entries.length === 0) lines.push(' # test: "..."', ' # build: "..."');
|
|
13248
13331
|
for (const [k, v] of entries) lines.push(` ${k}: ${JSON.stringify(v)}`);
|
|
13249
|
-
|
|
13332
|
+
if (release) {
|
|
13333
|
+
lines.push("", "release:", ` mode: ${release.mode}`);
|
|
13334
|
+
if (release.ci) lines.push(` ci: { provider: ${release.ci} }`);
|
|
13335
|
+
if (release.verifyMatch) lines.push(" verify:", ` match: ${release.verifyMatch}`);
|
|
13336
|
+
lines.push("");
|
|
13337
|
+
} else lines.push("", "release:", " # integrate: merging to the base branch is the release; nothing is deployed.", " mode: integrate", "");
|
|
13250
13338
|
const text = lines.join("\n");
|
|
13251
13339
|
parseRepoConfig(text, ".crew.yaml");
|
|
13252
13340
|
return text;
|
|
@@ -13255,8 +13343,180 @@ function existingRepoConfig(f) {
|
|
|
13255
13343
|
return REPO_CONFIG_FILENAMES.find((n) => f.exists(n));
|
|
13256
13344
|
}
|
|
13257
13345
|
|
|
13346
|
+
// src/repos-hooks-agent.ts
|
|
13347
|
+
import { spawn as spawn4, execFileSync as execFileSync4 } from "child_process";
|
|
13348
|
+
import { realpathSync as realpathSync2 } from "fs";
|
|
13349
|
+
import { isAbsolute as isAbsolute2, relative } from "path";
|
|
13350
|
+
var AgentSuggestError = class extends Error {
|
|
13351
|
+
reason;
|
|
13352
|
+
constructor(reason, message) {
|
|
13353
|
+
super(message);
|
|
13354
|
+
this.reason = reason;
|
|
13355
|
+
}
|
|
13356
|
+
};
|
|
13357
|
+
var HOOK_KEYS = ["test", "build", "version", "bump", "released"];
|
|
13358
|
+
var READ_ONLY_TOOLS = ["Read", "Grep", "Glob"];
|
|
13359
|
+
var WRITING_TOOLS = ["Bash", "Edit", "Write", "NotebookEdit", "WebFetch", "WebSearch", "Task", "Agent"];
|
|
13360
|
+
var DEFAULT_TIMEOUT_MS = 18e4;
|
|
13361
|
+
function buildAgentPrompt(sniff) {
|
|
13362
|
+
return [
|
|
13363
|
+
"You are reviewing a git checkout (the current directory) to work out how to test and build it.",
|
|
13364
|
+
"Read README*, CLAUDE.md, AGENTS.md, GEMINI.md, package/build manifests and .github/workflows/* as needed.",
|
|
13365
|
+
"You may only read files. Do not modify anything and do not run commands.",
|
|
13366
|
+
"",
|
|
13367
|
+
`A file sniff already proposed: ${JSON.stringify(sniff.hooks)}.`,
|
|
13368
|
+
"Propose the commands a contributor should run, using the repo's own documented ones over guesses.",
|
|
13369
|
+
"Omit any hook you cannot support from a file you read.",
|
|
13370
|
+
"",
|
|
13371
|
+
"Answer with ONLY one JSON object, no prose and no code fence:",
|
|
13372
|
+
'{"hooks": {"test": "...", "build": "...", "version": "...", "bump": "...", "released": "..."}, "notes": ["..."]}',
|
|
13373
|
+
"`version` prints the bare version, `bump` bumps by $CREW_BUMP (patch|minor|major) and prints the new version,",
|
|
13374
|
+
"`released` prints what is live (a version or sha). Keys you cannot support are left out."
|
|
13375
|
+
].join("\n");
|
|
13376
|
+
}
|
|
13377
|
+
function agentInvocation(spec, model) {
|
|
13378
|
+
if (spec.harness !== "claude") {
|
|
13379
|
+
throw new AgentSuggestError("unsupported_harness", `--agent supports the claude harness only (toolchain "${spec.name}" is ${spec.harness})`);
|
|
13380
|
+
}
|
|
13381
|
+
return wrapInvocation(spec, {
|
|
13382
|
+
model,
|
|
13383
|
+
args: [
|
|
13384
|
+
"-p",
|
|
13385
|
+
"--allowedTools",
|
|
13386
|
+
...READ_ONLY_TOOLS,
|
|
13387
|
+
"--disallowedTools",
|
|
13388
|
+
...WRITING_TOOLS,
|
|
13389
|
+
"--model",
|
|
13390
|
+
model,
|
|
13391
|
+
"--output-format",
|
|
13392
|
+
"stream-json",
|
|
13393
|
+
"--verbose",
|
|
13394
|
+
...spec.args
|
|
13395
|
+
]
|
|
13396
|
+
});
|
|
13397
|
+
}
|
|
13398
|
+
function parseAgentStream(out, dir) {
|
|
13399
|
+
let text = "";
|
|
13400
|
+
let isError = false;
|
|
13401
|
+
const files = /* @__PURE__ */ new Set();
|
|
13402
|
+
let base = dir;
|
|
13403
|
+
try {
|
|
13404
|
+
base = realpathSync2(dir);
|
|
13405
|
+
} catch {
|
|
13406
|
+
}
|
|
13407
|
+
for (const line of out.split("\n")) {
|
|
13408
|
+
if (!line.trim()) continue;
|
|
13409
|
+
let ev;
|
|
13410
|
+
try {
|
|
13411
|
+
ev = JSON.parse(line);
|
|
13412
|
+
} catch {
|
|
13413
|
+
continue;
|
|
13414
|
+
}
|
|
13415
|
+
if (ev?.type === "assistant" && Array.isArray(ev.message?.content)) {
|
|
13416
|
+
for (const b of ev.message.content) {
|
|
13417
|
+
const p = b?.type === "tool_use" && b.name === "Read" ? b.input?.file_path : void 0;
|
|
13418
|
+
if (typeof p === "string") files.add(isAbsolute2(p) ? relative(base, p) : p);
|
|
13419
|
+
}
|
|
13420
|
+
} else if (ev?.type === "result") {
|
|
13421
|
+
text = typeof ev.result === "string" ? ev.result : "";
|
|
13422
|
+
isError = ev.is_error === true;
|
|
13423
|
+
}
|
|
13424
|
+
}
|
|
13425
|
+
return { text, isError, filesRead: [...files].sort() };
|
|
13426
|
+
}
|
|
13427
|
+
function parseAgentHooks(text) {
|
|
13428
|
+
const start = text.indexOf("{");
|
|
13429
|
+
const end = text.lastIndexOf("}");
|
|
13430
|
+
if (start < 0 || end <= start) throw new AgentSuggestError("bad_output", "the toolchain did not answer with a JSON object");
|
|
13431
|
+
let obj;
|
|
13432
|
+
try {
|
|
13433
|
+
obj = JSON.parse(text.slice(start, end + 1));
|
|
13434
|
+
} catch {
|
|
13435
|
+
throw new AgentSuggestError("bad_output", "the toolchain's answer was not valid JSON");
|
|
13436
|
+
}
|
|
13437
|
+
const raw = obj && typeof obj === "object" && obj.hooks && typeof obj.hooks === "object" ? obj.hooks : {};
|
|
13438
|
+
const hooks = {};
|
|
13439
|
+
for (const k of HOOK_KEYS) if (typeof raw[k] === "string" && raw[k].trim()) hooks[k] = raw[k].trim();
|
|
13440
|
+
const notes = Array.isArray(obj.notes) ? obj.notes.filter((n) => typeof n === "string") : [];
|
|
13441
|
+
return { hooks, notes };
|
|
13442
|
+
}
|
|
13443
|
+
var SIGNED_OUT = /not logged in|\/login|invalid api key|authentication|unauthori[sz]ed|\b401\b|credit balance/i;
|
|
13444
|
+
function classifyFailure(o) {
|
|
13445
|
+
if (o.spawnError?.code === "ENOENT") return "not_installed";
|
|
13446
|
+
return SIGNED_OUT.test(o.output) ? "not_signed_in" : "failed";
|
|
13447
|
+
}
|
|
13448
|
+
function porcelain(dir) {
|
|
13449
|
+
try {
|
|
13450
|
+
return execFileSync4("git", ["status", "--porcelain", "--untracked-files=all"], { cwd: dir, encoding: "utf8" });
|
|
13451
|
+
} catch {
|
|
13452
|
+
return void 0;
|
|
13453
|
+
}
|
|
13454
|
+
}
|
|
13455
|
+
async function runAgentSuggest(o) {
|
|
13456
|
+
const model = o.model ?? o.spec.model;
|
|
13457
|
+
const { bin, args } = agentInvocation(o.spec, model);
|
|
13458
|
+
const adapter = adapterFor(o.spec.harness);
|
|
13459
|
+
const env = { ...process.env, ...adapter.runEnv(o.spec) };
|
|
13460
|
+
for (const v of adapter.unsetEnv) delete env[v];
|
|
13461
|
+
delete env.CREW_API_KEY;
|
|
13462
|
+
const before = porcelain(o.dir);
|
|
13463
|
+
const timeout = AbortSignal.timeout(o.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
13464
|
+
const signal = o.signal ? AbortSignal.any([o.signal, timeout]) : timeout;
|
|
13465
|
+
const run2 = await new Promise((resolve6) => {
|
|
13466
|
+
let out = "";
|
|
13467
|
+
let err = "";
|
|
13468
|
+
const child = spawn4(bin, args, { cwd: o.dir, env, detached: true, stdio: ["pipe", "pipe", "pipe"] });
|
|
13469
|
+
let settled = false;
|
|
13470
|
+
const settle = (r) => {
|
|
13471
|
+
if (settled) return;
|
|
13472
|
+
settled = true;
|
|
13473
|
+
signal.removeEventListener("abort", onAbort);
|
|
13474
|
+
resolve6(r);
|
|
13475
|
+
};
|
|
13476
|
+
const onAbort = () => {
|
|
13477
|
+
try {
|
|
13478
|
+
if (child.pid) process.kill(-child.pid, "SIGTERM");
|
|
13479
|
+
} catch {
|
|
13480
|
+
child.kill("SIGTERM");
|
|
13481
|
+
}
|
|
13482
|
+
child.stdout.destroy();
|
|
13483
|
+
child.stderr.destroy();
|
|
13484
|
+
settle({ code: null, out, err, aborted: true });
|
|
13485
|
+
};
|
|
13486
|
+
if (signal.aborted) onAbort();
|
|
13487
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
13488
|
+
child.stdout.on("data", (d) => {
|
|
13489
|
+
out += d.toString();
|
|
13490
|
+
});
|
|
13491
|
+
child.stderr.on("data", (d) => {
|
|
13492
|
+
err += d.toString();
|
|
13493
|
+
});
|
|
13494
|
+
child.on("error", (e) => settle({ code: null, out, err, spawnError: e, aborted: false }));
|
|
13495
|
+
child.on("close", (code) => settle({ code, out, err, aborted: false }));
|
|
13496
|
+
child.stdin.on("error", () => {
|
|
13497
|
+
});
|
|
13498
|
+
child.stdin.end(buildAgentPrompt(o.sniff));
|
|
13499
|
+
});
|
|
13500
|
+
if (run2.aborted || signal.aborted) {
|
|
13501
|
+
throw new AgentSuggestError(o.signal?.aborted ? "cancelled" : "timeout", o.signal?.aborted ? "cancelled" : "the toolchain did not answer in time");
|
|
13502
|
+
}
|
|
13503
|
+
const after = porcelain(o.dir);
|
|
13504
|
+
if (before !== void 0 && after !== before) {
|
|
13505
|
+
throw new AgentSuggestError("modified_checkout", "the checkout changed during --agent; inspect it with `git status` (the agent result was discarded)");
|
|
13506
|
+
}
|
|
13507
|
+
const parsed = parseAgentStream(run2.out, o.dir);
|
|
13508
|
+
if (run2.spawnError || run2.code !== 0 || parsed.isError) {
|
|
13509
|
+
const reason = classifyFailure({ code: run2.code, ...run2.spawnError ? { spawnError: run2.spawnError } : {}, output: `${parsed.text}
|
|
13510
|
+
${run2.err}` });
|
|
13511
|
+
throw new AgentSuggestError(reason, reason === "not_installed" ? `${bin} is not installed` : reason === "not_signed_in" ? `${o.spec.name} is not signed in` : (parsed.text || run2.err).trim().slice(0, 300) || `exited ${run2.code}`);
|
|
13512
|
+
}
|
|
13513
|
+
const { hooks, notes } = parseAgentHooks(parsed.text);
|
|
13514
|
+
const differsFromSniff = HOOK_KEYS.filter((k) => hooks[k] !== void 0 && hooks[k] !== o.sniff.hooks[k]);
|
|
13515
|
+
return { source: "agent", hooks, filesRead: parsed.filesRead, differsFromSniff, notes, toolchain: o.spec.name, model };
|
|
13516
|
+
}
|
|
13517
|
+
|
|
13258
13518
|
// src/sample-ticket.ts
|
|
13259
|
-
import { basename as basename8, dirname as
|
|
13519
|
+
import { basename as basename8, dirname as dirname15, join as join23 } from "path";
|
|
13260
13520
|
var SAMPLE_REPO = "crew-sample";
|
|
13261
13521
|
var SAMPLE_REMOTE = `local/${SAMPLE_REPO}`;
|
|
13262
13522
|
var SAMPLE_MARKER = "sample: true";
|
|
@@ -13279,8 +13539,8 @@ has something safe to work on: its test and build hooks always pass, it has no
|
|
|
13279
13539
|
remote, and \`crew sample-ticket --remove\` deletes it.
|
|
13280
13540
|
`;
|
|
13281
13541
|
function sampleDir(existing, reposBase, workspace) {
|
|
13282
|
-
if (existing.length > 0) return
|
|
13283
|
-
return
|
|
13542
|
+
if (existing.length > 0) return join23(dirname15(existing[0]), SAMPLE_REPO);
|
|
13543
|
+
return join23(reposBase, workspace, SAMPLE_REPO);
|
|
13284
13544
|
}
|
|
13285
13545
|
var SAMPLE_TITLE = "Sample: add a documented `greet` function with a test";
|
|
13286
13546
|
function sampleDescription() {
|
|
@@ -13361,8 +13621,8 @@ function schedulerInstalled(plan, exists, crontab) {
|
|
|
13361
13621
|
}
|
|
13362
13622
|
|
|
13363
13623
|
// src/worktree-sweep.ts
|
|
13364
|
-
import { existsSync as existsSync23, readFileSync as
|
|
13365
|
-
import { basename as basename9, dirname as
|
|
13624
|
+
import { existsSync as existsSync23, readFileSync as readFileSync18, readdirSync as readdirSync6 } from "fs";
|
|
13625
|
+
import { basename as basename9, dirname as dirname16, join as join24, resolve as resolve4 } from "path";
|
|
13366
13626
|
function planWorktreeSweep(target, tickets, contract) {
|
|
13367
13627
|
const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
|
|
13368
13628
|
const verified = contract.statuses.verified;
|
|
@@ -13416,7 +13676,7 @@ function worktreeTicketNumbers(target) {
|
|
|
13416
13676
|
return [...nums];
|
|
13417
13677
|
}
|
|
13418
13678
|
function siblingWorktreeDirs(target) {
|
|
13419
|
-
const parent =
|
|
13679
|
+
const parent = dirname16(target.dir.replace(/[/\\]+$/, ""));
|
|
13420
13680
|
const stem = `${basename9(target.dir.replace(/[/\\]+$/, ""))}-`;
|
|
13421
13681
|
const registered = new Set(worktrees(target.dir).map((w) => resolve4(w.path)));
|
|
13422
13682
|
let names;
|
|
@@ -13429,7 +13689,7 @@ function siblingWorktreeDirs(target) {
|
|
|
13429
13689
|
for (const name of names) {
|
|
13430
13690
|
if (!name.startsWith(stem)) continue;
|
|
13431
13691
|
const m = DIR_NUMBER_SUFFIXED.exec(name);
|
|
13432
|
-
const path =
|
|
13692
|
+
const path = join24(parent, name);
|
|
13433
13693
|
if (m && !registered.has(resolve4(path))) out.push({ path, number: m[1] });
|
|
13434
13694
|
}
|
|
13435
13695
|
return out;
|
|
@@ -13532,7 +13792,7 @@ function isHandBackMerge(path, base) {
|
|
|
13532
13792
|
if (!msgPath) return false;
|
|
13533
13793
|
let first;
|
|
13534
13794
|
try {
|
|
13535
|
-
first =
|
|
13795
|
+
first = readFileSync18(resolve4(path, msgPath), "utf8").split("\n")[0] ?? "";
|
|
13536
13796
|
} catch {
|
|
13537
13797
|
return false;
|
|
13538
13798
|
}
|
|
@@ -13638,13 +13898,13 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
|
|
|
13638
13898
|
|
|
13639
13899
|
// src/stream-sweep.ts
|
|
13640
13900
|
import { existsSync as existsSync24, readdirSync as readdirSync7, rmSync as rmSync10, statSync as statSync8 } from "fs";
|
|
13641
|
-
import { join as
|
|
13901
|
+
import { join as join25 } from "path";
|
|
13642
13902
|
var DAY_MS2 = 24 * 60 * 60 * 1e3;
|
|
13643
13903
|
function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
|
|
13644
|
-
const dir =
|
|
13904
|
+
const dir = join25(stateDir, "streams");
|
|
13645
13905
|
if (!existsSync24(dir)) return [];
|
|
13646
13906
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
13647
|
-
return readdirSync7(dir).map((name) =>
|
|
13907
|
+
return readdirSync7(dir).map((name) => join25(dir, name)).filter((path) => {
|
|
13648
13908
|
try {
|
|
13649
13909
|
return statSync8(path).mtimeMs < cutoff;
|
|
13650
13910
|
} catch {
|
|
@@ -13672,7 +13932,7 @@ function applyStreamSweep(paths, dryRun, log) {
|
|
|
13672
13932
|
function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
13673
13933
|
if (!existsSync24(stateDir)) return [];
|
|
13674
13934
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
13675
|
-
return readdirSync7(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) =>
|
|
13935
|
+
return readdirSync7(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join25(stateDir, name)).filter((path) => {
|
|
13676
13936
|
try {
|
|
13677
13937
|
return statSync8(path).mtimeMs < cutoff;
|
|
13678
13938
|
} catch {
|
|
@@ -13682,26 +13942,26 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
|
13682
13942
|
}
|
|
13683
13943
|
|
|
13684
13944
|
// src/cli.ts
|
|
13685
|
-
import { realpathSync as
|
|
13945
|
+
import { realpathSync as realpathSync3, readFileSync as readFileSync25, existsSync as existsSync31, mkdirSync as mkdirSync18, writeFileSync as writeFileSync19, rmSync as rmSync12, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync2, statSync as statSync10, readdirSync as readdirSync12 } from "fs";
|
|
13686
13946
|
import { createInterface } from "readline/promises";
|
|
13687
13947
|
import { dirname as dirOf, resolve as resolvePath } from "path";
|
|
13688
13948
|
|
|
13689
13949
|
// src/ssh-keys.ts
|
|
13690
|
-
import { execFileSync as
|
|
13691
|
-
import { existsSync as existsSync25, mkdirSync as
|
|
13692
|
-
import { dirname as
|
|
13950
|
+
import { execFileSync as execFileSync5 } from "child_process";
|
|
13951
|
+
import { existsSync as existsSync25, mkdirSync as mkdirSync13, readFileSync as readFileSync19, chmodSync } from "fs";
|
|
13952
|
+
import { dirname as dirname17, join as join26 } from "path";
|
|
13693
13953
|
var SshKeyError = class extends Error {
|
|
13694
13954
|
};
|
|
13695
13955
|
function shipSshKeyPathFor(stateDir) {
|
|
13696
|
-
return
|
|
13956
|
+
return join26(stateDir, "ssh", "id_ed25519");
|
|
13697
13957
|
}
|
|
13698
13958
|
function ensureShipSshKeypair(stateDir) {
|
|
13699
13959
|
const privateKeyPath = shipSshKeyPathFor(stateDir);
|
|
13700
13960
|
const publicKeyPath = `${privateKeyPath}.pub`;
|
|
13701
13961
|
if (!existsSync25(privateKeyPath)) {
|
|
13702
|
-
|
|
13962
|
+
mkdirSync13(dirname17(privateKeyPath), { recursive: true });
|
|
13703
13963
|
try {
|
|
13704
|
-
|
|
13964
|
+
execFileSync5("ssh-keygen", [
|
|
13705
13965
|
"-t",
|
|
13706
13966
|
"ed25519",
|
|
13707
13967
|
"-N",
|
|
@@ -13722,12 +13982,12 @@ function ensureShipSshKeypair(stateDir) {
|
|
|
13722
13982
|
if (!existsSync25(publicKeyPath)) {
|
|
13723
13983
|
throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
|
|
13724
13984
|
}
|
|
13725
|
-
const publicKey =
|
|
13985
|
+
const publicKey = readFileSync19(publicKeyPath, "utf8").trim();
|
|
13726
13986
|
return { privateKeyPath, publicKeyPath, publicKey };
|
|
13727
13987
|
}
|
|
13728
13988
|
function sshKeygenAvailable() {
|
|
13729
13989
|
try {
|
|
13730
|
-
|
|
13990
|
+
execFileSync5("ssh-keygen", ["-V"], { stdio: ["ignore", "ignore", "ignore"] });
|
|
13731
13991
|
return true;
|
|
13732
13992
|
} catch (e) {
|
|
13733
13993
|
return e.code !== "ENOENT";
|
|
@@ -13735,18 +13995,18 @@ function sshKeygenAvailable() {
|
|
|
13735
13995
|
}
|
|
13736
13996
|
|
|
13737
13997
|
// src/machine-id.ts
|
|
13738
|
-
import { execFileSync as
|
|
13998
|
+
import { execFileSync as execFileSync6 } from "child_process";
|
|
13739
13999
|
import { createHash as createHash4 } from "crypto";
|
|
13740
|
-
import { existsSync as existsSync26, mkdirSync as
|
|
13741
|
-
import { dirname as
|
|
14000
|
+
import { existsSync as existsSync26, mkdirSync as mkdirSync14, readFileSync as readFileSync20, writeFileSync as writeFileSync15 } from "fs";
|
|
14001
|
+
import { dirname as dirname18, join as join27 } from "path";
|
|
13742
14002
|
function machineIdPathFor(stateDir) {
|
|
13743
|
-
return
|
|
14003
|
+
return join27(stateDir, "machine-id");
|
|
13744
14004
|
}
|
|
13745
14005
|
var sha256 = (s) => createHash4("sha256").update(s).digest("hex");
|
|
13746
14006
|
function readRawMachineId(platform2 = process.platform) {
|
|
13747
14007
|
try {
|
|
13748
14008
|
if (platform2 === "darwin") {
|
|
13749
|
-
const out =
|
|
14009
|
+
const out = execFileSync6("ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"], {
|
|
13750
14010
|
encoding: "utf8",
|
|
13751
14011
|
stdio: ["ignore", "pipe", "ignore"]
|
|
13752
14012
|
});
|
|
@@ -13754,7 +14014,7 @@ function readRawMachineId(platform2 = process.platform) {
|
|
|
13754
14014
|
}
|
|
13755
14015
|
for (const p of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
|
|
13756
14016
|
if (existsSync26(p)) {
|
|
13757
|
-
const id =
|
|
14017
|
+
const id = readFileSync20(p, "utf8").trim();
|
|
13758
14018
|
if (id) return id;
|
|
13759
14019
|
}
|
|
13760
14020
|
}
|
|
@@ -13766,14 +14026,14 @@ function ensureMachineId(stateDir, opts = {}) {
|
|
|
13766
14026
|
if (opts.override?.trim()) return sha256(opts.override.trim());
|
|
13767
14027
|
const path = machineIdPathFor(stateDir);
|
|
13768
14028
|
if (existsSync26(path)) {
|
|
13769
|
-
const cached =
|
|
14029
|
+
const cached = readFileSync20(path, "utf8").trim();
|
|
13770
14030
|
if (/^[0-9a-f]{64}$/.test(cached)) return cached;
|
|
13771
14031
|
}
|
|
13772
14032
|
const raw = (opts.read ?? readRawMachineId)();
|
|
13773
14033
|
if (!raw) return void 0;
|
|
13774
14034
|
const hashed = sha256(raw);
|
|
13775
|
-
|
|
13776
|
-
|
|
14035
|
+
mkdirSync14(dirname18(path), { recursive: true });
|
|
14036
|
+
writeFileSync15(path, `${hashed}
|
|
13777
14037
|
`, { mode: 384 });
|
|
13778
14038
|
return hashed;
|
|
13779
14039
|
}
|
|
@@ -13785,11 +14045,11 @@ function matchShipRows(ships, shipName2, machineId) {
|
|
|
13785
14045
|
}
|
|
13786
14046
|
|
|
13787
14047
|
// src/passenger-containers.ts
|
|
13788
|
-
import { execFileSync as
|
|
14048
|
+
import { execFileSync as execFileSync7 } from "child_process";
|
|
13789
14049
|
import { createHash as createHash5 } from "crypto";
|
|
13790
14050
|
var realExec = (cmd, args) => {
|
|
13791
14051
|
try {
|
|
13792
|
-
return
|
|
14052
|
+
return execFileSync7(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
13793
14053
|
} catch (e) {
|
|
13794
14054
|
const err = e;
|
|
13795
14055
|
const why = [err.stderr, err.stdout].map((x) => (x ?? "").trim()).filter(Boolean).join(" / ");
|
|
@@ -13987,12 +14247,12 @@ function syncAllPassengerCheckouts(cfg) {
|
|
|
13987
14247
|
}
|
|
13988
14248
|
|
|
13989
14249
|
// src/tunnel.ts
|
|
13990
|
-
import { spawn as
|
|
13991
|
-
import { existsSync as existsSync27, mkdirSync as
|
|
14250
|
+
import { spawn as spawn5 } from "child_process";
|
|
14251
|
+
import { existsSync as existsSync27, mkdirSync as mkdirSync15, readFileSync as readFileSync21, writeFileSync as writeFileSync16, unlinkSync as unlinkSync3, readdirSync as readdirSync8 } from "fs";
|
|
13992
14252
|
import { randomInt } from "crypto";
|
|
13993
|
-
import { join as
|
|
14253
|
+
import { join as join28 } from "path";
|
|
13994
14254
|
var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
|
|
13995
|
-
var defaultSpawn = (cmd, args) =>
|
|
14255
|
+
var defaultSpawn = (cmd, args) => spawn5(cmd, args, DEFAULT_SPAWN_OPTIONS);
|
|
13996
14256
|
var SLUG_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
13997
14257
|
var SLUG_LENGTH = 12;
|
|
13998
14258
|
function generateTunnelSlug() {
|
|
@@ -14057,21 +14317,21 @@ function startTunnel(opts) {
|
|
|
14057
14317
|
};
|
|
14058
14318
|
}
|
|
14059
14319
|
function tunnelStatePath(stateDir, workspaceId) {
|
|
14060
|
-
return
|
|
14320
|
+
return join28(stateDir, "passengers", `${workspaceId}.json`);
|
|
14061
14321
|
}
|
|
14062
14322
|
function readPersistedTunnel(stateDir, workspaceId) {
|
|
14063
14323
|
const path = tunnelStatePath(stateDir, workspaceId);
|
|
14064
14324
|
if (!existsSync27(path)) return void 0;
|
|
14065
14325
|
try {
|
|
14066
|
-
return JSON.parse(
|
|
14326
|
+
return JSON.parse(readFileSync21(path, "utf8"));
|
|
14067
14327
|
} catch {
|
|
14068
14328
|
return void 0;
|
|
14069
14329
|
}
|
|
14070
14330
|
}
|
|
14071
14331
|
function writePersistedTunnel(stateDir, state) {
|
|
14072
14332
|
const path = tunnelStatePath(stateDir, state.workspaceId);
|
|
14073
|
-
|
|
14074
|
-
|
|
14333
|
+
mkdirSync15(join28(stateDir, "passengers"), { recursive: true });
|
|
14334
|
+
writeFileSync16(path, `${JSON.stringify(state, null, 2)}
|
|
14075
14335
|
`);
|
|
14076
14336
|
}
|
|
14077
14337
|
function updatePersistedTunnel(stateDir, workspaceId, patch2) {
|
|
@@ -14168,7 +14428,7 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
|
|
|
14168
14428
|
}
|
|
14169
14429
|
}
|
|
14170
14430
|
function readAllPersistedWorkspaceIds(stateDir) {
|
|
14171
|
-
const dir =
|
|
14431
|
+
const dir = join28(stateDir, "passengers");
|
|
14172
14432
|
if (!existsSync27(dir)) return [];
|
|
14173
14433
|
return readdirSync8(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
|
|
14174
14434
|
}
|
|
@@ -14288,10 +14548,10 @@ async function unregisterEndpoint(workspaceId, info, d) {
|
|
|
14288
14548
|
|
|
14289
14549
|
// src/passenger-sync-daemon.ts
|
|
14290
14550
|
import { createServer as createHttpServer } from "http";
|
|
14291
|
-
import { spawn as
|
|
14551
|
+
import { spawn as spawn6 } from "child_process";
|
|
14292
14552
|
import { randomBytes } from "crypto";
|
|
14293
|
-
import { existsSync as existsSync28, mkdirSync as
|
|
14294
|
-
import { join as
|
|
14553
|
+
import { existsSync as existsSync28, mkdirSync as mkdirSync16, readFileSync as readFileSync22, writeFileSync as writeFileSync17, unlinkSync as unlinkSync4, readdirSync as readdirSync9 } from "fs";
|
|
14554
|
+
import { join as join29 } from "path";
|
|
14295
14555
|
|
|
14296
14556
|
// src/secret-compare.ts
|
|
14297
14557
|
import { timingSafeEqual } from "crypto";
|
|
@@ -14375,26 +14635,26 @@ function runSyncDaemonFromEnv(env = process.env) {
|
|
|
14375
14635
|
return server;
|
|
14376
14636
|
}
|
|
14377
14637
|
var defaultSpawn2 = (cmd, args, env) => {
|
|
14378
|
-
const child =
|
|
14638
|
+
const child = spawn6(cmd, args, { detached: true, stdio: "ignore", env });
|
|
14379
14639
|
child.unref();
|
|
14380
14640
|
return child;
|
|
14381
14641
|
};
|
|
14382
14642
|
function syncDaemonStatePath(stateDir, workspaceId) {
|
|
14383
|
-
return
|
|
14643
|
+
return join29(stateDir, "passengers", `${workspaceId}-sync.json`);
|
|
14384
14644
|
}
|
|
14385
14645
|
function readPersistedSyncDaemon(stateDir, workspaceId) {
|
|
14386
14646
|
const path = syncDaemonStatePath(stateDir, workspaceId);
|
|
14387
14647
|
if (!existsSync28(path)) return void 0;
|
|
14388
14648
|
try {
|
|
14389
|
-
return JSON.parse(
|
|
14649
|
+
return JSON.parse(readFileSync22(path, "utf8"));
|
|
14390
14650
|
} catch {
|
|
14391
14651
|
return void 0;
|
|
14392
14652
|
}
|
|
14393
14653
|
}
|
|
14394
14654
|
function writePersistedSyncDaemon(stateDir, state) {
|
|
14395
14655
|
const path = syncDaemonStatePath(stateDir, state.workspaceId);
|
|
14396
|
-
|
|
14397
|
-
|
|
14656
|
+
mkdirSync16(join29(stateDir, "passengers"), { recursive: true });
|
|
14657
|
+
writeFileSync17(path, `${JSON.stringify(state, null, 2)}
|
|
14398
14658
|
`);
|
|
14399
14659
|
}
|
|
14400
14660
|
function removePersistedSyncDaemon(stateDir, workspaceId) {
|
|
@@ -14414,7 +14674,7 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
|
|
|
14414
14674
|
}
|
|
14415
14675
|
}
|
|
14416
14676
|
function readAllPersistedSyncWorkspaceIds(stateDir) {
|
|
14417
|
-
const dir =
|
|
14677
|
+
const dir = join29(stateDir, "passengers");
|
|
14418
14678
|
if (!existsSync28(dir)) return [];
|
|
14419
14679
|
const suffix = "-sync.json";
|
|
14420
14680
|
return readdirSync9(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
|
|
@@ -14472,12 +14732,12 @@ function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
|
|
|
14472
14732
|
}
|
|
14473
14733
|
|
|
14474
14734
|
// src/daemon.ts
|
|
14475
|
-
import { existsSync as existsSync30, mkdirSync as
|
|
14476
|
-
import { join as
|
|
14735
|
+
import { existsSync as existsSync30, mkdirSync as mkdirSync17, readFileSync as readFileSync24, readdirSync as readdirSync11, rmSync as rmSync11, statSync as statSync9, writeFileSync as writeFileSync18 } from "fs";
|
|
14736
|
+
import { join as join31 } from "path";
|
|
14477
14737
|
|
|
14478
14738
|
// src/interrupted.ts
|
|
14479
|
-
import { existsSync as existsSync29, readdirSync as readdirSync10, readFileSync as
|
|
14480
|
-
import { join as
|
|
14739
|
+
import { existsSync as existsSync29, readdirSync as readdirSync10, readFileSync as readFileSync23 } from "fs";
|
|
14740
|
+
import { join as join30 } from "path";
|
|
14481
14741
|
var INTERRUPTED_MARKER = calloutHeader("NOTE", "Interrupted");
|
|
14482
14742
|
function interruptedComment(b) {
|
|
14483
14743
|
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.";
|
|
@@ -14498,19 +14758,19 @@ function runFinished(streamPath) {
|
|
|
14498
14758
|
const path = `${streamPath}.meta.json`;
|
|
14499
14759
|
if (!existsSync29(path)) return false;
|
|
14500
14760
|
try {
|
|
14501
|
-
return JSON.parse(
|
|
14761
|
+
return JSON.parse(readFileSync23(path, "utf8")).running !== true;
|
|
14502
14762
|
} catch {
|
|
14503
14763
|
return true;
|
|
14504
14764
|
}
|
|
14505
14765
|
}
|
|
14506
14766
|
function orphanedStreams(o) {
|
|
14507
|
-
const dir =
|
|
14767
|
+
const dir = join30(o.stateDir, "streams");
|
|
14508
14768
|
if (!existsSync29(dir)) return [];
|
|
14509
14769
|
const ownerAlive = o.isOwnerAlive ?? pidAlive;
|
|
14510
14770
|
const out = [];
|
|
14511
14771
|
for (const name of readdirSync10(dir)) {
|
|
14512
14772
|
if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
|
|
14513
|
-
const streamPath =
|
|
14773
|
+
const streamPath = join30(dir, name);
|
|
14514
14774
|
if (runFinished(streamPath)) continue;
|
|
14515
14775
|
const sidecar = readRunSidecar(streamPath);
|
|
14516
14776
|
if (!sidecar || sidecar.route !== o.routeLabel) continue;
|
|
@@ -14610,7 +14870,7 @@ function latestMtime(path) {
|
|
|
14610
14870
|
if (!stat.isDirectory()) return 0;
|
|
14611
14871
|
let latest = stat.mtimeMs;
|
|
14612
14872
|
for (const entry of readdirSync11(path, { withFileTypes: true })) {
|
|
14613
|
-
latest = Math.max(latest, latestMtime(
|
|
14873
|
+
latest = Math.max(latest, latestMtime(join31(path, entry.name)));
|
|
14614
14874
|
}
|
|
14615
14875
|
return latest;
|
|
14616
14876
|
}
|
|
@@ -14622,20 +14882,20 @@ function makeStaleChecker(paths, opts = {}) {
|
|
|
14622
14882
|
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync30(opts.reloadSentinel);
|
|
14623
14883
|
}
|
|
14624
14884
|
function reloadSentinelFor(stateDir) {
|
|
14625
|
-
return
|
|
14885
|
+
return join31(stateDir, "reload-requested");
|
|
14626
14886
|
}
|
|
14627
14887
|
function daemonStampFor(stateDir) {
|
|
14628
|
-
return
|
|
14888
|
+
return join31(stateDir, "daemon.started");
|
|
14629
14889
|
}
|
|
14630
14890
|
function noteDaemonStarted(stateDir, now = Date.now()) {
|
|
14631
|
-
|
|
14891
|
+
mkdirSync17(stateDir, { recursive: true });
|
|
14632
14892
|
rmSync11(reloadSentinelFor(stateDir), { force: true });
|
|
14633
|
-
|
|
14893
|
+
writeFileSync18(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
|
|
14634
14894
|
`);
|
|
14635
14895
|
}
|
|
14636
14896
|
function readDaemonStarted(stateDir) {
|
|
14637
14897
|
try {
|
|
14638
|
-
const v = JSON.parse(
|
|
14898
|
+
const v = JSON.parse(readFileSync24(daemonStampFor(stateDir), "utf8")).startedAt;
|
|
14639
14899
|
return typeof v === "number" ? v : null;
|
|
14640
14900
|
} catch {
|
|
14641
14901
|
return null;
|
|
@@ -14695,7 +14955,7 @@ async function runOnePassUnguarded(o) {
|
|
|
14695
14955
|
await hydrateApiKeys([o.route], o.getSessionStore);
|
|
14696
14956
|
const emit = o.newEmitter();
|
|
14697
14957
|
const decide = o.decide ?? decideCycle;
|
|
14698
|
-
const
|
|
14958
|
+
const spawn7 = o.spawnRoleAgent ?? runRoleAgent;
|
|
14699
14959
|
const breaker = o.spawnBreaker ?? new SpawnBreaker();
|
|
14700
14960
|
const ctx = { route: o.route, ship: o.ship, state: o.state, emit, getSessionStore: o.getSessionStore };
|
|
14701
14961
|
try {
|
|
@@ -14756,7 +15016,7 @@ async function runOnePassUnguarded(o) {
|
|
|
14756
15016
|
continue;
|
|
14757
15017
|
}
|
|
14758
15018
|
started.push(role);
|
|
14759
|
-
const run2 =
|
|
15019
|
+
const run2 = spawn7(role, decision, ctx).then(() => {
|
|
14760
15020
|
breaker.recordSuccess(role);
|
|
14761
15021
|
clearShipAttention(o.state, `role_parked:${role}`);
|
|
14762
15022
|
}).catch(async (e) => {
|
|
@@ -15001,7 +15261,7 @@ async function buildEnvironment(route, ship, ticket) {
|
|
|
15001
15261
|
}
|
|
15002
15262
|
|
|
15003
15263
|
// src/cli.ts
|
|
15004
|
-
var CREW_HOME = isCompiledBinary(import.meta.url) ?
|
|
15264
|
+
var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname19(process.execPath) : resolve5(dirname19(fileURLToPath(import.meta.url)), "..");
|
|
15005
15265
|
var RELEASE_LOCK_TTL_MS = 20 * 60 * 1e3;
|
|
15006
15266
|
var CYCLE_SECONDS2 = 120;
|
|
15007
15267
|
var STALE_CYCLES = 3;
|
|
@@ -15147,7 +15407,7 @@ function usage() {
|
|
|
15147
15407
|
resume with no argument clears only the whole-crew pause; a role or release pause needs its own resume
|
|
15148
15408
|
crew log [route] tail the log
|
|
15149
15409
|
crew inbox [--member NAME] your tickets across every workspace (or a colleague's)
|
|
15150
|
-
crew connect resolve a workspace's ids into a crew.yaml block, then (admin key) sync personas/skills and mint seat keys (--no-agents-sync skips that; --no-mcp skips registering the Tablation MCP server with Claude Code; --mcp replace overwrites a different one; --dry-run works with no crew.yaml and writes none: no file, no rows, no seat keys \u2014 the device approval and workspace credential behave as in a real run)
|
|
15410
|
+
crew connect resolve a workspace's ids into a crew.yaml block, then (admin key) sync personas/skills and mint seat keys (--no-agents-sync skips that; --no-mcp skips registering the Tablation MCP server with Claude Code; --mcp replace overwrites a different one; --dry-run works with no crew.yaml and writes none: no file, no rows, no seat keys \u2014 the device approval and workspace credential behave as in a real run; with no workspace named it first reuses the keychain session a previous dry-run stored for the host, falling back to device approval when that is missing or rejected, and the signed_in event's session field says which)
|
|
15151
15411
|
crew disconnect remove what connect installed for Pair (Claude MCP; Codex/Gemini instructions block and MCP entry per ship.pair)
|
|
15152
15412
|
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)
|
|
15153
15413
|
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)
|
|
@@ -15155,7 +15415,7 @@ function usage() {
|
|
|
15155
15415
|
crew skills sync [route] push only crew's skill files (e.g. grill-me) into the workspace Agent Skills table
|
|
15156
15416
|
crew repos add [ROUTE] [NAME] [PATH] attach a local checkout to a route in crew.yaml (--dry-run previews)
|
|
15157
15417
|
crew repos clone [ROUTE] NAME clone a Repos row's remote into the route directory and attach it (--dir PATH, --dry-run, --json)
|
|
15158
|
-
crew repos hooks [ROUTE] NAME suggest test/build hooks for a checkout (--write route|repo, --json)
|
|
15418
|
+
crew repos hooks [ROUTE] NAME suggest test/build hooks for a checkout (--agent, --write route|repo, --recipes, --json)
|
|
15159
15419
|
crew repos list ROUTE the checkouts a route has, with the tracker's Repos row each matches
|
|
15160
15420
|
crew logbook list [route] recent Agent Log entries, filterable by --role/--ticket
|
|
15161
15421
|
crew logbook show [route] ID [--prompt] one entry; --prompt reconstructs and verifies its prompt
|
|
@@ -15568,7 +15828,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
15568
15828
|
}
|
|
15569
15829
|
if (host === "linux" && hasSystemd) {
|
|
15570
15830
|
const label = labelFor(CREW_HOME, "run");
|
|
15571
|
-
const staleTimer =
|
|
15831
|
+
const staleTimer = join32(homedir8(), ".config", "systemd", "user", `${label}.timer`);
|
|
15572
15832
|
if (existsSync31(staleTimer)) {
|
|
15573
15833
|
process.stdout.write(
|
|
15574
15834
|
`removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
|
|
@@ -15630,7 +15890,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
15630
15890
|
for (const a of plan.actions) {
|
|
15631
15891
|
if (a.kind === "enable") {
|
|
15632
15892
|
if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
|
|
15633
|
-
|
|
15893
|
+
writeFileSync19(cfg.configFile, planEnable(readFileSync25(cfg.configFile, "utf8"), a.route));
|
|
15634
15894
|
enabledSound.add(a.route);
|
|
15635
15895
|
changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
|
|
15636
15896
|
} else {
|
|
@@ -16084,10 +16344,12 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16084
16344
|
lock?.release();
|
|
16085
16345
|
lock = void 0;
|
|
16086
16346
|
};
|
|
16347
|
+
let sigintOwnedByCommand = false;
|
|
16087
16348
|
if (!dryRun) {
|
|
16088
16349
|
process.on("exit", dropLock);
|
|
16089
16350
|
for (const sig of ["SIGINT", "SIGTERM"]) {
|
|
16090
16351
|
process.on(sig, () => {
|
|
16352
|
+
if (sig === "SIGINT" && sigintOwnedByCommand) return;
|
|
16091
16353
|
dropLock();
|
|
16092
16354
|
process.exit(0);
|
|
16093
16355
|
});
|
|
@@ -16629,7 +16891,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16629
16891
|
process.exit(1);
|
|
16630
16892
|
}
|
|
16631
16893
|
requireArmed("run the daemon loop");
|
|
16632
|
-
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [
|
|
16894
|
+
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join32(CREW_HOME, "src"), join32(CREW_HOME, "dist"), join32(CREW_HOME, "bin")];
|
|
16633
16895
|
const configPaths = daemonConfigPaths(cfg);
|
|
16634
16896
|
watchPaths.push(...configPaths);
|
|
16635
16897
|
noteDaemonStarted(cfg.ship.stateDir);
|
|
@@ -16830,10 +17092,10 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16830
17092
|
`);
|
|
16831
17093
|
if (!dryRun) {
|
|
16832
17094
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
16833
|
-
const raw = existsSync31(resolvedPath) ? JSON.parse(
|
|
17095
|
+
const raw = existsSync31(resolvedPath) ? JSON.parse(readFileSync25(resolvedPath, "utf8")) : {};
|
|
16834
17096
|
raw.agentSkills = result.agentSkills;
|
|
16835
|
-
|
|
16836
|
-
|
|
17097
|
+
mkdirSync18(dirOf(resolvedPath), { recursive: true });
|
|
17098
|
+
writeFileSync19(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
16837
17099
|
`);
|
|
16838
17100
|
}
|
|
16839
17101
|
if (result.outcomes.some((o) => o.action === "diverged")) process.exitCode = 1;
|
|
@@ -16842,19 +17104,25 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16842
17104
|
case "repos": {
|
|
16843
17105
|
const sub = positional[1];
|
|
16844
17106
|
const usage2 = () => {
|
|
16845
|
-
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");
|
|
17107
|
+
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] [--agent] [--write route|repo] [--recipes ID,ID] [--json]\n");
|
|
16846
17108
|
process.exit(2);
|
|
16847
17109
|
};
|
|
16848
17110
|
if (sub !== "add" && sub !== "list" && sub !== "clone" && sub !== "hooks") usage2();
|
|
16849
|
-
const configText =
|
|
17111
|
+
const configText = readFileSync25(cfg.configFile, "utf8");
|
|
16850
17112
|
if (sub === "hooks") {
|
|
16851
17113
|
try {
|
|
16852
17114
|
const a = parseHooksArgs(positional.slice(2), routeNamesOf(configText));
|
|
16853
|
-
const dir = repoDirOf(configText,
|
|
17115
|
+
const dir = repoDirOf(configText, dirname19(cfg.configFile), a.route, a.name);
|
|
16854
17116
|
if (!existsSync31(dir)) throw new ReposError(`${dir} does not exist on disk`);
|
|
16855
|
-
const files = { exists: (r) => existsSync31(resolvePath(dir, r)),
|
|
17117
|
+
const files = { exists: (r) => existsSync31(resolvePath(dir, r)), list: (r) => {
|
|
17118
|
+
try {
|
|
17119
|
+
return readdirSync12(resolvePath(dir, r));
|
|
17120
|
+
} catch {
|
|
17121
|
+
return [];
|
|
17122
|
+
}
|
|
17123
|
+
}, read: (r) => {
|
|
16856
17124
|
try {
|
|
16857
|
-
return
|
|
17125
|
+
return readFileSync25(resolvePath(dir, r), "utf8");
|
|
16858
17126
|
} catch {
|
|
16859
17127
|
return void 0;
|
|
16860
17128
|
}
|
|
@@ -16862,21 +17130,43 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16862
17130
|
const suggestion = suggestHooks(files);
|
|
16863
17131
|
const write = value("write");
|
|
16864
17132
|
if (write !== void 0 && write !== "route" && write !== "repo") throw new ReposError(`--write takes "route" or "repo" (got "${write}")`);
|
|
17133
|
+
const recipeIds = (value("recipes") ?? "").split(",").map((x) => x.trim()).filter((x) => x && x !== "none");
|
|
17134
|
+
if (recipeIds.length > 0 && !write) throw new ReposError("--recipes only applies with --write route|repo");
|
|
17135
|
+
if (flag("agent") && write) throw new ReposError("--agent is read-only; drop --write (write the sniffed hooks, then re-run --agent to compare)");
|
|
17136
|
+
let agent = null;
|
|
17137
|
+
let agentError = null;
|
|
17138
|
+
if (flag("agent")) {
|
|
17139
|
+
const ac = new AbortController();
|
|
17140
|
+
const onSigint = () => ac.abort();
|
|
17141
|
+
process.once("SIGINT", onSigint);
|
|
17142
|
+
sigintOwnedByCommand = true;
|
|
17143
|
+
try {
|
|
17144
|
+
const routeCfg = cfg.routes.find((r) => r.route === a.route);
|
|
17145
|
+
agent = await runAgentSuggest({ spec: resolveToolchain(cfg.ship, routeCfg, a.name), dir, sniff: suggestion, signal: ac.signal });
|
|
17146
|
+
} catch (e) {
|
|
17147
|
+
if (!(e instanceof AgentSuggestError)) throw e;
|
|
17148
|
+
agentError = { reason: e.reason, message: e.message };
|
|
17149
|
+
} finally {
|
|
17150
|
+
process.off("SIGINT", onSigint);
|
|
17151
|
+
sigintOwnedByCommand = false;
|
|
17152
|
+
}
|
|
17153
|
+
}
|
|
17154
|
+
const picked = selectRecipes(suggestion, recipeIds);
|
|
16865
17155
|
let written;
|
|
16866
|
-
if (write && suggestion.source === null) throw new ReposError(`nothing to write: ${suggestion.notes[0]}`);
|
|
17156
|
+
if (write && suggestion.source === null && recipeIds.length === 0) throw new ReposError(`nothing to write: ${suggestion.notes[0]}`);
|
|
16867
17157
|
if (write === "route") {
|
|
16868
|
-
const plan = planRouteHooks(configText, a.route, a.name,
|
|
16869
|
-
|
|
17158
|
+
const plan = planRouteHooks(configText, a.route, a.name, picked.hooks);
|
|
17159
|
+
writeFileSync19(cfg.configFile, plan.text);
|
|
16870
17160
|
written = { target: "route", file: cfg.configFile, hooks: plan.written, kept: plan.kept };
|
|
16871
17161
|
} else if (write === "repo") {
|
|
16872
17162
|
const have = existingRepoConfig(files);
|
|
16873
17163
|
if (have) throw new ReposError(`${resolvePath(dir, have)} already exists; not overwriting it`);
|
|
16874
17164
|
const file = resolvePath(dir, ".crew.yaml");
|
|
16875
|
-
|
|
16876
|
-
written = { target: "repo", file, hooks: Object.keys(
|
|
17165
|
+
writeFileSync19(file, starterRepoConfig(picked.hooks, picked.release), { flag: "wx" });
|
|
17166
|
+
written = { target: "repo", file, hooks: Object.keys(picked.hooks), kept: [] };
|
|
16877
17167
|
}
|
|
16878
17168
|
if (flag("json")) {
|
|
16879
|
-
process.stdout.write(`${JSON.stringify({ route: a.route, repo: a.name, dir, ...suggestion, written: written ?? null })}
|
|
17169
|
+
process.stdout.write(`${JSON.stringify({ route: a.route, repo: a.name, dir, ...suggestion, agent, agentError, written: written ?? null })}
|
|
16880
17170
|
`);
|
|
16881
17171
|
} else {
|
|
16882
17172
|
if (suggestion.source) process.stdout.write(`${a.name}: proposed from ${suggestion.source}
|
|
@@ -16884,6 +17174,18 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16884
17174
|
for (const [k, v] of Object.entries(suggestion.hooks)) process.stdout.write(` ${k}: ${v}
|
|
16885
17175
|
`);
|
|
16886
17176
|
for (const n of suggestion.notes) process.stdout.write(`note: ${n}
|
|
17177
|
+
`);
|
|
17178
|
+
for (const r of suggestion.recipes) process.stdout.write(`recipe ${r.id}${r.defaultOn ? " (default on)" : ""}${r.writable ? "" : " (advisory)"}: ${r.title} [${r.evidence.join(", ")}]
|
|
17179
|
+
`);
|
|
17180
|
+
if (agent) {
|
|
17181
|
+
process.stdout.write(`agent (${agent.toolchain}, ${agent.model}) read ${agent.filesRead.join(", ") || "no files"}
|
|
17182
|
+
`);
|
|
17183
|
+
for (const [k, v] of Object.entries(agent.hooks)) process.stdout.write(` ${k}: ${v}${agent.differsFromSniff.includes(k) ? " (differs from file sniff)" : ""}
|
|
17184
|
+
`);
|
|
17185
|
+
for (const n of agent.notes) process.stdout.write(`note: ${n}
|
|
17186
|
+
`);
|
|
17187
|
+
}
|
|
17188
|
+
if (agentError) process.stderr.write(`crew repos: --agent failed (${agentError.reason}): ${agentError.message}
|
|
16887
17189
|
`);
|
|
16888
17190
|
if (written) {
|
|
16889
17191
|
process.stdout.write(`wrote ${written.hooks.join(", ") || "nothing new"} to ${written.file}
|
|
@@ -16891,8 +17193,11 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16891
17193
|
if (written.kept.length) process.stdout.write(`kept existing route hooks: ${written.kept.join(", ")}
|
|
16892
17194
|
`);
|
|
16893
17195
|
if (written.target === "repo") process.stdout.write("review it and commit it to the repository\n");
|
|
17196
|
+
if (written.target === "route" && picked.release) process.stdout.write(`note: release.mode ${picked.release.mode} is repo-level; use --write repo to set it
|
|
17197
|
+
`);
|
|
16894
17198
|
}
|
|
16895
17199
|
}
|
|
17200
|
+
if (agentError) process.exitCode = 1;
|
|
16896
17201
|
} catch (e) {
|
|
16897
17202
|
if (e instanceof ReposError) {
|
|
16898
17203
|
process.stderr.write(`crew repos: ${e.message}
|
|
@@ -16940,13 +17245,13 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16940
17245
|
let resolvedRemotes;
|
|
16941
17246
|
if (existsSync31(resolvedFile)) {
|
|
16942
17247
|
try {
|
|
16943
|
-
resolvedRemotes = JSON.parse(
|
|
17248
|
+
resolvedRemotes = JSON.parse(readFileSync25(resolvedFile, "utf8"));
|
|
16944
17249
|
} catch {
|
|
16945
17250
|
}
|
|
16946
17251
|
}
|
|
16947
17252
|
const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
|
|
16948
17253
|
const text = configText;
|
|
16949
|
-
const base =
|
|
17254
|
+
const base = dirname19(cfg.configFile);
|
|
16950
17255
|
try {
|
|
16951
17256
|
if (sub === "list") {
|
|
16952
17257
|
const repos = configuredRepos(text, routeName, base);
|
|
@@ -16987,7 +17292,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16987
17292
|
throw new ReposError(`${e.message}${authHint}
|
|
16988
17293
|
crew.yaml was not changed`);
|
|
16989
17294
|
}
|
|
16990
|
-
|
|
17295
|
+
writeFileSync19(cfg.configFile, plan2.text);
|
|
16991
17296
|
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}`);
|
|
16992
17297
|
break;
|
|
16993
17298
|
}
|
|
@@ -17042,14 +17347,14 @@ ${changed2.map((c) => ` - ${c}
|
|
|
17042
17347
|
process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
|
|
17043
17348
|
`);
|
|
17044
17349
|
if (existsSync31(resolvedFile)) {
|
|
17045
|
-
const raw = JSON.parse(
|
|
17350
|
+
const raw = JSON.parse(readFileSync25(resolvedFile, "utf8"));
|
|
17046
17351
|
raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
|
|
17047
17352
|
raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
|
|
17048
|
-
|
|
17353
|
+
writeFileSync19(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
17049
17354
|
`);
|
|
17050
17355
|
}
|
|
17051
17356
|
}
|
|
17052
|
-
|
|
17357
|
+
writeFileSync19(cfg.configFile, plan.text);
|
|
17053
17358
|
process.stdout.write(`added ${top} to ${routeName} as ${plan.mode === "dir" ? "dir" : `repos.${plan.name}`} in ${cfg.configFile}
|
|
17054
17359
|
`);
|
|
17055
17360
|
} catch (e) {
|
|
@@ -17179,6 +17484,30 @@ ${reconstruction.prompt}
|
|
|
17179
17484
|
} catch {
|
|
17180
17485
|
}
|
|
17181
17486
|
}
|
|
17487
|
+
let signInSource = apiKey ? "key" : "device";
|
|
17488
|
+
if (!apiKey && !workspace && dryRun && store) {
|
|
17489
|
+
const resumed = await resumeKeychainSession({
|
|
17490
|
+
store,
|
|
17491
|
+
stateDir: cfg.ship.stateDir,
|
|
17492
|
+
host: hostFromUrl3(baseUrl),
|
|
17493
|
+
verify: async (k) => {
|
|
17494
|
+
try {
|
|
17495
|
+
await get({ baseUrl, apiKey: k, userAgent: cfg.ship.userAgent }, "/auth/me");
|
|
17496
|
+
return true;
|
|
17497
|
+
} catch {
|
|
17498
|
+
return false;
|
|
17499
|
+
}
|
|
17500
|
+
}
|
|
17501
|
+
});
|
|
17502
|
+
if (resumed) {
|
|
17503
|
+
apiKey = resumed.apiKey;
|
|
17504
|
+
workspace = resumed.workspace;
|
|
17505
|
+
keychainBacked = true;
|
|
17506
|
+
signInSource = "keychain";
|
|
17507
|
+
process.stderr.write(`Reusing the existing keychain session for ${hostFromUrl3(baseUrl)}/${workspace}.
|
|
17508
|
+
`);
|
|
17509
|
+
}
|
|
17510
|
+
}
|
|
17182
17511
|
let mintedFromDevice = false;
|
|
17183
17512
|
let signedInEmitted = false;
|
|
17184
17513
|
if (!apiKey) {
|
|
@@ -17231,7 +17560,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
17231
17560
|
signedInEmitted = true;
|
|
17232
17561
|
connectEvents.emit("signed_in", `signed in as ${result.identity.email}`, {
|
|
17233
17562
|
identity: { id: result.identity.id, email: result.identity.email },
|
|
17234
|
-
workspaces: [{ slug: result.workspace.slug, name: result.workspace.name }]
|
|
17563
|
+
workspaces: [{ slug: result.workspace.slug, name: result.workspace.name }],
|
|
17564
|
+
session: "device"
|
|
17235
17565
|
});
|
|
17236
17566
|
const deviceWorkspace = "path" in result.workspace && typeof result.workspace.path === "string" ? result.workspace.path : result.workspace.slug;
|
|
17237
17567
|
if (!workspace) workspace = deviceWorkspace;
|
|
@@ -17245,6 +17575,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
17245
17575
|
createdAt: result.apiKey.createdAt
|
|
17246
17576
|
});
|
|
17247
17577
|
keychainBacked = true;
|
|
17578
|
+
rememberSession(cfg.ship.stateDir, hostFromUrl3(baseUrl), deviceWorkspace);
|
|
17248
17579
|
process.stderr.write(
|
|
17249
17580
|
`API key stored in the OS keychain for ${hostFromUrl3(baseUrl)}/${deviceWorkspace}.
|
|
17250
17581
|
`
|
|
@@ -17290,7 +17621,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
17290
17621
|
signedInEmitted = true;
|
|
17291
17622
|
connectEvents.emit("signed_in", `key belongs to ${found.meEmail ?? found.meId}`, {
|
|
17292
17623
|
identity: { id: found.meId, email: found.meEmail, name: found.meName },
|
|
17293
|
-
workspaces: [{ slug: found.workspaceSlug, name: found.workspaceName, role: found.role }]
|
|
17624
|
+
workspaces: [{ slug: found.workspaceSlug, name: found.workspaceName, role: found.role }],
|
|
17625
|
+
session: signInSource
|
|
17294
17626
|
});
|
|
17295
17627
|
}
|
|
17296
17628
|
connectEvents.emit("workspace_resolved", `workspace ${found.workspaceSlug}`, {
|
|
@@ -17380,7 +17712,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
17380
17712
|
const hadPreviousFile = existsSync31(resolvedPath);
|
|
17381
17713
|
if (hadPreviousFile) {
|
|
17382
17714
|
try {
|
|
17383
|
-
previous = JSON.parse(
|
|
17715
|
+
previous = JSON.parse(readFileSync25(resolvedPath, "utf8"));
|
|
17384
17716
|
} catch {
|
|
17385
17717
|
}
|
|
17386
17718
|
}
|
|
@@ -17489,10 +17821,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
17489
17821
|
process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
|
|
17490
17822
|
`);
|
|
17491
17823
|
} else {
|
|
17492
|
-
|
|
17824
|
+
mkdirSync18(dirname19(resolvedPath), { recursive: true });
|
|
17493
17825
|
if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
|
|
17494
17826
|
const tmpPath = `${resolvedPath}.tmp${process.pid}`;
|
|
17495
|
-
|
|
17827
|
+
writeFileSync19(tmpPath, `${JSON.stringify(resolved, null, 2)}
|
|
17496
17828
|
`);
|
|
17497
17829
|
renameSync2(tmpPath, resolvedPath);
|
|
17498
17830
|
process.stderr.write(`Resolved ids written to ${resolvedPath}
|
|
@@ -17527,8 +17859,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
17527
17859
|
process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
|
|
17528
17860
|
`);
|
|
17529
17861
|
} else {
|
|
17530
|
-
|
|
17531
|
-
|
|
17862
|
+
mkdirSync18(dirname19(apiKeyFile), { recursive: true });
|
|
17863
|
+
writeFileSync19(apiKeyFile, `${apiKeyVar}=${apiKey}
|
|
17532
17864
|
`, { mode: 384 });
|
|
17533
17865
|
process.stderr.write(`API key written to ${apiKeyFile}
|
|
17534
17866
|
`);
|
|
@@ -17537,7 +17869,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
17537
17869
|
const keyPersisted = keychainBacked || apiKeyFile !== void 0;
|
|
17538
17870
|
const wizard = firstRun;
|
|
17539
17871
|
const writeFirstRun = wizard !== void 0 && !dryRun && keyPersisted;
|
|
17540
|
-
const routeDir = value("dir") ?? (writeFirstRun ?
|
|
17872
|
+
const routeDir = value("dir") ?? (writeFirstRun ? join32(DEFAULT_REPOS_BASE_PATH, found.workspaceSlug) : void 0);
|
|
17541
17873
|
const routeBlock = renderConnection(
|
|
17542
17874
|
found,
|
|
17543
17875
|
route2,
|
|
@@ -18175,13 +18507,13 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
18175
18507
|
},
|
|
18176
18508
|
inspectCheckout(dir) {
|
|
18177
18509
|
const dirty = status(dir).map((c) => c.path);
|
|
18178
|
-
const linkedWorktrees = worktrees(dir).map((w) => w.path).filter((p) =>
|
|
18510
|
+
const linkedWorktrees = worktrees(dir).map((w) => w.path).filter((p) => realpathSync3(p) !== realpathSync3(dir));
|
|
18179
18511
|
return { dirty, linkedWorktrees };
|
|
18180
18512
|
}
|
|
18181
18513
|
};
|
|
18182
18514
|
const droppedRoutes = [];
|
|
18183
18515
|
try {
|
|
18184
|
-
const rawCfg = parseYaml2(
|
|
18516
|
+
const rawCfg = parseYaml2(readFileSync25(cfg.configFile, "utf8"));
|
|
18185
18517
|
for (const r of rawCfg?.routes ?? []) {
|
|
18186
18518
|
if (typeof r?.route !== "string" || cfg.routes.some((x) => x.route === r.route)) continue;
|
|
18187
18519
|
try {
|
|
@@ -18223,8 +18555,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
18223
18555
|
process.stdout.write("crew reload: no daemon unit is running \u2014 nothing to reload (the next run reads crew.yaml fresh)\n");
|
|
18224
18556
|
break;
|
|
18225
18557
|
}
|
|
18226
|
-
|
|
18227
|
-
|
|
18558
|
+
mkdirSync18(cfg.ship.stateDir, { recursive: true });
|
|
18559
|
+
writeFileSync19(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
|
|
18228
18560
|
`);
|
|
18229
18561
|
process.stdout.write("crew reload: requested \u2014 the daemon finishes its in-flight pass, then relaunches with the new config\n");
|
|
18230
18562
|
if (flag("now") && control?.stopCommand && control.startCommand) {
|
|
@@ -18275,7 +18607,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
18275
18607
|
}
|
|
18276
18608
|
case "log": {
|
|
18277
18609
|
try {
|
|
18278
|
-
const text =
|
|
18610
|
+
const text = readFileSync25(cfg.ship.logFile, "utf8").split("\n");
|
|
18279
18611
|
process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
|
|
18280
18612
|
`);
|
|
18281
18613
|
} catch {
|
|
@@ -18298,8 +18630,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
18298
18630
|
};
|
|
18299
18631
|
if (!route.resolved) fail(`route ${route.route} is not connected \u2014 run \`crew connect\` first`);
|
|
18300
18632
|
const tracker = new Tracker(route, cfg.ship);
|
|
18301
|
-
const configText =
|
|
18302
|
-
const cfgBase =
|
|
18633
|
+
const configText = readFileSync25(cfg.configFile, "utf8");
|
|
18634
|
+
const cfgBase = dirname19(cfg.configFile);
|
|
18303
18635
|
const configured = configuredRepos(configText, route.route, cfgBase);
|
|
18304
18636
|
const dir = configured.find((r) => r.name === SAMPLE_REPO)?.dir ?? sampleDir(configured.map((r) => r.dir), route.reposBasePath, route.route.split("/")[0]);
|
|
18305
18637
|
const resolvedFile = resolvedPathFor(cfg.ship.stateDir, route.route);
|
|
@@ -18327,7 +18659,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
18327
18659
|
}
|
|
18328
18660
|
if (configured.some((r) => r.dir === dir)) {
|
|
18329
18661
|
try {
|
|
18330
|
-
|
|
18662
|
+
writeFileSync19(cfg.configFile, planRepoRemove(configText, cfgBase, route.route, dir).text);
|
|
18331
18663
|
} catch (e) {
|
|
18332
18664
|
if (e instanceof ReposError) fail(e.message);
|
|
18333
18665
|
throw e;
|
|
@@ -18336,28 +18668,28 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
18336
18668
|
if (row) {
|
|
18337
18669
|
await tracker.deleteRepoRow(row.id);
|
|
18338
18670
|
if (existsSync31(resolvedFile)) {
|
|
18339
|
-
const raw = JSON.parse(
|
|
18671
|
+
const raw = JSON.parse(readFileSync25(resolvedFile, "utf8"));
|
|
18340
18672
|
if (raw.repoNames) delete raw.repoNames[row.id];
|
|
18341
18673
|
if (raw.repoRemotes) delete raw.repoRemotes[row.id];
|
|
18342
|
-
|
|
18674
|
+
writeFileSync19(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
18343
18675
|
`);
|
|
18344
18676
|
}
|
|
18345
18677
|
}
|
|
18346
|
-
if (existsSync31(
|
|
18678
|
+
if (existsSync31(join32(dir, ".git")) && basename10(dir) === SAMPLE_REPO) rmSync12(dir, { recursive: true, force: true });
|
|
18347
18679
|
say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
|
|
18348
18680
|
break;
|
|
18349
18681
|
}
|
|
18350
18682
|
const projectId = route.resolved.areaId ?? resolveRepoProject(await tracker.projectRows(), value("project"));
|
|
18351
|
-
const fresh = !existsSync31(
|
|
18683
|
+
const fresh = !existsSync31(join32(dir, ".git"));
|
|
18352
18684
|
if (existsSync31(dir) && fresh && readdirSync12(dir).length > 0) fail(`${dir} exists and is not a git checkout \u2014 move it aside`);
|
|
18353
18685
|
if (dryRun) {
|
|
18354
18686
|
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`);
|
|
18355
18687
|
break;
|
|
18356
18688
|
}
|
|
18357
18689
|
if (fresh) {
|
|
18358
|
-
|
|
18359
|
-
|
|
18360
|
-
|
|
18690
|
+
mkdirSync18(dir, { recursive: true });
|
|
18691
|
+
writeFileSync19(join32(dir, "README.md"), SAMPLE_README);
|
|
18692
|
+
writeFileSync19(join32(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
|
|
18361
18693
|
const g = (...a) => {
|
|
18362
18694
|
const r = spawnSync6("git", a, { cwd: dir, encoding: "utf8" });
|
|
18363
18695
|
if (r.status !== 0) fail(`git ${a[0]} failed in ${dir}: ${(r.stderr || r.stdout).trim()}`);
|
|
@@ -18376,16 +18708,16 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
18376
18708
|
});
|
|
18377
18709
|
repoId = (await tracker.createRepoRow(payload)).id;
|
|
18378
18710
|
if (existsSync31(resolvedFile)) {
|
|
18379
|
-
const raw = JSON.parse(
|
|
18711
|
+
const raw = JSON.parse(readFileSync25(resolvedFile, "utf8"));
|
|
18380
18712
|
raw.repoNames = { ...raw.repoNames, [repoId]: SAMPLE_REPO };
|
|
18381
18713
|
raw.repoRemotes = { ...raw.repoRemotes, [repoId]: SAMPLE_REMOTE };
|
|
18382
|
-
|
|
18714
|
+
writeFileSync19(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
18383
18715
|
`);
|
|
18384
18716
|
}
|
|
18385
18717
|
}
|
|
18386
18718
|
if (!configured.some((r) => r.dir === dir)) {
|
|
18387
18719
|
try {
|
|
18388
|
-
|
|
18720
|
+
writeFileSync19(cfg.configFile, planRepoAdd({ text: configText, base: cfgBase, route: route.route, path: dir, name: SAMPLE_REPO }).text);
|
|
18389
18721
|
} catch (e) {
|
|
18390
18722
|
if (e instanceof ReposError) fail(e.message);
|
|
18391
18723
|
throw e;
|
|
@@ -18718,7 +19050,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
18718
19050
|
const unitPlan = planInstall(cfg.ship, CREW_HOME, host, detectSystemd(), "run");
|
|
18719
19051
|
const abandons = unitAbandonsAgents(unitPlan, (p) => {
|
|
18720
19052
|
try {
|
|
18721
|
-
return
|
|
19053
|
+
return readFileSync25(p, "utf8");
|
|
18722
19054
|
} catch {
|
|
18723
19055
|
return null;
|
|
18724
19056
|
}
|