@tablation/crew 0.86.0 → 0.87.1
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
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";
|
|
@@ -7152,13 +7234,13 @@ function holdWarnings(tickets, ships, contract, humanHolds, now = Date.now()) {
|
|
|
7152
7234
|
}
|
|
7153
7235
|
|
|
7154
7236
|
// src/run-worktree.ts
|
|
7155
|
-
import { existsSync as
|
|
7156
|
-
import { dirname as dirname6, join as
|
|
7237
|
+
import { existsSync as existsSync15 } from "fs";
|
|
7238
|
+
import { dirname as dirname6, join as join16 } from "path";
|
|
7157
7239
|
|
|
7158
7240
|
// src/ports.ts
|
|
7159
7241
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
7160
|
-
import { existsSync as
|
|
7161
|
-
import { join as
|
|
7242
|
+
import { existsSync as existsSync14 } from "fs";
|
|
7243
|
+
import { join as join15 } from "path";
|
|
7162
7244
|
var run = (cmd, args) => {
|
|
7163
7245
|
try {
|
|
7164
7246
|
return execFileSync3(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
@@ -7203,7 +7285,7 @@ function ticketForPort(port, s = DEFAULT_PORTS) {
|
|
|
7203
7285
|
}
|
|
7204
7286
|
function worktreeExists(parent, prefix, n) {
|
|
7205
7287
|
for (const name of [`${prefix}${n}`, `${prefix}${String(n).padStart(3, "0")}`]) {
|
|
7206
|
-
if (
|
|
7288
|
+
if (existsSync14(join15(parent, name, ".git"))) return true;
|
|
7207
7289
|
}
|
|
7208
7290
|
return false;
|
|
7209
7291
|
}
|
|
@@ -7286,7 +7368,7 @@ function planRunWorktree(dir, cfg, t, role, opts = {}) {
|
|
|
7286
7368
|
const existing = worktreeForNumber(dir, num);
|
|
7287
7369
|
const located = locateBranchForTicket(dir, cfg, t, role);
|
|
7288
7370
|
const baseRef = hasRemote ? `${remote}/${base}` : base;
|
|
7289
|
-
if (existing && !existing.prunable &&
|
|
7371
|
+
if (existing && !existing.prunable && existsSync15(existing.path)) {
|
|
7290
7372
|
const branch = existing.branch;
|
|
7291
7373
|
if (status(existing.path).length > 0) {
|
|
7292
7374
|
return { action: opts.resumeInterrupted ? "resume-dirty" : "leave-dirty", path: existing.path, branch };
|
|
@@ -7302,7 +7384,7 @@ function planRunWorktree(dir, cfg, t, role, opts = {}) {
|
|
|
7302
7384
|
if (countCommits(existing.path, `${baseRef}..HEAD`) > 0) return { action: "leave-unpushed", path: existing.path, branch };
|
|
7303
7385
|
return { action: "recut", path: existing.path, branch, base: baseRef };
|
|
7304
7386
|
}
|
|
7305
|
-
const path =
|
|
7387
|
+
const path = join16(dirname6(dir), worktreeForTicket(dir, cfg, t, role));
|
|
7306
7388
|
if (located?.where === "remote" || located && hasRemote && remoteBranchExists(dir, remote, located.branch)) {
|
|
7307
7389
|
return { action: "cut-remote", path, branch: located.branch };
|
|
7308
7390
|
}
|
|
@@ -7356,7 +7438,7 @@ function dropEmptyRunBranch(dir, cfg, branch) {
|
|
|
7356
7438
|
if (remoteConfigured(dir, remote) && remoteBranchExists(dir, remote, branch)) return false;
|
|
7357
7439
|
if (countCommits(dir, `${baseRef}..${branch}`) !== 0) return false;
|
|
7358
7440
|
const wt = worktrees(dir).find((w) => w.branch === branch && !w.prunable && w.path !== dir);
|
|
7359
|
-
if (wt &&
|
|
7441
|
+
if (wt && existsSync15(wt.path)) {
|
|
7360
7442
|
if (status(wt.path).length > 0) return false;
|
|
7361
7443
|
if (gitOk(dir, ["worktree", "remove", wt.path]) === null) return false;
|
|
7362
7444
|
}
|
|
@@ -7364,7 +7446,7 @@ function dropEmptyRunBranch(dir, cfg, branch) {
|
|
|
7364
7446
|
}
|
|
7365
7447
|
async function finishRunWorktree(dir, cfg, branch) {
|
|
7366
7448
|
const wt = worktrees(dir).find((w) => w.branch === branch && !w.prunable && w.path !== dir);
|
|
7367
|
-
if (!wt || !
|
|
7449
|
+
if (!wt || !existsSync15(wt.path)) return { kind: "none" };
|
|
7368
7450
|
if (cfg.hooks.handoff) return { kind: "kept-handoff", path: wt.path };
|
|
7369
7451
|
if (status(wt.path).length > 0) return { kind: "kept-dirty", path: wt.path };
|
|
7370
7452
|
if (remoteConfigured(dir, cfg.branch.remote) && worktreeHoldsUnpushed(wt.path, cfg.branch.remote)) {
|
|
@@ -7376,7 +7458,7 @@ async function finishRunWorktree(dir, cfg, branch) {
|
|
|
7376
7458
|
if (pids.length) await killGently(pids);
|
|
7377
7459
|
}
|
|
7378
7460
|
gitOk(dir, ["worktree", "remove", wt.path]);
|
|
7379
|
-
return
|
|
7461
|
+
return existsSync15(wt.path) ? { kind: "kept-dirty", path: wt.path } : { kind: "removed", path: wt.path };
|
|
7380
7462
|
}
|
|
7381
7463
|
|
|
7382
7464
|
// src/handoff-guard.ts
|
|
@@ -7759,20 +7841,20 @@ async function guardRunStrays(route, ship, tracker, role, workingId, since2, log
|
|
|
7759
7841
|
|
|
7760
7842
|
// src/pending-handoff.ts
|
|
7761
7843
|
var PENDING_MAX_AGE_MS = 48 * 36e5;
|
|
7762
|
-
var pendingDirFor = (stateDir) =>
|
|
7763
|
-
var stateDirOfStream = (streamPath) =>
|
|
7844
|
+
var pendingDirFor = (stateDir) => join17(stateDir, "pending-handoffs");
|
|
7845
|
+
var stateDirOfStream = (streamPath) => join17(streamPath, "..", "..");
|
|
7764
7846
|
function writePendingHandoff(stateDir, rec) {
|
|
7765
7847
|
const dir = pendingDirFor(stateDir);
|
|
7766
|
-
|
|
7767
|
-
|
|
7848
|
+
mkdirSync9(dir, { recursive: true });
|
|
7849
|
+
writeFileSync10(join17(dir, `${rec.ticketRecordId}.json`), JSON.stringify(rec, null, 2));
|
|
7768
7850
|
}
|
|
7769
7851
|
function listPendingHandoffs(stateDir, route) {
|
|
7770
7852
|
const dir = pendingDirFor(stateDir);
|
|
7771
|
-
if (!
|
|
7853
|
+
if (!existsSync16(dir)) return [];
|
|
7772
7854
|
const out = [];
|
|
7773
|
-
for (const f of
|
|
7855
|
+
for (const f of readdirSync7(dir).filter((n) => n.endsWith(".json")).sort()) {
|
|
7774
7856
|
try {
|
|
7775
|
-
const rec = JSON.parse(
|
|
7857
|
+
const rec = JSON.parse(readFileSync14(join17(dir, f), "utf8"));
|
|
7776
7858
|
if (rec.version === 1 && rec.ticketRecordId && (!route || rec.route === route)) out.push(rec);
|
|
7777
7859
|
} catch {
|
|
7778
7860
|
}
|
|
@@ -7780,7 +7862,7 @@ function listPendingHandoffs(stateDir, route) {
|
|
|
7780
7862
|
return out;
|
|
7781
7863
|
}
|
|
7782
7864
|
function removePendingHandoff(stateDir, ticketRecordId) {
|
|
7783
|
-
|
|
7865
|
+
rmSync9(join17(pendingDirFor(stateDir), `${ticketRecordId}.json`), { force: true });
|
|
7784
7866
|
}
|
|
7785
7867
|
function pendingHandoffFor(stateDir, ticketRecordId) {
|
|
7786
7868
|
return listPendingHandoffs(stateDir).find((r) => r.ticketRecordId === ticketRecordId);
|
|
@@ -7890,8 +7972,8 @@ function expandSkillMarkers(text, lookup2, opts = {}) {
|
|
|
7890
7972
|
}
|
|
7891
7973
|
|
|
7892
7974
|
// src/skills.ts
|
|
7893
|
-
import { readFileSync as
|
|
7894
|
-
import { join as
|
|
7975
|
+
import { readFileSync as readFileSync16, readdirSync as readdirSync8, existsSync as existsSync18 } from "fs";
|
|
7976
|
+
import { join as join19 } from "path";
|
|
7895
7977
|
import { parse as parseYaml } from "yaml";
|
|
7896
7978
|
import { TablationClient as TablationClient3, StaleWriteError as StaleWriteError3, TablationApiError as TablationApiError2 } from "@tablation/client";
|
|
7897
7979
|
|
|
@@ -7907,8 +7989,8 @@ ${additions}`;
|
|
|
7907
7989
|
}
|
|
7908
7990
|
|
|
7909
7991
|
// src/agents.ts
|
|
7910
|
-
import { readFileSync as
|
|
7911
|
-
import { join as
|
|
7992
|
+
import { readFileSync as readFileSync15, existsSync as existsSync17 } from "fs";
|
|
7993
|
+
import { join as join18 } from "path";
|
|
7912
7994
|
import { TablationClient as TablationClient2, StaleWriteError as StaleWriteError2, TablationApiError } from "@tablation/client";
|
|
7913
7995
|
var PERSONA_NAME = {
|
|
7914
7996
|
dev: "Developer",
|
|
@@ -7922,12 +8004,12 @@ var CREW_AGENT_COLUMN = "agent_id";
|
|
|
7922
8004
|
var AgentsSyncError = class extends Error {
|
|
7923
8005
|
};
|
|
7924
8006
|
function personaDefaultPrompt(promptsDir, role) {
|
|
7925
|
-
const briefPath =
|
|
7926
|
-
if (!
|
|
7927
|
-
if (SELF_CONTAINED_ROLES.has(role)) return
|
|
7928
|
-
const commonPath =
|
|
7929
|
-
if (!
|
|
7930
|
-
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")}`;
|
|
7931
8013
|
}
|
|
7932
8014
|
async function resolveAgentId(route, role, opts) {
|
|
7933
8015
|
const cached = route.resolved?.agentPersonas?.[role]?.agentId;
|
|
@@ -8374,7 +8456,7 @@ function describeCrewLink(o) {
|
|
|
8374
8456
|
var SkillSyncError = class extends Error {
|
|
8375
8457
|
};
|
|
8376
8458
|
function parseSkillFile(path) {
|
|
8377
|
-
const text =
|
|
8459
|
+
const text = readFileSync16(path, "utf8");
|
|
8378
8460
|
const match = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/.exec(text);
|
|
8379
8461
|
if (!match) throw new SkillSyncError(`${path}: missing YAML frontmatter (expected a leading --- block)`);
|
|
8380
8462
|
const frontmatter = match[1];
|
|
@@ -8387,9 +8469,9 @@ function parseSkillFile(path) {
|
|
|
8387
8469
|
return { name: meta.name, description: meta.description, prompt: body, file: path };
|
|
8388
8470
|
}
|
|
8389
8471
|
function listLocalSkills(promptsDir) {
|
|
8390
|
-
const dir =
|
|
8391
|
-
if (!
|
|
8392
|
-
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)));
|
|
8393
8475
|
}
|
|
8394
8476
|
async function syncSkills(route, opts) {
|
|
8395
8477
|
if (!route.resolved) {
|
|
@@ -8591,26 +8673,26 @@ var AgentError = class extends Error {
|
|
|
8591
8673
|
};
|
|
8592
8674
|
var AgentSpawnError = class extends AgentError {
|
|
8593
8675
|
};
|
|
8594
|
-
var digestPathFor = (stateDir, routeLabel, role, runId) =>
|
|
8676
|
+
var digestPathFor = (stateDir, routeLabel, role, runId) => join20(stateDir, `digest-${routeLabel}-${role}${runId ? `-${runId}` : ""}.md`);
|
|
8595
8677
|
function planAgentRun(o) {
|
|
8596
|
-
const briefPath =
|
|
8597
|
-
if (!
|
|
8678
|
+
const briefPath = join20(o.route.promptsDir, "personas", `lane-${o.role}.md`);
|
|
8679
|
+
if (!existsSync19(briefPath)) {
|
|
8598
8680
|
throw new AgentError(`no brief at ${briefPath} \u2014 refusing to run an unscoped session`);
|
|
8599
8681
|
}
|
|
8600
|
-
const commonPath =
|
|
8601
|
-
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);
|
|
8602
8684
|
const now = o.now ?? (() => Date.now());
|
|
8603
8685
|
const routeLabel = routeSlug(o.route.route);
|
|
8604
8686
|
const ownDigest = o.runId ? digestPathFor(o.stateDir, routeLabel, o.role, o.runId) : void 0;
|
|
8605
|
-
const digestPath2 = ownDigest &&
|
|
8687
|
+
const digestPath2 = ownDigest && existsSync19(ownDigest) ? ownDigest : digestPathFor(o.stateDir, routeLabel, o.role);
|
|
8606
8688
|
let digest;
|
|
8607
8689
|
let ageSeconds;
|
|
8608
|
-
if (
|
|
8690
|
+
if (existsSync19(digestPath2)) {
|
|
8609
8691
|
ageSeconds = Math.floor((now() - statSync5(digestPath2).mtimeMs) / 1e3);
|
|
8610
|
-
if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest =
|
|
8692
|
+
if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync17(digestPath2, "utf8");
|
|
8611
8693
|
}
|
|
8612
|
-
const common = usesCommon ?
|
|
8613
|
-
const brief =
|
|
8694
|
+
const common = usesCommon ? readFileSync17(commonPath, "utf8") : "";
|
|
8695
|
+
const brief = readFileSync17(briefPath, "utf8");
|
|
8614
8696
|
const effectiveCommon = o.divergedPrompt ? "" : common;
|
|
8615
8697
|
const effectiveBrief = appendAdditions(o.divergedPrompt ?? brief, o.personaAdditions);
|
|
8616
8698
|
const localSkills = new Map(listLocalSkills(o.route.promptsDir).map((s) => [s.name, s.prompt]));
|
|
@@ -8632,7 +8714,7 @@ function planAgentRun(o) {
|
|
|
8632
8714
|
digest
|
|
8633
8715
|
});
|
|
8634
8716
|
const promptSha = createHash3("sha256").update(expanded.text).digest("hex");
|
|
8635
|
-
const streamsDir =
|
|
8717
|
+
const streamsDir = join20(o.stateDir, "streams");
|
|
8636
8718
|
const base = `${routeLabel}-${o.role}-${o.cycle}${o.runId ? `-${o.runId}` : ""}`;
|
|
8637
8719
|
const seat = resolveSeat(o.ship, o.route, o.role, o.agentPersona ?? (o.agentModel ? { model: o.agentModel } : {}), o.repo);
|
|
8638
8720
|
const { toolchain, model } = seat;
|
|
@@ -8702,8 +8784,8 @@ function planAgentRun(o) {
|
|
|
8702
8784
|
digestAttached: digest !== void 0,
|
|
8703
8785
|
digestAgeSeconds: ageSeconds,
|
|
8704
8786
|
model,
|
|
8705
|
-
streamPath:
|
|
8706
|
-
eventsPath:
|
|
8787
|
+
streamPath: join20(streamsDir, `${base}.jsonl`),
|
|
8788
|
+
eventsPath: join20(streamsDir, `${base}.events.jsonl`),
|
|
8707
8789
|
ticket: o.ticket,
|
|
8708
8790
|
ticketRecordId: o.ticketRecordId ?? null,
|
|
8709
8791
|
repo: o.repo ?? null,
|
|
@@ -8734,7 +8816,7 @@ function describePlan(p) {
|
|
|
8734
8816
|
var pidPathFor = (streamPath) => `${streamPath}.pid`;
|
|
8735
8817
|
function readRunSidecar(streamPath) {
|
|
8736
8818
|
try {
|
|
8737
|
-
const sc = JSON.parse(
|
|
8819
|
+
const sc = JSON.parse(readFileSync17(pidPathFor(streamPath), "utf8"));
|
|
8738
8820
|
return typeof sc.pid === "number" && typeof sc.spawnedAt === "string" && typeof sc.role === "string" ? sc : null;
|
|
8739
8821
|
} catch {
|
|
8740
8822
|
return null;
|
|
@@ -8770,7 +8852,7 @@ function identityOf(plan, adapterHarness) {
|
|
|
8770
8852
|
var withoutKey = ({ apiKey: _apiKey, ...rest }) => rest;
|
|
8771
8853
|
function openSink(path) {
|
|
8772
8854
|
try {
|
|
8773
|
-
|
|
8855
|
+
mkdirSync10(dirname7(path), { recursive: true });
|
|
8774
8856
|
return createWriteStream(path, { flags: "a" });
|
|
8775
8857
|
} catch {
|
|
8776
8858
|
return void 0;
|
|
@@ -8952,7 +9034,7 @@ var RunObserver = class {
|
|
|
8952
9034
|
};
|
|
8953
9035
|
function writeRunMeta(id, startedAt, liveTicket, extra) {
|
|
8954
9036
|
try {
|
|
8955
|
-
|
|
9037
|
+
writeFileSync11(`${id.streamPath}.meta.json`, JSON.stringify({
|
|
8956
9038
|
route: id.route,
|
|
8957
9039
|
role: id.role,
|
|
8958
9040
|
hint: id.ticket,
|
|
@@ -9112,7 +9194,7 @@ function spawnAgent(plan, emit, hintMoved) {
|
|
|
9112
9194
|
return new Promise((resolve6, reject) => {
|
|
9113
9195
|
let streamFd;
|
|
9114
9196
|
try {
|
|
9115
|
-
|
|
9197
|
+
mkdirSync10(dirname7(plan.streamPath), { recursive: true });
|
|
9116
9198
|
streamFd = openSync3(plan.streamPath, "a");
|
|
9117
9199
|
} catch (e) {
|
|
9118
9200
|
void endSink(eventsSink);
|
|
@@ -9147,7 +9229,7 @@ function spawnAgent(plan, emit, hintMoved) {
|
|
|
9147
9229
|
promptSha: plan.promptSha
|
|
9148
9230
|
};
|
|
9149
9231
|
try {
|
|
9150
|
-
|
|
9232
|
+
writeFileSync11(pidPathFor(plan.streamPath), JSON.stringify(sidecar));
|
|
9151
9233
|
} catch {
|
|
9152
9234
|
}
|
|
9153
9235
|
}
|
|
@@ -9650,7 +9732,7 @@ async function runSelftest(i, deps = realDeps) {
|
|
|
9650
9732
|
...adapter.runEnv(spec),
|
|
9651
9733
|
[KEY_VAR]: i.apiKey
|
|
9652
9734
|
};
|
|
9653
|
-
const cwd = mkdtempSync(
|
|
9735
|
+
const cwd = mkdtempSync(join21(tmpdir2(), "crew-selftest-"));
|
|
9654
9736
|
try {
|
|
9655
9737
|
const run2 = await deps.run({
|
|
9656
9738
|
bin: invocation.bin,
|
|
@@ -9672,7 +9754,7 @@ async function runSelftest(i, deps = realDeps) {
|
|
|
9672
9754
|
return finish({ ok: false, failure, detail, ...usage2 });
|
|
9673
9755
|
} finally {
|
|
9674
9756
|
try {
|
|
9675
|
-
|
|
9757
|
+
rmSync10(cwd, { recursive: true, force: true });
|
|
9676
9758
|
} catch {
|
|
9677
9759
|
}
|
|
9678
9760
|
}
|
|
@@ -9683,11 +9765,11 @@ function formatResult(r) {
|
|
|
9683
9765
|
const secs = `${(r.ms / 1e3).toFixed(1)}s`;
|
|
9684
9766
|
return r.ok ? `${r.toolchain}: PASS ${r.detail} (${secs}${tok})` : `${r.toolchain}: FAIL ${r.failure} \u2014 ${r.detail} (${secs})`;
|
|
9685
9767
|
}
|
|
9686
|
-
var fileFor2 = (stateDir) =>
|
|
9768
|
+
var fileFor2 = (stateDir) => join21(stateDir, "selftest.json");
|
|
9687
9769
|
function readSelftest(stateDir) {
|
|
9688
9770
|
try {
|
|
9689
9771
|
const p = fileFor2(stateDir);
|
|
9690
|
-
return
|
|
9772
|
+
return existsSync20(p) ? JSON.parse(readFileSync18(p, "utf8")) : {};
|
|
9691
9773
|
} catch {
|
|
9692
9774
|
return {};
|
|
9693
9775
|
}
|
|
@@ -9695,8 +9777,8 @@ function readSelftest(stateDir) {
|
|
|
9695
9777
|
function recordSelftest(stateDir, results) {
|
|
9696
9778
|
const all = readSelftest(stateDir);
|
|
9697
9779
|
for (const r of results) all[r.toolchain] = r;
|
|
9698
|
-
|
|
9699
|
-
|
|
9780
|
+
mkdirSync11(stateDir, { recursive: true });
|
|
9781
|
+
writeFileSync12(fileFor2(stateDir), `${JSON.stringify(all, null, 2)}
|
|
9700
9782
|
`);
|
|
9701
9783
|
}
|
|
9702
9784
|
function doctorSelftestLines(stored, toolchains) {
|
|
@@ -9708,7 +9790,7 @@ function doctorSelftestLines(stored, toolchains) {
|
|
|
9708
9790
|
}
|
|
9709
9791
|
|
|
9710
9792
|
// src/cli.ts
|
|
9711
|
-
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";
|
|
9712
9794
|
import { fileURLToPath } from "url";
|
|
9713
9795
|
import { hostname as hostname2, homedir as homedir10 } from "os";
|
|
9714
9796
|
import { spawnSync as spawnSync7 } from "child_process";
|
|
@@ -9890,14 +9972,14 @@ function rosterMarkdown(roster, meId) {
|
|
|
9890
9972
|
}
|
|
9891
9973
|
|
|
9892
9974
|
// src/connect-resume.ts
|
|
9893
|
-
import { mkdirSync as
|
|
9894
|
-
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";
|
|
9895
9977
|
function lastSessionFile(stateDir) {
|
|
9896
|
-
return
|
|
9978
|
+
return join22(stateDir, "connect-session.json");
|
|
9897
9979
|
}
|
|
9898
9980
|
function readPointers(stateDir) {
|
|
9899
9981
|
try {
|
|
9900
|
-
const parsed = JSON.parse(
|
|
9982
|
+
const parsed = JSON.parse(readFileSync19(lastSessionFile(stateDir), "utf8"));
|
|
9901
9983
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
9902
9984
|
const out = {};
|
|
9903
9985
|
for (const [host, v] of Object.entries(parsed)) {
|
|
@@ -9911,8 +9993,8 @@ function readPointers(stateDir) {
|
|
|
9911
9993
|
}
|
|
9912
9994
|
function writePointers(stateDir, pointers) {
|
|
9913
9995
|
const file = lastSessionFile(stateDir);
|
|
9914
|
-
|
|
9915
|
-
|
|
9996
|
+
mkdirSync12(dirname8(file), { recursive: true });
|
|
9997
|
+
writeFileSync13(file, `${JSON.stringify(pointers, null, 2)}
|
|
9916
9998
|
`, { mode: 384 });
|
|
9917
9999
|
}
|
|
9918
10000
|
function rememberSession(stateDir, host, workspaceSlug) {
|
|
@@ -9929,7 +10011,7 @@ function forgetWorkspace(stateDir, host, workspaceSlug) {
|
|
|
9929
10011
|
const { [host]: _old, ...rest } = all;
|
|
9930
10012
|
const next = left.length ? { ...rest, [host]: left } : rest;
|
|
9931
10013
|
if (Object.keys(next).length) writePointers(stateDir, next);
|
|
9932
|
-
else
|
|
10014
|
+
else rmSync11(lastSessionFile(stateDir), { force: true });
|
|
9933
10015
|
} catch {
|
|
9934
10016
|
}
|
|
9935
10017
|
}
|
|
@@ -9969,7 +10051,7 @@ function forgetSession(stateDir, host) {
|
|
|
9969
10051
|
try {
|
|
9970
10052
|
const { [host]: _gone, ...rest } = readPointers(stateDir);
|
|
9971
10053
|
if (Object.keys(rest).length) writePointers(stateDir, rest);
|
|
9972
|
-
else
|
|
10054
|
+
else rmSync11(lastSessionFile(stateDir), { force: true });
|
|
9973
10055
|
} catch {
|
|
9974
10056
|
}
|
|
9975
10057
|
}
|
|
@@ -9997,15 +10079,15 @@ async function signOutSessions(o) {
|
|
|
9997
10079
|
}
|
|
9998
10080
|
|
|
9999
10081
|
// src/reset.ts
|
|
10000
|
-
import { existsSync as
|
|
10001
|
-
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";
|
|
10002
10084
|
import { hostFromUrl as hostFromUrl3 } from "@tablation/client";
|
|
10003
10085
|
function clonedCheckoutsFile(stateDir) {
|
|
10004
|
-
return
|
|
10086
|
+
return join23(stateDir, "cloned-checkouts.json");
|
|
10005
10087
|
}
|
|
10006
10088
|
function readClonedCheckouts(stateDir) {
|
|
10007
10089
|
try {
|
|
10008
|
-
const parsed = JSON.parse(
|
|
10090
|
+
const parsed = JSON.parse(readFileSync20(clonedCheckoutsFile(stateDir), "utf8"));
|
|
10009
10091
|
return Array.isArray(parsed) ? parsed.filter((d) => typeof d === "string") : [];
|
|
10010
10092
|
} catch {
|
|
10011
10093
|
return [];
|
|
@@ -10015,8 +10097,8 @@ function recordClonedCheckout(stateDir, dir) {
|
|
|
10015
10097
|
const known = readClonedCheckouts(stateDir);
|
|
10016
10098
|
if (known.includes(dir)) return;
|
|
10017
10099
|
const file = clonedCheckoutsFile(stateDir);
|
|
10018
|
-
|
|
10019
|
-
|
|
10100
|
+
mkdirSync13(dirname9(file), { recursive: true });
|
|
10101
|
+
writeFileSync14(file, `${JSON.stringify([...known, dir], null, 2)}
|
|
10020
10102
|
`);
|
|
10021
10103
|
}
|
|
10022
10104
|
var mark = (dryRun) => dryRun ? "would-remove" : "removed";
|
|
@@ -10068,8 +10150,8 @@ async function runReset(o, fx) {
|
|
|
10068
10150
|
for (const dir of readClonedCheckouts(o.stateDir)) steps.push(removeCheckout(dir, o.dryRun, fx));
|
|
10069
10151
|
}
|
|
10070
10152
|
steps.push(...removeState(o.stateDir, o.dryRun));
|
|
10071
|
-
if (
|
|
10072
|
-
if (!o.dryRun)
|
|
10153
|
+
if (existsSync21(o.configFile)) {
|
|
10154
|
+
if (!o.dryRun) rmSync12(o.configFile, { force: true });
|
|
10073
10155
|
steps.push({ step: "config", target: o.configFile, action: mark(o.dryRun) });
|
|
10074
10156
|
} else {
|
|
10075
10157
|
steps.push({ step: "config", target: o.configFile, action: "absent" });
|
|
@@ -10077,7 +10159,7 @@ async function runReset(o, fx) {
|
|
|
10077
10159
|
return steps;
|
|
10078
10160
|
}
|
|
10079
10161
|
function removeCheckout(dir, dryRun, fx) {
|
|
10080
|
-
if (!
|
|
10162
|
+
if (!existsSync21(dir)) return { step: "checkout", target: dir, action: "absent", detail: "already gone" };
|
|
10081
10163
|
const { dirty, linkedWorktrees } = fx.inspectCheckout(dir);
|
|
10082
10164
|
if (dirty.length) {
|
|
10083
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" : ""})` };
|
|
@@ -10085,21 +10167,21 @@ function removeCheckout(dir, dryRun, fx) {
|
|
|
10085
10167
|
if (linkedWorktrees.length) {
|
|
10086
10168
|
return { step: "checkout", target: dir, action: "kept", detail: `has linked worktrees (${linkedWorktrees.join(", ")}); remove them first` };
|
|
10087
10169
|
}
|
|
10088
|
-
if (!dryRun)
|
|
10170
|
+
if (!dryRun) rmSync12(dir, { recursive: true, force: true });
|
|
10089
10171
|
return { step: "checkout", target: dir, action: mark(dryRun) };
|
|
10090
10172
|
}
|
|
10091
10173
|
function removeState(stateDir, dryRun) {
|
|
10092
10174
|
const out = [];
|
|
10093
|
-
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")];
|
|
10094
10176
|
try {
|
|
10095
|
-
for (const name of
|
|
10096
|
-
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));
|
|
10097
10179
|
}
|
|
10098
10180
|
} catch {
|
|
10099
10181
|
}
|
|
10100
10182
|
for (const t of targets) {
|
|
10101
|
-
if (!
|
|
10102
|
-
if (!dryRun)
|
|
10183
|
+
if (!existsSync21(t)) continue;
|
|
10184
|
+
if (!dryRun) rmSync12(t, { recursive: true, force: true });
|
|
10103
10185
|
out.push({ step: "state", target: t, action: mark(dryRun) });
|
|
10104
10186
|
}
|
|
10105
10187
|
if (!out.length) out.push({ step: "state", target: stateDir, action: "absent", detail: "nothing to remove" });
|
|
@@ -10886,8 +10968,8 @@ ${waiting.map((t) => `| ${displayKey2(t)} | ${t.status} | ${blockers(t, i)} |`).
|
|
|
10886
10968
|
}
|
|
10887
10969
|
|
|
10888
10970
|
// src/poll.ts
|
|
10889
|
-
import { writeFileSync as
|
|
10890
|
-
import { join as
|
|
10971
|
+
import { writeFileSync as writeFileSync15 } from "fs";
|
|
10972
|
+
import { join as join24 } from "path";
|
|
10891
10973
|
|
|
10892
10974
|
// src/stalled.ts
|
|
10893
10975
|
var STALLED_AFTER_CYCLES = 30;
|
|
@@ -11194,7 +11276,7 @@ async function decideCycle(o) {
|
|
|
11194
11276
|
}
|
|
11195
11277
|
return decision;
|
|
11196
11278
|
}
|
|
11197
|
-
var digestPath = (stateDir, route, role, runId) =>
|
|
11279
|
+
var digestPath = (stateDir, route, role, runId) => join24(stateDir, `digest-${route}-${role}${runId ? `-${runId}` : ""}.md`);
|
|
11198
11280
|
function otherRunsRemoved(tickets, own) {
|
|
11199
11281
|
if (!own) return tickets;
|
|
11200
11282
|
return tickets.filter((t) => t.id === own.ticket.id || !own.inFlight(t.id));
|
|
@@ -11272,7 +11354,7 @@ function writeDigest(o, d, role, stateDir, runId, own) {
|
|
|
11272
11354
|
|
|
11273
11355
|
${body}` : body;
|
|
11274
11356
|
const path = digestPath(stateDir, routeSlug(o.route.route), role, runId);
|
|
11275
|
-
|
|
11357
|
+
writeFileSync15(path, text);
|
|
11276
11358
|
o.emit.emit(`queue digest written for ${role} (${Buffer.byteLength(text)} bytes)`, {
|
|
11277
11359
|
data: { role, bytes: Buffer.byteLength(text) }
|
|
11278
11360
|
});
|
|
@@ -11755,13 +11837,13 @@ Ship ${shipName2} found: ${problem}. The release refuses to squash a ref nobody
|
|
|
11755
11837
|
var localRefSkipReported = (ticket, problem, events) => events.some((e) => e.record_id === ticket.id && e.event_type === "local_ref_skip" && (e.message ?? "").includes(problem));
|
|
11756
11838
|
|
|
11757
11839
|
// src/connect-wizard.ts
|
|
11758
|
-
import { delimiter, dirname as dirname10, join as
|
|
11759
|
-
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";
|
|
11760
11842
|
import { stringify } from "yaml";
|
|
11761
11843
|
var HOST_PASSENGERS_DOCS_URL = "https://crew.tablation.dev/host-passengers";
|
|
11762
11844
|
function shouldRunWizard(opts) {
|
|
11763
11845
|
if (!opts.isTTY) return false;
|
|
11764
|
-
const exists = opts.configExists ??
|
|
11846
|
+
const exists = opts.configExists ?? existsSync22;
|
|
11765
11847
|
return !configSearchPath(opts.crewHome).some((p) => exists(p));
|
|
11766
11848
|
}
|
|
11767
11849
|
function firstRunConfigPath() {
|
|
@@ -11778,7 +11860,7 @@ function defaultIsExecutable(p) {
|
|
|
11778
11860
|
function findOnPath2(name, pathEnv, isExecutable = defaultIsExecutable) {
|
|
11779
11861
|
for (const dir of pathEnv.split(delimiter)) {
|
|
11780
11862
|
if (!dir) continue;
|
|
11781
|
-
const candidate =
|
|
11863
|
+
const candidate = join25(dir, name);
|
|
11782
11864
|
if (isExecutable(candidate)) return candidate;
|
|
11783
11865
|
}
|
|
11784
11866
|
return void 0;
|
|
@@ -11839,9 +11921,9 @@ routes:
|
|
|
11839
11921
|
${routeBlock}`;
|
|
11840
11922
|
}
|
|
11841
11923
|
function writeNewConfig(path, text) {
|
|
11842
|
-
|
|
11924
|
+
mkdirSync14(dirname10(path), { recursive: true });
|
|
11843
11925
|
try {
|
|
11844
|
-
|
|
11926
|
+
writeFileSync16(path, text, { mode: 384, flag: "wx" });
|
|
11845
11927
|
return true;
|
|
11846
11928
|
} catch (e) {
|
|
11847
11929
|
if (e.code === "EEXIST") return false;
|
|
@@ -11875,7 +11957,7 @@ Wrote ${o.configPath}.
|
|
|
11875
11957
|
// src/connect-json.ts
|
|
11876
11958
|
import { accessSync as accessSync3, constants as constants3 } from "fs";
|
|
11877
11959
|
import { homedir as homedir8 } from "os";
|
|
11878
|
-
import { dirname as dirname11, join as
|
|
11960
|
+
import { dirname as dirname11, join as join26 } from "path";
|
|
11879
11961
|
import { delimiter as delimiter2 } from "path";
|
|
11880
11962
|
var CHOICE_EXIT_CODE = 3;
|
|
11881
11963
|
function createEmitter(json, write) {
|
|
@@ -11914,12 +11996,12 @@ function agentBinSearchDirs(home, pathEnv, extraPath) {
|
|
|
11914
11996
|
const dirs = [
|
|
11915
11997
|
...pathEnv.split(delimiter2),
|
|
11916
11998
|
...(extraPath ?? "").split(delimiter2),
|
|
11917
|
-
|
|
11918
|
-
|
|
11999
|
+
join26(home, ".local/bin"),
|
|
12000
|
+
join26(home, ".claude/local"),
|
|
11919
12001
|
"/opt/homebrew/bin",
|
|
11920
12002
|
"/usr/local/bin",
|
|
11921
|
-
|
|
11922
|
-
|
|
12003
|
+
join26(home, ".npm-global/bin"),
|
|
12004
|
+
join26(home, ".volta/bin")
|
|
11923
12005
|
];
|
|
11924
12006
|
return [...new Set(dirs.filter(Boolean))];
|
|
11925
12007
|
}
|
|
@@ -11955,8 +12037,8 @@ function flagWizardAnswers(f, env) {
|
|
|
11955
12037
|
}
|
|
11956
12038
|
|
|
11957
12039
|
// src/release-run.ts
|
|
11958
|
-
import { readFileSync as
|
|
11959
|
-
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";
|
|
11960
12042
|
import { tmpdir as tmpdir3 } from "os";
|
|
11961
12043
|
|
|
11962
12044
|
// src/release.ts
|
|
@@ -12697,13 +12779,13 @@ async function bump(o, size) {
|
|
|
12697
12779
|
}
|
|
12698
12780
|
return printed;
|
|
12699
12781
|
}
|
|
12700
|
-
const files = o.repo.release.versionFiles.map((f) =>
|
|
12782
|
+
const files = o.repo.release.versionFiles.map((f) => join27(o.cwd, f));
|
|
12701
12783
|
const first = files[0];
|
|
12702
|
-
if (!
|
|
12784
|
+
if (!existsSync23(first)) {
|
|
12703
12785
|
o.emit.error(`no ${o.repo.release.versionFiles[0]} to read a version from`);
|
|
12704
12786
|
return null;
|
|
12705
12787
|
}
|
|
12706
|
-
const current = JSON.parse(
|
|
12788
|
+
const current = JSON.parse(readFileSync21(first, "utf8")).version;
|
|
12707
12789
|
if (!current) {
|
|
12708
12790
|
o.emit.error(`no version field in ${o.repo.release.versionFiles[0]}`);
|
|
12709
12791
|
return null;
|
|
@@ -12716,27 +12798,27 @@ async function bump(o, size) {
|
|
|
12716
12798
|
return version;
|
|
12717
12799
|
}
|
|
12718
12800
|
for (const f of files) {
|
|
12719
|
-
const src =
|
|
12801
|
+
const src = readFileSync21(f, "utf8");
|
|
12720
12802
|
const out = src.replace(/("version"\s*:\s*")[^"]*(")/, `$1${version}$2`);
|
|
12721
12803
|
if (out === src) {
|
|
12722
12804
|
o.emit.error(`no version field in ${f}`);
|
|
12723
12805
|
return null;
|
|
12724
12806
|
}
|
|
12725
|
-
|
|
12807
|
+
writeFileSync17(f, out);
|
|
12726
12808
|
}
|
|
12727
12809
|
return version;
|
|
12728
12810
|
}
|
|
12729
12811
|
function writeChangelog(o, version, entries) {
|
|
12730
12812
|
const file = o.repo.release.changelog;
|
|
12731
12813
|
if (!file || entries.length === 0) return;
|
|
12732
|
-
const path =
|
|
12814
|
+
const path = join27(o.cwd, file);
|
|
12733
12815
|
const section = renderChangelogSection(version, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), entries);
|
|
12734
12816
|
if (o.dryRun) {
|
|
12735
12817
|
o.emit.emit(`would prepend ${entries.length} entr(ies) to ${file}`);
|
|
12736
12818
|
return;
|
|
12737
12819
|
}
|
|
12738
|
-
const existing =
|
|
12739
|
-
|
|
12820
|
+
const existing = existsSync23(path) ? readFileSync21(path, "utf8") : "# Changelog\n";
|
|
12821
|
+
writeFileSync17(path, insertChangelogSection(existing, section));
|
|
12740
12822
|
}
|
|
12741
12823
|
async function confirm(o, expected) {
|
|
12742
12824
|
if (!o.repo.hooks.released) return void 0;
|
|
@@ -12819,7 +12901,7 @@ function closeReleaseCheckout(cwd, root, dir, branch, run2 = gitOk) {
|
|
|
12819
12901
|
run2(cwd, ["worktree", "remove", "--force", dir]);
|
|
12820
12902
|
} finally {
|
|
12821
12903
|
try {
|
|
12822
|
-
|
|
12904
|
+
rmSync13(root, { recursive: true, force: true });
|
|
12823
12905
|
} finally {
|
|
12824
12906
|
run2(cwd, ["worktree", "prune"]);
|
|
12825
12907
|
run2(cwd, ["branch", "-D", branch]);
|
|
@@ -12829,8 +12911,8 @@ function closeReleaseCheckout(cwd, root, dir, branch, run2 = gitOk) {
|
|
|
12829
12911
|
function openReleaseCheckout(o) {
|
|
12830
12912
|
const { remote, base } = o.repo.branch;
|
|
12831
12913
|
const branch = `crew/release-${Date.now()}`;
|
|
12832
|
-
const root = mkdtempSync2(
|
|
12833
|
-
const dir =
|
|
12914
|
+
const root = mkdtempSync2(join27(tmpdir3(), "crew-release-"));
|
|
12915
|
+
const dir = join27(root, "checkout");
|
|
12834
12916
|
o.emit.emit(`release checkout: ${dir}`, { step: "release" });
|
|
12835
12917
|
const close = () => closeReleaseCheckout(o.cwd, root, dir, branch);
|
|
12836
12918
|
try {
|
|
@@ -12840,10 +12922,10 @@ function openReleaseCheckout(o) {
|
|
|
12840
12922
|
throw e;
|
|
12841
12923
|
}
|
|
12842
12924
|
for (const f of o.repo.worktrees.copy) {
|
|
12843
|
-
const src =
|
|
12844
|
-
if (!
|
|
12845
|
-
|
|
12846
|
-
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));
|
|
12847
12929
|
}
|
|
12848
12930
|
return { dir, branch, close };
|
|
12849
12931
|
}
|
|
@@ -13544,12 +13626,12 @@ Passed: ${passed}.${willClose ? " Closing this alert." : ""}`,
|
|
|
13544
13626
|
}
|
|
13545
13627
|
|
|
13546
13628
|
// src/live-runs.ts
|
|
13547
|
-
import { existsSync as
|
|
13548
|
-
import { join as
|
|
13629
|
+
import { existsSync as existsSync25, readdirSync as readdirSync11 } from "fs";
|
|
13630
|
+
import { join as join29 } from "path";
|
|
13549
13631
|
|
|
13550
13632
|
// src/interrupted.ts
|
|
13551
|
-
import { existsSync as
|
|
13552
|
-
import { join as
|
|
13633
|
+
import { existsSync as existsSync24, readdirSync as readdirSync10, readFileSync as readFileSync22 } from "fs";
|
|
13634
|
+
import { join as join28 } from "path";
|
|
13553
13635
|
var INTERRUPTED_MARKER = calloutHeader("NOTE", "Interrupted");
|
|
13554
13636
|
function interruptedComment(b) {
|
|
13555
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.";
|
|
@@ -13568,21 +13650,21 @@ var pidAlive = (pid) => {
|
|
|
13568
13650
|
};
|
|
13569
13651
|
function runFinished(streamPath) {
|
|
13570
13652
|
const path = `${streamPath}.meta.json`;
|
|
13571
|
-
if (!
|
|
13653
|
+
if (!existsSync24(path)) return false;
|
|
13572
13654
|
try {
|
|
13573
|
-
return JSON.parse(
|
|
13655
|
+
return JSON.parse(readFileSync22(path, "utf8")).running !== true;
|
|
13574
13656
|
} catch {
|
|
13575
13657
|
return true;
|
|
13576
13658
|
}
|
|
13577
13659
|
}
|
|
13578
13660
|
function orphanedStreams(o) {
|
|
13579
|
-
const dir =
|
|
13580
|
-
if (!
|
|
13661
|
+
const dir = join28(o.stateDir, "streams");
|
|
13662
|
+
if (!existsSync24(dir)) return [];
|
|
13581
13663
|
const ownerAlive = o.isOwnerAlive ?? pidAlive;
|
|
13582
13664
|
const out = [];
|
|
13583
|
-
for (const name of
|
|
13665
|
+
for (const name of readdirSync10(dir)) {
|
|
13584
13666
|
if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
|
|
13585
|
-
const streamPath =
|
|
13667
|
+
const streamPath = join28(dir, name);
|
|
13586
13668
|
if (runFinished(streamPath)) continue;
|
|
13587
13669
|
const sidecar = readRunSidecar(streamPath);
|
|
13588
13670
|
if (!sidecar || sidecar.route !== o.routeLabel) continue;
|
|
@@ -13636,12 +13718,12 @@ async function reconcileRuns(o) {
|
|
|
13636
13718
|
|
|
13637
13719
|
// src/live-runs.ts
|
|
13638
13720
|
function liveRuns(stateDir, isAlive = runIsAlive) {
|
|
13639
|
-
const dir =
|
|
13640
|
-
if (!
|
|
13721
|
+
const dir = join29(stateDir, "streams");
|
|
13722
|
+
if (!existsSync25(dir)) return [];
|
|
13641
13723
|
const out = [];
|
|
13642
|
-
for (const name of
|
|
13724
|
+
for (const name of readdirSync11(dir)) {
|
|
13643
13725
|
if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
|
|
13644
|
-
const streamPath =
|
|
13726
|
+
const streamPath = join29(dir, name);
|
|
13645
13727
|
if (runFinished(streamPath)) continue;
|
|
13646
13728
|
const sidecar = readRunSidecar(streamPath);
|
|
13647
13729
|
if (!sidecar || !isAlive(sidecar)) continue;
|
|
@@ -13911,7 +13993,7 @@ function missingReleaserWarning(repo, release, ships, now = Date.now()) {
|
|
|
13911
13993
|
}
|
|
13912
13994
|
|
|
13913
13995
|
// src/watch.ts
|
|
13914
|
-
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";
|
|
13915
13997
|
var E = "\x1B[";
|
|
13916
13998
|
var dim = (s) => `${E}2m${s}${E}0m`;
|
|
13917
13999
|
var bold = (s) => `${E}1m${s}${E}0m`;
|
|
@@ -14012,7 +14094,7 @@ function renderHeaders(states, now = Date.now()) {
|
|
|
14012
14094
|
return states.map((s) => renderHeader(s, now)).join("");
|
|
14013
14095
|
}
|
|
14014
14096
|
function readFrom(file, from) {
|
|
14015
|
-
if (!
|
|
14097
|
+
if (!existsSync26(file)) return { events: [], offset: from };
|
|
14016
14098
|
const size = statSync7(file).size;
|
|
14017
14099
|
if (size < from) return readFrom(file, 0);
|
|
14018
14100
|
if (size === from) return { events: [], offset: from };
|
|
@@ -14255,17 +14337,17 @@ function nextRoles(decision, ran) {
|
|
|
14255
14337
|
}
|
|
14256
14338
|
|
|
14257
14339
|
// src/ship-name.ts
|
|
14258
|
-
import { existsSync as
|
|
14340
|
+
import { existsSync as existsSync28, mkdirSync as mkdirSync17, readFileSync as readFileSync24, renameSync as renameSync2, writeFileSync as writeFileSync19 } from "fs";
|
|
14259
14341
|
import { dirname as dirname14 } from "path";
|
|
14260
14342
|
import { parseDocument } from "yaml";
|
|
14261
14343
|
|
|
14262
14344
|
// src/machine-id.ts
|
|
14263
14345
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
14264
14346
|
import { createHash as createHash4 } from "crypto";
|
|
14265
|
-
import { existsSync as
|
|
14266
|
-
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";
|
|
14267
14349
|
function machineIdPathFor(stateDir) {
|
|
14268
|
-
return
|
|
14350
|
+
return join30(stateDir, "machine-id");
|
|
14269
14351
|
}
|
|
14270
14352
|
var sha256 = (s) => createHash4("sha256").update(s).digest("hex");
|
|
14271
14353
|
function readRawMachineId(platform2 = process.platform) {
|
|
@@ -14278,8 +14360,8 @@ function readRawMachineId(platform2 = process.platform) {
|
|
|
14278
14360
|
return /"IOPlatformUUID"\s*=\s*"([^"]+)"/.exec(out)?.[1];
|
|
14279
14361
|
}
|
|
14280
14362
|
for (const p of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
|
|
14281
|
-
if (
|
|
14282
|
-
const id =
|
|
14363
|
+
if (existsSync27(p)) {
|
|
14364
|
+
const id = readFileSync23(p, "utf8").trim();
|
|
14283
14365
|
if (id) return id;
|
|
14284
14366
|
}
|
|
14285
14367
|
}
|
|
@@ -14290,15 +14372,15 @@ function readRawMachineId(platform2 = process.platform) {
|
|
|
14290
14372
|
function ensureMachineId(stateDir, opts = {}) {
|
|
14291
14373
|
if (opts.override?.trim()) return sha256(opts.override.trim());
|
|
14292
14374
|
const path = machineIdPathFor(stateDir);
|
|
14293
|
-
if (
|
|
14294
|
-
const cached =
|
|
14375
|
+
if (existsSync27(path)) {
|
|
14376
|
+
const cached = readFileSync23(path, "utf8").trim();
|
|
14295
14377
|
if (/^[0-9a-f]{64}$/.test(cached)) return cached;
|
|
14296
14378
|
}
|
|
14297
14379
|
const raw = (opts.read ?? readRawMachineId)();
|
|
14298
14380
|
if (!raw) return void 0;
|
|
14299
14381
|
const hashed = sha256(raw);
|
|
14300
|
-
|
|
14301
|
-
|
|
14382
|
+
mkdirSync16(dirname13(path), { recursive: true });
|
|
14383
|
+
writeFileSync18(path, `${hashed}
|
|
14302
14384
|
`, { mode: 384 });
|
|
14303
14385
|
return hashed;
|
|
14304
14386
|
}
|
|
@@ -14369,34 +14451,34 @@ async function syncRouteName(i) {
|
|
|
14369
14451
|
return { status: "ok", ...base, action, name: i.local };
|
|
14370
14452
|
}
|
|
14371
14453
|
function readSyncedName(resolvedFile) {
|
|
14372
|
-
if (!
|
|
14454
|
+
if (!existsSync28(resolvedFile)) return void 0;
|
|
14373
14455
|
try {
|
|
14374
|
-
const v = JSON.parse(
|
|
14456
|
+
const v = JSON.parse(readFileSync24(resolvedFile, "utf8")).shipName;
|
|
14375
14457
|
return typeof v === "string" ? v : void 0;
|
|
14376
14458
|
} catch {
|
|
14377
14459
|
return void 0;
|
|
14378
14460
|
}
|
|
14379
14461
|
}
|
|
14380
14462
|
function writeSyncedName(resolvedFile, name) {
|
|
14381
|
-
if (!
|
|
14463
|
+
if (!existsSync28(resolvedFile)) return;
|
|
14382
14464
|
let raw;
|
|
14383
14465
|
try {
|
|
14384
|
-
raw = JSON.parse(
|
|
14466
|
+
raw = JSON.parse(readFileSync24(resolvedFile, "utf8"));
|
|
14385
14467
|
} catch {
|
|
14386
14468
|
return;
|
|
14387
14469
|
}
|
|
14388
14470
|
if (raw.shipName === name) return;
|
|
14389
14471
|
raw.shipName = name;
|
|
14390
|
-
|
|
14472
|
+
mkdirSync17(dirname14(resolvedFile), { recursive: true });
|
|
14391
14473
|
const tmp = `${resolvedFile}.tmp${process.pid}`;
|
|
14392
|
-
|
|
14474
|
+
writeFileSync19(tmp, `${JSON.stringify(raw, null, 2)}
|
|
14393
14475
|
`);
|
|
14394
14476
|
renameSync2(tmp, resolvedFile);
|
|
14395
14477
|
}
|
|
14396
14478
|
async function reconcileShipName(i) {
|
|
14397
14479
|
let local = i.ship.name;
|
|
14398
14480
|
try {
|
|
14399
|
-
local = shipNameOfYaml(
|
|
14481
|
+
local = shipNameOfYaml(readFileSync24(i.configFile, "utf8")) ?? local;
|
|
14400
14482
|
} catch {
|
|
14401
14483
|
}
|
|
14402
14484
|
const resolvedFile = resolvedPathFor(i.stateDir, i.route);
|
|
@@ -14420,8 +14502,8 @@ async function reconcileShipName(i) {
|
|
|
14420
14502
|
}
|
|
14421
14503
|
if (result.status !== "ok") return result;
|
|
14422
14504
|
if (result.action === "adopt" || result.action === "row-wins") {
|
|
14423
|
-
const text =
|
|
14424
|
-
|
|
14505
|
+
const text = readFileSync24(i.configFile, "utf8");
|
|
14506
|
+
writeFileSync19(i.configFile, setShipNameInYaml(text, result.name));
|
|
14425
14507
|
i.ship.name = result.name;
|
|
14426
14508
|
if (result.action === "row-wins") {
|
|
14427
14509
|
i.emit?.warn(
|
|
@@ -15071,7 +15153,7 @@ function describeCapability(i) {
|
|
|
15071
15153
|
}
|
|
15072
15154
|
|
|
15073
15155
|
// src/agents-sync-run.ts
|
|
15074
|
-
import { existsSync as
|
|
15156
|
+
import { existsSync as existsSync29, mkdirSync as mkdirSync18, readFileSync as readFileSync25, writeFileSync as writeFileSync20 } from "fs";
|
|
15075
15157
|
import { dirname as dirname15 } from "path";
|
|
15076
15158
|
async function runAgentsSync(target, o) {
|
|
15077
15159
|
const dryRun = o.dryRun ?? false;
|
|
@@ -15105,12 +15187,12 @@ async function runAgentsSync(target, o) {
|
|
|
15105
15187
|
for (const s of skillResult?.outcomes ?? []) o.say(describeSkillSyncOutcome(s));
|
|
15106
15188
|
if (!dryRun) {
|
|
15107
15189
|
const resolvedPath = resolvedPathFor(o.stateDir, target.route);
|
|
15108
|
-
const raw =
|
|
15190
|
+
const raw = existsSync29(resolvedPath) ? JSON.parse(readFileSync25(resolvedPath, "utf8")) : {};
|
|
15109
15191
|
raw.agentPersonas = personaResult.agentPersonas;
|
|
15110
15192
|
if (skillResult) raw.agentSkills = skillResult.agentSkills;
|
|
15111
15193
|
if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
|
|
15112
|
-
|
|
15113
|
-
|
|
15194
|
+
mkdirSync18(dirname15(resolvedPath), { recursive: true });
|
|
15195
|
+
writeFileSync20(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
15114
15196
|
`);
|
|
15115
15197
|
if (target.resolved) {
|
|
15116
15198
|
target.resolved.agentPersonas = personaResult.agentPersonas;
|
|
@@ -15214,7 +15296,7 @@ async function showLogEntry(route, entryId, opts = {}) {
|
|
|
15214
15296
|
|
|
15215
15297
|
// src/repos-check.ts
|
|
15216
15298
|
import { spawnSync as spawnSync6 } from "child_process";
|
|
15217
|
-
import { readFileSync as
|
|
15299
|
+
import { readFileSync as readFileSync26 } from "fs";
|
|
15218
15300
|
import { basename as basename8 } from "path";
|
|
15219
15301
|
var POSIX_SHELL = /^(?:ba|z|da|k|a|mk)?sh$|^fish$/;
|
|
15220
15302
|
function checkHookSyntax(script, shell) {
|
|
@@ -15255,7 +15337,7 @@ function checkRepoSpec(dir, opts = {}) {
|
|
|
15255
15337
|
}
|
|
15256
15338
|
let parsed;
|
|
15257
15339
|
try {
|
|
15258
|
-
parsed = parseRepoConfig(
|
|
15340
|
+
parsed = parseRepoConfig(readFileSync26(file, "utf8"), file);
|
|
15259
15341
|
} catch (e) {
|
|
15260
15342
|
if (!(e instanceof RepoConfigError)) throw e;
|
|
15261
15343
|
return { ...empty, file, error: e.message, exitCode: 1 };
|
|
@@ -15312,7 +15394,7 @@ import { isMap as isMap2, parseDocument as parseDocument3, YAMLMap } from "yaml"
|
|
|
15312
15394
|
|
|
15313
15395
|
// src/repos-cmd.ts
|
|
15314
15396
|
import { homedir as homedir9 } from "os";
|
|
15315
|
-
import { basename as basename9, join as
|
|
15397
|
+
import { basename as basename9, join as join31, resolve as resolve3 } from "path";
|
|
15316
15398
|
import { isMap, isSeq, parseDocument as parseDocument2, stringify as stringify2 } from "yaml";
|
|
15317
15399
|
var ReposError = class extends Error {
|
|
15318
15400
|
};
|
|
@@ -15590,7 +15672,7 @@ function planRepoClone(i) {
|
|
|
15590
15672
|
}
|
|
15591
15673
|
const ship = doc.get("ship", true);
|
|
15592
15674
|
const basePath = String(m.get("reposBasePath") ?? (isMap(ship) ? ship.get("reposBasePath") : void 0) ?? "~/Crew");
|
|
15593
|
-
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);
|
|
15594
15676
|
const add = planRepoAdd({ text: i.text, base: i.base, route: i.route, path: target, origin: remote, resolved: i.resolved, name, isCheckout: i.isCheckout });
|
|
15595
15677
|
return { ...add, remote, target };
|
|
15596
15678
|
}
|
|
@@ -15989,7 +16071,7 @@ ${run2.err}` });
|
|
|
15989
16071
|
}
|
|
15990
16072
|
|
|
15991
16073
|
// src/sample-ticket.ts
|
|
15992
|
-
import { basename as basename10, dirname as dirname17, join as
|
|
16074
|
+
import { basename as basename10, dirname as dirname17, join as join32 } from "path";
|
|
15993
16075
|
var SAMPLE_REPO = "crew-sample";
|
|
15994
16076
|
var SAMPLE_REMOTE = `local/${SAMPLE_REPO}`;
|
|
15995
16077
|
var SAMPLE_MARKER = "sample: true";
|
|
@@ -16012,8 +16094,8 @@ has something safe to work on: its test and build hooks always pass, it has no
|
|
|
16012
16094
|
remote, and \`crew sample-ticket --remove\` deletes it.
|
|
16013
16095
|
`;
|
|
16014
16096
|
function sampleDir(existing, reposBase, workspace) {
|
|
16015
|
-
if (existing.length > 0) return
|
|
16016
|
-
return
|
|
16097
|
+
if (existing.length > 0) return join32(dirname17(existing[0]), SAMPLE_REPO);
|
|
16098
|
+
return join32(reposBase, workspace, SAMPLE_REPO);
|
|
16017
16099
|
}
|
|
16018
16100
|
var SAMPLE_TITLE = "Sample: add a documented `greet` function with a test";
|
|
16019
16101
|
function sampleDescription() {
|
|
@@ -16105,8 +16187,8 @@ function schedulerInstalled(plan, exists, crontab) {
|
|
|
16105
16187
|
}
|
|
16106
16188
|
|
|
16107
16189
|
// src/worktree-sweep.ts
|
|
16108
|
-
import { existsSync as
|
|
16109
|
-
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";
|
|
16110
16192
|
function planWorktreeSweep(target, tickets, contract) {
|
|
16111
16193
|
const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
|
|
16112
16194
|
const verified = contract.statuses.verified;
|
|
@@ -16165,7 +16247,7 @@ function siblingWorktreeDirs(target) {
|
|
|
16165
16247
|
const registered = new Set(worktrees(target.dir).map((w) => resolve4(w.path)));
|
|
16166
16248
|
let names;
|
|
16167
16249
|
try {
|
|
16168
|
-
names =
|
|
16250
|
+
names = readdirSync12(parent);
|
|
16169
16251
|
} catch {
|
|
16170
16252
|
return [];
|
|
16171
16253
|
}
|
|
@@ -16173,7 +16255,7 @@ function siblingWorktreeDirs(target) {
|
|
|
16173
16255
|
for (const name of names) {
|
|
16174
16256
|
if (!name.startsWith(stem)) continue;
|
|
16175
16257
|
const m = DIR_NUMBER_SUFFIXED.exec(name);
|
|
16176
|
-
const path =
|
|
16258
|
+
const path = join33(parent, name);
|
|
16177
16259
|
if (m && !registered.has(resolve4(path))) out.push({ path, number: m[1] });
|
|
16178
16260
|
}
|
|
16179
16261
|
return out;
|
|
@@ -16208,7 +16290,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
|
|
|
16208
16290
|
let removed = 0;
|
|
16209
16291
|
let keptBranches = 0;
|
|
16210
16292
|
for (const a of actions) {
|
|
16211
|
-
if (!dryRun && base && !a.keepBranch &&
|
|
16293
|
+
if (!dryRun && base && !a.keepBranch && existsSync30(a.path) && isHandBackMerge(a.path, base)) {
|
|
16212
16294
|
if (gitOk(a.path, ["merge", "--abort"]) !== null) {
|
|
16213
16295
|
log.emit(
|
|
16214
16296
|
`abandoned the hand-back merge of ${base} in ${a.path}: ${a.ticket.issue_id} deployed without it`,
|
|
@@ -16216,7 +16298,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
|
|
|
16216
16298
|
);
|
|
16217
16299
|
}
|
|
16218
16300
|
}
|
|
16219
|
-
if (!dryRun &&
|
|
16301
|
+
if (!dryRun && existsSync30(a.path)) {
|
|
16220
16302
|
let dirty = false;
|
|
16221
16303
|
try {
|
|
16222
16304
|
dirty = status(a.path).length > 0;
|
|
@@ -16227,7 +16309,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
|
|
|
16227
16309
|
continue;
|
|
16228
16310
|
}
|
|
16229
16311
|
}
|
|
16230
|
-
if (!dryRun && a.requirePushed && a.branch &&
|
|
16312
|
+
if (!dryRun && a.requirePushed && a.branch && existsSync30(a.path) && remoteConfigured(cwd, remote) && worktreeHoldsUnpushed(a.path, remote)) {
|
|
16231
16313
|
refuse(a, "unpushed commits", `${a.branch} has commits that are not on ${remote}`);
|
|
16232
16314
|
continue;
|
|
16233
16315
|
}
|
|
@@ -16276,7 +16358,7 @@ function isHandBackMerge(path, base) {
|
|
|
16276
16358
|
if (!msgPath) return false;
|
|
16277
16359
|
let first;
|
|
16278
16360
|
try {
|
|
16279
|
-
first =
|
|
16361
|
+
first = readFileSync27(resolve4(path, msgPath), "utf8").split("\n")[0] ?? "";
|
|
16280
16362
|
} catch {
|
|
16281
16363
|
return false;
|
|
16282
16364
|
}
|
|
@@ -16381,14 +16463,14 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
|
|
|
16381
16463
|
}
|
|
16382
16464
|
|
|
16383
16465
|
// src/stream-sweep.ts
|
|
16384
|
-
import { existsSync as
|
|
16385
|
-
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";
|
|
16386
16468
|
var DAY_MS2 = 24 * 60 * 60 * 1e3;
|
|
16387
16469
|
function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
|
|
16388
|
-
const dir =
|
|
16389
|
-
if (!
|
|
16470
|
+
const dir = join34(stateDir, "streams");
|
|
16471
|
+
if (!existsSync31(dir)) return [];
|
|
16390
16472
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
16391
|
-
return
|
|
16473
|
+
return readdirSync13(dir).map((name) => join34(dir, name)).filter((path) => {
|
|
16392
16474
|
try {
|
|
16393
16475
|
return statSync8(path).mtimeMs < cutoff;
|
|
16394
16476
|
} catch {
|
|
@@ -16404,7 +16486,7 @@ function applyStreamSweep(paths, dryRun, log) {
|
|
|
16404
16486
|
continue;
|
|
16405
16487
|
}
|
|
16406
16488
|
try {
|
|
16407
|
-
|
|
16489
|
+
rmSync14(path, { force: true });
|
|
16408
16490
|
removed++;
|
|
16409
16491
|
log.emit(`removed stream artifact ${path}`, { step: "worktree" });
|
|
16410
16492
|
} catch (e) {
|
|
@@ -16414,9 +16496,9 @@ function applyStreamSweep(paths, dryRun, log) {
|
|
|
16414
16496
|
return removed;
|
|
16415
16497
|
}
|
|
16416
16498
|
function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
16417
|
-
if (!
|
|
16499
|
+
if (!existsSync31(stateDir)) return [];
|
|
16418
16500
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
16419
|
-
return
|
|
16501
|
+
return readdirSync13(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join34(stateDir, name)).filter((path) => {
|
|
16420
16502
|
try {
|
|
16421
16503
|
return statSync8(path).mtimeMs < cutoff;
|
|
16422
16504
|
} catch {
|
|
@@ -16426,7 +16508,7 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
|
16426
16508
|
}
|
|
16427
16509
|
|
|
16428
16510
|
// src/cli.ts
|
|
16429
|
-
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";
|
|
16430
16512
|
import { createInterface } from "readline/promises";
|
|
16431
16513
|
|
|
16432
16514
|
// src/periscope-cmd.ts
|
|
@@ -16554,6 +16636,25 @@ ${tail(preview.output)}`);
|
|
|
16554
16636
|
return abandon(`recording the Periscopes row failed: ${e.message}`);
|
|
16555
16637
|
}
|
|
16556
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
|
+
}
|
|
16557
16658
|
await d.comment(draft.id, progressComment(url, handoff?.output)).catch((e) => {
|
|
16558
16659
|
d.say("warning", `progress comment not posted: ${e.message}`);
|
|
16559
16660
|
});
|
|
@@ -16590,6 +16691,13 @@ function prototypeSection(files) {
|
|
|
16590
16691
|
`${files.length} file${files.length === 1 ? "" : "s"} changed, ${added} insertion${added === 1 ? "" : "s"}(+), ${removed} deletion${removed === 1 ? "" : "s"}(\u2212)`
|
|
16591
16692
|
].join("\n");
|
|
16592
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
|
+
}
|
|
16593
16701
|
async function runPeriscopeDown(d, args) {
|
|
16594
16702
|
const rows = await d.rowsForTicket(args.number);
|
|
16595
16703
|
if (rows.length === 0) throw new PeriscopeError(`no periscope for ticket ${args.number} on this ship`);
|
|
@@ -16612,6 +16720,7 @@ async function runPeriscopeDown(d, args) {
|
|
|
16612
16720
|
d.say("ticket", `${row.ticket} closed as obsolete`);
|
|
16613
16721
|
await d.setRowState(row.id, "discarded");
|
|
16614
16722
|
d.say("row", "row is discarded");
|
|
16723
|
+
forgetLocal(d, row.ticketRecordId);
|
|
16615
16724
|
return { ticket: row.ticket, mode: "discard", outcome: "done", state: "discarded" };
|
|
16616
16725
|
}
|
|
16617
16726
|
const dirty = d.hasChanges(row.worktree);
|
|
@@ -16642,6 +16751,7 @@ ${section}`;
|
|
|
16642
16751
|
d.say("worktree", `removed ${row.worktree}; ${row.branch} stays, pushed`);
|
|
16643
16752
|
await d.setRowState(row.id, "kept");
|
|
16644
16753
|
d.say("row", "row is kept");
|
|
16754
|
+
forgetLocal(d, row.ticketRecordId);
|
|
16645
16755
|
return { ticket: row.ticket, mode: "keep", outcome: "done", state: "kept", sha: pushed.sha };
|
|
16646
16756
|
}
|
|
16647
16757
|
function periscopeListEntries(rows, now, isListening) {
|
|
@@ -16664,22 +16774,22 @@ function formatPeriscopeList(entries) {
|
|
|
16664
16774
|
}
|
|
16665
16775
|
|
|
16666
16776
|
// src/cli.ts
|
|
16667
|
-
import { dirname as
|
|
16777
|
+
import { dirname as dirOf2, resolve as resolvePath } from "path";
|
|
16668
16778
|
|
|
16669
16779
|
// src/ssh-keys.ts
|
|
16670
16780
|
import { execFileSync as execFileSync6 } from "child_process";
|
|
16671
|
-
import { existsSync as
|
|
16672
|
-
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";
|
|
16673
16783
|
var SshKeyError = class extends Error {
|
|
16674
16784
|
};
|
|
16675
16785
|
function shipSshKeyPathFor(stateDir) {
|
|
16676
|
-
return
|
|
16786
|
+
return join35(stateDir, "ssh", "id_ed25519");
|
|
16677
16787
|
}
|
|
16678
16788
|
function ensureShipSshKeypair(stateDir) {
|
|
16679
16789
|
const privateKeyPath = shipSshKeyPathFor(stateDir);
|
|
16680
16790
|
const publicKeyPath = `${privateKeyPath}.pub`;
|
|
16681
|
-
if (!
|
|
16682
|
-
|
|
16791
|
+
if (!existsSync32(privateKeyPath)) {
|
|
16792
|
+
mkdirSync19(dirname19(privateKeyPath), { recursive: true });
|
|
16683
16793
|
try {
|
|
16684
16794
|
execFileSync6("ssh-keygen", [
|
|
16685
16795
|
"-t",
|
|
@@ -16699,10 +16809,10 @@ function ensureShipSshKeypair(stateDir) {
|
|
|
16699
16809
|
}
|
|
16700
16810
|
chmodSync(privateKeyPath, 384);
|
|
16701
16811
|
}
|
|
16702
|
-
if (!
|
|
16812
|
+
if (!existsSync32(publicKeyPath)) {
|
|
16703
16813
|
throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
|
|
16704
16814
|
}
|
|
16705
|
-
const publicKey =
|
|
16815
|
+
const publicKey = readFileSync28(publicKeyPath, "utf8").trim();
|
|
16706
16816
|
return { privateKeyPath, publicKeyPath, publicKey };
|
|
16707
16817
|
}
|
|
16708
16818
|
function sshKeygenAvailable() {
|
|
@@ -16919,9 +17029,9 @@ function syncAllPassengerCheckouts(cfg) {
|
|
|
16919
17029
|
|
|
16920
17030
|
// src/tunnel.ts
|
|
16921
17031
|
import { spawn as spawn5 } from "child_process";
|
|
16922
|
-
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";
|
|
16923
17033
|
import { randomInt } from "crypto";
|
|
16924
|
-
import { join as
|
|
17034
|
+
import { join as join36 } from "path";
|
|
16925
17035
|
var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
|
|
16926
17036
|
var defaultSpawn = (cmd, args) => spawn5(cmd, args, DEFAULT_SPAWN_OPTIONS);
|
|
16927
17037
|
var SLUG_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
@@ -16988,21 +17098,21 @@ function startTunnel(opts) {
|
|
|
16988
17098
|
};
|
|
16989
17099
|
}
|
|
16990
17100
|
function tunnelStatePath(stateDir, workspaceId) {
|
|
16991
|
-
return
|
|
17101
|
+
return join36(stateDir, "passengers", `${workspaceId}.json`);
|
|
16992
17102
|
}
|
|
16993
17103
|
function readPersistedTunnel(stateDir, workspaceId) {
|
|
16994
17104
|
const path = tunnelStatePath(stateDir, workspaceId);
|
|
16995
|
-
if (!
|
|
17105
|
+
if (!existsSync33(path)) return void 0;
|
|
16996
17106
|
try {
|
|
16997
|
-
return JSON.parse(
|
|
17107
|
+
return JSON.parse(readFileSync29(path, "utf8"));
|
|
16998
17108
|
} catch {
|
|
16999
17109
|
return void 0;
|
|
17000
17110
|
}
|
|
17001
17111
|
}
|
|
17002
17112
|
function writePersistedTunnel(stateDir, state) {
|
|
17003
17113
|
const path = tunnelStatePath(stateDir, state.workspaceId);
|
|
17004
|
-
|
|
17005
|
-
|
|
17114
|
+
mkdirSync20(join36(stateDir, "passengers"), { recursive: true });
|
|
17115
|
+
writeFileSync21(path, `${JSON.stringify(state, null, 2)}
|
|
17006
17116
|
`);
|
|
17007
17117
|
}
|
|
17008
17118
|
function updatePersistedTunnel(stateDir, workspaceId, patch2) {
|
|
@@ -17099,9 +17209,9 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
|
|
|
17099
17209
|
}
|
|
17100
17210
|
}
|
|
17101
17211
|
function readAllPersistedWorkspaceIds(stateDir) {
|
|
17102
|
-
const dir =
|
|
17103
|
-
if (!
|
|
17104
|
-
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));
|
|
17105
17215
|
}
|
|
17106
17216
|
|
|
17107
17217
|
// src/mcp-endpoint-registry.ts
|
|
@@ -17221,8 +17331,8 @@ async function unregisterEndpoint(workspaceId, info, d) {
|
|
|
17221
17331
|
import { createServer as createHttpServer } from "http";
|
|
17222
17332
|
import { spawn as spawn6 } from "child_process";
|
|
17223
17333
|
import { randomBytes } from "crypto";
|
|
17224
|
-
import { existsSync as
|
|
17225
|
-
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";
|
|
17226
17336
|
|
|
17227
17337
|
// src/secret-compare.ts
|
|
17228
17338
|
import { timingSafeEqual } from "crypto";
|
|
@@ -17311,21 +17421,21 @@ var defaultSpawn2 = (cmd, args, env) => {
|
|
|
17311
17421
|
return child;
|
|
17312
17422
|
};
|
|
17313
17423
|
function syncDaemonStatePath(stateDir, workspaceId) {
|
|
17314
|
-
return
|
|
17424
|
+
return join37(stateDir, "passengers", `${workspaceId}-sync.json`);
|
|
17315
17425
|
}
|
|
17316
17426
|
function readPersistedSyncDaemon(stateDir, workspaceId) {
|
|
17317
17427
|
const path = syncDaemonStatePath(stateDir, workspaceId);
|
|
17318
|
-
if (!
|
|
17428
|
+
if (!existsSync34(path)) return void 0;
|
|
17319
17429
|
try {
|
|
17320
|
-
return JSON.parse(
|
|
17430
|
+
return JSON.parse(readFileSync30(path, "utf8"));
|
|
17321
17431
|
} catch {
|
|
17322
17432
|
return void 0;
|
|
17323
17433
|
}
|
|
17324
17434
|
}
|
|
17325
17435
|
function writePersistedSyncDaemon(stateDir, state) {
|
|
17326
17436
|
const path = syncDaemonStatePath(stateDir, state.workspaceId);
|
|
17327
|
-
|
|
17328
|
-
|
|
17437
|
+
mkdirSync21(join37(stateDir, "passengers"), { recursive: true });
|
|
17438
|
+
writeFileSync22(path, `${JSON.stringify(state, null, 2)}
|
|
17329
17439
|
`);
|
|
17330
17440
|
}
|
|
17331
17441
|
function removePersistedSyncDaemon(stateDir, workspaceId) {
|
|
@@ -17345,10 +17455,10 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
|
|
|
17345
17455
|
}
|
|
17346
17456
|
}
|
|
17347
17457
|
function readAllPersistedSyncWorkspaceIds(stateDir) {
|
|
17348
|
-
const dir =
|
|
17349
|
-
if (!
|
|
17458
|
+
const dir = join37(stateDir, "passengers");
|
|
17459
|
+
if (!existsSync34(dir)) return [];
|
|
17350
17460
|
const suffix = "-sync.json";
|
|
17351
|
-
return
|
|
17461
|
+
return readdirSync15(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
|
|
17352
17462
|
}
|
|
17353
17463
|
function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
|
|
17354
17464
|
const alive = deps.isPidAlive ?? isPidAlive;
|
|
@@ -17434,8 +17544,8 @@ function describeCause(err) {
|
|
|
17434
17544
|
}
|
|
17435
17545
|
|
|
17436
17546
|
// src/daemon.ts
|
|
17437
|
-
import { existsSync as
|
|
17438
|
-
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";
|
|
17439
17549
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
17440
17550
|
var DEFAULT_FLOOR_MS = 5e3;
|
|
17441
17551
|
var DEFAULT_CEILING_MS = INTERVAL_SECONDS * 1e3;
|
|
@@ -17505,8 +17615,8 @@ function latestMtime(path) {
|
|
|
17505
17615
|
if (stat.isFile()) return stat.mtimeMs;
|
|
17506
17616
|
if (!stat.isDirectory()) return 0;
|
|
17507
17617
|
let latest = stat.mtimeMs;
|
|
17508
|
-
for (const entry of
|
|
17509
|
-
latest = Math.max(latest, latestMtime(
|
|
17618
|
+
for (const entry of readdirSync16(path, { withFileTypes: true })) {
|
|
17619
|
+
latest = Math.max(latest, latestMtime(join38(path, entry.name)));
|
|
17510
17620
|
}
|
|
17511
17621
|
return latest;
|
|
17512
17622
|
}
|
|
@@ -17515,23 +17625,23 @@ function computeTreeSignature(paths) {
|
|
|
17515
17625
|
}
|
|
17516
17626
|
function makeStaleChecker(paths, opts = {}) {
|
|
17517
17627
|
const initial = computeTreeSignature(paths);
|
|
17518
|
-
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 &&
|
|
17628
|
+
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync35(opts.reloadSentinel);
|
|
17519
17629
|
}
|
|
17520
17630
|
function reloadSentinelFor(stateDir) {
|
|
17521
|
-
return
|
|
17631
|
+
return join38(stateDir, "reload-requested");
|
|
17522
17632
|
}
|
|
17523
17633
|
function daemonStampFor(stateDir) {
|
|
17524
|
-
return
|
|
17634
|
+
return join38(stateDir, "daemon.started");
|
|
17525
17635
|
}
|
|
17526
17636
|
function noteDaemonStarted(stateDir, now = Date.now()) {
|
|
17527
|
-
|
|
17528
|
-
|
|
17529
|
-
|
|
17637
|
+
mkdirSync22(stateDir, { recursive: true });
|
|
17638
|
+
rmSync15(reloadSentinelFor(stateDir), { force: true });
|
|
17639
|
+
writeFileSync23(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
|
|
17530
17640
|
`);
|
|
17531
17641
|
}
|
|
17532
17642
|
function readDaemonStarted(stateDir) {
|
|
17533
17643
|
try {
|
|
17534
|
-
const v = JSON.parse(
|
|
17644
|
+
const v = JSON.parse(readFileSync31(daemonStampFor(stateDir), "utf8")).startedAt;
|
|
17535
17645
|
return typeof v === "number" ? v : null;
|
|
17536
17646
|
} catch {
|
|
17537
17647
|
return null;
|
|
@@ -18403,7 +18513,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
18403
18513
|
} else {
|
|
18404
18514
|
const toleratesNoRoutes = command === "repos" || command === "ship" || command === "disconnect" && flag("reset") || command === "connect" && flag("sign-out");
|
|
18405
18515
|
const configless = command === "connect" && flag("sign-out") || command === "disconnect" && flag("reset");
|
|
18406
|
-
if (configless && !
|
|
18516
|
+
if (configless && !existsSync36(findConfigFile(CREW_HOME))) {
|
|
18407
18517
|
cfg = loadConfig(CREW_HOME, firstRunConfigPath(), { text: "ship: {}\n", allowNoRoutes: true });
|
|
18408
18518
|
} else {
|
|
18409
18519
|
cfg = loadConfig(CREW_HOME, void 0, toleratesNoRoutes ? { allowNoRoutes: true } : {});
|
|
@@ -18641,8 +18751,8 @@ if (command === "passenger-sync-daemon") {
|
|
|
18641
18751
|
}
|
|
18642
18752
|
if (host === "linux" && hasSystemd) {
|
|
18643
18753
|
const label = labelFor(CREW_HOME, "run");
|
|
18644
|
-
const staleTimer =
|
|
18645
|
-
if (
|
|
18754
|
+
const staleTimer = join39(homedir10(), ".config", "systemd", "user", `${label}.timer`);
|
|
18755
|
+
if (existsSync36(staleTimer)) {
|
|
18646
18756
|
process.stdout.write(
|
|
18647
18757
|
`removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
|
|
18648
18758
|
`
|
|
@@ -18669,7 +18779,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
18669
18779
|
const tracker = new Tracker(c, cfg.ship);
|
|
18670
18780
|
problems.push(...validateContract(tracker.contract).map((p) => `contract: ${p}`));
|
|
18671
18781
|
for (const r of await resolvedRepos(c)) {
|
|
18672
|
-
if (!
|
|
18782
|
+
if (!existsSync36(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
|
|
18673
18783
|
if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
|
|
18674
18784
|
readiness.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
|
|
18675
18785
|
}
|
|
@@ -18687,7 +18797,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
18687
18797
|
}
|
|
18688
18798
|
async function doctorFix(o) {
|
|
18689
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))));
|
|
18690
|
-
const unitExists = (p) =>
|
|
18800
|
+
const unitExists = (p) => existsSync36(p);
|
|
18691
18801
|
const crontab = o.host === "linux" ? spawnSync7("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
|
|
18692
18802
|
const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
|
|
18693
18803
|
const plan = planFix({ routes, schedulerAbsent, host: o.host });
|
|
@@ -18704,7 +18814,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
18704
18814
|
for (const a of plan.actions) {
|
|
18705
18815
|
if (a.kind === "enable") {
|
|
18706
18816
|
if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
|
|
18707
|
-
|
|
18817
|
+
writeFileSync24(cfg.configFile, planEnable(readFileSync32(cfg.configFile, "utf8"), a.route));
|
|
18708
18818
|
enabledSound.add(a.route);
|
|
18709
18819
|
changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
|
|
18710
18820
|
} else {
|
|
@@ -19808,7 +19918,7 @@ ${cu.tail.trimEnd()}
|
|
|
19808
19918
|
process.exit(1);
|
|
19809
19919
|
}
|
|
19810
19920
|
requireArmed("run the daemon loop");
|
|
19811
|
-
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")];
|
|
19812
19922
|
const configPaths = daemonConfigPaths(cfg);
|
|
19813
19923
|
watchPaths.push(...configPaths);
|
|
19814
19924
|
noteDaemonStarted(cfg.ship.stateDir);
|
|
@@ -20037,10 +20147,10 @@ ${cu.tail.trimEnd()}
|
|
|
20037
20147
|
`);
|
|
20038
20148
|
if (!dryRun) {
|
|
20039
20149
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
20040
|
-
const raw =
|
|
20150
|
+
const raw = existsSync36(resolvedPath) ? JSON.parse(readFileSync32(resolvedPath, "utf8")) : {};
|
|
20041
20151
|
raw.agentSkills = result.agentSkills;
|
|
20042
|
-
|
|
20043
|
-
|
|
20152
|
+
mkdirSync23(dirOf2(resolvedPath), { recursive: true });
|
|
20153
|
+
writeFileSync24(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
20044
20154
|
`);
|
|
20045
20155
|
}
|
|
20046
20156
|
if (result.outcomes.some((o) => o.action === "diverged")) process.exitCode = 1;
|
|
@@ -20063,7 +20173,7 @@ ${cu.tail.trimEnd()}
|
|
|
20063
20173
|
throw e;
|
|
20064
20174
|
}
|
|
20065
20175
|
const oldName = cfg.ship.name;
|
|
20066
|
-
|
|
20176
|
+
writeFileSync24(cfg.configFile, setShipNameInYaml(readFileSync32(cfg.configFile, "utf8"), newName));
|
|
20067
20177
|
cfg.ship.name = newName;
|
|
20068
20178
|
let renameMachineId;
|
|
20069
20179
|
try {
|
|
@@ -20110,7 +20220,7 @@ ${cu.tail.trimEnd()}
|
|
|
20110
20220
|
process.exit(2);
|
|
20111
20221
|
};
|
|
20112
20222
|
if (sub !== "add" && sub !== "list" && sub !== "clone" && sub !== "hooks" && sub !== "check") usage2();
|
|
20113
|
-
const configText =
|
|
20223
|
+
const configText = readFileSync32(cfg.configFile, "utf8");
|
|
20114
20224
|
if (sub === "check") {
|
|
20115
20225
|
try {
|
|
20116
20226
|
const routeArg = value("route") ?? positional[2];
|
|
@@ -20141,16 +20251,16 @@ ${cu.tail.trimEnd()}
|
|
|
20141
20251
|
try {
|
|
20142
20252
|
const a = parseHooksArgs(positional.slice(2), routeNamesOf(configText));
|
|
20143
20253
|
const dir = repoDirOf(configText, dirname20(cfg.configFile), a.route, a.name);
|
|
20144
|
-
if (!
|
|
20145
|
-
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) => {
|
|
20146
20256
|
try {
|
|
20147
|
-
return
|
|
20257
|
+
return readdirSync17(resolvePath(dir, r));
|
|
20148
20258
|
} catch {
|
|
20149
20259
|
return [];
|
|
20150
20260
|
}
|
|
20151
20261
|
}, read: (r) => {
|
|
20152
20262
|
try {
|
|
20153
|
-
return
|
|
20263
|
+
return readFileSync32(resolvePath(dir, r), "utf8");
|
|
20154
20264
|
} catch {
|
|
20155
20265
|
return void 0;
|
|
20156
20266
|
}
|
|
@@ -20192,19 +20302,19 @@ ${cu.tail.trimEnd()}
|
|
|
20192
20302
|
let cleared;
|
|
20193
20303
|
if (clear2) {
|
|
20194
20304
|
const plan = clearRouteHooks(configText, a.route, a.name);
|
|
20195
|
-
if (plan.removed.length > 0)
|
|
20305
|
+
if (plan.removed.length > 0) writeFileSync24(cfg.configFile, plan.text);
|
|
20196
20306
|
cleared = { file: cfg.configFile, removed: plan.removed };
|
|
20197
20307
|
}
|
|
20198
20308
|
if (write && suggestion.source === null && recipeIds.length === 0 && hookSpecs.length === 0) throw new ReposError(`nothing to write: ${suggestion.notes[0]}`);
|
|
20199
20309
|
if (write === "route") {
|
|
20200
20310
|
const plan = planRouteHooks(configText, a.route, a.name, picked.hooks, { replace: flag("replace") });
|
|
20201
|
-
|
|
20311
|
+
writeFileSync24(cfg.configFile, plan.text);
|
|
20202
20312
|
written = { target: "route", file: cfg.configFile, hooks: plan.written, kept: plan.kept, replaced: plan.replaced };
|
|
20203
20313
|
} else if (write === "repo") {
|
|
20204
20314
|
const have = existingRepoConfig(files);
|
|
20205
20315
|
if (have) throw new ReposError(`${resolvePath(dir, have)} already exists; not overwriting it`);
|
|
20206
20316
|
const file = resolvePath(dir, ".crew.yaml");
|
|
20207
|
-
|
|
20317
|
+
writeFileSync24(file, starterRepoConfig(picked.hooks, picked.release), { flag: "wx" });
|
|
20208
20318
|
written = { target: "repo", file, hooks: Object.keys(picked.hooks), kept: [], replaced: [] };
|
|
20209
20319
|
}
|
|
20210
20320
|
if (flag("json")) {
|
|
@@ -20260,7 +20370,7 @@ ${cu.tail.trimEnd()}
|
|
|
20260
20370
|
if (sub === "list" && !routeName) usage2();
|
|
20261
20371
|
if (sub === "add") {
|
|
20262
20372
|
try {
|
|
20263
|
-
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) =>
|
|
20373
|
+
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync36(resolvePath(p.replace(/^~(?=\/|$)/, homedir10()))));
|
|
20264
20374
|
routeName = a.route;
|
|
20265
20375
|
addPath = a.path;
|
|
20266
20376
|
addName = a.name;
|
|
@@ -20289,16 +20399,16 @@ ${cu.tail.trimEnd()}
|
|
|
20289
20399
|
}
|
|
20290
20400
|
const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
|
|
20291
20401
|
let resolvedRemotes;
|
|
20292
|
-
if (
|
|
20402
|
+
if (existsSync36(resolvedFile)) {
|
|
20293
20403
|
try {
|
|
20294
|
-
resolvedRemotes = JSON.parse(
|
|
20404
|
+
resolvedRemotes = JSON.parse(readFileSync32(resolvedFile, "utf8"));
|
|
20295
20405
|
} catch {
|
|
20296
20406
|
}
|
|
20297
20407
|
}
|
|
20298
20408
|
const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
|
|
20299
20409
|
const text = configText;
|
|
20300
20410
|
const base = dirname20(cfg.configFile);
|
|
20301
|
-
const isCheckout = (d) =>
|
|
20411
|
+
const isCheckout = (d) => existsSync36(`${d}/.git`);
|
|
20302
20412
|
try {
|
|
20303
20413
|
if (sub === "list") {
|
|
20304
20414
|
if (flag("json")) {
|
|
@@ -20308,11 +20418,11 @@ ${cu.tail.trimEnd()}
|
|
|
20308
20418
|
base,
|
|
20309
20419
|
resolved: resolvedRemotes,
|
|
20310
20420
|
originOf,
|
|
20311
|
-
exists: (d) =>
|
|
20421
|
+
exists: (d) => existsSync36(d),
|
|
20312
20422
|
isCheckout,
|
|
20313
20423
|
repoHasHooks: (d) => REPO_CONFIG_FILENAMES.some((f) => {
|
|
20314
20424
|
try {
|
|
20315
|
-
return Object.keys(parseRepoConfig(
|
|
20425
|
+
return Object.keys(parseRepoConfig(readFileSync32(resolvePath(d, f), "utf8"), f).hooks).length > 0;
|
|
20316
20426
|
} catch {
|
|
20317
20427
|
return false;
|
|
20318
20428
|
}
|
|
@@ -20329,7 +20439,7 @@ ${cu.tail.trimEnd()}
|
|
|
20329
20439
|
for (const r of repos) {
|
|
20330
20440
|
const origin2 = originOf(r.dir);
|
|
20331
20441
|
const row = matchRepoName(origin2, resolvedRemotes);
|
|
20332
|
-
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)"}`;
|
|
20333
20443
|
process.stdout.write(`${r.name} ${r.dir} ${note}
|
|
20334
20444
|
`);
|
|
20335
20445
|
}
|
|
@@ -20341,9 +20451,9 @@ ${cu.tail.trimEnd()}
|
|
|
20341
20451
|
` : `${human}
|
|
20342
20452
|
`);
|
|
20343
20453
|
const plan2 = planRepoClone({ text, base, route: routeName, name: addName, resolved: resolvedRemotes, dir: value("dir"), isCheckout });
|
|
20344
|
-
if (
|
|
20454
|
+
if (existsSync36(plan2.target) && (existsSync36(`${plan2.target}/.git`) || readdirSync17(plan2.target).length > 0)) {
|
|
20345
20455
|
throw new ReposError(
|
|
20346
|
-
|
|
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`
|
|
20347
20457
|
);
|
|
20348
20458
|
}
|
|
20349
20459
|
if (dryRun) {
|
|
@@ -20361,14 +20471,14 @@ ${cu.tail.trimEnd()}
|
|
|
20361
20471
|
throw new ReposError(`${e.message}${authHint}
|
|
20362
20472
|
crew.yaml was not changed`);
|
|
20363
20473
|
}
|
|
20364
|
-
|
|
20474
|
+
writeFileSync24(cfg.configFile, plan2.text);
|
|
20365
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:
|
|
20366
20476
|
`);
|
|
20367
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}`);
|
|
20368
20478
|
break;
|
|
20369
20479
|
}
|
|
20370
20480
|
const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir10()));
|
|
20371
|
-
const top =
|
|
20481
|
+
const top = existsSync36(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
|
|
20372
20482
|
if (!top) throw new ReposError(`${target} is not a git checkout`);
|
|
20373
20483
|
const origin = originOf(top);
|
|
20374
20484
|
const plan = planRepoAdd({
|
|
@@ -20420,15 +20530,15 @@ ${cu.tail.trimEnd()}
|
|
|
20420
20530
|
const created = await rowTracker.createRepoRow(rowPayload);
|
|
20421
20531
|
process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
|
|
20422
20532
|
`);
|
|
20423
|
-
if (
|
|
20424
|
-
const raw = JSON.parse(
|
|
20533
|
+
if (existsSync36(resolvedFile)) {
|
|
20534
|
+
const raw = JSON.parse(readFileSync32(resolvedFile, "utf8"));
|
|
20425
20535
|
raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
|
|
20426
20536
|
raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
|
|
20427
|
-
|
|
20537
|
+
writeFileSync24(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
20428
20538
|
`);
|
|
20429
20539
|
}
|
|
20430
20540
|
}
|
|
20431
|
-
|
|
20541
|
+
writeFileSync24(cfg.configFile, plan.text);
|
|
20432
20542
|
process.stdout.write(`added ${top} to ${routeName} as ${plan.mode === "dir" ? "dir" : `repos.${plan.name}`} in ${cfg.configFile}
|
|
20433
20543
|
`);
|
|
20434
20544
|
} catch (e) {
|
|
@@ -20795,7 +20905,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
20795
20905
|
process.exit(2);
|
|
20796
20906
|
}
|
|
20797
20907
|
if (found.shipNameAdopted && !dryRun) {
|
|
20798
|
-
|
|
20908
|
+
writeFileSync24(cfg.configFile, setShipNameInYaml(readFileSync32(cfg.configFile, "utf8"), found.shipNameAdopted));
|
|
20799
20909
|
cfg.ship.name = found.shipNameAdopted;
|
|
20800
20910
|
}
|
|
20801
20911
|
if (found.shipRow) {
|
|
@@ -20838,10 +20948,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
20838
20948
|
}
|
|
20839
20949
|
}
|
|
20840
20950
|
let previous;
|
|
20841
|
-
const hadPreviousFile =
|
|
20951
|
+
const hadPreviousFile = existsSync36(resolvedPath);
|
|
20842
20952
|
if (hadPreviousFile) {
|
|
20843
20953
|
try {
|
|
20844
|
-
previous = JSON.parse(
|
|
20954
|
+
previous = JSON.parse(readFileSync32(resolvedPath, "utf8"));
|
|
20845
20955
|
} catch {
|
|
20846
20956
|
}
|
|
20847
20957
|
}
|
|
@@ -20970,10 +21080,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
20970
21080
|
process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
|
|
20971
21081
|
`);
|
|
20972
21082
|
} else {
|
|
20973
|
-
|
|
21083
|
+
mkdirSync23(dirname20(resolvedPath), { recursive: true });
|
|
20974
21084
|
if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
|
|
20975
21085
|
const tmpPath = `${resolvedPath}.tmp${process.pid}`;
|
|
20976
|
-
|
|
21086
|
+
writeFileSync24(tmpPath, `${JSON.stringify(resolved, null, 2)}
|
|
20977
21087
|
`);
|
|
20978
21088
|
renameSync3(tmpPath, resolvedPath);
|
|
20979
21089
|
process.stderr.write(`Resolved ids written to ${resolvedPath}
|
|
@@ -21019,8 +21129,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
21019
21129
|
process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
|
|
21020
21130
|
`);
|
|
21021
21131
|
} else {
|
|
21022
|
-
|
|
21023
|
-
|
|
21132
|
+
mkdirSync23(dirname20(apiKeyFile), { recursive: true });
|
|
21133
|
+
writeFileSync24(apiKeyFile, `${apiKeyVar}=${apiKey}
|
|
21024
21134
|
`, { mode: 384 });
|
|
21025
21135
|
process.stderr.write(`API key written to ${apiKeyFile}
|
|
21026
21136
|
`);
|
|
@@ -21669,6 +21779,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
21669
21779
|
if (deleteBranch) gitOk(repoD.dir, ["branch", "-D", branch]);
|
|
21670
21780
|
},
|
|
21671
21781
|
setRowState: (id, state2) => trD.updatePeriscope(id, { state: state2 }),
|
|
21782
|
+
forgetLocal: (id) => removePeriscopeSidecar(cfg.ship.stateDir, id),
|
|
21672
21783
|
say
|
|
21673
21784
|
};
|
|
21674
21785
|
if (dryRun) {
|
|
@@ -21753,10 +21864,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
21753
21864
|
},
|
|
21754
21865
|
prepare: async (path) => {
|
|
21755
21866
|
for (const f of repoP.config.worktrees.copy) {
|
|
21756
|
-
const src =
|
|
21757
|
-
if (!
|
|
21758
|
-
|
|
21759
|
-
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));
|
|
21760
21871
|
}
|
|
21761
21872
|
const setup = repoP.config.hooks.setup;
|
|
21762
21873
|
return setup ? runScript(setup, { cwd: path, shell: shellP, onLine: (l) => {
|
|
@@ -21768,6 +21879,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
21768
21879
|
return script ? runScript(script, { cwd, env, shell: shellP, timeoutMs: name === "preview" ? 18e4 : void 0 }) : null;
|
|
21769
21880
|
},
|
|
21770
21881
|
createRow: (fields) => trackerP.createPeriscope(fields),
|
|
21882
|
+
recordLocal: (sc) => writePeriscopeSidecar(cfg.ship.stateDir, sc),
|
|
21771
21883
|
comment: (ticketId, body) => trackerP.postComment(ticketId, body, memberId),
|
|
21772
21884
|
teardown: async (path, branch) => {
|
|
21773
21885
|
const n = Number(path.match(/(\d+)\/?$/)?.[1]);
|
|
@@ -21979,7 +22091,7 @@ ${res.url}
|
|
|
21979
22091
|
};
|
|
21980
22092
|
const droppedRoutes = [];
|
|
21981
22093
|
try {
|
|
21982
|
-
const rawCfg = parseYaml2(
|
|
22094
|
+
const rawCfg = parseYaml2(readFileSync32(cfg.configFile, "utf8"));
|
|
21983
22095
|
for (const r of rawCfg?.routes ?? []) {
|
|
21984
22096
|
if (typeof r?.route !== "string" || cfg.routes.some((x) => x.route === r.route)) continue;
|
|
21985
22097
|
try {
|
|
@@ -22021,8 +22133,8 @@ ${res.url}
|
|
|
22021
22133
|
process.stdout.write("crew reload: no daemon unit is running \u2014 nothing to reload (the next run reads crew.yaml fresh)\n");
|
|
22022
22134
|
break;
|
|
22023
22135
|
}
|
|
22024
|
-
|
|
22025
|
-
|
|
22136
|
+
mkdirSync23(cfg.ship.stateDir, { recursive: true });
|
|
22137
|
+
writeFileSync24(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
|
|
22026
22138
|
`);
|
|
22027
22139
|
process.stdout.write("crew reload: requested \u2014 the daemon finishes its in-flight pass, then relaunches with the new config\n");
|
|
22028
22140
|
if (flag("now") && control?.stopCommand && control.startCommand) {
|
|
@@ -22073,7 +22185,7 @@ ${res.url}
|
|
|
22073
22185
|
}
|
|
22074
22186
|
case "log": {
|
|
22075
22187
|
try {
|
|
22076
|
-
const text =
|
|
22188
|
+
const text = readFileSync32(cfg.ship.logFile, "utf8").split("\n");
|
|
22077
22189
|
process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
|
|
22078
22190
|
`);
|
|
22079
22191
|
} catch {
|
|
@@ -22096,7 +22208,7 @@ ${res.url}
|
|
|
22096
22208
|
};
|
|
22097
22209
|
if (!route.resolved) fail(`route ${route.route} is not connected \u2014 run \`crew connect\` first`);
|
|
22098
22210
|
const tracker = new Tracker(route, cfg.ship);
|
|
22099
|
-
const configText =
|
|
22211
|
+
const configText = readFileSync32(cfg.configFile, "utf8");
|
|
22100
22212
|
const cfgBase = dirname20(cfg.configFile);
|
|
22101
22213
|
const configured = configuredRepos(configText, route.route, cfgBase);
|
|
22102
22214
|
const dir = configured.find((r) => r.name === SAMPLE_REPO)?.dir ?? sampleDir(configured.map((r) => r.dir), route.reposBasePath, route.route.split("/")[0]);
|
|
@@ -22125,7 +22237,7 @@ ${res.url}
|
|
|
22125
22237
|
}
|
|
22126
22238
|
if (configured.some((r) => r.dir === dir)) {
|
|
22127
22239
|
try {
|
|
22128
|
-
|
|
22240
|
+
writeFileSync24(cfg.configFile, planRepoRemove(configText, cfgBase, route.route, dir).text);
|
|
22129
22241
|
} catch (e) {
|
|
22130
22242
|
if (e instanceof ReposError) fail(e.message);
|
|
22131
22243
|
throw e;
|
|
@@ -22133,29 +22245,29 @@ ${res.url}
|
|
|
22133
22245
|
}
|
|
22134
22246
|
if (row) {
|
|
22135
22247
|
await tracker.deleteRepoRow(row.id);
|
|
22136
|
-
if (
|
|
22137
|
-
const raw = JSON.parse(
|
|
22248
|
+
if (existsSync36(resolvedFile)) {
|
|
22249
|
+
const raw = JSON.parse(readFileSync32(resolvedFile, "utf8"));
|
|
22138
22250
|
if (raw.repoNames) delete raw.repoNames[row.id];
|
|
22139
22251
|
if (raw.repoRemotes) delete raw.repoRemotes[row.id];
|
|
22140
|
-
|
|
22252
|
+
writeFileSync24(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
22141
22253
|
`);
|
|
22142
22254
|
}
|
|
22143
22255
|
}
|
|
22144
|
-
if (
|
|
22256
|
+
if (existsSync36(join39(dir, ".git")) && basename12(dir) === SAMPLE_REPO) rmSync16(dir, { recursive: true, force: true });
|
|
22145
22257
|
say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
|
|
22146
22258
|
break;
|
|
22147
22259
|
}
|
|
22148
22260
|
const projectId = route.resolved.areaId ?? resolveRepoProject(await tracker.projectRows(), value("project"));
|
|
22149
|
-
const fresh = !
|
|
22150
|
-
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`);
|
|
22151
22263
|
if (dryRun) {
|
|
22152
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`);
|
|
22153
22265
|
break;
|
|
22154
22266
|
}
|
|
22155
22267
|
if (fresh) {
|
|
22156
|
-
|
|
22157
|
-
|
|
22158
|
-
|
|
22268
|
+
mkdirSync23(dir, { recursive: true });
|
|
22269
|
+
writeFileSync24(join39(dir, "README.md"), SAMPLE_README);
|
|
22270
|
+
writeFileSync24(join39(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
|
|
22159
22271
|
const g = (...a) => {
|
|
22160
22272
|
const r = spawnSync7("git", a, { cwd: dir, encoding: "utf8" });
|
|
22161
22273
|
if (r.status !== 0) fail(`git ${a[0]} failed in ${dir}: ${(r.stderr || r.stdout).trim()}`);
|
|
@@ -22173,17 +22285,17 @@ ${res.url}
|
|
|
22173
22285
|
spec: { platform: "unix", releaseMode: "integrate", ciProvider: "none" }
|
|
22174
22286
|
});
|
|
22175
22287
|
repoId = (await tracker.createRepoRow(payload)).id;
|
|
22176
|
-
if (
|
|
22177
|
-
const raw = JSON.parse(
|
|
22288
|
+
if (existsSync36(resolvedFile)) {
|
|
22289
|
+
const raw = JSON.parse(readFileSync32(resolvedFile, "utf8"));
|
|
22178
22290
|
raw.repoNames = { ...raw.repoNames, [repoId]: SAMPLE_REPO };
|
|
22179
22291
|
raw.repoRemotes = { ...raw.repoRemotes, [repoId]: SAMPLE_REMOTE };
|
|
22180
|
-
|
|
22292
|
+
writeFileSync24(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
22181
22293
|
`);
|
|
22182
22294
|
}
|
|
22183
22295
|
}
|
|
22184
22296
|
if (!configured.some((r) => r.dir === dir)) {
|
|
22185
22297
|
try {
|
|
22186
|
-
|
|
22298
|
+
writeFileSync24(cfg.configFile, planRepoAdd({ text: configText, base: cfgBase, route: route.route, path: dir, name: SAMPLE_REPO }).text);
|
|
22187
22299
|
} catch (e) {
|
|
22188
22300
|
if (e instanceof ReposError) fail(e.message);
|
|
22189
22301
|
throw e;
|
|
@@ -22305,6 +22417,7 @@ ${res.url}
|
|
|
22305
22417
|
const projects = projectModeLabels(await statusTracker.projectRowsWithModes().catch(() => []));
|
|
22306
22418
|
const crewMinVersion = await readCrewFloor(route, cfg.ship) ?? null;
|
|
22307
22419
|
const runs = liveRuns(cfg.ship.stateDir);
|
|
22420
|
+
const periscopes = await periscopesForStatus(cfg.ship.stateDir);
|
|
22308
22421
|
if (flag("json")) {
|
|
22309
22422
|
const nameSync = await reconcileRouteShipName({ route, ship: cfg.ship, configFile: cfg.configFile, readOnly: true });
|
|
22310
22423
|
const repos = await resolvedRepos(route);
|
|
@@ -22369,6 +22482,8 @@ ${res.url}
|
|
|
22369
22482
|
},
|
|
22370
22483
|
// Additive (CREW-2822): every live run on this ship. `version` stays 1.
|
|
22371
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,
|
|
22372
22487
|
watermark: state.watermark(),
|
|
22373
22488
|
route: {
|
|
22374
22489
|
name: route.route,
|
|
@@ -22443,7 +22558,8 @@ ${owed.map((l) => ` ${l}
|
|
|
22443
22558
|
route: ${route.route} -> ${route.dir}
|
|
22444
22559
|
enabled: ${route.enabled}
|
|
22445
22560
|
periscopes: ${periscopeSummary(cfg)}
|
|
22446
|
-
|
|
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"}
|
|
22447
22563
|
release: ${state.releaseSkipReason() ? "paused" : "active"}
|
|
22448
22564
|
watermark: ${state.watermark()}
|
|
22449
22565
|
`
|
|
@@ -22514,7 +22630,7 @@ watermark: ${state.watermark()}
|
|
|
22514
22630
|
}
|
|
22515
22631
|
for (const r of repos) {
|
|
22516
22632
|
const remote = r.config.branch.remote;
|
|
22517
|
-
if (
|
|
22633
|
+
if (existsSync36(r.dir)) {
|
|
22518
22634
|
pushAuth.push({
|
|
22519
22635
|
route: c.route,
|
|
22520
22636
|
repo: r.name,
|
|
@@ -22561,7 +22677,12 @@ watermark: ${state.watermark()}
|
|
|
22561
22677
|
}
|
|
22562
22678
|
for (const message of roleCapWarnings(cfg.ship)) problems.push({ severity: "note", kind: "role_cap_above_ship_cap", message });
|
|
22563
22679
|
const previewProblem2 = await previewHostProblem(cfg);
|
|
22564
|
-
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
|
+
}
|
|
22565
22686
|
problems.push(...attentionProblems(openShipAttention(state), problems));
|
|
22566
22687
|
if (problems.some((p) => p.severity === "blocker")) process.exitCode = 1;
|
|
22567
22688
|
process.stdout.write(`${JSON.stringify({ offline: flag("offline"), pushAuth, tools: toolRows, problems, passed }, null, 2)}
|
|
@@ -22579,13 +22700,14 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
22579
22700
|
`);
|
|
22580
22701
|
const previewProblem = await previewHostProblem(cfg);
|
|
22581
22702
|
if (previewProblem) {
|
|
22582
|
-
|
|
22703
|
+
const blocks = previewHostFinding(previewProblem).severity === "blocker";
|
|
22704
|
+
process.stdout.write(` ${blocks ? "BLOCKER" : "ATTENTION"} ${previewProblem}
|
|
22583
22705
|
`);
|
|
22584
|
-
process.exitCode = 1;
|
|
22706
|
+
if (blocks) process.exitCode = 1;
|
|
22585
22707
|
}
|
|
22586
22708
|
try {
|
|
22587
22709
|
const evFile = eventFileFor(cfg.ship.stateDir);
|
|
22588
|
-
const evSize =
|
|
22710
|
+
const evSize = existsSync36(evFile) ? statSync10(evFile).size : 0;
|
|
22589
22711
|
process.stdout.write(
|
|
22590
22712
|
evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
|
|
22591
22713
|
` : "events log: ok\n"
|
|
@@ -22596,7 +22718,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
22596
22718
|
const unitPlan = planInstall(cfg.ship, CREW_HOME, host, detectSystemd(), "run");
|
|
22597
22719
|
const abandons = unitAbandonsAgents(unitPlan, (p) => {
|
|
22598
22720
|
try {
|
|
22599
|
-
return
|
|
22721
|
+
return readFileSync32(p, "utf8");
|
|
22600
22722
|
} catch {
|
|
22601
22723
|
return null;
|
|
22602
22724
|
}
|
|
@@ -22687,6 +22809,18 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
22687
22809
|
`
|
|
22688
22810
|
);
|
|
22689
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
|
+
}
|
|
22690
22824
|
process.stdout.write(
|
|
22691
22825
|
!periscopes.present ? "periscopes: Periscopes table absent \u2014 this workspace cannot host periscopes\n" : periscopes.missing.length === 0 ? `periscopes: Periscopes table present (${periscopes.modelId})
|
|
22692
22826
|
` : `periscopes: Periscopes table present (${periscopes.modelId}) but missing ${periscopes.missing.join(", ")} \u2014 update the Issues template
|