@tablation/crew 0.57.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 +6 -1
- package/dist/cli.js +583 -180
- 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";
|
|
@@ -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(" / ");
|
|
@@ -7201,6 +7279,25 @@ var Tracker = class {
|
|
|
7201
7279
|
if (!model) throw new Error("this workspace has no Repos table");
|
|
7202
7280
|
return this.client.records.create(model, fields);
|
|
7203
7281
|
}
|
|
7282
|
+
/** Every `Repos` row as `{ id, name }`, for finding one by name. */
|
|
7283
|
+
async repoRows() {
|
|
7284
|
+
const model = this.route.resolved?.reposModelId;
|
|
7285
|
+
if (!model) return [];
|
|
7286
|
+
return this.client.records.list(model, { limit: 200 });
|
|
7287
|
+
}
|
|
7288
|
+
/** Delete a `Repos` row (`crew sample-ticket --remove`). */
|
|
7289
|
+
async deleteRepoRow(id) {
|
|
7290
|
+
const model = this.route.resolved?.reposModelId;
|
|
7291
|
+
if (!model) throw new Error("this workspace has no Repos table");
|
|
7292
|
+
await this.client.records.remove(model, id);
|
|
7293
|
+
}
|
|
7294
|
+
/** Every ticket filed against one `Repos` row, any status. */
|
|
7295
|
+
async ticketsForRepo(repoId) {
|
|
7296
|
+
return this.client.records.list(this.models.issues, {
|
|
7297
|
+
filters: encodeFilters([{ columnName: this.contract.columns.repo, operator: "EQ", value: repoId }]),
|
|
7298
|
+
limit: 200
|
|
7299
|
+
});
|
|
7300
|
+
}
|
|
7204
7301
|
/**
|
|
7205
7302
|
* The Ships rows of this workspace, if it has a Ships table.
|
|
7206
7303
|
*
|
|
@@ -9224,12 +9321,12 @@ import { existsSync as existsSync17 } from "fs";
|
|
|
9224
9321
|
import { dirname as dirname7, join as join17 } from "path";
|
|
9225
9322
|
|
|
9226
9323
|
// src/ports.ts
|
|
9227
|
-
import { execFileSync as
|
|
9324
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
9228
9325
|
import { existsSync as existsSync16 } from "fs";
|
|
9229
9326
|
import { join as join16 } from "path";
|
|
9230
9327
|
var run = (cmd, args) => {
|
|
9231
9328
|
try {
|
|
9232
|
-
return
|
|
9329
|
+
return execFileSync3(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
9233
9330
|
} catch {
|
|
9234
9331
|
return "";
|
|
9235
9332
|
}
|
|
@@ -12420,6 +12517,60 @@ async function canMintAgentKeys(route, o = {}) {
|
|
|
12420
12517
|
}
|
|
12421
12518
|
}
|
|
12422
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
|
+
|
|
12423
12574
|
// src/logbook.ts
|
|
12424
12575
|
import { createHash as createHash3 } from "crypto";
|
|
12425
12576
|
import { TablationClient as TablationClient4 } from "@tablation/client";
|
|
@@ -12508,9 +12659,77 @@ async function showLogEntry(route, entryId, opts = {}) {
|
|
|
12508
12659
|
};
|
|
12509
12660
|
}
|
|
12510
12661
|
|
|
12662
|
+
// src/sample-ticket.ts
|
|
12663
|
+
import { basename as basename7, dirname as dirname12, join as join20 } from "path";
|
|
12664
|
+
var SAMPLE_REPO = "crew-sample";
|
|
12665
|
+
var SAMPLE_REMOTE = `local/${SAMPLE_REPO}`;
|
|
12666
|
+
var SAMPLE_MARKER = "sample: true";
|
|
12667
|
+
var SAMPLE_CREW_YAML = `version: 1
|
|
12668
|
+
platform: unix
|
|
12669
|
+
branch:
|
|
12670
|
+
base: main
|
|
12671
|
+
hooks:
|
|
12672
|
+
test: "true"
|
|
12673
|
+
build: "true"
|
|
12674
|
+
release:
|
|
12675
|
+
mode: integrate
|
|
12676
|
+
versioning: none
|
|
12677
|
+
changelog: false
|
|
12678
|
+
`;
|
|
12679
|
+
var SAMPLE_README = `# crew-sample
|
|
12680
|
+
|
|
12681
|
+
A throwaway repository created by \`crew sample-ticket\`. It exists so the crew
|
|
12682
|
+
has something safe to work on: its test and build hooks always pass, it has no
|
|
12683
|
+
remote, and \`crew sample-ticket --remove\` deletes it.
|
|
12684
|
+
`;
|
|
12685
|
+
function sampleDir(existing, reposBase, workspace) {
|
|
12686
|
+
if (existing.length > 0) return join20(dirname12(existing[0]), SAMPLE_REPO);
|
|
12687
|
+
return join20(reposBase, workspace, SAMPLE_REPO);
|
|
12688
|
+
}
|
|
12689
|
+
var SAMPLE_TITLE = "Sample: add a documented `greet` function with a test";
|
|
12690
|
+
function sampleDescription() {
|
|
12691
|
+
return [
|
|
12692
|
+
SAMPLE_MARKER,
|
|
12693
|
+
"",
|
|
12694
|
+
"> This is a sample ticket filed by `crew sample-ticket` on a throwaway repository. Nothing here is real work.",
|
|
12695
|
+
"",
|
|
12696
|
+
"## What",
|
|
12697
|
+
"",
|
|
12698
|
+
"Add `greet.js` exporting `greet(name)`, which returns `Hello, <name>!` (and `Hello, world!` for no name),",
|
|
12699
|
+
"with a JSDoc comment, and `greet.test.js` covering both cases using `node:test`.",
|
|
12700
|
+
"",
|
|
12701
|
+
"## Acceptance",
|
|
12702
|
+
"",
|
|
12703
|
+
'- `greet("Ada")` returns `Hello, Ada!`; `greet()` returns `Hello, world!`.',
|
|
12704
|
+
"- The test file runs with `node --test` and passes.",
|
|
12705
|
+
"- Add a line to the README saying the function exists.",
|
|
12706
|
+
"",
|
|
12707
|
+
"## Notes",
|
|
12708
|
+
"",
|
|
12709
|
+
"The repo has no remote: leave the branch local. The hooks in `.crew.yaml` always pass, so QA is checking the",
|
|
12710
|
+
"diff against the acceptance list above."
|
|
12711
|
+
].join("\n");
|
|
12712
|
+
}
|
|
12713
|
+
function isSampleTicket(description) {
|
|
12714
|
+
return (description ?? "").trimStart().startsWith(SAMPLE_MARKER);
|
|
12715
|
+
}
|
|
12716
|
+
function planSampleTicket(i) {
|
|
12717
|
+
return {
|
|
12718
|
+
title: SAMPLE_TITLE,
|
|
12719
|
+
description: sampleDescription(),
|
|
12720
|
+
report_type: "feature",
|
|
12721
|
+
severity: "s4",
|
|
12722
|
+
reporter_name: "crew sample-ticket",
|
|
12723
|
+
status: i.approvedStatus,
|
|
12724
|
+
project_id: i.projectId,
|
|
12725
|
+
repo_id: i.repoId,
|
|
12726
|
+
...i.filedBy ? { filed_by_id: i.filedBy } : {}
|
|
12727
|
+
};
|
|
12728
|
+
}
|
|
12729
|
+
|
|
12511
12730
|
// src/repos-cmd.ts
|
|
12512
12731
|
import { homedir as homedir7 } from "os";
|
|
12513
|
-
import { basename as
|
|
12732
|
+
import { basename as basename8, join as join21, resolve as resolve3 } from "path";
|
|
12514
12733
|
import { isMap, isSeq, parseDocument, stringify as stringify2 } from "yaml";
|
|
12515
12734
|
var ReposError = class extends Error {
|
|
12516
12735
|
};
|
|
@@ -12582,7 +12801,7 @@ function configuredRepos(text, route, base) {
|
|
|
12582
12801
|
});
|
|
12583
12802
|
}
|
|
12584
12803
|
const dir = m.get("dir");
|
|
12585
|
-
return dir ? [{ name:
|
|
12804
|
+
return dir ? [{ name: basename8(String(dir)), dir: expandDir(String(dir), base) }] : [];
|
|
12586
12805
|
}
|
|
12587
12806
|
function planRepoAdd(i) {
|
|
12588
12807
|
const doc = parseDocument(i.text);
|
|
@@ -12594,7 +12813,7 @@ function planRepoAdd(i) {
|
|
|
12594
12813
|
throw new ReposError(`${target} is already configured on route ${i.route}`);
|
|
12595
12814
|
}
|
|
12596
12815
|
const matched = matchRepoName(i.origin, i.resolved);
|
|
12597
|
-
const name = matched ?? i.name ?? normalizeRemote(i.origin)?.split("/").pop() ??
|
|
12816
|
+
const name = matched ?? i.name ?? normalizeRemote(i.origin)?.split("/").pop() ?? basename8(target);
|
|
12598
12817
|
let warning;
|
|
12599
12818
|
if (!matched) {
|
|
12600
12819
|
const known = Object.values(i.resolved?.repoNames ?? {});
|
|
@@ -12621,6 +12840,28 @@ function planRepoAdd(i) {
|
|
|
12621
12840
|
}
|
|
12622
12841
|
return { text: String(doc), mode, name, block: renderBlock(m), warning, unmatched: !matched };
|
|
12623
12842
|
}
|
|
12843
|
+
function planRepoRemove(text, base, route, path) {
|
|
12844
|
+
const doc = parseDocument(text);
|
|
12845
|
+
if (doc.errors.length > 0) throw new ReposError(`config is not valid YAML \u2014 ${doc.errors[0].message}`);
|
|
12846
|
+
const m = routeMapOf(doc, route);
|
|
12847
|
+
const target = expandDir(path, base);
|
|
12848
|
+
const existing = configuredRepos(text, route, base);
|
|
12849
|
+
const hit = existing.find((e) => e.dir === target);
|
|
12850
|
+
if (!hit) throw new ReposError(`${target} is not configured on route ${route}`);
|
|
12851
|
+
const repos = m.get("repos", true);
|
|
12852
|
+
if (isMap(repos)) {
|
|
12853
|
+
repos.delete(hit.name);
|
|
12854
|
+
const left = existing.filter((e) => e !== hit);
|
|
12855
|
+
if (left.length === 1) {
|
|
12856
|
+
const pair = m.items.find((p) => p.key.value === "repos");
|
|
12857
|
+
pair.key = doc.createNode("dir");
|
|
12858
|
+
pair.value = doc.createNode(String(repos.get(left[0].name)));
|
|
12859
|
+
} else if (left.length === 0) m.delete("repos");
|
|
12860
|
+
} else {
|
|
12861
|
+
m.delete("dir");
|
|
12862
|
+
}
|
|
12863
|
+
return { text: String(doc), name: hit.name };
|
|
12864
|
+
}
|
|
12624
12865
|
function renderBlock(m) {
|
|
12625
12866
|
return stringify2([m.toJSON()]).trimEnd();
|
|
12626
12867
|
}
|
|
@@ -12686,7 +12927,7 @@ function planRepoClone(i) {
|
|
|
12686
12927
|
}
|
|
12687
12928
|
const ship = doc.get("ship", true);
|
|
12688
12929
|
const basePath = String(m.get("reposBasePath") ?? (isMap(ship) ? ship.get("reposBasePath") : void 0) ?? "~/Crew");
|
|
12689
|
-
const target = i.dir ? expandDir(i.dir, i.base) :
|
|
12930
|
+
const target = i.dir ? expandDir(i.dir, i.base) : join21(expandDir(basePath, i.base), i.route.split("/")[0], name);
|
|
12690
12931
|
const add = planRepoAdd({ text: i.text, base: i.base, route: i.route, path: target, origin: remote, resolved: i.resolved, name });
|
|
12691
12932
|
return { ...add, remote, target };
|
|
12692
12933
|
}
|
|
@@ -12729,8 +12970,8 @@ function schedulerInstalled(plan, exists, crontab) {
|
|
|
12729
12970
|
}
|
|
12730
12971
|
|
|
12731
12972
|
// src/worktree-sweep.ts
|
|
12732
|
-
import { existsSync as
|
|
12733
|
-
import { basename 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";
|
|
12734
12975
|
function planWorktreeSweep(target, tickets, contract) {
|
|
12735
12976
|
const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
|
|
12736
12977
|
const verified = contract.statuses.verified;
|
|
@@ -12761,7 +13002,7 @@ function ticketForWorktree(w, byNumber, contract) {
|
|
|
12761
13002
|
const t = num ? byNumber.get(num) : void 0;
|
|
12762
13003
|
if (t && ticketStems(t).has(stem)) return t;
|
|
12763
13004
|
}
|
|
12764
|
-
const dir =
|
|
13005
|
+
const dir = basename9(w.path);
|
|
12765
13006
|
const m = DIR_NUMBER.exec(dir);
|
|
12766
13007
|
if (m && byNumber.has(m[1])) return byNumber.get(m[1]);
|
|
12767
13008
|
if (!w.branch) {
|
|
@@ -12777,15 +13018,15 @@ function worktreeTicketNumbers(target) {
|
|
|
12777
13018
|
const n = /-(\d+)$/.exec(BRANCH_STEM.exec(w.branch)?.[1] ?? "")?.[1];
|
|
12778
13019
|
if (n) nums.add(n);
|
|
12779
13020
|
}
|
|
12780
|
-
const m = DIR_NUMBER_SUFFIXED.exec(
|
|
13021
|
+
const m = DIR_NUMBER_SUFFIXED.exec(basename9(w.path));
|
|
12781
13022
|
if (m) nums.add(m[1]);
|
|
12782
13023
|
}
|
|
12783
13024
|
for (const d of siblingWorktreeDirs(target)) nums.add(d.number);
|
|
12784
13025
|
return [...nums];
|
|
12785
13026
|
}
|
|
12786
13027
|
function siblingWorktreeDirs(target) {
|
|
12787
|
-
const parent =
|
|
12788
|
-
const stem = `${
|
|
13028
|
+
const parent = dirname14(target.dir.replace(/[/\\]+$/, ""));
|
|
13029
|
+
const stem = `${basename9(target.dir.replace(/[/\\]+$/, ""))}-`;
|
|
12789
13030
|
const registered = new Set(worktrees(target.dir).map((w) => resolve4(w.path)));
|
|
12790
13031
|
let names;
|
|
12791
13032
|
try {
|
|
@@ -12797,7 +13038,7 @@ function siblingWorktreeDirs(target) {
|
|
|
12797
13038
|
for (const name of names) {
|
|
12798
13039
|
if (!name.startsWith(stem)) continue;
|
|
12799
13040
|
const m = DIR_NUMBER_SUFFIXED.exec(name);
|
|
12800
|
-
const path =
|
|
13041
|
+
const path = join22(parent, name);
|
|
12801
13042
|
if (m && !registered.has(resolve4(path))) out.push({ path, number: m[1] });
|
|
12802
13043
|
}
|
|
12803
13044
|
return out;
|
|
@@ -12810,7 +13051,7 @@ function orphanedWorktreeDirs(target, tickets, contract) {
|
|
|
12810
13051
|
}
|
|
12811
13052
|
function warnOrphanedWorktreeDirs(paths, log, held) {
|
|
12812
13053
|
for (const p of paths) {
|
|
12813
|
-
const number = DIR_NUMBER_SUFFIXED.exec(
|
|
13054
|
+
const number = DIR_NUMBER_SUFFIXED.exec(basename9(p))?.[1];
|
|
12814
13055
|
const when = held && number ? noteHeldWorktree(held.state, { repo: held.repo, path: p, ticket: number, status: "deployed", reason: "orphaned directory" }) : "first";
|
|
12815
13056
|
if (when) {
|
|
12816
13057
|
log.warn(`orphaned directory ${p}: its ticket is deployed but it is not a registered worktree; the sweep will not delete it \u2014 remove it by hand`, { step: "worktree" });
|
|
@@ -12832,7 +13073,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
|
|
|
12832
13073
|
let removed = 0;
|
|
12833
13074
|
let keptBranches = 0;
|
|
12834
13075
|
for (const a of actions) {
|
|
12835
|
-
if (!dryRun && base && !a.keepBranch &&
|
|
13076
|
+
if (!dryRun && base && !a.keepBranch && existsSync22(a.path) && isHandBackMerge(a.path, base)) {
|
|
12836
13077
|
if (gitOk(a.path, ["merge", "--abort"]) !== null) {
|
|
12837
13078
|
log.emit(
|
|
12838
13079
|
`abandoned the hand-back merge of ${base} in ${a.path}: ${a.ticket.issue_id} deployed without it`,
|
|
@@ -12840,7 +13081,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
|
|
|
12840
13081
|
);
|
|
12841
13082
|
}
|
|
12842
13083
|
}
|
|
12843
|
-
if (!dryRun &&
|
|
13084
|
+
if (!dryRun && existsSync22(a.path)) {
|
|
12844
13085
|
let dirty = false;
|
|
12845
13086
|
try {
|
|
12846
13087
|
dirty = status(a.path).length > 0;
|
|
@@ -12851,7 +13092,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
|
|
|
12851
13092
|
continue;
|
|
12852
13093
|
}
|
|
12853
13094
|
}
|
|
12854
|
-
if (!dryRun && a.requirePushed && a.branch &&
|
|
13095
|
+
if (!dryRun && a.requirePushed && a.branch && existsSync22(a.path) && remoteConfigured(cwd, remote) && worktreeHoldsUnpushed(a.path, remote)) {
|
|
12855
13096
|
refuse(a, "unpushed commits", `${a.branch} has commits that are not on ${remote}`);
|
|
12856
13097
|
continue;
|
|
12857
13098
|
}
|
|
@@ -12900,7 +13141,7 @@ function isHandBackMerge(path, base) {
|
|
|
12900
13141
|
if (!msgPath) return false;
|
|
12901
13142
|
let first;
|
|
12902
13143
|
try {
|
|
12903
|
-
first =
|
|
13144
|
+
first = readFileSync16(resolve4(path, msgPath), "utf8").split("\n")[0] ?? "";
|
|
12904
13145
|
} catch {
|
|
12905
13146
|
return false;
|
|
12906
13147
|
}
|
|
@@ -12954,7 +13195,7 @@ function isTicketBranch(name, base, stems) {
|
|
|
12954
13195
|
}
|
|
12955
13196
|
function sweepCandidatesExist(cwd, base) {
|
|
12956
13197
|
const linked = worktrees(cwd).slice(1);
|
|
12957
|
-
if (linked.some((w) => DIR_NUMBER_SUFFIXED.test(
|
|
13198
|
+
if (linked.some((w) => DIR_NUMBER_SUFFIXED.test(basename9(w.path)) || w.branch && BRANCH_STEM.test(w.branch))) return true;
|
|
12958
13199
|
if (siblingWorktreeDirs({ name: "", dir: cwd }).length) return true;
|
|
12959
13200
|
return localBranches(cwd).some((b) => b !== base && /-\d+(?:-.*)?$/.test(b));
|
|
12960
13201
|
}
|
|
@@ -13005,14 +13246,14 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
|
|
|
13005
13246
|
}
|
|
13006
13247
|
|
|
13007
13248
|
// src/stream-sweep.ts
|
|
13008
|
-
import { existsSync as
|
|
13009
|
-
import { join as
|
|
13249
|
+
import { existsSync as existsSync23, readdirSync as readdirSync6, rmSync as rmSync9, statSync as statSync7 } from "fs";
|
|
13250
|
+
import { join as join23 } from "path";
|
|
13010
13251
|
var DAY_MS2 = 24 * 60 * 60 * 1e3;
|
|
13011
13252
|
function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
|
|
13012
|
-
const dir =
|
|
13013
|
-
if (!
|
|
13253
|
+
const dir = join23(stateDir, "streams");
|
|
13254
|
+
if (!existsSync23(dir)) return [];
|
|
13014
13255
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
13015
|
-
return readdirSync6(dir).map((name) =>
|
|
13256
|
+
return readdirSync6(dir).map((name) => join23(dir, name)).filter((path) => {
|
|
13016
13257
|
try {
|
|
13017
13258
|
return statSync7(path).mtimeMs < cutoff;
|
|
13018
13259
|
} catch {
|
|
@@ -13038,9 +13279,9 @@ function applyStreamSweep(paths, dryRun, log) {
|
|
|
13038
13279
|
return removed;
|
|
13039
13280
|
}
|
|
13040
13281
|
function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
13041
|
-
if (!
|
|
13282
|
+
if (!existsSync23(stateDir)) return [];
|
|
13042
13283
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
13043
|
-
return readdirSync6(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) =>
|
|
13284
|
+
return readdirSync6(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join23(stateDir, name)).filter((path) => {
|
|
13044
13285
|
try {
|
|
13045
13286
|
return statSync7(path).mtimeMs < cutoff;
|
|
13046
13287
|
} catch {
|
|
@@ -13050,26 +13291,26 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
|
13050
13291
|
}
|
|
13051
13292
|
|
|
13052
13293
|
// src/cli.ts
|
|
13053
|
-
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";
|
|
13054
13295
|
import { createInterface } from "readline/promises";
|
|
13055
13296
|
import { dirname as dirOf, resolve as resolvePath } from "path";
|
|
13056
13297
|
|
|
13057
13298
|
// src/ssh-keys.ts
|
|
13058
|
-
import { execFileSync as
|
|
13059
|
-
import { existsSync as
|
|
13060
|
-
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";
|
|
13061
13302
|
var SshKeyError = class extends Error {
|
|
13062
13303
|
};
|
|
13063
13304
|
function shipSshKeyPathFor(stateDir) {
|
|
13064
|
-
return
|
|
13305
|
+
return join24(stateDir, "ssh", "id_ed25519");
|
|
13065
13306
|
}
|
|
13066
13307
|
function ensureShipSshKeypair(stateDir) {
|
|
13067
13308
|
const privateKeyPath = shipSshKeyPathFor(stateDir);
|
|
13068
13309
|
const publicKeyPath = `${privateKeyPath}.pub`;
|
|
13069
|
-
if (!
|
|
13070
|
-
|
|
13310
|
+
if (!existsSync24(privateKeyPath)) {
|
|
13311
|
+
mkdirSync11(dirname15(privateKeyPath), { recursive: true });
|
|
13071
13312
|
try {
|
|
13072
|
-
|
|
13313
|
+
execFileSync4("ssh-keygen", [
|
|
13073
13314
|
"-t",
|
|
13074
13315
|
"ed25519",
|
|
13075
13316
|
"-N",
|
|
@@ -13087,15 +13328,15 @@ function ensureShipSshKeypair(stateDir) {
|
|
|
13087
13328
|
}
|
|
13088
13329
|
chmodSync(privateKeyPath, 384);
|
|
13089
13330
|
}
|
|
13090
|
-
if (!
|
|
13331
|
+
if (!existsSync24(publicKeyPath)) {
|
|
13091
13332
|
throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
|
|
13092
13333
|
}
|
|
13093
|
-
const publicKey =
|
|
13334
|
+
const publicKey = readFileSync17(publicKeyPath, "utf8").trim();
|
|
13094
13335
|
return { privateKeyPath, publicKeyPath, publicKey };
|
|
13095
13336
|
}
|
|
13096
13337
|
function sshKeygenAvailable() {
|
|
13097
13338
|
try {
|
|
13098
|
-
|
|
13339
|
+
execFileSync4("ssh-keygen", ["-V"], { stdio: ["ignore", "ignore", "ignore"] });
|
|
13099
13340
|
return true;
|
|
13100
13341
|
} catch (e) {
|
|
13101
13342
|
return e.code !== "ENOENT";
|
|
@@ -13103,11 +13344,11 @@ function sshKeygenAvailable() {
|
|
|
13103
13344
|
}
|
|
13104
13345
|
|
|
13105
13346
|
// src/passenger-containers.ts
|
|
13106
|
-
import { execFileSync as
|
|
13347
|
+
import { execFileSync as execFileSync5 } from "child_process";
|
|
13107
13348
|
import { createHash as createHash4 } from "crypto";
|
|
13108
13349
|
var realExec = (cmd, args) => {
|
|
13109
13350
|
try {
|
|
13110
|
-
return
|
|
13351
|
+
return execFileSync5(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
13111
13352
|
} catch (e) {
|
|
13112
13353
|
const err = e;
|
|
13113
13354
|
const why = [err.stderr, err.stdout].map((x) => (x ?? "").trim()).filter(Boolean).join(" / ");
|
|
@@ -13306,9 +13547,9 @@ function syncAllPassengerCheckouts(cfg) {
|
|
|
13306
13547
|
|
|
13307
13548
|
// src/tunnel.ts
|
|
13308
13549
|
import { spawn as spawn4 } from "child_process";
|
|
13309
|
-
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";
|
|
13310
13551
|
import { randomInt } from "crypto";
|
|
13311
|
-
import { join as
|
|
13552
|
+
import { join as join25 } from "path";
|
|
13312
13553
|
var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
|
|
13313
13554
|
var defaultSpawn = (cmd, args) => spawn4(cmd, args, DEFAULT_SPAWN_OPTIONS);
|
|
13314
13555
|
var SLUG_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
@@ -13375,21 +13616,21 @@ function startTunnel(opts) {
|
|
|
13375
13616
|
};
|
|
13376
13617
|
}
|
|
13377
13618
|
function tunnelStatePath(stateDir, workspaceId) {
|
|
13378
|
-
return
|
|
13619
|
+
return join25(stateDir, "passengers", `${workspaceId}.json`);
|
|
13379
13620
|
}
|
|
13380
13621
|
function readPersistedTunnel(stateDir, workspaceId) {
|
|
13381
13622
|
const path = tunnelStatePath(stateDir, workspaceId);
|
|
13382
|
-
if (!
|
|
13623
|
+
if (!existsSync25(path)) return void 0;
|
|
13383
13624
|
try {
|
|
13384
|
-
return JSON.parse(
|
|
13625
|
+
return JSON.parse(readFileSync18(path, "utf8"));
|
|
13385
13626
|
} catch {
|
|
13386
13627
|
return void 0;
|
|
13387
13628
|
}
|
|
13388
13629
|
}
|
|
13389
13630
|
function writePersistedTunnel(stateDir, state) {
|
|
13390
13631
|
const path = tunnelStatePath(stateDir, state.workspaceId);
|
|
13391
|
-
|
|
13392
|
-
|
|
13632
|
+
mkdirSync12(join25(stateDir, "passengers"), { recursive: true });
|
|
13633
|
+
writeFileSync13(path, `${JSON.stringify(state, null, 2)}
|
|
13393
13634
|
`);
|
|
13394
13635
|
}
|
|
13395
13636
|
function updatePersistedTunnel(stateDir, workspaceId, patch2) {
|
|
@@ -13486,8 +13727,8 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
|
|
|
13486
13727
|
}
|
|
13487
13728
|
}
|
|
13488
13729
|
function readAllPersistedWorkspaceIds(stateDir) {
|
|
13489
|
-
const dir =
|
|
13490
|
-
if (!
|
|
13730
|
+
const dir = join25(stateDir, "passengers");
|
|
13731
|
+
if (!existsSync25(dir)) return [];
|
|
13491
13732
|
return readdirSync7(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
|
|
13492
13733
|
}
|
|
13493
13734
|
|
|
@@ -13608,8 +13849,8 @@ async function unregisterEndpoint(workspaceId, info, d) {
|
|
|
13608
13849
|
import { createServer as createHttpServer } from "http";
|
|
13609
13850
|
import { spawn as spawn5 } from "child_process";
|
|
13610
13851
|
import { randomBytes } from "crypto";
|
|
13611
|
-
import { existsSync as
|
|
13612
|
-
import { join as
|
|
13852
|
+
import { existsSync as existsSync26, mkdirSync as mkdirSync13, readFileSync as readFileSync19, writeFileSync as writeFileSync14, unlinkSync as unlinkSync4, readdirSync as readdirSync8 } from "fs";
|
|
13853
|
+
import { join as join26 } from "path";
|
|
13613
13854
|
|
|
13614
13855
|
// src/secret-compare.ts
|
|
13615
13856
|
import { timingSafeEqual } from "crypto";
|
|
@@ -13698,21 +13939,21 @@ var defaultSpawn2 = (cmd, args, env) => {
|
|
|
13698
13939
|
return child;
|
|
13699
13940
|
};
|
|
13700
13941
|
function syncDaemonStatePath(stateDir, workspaceId) {
|
|
13701
|
-
return
|
|
13942
|
+
return join26(stateDir, "passengers", `${workspaceId}-sync.json`);
|
|
13702
13943
|
}
|
|
13703
13944
|
function readPersistedSyncDaemon(stateDir, workspaceId) {
|
|
13704
13945
|
const path = syncDaemonStatePath(stateDir, workspaceId);
|
|
13705
|
-
if (!
|
|
13946
|
+
if (!existsSync26(path)) return void 0;
|
|
13706
13947
|
try {
|
|
13707
|
-
return JSON.parse(
|
|
13948
|
+
return JSON.parse(readFileSync19(path, "utf8"));
|
|
13708
13949
|
} catch {
|
|
13709
13950
|
return void 0;
|
|
13710
13951
|
}
|
|
13711
13952
|
}
|
|
13712
13953
|
function writePersistedSyncDaemon(stateDir, state) {
|
|
13713
13954
|
const path = syncDaemonStatePath(stateDir, state.workspaceId);
|
|
13714
|
-
|
|
13715
|
-
|
|
13955
|
+
mkdirSync13(join26(stateDir, "passengers"), { recursive: true });
|
|
13956
|
+
writeFileSync14(path, `${JSON.stringify(state, null, 2)}
|
|
13716
13957
|
`);
|
|
13717
13958
|
}
|
|
13718
13959
|
function removePersistedSyncDaemon(stateDir, workspaceId) {
|
|
@@ -13732,8 +13973,8 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
|
|
|
13732
13973
|
}
|
|
13733
13974
|
}
|
|
13734
13975
|
function readAllPersistedSyncWorkspaceIds(stateDir) {
|
|
13735
|
-
const dir =
|
|
13736
|
-
if (!
|
|
13976
|
+
const dir = join26(stateDir, "passengers");
|
|
13977
|
+
if (!existsSync26(dir)) return [];
|
|
13737
13978
|
const suffix = "-sync.json";
|
|
13738
13979
|
return readdirSync8(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
|
|
13739
13980
|
}
|
|
@@ -13790,12 +14031,12 @@ function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
|
|
|
13790
14031
|
}
|
|
13791
14032
|
|
|
13792
14033
|
// src/daemon.ts
|
|
13793
|
-
import { existsSync as
|
|
13794
|
-
import { join 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";
|
|
14035
|
+
import { join as join28 } from "path";
|
|
13795
14036
|
|
|
13796
14037
|
// src/interrupted.ts
|
|
13797
|
-
import { existsSync as
|
|
13798
|
-
import { join as
|
|
14038
|
+
import { existsSync as existsSync27, readdirSync as readdirSync9, readFileSync as readFileSync20 } from "fs";
|
|
14039
|
+
import { join as join27 } from "path";
|
|
13799
14040
|
var INTERRUPTED_MARKER = calloutHeader("NOTE", "Interrupted");
|
|
13800
14041
|
function interruptedComment(b) {
|
|
13801
14042
|
const tree = b.dirty === null ? "The worktree could not be checked." : b.dirty ? "The worktree has uncommitted changes \u2014 they are this seat's own, from that run." : "The worktree is clean.";
|
|
@@ -13814,21 +14055,21 @@ var pidAlive = (pid) => {
|
|
|
13814
14055
|
};
|
|
13815
14056
|
function runFinished(streamPath) {
|
|
13816
14057
|
const path = `${streamPath}.meta.json`;
|
|
13817
|
-
if (!
|
|
14058
|
+
if (!existsSync27(path)) return false;
|
|
13818
14059
|
try {
|
|
13819
|
-
return JSON.parse(
|
|
14060
|
+
return JSON.parse(readFileSync20(path, "utf8")).running !== true;
|
|
13820
14061
|
} catch {
|
|
13821
14062
|
return true;
|
|
13822
14063
|
}
|
|
13823
14064
|
}
|
|
13824
14065
|
function orphanedStreams(o) {
|
|
13825
|
-
const dir =
|
|
13826
|
-
if (!
|
|
14066
|
+
const dir = join27(o.stateDir, "streams");
|
|
14067
|
+
if (!existsSync27(dir)) return [];
|
|
13827
14068
|
const ownerAlive = o.isOwnerAlive ?? pidAlive;
|
|
13828
14069
|
const out = [];
|
|
13829
14070
|
for (const name of readdirSync9(dir)) {
|
|
13830
14071
|
if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
|
|
13831
|
-
const streamPath =
|
|
14072
|
+
const streamPath = join27(dir, name);
|
|
13832
14073
|
if (runFinished(streamPath)) continue;
|
|
13833
14074
|
const sidecar = readRunSidecar(streamPath);
|
|
13834
14075
|
if (!sidecar || sidecar.route !== o.routeLabel) continue;
|
|
@@ -13928,7 +14169,7 @@ function latestMtime(path) {
|
|
|
13928
14169
|
if (!stat.isDirectory()) return 0;
|
|
13929
14170
|
let latest = stat.mtimeMs;
|
|
13930
14171
|
for (const entry of readdirSync10(path, { withFileTypes: true })) {
|
|
13931
|
-
latest = Math.max(latest, latestMtime(
|
|
14172
|
+
latest = Math.max(latest, latestMtime(join28(path, entry.name)));
|
|
13932
14173
|
}
|
|
13933
14174
|
return latest;
|
|
13934
14175
|
}
|
|
@@ -13937,23 +14178,23 @@ function computeTreeSignature(paths) {
|
|
|
13937
14178
|
}
|
|
13938
14179
|
function makeStaleChecker(paths, opts = {}) {
|
|
13939
14180
|
const initial = computeTreeSignature(paths);
|
|
13940
|
-
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 &&
|
|
14181
|
+
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync28(opts.reloadSentinel);
|
|
13941
14182
|
}
|
|
13942
14183
|
function reloadSentinelFor(stateDir) {
|
|
13943
|
-
return
|
|
14184
|
+
return join28(stateDir, "reload-requested");
|
|
13944
14185
|
}
|
|
13945
14186
|
function daemonStampFor(stateDir) {
|
|
13946
|
-
return
|
|
14187
|
+
return join28(stateDir, "daemon.started");
|
|
13947
14188
|
}
|
|
13948
14189
|
function noteDaemonStarted(stateDir, now = Date.now()) {
|
|
13949
|
-
|
|
14190
|
+
mkdirSync14(stateDir, { recursive: true });
|
|
13950
14191
|
rmSync10(reloadSentinelFor(stateDir), { force: true });
|
|
13951
|
-
|
|
14192
|
+
writeFileSync15(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
|
|
13952
14193
|
`);
|
|
13953
14194
|
}
|
|
13954
14195
|
function readDaemonStarted(stateDir) {
|
|
13955
14196
|
try {
|
|
13956
|
-
const v = JSON.parse(
|
|
14197
|
+
const v = JSON.parse(readFileSync21(daemonStampFor(stateDir), "utf8")).startedAt;
|
|
13957
14198
|
return typeof v === "number" ? v : null;
|
|
13958
14199
|
} catch {
|
|
13959
14200
|
return null;
|
|
@@ -14309,7 +14550,7 @@ async function buildEnvironment(route, ship, ticket) {
|
|
|
14309
14550
|
}
|
|
14310
14551
|
|
|
14311
14552
|
// src/cli.ts
|
|
14312
|
-
var CREW_HOME = isCompiledBinary(import.meta.url) ?
|
|
14553
|
+
var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname16(process.execPath) : resolve5(dirname16(fileURLToPath(import.meta.url)), "..");
|
|
14313
14554
|
var RELEASE_LOCK_TTL_MS = 20 * 60 * 1e3;
|
|
14314
14555
|
var CYCLE_SECONDS2 = 120;
|
|
14315
14556
|
var STALE_CYCLES = 3;
|
|
@@ -14440,8 +14681,10 @@ function usage() {
|
|
|
14440
14681
|
crew status [route] [--json] paused/running state; --json for a machine reader
|
|
14441
14682
|
crew projects [--route R] [--json] the Projects rows each route can read (id, name, prefix, work mode)
|
|
14442
14683
|
crew reload make the running daemon re-read crew.yaml (--now: restart it immediately)
|
|
14443
|
-
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
|
|
14444
14686
|
crew selftest [route] [--toolchain NAME] [--json] launch each toolchain as a run would and prove it reaches the tracker
|
|
14687
|
+
crew sample-ticket [route] [--remove] [--dry-run] [--json] file one ticket on a throwaway repo that walks the dev, QA and release loop
|
|
14445
14688
|
crew ports [route] which checkout owns which ports, and what is up
|
|
14446
14689
|
crew reap [route] kill orphaned servers, drop worktrees for closed tickets
|
|
14447
14690
|
crew drop [route] NNN remove a merged ticket's worktree and branch
|
|
@@ -14453,7 +14696,7 @@ function usage() {
|
|
|
14453
14696
|
resume with no argument clears only the whole-crew pause; a role or release pause needs its own resume
|
|
14454
14697
|
crew log [route] tail the log
|
|
14455
14698
|
crew inbox [--member NAME] your tickets across every workspace (or a colleague's)
|
|
14456
|
-
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)
|
|
14457
14700
|
crew disconnect remove what connect installed for Pair (Claude MCP; Codex/Gemini instructions block and MCP entry per ship.pair)
|
|
14458
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)
|
|
14459
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)
|
|
@@ -14865,8 +15108,8 @@ if (command === "passenger-sync-daemon") {
|
|
|
14865
15108
|
}
|
|
14866
15109
|
if (host === "linux" && hasSystemd) {
|
|
14867
15110
|
const label = labelFor(CREW_HOME, "run");
|
|
14868
|
-
const staleTimer =
|
|
14869
|
-
if (
|
|
15111
|
+
const staleTimer = join29(homedir8(), ".config", "systemd", "user", `${label}.timer`);
|
|
15112
|
+
if (existsSync29(staleTimer)) {
|
|
14870
15113
|
process.stdout.write(
|
|
14871
15114
|
`removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
|
|
14872
15115
|
`
|
|
@@ -14892,7 +15135,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
14892
15135
|
const tracker = new Tracker(c, cfg.ship);
|
|
14893
15136
|
problems.push(...validateContract(tracker.contract).map((p) => `contract: ${p}`));
|
|
14894
15137
|
for (const r of await resolvedRepos(c)) {
|
|
14895
|
-
if (!
|
|
15138
|
+
if (!existsSync29(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
|
|
14896
15139
|
if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
|
|
14897
15140
|
problems.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
|
|
14898
15141
|
}
|
|
@@ -14910,7 +15153,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
14910
15153
|
}
|
|
14911
15154
|
async function doctorFix(o) {
|
|
14912
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))));
|
|
14913
|
-
const unitExists = (p) =>
|
|
15156
|
+
const unitExists = (p) => existsSync29(p);
|
|
14914
15157
|
const crontab = o.host === "linux" ? spawnSync6("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
|
|
14915
15158
|
const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
|
|
14916
15159
|
const plan = planFix({ routes, schedulerAbsent, host: o.host });
|
|
@@ -14927,7 +15170,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
14927
15170
|
for (const a of plan.actions) {
|
|
14928
15171
|
if (a.kind === "enable") {
|
|
14929
15172
|
if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
|
|
14930
|
-
|
|
15173
|
+
writeFileSync16(cfg.configFile, planEnable(readFileSync22(cfg.configFile, "utf8"), a.route));
|
|
14931
15174
|
enabledSound.add(a.route);
|
|
14932
15175
|
changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
|
|
14933
15176
|
} else {
|
|
@@ -14944,7 +15187,10 @@ ${changed2.map((c) => ` - ${c}
|
|
|
14944
15187
|
`).join("")}` : "fix: nothing changed\n");
|
|
14945
15188
|
}
|
|
14946
15189
|
async function resolvedRepos(c) {
|
|
14947
|
-
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
|
+
{
|
|
14948
15194
|
const id = repoIdForName(c, t.name);
|
|
14949
15195
|
const o = c.repoOverrides[t.name];
|
|
14950
15196
|
return {
|
|
@@ -14968,7 +15214,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
14968
15214
|
worktrees: { prefix: shipWorktreePrefixFor(c) ?? await areaWorktreePrefix(c, id), nameOverride: o?.worktreeName }
|
|
14969
15215
|
}, t.dir)
|
|
14970
15216
|
};
|
|
14971
|
-
}
|
|
15217
|
+
}
|
|
14972
15218
|
}
|
|
14973
15219
|
const worktreeLocations = async (c) => (await resolvedRepos(c)).map((r) => ({
|
|
14974
15220
|
parent: resolvePath(r.dir, ".."),
|
|
@@ -15923,7 +16169,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
15923
16169
|
process.exit(1);
|
|
15924
16170
|
}
|
|
15925
16171
|
requireArmed("run the daemon loop");
|
|
15926
|
-
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [
|
|
16172
|
+
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join29(CREW_HOME, "src"), join29(CREW_HOME, "dist"), join29(CREW_HOME, "bin")];
|
|
15927
16173
|
const configPaths = daemonConfigPaths(cfg);
|
|
15928
16174
|
watchPaths.push(...configPaths);
|
|
15929
16175
|
noteDaemonStarted(cfg.ship.stateDir);
|
|
@@ -16050,69 +16296,27 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16050
16296
|
`);
|
|
16051
16297
|
process.exit(2);
|
|
16052
16298
|
}
|
|
16053
|
-
|
|
16054
|
-
let readOnly = !isAdminRole(keyRole);
|
|
16055
|
-
let personaResult;
|
|
16056
|
-
const runPersonas = (ro) => syncPersonas(target, { userAgent: cfg.ship.userAgent, dryRun, force, readOnly: ro });
|
|
16299
|
+
let run2;
|
|
16057
16300
|
try {
|
|
16058
|
-
|
|
16059
|
-
|
|
16060
|
-
|
|
16061
|
-
|
|
16062
|
-
|
|
16063
|
-
|
|
16064
|
-
|
|
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
|
+
});
|
|
16065
16311
|
} catch (e) {
|
|
16066
|
-
if (e instanceof AgentsSyncError) {
|
|
16312
|
+
if (e instanceof AgentsSyncError || e instanceof SkillSyncError) {
|
|
16067
16313
|
process.stderr.write(`crew agents sync: ${e.message}
|
|
16068
16314
|
`);
|
|
16069
16315
|
process.exit(2);
|
|
16070
16316
|
}
|
|
16071
16317
|
throw e;
|
|
16072
16318
|
}
|
|
16073
|
-
|
|
16074
|
-
`);
|
|
16075
|
-
for (const o of personaResult.outcomes) process.stdout.write(`${describeSyncOutcome(o)}
|
|
16076
|
-
`);
|
|
16077
|
-
for (const o of personaResult.crewLinks) process.stdout.write(`${describeCrewLink(o)}
|
|
16078
|
-
`);
|
|
16079
|
-
if (personaResult.releaseNote) process.stdout.write(`${personaResult.releaseNote}
|
|
16080
|
-
`);
|
|
16081
|
-
const briefs = Object.fromEntries(ROLE_NAMES.map((r) => [r, personaDefaultPrompt(target.promptsDir, r)]));
|
|
16082
|
-
for (const w of skillMarkerWarnings(target.promptsDir, briefs)) process.stderr.write(`crew agents sync: warning: ${w}
|
|
16083
|
-
`);
|
|
16084
|
-
let skillResult;
|
|
16085
|
-
if (!readOnly) {
|
|
16086
|
-
try {
|
|
16087
|
-
skillResult = await syncSkills(target, { userAgent: cfg.ship.userAgent, dryRun, force });
|
|
16088
|
-
} catch (e) {
|
|
16089
|
-
if (isForbidden(e)) readOnly = true;
|
|
16090
|
-
else if (e instanceof SkillSyncError) {
|
|
16091
|
-
process.stderr.write(`crew agents sync: ${e.message}
|
|
16092
|
-
`);
|
|
16093
|
-
process.exit(2);
|
|
16094
|
-
} else throw e;
|
|
16095
|
-
}
|
|
16096
|
-
}
|
|
16097
|
-
for (const o of skillResult?.outcomes ?? []) process.stdout.write(`${describeSkillSyncOutcome(o)}
|
|
16098
|
-
`);
|
|
16099
|
-
if (!dryRun) {
|
|
16100
|
-
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
16101
|
-
const raw = existsSync28(resolvedPath) ? JSON.parse(readFileSync21(resolvedPath, "utf8")) : {};
|
|
16102
|
-
raw.agentPersonas = personaResult.agentPersonas;
|
|
16103
|
-
if (skillResult) raw.agentSkills = skillResult.agentSkills;
|
|
16104
|
-
if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
|
|
16105
|
-
mkdirSync14(dirOf(resolvedPath), { recursive: true });
|
|
16106
|
-
writeFileSync15(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
16107
|
-
`);
|
|
16108
|
-
}
|
|
16109
|
-
if (!dryRun) {
|
|
16110
|
-
target.resolved.agentPersonas = personaResult.agentPersonas;
|
|
16111
|
-
if (personaResult.releaseAgentId) target.resolved.releaseAgentId = personaResult.releaseAgentId;
|
|
16112
|
-
}
|
|
16113
|
-
if (readOnly) {
|
|
16114
|
-
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");
|
|
16115
|
-
}
|
|
16319
|
+
const { personaResult, skillResult } = run2;
|
|
16116
16320
|
if (flag("revoke-seat-keys")) {
|
|
16117
16321
|
for (const o of await revokeSeatKeys(target, { userAgent: cfg.ship.userAgent, dryRun })) {
|
|
16118
16322
|
process.stdout.write(`${describeRevokeOutcome(o)}
|
|
@@ -16124,9 +16328,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16124
16328
|
`);
|
|
16125
16329
|
}
|
|
16126
16330
|
}
|
|
16127
|
-
if (
|
|
16128
|
-
process.exitCode = 1;
|
|
16129
|
-
}
|
|
16331
|
+
if (run2.diverged) process.exitCode = 1;
|
|
16130
16332
|
break;
|
|
16131
16333
|
}
|
|
16132
16334
|
case "skills": {
|
|
@@ -16168,10 +16370,10 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16168
16370
|
`);
|
|
16169
16371
|
if (!dryRun) {
|
|
16170
16372
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
16171
|
-
const raw =
|
|
16373
|
+
const raw = existsSync29(resolvedPath) ? JSON.parse(readFileSync22(resolvedPath, "utf8")) : {};
|
|
16172
16374
|
raw.agentSkills = result.agentSkills;
|
|
16173
|
-
|
|
16174
|
-
|
|
16375
|
+
mkdirSync15(dirOf(resolvedPath), { recursive: true });
|
|
16376
|
+
writeFileSync16(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
16175
16377
|
`);
|
|
16176
16378
|
}
|
|
16177
16379
|
if (result.outcomes.some((o) => o.action === "diverged")) process.exitCode = 1;
|
|
@@ -16184,14 +16386,14 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16184
16386
|
process.exit(2);
|
|
16185
16387
|
};
|
|
16186
16388
|
if (sub !== "add" && sub !== "list" && sub !== "clone") usage2();
|
|
16187
|
-
const configText =
|
|
16389
|
+
const configText = readFileSync22(cfg.configFile, "utf8");
|
|
16188
16390
|
let routeName = positional[2];
|
|
16189
16391
|
let addPath = ".";
|
|
16190
16392
|
let addName;
|
|
16191
16393
|
if (sub === "list" && !routeName) usage2();
|
|
16192
16394
|
if (sub === "add") {
|
|
16193
16395
|
try {
|
|
16194
|
-
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) =>
|
|
16396
|
+
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync29(resolvePath(p.replace(/^~(?=\/|$)/, homedir8()))));
|
|
16195
16397
|
routeName = a.route;
|
|
16196
16398
|
addPath = a.path;
|
|
16197
16399
|
addName = a.name;
|
|
@@ -16220,15 +16422,15 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16220
16422
|
}
|
|
16221
16423
|
const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
|
|
16222
16424
|
let resolvedRemotes;
|
|
16223
|
-
if (
|
|
16425
|
+
if (existsSync29(resolvedFile)) {
|
|
16224
16426
|
try {
|
|
16225
|
-
resolvedRemotes = JSON.parse(
|
|
16427
|
+
resolvedRemotes = JSON.parse(readFileSync22(resolvedFile, "utf8"));
|
|
16226
16428
|
} catch {
|
|
16227
16429
|
}
|
|
16228
16430
|
}
|
|
16229
16431
|
const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
|
|
16230
16432
|
const text = configText;
|
|
16231
|
-
const base =
|
|
16433
|
+
const base = dirname16(cfg.configFile);
|
|
16232
16434
|
try {
|
|
16233
16435
|
if (sub === "list") {
|
|
16234
16436
|
const repos = configuredRepos(text, routeName, base);
|
|
@@ -16237,7 +16439,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16237
16439
|
for (const r of repos) {
|
|
16238
16440
|
const origin2 = originOf(r.dir);
|
|
16239
16441
|
const row = matchRepoName(origin2, resolvedRemotes);
|
|
16240
|
-
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)"}`;
|
|
16241
16443
|
process.stdout.write(`${r.name} ${r.dir} ${note}
|
|
16242
16444
|
`);
|
|
16243
16445
|
}
|
|
@@ -16249,9 +16451,9 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16249
16451
|
` : `${human}
|
|
16250
16452
|
`);
|
|
16251
16453
|
const plan2 = planRepoClone({ text, base, route: routeName, name: addName, resolved: resolvedRemotes, dir: value("dir") });
|
|
16252
|
-
if (
|
|
16454
|
+
if (existsSync29(plan2.target) && (existsSync29(`${plan2.target}/.git`) || readdirSync11(plan2.target).length > 0)) {
|
|
16253
16455
|
throw new ReposError(
|
|
16254
|
-
|
|
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`
|
|
16255
16457
|
);
|
|
16256
16458
|
}
|
|
16257
16459
|
if (dryRun) {
|
|
@@ -16269,12 +16471,12 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16269
16471
|
throw new ReposError(`${e.message}${authHint}
|
|
16270
16472
|
crew.yaml was not changed`);
|
|
16271
16473
|
}
|
|
16272
|
-
|
|
16474
|
+
writeFileSync16(cfg.configFile, plan2.text);
|
|
16273
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}`);
|
|
16274
16476
|
break;
|
|
16275
16477
|
}
|
|
16276
16478
|
const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir8()));
|
|
16277
|
-
const top =
|
|
16479
|
+
const top = existsSync29(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
|
|
16278
16480
|
if (!top) throw new ReposError(`${target} is not a git checkout`);
|
|
16279
16481
|
const origin = originOf(top);
|
|
16280
16482
|
const plan = planRepoAdd({
|
|
@@ -16323,15 +16525,15 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16323
16525
|
const created = await rowTracker.createRepoRow(rowPayload);
|
|
16324
16526
|
process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
|
|
16325
16527
|
`);
|
|
16326
|
-
if (
|
|
16327
|
-
const raw = JSON.parse(
|
|
16528
|
+
if (existsSync29(resolvedFile)) {
|
|
16529
|
+
const raw = JSON.parse(readFileSync22(resolvedFile, "utf8"));
|
|
16328
16530
|
raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
|
|
16329
16531
|
raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
|
|
16330
|
-
|
|
16532
|
+
writeFileSync16(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
16331
16533
|
`);
|
|
16332
16534
|
}
|
|
16333
16535
|
}
|
|
16334
|
-
|
|
16536
|
+
writeFileSync16(cfg.configFile, plan.text);
|
|
16335
16537
|
process.stdout.write(`added ${top} to ${routeName} as ${plan.mode === "dir" ? "dir" : `repos.${plan.name}`} in ${cfg.configFile}
|
|
16336
16538
|
`);
|
|
16337
16539
|
} catch (e) {
|
|
@@ -16641,10 +16843,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
16641
16843
|
}
|
|
16642
16844
|
}
|
|
16643
16845
|
let previous;
|
|
16644
|
-
const hadPreviousFile =
|
|
16846
|
+
const hadPreviousFile = existsSync29(resolvedPath);
|
|
16645
16847
|
if (hadPreviousFile) {
|
|
16646
16848
|
try {
|
|
16647
|
-
previous = JSON.parse(
|
|
16849
|
+
previous = JSON.parse(readFileSync22(resolvedPath, "utf8"));
|
|
16648
16850
|
} catch {
|
|
16649
16851
|
}
|
|
16650
16852
|
}
|
|
@@ -16752,10 +16954,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
16752
16954
|
process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
|
|
16753
16955
|
`);
|
|
16754
16956
|
} else {
|
|
16755
|
-
|
|
16957
|
+
mkdirSync15(dirname16(resolvedPath), { recursive: true });
|
|
16756
16958
|
if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
|
|
16757
16959
|
const tmpPath = `${resolvedPath}.tmp${process.pid}`;
|
|
16758
|
-
|
|
16960
|
+
writeFileSync16(tmpPath, `${JSON.stringify(resolved, null, 2)}
|
|
16759
16961
|
`);
|
|
16760
16962
|
renameSync2(tmpPath, resolvedPath);
|
|
16761
16963
|
process.stderr.write(`Resolved ids written to ${resolvedPath}
|
|
@@ -16790,8 +16992,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
16790
16992
|
process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
|
|
16791
16993
|
`);
|
|
16792
16994
|
} else {
|
|
16793
|
-
|
|
16794
|
-
|
|
16995
|
+
mkdirSync15(dirname16(apiKeyFile), { recursive: true });
|
|
16996
|
+
writeFileSync16(apiKeyFile, `${apiKeyVar}=${apiKey}
|
|
16795
16997
|
`, { mode: 384 });
|
|
16796
16998
|
process.stderr.write(`API key written to ${apiKeyFile}
|
|
16797
16999
|
`);
|
|
@@ -16800,7 +17002,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
16800
17002
|
const keyPersisted = keychainBacked || apiKeyFile !== void 0;
|
|
16801
17003
|
const wizard = firstRun;
|
|
16802
17004
|
const writeFirstRun = wizard !== void 0 && !dryRun && keyPersisted;
|
|
16803
|
-
const routeDir = value("dir") ?? (writeFirstRun ?
|
|
17005
|
+
const routeDir = value("dir") ?? (writeFirstRun ? join29(DEFAULT_REPOS_BASE_PATH, found.workspaceSlug) : void 0);
|
|
16804
17006
|
const routeBlock = renderConnection(
|
|
16805
17007
|
found,
|
|
16806
17008
|
route2,
|
|
@@ -16892,6 +17094,56 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
16892
17094
|
`
|
|
16893
17095
|
);
|
|
16894
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
|
+
}
|
|
16895
17147
|
connectEvents.emit("done", `connected ${route2}`, {
|
|
16896
17148
|
route: route2,
|
|
16897
17149
|
resolvedPath: dryRun ? void 0 : resolvedPath,
|
|
@@ -17370,8 +17622,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
17370
17622
|
process.stdout.write("crew reload: no daemon unit is running \u2014 nothing to reload (the next run reads crew.yaml fresh)\n");
|
|
17371
17623
|
break;
|
|
17372
17624
|
}
|
|
17373
|
-
|
|
17374
|
-
|
|
17625
|
+
mkdirSync15(cfg.ship.stateDir, { recursive: true });
|
|
17626
|
+
writeFileSync16(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
|
|
17375
17627
|
`);
|
|
17376
17628
|
process.stdout.write("crew reload: requested \u2014 the daemon finishes its in-flight pass, then relaunches with the new config\n");
|
|
17377
17629
|
if (flag("now") && control?.stopCommand && control.startCommand) {
|
|
@@ -17422,7 +17674,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
17422
17674
|
}
|
|
17423
17675
|
case "log": {
|
|
17424
17676
|
try {
|
|
17425
|
-
const text =
|
|
17677
|
+
const text = readFileSync22(cfg.ship.logFile, "utf8").split("\n");
|
|
17426
17678
|
process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
|
|
17427
17679
|
`);
|
|
17428
17680
|
} catch {
|
|
@@ -17431,6 +17683,121 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
17431
17683
|
}
|
|
17432
17684
|
break;
|
|
17433
17685
|
}
|
|
17686
|
+
case "sample-ticket": {
|
|
17687
|
+
const json = flag("json");
|
|
17688
|
+
const say = (event, extra, human) => process.stdout.write(json ? `${JSON.stringify({ event, ...extra })}
|
|
17689
|
+
` : `${human}
|
|
17690
|
+
`);
|
|
17691
|
+
const fail = (msg) => {
|
|
17692
|
+
if (json) process.stdout.write(`${JSON.stringify({ event: "error", message: msg })}
|
|
17693
|
+
`);
|
|
17694
|
+
else process.stderr.write(`crew sample-ticket: ${msg}
|
|
17695
|
+
`);
|
|
17696
|
+
return process.exit(2);
|
|
17697
|
+
};
|
|
17698
|
+
if (!route.resolved) fail(`route ${route.route} is not connected \u2014 run \`crew connect\` first`);
|
|
17699
|
+
const tracker = new Tracker(route, cfg.ship);
|
|
17700
|
+
const configText = readFileSync22(cfg.configFile, "utf8");
|
|
17701
|
+
const cfgBase = dirname16(cfg.configFile);
|
|
17702
|
+
const configured = configuredRepos(configText, route.route, cfgBase);
|
|
17703
|
+
const dir = configured.find((r) => r.name === SAMPLE_REPO)?.dir ?? sampleDir(configured.map((r) => r.dir), route.reposBasePath, route.route.split("/")[0]);
|
|
17704
|
+
const resolvedFile = resolvedPathFor(cfg.ship.stateDir, route.route);
|
|
17705
|
+
const rows = await tracker.repoRows();
|
|
17706
|
+
const row = rows.find((r) => r.name === SAMPLE_REPO);
|
|
17707
|
+
if (flag("remove")) {
|
|
17708
|
+
const closed = [];
|
|
17709
|
+
if (row) {
|
|
17710
|
+
const wontFix = tracker.contract.statuses.wontFix;
|
|
17711
|
+
const resolved = new Set(tracker.contract.statuses.resolved);
|
|
17712
|
+
for (const t of await tracker.ticketsForRepo(row.id)) {
|
|
17713
|
+
if (resolved.has(t.status) || !isSampleTicket(t.description)) continue;
|
|
17714
|
+
if (!dryRun) {
|
|
17715
|
+
await tracker.updateTicket(t.id, {
|
|
17716
|
+
[tracker.contract.columns.status]: wontFix,
|
|
17717
|
+
[tracker.contract.columns.resolutionNote]: "Sample ticket removed by `crew sample-ticket --remove`; obsolete."
|
|
17718
|
+
});
|
|
17719
|
+
}
|
|
17720
|
+
closed.push(t.issue_tag ?? t.issue_id);
|
|
17721
|
+
}
|
|
17722
|
+
}
|
|
17723
|
+
if (dryRun) {
|
|
17724
|
+
say("dry-run", { dir, row: row?.id ?? null, closed }, `would close ${closed.join(", ") || "no tickets"}, delete the Repos row, detach and delete ${dir}`);
|
|
17725
|
+
break;
|
|
17726
|
+
}
|
|
17727
|
+
if (configured.some((r) => r.dir === dir)) {
|
|
17728
|
+
try {
|
|
17729
|
+
writeFileSync16(cfg.configFile, planRepoRemove(configText, cfgBase, route.route, dir).text);
|
|
17730
|
+
} catch (e) {
|
|
17731
|
+
if (e instanceof ReposError) fail(e.message);
|
|
17732
|
+
throw e;
|
|
17733
|
+
}
|
|
17734
|
+
}
|
|
17735
|
+
if (row) {
|
|
17736
|
+
await tracker.deleteRepoRow(row.id);
|
|
17737
|
+
if (existsSync29(resolvedFile)) {
|
|
17738
|
+
const raw = JSON.parse(readFileSync22(resolvedFile, "utf8"));
|
|
17739
|
+
if (raw.repoNames) delete raw.repoNames[row.id];
|
|
17740
|
+
if (raw.repoRemotes) delete raw.repoRemotes[row.id];
|
|
17741
|
+
writeFileSync16(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
17742
|
+
`);
|
|
17743
|
+
}
|
|
17744
|
+
}
|
|
17745
|
+
if (existsSync29(join29(dir, ".git")) && basename10(dir) === SAMPLE_REPO) rmSync11(dir, { recursive: true, force: true });
|
|
17746
|
+
say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
|
|
17747
|
+
break;
|
|
17748
|
+
}
|
|
17749
|
+
const projectId = route.resolved.areaId ?? resolveRepoProject(await tracker.projectRows(), value("project"));
|
|
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`);
|
|
17752
|
+
if (dryRun) {
|
|
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`);
|
|
17754
|
+
break;
|
|
17755
|
+
}
|
|
17756
|
+
if (fresh) {
|
|
17757
|
+
mkdirSync15(dir, { recursive: true });
|
|
17758
|
+
writeFileSync16(join29(dir, "README.md"), SAMPLE_README);
|
|
17759
|
+
writeFileSync16(join29(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
|
|
17760
|
+
const g = (...a) => {
|
|
17761
|
+
const r = spawnSync6("git", a, { cwd: dir, encoding: "utf8" });
|
|
17762
|
+
if (r.status !== 0) fail(`git ${a[0]} failed in ${dir}: ${(r.stderr || r.stdout).trim()}`);
|
|
17763
|
+
};
|
|
17764
|
+
g("init", "-b", "main");
|
|
17765
|
+
g("add", ".");
|
|
17766
|
+
g("-c", "user.name=crew", "-c", "user.email=crew@localhost", "commit", "-m", "Initial commit");
|
|
17767
|
+
}
|
|
17768
|
+
let repoId = row?.id;
|
|
17769
|
+
if (!repoId) {
|
|
17770
|
+
const payload = planRepoRow({
|
|
17771
|
+
name: SAMPLE_REPO,
|
|
17772
|
+
origin: SAMPLE_REMOTE,
|
|
17773
|
+
projectId,
|
|
17774
|
+
spec: { platform: "unix", releaseMode: "integrate", ciProvider: "none" }
|
|
17775
|
+
});
|
|
17776
|
+
repoId = (await tracker.createRepoRow(payload)).id;
|
|
17777
|
+
if (existsSync29(resolvedFile)) {
|
|
17778
|
+
const raw = JSON.parse(readFileSync22(resolvedFile, "utf8"));
|
|
17779
|
+
raw.repoNames = { ...raw.repoNames, [repoId]: SAMPLE_REPO };
|
|
17780
|
+
raw.repoRemotes = { ...raw.repoRemotes, [repoId]: SAMPLE_REMOTE };
|
|
17781
|
+
writeFileSync16(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
17782
|
+
`);
|
|
17783
|
+
}
|
|
17784
|
+
}
|
|
17785
|
+
if (!configured.some((r) => r.dir === dir)) {
|
|
17786
|
+
try {
|
|
17787
|
+
writeFileSync16(cfg.configFile, planRepoAdd({ text: configText, base: cfgBase, route: route.route, path: dir, name: SAMPLE_REPO }).text);
|
|
17788
|
+
} catch (e) {
|
|
17789
|
+
if (e instanceof ReposError) fail(e.message);
|
|
17790
|
+
throw e;
|
|
17791
|
+
}
|
|
17792
|
+
}
|
|
17793
|
+
const ticket = await tracker.fileTicket(
|
|
17794
|
+
planSampleTicket({ projectId, repoId, approvedStatus: tracker.contract.statuses.approved }),
|
|
17795
|
+
route.resolved.seats.dev ?? ""
|
|
17796
|
+
);
|
|
17797
|
+
const tag = ticket.issue_tag ?? ticket.issue_id;
|
|
17798
|
+
say("filed", { ticket: tag, id: ticket.id, dir, repoId }, `filed ${tag} against ${dir}; follow it with \`crew watch\``);
|
|
17799
|
+
break;
|
|
17800
|
+
}
|
|
17434
17801
|
case "selftest": {
|
|
17435
17802
|
const wanted = value("toolchain");
|
|
17436
17803
|
const all = shipToolchains(cfg.ship);
|
|
@@ -17609,7 +17976,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
17609
17976
|
// `synthesis-tabl-123`. `worktreePrefix` above is kept for
|
|
17610
17977
|
// config-provenance display; this is the field that actually
|
|
17611
17978
|
// describes where a worktree lands by default.
|
|
17612
|
-
worktreeDirPattern: r.config.worktrees.name ?? (r.config.provenance["worktrees.prefix"] !== "default" ? `${r.config.worktrees.prefix}<number>` : `${
|
|
17979
|
+
worktreeDirPattern: r.config.worktrees.name ?? (r.config.provenance["worktrees.prefix"] !== "default" ? `${r.config.worktrees.prefix}<number>` : `${basename10(r.dir.replace(/[/\\]+$/, ""))}-<branch>`),
|
|
17613
17980
|
worktreeParent: resolvePath(r.dir, "..")
|
|
17614
17981
|
})),
|
|
17615
17982
|
waiting: waiting ? { ticket: waiting.ticket, since: waiting.since, streak: fairness.streak() } : null
|
|
@@ -17694,6 +18061,40 @@ watermark: ${state.watermark()}
|
|
|
17694
18061
|
}
|
|
17695
18062
|
case "doctor": {
|
|
17696
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
|
+
}
|
|
17697
18098
|
process.stdout.write(
|
|
17698
18099
|
`ship: ${cfg.ship.name} (${host})
|
|
17699
18100
|
routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
@@ -17701,7 +18102,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
17701
18102
|
);
|
|
17702
18103
|
try {
|
|
17703
18104
|
const evFile = eventFileFor(cfg.ship.stateDir);
|
|
17704
|
-
const evSize =
|
|
18105
|
+
const evSize = existsSync29(evFile) ? statSync9(evFile).size : 0;
|
|
17705
18106
|
process.stdout.write(
|
|
17706
18107
|
evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
|
|
17707
18108
|
` : "events log: ok\n"
|
|
@@ -17712,7 +18113,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
17712
18113
|
const unitPlan = planInstall(cfg.ship, CREW_HOME, host, detectSystemd(), "run");
|
|
17713
18114
|
const abandons = unitAbandonsAgents(unitPlan, (p) => {
|
|
17714
18115
|
try {
|
|
17715
|
-
return
|
|
18116
|
+
return readFileSync22(p, "utf8");
|
|
17716
18117
|
} catch {
|
|
17717
18118
|
return null;
|
|
17718
18119
|
}
|
|
@@ -17742,7 +18143,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
17742
18143
|
);
|
|
17743
18144
|
for (const r of await resolvedRepos(route)) {
|
|
17744
18145
|
const { config } = r;
|
|
17745
|
-
const worktreePattern = config.worktrees.name ?? (config.provenance["worktrees.prefix"] !== "default" ? `${config.worktrees.prefix}<number>` : `${
|
|
18146
|
+
const worktreePattern = config.worktrees.name ?? (config.provenance["worktrees.prefix"] !== "default" ? `${config.worktrees.prefix}<number>` : `${basename10(r.dir.replace(/[/\\]+$/, ""))}-${effectiveBranchTemplate(config, void 0)}`);
|
|
17746
18147
|
process.stdout.write(
|
|
17747
18148
|
`${`repo ${r.name}:`.padEnd(19)}${r.dir}
|
|
17748
18149
|
worktrees at ../${worktreePattern} (${config.provenance["worktrees.name"] ?? config.provenance["worktrees.prefix"] ?? "default"}), branch ${config.branch.name}
|
|
@@ -17917,6 +18318,8 @@ crew manifest: ${seats2.length ? seats2.map((c) => c.name).join(", ") : "non
|
|
|
17917
18318
|
if (ov.toolchain) process.stdout.write(`repo toolchain: ${repoName} \u2192 ${ov.toolchain} (binary and MCP checked above, as for every ship toolchain)
|
|
17918
18319
|
`);
|
|
17919
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));
|
|
17920
18323
|
process.stdout.write(`git: ${gitPath ? `OK (${gitPath})` : "NOT FOUND on the scheduler PATH"}
|
|
17921
18324
|
`);
|
|
17922
18325
|
if (!allBinsFound || !gitPath) {
|