@tablation/crew 0.85.0 → 0.87.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/dist/cli.js +888 -494
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -56,25 +56,107 @@ async function previewHostProblem(cfg, resolve6 = (h) => lookup(h)) {
|
|
|
56
56
|
}
|
|
57
57
|
return void 0;
|
|
58
58
|
}
|
|
59
|
+
function routePeriscopeProblems(o) {
|
|
60
|
+
if (!o.hosts) return [];
|
|
61
|
+
const out = [];
|
|
62
|
+
for (const r of o.repos) {
|
|
63
|
+
if (!r.hasPreviewHook) {
|
|
64
|
+
out.push({
|
|
65
|
+
severity: "note",
|
|
66
|
+
kind: "preview_hook_missing",
|
|
67
|
+
route: o.route,
|
|
68
|
+
repo: r.name,
|
|
69
|
+
message: `${o.route}/${r.name}: hostPeriscopes is on but ${r.name} declares no \`preview\` hook, so it cannot be opened as a periscope`
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (o.tablePresent === false) {
|
|
74
|
+
out.push({
|
|
75
|
+
severity: "blocker",
|
|
76
|
+
kind: "periscopes_table_missing",
|
|
77
|
+
route: o.route,
|
|
78
|
+
message: `${o.route}: hostPeriscopes is on but this workspace has no Periscopes table \u2014 update the Issues template and run \`crew connect\``
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
function previewHostFinding(message) {
|
|
84
|
+
return /does not resolve/.test(message) ? { severity: "attention", kind: "preview_host_unresolvable", message } : { severity: "blocker", kind: "preview_host", message };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// src/periscope-state.ts
|
|
88
|
+
import { existsSync as existsSync2, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
89
|
+
import { connect } from "net";
|
|
90
|
+
import { join } from "path";
|
|
91
|
+
var dirOf = (stateDir) => join(stateDir, "periscopes");
|
|
92
|
+
var fileOf = (stateDir, ticketRecordId) => join(dirOf(stateDir), `${ticketRecordId}.json`);
|
|
93
|
+
function writePeriscopeSidecar(stateDir, sc) {
|
|
94
|
+
mkdirSync(dirOf(stateDir), { recursive: true });
|
|
95
|
+
writeFileSync(fileOf(stateDir, sc.ticketRecordId), `${JSON.stringify(sc, null, 2)}
|
|
96
|
+
`);
|
|
97
|
+
}
|
|
98
|
+
function removePeriscopeSidecar(stateDir, ticketRecordId) {
|
|
99
|
+
rmSync(fileOf(stateDir, ticketRecordId), { force: true });
|
|
100
|
+
}
|
|
101
|
+
function readPeriscopeSidecars(stateDir) {
|
|
102
|
+
const dir = dirOf(stateDir);
|
|
103
|
+
if (!existsSync2(dir)) return [];
|
|
104
|
+
const out = [];
|
|
105
|
+
for (const name of readdirSync(dir)) {
|
|
106
|
+
if (!name.endsWith(".json")) continue;
|
|
107
|
+
try {
|
|
108
|
+
const sc = JSON.parse(readFileSync(join(dir, name), "utf8"));
|
|
109
|
+
if (sc && typeof sc.ticketRecordId === "string" && typeof sc.url === "string") out.push(sc);
|
|
110
|
+
} catch {
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return out.sort((a, b) => a.startedAt.localeCompare(b.startedAt));
|
|
114
|
+
}
|
|
115
|
+
function urlPort(url) {
|
|
116
|
+
try {
|
|
117
|
+
const u = new URL(url);
|
|
118
|
+
return u.port ? Number(u.port) : u.protocol === "https:" ? 443 : 80;
|
|
119
|
+
} catch {
|
|
120
|
+
return void 0;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function portListening(port, timeoutMs = 500) {
|
|
124
|
+
return new Promise((resolve6) => {
|
|
125
|
+
const s = connect({ port, host: "127.0.0.1" });
|
|
126
|
+
const done = (ok) => {
|
|
127
|
+
s.destroy();
|
|
128
|
+
resolve6(ok);
|
|
129
|
+
};
|
|
130
|
+
s.setTimeout(timeoutMs, () => done(false));
|
|
131
|
+
s.once("connect", () => done(true));
|
|
132
|
+
s.once("error", () => done(false));
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
async function periscopesForStatus(stateDir, probe = portListening) {
|
|
136
|
+
return Promise.all(readPeriscopeSidecars(stateDir).map(async (sc) => {
|
|
137
|
+
const port = urlPort(sc.url);
|
|
138
|
+
return { ...sc, serving: port === void 0 ? false : await probe(port) };
|
|
139
|
+
}));
|
|
140
|
+
}
|
|
59
141
|
|
|
60
142
|
// src/config.ts
|
|
61
|
-
import { readFileSync as
|
|
62
|
-
import { resolve, dirname as dirname3, isAbsolute, join as
|
|
143
|
+
import { readFileSync as readFileSync9, existsSync as existsSync8 } from "fs";
|
|
144
|
+
import { resolve, dirname as dirname3, isAbsolute, join as join10, basename as basename3 } from "path";
|
|
63
145
|
import { homedir as homedir6, tmpdir } from "os";
|
|
64
146
|
import { parse as parse2 } from "yaml";
|
|
65
147
|
|
|
66
148
|
// src/events.ts
|
|
67
149
|
import {
|
|
68
150
|
appendFileSync,
|
|
69
|
-
mkdirSync,
|
|
70
|
-
existsSync as
|
|
151
|
+
mkdirSync as mkdirSync2,
|
|
152
|
+
existsSync as existsSync3,
|
|
71
153
|
statSync,
|
|
72
154
|
renameSync,
|
|
73
155
|
openSync,
|
|
74
156
|
closeSync,
|
|
75
|
-
rmSync
|
|
157
|
+
rmSync as rmSync2
|
|
76
158
|
} from "fs";
|
|
77
|
-
import { join, dirname } from "path";
|
|
159
|
+
import { join as join2, dirname } from "path";
|
|
78
160
|
function render(e) {
|
|
79
161
|
const scope = e.role ? `${e.step}[${e.role}]` : e.step;
|
|
80
162
|
const ticket = e.ticket ? ` ${e.ticket}` : "";
|
|
@@ -86,7 +168,7 @@ var ROTATED_EVENTS_RE = /^events\.\d{8}T\d{6}Z(?:-\d+)?\.jsonl$/;
|
|
|
86
168
|
var ROTATE_LOCK_STALE_MS = 6e4;
|
|
87
169
|
function rotateEventsFile(file, maxBytes, now = /* @__PURE__ */ new Date()) {
|
|
88
170
|
try {
|
|
89
|
-
if (!
|
|
171
|
+
if (!existsSync3(file) || statSync(file).size <= maxBytes) return void 0;
|
|
90
172
|
} catch {
|
|
91
173
|
return void 0;
|
|
92
174
|
}
|
|
@@ -97,7 +179,7 @@ function rotateEventsFile(file, maxBytes, now = /* @__PURE__ */ new Date()) {
|
|
|
97
179
|
} catch {
|
|
98
180
|
try {
|
|
99
181
|
if (Date.now() - statSync(lock).mtimeMs < ROTATE_LOCK_STALE_MS) return void 0;
|
|
100
|
-
|
|
182
|
+
rmSync2(lock, { force: true });
|
|
101
183
|
closeSync(openSync(lock, "wx"));
|
|
102
184
|
} catch {
|
|
103
185
|
return void 0;
|
|
@@ -106,12 +188,12 @@ function rotateEventsFile(file, maxBytes, now = /* @__PURE__ */ new Date()) {
|
|
|
106
188
|
try {
|
|
107
189
|
if (statSync(file).size <= maxBytes) return void 0;
|
|
108
190
|
const stamp = now.toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
|
|
109
|
-
let dest =
|
|
110
|
-
for (let n = 1;
|
|
191
|
+
let dest = join2(dirname(file), `events.${stamp}.jsonl`);
|
|
192
|
+
for (let n = 1; existsSync3(dest); n++) dest = join2(dirname(file), `events.${stamp}-${n}.jsonl`);
|
|
111
193
|
renameSync(file, dest);
|
|
112
194
|
return dest;
|
|
113
195
|
} finally {
|
|
114
|
-
|
|
196
|
+
rmSync2(lock, { force: true });
|
|
115
197
|
}
|
|
116
198
|
} catch {
|
|
117
199
|
return void 0;
|
|
@@ -130,7 +212,7 @@ var Emitter = class _Emitter {
|
|
|
130
212
|
for (const f of [opts.eventFile, opts.logFile]) {
|
|
131
213
|
if (!f) continue;
|
|
132
214
|
try {
|
|
133
|
-
|
|
215
|
+
mkdirSync2(dirname(f), { recursive: true });
|
|
134
216
|
} catch {
|
|
135
217
|
}
|
|
136
218
|
}
|
|
@@ -206,18 +288,18 @@ var Emitter = class _Emitter {
|
|
|
206
288
|
}
|
|
207
289
|
}
|
|
208
290
|
};
|
|
209
|
-
var eventFileFor = (stateDir) =>
|
|
291
|
+
var eventFileFor = (stateDir) => join2(stateDir, "events.jsonl");
|
|
210
292
|
|
|
211
293
|
// src/pair-install.ts
|
|
212
|
-
import { existsSync as
|
|
294
|
+
import { existsSync as existsSync6, rmSync as rmSync5, writeFileSync as writeFileSync5 } from "fs";
|
|
213
295
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
214
296
|
import { homedir as homedir5 } from "os";
|
|
215
|
-
import { join as
|
|
297
|
+
import { join as join8 } from "path";
|
|
216
298
|
|
|
217
299
|
// src/adapters/claude.ts
|
|
218
|
-
import { readFileSync as
|
|
300
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
219
301
|
import { homedir as homedir3 } from "os";
|
|
220
|
-
import { join as
|
|
302
|
+
import { join as join6 } from "path";
|
|
221
303
|
|
|
222
304
|
// src/stream.ts
|
|
223
305
|
function toolTarget(name, input) {
|
|
@@ -276,19 +358,19 @@ function extractResult(line) {
|
|
|
276
358
|
|
|
277
359
|
// src/claude-mcp.ts
|
|
278
360
|
import { spawnSync } from "child_process";
|
|
279
|
-
import { readFileSync as
|
|
361
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
280
362
|
import { homedir } from "os";
|
|
281
|
-
import { join as
|
|
363
|
+
import { join as join4 } from "path";
|
|
282
364
|
|
|
283
365
|
// src/mcp-registrations.ts
|
|
284
366
|
import { createHash } from "crypto";
|
|
285
|
-
import { mkdirSync as
|
|
286
|
-
import { join as
|
|
287
|
-
var fileFor = (stateDir) =>
|
|
367
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
368
|
+
import { join as join3 } from "path";
|
|
369
|
+
var fileFor = (stateDir) => join3(stateDir, "mcp-registrations.json");
|
|
288
370
|
var keyFingerprint = (key) => createHash("sha256").update(key).digest("hex").slice(0, 8);
|
|
289
371
|
function readMcpRegistrations(stateDir) {
|
|
290
372
|
try {
|
|
291
|
-
const j = JSON.parse(
|
|
373
|
+
const j = JSON.parse(readFileSync3(fileFor(stateDir), "utf8"));
|
|
292
374
|
return Array.isArray(j) ? j : [];
|
|
293
375
|
} catch {
|
|
294
376
|
return [];
|
|
@@ -296,8 +378,8 @@ function readMcpRegistrations(stateDir) {
|
|
|
296
378
|
}
|
|
297
379
|
function recordMcpRegistration(stateDir, r, now = /* @__PURE__ */ new Date()) {
|
|
298
380
|
const rest = readMcpRegistrations(stateDir).filter((x) => !(x.harness === r.harness && x.scope === r.scope && x.name === r.name));
|
|
299
|
-
|
|
300
|
-
|
|
381
|
+
mkdirSync3(stateDir, { recursive: true });
|
|
382
|
+
writeFileSync2(fileFor(stateDir), `${JSON.stringify([...rest, { ...r, at: now.toISOString() }], null, 2)}
|
|
301
383
|
`, { mode: 384 });
|
|
302
384
|
}
|
|
303
385
|
function findMcpRegistration(stateDir, r) {
|
|
@@ -320,7 +402,7 @@ function runClaude(bin, args) {
|
|
|
320
402
|
}
|
|
321
403
|
function userScopeEntry(home) {
|
|
322
404
|
try {
|
|
323
|
-
const j = JSON.parse(
|
|
405
|
+
const j = JSON.parse(readFileSync4(join4(home, ".claude.json"), "utf8"));
|
|
324
406
|
return j.mcpServers?.[MCP_SERVER_NAME];
|
|
325
407
|
} catch {
|
|
326
408
|
return void 0;
|
|
@@ -404,8 +486,8 @@ function removeClaudeMcp(o) {
|
|
|
404
486
|
}
|
|
405
487
|
|
|
406
488
|
// src/launcher.ts
|
|
407
|
-
import { basename, join as
|
|
408
|
-
import { existsSync as
|
|
489
|
+
import { basename, join as join5 } from "path";
|
|
490
|
+
import { existsSync as existsSync4, readFileSync as readFileSync5, readdirSync as readdirSync2 } from "fs";
|
|
409
491
|
import { homedir as homedir2 } from "os";
|
|
410
492
|
function isOriLauncher(spec) {
|
|
411
493
|
return !!spec.launcher && basename(spec.launcher) === "ori";
|
|
@@ -414,7 +496,7 @@ function isOmlxLauncher(spec) {
|
|
|
414
496
|
return !!spec.launcher && basename(spec.launcher) === "omlx";
|
|
415
497
|
}
|
|
416
498
|
function defaultOmlxBin(home = homedir2()) {
|
|
417
|
-
return
|
|
499
|
+
return join5(home, ".omlx", "bin", "omlx");
|
|
418
500
|
}
|
|
419
501
|
function launcherExecutable(spec, home) {
|
|
420
502
|
if (!spec.launcher) return void 0;
|
|
@@ -460,15 +542,15 @@ function interpretOmlxProbe(status2) {
|
|
|
460
542
|
function omlxModelPresent(model, dirs) {
|
|
461
543
|
return dirs.some((d) => {
|
|
462
544
|
try {
|
|
463
|
-
return
|
|
545
|
+
return readdirSync2(d).includes(model);
|
|
464
546
|
} catch {
|
|
465
547
|
return false;
|
|
466
548
|
}
|
|
467
549
|
});
|
|
468
550
|
}
|
|
469
551
|
function readOmlxSettings(home = homedir2()) {
|
|
470
|
-
const p =
|
|
471
|
-
return
|
|
552
|
+
const p = join5(home, ".omlx", "settings.json");
|
|
553
|
+
return existsSync4(p) ? parseOmlxSettings(readFileSync5(p, "utf8")) : void 0;
|
|
472
554
|
}
|
|
473
555
|
|
|
474
556
|
// src/capability.ts
|
|
@@ -526,7 +608,7 @@ function describeClaudeAuth(spec, env = process.env, home = homedir3()) {
|
|
|
526
608
|
if (spec.auth === "subscription") return "subscription (settings override)";
|
|
527
609
|
let fromSettings;
|
|
528
610
|
try {
|
|
529
|
-
const parsed = JSON.parse(
|
|
611
|
+
const parsed = JSON.parse(readFileSync6(join6(home, ".claude", "settings.json"), "utf8"));
|
|
530
612
|
const e = parsed.env ?? {};
|
|
531
613
|
const on = (k) => e[k] !== void 0 && e[k] !== "" && e[k] !== "0";
|
|
532
614
|
if (on("CLAUDE_CODE_USE_VERTEX")) fromSettings = "vertex from ~/.claude/settings.json";
|
|
@@ -622,11 +704,11 @@ var claudeAdapter = {
|
|
|
622
704
|
// src/adapters/codex.ts
|
|
623
705
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
624
706
|
import { homedir as homedir4 } from "os";
|
|
625
|
-
import { join as
|
|
626
|
-
import { rmSync as
|
|
707
|
+
import { join as join7 } from "path";
|
|
708
|
+
import { rmSync as rmSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
627
709
|
|
|
628
710
|
// src/pair-files.ts
|
|
629
|
-
import { existsSync as
|
|
711
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync3, rmSync as rmSync3 } from "fs";
|
|
630
712
|
import { dirname as dirname2 } from "path";
|
|
631
713
|
var PAIR_BLOCK_START = "<!-- crew:pair start -->";
|
|
632
714
|
var PAIR_BLOCK_END = "<!-- crew:pair end -->";
|
|
@@ -667,11 +749,11 @@ function removePairBlock(text) {
|
|
|
667
749
|
return text.replace(new RegExp(`\\n*${BLOCK_RE.source}`), "\n").replace(/^\n+/, "").replace(/\n{3,}/g, "\n\n");
|
|
668
750
|
}
|
|
669
751
|
function readIfExists(path) {
|
|
670
|
-
return
|
|
752
|
+
return existsSync5(path) ? readFileSync7(path, "utf8") : void 0;
|
|
671
753
|
}
|
|
672
754
|
function writeFileMkdir(path, text) {
|
|
673
|
-
|
|
674
|
-
|
|
755
|
+
mkdirSync4(dirname2(path), { recursive: true });
|
|
756
|
+
writeFileSync3(path, text);
|
|
675
757
|
}
|
|
676
758
|
function installBlockFile(path, harness, dryRun) {
|
|
677
759
|
const before = readIfExists(path) ?? "";
|
|
@@ -686,8 +768,8 @@ function removeBlockFile(path, dryRun) {
|
|
|
686
768
|
if (before === void 0 || !hasPairBlock(before)) return false;
|
|
687
769
|
if (dryRun) return true;
|
|
688
770
|
const after = removePairBlock(before);
|
|
689
|
-
if (after.trim() === "")
|
|
690
|
-
else
|
|
771
|
+
if (after.trim() === "") rmSync3(path);
|
|
772
|
+
else writeFileSync3(path, after);
|
|
691
773
|
return true;
|
|
692
774
|
}
|
|
693
775
|
function blockVerdict(path, harness) {
|
|
@@ -814,8 +896,8 @@ function codexTable(url) {
|
|
|
814
896
|
return [`[mcp_servers.${SERVER}]`, `url = ${tomlString(url)}`, `bearer_token_env_var = ${tomlString(PAIR_KEY_VAR)}`];
|
|
815
897
|
}
|
|
816
898
|
var codexPaths = (home) => ({
|
|
817
|
-
instructions:
|
|
818
|
-
config:
|
|
899
|
+
instructions: join7(home, ".codex", "AGENTS.md"),
|
|
900
|
+
config: join7(home, ".codex", "config.toml")
|
|
819
901
|
});
|
|
820
902
|
function codexMcpState(config, url) {
|
|
821
903
|
const lines = config.split("\n");
|
|
@@ -865,8 +947,8 @@ var codexPair = {
|
|
|
865
947
|
const kept = st.state === "different" ? `; left the existing ${SERVER} server alone \u2014 ${st.detail}` : "";
|
|
866
948
|
if (hadMcp && !o.dryRun) {
|
|
867
949
|
const after = removeTomlTable(config, `mcp_servers.${SERVER}`);
|
|
868
|
-
if (after === "")
|
|
869
|
-
else
|
|
950
|
+
if (after === "") rmSync4(p.config);
|
|
951
|
+
else writeFileSync4(p.config, after);
|
|
870
952
|
}
|
|
871
953
|
const what = hadBlock && hadMcp ? `the Pair block and ${SERVER} server` : hadBlock ? "the Pair block" : `the ${SERVER} server`;
|
|
872
954
|
return hadBlock || hadMcp ? `pair(codex): ${o.dryRun ? "(dry run) would remove" : "removed"} ${what}${kept}` : `pair(codex): nothing to remove${kept}`;
|
|
@@ -962,8 +1044,8 @@ var codexAdapter = {
|
|
|
962
1044
|
|
|
963
1045
|
// src/pair-install.ts
|
|
964
1046
|
var geminiPaths = (home) => ({
|
|
965
|
-
instructions:
|
|
966
|
-
config:
|
|
1047
|
+
instructions: join8(home, ".gemini", "GEMINI.md"),
|
|
1048
|
+
config: join8(home, ".gemini", "settings.json")
|
|
967
1049
|
});
|
|
968
1050
|
function readGeminiSettings(path) {
|
|
969
1051
|
const text = readIfExists(path);
|
|
@@ -1050,8 +1132,8 @@ var geminiPair = {
|
|
|
1050
1132
|
const next = { ...settings };
|
|
1051
1133
|
if (Object.keys(rest).length > 0) next.mcpServers = rest;
|
|
1052
1134
|
else delete next.mcpServers;
|
|
1053
|
-
if (Object.keys(next).length === 0)
|
|
1054
|
-
else
|
|
1135
|
+
if (Object.keys(next).length === 0) rmSync5(p.config);
|
|
1136
|
+
else writeFileSync5(p.config, `${JSON.stringify(next, null, 2)}
|
|
1055
1137
|
`);
|
|
1056
1138
|
}
|
|
1057
1139
|
const what = hadBlock && hadMcp ? `the Pair block and ${SERVER} server` : hadBlock ? "the Pair block" : `the ${SERVER} server`;
|
|
@@ -1062,9 +1144,9 @@ var VSCODE_FLAVOURS = ["Code", "Code - Insiders"];
|
|
|
1062
1144
|
function vscodeUserDirs(o) {
|
|
1063
1145
|
const home = o.home ?? homedir5();
|
|
1064
1146
|
const platform2 = o.platform ?? process.platform;
|
|
1065
|
-
const root = platform2 === "darwin" ?
|
|
1066
|
-
const all = VSCODE_FLAVOURS.map((f) =>
|
|
1067
|
-
const present = all.filter((d) =>
|
|
1147
|
+
const root = platform2 === "darwin" ? join8(home, "Library", "Application Support") : platform2 === "win32" ? process.env.APPDATA ?? join8(home, "AppData", "Roaming") : join8(home, ".config");
|
|
1148
|
+
const all = VSCODE_FLAVOURS.map((f) => join8(root, f, "User"));
|
|
1149
|
+
const present = all.filter((d) => existsSync6(d));
|
|
1068
1150
|
return present.length > 0 ? present : [all[0]];
|
|
1069
1151
|
}
|
|
1070
1152
|
var COPILOT_FRONTMATTER = '---\napplyTo: "**"\n---\n\n';
|
|
@@ -1101,8 +1183,8 @@ function removeCopilotInstructions(path, dryRun) {
|
|
|
1101
1183
|
if (before === void 0 || !hasPairBlock(before)) return false;
|
|
1102
1184
|
if (dryRun) return true;
|
|
1103
1185
|
const after = removePairBlock(before);
|
|
1104
|
-
if (after.trim() === "" || after.trim() === COPILOT_FRONTMATTER.trim())
|
|
1105
|
-
else
|
|
1186
|
+
if (after.trim() === "" || after.trim() === COPILOT_FRONTMATTER.trim()) rmSync5(path);
|
|
1187
|
+
else writeFileSync5(path, after);
|
|
1106
1188
|
return true;
|
|
1107
1189
|
}
|
|
1108
1190
|
var copilotPair = {
|
|
@@ -1111,8 +1193,8 @@ var copilotPair = {
|
|
|
1111
1193
|
installPair(o) {
|
|
1112
1194
|
const lines = [];
|
|
1113
1195
|
for (const dir of vscodeUserDirs(o)) {
|
|
1114
|
-
const mcpPath =
|
|
1115
|
-
const instrPath =
|
|
1196
|
+
const mcpPath = join8(dir, "mcp.json");
|
|
1197
|
+
const instrPath = join8(dir, "prompts", "crew-pair.instructions.md");
|
|
1116
1198
|
const settings = readVscodeMcp(mcpPath);
|
|
1117
1199
|
if (!settings) {
|
|
1118
1200
|
lines.push(`pair(copilot): ${mcpPath} is not a plain JSON object (comments are not supported) \u2014 fix it by hand; nothing was written there`);
|
|
@@ -1140,8 +1222,8 @@ var copilotPair = {
|
|
|
1140
1222
|
},
|
|
1141
1223
|
verifyPair(o) {
|
|
1142
1224
|
const verdicts = vscodeUserDirs(o).map((dir) => {
|
|
1143
|
-
const instrPath =
|
|
1144
|
-
const mcpPath =
|
|
1225
|
+
const instrPath = join8(dir, "prompts", "crew-pair.instructions.md");
|
|
1226
|
+
const mcpPath = join8(dir, "mcp.json");
|
|
1145
1227
|
const b = blockVerdict(instrPath, "copilot");
|
|
1146
1228
|
if (!b.ok) return b;
|
|
1147
1229
|
const settings = readVscodeMcp(mcpPath);
|
|
@@ -1156,8 +1238,8 @@ var copilotPair = {
|
|
|
1156
1238
|
removePair(o) {
|
|
1157
1239
|
const lines = [];
|
|
1158
1240
|
for (const dir of vscodeUserDirs(o)) {
|
|
1159
|
-
const mcpPath =
|
|
1160
|
-
const hadBlock = removeCopilotInstructions(
|
|
1241
|
+
const mcpPath = join8(dir, "mcp.json");
|
|
1242
|
+
const hadBlock = removeCopilotInstructions(join8(dir, "prompts", "crew-pair.instructions.md"), o.dryRun);
|
|
1161
1243
|
const settings = readVscodeMcp(mcpPath);
|
|
1162
1244
|
const st = settings ? copilotMcpState(settings, o.url) : { state: "missing" };
|
|
1163
1245
|
const hadMcp = st.state === "registered";
|
|
@@ -1167,8 +1249,8 @@ var copilotPair = {
|
|
|
1167
1249
|
const next = { ...settings };
|
|
1168
1250
|
if (Object.keys(rest).length > 0) next.servers = rest;
|
|
1169
1251
|
else delete next.servers;
|
|
1170
|
-
if (Object.keys(next).length === 0)
|
|
1171
|
-
else
|
|
1252
|
+
if (Object.keys(next).length === 0) rmSync5(mcpPath);
|
|
1253
|
+
else writeFileSync5(mcpPath, `${JSON.stringify(next, null, 2)}
|
|
1172
1254
|
`);
|
|
1173
1255
|
}
|
|
1174
1256
|
const what = hadBlock && hadMcp ? `the Pair instructions and ${SERVER} server` : hadBlock ? "the Pair instructions" : `the ${SERVER} server`;
|
|
@@ -1456,8 +1538,8 @@ function explain(ship, required) {
|
|
|
1456
1538
|
}
|
|
1457
1539
|
|
|
1458
1540
|
// src/repo-config.ts
|
|
1459
|
-
import { readFileSync as
|
|
1460
|
-
import { join as
|
|
1541
|
+
import { readFileSync as readFileSync8, existsSync as existsSync7 } from "fs";
|
|
1542
|
+
import { join as join9, basename as basename2 } from "path";
|
|
1461
1543
|
import { parse } from "yaml";
|
|
1462
1544
|
var SPEC_VERSION = 1;
|
|
1463
1545
|
var REPO_CONFIG_FILENAMES = [".crew.yaml", ".crew.yml", ".crew.json"];
|
|
@@ -1541,8 +1623,8 @@ var MODES = ["local", "integrate", "ci_manual", "ci_auto", "external"];
|
|
|
1541
1623
|
var PROVIDERS = ["github", "buildkite", "other", "none"];
|
|
1542
1624
|
function findRepoConfig(dir) {
|
|
1543
1625
|
for (const name of REPO_CONFIG_FILENAMES) {
|
|
1544
|
-
const p =
|
|
1545
|
-
if (
|
|
1626
|
+
const p = join9(dir, name);
|
|
1627
|
+
if (existsSync7(p)) return p;
|
|
1546
1628
|
}
|
|
1547
1629
|
return null;
|
|
1548
1630
|
}
|
|
@@ -1780,7 +1862,7 @@ function parseRepoConfig(text, file) {
|
|
|
1780
1862
|
function loadRepoConfig(dir) {
|
|
1781
1863
|
const file = findRepoConfig(dir);
|
|
1782
1864
|
if (!file) return null;
|
|
1783
|
-
return parseRepoConfig(
|
|
1865
|
+
return parseRepoConfig(readFileSync8(file, "utf8"), file);
|
|
1784
1866
|
}
|
|
1785
1867
|
function hookLabel(cfg, hook2) {
|
|
1786
1868
|
return cfg.labels[hook2] ?? cfg.hooks[hook2] ?? hook2;
|
|
@@ -2241,11 +2323,11 @@ function expand(p, base) {
|
|
|
2241
2323
|
return isAbsolute(out) ? out : resolve(base, out);
|
|
2242
2324
|
}
|
|
2243
2325
|
function resolvePromptsDir(promptSet, crewHome, base) {
|
|
2244
|
-
if (!promptSet) return
|
|
2326
|
+
if (!promptSet) return join10(crewHome, "prompts", "default");
|
|
2245
2327
|
if (promptSet.includes("/") || promptSet.startsWith(".") || promptSet.startsWith("~")) {
|
|
2246
2328
|
return expand(promptSet, base);
|
|
2247
2329
|
}
|
|
2248
|
-
return
|
|
2330
|
+
return join10(crewHome, "prompts", promptSet);
|
|
2249
2331
|
}
|
|
2250
2332
|
var Missing = class {
|
|
2251
2333
|
paths = [];
|
|
@@ -2269,7 +2351,7 @@ var Missing = class {
|
|
|
2269
2351
|
};
|
|
2270
2352
|
function crewVersion(crewHome) {
|
|
2271
2353
|
try {
|
|
2272
|
-
const pkg = JSON.parse(
|
|
2354
|
+
const pkg = JSON.parse(readFileSync9(join10(crewHome, "package.json"), "utf8"));
|
|
2273
2355
|
return pkg.version ?? "0.0.0";
|
|
2274
2356
|
} catch {
|
|
2275
2357
|
return "0.0.0";
|
|
@@ -2279,26 +2361,26 @@ function configSearchPath(crewHome) {
|
|
|
2279
2361
|
if (process.env.CREW_CONFIG) return [resolve(process.env.CREW_CONFIG)];
|
|
2280
2362
|
const xdg = process.env.XDG_CONFIG_HOME;
|
|
2281
2363
|
return [
|
|
2282
|
-
...xdg ? [
|
|
2283
|
-
|
|
2364
|
+
...xdg ? [join10(xdg, "crew", "crew.yaml")] : [],
|
|
2365
|
+
join10(homedir6(), ".config", "crew", "crew.yaml"),
|
|
2284
2366
|
resolve(crewHome, "crew.yaml")
|
|
2285
2367
|
];
|
|
2286
2368
|
}
|
|
2287
2369
|
function findConfigFile(crewHome) {
|
|
2288
2370
|
const candidates = configSearchPath(crewHome);
|
|
2289
|
-
return candidates.find((c) =>
|
|
2371
|
+
return candidates.find((c) => existsSync8(c)) ?? candidates[candidates.length - 1];
|
|
2290
2372
|
}
|
|
2291
|
-
var defaultConfigPath = () =>
|
|
2373
|
+
var defaultConfigPath = () => join10(process.env.XDG_CONFIG_HOME ?? join10(homedir6(), ".config"), "crew", "crew.yaml");
|
|
2292
2374
|
function routeSlug(route) {
|
|
2293
2375
|
return route.replace(/\//g, "-");
|
|
2294
2376
|
}
|
|
2295
2377
|
function resolvedPathFor(stateDir, route) {
|
|
2296
2378
|
const [workspace, project] = splitRoute(route);
|
|
2297
|
-
return
|
|
2379
|
+
return join10(stateDir, "resolved", workspace, `${project}.json`);
|
|
2298
2380
|
}
|
|
2299
2381
|
function apiKeyPathFor(stateDir, route) {
|
|
2300
2382
|
const [workspace, project] = splitRoute(route);
|
|
2301
|
-
return
|
|
2383
|
+
return join10(stateDir, "keys", workspace, `${project}.env`);
|
|
2302
2384
|
}
|
|
2303
2385
|
function splitRoute(route) {
|
|
2304
2386
|
const parts = route.split("/");
|
|
@@ -2309,9 +2391,9 @@ function splitRoute(route) {
|
|
|
2309
2391
|
}
|
|
2310
2392
|
function readResolvedFile(stateDir, route) {
|
|
2311
2393
|
const path = resolvedPathFor(stateDir, route);
|
|
2312
|
-
if (!
|
|
2394
|
+
if (!existsSync8(path)) return void 0;
|
|
2313
2395
|
try {
|
|
2314
|
-
return JSON.parse(
|
|
2396
|
+
return JSON.parse(readFileSync9(path, "utf8"));
|
|
2315
2397
|
} catch (e) {
|
|
2316
2398
|
throw new ConfigError(`${path}: not valid JSON \u2014 ${e.message}`);
|
|
2317
2399
|
}
|
|
@@ -2371,18 +2453,18 @@ function parseResolved(raw, m, where) {
|
|
|
2371
2453
|
}
|
|
2372
2454
|
function loadConfig(crewHome, configFile, opts = {}) {
|
|
2373
2455
|
const file = configFile ?? findConfigFile(crewHome);
|
|
2374
|
-
if (opts.text === void 0 && !
|
|
2456
|
+
if (opts.text === void 0 && !existsSync8(file)) {
|
|
2375
2457
|
throw new ConfigError(
|
|
2376
2458
|
`no config found. Looked in:
|
|
2377
2459
|
` + configSearchPath(crewHome).map((c) => ` - ${c}`).join("\n") + `
|
|
2378
2460
|
|
|
2379
2461
|
Create the first of those:
|
|
2380
|
-
mkdir -p ${dirname3(defaultConfigPath())} && cp ${
|
|
2462
|
+
mkdir -p ${dirname3(defaultConfigPath())} && cp ${join10(crewHome, "crew.example.yaml")} ${defaultConfigPath()}`
|
|
2381
2463
|
);
|
|
2382
2464
|
}
|
|
2383
2465
|
let raw;
|
|
2384
2466
|
try {
|
|
2385
|
-
raw = parse2(opts.text ??
|
|
2467
|
+
raw = parse2(opts.text ?? readFileSync9(file, "utf8"));
|
|
2386
2468
|
} catch (e) {
|
|
2387
2469
|
throw new ConfigError(`${file}: not valid YAML \u2014 ${e.message}`);
|
|
2388
2470
|
}
|
|
@@ -2492,7 +2574,7 @@ Create the first of those:
|
|
|
2492
2574
|
useNvm: shipRaw.useNvm !== false,
|
|
2493
2575
|
nvmSh: shipRaw.nvmSh,
|
|
2494
2576
|
stateDir,
|
|
2495
|
-
logFile: expand(shipRaw.logFile ??
|
|
2577
|
+
logFile: expand(shipRaw.logFile ?? join10(tmpdir(), "crew.log"), base),
|
|
2496
2578
|
userAgent: shipRaw.userAgent ?? `Mozilla/5.0 CrewAgent/${crewVersion(crewHome)}`,
|
|
2497
2579
|
maxConcurrentAgents,
|
|
2498
2580
|
maxConcurrentAgentsSet,
|
|
@@ -2652,7 +2734,7 @@ function parseOneRoute(c, where, file, base, stateDir, raw, missing, crewHome, t
|
|
|
2652
2734
|
const hasRepos = Object.keys(repoDirs).length > 0;
|
|
2653
2735
|
const reposBasePath = expand(String(c?.reposBasePath ?? raw.ship?.reposBasePath ?? DEFAULT_REPOS_BASE_PATH), base);
|
|
2654
2736
|
const dirIsCloneBase = !c?.dir && !hasRepos && !Array.isArray(c?.repos);
|
|
2655
|
-
const dir = c?.dir ? expand(String(c.dir), base) : hasRepos ? Object.values(repoDirs)[0] : dirIsCloneBase ?
|
|
2737
|
+
const dir = c?.dir ? expand(String(c.dir), base) : hasRepos ? Object.values(repoDirs)[0] : dirIsCloneBase ? join10(reposBasePath, splitRoute(route)[0]) : "";
|
|
2656
2738
|
const weight = c?.weight === void 0 ? void 0 : Number(c.weight);
|
|
2657
2739
|
if (weight !== void 0 && !(Number.isFinite(weight) && weight > 0)) {
|
|
2658
2740
|
missing.add(`${where}.weight must be a positive number (got "${c?.weight}")`);
|
|
@@ -2736,8 +2818,8 @@ function repoIdForName(r, name) {
|
|
|
2736
2818
|
}
|
|
2737
2819
|
function resolveApiKey(r) {
|
|
2738
2820
|
if (r.apiKey) return r.apiKey;
|
|
2739
|
-
if (r.apiKeyFile && r.apiKeyVar &&
|
|
2740
|
-
for (const line of
|
|
2821
|
+
if (r.apiKeyFile && r.apiKeyVar && existsSync8(r.apiKeyFile)) {
|
|
2822
|
+
for (const line of readFileSync9(r.apiKeyFile, "utf8").split("\n")) {
|
|
2741
2823
|
const m = line.match(new RegExp(`^${r.apiKeyVar}=(.*)$`));
|
|
2742
2824
|
if (m) return (m[1] ?? "").trim().replace(/^["']|["']$/g, "");
|
|
2743
2825
|
}
|
|
@@ -2750,9 +2832,9 @@ function resolveApiKey(r) {
|
|
|
2750
2832
|
}
|
|
2751
2833
|
function credentialSource(r) {
|
|
2752
2834
|
if (r.apiKey) return KEYCHAIN_ELIGIBLE_ROUTES.has(r) ? "keychain" : "inline";
|
|
2753
|
-
if (r.apiKeyFile && r.apiKeyVar &&
|
|
2835
|
+
if (r.apiKeyFile && r.apiKeyVar && existsSync8(r.apiKeyFile)) {
|
|
2754
2836
|
const re = new RegExp(`^${r.apiKeyVar}=`);
|
|
2755
|
-
if (
|
|
2837
|
+
if (readFileSync9(r.apiKeyFile, "utf8").split("\n").some((l) => re.test(l))) return "apiKeyFile";
|
|
2756
2838
|
}
|
|
2757
2839
|
return process.env.CREW_API_KEY?.trim() ? "CREW_API_KEY" : "none";
|
|
2758
2840
|
}
|
|
@@ -2808,7 +2890,7 @@ function configuredMembers(rt) {
|
|
|
2808
2890
|
return out;
|
|
2809
2891
|
}
|
|
2810
2892
|
function defaultRepoDir(basePath, workspace, repoName) {
|
|
2811
|
-
return
|
|
2893
|
+
return join10(basePath, workspace, repoName);
|
|
2812
2894
|
}
|
|
2813
2895
|
function reposOf(r) {
|
|
2814
2896
|
const named = Object.entries(r.repos);
|
|
@@ -2841,7 +2923,7 @@ function passengerRepoTargets(r) {
|
|
|
2841
2923
|
return reposOf(r).filter((t) => r.repoOverrides[t.name]?.hostPassengers !== false);
|
|
2842
2924
|
}
|
|
2843
2925
|
function passengerCheckoutDir(basePath, workspace, repoName) {
|
|
2844
|
-
return
|
|
2926
|
+
return join10(basePath, workspace, ".passenger-checkouts", repoName);
|
|
2845
2927
|
}
|
|
2846
2928
|
function passengerCheckoutTargets(r) {
|
|
2847
2929
|
if (!r.hostPassengers) return [];
|
|
@@ -3128,9 +3210,9 @@ function recoveredFailureHeadlines(o, route) {
|
|
|
3128
3210
|
}
|
|
3129
3211
|
|
|
3130
3212
|
// src/install.ts
|
|
3131
|
-
import { dirname as dirname4, join as
|
|
3213
|
+
import { dirname as dirname4, join as join11, extname } from "path";
|
|
3132
3214
|
import { homedir as homedir7 } from "os";
|
|
3133
|
-
import { accessSync, constants, existsSync as
|
|
3215
|
+
import { accessSync, constants, existsSync as existsSync9, mkdirSync as mkdirSync5, readdirSync as readdirSync3, readFileSync as readFileSync10, statSync as statSync2, writeFileSync as writeFileSync6, unlinkSync, realpathSync } from "fs";
|
|
3134
3216
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
3135
3217
|
import { createHash as createHash2 } from "crypto";
|
|
3136
3218
|
|
|
@@ -3142,7 +3224,7 @@ function isCompiledBinary(moduleUrl) {
|
|
|
3142
3224
|
// src/install.ts
|
|
3143
3225
|
function crewInvocation(crewHome) {
|
|
3144
3226
|
if (isCompiledBinary(import.meta.url)) return [process.execPath];
|
|
3145
|
-
return [process.execPath,
|
|
3227
|
+
return [process.execPath, join11(crewHome, "bin", "crew")];
|
|
3146
3228
|
}
|
|
3147
3229
|
var INTERVAL_SECONDS = 120;
|
|
3148
3230
|
function labelFor(crewHome, job) {
|
|
@@ -3180,7 +3262,7 @@ function pathFor(ship) {
|
|
|
3180
3262
|
return parts.join(":");
|
|
3181
3263
|
}
|
|
3182
3264
|
function findOnPath(bin, searchPath) {
|
|
3183
|
-
const candidates = bin.includes("/") ? [bin] : searchPath.split(":").filter(Boolean).map((dir) =>
|
|
3265
|
+
const candidates = bin.includes("/") ? [bin] : searchPath.split(":").filter(Boolean).map((dir) => join11(dir, bin));
|
|
3184
3266
|
for (const candidate of candidates) {
|
|
3185
3267
|
try {
|
|
3186
3268
|
accessSync(candidate, constants.X_OK);
|
|
@@ -3190,7 +3272,7 @@ function findOnPath(bin, searchPath) {
|
|
|
3190
3272
|
}
|
|
3191
3273
|
return void 0;
|
|
3192
3274
|
}
|
|
3193
|
-
var COMMON_DOCKER_DIRS = ["/usr/local/bin", "/opt/homebrew/bin",
|
|
3275
|
+
var COMMON_DOCKER_DIRS = ["/usr/local/bin", "/opt/homebrew/bin", join11(homedir7(), ".docker", "bin")].join(":");
|
|
3194
3276
|
function dockerPathProblem(ship, shellPath = process.env.PATH ?? "") {
|
|
3195
3277
|
const schedulerPath = pathFor(ship);
|
|
3196
3278
|
if (findOnPath("docker", schedulerPath)) return void 0;
|
|
@@ -3202,7 +3284,7 @@ function xmlEscape(s) {
|
|
|
3202
3284
|
}
|
|
3203
3285
|
function planLaunchd(ship, crewHome, job) {
|
|
3204
3286
|
const label = labelFor(crewHome, job);
|
|
3205
|
-
const unitPath =
|
|
3287
|
+
const unitPath = join11(homedir7(), "Library", "LaunchAgents", `${label}.plist`);
|
|
3206
3288
|
const crewLog = ship.logFile;
|
|
3207
3289
|
const schedulerLog = schedulerLogFor(crewLog, job);
|
|
3208
3290
|
const persistent = job === "run";
|
|
@@ -3256,8 +3338,8 @@ ${scheduleKeys}
|
|
|
3256
3338
|
}
|
|
3257
3339
|
function planSystemd(ship, crewHome, job) {
|
|
3258
3340
|
const label = labelFor(crewHome, job);
|
|
3259
|
-
const unitDir =
|
|
3260
|
-
const servicePath =
|
|
3341
|
+
const unitDir = join11(homedir7(), ".config", "systemd", "user");
|
|
3342
|
+
const servicePath = join11(unitDir, `${label}.service`);
|
|
3261
3343
|
const crewLog = ship.logFile;
|
|
3262
3344
|
const schedulerLog = schedulerLogFor(crewLog, job);
|
|
3263
3345
|
const description = job === "release" ? "Tablation crew \u2014 release" : job === "passengers" ? "Tablation crew \u2014 Host Passengers sync" : "Tablation crew \u2014 persistent supervisor loop";
|
|
@@ -3297,7 +3379,7 @@ WantedBy=default.target
|
|
|
3297
3379
|
schedulerLog
|
|
3298
3380
|
};
|
|
3299
3381
|
}
|
|
3300
|
-
const timerPath =
|
|
3382
|
+
const timerPath = join11(unitDir, `${label}.timer`);
|
|
3301
3383
|
const service = `# Written by \`crew install\` \u2014 re-run it rather than hand-editing this file.
|
|
3302
3384
|
[Unit]
|
|
3303
3385
|
Description=${description}
|
|
@@ -3371,7 +3453,7 @@ function planInstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
|
|
|
3371
3453
|
function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
|
|
3372
3454
|
const label = labelFor(crewHome, job);
|
|
3373
3455
|
if (hostShip === "macos") {
|
|
3374
|
-
const unitPath =
|
|
3456
|
+
const unitPath = join11(homedir7(), "Library", "LaunchAgents", `${label}.plist`);
|
|
3375
3457
|
return {
|
|
3376
3458
|
mechanism: "launchd",
|
|
3377
3459
|
unitPaths: [unitPath],
|
|
@@ -3381,18 +3463,18 @@ function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
|
|
|
3381
3463
|
};
|
|
3382
3464
|
}
|
|
3383
3465
|
if (hostShip === "linux" && hasSystemd) {
|
|
3384
|
-
const unitDir =
|
|
3466
|
+
const unitDir = join11(homedir7(), ".config", "systemd", "user");
|
|
3385
3467
|
if (job === "run") {
|
|
3386
3468
|
return {
|
|
3387
3469
|
mechanism: "systemd",
|
|
3388
|
-
unitPaths: [
|
|
3470
|
+
unitPaths: [join11(unitDir, `${label}.service`)],
|
|
3389
3471
|
crontabLine: null,
|
|
3390
3472
|
unloadCommands: [["systemctl", "--user", "disable", "--now", `${label}.service`]]
|
|
3391
3473
|
};
|
|
3392
3474
|
}
|
|
3393
3475
|
return {
|
|
3394
3476
|
mechanism: "systemd",
|
|
3395
|
-
unitPaths: [
|
|
3477
|
+
unitPaths: [join11(unitDir, `${label}.service`), join11(unitDir, `${label}.timer`)],
|
|
3396
3478
|
crontabLine: null,
|
|
3397
3479
|
unloadCommands: [["systemctl", "--user", "disable", "--now", `${label}.timer`]]
|
|
3398
3480
|
};
|
|
@@ -3410,7 +3492,7 @@ function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
|
|
|
3410
3492
|
function planDaemonControl(crewHome, hostShip, hasSystemd) {
|
|
3411
3493
|
const label = labelFor(crewHome, "run");
|
|
3412
3494
|
if (hostShip === "macos") {
|
|
3413
|
-
const unitPath =
|
|
3495
|
+
const unitPath = join11(homedir7(), "Library", "LaunchAgents", `${label}.plist`);
|
|
3414
3496
|
return {
|
|
3415
3497
|
mechanism: "launchd",
|
|
3416
3498
|
startCommand: ["launchctl", "load", unitPath],
|
|
@@ -3450,8 +3532,8 @@ async function applyInstall(plan, crewHome, dryRun, log) {
|
|
|
3450
3532
|
log.emit(`would write ${path}`);
|
|
3451
3533
|
continue;
|
|
3452
3534
|
}
|
|
3453
|
-
|
|
3454
|
-
|
|
3535
|
+
mkdirSync5(dirname4(path), { recursive: true });
|
|
3536
|
+
writeFileSync6(path, plan.unitContent[path]);
|
|
3455
3537
|
log.emit(`wrote ${path}`);
|
|
3456
3538
|
}
|
|
3457
3539
|
if (plan.crontabLine) {
|
|
@@ -3505,7 +3587,7 @@ async function applyUninstall(plan, crewHome, dryRun, log) {
|
|
|
3505
3587
|
}
|
|
3506
3588
|
}
|
|
3507
3589
|
for (const path of plan.unitPaths) {
|
|
3508
|
-
if (!
|
|
3590
|
+
if (!existsSync9(path)) continue;
|
|
3509
3591
|
if (dryRun) {
|
|
3510
3592
|
log.emit(`would remove ${path}`);
|
|
3511
3593
|
continue;
|
|
@@ -3524,7 +3606,7 @@ async function applyUninstall(plan, crewHome, dryRun, log) {
|
|
|
3524
3606
|
}
|
|
3525
3607
|
var CREW_LAUNCHD_PREFIX = "com.tablation.crew";
|
|
3526
3608
|
function launchAgentsDir() {
|
|
3527
|
-
return
|
|
3609
|
+
return join11(homedir7(), "Library", "LaunchAgents");
|
|
3528
3610
|
}
|
|
3529
3611
|
function xmlUnescape(s) {
|
|
3530
3612
|
return s.replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&");
|
|
@@ -3541,25 +3623,25 @@ function loadedCrewLabels() {
|
|
|
3541
3623
|
return [...parseLaunchctlList(list.stdout ?? "").keys()].filter((l) => l.startsWith(CREW_LAUNCHD_PREFIX));
|
|
3542
3624
|
}
|
|
3543
3625
|
function listLaunchdUnits(dir = launchAgentsDir(), loaded = []) {
|
|
3544
|
-
const files =
|
|
3626
|
+
const files = existsSync9(dir) ? readdirSync3(dir) : [];
|
|
3545
3627
|
const units = files.filter((f) => f.startsWith(CREW_LAUNCHD_PREFIX) && f.endsWith(".plist")).sort().map((f) => {
|
|
3546
|
-
const plistPath =
|
|
3628
|
+
const plistPath = join11(dir, f);
|
|
3547
3629
|
let program = null;
|
|
3548
3630
|
try {
|
|
3549
|
-
program = programFromPlist(
|
|
3631
|
+
program = programFromPlist(readFileSync10(plistPath, "utf8"));
|
|
3550
3632
|
} catch {
|
|
3551
3633
|
}
|
|
3552
3634
|
return {
|
|
3553
3635
|
label: f.slice(0, -".plist".length),
|
|
3554
3636
|
plistPath,
|
|
3555
3637
|
program,
|
|
3556
|
-
programExists: program !== null &&
|
|
3638
|
+
programExists: program !== null && existsSync9(program)
|
|
3557
3639
|
};
|
|
3558
3640
|
});
|
|
3559
3641
|
const seen = new Set(units.map((u) => u.label));
|
|
3560
3642
|
for (const label of [...loaded].sort()) {
|
|
3561
3643
|
if (!label.startsWith(CREW_LAUNCHD_PREFIX) || seen.has(label)) continue;
|
|
3562
|
-
units.push({ label, plistPath:
|
|
3644
|
+
units.push({ label, plistPath: join11(dir, `${label}.plist`), program: null, programExists: false, orphan: true });
|
|
3563
3645
|
}
|
|
3564
3646
|
return units;
|
|
3565
3647
|
}
|
|
@@ -3572,9 +3654,9 @@ function removeLaunchdUnit(unit, dryRun, log) {
|
|
|
3572
3654
|
log.emit(`would unload and remove ${unit.label}`);
|
|
3573
3655
|
return;
|
|
3574
3656
|
}
|
|
3575
|
-
if (
|
|
3657
|
+
if (existsSync9(unit.plistPath)) spawnSync4("launchctl", ["unload", unit.plistPath], { encoding: "utf8" });
|
|
3576
3658
|
spawnSync4("launchctl", ["remove", unit.label], { encoding: "utf8" });
|
|
3577
|
-
if (
|
|
3659
|
+
if (existsSync9(unit.plistPath)) unlinkSync(unit.plistPath);
|
|
3578
3660
|
log.emit(`removed ${unit.label} (${unit.plistPath})`);
|
|
3579
3661
|
}
|
|
3580
3662
|
function clearForeignLaunchdUnits(crewHome, replace, dryRun, log, dir, loaded) {
|
|
@@ -3652,24 +3734,24 @@ function detectSystemd() {
|
|
|
3652
3734
|
}
|
|
3653
3735
|
|
|
3654
3736
|
// src/ship-attention.ts
|
|
3655
|
-
import { existsSync as
|
|
3737
|
+
import { existsSync as existsSync11, statfsSync } from "fs";
|
|
3656
3738
|
|
|
3657
3739
|
// src/git.ts
|
|
3658
3740
|
import { execFileSync } from "child_process";
|
|
3659
3741
|
import {
|
|
3660
|
-
existsSync as
|
|
3661
|
-
mkdirSync as
|
|
3742
|
+
existsSync as existsSync10,
|
|
3743
|
+
mkdirSync as mkdirSync6,
|
|
3662
3744
|
openSync as openSync2,
|
|
3663
3745
|
closeSync as closeSync2,
|
|
3664
3746
|
writeSync,
|
|
3665
3747
|
statSync as statSync3,
|
|
3666
3748
|
unlinkSync as unlinkSync2,
|
|
3667
|
-
readFileSync as
|
|
3668
|
-
writeFileSync as
|
|
3669
|
-
readdirSync as
|
|
3670
|
-
rmSync as
|
|
3749
|
+
readFileSync as readFileSync11,
|
|
3750
|
+
writeFileSync as writeFileSync7,
|
|
3751
|
+
readdirSync as readdirSync4,
|
|
3752
|
+
rmSync as rmSync6
|
|
3671
3753
|
} from "fs";
|
|
3672
|
-
import { dirname as dirname5, basename as basename4, join as
|
|
3754
|
+
import { dirname as dirname5, basename as basename4, join as join12 } from "path";
|
|
3673
3755
|
var GitError = class extends Error {
|
|
3674
3756
|
};
|
|
3675
3757
|
function streamText(x) {
|
|
@@ -4050,7 +4132,7 @@ function recoverStrandedReleaseCheckouts(cwd) {
|
|
|
4050
4132
|
else if (line.startsWith("branch refs/heads/crew/release-") && path) {
|
|
4051
4133
|
gitOk(cwd, ["worktree", "remove", "--force", path]);
|
|
4052
4134
|
const root = dirname5(path);
|
|
4053
|
-
if (basename4(root).startsWith("crew-release-"))
|
|
4135
|
+
if (basename4(root).startsWith("crew-release-")) rmSync6(root, { recursive: true, force: true });
|
|
4054
4136
|
}
|
|
4055
4137
|
}
|
|
4056
4138
|
const stranded = (gitOk(cwd, ["for-each-ref", "--format=%(refname:short)", "refs/heads/crew/release-*"]) ?? "").split("\n").filter(Boolean);
|
|
@@ -4061,18 +4143,18 @@ function sweepStaleReleaseRoots(tmp, now = Date.now(), maxAgeMs = 60 * 60 * 1e3)
|
|
|
4061
4143
|
const removed = [];
|
|
4062
4144
|
let names;
|
|
4063
4145
|
try {
|
|
4064
|
-
names =
|
|
4146
|
+
names = readdirSync4(tmp);
|
|
4065
4147
|
} catch {
|
|
4066
4148
|
return removed;
|
|
4067
4149
|
}
|
|
4068
4150
|
for (const name of names) {
|
|
4069
4151
|
if (!name.startsWith("crew-release-")) continue;
|
|
4070
|
-
const root =
|
|
4152
|
+
const root = join12(tmp, name);
|
|
4071
4153
|
try {
|
|
4072
4154
|
const st = statSync3(root);
|
|
4073
4155
|
if (!st.isDirectory() || now - st.mtimeMs < maxAgeMs) continue;
|
|
4074
|
-
if (
|
|
4075
|
-
|
|
4156
|
+
if (existsSync10(join12(root, "checkout"))) continue;
|
|
4157
|
+
rmSync6(root, { recursive: true, force: true });
|
|
4076
4158
|
removed.push(name);
|
|
4077
4159
|
} catch {
|
|
4078
4160
|
}
|
|
@@ -4117,13 +4199,13 @@ function originUrlOf(dir, remoteName) {
|
|
|
4117
4199
|
}
|
|
4118
4200
|
}
|
|
4119
4201
|
function ensureRepoCheckout(dir, remote, opts = {}) {
|
|
4120
|
-
if (
|
|
4202
|
+
if (existsSync10(`${dir}/.git`)) return false;
|
|
4121
4203
|
if (!remote) {
|
|
4122
4204
|
throw new GitError(
|
|
4123
4205
|
`${dir} does not exist and this repo has no discovered remote to clone from \u2014 re-run \`crew connect\`, or check out the repository yourself and point \`repos:\` at it`
|
|
4124
4206
|
);
|
|
4125
4207
|
}
|
|
4126
|
-
|
|
4208
|
+
mkdirSync6(dirname5(dir), { recursive: true });
|
|
4127
4209
|
const url = cloneUrlFor(remote, opts);
|
|
4128
4210
|
try {
|
|
4129
4211
|
execFileSync("git", ["clone", url, dir], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
@@ -4148,7 +4230,7 @@ function sleepSync(ms2) {
|
|
|
4148
4230
|
}
|
|
4149
4231
|
function acquireCheckoutLock(dir, timeoutMs = 3e4) {
|
|
4150
4232
|
const lockPath = checkoutLockPath(dir);
|
|
4151
|
-
|
|
4233
|
+
mkdirSync6(dirname5(lockPath), { recursive: true });
|
|
4152
4234
|
const deadline = Date.now() + timeoutMs;
|
|
4153
4235
|
for (; ; ) {
|
|
4154
4236
|
try {
|
|
@@ -4185,17 +4267,17 @@ function acquireCheckoutLock(dir, timeoutMs = 3e4) {
|
|
|
4185
4267
|
function syncPassengerCheckout(dir, githubRemote, gitRemoteName, base, clone = {}) {
|
|
4186
4268
|
const release = acquireCheckoutLock(dir);
|
|
4187
4269
|
try {
|
|
4188
|
-
if (!
|
|
4270
|
+
if (!existsSync10(`${dir}/.git`) && githubRemote) {
|
|
4189
4271
|
const url = cloneUrlFor(githubRemote, clone);
|
|
4190
4272
|
const marker = cloneFailureMarker(dir);
|
|
4191
|
-
if (
|
|
4192
|
-
return { action: "clone-failed", detail:
|
|
4273
|
+
if (existsSync10(marker) && readFileSync11(marker, "utf8").split("\n")[0] === url) {
|
|
4274
|
+
return { action: "clone-failed", detail: readFileSync11(marker, "utf8").split("\n").slice(1).join("\n"), repeat: true };
|
|
4193
4275
|
}
|
|
4194
4276
|
try {
|
|
4195
4277
|
ensureRepoCheckout(dir, githubRemote, clone);
|
|
4196
4278
|
} catch (e) {
|
|
4197
4279
|
if (!(e instanceof GitError)) throw e;
|
|
4198
|
-
|
|
4280
|
+
writeFileSync7(marker, `${url}
|
|
4199
4281
|
${e.message}`);
|
|
4200
4282
|
return { action: "clone-failed", detail: e.message, repeat: false };
|
|
4201
4283
|
}
|
|
@@ -4402,7 +4484,7 @@ function noteHeldWorktree(state, h, nowMs = Date.now()) {
|
|
|
4402
4484
|
function reconcileHeldWorktrees(state, repo, log, stillRefused) {
|
|
4403
4485
|
for (const i of state.shipAttention().previous()) {
|
|
4404
4486
|
if (i.kind !== "worktree_held" || !i.held || i.held.repo !== repo) continue;
|
|
4405
|
-
const gone = !
|
|
4487
|
+
const gone = !existsSync11(i.held.path);
|
|
4406
4488
|
if (!gone && !(stillRefused && !stillRefused.has(i.held.path))) continue;
|
|
4407
4489
|
clearShipAttention(state, i.key);
|
|
4408
4490
|
log.emit(
|
|
@@ -4630,30 +4712,30 @@ function roleCapWarnings(ship) {
|
|
|
4630
4712
|
|
|
4631
4713
|
// src/selftest.ts
|
|
4632
4714
|
import { spawn as spawn3 } from "child_process";
|
|
4633
|
-
import { existsSync as
|
|
4715
|
+
import { existsSync as existsSync20, mkdirSync as mkdirSync11, mkdtempSync, readFileSync as readFileSync18, rmSync as rmSync10, writeFileSync as writeFileSync12 } from "fs";
|
|
4634
4716
|
import { tmpdir as tmpdir2 } from "os";
|
|
4635
|
-
import { join as
|
|
4717
|
+
import { join as join21 } from "path";
|
|
4636
4718
|
|
|
4637
4719
|
// src/agent.ts
|
|
4638
4720
|
import { spawn as spawn2, spawnSync as spawnSync5 } from "child_process";
|
|
4639
|
-
import { readFileSync as
|
|
4721
|
+
import { readFileSync as readFileSync17, existsSync as existsSync19, statSync as statSync5, mkdirSync as mkdirSync10, writeFileSync as writeFileSync11, createWriteStream, openSync as openSync3, closeSync as closeSync3, readSync } from "fs";
|
|
4640
4722
|
import { StringDecoder } from "string_decoder";
|
|
4641
4723
|
import { finished } from "stream/promises";
|
|
4642
|
-
import { join as
|
|
4724
|
+
import { join as join20, dirname as dirname7, basename as basename7 } from "path";
|
|
4643
4725
|
import { createHash as createHash3 } from "crypto";
|
|
4644
4726
|
|
|
4645
4727
|
// src/state.ts
|
|
4646
|
-
import { readFileSync as
|
|
4647
|
-
import { join as
|
|
4728
|
+
import { readFileSync as readFileSync12, writeFileSync as writeFileSync8, existsSync as existsSync12, mkdirSync as mkdirSync7, rmSync as rmSync7, statSync as statSync4, readdirSync as readdirSync5 } from "fs";
|
|
4729
|
+
import { join as join13 } from "path";
|
|
4648
4730
|
var EPOCH = "1970-01-01T00:00:00.000Z";
|
|
4649
4731
|
var State = class _State {
|
|
4650
4732
|
dir;
|
|
4651
4733
|
constructor(dir) {
|
|
4652
4734
|
this.dir = dir;
|
|
4653
|
-
|
|
4735
|
+
mkdirSync7(dir, { recursive: true });
|
|
4654
4736
|
}
|
|
4655
4737
|
path(...parts) {
|
|
4656
|
-
return
|
|
4738
|
+
return join13(this.dir, ...parts);
|
|
4657
4739
|
}
|
|
4658
4740
|
/**
|
|
4659
4741
|
* The timestamp every "new since last poll" check is measured against.
|
|
@@ -4664,14 +4746,14 @@ var State = class _State {
|
|
|
4664
4746
|
*/
|
|
4665
4747
|
watermark() {
|
|
4666
4748
|
try {
|
|
4667
|
-
const v =
|
|
4749
|
+
const v = readFileSync12(this.path(".poll-watermark"), "utf8").trim();
|
|
4668
4750
|
return v || EPOCH;
|
|
4669
4751
|
} catch {
|
|
4670
4752
|
return EPOCH;
|
|
4671
4753
|
}
|
|
4672
4754
|
}
|
|
4673
4755
|
setWatermark(at) {
|
|
4674
|
-
|
|
4756
|
+
writeFileSync8(this.path(".poll-watermark"), `${at}
|
|
4675
4757
|
`);
|
|
4676
4758
|
}
|
|
4677
4759
|
/**
|
|
@@ -4689,19 +4771,19 @@ var State = class _State {
|
|
|
4689
4771
|
}
|
|
4690
4772
|
/** Whole-crew pause, and the per-role sentinels one level down. */
|
|
4691
4773
|
isPaused() {
|
|
4692
|
-
return
|
|
4774
|
+
return existsSync12(this.path(".crew-paused"));
|
|
4693
4775
|
}
|
|
4694
4776
|
isRolePaused(role) {
|
|
4695
|
-
return
|
|
4777
|
+
return existsSync12(this.path(`.role-paused-${role}`));
|
|
4696
4778
|
}
|
|
4697
4779
|
pausedRoles(roles) {
|
|
4698
4780
|
return new Set(roles.filter((r) => this.isRolePaused(r)));
|
|
4699
4781
|
}
|
|
4700
4782
|
pause(role) {
|
|
4701
|
-
|
|
4783
|
+
writeFileSync8(role ? this.path(`.role-paused-${role}`) : this.path(".crew-paused"), "");
|
|
4702
4784
|
}
|
|
4703
4785
|
resume(role) {
|
|
4704
|
-
|
|
4786
|
+
rmSync7(role ? this.path(`.role-paused-${role}`) : this.path(".crew-paused"), { force: true });
|
|
4705
4787
|
}
|
|
4706
4788
|
/**
|
|
4707
4789
|
* The release phase (merge, deploy, release) has its own sentinel, one
|
|
@@ -4709,11 +4791,11 @@ var State = class _State {
|
|
|
4709
4791
|
* while agents keep working.
|
|
4710
4792
|
*/
|
|
4711
4793
|
isReleasePaused() {
|
|
4712
|
-
return
|
|
4794
|
+
return existsSync12(this.path(".release-paused"));
|
|
4713
4795
|
}
|
|
4714
4796
|
setReleasePaused(paused) {
|
|
4715
|
-
if (paused)
|
|
4716
|
-
else
|
|
4797
|
+
if (paused) writeFileSync8(this.path(".release-paused"), "");
|
|
4798
|
+
else rmSync7(this.path(".release-paused"), { force: true });
|
|
4717
4799
|
}
|
|
4718
4800
|
/**
|
|
4719
4801
|
* Why a release cycle must not run right now, or null when it may. Every
|
|
@@ -4750,7 +4832,7 @@ var State = class _State {
|
|
|
4750
4832
|
const name = _State.safe(rawName);
|
|
4751
4833
|
const file = this.path(`.${name}.lock`);
|
|
4752
4834
|
try {
|
|
4753
|
-
const existing = Number.parseInt(
|
|
4835
|
+
const existing = Number.parseInt(readFileSync12(file, "utf8").trim(), 10);
|
|
4754
4836
|
if (Number.isFinite(existing) && existing > 0) {
|
|
4755
4837
|
try {
|
|
4756
4838
|
process.kill(existing, 0);
|
|
@@ -4760,13 +4842,13 @@ var State = class _State {
|
|
|
4760
4842
|
}
|
|
4761
4843
|
} catch {
|
|
4762
4844
|
}
|
|
4763
|
-
|
|
4845
|
+
writeFileSync8(file, `${process.pid}
|
|
4764
4846
|
`);
|
|
4765
4847
|
return {
|
|
4766
4848
|
ok: true,
|
|
4767
4849
|
release: () => {
|
|
4768
4850
|
try {
|
|
4769
|
-
|
|
4851
|
+
rmSync7(file, { force: true });
|
|
4770
4852
|
} catch {
|
|
4771
4853
|
}
|
|
4772
4854
|
}
|
|
@@ -4780,7 +4862,7 @@ var State = class _State {
|
|
|
4780
4862
|
releaseInProgress() {
|
|
4781
4863
|
let names;
|
|
4782
4864
|
try {
|
|
4783
|
-
names =
|
|
4865
|
+
names = readdirSync5(this.dir);
|
|
4784
4866
|
} catch {
|
|
4785
4867
|
return false;
|
|
4786
4868
|
}
|
|
@@ -4788,7 +4870,7 @@ var State = class _State {
|
|
|
4788
4870
|
if (!/^\.release-.+\.lock$/.test(n)) continue;
|
|
4789
4871
|
let pid = 0;
|
|
4790
4872
|
try {
|
|
4791
|
-
pid = Number.parseInt(
|
|
4873
|
+
pid = Number.parseInt(readFileSync12(this.path(n), "utf8").trim(), 10);
|
|
4792
4874
|
} catch {
|
|
4793
4875
|
continue;
|
|
4794
4876
|
}
|
|
@@ -4874,7 +4956,7 @@ var State = class _State {
|
|
|
4874
4956
|
ticketInFlight(recordId) {
|
|
4875
4957
|
const file = this.path(`.${_State.safe(`crew-ticket-${recordId}`)}.lock`);
|
|
4876
4958
|
try {
|
|
4877
|
-
const pid = Number.parseInt(
|
|
4959
|
+
const pid = Number.parseInt(readFileSync12(file, "utf8").trim(), 10);
|
|
4878
4960
|
if (!Number.isFinite(pid) || pid <= 0) return false;
|
|
4879
4961
|
process.kill(pid, 0);
|
|
4880
4962
|
return true;
|
|
@@ -4908,7 +4990,7 @@ var State = class _State {
|
|
|
4908
4990
|
const heldReported = `.release-held-${suffix}`;
|
|
4909
4991
|
const read = (f) => {
|
|
4910
4992
|
try {
|
|
4911
|
-
return
|
|
4993
|
+
return readFileSync12(this.path(f), "utf8").trim() || null;
|
|
4912
4994
|
} catch {
|
|
4913
4995
|
return null;
|
|
4914
4996
|
}
|
|
@@ -4917,12 +4999,12 @@ var State = class _State {
|
|
|
4917
4999
|
blockedCount: () => Number.parseInt(read(blocked) ?? "", 10) || 0,
|
|
4918
5000
|
noteBlocked: () => {
|
|
4919
5001
|
const n = (Number.parseInt(read(blocked) ?? "", 10) || 0) + 1;
|
|
4920
|
-
|
|
5002
|
+
writeFileSync8(this.path(blocked), `${n}
|
|
4921
5003
|
`);
|
|
4922
5004
|
return n;
|
|
4923
5005
|
},
|
|
4924
5006
|
clearBlocked: () => {
|
|
4925
|
-
|
|
5007
|
+
rmSync7(this.path(blocked), { force: true });
|
|
4926
5008
|
},
|
|
4927
5009
|
/**
|
|
4928
5010
|
* The commit a deploy last failed on.
|
|
@@ -4934,11 +5016,11 @@ var State = class _State {
|
|
|
4934
5016
|
*/
|
|
4935
5017
|
deployFailedSha: () => read(failed),
|
|
4936
5018
|
noteDeployFailed: (sha) => {
|
|
4937
|
-
|
|
5019
|
+
writeFileSync8(this.path(failed), `${sha}
|
|
4938
5020
|
`);
|
|
4939
5021
|
},
|
|
4940
5022
|
clearDeployFailed: () => {
|
|
4941
|
-
|
|
5023
|
+
rmSync7(this.path(failed), { force: true });
|
|
4942
5024
|
},
|
|
4943
5025
|
/**
|
|
4944
5026
|
* Consecutive test-gate failures on the same head (CREW-1368). A failure
|
|
@@ -4948,31 +5030,31 @@ var State = class _State {
|
|
|
4948
5030
|
noteTestGateFailed: (sha) => {
|
|
4949
5031
|
const [prev, n] = (read(gateFailed) ?? "").split(" ");
|
|
4950
5032
|
const next = prev === sha ? (Number.parseInt(n ?? "", 10) || 0) + 1 : 1;
|
|
4951
|
-
|
|
5033
|
+
writeFileSync8(this.path(gateFailed), `${sha} ${next}
|
|
4952
5034
|
`);
|
|
4953
5035
|
return next;
|
|
4954
5036
|
},
|
|
4955
5037
|
clearTestGateFailed: () => {
|
|
4956
|
-
|
|
5038
|
+
rmSync7(this.path(gateFailed), { force: true });
|
|
4957
5039
|
},
|
|
4958
5040
|
/** Whether the board was already told about this head's red gate. */
|
|
4959
5041
|
heldReported: () => {
|
|
4960
5042
|
try {
|
|
4961
|
-
return
|
|
5043
|
+
return readFileSync12(this.path(heldReported), "utf8").replace(/\n$/, "") || null;
|
|
4962
5044
|
} catch {
|
|
4963
5045
|
return null;
|
|
4964
5046
|
}
|
|
4965
5047
|
},
|
|
4966
5048
|
noteHeldReported: (signature) => {
|
|
4967
|
-
|
|
5049
|
+
writeFileSync8(this.path(heldReported), `${signature}
|
|
4968
5050
|
`);
|
|
4969
5051
|
},
|
|
4970
5052
|
clearHeldReported: () => {
|
|
4971
|
-
|
|
5053
|
+
rmSync7(this.path(heldReported), { force: true });
|
|
4972
5054
|
},
|
|
4973
5055
|
testGateReported: (sha) => read(gateReported) === sha,
|
|
4974
5056
|
noteTestGateReported: (sha) => {
|
|
4975
|
-
|
|
5057
|
+
writeFileSync8(this.path(gateReported), `${sha}
|
|
4976
5058
|
`);
|
|
4977
5059
|
}
|
|
4978
5060
|
};
|
|
@@ -4995,13 +5077,13 @@ var State = class _State {
|
|
|
4995
5077
|
const ticketStreakFile = `.fairness-ticket-streak-${suffix}`;
|
|
4996
5078
|
const read = (f) => {
|
|
4997
5079
|
try {
|
|
4998
|
-
return
|
|
5080
|
+
return readFileSync12(this.path(f), "utf8").trim() || null;
|
|
4999
5081
|
} catch {
|
|
5000
5082
|
return null;
|
|
5001
5083
|
}
|
|
5002
5084
|
};
|
|
5003
5085
|
const clear2 = () => {
|
|
5004
|
-
for (const f of [streakFile, ticketFile, sinceFile, ticketStreakFile])
|
|
5086
|
+
for (const f of [streakFile, ticketFile, sinceFile, ticketStreakFile]) rmSync7(this.path(f), { force: true });
|
|
5005
5087
|
};
|
|
5006
5088
|
return {
|
|
5007
5089
|
/** Consecutive cycles this route had actionable work and did not win. */
|
|
@@ -5028,18 +5110,18 @@ var State = class _State {
|
|
|
5028
5110
|
clear2();
|
|
5029
5111
|
return;
|
|
5030
5112
|
}
|
|
5031
|
-
|
|
5113
|
+
writeFileSync8(this.path(streakFile), `${this.readInt(streakFile) + 1}
|
|
5032
5114
|
`);
|
|
5033
5115
|
const prior = read(ticketFile);
|
|
5034
5116
|
if (topTicket && topTicket === prior) {
|
|
5035
|
-
|
|
5117
|
+
writeFileSync8(this.path(ticketStreakFile), `${this.readInt(ticketStreakFile) + 1}
|
|
5036
5118
|
`);
|
|
5037
5119
|
} else if (topTicket) {
|
|
5038
|
-
|
|
5120
|
+
writeFileSync8(this.path(ticketFile), `${topTicket}
|
|
5039
5121
|
`);
|
|
5040
|
-
|
|
5122
|
+
writeFileSync8(this.path(sinceFile), `${(/* @__PURE__ */ new Date()).toISOString()}
|
|
5041
5123
|
`);
|
|
5042
|
-
|
|
5124
|
+
writeFileSync8(this.path(ticketStreakFile), "1\n");
|
|
5043
5125
|
}
|
|
5044
5126
|
}
|
|
5045
5127
|
};
|
|
@@ -5060,14 +5142,14 @@ var State = class _State {
|
|
|
5060
5142
|
return {
|
|
5061
5143
|
previous: () => {
|
|
5062
5144
|
try {
|
|
5063
|
-
const parsed = JSON.parse(
|
|
5145
|
+
const parsed = JSON.parse(readFileSync12(this.path(file), "utf8"));
|
|
5064
5146
|
return parsed && typeof parsed === "object" ? parsed : {};
|
|
5065
5147
|
} catch {
|
|
5066
5148
|
return {};
|
|
5067
5149
|
}
|
|
5068
5150
|
},
|
|
5069
5151
|
persist: (next) => {
|
|
5070
|
-
|
|
5152
|
+
writeFileSync8(this.path(file), JSON.stringify(next));
|
|
5071
5153
|
}
|
|
5072
5154
|
};
|
|
5073
5155
|
}
|
|
@@ -5083,14 +5165,14 @@ var State = class _State {
|
|
|
5083
5165
|
return {
|
|
5084
5166
|
previous: () => {
|
|
5085
5167
|
try {
|
|
5086
|
-
const parsed = JSON.parse(
|
|
5168
|
+
const parsed = JSON.parse(readFileSync12(this.path(file), "utf8"));
|
|
5087
5169
|
return Array.isArray(parsed) ? parsed.filter((x) => typeof x === "string") : [];
|
|
5088
5170
|
} catch {
|
|
5089
5171
|
return [];
|
|
5090
5172
|
}
|
|
5091
5173
|
},
|
|
5092
5174
|
persist: (next) => {
|
|
5093
|
-
|
|
5175
|
+
writeFileSync8(this.path(file), JSON.stringify(next));
|
|
5094
5176
|
}
|
|
5095
5177
|
};
|
|
5096
5178
|
}
|
|
@@ -5104,14 +5186,14 @@ var State = class _State {
|
|
|
5104
5186
|
return {
|
|
5105
5187
|
previous: () => {
|
|
5106
5188
|
try {
|
|
5107
|
-
const parsed = JSON.parse(
|
|
5189
|
+
const parsed = JSON.parse(readFileSync12(this.path(file), "utf8"));
|
|
5108
5190
|
return Array.isArray(parsed) ? parsed : [];
|
|
5109
5191
|
} catch {
|
|
5110
5192
|
return [];
|
|
5111
5193
|
}
|
|
5112
5194
|
},
|
|
5113
5195
|
persist: (next) => {
|
|
5114
|
-
|
|
5196
|
+
writeFileSync8(this.path(file), JSON.stringify(next));
|
|
5115
5197
|
}
|
|
5116
5198
|
};
|
|
5117
5199
|
}
|
|
@@ -5124,7 +5206,7 @@ var State = class _State {
|
|
|
5124
5206
|
const file = ".release-last-run";
|
|
5125
5207
|
return {
|
|
5126
5208
|
touch: () => {
|
|
5127
|
-
|
|
5209
|
+
writeFileSync8(this.path(file), (/* @__PURE__ */ new Date()).toISOString());
|
|
5128
5210
|
},
|
|
5129
5211
|
at: () => {
|
|
5130
5212
|
try {
|
|
@@ -5137,7 +5219,7 @@ var State = class _State {
|
|
|
5137
5219
|
}
|
|
5138
5220
|
readInt(file) {
|
|
5139
5221
|
try {
|
|
5140
|
-
return Number.parseInt(
|
|
5222
|
+
return Number.parseInt(readFileSync12(this.path(file), "utf8").trim(), 10) || 0;
|
|
5141
5223
|
} catch {
|
|
5142
5224
|
return 0;
|
|
5143
5225
|
}
|
|
@@ -5620,8 +5702,8 @@ var PERISCOPE_COLUMNS = [
|
|
|
5620
5702
|
];
|
|
5621
5703
|
|
|
5622
5704
|
// src/agent-log.ts
|
|
5623
|
-
import { existsSync as
|
|
5624
|
-
import { join as
|
|
5705
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync8, readdirSync as readdirSync6, readFileSync as readFileSync13, rmSync as rmSync8, writeFileSync as writeFileSync9 } from "fs";
|
|
5706
|
+
import { join as join14 } from "path";
|
|
5625
5707
|
|
|
5626
5708
|
// src/pending-backoff.ts
|
|
5627
5709
|
var sleepBackoff = (attempts) => Math.min(6e4 * 2 ** Math.max(0, attempts - 1), 30 * 6e4);
|
|
@@ -5793,20 +5875,20 @@ async function addRunToTicket(target, t, recordId, run2) {
|
|
|
5793
5875
|
};
|
|
5794
5876
|
}
|
|
5795
5877
|
}
|
|
5796
|
-
var queueDir = (stateDir) =>
|
|
5878
|
+
var queueDir = (stateDir) => join14(stateDir, "pending-agent-logs");
|
|
5797
5879
|
function queueAgentRun(stateDir, name, q) {
|
|
5798
|
-
|
|
5799
|
-
|
|
5880
|
+
mkdirSync8(queueDir(stateDir), { recursive: true });
|
|
5881
|
+
writeFileSync9(join14(queueDir(stateDir), `${name}.json`), JSON.stringify(q));
|
|
5800
5882
|
}
|
|
5801
5883
|
async function replayQueuedAgentRuns(stateDir, route, keyFor, log, now = /* @__PURE__ */ new Date()) {
|
|
5802
5884
|
const dir = queueDir(stateDir);
|
|
5803
|
-
if (!
|
|
5885
|
+
if (!existsSync13(dir)) return 0;
|
|
5804
5886
|
let sent = 0;
|
|
5805
|
-
for (const f of
|
|
5806
|
-
const path =
|
|
5887
|
+
for (const f of readdirSync6(dir).filter((n) => n.endsWith(".json")).sort()) {
|
|
5888
|
+
const path = join14(dir, f);
|
|
5807
5889
|
let q;
|
|
5808
5890
|
try {
|
|
5809
|
-
q = JSON.parse(
|
|
5891
|
+
q = JSON.parse(readFileSync13(path, "utf8"));
|
|
5810
5892
|
} catch {
|
|
5811
5893
|
continue;
|
|
5812
5894
|
}
|
|
@@ -5825,17 +5907,17 @@ async function replayQueuedAgentRuns(stateDir, route, keyFor, log, now = /* @__P
|
|
|
5825
5907
|
attempts: 0,
|
|
5826
5908
|
nextAttemptAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5827
5909
|
});
|
|
5828
|
-
|
|
5910
|
+
rmSync8(path, { force: true });
|
|
5829
5911
|
sent += 1;
|
|
5830
5912
|
log.emit(`sent a queued Agent Log report (${f})`);
|
|
5831
5913
|
} catch (e) {
|
|
5832
5914
|
if (!(e instanceof AgentLogUnreachableError)) {
|
|
5833
|
-
|
|
5915
|
+
rmSync8(path, { force: true });
|
|
5834
5916
|
log.warn(`dropped a queued Agent Log report (${f}): ${e.message}`);
|
|
5835
5917
|
continue;
|
|
5836
5918
|
}
|
|
5837
5919
|
const attempts = q.attempts + 1;
|
|
5838
|
-
|
|
5920
|
+
writeFileSync9(path, JSON.stringify({
|
|
5839
5921
|
...q,
|
|
5840
5922
|
attempts,
|
|
5841
5923
|
warned: true,
|
|
@@ -5846,20 +5928,20 @@ async function replayQueuedAgentRuns(stateDir, route, keyFor, log, now = /* @__P
|
|
|
5846
5928
|
}
|
|
5847
5929
|
return sent;
|
|
5848
5930
|
}
|
|
5849
|
-
var countersDir = (stateDir) =>
|
|
5931
|
+
var countersDir = (stateDir) => join14(stateDir, "pending-ticket-counters");
|
|
5850
5932
|
function queueTicketCounters(stateDir, name, q) {
|
|
5851
|
-
|
|
5852
|
-
|
|
5933
|
+
mkdirSync8(countersDir(stateDir), { recursive: true });
|
|
5934
|
+
writeFileSync9(join14(countersDir(stateDir), `${name}.json`), JSON.stringify(q));
|
|
5853
5935
|
}
|
|
5854
5936
|
async function replayQueuedTicketCounters(stateDir, route, keyFor, log, now = /* @__PURE__ */ new Date()) {
|
|
5855
5937
|
const dir = countersDir(stateDir);
|
|
5856
|
-
if (!
|
|
5938
|
+
if (!existsSync13(dir)) return 0;
|
|
5857
5939
|
let sent = 0;
|
|
5858
|
-
for (const f of
|
|
5859
|
-
const path =
|
|
5940
|
+
for (const f of readdirSync6(dir).filter((n) => n.endsWith(".json")).sort()) {
|
|
5941
|
+
const path = join14(dir, f);
|
|
5860
5942
|
let q;
|
|
5861
5943
|
try {
|
|
5862
|
-
q = JSON.parse(
|
|
5944
|
+
q = JSON.parse(readFileSync13(path, "utf8"));
|
|
5863
5945
|
} catch {
|
|
5864
5946
|
continue;
|
|
5865
5947
|
}
|
|
@@ -5868,17 +5950,17 @@ async function replayQueuedTicketCounters(stateDir, route, keyFor, log, now = /*
|
|
|
5868
5950
|
const apiKey = await keyFor(q.role);
|
|
5869
5951
|
if (!apiKey) continue;
|
|
5870
5952
|
await writeCounters({ ...q.target, apiKey }, q.write);
|
|
5871
|
-
|
|
5953
|
+
rmSync8(path, { force: true });
|
|
5872
5954
|
sent += 1;
|
|
5873
5955
|
log.emit(`added a queued run's spend to ${q.write.ticketReference ?? q.write.recordId} (${f})`);
|
|
5874
5956
|
} catch (e) {
|
|
5875
5957
|
if (!isRetryable(e)) {
|
|
5876
|
-
|
|
5958
|
+
rmSync8(path, { force: true });
|
|
5877
5959
|
log.warn(`dropped a queued ticket counter update (${f}): ${e.message}`);
|
|
5878
5960
|
continue;
|
|
5879
5961
|
}
|
|
5880
5962
|
const attempts = q.attempts + 1;
|
|
5881
|
-
|
|
5963
|
+
writeFileSync9(path, JSON.stringify({
|
|
5882
5964
|
...q,
|
|
5883
5965
|
attempts,
|
|
5884
5966
|
warned: true,
|
|
@@ -6081,8 +6163,8 @@ var HandoffTranscriptTracker = class {
|
|
|
6081
6163
|
};
|
|
6082
6164
|
|
|
6083
6165
|
// src/pending-handoff.ts
|
|
6084
|
-
import { existsSync as
|
|
6085
|
-
import { join as
|
|
6166
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync9, readdirSync as readdirSync7, readFileSync as readFileSync14, rmSync as rmSync9, writeFileSync as writeFileSync10 } from "fs";
|
|
6167
|
+
import { join as join17 } from "path";
|
|
6086
6168
|
|
|
6087
6169
|
// src/ticket-branch.ts
|
|
6088
6170
|
import { basename as basename6 } from "path";
|
|
@@ -6905,6 +6987,33 @@ var Tracker = class {
|
|
|
6905
6987
|
const { shipId } = await this.authorshipProbe();
|
|
6906
6988
|
return this.client.records.create(modelId, { ...fields, ...shipId ? { ship_id: shipId } : {} });
|
|
6907
6989
|
}
|
|
6990
|
+
/**
|
|
6991
|
+
* This ship's Periscopes rows (EPIC-077), newest first, with the ticket each
|
|
6992
|
+
* belongs to. Empty when there is no Periscopes table or no Ships row yet.
|
|
6993
|
+
*/
|
|
6994
|
+
async periscopeRows(states) {
|
|
6995
|
+
const modelId = this.route.resolved?.periscopesModelId;
|
|
6996
|
+
const { shipId } = await this.authorshipProbe();
|
|
6997
|
+
if (!modelId || !shipId) return [];
|
|
6998
|
+
const conds = [{ columnName: "ship_id", operator: "EQ", value: shipId }];
|
|
6999
|
+
if (states?.length) conds.push({ columnName: "state", operator: "IN", value: states });
|
|
7000
|
+
const rows = await this.client.records.list(modelId, { filters: encodeFilters(conds), limit: 200 });
|
|
7001
|
+
const refId = (v) => typeof v === "string" ? v : v && typeof v === "object" && typeof v.id === "string" ? v.id : void 0;
|
|
7002
|
+
const ids = [...new Set(rows.map((r) => refId(r.issue_id)).filter((x) => !!x))];
|
|
7003
|
+
const tickets = new Map((await this.ticketsByIds(ids)).map((t) => [t.id, t]));
|
|
7004
|
+
const out = [];
|
|
7005
|
+
for (const row of rows) {
|
|
7006
|
+
const t = tickets.get(refId(row.issue_id) ?? "");
|
|
7007
|
+
if (t) out.push({ row, ticket: t });
|
|
7008
|
+
}
|
|
7009
|
+
return out.sort((a, b) => String(b.row.started_at ?? "").localeCompare(String(a.row.started_at ?? "")));
|
|
7010
|
+
}
|
|
7011
|
+
/** Updates one Periscopes row (EPIC-077). */
|
|
7012
|
+
async updatePeriscope(id, patch2) {
|
|
7013
|
+
const modelId = this.route.resolved?.periscopesModelId;
|
|
7014
|
+
if (!modelId) throw new Error("this workspace has no Periscopes table");
|
|
7015
|
+
await this.client.records.update(modelId, id, patch2);
|
|
7016
|
+
}
|
|
6908
7017
|
/**
|
|
6909
7018
|
* The authorship columns this workspace is missing, as `table.column` names
|
|
6910
7019
|
* for `crew doctor` to report (CREW-1371). Empty on a fully updated install.
|
|
@@ -7125,13 +7234,13 @@ function holdWarnings(tickets, ships, contract, humanHolds, now = Date.now()) {
|
|
|
7125
7234
|
}
|
|
7126
7235
|
|
|
7127
7236
|
// src/run-worktree.ts
|
|
7128
|
-
import { existsSync as
|
|
7129
|
-
import { dirname as dirname6, join as
|
|
7237
|
+
import { existsSync as existsSync15 } from "fs";
|
|
7238
|
+
import { dirname as dirname6, join as join16 } from "path";
|
|
7130
7239
|
|
|
7131
7240
|
// src/ports.ts
|
|
7132
7241
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
7133
|
-
import { existsSync as
|
|
7134
|
-
import { join as
|
|
7242
|
+
import { existsSync as existsSync14 } from "fs";
|
|
7243
|
+
import { join as join15 } from "path";
|
|
7135
7244
|
var run = (cmd, args) => {
|
|
7136
7245
|
try {
|
|
7137
7246
|
return execFileSync3(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
@@ -7176,7 +7285,7 @@ function ticketForPort(port, s = DEFAULT_PORTS) {
|
|
|
7176
7285
|
}
|
|
7177
7286
|
function worktreeExists(parent, prefix, n) {
|
|
7178
7287
|
for (const name of [`${prefix}${n}`, `${prefix}${String(n).padStart(3, "0")}`]) {
|
|
7179
|
-
if (
|
|
7288
|
+
if (existsSync14(join15(parent, name, ".git"))) return true;
|
|
7180
7289
|
}
|
|
7181
7290
|
return false;
|
|
7182
7291
|
}
|
|
@@ -7259,7 +7368,7 @@ function planRunWorktree(dir, cfg, t, role, opts = {}) {
|
|
|
7259
7368
|
const existing = worktreeForNumber(dir, num);
|
|
7260
7369
|
const located = locateBranchForTicket(dir, cfg, t, role);
|
|
7261
7370
|
const baseRef = hasRemote ? `${remote}/${base}` : base;
|
|
7262
|
-
if (existing && !existing.prunable &&
|
|
7371
|
+
if (existing && !existing.prunable && existsSync15(existing.path)) {
|
|
7263
7372
|
const branch = existing.branch;
|
|
7264
7373
|
if (status(existing.path).length > 0) {
|
|
7265
7374
|
return { action: opts.resumeInterrupted ? "resume-dirty" : "leave-dirty", path: existing.path, branch };
|
|
@@ -7275,7 +7384,7 @@ function planRunWorktree(dir, cfg, t, role, opts = {}) {
|
|
|
7275
7384
|
if (countCommits(existing.path, `${baseRef}..HEAD`) > 0) return { action: "leave-unpushed", path: existing.path, branch };
|
|
7276
7385
|
return { action: "recut", path: existing.path, branch, base: baseRef };
|
|
7277
7386
|
}
|
|
7278
|
-
const path =
|
|
7387
|
+
const path = join16(dirname6(dir), worktreeForTicket(dir, cfg, t, role));
|
|
7279
7388
|
if (located?.where === "remote" || located && hasRemote && remoteBranchExists(dir, remote, located.branch)) {
|
|
7280
7389
|
return { action: "cut-remote", path, branch: located.branch };
|
|
7281
7390
|
}
|
|
@@ -7329,7 +7438,7 @@ function dropEmptyRunBranch(dir, cfg, branch) {
|
|
|
7329
7438
|
if (remoteConfigured(dir, remote) && remoteBranchExists(dir, remote, branch)) return false;
|
|
7330
7439
|
if (countCommits(dir, `${baseRef}..${branch}`) !== 0) return false;
|
|
7331
7440
|
const wt = worktrees(dir).find((w) => w.branch === branch && !w.prunable && w.path !== dir);
|
|
7332
|
-
if (wt &&
|
|
7441
|
+
if (wt && existsSync15(wt.path)) {
|
|
7333
7442
|
if (status(wt.path).length > 0) return false;
|
|
7334
7443
|
if (gitOk(dir, ["worktree", "remove", wt.path]) === null) return false;
|
|
7335
7444
|
}
|
|
@@ -7337,7 +7446,7 @@ function dropEmptyRunBranch(dir, cfg, branch) {
|
|
|
7337
7446
|
}
|
|
7338
7447
|
async function finishRunWorktree(dir, cfg, branch) {
|
|
7339
7448
|
const wt = worktrees(dir).find((w) => w.branch === branch && !w.prunable && w.path !== dir);
|
|
7340
|
-
if (!wt || !
|
|
7449
|
+
if (!wt || !existsSync15(wt.path)) return { kind: "none" };
|
|
7341
7450
|
if (cfg.hooks.handoff) return { kind: "kept-handoff", path: wt.path };
|
|
7342
7451
|
if (status(wt.path).length > 0) return { kind: "kept-dirty", path: wt.path };
|
|
7343
7452
|
if (remoteConfigured(dir, cfg.branch.remote) && worktreeHoldsUnpushed(wt.path, cfg.branch.remote)) {
|
|
@@ -7349,7 +7458,7 @@ async function finishRunWorktree(dir, cfg, branch) {
|
|
|
7349
7458
|
if (pids.length) await killGently(pids);
|
|
7350
7459
|
}
|
|
7351
7460
|
gitOk(dir, ["worktree", "remove", wt.path]);
|
|
7352
|
-
return
|
|
7461
|
+
return existsSync15(wt.path) ? { kind: "kept-dirty", path: wt.path } : { kind: "removed", path: wt.path };
|
|
7353
7462
|
}
|
|
7354
7463
|
|
|
7355
7464
|
// src/handoff-guard.ts
|
|
@@ -7732,20 +7841,20 @@ async function guardRunStrays(route, ship, tracker, role, workingId, since2, log
|
|
|
7732
7841
|
|
|
7733
7842
|
// src/pending-handoff.ts
|
|
7734
7843
|
var PENDING_MAX_AGE_MS = 48 * 36e5;
|
|
7735
|
-
var pendingDirFor = (stateDir) =>
|
|
7736
|
-
var stateDirOfStream = (streamPath) =>
|
|
7844
|
+
var pendingDirFor = (stateDir) => join17(stateDir, "pending-handoffs");
|
|
7845
|
+
var stateDirOfStream = (streamPath) => join17(streamPath, "..", "..");
|
|
7737
7846
|
function writePendingHandoff(stateDir, rec) {
|
|
7738
7847
|
const dir = pendingDirFor(stateDir);
|
|
7739
|
-
|
|
7740
|
-
|
|
7848
|
+
mkdirSync9(dir, { recursive: true });
|
|
7849
|
+
writeFileSync10(join17(dir, `${rec.ticketRecordId}.json`), JSON.stringify(rec, null, 2));
|
|
7741
7850
|
}
|
|
7742
7851
|
function listPendingHandoffs(stateDir, route) {
|
|
7743
7852
|
const dir = pendingDirFor(stateDir);
|
|
7744
|
-
if (!
|
|
7853
|
+
if (!existsSync16(dir)) return [];
|
|
7745
7854
|
const out = [];
|
|
7746
|
-
for (const f of
|
|
7855
|
+
for (const f of readdirSync7(dir).filter((n) => n.endsWith(".json")).sort()) {
|
|
7747
7856
|
try {
|
|
7748
|
-
const rec = JSON.parse(
|
|
7857
|
+
const rec = JSON.parse(readFileSync14(join17(dir, f), "utf8"));
|
|
7749
7858
|
if (rec.version === 1 && rec.ticketRecordId && (!route || rec.route === route)) out.push(rec);
|
|
7750
7859
|
} catch {
|
|
7751
7860
|
}
|
|
@@ -7753,7 +7862,7 @@ function listPendingHandoffs(stateDir, route) {
|
|
|
7753
7862
|
return out;
|
|
7754
7863
|
}
|
|
7755
7864
|
function removePendingHandoff(stateDir, ticketRecordId) {
|
|
7756
|
-
|
|
7865
|
+
rmSync9(join17(pendingDirFor(stateDir), `${ticketRecordId}.json`), { force: true });
|
|
7757
7866
|
}
|
|
7758
7867
|
function pendingHandoffFor(stateDir, ticketRecordId) {
|
|
7759
7868
|
return listPendingHandoffs(stateDir).find((r) => r.ticketRecordId === ticketRecordId);
|
|
@@ -7863,8 +7972,8 @@ function expandSkillMarkers(text, lookup2, opts = {}) {
|
|
|
7863
7972
|
}
|
|
7864
7973
|
|
|
7865
7974
|
// src/skills.ts
|
|
7866
|
-
import { readFileSync as
|
|
7867
|
-
import { join as
|
|
7975
|
+
import { readFileSync as readFileSync16, readdirSync as readdirSync8, existsSync as existsSync18 } from "fs";
|
|
7976
|
+
import { join as join19 } from "path";
|
|
7868
7977
|
import { parse as parseYaml } from "yaml";
|
|
7869
7978
|
import { TablationClient as TablationClient3, StaleWriteError as StaleWriteError3, TablationApiError as TablationApiError2 } from "@tablation/client";
|
|
7870
7979
|
|
|
@@ -7880,8 +7989,8 @@ ${additions}`;
|
|
|
7880
7989
|
}
|
|
7881
7990
|
|
|
7882
7991
|
// src/agents.ts
|
|
7883
|
-
import { readFileSync as
|
|
7884
|
-
import { join as
|
|
7992
|
+
import { readFileSync as readFileSync15, existsSync as existsSync17 } from "fs";
|
|
7993
|
+
import { join as join18 } from "path";
|
|
7885
7994
|
import { TablationClient as TablationClient2, StaleWriteError as StaleWriteError2, TablationApiError } from "@tablation/client";
|
|
7886
7995
|
var PERSONA_NAME = {
|
|
7887
7996
|
dev: "Developer",
|
|
@@ -7895,12 +8004,12 @@ var CREW_AGENT_COLUMN = "agent_id";
|
|
|
7895
8004
|
var AgentsSyncError = class extends Error {
|
|
7896
8005
|
};
|
|
7897
8006
|
function personaDefaultPrompt(promptsDir, role) {
|
|
7898
|
-
const briefPath =
|
|
7899
|
-
if (!
|
|
7900
|
-
if (SELF_CONTAINED_ROLES.has(role)) return
|
|
7901
|
-
const commonPath =
|
|
7902
|
-
if (!
|
|
7903
|
-
return `${
|
|
8007
|
+
const briefPath = join18(promptsDir, "personas", `lane-${role}.md`);
|
|
8008
|
+
if (!existsSync17(briefPath)) throw new AgentsSyncError(`no brief at ${briefPath}`);
|
|
8009
|
+
if (SELF_CONTAINED_ROLES.has(role)) return readFileSync15(briefPath, "utf8");
|
|
8010
|
+
const commonPath = join18(promptsDir, "personas", "common.md");
|
|
8011
|
+
if (!existsSync17(commonPath)) return readFileSync15(briefPath, "utf8");
|
|
8012
|
+
return `${readFileSync15(commonPath, "utf8")}${readFileSync15(briefPath, "utf8")}`;
|
|
7904
8013
|
}
|
|
7905
8014
|
async function resolveAgentId(route, role, opts) {
|
|
7906
8015
|
const cached = route.resolved?.agentPersonas?.[role]?.agentId;
|
|
@@ -8347,7 +8456,7 @@ function describeCrewLink(o) {
|
|
|
8347
8456
|
var SkillSyncError = class extends Error {
|
|
8348
8457
|
};
|
|
8349
8458
|
function parseSkillFile(path) {
|
|
8350
|
-
const text =
|
|
8459
|
+
const text = readFileSync16(path, "utf8");
|
|
8351
8460
|
const match = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/.exec(text);
|
|
8352
8461
|
if (!match) throw new SkillSyncError(`${path}: missing YAML frontmatter (expected a leading --- block)`);
|
|
8353
8462
|
const frontmatter = match[1];
|
|
@@ -8360,9 +8469,9 @@ function parseSkillFile(path) {
|
|
|
8360
8469
|
return { name: meta.name, description: meta.description, prompt: body, file: path };
|
|
8361
8470
|
}
|
|
8362
8471
|
function listLocalSkills(promptsDir) {
|
|
8363
|
-
const dir =
|
|
8364
|
-
if (!
|
|
8365
|
-
return
|
|
8472
|
+
const dir = join19(promptsDir, "skills");
|
|
8473
|
+
if (!existsSync18(dir)) return [];
|
|
8474
|
+
return readdirSync8(dir).filter((f) => f.endsWith(".md")).map((f) => parseSkillFile(join19(dir, f)));
|
|
8366
8475
|
}
|
|
8367
8476
|
async function syncSkills(route, opts) {
|
|
8368
8477
|
if (!route.resolved) {
|
|
@@ -8564,26 +8673,26 @@ var AgentError = class extends Error {
|
|
|
8564
8673
|
};
|
|
8565
8674
|
var AgentSpawnError = class extends AgentError {
|
|
8566
8675
|
};
|
|
8567
|
-
var digestPathFor = (stateDir, routeLabel, role, runId) =>
|
|
8676
|
+
var digestPathFor = (stateDir, routeLabel, role, runId) => join20(stateDir, `digest-${routeLabel}-${role}${runId ? `-${runId}` : ""}.md`);
|
|
8568
8677
|
function planAgentRun(o) {
|
|
8569
|
-
const briefPath =
|
|
8570
|
-
if (!
|
|
8678
|
+
const briefPath = join20(o.route.promptsDir, "personas", `lane-${o.role}.md`);
|
|
8679
|
+
if (!existsSync19(briefPath)) {
|
|
8571
8680
|
throw new AgentError(`no brief at ${briefPath} \u2014 refusing to run an unscoped session`);
|
|
8572
8681
|
}
|
|
8573
|
-
const commonPath =
|
|
8574
|
-
const usesCommon = !SELF_CONTAINED_ROLES.has(o.role) &&
|
|
8682
|
+
const commonPath = join20(o.route.promptsDir, "personas", "common.md");
|
|
8683
|
+
const usesCommon = !SELF_CONTAINED_ROLES.has(o.role) && existsSync19(commonPath);
|
|
8575
8684
|
const now = o.now ?? (() => Date.now());
|
|
8576
8685
|
const routeLabel = routeSlug(o.route.route);
|
|
8577
8686
|
const ownDigest = o.runId ? digestPathFor(o.stateDir, routeLabel, o.role, o.runId) : void 0;
|
|
8578
|
-
const digestPath2 = ownDigest &&
|
|
8687
|
+
const digestPath2 = ownDigest && existsSync19(ownDigest) ? ownDigest : digestPathFor(o.stateDir, routeLabel, o.role);
|
|
8579
8688
|
let digest;
|
|
8580
8689
|
let ageSeconds;
|
|
8581
|
-
if (
|
|
8690
|
+
if (existsSync19(digestPath2)) {
|
|
8582
8691
|
ageSeconds = Math.floor((now() - statSync5(digestPath2).mtimeMs) / 1e3);
|
|
8583
|
-
if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest =
|
|
8692
|
+
if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync17(digestPath2, "utf8");
|
|
8584
8693
|
}
|
|
8585
|
-
const common = usesCommon ?
|
|
8586
|
-
const brief =
|
|
8694
|
+
const common = usesCommon ? readFileSync17(commonPath, "utf8") : "";
|
|
8695
|
+
const brief = readFileSync17(briefPath, "utf8");
|
|
8587
8696
|
const effectiveCommon = o.divergedPrompt ? "" : common;
|
|
8588
8697
|
const effectiveBrief = appendAdditions(o.divergedPrompt ?? brief, o.personaAdditions);
|
|
8589
8698
|
const localSkills = new Map(listLocalSkills(o.route.promptsDir).map((s) => [s.name, s.prompt]));
|
|
@@ -8605,7 +8714,7 @@ function planAgentRun(o) {
|
|
|
8605
8714
|
digest
|
|
8606
8715
|
});
|
|
8607
8716
|
const promptSha = createHash3("sha256").update(expanded.text).digest("hex");
|
|
8608
|
-
const streamsDir =
|
|
8717
|
+
const streamsDir = join20(o.stateDir, "streams");
|
|
8609
8718
|
const base = `${routeLabel}-${o.role}-${o.cycle}${o.runId ? `-${o.runId}` : ""}`;
|
|
8610
8719
|
const seat = resolveSeat(o.ship, o.route, o.role, o.agentPersona ?? (o.agentModel ? { model: o.agentModel } : {}), o.repo);
|
|
8611
8720
|
const { toolchain, model } = seat;
|
|
@@ -8675,8 +8784,8 @@ function planAgentRun(o) {
|
|
|
8675
8784
|
digestAttached: digest !== void 0,
|
|
8676
8785
|
digestAgeSeconds: ageSeconds,
|
|
8677
8786
|
model,
|
|
8678
|
-
streamPath:
|
|
8679
|
-
eventsPath:
|
|
8787
|
+
streamPath: join20(streamsDir, `${base}.jsonl`),
|
|
8788
|
+
eventsPath: join20(streamsDir, `${base}.events.jsonl`),
|
|
8680
8789
|
ticket: o.ticket,
|
|
8681
8790
|
ticketRecordId: o.ticketRecordId ?? null,
|
|
8682
8791
|
repo: o.repo ?? null,
|
|
@@ -8707,7 +8816,7 @@ function describePlan(p) {
|
|
|
8707
8816
|
var pidPathFor = (streamPath) => `${streamPath}.pid`;
|
|
8708
8817
|
function readRunSidecar(streamPath) {
|
|
8709
8818
|
try {
|
|
8710
|
-
const sc = JSON.parse(
|
|
8819
|
+
const sc = JSON.parse(readFileSync17(pidPathFor(streamPath), "utf8"));
|
|
8711
8820
|
return typeof sc.pid === "number" && typeof sc.spawnedAt === "string" && typeof sc.role === "string" ? sc : null;
|
|
8712
8821
|
} catch {
|
|
8713
8822
|
return null;
|
|
@@ -8743,7 +8852,7 @@ function identityOf(plan, adapterHarness) {
|
|
|
8743
8852
|
var withoutKey = ({ apiKey: _apiKey, ...rest }) => rest;
|
|
8744
8853
|
function openSink(path) {
|
|
8745
8854
|
try {
|
|
8746
|
-
|
|
8855
|
+
mkdirSync10(dirname7(path), { recursive: true });
|
|
8747
8856
|
return createWriteStream(path, { flags: "a" });
|
|
8748
8857
|
} catch {
|
|
8749
8858
|
return void 0;
|
|
@@ -8925,7 +9034,7 @@ var RunObserver = class {
|
|
|
8925
9034
|
};
|
|
8926
9035
|
function writeRunMeta(id, startedAt, liveTicket, extra) {
|
|
8927
9036
|
try {
|
|
8928
|
-
|
|
9037
|
+
writeFileSync11(`${id.streamPath}.meta.json`, JSON.stringify({
|
|
8929
9038
|
route: id.route,
|
|
8930
9039
|
role: id.role,
|
|
8931
9040
|
hint: id.ticket,
|
|
@@ -9085,7 +9194,7 @@ function spawnAgent(plan, emit, hintMoved) {
|
|
|
9085
9194
|
return new Promise((resolve6, reject) => {
|
|
9086
9195
|
let streamFd;
|
|
9087
9196
|
try {
|
|
9088
|
-
|
|
9197
|
+
mkdirSync10(dirname7(plan.streamPath), { recursive: true });
|
|
9089
9198
|
streamFd = openSync3(plan.streamPath, "a");
|
|
9090
9199
|
} catch (e) {
|
|
9091
9200
|
void endSink(eventsSink);
|
|
@@ -9120,7 +9229,7 @@ function spawnAgent(plan, emit, hintMoved) {
|
|
|
9120
9229
|
promptSha: plan.promptSha
|
|
9121
9230
|
};
|
|
9122
9231
|
try {
|
|
9123
|
-
|
|
9232
|
+
writeFileSync11(pidPathFor(plan.streamPath), JSON.stringify(sidecar));
|
|
9124
9233
|
} catch {
|
|
9125
9234
|
}
|
|
9126
9235
|
}
|
|
@@ -9623,7 +9732,7 @@ async function runSelftest(i, deps = realDeps) {
|
|
|
9623
9732
|
...adapter.runEnv(spec),
|
|
9624
9733
|
[KEY_VAR]: i.apiKey
|
|
9625
9734
|
};
|
|
9626
|
-
const cwd = mkdtempSync(
|
|
9735
|
+
const cwd = mkdtempSync(join21(tmpdir2(), "crew-selftest-"));
|
|
9627
9736
|
try {
|
|
9628
9737
|
const run2 = await deps.run({
|
|
9629
9738
|
bin: invocation.bin,
|
|
@@ -9645,7 +9754,7 @@ async function runSelftest(i, deps = realDeps) {
|
|
|
9645
9754
|
return finish({ ok: false, failure, detail, ...usage2 });
|
|
9646
9755
|
} finally {
|
|
9647
9756
|
try {
|
|
9648
|
-
|
|
9757
|
+
rmSync10(cwd, { recursive: true, force: true });
|
|
9649
9758
|
} catch {
|
|
9650
9759
|
}
|
|
9651
9760
|
}
|
|
@@ -9656,11 +9765,11 @@ function formatResult(r) {
|
|
|
9656
9765
|
const secs = `${(r.ms / 1e3).toFixed(1)}s`;
|
|
9657
9766
|
return r.ok ? `${r.toolchain}: PASS ${r.detail} (${secs}${tok})` : `${r.toolchain}: FAIL ${r.failure} \u2014 ${r.detail} (${secs})`;
|
|
9658
9767
|
}
|
|
9659
|
-
var fileFor2 = (stateDir) =>
|
|
9768
|
+
var fileFor2 = (stateDir) => join21(stateDir, "selftest.json");
|
|
9660
9769
|
function readSelftest(stateDir) {
|
|
9661
9770
|
try {
|
|
9662
9771
|
const p = fileFor2(stateDir);
|
|
9663
|
-
return
|
|
9772
|
+
return existsSync20(p) ? JSON.parse(readFileSync18(p, "utf8")) : {};
|
|
9664
9773
|
} catch {
|
|
9665
9774
|
return {};
|
|
9666
9775
|
}
|
|
@@ -9668,8 +9777,8 @@ function readSelftest(stateDir) {
|
|
|
9668
9777
|
function recordSelftest(stateDir, results) {
|
|
9669
9778
|
const all = readSelftest(stateDir);
|
|
9670
9779
|
for (const r of results) all[r.toolchain] = r;
|
|
9671
|
-
|
|
9672
|
-
|
|
9780
|
+
mkdirSync11(stateDir, { recursive: true });
|
|
9781
|
+
writeFileSync12(fileFor2(stateDir), `${JSON.stringify(all, null, 2)}
|
|
9673
9782
|
`);
|
|
9674
9783
|
}
|
|
9675
9784
|
function doctorSelftestLines(stored, toolchains) {
|
|
@@ -9681,7 +9790,7 @@ function doctorSelftestLines(stored, toolchains) {
|
|
|
9681
9790
|
}
|
|
9682
9791
|
|
|
9683
9792
|
// src/cli.ts
|
|
9684
|
-
import { resolve as resolve5, dirname as dirname20, join as
|
|
9793
|
+
import { resolve as resolve5, dirname as dirname20, join as join39, basename as basename12 } from "path";
|
|
9685
9794
|
import { fileURLToPath } from "url";
|
|
9686
9795
|
import { hostname as hostname2, homedir as homedir10 } from "os";
|
|
9687
9796
|
import { spawnSync as spawnSync7 } from "child_process";
|
|
@@ -9863,14 +9972,14 @@ function rosterMarkdown(roster, meId) {
|
|
|
9863
9972
|
}
|
|
9864
9973
|
|
|
9865
9974
|
// src/connect-resume.ts
|
|
9866
|
-
import { mkdirSync as
|
|
9867
|
-
import { dirname as dirname8, join as
|
|
9975
|
+
import { mkdirSync as mkdirSync12, readFileSync as readFileSync19, rmSync as rmSync11, writeFileSync as writeFileSync13 } from "fs";
|
|
9976
|
+
import { dirname as dirname8, join as join22 } from "path";
|
|
9868
9977
|
function lastSessionFile(stateDir) {
|
|
9869
|
-
return
|
|
9978
|
+
return join22(stateDir, "connect-session.json");
|
|
9870
9979
|
}
|
|
9871
9980
|
function readPointers(stateDir) {
|
|
9872
9981
|
try {
|
|
9873
|
-
const parsed = JSON.parse(
|
|
9982
|
+
const parsed = JSON.parse(readFileSync19(lastSessionFile(stateDir), "utf8"));
|
|
9874
9983
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
9875
9984
|
const out = {};
|
|
9876
9985
|
for (const [host, v] of Object.entries(parsed)) {
|
|
@@ -9884,8 +9993,8 @@ function readPointers(stateDir) {
|
|
|
9884
9993
|
}
|
|
9885
9994
|
function writePointers(stateDir, pointers) {
|
|
9886
9995
|
const file = lastSessionFile(stateDir);
|
|
9887
|
-
|
|
9888
|
-
|
|
9996
|
+
mkdirSync12(dirname8(file), { recursive: true });
|
|
9997
|
+
writeFileSync13(file, `${JSON.stringify(pointers, null, 2)}
|
|
9889
9998
|
`, { mode: 384 });
|
|
9890
9999
|
}
|
|
9891
10000
|
function rememberSession(stateDir, host, workspaceSlug) {
|
|
@@ -9902,7 +10011,7 @@ function forgetWorkspace(stateDir, host, workspaceSlug) {
|
|
|
9902
10011
|
const { [host]: _old, ...rest } = all;
|
|
9903
10012
|
const next = left.length ? { ...rest, [host]: left } : rest;
|
|
9904
10013
|
if (Object.keys(next).length) writePointers(stateDir, next);
|
|
9905
|
-
else
|
|
10014
|
+
else rmSync11(lastSessionFile(stateDir), { force: true });
|
|
9906
10015
|
} catch {
|
|
9907
10016
|
}
|
|
9908
10017
|
}
|
|
@@ -9942,7 +10051,7 @@ function forgetSession(stateDir, host) {
|
|
|
9942
10051
|
try {
|
|
9943
10052
|
const { [host]: _gone, ...rest } = readPointers(stateDir);
|
|
9944
10053
|
if (Object.keys(rest).length) writePointers(stateDir, rest);
|
|
9945
|
-
else
|
|
10054
|
+
else rmSync11(lastSessionFile(stateDir), { force: true });
|
|
9946
10055
|
} catch {
|
|
9947
10056
|
}
|
|
9948
10057
|
}
|
|
@@ -9970,15 +10079,15 @@ async function signOutSessions(o) {
|
|
|
9970
10079
|
}
|
|
9971
10080
|
|
|
9972
10081
|
// src/reset.ts
|
|
9973
|
-
import { existsSync as
|
|
9974
|
-
import { dirname as dirname9, join as
|
|
10082
|
+
import { existsSync as existsSync21, mkdirSync as mkdirSync13, readFileSync as readFileSync20, readdirSync as readdirSync9, rmSync as rmSync12, statSync as statSync6, writeFileSync as writeFileSync14 } from "fs";
|
|
10083
|
+
import { dirname as dirname9, join as join23 } from "path";
|
|
9975
10084
|
import { hostFromUrl as hostFromUrl3 } from "@tablation/client";
|
|
9976
10085
|
function clonedCheckoutsFile(stateDir) {
|
|
9977
|
-
return
|
|
10086
|
+
return join23(stateDir, "cloned-checkouts.json");
|
|
9978
10087
|
}
|
|
9979
10088
|
function readClonedCheckouts(stateDir) {
|
|
9980
10089
|
try {
|
|
9981
|
-
const parsed = JSON.parse(
|
|
10090
|
+
const parsed = JSON.parse(readFileSync20(clonedCheckoutsFile(stateDir), "utf8"));
|
|
9982
10091
|
return Array.isArray(parsed) ? parsed.filter((d) => typeof d === "string") : [];
|
|
9983
10092
|
} catch {
|
|
9984
10093
|
return [];
|
|
@@ -9988,8 +10097,8 @@ function recordClonedCheckout(stateDir, dir) {
|
|
|
9988
10097
|
const known = readClonedCheckouts(stateDir);
|
|
9989
10098
|
if (known.includes(dir)) return;
|
|
9990
10099
|
const file = clonedCheckoutsFile(stateDir);
|
|
9991
|
-
|
|
9992
|
-
|
|
10100
|
+
mkdirSync13(dirname9(file), { recursive: true });
|
|
10101
|
+
writeFileSync14(file, `${JSON.stringify([...known, dir], null, 2)}
|
|
9993
10102
|
`);
|
|
9994
10103
|
}
|
|
9995
10104
|
var mark = (dryRun) => dryRun ? "would-remove" : "removed";
|
|
@@ -10041,8 +10150,8 @@ async function runReset(o, fx) {
|
|
|
10041
10150
|
for (const dir of readClonedCheckouts(o.stateDir)) steps.push(removeCheckout(dir, o.dryRun, fx));
|
|
10042
10151
|
}
|
|
10043
10152
|
steps.push(...removeState(o.stateDir, o.dryRun));
|
|
10044
|
-
if (
|
|
10045
|
-
if (!o.dryRun)
|
|
10153
|
+
if (existsSync21(o.configFile)) {
|
|
10154
|
+
if (!o.dryRun) rmSync12(o.configFile, { force: true });
|
|
10046
10155
|
steps.push({ step: "config", target: o.configFile, action: mark(o.dryRun) });
|
|
10047
10156
|
} else {
|
|
10048
10157
|
steps.push({ step: "config", target: o.configFile, action: "absent" });
|
|
@@ -10050,7 +10159,7 @@ async function runReset(o, fx) {
|
|
|
10050
10159
|
return steps;
|
|
10051
10160
|
}
|
|
10052
10161
|
function removeCheckout(dir, dryRun, fx) {
|
|
10053
|
-
if (!
|
|
10162
|
+
if (!existsSync21(dir)) return { step: "checkout", target: dir, action: "absent", detail: "already gone" };
|
|
10054
10163
|
const { dirty, linkedWorktrees } = fx.inspectCheckout(dir);
|
|
10055
10164
|
if (dirty.length) {
|
|
10056
10165
|
return { step: "checkout", target: dir, action: "kept", detail: `has uncommitted changes (${dirty.length} path${dirty.length === 1 ? "" : "s"}: ${dirty.slice(0, 5).join(", ")}${dirty.length > 5 ? ", \u2026" : ""})` };
|
|
@@ -10058,21 +10167,21 @@ function removeCheckout(dir, dryRun, fx) {
|
|
|
10058
10167
|
if (linkedWorktrees.length) {
|
|
10059
10168
|
return { step: "checkout", target: dir, action: "kept", detail: `has linked worktrees (${linkedWorktrees.join(", ")}); remove them first` };
|
|
10060
10169
|
}
|
|
10061
|
-
if (!dryRun)
|
|
10170
|
+
if (!dryRun) rmSync12(dir, { recursive: true, force: true });
|
|
10062
10171
|
return { step: "checkout", target: dir, action: mark(dryRun) };
|
|
10063
10172
|
}
|
|
10064
10173
|
function removeState(stateDir, dryRun) {
|
|
10065
10174
|
const out = [];
|
|
10066
|
-
const targets = [
|
|
10175
|
+
const targets = [join23(stateDir, "resolved"), join23(stateDir, "keys"), clonedCheckoutsFile(stateDir), join23(stateDir, "connect-session.json"), join23(stateDir, "reload-requested"), join23(stateDir, "daemon.started")];
|
|
10067
10176
|
try {
|
|
10068
|
-
for (const name of
|
|
10069
|
-
if (name.startsWith(".") && statSync6(
|
|
10177
|
+
for (const name of readdirSync9(stateDir)) {
|
|
10178
|
+
if (name.startsWith(".") && statSync6(join23(stateDir, name)).isFile()) targets.push(join23(stateDir, name));
|
|
10070
10179
|
}
|
|
10071
10180
|
} catch {
|
|
10072
10181
|
}
|
|
10073
10182
|
for (const t of targets) {
|
|
10074
|
-
if (!
|
|
10075
|
-
if (!dryRun)
|
|
10183
|
+
if (!existsSync21(t)) continue;
|
|
10184
|
+
if (!dryRun) rmSync12(t, { recursive: true, force: true });
|
|
10076
10185
|
out.push({ step: "state", target: t, action: mark(dryRun) });
|
|
10077
10186
|
}
|
|
10078
10187
|
if (!out.length) out.push({ step: "state", target: stateDir, action: "absent", detail: "nothing to remove" });
|
|
@@ -10859,8 +10968,8 @@ ${waiting.map((t) => `| ${displayKey2(t)} | ${t.status} | ${blockers(t, i)} |`).
|
|
|
10859
10968
|
}
|
|
10860
10969
|
|
|
10861
10970
|
// src/poll.ts
|
|
10862
|
-
import { writeFileSync as
|
|
10863
|
-
import { join as
|
|
10971
|
+
import { writeFileSync as writeFileSync15 } from "fs";
|
|
10972
|
+
import { join as join24 } from "path";
|
|
10864
10973
|
|
|
10865
10974
|
// src/stalled.ts
|
|
10866
10975
|
var STALLED_AFTER_CYCLES = 30;
|
|
@@ -11167,7 +11276,7 @@ async function decideCycle(o) {
|
|
|
11167
11276
|
}
|
|
11168
11277
|
return decision;
|
|
11169
11278
|
}
|
|
11170
|
-
var digestPath = (stateDir, route, role, runId) =>
|
|
11279
|
+
var digestPath = (stateDir, route, role, runId) => join24(stateDir, `digest-${route}-${role}${runId ? `-${runId}` : ""}.md`);
|
|
11171
11280
|
function otherRunsRemoved(tickets, own) {
|
|
11172
11281
|
if (!own) return tickets;
|
|
11173
11282
|
return tickets.filter((t) => t.id === own.ticket.id || !own.inFlight(t.id));
|
|
@@ -11245,7 +11354,7 @@ function writeDigest(o, d, role, stateDir, runId, own) {
|
|
|
11245
11354
|
|
|
11246
11355
|
${body}` : body;
|
|
11247
11356
|
const path = digestPath(stateDir, routeSlug(o.route.route), role, runId);
|
|
11248
|
-
|
|
11357
|
+
writeFileSync15(path, text);
|
|
11249
11358
|
o.emit.emit(`queue digest written for ${role} (${Buffer.byteLength(text)} bytes)`, {
|
|
11250
11359
|
data: { role, bytes: Buffer.byteLength(text) }
|
|
11251
11360
|
});
|
|
@@ -11728,13 +11837,13 @@ Ship ${shipName2} found: ${problem}. The release refuses to squash a ref nobody
|
|
|
11728
11837
|
var localRefSkipReported = (ticket, problem, events) => events.some((e) => e.record_id === ticket.id && e.event_type === "local_ref_skip" && (e.message ?? "").includes(problem));
|
|
11729
11838
|
|
|
11730
11839
|
// src/connect-wizard.ts
|
|
11731
|
-
import { delimiter, dirname as dirname10, join as
|
|
11732
|
-
import { accessSync as accessSync2, constants as constants2, existsSync as
|
|
11840
|
+
import { delimiter, dirname as dirname10, join as join25 } from "path";
|
|
11841
|
+
import { accessSync as accessSync2, constants as constants2, existsSync as existsSync22, mkdirSync as mkdirSync14, writeFileSync as writeFileSync16 } from "fs";
|
|
11733
11842
|
import { stringify } from "yaml";
|
|
11734
11843
|
var HOST_PASSENGERS_DOCS_URL = "https://crew.tablation.dev/host-passengers";
|
|
11735
11844
|
function shouldRunWizard(opts) {
|
|
11736
11845
|
if (!opts.isTTY) return false;
|
|
11737
|
-
const exists = opts.configExists ??
|
|
11846
|
+
const exists = opts.configExists ?? existsSync22;
|
|
11738
11847
|
return !configSearchPath(opts.crewHome).some((p) => exists(p));
|
|
11739
11848
|
}
|
|
11740
11849
|
function firstRunConfigPath() {
|
|
@@ -11751,7 +11860,7 @@ function defaultIsExecutable(p) {
|
|
|
11751
11860
|
function findOnPath2(name, pathEnv, isExecutable = defaultIsExecutable) {
|
|
11752
11861
|
for (const dir of pathEnv.split(delimiter)) {
|
|
11753
11862
|
if (!dir) continue;
|
|
11754
|
-
const candidate =
|
|
11863
|
+
const candidate = join25(dir, name);
|
|
11755
11864
|
if (isExecutable(candidate)) return candidate;
|
|
11756
11865
|
}
|
|
11757
11866
|
return void 0;
|
|
@@ -11812,9 +11921,9 @@ routes:
|
|
|
11812
11921
|
${routeBlock}`;
|
|
11813
11922
|
}
|
|
11814
11923
|
function writeNewConfig(path, text) {
|
|
11815
|
-
|
|
11924
|
+
mkdirSync14(dirname10(path), { recursive: true });
|
|
11816
11925
|
try {
|
|
11817
|
-
|
|
11926
|
+
writeFileSync16(path, text, { mode: 384, flag: "wx" });
|
|
11818
11927
|
return true;
|
|
11819
11928
|
} catch (e) {
|
|
11820
11929
|
if (e.code === "EEXIST") return false;
|
|
@@ -11848,7 +11957,7 @@ Wrote ${o.configPath}.
|
|
|
11848
11957
|
// src/connect-json.ts
|
|
11849
11958
|
import { accessSync as accessSync3, constants as constants3 } from "fs";
|
|
11850
11959
|
import { homedir as homedir8 } from "os";
|
|
11851
|
-
import { dirname as dirname11, join as
|
|
11960
|
+
import { dirname as dirname11, join as join26 } from "path";
|
|
11852
11961
|
import { delimiter as delimiter2 } from "path";
|
|
11853
11962
|
var CHOICE_EXIT_CODE = 3;
|
|
11854
11963
|
function createEmitter(json, write) {
|
|
@@ -11887,12 +11996,12 @@ function agentBinSearchDirs(home, pathEnv, extraPath) {
|
|
|
11887
11996
|
const dirs = [
|
|
11888
11997
|
...pathEnv.split(delimiter2),
|
|
11889
11998
|
...(extraPath ?? "").split(delimiter2),
|
|
11890
|
-
|
|
11891
|
-
|
|
11999
|
+
join26(home, ".local/bin"),
|
|
12000
|
+
join26(home, ".claude/local"),
|
|
11892
12001
|
"/opt/homebrew/bin",
|
|
11893
12002
|
"/usr/local/bin",
|
|
11894
|
-
|
|
11895
|
-
|
|
12003
|
+
join26(home, ".npm-global/bin"),
|
|
12004
|
+
join26(home, ".volta/bin")
|
|
11896
12005
|
];
|
|
11897
12006
|
return [...new Set(dirs.filter(Boolean))];
|
|
11898
12007
|
}
|
|
@@ -11928,8 +12037,8 @@ function flagWizardAnswers(f, env) {
|
|
|
11928
12037
|
}
|
|
11929
12038
|
|
|
11930
12039
|
// src/release-run.ts
|
|
11931
|
-
import { readFileSync as
|
|
11932
|
-
import { dirname as dirname12, join as
|
|
12040
|
+
import { readFileSync as readFileSync21, writeFileSync as writeFileSync17, existsSync as existsSync23, copyFileSync, mkdirSync as mkdirSync15, mkdtempSync as mkdtempSync2, rmSync as rmSync13 } from "fs";
|
|
12041
|
+
import { dirname as dirname12, join as join27 } from "path";
|
|
11933
12042
|
import { tmpdir as tmpdir3 } from "os";
|
|
11934
12043
|
|
|
11935
12044
|
// src/release.ts
|
|
@@ -12670,13 +12779,13 @@ async function bump(o, size) {
|
|
|
12670
12779
|
}
|
|
12671
12780
|
return printed;
|
|
12672
12781
|
}
|
|
12673
|
-
const files = o.repo.release.versionFiles.map((f) =>
|
|
12782
|
+
const files = o.repo.release.versionFiles.map((f) => join27(o.cwd, f));
|
|
12674
12783
|
const first = files[0];
|
|
12675
|
-
if (!
|
|
12784
|
+
if (!existsSync23(first)) {
|
|
12676
12785
|
o.emit.error(`no ${o.repo.release.versionFiles[0]} to read a version from`);
|
|
12677
12786
|
return null;
|
|
12678
12787
|
}
|
|
12679
|
-
const current = JSON.parse(
|
|
12788
|
+
const current = JSON.parse(readFileSync21(first, "utf8")).version;
|
|
12680
12789
|
if (!current) {
|
|
12681
12790
|
o.emit.error(`no version field in ${o.repo.release.versionFiles[0]}`);
|
|
12682
12791
|
return null;
|
|
@@ -12689,27 +12798,27 @@ async function bump(o, size) {
|
|
|
12689
12798
|
return version;
|
|
12690
12799
|
}
|
|
12691
12800
|
for (const f of files) {
|
|
12692
|
-
const src =
|
|
12801
|
+
const src = readFileSync21(f, "utf8");
|
|
12693
12802
|
const out = src.replace(/("version"\s*:\s*")[^"]*(")/, `$1${version}$2`);
|
|
12694
12803
|
if (out === src) {
|
|
12695
12804
|
o.emit.error(`no version field in ${f}`);
|
|
12696
12805
|
return null;
|
|
12697
12806
|
}
|
|
12698
|
-
|
|
12807
|
+
writeFileSync17(f, out);
|
|
12699
12808
|
}
|
|
12700
12809
|
return version;
|
|
12701
12810
|
}
|
|
12702
12811
|
function writeChangelog(o, version, entries) {
|
|
12703
12812
|
const file = o.repo.release.changelog;
|
|
12704
12813
|
if (!file || entries.length === 0) return;
|
|
12705
|
-
const path =
|
|
12814
|
+
const path = join27(o.cwd, file);
|
|
12706
12815
|
const section = renderChangelogSection(version, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), entries);
|
|
12707
12816
|
if (o.dryRun) {
|
|
12708
12817
|
o.emit.emit(`would prepend ${entries.length} entr(ies) to ${file}`);
|
|
12709
12818
|
return;
|
|
12710
12819
|
}
|
|
12711
|
-
const existing =
|
|
12712
|
-
|
|
12820
|
+
const existing = existsSync23(path) ? readFileSync21(path, "utf8") : "# Changelog\n";
|
|
12821
|
+
writeFileSync17(path, insertChangelogSection(existing, section));
|
|
12713
12822
|
}
|
|
12714
12823
|
async function confirm(o, expected) {
|
|
12715
12824
|
if (!o.repo.hooks.released) return void 0;
|
|
@@ -12792,7 +12901,7 @@ function closeReleaseCheckout(cwd, root, dir, branch, run2 = gitOk) {
|
|
|
12792
12901
|
run2(cwd, ["worktree", "remove", "--force", dir]);
|
|
12793
12902
|
} finally {
|
|
12794
12903
|
try {
|
|
12795
|
-
|
|
12904
|
+
rmSync13(root, { recursive: true, force: true });
|
|
12796
12905
|
} finally {
|
|
12797
12906
|
run2(cwd, ["worktree", "prune"]);
|
|
12798
12907
|
run2(cwd, ["branch", "-D", branch]);
|
|
@@ -12802,8 +12911,8 @@ function closeReleaseCheckout(cwd, root, dir, branch, run2 = gitOk) {
|
|
|
12802
12911
|
function openReleaseCheckout(o) {
|
|
12803
12912
|
const { remote, base } = o.repo.branch;
|
|
12804
12913
|
const branch = `crew/release-${Date.now()}`;
|
|
12805
|
-
const root = mkdtempSync2(
|
|
12806
|
-
const dir =
|
|
12914
|
+
const root = mkdtempSync2(join27(tmpdir3(), "crew-release-"));
|
|
12915
|
+
const dir = join27(root, "checkout");
|
|
12807
12916
|
o.emit.emit(`release checkout: ${dir}`, { step: "release" });
|
|
12808
12917
|
const close = () => closeReleaseCheckout(o.cwd, root, dir, branch);
|
|
12809
12918
|
try {
|
|
@@ -12813,10 +12922,10 @@ function openReleaseCheckout(o) {
|
|
|
12813
12922
|
throw e;
|
|
12814
12923
|
}
|
|
12815
12924
|
for (const f of o.repo.worktrees.copy) {
|
|
12816
|
-
const src =
|
|
12817
|
-
if (!
|
|
12818
|
-
|
|
12819
|
-
copyFileSync(src,
|
|
12925
|
+
const src = join27(o.cwd, f);
|
|
12926
|
+
if (!existsSync23(src)) continue;
|
|
12927
|
+
mkdirSync15(dirname12(join27(dir, f)), { recursive: true });
|
|
12928
|
+
copyFileSync(src, join27(dir, f));
|
|
12820
12929
|
}
|
|
12821
12930
|
return { dir, branch, close };
|
|
12822
12931
|
}
|
|
@@ -13517,12 +13626,12 @@ Passed: ${passed}.${willClose ? " Closing this alert." : ""}`,
|
|
|
13517
13626
|
}
|
|
13518
13627
|
|
|
13519
13628
|
// src/live-runs.ts
|
|
13520
|
-
import { existsSync as
|
|
13521
|
-
import { join as
|
|
13629
|
+
import { existsSync as existsSync25, readdirSync as readdirSync11 } from "fs";
|
|
13630
|
+
import { join as join29 } from "path";
|
|
13522
13631
|
|
|
13523
13632
|
// src/interrupted.ts
|
|
13524
|
-
import { existsSync as
|
|
13525
|
-
import { join as
|
|
13633
|
+
import { existsSync as existsSync24, readdirSync as readdirSync10, readFileSync as readFileSync22 } from "fs";
|
|
13634
|
+
import { join as join28 } from "path";
|
|
13526
13635
|
var INTERRUPTED_MARKER = calloutHeader("NOTE", "Interrupted");
|
|
13527
13636
|
function interruptedComment(b) {
|
|
13528
13637
|
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.";
|
|
@@ -13541,21 +13650,21 @@ var pidAlive = (pid) => {
|
|
|
13541
13650
|
};
|
|
13542
13651
|
function runFinished(streamPath) {
|
|
13543
13652
|
const path = `${streamPath}.meta.json`;
|
|
13544
|
-
if (!
|
|
13653
|
+
if (!existsSync24(path)) return false;
|
|
13545
13654
|
try {
|
|
13546
|
-
return JSON.parse(
|
|
13655
|
+
return JSON.parse(readFileSync22(path, "utf8")).running !== true;
|
|
13547
13656
|
} catch {
|
|
13548
13657
|
return true;
|
|
13549
13658
|
}
|
|
13550
13659
|
}
|
|
13551
13660
|
function orphanedStreams(o) {
|
|
13552
|
-
const dir =
|
|
13553
|
-
if (!
|
|
13661
|
+
const dir = join28(o.stateDir, "streams");
|
|
13662
|
+
if (!existsSync24(dir)) return [];
|
|
13554
13663
|
const ownerAlive = o.isOwnerAlive ?? pidAlive;
|
|
13555
13664
|
const out = [];
|
|
13556
|
-
for (const name of
|
|
13665
|
+
for (const name of readdirSync10(dir)) {
|
|
13557
13666
|
if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
|
|
13558
|
-
const streamPath =
|
|
13667
|
+
const streamPath = join28(dir, name);
|
|
13559
13668
|
if (runFinished(streamPath)) continue;
|
|
13560
13669
|
const sidecar = readRunSidecar(streamPath);
|
|
13561
13670
|
if (!sidecar || sidecar.route !== o.routeLabel) continue;
|
|
@@ -13609,12 +13718,12 @@ async function reconcileRuns(o) {
|
|
|
13609
13718
|
|
|
13610
13719
|
// src/live-runs.ts
|
|
13611
13720
|
function liveRuns(stateDir, isAlive = runIsAlive) {
|
|
13612
|
-
const dir =
|
|
13613
|
-
if (!
|
|
13721
|
+
const dir = join29(stateDir, "streams");
|
|
13722
|
+
if (!existsSync25(dir)) return [];
|
|
13614
13723
|
const out = [];
|
|
13615
|
-
for (const name of
|
|
13724
|
+
for (const name of readdirSync11(dir)) {
|
|
13616
13725
|
if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
|
|
13617
|
-
const streamPath =
|
|
13726
|
+
const streamPath = join29(dir, name);
|
|
13618
13727
|
if (runFinished(streamPath)) continue;
|
|
13619
13728
|
const sidecar = readRunSidecar(streamPath);
|
|
13620
13729
|
if (!sidecar || !isAlive(sidecar)) continue;
|
|
@@ -13884,7 +13993,7 @@ function missingReleaserWarning(repo, release, ships, now = Date.now()) {
|
|
|
13884
13993
|
}
|
|
13885
13994
|
|
|
13886
13995
|
// src/watch.ts
|
|
13887
|
-
import { watch as watchFile, existsSync as
|
|
13996
|
+
import { watch as watchFile, existsSync as existsSync26, statSync as statSync7, openSync as openSync4, readSync as readSync2, closeSync as closeSync4 } from "fs";
|
|
13888
13997
|
var E = "\x1B[";
|
|
13889
13998
|
var dim = (s) => `${E}2m${s}${E}0m`;
|
|
13890
13999
|
var bold = (s) => `${E}1m${s}${E}0m`;
|
|
@@ -13985,7 +14094,7 @@ function renderHeaders(states, now = Date.now()) {
|
|
|
13985
14094
|
return states.map((s) => renderHeader(s, now)).join("");
|
|
13986
14095
|
}
|
|
13987
14096
|
function readFrom(file, from) {
|
|
13988
|
-
if (!
|
|
14097
|
+
if (!existsSync26(file)) return { events: [], offset: from };
|
|
13989
14098
|
const size = statSync7(file).size;
|
|
13990
14099
|
if (size < from) return readFrom(file, 0);
|
|
13991
14100
|
if (size === from) return { events: [], offset: from };
|
|
@@ -14228,17 +14337,17 @@ function nextRoles(decision, ran) {
|
|
|
14228
14337
|
}
|
|
14229
14338
|
|
|
14230
14339
|
// src/ship-name.ts
|
|
14231
|
-
import { existsSync as
|
|
14340
|
+
import { existsSync as existsSync28, mkdirSync as mkdirSync17, readFileSync as readFileSync24, renameSync as renameSync2, writeFileSync as writeFileSync19 } from "fs";
|
|
14232
14341
|
import { dirname as dirname14 } from "path";
|
|
14233
14342
|
import { parseDocument } from "yaml";
|
|
14234
14343
|
|
|
14235
14344
|
// src/machine-id.ts
|
|
14236
14345
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
14237
14346
|
import { createHash as createHash4 } from "crypto";
|
|
14238
|
-
import { existsSync as
|
|
14239
|
-
import { dirname as dirname13, join as
|
|
14347
|
+
import { existsSync as existsSync27, mkdirSync as mkdirSync16, readFileSync as readFileSync23, writeFileSync as writeFileSync18 } from "fs";
|
|
14348
|
+
import { dirname as dirname13, join as join30 } from "path";
|
|
14240
14349
|
function machineIdPathFor(stateDir) {
|
|
14241
|
-
return
|
|
14350
|
+
return join30(stateDir, "machine-id");
|
|
14242
14351
|
}
|
|
14243
14352
|
var sha256 = (s) => createHash4("sha256").update(s).digest("hex");
|
|
14244
14353
|
function readRawMachineId(platform2 = process.platform) {
|
|
@@ -14251,8 +14360,8 @@ function readRawMachineId(platform2 = process.platform) {
|
|
|
14251
14360
|
return /"IOPlatformUUID"\s*=\s*"([^"]+)"/.exec(out)?.[1];
|
|
14252
14361
|
}
|
|
14253
14362
|
for (const p of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
|
|
14254
|
-
if (
|
|
14255
|
-
const id =
|
|
14363
|
+
if (existsSync27(p)) {
|
|
14364
|
+
const id = readFileSync23(p, "utf8").trim();
|
|
14256
14365
|
if (id) return id;
|
|
14257
14366
|
}
|
|
14258
14367
|
}
|
|
@@ -14263,15 +14372,15 @@ function readRawMachineId(platform2 = process.platform) {
|
|
|
14263
14372
|
function ensureMachineId(stateDir, opts = {}) {
|
|
14264
14373
|
if (opts.override?.trim()) return sha256(opts.override.trim());
|
|
14265
14374
|
const path = machineIdPathFor(stateDir);
|
|
14266
|
-
if (
|
|
14267
|
-
const cached =
|
|
14375
|
+
if (existsSync27(path)) {
|
|
14376
|
+
const cached = readFileSync23(path, "utf8").trim();
|
|
14268
14377
|
if (/^[0-9a-f]{64}$/.test(cached)) return cached;
|
|
14269
14378
|
}
|
|
14270
14379
|
const raw = (opts.read ?? readRawMachineId)();
|
|
14271
14380
|
if (!raw) return void 0;
|
|
14272
14381
|
const hashed = sha256(raw);
|
|
14273
|
-
|
|
14274
|
-
|
|
14382
|
+
mkdirSync16(dirname13(path), { recursive: true });
|
|
14383
|
+
writeFileSync18(path, `${hashed}
|
|
14275
14384
|
`, { mode: 384 });
|
|
14276
14385
|
return hashed;
|
|
14277
14386
|
}
|
|
@@ -14342,34 +14451,34 @@ async function syncRouteName(i) {
|
|
|
14342
14451
|
return { status: "ok", ...base, action, name: i.local };
|
|
14343
14452
|
}
|
|
14344
14453
|
function readSyncedName(resolvedFile) {
|
|
14345
|
-
if (!
|
|
14454
|
+
if (!existsSync28(resolvedFile)) return void 0;
|
|
14346
14455
|
try {
|
|
14347
|
-
const v = JSON.parse(
|
|
14456
|
+
const v = JSON.parse(readFileSync24(resolvedFile, "utf8")).shipName;
|
|
14348
14457
|
return typeof v === "string" ? v : void 0;
|
|
14349
14458
|
} catch {
|
|
14350
14459
|
return void 0;
|
|
14351
14460
|
}
|
|
14352
14461
|
}
|
|
14353
14462
|
function writeSyncedName(resolvedFile, name) {
|
|
14354
|
-
if (!
|
|
14463
|
+
if (!existsSync28(resolvedFile)) return;
|
|
14355
14464
|
let raw;
|
|
14356
14465
|
try {
|
|
14357
|
-
raw = JSON.parse(
|
|
14466
|
+
raw = JSON.parse(readFileSync24(resolvedFile, "utf8"));
|
|
14358
14467
|
} catch {
|
|
14359
14468
|
return;
|
|
14360
14469
|
}
|
|
14361
14470
|
if (raw.shipName === name) return;
|
|
14362
14471
|
raw.shipName = name;
|
|
14363
|
-
|
|
14472
|
+
mkdirSync17(dirname14(resolvedFile), { recursive: true });
|
|
14364
14473
|
const tmp = `${resolvedFile}.tmp${process.pid}`;
|
|
14365
|
-
|
|
14474
|
+
writeFileSync19(tmp, `${JSON.stringify(raw, null, 2)}
|
|
14366
14475
|
`);
|
|
14367
14476
|
renameSync2(tmp, resolvedFile);
|
|
14368
14477
|
}
|
|
14369
14478
|
async function reconcileShipName(i) {
|
|
14370
14479
|
let local = i.ship.name;
|
|
14371
14480
|
try {
|
|
14372
|
-
local = shipNameOfYaml(
|
|
14481
|
+
local = shipNameOfYaml(readFileSync24(i.configFile, "utf8")) ?? local;
|
|
14373
14482
|
} catch {
|
|
14374
14483
|
}
|
|
14375
14484
|
const resolvedFile = resolvedPathFor(i.stateDir, i.route);
|
|
@@ -14393,8 +14502,8 @@ async function reconcileShipName(i) {
|
|
|
14393
14502
|
}
|
|
14394
14503
|
if (result.status !== "ok") return result;
|
|
14395
14504
|
if (result.action === "adopt" || result.action === "row-wins") {
|
|
14396
|
-
const text =
|
|
14397
|
-
|
|
14505
|
+
const text = readFileSync24(i.configFile, "utf8");
|
|
14506
|
+
writeFileSync19(i.configFile, setShipNameInYaml(text, result.name));
|
|
14398
14507
|
i.ship.name = result.name;
|
|
14399
14508
|
if (result.action === "row-wins") {
|
|
14400
14509
|
i.emit?.warn(
|
|
@@ -15044,7 +15153,7 @@ function describeCapability(i) {
|
|
|
15044
15153
|
}
|
|
15045
15154
|
|
|
15046
15155
|
// src/agents-sync-run.ts
|
|
15047
|
-
import { existsSync as
|
|
15156
|
+
import { existsSync as existsSync29, mkdirSync as mkdirSync18, readFileSync as readFileSync25, writeFileSync as writeFileSync20 } from "fs";
|
|
15048
15157
|
import { dirname as dirname15 } from "path";
|
|
15049
15158
|
async function runAgentsSync(target, o) {
|
|
15050
15159
|
const dryRun = o.dryRun ?? false;
|
|
@@ -15078,12 +15187,12 @@ async function runAgentsSync(target, o) {
|
|
|
15078
15187
|
for (const s of skillResult?.outcomes ?? []) o.say(describeSkillSyncOutcome(s));
|
|
15079
15188
|
if (!dryRun) {
|
|
15080
15189
|
const resolvedPath = resolvedPathFor(o.stateDir, target.route);
|
|
15081
|
-
const raw =
|
|
15190
|
+
const raw = existsSync29(resolvedPath) ? JSON.parse(readFileSync25(resolvedPath, "utf8")) : {};
|
|
15082
15191
|
raw.agentPersonas = personaResult.agentPersonas;
|
|
15083
15192
|
if (skillResult) raw.agentSkills = skillResult.agentSkills;
|
|
15084
15193
|
if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
|
|
15085
|
-
|
|
15086
|
-
|
|
15194
|
+
mkdirSync18(dirname15(resolvedPath), { recursive: true });
|
|
15195
|
+
writeFileSync20(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
15087
15196
|
`);
|
|
15088
15197
|
if (target.resolved) {
|
|
15089
15198
|
target.resolved.agentPersonas = personaResult.agentPersonas;
|
|
@@ -15187,7 +15296,7 @@ async function showLogEntry(route, entryId, opts = {}) {
|
|
|
15187
15296
|
|
|
15188
15297
|
// src/repos-check.ts
|
|
15189
15298
|
import { spawnSync as spawnSync6 } from "child_process";
|
|
15190
|
-
import { readFileSync as
|
|
15299
|
+
import { readFileSync as readFileSync26 } from "fs";
|
|
15191
15300
|
import { basename as basename8 } from "path";
|
|
15192
15301
|
var POSIX_SHELL = /^(?:ba|z|da|k|a|mk)?sh$|^fish$/;
|
|
15193
15302
|
function checkHookSyntax(script, shell) {
|
|
@@ -15228,7 +15337,7 @@ function checkRepoSpec(dir, opts = {}) {
|
|
|
15228
15337
|
}
|
|
15229
15338
|
let parsed;
|
|
15230
15339
|
try {
|
|
15231
|
-
parsed = parseRepoConfig(
|
|
15340
|
+
parsed = parseRepoConfig(readFileSync26(file, "utf8"), file);
|
|
15232
15341
|
} catch (e) {
|
|
15233
15342
|
if (!(e instanceof RepoConfigError)) throw e;
|
|
15234
15343
|
return { ...empty, file, error: e.message, exitCode: 1 };
|
|
@@ -15285,7 +15394,7 @@ import { isMap as isMap2, parseDocument as parseDocument3, YAMLMap } from "yaml"
|
|
|
15285
15394
|
|
|
15286
15395
|
// src/repos-cmd.ts
|
|
15287
15396
|
import { homedir as homedir9 } from "os";
|
|
15288
|
-
import { basename as basename9, join as
|
|
15397
|
+
import { basename as basename9, join as join31, resolve as resolve3 } from "path";
|
|
15289
15398
|
import { isMap, isSeq, parseDocument as parseDocument2, stringify as stringify2 } from "yaml";
|
|
15290
15399
|
var ReposError = class extends Error {
|
|
15291
15400
|
};
|
|
@@ -15563,7 +15672,7 @@ function planRepoClone(i) {
|
|
|
15563
15672
|
}
|
|
15564
15673
|
const ship = doc.get("ship", true);
|
|
15565
15674
|
const basePath = String(m.get("reposBasePath") ?? (isMap(ship) ? ship.get("reposBasePath") : void 0) ?? "~/Crew");
|
|
15566
|
-
const target = i.dir ? expandDir(i.dir, i.base) :
|
|
15675
|
+
const target = i.dir ? expandDir(i.dir, i.base) : join31(expandDir(basePath, i.base), i.route.split("/")[0], name);
|
|
15567
15676
|
const add = planRepoAdd({ text: i.text, base: i.base, route: i.route, path: target, origin: remote, resolved: i.resolved, name, isCheckout: i.isCheckout });
|
|
15568
15677
|
return { ...add, remote, target };
|
|
15569
15678
|
}
|
|
@@ -15962,7 +16071,7 @@ ${run2.err}` });
|
|
|
15962
16071
|
}
|
|
15963
16072
|
|
|
15964
16073
|
// src/sample-ticket.ts
|
|
15965
|
-
import { basename as basename10, dirname as dirname17, join as
|
|
16074
|
+
import { basename as basename10, dirname as dirname17, join as join32 } from "path";
|
|
15966
16075
|
var SAMPLE_REPO = "crew-sample";
|
|
15967
16076
|
var SAMPLE_REMOTE = `local/${SAMPLE_REPO}`;
|
|
15968
16077
|
var SAMPLE_MARKER = "sample: true";
|
|
@@ -15985,8 +16094,8 @@ has something safe to work on: its test and build hooks always pass, it has no
|
|
|
15985
16094
|
remote, and \`crew sample-ticket --remove\` deletes it.
|
|
15986
16095
|
`;
|
|
15987
16096
|
function sampleDir(existing, reposBase, workspace) {
|
|
15988
|
-
if (existing.length > 0) return
|
|
15989
|
-
return
|
|
16097
|
+
if (existing.length > 0) return join32(dirname17(existing[0]), SAMPLE_REPO);
|
|
16098
|
+
return join32(reposBase, workspace, SAMPLE_REPO);
|
|
15990
16099
|
}
|
|
15991
16100
|
var SAMPLE_TITLE = "Sample: add a documented `greet` function with a test";
|
|
15992
16101
|
function sampleDescription() {
|
|
@@ -16078,8 +16187,8 @@ function schedulerInstalled(plan, exists, crontab) {
|
|
|
16078
16187
|
}
|
|
16079
16188
|
|
|
16080
16189
|
// src/worktree-sweep.ts
|
|
16081
|
-
import { existsSync as
|
|
16082
|
-
import { basename as basename11, dirname as dirname18, join as
|
|
16190
|
+
import { existsSync as existsSync30, readFileSync as readFileSync27, readdirSync as readdirSync12 } from "fs";
|
|
16191
|
+
import { basename as basename11, dirname as dirname18, join as join33, resolve as resolve4 } from "path";
|
|
16083
16192
|
function planWorktreeSweep(target, tickets, contract) {
|
|
16084
16193
|
const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
|
|
16085
16194
|
const verified = contract.statuses.verified;
|
|
@@ -16138,7 +16247,7 @@ function siblingWorktreeDirs(target) {
|
|
|
16138
16247
|
const registered = new Set(worktrees(target.dir).map((w) => resolve4(w.path)));
|
|
16139
16248
|
let names;
|
|
16140
16249
|
try {
|
|
16141
|
-
names =
|
|
16250
|
+
names = readdirSync12(parent);
|
|
16142
16251
|
} catch {
|
|
16143
16252
|
return [];
|
|
16144
16253
|
}
|
|
@@ -16146,7 +16255,7 @@ function siblingWorktreeDirs(target) {
|
|
|
16146
16255
|
for (const name of names) {
|
|
16147
16256
|
if (!name.startsWith(stem)) continue;
|
|
16148
16257
|
const m = DIR_NUMBER_SUFFIXED.exec(name);
|
|
16149
|
-
const path =
|
|
16258
|
+
const path = join33(parent, name);
|
|
16150
16259
|
if (m && !registered.has(resolve4(path))) out.push({ path, number: m[1] });
|
|
16151
16260
|
}
|
|
16152
16261
|
return out;
|
|
@@ -16181,7 +16290,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
|
|
|
16181
16290
|
let removed = 0;
|
|
16182
16291
|
let keptBranches = 0;
|
|
16183
16292
|
for (const a of actions) {
|
|
16184
|
-
if (!dryRun && base && !a.keepBranch &&
|
|
16293
|
+
if (!dryRun && base && !a.keepBranch && existsSync30(a.path) && isHandBackMerge(a.path, base)) {
|
|
16185
16294
|
if (gitOk(a.path, ["merge", "--abort"]) !== null) {
|
|
16186
16295
|
log.emit(
|
|
16187
16296
|
`abandoned the hand-back merge of ${base} in ${a.path}: ${a.ticket.issue_id} deployed without it`,
|
|
@@ -16189,7 +16298,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
|
|
|
16189
16298
|
);
|
|
16190
16299
|
}
|
|
16191
16300
|
}
|
|
16192
|
-
if (!dryRun &&
|
|
16301
|
+
if (!dryRun && existsSync30(a.path)) {
|
|
16193
16302
|
let dirty = false;
|
|
16194
16303
|
try {
|
|
16195
16304
|
dirty = status(a.path).length > 0;
|
|
@@ -16200,7 +16309,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
|
|
|
16200
16309
|
continue;
|
|
16201
16310
|
}
|
|
16202
16311
|
}
|
|
16203
|
-
if (!dryRun && a.requirePushed && a.branch &&
|
|
16312
|
+
if (!dryRun && a.requirePushed && a.branch && existsSync30(a.path) && remoteConfigured(cwd, remote) && worktreeHoldsUnpushed(a.path, remote)) {
|
|
16204
16313
|
refuse(a, "unpushed commits", `${a.branch} has commits that are not on ${remote}`);
|
|
16205
16314
|
continue;
|
|
16206
16315
|
}
|
|
@@ -16249,7 +16358,7 @@ function isHandBackMerge(path, base) {
|
|
|
16249
16358
|
if (!msgPath) return false;
|
|
16250
16359
|
let first;
|
|
16251
16360
|
try {
|
|
16252
|
-
first =
|
|
16361
|
+
first = readFileSync27(resolve4(path, msgPath), "utf8").split("\n")[0] ?? "";
|
|
16253
16362
|
} catch {
|
|
16254
16363
|
return false;
|
|
16255
16364
|
}
|
|
@@ -16354,14 +16463,14 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
|
|
|
16354
16463
|
}
|
|
16355
16464
|
|
|
16356
16465
|
// src/stream-sweep.ts
|
|
16357
|
-
import { existsSync as
|
|
16358
|
-
import { join as
|
|
16466
|
+
import { existsSync as existsSync31, readdirSync as readdirSync13, rmSync as rmSync14, statSync as statSync8 } from "fs";
|
|
16467
|
+
import { join as join34 } from "path";
|
|
16359
16468
|
var DAY_MS2 = 24 * 60 * 60 * 1e3;
|
|
16360
16469
|
function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
|
|
16361
|
-
const dir =
|
|
16362
|
-
if (!
|
|
16470
|
+
const dir = join34(stateDir, "streams");
|
|
16471
|
+
if (!existsSync31(dir)) return [];
|
|
16363
16472
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
16364
|
-
return
|
|
16473
|
+
return readdirSync13(dir).map((name) => join34(dir, name)).filter((path) => {
|
|
16365
16474
|
try {
|
|
16366
16475
|
return statSync8(path).mtimeMs < cutoff;
|
|
16367
16476
|
} catch {
|
|
@@ -16377,7 +16486,7 @@ function applyStreamSweep(paths, dryRun, log) {
|
|
|
16377
16486
|
continue;
|
|
16378
16487
|
}
|
|
16379
16488
|
try {
|
|
16380
|
-
|
|
16489
|
+
rmSync14(path, { force: true });
|
|
16381
16490
|
removed++;
|
|
16382
16491
|
log.emit(`removed stream artifact ${path}`, { step: "worktree" });
|
|
16383
16492
|
} catch (e) {
|
|
@@ -16387,9 +16496,9 @@ function applyStreamSweep(paths, dryRun, log) {
|
|
|
16387
16496
|
return removed;
|
|
16388
16497
|
}
|
|
16389
16498
|
function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
16390
|
-
if (!
|
|
16499
|
+
if (!existsSync31(stateDir)) return [];
|
|
16391
16500
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
16392
|
-
return
|
|
16501
|
+
return readdirSync13(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join34(stateDir, name)).filter((path) => {
|
|
16393
16502
|
try {
|
|
16394
16503
|
return statSync8(path).mtimeMs < cutoff;
|
|
16395
16504
|
} catch {
|
|
@@ -16399,7 +16508,7 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
|
16399
16508
|
}
|
|
16400
16509
|
|
|
16401
16510
|
// src/cli.ts
|
|
16402
|
-
import { realpathSync as realpathSync3, readFileSync as
|
|
16511
|
+
import { realpathSync as realpathSync3, readFileSync as readFileSync32, existsSync as existsSync36, mkdirSync as mkdirSync23, writeFileSync as writeFileSync24, rmSync as rmSync16, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync3, statSync as statSync10, readdirSync as readdirSync17 } from "fs";
|
|
16403
16512
|
import { createInterface } from "readline/promises";
|
|
16404
16513
|
|
|
16405
16514
|
// src/periscope-cmd.ts
|
|
@@ -16527,29 +16636,160 @@ ${tail(preview.output)}`);
|
|
|
16527
16636
|
return abandon(`recording the Periscopes row failed: ${e.message}`);
|
|
16528
16637
|
}
|
|
16529
16638
|
d.say("recorded", "recorded the Periscopes row", { periscopeRecordId: row.id });
|
|
16639
|
+
try {
|
|
16640
|
+
d.recordLocal?.({
|
|
16641
|
+
ticket: draft.tag,
|
|
16642
|
+
ticketRecordId: draft.id,
|
|
16643
|
+
periscopeRecordId: row.id,
|
|
16644
|
+
repo: args.repo,
|
|
16645
|
+
worktree: wt.path,
|
|
16646
|
+
branch: wt.branch,
|
|
16647
|
+
url,
|
|
16648
|
+
host: d.host,
|
|
16649
|
+
state: "live",
|
|
16650
|
+
isolated: args.isolate,
|
|
16651
|
+
startedAt: now.toISOString(),
|
|
16652
|
+
lastActivityAt: now.toISOString(),
|
|
16653
|
+
expiresAt: new Date(now.getTime() + d.caps.idleMinutes * 6e4).toISOString()
|
|
16654
|
+
});
|
|
16655
|
+
} catch (e) {
|
|
16656
|
+
d.say("warning", `local periscope record not written: ${e.message}`);
|
|
16657
|
+
}
|
|
16530
16658
|
await d.comment(draft.id, progressComment(url, handoff?.output)).catch((e) => {
|
|
16531
16659
|
d.say("warning", `progress comment not posted: ${e.message}`);
|
|
16532
16660
|
});
|
|
16533
16661
|
return { ticket: draft.tag, ticketRecordId: draft.id, worktree: wt.path, branch: wt.branch, url, periscopeRecordId: row.id };
|
|
16534
16662
|
}
|
|
16663
|
+
function parsePeriscopeDownArgs(positional2, flags) {
|
|
16664
|
+
const usage2 = "usage: crew periscope down [route] NNN --keep|--discard [--allow-empty] [--json]";
|
|
16665
|
+
const rest = positional2.slice(2);
|
|
16666
|
+
if (rest.length === 0 || rest.length > 2) throw new PeriscopeError(usage2);
|
|
16667
|
+
if (flags.keep === flags.discard) throw new PeriscopeError(`pass exactly one of --keep or --discard
|
|
16668
|
+
${usage2}`);
|
|
16669
|
+
const m = /^(?:[A-Za-z]+-)?(\d+)$/.exec(rest[rest.length - 1]);
|
|
16670
|
+
if (!m) throw new PeriscopeError(`"${rest[rest.length - 1]}" is not a ticket number
|
|
16671
|
+
${usage2}`);
|
|
16672
|
+
return {
|
|
16673
|
+
...rest.length === 2 ? { route: rest[0] } : {},
|
|
16674
|
+
number: m[1],
|
|
16675
|
+
mode: flags.keep ? "keep" : "discard",
|
|
16676
|
+
allowEmpty: flags.allowEmpty,
|
|
16677
|
+
json: flags.json
|
|
16678
|
+
};
|
|
16679
|
+
}
|
|
16680
|
+
var PROTOTYPE_HEADING = "## Prototype";
|
|
16681
|
+
function prototypeSection(files) {
|
|
16682
|
+
const added = files.reduce((n, f) => n + f.added, 0);
|
|
16683
|
+
const removed = files.reduce((n, f) => n + f.removed, 0);
|
|
16684
|
+
return [
|
|
16685
|
+
PROTOTYPE_HEADING,
|
|
16686
|
+
"",
|
|
16687
|
+
"This branch began as a Periscope prototype and may be rewritten rather than treated as reviewed work.",
|
|
16688
|
+
"",
|
|
16689
|
+
...files.length === 0 ? ["No files differ from the base."] : files.map((f) => `- \`${f.path}\` (+${f.added} \u2212${f.removed})`),
|
|
16690
|
+
"",
|
|
16691
|
+
`${files.length} file${files.length === 1 ? "" : "s"} changed, ${added} insertion${added === 1 ? "" : "s"}(+), ${removed} deletion${removed === 1 ? "" : "s"}(\u2212)`
|
|
16692
|
+
].join("\n");
|
|
16693
|
+
}
|
|
16694
|
+
function forgetLocal(d, ticketRecordId) {
|
|
16695
|
+
try {
|
|
16696
|
+
d.forgetLocal?.(ticketRecordId);
|
|
16697
|
+
} catch (e) {
|
|
16698
|
+
d.say("warn", `could not remove the local periscope record: ${e.message}`);
|
|
16699
|
+
}
|
|
16700
|
+
}
|
|
16701
|
+
async function runPeriscopeDown(d, args) {
|
|
16702
|
+
const rows = await d.rowsForTicket(args.number);
|
|
16703
|
+
if (rows.length === 0) throw new PeriscopeError(`no periscope for ticket ${args.number} on this ship`);
|
|
16704
|
+
const row = rows.find((r) => r.state === "live");
|
|
16705
|
+
if (!row) {
|
|
16706
|
+
const done = rows[0];
|
|
16707
|
+
d.say("unchanged", `${done.ticket} is already ${done.state}; nothing to do`);
|
|
16708
|
+
return { ticket: done.ticket, mode: args.mode, outcome: "unchanged", state: done.state };
|
|
16709
|
+
}
|
|
16710
|
+
if (args.mode === "discard") {
|
|
16711
|
+
const killed2 = await d.stopServer(row.worktree, row.number);
|
|
16712
|
+
d.say("server", killed2 ? `stopped ${killed2} process(es)` : "no server was listening");
|
|
16713
|
+
d.removeWorktree(row.worktree, row.branch, true);
|
|
16714
|
+
d.say("worktree", `removed ${row.worktree} and branch ${row.branch}`);
|
|
16715
|
+
await d.updateTicket(row.ticketRecordId, {
|
|
16716
|
+
status: "discarded",
|
|
16717
|
+
fillProjectAndRepo: false,
|
|
16718
|
+
note: "The periscope was discarded; nothing was pushed."
|
|
16719
|
+
});
|
|
16720
|
+
d.say("ticket", `${row.ticket} closed as obsolete`);
|
|
16721
|
+
await d.setRowState(row.id, "discarded");
|
|
16722
|
+
d.say("row", "row is discarded");
|
|
16723
|
+
forgetLocal(d, row.ticketRecordId);
|
|
16724
|
+
return { ticket: row.ticket, mode: "discard", outcome: "done", state: "discarded" };
|
|
16725
|
+
}
|
|
16726
|
+
const dirty = d.hasChanges(row.worktree);
|
|
16727
|
+
if (!dirty && !args.allowEmpty) {
|
|
16728
|
+
throw new PeriscopeError(`${row.ticket} has nothing to commit; pass --allow-empty to keep the branch anyway, or --discard`);
|
|
16729
|
+
}
|
|
16730
|
+
if (dirty) {
|
|
16731
|
+
d.commitAll(row.worktree, `${row.ticket} Periscope prototype`);
|
|
16732
|
+
d.say("commit", `committed the worktree on ${row.branch}`);
|
|
16733
|
+
}
|
|
16734
|
+
const pushed = d.push(row.worktree, row.branch);
|
|
16735
|
+
d.say("push", `pushed ${pushed.sha} to ${pushed.remote}/${row.branch}`, { sha: pushed.sha });
|
|
16736
|
+
const t = await d.readTicket(row.ticketRecordId);
|
|
16737
|
+
const section = prototypeSection(d.changedFiles(row.worktree));
|
|
16738
|
+
const description = t.description.includes(PROTOTYPE_HEADING) ? t.description : `${t.description.trimEnd()}
|
|
16739
|
+
|
|
16740
|
+
${section}`;
|
|
16741
|
+
await d.updateTicket(row.ticketRecordId, {
|
|
16742
|
+
description,
|
|
16743
|
+
fillProjectAndRepo: !t.hasProject || !t.hasRepo,
|
|
16744
|
+
status: "new"
|
|
16745
|
+
});
|
|
16746
|
+
d.say("ticket", `${row.ticket} is new and unassigned with a Prototype section`);
|
|
16747
|
+
await d.postPushed(row.ticketRecordId, pushed.sha, pushed.remote, row.branch);
|
|
16748
|
+
const killed = await d.stopServer(row.worktree, row.number);
|
|
16749
|
+
d.say("server", killed ? `stopped ${killed} process(es)` : "no server was listening");
|
|
16750
|
+
d.removeWorktree(row.worktree, row.branch, false);
|
|
16751
|
+
d.say("worktree", `removed ${row.worktree}; ${row.branch} stays, pushed`);
|
|
16752
|
+
await d.setRowState(row.id, "kept");
|
|
16753
|
+
d.say("row", "row is kept");
|
|
16754
|
+
forgetLocal(d, row.ticketRecordId);
|
|
16755
|
+
return { ticket: row.ticket, mode: "keep", outcome: "done", state: "kept", sha: pushed.sha };
|
|
16756
|
+
}
|
|
16757
|
+
function periscopeListEntries(rows, now, isListening) {
|
|
16758
|
+
return rows.filter((r) => r.state === "live").map((r) => {
|
|
16759
|
+
const at = r.lastActivityAt ? new Date(r.lastActivityAt).getTime() : NaN;
|
|
16760
|
+
return { ...r, idleMinutes: Number.isNaN(at) ? null : Math.max(0, Math.floor((now.getTime() - at) / 6e4)), listening: isListening(r) };
|
|
16761
|
+
});
|
|
16762
|
+
}
|
|
16763
|
+
function formatPeriscopeList(entries) {
|
|
16764
|
+
if (entries.length === 0) return "no live periscopes on this ship";
|
|
16765
|
+
return entries.map((e) => [
|
|
16766
|
+
e.ticket,
|
|
16767
|
+
e.repo,
|
|
16768
|
+
e.worktree,
|
|
16769
|
+
e.url,
|
|
16770
|
+
e.state,
|
|
16771
|
+
e.idleMinutes === null ? "idle ?" : `idle ${e.idleMinutes} min`,
|
|
16772
|
+
e.listening ? "listening" : "not listening"
|
|
16773
|
+
].join(" ")).join("\n");
|
|
16774
|
+
}
|
|
16535
16775
|
|
|
16536
16776
|
// src/cli.ts
|
|
16537
|
-
import { dirname as
|
|
16777
|
+
import { dirname as dirOf2, resolve as resolvePath } from "path";
|
|
16538
16778
|
|
|
16539
16779
|
// src/ssh-keys.ts
|
|
16540
16780
|
import { execFileSync as execFileSync6 } from "child_process";
|
|
16541
|
-
import { existsSync as
|
|
16542
|
-
import { dirname as dirname19, join as
|
|
16781
|
+
import { existsSync as existsSync32, mkdirSync as mkdirSync19, readFileSync as readFileSync28, chmodSync } from "fs";
|
|
16782
|
+
import { dirname as dirname19, join as join35 } from "path";
|
|
16543
16783
|
var SshKeyError = class extends Error {
|
|
16544
16784
|
};
|
|
16545
16785
|
function shipSshKeyPathFor(stateDir) {
|
|
16546
|
-
return
|
|
16786
|
+
return join35(stateDir, "ssh", "id_ed25519");
|
|
16547
16787
|
}
|
|
16548
16788
|
function ensureShipSshKeypair(stateDir) {
|
|
16549
16789
|
const privateKeyPath = shipSshKeyPathFor(stateDir);
|
|
16550
16790
|
const publicKeyPath = `${privateKeyPath}.pub`;
|
|
16551
|
-
if (!
|
|
16552
|
-
|
|
16791
|
+
if (!existsSync32(privateKeyPath)) {
|
|
16792
|
+
mkdirSync19(dirname19(privateKeyPath), { recursive: true });
|
|
16553
16793
|
try {
|
|
16554
16794
|
execFileSync6("ssh-keygen", [
|
|
16555
16795
|
"-t",
|
|
@@ -16569,10 +16809,10 @@ function ensureShipSshKeypair(stateDir) {
|
|
|
16569
16809
|
}
|
|
16570
16810
|
chmodSync(privateKeyPath, 384);
|
|
16571
16811
|
}
|
|
16572
|
-
if (!
|
|
16812
|
+
if (!existsSync32(publicKeyPath)) {
|
|
16573
16813
|
throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
|
|
16574
16814
|
}
|
|
16575
|
-
const publicKey =
|
|
16815
|
+
const publicKey = readFileSync28(publicKeyPath, "utf8").trim();
|
|
16576
16816
|
return { privateKeyPath, publicKeyPath, publicKey };
|
|
16577
16817
|
}
|
|
16578
16818
|
function sshKeygenAvailable() {
|
|
@@ -16789,9 +17029,9 @@ function syncAllPassengerCheckouts(cfg) {
|
|
|
16789
17029
|
|
|
16790
17030
|
// src/tunnel.ts
|
|
16791
17031
|
import { spawn as spawn5 } from "child_process";
|
|
16792
|
-
import { existsSync as
|
|
17032
|
+
import { existsSync as existsSync33, mkdirSync as mkdirSync20, readFileSync as readFileSync29, writeFileSync as writeFileSync21, unlinkSync as unlinkSync3, readdirSync as readdirSync14 } from "fs";
|
|
16793
17033
|
import { randomInt } from "crypto";
|
|
16794
|
-
import { join as
|
|
17034
|
+
import { join as join36 } from "path";
|
|
16795
17035
|
var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
|
|
16796
17036
|
var defaultSpawn = (cmd, args) => spawn5(cmd, args, DEFAULT_SPAWN_OPTIONS);
|
|
16797
17037
|
var SLUG_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
@@ -16858,21 +17098,21 @@ function startTunnel(opts) {
|
|
|
16858
17098
|
};
|
|
16859
17099
|
}
|
|
16860
17100
|
function tunnelStatePath(stateDir, workspaceId) {
|
|
16861
|
-
return
|
|
17101
|
+
return join36(stateDir, "passengers", `${workspaceId}.json`);
|
|
16862
17102
|
}
|
|
16863
17103
|
function readPersistedTunnel(stateDir, workspaceId) {
|
|
16864
17104
|
const path = tunnelStatePath(stateDir, workspaceId);
|
|
16865
|
-
if (!
|
|
17105
|
+
if (!existsSync33(path)) return void 0;
|
|
16866
17106
|
try {
|
|
16867
|
-
return JSON.parse(
|
|
17107
|
+
return JSON.parse(readFileSync29(path, "utf8"));
|
|
16868
17108
|
} catch {
|
|
16869
17109
|
return void 0;
|
|
16870
17110
|
}
|
|
16871
17111
|
}
|
|
16872
17112
|
function writePersistedTunnel(stateDir, state) {
|
|
16873
17113
|
const path = tunnelStatePath(stateDir, state.workspaceId);
|
|
16874
|
-
|
|
16875
|
-
|
|
17114
|
+
mkdirSync20(join36(stateDir, "passengers"), { recursive: true });
|
|
17115
|
+
writeFileSync21(path, `${JSON.stringify(state, null, 2)}
|
|
16876
17116
|
`);
|
|
16877
17117
|
}
|
|
16878
17118
|
function updatePersistedTunnel(stateDir, workspaceId, patch2) {
|
|
@@ -16969,9 +17209,9 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
|
|
|
16969
17209
|
}
|
|
16970
17210
|
}
|
|
16971
17211
|
function readAllPersistedWorkspaceIds(stateDir) {
|
|
16972
|
-
const dir =
|
|
16973
|
-
if (!
|
|
16974
|
-
return
|
|
17212
|
+
const dir = join36(stateDir, "passengers");
|
|
17213
|
+
if (!existsSync33(dir)) return [];
|
|
17214
|
+
return readdirSync14(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
|
|
16975
17215
|
}
|
|
16976
17216
|
|
|
16977
17217
|
// src/mcp-endpoint-registry.ts
|
|
@@ -17091,8 +17331,8 @@ async function unregisterEndpoint(workspaceId, info, d) {
|
|
|
17091
17331
|
import { createServer as createHttpServer } from "http";
|
|
17092
17332
|
import { spawn as spawn6 } from "child_process";
|
|
17093
17333
|
import { randomBytes } from "crypto";
|
|
17094
|
-
import { existsSync as
|
|
17095
|
-
import { join as
|
|
17334
|
+
import { existsSync as existsSync34, mkdirSync as mkdirSync21, readFileSync as readFileSync30, writeFileSync as writeFileSync22, unlinkSync as unlinkSync4, readdirSync as readdirSync15 } from "fs";
|
|
17335
|
+
import { join as join37 } from "path";
|
|
17096
17336
|
|
|
17097
17337
|
// src/secret-compare.ts
|
|
17098
17338
|
import { timingSafeEqual } from "crypto";
|
|
@@ -17181,21 +17421,21 @@ var defaultSpawn2 = (cmd, args, env) => {
|
|
|
17181
17421
|
return child;
|
|
17182
17422
|
};
|
|
17183
17423
|
function syncDaemonStatePath(stateDir, workspaceId) {
|
|
17184
|
-
return
|
|
17424
|
+
return join37(stateDir, "passengers", `${workspaceId}-sync.json`);
|
|
17185
17425
|
}
|
|
17186
17426
|
function readPersistedSyncDaemon(stateDir, workspaceId) {
|
|
17187
17427
|
const path = syncDaemonStatePath(stateDir, workspaceId);
|
|
17188
|
-
if (!
|
|
17428
|
+
if (!existsSync34(path)) return void 0;
|
|
17189
17429
|
try {
|
|
17190
|
-
return JSON.parse(
|
|
17430
|
+
return JSON.parse(readFileSync30(path, "utf8"));
|
|
17191
17431
|
} catch {
|
|
17192
17432
|
return void 0;
|
|
17193
17433
|
}
|
|
17194
17434
|
}
|
|
17195
17435
|
function writePersistedSyncDaemon(stateDir, state) {
|
|
17196
17436
|
const path = syncDaemonStatePath(stateDir, state.workspaceId);
|
|
17197
|
-
|
|
17198
|
-
|
|
17437
|
+
mkdirSync21(join37(stateDir, "passengers"), { recursive: true });
|
|
17438
|
+
writeFileSync22(path, `${JSON.stringify(state, null, 2)}
|
|
17199
17439
|
`);
|
|
17200
17440
|
}
|
|
17201
17441
|
function removePersistedSyncDaemon(stateDir, workspaceId) {
|
|
@@ -17215,10 +17455,10 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
|
|
|
17215
17455
|
}
|
|
17216
17456
|
}
|
|
17217
17457
|
function readAllPersistedSyncWorkspaceIds(stateDir) {
|
|
17218
|
-
const dir =
|
|
17219
|
-
if (!
|
|
17458
|
+
const dir = join37(stateDir, "passengers");
|
|
17459
|
+
if (!existsSync34(dir)) return [];
|
|
17220
17460
|
const suffix = "-sync.json";
|
|
17221
|
-
return
|
|
17461
|
+
return readdirSync15(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
|
|
17222
17462
|
}
|
|
17223
17463
|
function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
|
|
17224
17464
|
const alive = deps.isPidAlive ?? isPidAlive;
|
|
@@ -17304,8 +17544,8 @@ function describeCause(err) {
|
|
|
17304
17544
|
}
|
|
17305
17545
|
|
|
17306
17546
|
// src/daemon.ts
|
|
17307
|
-
import { existsSync as
|
|
17308
|
-
import { join as
|
|
17547
|
+
import { existsSync as existsSync35, mkdirSync as mkdirSync22, readFileSync as readFileSync31, readdirSync as readdirSync16, rmSync as rmSync15, statSync as statSync9, writeFileSync as writeFileSync23 } from "fs";
|
|
17548
|
+
import { join as join38 } from "path";
|
|
17309
17549
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
17310
17550
|
var DEFAULT_FLOOR_MS = 5e3;
|
|
17311
17551
|
var DEFAULT_CEILING_MS = INTERVAL_SECONDS * 1e3;
|
|
@@ -17375,8 +17615,8 @@ function latestMtime(path) {
|
|
|
17375
17615
|
if (stat.isFile()) return stat.mtimeMs;
|
|
17376
17616
|
if (!stat.isDirectory()) return 0;
|
|
17377
17617
|
let latest = stat.mtimeMs;
|
|
17378
|
-
for (const entry of
|
|
17379
|
-
latest = Math.max(latest, latestMtime(
|
|
17618
|
+
for (const entry of readdirSync16(path, { withFileTypes: true })) {
|
|
17619
|
+
latest = Math.max(latest, latestMtime(join38(path, entry.name)));
|
|
17380
17620
|
}
|
|
17381
17621
|
return latest;
|
|
17382
17622
|
}
|
|
@@ -17385,23 +17625,23 @@ function computeTreeSignature(paths) {
|
|
|
17385
17625
|
}
|
|
17386
17626
|
function makeStaleChecker(paths, opts = {}) {
|
|
17387
17627
|
const initial = computeTreeSignature(paths);
|
|
17388
|
-
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 &&
|
|
17628
|
+
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync35(opts.reloadSentinel);
|
|
17389
17629
|
}
|
|
17390
17630
|
function reloadSentinelFor(stateDir) {
|
|
17391
|
-
return
|
|
17631
|
+
return join38(stateDir, "reload-requested");
|
|
17392
17632
|
}
|
|
17393
17633
|
function daemonStampFor(stateDir) {
|
|
17394
|
-
return
|
|
17634
|
+
return join38(stateDir, "daemon.started");
|
|
17395
17635
|
}
|
|
17396
17636
|
function noteDaemonStarted(stateDir, now = Date.now()) {
|
|
17397
|
-
|
|
17398
|
-
|
|
17399
|
-
|
|
17637
|
+
mkdirSync22(stateDir, { recursive: true });
|
|
17638
|
+
rmSync15(reloadSentinelFor(stateDir), { force: true });
|
|
17639
|
+
writeFileSync23(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
|
|
17400
17640
|
`);
|
|
17401
17641
|
}
|
|
17402
17642
|
function readDaemonStarted(stateDir) {
|
|
17403
17643
|
try {
|
|
17404
|
-
const v = JSON.parse(
|
|
17644
|
+
const v = JSON.parse(readFileSync31(daemonStampFor(stateDir), "utf8")).startedAt;
|
|
17405
17645
|
return typeof v === "number" ? v : null;
|
|
17406
17646
|
} catch {
|
|
17407
17647
|
return null;
|
|
@@ -17996,6 +18236,9 @@ function usage(problem, requested = false) {
|
|
|
17996
18236
|
crew rotate-passenger-url [route] force a Host Passengers tunnel to reconnect with a fresh public URL
|
|
17997
18237
|
crew periscope up [route] REPO --topic TEXT [--isolate] [--host H] [--assignee ID] [--dry-run] [--json]
|
|
17998
18238
|
file a draft ticket, cut and prepare its worktree, start the repo's preview hook and record a Periscopes row (alias: start)
|
|
18239
|
+
crew periscope down [route] NNN --keep|--discard [--allow-empty] [--json]
|
|
18240
|
+
end a periscope: commit and push its branch (ticket back to new), or throw it away (alias: end)
|
|
18241
|
+
crew periscope list [route] [--json] the live periscopes on this ship, their idle minutes and whether each server is listening
|
|
17999
18242
|
crew worktree [route] NNN cut (or refresh) this run's worktree for a ticket from the remote; prints its path
|
|
18000
18243
|
crew unassign [route] NNN hand back a session's ticket \u2014 clears assignee, next cycle picks it up
|
|
18001
18244
|
crew sync [route] fast-forward the checkout and its worktrees from the remote
|
|
@@ -18270,7 +18513,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
18270
18513
|
} else {
|
|
18271
18514
|
const toleratesNoRoutes = command === "repos" || command === "ship" || command === "disconnect" && flag("reset") || command === "connect" && flag("sign-out");
|
|
18272
18515
|
const configless = command === "connect" && flag("sign-out") || command === "disconnect" && flag("reset");
|
|
18273
|
-
if (configless && !
|
|
18516
|
+
if (configless && !existsSync36(findConfigFile(CREW_HOME))) {
|
|
18274
18517
|
cfg = loadConfig(CREW_HOME, firstRunConfigPath(), { text: "ship: {}\n", allowNoRoutes: true });
|
|
18275
18518
|
} else {
|
|
18276
18519
|
cfg = loadConfig(CREW_HOME, void 0, toleratesNoRoutes ? { allowNoRoutes: true } : {});
|
|
@@ -18508,8 +18751,8 @@ if (command === "passenger-sync-daemon") {
|
|
|
18508
18751
|
}
|
|
18509
18752
|
if (host === "linux" && hasSystemd) {
|
|
18510
18753
|
const label = labelFor(CREW_HOME, "run");
|
|
18511
|
-
const staleTimer =
|
|
18512
|
-
if (
|
|
18754
|
+
const staleTimer = join39(homedir10(), ".config", "systemd", "user", `${label}.timer`);
|
|
18755
|
+
if (existsSync36(staleTimer)) {
|
|
18513
18756
|
process.stdout.write(
|
|
18514
18757
|
`removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
|
|
18515
18758
|
`
|
|
@@ -18536,7 +18779,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
18536
18779
|
const tracker = new Tracker(c, cfg.ship);
|
|
18537
18780
|
problems.push(...validateContract(tracker.contract).map((p) => `contract: ${p}`));
|
|
18538
18781
|
for (const r of await resolvedRepos(c)) {
|
|
18539
|
-
if (!
|
|
18782
|
+
if (!existsSync36(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
|
|
18540
18783
|
if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
|
|
18541
18784
|
readiness.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
|
|
18542
18785
|
}
|
|
@@ -18554,7 +18797,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
18554
18797
|
}
|
|
18555
18798
|
async function doctorFix(o) {
|
|
18556
18799
|
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))));
|
|
18557
|
-
const unitExists = (p) =>
|
|
18800
|
+
const unitExists = (p) => existsSync36(p);
|
|
18558
18801
|
const crontab = o.host === "linux" ? spawnSync7("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
|
|
18559
18802
|
const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
|
|
18560
18803
|
const plan = planFix({ routes, schedulerAbsent, host: o.host });
|
|
@@ -18571,7 +18814,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
18571
18814
|
for (const a of plan.actions) {
|
|
18572
18815
|
if (a.kind === "enable") {
|
|
18573
18816
|
if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
|
|
18574
|
-
|
|
18817
|
+
writeFileSync24(cfg.configFile, planEnable(readFileSync32(cfg.configFile, "utf8"), a.route));
|
|
18575
18818
|
enabledSound.add(a.route);
|
|
18576
18819
|
changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
|
|
18577
18820
|
} else {
|
|
@@ -19675,7 +19918,7 @@ ${cu.tail.trimEnd()}
|
|
|
19675
19918
|
process.exit(1);
|
|
19676
19919
|
}
|
|
19677
19920
|
requireArmed("run the daemon loop");
|
|
19678
|
-
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [
|
|
19921
|
+
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join39(CREW_HOME, "src"), join39(CREW_HOME, "dist"), join39(CREW_HOME, "bin")];
|
|
19679
19922
|
const configPaths = daemonConfigPaths(cfg);
|
|
19680
19923
|
watchPaths.push(...configPaths);
|
|
19681
19924
|
noteDaemonStarted(cfg.ship.stateDir);
|
|
@@ -19904,10 +20147,10 @@ ${cu.tail.trimEnd()}
|
|
|
19904
20147
|
`);
|
|
19905
20148
|
if (!dryRun) {
|
|
19906
20149
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
19907
|
-
const raw =
|
|
20150
|
+
const raw = existsSync36(resolvedPath) ? JSON.parse(readFileSync32(resolvedPath, "utf8")) : {};
|
|
19908
20151
|
raw.agentSkills = result.agentSkills;
|
|
19909
|
-
|
|
19910
|
-
|
|
20152
|
+
mkdirSync23(dirOf2(resolvedPath), { recursive: true });
|
|
20153
|
+
writeFileSync24(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
19911
20154
|
`);
|
|
19912
20155
|
}
|
|
19913
20156
|
if (result.outcomes.some((o) => o.action === "diverged")) process.exitCode = 1;
|
|
@@ -19930,7 +20173,7 @@ ${cu.tail.trimEnd()}
|
|
|
19930
20173
|
throw e;
|
|
19931
20174
|
}
|
|
19932
20175
|
const oldName = cfg.ship.name;
|
|
19933
|
-
|
|
20176
|
+
writeFileSync24(cfg.configFile, setShipNameInYaml(readFileSync32(cfg.configFile, "utf8"), newName));
|
|
19934
20177
|
cfg.ship.name = newName;
|
|
19935
20178
|
let renameMachineId;
|
|
19936
20179
|
try {
|
|
@@ -19977,7 +20220,7 @@ ${cu.tail.trimEnd()}
|
|
|
19977
20220
|
process.exit(2);
|
|
19978
20221
|
};
|
|
19979
20222
|
if (sub !== "add" && sub !== "list" && sub !== "clone" && sub !== "hooks" && sub !== "check") usage2();
|
|
19980
|
-
const configText =
|
|
20223
|
+
const configText = readFileSync32(cfg.configFile, "utf8");
|
|
19981
20224
|
if (sub === "check") {
|
|
19982
20225
|
try {
|
|
19983
20226
|
const routeArg = value("route") ?? positional[2];
|
|
@@ -20008,16 +20251,16 @@ ${cu.tail.trimEnd()}
|
|
|
20008
20251
|
try {
|
|
20009
20252
|
const a = parseHooksArgs(positional.slice(2), routeNamesOf(configText));
|
|
20010
20253
|
const dir = repoDirOf(configText, dirname20(cfg.configFile), a.route, a.name);
|
|
20011
|
-
if (!
|
|
20012
|
-
const files = { exists: (r) =>
|
|
20254
|
+
if (!existsSync36(dir)) throw new ReposError(`${dir} does not exist on disk`);
|
|
20255
|
+
const files = { exists: (r) => existsSync36(resolvePath(dir, r)), list: (r) => {
|
|
20013
20256
|
try {
|
|
20014
|
-
return
|
|
20257
|
+
return readdirSync17(resolvePath(dir, r));
|
|
20015
20258
|
} catch {
|
|
20016
20259
|
return [];
|
|
20017
20260
|
}
|
|
20018
20261
|
}, read: (r) => {
|
|
20019
20262
|
try {
|
|
20020
|
-
return
|
|
20263
|
+
return readFileSync32(resolvePath(dir, r), "utf8");
|
|
20021
20264
|
} catch {
|
|
20022
20265
|
return void 0;
|
|
20023
20266
|
}
|
|
@@ -20059,19 +20302,19 @@ ${cu.tail.trimEnd()}
|
|
|
20059
20302
|
let cleared;
|
|
20060
20303
|
if (clear2) {
|
|
20061
20304
|
const plan = clearRouteHooks(configText, a.route, a.name);
|
|
20062
|
-
if (plan.removed.length > 0)
|
|
20305
|
+
if (plan.removed.length > 0) writeFileSync24(cfg.configFile, plan.text);
|
|
20063
20306
|
cleared = { file: cfg.configFile, removed: plan.removed };
|
|
20064
20307
|
}
|
|
20065
20308
|
if (write && suggestion.source === null && recipeIds.length === 0 && hookSpecs.length === 0) throw new ReposError(`nothing to write: ${suggestion.notes[0]}`);
|
|
20066
20309
|
if (write === "route") {
|
|
20067
20310
|
const plan = planRouteHooks(configText, a.route, a.name, picked.hooks, { replace: flag("replace") });
|
|
20068
|
-
|
|
20311
|
+
writeFileSync24(cfg.configFile, plan.text);
|
|
20069
20312
|
written = { target: "route", file: cfg.configFile, hooks: plan.written, kept: plan.kept, replaced: plan.replaced };
|
|
20070
20313
|
} else if (write === "repo") {
|
|
20071
20314
|
const have = existingRepoConfig(files);
|
|
20072
20315
|
if (have) throw new ReposError(`${resolvePath(dir, have)} already exists; not overwriting it`);
|
|
20073
20316
|
const file = resolvePath(dir, ".crew.yaml");
|
|
20074
|
-
|
|
20317
|
+
writeFileSync24(file, starterRepoConfig(picked.hooks, picked.release), { flag: "wx" });
|
|
20075
20318
|
written = { target: "repo", file, hooks: Object.keys(picked.hooks), kept: [], replaced: [] };
|
|
20076
20319
|
}
|
|
20077
20320
|
if (flag("json")) {
|
|
@@ -20127,7 +20370,7 @@ ${cu.tail.trimEnd()}
|
|
|
20127
20370
|
if (sub === "list" && !routeName) usage2();
|
|
20128
20371
|
if (sub === "add") {
|
|
20129
20372
|
try {
|
|
20130
|
-
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) =>
|
|
20373
|
+
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync36(resolvePath(p.replace(/^~(?=\/|$)/, homedir10()))));
|
|
20131
20374
|
routeName = a.route;
|
|
20132
20375
|
addPath = a.path;
|
|
20133
20376
|
addName = a.name;
|
|
@@ -20156,16 +20399,16 @@ ${cu.tail.trimEnd()}
|
|
|
20156
20399
|
}
|
|
20157
20400
|
const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
|
|
20158
20401
|
let resolvedRemotes;
|
|
20159
|
-
if (
|
|
20402
|
+
if (existsSync36(resolvedFile)) {
|
|
20160
20403
|
try {
|
|
20161
|
-
resolvedRemotes = JSON.parse(
|
|
20404
|
+
resolvedRemotes = JSON.parse(readFileSync32(resolvedFile, "utf8"));
|
|
20162
20405
|
} catch {
|
|
20163
20406
|
}
|
|
20164
20407
|
}
|
|
20165
20408
|
const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
|
|
20166
20409
|
const text = configText;
|
|
20167
20410
|
const base = dirname20(cfg.configFile);
|
|
20168
|
-
const isCheckout = (d) =>
|
|
20411
|
+
const isCheckout = (d) => existsSync36(`${d}/.git`);
|
|
20169
20412
|
try {
|
|
20170
20413
|
if (sub === "list") {
|
|
20171
20414
|
if (flag("json")) {
|
|
@@ -20175,11 +20418,11 @@ ${cu.tail.trimEnd()}
|
|
|
20175
20418
|
base,
|
|
20176
20419
|
resolved: resolvedRemotes,
|
|
20177
20420
|
originOf,
|
|
20178
|
-
exists: (d) =>
|
|
20421
|
+
exists: (d) => existsSync36(d),
|
|
20179
20422
|
isCheckout,
|
|
20180
20423
|
repoHasHooks: (d) => REPO_CONFIG_FILENAMES.some((f) => {
|
|
20181
20424
|
try {
|
|
20182
|
-
return Object.keys(parseRepoConfig(
|
|
20425
|
+
return Object.keys(parseRepoConfig(readFileSync32(resolvePath(d, f), "utf8"), f).hooks).length > 0;
|
|
20183
20426
|
} catch {
|
|
20184
20427
|
return false;
|
|
20185
20428
|
}
|
|
@@ -20196,7 +20439,7 @@ ${cu.tail.trimEnd()}
|
|
|
20196
20439
|
for (const r of repos) {
|
|
20197
20440
|
const origin2 = originOf(r.dir);
|
|
20198
20441
|
const row = matchRepoName(origin2, resolvedRemotes);
|
|
20199
|
-
const note = !
|
|
20442
|
+
const note = !existsSync36(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
|
|
20200
20443
|
process.stdout.write(`${r.name} ${r.dir} ${note}
|
|
20201
20444
|
`);
|
|
20202
20445
|
}
|
|
@@ -20208,9 +20451,9 @@ ${cu.tail.trimEnd()}
|
|
|
20208
20451
|
` : `${human}
|
|
20209
20452
|
`);
|
|
20210
20453
|
const plan2 = planRepoClone({ text, base, route: routeName, name: addName, resolved: resolvedRemotes, dir: value("dir"), isCheckout });
|
|
20211
|
-
if (
|
|
20454
|
+
if (existsSync36(plan2.target) && (existsSync36(`${plan2.target}/.git`) || readdirSync17(plan2.target).length > 0)) {
|
|
20212
20455
|
throw new ReposError(
|
|
20213
|
-
|
|
20456
|
+
existsSync36(`${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`
|
|
20214
20457
|
);
|
|
20215
20458
|
}
|
|
20216
20459
|
if (dryRun) {
|
|
@@ -20228,14 +20471,14 @@ ${cu.tail.trimEnd()}
|
|
|
20228
20471
|
throw new ReposError(`${e.message}${authHint}
|
|
20229
20472
|
crew.yaml was not changed`);
|
|
20230
20473
|
}
|
|
20231
|
-
|
|
20474
|
+
writeFileSync24(cfg.configFile, plan2.text);
|
|
20232
20475
|
if (plan2.droppedDir) process.stderr.write(`crew repos: dropped dir: ${plan2.droppedDir} from route ${routeName} \u2014 it is not a checkout (the clone base); checkouts now live under repos:
|
|
20233
20476
|
`);
|
|
20234
20477
|
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}`);
|
|
20235
20478
|
break;
|
|
20236
20479
|
}
|
|
20237
20480
|
const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir10()));
|
|
20238
|
-
const top =
|
|
20481
|
+
const top = existsSync36(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
|
|
20239
20482
|
if (!top) throw new ReposError(`${target} is not a git checkout`);
|
|
20240
20483
|
const origin = originOf(top);
|
|
20241
20484
|
const plan = planRepoAdd({
|
|
@@ -20287,15 +20530,15 @@ ${cu.tail.trimEnd()}
|
|
|
20287
20530
|
const created = await rowTracker.createRepoRow(rowPayload);
|
|
20288
20531
|
process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
|
|
20289
20532
|
`);
|
|
20290
|
-
if (
|
|
20291
|
-
const raw = JSON.parse(
|
|
20533
|
+
if (existsSync36(resolvedFile)) {
|
|
20534
|
+
const raw = JSON.parse(readFileSync32(resolvedFile, "utf8"));
|
|
20292
20535
|
raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
|
|
20293
20536
|
raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
|
|
20294
|
-
|
|
20537
|
+
writeFileSync24(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
20295
20538
|
`);
|
|
20296
20539
|
}
|
|
20297
20540
|
}
|
|
20298
|
-
|
|
20541
|
+
writeFileSync24(cfg.configFile, plan.text);
|
|
20299
20542
|
process.stdout.write(`added ${top} to ${routeName} as ${plan.mode === "dir" ? "dir" : `repos.${plan.name}`} in ${cfg.configFile}
|
|
20300
20543
|
`);
|
|
20301
20544
|
} catch (e) {
|
|
@@ -20662,7 +20905,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
20662
20905
|
process.exit(2);
|
|
20663
20906
|
}
|
|
20664
20907
|
if (found.shipNameAdopted && !dryRun) {
|
|
20665
|
-
|
|
20908
|
+
writeFileSync24(cfg.configFile, setShipNameInYaml(readFileSync32(cfg.configFile, "utf8"), found.shipNameAdopted));
|
|
20666
20909
|
cfg.ship.name = found.shipNameAdopted;
|
|
20667
20910
|
}
|
|
20668
20911
|
if (found.shipRow) {
|
|
@@ -20705,10 +20948,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
20705
20948
|
}
|
|
20706
20949
|
}
|
|
20707
20950
|
let previous;
|
|
20708
|
-
const hadPreviousFile =
|
|
20951
|
+
const hadPreviousFile = existsSync36(resolvedPath);
|
|
20709
20952
|
if (hadPreviousFile) {
|
|
20710
20953
|
try {
|
|
20711
|
-
previous = JSON.parse(
|
|
20954
|
+
previous = JSON.parse(readFileSync32(resolvedPath, "utf8"));
|
|
20712
20955
|
} catch {
|
|
20713
20956
|
}
|
|
20714
20957
|
}
|
|
@@ -20837,10 +21080,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
20837
21080
|
process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
|
|
20838
21081
|
`);
|
|
20839
21082
|
} else {
|
|
20840
|
-
|
|
21083
|
+
mkdirSync23(dirname20(resolvedPath), { recursive: true });
|
|
20841
21084
|
if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
|
|
20842
21085
|
const tmpPath = `${resolvedPath}.tmp${process.pid}`;
|
|
20843
|
-
|
|
21086
|
+
writeFileSync24(tmpPath, `${JSON.stringify(resolved, null, 2)}
|
|
20844
21087
|
`);
|
|
20845
21088
|
renameSync3(tmpPath, resolvedPath);
|
|
20846
21089
|
process.stderr.write(`Resolved ids written to ${resolvedPath}
|
|
@@ -20886,8 +21129,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
20886
21129
|
process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
|
|
20887
21130
|
`);
|
|
20888
21131
|
} else {
|
|
20889
|
-
|
|
20890
|
-
|
|
21132
|
+
mkdirSync23(dirname20(apiKeyFile), { recursive: true });
|
|
21133
|
+
writeFileSync24(apiKeyFile, `${apiKeyVar}=${apiKey}
|
|
20891
21134
|
`, { mode: 384 });
|
|
20892
21135
|
process.stderr.write(`API key written to ${apiKeyFile}
|
|
20893
21136
|
`);
|
|
@@ -21422,14 +21665,142 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
21422
21665
|
}
|
|
21423
21666
|
case "periscope": {
|
|
21424
21667
|
const subP = positional[1];
|
|
21425
|
-
if (subP !== "up" && subP !== "start") {
|
|
21668
|
+
if (subP !== "up" && subP !== "start" && subP !== "down" && subP !== "end" && subP !== "list") {
|
|
21426
21669
|
process.stderr.write("crew periscope up [route] REPO --topic TEXT [--isolate] [--host H] [--assignee ID] [--dry-run] [--json]\n");
|
|
21670
|
+
process.stderr.write("crew periscope down [route] NNN --keep|--discard [--allow-empty] [--json]\n");
|
|
21671
|
+
process.stderr.write("crew periscope list [route] [--json]\n");
|
|
21427
21672
|
process.exit(2);
|
|
21428
21673
|
}
|
|
21429
21674
|
const jsonP = flag("json");
|
|
21430
21675
|
const say = (event, message, data = {}) => process.stderr.write(jsonP ? `${JSON.stringify({ ...data, event, message })}
|
|
21431
21676
|
` : `${message}
|
|
21432
21677
|
`);
|
|
21678
|
+
if (subP === "down" || subP === "end" || subP === "list") {
|
|
21679
|
+
try {
|
|
21680
|
+
const downArgs = subP === "list" ? void 0 : parsePeriscopeDownArgs(positional, { keep: flag("keep"), discard: flag("discard"), allowEmpty: flag("allow-empty"), json: jsonP });
|
|
21681
|
+
const routeName = subP === "list" ? positional[2] : downArgs.route;
|
|
21682
|
+
const routesP = (routeName ? [findRoute(cfg, routeName)] : cfg.routes.filter((r) => r.enabled)).filter((r) => r.resolved?.periscopesModelId);
|
|
21683
|
+
if (routesP.length === 0) throw new PeriscopeError("no connected route has a Periscopes table");
|
|
21684
|
+
const gather = async (rt) => {
|
|
21685
|
+
const tr = new Tracker(rt, cfg.ship);
|
|
21686
|
+
const repos = await resolvedRepos(rt);
|
|
21687
|
+
return (await tr.periscopeRows()).map(({ row, ticket }) => {
|
|
21688
|
+
const repo = repos.find((r) => r.id && r.id === ticket[tr.contract.columns.repo]) ?? (repos.length === 1 ? repos[0] : void 0);
|
|
21689
|
+
const str = (v) => typeof v === "string" ? v : "";
|
|
21690
|
+
return {
|
|
21691
|
+
id: row.id,
|
|
21692
|
+
ticketRecordId: ticket.id,
|
|
21693
|
+
ticket: ticket.issue_tag ?? ticket.issue_id,
|
|
21694
|
+
number: /(\d+)$/.exec(ticket.issue_id)?.[1] ?? "",
|
|
21695
|
+
repo: repo?.name ?? "?",
|
|
21696
|
+
state: str(row.state),
|
|
21697
|
+
url: str(row.url),
|
|
21698
|
+
worktree: str(row.worktree),
|
|
21699
|
+
branch: str(row.branch),
|
|
21700
|
+
lastActivityAt: str(row.last_activity_at) || null
|
|
21701
|
+
};
|
|
21702
|
+
});
|
|
21703
|
+
};
|
|
21704
|
+
if (subP === "list") {
|
|
21705
|
+
const entries = periscopeListEntries(
|
|
21706
|
+
(await Promise.all(routesP.map(gather))).flat(),
|
|
21707
|
+
/* @__PURE__ */ new Date(),
|
|
21708
|
+
(r) => pidsInWorktree(r.worktree, Number(r.number)).length > 0
|
|
21709
|
+
);
|
|
21710
|
+
process.stdout.write(jsonP ? `${JSON.stringify(entries)}
|
|
21711
|
+
` : `${formatPeriscopeList(entries)}
|
|
21712
|
+
`);
|
|
21713
|
+
break;
|
|
21714
|
+
}
|
|
21715
|
+
const a = downArgs;
|
|
21716
|
+
const hits = [];
|
|
21717
|
+
for (const rt of routesP) {
|
|
21718
|
+
const rows = (await gather(rt)).filter((r) => r.number === a.number);
|
|
21719
|
+
if (rows.length) hits.push({ rt, rows });
|
|
21720
|
+
}
|
|
21721
|
+
if (hits.length !== 1) {
|
|
21722
|
+
throw new PeriscopeError(hits.length === 0 ? `no periscope for ticket ${a.number} on this ship` : `ticket ${a.number} has periscopes on more than one route (${hits.map((h) => h.rt.route).join(", ")}) \u2014 name the route`);
|
|
21723
|
+
}
|
|
21724
|
+
const { rt: routeD, rows: rowsD } = hits[0];
|
|
21725
|
+
const trD = new Tracker(routeD, cfg.ship);
|
|
21726
|
+
const cD = trD.contract;
|
|
21727
|
+
const reposD = await resolvedRepos(routeD);
|
|
21728
|
+
const repoD = reposD.find((r) => r.name === rowsD[0].repo);
|
|
21729
|
+
if (!repoD) throw new PeriscopeError(`cannot tell which repository ${rowsD[0].ticket} belongs to`);
|
|
21730
|
+
const { remote, base } = repoD.config.branch;
|
|
21731
|
+
const memberD = routeD.resolved.seats.pair ?? routeD.resolved.operator;
|
|
21732
|
+
const closedD = cD.statuses.resolved.includes("closed_obsolete") ? "closed_obsolete" : cD.statuses.wontFix;
|
|
21733
|
+
const depsD = {
|
|
21734
|
+
rowsForTicket: async () => rowsD,
|
|
21735
|
+
hasChanges: (wt) => status(wt).length > 0,
|
|
21736
|
+
commitAll: (wt, msg) => {
|
|
21737
|
+
git(wt, ["add", "-A"]);
|
|
21738
|
+
git(wt, ["commit", "-m", msg]);
|
|
21739
|
+
},
|
|
21740
|
+
push: (wt, branch) => {
|
|
21741
|
+
pushBranchUpstream(wt, remote, branch);
|
|
21742
|
+
return { sha: headSha(wt), remote };
|
|
21743
|
+
},
|
|
21744
|
+
changedFiles: (wt) => {
|
|
21745
|
+
const out = gitOk(wt, ["diff", "--numstat", `${remote}/${base}...HEAD`]) ?? "";
|
|
21746
|
+
return out.split("\n").filter(Boolean).map((l) => {
|
|
21747
|
+
const [add, rem, ...p] = l.split(" ");
|
|
21748
|
+
return { path: p.join(" "), added: Number(add) || 0, removed: Number(rem) || 0 };
|
|
21749
|
+
});
|
|
21750
|
+
},
|
|
21751
|
+
readTicket: async (id) => {
|
|
21752
|
+
const t = await trD.ticket(id);
|
|
21753
|
+
return { description: String(t.description ?? ""), hasProject: !!t[cD.columns.slice], hasRepo: !!t[cD.columns.repo] };
|
|
21754
|
+
},
|
|
21755
|
+
updateTicket: async (id, p) => {
|
|
21756
|
+
const patch2 = {};
|
|
21757
|
+
if (p.status === "new") {
|
|
21758
|
+
patch2[cD.columns.status] = "new";
|
|
21759
|
+
patch2[cD.columns.assignee] = null;
|
|
21760
|
+
if (p.description !== void 0) patch2.description = p.description;
|
|
21761
|
+
if (p.fillProjectAndRepo) {
|
|
21762
|
+
if (routeD.resolved.areaId) patch2[cD.columns.slice] = routeD.resolved.areaId;
|
|
21763
|
+
if (repoD.id) patch2[cD.columns.repo] = repoD.id;
|
|
21764
|
+
}
|
|
21765
|
+
} else {
|
|
21766
|
+
patch2[cD.columns.status] = closedD;
|
|
21767
|
+
patch2[cD.columns.resolutionNote] = p.note;
|
|
21768
|
+
}
|
|
21769
|
+
await trD.updateTicket(id, patch2);
|
|
21770
|
+
},
|
|
21771
|
+
postPushed: (id, sha, rem, branch) => trD.postEvent(id, pushedEventBody(sha, rem, branch), memberD, "pushed"),
|
|
21772
|
+
stopServer: async (wt, n) => {
|
|
21773
|
+
const pids = pidsInWorktree(wt, Number(n));
|
|
21774
|
+
if (pids.length) await killGently(pids);
|
|
21775
|
+
return pids.length;
|
|
21776
|
+
},
|
|
21777
|
+
removeWorktree: (wt, branch, deleteBranch) => {
|
|
21778
|
+
gitOk(repoD.dir, ["worktree", "remove", "--force", wt]);
|
|
21779
|
+
if (deleteBranch) gitOk(repoD.dir, ["branch", "-D", branch]);
|
|
21780
|
+
},
|
|
21781
|
+
setRowState: (id, state2) => trD.updatePeriscope(id, { state: state2 }),
|
|
21782
|
+
forgetLocal: (id) => removePeriscopeSidecar(cfg.ship.stateDir, id),
|
|
21783
|
+
say
|
|
21784
|
+
};
|
|
21785
|
+
if (dryRun) {
|
|
21786
|
+
say("dry-run", `would end the periscope for ${rowsD[0].ticket} (${a.mode})`);
|
|
21787
|
+
break;
|
|
21788
|
+
}
|
|
21789
|
+
const res = await runPeriscopeDown(depsD, a);
|
|
21790
|
+
if (jsonP) process.stdout.write(`${JSON.stringify({ event: "done", ...res })}
|
|
21791
|
+
`);
|
|
21792
|
+
else process.stdout.write(`${res.ticket} ${res.state}
|
|
21793
|
+
`);
|
|
21794
|
+
} catch (e) {
|
|
21795
|
+
if (e instanceof PeriscopeError || e instanceof ConfigError) {
|
|
21796
|
+
process.stderr.write(`crew periscope: ${e.message}
|
|
21797
|
+
`);
|
|
21798
|
+
process.exit(1);
|
|
21799
|
+
}
|
|
21800
|
+
throw e;
|
|
21801
|
+
}
|
|
21802
|
+
break;
|
|
21803
|
+
}
|
|
21433
21804
|
try {
|
|
21434
21805
|
const a = parsePeriscopeUpArgs(positional, { topic: value("topic"), host: value("host"), assignee: value("assignee"), isolate: flag("isolate"), json: jsonP });
|
|
21435
21806
|
const candidatesP = a.route ? [findRoute(cfg, a.route)] : cfg.routes.filter((r) => r.enabled);
|
|
@@ -21493,10 +21864,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
21493
21864
|
},
|
|
21494
21865
|
prepare: async (path) => {
|
|
21495
21866
|
for (const f of repoP.config.worktrees.copy) {
|
|
21496
|
-
const src =
|
|
21497
|
-
if (!
|
|
21498
|
-
|
|
21499
|
-
copyFileSync2(src,
|
|
21867
|
+
const src = join39(repoP.dir, f);
|
|
21868
|
+
if (!existsSync36(src)) continue;
|
|
21869
|
+
mkdirSync23(dirOf2(join39(path, f)), { recursive: true });
|
|
21870
|
+
copyFileSync2(src, join39(path, f));
|
|
21500
21871
|
}
|
|
21501
21872
|
const setup = repoP.config.hooks.setup;
|
|
21502
21873
|
return setup ? runScript(setup, { cwd: path, shell: shellP, onLine: (l) => {
|
|
@@ -21508,6 +21879,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
21508
21879
|
return script ? runScript(script, { cwd, env, shell: shellP, timeoutMs: name === "preview" ? 18e4 : void 0 }) : null;
|
|
21509
21880
|
},
|
|
21510
21881
|
createRow: (fields) => trackerP.createPeriscope(fields),
|
|
21882
|
+
recordLocal: (sc) => writePeriscopeSidecar(cfg.ship.stateDir, sc),
|
|
21511
21883
|
comment: (ticketId, body) => trackerP.postComment(ticketId, body, memberId),
|
|
21512
21884
|
teardown: async (path, branch) => {
|
|
21513
21885
|
const n = Number(path.match(/(\d+)\/?$/)?.[1]);
|
|
@@ -21719,7 +22091,7 @@ ${res.url}
|
|
|
21719
22091
|
};
|
|
21720
22092
|
const droppedRoutes = [];
|
|
21721
22093
|
try {
|
|
21722
|
-
const rawCfg = parseYaml2(
|
|
22094
|
+
const rawCfg = parseYaml2(readFileSync32(cfg.configFile, "utf8"));
|
|
21723
22095
|
for (const r of rawCfg?.routes ?? []) {
|
|
21724
22096
|
if (typeof r?.route !== "string" || cfg.routes.some((x) => x.route === r.route)) continue;
|
|
21725
22097
|
try {
|
|
@@ -21761,8 +22133,8 @@ ${res.url}
|
|
|
21761
22133
|
process.stdout.write("crew reload: no daemon unit is running \u2014 nothing to reload (the next run reads crew.yaml fresh)\n");
|
|
21762
22134
|
break;
|
|
21763
22135
|
}
|
|
21764
|
-
|
|
21765
|
-
|
|
22136
|
+
mkdirSync23(cfg.ship.stateDir, { recursive: true });
|
|
22137
|
+
writeFileSync24(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
|
|
21766
22138
|
`);
|
|
21767
22139
|
process.stdout.write("crew reload: requested \u2014 the daemon finishes its in-flight pass, then relaunches with the new config\n");
|
|
21768
22140
|
if (flag("now") && control?.stopCommand && control.startCommand) {
|
|
@@ -21813,7 +22185,7 @@ ${res.url}
|
|
|
21813
22185
|
}
|
|
21814
22186
|
case "log": {
|
|
21815
22187
|
try {
|
|
21816
|
-
const text =
|
|
22188
|
+
const text = readFileSync32(cfg.ship.logFile, "utf8").split("\n");
|
|
21817
22189
|
process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
|
|
21818
22190
|
`);
|
|
21819
22191
|
} catch {
|
|
@@ -21836,7 +22208,7 @@ ${res.url}
|
|
|
21836
22208
|
};
|
|
21837
22209
|
if (!route.resolved) fail(`route ${route.route} is not connected \u2014 run \`crew connect\` first`);
|
|
21838
22210
|
const tracker = new Tracker(route, cfg.ship);
|
|
21839
|
-
const configText =
|
|
22211
|
+
const configText = readFileSync32(cfg.configFile, "utf8");
|
|
21840
22212
|
const cfgBase = dirname20(cfg.configFile);
|
|
21841
22213
|
const configured = configuredRepos(configText, route.route, cfgBase);
|
|
21842
22214
|
const dir = configured.find((r) => r.name === SAMPLE_REPO)?.dir ?? sampleDir(configured.map((r) => r.dir), route.reposBasePath, route.route.split("/")[0]);
|
|
@@ -21865,7 +22237,7 @@ ${res.url}
|
|
|
21865
22237
|
}
|
|
21866
22238
|
if (configured.some((r) => r.dir === dir)) {
|
|
21867
22239
|
try {
|
|
21868
|
-
|
|
22240
|
+
writeFileSync24(cfg.configFile, planRepoRemove(configText, cfgBase, route.route, dir).text);
|
|
21869
22241
|
} catch (e) {
|
|
21870
22242
|
if (e instanceof ReposError) fail(e.message);
|
|
21871
22243
|
throw e;
|
|
@@ -21873,29 +22245,29 @@ ${res.url}
|
|
|
21873
22245
|
}
|
|
21874
22246
|
if (row) {
|
|
21875
22247
|
await tracker.deleteRepoRow(row.id);
|
|
21876
|
-
if (
|
|
21877
|
-
const raw = JSON.parse(
|
|
22248
|
+
if (existsSync36(resolvedFile)) {
|
|
22249
|
+
const raw = JSON.parse(readFileSync32(resolvedFile, "utf8"));
|
|
21878
22250
|
if (raw.repoNames) delete raw.repoNames[row.id];
|
|
21879
22251
|
if (raw.repoRemotes) delete raw.repoRemotes[row.id];
|
|
21880
|
-
|
|
22252
|
+
writeFileSync24(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
21881
22253
|
`);
|
|
21882
22254
|
}
|
|
21883
22255
|
}
|
|
21884
|
-
if (
|
|
22256
|
+
if (existsSync36(join39(dir, ".git")) && basename12(dir) === SAMPLE_REPO) rmSync16(dir, { recursive: true, force: true });
|
|
21885
22257
|
say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
|
|
21886
22258
|
break;
|
|
21887
22259
|
}
|
|
21888
22260
|
const projectId = route.resolved.areaId ?? resolveRepoProject(await tracker.projectRows(), value("project"));
|
|
21889
|
-
const fresh = !
|
|
21890
|
-
if (
|
|
22261
|
+
const fresh = !existsSync36(join39(dir, ".git"));
|
|
22262
|
+
if (existsSync36(dir) && fresh && readdirSync17(dir).length > 0) fail(`${dir} exists and is not a git checkout \u2014 move it aside`);
|
|
21891
22263
|
if (dryRun) {
|
|
21892
22264
|
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`);
|
|
21893
22265
|
break;
|
|
21894
22266
|
}
|
|
21895
22267
|
if (fresh) {
|
|
21896
|
-
|
|
21897
|
-
|
|
21898
|
-
|
|
22268
|
+
mkdirSync23(dir, { recursive: true });
|
|
22269
|
+
writeFileSync24(join39(dir, "README.md"), SAMPLE_README);
|
|
22270
|
+
writeFileSync24(join39(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
|
|
21899
22271
|
const g = (...a) => {
|
|
21900
22272
|
const r = spawnSync7("git", a, { cwd: dir, encoding: "utf8" });
|
|
21901
22273
|
if (r.status !== 0) fail(`git ${a[0]} failed in ${dir}: ${(r.stderr || r.stdout).trim()}`);
|
|
@@ -21913,17 +22285,17 @@ ${res.url}
|
|
|
21913
22285
|
spec: { platform: "unix", releaseMode: "integrate", ciProvider: "none" }
|
|
21914
22286
|
});
|
|
21915
22287
|
repoId = (await tracker.createRepoRow(payload)).id;
|
|
21916
|
-
if (
|
|
21917
|
-
const raw = JSON.parse(
|
|
22288
|
+
if (existsSync36(resolvedFile)) {
|
|
22289
|
+
const raw = JSON.parse(readFileSync32(resolvedFile, "utf8"));
|
|
21918
22290
|
raw.repoNames = { ...raw.repoNames, [repoId]: SAMPLE_REPO };
|
|
21919
22291
|
raw.repoRemotes = { ...raw.repoRemotes, [repoId]: SAMPLE_REMOTE };
|
|
21920
|
-
|
|
22292
|
+
writeFileSync24(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
21921
22293
|
`);
|
|
21922
22294
|
}
|
|
21923
22295
|
}
|
|
21924
22296
|
if (!configured.some((r) => r.dir === dir)) {
|
|
21925
22297
|
try {
|
|
21926
|
-
|
|
22298
|
+
writeFileSync24(cfg.configFile, planRepoAdd({ text: configText, base: cfgBase, route: route.route, path: dir, name: SAMPLE_REPO }).text);
|
|
21927
22299
|
} catch (e) {
|
|
21928
22300
|
if (e instanceof ReposError) fail(e.message);
|
|
21929
22301
|
throw e;
|
|
@@ -22045,6 +22417,7 @@ ${res.url}
|
|
|
22045
22417
|
const projects = projectModeLabels(await statusTracker.projectRowsWithModes().catch(() => []));
|
|
22046
22418
|
const crewMinVersion = await readCrewFloor(route, cfg.ship) ?? null;
|
|
22047
22419
|
const runs = liveRuns(cfg.ship.stateDir);
|
|
22420
|
+
const periscopes = await periscopesForStatus(cfg.ship.stateDir);
|
|
22048
22421
|
if (flag("json")) {
|
|
22049
22422
|
const nameSync = await reconcileRouteShipName({ route, ship: cfg.ship, configFile: cfg.configFile, readOnly: true });
|
|
22050
22423
|
const repos = await resolvedRepos(route);
|
|
@@ -22109,6 +22482,8 @@ ${res.url}
|
|
|
22109
22482
|
},
|
|
22110
22483
|
// Additive (CREW-2822): every live run on this ship. `version` stays 1.
|
|
22111
22484
|
runs: runs.map((x) => describeRun(x, route, seatIds)),
|
|
22485
|
+
// Additive (CREW-2857): every live periscope on this ship, from its local record. `version` stays 1.
|
|
22486
|
+
periscopes,
|
|
22112
22487
|
watermark: state.watermark(),
|
|
22113
22488
|
route: {
|
|
22114
22489
|
name: route.route,
|
|
@@ -22183,7 +22558,8 @@ ${owed.map((l) => ` ${l}
|
|
|
22183
22558
|
route: ${route.route} -> ${route.dir}
|
|
22184
22559
|
enabled: ${route.enabled}
|
|
22185
22560
|
periscopes: ${periscopeSummary(cfg)}
|
|
22186
|
-
|
|
22561
|
+
` + periscopes.map((p) => `periscope: ${p.ticket} ${p.repo} ${p.url} ${p.state}${p.serving ? "" : " (not serving)"} expires ${p.expiresAt}
|
|
22562
|
+
`).join("") + `crew: ${state.isPaused() ? "paused" : "active"}
|
|
22187
22563
|
release: ${state.releaseSkipReason() ? "paused" : "active"}
|
|
22188
22564
|
watermark: ${state.watermark()}
|
|
22189
22565
|
`
|
|
@@ -22254,7 +22630,7 @@ watermark: ${state.watermark()}
|
|
|
22254
22630
|
}
|
|
22255
22631
|
for (const r of repos) {
|
|
22256
22632
|
const remote = r.config.branch.remote;
|
|
22257
|
-
if (
|
|
22633
|
+
if (existsSync36(r.dir)) {
|
|
22258
22634
|
pushAuth.push({
|
|
22259
22635
|
route: c.route,
|
|
22260
22636
|
repo: r.name,
|
|
@@ -22301,7 +22677,12 @@ watermark: ${state.watermark()}
|
|
|
22301
22677
|
}
|
|
22302
22678
|
for (const message of roleCapWarnings(cfg.ship)) problems.push({ severity: "note", kind: "role_cap_above_ship_cap", message });
|
|
22303
22679
|
const previewProblem2 = await previewHostProblem(cfg);
|
|
22304
|
-
if (previewProblem2) problems.push(
|
|
22680
|
+
if (previewProblem2) problems.push(previewHostFinding(previewProblem2));
|
|
22681
|
+
if (cfg.routes.some((x) => x.route === route.route && x.enabled && x.hostPeriscopes)) {
|
|
22682
|
+
const { settled } = await settleRepos(reposOf(route), (t) => t.name, (t) => resolveOneRepo(route, t));
|
|
22683
|
+
const tablePresent = flag("offline") ? void 0 : (await new Tracker(route, cfg.ship).periscopesStatus().catch(() => ({ present: true }))).present;
|
|
22684
|
+
problems.push(...routePeriscopeProblems({ route: route.route, hosts: true, repos: settled.map((r) => ({ name: r.name, hasPreviewHook: !!r.config.hooks.preview })), tablePresent }));
|
|
22685
|
+
}
|
|
22305
22686
|
problems.push(...attentionProblems(openShipAttention(state), problems));
|
|
22306
22687
|
if (problems.some((p) => p.severity === "blocker")) process.exitCode = 1;
|
|
22307
22688
|
process.stdout.write(`${JSON.stringify({ offline: flag("offline"), pushAuth, tools: toolRows, problems, passed }, null, 2)}
|
|
@@ -22319,13 +22700,14 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
22319
22700
|
`);
|
|
22320
22701
|
const previewProblem = await previewHostProblem(cfg);
|
|
22321
22702
|
if (previewProblem) {
|
|
22322
|
-
|
|
22703
|
+
const blocks = previewHostFinding(previewProblem).severity === "blocker";
|
|
22704
|
+
process.stdout.write(` ${blocks ? "BLOCKER" : "ATTENTION"} ${previewProblem}
|
|
22323
22705
|
`);
|
|
22324
|
-
process.exitCode = 1;
|
|
22706
|
+
if (blocks) process.exitCode = 1;
|
|
22325
22707
|
}
|
|
22326
22708
|
try {
|
|
22327
22709
|
const evFile = eventFileFor(cfg.ship.stateDir);
|
|
22328
|
-
const evSize =
|
|
22710
|
+
const evSize = existsSync36(evFile) ? statSync10(evFile).size : 0;
|
|
22329
22711
|
process.stdout.write(
|
|
22330
22712
|
evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
|
|
22331
22713
|
` : "events log: ok\n"
|
|
@@ -22336,7 +22718,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
22336
22718
|
const unitPlan = planInstall(cfg.ship, CREW_HOME, host, detectSystemd(), "run");
|
|
22337
22719
|
const abandons = unitAbandonsAgents(unitPlan, (p) => {
|
|
22338
22720
|
try {
|
|
22339
|
-
return
|
|
22721
|
+
return readFileSync32(p, "utf8");
|
|
22340
22722
|
} catch {
|
|
22341
22723
|
return null;
|
|
22342
22724
|
}
|
|
@@ -22427,6 +22809,18 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
22427
22809
|
`
|
|
22428
22810
|
);
|
|
22429
22811
|
const periscopes = await tracker.periscopesStatus();
|
|
22812
|
+
if (cfg.routes.some((x) => x.route === route.route && x.enabled && x.hostPeriscopes)) {
|
|
22813
|
+
const { settled } = await settleRepos(reposOf(route), (t) => t.name, (t) => resolveOneRepo(route, t));
|
|
22814
|
+
for (const f of routePeriscopeProblems({
|
|
22815
|
+
route: route.route,
|
|
22816
|
+
hosts: true,
|
|
22817
|
+
tablePresent: periscopes.present,
|
|
22818
|
+
repos: settled.map((r) => ({ name: r.name, hasPreviewHook: !!r.config.hooks.preview }))
|
|
22819
|
+
})) {
|
|
22820
|
+
process.stdout.write(`periscopes: ${f.severity.toUpperCase()} ${f.message}
|
|
22821
|
+
`);
|
|
22822
|
+
}
|
|
22823
|
+
}
|
|
22430
22824
|
process.stdout.write(
|
|
22431
22825
|
!periscopes.present ? "periscopes: Periscopes table absent \u2014 this workspace cannot host periscopes\n" : periscopes.missing.length === 0 ? `periscopes: Periscopes table present (${periscopes.modelId})
|
|
22432
22826
|
` : `periscopes: Periscopes table present (${periscopes.modelId}) but missing ${periscopes.missing.join(", ")} \u2014 update the Issues template
|