@tablation/crew 0.58.0 → 0.59.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 +5 -1
- package/dist/cli.js +343 -165
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -71,7 +71,11 @@ demand. `crew agents sync [route]` pushes both personas and skills in one
|
|
|
71
71
|
run — "agents" here means every agent-shaped resource crew owns, not just
|
|
72
72
|
the Agents table; `crew skills sync [route]` is the same create/update/
|
|
73
73
|
diverged sync narrowed to skills alone, for when you've only touched
|
|
74
|
-
`skills/`.
|
|
74
|
+
`skills/`. `crew connect` runs this same sync (then the seat key mint) at the
|
|
75
|
+
end for an admin key, so a fresh workspace needs no second command; a member
|
|
76
|
+
key links seats to the existing rows and is told an admin has to run it once.
|
|
77
|
+
`--no-agents-sync` skips it, and `--json` adds `agents_synced` and `seat_key`
|
|
78
|
+
events on stdout. Example use — a Pair session invoked via an Epic's
|
|
75
79
|
`grill_link` looks up the `Grill-Me` skill through the Tablation MCP tool
|
|
76
80
|
`agent_skills_controller_get_skill` and follows it. `prompts/default/skills/
|
|
77
81
|
grill-me.md` is one shipped example; the rest are the building lanes'
|
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";
|
|
@@ -6078,7 +6156,7 @@ function recoveredFailureHeadlines(o, route) {
|
|
|
6078
6156
|
import { existsSync as existsSync15, statfsSync } from "fs";
|
|
6079
6157
|
|
|
6080
6158
|
// src/git.ts
|
|
6081
|
-
import { execFileSync } from "child_process";
|
|
6159
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
6082
6160
|
import {
|
|
6083
6161
|
existsSync as existsSync14,
|
|
6084
6162
|
mkdirSync as mkdirSync7,
|
|
@@ -6100,7 +6178,7 @@ function streamText(x) {
|
|
|
6100
6178
|
}
|
|
6101
6179
|
function gitRaw(cwd, args) {
|
|
6102
6180
|
try {
|
|
6103
|
-
return
|
|
6181
|
+
return execFileSync2("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
6104
6182
|
} catch (e) {
|
|
6105
6183
|
const err = e;
|
|
6106
6184
|
const why = [err.stderr, err.stdout].map(streamText).filter(Boolean).join(" / ");
|
|
@@ -6109,7 +6187,7 @@ function gitRaw(cwd, args) {
|
|
|
6109
6187
|
}
|
|
6110
6188
|
function git(cwd, args) {
|
|
6111
6189
|
try {
|
|
6112
|
-
return
|
|
6190
|
+
return execFileSync2("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
6113
6191
|
} catch (e) {
|
|
6114
6192
|
const err = e;
|
|
6115
6193
|
const why = [err.stderr, err.stdout].map(streamText).filter(Boolean).join(" / ");
|
|
@@ -6266,9 +6344,9 @@ function keyCommitsInRange(cwd, key, range) {
|
|
|
6266
6344
|
}
|
|
6267
6345
|
function patchIdOf(cwd, from, to) {
|
|
6268
6346
|
try {
|
|
6269
|
-
const diff =
|
|
6347
|
+
const diff = execFileSync2("git", ["diff", from, to], { cwd, encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
|
|
6270
6348
|
if (!diff) return null;
|
|
6271
|
-
const out =
|
|
6349
|
+
const out = execFileSync2("git", ["patch-id", "--stable"], { cwd, encoding: "utf8", input: diff });
|
|
6272
6350
|
return out.split(" ")[0]?.trim() || null;
|
|
6273
6351
|
} catch {
|
|
6274
6352
|
return null;
|
|
@@ -6532,7 +6610,7 @@ function ensureRepoCheckout(dir, remote, opts = {}) {
|
|
|
6532
6610
|
mkdirSync7(dirname6(dir), { recursive: true });
|
|
6533
6611
|
const url = cloneUrlFor(remote, opts);
|
|
6534
6612
|
try {
|
|
6535
|
-
|
|
6613
|
+
execFileSync2("git", ["clone", url, dir], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
6536
6614
|
} catch (e) {
|
|
6537
6615
|
const err = e;
|
|
6538
6616
|
const why = [err.stderr, err.stdout].map(streamText).filter(Boolean).join(" / ");
|
|
@@ -9243,12 +9321,12 @@ import { existsSync as existsSync17 } from "fs";
|
|
|
9243
9321
|
import { dirname as dirname7, join as join17 } from "path";
|
|
9244
9322
|
|
|
9245
9323
|
// src/ports.ts
|
|
9246
|
-
import { execFileSync as
|
|
9324
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
9247
9325
|
import { existsSync as existsSync16 } from "fs";
|
|
9248
9326
|
import { join as join16 } from "path";
|
|
9249
9327
|
var run = (cmd, args) => {
|
|
9250
9328
|
try {
|
|
9251
|
-
return
|
|
9329
|
+
return execFileSync3(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
9252
9330
|
} catch {
|
|
9253
9331
|
return "";
|
|
9254
9332
|
}
|
|
@@ -12439,6 +12517,60 @@ async function canMintAgentKeys(route, o = {}) {
|
|
|
12439
12517
|
}
|
|
12440
12518
|
}
|
|
12441
12519
|
|
|
12520
|
+
// src/agents-sync-run.ts
|
|
12521
|
+
import { existsSync as existsSync21, mkdirSync as mkdirSync10, readFileSync as readFileSync15, writeFileSync as writeFileSync12 } from "fs";
|
|
12522
|
+
import { dirname as dirname11 } from "path";
|
|
12523
|
+
async function runAgentsSync(target, o) {
|
|
12524
|
+
const dryRun = o.dryRun ?? false;
|
|
12525
|
+
const force = o.force ?? false;
|
|
12526
|
+
const keyRole = target.resolved?.role ?? await fetchKeyRole(target, o.userAgent);
|
|
12527
|
+
let readOnly = !isAdminRole(keyRole);
|
|
12528
|
+
const runPersonas = (ro) => syncPersonas(target, { userAgent: o.userAgent, dryRun, force, readOnly: ro });
|
|
12529
|
+
let personaResult;
|
|
12530
|
+
try {
|
|
12531
|
+
personaResult = await runPersonas(readOnly);
|
|
12532
|
+
} catch (e) {
|
|
12533
|
+
if (readOnly || !isForbidden(e)) throw e;
|
|
12534
|
+
readOnly = true;
|
|
12535
|
+
personaResult = await runPersonas(true);
|
|
12536
|
+
}
|
|
12537
|
+
o.say(`route: ${target.route}`);
|
|
12538
|
+
for (const p of personaResult.outcomes) o.say(describeSyncOutcome(p));
|
|
12539
|
+
for (const l of personaResult.crewLinks) o.say(describeCrewLink(l));
|
|
12540
|
+
if (personaResult.releaseNote) o.say(personaResult.releaseNote);
|
|
12541
|
+
const briefs = Object.fromEntries(ROLE_NAMES.map((r) => [r, personaDefaultPrompt(target.promptsDir, r)]));
|
|
12542
|
+
for (const w of skillMarkerWarnings(target.promptsDir, briefs)) o.warn(`warning: ${w}`);
|
|
12543
|
+
let skillResult;
|
|
12544
|
+
if (!readOnly) {
|
|
12545
|
+
try {
|
|
12546
|
+
skillResult = await syncSkills(target, { userAgent: o.userAgent, dryRun, force });
|
|
12547
|
+
} catch (e) {
|
|
12548
|
+
if (isForbidden(e)) readOnly = true;
|
|
12549
|
+
else throw e;
|
|
12550
|
+
}
|
|
12551
|
+
}
|
|
12552
|
+
for (const s of skillResult?.outcomes ?? []) o.say(describeSkillSyncOutcome(s));
|
|
12553
|
+
if (!dryRun) {
|
|
12554
|
+
const resolvedPath = resolvedPathFor(o.stateDir, target.route);
|
|
12555
|
+
const raw = existsSync21(resolvedPath) ? JSON.parse(readFileSync15(resolvedPath, "utf8")) : {};
|
|
12556
|
+
raw.agentPersonas = personaResult.agentPersonas;
|
|
12557
|
+
if (skillResult) raw.agentSkills = skillResult.agentSkills;
|
|
12558
|
+
if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
|
|
12559
|
+
mkdirSync10(dirname11(resolvedPath), { recursive: true });
|
|
12560
|
+
writeFileSync12(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
12561
|
+
`);
|
|
12562
|
+
if (target.resolved) {
|
|
12563
|
+
target.resolved.agentPersonas = personaResult.agentPersonas;
|
|
12564
|
+
if (personaResult.releaseAgentId) target.resolved.releaseAgentId = personaResult.releaseAgentId;
|
|
12565
|
+
}
|
|
12566
|
+
}
|
|
12567
|
+
if (readOnly) {
|
|
12568
|
+
o.say("personas and skills: not synced (this key is not a workspace admin; the workspace's rows are used as they are)");
|
|
12569
|
+
}
|
|
12570
|
+
const diverged = personaResult.outcomes.some((p) => p.action === "diverged") || (skillResult?.outcomes.some((s) => s.action === "diverged") ?? false);
|
|
12571
|
+
return { personaResult, skillResult, readOnly, diverged };
|
|
12572
|
+
}
|
|
12573
|
+
|
|
12442
12574
|
// src/logbook.ts
|
|
12443
12575
|
import { createHash as createHash3 } from "crypto";
|
|
12444
12576
|
import { TablationClient as TablationClient4 } from "@tablation/client";
|
|
@@ -12528,7 +12660,7 @@ async function showLogEntry(route, entryId, opts = {}) {
|
|
|
12528
12660
|
}
|
|
12529
12661
|
|
|
12530
12662
|
// src/sample-ticket.ts
|
|
12531
|
-
import { basename as basename7, dirname as
|
|
12663
|
+
import { basename as basename7, dirname as dirname12, join as join20 } from "path";
|
|
12532
12664
|
var SAMPLE_REPO = "crew-sample";
|
|
12533
12665
|
var SAMPLE_REMOTE = `local/${SAMPLE_REPO}`;
|
|
12534
12666
|
var SAMPLE_MARKER = "sample: true";
|
|
@@ -12551,7 +12683,7 @@ has something safe to work on: its test and build hooks always pass, it has no
|
|
|
12551
12683
|
remote, and \`crew sample-ticket --remove\` deletes it.
|
|
12552
12684
|
`;
|
|
12553
12685
|
function sampleDir(existing, reposBase, workspace) {
|
|
12554
|
-
if (existing.length > 0) return join20(
|
|
12686
|
+
if (existing.length > 0) return join20(dirname12(existing[0]), SAMPLE_REPO);
|
|
12555
12687
|
return join20(reposBase, workspace, SAMPLE_REPO);
|
|
12556
12688
|
}
|
|
12557
12689
|
var SAMPLE_TITLE = "Sample: add a documented `greet` function with a test";
|
|
@@ -12838,8 +12970,8 @@ function schedulerInstalled(plan, exists, crontab) {
|
|
|
12838
12970
|
}
|
|
12839
12971
|
|
|
12840
12972
|
// src/worktree-sweep.ts
|
|
12841
|
-
import { existsSync as
|
|
12842
|
-
import { basename as basename9, dirname as
|
|
12973
|
+
import { existsSync as existsSync22, readFileSync as readFileSync16, readdirSync as readdirSync5 } from "fs";
|
|
12974
|
+
import { basename as basename9, dirname as dirname14, join as join22, resolve as resolve4 } from "path";
|
|
12843
12975
|
function planWorktreeSweep(target, tickets, contract) {
|
|
12844
12976
|
const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
|
|
12845
12977
|
const verified = contract.statuses.verified;
|
|
@@ -12893,7 +13025,7 @@ function worktreeTicketNumbers(target) {
|
|
|
12893
13025
|
return [...nums];
|
|
12894
13026
|
}
|
|
12895
13027
|
function siblingWorktreeDirs(target) {
|
|
12896
|
-
const parent =
|
|
13028
|
+
const parent = dirname14(target.dir.replace(/[/\\]+$/, ""));
|
|
12897
13029
|
const stem = `${basename9(target.dir.replace(/[/\\]+$/, ""))}-`;
|
|
12898
13030
|
const registered = new Set(worktrees(target.dir).map((w) => resolve4(w.path)));
|
|
12899
13031
|
let names;
|
|
@@ -12941,7 +13073,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
|
|
|
12941
13073
|
let removed = 0;
|
|
12942
13074
|
let keptBranches = 0;
|
|
12943
13075
|
for (const a of actions) {
|
|
12944
|
-
if (!dryRun && base && !a.keepBranch &&
|
|
13076
|
+
if (!dryRun && base && !a.keepBranch && existsSync22(a.path) && isHandBackMerge(a.path, base)) {
|
|
12945
13077
|
if (gitOk(a.path, ["merge", "--abort"]) !== null) {
|
|
12946
13078
|
log.emit(
|
|
12947
13079
|
`abandoned the hand-back merge of ${base} in ${a.path}: ${a.ticket.issue_id} deployed without it`,
|
|
@@ -12949,7 +13081,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
|
|
|
12949
13081
|
);
|
|
12950
13082
|
}
|
|
12951
13083
|
}
|
|
12952
|
-
if (!dryRun &&
|
|
13084
|
+
if (!dryRun && existsSync22(a.path)) {
|
|
12953
13085
|
let dirty = false;
|
|
12954
13086
|
try {
|
|
12955
13087
|
dirty = status(a.path).length > 0;
|
|
@@ -12960,7 +13092,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
|
|
|
12960
13092
|
continue;
|
|
12961
13093
|
}
|
|
12962
13094
|
}
|
|
12963
|
-
if (!dryRun && a.requirePushed && a.branch &&
|
|
13095
|
+
if (!dryRun && a.requirePushed && a.branch && existsSync22(a.path) && remoteConfigured(cwd, remote) && worktreeHoldsUnpushed(a.path, remote)) {
|
|
12964
13096
|
refuse(a, "unpushed commits", `${a.branch} has commits that are not on ${remote}`);
|
|
12965
13097
|
continue;
|
|
12966
13098
|
}
|
|
@@ -13009,7 +13141,7 @@ function isHandBackMerge(path, base) {
|
|
|
13009
13141
|
if (!msgPath) return false;
|
|
13010
13142
|
let first;
|
|
13011
13143
|
try {
|
|
13012
|
-
first =
|
|
13144
|
+
first = readFileSync16(resolve4(path, msgPath), "utf8").split("\n")[0] ?? "";
|
|
13013
13145
|
} catch {
|
|
13014
13146
|
return false;
|
|
13015
13147
|
}
|
|
@@ -13114,12 +13246,12 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
|
|
|
13114
13246
|
}
|
|
13115
13247
|
|
|
13116
13248
|
// src/stream-sweep.ts
|
|
13117
|
-
import { existsSync as
|
|
13249
|
+
import { existsSync as existsSync23, readdirSync as readdirSync6, rmSync as rmSync9, statSync as statSync7 } from "fs";
|
|
13118
13250
|
import { join as join23 } from "path";
|
|
13119
13251
|
var DAY_MS2 = 24 * 60 * 60 * 1e3;
|
|
13120
13252
|
function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
|
|
13121
13253
|
const dir = join23(stateDir, "streams");
|
|
13122
|
-
if (!
|
|
13254
|
+
if (!existsSync23(dir)) return [];
|
|
13123
13255
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
13124
13256
|
return readdirSync6(dir).map((name) => join23(dir, name)).filter((path) => {
|
|
13125
13257
|
try {
|
|
@@ -13147,7 +13279,7 @@ function applyStreamSweep(paths, dryRun, log) {
|
|
|
13147
13279
|
return removed;
|
|
13148
13280
|
}
|
|
13149
13281
|
function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
13150
|
-
if (!
|
|
13282
|
+
if (!existsSync23(stateDir)) return [];
|
|
13151
13283
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
13152
13284
|
return readdirSync6(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join23(stateDir, name)).filter((path) => {
|
|
13153
13285
|
try {
|
|
@@ -13159,14 +13291,14 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
|
13159
13291
|
}
|
|
13160
13292
|
|
|
13161
13293
|
// src/cli.ts
|
|
13162
|
-
import { readFileSync as
|
|
13294
|
+
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
13295
|
import { createInterface } from "readline/promises";
|
|
13164
13296
|
import { dirname as dirOf, resolve as resolvePath } from "path";
|
|
13165
13297
|
|
|
13166
13298
|
// src/ssh-keys.ts
|
|
13167
|
-
import { execFileSync as
|
|
13168
|
-
import { existsSync as
|
|
13169
|
-
import { dirname as
|
|
13299
|
+
import { execFileSync as execFileSync4 } from "child_process";
|
|
13300
|
+
import { existsSync as existsSync24, mkdirSync as mkdirSync11, readFileSync as readFileSync17, chmodSync } from "fs";
|
|
13301
|
+
import { dirname as dirname15, join as join24 } from "path";
|
|
13170
13302
|
var SshKeyError = class extends Error {
|
|
13171
13303
|
};
|
|
13172
13304
|
function shipSshKeyPathFor(stateDir) {
|
|
@@ -13175,10 +13307,10 @@ function shipSshKeyPathFor(stateDir) {
|
|
|
13175
13307
|
function ensureShipSshKeypair(stateDir) {
|
|
13176
13308
|
const privateKeyPath = shipSshKeyPathFor(stateDir);
|
|
13177
13309
|
const publicKeyPath = `${privateKeyPath}.pub`;
|
|
13178
|
-
if (!
|
|
13179
|
-
|
|
13310
|
+
if (!existsSync24(privateKeyPath)) {
|
|
13311
|
+
mkdirSync11(dirname15(privateKeyPath), { recursive: true });
|
|
13180
13312
|
try {
|
|
13181
|
-
|
|
13313
|
+
execFileSync4("ssh-keygen", [
|
|
13182
13314
|
"-t",
|
|
13183
13315
|
"ed25519",
|
|
13184
13316
|
"-N",
|
|
@@ -13196,15 +13328,15 @@ function ensureShipSshKeypair(stateDir) {
|
|
|
13196
13328
|
}
|
|
13197
13329
|
chmodSync(privateKeyPath, 384);
|
|
13198
13330
|
}
|
|
13199
|
-
if (!
|
|
13331
|
+
if (!existsSync24(publicKeyPath)) {
|
|
13200
13332
|
throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
|
|
13201
13333
|
}
|
|
13202
|
-
const publicKey =
|
|
13334
|
+
const publicKey = readFileSync17(publicKeyPath, "utf8").trim();
|
|
13203
13335
|
return { privateKeyPath, publicKeyPath, publicKey };
|
|
13204
13336
|
}
|
|
13205
13337
|
function sshKeygenAvailable() {
|
|
13206
13338
|
try {
|
|
13207
|
-
|
|
13339
|
+
execFileSync4("ssh-keygen", ["-V"], { stdio: ["ignore", "ignore", "ignore"] });
|
|
13208
13340
|
return true;
|
|
13209
13341
|
} catch (e) {
|
|
13210
13342
|
return e.code !== "ENOENT";
|
|
@@ -13212,11 +13344,11 @@ function sshKeygenAvailable() {
|
|
|
13212
13344
|
}
|
|
13213
13345
|
|
|
13214
13346
|
// src/passenger-containers.ts
|
|
13215
|
-
import { execFileSync as
|
|
13347
|
+
import { execFileSync as execFileSync5 } from "child_process";
|
|
13216
13348
|
import { createHash as createHash4 } from "crypto";
|
|
13217
13349
|
var realExec = (cmd, args) => {
|
|
13218
13350
|
try {
|
|
13219
|
-
return
|
|
13351
|
+
return execFileSync5(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
13220
13352
|
} catch (e) {
|
|
13221
13353
|
const err = e;
|
|
13222
13354
|
const why = [err.stderr, err.stdout].map((x) => (x ?? "").trim()).filter(Boolean).join(" / ");
|
|
@@ -13415,7 +13547,7 @@ function syncAllPassengerCheckouts(cfg) {
|
|
|
13415
13547
|
|
|
13416
13548
|
// src/tunnel.ts
|
|
13417
13549
|
import { spawn as spawn4 } from "child_process";
|
|
13418
|
-
import { existsSync as
|
|
13550
|
+
import { existsSync as existsSync25, mkdirSync as mkdirSync12, readFileSync as readFileSync18, writeFileSync as writeFileSync13, unlinkSync as unlinkSync3, readdirSync as readdirSync7 } from "fs";
|
|
13419
13551
|
import { randomInt } from "crypto";
|
|
13420
13552
|
import { join as join25 } from "path";
|
|
13421
13553
|
var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
|
|
@@ -13488,17 +13620,17 @@ function tunnelStatePath(stateDir, workspaceId) {
|
|
|
13488
13620
|
}
|
|
13489
13621
|
function readPersistedTunnel(stateDir, workspaceId) {
|
|
13490
13622
|
const path = tunnelStatePath(stateDir, workspaceId);
|
|
13491
|
-
if (!
|
|
13623
|
+
if (!existsSync25(path)) return void 0;
|
|
13492
13624
|
try {
|
|
13493
|
-
return JSON.parse(
|
|
13625
|
+
return JSON.parse(readFileSync18(path, "utf8"));
|
|
13494
13626
|
} catch {
|
|
13495
13627
|
return void 0;
|
|
13496
13628
|
}
|
|
13497
13629
|
}
|
|
13498
13630
|
function writePersistedTunnel(stateDir, state) {
|
|
13499
13631
|
const path = tunnelStatePath(stateDir, state.workspaceId);
|
|
13500
|
-
|
|
13501
|
-
|
|
13632
|
+
mkdirSync12(join25(stateDir, "passengers"), { recursive: true });
|
|
13633
|
+
writeFileSync13(path, `${JSON.stringify(state, null, 2)}
|
|
13502
13634
|
`);
|
|
13503
13635
|
}
|
|
13504
13636
|
function updatePersistedTunnel(stateDir, workspaceId, patch2) {
|
|
@@ -13596,7 +13728,7 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
|
|
|
13596
13728
|
}
|
|
13597
13729
|
function readAllPersistedWorkspaceIds(stateDir) {
|
|
13598
13730
|
const dir = join25(stateDir, "passengers");
|
|
13599
|
-
if (!
|
|
13731
|
+
if (!existsSync25(dir)) return [];
|
|
13600
13732
|
return readdirSync7(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
|
|
13601
13733
|
}
|
|
13602
13734
|
|
|
@@ -13717,7 +13849,7 @@ async function unregisterEndpoint(workspaceId, info, d) {
|
|
|
13717
13849
|
import { createServer as createHttpServer } from "http";
|
|
13718
13850
|
import { spawn as spawn5 } from "child_process";
|
|
13719
13851
|
import { randomBytes } from "crypto";
|
|
13720
|
-
import { existsSync as
|
|
13852
|
+
import { existsSync as existsSync26, mkdirSync as mkdirSync13, readFileSync as readFileSync19, writeFileSync as writeFileSync14, unlinkSync as unlinkSync4, readdirSync as readdirSync8 } from "fs";
|
|
13721
13853
|
import { join as join26 } from "path";
|
|
13722
13854
|
|
|
13723
13855
|
// src/secret-compare.ts
|
|
@@ -13811,17 +13943,17 @@ function syncDaemonStatePath(stateDir, workspaceId) {
|
|
|
13811
13943
|
}
|
|
13812
13944
|
function readPersistedSyncDaemon(stateDir, workspaceId) {
|
|
13813
13945
|
const path = syncDaemonStatePath(stateDir, workspaceId);
|
|
13814
|
-
if (!
|
|
13946
|
+
if (!existsSync26(path)) return void 0;
|
|
13815
13947
|
try {
|
|
13816
|
-
return JSON.parse(
|
|
13948
|
+
return JSON.parse(readFileSync19(path, "utf8"));
|
|
13817
13949
|
} catch {
|
|
13818
13950
|
return void 0;
|
|
13819
13951
|
}
|
|
13820
13952
|
}
|
|
13821
13953
|
function writePersistedSyncDaemon(stateDir, state) {
|
|
13822
13954
|
const path = syncDaemonStatePath(stateDir, state.workspaceId);
|
|
13823
|
-
|
|
13824
|
-
|
|
13955
|
+
mkdirSync13(join26(stateDir, "passengers"), { recursive: true });
|
|
13956
|
+
writeFileSync14(path, `${JSON.stringify(state, null, 2)}
|
|
13825
13957
|
`);
|
|
13826
13958
|
}
|
|
13827
13959
|
function removePersistedSyncDaemon(stateDir, workspaceId) {
|
|
@@ -13842,7 +13974,7 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
|
|
|
13842
13974
|
}
|
|
13843
13975
|
function readAllPersistedSyncWorkspaceIds(stateDir) {
|
|
13844
13976
|
const dir = join26(stateDir, "passengers");
|
|
13845
|
-
if (!
|
|
13977
|
+
if (!existsSync26(dir)) return [];
|
|
13846
13978
|
const suffix = "-sync.json";
|
|
13847
13979
|
return readdirSync8(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
|
|
13848
13980
|
}
|
|
@@ -13899,11 +14031,11 @@ function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
|
|
|
13899
14031
|
}
|
|
13900
14032
|
|
|
13901
14033
|
// src/daemon.ts
|
|
13902
|
-
import { existsSync as
|
|
14034
|
+
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
14035
|
import { join as join28 } from "path";
|
|
13904
14036
|
|
|
13905
14037
|
// src/interrupted.ts
|
|
13906
|
-
import { existsSync as
|
|
14038
|
+
import { existsSync as existsSync27, readdirSync as readdirSync9, readFileSync as readFileSync20 } from "fs";
|
|
13907
14039
|
import { join as join27 } from "path";
|
|
13908
14040
|
var INTERRUPTED_MARKER = calloutHeader("NOTE", "Interrupted");
|
|
13909
14041
|
function interruptedComment(b) {
|
|
@@ -13923,16 +14055,16 @@ var pidAlive = (pid) => {
|
|
|
13923
14055
|
};
|
|
13924
14056
|
function runFinished(streamPath) {
|
|
13925
14057
|
const path = `${streamPath}.meta.json`;
|
|
13926
|
-
if (!
|
|
14058
|
+
if (!existsSync27(path)) return false;
|
|
13927
14059
|
try {
|
|
13928
|
-
return JSON.parse(
|
|
14060
|
+
return JSON.parse(readFileSync20(path, "utf8")).running !== true;
|
|
13929
14061
|
} catch {
|
|
13930
14062
|
return true;
|
|
13931
14063
|
}
|
|
13932
14064
|
}
|
|
13933
14065
|
function orphanedStreams(o) {
|
|
13934
14066
|
const dir = join27(o.stateDir, "streams");
|
|
13935
|
-
if (!
|
|
14067
|
+
if (!existsSync27(dir)) return [];
|
|
13936
14068
|
const ownerAlive = o.isOwnerAlive ?? pidAlive;
|
|
13937
14069
|
const out = [];
|
|
13938
14070
|
for (const name of readdirSync9(dir)) {
|
|
@@ -14046,7 +14178,7 @@ function computeTreeSignature(paths) {
|
|
|
14046
14178
|
}
|
|
14047
14179
|
function makeStaleChecker(paths, opts = {}) {
|
|
14048
14180
|
const initial = computeTreeSignature(paths);
|
|
14049
|
-
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 &&
|
|
14181
|
+
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync28(opts.reloadSentinel);
|
|
14050
14182
|
}
|
|
14051
14183
|
function reloadSentinelFor(stateDir) {
|
|
14052
14184
|
return join28(stateDir, "reload-requested");
|
|
@@ -14055,14 +14187,14 @@ function daemonStampFor(stateDir) {
|
|
|
14055
14187
|
return join28(stateDir, "daemon.started");
|
|
14056
14188
|
}
|
|
14057
14189
|
function noteDaemonStarted(stateDir, now = Date.now()) {
|
|
14058
|
-
|
|
14190
|
+
mkdirSync14(stateDir, { recursive: true });
|
|
14059
14191
|
rmSync10(reloadSentinelFor(stateDir), { force: true });
|
|
14060
|
-
|
|
14192
|
+
writeFileSync15(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
|
|
14061
14193
|
`);
|
|
14062
14194
|
}
|
|
14063
14195
|
function readDaemonStarted(stateDir) {
|
|
14064
14196
|
try {
|
|
14065
|
-
const v = JSON.parse(
|
|
14197
|
+
const v = JSON.parse(readFileSync21(daemonStampFor(stateDir), "utf8")).startedAt;
|
|
14066
14198
|
return typeof v === "number" ? v : null;
|
|
14067
14199
|
} catch {
|
|
14068
14200
|
return null;
|
|
@@ -14418,7 +14550,7 @@ async function buildEnvironment(route, ship, ticket) {
|
|
|
14418
14550
|
}
|
|
14419
14551
|
|
|
14420
14552
|
// src/cli.ts
|
|
14421
|
-
var CREW_HOME = isCompiledBinary(import.meta.url) ?
|
|
14553
|
+
var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname16(process.execPath) : resolve5(dirname16(fileURLToPath(import.meta.url)), "..");
|
|
14422
14554
|
var RELEASE_LOCK_TTL_MS = 20 * 60 * 1e3;
|
|
14423
14555
|
var CYCLE_SECONDS2 = 120;
|
|
14424
14556
|
var STALE_CYCLES = 3;
|
|
@@ -14549,7 +14681,8 @@ function usage() {
|
|
|
14549
14681
|
crew status [route] [--json] paused/running state; --json for a machine reader
|
|
14550
14682
|
crew projects [--route R] [--json] the Projects rows each route can read (id, name, prefix, work mode)
|
|
14551
14683
|
crew reload make the running daemon re-read crew.yaml (--now: restart it immediately)
|
|
14552
|
-
crew doctor [route] [--fix]
|
|
14684
|
+
crew doctor [route] [--fix] [--offline] [--json] preflight; --fix enables clean routes and offers crew install;
|
|
14685
|
+
--offline skips push probes; --json prints push-auth and tool checks only
|
|
14553
14686
|
crew selftest [route] [--toolchain NAME] [--json] launch each toolchain as a run would and prove it reaches the tracker
|
|
14554
14687
|
crew sample-ticket [route] [--remove] [--dry-run] [--json] file one ticket on a throwaway repo that walks the dev, QA and release loop
|
|
14555
14688
|
crew ports [route] which checkout owns which ports, and what is up
|
|
@@ -14563,7 +14696,7 @@ function usage() {
|
|
|
14563
14696
|
resume with no argument clears only the whole-crew pause; a role or release pause needs its own resume
|
|
14564
14697
|
crew log [route] tail the log
|
|
14565
14698
|
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)
|
|
14699
|
+
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
14700
|
crew disconnect remove what connect installed for Pair (Claude MCP; Codex/Gemini instructions block and MCP entry per ship.pair)
|
|
14568
14701
|
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
14702
|
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)
|
|
@@ -14976,7 +15109,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
14976
15109
|
if (host === "linux" && hasSystemd) {
|
|
14977
15110
|
const label = labelFor(CREW_HOME, "run");
|
|
14978
15111
|
const staleTimer = join29(homedir8(), ".config", "systemd", "user", `${label}.timer`);
|
|
14979
|
-
if (
|
|
15112
|
+
if (existsSync29(staleTimer)) {
|
|
14980
15113
|
process.stdout.write(
|
|
14981
15114
|
`removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
|
|
14982
15115
|
`
|
|
@@ -15002,7 +15135,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
15002
15135
|
const tracker = new Tracker(c, cfg.ship);
|
|
15003
15136
|
problems.push(...validateContract(tracker.contract).map((p) => `contract: ${p}`));
|
|
15004
15137
|
for (const r of await resolvedRepos(c)) {
|
|
15005
|
-
if (!
|
|
15138
|
+
if (!existsSync29(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
|
|
15006
15139
|
if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
|
|
15007
15140
|
problems.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
|
|
15008
15141
|
}
|
|
@@ -15020,7 +15153,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
15020
15153
|
}
|
|
15021
15154
|
async function doctorFix(o) {
|
|
15022
15155
|
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) =>
|
|
15156
|
+
const unitExists = (p) => existsSync29(p);
|
|
15024
15157
|
const crontab = o.host === "linux" ? spawnSync6("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
|
|
15025
15158
|
const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
|
|
15026
15159
|
const plan = planFix({ routes, schedulerAbsent, host: o.host });
|
|
@@ -15037,7 +15170,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
15037
15170
|
for (const a of plan.actions) {
|
|
15038
15171
|
if (a.kind === "enable") {
|
|
15039
15172
|
if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
|
|
15040
|
-
|
|
15173
|
+
writeFileSync16(cfg.configFile, planEnable(readFileSync22(cfg.configFile, "utf8"), a.route));
|
|
15041
15174
|
enabledSound.add(a.route);
|
|
15042
15175
|
changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
|
|
15043
15176
|
} else {
|
|
@@ -15054,7 +15187,10 @@ ${changed2.map((c) => ` - ${c}
|
|
|
15054
15187
|
`).join("")}` : "fix: nothing changed\n");
|
|
15055
15188
|
}
|
|
15056
15189
|
async function resolvedRepos(c) {
|
|
15057
|
-
return Promise.all(reposOf(c).map(
|
|
15190
|
+
return Promise.all(reposOf(c).map((t) => resolveOneRepo(c, t)));
|
|
15191
|
+
}
|
|
15192
|
+
async function resolveOneRepo(c, t) {
|
|
15193
|
+
{
|
|
15058
15194
|
const id = repoIdForName(c, t.name);
|
|
15059
15195
|
const o = c.repoOverrides[t.name];
|
|
15060
15196
|
return {
|
|
@@ -15078,7 +15214,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
15078
15214
|
worktrees: { prefix: shipWorktreePrefixFor(c) ?? await areaWorktreePrefix(c, id), nameOverride: o?.worktreeName }
|
|
15079
15215
|
}, t.dir)
|
|
15080
15216
|
};
|
|
15081
|
-
}
|
|
15217
|
+
}
|
|
15082
15218
|
}
|
|
15083
15219
|
const worktreeLocations = async (c) => (await resolvedRepos(c)).map((r) => ({
|
|
15084
15220
|
parent: resolvePath(r.dir, ".."),
|
|
@@ -16160,69 +16296,27 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16160
16296
|
`);
|
|
16161
16297
|
process.exit(2);
|
|
16162
16298
|
}
|
|
16163
|
-
|
|
16164
|
-
let readOnly = !isAdminRole(keyRole);
|
|
16165
|
-
let personaResult;
|
|
16166
|
-
const runPersonas = (ro) => syncPersonas(target, { userAgent: cfg.ship.userAgent, dryRun, force, readOnly: ro });
|
|
16299
|
+
let run2;
|
|
16167
16300
|
try {
|
|
16168
|
-
|
|
16169
|
-
|
|
16170
|
-
|
|
16171
|
-
|
|
16172
|
-
|
|
16173
|
-
|
|
16174
|
-
|
|
16301
|
+
run2 = await runAgentsSync(target, {
|
|
16302
|
+
userAgent: cfg.ship.userAgent,
|
|
16303
|
+
stateDir: cfg.ship.stateDir,
|
|
16304
|
+
dryRun,
|
|
16305
|
+
force,
|
|
16306
|
+
say: (l) => process.stdout.write(`${l}
|
|
16307
|
+
`),
|
|
16308
|
+
warn: (l) => process.stderr.write(`crew agents sync: ${l}
|
|
16309
|
+
`)
|
|
16310
|
+
});
|
|
16175
16311
|
} catch (e) {
|
|
16176
|
-
if (e instanceof AgentsSyncError) {
|
|
16312
|
+
if (e instanceof AgentsSyncError || e instanceof SkillSyncError) {
|
|
16177
16313
|
process.stderr.write(`crew agents sync: ${e.message}
|
|
16178
16314
|
`);
|
|
16179
16315
|
process.exit(2);
|
|
16180
16316
|
}
|
|
16181
16317
|
throw e;
|
|
16182
16318
|
}
|
|
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
|
-
}
|
|
16319
|
+
const { personaResult, skillResult } = run2;
|
|
16226
16320
|
if (flag("revoke-seat-keys")) {
|
|
16227
16321
|
for (const o of await revokeSeatKeys(target, { userAgent: cfg.ship.userAgent, dryRun })) {
|
|
16228
16322
|
process.stdout.write(`${describeRevokeOutcome(o)}
|
|
@@ -16234,9 +16328,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16234
16328
|
`);
|
|
16235
16329
|
}
|
|
16236
16330
|
}
|
|
16237
|
-
if (
|
|
16238
|
-
process.exitCode = 1;
|
|
16239
|
-
}
|
|
16331
|
+
if (run2.diverged) process.exitCode = 1;
|
|
16240
16332
|
break;
|
|
16241
16333
|
}
|
|
16242
16334
|
case "skills": {
|
|
@@ -16278,10 +16370,10 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16278
16370
|
`);
|
|
16279
16371
|
if (!dryRun) {
|
|
16280
16372
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
16281
|
-
const raw =
|
|
16373
|
+
const raw = existsSync29(resolvedPath) ? JSON.parse(readFileSync22(resolvedPath, "utf8")) : {};
|
|
16282
16374
|
raw.agentSkills = result.agentSkills;
|
|
16283
|
-
|
|
16284
|
-
|
|
16375
|
+
mkdirSync15(dirOf(resolvedPath), { recursive: true });
|
|
16376
|
+
writeFileSync16(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
16285
16377
|
`);
|
|
16286
16378
|
}
|
|
16287
16379
|
if (result.outcomes.some((o) => o.action === "diverged")) process.exitCode = 1;
|
|
@@ -16294,14 +16386,14 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16294
16386
|
process.exit(2);
|
|
16295
16387
|
};
|
|
16296
16388
|
if (sub !== "add" && sub !== "list" && sub !== "clone") usage2();
|
|
16297
|
-
const configText =
|
|
16389
|
+
const configText = readFileSync22(cfg.configFile, "utf8");
|
|
16298
16390
|
let routeName = positional[2];
|
|
16299
16391
|
let addPath = ".";
|
|
16300
16392
|
let addName;
|
|
16301
16393
|
if (sub === "list" && !routeName) usage2();
|
|
16302
16394
|
if (sub === "add") {
|
|
16303
16395
|
try {
|
|
16304
|
-
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) =>
|
|
16396
|
+
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync29(resolvePath(p.replace(/^~(?=\/|$)/, homedir8()))));
|
|
16305
16397
|
routeName = a.route;
|
|
16306
16398
|
addPath = a.path;
|
|
16307
16399
|
addName = a.name;
|
|
@@ -16330,15 +16422,15 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16330
16422
|
}
|
|
16331
16423
|
const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
|
|
16332
16424
|
let resolvedRemotes;
|
|
16333
|
-
if (
|
|
16425
|
+
if (existsSync29(resolvedFile)) {
|
|
16334
16426
|
try {
|
|
16335
|
-
resolvedRemotes = JSON.parse(
|
|
16427
|
+
resolvedRemotes = JSON.parse(readFileSync22(resolvedFile, "utf8"));
|
|
16336
16428
|
} catch {
|
|
16337
16429
|
}
|
|
16338
16430
|
}
|
|
16339
16431
|
const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
|
|
16340
16432
|
const text = configText;
|
|
16341
|
-
const base =
|
|
16433
|
+
const base = dirname16(cfg.configFile);
|
|
16342
16434
|
try {
|
|
16343
16435
|
if (sub === "list") {
|
|
16344
16436
|
const repos = configuredRepos(text, routeName, base);
|
|
@@ -16347,7 +16439,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16347
16439
|
for (const r of repos) {
|
|
16348
16440
|
const origin2 = originOf(r.dir);
|
|
16349
16441
|
const row = matchRepoName(origin2, resolvedRemotes);
|
|
16350
|
-
const note = !
|
|
16442
|
+
const note = !existsSync29(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
|
|
16351
16443
|
process.stdout.write(`${r.name} ${r.dir} ${note}
|
|
16352
16444
|
`);
|
|
16353
16445
|
}
|
|
@@ -16359,9 +16451,9 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16359
16451
|
` : `${human}
|
|
16360
16452
|
`);
|
|
16361
16453
|
const plan2 = planRepoClone({ text, base, route: routeName, name: addName, resolved: resolvedRemotes, dir: value("dir") });
|
|
16362
|
-
if (
|
|
16454
|
+
if (existsSync29(plan2.target) && (existsSync29(`${plan2.target}/.git`) || readdirSync11(plan2.target).length > 0)) {
|
|
16363
16455
|
throw new ReposError(
|
|
16364
|
-
|
|
16456
|
+
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
16457
|
);
|
|
16366
16458
|
}
|
|
16367
16459
|
if (dryRun) {
|
|
@@ -16379,12 +16471,12 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16379
16471
|
throw new ReposError(`${e.message}${authHint}
|
|
16380
16472
|
crew.yaml was not changed`);
|
|
16381
16473
|
}
|
|
16382
|
-
|
|
16474
|
+
writeFileSync16(cfg.configFile, plan2.text);
|
|
16383
16475
|
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
16476
|
break;
|
|
16385
16477
|
}
|
|
16386
16478
|
const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir8()));
|
|
16387
|
-
const top =
|
|
16479
|
+
const top = existsSync29(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
|
|
16388
16480
|
if (!top) throw new ReposError(`${target} is not a git checkout`);
|
|
16389
16481
|
const origin = originOf(top);
|
|
16390
16482
|
const plan = planRepoAdd({
|
|
@@ -16433,15 +16525,15 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16433
16525
|
const created = await rowTracker.createRepoRow(rowPayload);
|
|
16434
16526
|
process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
|
|
16435
16527
|
`);
|
|
16436
|
-
if (
|
|
16437
|
-
const raw = JSON.parse(
|
|
16528
|
+
if (existsSync29(resolvedFile)) {
|
|
16529
|
+
const raw = JSON.parse(readFileSync22(resolvedFile, "utf8"));
|
|
16438
16530
|
raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
|
|
16439
16531
|
raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
|
|
16440
|
-
|
|
16532
|
+
writeFileSync16(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
16441
16533
|
`);
|
|
16442
16534
|
}
|
|
16443
16535
|
}
|
|
16444
|
-
|
|
16536
|
+
writeFileSync16(cfg.configFile, plan.text);
|
|
16445
16537
|
process.stdout.write(`added ${top} to ${routeName} as ${plan.mode === "dir" ? "dir" : `repos.${plan.name}`} in ${cfg.configFile}
|
|
16446
16538
|
`);
|
|
16447
16539
|
} catch (e) {
|
|
@@ -16751,10 +16843,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
16751
16843
|
}
|
|
16752
16844
|
}
|
|
16753
16845
|
let previous;
|
|
16754
|
-
const hadPreviousFile =
|
|
16846
|
+
const hadPreviousFile = existsSync29(resolvedPath);
|
|
16755
16847
|
if (hadPreviousFile) {
|
|
16756
16848
|
try {
|
|
16757
|
-
previous = JSON.parse(
|
|
16849
|
+
previous = JSON.parse(readFileSync22(resolvedPath, "utf8"));
|
|
16758
16850
|
} catch {
|
|
16759
16851
|
}
|
|
16760
16852
|
}
|
|
@@ -16862,10 +16954,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
16862
16954
|
process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
|
|
16863
16955
|
`);
|
|
16864
16956
|
} else {
|
|
16865
|
-
|
|
16957
|
+
mkdirSync15(dirname16(resolvedPath), { recursive: true });
|
|
16866
16958
|
if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
|
|
16867
16959
|
const tmpPath = `${resolvedPath}.tmp${process.pid}`;
|
|
16868
|
-
|
|
16960
|
+
writeFileSync16(tmpPath, `${JSON.stringify(resolved, null, 2)}
|
|
16869
16961
|
`);
|
|
16870
16962
|
renameSync2(tmpPath, resolvedPath);
|
|
16871
16963
|
process.stderr.write(`Resolved ids written to ${resolvedPath}
|
|
@@ -16900,8 +16992,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
16900
16992
|
process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
|
|
16901
16993
|
`);
|
|
16902
16994
|
} else {
|
|
16903
|
-
|
|
16904
|
-
|
|
16995
|
+
mkdirSync15(dirname16(apiKeyFile), { recursive: true });
|
|
16996
|
+
writeFileSync16(apiKeyFile, `${apiKeyVar}=${apiKey}
|
|
16905
16997
|
`, { mode: 384 });
|
|
16906
16998
|
process.stderr.write(`API key written to ${apiKeyFile}
|
|
16907
16999
|
`);
|
|
@@ -17002,6 +17094,56 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
17002
17094
|
`
|
|
17003
17095
|
);
|
|
17004
17096
|
}
|
|
17097
|
+
if (!dryRun && !flag("no-agents-sync")) {
|
|
17098
|
+
const skip = (why) => {
|
|
17099
|
+
process.stderr.write(`
|
|
17100
|
+
agents sync: skipped (${why})
|
|
17101
|
+
`);
|
|
17102
|
+
connectEvents.emit("agents_synced", `agents sync skipped: ${why}`, { route: route2, skipped: why });
|
|
17103
|
+
};
|
|
17104
|
+
let syncRoute;
|
|
17105
|
+
try {
|
|
17106
|
+
const reloaded = loadConfig(CREW_HOME, wizard?.configPath ?? cfg.configFile, { allowNoRoutes: true });
|
|
17107
|
+
syncRoute = reloaded.routes.find((r) => r.route === route2);
|
|
17108
|
+
if (syncRoute) await hydrateApiKeys([syncRoute]);
|
|
17109
|
+
} catch (e) {
|
|
17110
|
+
skip(`config did not load: ${e.message}`);
|
|
17111
|
+
}
|
|
17112
|
+
if (syncRoute?.resolved) {
|
|
17113
|
+
try {
|
|
17114
|
+
process.stderr.write("\n");
|
|
17115
|
+
const run2 = await runAgentsSync(syncRoute, {
|
|
17116
|
+
userAgent: cfg.ship.userAgent,
|
|
17117
|
+
stateDir: cfg.ship.stateDir,
|
|
17118
|
+
say: (l) => process.stderr.write(` ${l}
|
|
17119
|
+
`),
|
|
17120
|
+
warn: (l) => process.stderr.write(` agents sync: ${l}
|
|
17121
|
+
`)
|
|
17122
|
+
});
|
|
17123
|
+
const minted = await mintMissingSeatKeys(syncRoute, { userAgent: cfg.ship.userAgent, shipName: cfg.ship.name });
|
|
17124
|
+
for (const m of minted) process.stderr.write(` ${describeMintOutcome(m)}
|
|
17125
|
+
`);
|
|
17126
|
+
if (run2.readOnly) {
|
|
17127
|
+
const missing = run2.personaResult.outcomes.some((p) => p.action === "missing");
|
|
17128
|
+
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");
|
|
17129
|
+
}
|
|
17130
|
+
connectEvents.emit("agents_synced", run2.readOnly ? "agents linked (read-only key)" : "agents synced", {
|
|
17131
|
+
route: route2,
|
|
17132
|
+
admin: !run2.readOnly,
|
|
17133
|
+
personas: run2.personaResult.outcomes,
|
|
17134
|
+
skills: run2.skillResult?.outcomes ?? [],
|
|
17135
|
+
diverged: run2.diverged
|
|
17136
|
+
});
|
|
17137
|
+
for (const m of minted) connectEvents.emit("seat_key", describeMintOutcome(m), { route: route2, ...m });
|
|
17138
|
+
if (run2.diverged) process.exitCode = 1;
|
|
17139
|
+
} catch (e) {
|
|
17140
|
+
if (e instanceof AgentsSyncError || e instanceof SkillSyncError) skip(e.message);
|
|
17141
|
+
else throw e;
|
|
17142
|
+
}
|
|
17143
|
+
} else if (!syncRoute) {
|
|
17144
|
+
skip(`"${route2}" is not in crew.yaml yet; add the block above, then run \`crew agents sync\``);
|
|
17145
|
+
}
|
|
17146
|
+
}
|
|
17005
17147
|
connectEvents.emit("done", `connected ${route2}`, {
|
|
17006
17148
|
route: route2,
|
|
17007
17149
|
resolvedPath: dryRun ? void 0 : resolvedPath,
|
|
@@ -17480,8 +17622,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
17480
17622
|
process.stdout.write("crew reload: no daemon unit is running \u2014 nothing to reload (the next run reads crew.yaml fresh)\n");
|
|
17481
17623
|
break;
|
|
17482
17624
|
}
|
|
17483
|
-
|
|
17484
|
-
|
|
17625
|
+
mkdirSync15(cfg.ship.stateDir, { recursive: true });
|
|
17626
|
+
writeFileSync16(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
|
|
17485
17627
|
`);
|
|
17486
17628
|
process.stdout.write("crew reload: requested \u2014 the daemon finishes its in-flight pass, then relaunches with the new config\n");
|
|
17487
17629
|
if (flag("now") && control?.stopCommand && control.startCommand) {
|
|
@@ -17532,7 +17674,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
17532
17674
|
}
|
|
17533
17675
|
case "log": {
|
|
17534
17676
|
try {
|
|
17535
|
-
const text =
|
|
17677
|
+
const text = readFileSync22(cfg.ship.logFile, "utf8").split("\n");
|
|
17536
17678
|
process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
|
|
17537
17679
|
`);
|
|
17538
17680
|
} catch {
|
|
@@ -17555,8 +17697,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
17555
17697
|
};
|
|
17556
17698
|
if (!route.resolved) fail(`route ${route.route} is not connected \u2014 run \`crew connect\` first`);
|
|
17557
17699
|
const tracker = new Tracker(route, cfg.ship);
|
|
17558
|
-
const configText =
|
|
17559
|
-
const cfgBase =
|
|
17700
|
+
const configText = readFileSync22(cfg.configFile, "utf8");
|
|
17701
|
+
const cfgBase = dirname16(cfg.configFile);
|
|
17560
17702
|
const configured = configuredRepos(configText, route.route, cfgBase);
|
|
17561
17703
|
const dir = configured.find((r) => r.name === SAMPLE_REPO)?.dir ?? sampleDir(configured.map((r) => r.dir), route.reposBasePath, route.route.split("/")[0]);
|
|
17562
17704
|
const resolvedFile = resolvedPathFor(cfg.ship.stateDir, route.route);
|
|
@@ -17584,7 +17726,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
17584
17726
|
}
|
|
17585
17727
|
if (configured.some((r) => r.dir === dir)) {
|
|
17586
17728
|
try {
|
|
17587
|
-
|
|
17729
|
+
writeFileSync16(cfg.configFile, planRepoRemove(configText, cfgBase, route.route, dir).text);
|
|
17588
17730
|
} catch (e) {
|
|
17589
17731
|
if (e instanceof ReposError) fail(e.message);
|
|
17590
17732
|
throw e;
|
|
@@ -17592,29 +17734,29 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
17592
17734
|
}
|
|
17593
17735
|
if (row) {
|
|
17594
17736
|
await tracker.deleteRepoRow(row.id);
|
|
17595
|
-
if (
|
|
17596
|
-
const raw = JSON.parse(
|
|
17737
|
+
if (existsSync29(resolvedFile)) {
|
|
17738
|
+
const raw = JSON.parse(readFileSync22(resolvedFile, "utf8"));
|
|
17597
17739
|
if (raw.repoNames) delete raw.repoNames[row.id];
|
|
17598
17740
|
if (raw.repoRemotes) delete raw.repoRemotes[row.id];
|
|
17599
|
-
|
|
17741
|
+
writeFileSync16(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
17600
17742
|
`);
|
|
17601
17743
|
}
|
|
17602
17744
|
}
|
|
17603
|
-
if (
|
|
17745
|
+
if (existsSync29(join29(dir, ".git")) && basename10(dir) === SAMPLE_REPO) rmSync11(dir, { recursive: true, force: true });
|
|
17604
17746
|
say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
|
|
17605
17747
|
break;
|
|
17606
17748
|
}
|
|
17607
17749
|
const projectId = route.resolved.areaId ?? resolveRepoProject(await tracker.projectRows(), value("project"));
|
|
17608
|
-
const fresh = !
|
|
17609
|
-
if (
|
|
17750
|
+
const fresh = !existsSync29(join29(dir, ".git"));
|
|
17751
|
+
if (existsSync29(dir) && fresh && readdirSync11(dir).length > 0) fail(`${dir} exists and is not a git checkout \u2014 move it aside`);
|
|
17610
17752
|
if (dryRun) {
|
|
17611
17753
|
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
17754
|
break;
|
|
17613
17755
|
}
|
|
17614
17756
|
if (fresh) {
|
|
17615
|
-
|
|
17616
|
-
|
|
17617
|
-
|
|
17757
|
+
mkdirSync15(dir, { recursive: true });
|
|
17758
|
+
writeFileSync16(join29(dir, "README.md"), SAMPLE_README);
|
|
17759
|
+
writeFileSync16(join29(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
|
|
17618
17760
|
const g = (...a) => {
|
|
17619
17761
|
const r = spawnSync6("git", a, { cwd: dir, encoding: "utf8" });
|
|
17620
17762
|
if (r.status !== 0) fail(`git ${a[0]} failed in ${dir}: ${(r.stderr || r.stdout).trim()}`);
|
|
@@ -17632,17 +17774,17 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
17632
17774
|
spec: { platform: "unix", releaseMode: "integrate", ciProvider: "none" }
|
|
17633
17775
|
});
|
|
17634
17776
|
repoId = (await tracker.createRepoRow(payload)).id;
|
|
17635
|
-
if (
|
|
17636
|
-
const raw = JSON.parse(
|
|
17777
|
+
if (existsSync29(resolvedFile)) {
|
|
17778
|
+
const raw = JSON.parse(readFileSync22(resolvedFile, "utf8"));
|
|
17637
17779
|
raw.repoNames = { ...raw.repoNames, [repoId]: SAMPLE_REPO };
|
|
17638
17780
|
raw.repoRemotes = { ...raw.repoRemotes, [repoId]: SAMPLE_REMOTE };
|
|
17639
|
-
|
|
17781
|
+
writeFileSync16(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
17640
17782
|
`);
|
|
17641
17783
|
}
|
|
17642
17784
|
}
|
|
17643
17785
|
if (!configured.some((r) => r.dir === dir)) {
|
|
17644
17786
|
try {
|
|
17645
|
-
|
|
17787
|
+
writeFileSync16(cfg.configFile, planRepoAdd({ text: configText, base: cfgBase, route: route.route, path: dir, name: SAMPLE_REPO }).text);
|
|
17646
17788
|
} catch (e) {
|
|
17647
17789
|
if (e instanceof ReposError) fail(e.message);
|
|
17648
17790
|
throw e;
|
|
@@ -17919,6 +18061,40 @@ watermark: ${state.watermark()}
|
|
|
17919
18061
|
}
|
|
17920
18062
|
case "doctor": {
|
|
17921
18063
|
const host = hostPlatform();
|
|
18064
|
+
const doctorSchedulerPath = pathFor(cfg.ship);
|
|
18065
|
+
const pushAuth = [];
|
|
18066
|
+
const toolRows = [];
|
|
18067
|
+
{
|
|
18068
|
+
const prBased = /* @__PURE__ */ new Set();
|
|
18069
|
+
let mentionNode = false;
|
|
18070
|
+
for (const c of [route]) {
|
|
18071
|
+
const { settled: repos, errors } = await settleRepos(reposOf(c), (t) => t.name, (t) => resolveOneRepo(c, t));
|
|
18072
|
+
for (const e of errors) {
|
|
18073
|
+
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" } });
|
|
18074
|
+
}
|
|
18075
|
+
for (const r of repos) {
|
|
18076
|
+
const remote = r.config.branch.remote;
|
|
18077
|
+
if (existsSync29(r.dir)) {
|
|
18078
|
+
pushAuth.push({
|
|
18079
|
+
route: c.route,
|
|
18080
|
+
repo: r.name,
|
|
18081
|
+
result: probePushAuth({ dir: r.dir, remote, remoteUrl: originUrlOf(r.dir, remote), offline: flag("offline") })
|
|
18082
|
+
});
|
|
18083
|
+
}
|
|
18084
|
+
if (isPrBased(r.config.hooks)) prBased.add(r.name);
|
|
18085
|
+
if (Object.values(r.config.hooks).some((h) => typeof h === "string" && mentionsNode(h))) mentionNode = true;
|
|
18086
|
+
}
|
|
18087
|
+
}
|
|
18088
|
+
toolRows.push(...toolChecks(
|
|
18089
|
+
{ prBased: prBased.size > 0, hostPassengers: cfg.routes.some((x) => x.enabled && x.hostPassengers), hooksMentionNode: mentionNode },
|
|
18090
|
+
(bin) => findOnPath(bin, doctorSchedulerPath)
|
|
18091
|
+
));
|
|
18092
|
+
}
|
|
18093
|
+
if (flag("json")) {
|
|
18094
|
+
process.stdout.write(`${JSON.stringify({ offline: flag("offline"), pushAuth, tools: toolRows }, null, 2)}
|
|
18095
|
+
`);
|
|
18096
|
+
break;
|
|
18097
|
+
}
|
|
17922
18098
|
process.stdout.write(
|
|
17923
18099
|
`ship: ${cfg.ship.name} (${host})
|
|
17924
18100
|
routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
@@ -17926,7 +18102,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
17926
18102
|
);
|
|
17927
18103
|
try {
|
|
17928
18104
|
const evFile = eventFileFor(cfg.ship.stateDir);
|
|
17929
|
-
const evSize =
|
|
18105
|
+
const evSize = existsSync29(evFile) ? statSync9(evFile).size : 0;
|
|
17930
18106
|
process.stdout.write(
|
|
17931
18107
|
evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
|
|
17932
18108
|
` : "events log: ok\n"
|
|
@@ -17937,7 +18113,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
17937
18113
|
const unitPlan = planInstall(cfg.ship, CREW_HOME, host, detectSystemd(), "run");
|
|
17938
18114
|
const abandons = unitAbandonsAgents(unitPlan, (p) => {
|
|
17939
18115
|
try {
|
|
17940
|
-
return
|
|
18116
|
+
return readFileSync22(p, "utf8");
|
|
17941
18117
|
} catch {
|
|
17942
18118
|
return null;
|
|
17943
18119
|
}
|
|
@@ -18142,6 +18318,8 @@ crew manifest: ${seats2.length ? seats2.map((c) => c.name).join(", ") : "non
|
|
|
18142
18318
|
if (ov.toolchain) process.stdout.write(`repo toolchain: ${repoName} \u2192 ${ov.toolchain} (binary and MCP checked above, as for every ship toolchain)
|
|
18143
18319
|
`);
|
|
18144
18320
|
}
|
|
18321
|
+
for (const p of pushAuth) process.stdout.write(formatPushAuth(`${p.repo}`, p.result));
|
|
18322
|
+
for (const t of toolRows) process.stdout.write(formatToolCheck(t));
|
|
18145
18323
|
process.stdout.write(`git: ${gitPath ? `OK (${gitPath})` : "NOT FOUND on the scheduler PATH"}
|
|
18146
18324
|
`);
|
|
18147
18325
|
if (!allBinsFound || !gitPath) {
|
package/package.json
CHANGED