@tablation/crew 0.25.0 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/crew.example.yaml +6 -0
- package/dist/cli.js +352 -196
- package/package.json +1 -1
package/crew.example.yaml
CHANGED
|
@@ -76,6 +76,12 @@ routes:
|
|
|
76
76
|
|
|
77
77
|
worktreePrefix: "myproject-issue-"
|
|
78
78
|
|
|
79
|
+
# A repo may also be written as an object to name its worktree directory
|
|
80
|
+
# for this ship only (CREW-1504) — a template over {dir} {issue} {number}
|
|
81
|
+
# {key} {slug} {tag} {prefix} {role}. Outranks the repo's own
|
|
82
|
+
# `worktrees.name`, which outranks worktreePrefix; see docs/REPO_SPEC.md.
|
|
83
|
+
# My Product: { dir: "/home/me/src/my-product", worktreeName: "{dir}-{issue}" }
|
|
84
|
+
|
|
79
85
|
# Opt this route's ship into Host Passengers for this route's workspace
|
|
80
86
|
# (ISSUE-644): `crew connect` syncs this onto that workspace's own Ships
|
|
81
87
|
# row. Config-truth lives here, on the machine — never edit a Ships row
|
package/dist/cli.js
CHANGED
|
@@ -1,16 +1,163 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { resolve as resolve4, dirname as dirname11, join as join20, basename as
|
|
4
|
+
import { resolve as resolve4, dirname as dirname11, join as join20, basename as basename8 } from "path";
|
|
5
5
|
import { fileURLToPath } from "url";
|
|
6
6
|
import { hostname as hostname2, homedir as homedir5 } from "os";
|
|
7
7
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
8
8
|
|
|
9
9
|
// src/config.ts
|
|
10
|
-
import { readFileSync as
|
|
11
|
-
import { resolve, dirname, isAbsolute, join as
|
|
10
|
+
import { readFileSync as readFileSync3, existsSync as existsSync3 } from "fs";
|
|
11
|
+
import { resolve, dirname as dirname2, isAbsolute, join as join3, basename as basename2 } from "path";
|
|
12
12
|
import { homedir, tmpdir } from "os";
|
|
13
13
|
import { parse as parse2 } from "yaml";
|
|
14
|
+
|
|
15
|
+
// src/events.ts
|
|
16
|
+
import {
|
|
17
|
+
appendFileSync,
|
|
18
|
+
mkdirSync,
|
|
19
|
+
existsSync,
|
|
20
|
+
statSync,
|
|
21
|
+
renameSync,
|
|
22
|
+
openSync,
|
|
23
|
+
closeSync,
|
|
24
|
+
rmSync
|
|
25
|
+
} from "fs";
|
|
26
|
+
import { join, dirname } from "path";
|
|
27
|
+
function render(e) {
|
|
28
|
+
const scope = e.role ? `${e.step}[${e.role}]` : e.step;
|
|
29
|
+
const ticket = e.ticket ? ` ${e.ticket}` : "";
|
|
30
|
+
const level = e.level === "info" ? "" : `${e.level.toUpperCase()}: `;
|
|
31
|
+
return `${e.at} ${scope}:${ticket} ${level}${e.message}`;
|
|
32
|
+
}
|
|
33
|
+
var DEFAULT_EVENTS_ROTATE_BYTES = 64 * 1024 * 1024;
|
|
34
|
+
var ROTATED_EVENTS_RE = /^events\.\d{8}T\d{6}Z(?:-\d+)?\.jsonl$/;
|
|
35
|
+
var ROTATE_LOCK_STALE_MS = 6e4;
|
|
36
|
+
function rotateEventsFile(file, maxBytes, now = /* @__PURE__ */ new Date()) {
|
|
37
|
+
try {
|
|
38
|
+
if (!existsSync(file) || statSync(file).size <= maxBytes) return void 0;
|
|
39
|
+
} catch {
|
|
40
|
+
return void 0;
|
|
41
|
+
}
|
|
42
|
+
const lock = `${file}.rotate.lock`;
|
|
43
|
+
try {
|
|
44
|
+
try {
|
|
45
|
+
closeSync(openSync(lock, "wx"));
|
|
46
|
+
} catch {
|
|
47
|
+
try {
|
|
48
|
+
if (Date.now() - statSync(lock).mtimeMs < ROTATE_LOCK_STALE_MS) return void 0;
|
|
49
|
+
rmSync(lock, { force: true });
|
|
50
|
+
closeSync(openSync(lock, "wx"));
|
|
51
|
+
} catch {
|
|
52
|
+
return void 0;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
if (statSync(file).size <= maxBytes) return void 0;
|
|
57
|
+
const stamp = now.toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
|
|
58
|
+
let dest = join(dirname(file), `events.${stamp}.jsonl`);
|
|
59
|
+
for (let n = 1; existsSync(dest); n++) dest = join(dirname(file), `events.${stamp}-${n}.jsonl`);
|
|
60
|
+
renameSync(file, dest);
|
|
61
|
+
return dest;
|
|
62
|
+
} finally {
|
|
63
|
+
rmSync(lock, { force: true });
|
|
64
|
+
}
|
|
65
|
+
} catch {
|
|
66
|
+
return void 0;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
var Emitter = class _Emitter {
|
|
70
|
+
cycle;
|
|
71
|
+
opts;
|
|
72
|
+
now;
|
|
73
|
+
step = "poll";
|
|
74
|
+
role;
|
|
75
|
+
constructor(opts) {
|
|
76
|
+
this.opts = opts;
|
|
77
|
+
this.now = opts.now ?? (() => /* @__PURE__ */ new Date());
|
|
78
|
+
this.cycle = opts.cycleId ?? this.now().toISOString().replace(/[-:.TZ]/g, "").slice(0, 14);
|
|
79
|
+
for (const f of [opts.eventFile, opts.logFile]) {
|
|
80
|
+
if (!f) continue;
|
|
81
|
+
try {
|
|
82
|
+
mkdirSync(dirname(f), { recursive: true });
|
|
83
|
+
} catch {
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/** Everything emitted after this belongs to `step`, until the next one. */
|
|
88
|
+
enter(step, role) {
|
|
89
|
+
this.step = step;
|
|
90
|
+
this.role = role;
|
|
91
|
+
}
|
|
92
|
+
emit(message, extra = {}) {
|
|
93
|
+
const e = {
|
|
94
|
+
at: this.now().toISOString(),
|
|
95
|
+
cycle: this.cycle,
|
|
96
|
+
route: this.opts.route,
|
|
97
|
+
step: extra.step ?? this.step,
|
|
98
|
+
level: extra.level ?? "info",
|
|
99
|
+
message,
|
|
100
|
+
...extra.role ?? this.role ? { role: extra.role ?? this.role } : {},
|
|
101
|
+
...extra.ticket ? { ticket: extra.ticket } : {},
|
|
102
|
+
...extra.data ? { data: extra.data } : {}
|
|
103
|
+
};
|
|
104
|
+
const line = render(e);
|
|
105
|
+
try {
|
|
106
|
+
if (this.opts.eventFile && this.opts.rotateBytes) rotateEventsFile(this.opts.eventFile, this.opts.rotateBytes, this.now());
|
|
107
|
+
} catch {
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
if (this.opts.eventFile) appendFileSync(this.opts.eventFile, `${JSON.stringify(e)}
|
|
111
|
+
`);
|
|
112
|
+
} catch {
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
if (this.opts.logFile) appendFileSync(this.opts.logFile, `${line}
|
|
116
|
+
`);
|
|
117
|
+
} catch {
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
(this.opts.console ?? ((l) => process.stderr.write(`${l}
|
|
121
|
+
`)))(line);
|
|
122
|
+
} catch {
|
|
123
|
+
}
|
|
124
|
+
return e;
|
|
125
|
+
}
|
|
126
|
+
warn(message, extra = {}) {
|
|
127
|
+
return this.emit(message, { ...extra, level: "warn" });
|
|
128
|
+
}
|
|
129
|
+
error(message, extra = {}) {
|
|
130
|
+
return this.emit(message, { ...extra, level: "error" });
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* A view of this emitter labelling every event with a different route,
|
|
134
|
+
* sharing the same sinks and cycle id. For a fleet-wide run, the top-level
|
|
135
|
+
* Emitter is stamped with just one route (see cli.ts) — a phase that
|
|
136
|
+
* actually knows which route it's working (release, per-repo poll) uses
|
|
137
|
+
* this instead, so its events aren't all attributed to `routes[0]`.
|
|
138
|
+
*/
|
|
139
|
+
forRoute(route) {
|
|
140
|
+
return new _Emitter({ ...this.opts, route, cycleId: this.cycle });
|
|
141
|
+
}
|
|
142
|
+
/** Times a step and emits its outcome, so a view can show duration. */
|
|
143
|
+
async timed(step, message, fn, role) {
|
|
144
|
+
this.enter(step, role);
|
|
145
|
+
const started = this.now().getTime();
|
|
146
|
+
try {
|
|
147
|
+
const result = await fn();
|
|
148
|
+
this.emit(message, { data: { ms: this.now().getTime() - started, ok: true } });
|
|
149
|
+
return result;
|
|
150
|
+
} catch (err) {
|
|
151
|
+
this.error(`${message} \u2014 ${err.message}`, {
|
|
152
|
+
data: { ms: this.now().getTime() - started, ok: false }
|
|
153
|
+
});
|
|
154
|
+
throw err;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
var eventFileFor = (stateDir) => join(stateDir, "events.jsonl");
|
|
159
|
+
|
|
160
|
+
// src/config.ts
|
|
14
161
|
import { getSessionStore, hostFromUrl } from "@tablation/client";
|
|
15
162
|
|
|
16
163
|
// src/platform.ts
|
|
@@ -50,8 +197,8 @@ function explain(ship, required) {
|
|
|
50
197
|
}
|
|
51
198
|
|
|
52
199
|
// src/repo-config.ts
|
|
53
|
-
import { readFileSync, existsSync } from "fs";
|
|
54
|
-
import { join, basename } from "path";
|
|
200
|
+
import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
|
|
201
|
+
import { join as join2, basename } from "path";
|
|
55
202
|
import { parse } from "yaml";
|
|
56
203
|
var SPEC_VERSION = 1;
|
|
57
204
|
var REPO_CONFIG_FILENAMES = [".crew.yaml", ".crew.yml", ".crew.json"];
|
|
@@ -84,6 +231,27 @@ var TOP_LEVEL = /* @__PURE__ */ new Set([
|
|
|
84
231
|
]);
|
|
85
232
|
var BRANCH_KEYS = /* @__PURE__ */ new Set(["base", "name", "push", "remote"]);
|
|
86
233
|
var PLACEHOLDER = /\{(key|number|slug|role|prefix|tag)\}/g;
|
|
234
|
+
var WORKTREE_PLACEHOLDERS = ["dir", "issue", "number", "key", "slug", "role", "prefix", "tag"];
|
|
235
|
+
function worktreeNameProblem(template) {
|
|
236
|
+
const valid = `Valid placeholders: ${WORKTREE_PLACEHOLDERS.map((p) => `{${p}}`).join(" ")}.`;
|
|
237
|
+
if (template.trim() === "") return `must be a non-empty string. ${valid}`;
|
|
238
|
+
if (/[/\\]/.test(template)) {
|
|
239
|
+
return "must not contain a path separator \u2014 a worktree is cut beside the checkout, not inside a directory of its own";
|
|
240
|
+
}
|
|
241
|
+
const stripped = template.replace(/\{([^{}]*)\}/g, (_m, name) => {
|
|
242
|
+
if (!WORKTREE_PLACEHOLDERS.includes(name)) return "\0";
|
|
243
|
+
return "";
|
|
244
|
+
});
|
|
245
|
+
if (stripped.includes("\0")) {
|
|
246
|
+
const bad = [...template.matchAll(/\{([^{}]*)\}/g)].map((m) => m[1] ?? "").filter((n) => !WORKTREE_PLACEHOLDERS.includes(n));
|
|
247
|
+
return `names unknown placeholder ${bad.map((b) => `{${b}}`).join(", ")}. ${valid}`;
|
|
248
|
+
}
|
|
249
|
+
if (/[{}]/.test(stripped)) return `has an unbalanced brace. ${valid}`;
|
|
250
|
+
if (!/\{(issue|number|key|slug|tag)\}/.test(template)) {
|
|
251
|
+
return "names no per-ticket placeholder \u2014 every ticket would get the same worktree. Use at least one of {issue} {number} {key} {slug} {tag}.";
|
|
252
|
+
}
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
87
255
|
var RELEASE_KEYS = /* @__PURE__ */ new Set([
|
|
88
256
|
"mode",
|
|
89
257
|
"ship",
|
|
@@ -103,8 +271,8 @@ var MODES = ["local", "integrate", "ci_manual", "ci_auto", "external"];
|
|
|
103
271
|
var PROVIDERS = ["github", "buildkite", "other", "none"];
|
|
104
272
|
function findRepoConfig(dir) {
|
|
105
273
|
for (const name of REPO_CONFIG_FILENAMES) {
|
|
106
|
-
const p =
|
|
107
|
-
if (
|
|
274
|
+
const p = join2(dir, name);
|
|
275
|
+
if (existsSync2(p)) return p;
|
|
108
276
|
}
|
|
109
277
|
return null;
|
|
110
278
|
}
|
|
@@ -132,7 +300,7 @@ function parseRepoConfig(text, file) {
|
|
|
132
300
|
rejectUnknown(raw.release?.ci, CI_KEYS, "release.ci", file);
|
|
133
301
|
rejectUnknown(raw.release?.verify, VERIFY_KEYS, "release.verify", file);
|
|
134
302
|
rejectUnknown(raw.branch, BRANCH_KEYS, "branch", file);
|
|
135
|
-
rejectUnknown(raw.worktrees, /* @__PURE__ */ new Set(["copy", "prefix"]), "worktrees", file);
|
|
303
|
+
rejectUnknown(raw.worktrees, /* @__PURE__ */ new Set(["copy", "prefix", "name"]), "worktrees", file);
|
|
136
304
|
rejectUnknown(raw.docs, /* @__PURE__ */ new Set(["triagePolicy", "designGuide"]), "docs", file);
|
|
137
305
|
if (raw.version === void 0) throw new RepoConfigError(`${file}: version is required`);
|
|
138
306
|
if (raw.version !== SPEC_VERSION) {
|
|
@@ -167,7 +335,13 @@ function parseRepoConfig(text, file) {
|
|
|
167
335
|
`${file}: worktrees.prefix ("${prefixRaw}") must not contain a path separator \u2014 a worktree is cut beside the checkout, not inside a directory of its own`
|
|
168
336
|
);
|
|
169
337
|
}
|
|
338
|
+
const nameRaw = raw.worktrees?.name;
|
|
339
|
+
if (nameRaw !== void 0) {
|
|
340
|
+
const problem = typeof nameRaw === "string" ? worktreeNameProblem(nameRaw) : "must be a string";
|
|
341
|
+
if (problem) throw new RepoConfigError(`${file}: worktrees.name ("${String(nameRaw)}") ${problem}`);
|
|
342
|
+
}
|
|
170
343
|
const worktrees2 = {
|
|
344
|
+
...nameRaw !== void 0 ? { name: String(nameRaw) } : {},
|
|
171
345
|
copy: (copyRaw ?? []).map((c) => String(c)),
|
|
172
346
|
// Empty means "not declared" here; the effective config fills it in, since
|
|
173
347
|
// only that layer knows the ship's setting and the checkout's directory.
|
|
@@ -328,7 +502,7 @@ function parseRepoConfig(text, file) {
|
|
|
328
502
|
function loadRepoConfig(dir) {
|
|
329
503
|
const file = findRepoConfig(dir);
|
|
330
504
|
if (!file) return null;
|
|
331
|
-
return parseRepoConfig(
|
|
505
|
+
return parseRepoConfig(readFileSync2(file, "utf8"), file);
|
|
332
506
|
}
|
|
333
507
|
function hookLabel(cfg, hook2) {
|
|
334
508
|
return cfg.labels[hook2] ?? cfg.hooks[hook2] ?? hook2;
|
|
@@ -358,6 +532,10 @@ function renderBranchName(template, ctx) {
|
|
|
358
532
|
}
|
|
359
533
|
});
|
|
360
534
|
}
|
|
535
|
+
function renderWorktreeName(template, ctx) {
|
|
536
|
+
const issue = (ctx.tag ?? ctx.key).toLowerCase();
|
|
537
|
+
return renderBranchName(template.replace(/\{dir\}/g, ctx.dir).replace(/\{issue\}/g, issue), ctx);
|
|
538
|
+
}
|
|
361
539
|
function defaultWorktreePrefix(dir) {
|
|
362
540
|
const name = basename(dir.replace(/[/\\]+$/, ""));
|
|
363
541
|
return name ? `${name}-issue-` : "issue-";
|
|
@@ -389,6 +567,10 @@ function resolveRepoConfig(repo, ship, dir) {
|
|
|
389
567
|
provenance[key] = "default";
|
|
390
568
|
return fallback;
|
|
391
569
|
};
|
|
570
|
+
if (ship?.worktrees?.nameOverride) {
|
|
571
|
+
if (repo?.worktrees.name) shadowed.push("worktrees.name");
|
|
572
|
+
provenance["worktrees.name"] = "ship";
|
|
573
|
+
} else if (repo?.worktrees.name) provenance["worktrees.name"] = "repo";
|
|
392
574
|
const hooks = {};
|
|
393
575
|
const labels = {};
|
|
394
576
|
for (const h of HOOK_NAMES) {
|
|
@@ -424,7 +606,8 @@ function resolveRepoConfig(repo, ship, dir) {
|
|
|
424
606
|
repo?.worktrees.prefix || void 0,
|
|
425
607
|
ship?.worktrees?.prefix,
|
|
426
608
|
defaultWorktreePrefix(dir)
|
|
427
|
-
)
|
|
609
|
+
),
|
|
610
|
+
...ship?.worktrees?.nameOverride || repo?.worktrees.name ? { name: ship?.worktrees?.nameOverride || repo?.worktrees.name } : {}
|
|
428
611
|
},
|
|
429
612
|
docs: repo?.docs ?? {},
|
|
430
613
|
branch: {
|
|
@@ -525,7 +708,8 @@ function effectiveBranchTemplate(cfg, prefix) {
|
|
|
525
708
|
if (cfg.provenance["branch.name"] !== "default") return cfg.branch.name;
|
|
526
709
|
return prefix ? "{prefix}-{number}" : "issue-{number}";
|
|
527
710
|
}
|
|
528
|
-
function effectiveWorktreeDirName(cfg, dir, branchName, number) {
|
|
711
|
+
function effectiveWorktreeDirName(cfg, dir, branchName, number, renderName) {
|
|
712
|
+
if (cfg.worktrees.name && renderName) return renderName(cfg.worktrees.name);
|
|
529
713
|
if (cfg.provenance["worktrees.prefix"] !== "default") return `${cfg.worktrees.prefix}${number}`;
|
|
530
714
|
return `${basename(dir.replace(/[/\\]+$/, ""))}-${branchName}`;
|
|
531
715
|
}
|
|
@@ -594,6 +778,7 @@ var SHIP_KEYS = /* @__PURE__ */ new Set([
|
|
|
594
778
|
"apiKey",
|
|
595
779
|
"maxConcurrentAgents",
|
|
596
780
|
"streamRetentionDays",
|
|
781
|
+
"eventsRotateBytes",
|
|
597
782
|
"reposBasePath",
|
|
598
783
|
"relayHost",
|
|
599
784
|
"relayPort",
|
|
@@ -623,7 +808,12 @@ var ROUTE_KEYS = /* @__PURE__ */ new Set([
|
|
|
623
808
|
"promptSet",
|
|
624
809
|
"hostPassengers"
|
|
625
810
|
]);
|
|
626
|
-
|
|
811
|
+
function parseWorktreeName(v, where, file) {
|
|
812
|
+
const problem = typeof v === "string" ? worktreeNameProblem(v) : "must be a string";
|
|
813
|
+
if (problem) throw new ConfigError(`${file}: ${where}.worktreeName ("${String(v)}") ${problem}`);
|
|
814
|
+
return String(v);
|
|
815
|
+
}
|
|
816
|
+
var REPO_ENTRY_KEYS = /* @__PURE__ */ new Set(["dir", "hooks", "labels", "release", "branch", "hostPassengers", "worktreeName"]);
|
|
627
817
|
var BRANCH_OVERRIDE_KEYS = /* @__PURE__ */ new Set(["base", "name", "push", "remote"]);
|
|
628
818
|
var RELEASE_OVERRIDE_KEYS = /* @__PURE__ */ new Set([
|
|
629
819
|
"mode",
|
|
@@ -725,11 +915,11 @@ function expand(p, base) {
|
|
|
725
915
|
return isAbsolute(out) ? out : resolve(base, out);
|
|
726
916
|
}
|
|
727
917
|
function resolvePromptsDir(promptSet, crewHome, base) {
|
|
728
|
-
if (!promptSet) return
|
|
918
|
+
if (!promptSet) return join3(crewHome, "prompts", "default");
|
|
729
919
|
if (promptSet.includes("/") || promptSet.startsWith(".") || promptSet.startsWith("~")) {
|
|
730
920
|
return expand(promptSet, base);
|
|
731
921
|
}
|
|
732
|
-
return
|
|
922
|
+
return join3(crewHome, "prompts", promptSet);
|
|
733
923
|
}
|
|
734
924
|
var Missing = class {
|
|
735
925
|
paths = [];
|
|
@@ -753,7 +943,7 @@ var Missing = class {
|
|
|
753
943
|
};
|
|
754
944
|
function crewVersion(crewHome) {
|
|
755
945
|
try {
|
|
756
|
-
const pkg = JSON.parse(
|
|
946
|
+
const pkg = JSON.parse(readFileSync3(join3(crewHome, "package.json"), "utf8"));
|
|
757
947
|
return pkg.version ?? "0.0.0";
|
|
758
948
|
} catch {
|
|
759
949
|
return "0.0.0";
|
|
@@ -763,26 +953,26 @@ function configSearchPath(crewHome) {
|
|
|
763
953
|
if (process.env.CREW_CONFIG) return [resolve(process.env.CREW_CONFIG)];
|
|
764
954
|
const xdg = process.env.XDG_CONFIG_HOME;
|
|
765
955
|
return [
|
|
766
|
-
...xdg ? [
|
|
767
|
-
|
|
956
|
+
...xdg ? [join3(xdg, "crew", "crew.yaml")] : [],
|
|
957
|
+
join3(homedir(), ".config", "crew", "crew.yaml"),
|
|
768
958
|
resolve(crewHome, "crew.yaml")
|
|
769
959
|
];
|
|
770
960
|
}
|
|
771
961
|
function findConfigFile(crewHome) {
|
|
772
962
|
const candidates = configSearchPath(crewHome);
|
|
773
|
-
return candidates.find((c) =>
|
|
963
|
+
return candidates.find((c) => existsSync3(c)) ?? candidates[candidates.length - 1];
|
|
774
964
|
}
|
|
775
|
-
var defaultConfigPath = () =>
|
|
965
|
+
var defaultConfigPath = () => join3(process.env.XDG_CONFIG_HOME ?? join3(homedir(), ".config"), "crew", "crew.yaml");
|
|
776
966
|
function routeSlug(route) {
|
|
777
967
|
return route.replace(/\//g, "-");
|
|
778
968
|
}
|
|
779
969
|
function resolvedPathFor(stateDir, route) {
|
|
780
970
|
const [workspace, project] = splitRoute(route);
|
|
781
|
-
return
|
|
971
|
+
return join3(stateDir, "resolved", workspace, `${project}.json`);
|
|
782
972
|
}
|
|
783
973
|
function apiKeyPathFor(stateDir, route) {
|
|
784
974
|
const [workspace, project] = splitRoute(route);
|
|
785
|
-
return
|
|
975
|
+
return join3(stateDir, "keys", workspace, `${project}.env`);
|
|
786
976
|
}
|
|
787
977
|
function splitRoute(route) {
|
|
788
978
|
const parts = route.split("/");
|
|
@@ -793,9 +983,9 @@ function splitRoute(route) {
|
|
|
793
983
|
}
|
|
794
984
|
function readResolvedFile(stateDir, route) {
|
|
795
985
|
const path = resolvedPathFor(stateDir, route);
|
|
796
|
-
if (!
|
|
986
|
+
if (!existsSync3(path)) return void 0;
|
|
797
987
|
try {
|
|
798
|
-
return JSON.parse(
|
|
988
|
+
return JSON.parse(readFileSync3(path, "utf8"));
|
|
799
989
|
} catch (e) {
|
|
800
990
|
throw new ConfigError(`${path}: not valid JSON \u2014 ${e.message}`);
|
|
801
991
|
}
|
|
@@ -851,23 +1041,23 @@ function parseResolved(raw, m, where) {
|
|
|
851
1041
|
}
|
|
852
1042
|
function loadConfig(crewHome, configFile, opts = {}) {
|
|
853
1043
|
const file = configFile ?? findConfigFile(crewHome);
|
|
854
|
-
if (opts.text === void 0 && !
|
|
1044
|
+
if (opts.text === void 0 && !existsSync3(file)) {
|
|
855
1045
|
throw new ConfigError(
|
|
856
1046
|
`no config found. Looked in:
|
|
857
1047
|
` + configSearchPath(crewHome).map((c) => ` - ${c}`).join("\n") + `
|
|
858
1048
|
|
|
859
1049
|
Create the first of those:
|
|
860
|
-
mkdir -p ${
|
|
1050
|
+
mkdir -p ${dirname2(defaultConfigPath())} && cp ${join3(crewHome, "crew.example.yaml")} ${defaultConfigPath()}`
|
|
861
1051
|
);
|
|
862
1052
|
}
|
|
863
1053
|
let raw;
|
|
864
1054
|
try {
|
|
865
|
-
raw = parse2(opts.text ??
|
|
1055
|
+
raw = parse2(opts.text ?? readFileSync3(file, "utf8"));
|
|
866
1056
|
} catch (e) {
|
|
867
1057
|
throw new ConfigError(`${file}: not valid YAML \u2014 ${e.message}`);
|
|
868
1058
|
}
|
|
869
1059
|
if (!raw || typeof raw !== "object") throw new ConfigError(`${file} is empty or not a mapping`);
|
|
870
|
-
const base =
|
|
1060
|
+
const base = dirname2(file);
|
|
871
1061
|
const missing = new Missing();
|
|
872
1062
|
rejectUnknownKeys(raw, /* @__PURE__ */ new Set(["ship", "routes"]), "top-level", file);
|
|
873
1063
|
const shipRaw = raw.ship ?? {};
|
|
@@ -884,6 +1074,10 @@ Create the first of those:
|
|
|
884
1074
|
if (!Number.isInteger(streamRetentionDays) || streamRetentionDays < 1) {
|
|
885
1075
|
missing.add(`ship.streamRetentionDays must be a positive integer (got "${shipRaw.streamRetentionDays}")`);
|
|
886
1076
|
}
|
|
1077
|
+
const eventsRotateBytes = shipRaw.eventsRotateBytes === void 0 ? DEFAULT_EVENTS_ROTATE_BYTES : Number(shipRaw.eventsRotateBytes);
|
|
1078
|
+
if (!Number.isInteger(eventsRotateBytes) || eventsRotateBytes < 1) {
|
|
1079
|
+
missing.add(`ship.eventsRotateBytes must be a positive integer (got "${shipRaw.eventsRotateBytes}")`);
|
|
1080
|
+
}
|
|
887
1081
|
const maxThinkingTokens = shipRaw.agent?.maxThinkingTokens === void 0 ? DEFAULT_MAX_THINKING_TOKENS : Number(shipRaw.agent.maxThinkingTokens);
|
|
888
1082
|
if (!Number.isInteger(maxThinkingTokens) || maxThinkingTokens < 0) {
|
|
889
1083
|
missing.add(`ship.agent.maxThinkingTokens must be a non-negative integer (got "${shipRaw.agent?.maxThinkingTokens}")`);
|
|
@@ -926,10 +1120,11 @@ Create the first of those:
|
|
|
926
1120
|
useNvm: shipRaw.useNvm !== false,
|
|
927
1121
|
nvmSh: shipRaw.nvmSh,
|
|
928
1122
|
stateDir,
|
|
929
|
-
logFile: expand(shipRaw.logFile ??
|
|
1123
|
+
logFile: expand(shipRaw.logFile ?? join3(tmpdir(), "crew.log"), base),
|
|
930
1124
|
userAgent: shipRaw.userAgent ?? `Mozilla/5.0 CrewAgent/${crewVersion(crewHome)}`,
|
|
931
1125
|
maxConcurrentAgents,
|
|
932
1126
|
streamRetentionDays,
|
|
1127
|
+
eventsRotateBytes,
|
|
933
1128
|
relayHost: shipRaw.relayHost ? String(shipRaw.relayHost) : void 0,
|
|
934
1129
|
// 2222 matches the relay's own RELAY_SSH_PORT default (crew-relay README).
|
|
935
1130
|
relayPort: shipRaw.relayPort !== void 0 ? Number(shipRaw.relayPort) : 2222,
|
|
@@ -959,7 +1154,7 @@ function parseOneRoute(c, where, file, base, stateDir, raw, missing, crewHome) {
|
|
|
959
1154
|
const e = entry;
|
|
960
1155
|
rejectUnknownKeys(e, REPO_ENTRY_KEYS, `${where}.repos.${name}`, file);
|
|
961
1156
|
repoDirs[name] = expand(String(missing.req(e.dir, `${where}.repos.${name}.dir`) || ""), base);
|
|
962
|
-
if (e.hooks || e.labels || e.release || e.branch || e.hostPassengers !== void 0) {
|
|
1157
|
+
if (e.hooks || e.labels || e.release || e.branch || e.hostPassengers !== void 0 || e.worktreeName !== void 0) {
|
|
963
1158
|
repoOverrides[name] = {
|
|
964
1159
|
hooks: e.hooks,
|
|
965
1160
|
labels: e.labels,
|
|
@@ -969,7 +1164,8 @@ function parseOneRoute(c, where, file, base, stateDir, raw, missing, crewHome) {
|
|
|
969
1164
|
// shape as one that never had the key, and existing callers
|
|
970
1165
|
// compare this literally.
|
|
971
1166
|
...e.branch ? { branch: parseBranchOverride(e.branch, `${where}.repos.${name}`, file) } : {},
|
|
972
|
-
...e.hostPassengers !== void 0 ? { hostPassengers: e.hostPassengers === true } : {}
|
|
1167
|
+
...e.hostPassengers !== void 0 ? { hostPassengers: e.hostPassengers === true } : {},
|
|
1168
|
+
...e.worktreeName !== void 0 ? { worktreeName: parseWorktreeName(e.worktreeName, `${where}.repos.${name}`, file) } : {}
|
|
973
1169
|
};
|
|
974
1170
|
}
|
|
975
1171
|
} else {
|
|
@@ -1059,8 +1255,8 @@ function repoIdForName(r, name) {
|
|
|
1059
1255
|
}
|
|
1060
1256
|
function resolveApiKey(r) {
|
|
1061
1257
|
if (r.apiKey) return r.apiKey;
|
|
1062
|
-
if (r.apiKeyFile && r.apiKeyVar &&
|
|
1063
|
-
for (const line of
|
|
1258
|
+
if (r.apiKeyFile && r.apiKeyVar && existsSync3(r.apiKeyFile)) {
|
|
1259
|
+
for (const line of readFileSync3(r.apiKeyFile, "utf8").split("\n")) {
|
|
1064
1260
|
const m = line.match(new RegExp(`^${r.apiKeyVar}=(.*)$`));
|
|
1065
1261
|
if (m) return (m[1] ?? "").trim().replace(/^["']|["']$/g, "");
|
|
1066
1262
|
}
|
|
@@ -1123,7 +1319,7 @@ function configuredMembers(rt) {
|
|
|
1123
1319
|
return out;
|
|
1124
1320
|
}
|
|
1125
1321
|
function defaultRepoDir(basePath, workspace, repoName) {
|
|
1126
|
-
return
|
|
1322
|
+
return join3(basePath, workspace, repoName);
|
|
1127
1323
|
}
|
|
1128
1324
|
function reposOf(r) {
|
|
1129
1325
|
const named = Object.entries(r.repos);
|
|
@@ -1147,7 +1343,7 @@ function passengerRepoTargets(r) {
|
|
|
1147
1343
|
return reposOf(r).filter((t) => r.repoOverrides[t.name]?.hostPassengers !== false);
|
|
1148
1344
|
}
|
|
1149
1345
|
function passengerCheckoutDir(basePath, workspace, repoName) {
|
|
1150
|
-
return
|
|
1346
|
+
return join3(basePath, workspace, ".passenger-checkouts", repoName);
|
|
1151
1347
|
}
|
|
1152
1348
|
function passengerCheckoutTargets(r) {
|
|
1153
1349
|
if (!r.hostPassengers) return [];
|
|
@@ -1279,17 +1475,17 @@ function rosterMarkdown(roster, meId) {
|
|
|
1279
1475
|
}
|
|
1280
1476
|
|
|
1281
1477
|
// src/state.ts
|
|
1282
|
-
import { readFileSync as
|
|
1283
|
-
import { join as
|
|
1478
|
+
import { readFileSync as readFileSync4, writeFileSync, existsSync as existsSync4, mkdirSync as mkdirSync2, rmSync as rmSync2, statSync as statSync2 } from "fs";
|
|
1479
|
+
import { join as join4 } from "path";
|
|
1284
1480
|
var EPOCH = "1970-01-01T00:00:00.000Z";
|
|
1285
1481
|
var State = class _State {
|
|
1286
1482
|
dir;
|
|
1287
1483
|
constructor(dir) {
|
|
1288
1484
|
this.dir = dir;
|
|
1289
|
-
|
|
1485
|
+
mkdirSync2(dir, { recursive: true });
|
|
1290
1486
|
}
|
|
1291
1487
|
path(...parts) {
|
|
1292
|
-
return
|
|
1488
|
+
return join4(this.dir, ...parts);
|
|
1293
1489
|
}
|
|
1294
1490
|
/**
|
|
1295
1491
|
* The timestamp every "new since last poll" check is measured against.
|
|
@@ -1300,7 +1496,7 @@ var State = class _State {
|
|
|
1300
1496
|
*/
|
|
1301
1497
|
watermark() {
|
|
1302
1498
|
try {
|
|
1303
|
-
const v =
|
|
1499
|
+
const v = readFileSync4(this.path(".poll-watermark"), "utf8").trim();
|
|
1304
1500
|
return v || EPOCH;
|
|
1305
1501
|
} catch {
|
|
1306
1502
|
return EPOCH;
|
|
@@ -1325,10 +1521,10 @@ var State = class _State {
|
|
|
1325
1521
|
}
|
|
1326
1522
|
/** Whole-crew pause, and the per-role sentinels one level down. */
|
|
1327
1523
|
isPaused() {
|
|
1328
|
-
return
|
|
1524
|
+
return existsSync4(this.path(".crew-paused"));
|
|
1329
1525
|
}
|
|
1330
1526
|
isRolePaused(role) {
|
|
1331
|
-
return
|
|
1527
|
+
return existsSync4(this.path(`.role-paused-${role}`));
|
|
1332
1528
|
}
|
|
1333
1529
|
pausedRoles(roles) {
|
|
1334
1530
|
return new Set(roles.filter((r) => this.isRolePaused(r)));
|
|
@@ -1337,7 +1533,7 @@ var State = class _State {
|
|
|
1337
1533
|
writeFileSync(role ? this.path(`.role-paused-${role}`) : this.path(".crew-paused"), "");
|
|
1338
1534
|
}
|
|
1339
1535
|
resume(role) {
|
|
1340
|
-
|
|
1536
|
+
rmSync2(role ? this.path(`.role-paused-${role}`) : this.path(".crew-paused"), { force: true });
|
|
1341
1537
|
}
|
|
1342
1538
|
/**
|
|
1343
1539
|
* The release phase (merge, deploy, release) has its own sentinel, one
|
|
@@ -1345,11 +1541,11 @@ var State = class _State {
|
|
|
1345
1541
|
* while agents keep working.
|
|
1346
1542
|
*/
|
|
1347
1543
|
isReleasePaused() {
|
|
1348
|
-
return
|
|
1544
|
+
return existsSync4(this.path(".release-paused"));
|
|
1349
1545
|
}
|
|
1350
1546
|
setReleasePaused(paused) {
|
|
1351
1547
|
if (paused) writeFileSync(this.path(".release-paused"), "");
|
|
1352
|
-
else
|
|
1548
|
+
else rmSync2(this.path(".release-paused"), { force: true });
|
|
1353
1549
|
}
|
|
1354
1550
|
/**
|
|
1355
1551
|
* Why a release cycle must not run right now, or null when it may. Every
|
|
@@ -1386,7 +1582,7 @@ var State = class _State {
|
|
|
1386
1582
|
const name = _State.safe(rawName);
|
|
1387
1583
|
const file = this.path(`.${name}.lock`);
|
|
1388
1584
|
try {
|
|
1389
|
-
const existing = Number.parseInt(
|
|
1585
|
+
const existing = Number.parseInt(readFileSync4(file, "utf8").trim(), 10);
|
|
1390
1586
|
if (Number.isFinite(existing) && existing > 0) {
|
|
1391
1587
|
try {
|
|
1392
1588
|
process.kill(existing, 0);
|
|
@@ -1402,7 +1598,7 @@ var State = class _State {
|
|
|
1402
1598
|
ok: true,
|
|
1403
1599
|
release: () => {
|
|
1404
1600
|
try {
|
|
1405
|
-
|
|
1601
|
+
rmSync2(file, { force: true });
|
|
1406
1602
|
} catch {
|
|
1407
1603
|
}
|
|
1408
1604
|
}
|
|
@@ -1480,7 +1676,7 @@ var State = class _State {
|
|
|
1480
1676
|
const heldReported = `.release-held-${suffix}`;
|
|
1481
1677
|
const read = (f) => {
|
|
1482
1678
|
try {
|
|
1483
|
-
return
|
|
1679
|
+
return readFileSync4(this.path(f), "utf8").trim() || null;
|
|
1484
1680
|
} catch {
|
|
1485
1681
|
return null;
|
|
1486
1682
|
}
|
|
@@ -1494,7 +1690,7 @@ var State = class _State {
|
|
|
1494
1690
|
return n;
|
|
1495
1691
|
},
|
|
1496
1692
|
clearBlocked: () => {
|
|
1497
|
-
|
|
1693
|
+
rmSync2(this.path(blocked), { force: true });
|
|
1498
1694
|
},
|
|
1499
1695
|
/**
|
|
1500
1696
|
* The commit a deploy last failed on.
|
|
@@ -1510,7 +1706,7 @@ var State = class _State {
|
|
|
1510
1706
|
`);
|
|
1511
1707
|
},
|
|
1512
1708
|
clearDeployFailed: () => {
|
|
1513
|
-
|
|
1709
|
+
rmSync2(this.path(failed), { force: true });
|
|
1514
1710
|
},
|
|
1515
1711
|
/**
|
|
1516
1712
|
* Consecutive test-gate failures on the same head (CREW-1368). A failure
|
|
@@ -1525,12 +1721,12 @@ var State = class _State {
|
|
|
1525
1721
|
return next;
|
|
1526
1722
|
},
|
|
1527
1723
|
clearTestGateFailed: () => {
|
|
1528
|
-
|
|
1724
|
+
rmSync2(this.path(gateFailed), { force: true });
|
|
1529
1725
|
},
|
|
1530
1726
|
/** Whether the board was already told about this head's red gate. */
|
|
1531
1727
|
heldReported: () => {
|
|
1532
1728
|
try {
|
|
1533
|
-
return
|
|
1729
|
+
return readFileSync4(this.path(heldReported), "utf8").replace(/\n$/, "") || null;
|
|
1534
1730
|
} catch {
|
|
1535
1731
|
return null;
|
|
1536
1732
|
}
|
|
@@ -1540,7 +1736,7 @@ var State = class _State {
|
|
|
1540
1736
|
`);
|
|
1541
1737
|
},
|
|
1542
1738
|
clearHeldReported: () => {
|
|
1543
|
-
|
|
1739
|
+
rmSync2(this.path(heldReported), { force: true });
|
|
1544
1740
|
},
|
|
1545
1741
|
testGateReported: (sha) => read(gateReported) === sha,
|
|
1546
1742
|
noteTestGateReported: (sha) => {
|
|
@@ -1567,13 +1763,13 @@ var State = class _State {
|
|
|
1567
1763
|
const ticketStreakFile = `.fairness-ticket-streak-${suffix}`;
|
|
1568
1764
|
const read = (f) => {
|
|
1569
1765
|
try {
|
|
1570
|
-
return
|
|
1766
|
+
return readFileSync4(this.path(f), "utf8").trim() || null;
|
|
1571
1767
|
} catch {
|
|
1572
1768
|
return null;
|
|
1573
1769
|
}
|
|
1574
1770
|
};
|
|
1575
1771
|
const clear2 = () => {
|
|
1576
|
-
for (const f of [streakFile, ticketFile, sinceFile, ticketStreakFile])
|
|
1772
|
+
for (const f of [streakFile, ticketFile, sinceFile, ticketStreakFile]) rmSync2(this.path(f), { force: true });
|
|
1577
1773
|
};
|
|
1578
1774
|
return {
|
|
1579
1775
|
/** Consecutive cycles this route had actionable work and did not win. */
|
|
@@ -1632,7 +1828,7 @@ var State = class _State {
|
|
|
1632
1828
|
return {
|
|
1633
1829
|
previous: () => {
|
|
1634
1830
|
try {
|
|
1635
|
-
const parsed = JSON.parse(
|
|
1831
|
+
const parsed = JSON.parse(readFileSync4(this.path(file), "utf8"));
|
|
1636
1832
|
return parsed && typeof parsed === "object" ? parsed : {};
|
|
1637
1833
|
} catch {
|
|
1638
1834
|
return {};
|
|
@@ -1653,7 +1849,7 @@ var State = class _State {
|
|
|
1653
1849
|
return {
|
|
1654
1850
|
previous: () => {
|
|
1655
1851
|
try {
|
|
1656
|
-
const parsed = JSON.parse(
|
|
1852
|
+
const parsed = JSON.parse(readFileSync4(this.path(file), "utf8"));
|
|
1657
1853
|
return Array.isArray(parsed) ? parsed : [];
|
|
1658
1854
|
} catch {
|
|
1659
1855
|
return [];
|
|
@@ -1677,7 +1873,7 @@ var State = class _State {
|
|
|
1677
1873
|
},
|
|
1678
1874
|
at: () => {
|
|
1679
1875
|
try {
|
|
1680
|
-
return
|
|
1876
|
+
return statSync2(this.path(file)).mtimeMs;
|
|
1681
1877
|
} catch {
|
|
1682
1878
|
return null;
|
|
1683
1879
|
}
|
|
@@ -1686,109 +1882,13 @@ var State = class _State {
|
|
|
1686
1882
|
}
|
|
1687
1883
|
readInt(file) {
|
|
1688
1884
|
try {
|
|
1689
|
-
return Number.parseInt(
|
|
1885
|
+
return Number.parseInt(readFileSync4(this.path(file), "utf8").trim(), 10) || 0;
|
|
1690
1886
|
} catch {
|
|
1691
1887
|
return 0;
|
|
1692
1888
|
}
|
|
1693
1889
|
}
|
|
1694
1890
|
};
|
|
1695
1891
|
|
|
1696
|
-
// src/events.ts
|
|
1697
|
-
import { appendFileSync, mkdirSync as mkdirSync2 } from "fs";
|
|
1698
|
-
import { join as join4, dirname as dirname2 } from "path";
|
|
1699
|
-
function render(e) {
|
|
1700
|
-
const scope = e.role ? `${e.step}[${e.role}]` : e.step;
|
|
1701
|
-
const ticket = e.ticket ? ` ${e.ticket}` : "";
|
|
1702
|
-
const level = e.level === "info" ? "" : `${e.level.toUpperCase()}: `;
|
|
1703
|
-
return `${e.at} ${scope}:${ticket} ${level}${e.message}`;
|
|
1704
|
-
}
|
|
1705
|
-
var Emitter = class _Emitter {
|
|
1706
|
-
cycle;
|
|
1707
|
-
opts;
|
|
1708
|
-
now;
|
|
1709
|
-
step = "poll";
|
|
1710
|
-
role;
|
|
1711
|
-
constructor(opts) {
|
|
1712
|
-
this.opts = opts;
|
|
1713
|
-
this.now = opts.now ?? (() => /* @__PURE__ */ new Date());
|
|
1714
|
-
this.cycle = opts.cycleId ?? this.now().toISOString().replace(/[-:.TZ]/g, "").slice(0, 14);
|
|
1715
|
-
for (const f of [opts.eventFile, opts.logFile]) {
|
|
1716
|
-
if (!f) continue;
|
|
1717
|
-
try {
|
|
1718
|
-
mkdirSync2(dirname2(f), { recursive: true });
|
|
1719
|
-
} catch {
|
|
1720
|
-
}
|
|
1721
|
-
}
|
|
1722
|
-
}
|
|
1723
|
-
/** Everything emitted after this belongs to `step`, until the next one. */
|
|
1724
|
-
enter(step, role) {
|
|
1725
|
-
this.step = step;
|
|
1726
|
-
this.role = role;
|
|
1727
|
-
}
|
|
1728
|
-
emit(message, extra = {}) {
|
|
1729
|
-
const e = {
|
|
1730
|
-
at: this.now().toISOString(),
|
|
1731
|
-
cycle: this.cycle,
|
|
1732
|
-
route: this.opts.route,
|
|
1733
|
-
step: extra.step ?? this.step,
|
|
1734
|
-
level: extra.level ?? "info",
|
|
1735
|
-
message,
|
|
1736
|
-
...extra.role ?? this.role ? { role: extra.role ?? this.role } : {},
|
|
1737
|
-
...extra.ticket ? { ticket: extra.ticket } : {},
|
|
1738
|
-
...extra.data ? { data: extra.data } : {}
|
|
1739
|
-
};
|
|
1740
|
-
const line = render(e);
|
|
1741
|
-
try {
|
|
1742
|
-
if (this.opts.eventFile) appendFileSync(this.opts.eventFile, `${JSON.stringify(e)}
|
|
1743
|
-
`);
|
|
1744
|
-
} catch {
|
|
1745
|
-
}
|
|
1746
|
-
try {
|
|
1747
|
-
if (this.opts.logFile) appendFileSync(this.opts.logFile, `${line}
|
|
1748
|
-
`);
|
|
1749
|
-
} catch {
|
|
1750
|
-
}
|
|
1751
|
-
try {
|
|
1752
|
-
(this.opts.console ?? ((l) => process.stderr.write(`${l}
|
|
1753
|
-
`)))(line);
|
|
1754
|
-
} catch {
|
|
1755
|
-
}
|
|
1756
|
-
return e;
|
|
1757
|
-
}
|
|
1758
|
-
warn(message, extra = {}) {
|
|
1759
|
-
return this.emit(message, { ...extra, level: "warn" });
|
|
1760
|
-
}
|
|
1761
|
-
error(message, extra = {}) {
|
|
1762
|
-
return this.emit(message, { ...extra, level: "error" });
|
|
1763
|
-
}
|
|
1764
|
-
/**
|
|
1765
|
-
* A view of this emitter labelling every event with a different route,
|
|
1766
|
-
* sharing the same sinks and cycle id. For a fleet-wide run, the top-level
|
|
1767
|
-
* Emitter is stamped with just one route (see cli.ts) — a phase that
|
|
1768
|
-
* actually knows which route it's working (release, per-repo poll) uses
|
|
1769
|
-
* this instead, so its events aren't all attributed to `routes[0]`.
|
|
1770
|
-
*/
|
|
1771
|
-
forRoute(route) {
|
|
1772
|
-
return new _Emitter({ ...this.opts, route, cycleId: this.cycle });
|
|
1773
|
-
}
|
|
1774
|
-
/** Times a step and emits its outcome, so a view can show duration. */
|
|
1775
|
-
async timed(step, message, fn, role) {
|
|
1776
|
-
this.enter(step, role);
|
|
1777
|
-
const started = this.now().getTime();
|
|
1778
|
-
try {
|
|
1779
|
-
const result = await fn();
|
|
1780
|
-
this.emit(message, { data: { ms: this.now().getTime() - started, ok: true } });
|
|
1781
|
-
return result;
|
|
1782
|
-
} catch (err) {
|
|
1783
|
-
this.error(`${message} \u2014 ${err.message}`, {
|
|
1784
|
-
data: { ms: this.now().getTime() - started, ok: false }
|
|
1785
|
-
});
|
|
1786
|
-
throw err;
|
|
1787
|
-
}
|
|
1788
|
-
}
|
|
1789
|
-
};
|
|
1790
|
-
var eventFileFor = (stateDir) => join4(stateDir, "events.jsonl");
|
|
1791
|
-
|
|
1792
1892
|
// src/tracker.ts
|
|
1793
1893
|
import { hostname } from "os";
|
|
1794
1894
|
import { TablationClient } from "@tablation/client";
|
|
@@ -2088,7 +2188,7 @@ function describeRelease(o, route) {
|
|
|
2088
2188
|
// src/install.ts
|
|
2089
2189
|
import { dirname as dirname3, join as join5, extname } from "path";
|
|
2090
2190
|
import { homedir as homedir2 } from "os";
|
|
2091
|
-
import { accessSync, constants, existsSync as existsSync5, mkdirSync as mkdirSync3, readdirSync, readFileSync as readFileSync5, statSync as
|
|
2191
|
+
import { accessSync, constants, existsSync as existsSync5, mkdirSync as mkdirSync3, readdirSync, readFileSync as readFileSync5, statSync as statSync3, writeFileSync as writeFileSync2, unlinkSync, realpathSync } from "fs";
|
|
2092
2192
|
import { spawnSync } from "child_process";
|
|
2093
2193
|
import { createHash } from "crypto";
|
|
2094
2194
|
|
|
@@ -2142,7 +2242,7 @@ function findOnPath(bin, searchPath) {
|
|
|
2142
2242
|
for (const candidate of candidates) {
|
|
2143
2243
|
try {
|
|
2144
2244
|
accessSync(candidate, constants.X_OK);
|
|
2145
|
-
if (
|
|
2245
|
+
if (statSync3(candidate).isFile()) return candidate;
|
|
2146
2246
|
} catch {
|
|
2147
2247
|
}
|
|
2148
2248
|
}
|
|
@@ -2605,10 +2705,10 @@ import { execFileSync } from "child_process";
|
|
|
2605
2705
|
import {
|
|
2606
2706
|
existsSync as existsSync6,
|
|
2607
2707
|
mkdirSync as mkdirSync4,
|
|
2608
|
-
openSync,
|
|
2609
|
-
closeSync,
|
|
2708
|
+
openSync as openSync2,
|
|
2709
|
+
closeSync as closeSync2,
|
|
2610
2710
|
writeSync,
|
|
2611
|
-
statSync as
|
|
2711
|
+
statSync as statSync4,
|
|
2612
2712
|
unlinkSync as unlinkSync2
|
|
2613
2713
|
} from "fs";
|
|
2614
2714
|
import { dirname as dirname4, basename as basename3 } from "path";
|
|
@@ -3004,10 +3104,10 @@ function acquireCheckoutLock(dir, timeoutMs = 3e4) {
|
|
|
3004
3104
|
const deadline = Date.now() + timeoutMs;
|
|
3005
3105
|
for (; ; ) {
|
|
3006
3106
|
try {
|
|
3007
|
-
const fd =
|
|
3107
|
+
const fd = openSync2(lockPath, "wx");
|
|
3008
3108
|
writeSync(fd, `${process.pid}
|
|
3009
3109
|
`);
|
|
3010
|
-
|
|
3110
|
+
closeSync2(fd);
|
|
3011
3111
|
return () => {
|
|
3012
3112
|
try {
|
|
3013
3113
|
unlinkSync2(lockPath);
|
|
@@ -3018,7 +3118,7 @@ function acquireCheckoutLock(dir, timeoutMs = 3e4) {
|
|
|
3018
3118
|
const err = e;
|
|
3019
3119
|
if (err.code !== "EEXIST") throw err;
|
|
3020
3120
|
try {
|
|
3021
|
-
if (Date.now() -
|
|
3121
|
+
if (Date.now() - statSync4(lockPath).mtimeMs > CHECKOUT_LOCK_STALE_MS) {
|
|
3022
3122
|
try {
|
|
3023
3123
|
unlinkSync2(lockPath);
|
|
3024
3124
|
} catch {
|
|
@@ -4661,6 +4761,7 @@ ${waiting.map((t) => `| ${displayKey2(t)} | ${t.status} | ${blockers(t, i)} |`).
|
|
|
4661
4761
|
}
|
|
4662
4762
|
|
|
4663
4763
|
// src/ticket-branch.ts
|
|
4764
|
+
import { basename as basename4 } from "path";
|
|
4664
4765
|
function branchRenderer(t, role) {
|
|
4665
4766
|
const { tag, prefix } = ticketBranchContext(t);
|
|
4666
4767
|
return (template) => renderBranchName(template, { key: t.issue_id, title: t.title ?? void 0, role, tag, prefix });
|
|
@@ -4669,7 +4770,15 @@ function worktreeForTicket(dir, cfg, t, role) {
|
|
|
4669
4770
|
const { prefix } = ticketBranchContext(t);
|
|
4670
4771
|
const branchName = branchRenderer(t, role)(effectiveBranchTemplate(cfg, prefix));
|
|
4671
4772
|
const number = t.issue_id.replace(/^\D+/, "");
|
|
4672
|
-
|
|
4773
|
+
const { tag } = ticketBranchContext(t);
|
|
4774
|
+
return effectiveWorktreeDirName(cfg, dir, branchName, number, (template) => renderWorktreeName(template, {
|
|
4775
|
+
dir: basename4(dir.replace(/[/\\]+$/, "")),
|
|
4776
|
+
key: t.issue_id,
|
|
4777
|
+
title: t.title ?? void 0,
|
|
4778
|
+
role,
|
|
4779
|
+
tag,
|
|
4780
|
+
prefix
|
|
4781
|
+
}));
|
|
4673
4782
|
}
|
|
4674
4783
|
function branchTemplates(cfg, t) {
|
|
4675
4784
|
const { prefix } = ticketBranchContext(t);
|
|
@@ -4985,7 +5094,9 @@ function writeDigest(o, d, role, stateDir) {
|
|
|
4985
5094
|
const repoFor = (dir) => {
|
|
4986
5095
|
const hit = repoCache.get(dir);
|
|
4987
5096
|
if (hit) return hit;
|
|
4988
|
-
const
|
|
5097
|
+
const target = reposOf(o.route).find((r) => r.dir === dir);
|
|
5098
|
+
const nameOverride = target ? o.route.repoOverrides[target.name]?.worktreeName : void 0;
|
|
5099
|
+
const cfg = resolveRepoConfig(loadRepoConfig(dir), nameOverride ? { worktrees: { nameOverride } } : void 0, dir);
|
|
4989
5100
|
repoCache.set(dir, cfg);
|
|
4990
5101
|
return cfg;
|
|
4991
5102
|
};
|
|
@@ -5344,13 +5455,13 @@ var alreadyReported = (ticket, g, comments) => comments.some((c) => c.ticket_id
|
|
|
5344
5455
|
|
|
5345
5456
|
// src/agent.ts
|
|
5346
5457
|
import { spawn as spawn2 } from "child_process";
|
|
5347
|
-
import { readFileSync as readFileSync6, existsSync as existsSync7, statSync as
|
|
5458
|
+
import { readFileSync as readFileSync6, existsSync as existsSync7, statSync as statSync5, mkdirSync as mkdirSync5, writeFileSync as writeFileSync4, createWriteStream } from "fs";
|
|
5348
5459
|
import { finished } from "stream/promises";
|
|
5349
5460
|
import { join as join7, dirname as dirname5 } from "path";
|
|
5350
5461
|
import { createHash as createHash2 } from "crypto";
|
|
5351
5462
|
|
|
5352
5463
|
// src/environment.ts
|
|
5353
|
-
import { basename as
|
|
5464
|
+
import { basename as basename5 } from "path";
|
|
5354
5465
|
var API_KEY_VAR = "CREW_API_KEY";
|
|
5355
5466
|
function authorshipBullets(a, contract) {
|
|
5356
5467
|
if (!a) return [];
|
|
@@ -5401,8 +5512,16 @@ function repoSection(r, key, heading) {
|
|
|
5401
5512
|
const repo = r.config;
|
|
5402
5513
|
const branchTemplate = effectiveBranchTemplate(repo, repoPrefix(repo));
|
|
5403
5514
|
const branchName = branchExample(repo, key);
|
|
5404
|
-
const
|
|
5405
|
-
const
|
|
5515
|
+
const checkoutName = basename5(r.dir.replace(/[/\\]+$/, ""));
|
|
5516
|
+
const worktreeName = effectiveWorktreeDirName(
|
|
5517
|
+
repo,
|
|
5518
|
+
r.dir,
|
|
5519
|
+
branchName,
|
|
5520
|
+
key.replace(/^\D+/, ""),
|
|
5521
|
+
// The example ticket has no tag of its own, so `{issue}` is the key.
|
|
5522
|
+
(template) => renderWorktreeName(template, { dir: checkoutName, key, title: "Fix the widget", role: "dev", prefix: repoPrefix(repo) })
|
|
5523
|
+
);
|
|
5524
|
+
const worktreePattern = repo.worktrees.name ?? (repo.provenance["worktrees.prefix"] !== "default" ? `${repo.worktrees.prefix}<number>` : `${checkoutName}-<branch>`);
|
|
5406
5525
|
const lines = [
|
|
5407
5526
|
"",
|
|
5408
5527
|
heading,
|
|
@@ -5807,7 +5926,7 @@ function planAgentRun(o) {
|
|
|
5807
5926
|
let digest;
|
|
5808
5927
|
let ageSeconds;
|
|
5809
5928
|
if (existsSync7(digestPath2)) {
|
|
5810
|
-
ageSeconds = Math.floor((now() -
|
|
5929
|
+
ageSeconds = Math.floor((now() - statSync5(digestPath2).mtimeMs) / 1e3);
|
|
5811
5930
|
if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync6(digestPath2, "utf8");
|
|
5812
5931
|
}
|
|
5813
5932
|
const common = readFileSync6(commonPath, "utf8");
|
|
@@ -6375,7 +6494,8 @@ async function guardRunHandoff(route, ship, tracker, role, workingId, log) {
|
|
|
6375
6494
|
const cfg = resolveRepoConfig(loadRepoConfig(target.dir), {
|
|
6376
6495
|
hooks: { ...route.hooks, ...o?.hooks },
|
|
6377
6496
|
labels: { ...route.labels, ...o?.labels },
|
|
6378
|
-
branch: { ...route.branch, ...o?.branch }
|
|
6497
|
+
branch: { ...route.branch, ...o?.branch },
|
|
6498
|
+
worktrees: { nameOverride: o?.worktreeName }
|
|
6379
6499
|
}, target.dir);
|
|
6380
6500
|
if (role === "qa") {
|
|
6381
6501
|
await retireRunWorktree(tracker, target.dir, cfg, t, role, seatId, ship.name, log);
|
|
@@ -6537,7 +6657,7 @@ Wrote ${o.configPath}.
|
|
|
6537
6657
|
}
|
|
6538
6658
|
|
|
6539
6659
|
// src/release-run.ts
|
|
6540
|
-
import { readFileSync as readFileSync7, writeFileSync as writeFileSync6, existsSync as existsSync11, copyFileSync, mkdirSync as mkdirSync7, mkdtempSync, rmSync as
|
|
6660
|
+
import { readFileSync as readFileSync7, writeFileSync as writeFileSync6, existsSync as existsSync11, copyFileSync, mkdirSync as mkdirSync7, mkdtempSync, rmSync as rmSync3 } from "fs";
|
|
6541
6661
|
import { dirname as dirname8, join as join11 } from "path";
|
|
6542
6662
|
import { tmpdir as tmpdir2 } from "os";
|
|
6543
6663
|
|
|
@@ -7320,7 +7440,7 @@ function openReleaseCheckout(o) {
|
|
|
7320
7440
|
const dir = join11(root, "checkout");
|
|
7321
7441
|
const close = () => {
|
|
7322
7442
|
gitOk(o.cwd, ["worktree", "remove", "--force", dir]);
|
|
7323
|
-
|
|
7443
|
+
rmSync3(root, { recursive: true, force: true });
|
|
7324
7444
|
gitOk(o.cwd, ["worktree", "prune"]);
|
|
7325
7445
|
gitOk(o.cwd, ["branch", "-D", branch]);
|
|
7326
7446
|
};
|
|
@@ -7803,7 +7923,7 @@ function missingReleaserWarning(repo, release, ships, now = Date.now()) {
|
|
|
7803
7923
|
}
|
|
7804
7924
|
|
|
7805
7925
|
// src/watch.ts
|
|
7806
|
-
import { watch as watchFile, existsSync as existsSync12, statSync as
|
|
7926
|
+
import { watch as watchFile, existsSync as existsSync12, statSync as statSync6, openSync as openSync3, readSync, closeSync as closeSync3 } from "fs";
|
|
7807
7927
|
var E = "\x1B[";
|
|
7808
7928
|
var dim = (s) => `${E}2m${s}${E}0m`;
|
|
7809
7929
|
var bold = (s) => `${E}1m${s}${E}0m`;
|
|
@@ -7905,13 +8025,13 @@ function renderHeaders(states, now = Date.now()) {
|
|
|
7905
8025
|
}
|
|
7906
8026
|
function readFrom(file, from) {
|
|
7907
8027
|
if (!existsSync12(file)) return { events: [], offset: from };
|
|
7908
|
-
const size =
|
|
8028
|
+
const size = statSync6(file).size;
|
|
7909
8029
|
if (size < from) return readFrom(file, 0);
|
|
7910
8030
|
if (size === from) return { events: [], offset: from };
|
|
7911
|
-
const fd =
|
|
8031
|
+
const fd = openSync3(file, "r");
|
|
7912
8032
|
const buf = Buffer.alloc(size - from);
|
|
7913
8033
|
readSync(fd, buf, 0, buf.length, from);
|
|
7914
|
-
|
|
8034
|
+
closeSync3(fd);
|
|
7915
8035
|
const text = buf.toString("utf8");
|
|
7916
8036
|
const lastNewline = text.lastIndexOf("\n");
|
|
7917
8037
|
if (lastNewline === -1) return { events: [], offset: from };
|
|
@@ -9032,7 +9152,7 @@ async function showLogEntry(route, entryId, opts = {}) {
|
|
|
9032
9152
|
|
|
9033
9153
|
// src/repos-cmd.ts
|
|
9034
9154
|
import { homedir as homedir3 } from "os";
|
|
9035
|
-
import { basename as
|
|
9155
|
+
import { basename as basename6, resolve as resolve3 } from "path";
|
|
9036
9156
|
import { isMap, isSeq, parseDocument, stringify as stringify2 } from "yaml";
|
|
9037
9157
|
var ReposError = class extends Error {
|
|
9038
9158
|
};
|
|
@@ -9104,7 +9224,7 @@ function configuredRepos(text, route, base) {
|
|
|
9104
9224
|
});
|
|
9105
9225
|
}
|
|
9106
9226
|
const dir = m.get("dir");
|
|
9107
|
-
return dir ? [{ name:
|
|
9227
|
+
return dir ? [{ name: basename6(String(dir)), dir: expandDir(String(dir), base) }] : [];
|
|
9108
9228
|
}
|
|
9109
9229
|
function planRepoAdd(i) {
|
|
9110
9230
|
const doc = parseDocument(i.text);
|
|
@@ -9116,7 +9236,7 @@ function planRepoAdd(i) {
|
|
|
9116
9236
|
throw new ReposError(`${target} is already configured on route ${i.route}`);
|
|
9117
9237
|
}
|
|
9118
9238
|
const matched = matchRepoName(i.origin, i.resolved);
|
|
9119
|
-
const name = matched ?? i.name ?? normalizeRemote(i.origin)?.split("/").pop() ??
|
|
9239
|
+
const name = matched ?? i.name ?? normalizeRemote(i.origin)?.split("/").pop() ?? basename6(target);
|
|
9120
9240
|
let warning;
|
|
9121
9241
|
if (!matched) {
|
|
9122
9242
|
const known = Object.values(i.resolved?.repoNames ?? {});
|
|
@@ -9280,14 +9400,14 @@ function schedulerInstalled(plan, exists, crontab) {
|
|
|
9280
9400
|
|
|
9281
9401
|
// src/worktree-sweep.ts
|
|
9282
9402
|
import { existsSync as existsSync15 } from "fs";
|
|
9283
|
-
import { basename as
|
|
9403
|
+
import { basename as basename7 } from "path";
|
|
9284
9404
|
function planWorktreeSweep(target, tickets, contract) {
|
|
9285
9405
|
const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
|
|
9286
9406
|
const verified = contract.statuses.verified;
|
|
9287
9407
|
const byNumber = new Map(tickets.map((t) => [t.issue_id.replace(/^\D+/, ""), t]));
|
|
9288
9408
|
const actions = [];
|
|
9289
9409
|
for (const w of worktrees(target.dir)) {
|
|
9290
|
-
const m = /(?:^|-)(\d+)$/.exec(
|
|
9410
|
+
const m = /(?:^|-)(\d+)$/.exec(basename7(w.path));
|
|
9291
9411
|
if (!m) continue;
|
|
9292
9412
|
const ticket = byNumber.get(m[1]);
|
|
9293
9413
|
if (!ticket) continue;
|
|
@@ -9402,7 +9522,7 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
|
|
|
9402
9522
|
}
|
|
9403
9523
|
|
|
9404
9524
|
// src/stream-sweep.ts
|
|
9405
|
-
import { existsSync as existsSync16, readdirSync as readdirSync3, rmSync as
|
|
9525
|
+
import { existsSync as existsSync16, readdirSync as readdirSync3, rmSync as rmSync4, statSync as statSync7 } from "fs";
|
|
9406
9526
|
import { join as join15 } from "path";
|
|
9407
9527
|
var DAY_MS2 = 24 * 60 * 60 * 1e3;
|
|
9408
9528
|
function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
|
|
@@ -9411,7 +9531,7 @@ function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
|
|
|
9411
9531
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
9412
9532
|
return readdirSync3(dir).map((name) => join15(dir, name)).filter((path) => {
|
|
9413
9533
|
try {
|
|
9414
|
-
return
|
|
9534
|
+
return statSync7(path).mtimeMs < cutoff;
|
|
9415
9535
|
} catch {
|
|
9416
9536
|
return false;
|
|
9417
9537
|
}
|
|
@@ -9425,7 +9545,7 @@ function applyStreamSweep(paths, dryRun, log) {
|
|
|
9425
9545
|
continue;
|
|
9426
9546
|
}
|
|
9427
9547
|
try {
|
|
9428
|
-
|
|
9548
|
+
rmSync4(path, { force: true });
|
|
9429
9549
|
removed++;
|
|
9430
9550
|
log.emit(`removed stream artifact ${path}`, { step: "worktree" });
|
|
9431
9551
|
} catch (e) {
|
|
@@ -9434,9 +9554,20 @@ function applyStreamSweep(paths, dryRun, log) {
|
|
|
9434
9554
|
}
|
|
9435
9555
|
return removed;
|
|
9436
9556
|
}
|
|
9557
|
+
function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
9558
|
+
if (!existsSync16(stateDir)) return [];
|
|
9559
|
+
const cutoff = now - retentionDays * DAY_MS2;
|
|
9560
|
+
return readdirSync3(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join15(stateDir, name)).filter((path) => {
|
|
9561
|
+
try {
|
|
9562
|
+
return statSync7(path).mtimeMs < cutoff;
|
|
9563
|
+
} catch {
|
|
9564
|
+
return false;
|
|
9565
|
+
}
|
|
9566
|
+
}).sort();
|
|
9567
|
+
}
|
|
9437
9568
|
|
|
9438
9569
|
// src/cli.ts
|
|
9439
|
-
import { readFileSync as readFileSync14, existsSync as existsSync20, mkdirSync as mkdirSync11, writeFileSync as writeFileSync9, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync } from "fs";
|
|
9570
|
+
import { readFileSync as readFileSync14, existsSync as existsSync20, mkdirSync as mkdirSync11, writeFileSync as writeFileSync9, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync2, statSync as statSync9 } from "fs";
|
|
9440
9571
|
import { createInterface } from "readline/promises";
|
|
9441
9572
|
import { dirname as dirOf, resolve as resolvePath } from "path";
|
|
9442
9573
|
|
|
@@ -10174,7 +10305,7 @@ function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
|
|
|
10174
10305
|
}
|
|
10175
10306
|
|
|
10176
10307
|
// src/daemon.ts
|
|
10177
|
-
import { readdirSync as readdirSync6, statSync as
|
|
10308
|
+
import { readdirSync as readdirSync6, statSync as statSync8 } from "fs";
|
|
10178
10309
|
import { join as join19 } from "path";
|
|
10179
10310
|
var DEFAULT_FLOOR_MS = 5e3;
|
|
10180
10311
|
var DEFAULT_CEILING_MS = INTERVAL_SECONDS * 1e3;
|
|
@@ -10206,7 +10337,7 @@ async function runDaemonLoop(o) {
|
|
|
10206
10337
|
function latestMtime(path) {
|
|
10207
10338
|
let stat;
|
|
10208
10339
|
try {
|
|
10209
|
-
stat =
|
|
10340
|
+
stat = statSync8(path);
|
|
10210
10341
|
} catch {
|
|
10211
10342
|
return 0;
|
|
10212
10343
|
}
|
|
@@ -10490,7 +10621,8 @@ async function buildEnvironment(route, ship, ticket) {
|
|
|
10490
10621
|
config: resolveRepoConfig(loadRepoConfig(t.dir), {
|
|
10491
10622
|
hooks: { ...route.hooks, ...o?.hooks },
|
|
10492
10623
|
labels: { ...route.labels, ...o?.labels },
|
|
10493
|
-
branch: { ...route.branch, ...o?.branch }
|
|
10624
|
+
branch: { ...route.branch, ...o?.branch },
|
|
10625
|
+
worktrees: { nameOverride: o?.worktreeName }
|
|
10494
10626
|
}, t.dir)
|
|
10495
10627
|
};
|
|
10496
10628
|
});
|
|
@@ -10802,6 +10934,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
10802
10934
|
const emit = new Emitter({
|
|
10803
10935
|
route: route?.route ?? "",
|
|
10804
10936
|
eventFile: dryRun ? void 0 : eventFileFor(cfg.ship.stateDir),
|
|
10937
|
+
rotateBytes: cfg.ship.eventsRotateBytes,
|
|
10805
10938
|
logFile: dryRun ? void 0 : cfg.ship.logFile,
|
|
10806
10939
|
console: (l) => process.stderr.write(`${l}
|
|
10807
10940
|
`)
|
|
@@ -10904,7 +11037,10 @@ if (command === "passenger-sync-daemon") {
|
|
|
10904
11037
|
emitReleaseSummary(emit, summaries);
|
|
10905
11038
|
if (!dryRun) state.releaseHeartbeat().touch();
|
|
10906
11039
|
try {
|
|
10907
|
-
const paths =
|
|
11040
|
+
const paths = [
|
|
11041
|
+
...planStreamSweep(cfg.ship.stateDir, cfg.ship.streamRetentionDays),
|
|
11042
|
+
...planEventsSweep(cfg.ship.stateDir, cfg.ship.streamRetentionDays)
|
|
11043
|
+
];
|
|
10908
11044
|
if (paths.length) {
|
|
10909
11045
|
emit.enter("worktree");
|
|
10910
11046
|
const removed = applyStreamSweep(paths, dryRun, emit);
|
|
@@ -11085,7 +11221,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
11085
11221
|
// that declares its own `worktrees.prefix` shadows either, and
|
|
11086
11222
|
// `doctor` reports that the way it reports every other shadowed
|
|
11087
11223
|
// setting.
|
|
11088
|
-
worktrees: { prefix: shipWorktreePrefixFor(c) ?? await areaWorktreePrefix(c, id) }
|
|
11224
|
+
worktrees: { prefix: shipWorktreePrefixFor(c) ?? await areaWorktreePrefix(c, id), nameOverride: o?.worktreeName }
|
|
11089
11225
|
}, t.dir)
|
|
11090
11226
|
};
|
|
11091
11227
|
}));
|
|
@@ -11142,7 +11278,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
11142
11278
|
labels: { ...c.labels, ...o?.labels },
|
|
11143
11279
|
release: mergeRouteRelease(c.release, o?.release),
|
|
11144
11280
|
branch: { ...c.branch, ...o?.branch },
|
|
11145
|
-
worktrees: { prefix: shipWorktreePrefixFor(c) }
|
|
11281
|
+
worktrees: { prefix: shipWorktreePrefixFor(c), nameOverride: o?.worktreeName }
|
|
11146
11282
|
}, target.dir);
|
|
11147
11283
|
const problems = validateEffective(repo);
|
|
11148
11284
|
for (const p of problems) remit.warn(`${scope}: ${p}`);
|
|
@@ -11956,6 +12092,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
11956
12092
|
newEmitter: () => new Emitter({
|
|
11957
12093
|
route: route.route,
|
|
11958
12094
|
eventFile: eventFileFor(cfg.ship.stateDir),
|
|
12095
|
+
rotateBytes: cfg.ship.eventsRotateBytes,
|
|
11959
12096
|
logFile: cfg.ship.logFile,
|
|
11960
12097
|
console: (l) => process.stderr.write(`${l}
|
|
11961
12098
|
`)
|
|
@@ -12565,7 +12702,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
12565
12702
|
const tmpPath = `${resolvedPath}.tmp${process.pid}`;
|
|
12566
12703
|
writeFileSync9(tmpPath, `${JSON.stringify(resolved, null, 2)}
|
|
12567
12704
|
`);
|
|
12568
|
-
|
|
12705
|
+
renameSync2(tmpPath, resolvedPath);
|
|
12569
12706
|
process.stderr.write(`Resolved ids written to ${resolvedPath}
|
|
12570
12707
|
`);
|
|
12571
12708
|
}
|
|
@@ -12782,7 +12919,17 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
12782
12919
|
{ step: "worktree" }
|
|
12783
12920
|
);
|
|
12784
12921
|
}
|
|
12785
|
-
|
|
12922
|
+
if (!dryRun) {
|
|
12923
|
+
const rotated = rotateEventsFile(eventFileFor(cfg.ship.stateDir), cfg.ship.eventsRotateBytes);
|
|
12924
|
+
if (rotated) {
|
|
12925
|
+
anything = true;
|
|
12926
|
+
emit.emit(`rotated oversized event log to ${rotated}`, { step: "worktree" });
|
|
12927
|
+
}
|
|
12928
|
+
}
|
|
12929
|
+
const streamPaths = [
|
|
12930
|
+
...planStreamSweep(cfg.ship.stateDir, cfg.ship.streamRetentionDays),
|
|
12931
|
+
...planEventsSweep(cfg.ship.stateDir, cfg.ship.streamRetentionDays)
|
|
12932
|
+
];
|
|
12786
12933
|
if (streamPaths.length) {
|
|
12787
12934
|
anything = true;
|
|
12788
12935
|
const removed = applyStreamSweep(streamPaths, dryRun, emit);
|
|
@@ -13195,7 +13342,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
13195
13342
|
// `synthesis-tabl-123`. `worktreePrefix` above is kept for
|
|
13196
13343
|
// config-provenance display; this is the field that actually
|
|
13197
13344
|
// describes where a worktree lands by default.
|
|
13198
|
-
worktreeDirPattern: r.config.provenance["worktrees.prefix"] !== "default" ? `${r.config.worktrees.prefix}<number>` : `${
|
|
13345
|
+
worktreeDirPattern: r.config.worktrees.name ?? (r.config.provenance["worktrees.prefix"] !== "default" ? `${r.config.worktrees.prefix}<number>` : `${basename8(r.dir.replace(/[/\\]+$/, ""))}-<branch>`),
|
|
13199
13346
|
worktreeParent: resolvePath(r.dir, "..")
|
|
13200
13347
|
})),
|
|
13201
13348
|
waiting: waiting ? { ticket: waiting.ticket, since: waiting.since, streak: fairness.streak() } : null
|
|
@@ -13285,6 +13432,15 @@ watermark: ${state.watermark()}
|
|
|
13285
13432
|
routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
13286
13433
|
`
|
|
13287
13434
|
);
|
|
13435
|
+
try {
|
|
13436
|
+
const evFile = eventFileFor(cfg.ship.stateDir);
|
|
13437
|
+
const evSize = existsSync20(evFile) ? statSync9(evFile).size : 0;
|
|
13438
|
+
process.stdout.write(
|
|
13439
|
+
evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
|
|
13440
|
+
` : "events log: ok\n"
|
|
13441
|
+
);
|
|
13442
|
+
} catch {
|
|
13443
|
+
}
|
|
13288
13444
|
const tracker = new Tracker(route, cfg.ship);
|
|
13289
13445
|
const contractProblems = validateContract(tracker.contract);
|
|
13290
13446
|
process.stdout.write(
|
|
@@ -13294,10 +13450,10 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
13294
13450
|
);
|
|
13295
13451
|
for (const r of await resolvedRepos(route)) {
|
|
13296
13452
|
const { config } = r;
|
|
13297
|
-
const worktreePattern = config.provenance["worktrees.prefix"] !== "default" ? `${config.worktrees.prefix}<number>` : `${
|
|
13453
|
+
const worktreePattern = config.worktrees.name ?? (config.provenance["worktrees.prefix"] !== "default" ? `${config.worktrees.prefix}<number>` : `${basename8(r.dir.replace(/[/\\]+$/, ""))}-${effectiveBranchTemplate(config, void 0)}`);
|
|
13298
13454
|
process.stdout.write(
|
|
13299
13455
|
`${`repo ${r.name}:`.padEnd(19)}${r.dir}
|
|
13300
|
-
worktrees at ../${worktreePattern} (${config.provenance["worktrees.prefix"] ?? "default"}), branch ${config.branch.name}
|
|
13456
|
+
worktrees at ../${worktreePattern} (${config.provenance["worktrees.name"] ?? config.provenance["worktrees.prefix"] ?? "default"}), branch ${config.branch.name}
|
|
13301
13457
|
requires ${config.platform} \u2014 ${satisfies(host, config.platform) ? "OK" : "MISMATCH"}
|
|
13302
13458
|
`
|
|
13303
13459
|
);
|
package/package.json
CHANGED