@tablation/crew 0.58.0 → 0.60.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 +6 -1
- package/dist/cli.js +615 -239
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -31,6 +31,84 @@ function doctorBaseReport(r, base, health) {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
// src/doctor-checks.ts
|
|
35
|
+
import { execFileSync } from "child_process";
|
|
36
|
+
var PROBE_BRANCH = "crew-probe-branch";
|
|
37
|
+
var realGit = (cwd, args, env) => {
|
|
38
|
+
execFileSync("git", args, { cwd, env, stdio: ["ignore", "pipe", "pipe"], timeout: 3e4 });
|
|
39
|
+
};
|
|
40
|
+
function isSshRemote(url) {
|
|
41
|
+
return /^ssh:\/\//.test(url) || /^[\w.-]+@[\w.-]+:/.test(url);
|
|
42
|
+
}
|
|
43
|
+
function pushAuthHint(remoteUrl) {
|
|
44
|
+
return isSshRemote(remoteUrl) ? "make sure an SSH key is loaded for this host (ssh-add), and that the scheduler user can reach it" : "run `gh auth login`, then `gh auth setup-git`";
|
|
45
|
+
}
|
|
46
|
+
function probePushAuth(o, run2 = realGit, baseEnv = process.env) {
|
|
47
|
+
if (o.offline) return { state: "skipped", detail: "skipped (--offline)" };
|
|
48
|
+
if (!o.remoteUrl) return { state: "skipped", detail: `no remote "${o.remote}" configured` };
|
|
49
|
+
const env = { ...baseEnv, GIT_TERMINAL_PROMPT: "0", GCM_INTERACTIVE: "never", GIT_SSH_COMMAND: `${baseEnv.GIT_SSH_COMMAND ?? "ssh"} -o BatchMode=yes` };
|
|
50
|
+
try {
|
|
51
|
+
run2(o.dir, ["push", "--dry-run", o.remote, `HEAD:refs/heads/${PROBE_BRANCH}`], env);
|
|
52
|
+
return { state: "ok" };
|
|
53
|
+
} catch (e) {
|
|
54
|
+
const err = e;
|
|
55
|
+
const raw = (err.stderr ? String(err.stderr) : err.message).trim().split("\n").filter(Boolean).slice(-2).join(" / ");
|
|
56
|
+
return { state: "failed", detail: raw || "push dry-run failed", hint: pushAuthHint(o.remoteUrl) };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
var INSTALL_HINTS = {
|
|
60
|
+
gh: "install GitHub CLI: brew install gh (https://cli.github.com)",
|
|
61
|
+
node: "install Node: brew install node, or https://nodejs.org",
|
|
62
|
+
docker: "install Docker Desktop: https://docs.docker.com/get-docker/"
|
|
63
|
+
};
|
|
64
|
+
function mentionsNode(script) {
|
|
65
|
+
return !!script && /\b(node|npm|pnpm|npx|yarn)\b/.test(script);
|
|
66
|
+
}
|
|
67
|
+
function isPrBased(hooks) {
|
|
68
|
+
return !!hooks.pr || /\bgh\b/.test(hooks.merged ?? "");
|
|
69
|
+
}
|
|
70
|
+
function toolChecks(o, find) {
|
|
71
|
+
const mk = (name, need) => {
|
|
72
|
+
const path = find(name);
|
|
73
|
+
return { name, need, found: !!path, ...path ? { path } : {}, hint: INSTALL_HINTS[name] };
|
|
74
|
+
};
|
|
75
|
+
const node = mk("node", "optional");
|
|
76
|
+
if (o.hooksMentionNode) node.hint = `your repositories' hooks may need it \u2014 ${node.hint}`;
|
|
77
|
+
return [mk("gh", o.prBased ? "required" : "optional"), node, mk("docker", o.hostPassengers ? "required" : "optional")];
|
|
78
|
+
}
|
|
79
|
+
function formatToolCheck(t) {
|
|
80
|
+
const label = `${t.name}:`.padEnd(19);
|
|
81
|
+
if (t.found) return `${label}OK (${t.path}) [${t.need}]
|
|
82
|
+
`;
|
|
83
|
+
return `${label}${t.need === "required" ? "NOT FOUND" : "not found"} [${t.need}] \u2014 ${t.hint}
|
|
84
|
+
`;
|
|
85
|
+
}
|
|
86
|
+
function formatPushAuth(repo, r) {
|
|
87
|
+
const label = `push auth ${repo}:`.padEnd(19);
|
|
88
|
+
if (r.state === "ok") return `${label}ok
|
|
89
|
+
`;
|
|
90
|
+
if (r.state === "skipped") return `${label}${r.detail}
|
|
91
|
+
`;
|
|
92
|
+
return `${label}FAILED \u2014 ${r.detail}; ${r.hint}
|
|
93
|
+
`;
|
|
94
|
+
}
|
|
95
|
+
async function settleRepos(items, nameOf, resolve6) {
|
|
96
|
+
const out = await Promise.all(items.map(async (t) => {
|
|
97
|
+
try {
|
|
98
|
+
return { ok: true, value: await resolve6(t) };
|
|
99
|
+
} catch (e) {
|
|
100
|
+
return { ok: false, repo: nameOf(t), detail: e.message };
|
|
101
|
+
}
|
|
102
|
+
}));
|
|
103
|
+
const settled = [];
|
|
104
|
+
const errors = [];
|
|
105
|
+
for (const o of out) {
|
|
106
|
+
if (o.ok) settled.push(o.value);
|
|
107
|
+
else errors.push({ repo: o.repo, detail: o.detail });
|
|
108
|
+
}
|
|
109
|
+
return { settled, errors };
|
|
110
|
+
}
|
|
111
|
+
|
|
34
112
|
// src/selftest.ts
|
|
35
113
|
import { spawn as spawn2 } from "child_process";
|
|
36
114
|
import { existsSync as existsSync12, mkdirSync as mkdirSync5, mkdtempSync, readFileSync as readFileSync11, rmSync as rmSync5, writeFileSync as writeFileSync6 } from "fs";
|
|
@@ -5104,7 +5182,7 @@ function doctorSelftestLines(stored, toolchains) {
|
|
|
5104
5182
|
}
|
|
5105
5183
|
|
|
5106
5184
|
// src/cli.ts
|
|
5107
|
-
import { resolve as resolve5, dirname as
|
|
5185
|
+
import { resolve as resolve5, dirname as dirname16, join as join29, basename as basename10 } from "path";
|
|
5108
5186
|
import { fileURLToPath } from "url";
|
|
5109
5187
|
import { hostname as hostname2, homedir as homedir8 } from "os";
|
|
5110
5188
|
import { spawnSync as spawnSync6 } from "child_process";
|
|
@@ -5133,7 +5211,8 @@ var VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
|
5133
5211
|
"tail",
|
|
5134
5212
|
"ticket",
|
|
5135
5213
|
"workspace",
|
|
5136
|
-
"workspace-id"
|
|
5214
|
+
"workspace-id",
|
|
5215
|
+
"write"
|
|
5137
5216
|
]);
|
|
5138
5217
|
function positionalArgs(argv2) {
|
|
5139
5218
|
const out = [];
|
|
@@ -6078,7 +6157,7 @@ function recoveredFailureHeadlines(o, route) {
|
|
|
6078
6157
|
import { existsSync as existsSync15, statfsSync } from "fs";
|
|
6079
6158
|
|
|
6080
6159
|
// src/git.ts
|
|
6081
|
-
import { execFileSync } from "child_process";
|
|
6160
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
6082
6161
|
import {
|
|
6083
6162
|
existsSync as existsSync14,
|
|
6084
6163
|
mkdirSync as mkdirSync7,
|
|
@@ -6100,7 +6179,7 @@ function streamText(x) {
|
|
|
6100
6179
|
}
|
|
6101
6180
|
function gitRaw(cwd, args) {
|
|
6102
6181
|
try {
|
|
6103
|
-
return
|
|
6182
|
+
return execFileSync2("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
6104
6183
|
} catch (e) {
|
|
6105
6184
|
const err = e;
|
|
6106
6185
|
const why = [err.stderr, err.stdout].map(streamText).filter(Boolean).join(" / ");
|
|
@@ -6109,7 +6188,7 @@ function gitRaw(cwd, args) {
|
|
|
6109
6188
|
}
|
|
6110
6189
|
function git(cwd, args) {
|
|
6111
6190
|
try {
|
|
6112
|
-
return
|
|
6191
|
+
return execFileSync2("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
6113
6192
|
} catch (e) {
|
|
6114
6193
|
const err = e;
|
|
6115
6194
|
const why = [err.stderr, err.stdout].map(streamText).filter(Boolean).join(" / ");
|
|
@@ -6266,9 +6345,9 @@ function keyCommitsInRange(cwd, key, range) {
|
|
|
6266
6345
|
}
|
|
6267
6346
|
function patchIdOf(cwd, from, to) {
|
|
6268
6347
|
try {
|
|
6269
|
-
const diff =
|
|
6348
|
+
const diff = execFileSync2("git", ["diff", from, to], { cwd, encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
|
|
6270
6349
|
if (!diff) return null;
|
|
6271
|
-
const out =
|
|
6350
|
+
const out = execFileSync2("git", ["patch-id", "--stable"], { cwd, encoding: "utf8", input: diff });
|
|
6272
6351
|
return out.split(" ")[0]?.trim() || null;
|
|
6273
6352
|
} catch {
|
|
6274
6353
|
return null;
|
|
@@ -6532,7 +6611,7 @@ function ensureRepoCheckout(dir, remote, opts = {}) {
|
|
|
6532
6611
|
mkdirSync7(dirname6(dir), { recursive: true });
|
|
6533
6612
|
const url = cloneUrlFor(remote, opts);
|
|
6534
6613
|
try {
|
|
6535
|
-
|
|
6614
|
+
execFileSync2("git", ["clone", url, dir], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
6536
6615
|
} catch (e) {
|
|
6537
6616
|
const err = e;
|
|
6538
6617
|
const why = [err.stderr, err.stdout].map(streamText).filter(Boolean).join(" / ");
|
|
@@ -9243,12 +9322,12 @@ import { existsSync as existsSync17 } from "fs";
|
|
|
9243
9322
|
import { dirname as dirname7, join as join17 } from "path";
|
|
9244
9323
|
|
|
9245
9324
|
// src/ports.ts
|
|
9246
|
-
import { execFileSync as
|
|
9325
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
9247
9326
|
import { existsSync as existsSync16 } from "fs";
|
|
9248
9327
|
import { join as join16 } from "path";
|
|
9249
9328
|
var run = (cmd, args) => {
|
|
9250
9329
|
try {
|
|
9251
|
-
return
|
|
9330
|
+
return execFileSync3(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
9252
9331
|
} catch {
|
|
9253
9332
|
return "";
|
|
9254
9333
|
}
|
|
@@ -12439,6 +12518,60 @@ async function canMintAgentKeys(route, o = {}) {
|
|
|
12439
12518
|
}
|
|
12440
12519
|
}
|
|
12441
12520
|
|
|
12521
|
+
// src/agents-sync-run.ts
|
|
12522
|
+
import { existsSync as existsSync21, mkdirSync as mkdirSync10, readFileSync as readFileSync15, writeFileSync as writeFileSync12 } from "fs";
|
|
12523
|
+
import { dirname as dirname11 } from "path";
|
|
12524
|
+
async function runAgentsSync(target, o) {
|
|
12525
|
+
const dryRun = o.dryRun ?? false;
|
|
12526
|
+
const force = o.force ?? false;
|
|
12527
|
+
const keyRole = target.resolved?.role ?? await fetchKeyRole(target, o.userAgent);
|
|
12528
|
+
let readOnly = !isAdminRole(keyRole);
|
|
12529
|
+
const runPersonas = (ro) => syncPersonas(target, { userAgent: o.userAgent, dryRun, force, readOnly: ro });
|
|
12530
|
+
let personaResult;
|
|
12531
|
+
try {
|
|
12532
|
+
personaResult = await runPersonas(readOnly);
|
|
12533
|
+
} catch (e) {
|
|
12534
|
+
if (readOnly || !isForbidden(e)) throw e;
|
|
12535
|
+
readOnly = true;
|
|
12536
|
+
personaResult = await runPersonas(true);
|
|
12537
|
+
}
|
|
12538
|
+
o.say(`route: ${target.route}`);
|
|
12539
|
+
for (const p of personaResult.outcomes) o.say(describeSyncOutcome(p));
|
|
12540
|
+
for (const l of personaResult.crewLinks) o.say(describeCrewLink(l));
|
|
12541
|
+
if (personaResult.releaseNote) o.say(personaResult.releaseNote);
|
|
12542
|
+
const briefs = Object.fromEntries(ROLE_NAMES.map((r) => [r, personaDefaultPrompt(target.promptsDir, r)]));
|
|
12543
|
+
for (const w of skillMarkerWarnings(target.promptsDir, briefs)) o.warn(`warning: ${w}`);
|
|
12544
|
+
let skillResult;
|
|
12545
|
+
if (!readOnly) {
|
|
12546
|
+
try {
|
|
12547
|
+
skillResult = await syncSkills(target, { userAgent: o.userAgent, dryRun, force });
|
|
12548
|
+
} catch (e) {
|
|
12549
|
+
if (isForbidden(e)) readOnly = true;
|
|
12550
|
+
else throw e;
|
|
12551
|
+
}
|
|
12552
|
+
}
|
|
12553
|
+
for (const s of skillResult?.outcomes ?? []) o.say(describeSkillSyncOutcome(s));
|
|
12554
|
+
if (!dryRun) {
|
|
12555
|
+
const resolvedPath = resolvedPathFor(o.stateDir, target.route);
|
|
12556
|
+
const raw = existsSync21(resolvedPath) ? JSON.parse(readFileSync15(resolvedPath, "utf8")) : {};
|
|
12557
|
+
raw.agentPersonas = personaResult.agentPersonas;
|
|
12558
|
+
if (skillResult) raw.agentSkills = skillResult.agentSkills;
|
|
12559
|
+
if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
|
|
12560
|
+
mkdirSync10(dirname11(resolvedPath), { recursive: true });
|
|
12561
|
+
writeFileSync12(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
12562
|
+
`);
|
|
12563
|
+
if (target.resolved) {
|
|
12564
|
+
target.resolved.agentPersonas = personaResult.agentPersonas;
|
|
12565
|
+
if (personaResult.releaseAgentId) target.resolved.releaseAgentId = personaResult.releaseAgentId;
|
|
12566
|
+
}
|
|
12567
|
+
}
|
|
12568
|
+
if (readOnly) {
|
|
12569
|
+
o.say("personas and skills: not synced (this key is not a workspace admin; the workspace's rows are used as they are)");
|
|
12570
|
+
}
|
|
12571
|
+
const diverged = personaResult.outcomes.some((p) => p.action === "diverged") || (skillResult?.outcomes.some((s) => s.action === "diverged") ?? false);
|
|
12572
|
+
return { personaResult, skillResult, readOnly, diverged };
|
|
12573
|
+
}
|
|
12574
|
+
|
|
12442
12575
|
// src/logbook.ts
|
|
12443
12576
|
import { createHash as createHash3 } from "crypto";
|
|
12444
12577
|
import { TablationClient as TablationClient4 } from "@tablation/client";
|
|
@@ -12527,77 +12660,12 @@ async function showLogEntry(route, entryId, opts = {}) {
|
|
|
12527
12660
|
};
|
|
12528
12661
|
}
|
|
12529
12662
|
|
|
12530
|
-
// src/
|
|
12531
|
-
import {
|
|
12532
|
-
var SAMPLE_REPO = "crew-sample";
|
|
12533
|
-
var SAMPLE_REMOTE = `local/${SAMPLE_REPO}`;
|
|
12534
|
-
var SAMPLE_MARKER = "sample: true";
|
|
12535
|
-
var SAMPLE_CREW_YAML = `version: 1
|
|
12536
|
-
platform: unix
|
|
12537
|
-
branch:
|
|
12538
|
-
base: main
|
|
12539
|
-
hooks:
|
|
12540
|
-
test: "true"
|
|
12541
|
-
build: "true"
|
|
12542
|
-
release:
|
|
12543
|
-
mode: integrate
|
|
12544
|
-
versioning: none
|
|
12545
|
-
changelog: false
|
|
12546
|
-
`;
|
|
12547
|
-
var SAMPLE_README = `# crew-sample
|
|
12548
|
-
|
|
12549
|
-
A throwaway repository created by \`crew sample-ticket\`. It exists so the crew
|
|
12550
|
-
has something safe to work on: its test and build hooks always pass, it has no
|
|
12551
|
-
remote, and \`crew sample-ticket --remove\` deletes it.
|
|
12552
|
-
`;
|
|
12553
|
-
function sampleDir(existing, reposBase, workspace) {
|
|
12554
|
-
if (existing.length > 0) return join20(dirname11(existing[0]), SAMPLE_REPO);
|
|
12555
|
-
return join20(reposBase, workspace, SAMPLE_REPO);
|
|
12556
|
-
}
|
|
12557
|
-
var SAMPLE_TITLE = "Sample: add a documented `greet` function with a test";
|
|
12558
|
-
function sampleDescription() {
|
|
12559
|
-
return [
|
|
12560
|
-
SAMPLE_MARKER,
|
|
12561
|
-
"",
|
|
12562
|
-
"> This is a sample ticket filed by `crew sample-ticket` on a throwaway repository. Nothing here is real work.",
|
|
12563
|
-
"",
|
|
12564
|
-
"## What",
|
|
12565
|
-
"",
|
|
12566
|
-
"Add `greet.js` exporting `greet(name)`, which returns `Hello, <name>!` (and `Hello, world!` for no name),",
|
|
12567
|
-
"with a JSDoc comment, and `greet.test.js` covering both cases using `node:test`.",
|
|
12568
|
-
"",
|
|
12569
|
-
"## Acceptance",
|
|
12570
|
-
"",
|
|
12571
|
-
'- `greet("Ada")` returns `Hello, Ada!`; `greet()` returns `Hello, world!`.',
|
|
12572
|
-
"- The test file runs with `node --test` and passes.",
|
|
12573
|
-
"- Add a line to the README saying the function exists.",
|
|
12574
|
-
"",
|
|
12575
|
-
"## Notes",
|
|
12576
|
-
"",
|
|
12577
|
-
"The repo has no remote: leave the branch local. The hooks in `.crew.yaml` always pass, so QA is checking the",
|
|
12578
|
-
"diff against the acceptance list above."
|
|
12579
|
-
].join("\n");
|
|
12580
|
-
}
|
|
12581
|
-
function isSampleTicket(description) {
|
|
12582
|
-
return (description ?? "").trimStart().startsWith(SAMPLE_MARKER);
|
|
12583
|
-
}
|
|
12584
|
-
function planSampleTicket(i) {
|
|
12585
|
-
return {
|
|
12586
|
-
title: SAMPLE_TITLE,
|
|
12587
|
-
description: sampleDescription(),
|
|
12588
|
-
report_type: "feature",
|
|
12589
|
-
severity: "s4",
|
|
12590
|
-
reporter_name: "crew sample-ticket",
|
|
12591
|
-
status: i.approvedStatus,
|
|
12592
|
-
project_id: i.projectId,
|
|
12593
|
-
repo_id: i.repoId,
|
|
12594
|
-
...i.filedBy ? { filed_by_id: i.filedBy } : {}
|
|
12595
|
-
};
|
|
12596
|
-
}
|
|
12663
|
+
// src/repos-hooks.ts
|
|
12664
|
+
import { isMap as isMap2, parseDocument as parseDocument2, YAMLMap } from "yaml";
|
|
12597
12665
|
|
|
12598
12666
|
// src/repos-cmd.ts
|
|
12599
12667
|
import { homedir as homedir7 } from "os";
|
|
12600
|
-
import { basename as
|
|
12668
|
+
import { basename as basename7, join as join20, resolve as resolve3 } from "path";
|
|
12601
12669
|
import { isMap, isSeq, parseDocument, stringify as stringify2 } from "yaml";
|
|
12602
12670
|
var ReposError = class extends Error {
|
|
12603
12671
|
};
|
|
@@ -12669,7 +12737,7 @@ function configuredRepos(text, route, base) {
|
|
|
12669
12737
|
});
|
|
12670
12738
|
}
|
|
12671
12739
|
const dir = m.get("dir");
|
|
12672
|
-
return dir ? [{ name:
|
|
12740
|
+
return dir ? [{ name: basename7(String(dir)), dir: expandDir(String(dir), base) }] : [];
|
|
12673
12741
|
}
|
|
12674
12742
|
function planRepoAdd(i) {
|
|
12675
12743
|
const doc = parseDocument(i.text);
|
|
@@ -12681,7 +12749,7 @@ function planRepoAdd(i) {
|
|
|
12681
12749
|
throw new ReposError(`${target} is already configured on route ${i.route}`);
|
|
12682
12750
|
}
|
|
12683
12751
|
const matched = matchRepoName(i.origin, i.resolved);
|
|
12684
|
-
const name = matched ?? i.name ?? normalizeRemote(i.origin)?.split("/").pop() ??
|
|
12752
|
+
const name = matched ?? i.name ?? normalizeRemote(i.origin)?.split("/").pop() ?? basename7(target);
|
|
12685
12753
|
let warning;
|
|
12686
12754
|
if (!matched) {
|
|
12687
12755
|
const known = Object.values(i.resolved?.repoNames ?? {});
|
|
@@ -12795,13 +12863,218 @@ function planRepoClone(i) {
|
|
|
12795
12863
|
}
|
|
12796
12864
|
const ship = doc.get("ship", true);
|
|
12797
12865
|
const basePath = String(m.get("reposBasePath") ?? (isMap(ship) ? ship.get("reposBasePath") : void 0) ?? "~/Crew");
|
|
12798
|
-
const target = i.dir ? expandDir(i.dir, i.base) :
|
|
12866
|
+
const target = i.dir ? expandDir(i.dir, i.base) : join20(expandDir(basePath, i.base), i.route.split("/")[0], name);
|
|
12799
12867
|
const add = planRepoAdd({ text: i.text, base: i.base, route: i.route, path: target, origin: remote, resolved: i.resolved, name });
|
|
12800
12868
|
return { ...add, remote, target };
|
|
12801
12869
|
}
|
|
12802
12870
|
|
|
12871
|
+
// src/repos-hooks.ts
|
|
12872
|
+
function packageManager(f) {
|
|
12873
|
+
if (f.exists("pnpm-lock.yaml")) return "pnpm";
|
|
12874
|
+
if (f.exists("yarn.lock")) return "yarn";
|
|
12875
|
+
if (f.exists("bun.lock") || f.exists("bun.lockb")) return "bun";
|
|
12876
|
+
return "npm";
|
|
12877
|
+
}
|
|
12878
|
+
function fromPackageJson(f) {
|
|
12879
|
+
const text = f.read("package.json");
|
|
12880
|
+
if (text === void 0) return void 0;
|
|
12881
|
+
let pkg;
|
|
12882
|
+
try {
|
|
12883
|
+
pkg = JSON.parse(text);
|
|
12884
|
+
} catch {
|
|
12885
|
+
return void 0;
|
|
12886
|
+
}
|
|
12887
|
+
if (!pkg || typeof pkg !== "object") return void 0;
|
|
12888
|
+
const pm = packageManager(f);
|
|
12889
|
+
const scripts = pkg.scripts && typeof pkg.scripts === "object" ? pkg.scripts : {};
|
|
12890
|
+
const out = {};
|
|
12891
|
+
if (typeof scripts.test === "string") out.test = `${pm} test`;
|
|
12892
|
+
if (typeof scripts.build === "string") out.build = pm === "yarn" ? "yarn build" : `${pm} run build`;
|
|
12893
|
+
if (typeof pkg.version === "string") {
|
|
12894
|
+
out.version = `node -p "require('./package.json').version"`;
|
|
12895
|
+
out.bump = `npm version "$CREW_BUMP" --no-git-tag-version | sed 's/^v//'`;
|
|
12896
|
+
}
|
|
12897
|
+
return out;
|
|
12898
|
+
}
|
|
12899
|
+
function fromPackageSwift(f) {
|
|
12900
|
+
return f.exists("Package.swift") ? { test: "swift test", build: "swift build" } : void 0;
|
|
12901
|
+
}
|
|
12902
|
+
function fromPyproject(f) {
|
|
12903
|
+
return f.exists("pyproject.toml") ? { test: "pytest" } : void 0;
|
|
12904
|
+
}
|
|
12905
|
+
function fromMakefile(f) {
|
|
12906
|
+
const text = f.read("Makefile") ?? f.read("makefile") ?? f.read("GNUmakefile");
|
|
12907
|
+
if (text === void 0) return void 0;
|
|
12908
|
+
const out = {};
|
|
12909
|
+
for (const target of ["test", "build"]) {
|
|
12910
|
+
if (new RegExp(`^${target}\\s*:`, "m").test(text)) out[target] = `make ${target}`;
|
|
12911
|
+
}
|
|
12912
|
+
return out;
|
|
12913
|
+
}
|
|
12914
|
+
var DETECTORS = [
|
|
12915
|
+
{ source: "package.json", detect: fromPackageJson },
|
|
12916
|
+
{ source: "Package.swift", detect: fromPackageSwift },
|
|
12917
|
+
{ source: "pyproject.toml", detect: fromPyproject },
|
|
12918
|
+
{ source: "Makefile", detect: fromMakefile }
|
|
12919
|
+
];
|
|
12920
|
+
function suggestHooks(f) {
|
|
12921
|
+
const found = DETECTORS.flatMap((d) => {
|
|
12922
|
+
const hooks = d.detect(f);
|
|
12923
|
+
return hooks && Object.keys(hooks).length > 0 ? [{ source: d.source, hooks }] : [];
|
|
12924
|
+
});
|
|
12925
|
+
if (found.length === 0) {
|
|
12926
|
+
return { source: null, hooks: {}, notes: ["no package.json scripts, Package.swift, pyproject.toml or Makefile test/build targets found \u2014 nothing to propose"] };
|
|
12927
|
+
}
|
|
12928
|
+
const [first, ...others] = found;
|
|
12929
|
+
const notes = others.length > 0 ? [`also found ${others.map((o) => o.source).join(", ")}; proposing from ${first.source} only`] : [];
|
|
12930
|
+
return { source: first.source, hooks: first.hooks, notes };
|
|
12931
|
+
}
|
|
12932
|
+
function parseHooksArgs(args, routes) {
|
|
12933
|
+
const rest = [...args];
|
|
12934
|
+
let route;
|
|
12935
|
+
if (rest.length > 1 && routes.includes(rest[0])) route = rest.shift();
|
|
12936
|
+
if (!route) {
|
|
12937
|
+
if (routes.length === 1) route = routes[0];
|
|
12938
|
+
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(", ")}`);
|
|
12939
|
+
}
|
|
12940
|
+
if (rest.length !== 1) throw new ReposError("expected: crew repos hooks [route] <name> [--suggest] [--write route|repo] [--json]");
|
|
12941
|
+
return { route, name: rest[0] };
|
|
12942
|
+
}
|
|
12943
|
+
function repoDirOf(text, base, route, name) {
|
|
12944
|
+
const repos = configuredRepos(text, route, base);
|
|
12945
|
+
const hit = repos.find((r) => r.name === name);
|
|
12946
|
+
if (!hit) {
|
|
12947
|
+
throw new ReposError(`route ${route} has no repo named "${name}" (have: ${repos.map((r) => r.name).join(", ") || "none"})`);
|
|
12948
|
+
}
|
|
12949
|
+
return hit.dir;
|
|
12950
|
+
}
|
|
12951
|
+
function planRouteHooks(text, route, name, hooks) {
|
|
12952
|
+
const doc = parseDocument2(text);
|
|
12953
|
+
if (doc.errors.length > 0) throw new ReposError(`config is not valid YAML \u2014 ${doc.errors[0].message}`);
|
|
12954
|
+
const m = routeMapOf(doc, route);
|
|
12955
|
+
const repos = m.get("repos", true);
|
|
12956
|
+
let target = m;
|
|
12957
|
+
if (isMap2(repos)) {
|
|
12958
|
+
const entry = repos.get(name, true);
|
|
12959
|
+
if (isMap2(entry)) target = entry;
|
|
12960
|
+
else {
|
|
12961
|
+
const created = new YAMLMap();
|
|
12962
|
+
created.set("dir", String(repos.get(name)));
|
|
12963
|
+
repos.set(name, created);
|
|
12964
|
+
target = created;
|
|
12965
|
+
}
|
|
12966
|
+
}
|
|
12967
|
+
const current = target.get("hooks", true);
|
|
12968
|
+
let hooksMap;
|
|
12969
|
+
if (isMap2(current)) hooksMap = current;
|
|
12970
|
+
else {
|
|
12971
|
+
hooksMap = new YAMLMap();
|
|
12972
|
+
target.set("hooks", hooksMap);
|
|
12973
|
+
}
|
|
12974
|
+
const written = [];
|
|
12975
|
+
const kept = [];
|
|
12976
|
+
for (const [k, v] of Object.entries(hooks)) {
|
|
12977
|
+
if (v === void 0) continue;
|
|
12978
|
+
if (hooksMap.has(k)) kept.push(k);
|
|
12979
|
+
else {
|
|
12980
|
+
hooksMap.set(k, v);
|
|
12981
|
+
written.push(k);
|
|
12982
|
+
}
|
|
12983
|
+
}
|
|
12984
|
+
return { text: String(doc), written, kept };
|
|
12985
|
+
}
|
|
12986
|
+
function starterRepoConfig(hooks) {
|
|
12987
|
+
const lines = [
|
|
12988
|
+
"# Starter .crew.yaml proposed by `crew repos hooks --write repo`.",
|
|
12989
|
+
"# Review the hooks, then commit this file; it is the durable home for them",
|
|
12990
|
+
"# (docs/REPO_SPEC.md). A route-level copy in crew.yaml is shadowed by it.",
|
|
12991
|
+
`version: ${SPEC_VERSION}`,
|
|
12992
|
+
"platform: any",
|
|
12993
|
+
"",
|
|
12994
|
+
"hooks:"
|
|
12995
|
+
];
|
|
12996
|
+
const entries = Object.entries(hooks).filter(([, v]) => v !== void 0);
|
|
12997
|
+
if (entries.length === 0) lines.push(' # test: "..."', ' # build: "..."');
|
|
12998
|
+
for (const [k, v] of entries) lines.push(` ${k}: ${JSON.stringify(v)}`);
|
|
12999
|
+
lines.push("", "release:", " # integrate: merging to the base branch is the release; nothing is deployed.", " mode: integrate", "");
|
|
13000
|
+
const text = lines.join("\n");
|
|
13001
|
+
parseRepoConfig(text, ".crew.yaml");
|
|
13002
|
+
return text;
|
|
13003
|
+
}
|
|
13004
|
+
function existingRepoConfig(f) {
|
|
13005
|
+
return REPO_CONFIG_FILENAMES.find((n) => f.exists(n));
|
|
13006
|
+
}
|
|
13007
|
+
|
|
13008
|
+
// src/sample-ticket.ts
|
|
13009
|
+
import { basename as basename8, dirname as dirname13, join as join21 } from "path";
|
|
13010
|
+
var SAMPLE_REPO = "crew-sample";
|
|
13011
|
+
var SAMPLE_REMOTE = `local/${SAMPLE_REPO}`;
|
|
13012
|
+
var SAMPLE_MARKER = "sample: true";
|
|
13013
|
+
var SAMPLE_CREW_YAML = `version: 1
|
|
13014
|
+
platform: unix
|
|
13015
|
+
branch:
|
|
13016
|
+
base: main
|
|
13017
|
+
hooks:
|
|
13018
|
+
test: "true"
|
|
13019
|
+
build: "true"
|
|
13020
|
+
release:
|
|
13021
|
+
mode: integrate
|
|
13022
|
+
versioning: none
|
|
13023
|
+
changelog: false
|
|
13024
|
+
`;
|
|
13025
|
+
var SAMPLE_README = `# crew-sample
|
|
13026
|
+
|
|
13027
|
+
A throwaway repository created by \`crew sample-ticket\`. It exists so the crew
|
|
13028
|
+
has something safe to work on: its test and build hooks always pass, it has no
|
|
13029
|
+
remote, and \`crew sample-ticket --remove\` deletes it.
|
|
13030
|
+
`;
|
|
13031
|
+
function sampleDir(existing, reposBase, workspace) {
|
|
13032
|
+
if (existing.length > 0) return join21(dirname13(existing[0]), SAMPLE_REPO);
|
|
13033
|
+
return join21(reposBase, workspace, SAMPLE_REPO);
|
|
13034
|
+
}
|
|
13035
|
+
var SAMPLE_TITLE = "Sample: add a documented `greet` function with a test";
|
|
13036
|
+
function sampleDescription() {
|
|
13037
|
+
return [
|
|
13038
|
+
SAMPLE_MARKER,
|
|
13039
|
+
"",
|
|
13040
|
+
"> This is a sample ticket filed by `crew sample-ticket` on a throwaway repository. Nothing here is real work.",
|
|
13041
|
+
"",
|
|
13042
|
+
"## What",
|
|
13043
|
+
"",
|
|
13044
|
+
"Add `greet.js` exporting `greet(name)`, which returns `Hello, <name>!` (and `Hello, world!` for no name),",
|
|
13045
|
+
"with a JSDoc comment, and `greet.test.js` covering both cases using `node:test`.",
|
|
13046
|
+
"",
|
|
13047
|
+
"## Acceptance",
|
|
13048
|
+
"",
|
|
13049
|
+
'- `greet("Ada")` returns `Hello, Ada!`; `greet()` returns `Hello, world!`.',
|
|
13050
|
+
"- The test file runs with `node --test` and passes.",
|
|
13051
|
+
"- Add a line to the README saying the function exists.",
|
|
13052
|
+
"",
|
|
13053
|
+
"## Notes",
|
|
13054
|
+
"",
|
|
13055
|
+
"The repo has no remote: leave the branch local. The hooks in `.crew.yaml` always pass, so QA is checking the",
|
|
13056
|
+
"diff against the acceptance list above."
|
|
13057
|
+
].join("\n");
|
|
13058
|
+
}
|
|
13059
|
+
function isSampleTicket(description) {
|
|
13060
|
+
return (description ?? "").trimStart().startsWith(SAMPLE_MARKER);
|
|
13061
|
+
}
|
|
13062
|
+
function planSampleTicket(i) {
|
|
13063
|
+
return {
|
|
13064
|
+
title: SAMPLE_TITLE,
|
|
13065
|
+
description: sampleDescription(),
|
|
13066
|
+
report_type: "feature",
|
|
13067
|
+
severity: "s4",
|
|
13068
|
+
reporter_name: "crew sample-ticket",
|
|
13069
|
+
status: i.approvedStatus,
|
|
13070
|
+
project_id: i.projectId,
|
|
13071
|
+
repo_id: i.repoId,
|
|
13072
|
+
...i.filedBy ? { filed_by_id: i.filedBy } : {}
|
|
13073
|
+
};
|
|
13074
|
+
}
|
|
13075
|
+
|
|
12803
13076
|
// src/doctor-fix.ts
|
|
12804
|
-
import { parseDocument as
|
|
13077
|
+
import { parseDocument as parseDocument3 } from "yaml";
|
|
12805
13078
|
var sound = (r) => r.hasRepos && r.problems.length === 0;
|
|
12806
13079
|
function planFix(f) {
|
|
12807
13080
|
const actions = [];
|
|
@@ -12828,7 +13101,7 @@ function routesInScope(routes, named) {
|
|
|
12828
13101
|
return named ? routes.filter((r) => r.route === named) : routes;
|
|
12829
13102
|
}
|
|
12830
13103
|
function planEnable(text, route) {
|
|
12831
|
-
const doc =
|
|
13104
|
+
const doc = parseDocument3(text);
|
|
12832
13105
|
routeMapOf(doc, route).set("enabled", true);
|
|
12833
13106
|
return String(doc);
|
|
12834
13107
|
}
|
|
@@ -12838,8 +13111,8 @@ function schedulerInstalled(plan, exists, crontab) {
|
|
|
12838
13111
|
}
|
|
12839
13112
|
|
|
12840
13113
|
// src/worktree-sweep.ts
|
|
12841
|
-
import { existsSync as
|
|
12842
|
-
import { basename as basename9, dirname as
|
|
13114
|
+
import { existsSync as existsSync22, readFileSync as readFileSync16, readdirSync as readdirSync5 } from "fs";
|
|
13115
|
+
import { basename as basename9, dirname as dirname14, join as join22, resolve as resolve4 } from "path";
|
|
12843
13116
|
function planWorktreeSweep(target, tickets, contract) {
|
|
12844
13117
|
const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
|
|
12845
13118
|
const verified = contract.statuses.verified;
|
|
@@ -12893,7 +13166,7 @@ function worktreeTicketNumbers(target) {
|
|
|
12893
13166
|
return [...nums];
|
|
12894
13167
|
}
|
|
12895
13168
|
function siblingWorktreeDirs(target) {
|
|
12896
|
-
const parent =
|
|
13169
|
+
const parent = dirname14(target.dir.replace(/[/\\]+$/, ""));
|
|
12897
13170
|
const stem = `${basename9(target.dir.replace(/[/\\]+$/, ""))}-`;
|
|
12898
13171
|
const registered = new Set(worktrees(target.dir).map((w) => resolve4(w.path)));
|
|
12899
13172
|
let names;
|
|
@@ -12941,7 +13214,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
|
|
|
12941
13214
|
let removed = 0;
|
|
12942
13215
|
let keptBranches = 0;
|
|
12943
13216
|
for (const a of actions) {
|
|
12944
|
-
if (!dryRun && base && !a.keepBranch &&
|
|
13217
|
+
if (!dryRun && base && !a.keepBranch && existsSync22(a.path) && isHandBackMerge(a.path, base)) {
|
|
12945
13218
|
if (gitOk(a.path, ["merge", "--abort"]) !== null) {
|
|
12946
13219
|
log.emit(
|
|
12947
13220
|
`abandoned the hand-back merge of ${base} in ${a.path}: ${a.ticket.issue_id} deployed without it`,
|
|
@@ -12949,7 +13222,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
|
|
|
12949
13222
|
);
|
|
12950
13223
|
}
|
|
12951
13224
|
}
|
|
12952
|
-
if (!dryRun &&
|
|
13225
|
+
if (!dryRun && existsSync22(a.path)) {
|
|
12953
13226
|
let dirty = false;
|
|
12954
13227
|
try {
|
|
12955
13228
|
dirty = status(a.path).length > 0;
|
|
@@ -12960,7 +13233,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
|
|
|
12960
13233
|
continue;
|
|
12961
13234
|
}
|
|
12962
13235
|
}
|
|
12963
|
-
if (!dryRun && a.requirePushed && a.branch &&
|
|
13236
|
+
if (!dryRun && a.requirePushed && a.branch && existsSync22(a.path) && remoteConfigured(cwd, remote) && worktreeHoldsUnpushed(a.path, remote)) {
|
|
12964
13237
|
refuse(a, "unpushed commits", `${a.branch} has commits that are not on ${remote}`);
|
|
12965
13238
|
continue;
|
|
12966
13239
|
}
|
|
@@ -13009,7 +13282,7 @@ function isHandBackMerge(path, base) {
|
|
|
13009
13282
|
if (!msgPath) return false;
|
|
13010
13283
|
let first;
|
|
13011
13284
|
try {
|
|
13012
|
-
first =
|
|
13285
|
+
first = readFileSync16(resolve4(path, msgPath), "utf8").split("\n")[0] ?? "";
|
|
13013
13286
|
} catch {
|
|
13014
13287
|
return false;
|
|
13015
13288
|
}
|
|
@@ -13114,12 +13387,12 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
|
|
|
13114
13387
|
}
|
|
13115
13388
|
|
|
13116
13389
|
// src/stream-sweep.ts
|
|
13117
|
-
import { existsSync as
|
|
13390
|
+
import { existsSync as existsSync23, readdirSync as readdirSync6, rmSync as rmSync9, statSync as statSync7 } from "fs";
|
|
13118
13391
|
import { join as join23 } from "path";
|
|
13119
13392
|
var DAY_MS2 = 24 * 60 * 60 * 1e3;
|
|
13120
13393
|
function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
|
|
13121
13394
|
const dir = join23(stateDir, "streams");
|
|
13122
|
-
if (!
|
|
13395
|
+
if (!existsSync23(dir)) return [];
|
|
13123
13396
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
13124
13397
|
return readdirSync6(dir).map((name) => join23(dir, name)).filter((path) => {
|
|
13125
13398
|
try {
|
|
@@ -13147,7 +13420,7 @@ function applyStreamSweep(paths, dryRun, log) {
|
|
|
13147
13420
|
return removed;
|
|
13148
13421
|
}
|
|
13149
13422
|
function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
13150
|
-
if (!
|
|
13423
|
+
if (!existsSync23(stateDir)) return [];
|
|
13151
13424
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
13152
13425
|
return readdirSync6(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join23(stateDir, name)).filter((path) => {
|
|
13153
13426
|
try {
|
|
@@ -13159,14 +13432,14 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
|
13159
13432
|
}
|
|
13160
13433
|
|
|
13161
13434
|
// src/cli.ts
|
|
13162
|
-
import { readFileSync as
|
|
13435
|
+
import { readFileSync as readFileSync22, existsSync as existsSync29, mkdirSync as mkdirSync15, writeFileSync as writeFileSync16, rmSync as rmSync11, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync2, statSync as statSync9, readdirSync as readdirSync11 } from "fs";
|
|
13163
13436
|
import { createInterface } from "readline/promises";
|
|
13164
13437
|
import { dirname as dirOf, resolve as resolvePath } from "path";
|
|
13165
13438
|
|
|
13166
13439
|
// src/ssh-keys.ts
|
|
13167
|
-
import { execFileSync as
|
|
13168
|
-
import { existsSync as
|
|
13169
|
-
import { dirname as
|
|
13440
|
+
import { execFileSync as execFileSync4 } from "child_process";
|
|
13441
|
+
import { existsSync as existsSync24, mkdirSync as mkdirSync11, readFileSync as readFileSync17, chmodSync } from "fs";
|
|
13442
|
+
import { dirname as dirname15, join as join24 } from "path";
|
|
13170
13443
|
var SshKeyError = class extends Error {
|
|
13171
13444
|
};
|
|
13172
13445
|
function shipSshKeyPathFor(stateDir) {
|
|
@@ -13175,10 +13448,10 @@ function shipSshKeyPathFor(stateDir) {
|
|
|
13175
13448
|
function ensureShipSshKeypair(stateDir) {
|
|
13176
13449
|
const privateKeyPath = shipSshKeyPathFor(stateDir);
|
|
13177
13450
|
const publicKeyPath = `${privateKeyPath}.pub`;
|
|
13178
|
-
if (!
|
|
13179
|
-
|
|
13451
|
+
if (!existsSync24(privateKeyPath)) {
|
|
13452
|
+
mkdirSync11(dirname15(privateKeyPath), { recursive: true });
|
|
13180
13453
|
try {
|
|
13181
|
-
|
|
13454
|
+
execFileSync4("ssh-keygen", [
|
|
13182
13455
|
"-t",
|
|
13183
13456
|
"ed25519",
|
|
13184
13457
|
"-N",
|
|
@@ -13196,15 +13469,15 @@ function ensureShipSshKeypair(stateDir) {
|
|
|
13196
13469
|
}
|
|
13197
13470
|
chmodSync(privateKeyPath, 384);
|
|
13198
13471
|
}
|
|
13199
|
-
if (!
|
|
13472
|
+
if (!existsSync24(publicKeyPath)) {
|
|
13200
13473
|
throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
|
|
13201
13474
|
}
|
|
13202
|
-
const publicKey =
|
|
13475
|
+
const publicKey = readFileSync17(publicKeyPath, "utf8").trim();
|
|
13203
13476
|
return { privateKeyPath, publicKeyPath, publicKey };
|
|
13204
13477
|
}
|
|
13205
13478
|
function sshKeygenAvailable() {
|
|
13206
13479
|
try {
|
|
13207
|
-
|
|
13480
|
+
execFileSync4("ssh-keygen", ["-V"], { stdio: ["ignore", "ignore", "ignore"] });
|
|
13208
13481
|
return true;
|
|
13209
13482
|
} catch (e) {
|
|
13210
13483
|
return e.code !== "ENOENT";
|
|
@@ -13212,11 +13485,11 @@ function sshKeygenAvailable() {
|
|
|
13212
13485
|
}
|
|
13213
13486
|
|
|
13214
13487
|
// src/passenger-containers.ts
|
|
13215
|
-
import { execFileSync as
|
|
13488
|
+
import { execFileSync as execFileSync5 } from "child_process";
|
|
13216
13489
|
import { createHash as createHash4 } from "crypto";
|
|
13217
13490
|
var realExec = (cmd, args) => {
|
|
13218
13491
|
try {
|
|
13219
|
-
return
|
|
13492
|
+
return execFileSync5(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
13220
13493
|
} catch (e) {
|
|
13221
13494
|
const err = e;
|
|
13222
13495
|
const why = [err.stderr, err.stdout].map((x) => (x ?? "").trim()).filter(Boolean).join(" / ");
|
|
@@ -13415,7 +13688,7 @@ function syncAllPassengerCheckouts(cfg) {
|
|
|
13415
13688
|
|
|
13416
13689
|
// src/tunnel.ts
|
|
13417
13690
|
import { spawn as spawn4 } from "child_process";
|
|
13418
|
-
import { existsSync as
|
|
13691
|
+
import { existsSync as existsSync25, mkdirSync as mkdirSync12, readFileSync as readFileSync18, writeFileSync as writeFileSync13, unlinkSync as unlinkSync3, readdirSync as readdirSync7 } from "fs";
|
|
13419
13692
|
import { randomInt } from "crypto";
|
|
13420
13693
|
import { join as join25 } from "path";
|
|
13421
13694
|
var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
|
|
@@ -13488,17 +13761,17 @@ function tunnelStatePath(stateDir, workspaceId) {
|
|
|
13488
13761
|
}
|
|
13489
13762
|
function readPersistedTunnel(stateDir, workspaceId) {
|
|
13490
13763
|
const path = tunnelStatePath(stateDir, workspaceId);
|
|
13491
|
-
if (!
|
|
13764
|
+
if (!existsSync25(path)) return void 0;
|
|
13492
13765
|
try {
|
|
13493
|
-
return JSON.parse(
|
|
13766
|
+
return JSON.parse(readFileSync18(path, "utf8"));
|
|
13494
13767
|
} catch {
|
|
13495
13768
|
return void 0;
|
|
13496
13769
|
}
|
|
13497
13770
|
}
|
|
13498
13771
|
function writePersistedTunnel(stateDir, state) {
|
|
13499
13772
|
const path = tunnelStatePath(stateDir, state.workspaceId);
|
|
13500
|
-
|
|
13501
|
-
|
|
13773
|
+
mkdirSync12(join25(stateDir, "passengers"), { recursive: true });
|
|
13774
|
+
writeFileSync13(path, `${JSON.stringify(state, null, 2)}
|
|
13502
13775
|
`);
|
|
13503
13776
|
}
|
|
13504
13777
|
function updatePersistedTunnel(stateDir, workspaceId, patch2) {
|
|
@@ -13596,7 +13869,7 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
|
|
|
13596
13869
|
}
|
|
13597
13870
|
function readAllPersistedWorkspaceIds(stateDir) {
|
|
13598
13871
|
const dir = join25(stateDir, "passengers");
|
|
13599
|
-
if (!
|
|
13872
|
+
if (!existsSync25(dir)) return [];
|
|
13600
13873
|
return readdirSync7(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
|
|
13601
13874
|
}
|
|
13602
13875
|
|
|
@@ -13717,7 +13990,7 @@ async function unregisterEndpoint(workspaceId, info, d) {
|
|
|
13717
13990
|
import { createServer as createHttpServer } from "http";
|
|
13718
13991
|
import { spawn as spawn5 } from "child_process";
|
|
13719
13992
|
import { randomBytes } from "crypto";
|
|
13720
|
-
import { existsSync as
|
|
13993
|
+
import { existsSync as existsSync26, mkdirSync as mkdirSync13, readFileSync as readFileSync19, writeFileSync as writeFileSync14, unlinkSync as unlinkSync4, readdirSync as readdirSync8 } from "fs";
|
|
13721
13994
|
import { join as join26 } from "path";
|
|
13722
13995
|
|
|
13723
13996
|
// src/secret-compare.ts
|
|
@@ -13811,17 +14084,17 @@ function syncDaemonStatePath(stateDir, workspaceId) {
|
|
|
13811
14084
|
}
|
|
13812
14085
|
function readPersistedSyncDaemon(stateDir, workspaceId) {
|
|
13813
14086
|
const path = syncDaemonStatePath(stateDir, workspaceId);
|
|
13814
|
-
if (!
|
|
14087
|
+
if (!existsSync26(path)) return void 0;
|
|
13815
14088
|
try {
|
|
13816
|
-
return JSON.parse(
|
|
14089
|
+
return JSON.parse(readFileSync19(path, "utf8"));
|
|
13817
14090
|
} catch {
|
|
13818
14091
|
return void 0;
|
|
13819
14092
|
}
|
|
13820
14093
|
}
|
|
13821
14094
|
function writePersistedSyncDaemon(stateDir, state) {
|
|
13822
14095
|
const path = syncDaemonStatePath(stateDir, state.workspaceId);
|
|
13823
|
-
|
|
13824
|
-
|
|
14096
|
+
mkdirSync13(join26(stateDir, "passengers"), { recursive: true });
|
|
14097
|
+
writeFileSync14(path, `${JSON.stringify(state, null, 2)}
|
|
13825
14098
|
`);
|
|
13826
14099
|
}
|
|
13827
14100
|
function removePersistedSyncDaemon(stateDir, workspaceId) {
|
|
@@ -13842,7 +14115,7 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
|
|
|
13842
14115
|
}
|
|
13843
14116
|
function readAllPersistedSyncWorkspaceIds(stateDir) {
|
|
13844
14117
|
const dir = join26(stateDir, "passengers");
|
|
13845
|
-
if (!
|
|
14118
|
+
if (!existsSync26(dir)) return [];
|
|
13846
14119
|
const suffix = "-sync.json";
|
|
13847
14120
|
return readdirSync8(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
|
|
13848
14121
|
}
|
|
@@ -13899,11 +14172,11 @@ function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
|
|
|
13899
14172
|
}
|
|
13900
14173
|
|
|
13901
14174
|
// src/daemon.ts
|
|
13902
|
-
import { existsSync as
|
|
14175
|
+
import { existsSync as existsSync28, mkdirSync as mkdirSync14, readFileSync as readFileSync21, readdirSync as readdirSync10, rmSync as rmSync10, statSync as statSync8, writeFileSync as writeFileSync15 } from "fs";
|
|
13903
14176
|
import { join as join28 } from "path";
|
|
13904
14177
|
|
|
13905
14178
|
// src/interrupted.ts
|
|
13906
|
-
import { existsSync as
|
|
14179
|
+
import { existsSync as existsSync27, readdirSync as readdirSync9, readFileSync as readFileSync20 } from "fs";
|
|
13907
14180
|
import { join as join27 } from "path";
|
|
13908
14181
|
var INTERRUPTED_MARKER = calloutHeader("NOTE", "Interrupted");
|
|
13909
14182
|
function interruptedComment(b) {
|
|
@@ -13923,16 +14196,16 @@ var pidAlive = (pid) => {
|
|
|
13923
14196
|
};
|
|
13924
14197
|
function runFinished(streamPath) {
|
|
13925
14198
|
const path = `${streamPath}.meta.json`;
|
|
13926
|
-
if (!
|
|
14199
|
+
if (!existsSync27(path)) return false;
|
|
13927
14200
|
try {
|
|
13928
|
-
return JSON.parse(
|
|
14201
|
+
return JSON.parse(readFileSync20(path, "utf8")).running !== true;
|
|
13929
14202
|
} catch {
|
|
13930
14203
|
return true;
|
|
13931
14204
|
}
|
|
13932
14205
|
}
|
|
13933
14206
|
function orphanedStreams(o) {
|
|
13934
14207
|
const dir = join27(o.stateDir, "streams");
|
|
13935
|
-
if (!
|
|
14208
|
+
if (!existsSync27(dir)) return [];
|
|
13936
14209
|
const ownerAlive = o.isOwnerAlive ?? pidAlive;
|
|
13937
14210
|
const out = [];
|
|
13938
14211
|
for (const name of readdirSync9(dir)) {
|
|
@@ -14046,7 +14319,7 @@ function computeTreeSignature(paths) {
|
|
|
14046
14319
|
}
|
|
14047
14320
|
function makeStaleChecker(paths, opts = {}) {
|
|
14048
14321
|
const initial = computeTreeSignature(paths);
|
|
14049
|
-
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 &&
|
|
14322
|
+
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync28(opts.reloadSentinel);
|
|
14050
14323
|
}
|
|
14051
14324
|
function reloadSentinelFor(stateDir) {
|
|
14052
14325
|
return join28(stateDir, "reload-requested");
|
|
@@ -14055,14 +14328,14 @@ function daemonStampFor(stateDir) {
|
|
|
14055
14328
|
return join28(stateDir, "daemon.started");
|
|
14056
14329
|
}
|
|
14057
14330
|
function noteDaemonStarted(stateDir, now = Date.now()) {
|
|
14058
|
-
|
|
14331
|
+
mkdirSync14(stateDir, { recursive: true });
|
|
14059
14332
|
rmSync10(reloadSentinelFor(stateDir), { force: true });
|
|
14060
|
-
|
|
14333
|
+
writeFileSync15(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
|
|
14061
14334
|
`);
|
|
14062
14335
|
}
|
|
14063
14336
|
function readDaemonStarted(stateDir) {
|
|
14064
14337
|
try {
|
|
14065
|
-
const v = JSON.parse(
|
|
14338
|
+
const v = JSON.parse(readFileSync21(daemonStampFor(stateDir), "utf8")).startedAt;
|
|
14066
14339
|
return typeof v === "number" ? v : null;
|
|
14067
14340
|
} catch {
|
|
14068
14341
|
return null;
|
|
@@ -14418,7 +14691,7 @@ async function buildEnvironment(route, ship, ticket) {
|
|
|
14418
14691
|
}
|
|
14419
14692
|
|
|
14420
14693
|
// src/cli.ts
|
|
14421
|
-
var CREW_HOME = isCompiledBinary(import.meta.url) ?
|
|
14694
|
+
var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname16(process.execPath) : resolve5(dirname16(fileURLToPath(import.meta.url)), "..");
|
|
14422
14695
|
var RELEASE_LOCK_TTL_MS = 20 * 60 * 1e3;
|
|
14423
14696
|
var CYCLE_SECONDS2 = 120;
|
|
14424
14697
|
var STALE_CYCLES = 3;
|
|
@@ -14549,7 +14822,8 @@ function usage() {
|
|
|
14549
14822
|
crew status [route] [--json] paused/running state; --json for a machine reader
|
|
14550
14823
|
crew projects [--route R] [--json] the Projects rows each route can read (id, name, prefix, work mode)
|
|
14551
14824
|
crew reload make the running daemon re-read crew.yaml (--now: restart it immediately)
|
|
14552
|
-
crew doctor [route] [--fix]
|
|
14825
|
+
crew doctor [route] [--fix] [--offline] [--json] preflight; --fix enables clean routes and offers crew install;
|
|
14826
|
+
--offline skips push probes; --json prints push-auth and tool checks only
|
|
14553
14827
|
crew selftest [route] [--toolchain NAME] [--json] launch each toolchain as a run would and prove it reaches the tracker
|
|
14554
14828
|
crew sample-ticket [route] [--remove] [--dry-run] [--json] file one ticket on a throwaway repo that walks the dev, QA and release loop
|
|
14555
14829
|
crew ports [route] which checkout owns which ports, and what is up
|
|
@@ -14563,13 +14837,14 @@ function usage() {
|
|
|
14563
14837
|
resume with no argument clears only the whole-crew pause; a role or release pause needs its own resume
|
|
14564
14838
|
crew log [route] tail the log
|
|
14565
14839
|
crew inbox [--member NAME] your tickets across every workspace (or a colleague's)
|
|
14566
|
-
crew connect resolve a workspace's ids into a crew.yaml block (--no-mcp skips registering the Tablation MCP server with Claude Code; --mcp replace overwrites a different one)
|
|
14840
|
+
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)
|
|
14567
14841
|
crew disconnect remove what connect installed for Pair (Claude MCP; Codex/Gemini instructions block and MCP entry per ship.pair)
|
|
14568
14842
|
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)
|
|
14569
14843
|
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)
|
|
14570
14844
|
crew skills sync [route] push only crew's skill files (e.g. grill-me) into the workspace Agent Skills table
|
|
14571
14845
|
crew repos add [ROUTE] [NAME] [PATH] attach a local checkout to a route in crew.yaml (--dry-run previews)
|
|
14572
14846
|
crew repos clone [ROUTE] NAME clone a Repos row's remote into the route directory and attach it (--dir PATH, --dry-run, --json)
|
|
14847
|
+
crew repos hooks [ROUTE] NAME suggest test/build hooks for a checkout (--write route|repo, --json)
|
|
14573
14848
|
crew repos list ROUTE the checkouts a route has, with the tracker's Repos row each matches
|
|
14574
14849
|
crew logbook list [route] recent Agent Log entries, filterable by --role/--ticket
|
|
14575
14850
|
crew logbook show [route] ID [--prompt] one entry; --prompt reconstructs and verifies its prompt
|
|
@@ -14976,7 +15251,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
14976
15251
|
if (host === "linux" && hasSystemd) {
|
|
14977
15252
|
const label = labelFor(CREW_HOME, "run");
|
|
14978
15253
|
const staleTimer = join29(homedir8(), ".config", "systemd", "user", `${label}.timer`);
|
|
14979
|
-
if (
|
|
15254
|
+
if (existsSync29(staleTimer)) {
|
|
14980
15255
|
process.stdout.write(
|
|
14981
15256
|
`removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
|
|
14982
15257
|
`
|
|
@@ -15002,7 +15277,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
15002
15277
|
const tracker = new Tracker(c, cfg.ship);
|
|
15003
15278
|
problems.push(...validateContract(tracker.contract).map((p) => `contract: ${p}`));
|
|
15004
15279
|
for (const r of await resolvedRepos(c)) {
|
|
15005
|
-
if (!
|
|
15280
|
+
if (!existsSync29(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
|
|
15006
15281
|
if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
|
|
15007
15282
|
problems.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
|
|
15008
15283
|
}
|
|
@@ -15020,7 +15295,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
15020
15295
|
}
|
|
15021
15296
|
async function doctorFix(o) {
|
|
15022
15297
|
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))));
|
|
15023
|
-
const unitExists = (p) =>
|
|
15298
|
+
const unitExists = (p) => existsSync29(p);
|
|
15024
15299
|
const crontab = o.host === "linux" ? spawnSync6("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
|
|
15025
15300
|
const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
|
|
15026
15301
|
const plan = planFix({ routes, schedulerAbsent, host: o.host });
|
|
@@ -15037,7 +15312,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
15037
15312
|
for (const a of plan.actions) {
|
|
15038
15313
|
if (a.kind === "enable") {
|
|
15039
15314
|
if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
|
|
15040
|
-
|
|
15315
|
+
writeFileSync16(cfg.configFile, planEnable(readFileSync22(cfg.configFile, "utf8"), a.route));
|
|
15041
15316
|
enabledSound.add(a.route);
|
|
15042
15317
|
changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
|
|
15043
15318
|
} else {
|
|
@@ -15054,7 +15329,10 @@ ${changed2.map((c) => ` - ${c}
|
|
|
15054
15329
|
`).join("")}` : "fix: nothing changed\n");
|
|
15055
15330
|
}
|
|
15056
15331
|
async function resolvedRepos(c) {
|
|
15057
|
-
return Promise.all(reposOf(c).map(
|
|
15332
|
+
return Promise.all(reposOf(c).map((t) => resolveOneRepo(c, t)));
|
|
15333
|
+
}
|
|
15334
|
+
async function resolveOneRepo(c, t) {
|
|
15335
|
+
{
|
|
15058
15336
|
const id = repoIdForName(c, t.name);
|
|
15059
15337
|
const o = c.repoOverrides[t.name];
|
|
15060
15338
|
return {
|
|
@@ -15078,7 +15356,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
15078
15356
|
worktrees: { prefix: shipWorktreePrefixFor(c) ?? await areaWorktreePrefix(c, id), nameOverride: o?.worktreeName }
|
|
15079
15357
|
}, t.dir)
|
|
15080
15358
|
};
|
|
15081
|
-
}
|
|
15359
|
+
}
|
|
15082
15360
|
}
|
|
15083
15361
|
const worktreeLocations = async (c) => (await resolvedRepos(c)).map((r) => ({
|
|
15084
15362
|
parent: resolvePath(r.dir, ".."),
|
|
@@ -16160,69 +16438,27 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16160
16438
|
`);
|
|
16161
16439
|
process.exit(2);
|
|
16162
16440
|
}
|
|
16163
|
-
|
|
16164
|
-
let readOnly = !isAdminRole(keyRole);
|
|
16165
|
-
let personaResult;
|
|
16166
|
-
const runPersonas = (ro) => syncPersonas(target, { userAgent: cfg.ship.userAgent, dryRun, force, readOnly: ro });
|
|
16441
|
+
let run2;
|
|
16167
16442
|
try {
|
|
16168
|
-
|
|
16169
|
-
|
|
16170
|
-
|
|
16171
|
-
|
|
16172
|
-
|
|
16173
|
-
|
|
16174
|
-
|
|
16443
|
+
run2 = await runAgentsSync(target, {
|
|
16444
|
+
userAgent: cfg.ship.userAgent,
|
|
16445
|
+
stateDir: cfg.ship.stateDir,
|
|
16446
|
+
dryRun,
|
|
16447
|
+
force,
|
|
16448
|
+
say: (l) => process.stdout.write(`${l}
|
|
16449
|
+
`),
|
|
16450
|
+
warn: (l) => process.stderr.write(`crew agents sync: ${l}
|
|
16451
|
+
`)
|
|
16452
|
+
});
|
|
16175
16453
|
} catch (e) {
|
|
16176
|
-
if (e instanceof AgentsSyncError) {
|
|
16454
|
+
if (e instanceof AgentsSyncError || e instanceof SkillSyncError) {
|
|
16177
16455
|
process.stderr.write(`crew agents sync: ${e.message}
|
|
16178
16456
|
`);
|
|
16179
16457
|
process.exit(2);
|
|
16180
16458
|
}
|
|
16181
16459
|
throw e;
|
|
16182
16460
|
}
|
|
16183
|
-
|
|
16184
|
-
`);
|
|
16185
|
-
for (const o of personaResult.outcomes) process.stdout.write(`${describeSyncOutcome(o)}
|
|
16186
|
-
`);
|
|
16187
|
-
for (const o of personaResult.crewLinks) process.stdout.write(`${describeCrewLink(o)}
|
|
16188
|
-
`);
|
|
16189
|
-
if (personaResult.releaseNote) process.stdout.write(`${personaResult.releaseNote}
|
|
16190
|
-
`);
|
|
16191
|
-
const briefs = Object.fromEntries(ROLE_NAMES.map((r) => [r, personaDefaultPrompt(target.promptsDir, r)]));
|
|
16192
|
-
for (const w of skillMarkerWarnings(target.promptsDir, briefs)) process.stderr.write(`crew agents sync: warning: ${w}
|
|
16193
|
-
`);
|
|
16194
|
-
let skillResult;
|
|
16195
|
-
if (!readOnly) {
|
|
16196
|
-
try {
|
|
16197
|
-
skillResult = await syncSkills(target, { userAgent: cfg.ship.userAgent, dryRun, force });
|
|
16198
|
-
} catch (e) {
|
|
16199
|
-
if (isForbidden(e)) readOnly = true;
|
|
16200
|
-
else if (e instanceof SkillSyncError) {
|
|
16201
|
-
process.stderr.write(`crew agents sync: ${e.message}
|
|
16202
|
-
`);
|
|
16203
|
-
process.exit(2);
|
|
16204
|
-
} else throw e;
|
|
16205
|
-
}
|
|
16206
|
-
}
|
|
16207
|
-
for (const o of skillResult?.outcomes ?? []) process.stdout.write(`${describeSkillSyncOutcome(o)}
|
|
16208
|
-
`);
|
|
16209
|
-
if (!dryRun) {
|
|
16210
|
-
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
16211
|
-
const raw = existsSync28(resolvedPath) ? JSON.parse(readFileSync21(resolvedPath, "utf8")) : {};
|
|
16212
|
-
raw.agentPersonas = personaResult.agentPersonas;
|
|
16213
|
-
if (skillResult) raw.agentSkills = skillResult.agentSkills;
|
|
16214
|
-
if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
|
|
16215
|
-
mkdirSync14(dirOf(resolvedPath), { recursive: true });
|
|
16216
|
-
writeFileSync15(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
16217
|
-
`);
|
|
16218
|
-
}
|
|
16219
|
-
if (!dryRun) {
|
|
16220
|
-
target.resolved.agentPersonas = personaResult.agentPersonas;
|
|
16221
|
-
if (personaResult.releaseAgentId) target.resolved.releaseAgentId = personaResult.releaseAgentId;
|
|
16222
|
-
}
|
|
16223
|
-
if (readOnly) {
|
|
16224
|
-
process.stdout.write("personas and skills: not synced (this key is not a workspace admin; the workspace's rows are used as they are)\n");
|
|
16225
|
-
}
|
|
16461
|
+
const { personaResult, skillResult } = run2;
|
|
16226
16462
|
if (flag("revoke-seat-keys")) {
|
|
16227
16463
|
for (const o of await revokeSeatKeys(target, { userAgent: cfg.ship.userAgent, dryRun })) {
|
|
16228
16464
|
process.stdout.write(`${describeRevokeOutcome(o)}
|
|
@@ -16234,9 +16470,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16234
16470
|
`);
|
|
16235
16471
|
}
|
|
16236
16472
|
}
|
|
16237
|
-
if (
|
|
16238
|
-
process.exitCode = 1;
|
|
16239
|
-
}
|
|
16473
|
+
if (run2.diverged) process.exitCode = 1;
|
|
16240
16474
|
break;
|
|
16241
16475
|
}
|
|
16242
16476
|
case "skills": {
|
|
@@ -16278,10 +16512,10 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16278
16512
|
`);
|
|
16279
16513
|
if (!dryRun) {
|
|
16280
16514
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
16281
|
-
const raw =
|
|
16515
|
+
const raw = existsSync29(resolvedPath) ? JSON.parse(readFileSync22(resolvedPath, "utf8")) : {};
|
|
16282
16516
|
raw.agentSkills = result.agentSkills;
|
|
16283
|
-
|
|
16284
|
-
|
|
16517
|
+
mkdirSync15(dirOf(resolvedPath), { recursive: true });
|
|
16518
|
+
writeFileSync16(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
16285
16519
|
`);
|
|
16286
16520
|
}
|
|
16287
16521
|
if (result.outcomes.some((o) => o.action === "diverged")) process.exitCode = 1;
|
|
@@ -16290,18 +16524,74 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16290
16524
|
case "repos": {
|
|
16291
16525
|
const sub = positional[1];
|
|
16292
16526
|
const usage2 = () => {
|
|
16293
|
-
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");
|
|
16527
|
+
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");
|
|
16294
16528
|
process.exit(2);
|
|
16295
16529
|
};
|
|
16296
|
-
if (sub !== "add" && sub !== "list" && sub !== "clone") usage2();
|
|
16297
|
-
const configText =
|
|
16530
|
+
if (sub !== "add" && sub !== "list" && sub !== "clone" && sub !== "hooks") usage2();
|
|
16531
|
+
const configText = readFileSync22(cfg.configFile, "utf8");
|
|
16532
|
+
if (sub === "hooks") {
|
|
16533
|
+
try {
|
|
16534
|
+
const a = parseHooksArgs(positional.slice(2), routeNamesOf(configText));
|
|
16535
|
+
const dir = repoDirOf(configText, dirname16(cfg.configFile), a.route, a.name);
|
|
16536
|
+
if (!existsSync29(dir)) throw new ReposError(`${dir} does not exist on disk`);
|
|
16537
|
+
const files = { exists: (r) => existsSync29(resolvePath(dir, r)), read: (r) => {
|
|
16538
|
+
try {
|
|
16539
|
+
return readFileSync22(resolvePath(dir, r), "utf8");
|
|
16540
|
+
} catch {
|
|
16541
|
+
return void 0;
|
|
16542
|
+
}
|
|
16543
|
+
} };
|
|
16544
|
+
const suggestion = suggestHooks(files);
|
|
16545
|
+
const write = value("write");
|
|
16546
|
+
if (write !== void 0 && write !== "route" && write !== "repo") throw new ReposError(`--write takes "route" or "repo" (got "${write}")`);
|
|
16547
|
+
let written;
|
|
16548
|
+
if (write && suggestion.source === null) throw new ReposError(`nothing to write: ${suggestion.notes[0]}`);
|
|
16549
|
+
if (write === "route") {
|
|
16550
|
+
const plan = planRouteHooks(configText, a.route, a.name, suggestion.hooks);
|
|
16551
|
+
writeFileSync16(cfg.configFile, plan.text);
|
|
16552
|
+
written = { target: "route", file: cfg.configFile, hooks: plan.written, kept: plan.kept };
|
|
16553
|
+
} else if (write === "repo") {
|
|
16554
|
+
const have = existingRepoConfig(files);
|
|
16555
|
+
if (have) throw new ReposError(`${resolvePath(dir, have)} already exists; not overwriting it`);
|
|
16556
|
+
const file = resolvePath(dir, ".crew.yaml");
|
|
16557
|
+
writeFileSync16(file, starterRepoConfig(suggestion.hooks), { flag: "wx" });
|
|
16558
|
+
written = { target: "repo", file, hooks: Object.keys(suggestion.hooks), kept: [] };
|
|
16559
|
+
}
|
|
16560
|
+
if (flag("json")) {
|
|
16561
|
+
process.stdout.write(`${JSON.stringify({ route: a.route, repo: a.name, dir, ...suggestion, written: written ?? null })}
|
|
16562
|
+
`);
|
|
16563
|
+
} else {
|
|
16564
|
+
if (suggestion.source) process.stdout.write(`${a.name}: proposed from ${suggestion.source}
|
|
16565
|
+
`);
|
|
16566
|
+
for (const [k, v] of Object.entries(suggestion.hooks)) process.stdout.write(` ${k}: ${v}
|
|
16567
|
+
`);
|
|
16568
|
+
for (const n of suggestion.notes) process.stdout.write(`note: ${n}
|
|
16569
|
+
`);
|
|
16570
|
+
if (written) {
|
|
16571
|
+
process.stdout.write(`wrote ${written.hooks.join(", ") || "nothing new"} to ${written.file}
|
|
16572
|
+
`);
|
|
16573
|
+
if (written.kept.length) process.stdout.write(`kept existing route hooks: ${written.kept.join(", ")}
|
|
16574
|
+
`);
|
|
16575
|
+
if (written.target === "repo") process.stdout.write("review it and commit it to the repository\n");
|
|
16576
|
+
}
|
|
16577
|
+
}
|
|
16578
|
+
} catch (e) {
|
|
16579
|
+
if (e instanceof ReposError) {
|
|
16580
|
+
process.stderr.write(`crew repos: ${e.message}
|
|
16581
|
+
`);
|
|
16582
|
+
process.exit(2);
|
|
16583
|
+
}
|
|
16584
|
+
throw e;
|
|
16585
|
+
}
|
|
16586
|
+
break;
|
|
16587
|
+
}
|
|
16298
16588
|
let routeName = positional[2];
|
|
16299
16589
|
let addPath = ".";
|
|
16300
16590
|
let addName;
|
|
16301
16591
|
if (sub === "list" && !routeName) usage2();
|
|
16302
16592
|
if (sub === "add") {
|
|
16303
16593
|
try {
|
|
16304
|
-
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) =>
|
|
16594
|
+
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync29(resolvePath(p.replace(/^~(?=\/|$)/, homedir8()))));
|
|
16305
16595
|
routeName = a.route;
|
|
16306
16596
|
addPath = a.path;
|
|
16307
16597
|
addName = a.name;
|
|
@@ -16330,15 +16620,15 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16330
16620
|
}
|
|
16331
16621
|
const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
|
|
16332
16622
|
let resolvedRemotes;
|
|
16333
|
-
if (
|
|
16623
|
+
if (existsSync29(resolvedFile)) {
|
|
16334
16624
|
try {
|
|
16335
|
-
resolvedRemotes = JSON.parse(
|
|
16625
|
+
resolvedRemotes = JSON.parse(readFileSync22(resolvedFile, "utf8"));
|
|
16336
16626
|
} catch {
|
|
16337
16627
|
}
|
|
16338
16628
|
}
|
|
16339
16629
|
const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
|
|
16340
16630
|
const text = configText;
|
|
16341
|
-
const base =
|
|
16631
|
+
const base = dirname16(cfg.configFile);
|
|
16342
16632
|
try {
|
|
16343
16633
|
if (sub === "list") {
|
|
16344
16634
|
const repos = configuredRepos(text, routeName, base);
|
|
@@ -16347,7 +16637,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16347
16637
|
for (const r of repos) {
|
|
16348
16638
|
const origin2 = originOf(r.dir);
|
|
16349
16639
|
const row = matchRepoName(origin2, resolvedRemotes);
|
|
16350
|
-
const note = !
|
|
16640
|
+
const note = !existsSync29(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
|
|
16351
16641
|
process.stdout.write(`${r.name} ${r.dir} ${note}
|
|
16352
16642
|
`);
|
|
16353
16643
|
}
|
|
@@ -16359,9 +16649,9 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16359
16649
|
` : `${human}
|
|
16360
16650
|
`);
|
|
16361
16651
|
const plan2 = planRepoClone({ text, base, route: routeName, name: addName, resolved: resolvedRemotes, dir: value("dir") });
|
|
16362
|
-
if (
|
|
16652
|
+
if (existsSync29(plan2.target) && (existsSync29(`${plan2.target}/.git`) || readdirSync11(plan2.target).length > 0)) {
|
|
16363
16653
|
throw new ReposError(
|
|
16364
|
-
|
|
16654
|
+
existsSync29(`${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`
|
|
16365
16655
|
);
|
|
16366
16656
|
}
|
|
16367
16657
|
if (dryRun) {
|
|
@@ -16379,12 +16669,12 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16379
16669
|
throw new ReposError(`${e.message}${authHint}
|
|
16380
16670
|
crew.yaml was not changed`);
|
|
16381
16671
|
}
|
|
16382
|
-
|
|
16672
|
+
writeFileSync16(cfg.configFile, plan2.text);
|
|
16383
16673
|
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}`);
|
|
16384
16674
|
break;
|
|
16385
16675
|
}
|
|
16386
16676
|
const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir8()));
|
|
16387
|
-
const top =
|
|
16677
|
+
const top = existsSync29(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
|
|
16388
16678
|
if (!top) throw new ReposError(`${target} is not a git checkout`);
|
|
16389
16679
|
const origin = originOf(top);
|
|
16390
16680
|
const plan = planRepoAdd({
|
|
@@ -16433,15 +16723,15 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16433
16723
|
const created = await rowTracker.createRepoRow(rowPayload);
|
|
16434
16724
|
process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
|
|
16435
16725
|
`);
|
|
16436
|
-
if (
|
|
16437
|
-
const raw = JSON.parse(
|
|
16726
|
+
if (existsSync29(resolvedFile)) {
|
|
16727
|
+
const raw = JSON.parse(readFileSync22(resolvedFile, "utf8"));
|
|
16438
16728
|
raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
|
|
16439
16729
|
raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
|
|
16440
|
-
|
|
16730
|
+
writeFileSync16(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
16441
16731
|
`);
|
|
16442
16732
|
}
|
|
16443
16733
|
}
|
|
16444
|
-
|
|
16734
|
+
writeFileSync16(cfg.configFile, plan.text);
|
|
16445
16735
|
process.stdout.write(`added ${top} to ${routeName} as ${plan.mode === "dir" ? "dir" : `repos.${plan.name}`} in ${cfg.configFile}
|
|
16446
16736
|
`);
|
|
16447
16737
|
} catch (e) {
|
|
@@ -16751,10 +17041,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
16751
17041
|
}
|
|
16752
17042
|
}
|
|
16753
17043
|
let previous;
|
|
16754
|
-
const hadPreviousFile =
|
|
17044
|
+
const hadPreviousFile = existsSync29(resolvedPath);
|
|
16755
17045
|
if (hadPreviousFile) {
|
|
16756
17046
|
try {
|
|
16757
|
-
previous = JSON.parse(
|
|
17047
|
+
previous = JSON.parse(readFileSync22(resolvedPath, "utf8"));
|
|
16758
17048
|
} catch {
|
|
16759
17049
|
}
|
|
16760
17050
|
}
|
|
@@ -16862,10 +17152,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
16862
17152
|
process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
|
|
16863
17153
|
`);
|
|
16864
17154
|
} else {
|
|
16865
|
-
|
|
17155
|
+
mkdirSync15(dirname16(resolvedPath), { recursive: true });
|
|
16866
17156
|
if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
|
|
16867
17157
|
const tmpPath = `${resolvedPath}.tmp${process.pid}`;
|
|
16868
|
-
|
|
17158
|
+
writeFileSync16(tmpPath, `${JSON.stringify(resolved, null, 2)}
|
|
16869
17159
|
`);
|
|
16870
17160
|
renameSync2(tmpPath, resolvedPath);
|
|
16871
17161
|
process.stderr.write(`Resolved ids written to ${resolvedPath}
|
|
@@ -16900,8 +17190,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
16900
17190
|
process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
|
|
16901
17191
|
`);
|
|
16902
17192
|
} else {
|
|
16903
|
-
|
|
16904
|
-
|
|
17193
|
+
mkdirSync15(dirname16(apiKeyFile), { recursive: true });
|
|
17194
|
+
writeFileSync16(apiKeyFile, `${apiKeyVar}=${apiKey}
|
|
16905
17195
|
`, { mode: 384 });
|
|
16906
17196
|
process.stderr.write(`API key written to ${apiKeyFile}
|
|
16907
17197
|
`);
|
|
@@ -17002,6 +17292,56 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
17002
17292
|
`
|
|
17003
17293
|
);
|
|
17004
17294
|
}
|
|
17295
|
+
if (!dryRun && !flag("no-agents-sync")) {
|
|
17296
|
+
const skip = (why) => {
|
|
17297
|
+
process.stderr.write(`
|
|
17298
|
+
agents sync: skipped (${why})
|
|
17299
|
+
`);
|
|
17300
|
+
connectEvents.emit("agents_synced", `agents sync skipped: ${why}`, { route: route2, skipped: why });
|
|
17301
|
+
};
|
|
17302
|
+
let syncRoute;
|
|
17303
|
+
try {
|
|
17304
|
+
const reloaded = loadConfig(CREW_HOME, wizard?.configPath ?? cfg.configFile, { allowNoRoutes: true });
|
|
17305
|
+
syncRoute = reloaded.routes.find((r) => r.route === route2);
|
|
17306
|
+
if (syncRoute) await hydrateApiKeys([syncRoute]);
|
|
17307
|
+
} catch (e) {
|
|
17308
|
+
skip(`config did not load: ${e.message}`);
|
|
17309
|
+
}
|
|
17310
|
+
if (syncRoute?.resolved) {
|
|
17311
|
+
try {
|
|
17312
|
+
process.stderr.write("\n");
|
|
17313
|
+
const run2 = await runAgentsSync(syncRoute, {
|
|
17314
|
+
userAgent: cfg.ship.userAgent,
|
|
17315
|
+
stateDir: cfg.ship.stateDir,
|
|
17316
|
+
say: (l) => process.stderr.write(` ${l}
|
|
17317
|
+
`),
|
|
17318
|
+
warn: (l) => process.stderr.write(` agents sync: ${l}
|
|
17319
|
+
`)
|
|
17320
|
+
});
|
|
17321
|
+
const minted = await mintMissingSeatKeys(syncRoute, { userAgent: cfg.ship.userAgent, shipName: cfg.ship.name });
|
|
17322
|
+
for (const m of minted) process.stderr.write(` ${describeMintOutcome(m)}
|
|
17323
|
+
`);
|
|
17324
|
+
if (run2.readOnly) {
|
|
17325
|
+
const missing = run2.personaResult.outcomes.some((p) => p.action === "missing");
|
|
17326
|
+
process.stderr.write(missing ? " A workspace admin has to open the Crew app or run `crew agents sync` once before this ship's seats can run.\n" : " Seats are linked to the workspace's existing personas.\n");
|
|
17327
|
+
}
|
|
17328
|
+
connectEvents.emit("agents_synced", run2.readOnly ? "agents linked (read-only key)" : "agents synced", {
|
|
17329
|
+
route: route2,
|
|
17330
|
+
admin: !run2.readOnly,
|
|
17331
|
+
personas: run2.personaResult.outcomes,
|
|
17332
|
+
skills: run2.skillResult?.outcomes ?? [],
|
|
17333
|
+
diverged: run2.diverged
|
|
17334
|
+
});
|
|
17335
|
+
for (const m of minted) connectEvents.emit("seat_key", describeMintOutcome(m), { route: route2, ...m });
|
|
17336
|
+
if (run2.diverged) process.exitCode = 1;
|
|
17337
|
+
} catch (e) {
|
|
17338
|
+
if (e instanceof AgentsSyncError || e instanceof SkillSyncError) skip(e.message);
|
|
17339
|
+
else throw e;
|
|
17340
|
+
}
|
|
17341
|
+
} else if (!syncRoute) {
|
|
17342
|
+
skip(`"${route2}" is not in crew.yaml yet; add the block above, then run \`crew agents sync\``);
|
|
17343
|
+
}
|
|
17344
|
+
}
|
|
17005
17345
|
connectEvents.emit("done", `connected ${route2}`, {
|
|
17006
17346
|
route: route2,
|
|
17007
17347
|
resolvedPath: dryRun ? void 0 : resolvedPath,
|
|
@@ -17480,8 +17820,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
17480
17820
|
process.stdout.write("crew reload: no daemon unit is running \u2014 nothing to reload (the next run reads crew.yaml fresh)\n");
|
|
17481
17821
|
break;
|
|
17482
17822
|
}
|
|
17483
|
-
|
|
17484
|
-
|
|
17823
|
+
mkdirSync15(cfg.ship.stateDir, { recursive: true });
|
|
17824
|
+
writeFileSync16(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
|
|
17485
17825
|
`);
|
|
17486
17826
|
process.stdout.write("crew reload: requested \u2014 the daemon finishes its in-flight pass, then relaunches with the new config\n");
|
|
17487
17827
|
if (flag("now") && control?.stopCommand && control.startCommand) {
|
|
@@ -17532,7 +17872,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
17532
17872
|
}
|
|
17533
17873
|
case "log": {
|
|
17534
17874
|
try {
|
|
17535
|
-
const text =
|
|
17875
|
+
const text = readFileSync22(cfg.ship.logFile, "utf8").split("\n");
|
|
17536
17876
|
process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
|
|
17537
17877
|
`);
|
|
17538
17878
|
} catch {
|
|
@@ -17555,8 +17895,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
17555
17895
|
};
|
|
17556
17896
|
if (!route.resolved) fail(`route ${route.route} is not connected \u2014 run \`crew connect\` first`);
|
|
17557
17897
|
const tracker = new Tracker(route, cfg.ship);
|
|
17558
|
-
const configText =
|
|
17559
|
-
const cfgBase =
|
|
17898
|
+
const configText = readFileSync22(cfg.configFile, "utf8");
|
|
17899
|
+
const cfgBase = dirname16(cfg.configFile);
|
|
17560
17900
|
const configured = configuredRepos(configText, route.route, cfgBase);
|
|
17561
17901
|
const dir = configured.find((r) => r.name === SAMPLE_REPO)?.dir ?? sampleDir(configured.map((r) => r.dir), route.reposBasePath, route.route.split("/")[0]);
|
|
17562
17902
|
const resolvedFile = resolvedPathFor(cfg.ship.stateDir, route.route);
|
|
@@ -17584,7 +17924,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
17584
17924
|
}
|
|
17585
17925
|
if (configured.some((r) => r.dir === dir)) {
|
|
17586
17926
|
try {
|
|
17587
|
-
|
|
17927
|
+
writeFileSync16(cfg.configFile, planRepoRemove(configText, cfgBase, route.route, dir).text);
|
|
17588
17928
|
} catch (e) {
|
|
17589
17929
|
if (e instanceof ReposError) fail(e.message);
|
|
17590
17930
|
throw e;
|
|
@@ -17592,29 +17932,29 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
17592
17932
|
}
|
|
17593
17933
|
if (row) {
|
|
17594
17934
|
await tracker.deleteRepoRow(row.id);
|
|
17595
|
-
if (
|
|
17596
|
-
const raw = JSON.parse(
|
|
17935
|
+
if (existsSync29(resolvedFile)) {
|
|
17936
|
+
const raw = JSON.parse(readFileSync22(resolvedFile, "utf8"));
|
|
17597
17937
|
if (raw.repoNames) delete raw.repoNames[row.id];
|
|
17598
17938
|
if (raw.repoRemotes) delete raw.repoRemotes[row.id];
|
|
17599
|
-
|
|
17939
|
+
writeFileSync16(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
17600
17940
|
`);
|
|
17601
17941
|
}
|
|
17602
17942
|
}
|
|
17603
|
-
if (
|
|
17943
|
+
if (existsSync29(join29(dir, ".git")) && basename10(dir) === SAMPLE_REPO) rmSync11(dir, { recursive: true, force: true });
|
|
17604
17944
|
say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
|
|
17605
17945
|
break;
|
|
17606
17946
|
}
|
|
17607
17947
|
const projectId = route.resolved.areaId ?? resolveRepoProject(await tracker.projectRows(), value("project"));
|
|
17608
|
-
const fresh = !
|
|
17609
|
-
if (
|
|
17948
|
+
const fresh = !existsSync29(join29(dir, ".git"));
|
|
17949
|
+
if (existsSync29(dir) && fresh && readdirSync11(dir).length > 0) fail(`${dir} exists and is not a git checkout \u2014 move it aside`);
|
|
17610
17950
|
if (dryRun) {
|
|
17611
17951
|
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`);
|
|
17612
17952
|
break;
|
|
17613
17953
|
}
|
|
17614
17954
|
if (fresh) {
|
|
17615
|
-
|
|
17616
|
-
|
|
17617
|
-
|
|
17955
|
+
mkdirSync15(dir, { recursive: true });
|
|
17956
|
+
writeFileSync16(join29(dir, "README.md"), SAMPLE_README);
|
|
17957
|
+
writeFileSync16(join29(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
|
|
17618
17958
|
const g = (...a) => {
|
|
17619
17959
|
const r = spawnSync6("git", a, { cwd: dir, encoding: "utf8" });
|
|
17620
17960
|
if (r.status !== 0) fail(`git ${a[0]} failed in ${dir}: ${(r.stderr || r.stdout).trim()}`);
|
|
@@ -17632,17 +17972,17 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
17632
17972
|
spec: { platform: "unix", releaseMode: "integrate", ciProvider: "none" }
|
|
17633
17973
|
});
|
|
17634
17974
|
repoId = (await tracker.createRepoRow(payload)).id;
|
|
17635
|
-
if (
|
|
17636
|
-
const raw = JSON.parse(
|
|
17975
|
+
if (existsSync29(resolvedFile)) {
|
|
17976
|
+
const raw = JSON.parse(readFileSync22(resolvedFile, "utf8"));
|
|
17637
17977
|
raw.repoNames = { ...raw.repoNames, [repoId]: SAMPLE_REPO };
|
|
17638
17978
|
raw.repoRemotes = { ...raw.repoRemotes, [repoId]: SAMPLE_REMOTE };
|
|
17639
|
-
|
|
17979
|
+
writeFileSync16(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
17640
17980
|
`);
|
|
17641
17981
|
}
|
|
17642
17982
|
}
|
|
17643
17983
|
if (!configured.some((r) => r.dir === dir)) {
|
|
17644
17984
|
try {
|
|
17645
|
-
|
|
17985
|
+
writeFileSync16(cfg.configFile, planRepoAdd({ text: configText, base: cfgBase, route: route.route, path: dir, name: SAMPLE_REPO }).text);
|
|
17646
17986
|
} catch (e) {
|
|
17647
17987
|
if (e instanceof ReposError) fail(e.message);
|
|
17648
17988
|
throw e;
|
|
@@ -17919,6 +18259,40 @@ watermark: ${state.watermark()}
|
|
|
17919
18259
|
}
|
|
17920
18260
|
case "doctor": {
|
|
17921
18261
|
const host = hostPlatform();
|
|
18262
|
+
const doctorSchedulerPath = pathFor(cfg.ship);
|
|
18263
|
+
const pushAuth = [];
|
|
18264
|
+
const toolRows = [];
|
|
18265
|
+
{
|
|
18266
|
+
const prBased = /* @__PURE__ */ new Set();
|
|
18267
|
+
let mentionNode = false;
|
|
18268
|
+
for (const c of [route]) {
|
|
18269
|
+
const { settled: repos, errors } = await settleRepos(reposOf(c), (t) => t.name, (t) => resolveOneRepo(c, t));
|
|
18270
|
+
for (const e of errors) {
|
|
18271
|
+
pushAuth.push({ route: c.route, repo: e.repo, result: { state: "failed", detail: `could not resolve this repo's configuration: ${e.detail}`, hint: "fix the repo's .crew.yaml" } });
|
|
18272
|
+
}
|
|
18273
|
+
for (const r of repos) {
|
|
18274
|
+
const remote = r.config.branch.remote;
|
|
18275
|
+
if (existsSync29(r.dir)) {
|
|
18276
|
+
pushAuth.push({
|
|
18277
|
+
route: c.route,
|
|
18278
|
+
repo: r.name,
|
|
18279
|
+
result: probePushAuth({ dir: r.dir, remote, remoteUrl: originUrlOf(r.dir, remote), offline: flag("offline") })
|
|
18280
|
+
});
|
|
18281
|
+
}
|
|
18282
|
+
if (isPrBased(r.config.hooks)) prBased.add(r.name);
|
|
18283
|
+
if (Object.values(r.config.hooks).some((h) => typeof h === "string" && mentionsNode(h))) mentionNode = true;
|
|
18284
|
+
}
|
|
18285
|
+
}
|
|
18286
|
+
toolRows.push(...toolChecks(
|
|
18287
|
+
{ prBased: prBased.size > 0, hostPassengers: cfg.routes.some((x) => x.enabled && x.hostPassengers), hooksMentionNode: mentionNode },
|
|
18288
|
+
(bin) => findOnPath(bin, doctorSchedulerPath)
|
|
18289
|
+
));
|
|
18290
|
+
}
|
|
18291
|
+
if (flag("json")) {
|
|
18292
|
+
process.stdout.write(`${JSON.stringify({ offline: flag("offline"), pushAuth, tools: toolRows }, null, 2)}
|
|
18293
|
+
`);
|
|
18294
|
+
break;
|
|
18295
|
+
}
|
|
17922
18296
|
process.stdout.write(
|
|
17923
18297
|
`ship: ${cfg.ship.name} (${host})
|
|
17924
18298
|
routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
@@ -17926,7 +18300,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
17926
18300
|
);
|
|
17927
18301
|
try {
|
|
17928
18302
|
const evFile = eventFileFor(cfg.ship.stateDir);
|
|
17929
|
-
const evSize =
|
|
18303
|
+
const evSize = existsSync29(evFile) ? statSync9(evFile).size : 0;
|
|
17930
18304
|
process.stdout.write(
|
|
17931
18305
|
evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
|
|
17932
18306
|
` : "events log: ok\n"
|
|
@@ -17937,7 +18311,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
17937
18311
|
const unitPlan = planInstall(cfg.ship, CREW_HOME, host, detectSystemd(), "run");
|
|
17938
18312
|
const abandons = unitAbandonsAgents(unitPlan, (p) => {
|
|
17939
18313
|
try {
|
|
17940
|
-
return
|
|
18314
|
+
return readFileSync22(p, "utf8");
|
|
17941
18315
|
} catch {
|
|
17942
18316
|
return null;
|
|
17943
18317
|
}
|
|
@@ -18142,6 +18516,8 @@ crew manifest: ${seats2.length ? seats2.map((c) => c.name).join(", ") : "non
|
|
|
18142
18516
|
if (ov.toolchain) process.stdout.write(`repo toolchain: ${repoName} \u2192 ${ov.toolchain} (binary and MCP checked above, as for every ship toolchain)
|
|
18143
18517
|
`);
|
|
18144
18518
|
}
|
|
18519
|
+
for (const p of pushAuth) process.stdout.write(formatPushAuth(`${p.repo}`, p.result));
|
|
18520
|
+
for (const t of toolRows) process.stdout.write(formatToolCheck(t));
|
|
18145
18521
|
process.stdout.write(`git: ${gitPath ? `OK (${gitPath})` : "NOT FOUND on the scheduler PATH"}
|
|
18146
18522
|
`);
|
|
18147
18523
|
if (!allBinsFound || !gitPath) {
|