@tablation/crew 0.104.0 → 0.105.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1110 -754
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1912,8 +1912,8 @@ function configureSecretStore(c) {
|
|
|
1912
1912
|
function keysDir(stateDir) {
|
|
1913
1913
|
return join10(stateDir, "keys");
|
|
1914
1914
|
}
|
|
1915
|
-
function entryName(host,
|
|
1916
|
-
return `${createHash2("sha256").update(`${host}\0${
|
|
1915
|
+
function entryName(host, slug2) {
|
|
1916
|
+
return `${createHash2("sha256").update(`${host}\0${slug2}`).digest("hex")}.json`;
|
|
1917
1917
|
}
|
|
1918
1918
|
var FileSessionStore = class {
|
|
1919
1919
|
dir;
|
|
@@ -1924,23 +1924,23 @@ var FileSessionStore = class {
|
|
|
1924
1924
|
mkdirSync6(this.dir, { recursive: true, mode: 448 });
|
|
1925
1925
|
if ((statSync3(this.dir).mode & 63) !== 0) chmodSync2(this.dir, 448);
|
|
1926
1926
|
}
|
|
1927
|
-
async get(host,
|
|
1928
|
-
const file = join10(this.dir, entryName(host,
|
|
1927
|
+
async get(host, slug2) {
|
|
1928
|
+
const file = join10(this.dir, entryName(host, slug2));
|
|
1929
1929
|
if (!existsSync8(this.dir)) return void 0;
|
|
1930
1930
|
accessSync(this.dir, constants.R_OK | constants.X_OK);
|
|
1931
1931
|
if (!existsSync8(file)) return void 0;
|
|
1932
1932
|
return JSON.parse(readFileSync9(file, "utf8"));
|
|
1933
1933
|
}
|
|
1934
|
-
async set(host,
|
|
1934
|
+
async set(host, slug2, session) {
|
|
1935
1935
|
this.ensureDir();
|
|
1936
|
-
const file = join10(this.dir, entryName(host,
|
|
1936
|
+
const file = join10(this.dir, entryName(host, slug2));
|
|
1937
1937
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
1938
1938
|
writeFileSync7(tmp, JSON.stringify(session), { mode: 384 });
|
|
1939
1939
|
renameSync2(tmp, file);
|
|
1940
1940
|
}
|
|
1941
|
-
async clear(host,
|
|
1941
|
+
async clear(host, slug2) {
|
|
1942
1942
|
if (!existsSync8(this.dir)) return;
|
|
1943
|
-
rmSync7(join10(this.dir, entryName(host,
|
|
1943
|
+
rmSync7(join10(this.dir, entryName(host, slug2)), { force: true });
|
|
1944
1944
|
}
|
|
1945
1945
|
/** Whether a key could be written here right now, without leaving anything behind. */
|
|
1946
1946
|
writable() {
|
|
@@ -1973,23 +1973,23 @@ var CrewHintedStore = class {
|
|
|
1973
1973
|
constructor(inner) {
|
|
1974
1974
|
this.inner = inner;
|
|
1975
1975
|
}
|
|
1976
|
-
async get(host,
|
|
1976
|
+
async get(host, slug2) {
|
|
1977
1977
|
try {
|
|
1978
|
-
return await this.inner.get(host,
|
|
1978
|
+
return await this.inner.get(host, slug2);
|
|
1979
1979
|
} catch (e) {
|
|
1980
1980
|
throw crewifyStoreError(e);
|
|
1981
1981
|
}
|
|
1982
1982
|
}
|
|
1983
|
-
async set(host,
|
|
1983
|
+
async set(host, slug2, s) {
|
|
1984
1984
|
try {
|
|
1985
|
-
await this.inner.set(host,
|
|
1985
|
+
await this.inner.set(host, slug2, s);
|
|
1986
1986
|
} catch (e) {
|
|
1987
1987
|
throw crewifyStoreError(e);
|
|
1988
1988
|
}
|
|
1989
1989
|
}
|
|
1990
|
-
async clear(host,
|
|
1990
|
+
async clear(host, slug2) {
|
|
1991
1991
|
try {
|
|
1992
|
-
await this.inner.clear(host,
|
|
1992
|
+
await this.inner.clear(host, slug2);
|
|
1993
1993
|
} catch (e) {
|
|
1994
1994
|
throw crewifyStoreError(e);
|
|
1995
1995
|
}
|
|
@@ -2102,7 +2102,8 @@ var TOP_LEVEL = /* @__PURE__ */ new Set([
|
|
|
2102
2102
|
"docs",
|
|
2103
2103
|
"hooks",
|
|
2104
2104
|
"labels",
|
|
2105
|
-
"release"
|
|
2105
|
+
"release",
|
|
2106
|
+
"sandbox"
|
|
2106
2107
|
]);
|
|
2107
2108
|
var BRANCH_KEYS = /* @__PURE__ */ new Set(["base", "name", "push", "remote", "lowercase"]);
|
|
2108
2109
|
var PLACEHOLDER = /\{(key|number|slug|role|prefix|tag|issue)\}/g;
|
|
@@ -2169,6 +2170,36 @@ function rejectUnknown(obj, allowed, where, file) {
|
|
|
2169
2170
|
);
|
|
2170
2171
|
}
|
|
2171
2172
|
}
|
|
2173
|
+
var PACKAGE_NAME = /^[a-z0-9][a-z0-9+.\-]*(=[A-Za-z0-9.+:~\-]+)?$/;
|
|
2174
|
+
function stringList(raw, where, file) {
|
|
2175
|
+
if (raw === void 0 || raw === null) return [];
|
|
2176
|
+
if (!Array.isArray(raw)) throw new RepoConfigError(`${file}: ${where} must be a list of strings`);
|
|
2177
|
+
return raw.map((v) => {
|
|
2178
|
+
if (typeof v !== "string" || v.trim() === "") throw new RepoConfigError(`${file}: ${where} entries must be non-empty strings`);
|
|
2179
|
+
return v;
|
|
2180
|
+
});
|
|
2181
|
+
}
|
|
2182
|
+
function parseSandbox(raw, file) {
|
|
2183
|
+
if (raw === void 0 || raw === null) return void 0;
|
|
2184
|
+
if (typeof raw !== "object" || Array.isArray(raw)) throw new RepoConfigError(`${file}: sandbox must be a mapping`);
|
|
2185
|
+
const r = raw;
|
|
2186
|
+
const packages = stringList(r.packages, "sandbox.packages", file);
|
|
2187
|
+
const setup = stringList(r.setup, "sandbox.setup", file);
|
|
2188
|
+
for (const p of packages) {
|
|
2189
|
+
if (!PACKAGE_NAME.test(p)) throw new RepoConfigError(`${file}: sandbox.packages entry "${p}" is not a package name (lowercase letters, digits, + . - and an optional =version)`);
|
|
2190
|
+
}
|
|
2191
|
+
let image;
|
|
2192
|
+
if (r.image !== void 0 && r.image !== null) {
|
|
2193
|
+
if (typeof r.image !== "string" || r.image.trim() === "" || /\s/.test(r.image)) {
|
|
2194
|
+
throw new RepoConfigError(`${file}: sandbox.image must be an image reference with no whitespace`);
|
|
2195
|
+
}
|
|
2196
|
+
image = r.image;
|
|
2197
|
+
if (packages.length > 0) {
|
|
2198
|
+
throw new RepoConfigError(`${file}: sandbox.image and sandbox.packages are alternatives \u2014 a published image already carries its packages; put the rest in sandbox.setup`);
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
return { ...image ? { image } : {}, packages, setup };
|
|
2202
|
+
}
|
|
2172
2203
|
function parseRepoConfig(text2, file) {
|
|
2173
2204
|
let raw;
|
|
2174
2205
|
try {
|
|
@@ -2187,6 +2218,7 @@ function parseRepoConfig(text2, file) {
|
|
|
2187
2218
|
rejectUnknown(raw.branch, BRANCH_KEYS, "branch", file);
|
|
2188
2219
|
rejectUnknown(raw.worktrees, /* @__PURE__ */ new Set(["copy", "prefix", "name"]), "worktrees", file);
|
|
2189
2220
|
rejectUnknown(raw.docs, /* @__PURE__ */ new Set(["triagePolicy", "designGuide"]), "docs", file);
|
|
2221
|
+
rejectUnknown(raw.sandbox, /* @__PURE__ */ new Set(["image", "packages", "setup"]), "sandbox", file);
|
|
2190
2222
|
if (raw.version === void 0) throw new RepoConfigError(`${file}: version is required`);
|
|
2191
2223
|
if (raw.version !== SPEC_VERSION) {
|
|
2192
2224
|
throw new RepoConfigError(
|
|
@@ -2243,6 +2275,7 @@ function parseRepoConfig(text2, file) {
|
|
|
2243
2275
|
);
|
|
2244
2276
|
}
|
|
2245
2277
|
}
|
|
2278
|
+
const sandbox = parseSandbox(raw.sandbox, file);
|
|
2246
2279
|
const platform2 = String(raw.platform ?? "any");
|
|
2247
2280
|
if (!isPlatformRequirement(platform2)) {
|
|
2248
2281
|
throw new RepoConfigError(
|
|
@@ -2371,6 +2404,7 @@ function parseRepoConfig(text2, file) {
|
|
|
2371
2404
|
branch,
|
|
2372
2405
|
worktrees: worktrees2,
|
|
2373
2406
|
docs,
|
|
2407
|
+
...sandbox ? { sandbox } : {},
|
|
2374
2408
|
platform: platform2,
|
|
2375
2409
|
shell: raw.shell ? String(raw.shell) : void 0,
|
|
2376
2410
|
hooks,
|
|
@@ -2489,6 +2523,8 @@ function resolveRepoConfig(repo, ship, dir) {
|
|
|
2489
2523
|
);
|
|
2490
2524
|
return {
|
|
2491
2525
|
version: SPEC_VERSION,
|
|
2526
|
+
// Repo-only, like platform: what a repo needs installed is a fact about it.
|
|
2527
|
+
...repo?.sandbox ? { sandbox: repo.sandbox } : {},
|
|
2492
2528
|
worktrees: {
|
|
2493
2529
|
// Not merged: which gitignored files a worktree needs is a fact about
|
|
2494
2530
|
// the repository and nothing else, so a ship cannot supply or override
|
|
@@ -3864,9 +3900,9 @@ function labelFor(crewHome, job) {
|
|
|
3864
3900
|
return crewHome;
|
|
3865
3901
|
}
|
|
3866
3902
|
})();
|
|
3867
|
-
const
|
|
3903
|
+
const hash2 = createHash3("sha1").update(real).digest("hex").slice(0, 8);
|
|
3868
3904
|
const stem = job === "release" ? "com.tablation.crew-release" : job === "passengers" ? "com.tablation.crew-passengers" : "com.tablation.crew";
|
|
3869
|
-
return `${stem}.${
|
|
3905
|
+
return `${stem}.${hash2}`;
|
|
3870
3906
|
}
|
|
3871
3907
|
function cronMarkerFor(label) {
|
|
3872
3908
|
return `# crew:${label} \u2014 managed by \`crew install\`, do not edit by hand`;
|
|
@@ -5313,11 +5349,433 @@ function attentionProblems(items, already) {
|
|
|
5313
5349
|
return items.filter((i) => !seen.has(i.key)).map((i) => ({ severity: "attention", kind: i.kind, message: i.message }));
|
|
5314
5350
|
}
|
|
5315
5351
|
|
|
5316
|
-
// src/
|
|
5352
|
+
// src/sandbox-image.ts
|
|
5353
|
+
import { createHash as createHash5 } from "crypto";
|
|
5354
|
+
import { spawn as spawn2 } from "child_process";
|
|
5355
|
+
import { mkdirSync as mkdirSync9, readFileSync as readFileSync14, renameSync as renameSync3, writeFileSync as writeFileSync10, rmSync as rmSync9 } from "fs";
|
|
5356
|
+
import { join as join15 } from "path";
|
|
5357
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
5358
|
+
|
|
5359
|
+
// src/passenger-containers.ts
|
|
5317
5360
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
5361
|
+
import { createHash as createHash4 } from "crypto";
|
|
5362
|
+
var realExec = (cmd, args, opts) => {
|
|
5363
|
+
try {
|
|
5364
|
+
return execFileSync3(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: opts?.timeoutMs });
|
|
5365
|
+
} catch (e) {
|
|
5366
|
+
const err3 = e;
|
|
5367
|
+
const why = [err3.stderr, err3.stdout].map((x) => (x ?? "").trim()).filter(Boolean).join(" / ");
|
|
5368
|
+
throw new Error(`${cmd} ${args.join(" ")}: ${why || err3.message.trim()}`);
|
|
5369
|
+
}
|
|
5370
|
+
};
|
|
5371
|
+
var DOCKER_INFO_TIMEOUT_MS = 1e4;
|
|
5372
|
+
function dockerAvailable(exec = realExec) {
|
|
5373
|
+
try {
|
|
5374
|
+
exec("docker", ["info"], { timeoutMs: DOCKER_INFO_TIMEOUT_MS });
|
|
5375
|
+
return true;
|
|
5376
|
+
} catch {
|
|
5377
|
+
return false;
|
|
5378
|
+
}
|
|
5379
|
+
}
|
|
5380
|
+
var PASSENGER_IMAGE_REPO = "tablation/crew-passenger-mcp";
|
|
5381
|
+
function passengerImageRef(cfg) {
|
|
5382
|
+
return process.env.CREW_PASSENGER_IMAGE || cfg.ship.passengerImage || `${PASSENGER_IMAGE_REPO}:${crewVersion(cfg.crewHome)}`;
|
|
5383
|
+
}
|
|
5384
|
+
function imagePresent(image, exec = realExec) {
|
|
5385
|
+
try {
|
|
5386
|
+
exec("docker", ["image", "inspect", image]);
|
|
5387
|
+
return true;
|
|
5388
|
+
} catch {
|
|
5389
|
+
return false;
|
|
5390
|
+
}
|
|
5391
|
+
}
|
|
5392
|
+
function ensureImage(image, exec = realExec) {
|
|
5393
|
+
if (!imagePresent(image, exec)) exec("docker", ["pull", image]);
|
|
5394
|
+
}
|
|
5395
|
+
function containerNameFor(workspaceId) {
|
|
5396
|
+
return `crew-passenger-${workspaceId}`;
|
|
5397
|
+
}
|
|
5398
|
+
function portFor(workspaceId, base = 28765, span = 1e3) {
|
|
5399
|
+
const digest = createHash4("sha256").update(workspaceId).digest();
|
|
5400
|
+
return base + digest.readUInt32BE(0) % span;
|
|
5401
|
+
}
|
|
5402
|
+
function mountsDigest(mounts, image) {
|
|
5403
|
+
const sorted2 = [...mounts].sort((a, b) => a.containerPath.localeCompare(b.containerPath));
|
|
5404
|
+
return createHash4("sha256").update(JSON.stringify({ mounts: sorted2, image })).digest("hex").slice(0, 16);
|
|
5405
|
+
}
|
|
5406
|
+
function planContainers(cfg) {
|
|
5407
|
+
const byWorkspace = /* @__PURE__ */ new Map();
|
|
5408
|
+
const image = passengerImageRef(cfg);
|
|
5409
|
+
for (const route2 of cfg.routes) {
|
|
5410
|
+
if (!route2.enabled || !route2.hostPassengers || !route2.resolved?.workspaceId) continue;
|
|
5411
|
+
const workspaceId = route2.resolved.workspaceId;
|
|
5412
|
+
const mounts = byWorkspace.get(workspaceId) ?? /* @__PURE__ */ new Map();
|
|
5413
|
+
for (const target of passengerCheckoutTargets(route2)) {
|
|
5414
|
+
mounts.set(target.name, { hostPath: target.dir, containerPath: `/workspace/${target.name}` });
|
|
5415
|
+
}
|
|
5416
|
+
byWorkspace.set(workspaceId, mounts);
|
|
5417
|
+
}
|
|
5418
|
+
return [...byWorkspace.entries()].filter(([, mounts]) => mounts.size > 0).map(([workspaceId, mounts]) => {
|
|
5419
|
+
const mountList = [...mounts.values()];
|
|
5420
|
+
return {
|
|
5421
|
+
workspaceId,
|
|
5422
|
+
containerName: containerNameFor(workspaceId),
|
|
5423
|
+
port: portFor(workspaceId),
|
|
5424
|
+
image,
|
|
5425
|
+
mounts: mountList,
|
|
5426
|
+
mountsHash: mountsDigest(mountList, image)
|
|
5427
|
+
};
|
|
5428
|
+
});
|
|
5429
|
+
}
|
|
5430
|
+
function listPassengerContainers(exec = realExec) {
|
|
5431
|
+
const out = exec("docker", ["ps", "-a", "--filter", "name=crew-passenger-", "--format", "{{.Names}} {{.State}}"]);
|
|
5432
|
+
return out.split("\n").map((l) => l.trim()).filter(Boolean).map((line) => {
|
|
5433
|
+
const [name, state] = line.split(" ");
|
|
5434
|
+
return { name, running: state === "running" };
|
|
5435
|
+
});
|
|
5436
|
+
}
|
|
5437
|
+
function currentMountsHash(name, exec) {
|
|
5438
|
+
try {
|
|
5439
|
+
const out = exec("docker", ["inspect", "--format", '{{ index .Config.Labels "crew.mounts.hash" }}', name]);
|
|
5440
|
+
const hash2 = out.trim();
|
|
5441
|
+
return hash2 || void 0;
|
|
5442
|
+
} catch {
|
|
5443
|
+
return void 0;
|
|
5444
|
+
}
|
|
5445
|
+
}
|
|
5446
|
+
function stopContainer(name, exec = realExec) {
|
|
5447
|
+
try {
|
|
5448
|
+
exec("docker", ["rm", "-f", name]);
|
|
5449
|
+
} catch {
|
|
5450
|
+
}
|
|
5451
|
+
}
|
|
5452
|
+
function startContainer(plan, tablationApiBaseUrl, sync, exec = realExec) {
|
|
5453
|
+
const args = [
|
|
5454
|
+
"run",
|
|
5455
|
+
"-d",
|
|
5456
|
+
"--name",
|
|
5457
|
+
plan.containerName,
|
|
5458
|
+
"--restart",
|
|
5459
|
+
"unless-stopped",
|
|
5460
|
+
"-p",
|
|
5461
|
+
`${plan.port}:8765`,
|
|
5462
|
+
"-e",
|
|
5463
|
+
`PASSENGER_MCP_WORKSPACE_ID=${plan.workspaceId}`,
|
|
5464
|
+
"-e",
|
|
5465
|
+
`TABLATION_API_BASE_URL=${tablationApiBaseUrl}`,
|
|
5466
|
+
"--label",
|
|
5467
|
+
`crew.mounts.hash=${plan.mountsHash}`
|
|
5468
|
+
];
|
|
5469
|
+
if (sync) {
|
|
5470
|
+
args.push(
|
|
5471
|
+
"--add-host",
|
|
5472
|
+
"host.docker.internal:host-gateway",
|
|
5473
|
+
"-e",
|
|
5474
|
+
`PASSENGER_SYNC_URL=http://${sync.host}:${sync.port}/sync`,
|
|
5475
|
+
"-e",
|
|
5476
|
+
`PASSENGER_SYNC_SECRET=${sync.secret}`,
|
|
5477
|
+
"-e",
|
|
5478
|
+
`PASSENGER_MCP_SECRET=${sync.secret}`
|
|
5479
|
+
);
|
|
5480
|
+
}
|
|
5481
|
+
for (const m of plan.mounts) args.push("-v", `${m.hostPath}:${m.containerPath}:ro`);
|
|
5482
|
+
args.push(plan.image);
|
|
5483
|
+
exec("docker", args);
|
|
5484
|
+
}
|
|
5485
|
+
function syncPassengerContainers(cfg, tablationApiBaseUrl, exec = realExec, syncEndpoints = /* @__PURE__ */ new Map()) {
|
|
5486
|
+
const plans = planContainers(cfg);
|
|
5487
|
+
const plannedByName = new Map(plans.map((p) => [p.containerName, p]));
|
|
5488
|
+
const existing = listPassengerContainers(exec);
|
|
5489
|
+
const existingByName = new Map(existing.map((c) => [c.name, c]));
|
|
5490
|
+
const started = [];
|
|
5491
|
+
const recreated = [];
|
|
5492
|
+
const stopped = [];
|
|
5493
|
+
const ensured = /* @__PURE__ */ new Set();
|
|
5494
|
+
const start = (plan, sync) => {
|
|
5495
|
+
if (!ensured.has(plan.image)) {
|
|
5496
|
+
ensureImage(plan.image, exec);
|
|
5497
|
+
ensured.add(plan.image);
|
|
5498
|
+
}
|
|
5499
|
+
startContainer(plan, tablationApiBaseUrl, sync, exec);
|
|
5500
|
+
};
|
|
5501
|
+
for (const plan of plans) {
|
|
5502
|
+
const sync = syncEndpoints.get(plan.workspaceId);
|
|
5503
|
+
const current = existingByName.get(plan.containerName);
|
|
5504
|
+
if (!current) {
|
|
5505
|
+
start(plan, sync);
|
|
5506
|
+
started.push(plan.containerName);
|
|
5507
|
+
} else if (!current.running) {
|
|
5508
|
+
stopContainer(plan.containerName, exec);
|
|
5509
|
+
start(plan, sync);
|
|
5510
|
+
started.push(plan.containerName);
|
|
5511
|
+
} else if (currentMountsHash(plan.containerName, exec) !== plan.mountsHash) {
|
|
5512
|
+
stopContainer(plan.containerName, exec);
|
|
5513
|
+
start(plan, sync);
|
|
5514
|
+
recreated.push(plan.containerName);
|
|
5515
|
+
}
|
|
5516
|
+
}
|
|
5517
|
+
for (const { name } of existing) {
|
|
5518
|
+
if (!plannedByName.has(name)) {
|
|
5519
|
+
stopContainer(name, exec);
|
|
5520
|
+
stopped.push(name);
|
|
5521
|
+
}
|
|
5522
|
+
}
|
|
5523
|
+
return { started, recreated, stopped };
|
|
5524
|
+
}
|
|
5525
|
+
function passengerCheckoutSyncSpecs(cfg, workspaceId) {
|
|
5526
|
+
const specs = [];
|
|
5527
|
+
for (const route2 of cfg.routes) {
|
|
5528
|
+
if (!route2.enabled || !route2.hostPassengers || !route2.resolved?.workspaceId) continue;
|
|
5529
|
+
if (workspaceId && route2.resolved.workspaceId !== workspaceId) continue;
|
|
5530
|
+
const routeWorkspaceId = route2.resolved.workspaceId;
|
|
5531
|
+
const ordinaryByName = new Map(reposOf(route2).map((t) => [t.name, t]));
|
|
5532
|
+
for (const target of passengerCheckoutTargets(route2)) {
|
|
5533
|
+
const ordinary = ordinaryByName.get(target.name);
|
|
5534
|
+
const override = route2.repoOverrides[target.name];
|
|
5535
|
+
const effective = resolveRepoConfig(
|
|
5536
|
+
ordinary ? loadRepoConfig(ordinary.dir) : null,
|
|
5537
|
+
{ branch: { ...route2.branch, ...override?.branch } },
|
|
5538
|
+
ordinary?.dir ?? target.dir
|
|
5539
|
+
);
|
|
5540
|
+
specs.push({
|
|
5541
|
+
name: target.name,
|
|
5542
|
+
workspaceId: routeWorkspaceId,
|
|
5543
|
+
dir: target.dir,
|
|
5544
|
+
remote: target.remote,
|
|
5545
|
+
gitRemoteName: effective.branch.remote,
|
|
5546
|
+
base: effective.branch.base,
|
|
5547
|
+
originUrl: ordinary ? originUrlOf(ordinary.dir, effective.branch.remote) : null,
|
|
5548
|
+
cloneScheme: cfg.ship.cloneScheme,
|
|
5549
|
+
...target.forgeHost ? { forgeHost: target.forgeHost } : {}
|
|
5550
|
+
});
|
|
5551
|
+
}
|
|
5552
|
+
}
|
|
5553
|
+
return specs;
|
|
5554
|
+
}
|
|
5555
|
+
function syncAllPassengerCheckouts(cfg) {
|
|
5556
|
+
return passengerCheckoutSyncSpecs(cfg).map((spec) => ({
|
|
5557
|
+
name: spec.name,
|
|
5558
|
+
workspaceId: spec.workspaceId,
|
|
5559
|
+
outcome: syncPassengerCheckout(spec.dir, spec.remote, spec.gitRemoteName, spec.base, { originUrl: spec.originUrl, scheme: spec.cloneScheme, host: spec.forgeHost })
|
|
5560
|
+
}));
|
|
5561
|
+
}
|
|
5562
|
+
|
|
5563
|
+
// src/sandbox-image.ts
|
|
5564
|
+
var SANDBOX_BASE_IMAGE = "nvcr.io/nvidia/base/ubuntu:24.04";
|
|
5565
|
+
var SANDBOX_HARNESS_VERSION = "0.1.6";
|
|
5566
|
+
var SANDBOX_UID = 1e3;
|
|
5567
|
+
var CREW_PREFIX = "/opt/crew";
|
|
5568
|
+
var SANDBOX_RETRY_AFTER_MS = 60 * 60 * 1e3;
|
|
5569
|
+
var KEEP_TAGS = 2;
|
|
5570
|
+
var CREDENTIAL_HELPER = `#!/bin/sh
|
|
5571
|
+
# git credential helper: answers from the environment the runner passes in, so
|
|
5572
|
+
# no token is ever written into an image layer.
|
|
5573
|
+
[ "$1" = get ] || exit 0
|
|
5574
|
+
[ -n "$CREW_FORGE_TOKEN" ] || exit 0
|
|
5575
|
+
echo "username=\${CREW_FORGE_USER:-x-access-token}"
|
|
5576
|
+
echo "password=$CREW_FORGE_TOKEN"
|
|
5577
|
+
`;
|
|
5578
|
+
var MCP_CONFIG = `${JSON.stringify({ mcpServers: {} }, null, 2)}
|
|
5579
|
+
`;
|
|
5580
|
+
var hash = (parts) => createHash5("sha256").update(JSON.stringify(parts)).digest("hex").slice(0, 12);
|
|
5581
|
+
var slug = (s) => s.toLowerCase().replace(/[^a-z0-9_.-]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
|
|
5582
|
+
var run = (cmd) => `RUN ["/bin/bash", "-c", ${JSON.stringify(`set -euo pipefail; ${cmd}`)}]`;
|
|
5583
|
+
var CREW_ENV = {
|
|
5584
|
+
CREW_SANDBOX: "1",
|
|
5585
|
+
CREW_CACHE_DIR: `${CREW_PREFIX}/cache`,
|
|
5586
|
+
XDG_CACHE_HOME: `${CREW_PREFIX}/cache`,
|
|
5587
|
+
npm_config_cache: `${CREW_PREFIX}/cache/npm`,
|
|
5588
|
+
npm_config_store_dir: `${CREW_PREFIX}/pnpm-store`,
|
|
5589
|
+
PNPM_HOME: `${CREW_PREFIX}/pnpm`
|
|
5590
|
+
};
|
|
5591
|
+
function baseLayer() {
|
|
5592
|
+
const dockerfile = [
|
|
5593
|
+
`FROM ${SANDBOX_BASE_IMAGE}`,
|
|
5594
|
+
`LABEL io.crew.sandbox.layer="base" io.crew.sandbox.harness="${SANDBOX_HARNESS_VERSION}"`,
|
|
5595
|
+
""
|
|
5596
|
+
].join("\n");
|
|
5597
|
+
return { name: "base", tag: `crew-sandbox-base:${hash({ SANDBOX_BASE_IMAGE, SANDBOX_HARNESS_VERSION })}`, dockerfile, files: {} };
|
|
5598
|
+
}
|
|
5599
|
+
function crewLayer(base) {
|
|
5600
|
+
const files = { "credential-helper": CREDENTIAL_HELPER, "mcp.json": MCP_CONFIG };
|
|
5601
|
+
const env = Object.entries(CREW_ENV).map(([k, v]) => `${k}=${v}`).join(" ");
|
|
5602
|
+
const dockerfile = [
|
|
5603
|
+
`FROM ${base.tag}`,
|
|
5604
|
+
"USER root",
|
|
5605
|
+
run("apt-get update && apt-get install -y --no-install-recommends ca-certificates curl git && rm -rf /var/lib/apt/lists/*"),
|
|
5606
|
+
`COPY credential-helper ${CREW_PREFIX}/bin/credential-helper`,
|
|
5607
|
+
`COPY mcp.json ${CREW_PREFIX}/mcp/mcp.json`,
|
|
5608
|
+
run(`chmod 0755 ${CREW_PREFIX}/bin/credential-helper && git config --system credential.helper ${CREW_PREFIX}/bin/credential-helper`),
|
|
5609
|
+
run(`mkdir -p ${CREW_PREFIX}/cache ${CREW_PREFIX}/pnpm-store ${CREW_PREFIX}/pnpm /sandbox && chown -R ${SANDBOX_UID}:${SANDBOX_UID} ${CREW_PREFIX} /sandbox`),
|
|
5610
|
+
`ENV ${env} PATH=${CREW_PREFIX}/pnpm:${CREW_PREFIX}/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin`,
|
|
5611
|
+
`LABEL io.crew.sandbox.layer="crew"`,
|
|
5612
|
+
""
|
|
5613
|
+
].join("\n");
|
|
5614
|
+
return { name: "crew", tag: `crew-sandbox-crew:${hash({ base: base.tag, dockerfile, files })}`, dockerfile, files };
|
|
5615
|
+
}
|
|
5616
|
+
function repoLayer(repo, decl, crew) {
|
|
5617
|
+
const env = Object.entries(CREW_ENV).map(([k, v]) => `${k}=${v}`).join(" ");
|
|
5618
|
+
const lines2 = [];
|
|
5619
|
+
if (decl.image) {
|
|
5620
|
+
lines2.push(
|
|
5621
|
+
`FROM ${crew.tag} AS crew`,
|
|
5622
|
+
`FROM ${decl.image}`,
|
|
5623
|
+
"USER root",
|
|
5624
|
+
`COPY --from=crew ${CREW_PREFIX} ${CREW_PREFIX}`,
|
|
5625
|
+
`ENV ${env} PATH=${CREW_PREFIX}/pnpm:${CREW_PREFIX}/bin:$PATH`
|
|
5626
|
+
);
|
|
5627
|
+
} else {
|
|
5628
|
+
lines2.push(`FROM ${crew.tag}`, "USER root");
|
|
5629
|
+
}
|
|
5630
|
+
if (decl.packages.length > 0) {
|
|
5631
|
+
lines2.push(run(`apt-get update && apt-get install -y --no-install-recommends ${decl.packages.join(" ")} && rm -rf /var/lib/apt/lists/*`));
|
|
5632
|
+
}
|
|
5633
|
+
lines2.push(
|
|
5634
|
+
run(`mkdir -p /sandbox && chown -R ${SANDBOX_UID}:${SANDBOX_UID} /sandbox ${CREW_PREFIX}`),
|
|
5635
|
+
`USER ${SANDBOX_UID}`,
|
|
5636
|
+
"WORKDIR /sandbox"
|
|
5637
|
+
);
|
|
5638
|
+
for (const step of decl.setup) lines2.push(run(step));
|
|
5639
|
+
lines2.push(`LABEL io.crew.sandbox.layer="repo" io.crew.sandbox.repo=${JSON.stringify(repo)}`, "");
|
|
5640
|
+
const dockerfile = lines2.join("\n");
|
|
5641
|
+
return { name: "repo", tag: `crew-sandbox/${slug(repo)}:${hash({ crew: crew.tag, dockerfile })}`, dockerfile, files: {} };
|
|
5642
|
+
}
|
|
5643
|
+
function planSandbox(repo, decl) {
|
|
5644
|
+
const base = baseLayer();
|
|
5645
|
+
const crew = crewLayer(base);
|
|
5646
|
+
const layer = repoLayer(repo, decl, crew);
|
|
5647
|
+
return { repo, tag: layer.tag, layers: [base, crew, layer] };
|
|
5648
|
+
}
|
|
5649
|
+
var sandboxStatePath = (stateDir) => join15(stateDir, "sandbox-images.json");
|
|
5650
|
+
function readSandboxState(stateDir) {
|
|
5651
|
+
try {
|
|
5652
|
+
const v = JSON.parse(readFileSync14(sandboxStatePath(stateDir), "utf8"));
|
|
5653
|
+
return v && typeof v === "object" && !Array.isArray(v) ? v : {};
|
|
5654
|
+
} catch {
|
|
5655
|
+
return {};
|
|
5656
|
+
}
|
|
5657
|
+
}
|
|
5658
|
+
function writeSandboxState(stateDir, state) {
|
|
5659
|
+
mkdirSync9(stateDir, { recursive: true });
|
|
5660
|
+
const file = sandboxStatePath(stateDir);
|
|
5661
|
+
writeFileSync10(`${file}.tmp`, `${JSON.stringify(state, null, 2)}
|
|
5662
|
+
`);
|
|
5663
|
+
renameSync3(`${file}.tmp`, file);
|
|
5664
|
+
}
|
|
5665
|
+
function sandboxImageStatus(state, plan) {
|
|
5666
|
+
const s = state[plan.repo];
|
|
5667
|
+
const current = s?.tags[0];
|
|
5668
|
+
if (s?.failure && s.failure.tag === plan.tag) {
|
|
5669
|
+
return { status: "failed", detail: `last build of ${plan.tag} failed at ${s.failure.at}; log ${s.failure.logPath}${current ? `; running ${current.tag}` : ""}` };
|
|
5670
|
+
}
|
|
5671
|
+
if (!current) return { status: "missing", detail: `no image built yet (wants ${plan.tag}); run \`crew sandbox build\`` };
|
|
5672
|
+
if (current.tag !== plan.tag) return { status: "stale", detail: `running ${current.tag}, declaration now builds ${plan.tag}` };
|
|
5673
|
+
return { status: "ok", detail: `${current.tag} built ${current.builtAt}` };
|
|
5674
|
+
}
|
|
5675
|
+
var spawnBuildRunner = (engine = "docker") => (args, logPath) => new Promise((resolve7) => {
|
|
5676
|
+
const child = spawn2(engine, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
5677
|
+
const chunks = [];
|
|
5678
|
+
child.stdout.on("data", (d) => chunks.push(d));
|
|
5679
|
+
child.stderr.on("data", (d) => chunks.push(d));
|
|
5680
|
+
const done = (code, extra = "") => {
|
|
5681
|
+
writeFileSync10(logPath, `$ ${engine} ${args.join(" ")}
|
|
5682
|
+
${Buffer.concat(chunks).toString("utf8")}${extra}`, { flag: "a" });
|
|
5683
|
+
resolve7(code);
|
|
5684
|
+
};
|
|
5685
|
+
child.on("error", (e) => done(127, `${e.message}
|
|
5686
|
+
`));
|
|
5687
|
+
child.on("close", (code) => done(code ?? 1));
|
|
5688
|
+
});
|
|
5689
|
+
var engineHasImage = (engine = "docker", exec = realExec) => (tag) => {
|
|
5690
|
+
try {
|
|
5691
|
+
exec(engine, ["image", "inspect", tag], { timeoutMs: 2e4 });
|
|
5692
|
+
return true;
|
|
5693
|
+
} catch {
|
|
5694
|
+
return false;
|
|
5695
|
+
}
|
|
5696
|
+
};
|
|
5697
|
+
var engineRemoveImage = (engine = "docker", exec = realExec) => (tag) => {
|
|
5698
|
+
try {
|
|
5699
|
+
exec(engine, ["image", "rm", tag], { timeoutMs: 6e4 });
|
|
5700
|
+
} catch {
|
|
5701
|
+
}
|
|
5702
|
+
};
|
|
5703
|
+
var sandboxLogDir = (stateDir) => join15(stateDir, "sandbox-logs");
|
|
5704
|
+
async function buildSandboxImage(plan, deps) {
|
|
5705
|
+
const now = deps.now ?? (() => /* @__PURE__ */ new Date());
|
|
5706
|
+
const state = readSandboxState(deps.stateDir);
|
|
5707
|
+
const entry = state[plan.repo] ?? { tags: [] };
|
|
5708
|
+
if (entry.tags[0]?.tag === plan.tag && deps.hasImage(plan.tag)) {
|
|
5709
|
+
return { kind: "reused", tag: plan.tag };
|
|
5710
|
+
}
|
|
5711
|
+
const logDir = sandboxLogDir(deps.stateDir);
|
|
5712
|
+
mkdirSync9(logDir, { recursive: true });
|
|
5713
|
+
const logPath = join15(logDir, `${slug(plan.repo)}-${now().toISOString().replace(/[:.]/g, "-")}.log`);
|
|
5714
|
+
const builtLayers = [];
|
|
5715
|
+
for (const layer of plan.layers) {
|
|
5716
|
+
if (deps.hasImage(layer.tag)) continue;
|
|
5717
|
+
const ctx = join15(tmpdir2(), `crew-sandbox-${hash([layer.tag, process.pid, Date.now()])}`);
|
|
5718
|
+
mkdirSync9(ctx, { recursive: true });
|
|
5719
|
+
try {
|
|
5720
|
+
writeFileSync10(join15(ctx, "Dockerfile"), layer.dockerfile);
|
|
5721
|
+
for (const [name, content] of Object.entries(layer.files)) writeFileSync10(join15(ctx, name), content);
|
|
5722
|
+
const code = await deps.run(["build", "--tag", layer.tag, "--file", join15(ctx, "Dockerfile"), ctx], logPath);
|
|
5723
|
+
if (code !== 0) {
|
|
5724
|
+
state[plan.repo] = { ...entry, failure: { tag: plan.tag, at: now().toISOString(), logPath } };
|
|
5725
|
+
writeSandboxState(deps.stateDir, state);
|
|
5726
|
+
return { kind: "failed", tag: plan.tag, logPath, layer: layer.name };
|
|
5727
|
+
}
|
|
5728
|
+
builtLayers.push(layer.name);
|
|
5729
|
+
} finally {
|
|
5730
|
+
rmSync9(ctx, { recursive: true, force: true });
|
|
5731
|
+
}
|
|
5732
|
+
}
|
|
5733
|
+
const tags = [{ tag: plan.tag, builtAt: now().toISOString() }, ...entry.tags.filter((t) => t.tag !== plan.tag)];
|
|
5734
|
+
const dropped = tags.slice(KEEP_TAGS);
|
|
5735
|
+
state[plan.repo] = { tags: tags.slice(0, KEEP_TAGS) };
|
|
5736
|
+
writeSandboxState(deps.stateDir, state);
|
|
5737
|
+
for (const t of dropped) deps.removeImage(t.tag);
|
|
5738
|
+
return { kind: "built", tag: plan.tag, builtLayers };
|
|
5739
|
+
}
|
|
5740
|
+
var sandboxFailedKey = (repo) => `sandbox_build_failed:${repo}`;
|
|
5741
|
+
function sandboxFailedItem(repo, tag, layer, logPath, nowIso = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
5742
|
+
return {
|
|
5743
|
+
kind: "sandbox_build_failed",
|
|
5744
|
+
key: sandboxFailedKey(repo),
|
|
5745
|
+
since: nowIso,
|
|
5746
|
+
message: `${repo}: sandbox image ${tag} failed to build at the ${layer} layer (log: ${logPath}); sandboxes keep running the previous image. Retries automatically after the declaration changes or in an hour; \`crew sandbox build\` forces it`
|
|
5747
|
+
};
|
|
5748
|
+
}
|
|
5749
|
+
async function sandboxBuildTick(repos, deps, force = false) {
|
|
5750
|
+
const nowMs = (deps.now ?? (() => /* @__PURE__ */ new Date()))().getTime();
|
|
5751
|
+
for (const r of repos) {
|
|
5752
|
+
const plan = planSandbox(r.name, r.decl);
|
|
5753
|
+
const state = readSandboxState(deps.stateDir)[r.name];
|
|
5754
|
+
const upToDate = state?.tags[0]?.tag === plan.tag && deps.hasImage(plan.tag);
|
|
5755
|
+
if (upToDate) {
|
|
5756
|
+
deps.clear(sandboxFailedKey(r.name));
|
|
5757
|
+
continue;
|
|
5758
|
+
}
|
|
5759
|
+
if (!force && state?.failure?.tag === plan.tag && nowMs - Date.parse(state.failure.at) < SANDBOX_RETRY_AFTER_MS) continue;
|
|
5760
|
+
deps.log(`sandbox image for ${r.name}: building ${plan.tag}`);
|
|
5761
|
+
const out = await buildSandboxImage(plan, deps);
|
|
5762
|
+
if (out.kind === "failed") {
|
|
5763
|
+
deps.raise(sandboxFailedItem(r.name, out.tag, out.layer, out.logPath));
|
|
5764
|
+
deps.log(`sandbox image for ${r.name}: build failed at the ${out.layer} layer, log ${out.logPath}`);
|
|
5765
|
+
} else {
|
|
5766
|
+
deps.clear(sandboxFailedKey(r.name));
|
|
5767
|
+
deps.log(`sandbox image for ${r.name}: ${out.kind === "built" ? `built ${out.tag}` : `${out.tag} already built`}`);
|
|
5768
|
+
}
|
|
5769
|
+
return r.name;
|
|
5770
|
+
}
|
|
5771
|
+
return null;
|
|
5772
|
+
}
|
|
5773
|
+
|
|
5774
|
+
// src/doctor-checks.ts
|
|
5775
|
+
import { execFileSync as execFileSync4 } from "child_process";
|
|
5318
5776
|
var PROBE_BRANCH = "crew-probe-branch";
|
|
5319
5777
|
var realGit = (cwd, args, env) => {
|
|
5320
|
-
|
|
5778
|
+
execFileSync4("git", args, { cwd, env, stdio: ["ignore", "pipe", "pipe"], timeout: 3e4 });
|
|
5321
5779
|
};
|
|
5322
5780
|
function isSshRemote(url) {
|
|
5323
5781
|
return /^ssh:\/\//.test(url) || /^[\w.-]+@[\w.-]+:/.test(url);
|
|
@@ -5325,12 +5783,12 @@ function isSshRemote(url) {
|
|
|
5325
5783
|
function pushAuthHint(remoteUrl) {
|
|
5326
5784
|
return isSshRemote(remoteUrl) ? "make sure an SSH key is loaded for this host (ssh-add), and that the scheduler user can reach it" : "run `gh auth login`, then `gh auth setup-git`";
|
|
5327
5785
|
}
|
|
5328
|
-
function probePushAuth(o,
|
|
5786
|
+
function probePushAuth(o, run3 = realGit, baseEnv = process.env) {
|
|
5329
5787
|
if (o.offline) return { state: "skipped", detail: "skipped (--offline)" };
|
|
5330
5788
|
if (!o.remoteUrl) return { state: "skipped", detail: `no remote "${o.remote}" configured` };
|
|
5331
5789
|
const env = { ...baseEnv, GIT_TERMINAL_PROMPT: "0", GCM_INTERACTIVE: "never", GIT_SSH_COMMAND: `${baseEnv.GIT_SSH_COMMAND ?? "ssh"} -o BatchMode=yes` };
|
|
5332
5790
|
try {
|
|
5333
|
-
|
|
5791
|
+
run3(o.dir, ["push", "--dry-run", o.remote, `HEAD:refs/heads/${PROBE_BRANCH}`], env);
|
|
5334
5792
|
return { state: "ok" };
|
|
5335
5793
|
} catch (e) {
|
|
5336
5794
|
const err3 = e;
|
|
@@ -5346,7 +5804,7 @@ function probePushAuth(o, run2 = realGit, baseEnv = process.env) {
|
|
|
5346
5804
|
};
|
|
5347
5805
|
}
|
|
5348
5806
|
try {
|
|
5349
|
-
|
|
5807
|
+
run3(o.dir, ["ls-remote", "--exit-code", o.remote, "HEAD"], env);
|
|
5350
5808
|
return {
|
|
5351
5809
|
state: "failed",
|
|
5352
5810
|
detail: `the token for forge ${f.host} is read-only (fetch works, push refused): ${detail}`,
|
|
@@ -5438,31 +5896,31 @@ function roleCapWarnings(ship) {
|
|
|
5438
5896
|
}
|
|
5439
5897
|
|
|
5440
5898
|
// src/selftest.ts
|
|
5441
|
-
import { spawn as
|
|
5442
|
-
import { existsSync as existsSync22, mkdirSync as
|
|
5443
|
-
import { tmpdir as
|
|
5444
|
-
import { join as
|
|
5899
|
+
import { spawn as spawn5 } from "child_process";
|
|
5900
|
+
import { existsSync as existsSync22, mkdirSync as mkdirSync14, mkdtempSync as mkdtempSync2, readFileSync as readFileSync21, rmSync as rmSync14, writeFileSync as writeFileSync16 } from "fs";
|
|
5901
|
+
import { tmpdir as tmpdir4 } from "os";
|
|
5902
|
+
import { join as join25 } from "path";
|
|
5445
5903
|
|
|
5446
5904
|
// src/agent.ts
|
|
5447
5905
|
import { spawnSync as spawnSync5 } from "child_process";
|
|
5448
|
-
import { readFileSync as
|
|
5906
|
+
import { readFileSync as readFileSync20, existsSync as existsSync21, statSync as statSync7, mkdirSync as mkdirSync13, writeFileSync as writeFileSync15, createWriteStream, openSync as openSync3, closeSync as closeSync3, readSync } from "fs";
|
|
5449
5907
|
import { StringDecoder } from "string_decoder";
|
|
5450
5908
|
import { finished } from "stream/promises";
|
|
5451
|
-
import { join as
|
|
5452
|
-
import { createHash as
|
|
5909
|
+
import { join as join24, dirname as dirname8, basename as basename8 } from "path";
|
|
5910
|
+
import { createHash as createHash6 } from "crypto";
|
|
5453
5911
|
|
|
5454
5912
|
// src/state.ts
|
|
5455
|
-
import { readFileSync as
|
|
5456
|
-
import { join as
|
|
5913
|
+
import { readFileSync as readFileSync15, writeFileSync as writeFileSync11, existsSync as existsSync14, mkdirSync as mkdirSync10, rmSync as rmSync10, statSync as statSync6, readdirSync as readdirSync5 } from "fs";
|
|
5914
|
+
import { join as join16 } from "path";
|
|
5457
5915
|
var EPOCH = "1970-01-01T00:00:00.000Z";
|
|
5458
5916
|
var State = class _State {
|
|
5459
5917
|
dir;
|
|
5460
5918
|
constructor(dir) {
|
|
5461
5919
|
this.dir = dir;
|
|
5462
|
-
|
|
5920
|
+
mkdirSync10(dir, { recursive: true });
|
|
5463
5921
|
}
|
|
5464
5922
|
path(...parts) {
|
|
5465
|
-
return
|
|
5923
|
+
return join16(this.dir, ...parts);
|
|
5466
5924
|
}
|
|
5467
5925
|
/**
|
|
5468
5926
|
* The timestamp every "new since last poll" check is measured against.
|
|
@@ -5473,14 +5931,14 @@ var State = class _State {
|
|
|
5473
5931
|
*/
|
|
5474
5932
|
watermark() {
|
|
5475
5933
|
try {
|
|
5476
|
-
const v =
|
|
5934
|
+
const v = readFileSync15(this.path(".poll-watermark"), "utf8").trim();
|
|
5477
5935
|
return v || EPOCH;
|
|
5478
5936
|
} catch {
|
|
5479
5937
|
return EPOCH;
|
|
5480
5938
|
}
|
|
5481
5939
|
}
|
|
5482
5940
|
setWatermark(at) {
|
|
5483
|
-
|
|
5941
|
+
writeFileSync11(this.path(".poll-watermark"), `${at}
|
|
5484
5942
|
`);
|
|
5485
5943
|
}
|
|
5486
5944
|
/**
|
|
@@ -5507,10 +5965,10 @@ var State = class _State {
|
|
|
5507
5965
|
return new Set(roles.filter((r) => this.isRolePaused(r)));
|
|
5508
5966
|
}
|
|
5509
5967
|
pause(role) {
|
|
5510
|
-
|
|
5968
|
+
writeFileSync11(role ? this.path(`.role-paused-${role}`) : this.path(".crew-paused"), "");
|
|
5511
5969
|
}
|
|
5512
5970
|
resume(role) {
|
|
5513
|
-
|
|
5971
|
+
rmSync10(role ? this.path(`.role-paused-${role}`) : this.path(".crew-paused"), { force: true });
|
|
5514
5972
|
}
|
|
5515
5973
|
/**
|
|
5516
5974
|
* The release phase (merge, deploy, release) has its own sentinel, one
|
|
@@ -5521,8 +5979,8 @@ var State = class _State {
|
|
|
5521
5979
|
return existsSync14(this.path(".release-paused"));
|
|
5522
5980
|
}
|
|
5523
5981
|
setReleasePaused(paused) {
|
|
5524
|
-
if (paused)
|
|
5525
|
-
else
|
|
5982
|
+
if (paused) writeFileSync11(this.path(".release-paused"), "");
|
|
5983
|
+
else rmSync10(this.path(".release-paused"), { force: true });
|
|
5526
5984
|
}
|
|
5527
5985
|
/**
|
|
5528
5986
|
* Why a release cycle must not run right now, or null when it may. Every
|
|
@@ -5559,7 +6017,7 @@ var State = class _State {
|
|
|
5559
6017
|
const name = _State.safe(rawName);
|
|
5560
6018
|
const file = this.path(`.${name}.lock`);
|
|
5561
6019
|
try {
|
|
5562
|
-
const existing = Number.parseInt(
|
|
6020
|
+
const existing = Number.parseInt(readFileSync15(file, "utf8").trim(), 10);
|
|
5563
6021
|
if (Number.isFinite(existing) && existing > 0) {
|
|
5564
6022
|
try {
|
|
5565
6023
|
process.kill(existing, 0);
|
|
@@ -5569,13 +6027,13 @@ var State = class _State {
|
|
|
5569
6027
|
}
|
|
5570
6028
|
} catch {
|
|
5571
6029
|
}
|
|
5572
|
-
|
|
6030
|
+
writeFileSync11(file, `${process.pid}
|
|
5573
6031
|
`);
|
|
5574
6032
|
return {
|
|
5575
6033
|
ok: true,
|
|
5576
6034
|
release: () => {
|
|
5577
6035
|
try {
|
|
5578
|
-
|
|
6036
|
+
rmSync10(file, { force: true });
|
|
5579
6037
|
} catch {
|
|
5580
6038
|
}
|
|
5581
6039
|
}
|
|
@@ -5597,7 +6055,7 @@ var State = class _State {
|
|
|
5597
6055
|
if (!/^\.release-.+\.lock$/.test(n)) continue;
|
|
5598
6056
|
let pid = 0;
|
|
5599
6057
|
try {
|
|
5600
|
-
pid = Number.parseInt(
|
|
6058
|
+
pid = Number.parseInt(readFileSync15(this.path(n), "utf8").trim(), 10);
|
|
5601
6059
|
} catch {
|
|
5602
6060
|
continue;
|
|
5603
6061
|
}
|
|
@@ -5683,7 +6141,7 @@ var State = class _State {
|
|
|
5683
6141
|
ticketInFlight(recordId) {
|
|
5684
6142
|
const file = this.path(`.${_State.safe(`crew-ticket-${recordId}`)}.lock`);
|
|
5685
6143
|
try {
|
|
5686
|
-
const pid = Number.parseInt(
|
|
6144
|
+
const pid = Number.parseInt(readFileSync15(file, "utf8").trim(), 10);
|
|
5687
6145
|
if (!Number.isFinite(pid) || pid <= 0) return false;
|
|
5688
6146
|
process.kill(pid, 0);
|
|
5689
6147
|
return true;
|
|
@@ -5717,7 +6175,7 @@ var State = class _State {
|
|
|
5717
6175
|
const heldReported = `.release-held-${suffix}`;
|
|
5718
6176
|
const read = (f) => {
|
|
5719
6177
|
try {
|
|
5720
|
-
return
|
|
6178
|
+
return readFileSync15(this.path(f), "utf8").trim() || null;
|
|
5721
6179
|
} catch {
|
|
5722
6180
|
return null;
|
|
5723
6181
|
}
|
|
@@ -5726,12 +6184,12 @@ var State = class _State {
|
|
|
5726
6184
|
blockedCount: () => Number.parseInt(read(blocked) ?? "", 10) || 0,
|
|
5727
6185
|
noteBlocked: () => {
|
|
5728
6186
|
const n = (Number.parseInt(read(blocked) ?? "", 10) || 0) + 1;
|
|
5729
|
-
|
|
6187
|
+
writeFileSync11(this.path(blocked), `${n}
|
|
5730
6188
|
`);
|
|
5731
6189
|
return n;
|
|
5732
6190
|
},
|
|
5733
6191
|
clearBlocked: () => {
|
|
5734
|
-
|
|
6192
|
+
rmSync10(this.path(blocked), { force: true });
|
|
5735
6193
|
},
|
|
5736
6194
|
/**
|
|
5737
6195
|
* The commit a deploy last failed on.
|
|
@@ -5743,11 +6201,11 @@ var State = class _State {
|
|
|
5743
6201
|
*/
|
|
5744
6202
|
deployFailedSha: () => read(failed),
|
|
5745
6203
|
noteDeployFailed: (sha) => {
|
|
5746
|
-
|
|
6204
|
+
writeFileSync11(this.path(failed), `${sha}
|
|
5747
6205
|
`);
|
|
5748
6206
|
},
|
|
5749
6207
|
clearDeployFailed: () => {
|
|
5750
|
-
|
|
6208
|
+
rmSync10(this.path(failed), { force: true });
|
|
5751
6209
|
},
|
|
5752
6210
|
/**
|
|
5753
6211
|
* Consecutive test-gate failures on the same head (CREW-1368). A failure
|
|
@@ -5757,31 +6215,31 @@ var State = class _State {
|
|
|
5757
6215
|
noteTestGateFailed: (sha) => {
|
|
5758
6216
|
const [prev, n] = (read(gateFailed) ?? "").split(" ");
|
|
5759
6217
|
const next = prev === sha ? (Number.parseInt(n ?? "", 10) || 0) + 1 : 1;
|
|
5760
|
-
|
|
6218
|
+
writeFileSync11(this.path(gateFailed), `${sha} ${next}
|
|
5761
6219
|
`);
|
|
5762
6220
|
return next;
|
|
5763
6221
|
},
|
|
5764
6222
|
clearTestGateFailed: () => {
|
|
5765
|
-
|
|
6223
|
+
rmSync10(this.path(gateFailed), { force: true });
|
|
5766
6224
|
},
|
|
5767
6225
|
/** Whether the board was already told about this head's red gate. */
|
|
5768
6226
|
heldReported: () => {
|
|
5769
6227
|
try {
|
|
5770
|
-
return
|
|
6228
|
+
return readFileSync15(this.path(heldReported), "utf8").replace(/\n$/, "") || null;
|
|
5771
6229
|
} catch {
|
|
5772
6230
|
return null;
|
|
5773
6231
|
}
|
|
5774
6232
|
},
|
|
5775
6233
|
noteHeldReported: (signature) => {
|
|
5776
|
-
|
|
6234
|
+
writeFileSync11(this.path(heldReported), `${signature}
|
|
5777
6235
|
`);
|
|
5778
6236
|
},
|
|
5779
6237
|
clearHeldReported: () => {
|
|
5780
|
-
|
|
6238
|
+
rmSync10(this.path(heldReported), { force: true });
|
|
5781
6239
|
},
|
|
5782
6240
|
testGateReported: (sha) => read(gateReported) === sha,
|
|
5783
6241
|
noteTestGateReported: (sha) => {
|
|
5784
|
-
|
|
6242
|
+
writeFileSync11(this.path(gateReported), `${sha}
|
|
5785
6243
|
`);
|
|
5786
6244
|
}
|
|
5787
6245
|
};
|
|
@@ -5804,13 +6262,13 @@ var State = class _State {
|
|
|
5804
6262
|
const ticketStreakFile = `.fairness-ticket-streak-${suffix}`;
|
|
5805
6263
|
const read = (f) => {
|
|
5806
6264
|
try {
|
|
5807
|
-
return
|
|
6265
|
+
return readFileSync15(this.path(f), "utf8").trim() || null;
|
|
5808
6266
|
} catch {
|
|
5809
6267
|
return null;
|
|
5810
6268
|
}
|
|
5811
6269
|
};
|
|
5812
6270
|
const clear2 = () => {
|
|
5813
|
-
for (const f of [streakFile, ticketFile, sinceFile, ticketStreakFile])
|
|
6271
|
+
for (const f of [streakFile, ticketFile, sinceFile, ticketStreakFile]) rmSync10(this.path(f), { force: true });
|
|
5814
6272
|
};
|
|
5815
6273
|
return {
|
|
5816
6274
|
/** Consecutive cycles this route had actionable work and did not win. */
|
|
@@ -5837,18 +6295,18 @@ var State = class _State {
|
|
|
5837
6295
|
clear2();
|
|
5838
6296
|
return;
|
|
5839
6297
|
}
|
|
5840
|
-
|
|
6298
|
+
writeFileSync11(this.path(streakFile), `${this.readInt(streakFile) + 1}
|
|
5841
6299
|
`);
|
|
5842
6300
|
const prior = read(ticketFile);
|
|
5843
6301
|
if (topTicket && topTicket === prior) {
|
|
5844
|
-
|
|
6302
|
+
writeFileSync11(this.path(ticketStreakFile), `${this.readInt(ticketStreakFile) + 1}
|
|
5845
6303
|
`);
|
|
5846
6304
|
} else if (topTicket) {
|
|
5847
|
-
|
|
6305
|
+
writeFileSync11(this.path(ticketFile), `${topTicket}
|
|
5848
6306
|
`);
|
|
5849
|
-
|
|
6307
|
+
writeFileSync11(this.path(sinceFile), `${(/* @__PURE__ */ new Date()).toISOString()}
|
|
5850
6308
|
`);
|
|
5851
|
-
|
|
6309
|
+
writeFileSync11(this.path(ticketStreakFile), "1\n");
|
|
5852
6310
|
}
|
|
5853
6311
|
}
|
|
5854
6312
|
};
|
|
@@ -5869,14 +6327,14 @@ var State = class _State {
|
|
|
5869
6327
|
return {
|
|
5870
6328
|
previous: () => {
|
|
5871
6329
|
try {
|
|
5872
|
-
const parsed = JSON.parse(
|
|
6330
|
+
const parsed = JSON.parse(readFileSync15(this.path(file), "utf8"));
|
|
5873
6331
|
return parsed && typeof parsed === "object" ? parsed : {};
|
|
5874
6332
|
} catch {
|
|
5875
6333
|
return {};
|
|
5876
6334
|
}
|
|
5877
6335
|
},
|
|
5878
6336
|
persist: (next) => {
|
|
5879
|
-
|
|
6337
|
+
writeFileSync11(this.path(file), JSON.stringify(next));
|
|
5880
6338
|
}
|
|
5881
6339
|
};
|
|
5882
6340
|
}
|
|
@@ -5892,14 +6350,14 @@ var State = class _State {
|
|
|
5892
6350
|
return {
|
|
5893
6351
|
previous: () => {
|
|
5894
6352
|
try {
|
|
5895
|
-
const parsed = JSON.parse(
|
|
6353
|
+
const parsed = JSON.parse(readFileSync15(this.path(file), "utf8"));
|
|
5896
6354
|
return Array.isArray(parsed) ? parsed.filter((x) => typeof x === "string") : [];
|
|
5897
6355
|
} catch {
|
|
5898
6356
|
return [];
|
|
5899
6357
|
}
|
|
5900
6358
|
},
|
|
5901
6359
|
persist: (next) => {
|
|
5902
|
-
|
|
6360
|
+
writeFileSync11(this.path(file), JSON.stringify(next));
|
|
5903
6361
|
}
|
|
5904
6362
|
};
|
|
5905
6363
|
}
|
|
@@ -5913,14 +6371,14 @@ var State = class _State {
|
|
|
5913
6371
|
return {
|
|
5914
6372
|
previous: () => {
|
|
5915
6373
|
try {
|
|
5916
|
-
const parsed = JSON.parse(
|
|
6374
|
+
const parsed = JSON.parse(readFileSync15(this.path(file), "utf8"));
|
|
5917
6375
|
return Array.isArray(parsed) ? parsed : [];
|
|
5918
6376
|
} catch {
|
|
5919
6377
|
return [];
|
|
5920
6378
|
}
|
|
5921
6379
|
},
|
|
5922
6380
|
persist: (next) => {
|
|
5923
|
-
|
|
6381
|
+
writeFileSync11(this.path(file), JSON.stringify(next));
|
|
5924
6382
|
}
|
|
5925
6383
|
};
|
|
5926
6384
|
}
|
|
@@ -5933,7 +6391,7 @@ var State = class _State {
|
|
|
5933
6391
|
const file = ".release-last-run";
|
|
5934
6392
|
return {
|
|
5935
6393
|
touch: () => {
|
|
5936
|
-
|
|
6394
|
+
writeFileSync11(this.path(file), (/* @__PURE__ */ new Date()).toISOString());
|
|
5937
6395
|
},
|
|
5938
6396
|
at: () => {
|
|
5939
6397
|
try {
|
|
@@ -5946,7 +6404,7 @@ var State = class _State {
|
|
|
5946
6404
|
}
|
|
5947
6405
|
readInt(file) {
|
|
5948
6406
|
try {
|
|
5949
|
-
return Number.parseInt(
|
|
6407
|
+
return Number.parseInt(readFileSync15(this.path(file), "utf8").trim(), 10) || 0;
|
|
5950
6408
|
} catch {
|
|
5951
6409
|
return 0;
|
|
5952
6410
|
}
|
|
@@ -6429,8 +6887,8 @@ var PERISCOPE_COLUMNS = [
|
|
|
6429
6887
|
];
|
|
6430
6888
|
|
|
6431
6889
|
// src/agent-log.ts
|
|
6432
|
-
import { existsSync as existsSync15, mkdirSync as
|
|
6433
|
-
import { join as
|
|
6890
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync11, readdirSync as readdirSync6, readFileSync as readFileSync16, rmSync as rmSync11, writeFileSync as writeFileSync12 } from "fs";
|
|
6891
|
+
import { join as join17 } from "path";
|
|
6434
6892
|
|
|
6435
6893
|
// src/pending-backoff.ts
|
|
6436
6894
|
var sleepBackoff = (attempts) => Math.min(6e4 * 2 ** Math.max(0, attempts - 1), 30 * 6e4);
|
|
@@ -6487,14 +6945,14 @@ async function findTicketRecord(target, t, ref) {
|
|
|
6487
6945
|
);
|
|
6488
6946
|
return rows.length === 1 ? rows[0].id : void 0;
|
|
6489
6947
|
}
|
|
6490
|
-
function counterIncrements(
|
|
6948
|
+
function counterIncrements(run3, cols, have) {
|
|
6491
6949
|
const figures = [
|
|
6492
6950
|
[cols.agentRuns, 1],
|
|
6493
|
-
[cols.tokensIn,
|
|
6494
|
-
[cols.tokensOut,
|
|
6495
|
-
[cols.cacheReadTokens,
|
|
6496
|
-
[cols.cacheWriteTokens,
|
|
6497
|
-
[cols.costUsd,
|
|
6951
|
+
[cols.tokensIn, run3.tokensIn],
|
|
6952
|
+
[cols.tokensOut, run3.tokensOut],
|
|
6953
|
+
[cols.cacheReadTokens, run3.cacheReadTokens],
|
|
6954
|
+
[cols.cacheWriteTokens, run3.cacheWriteTokens],
|
|
6955
|
+
[cols.costUsd, run3.costUsd]
|
|
6498
6956
|
];
|
|
6499
6957
|
const out = {};
|
|
6500
6958
|
for (const [column, value2] of figures) {
|
|
@@ -6517,41 +6975,41 @@ async function post(target, path, body) {
|
|
|
6517
6975
|
if (!res.ok) throw new AgentLogHttpError(`${path}: ${res.status} ${res.statusText}`, res.status);
|
|
6518
6976
|
return await res.json();
|
|
6519
6977
|
}
|
|
6520
|
-
async function reportAgentRun(target,
|
|
6978
|
+
async function reportAgentRun(target, run3) {
|
|
6521
6979
|
let ticketRecordId;
|
|
6522
|
-
if (target.tickets &&
|
|
6980
|
+
if (target.tickets && run3.ticketReference) {
|
|
6523
6981
|
try {
|
|
6524
|
-
ticketRecordId = await findTicketRecord(target, target.tickets,
|
|
6982
|
+
ticketRecordId = await findTicketRecord(target, target.tickets, run3.ticketReference);
|
|
6525
6983
|
} catch {
|
|
6526
6984
|
}
|
|
6527
6985
|
}
|
|
6528
6986
|
const body = {
|
|
6529
6987
|
agentId: target.agentId,
|
|
6530
|
-
ticketReference:
|
|
6531
|
-
outcome:
|
|
6532
|
-
startedAt:
|
|
6533
|
-
finishedAt:
|
|
6988
|
+
ticketReference: run3.ticketReference,
|
|
6989
|
+
outcome: run3.outcome,
|
|
6990
|
+
startedAt: run3.startedAt,
|
|
6991
|
+
finishedAt: run3.finishedAt,
|
|
6534
6992
|
source: "client",
|
|
6535
6993
|
client: "crew",
|
|
6536
|
-
model:
|
|
6537
|
-
tokensIn:
|
|
6538
|
-
tokensOut:
|
|
6539
|
-
cacheReadTokens:
|
|
6540
|
-
cacheWriteTokens:
|
|
6541
|
-
costUsd:
|
|
6542
|
-
numTurns:
|
|
6543
|
-
prefixTokens:
|
|
6544
|
-
promptVersion:
|
|
6545
|
-
promptSha:
|
|
6994
|
+
model: run3.model,
|
|
6995
|
+
tokensIn: run3.tokensIn,
|
|
6996
|
+
tokensOut: run3.tokensOut,
|
|
6997
|
+
cacheReadTokens: run3.cacheReadTokens,
|
|
6998
|
+
cacheWriteTokens: run3.cacheWriteTokens,
|
|
6999
|
+
costUsd: run3.costUsd,
|
|
7000
|
+
numTurns: run3.numTurns,
|
|
7001
|
+
prefixTokens: run3.prefixTokens,
|
|
7002
|
+
promptVersion: run3.promptVersion,
|
|
7003
|
+
promptSha: run3.promptSha
|
|
6546
7004
|
};
|
|
6547
7005
|
const workItem = target.tickets && ticketRecordId ? { workItemTable: target.tickets.issuesModelId, workItemRecordId: ticketRecordId } : {};
|
|
6548
7006
|
let entry;
|
|
6549
7007
|
try {
|
|
6550
|
-
entry = await post(target, "/agents/log", { ...body, ...workItem, harness:
|
|
7008
|
+
entry = await post(target, "/agents/log", { ...body, ...workItem, harness: run3.harness, provider: run3.provider });
|
|
6551
7009
|
} catch (e) {
|
|
6552
7010
|
if (!(e instanceof AgentLogHttpError)) throw new AgentLogUnreachableError(e.message);
|
|
6553
7011
|
if (e.status >= 500) throw new AgentLogUnreachableError(e.message);
|
|
6554
|
-
if (e.status !== 400 || !
|
|
7012
|
+
if (e.status !== 400 || !run3.harness && !run3.provider && !ticketRecordId) {
|
|
6555
7013
|
e.row = true;
|
|
6556
7014
|
throw e;
|
|
6557
7015
|
}
|
|
@@ -6562,7 +7020,7 @@ async function reportAgentRun(target, run2) {
|
|
|
6562
7020
|
throw e2;
|
|
6563
7021
|
}
|
|
6564
7022
|
}
|
|
6565
|
-
for (const cycle of
|
|
7023
|
+
for (const cycle of run3.cycles) {
|
|
6566
7024
|
await post(target, `/agents/log/${entry.id}/cycles`, {
|
|
6567
7025
|
cycleIndex: cycle.cycleIndex,
|
|
6568
7026
|
occurredAt: cycle.occurredAt,
|
|
@@ -6570,7 +7028,7 @@ async function reportAgentRun(target, run2) {
|
|
|
6570
7028
|
...cycle.role ? { role: cycle.role } : {}
|
|
6571
7029
|
});
|
|
6572
7030
|
}
|
|
6573
|
-
const counters = target.tickets && ticketRecordId ? await addRunToTicket(target, target.tickets, ticketRecordId,
|
|
7031
|
+
const counters = target.tickets && ticketRecordId ? await addRunToTicket(target, target.tickets, ticketRecordId, run3) : void 0;
|
|
6574
7032
|
return {
|
|
6575
7033
|
agentLogId: entry.id,
|
|
6576
7034
|
...counters?.warning ? { counterWarning: counters.warning } : {},
|
|
@@ -6584,38 +7042,38 @@ async function writeCounters(target, w) {
|
|
|
6584
7042
|
if (Object.keys(body).length === 0) return;
|
|
6585
7043
|
await request(target, "PATCH", `/data-models/${w.issuesModelId}/records/${w.recordId}`, body);
|
|
6586
7044
|
}
|
|
6587
|
-
async function addRunToTicket(target, t, recordId,
|
|
7045
|
+
async function addRunToTicket(target, t, recordId, run3) {
|
|
6588
7046
|
const write = {
|
|
6589
7047
|
issuesModelId: t.issuesModelId,
|
|
6590
7048
|
recordId,
|
|
6591
|
-
ticketReference:
|
|
7049
|
+
ticketReference: run3.ticketReference,
|
|
6592
7050
|
counters: t.counters,
|
|
6593
|
-
body: counterIncrements(
|
|
7051
|
+
body: counterIncrements(run3, t.counters, new Set(Object.values(t.counters)))
|
|
6594
7052
|
};
|
|
6595
7053
|
try {
|
|
6596
7054
|
await writeCounters(target, write);
|
|
6597
7055
|
return {};
|
|
6598
7056
|
} catch (e) {
|
|
6599
7057
|
return {
|
|
6600
|
-
warning: `could not add run spend to ${
|
|
7058
|
+
warning: `could not add run spend to ${run3.ticketReference}: ${e.message}`,
|
|
6601
7059
|
...isRetryable(e) ? { retry: write } : {}
|
|
6602
7060
|
};
|
|
6603
7061
|
}
|
|
6604
7062
|
}
|
|
6605
|
-
var queueDir = (stateDir) =>
|
|
7063
|
+
var queueDir = (stateDir) => join17(stateDir, "pending-agent-logs");
|
|
6606
7064
|
function queueAgentRun(stateDir, name, q) {
|
|
6607
|
-
|
|
6608
|
-
|
|
7065
|
+
mkdirSync11(queueDir(stateDir), { recursive: true });
|
|
7066
|
+
writeFileSync12(join17(queueDir(stateDir), `${name}.json`), JSON.stringify(q));
|
|
6609
7067
|
}
|
|
6610
7068
|
async function replayQueuedAgentRuns(stateDir, route2, keyFor, log, now = /* @__PURE__ */ new Date()) {
|
|
6611
7069
|
const dir = queueDir(stateDir);
|
|
6612
7070
|
if (!existsSync15(dir)) return 0;
|
|
6613
7071
|
let sent = 0;
|
|
6614
7072
|
for (const f of readdirSync6(dir).filter((n) => n.endsWith(".json")).sort()) {
|
|
6615
|
-
const path =
|
|
7073
|
+
const path = join17(dir, f);
|
|
6616
7074
|
let q;
|
|
6617
7075
|
try {
|
|
6618
|
-
q = JSON.parse(
|
|
7076
|
+
q = JSON.parse(readFileSync16(path, "utf8"));
|
|
6619
7077
|
} catch {
|
|
6620
7078
|
continue;
|
|
6621
7079
|
}
|
|
@@ -6634,17 +7092,17 @@ async function replayQueuedAgentRuns(stateDir, route2, keyFor, log, now = /* @__
|
|
|
6634
7092
|
attempts: 0,
|
|
6635
7093
|
nextAttemptAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6636
7094
|
});
|
|
6637
|
-
|
|
7095
|
+
rmSync11(path, { force: true });
|
|
6638
7096
|
sent += 1;
|
|
6639
7097
|
log.emit(`sent a queued Agent Log report (${f})`);
|
|
6640
7098
|
} catch (e) {
|
|
6641
7099
|
if (!(e instanceof AgentLogUnreachableError)) {
|
|
6642
|
-
|
|
7100
|
+
rmSync11(path, { force: true });
|
|
6643
7101
|
log.warn(`dropped a queued Agent Log report (${f}): ${e.message}`);
|
|
6644
7102
|
continue;
|
|
6645
7103
|
}
|
|
6646
7104
|
const attempts = q.attempts + 1;
|
|
6647
|
-
|
|
7105
|
+
writeFileSync12(path, JSON.stringify({
|
|
6648
7106
|
...q,
|
|
6649
7107
|
attempts,
|
|
6650
7108
|
warned: true,
|
|
@@ -6655,20 +7113,20 @@ async function replayQueuedAgentRuns(stateDir, route2, keyFor, log, now = /* @__
|
|
|
6655
7113
|
}
|
|
6656
7114
|
return sent;
|
|
6657
7115
|
}
|
|
6658
|
-
var countersDir = (stateDir) =>
|
|
7116
|
+
var countersDir = (stateDir) => join17(stateDir, "pending-ticket-counters");
|
|
6659
7117
|
function queueTicketCounters(stateDir, name, q) {
|
|
6660
|
-
|
|
6661
|
-
|
|
7118
|
+
mkdirSync11(countersDir(stateDir), { recursive: true });
|
|
7119
|
+
writeFileSync12(join17(countersDir(stateDir), `${name}.json`), JSON.stringify(q));
|
|
6662
7120
|
}
|
|
6663
7121
|
async function replayQueuedTicketCounters(stateDir, route2, keyFor, log, now = /* @__PURE__ */ new Date()) {
|
|
6664
7122
|
const dir = countersDir(stateDir);
|
|
6665
7123
|
if (!existsSync15(dir)) return 0;
|
|
6666
7124
|
let sent = 0;
|
|
6667
7125
|
for (const f of readdirSync6(dir).filter((n) => n.endsWith(".json")).sort()) {
|
|
6668
|
-
const path =
|
|
7126
|
+
const path = join17(dir, f);
|
|
6669
7127
|
let q;
|
|
6670
7128
|
try {
|
|
6671
|
-
q = JSON.parse(
|
|
7129
|
+
q = JSON.parse(readFileSync16(path, "utf8"));
|
|
6672
7130
|
} catch {
|
|
6673
7131
|
continue;
|
|
6674
7132
|
}
|
|
@@ -6677,17 +7135,17 @@ async function replayQueuedTicketCounters(stateDir, route2, keyFor, log, now = /
|
|
|
6677
7135
|
const apiKey = await keyFor(q.role);
|
|
6678
7136
|
if (!apiKey) continue;
|
|
6679
7137
|
await writeCounters({ ...q.target, apiKey }, q.write);
|
|
6680
|
-
|
|
7138
|
+
rmSync11(path, { force: true });
|
|
6681
7139
|
sent += 1;
|
|
6682
7140
|
log.emit(`added a queued run's spend to ${q.write.ticketReference ?? q.write.recordId} (${f})`);
|
|
6683
7141
|
} catch (e) {
|
|
6684
7142
|
if (!isRetryable(e)) {
|
|
6685
|
-
|
|
7143
|
+
rmSync11(path, { force: true });
|
|
6686
7144
|
log.warn(`dropped a queued ticket counter update (${f}): ${e.message}`);
|
|
6687
7145
|
continue;
|
|
6688
7146
|
}
|
|
6689
7147
|
const attempts = q.attempts + 1;
|
|
6690
|
-
|
|
7148
|
+
writeFileSync12(path, JSON.stringify({
|
|
6691
7149
|
...q,
|
|
6692
7150
|
attempts,
|
|
6693
7151
|
warned: true,
|
|
@@ -6890,8 +7348,8 @@ var HandoffTranscriptTracker = class {
|
|
|
6890
7348
|
};
|
|
6891
7349
|
|
|
6892
7350
|
// src/pending-handoff.ts
|
|
6893
|
-
import { existsSync as existsSync18, mkdirSync as
|
|
6894
|
-
import { join as
|
|
7351
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync12, readdirSync as readdirSync7, readFileSync as readFileSync17, rmSync as rmSync12, writeFileSync as writeFileSync13 } from "fs";
|
|
7352
|
+
import { join as join20 } from "path";
|
|
6895
7353
|
|
|
6896
7354
|
// src/ticket-branch.ts
|
|
6897
7355
|
import { basename as basename6 } from "path";
|
|
@@ -8058,15 +8516,15 @@ function holdWarnings(tickets, ships, contract, humanHolds, now = Date.now()) {
|
|
|
8058
8516
|
|
|
8059
8517
|
// src/run-worktree.ts
|
|
8060
8518
|
import { existsSync as existsSync17 } from "fs";
|
|
8061
|
-
import { dirname as dirname7, join as
|
|
8519
|
+
import { dirname as dirname7, join as join19 } from "path";
|
|
8062
8520
|
|
|
8063
8521
|
// src/ports.ts
|
|
8064
|
-
import { execFileSync as
|
|
8522
|
+
import { execFileSync as execFileSync5 } from "child_process";
|
|
8065
8523
|
import { existsSync as existsSync16 } from "fs";
|
|
8066
|
-
import { join as
|
|
8067
|
-
var
|
|
8524
|
+
import { join as join18 } from "path";
|
|
8525
|
+
var run2 = (cmd, args) => {
|
|
8068
8526
|
try {
|
|
8069
|
-
return
|
|
8527
|
+
return execFileSync5(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
8070
8528
|
} catch {
|
|
8071
8529
|
return "";
|
|
8072
8530
|
}
|
|
@@ -8080,7 +8538,7 @@ function listeners() {
|
|
|
8080
8538
|
seen.add(key);
|
|
8081
8539
|
out.push({ pid, port });
|
|
8082
8540
|
};
|
|
8083
|
-
const lsof =
|
|
8541
|
+
const lsof = run2("lsof", ["-nP", "-iTCP", "-sTCP:LISTEN"]);
|
|
8084
8542
|
if (lsof) {
|
|
8085
8543
|
for (const line of lsof.split("\n").slice(1)) {
|
|
8086
8544
|
const cols = line.split(/\s+/);
|
|
@@ -8090,7 +8548,7 @@ function listeners() {
|
|
|
8090
8548
|
}
|
|
8091
8549
|
return out;
|
|
8092
8550
|
}
|
|
8093
|
-
const ss =
|
|
8551
|
+
const ss = run2("ss", ["-ltnpH"]);
|
|
8094
8552
|
for (const line of ss.split("\n")) {
|
|
8095
8553
|
const local = line.trim().split(/\s+/)[3];
|
|
8096
8554
|
const pid = /pid=(\d+)/.exec(line)?.[1];
|
|
@@ -8108,7 +8566,7 @@ function ticketForPort(port, s = DEFAULT_PORTS) {
|
|
|
8108
8566
|
}
|
|
8109
8567
|
function worktreeExists(parent, prefix, n) {
|
|
8110
8568
|
for (const name of [`${prefix}${n}`, `${prefix}${String(n).padStart(3, "0")}`]) {
|
|
8111
|
-
if (existsSync16(
|
|
8569
|
+
if (existsSync16(join18(parent, name, ".git"))) return true;
|
|
8112
8570
|
}
|
|
8113
8571
|
return false;
|
|
8114
8572
|
}
|
|
@@ -8142,7 +8600,7 @@ async function killGently(pids, graceMs = 2e3) {
|
|
|
8142
8600
|
function pidsInWorktree(path, n, s = DEFAULT_PORTS) {
|
|
8143
8601
|
const pids = /* @__PURE__ */ new Set();
|
|
8144
8602
|
for (const port of [s.backendBase + n, s.frontendBase + n]) for (const p of pidsOnPort(port)) pids.add(p);
|
|
8145
|
-
const matched =
|
|
8603
|
+
const matched = run2("pgrep", ["-f", `${path}/`]);
|
|
8146
8604
|
for (const line of matched.split("\n")) {
|
|
8147
8605
|
const pid = Number(line.trim());
|
|
8148
8606
|
if (Number.isFinite(pid) && pid > 0 && pid !== process.pid) pids.add(pid);
|
|
@@ -8207,7 +8665,7 @@ function planRunWorktree(dir, cfg, t, role, opts = {}) {
|
|
|
8207
8665
|
if (countCommits(existing.path, `${baseRef}..HEAD`) > 0) return { action: "leave-unpushed", path: existing.path, branch };
|
|
8208
8666
|
return { action: "recut", path: existing.path, branch, base: baseRef };
|
|
8209
8667
|
}
|
|
8210
|
-
const path =
|
|
8668
|
+
const path = join19(dirname7(dir), worktreeForTicket(dir, cfg, t, role));
|
|
8211
8669
|
if (located?.where === "remote" || located && hasRemote && remoteBranchExists(dir, remote, located.branch)) {
|
|
8212
8670
|
return { action: "cut-remote", path, branch: located.branch };
|
|
8213
8671
|
}
|
|
@@ -8664,12 +9122,12 @@ async function guardRunStrays(route2, ship, tracker, role, workingId, since2, lo
|
|
|
8664
9122
|
|
|
8665
9123
|
// src/pending-handoff.ts
|
|
8666
9124
|
var PENDING_MAX_AGE_MS = 48 * 36e5;
|
|
8667
|
-
var pendingDirFor = (stateDir) =>
|
|
8668
|
-
var stateDirOfStream = (streamPath) =>
|
|
9125
|
+
var pendingDirFor = (stateDir) => join20(stateDir, "pending-handoffs");
|
|
9126
|
+
var stateDirOfStream = (streamPath) => join20(streamPath, "..", "..");
|
|
8669
9127
|
function writePendingHandoff(stateDir, rec) {
|
|
8670
9128
|
const dir = pendingDirFor(stateDir);
|
|
8671
|
-
|
|
8672
|
-
|
|
9129
|
+
mkdirSync12(dir, { recursive: true });
|
|
9130
|
+
writeFileSync13(join20(dir, `${rec.ticketRecordId}.json`), JSON.stringify(rec, null, 2));
|
|
8673
9131
|
}
|
|
8674
9132
|
function listPendingHandoffs(stateDir, route2) {
|
|
8675
9133
|
const dir = pendingDirFor(stateDir);
|
|
@@ -8677,7 +9135,7 @@ function listPendingHandoffs(stateDir, route2) {
|
|
|
8677
9135
|
const out = [];
|
|
8678
9136
|
for (const f of readdirSync7(dir).filter((n) => n.endsWith(".json")).sort()) {
|
|
8679
9137
|
try {
|
|
8680
|
-
const rec = JSON.parse(
|
|
9138
|
+
const rec = JSON.parse(readFileSync17(join20(dir, f), "utf8"));
|
|
8681
9139
|
if (rec.version === 1 && rec.ticketRecordId && (!route2 || rec.route === route2)) out.push(rec);
|
|
8682
9140
|
} catch {
|
|
8683
9141
|
}
|
|
@@ -8685,7 +9143,7 @@ function listPendingHandoffs(stateDir, route2) {
|
|
|
8685
9143
|
return out;
|
|
8686
9144
|
}
|
|
8687
9145
|
function removePendingHandoff(stateDir, ticketRecordId) {
|
|
8688
|
-
|
|
9146
|
+
rmSync12(join20(pendingDirFor(stateDir), `${ticketRecordId}.json`), { force: true });
|
|
8689
9147
|
}
|
|
8690
9148
|
function pendingHandoffFor(stateDir, ticketRecordId) {
|
|
8691
9149
|
return listPendingHandoffs(stateDir).find((r) => r.ticketRecordId === ticketRecordId);
|
|
@@ -8795,8 +9253,8 @@ function expandSkillMarkers(text2, lookup2, opts = {}) {
|
|
|
8795
9253
|
}
|
|
8796
9254
|
|
|
8797
9255
|
// src/skills.ts
|
|
8798
|
-
import { readFileSync as
|
|
8799
|
-
import { join as
|
|
9256
|
+
import { readFileSync as readFileSync19, readdirSync as readdirSync8, existsSync as existsSync20 } from "fs";
|
|
9257
|
+
import { join as join22 } from "path";
|
|
8800
9258
|
import { parse as parseYaml } from "yaml";
|
|
8801
9259
|
import { TablationClient as TablationClient3, StaleWriteError as StaleWriteError3, TablationApiError as TablationApiError2 } from "@tablation/client";
|
|
8802
9260
|
|
|
@@ -8812,8 +9270,8 @@ ${additions}`;
|
|
|
8812
9270
|
}
|
|
8813
9271
|
|
|
8814
9272
|
// src/agents.ts
|
|
8815
|
-
import { readFileSync as
|
|
8816
|
-
import { join as
|
|
9273
|
+
import { readFileSync as readFileSync18, existsSync as existsSync19 } from "fs";
|
|
9274
|
+
import { join as join21 } from "path";
|
|
8817
9275
|
import { TablationClient as TablationClient2, StaleWriteError as StaleWriteError2, TablationApiError } from "@tablation/client";
|
|
8818
9276
|
var PERSONA_NAME = {
|
|
8819
9277
|
dev: "Developer",
|
|
@@ -8827,12 +9285,12 @@ var CREW_AGENT_COLUMN = "agent_id";
|
|
|
8827
9285
|
var AgentsSyncError = class extends Error {
|
|
8828
9286
|
};
|
|
8829
9287
|
function personaDefaultPrompt(promptsDir, role) {
|
|
8830
|
-
const briefPath =
|
|
9288
|
+
const briefPath = join21(promptsDir, "personas", `lane-${role}.md`);
|
|
8831
9289
|
if (!existsSync19(briefPath)) throw new AgentsSyncError(`no brief at ${briefPath}`);
|
|
8832
|
-
if (SELF_CONTAINED_ROLES.has(role)) return
|
|
8833
|
-
const commonPath =
|
|
8834
|
-
if (!existsSync19(commonPath)) return
|
|
8835
|
-
return `${
|
|
9290
|
+
if (SELF_CONTAINED_ROLES.has(role)) return readFileSync18(briefPath, "utf8");
|
|
9291
|
+
const commonPath = join21(promptsDir, "personas", "common.md");
|
|
9292
|
+
if (!existsSync19(commonPath)) return readFileSync18(briefPath, "utf8");
|
|
9293
|
+
return `${readFileSync18(commonPath, "utf8")}${readFileSync18(briefPath, "utf8")}`;
|
|
8836
9294
|
}
|
|
8837
9295
|
async function resolveAgentId(route2, role, opts) {
|
|
8838
9296
|
const cached = route2.resolved?.agentPersonas?.[role]?.agentId;
|
|
@@ -9279,7 +9737,7 @@ function describeCrewLink(o) {
|
|
|
9279
9737
|
var SkillSyncError = class extends Error {
|
|
9280
9738
|
};
|
|
9281
9739
|
function parseSkillFile(path) {
|
|
9282
|
-
const text2 =
|
|
9740
|
+
const text2 = readFileSync19(path, "utf8");
|
|
9283
9741
|
const match = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/.exec(text2);
|
|
9284
9742
|
if (!match) throw new SkillSyncError(`${path}: missing YAML frontmatter (expected a leading --- block)`);
|
|
9285
9743
|
const frontmatter = match[1];
|
|
@@ -9292,9 +9750,9 @@ function parseSkillFile(path) {
|
|
|
9292
9750
|
return { name: meta.name, description: meta.description, prompt: body, file: path };
|
|
9293
9751
|
}
|
|
9294
9752
|
function listLocalSkills(promptsDir) {
|
|
9295
|
-
const dir =
|
|
9753
|
+
const dir = join22(promptsDir, "skills");
|
|
9296
9754
|
if (!existsSync20(dir)) return [];
|
|
9297
|
-
return readdirSync8(dir).filter((f) => f.endsWith(".md")).map((f) => parseSkillFile(
|
|
9755
|
+
return readdirSync8(dir).filter((f) => f.endsWith(".md")).map((f) => parseSkillFile(join22(dir, f)));
|
|
9298
9756
|
}
|
|
9299
9757
|
async function syncSkills(route2, opts) {
|
|
9300
9758
|
if (!route2.resolved) {
|
|
@@ -9478,10 +9936,10 @@ function skillMarkerWarnings(promptsDir, personaPrompts) {
|
|
|
9478
9936
|
}
|
|
9479
9937
|
|
|
9480
9938
|
// src/runner.ts
|
|
9481
|
-
import { spawn as
|
|
9482
|
-
import { mkdtempSync, writeFileSync as
|
|
9483
|
-
import { tmpdir as
|
|
9484
|
-
import { basename as basename7, join as
|
|
9939
|
+
import { spawn as spawn3, execFile, execFileSync as execFileSync6 } from "child_process";
|
|
9940
|
+
import { mkdtempSync, writeFileSync as writeFileSync14, rmSync as rmSync13 } from "fs";
|
|
9941
|
+
import { tmpdir as tmpdir3 } from "os";
|
|
9942
|
+
import { basename as basename7, join as join23 } from "path";
|
|
9485
9943
|
import { promisify } from "util";
|
|
9486
9944
|
|
|
9487
9945
|
// src/run-limit.ts
|
|
@@ -9545,7 +10003,7 @@ var SANDBOX_PROMPT_PATH = "/sandbox/prompt.txt";
|
|
|
9545
10003
|
var HostRunner = class {
|
|
9546
10004
|
kind = "host";
|
|
9547
10005
|
start(plan, o) {
|
|
9548
|
-
const child =
|
|
10006
|
+
const child = spawn3(plan.bin, plan.args, {
|
|
9549
10007
|
cwd: plan.cwd,
|
|
9550
10008
|
env: o.env,
|
|
9551
10009
|
stdio: ["pipe", o.streamFd, "inherit"],
|
|
@@ -9632,17 +10090,17 @@ var SandboxRunner = class {
|
|
|
9632
10090
|
const harness = cfg.agentBin ?? basename7(plan.bin);
|
|
9633
10091
|
const viaUpload = Buffer.byteLength(plan.prompt) > SANDBOX_STDIN_LIMIT;
|
|
9634
10092
|
if (viaUpload) {
|
|
9635
|
-
const tmp = mkdtempSync(
|
|
10093
|
+
const tmp = mkdtempSync(join23(tmpdir3(), "crew-prompt-"));
|
|
9636
10094
|
try {
|
|
9637
|
-
const file =
|
|
9638
|
-
|
|
10095
|
+
const file = join23(tmp, "prompt.txt");
|
|
10096
|
+
writeFileSync14(file, plan.prompt, { mode: 384 });
|
|
9639
10097
|
await this.os(["sandbox", "upload", name, file, SANDBOX_PROMPT_PATH]);
|
|
9640
10098
|
} finally {
|
|
9641
|
-
|
|
10099
|
+
rmSync13(tmp, { recursive: true, force: true });
|
|
9642
10100
|
}
|
|
9643
10101
|
}
|
|
9644
10102
|
const command2 = viaUpload ? ["sh", "-c", `exec ${[harness, ...plan.args].map(shQuote2).join(" ")} < ${SANDBOX_PROMPT_PATH}`] : [harness, ...plan.args];
|
|
9645
|
-
const child =
|
|
10103
|
+
const child = spawn3(cfg.bin, ["sandbox", "exec", "-n", name, "--no-login-shell", "--workdir", cfg.workDir, "--", ...command2], {
|
|
9646
10104
|
env: process.env,
|
|
9647
10105
|
stdio: ["pipe", o.streamFd, "inherit"],
|
|
9648
10106
|
detached: true
|
|
@@ -9682,7 +10140,7 @@ function describeExecError(e) {
|
|
|
9682
10140
|
}
|
|
9683
10141
|
function gitOrigin(cwd) {
|
|
9684
10142
|
try {
|
|
9685
|
-
return
|
|
10143
|
+
return execFileSync6("git", ["-C", cwd, "remote", "get-url", "origin"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
9686
10144
|
} catch {
|
|
9687
10145
|
return null;
|
|
9688
10146
|
}
|
|
@@ -9693,16 +10151,16 @@ function runnerFor(kind, sandbox) {
|
|
|
9693
10151
|
function runnerOfSidecar(sc) {
|
|
9694
10152
|
return sc.runner === "sandbox" ? new SandboxRunner(sc.sandboxBin ? { bin: sc.sandboxBin } : {}) : new HostRunner();
|
|
9695
10153
|
}
|
|
9696
|
-
function checkSandboxRunner(cfg = {},
|
|
10154
|
+
function checkSandboxRunner(cfg = {}, run3 = (b, a) => execFileSync6(b, a, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 15e3 })) {
|
|
9697
10155
|
const bin = cfg.bin ?? DEFAULT_SANDBOX_CONFIG.bin;
|
|
9698
10156
|
let version;
|
|
9699
10157
|
try {
|
|
9700
|
-
version =
|
|
10158
|
+
version = run3(bin, ["--version"]).trim();
|
|
9701
10159
|
} catch (e) {
|
|
9702
10160
|
return { ok: false, detail: `${bin} is not installed or not on the scheduler PATH (${e.message.split("\n")[0]})` };
|
|
9703
10161
|
}
|
|
9704
10162
|
try {
|
|
9705
|
-
|
|
10163
|
+
run3(bin, ["status"]);
|
|
9706
10164
|
} catch (e) {
|
|
9707
10165
|
const err3 = e;
|
|
9708
10166
|
return {
|
|
@@ -9740,7 +10198,7 @@ var ALLOWANCE_REFUSAL_MARKER = "AI_ALLOWANCE_EXCEEDED";
|
|
|
9740
10198
|
function streamShowsAllowanceRefusal(streamPath) {
|
|
9741
10199
|
let raw;
|
|
9742
10200
|
try {
|
|
9743
|
-
raw =
|
|
10201
|
+
raw = readFileSync20(streamPath, "utf8");
|
|
9744
10202
|
} catch {
|
|
9745
10203
|
return false;
|
|
9746
10204
|
}
|
|
@@ -9763,13 +10221,13 @@ function streamShowsAllowanceRefusal(streamPath) {
|
|
|
9763
10221
|
}
|
|
9764
10222
|
return false;
|
|
9765
10223
|
}
|
|
9766
|
-
var digestPathFor = (stateDir, routeLabel, role, runId) =>
|
|
10224
|
+
var digestPathFor = (stateDir, routeLabel, role, runId) => join24(stateDir, `digest-${routeLabel}-${role}${runId ? `-${runId}` : ""}.md`);
|
|
9767
10225
|
function planAgentRun(o) {
|
|
9768
|
-
const briefPath =
|
|
10226
|
+
const briefPath = join24(o.route.promptsDir, "personas", `lane-${o.role}.md`);
|
|
9769
10227
|
if (!existsSync21(briefPath)) {
|
|
9770
10228
|
throw new AgentError(`no brief at ${briefPath} \u2014 refusing to run an unscoped session`);
|
|
9771
10229
|
}
|
|
9772
|
-
const commonPath =
|
|
10230
|
+
const commonPath = join24(o.route.promptsDir, "personas", "common.md");
|
|
9773
10231
|
const usesCommon = !SELF_CONTAINED_ROLES.has(o.role) && existsSync21(commonPath);
|
|
9774
10232
|
const now = o.now ?? (() => Date.now());
|
|
9775
10233
|
const routeLabel = routeSlug(o.route.route);
|
|
@@ -9779,10 +10237,10 @@ function planAgentRun(o) {
|
|
|
9779
10237
|
let ageSeconds;
|
|
9780
10238
|
if (existsSync21(digestPath2)) {
|
|
9781
10239
|
ageSeconds = Math.floor((now() - statSync7(digestPath2).mtimeMs) / 1e3);
|
|
9782
|
-
if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest =
|
|
10240
|
+
if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync20(digestPath2, "utf8");
|
|
9783
10241
|
}
|
|
9784
|
-
const common = usesCommon ?
|
|
9785
|
-
const brief =
|
|
10242
|
+
const common = usesCommon ? readFileSync20(commonPath, "utf8") : "";
|
|
10243
|
+
const brief = readFileSync20(briefPath, "utf8");
|
|
9786
10244
|
const effectiveCommon = o.divergedPrompt ? "" : common;
|
|
9787
10245
|
const effectiveBrief = appendAdditions(o.divergedPrompt ?? brief, o.personaAdditions);
|
|
9788
10246
|
const localSkills = new Map(listLocalSkills(o.route.promptsDir).map((s) => [s.name, s.prompt]));
|
|
@@ -9803,8 +10261,8 @@ function planAgentRun(o) {
|
|
|
9803
10261
|
brief: expanded.text,
|
|
9804
10262
|
digest
|
|
9805
10263
|
});
|
|
9806
|
-
const promptSha =
|
|
9807
|
-
const streamsDir =
|
|
10264
|
+
const promptSha = createHash6("sha256").update(expanded.text).digest("hex");
|
|
10265
|
+
const streamsDir = join24(o.stateDir, "streams");
|
|
9808
10266
|
const base = `${routeLabel}-${o.role}-${o.cycle}${o.runId ? `-${o.runId}` : ""}`;
|
|
9809
10267
|
const seat = resolveSeat(o.ship, o.route, o.role, o.agentPersona ?? (o.agentModel ? { model: o.agentModel } : {}), o.repo);
|
|
9810
10268
|
const { model } = seat;
|
|
@@ -9890,8 +10348,8 @@ function planAgentRun(o) {
|
|
|
9890
10348
|
digestAttached: digest !== void 0,
|
|
9891
10349
|
digestAgeSeconds: ageSeconds,
|
|
9892
10350
|
model,
|
|
9893
|
-
streamPath:
|
|
9894
|
-
eventsPath:
|
|
10351
|
+
streamPath: join24(streamsDir, `${base}.jsonl`),
|
|
10352
|
+
eventsPath: join24(streamsDir, `${base}.events.jsonl`),
|
|
9895
10353
|
ticket: o.ticket,
|
|
9896
10354
|
ticketRecordId: o.ticketRecordId ?? null,
|
|
9897
10355
|
repo: o.repo ?? null,
|
|
@@ -9922,7 +10380,7 @@ function describePlan(p) {
|
|
|
9922
10380
|
var pidPathFor = (streamPath) => `${streamPath}.pid`;
|
|
9923
10381
|
function readRunSidecar(streamPath) {
|
|
9924
10382
|
try {
|
|
9925
|
-
const sc = JSON.parse(
|
|
10383
|
+
const sc = JSON.parse(readFileSync20(pidPathFor(streamPath), "utf8"));
|
|
9926
10384
|
return typeof sc.pid === "number" && typeof sc.spawnedAt === "string" && typeof sc.role === "string" ? sc : null;
|
|
9927
10385
|
} catch {
|
|
9928
10386
|
return null;
|
|
@@ -9969,7 +10427,7 @@ async function killForDeadline(kill, pid, emit, role, ticket, spawnedAt, limitMi
|
|
|
9969
10427
|
var withoutKey = ({ apiKey: _apiKey, ...rest }) => rest;
|
|
9970
10428
|
function openSink(path) {
|
|
9971
10429
|
try {
|
|
9972
|
-
|
|
10430
|
+
mkdirSync13(dirname8(path), { recursive: true });
|
|
9973
10431
|
return createWriteStream(path, { flags: "a" });
|
|
9974
10432
|
} catch {
|
|
9975
10433
|
return void 0;
|
|
@@ -10151,7 +10609,7 @@ var RunObserver = class {
|
|
|
10151
10609
|
};
|
|
10152
10610
|
function writeRunMeta(id, startedAt, liveTicket, extra) {
|
|
10153
10611
|
try {
|
|
10154
|
-
|
|
10612
|
+
writeFileSync15(`${id.streamPath}.meta.json`, JSON.stringify({
|
|
10155
10613
|
route: id.route,
|
|
10156
10614
|
role: id.role,
|
|
10157
10615
|
hint: id.ticket,
|
|
@@ -10311,7 +10769,7 @@ function spawnAgent(plan, emit, hintMoved) {
|
|
|
10311
10769
|
const id = identityOf(plan, adapter.harness);
|
|
10312
10770
|
let streamFd;
|
|
10313
10771
|
try {
|
|
10314
|
-
|
|
10772
|
+
mkdirSync13(dirname8(plan.streamPath), { recursive: true });
|
|
10315
10773
|
streamFd = openSync3(plan.streamPath, "a");
|
|
10316
10774
|
} catch (e) {
|
|
10317
10775
|
void endSink(eventsSink);
|
|
@@ -10330,9 +10788,9 @@ function spawnAgent(plan, emit, hintMoved) {
|
|
|
10330
10788
|
}
|
|
10331
10789
|
if (begun instanceof Promise) {
|
|
10332
10790
|
return begun.then(
|
|
10333
|
-
(
|
|
10791
|
+
(run3) => {
|
|
10334
10792
|
closeSync3(streamFd);
|
|
10335
|
-
return supervise(
|
|
10793
|
+
return supervise(run3);
|
|
10336
10794
|
},
|
|
10337
10795
|
(e) => {
|
|
10338
10796
|
closeSync3(streamFd);
|
|
@@ -10369,7 +10827,7 @@ function spawnAgent(plan, emit, hintMoved) {
|
|
|
10369
10827
|
promptSha: plan.promptSha
|
|
10370
10828
|
};
|
|
10371
10829
|
try {
|
|
10372
|
-
|
|
10830
|
+
writeFileSync15(pidPathFor(plan.streamPath), JSON.stringify(sidecar));
|
|
10373
10831
|
} catch {
|
|
10374
10832
|
}
|
|
10375
10833
|
}
|
|
@@ -10582,9 +11040,9 @@ async function mintMissingSeatKeys(route2, o) {
|
|
|
10582
11040
|
out.push({ role, action: "no_agent" });
|
|
10583
11041
|
continue;
|
|
10584
11042
|
}
|
|
10585
|
-
const
|
|
11043
|
+
const slug2 = slugFor(route2, agentId);
|
|
10586
11044
|
try {
|
|
10587
|
-
if ((await store.get(host,
|
|
11045
|
+
if ((await store.get(host, slug2))?.apiKey) {
|
|
10588
11046
|
out.push({ role, action: "already_stored" });
|
|
10589
11047
|
continue;
|
|
10590
11048
|
}
|
|
@@ -10622,7 +11080,7 @@ async function mintMissingSeatKeys(route2, o) {
|
|
|
10622
11080
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10623
11081
|
...body.id ? { keyId: body.id } : {}
|
|
10624
11082
|
};
|
|
10625
|
-
await store.set(host,
|
|
11083
|
+
await store.set(host, slug2, session);
|
|
10626
11084
|
} catch (e) {
|
|
10627
11085
|
let revoked = false;
|
|
10628
11086
|
if (body.id) {
|
|
@@ -10686,9 +11144,9 @@ async function revokeSeatKeys(route2, o) {
|
|
|
10686
11144
|
out.push({ role, action: "no_agent" });
|
|
10687
11145
|
continue;
|
|
10688
11146
|
}
|
|
10689
|
-
const
|
|
11147
|
+
const slug2 = slugFor(route2, agentId);
|
|
10690
11148
|
try {
|
|
10691
|
-
const session = await store.get(host,
|
|
11149
|
+
const session = await store.get(host, slug2);
|
|
10692
11150
|
if (!session?.apiKey) {
|
|
10693
11151
|
out.push({ role, action: "not_stored" });
|
|
10694
11152
|
continue;
|
|
@@ -10709,7 +11167,7 @@ async function revokeSeatKeys(route2, o) {
|
|
|
10709
11167
|
out.push({ role, action: "failed", detail: `${del2.status} ${del2.statusText}` });
|
|
10710
11168
|
continue;
|
|
10711
11169
|
}
|
|
10712
|
-
await store.clear(host,
|
|
11170
|
+
await store.clear(host, slug2);
|
|
10713
11171
|
out.push({ role, action: "revoked", detail: del2.ok ? void 0 : "server already had no such key" });
|
|
10714
11172
|
} catch (e) {
|
|
10715
11173
|
out.push({ role, action: "failed", detail: /Command failed|security|sk_|-w /i.test(e.message) ? describeStoreError(e) : e.message });
|
|
@@ -10761,7 +11219,7 @@ var realDeps = {
|
|
|
10761
11219
|
resolve7(r);
|
|
10762
11220
|
}
|
|
10763
11221
|
};
|
|
10764
|
-
const child =
|
|
11222
|
+
const child = spawn5(o.bin, o.args, { cwd: o.cwd, env: o.env, stdio: ["pipe", "pipe", "pipe"], detached: true });
|
|
10765
11223
|
const killGroup = () => {
|
|
10766
11224
|
try {
|
|
10767
11225
|
if (child.pid !== void 0) process.kill(-child.pid, "SIGKILL");
|
|
@@ -10859,17 +11317,17 @@ var REMEDY = {
|
|
|
10859
11317
|
timeout: "the harness did not answer in time \u2014 run it by hand to see where it stalls",
|
|
10860
11318
|
"wrong-answer": "the agent answered but did not name the workspace \u2014 run it by hand to see what it said"
|
|
10861
11319
|
};
|
|
10862
|
-
function classify(spec,
|
|
11320
|
+
function classify(spec, run3, seen) {
|
|
10863
11321
|
const bin = spec.launcher ?? spec.bin;
|
|
10864
11322
|
const fail = (failure, what) => ({ failure, detail: `${what} \u2014 ${REMEDY[failure]}` });
|
|
10865
|
-
if (
|
|
10866
|
-
if (
|
|
10867
|
-
return fail("binary-not-found", `could not start \`${bin}\` (${
|
|
11323
|
+
if (run3.spawnError) {
|
|
11324
|
+
if (run3.spawnError.code === "ENOENT") return fail("binary-not-found", `\`${bin}\` is not on the scheduler PATH`);
|
|
11325
|
+
return fail("binary-not-found", `could not start \`${bin}\` (${run3.spawnError.message})`);
|
|
10868
11326
|
}
|
|
10869
|
-
if (
|
|
11327
|
+
if (run3.timedOut) return fail("timeout", "no answer within the time limit");
|
|
10870
11328
|
const text2 = `${seen.toolOutput}
|
|
10871
|
-
${
|
|
10872
|
-
if (REFUSED.test(seen.toolOutput) || seen.tools.some((t) => t.startsWith("mcp__")) && REFUSED.test(
|
|
11329
|
+
${run3.stderr}`;
|
|
11330
|
+
if (REFUSED.test(seen.toolOutput) || seen.tools.some((t) => t.startsWith("mcp__")) && REFUSED.test(run3.stderr)) {
|
|
10873
11331
|
return fail("key-refused", "the tracker returned 401/403 to the MCP call");
|
|
10874
11332
|
}
|
|
10875
11333
|
if (seen.mcpFailed || !seen.tools.some((t) => t.startsWith("mcp__")) && MCP_GONE.test(`${text2}
|
|
@@ -10877,8 +11335,8 @@ ${seen.texts.join("\n")}`)) {
|
|
|
10877
11335
|
return fail("mcp-unavailable", "the tracker MCP tool was not available to the session");
|
|
10878
11336
|
}
|
|
10879
11337
|
const answered = seen.texts.length > 0 || seen.finalText;
|
|
10880
|
-
if (!answered || LOGIN.test(
|
|
10881
|
-
return fail("login-missing", `\`${bin}\` exited${
|
|
11338
|
+
if (!answered || LOGIN.test(run3.stderr) || LOGIN.test(seen.finalText ?? "")) {
|
|
11339
|
+
return fail("login-missing", `\`${bin}\` exited${run3.code !== null ? ` (${run3.code})` : ""} before answering; the vendor login is probably missing`);
|
|
10882
11340
|
}
|
|
10883
11341
|
return fail("wrong-answer", `the agent replied "${(seen.finalText ?? seen.texts.at(-1) ?? "").trim().slice(0, 80)}"`);
|
|
10884
11342
|
}
|
|
@@ -10906,9 +11364,9 @@ async function runSelftest(i, deps = realDeps) {
|
|
|
10906
11364
|
...adapter.runEnv(spec),
|
|
10907
11365
|
[KEY_VAR]: i.apiKey
|
|
10908
11366
|
};
|
|
10909
|
-
const cwd = mkdtempSync2(
|
|
11367
|
+
const cwd = mkdtempSync2(join25(tmpdir4(), "crew-selftest-"));
|
|
10910
11368
|
try {
|
|
10911
|
-
const
|
|
11369
|
+
const run3 = await deps.run({
|
|
10912
11370
|
bin: invocation.bin,
|
|
10913
11371
|
args: invocation.args,
|
|
10914
11372
|
cwd,
|
|
@@ -10917,18 +11375,18 @@ async function runSelftest(i, deps = realDeps) {
|
|
|
10917
11375
|
`,
|
|
10918
11376
|
timeoutMs: i.timeoutMs ?? SELFTEST_TIMEOUT_MS
|
|
10919
11377
|
});
|
|
10920
|
-
const seen = observe(spec,
|
|
11378
|
+
const seen = observe(spec, run3.stdout);
|
|
10921
11379
|
const answer = [seen.finalText ?? "", ...seen.texts].join("\n");
|
|
10922
11380
|
const usage2 = { ...seen.tokens !== void 0 ? { tokens: seen.tokens } : {}, ...seen.costUsd !== void 0 ? { costUsd: seen.costUsd } : {} };
|
|
10923
|
-
if (!
|
|
11381
|
+
if (!run3.spawnError && !run3.timedOut && answer.toLowerCase().includes(i.workspaceName.toLowerCase())) {
|
|
10924
11382
|
const auth = spec.harness === "claude" ? ` on ${describeClaudeAuth(spec)}` : "";
|
|
10925
11383
|
return finish({ ok: true, workspace: i.workspaceName, detail: `read workspace "${i.workspaceName}"${auth}`, ...usage2 });
|
|
10926
11384
|
}
|
|
10927
|
-
const { failure, detail } = classify(spec,
|
|
11385
|
+
const { failure, detail } = classify(spec, run3, seen);
|
|
10928
11386
|
return finish({ ok: false, failure, detail, ...usage2 });
|
|
10929
11387
|
} finally {
|
|
10930
11388
|
try {
|
|
10931
|
-
|
|
11389
|
+
rmSync14(cwd, { recursive: true, force: true });
|
|
10932
11390
|
} catch {
|
|
10933
11391
|
}
|
|
10934
11392
|
}
|
|
@@ -10939,11 +11397,11 @@ function formatResult(r) {
|
|
|
10939
11397
|
const secs = `${(r.ms / 1e3).toFixed(1)}s`;
|
|
10940
11398
|
return r.ok ? `${r.toolchain}: PASS ${r.detail} (${secs}${tok})` : `${r.toolchain}: FAIL ${r.failure} \u2014 ${r.detail} (${secs})`;
|
|
10941
11399
|
}
|
|
10942
|
-
var fileFor2 = (stateDir) =>
|
|
11400
|
+
var fileFor2 = (stateDir) => join25(stateDir, "selftest.json");
|
|
10943
11401
|
function readSelftest(stateDir) {
|
|
10944
11402
|
try {
|
|
10945
11403
|
const p = fileFor2(stateDir);
|
|
10946
|
-
return existsSync22(p) ? JSON.parse(
|
|
11404
|
+
return existsSync22(p) ? JSON.parse(readFileSync21(p, "utf8")) : {};
|
|
10947
11405
|
} catch {
|
|
10948
11406
|
return {};
|
|
10949
11407
|
}
|
|
@@ -10951,8 +11409,8 @@ function readSelftest(stateDir) {
|
|
|
10951
11409
|
function recordSelftest(stateDir, results) {
|
|
10952
11410
|
const all = readSelftest(stateDir);
|
|
10953
11411
|
for (const r of results) all[r.toolchain] = r;
|
|
10954
|
-
|
|
10955
|
-
|
|
11412
|
+
mkdirSync14(stateDir, { recursive: true });
|
|
11413
|
+
writeFileSync16(fileFor2(stateDir), `${JSON.stringify(all, null, 2)}
|
|
10956
11414
|
`);
|
|
10957
11415
|
}
|
|
10958
11416
|
function doctorSelftestLines(stored, toolchains) {
|
|
@@ -10964,7 +11422,7 @@ function doctorSelftestLines(stored, toolchains) {
|
|
|
10964
11422
|
}
|
|
10965
11423
|
|
|
10966
11424
|
// src/cli.ts
|
|
10967
|
-
import { resolve as resolve6, dirname as dirname25, join as
|
|
11425
|
+
import { resolve as resolve6, dirname as dirname25, join as join49, basename as basename15 } from "path";
|
|
10968
11426
|
import { fileURLToPath } from "url";
|
|
10969
11427
|
import { hostname as hostname4, homedir as homedir11 } from "os";
|
|
10970
11428
|
import { spawnSync as spawnSync10 } from "child_process";
|
|
@@ -11161,14 +11619,14 @@ function rosterMarkdown(roster, meId) {
|
|
|
11161
11619
|
}
|
|
11162
11620
|
|
|
11163
11621
|
// src/connect-resume.ts
|
|
11164
|
-
import { mkdirSync as
|
|
11165
|
-
import { dirname as dirname9, join as
|
|
11622
|
+
import { mkdirSync as mkdirSync15, readFileSync as readFileSync22, rmSync as rmSync15, writeFileSync as writeFileSync17 } from "fs";
|
|
11623
|
+
import { dirname as dirname9, join as join26 } from "path";
|
|
11166
11624
|
function lastSessionFile(stateDir) {
|
|
11167
|
-
return
|
|
11625
|
+
return join26(stateDir, "connect-session.json");
|
|
11168
11626
|
}
|
|
11169
11627
|
function readPointers(stateDir) {
|
|
11170
11628
|
try {
|
|
11171
|
-
const parsed = JSON.parse(
|
|
11629
|
+
const parsed = JSON.parse(readFileSync22(lastSessionFile(stateDir), "utf8"));
|
|
11172
11630
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
11173
11631
|
const out = {};
|
|
11174
11632
|
for (const [host, v] of Object.entries(parsed)) {
|
|
@@ -11182,8 +11640,8 @@ function readPointers(stateDir) {
|
|
|
11182
11640
|
}
|
|
11183
11641
|
function writePointers(stateDir, pointers) {
|
|
11184
11642
|
const file = lastSessionFile(stateDir);
|
|
11185
|
-
|
|
11186
|
-
|
|
11643
|
+
mkdirSync15(dirname9(file), { recursive: true });
|
|
11644
|
+
writeFileSync17(file, `${JSON.stringify(pointers, null, 2)}
|
|
11187
11645
|
`, { mode: 384 });
|
|
11188
11646
|
}
|
|
11189
11647
|
function rememberSession(stateDir, host, workspaceSlug) {
|
|
@@ -11200,7 +11658,7 @@ function forgetWorkspace(stateDir, host, workspaceSlug) {
|
|
|
11200
11658
|
const { [host]: _old, ...rest } = all;
|
|
11201
11659
|
const next = left.length ? { ...rest, [host]: left } : rest;
|
|
11202
11660
|
if (Object.keys(next).length) writePointers(stateDir, next);
|
|
11203
|
-
else
|
|
11661
|
+
else rmSync15(lastSessionFile(stateDir), { force: true });
|
|
11204
11662
|
} catch {
|
|
11205
11663
|
}
|
|
11206
11664
|
}
|
|
@@ -11240,7 +11698,7 @@ function forgetSession(stateDir, host) {
|
|
|
11240
11698
|
try {
|
|
11241
11699
|
const { [host]: _gone, ...rest } = readPointers(stateDir);
|
|
11242
11700
|
if (Object.keys(rest).length) writePointers(stateDir, rest);
|
|
11243
|
-
else
|
|
11701
|
+
else rmSync15(lastSessionFile(stateDir), { force: true });
|
|
11244
11702
|
} catch {
|
|
11245
11703
|
}
|
|
11246
11704
|
}
|
|
@@ -11268,15 +11726,15 @@ async function signOutSessions(o) {
|
|
|
11268
11726
|
}
|
|
11269
11727
|
|
|
11270
11728
|
// src/reset.ts
|
|
11271
|
-
import { existsSync as existsSync23, mkdirSync as
|
|
11272
|
-
import { dirname as dirname10, join as
|
|
11729
|
+
import { existsSync as existsSync23, mkdirSync as mkdirSync16, readFileSync as readFileSync23, readdirSync as readdirSync9, rmSync as rmSync16, statSync as statSync8, writeFileSync as writeFileSync18 } from "fs";
|
|
11730
|
+
import { dirname as dirname10, join as join27 } from "path";
|
|
11273
11731
|
import { hostFromUrl as hostFromUrl3 } from "@tablation/client";
|
|
11274
11732
|
function clonedCheckoutsFile(stateDir) {
|
|
11275
|
-
return
|
|
11733
|
+
return join27(stateDir, "cloned-checkouts.json");
|
|
11276
11734
|
}
|
|
11277
11735
|
function readClonedCheckouts(stateDir) {
|
|
11278
11736
|
try {
|
|
11279
|
-
const parsed = JSON.parse(
|
|
11737
|
+
const parsed = JSON.parse(readFileSync23(clonedCheckoutsFile(stateDir), "utf8"));
|
|
11280
11738
|
return Array.isArray(parsed) ? parsed.filter((d) => typeof d === "string") : [];
|
|
11281
11739
|
} catch {
|
|
11282
11740
|
return [];
|
|
@@ -11286,8 +11744,8 @@ function recordClonedCheckout(stateDir, dir) {
|
|
|
11286
11744
|
const known = readClonedCheckouts(stateDir);
|
|
11287
11745
|
if (known.includes(dir)) return;
|
|
11288
11746
|
const file = clonedCheckoutsFile(stateDir);
|
|
11289
|
-
|
|
11290
|
-
|
|
11747
|
+
mkdirSync16(dirname10(file), { recursive: true });
|
|
11748
|
+
writeFileSync18(file, `${JSON.stringify([...known, dir], null, 2)}
|
|
11291
11749
|
`);
|
|
11292
11750
|
}
|
|
11293
11751
|
var mark = (dryRun) => dryRun ? "would-remove" : "removed";
|
|
@@ -11342,7 +11800,7 @@ async function runReset(o, fx) {
|
|
|
11342
11800
|
if (o.beforeState) steps.push(...await o.beforeState());
|
|
11343
11801
|
steps.push(...o.wipeState ? wipeStateDir(o.stateDir, o.dryRun) : removeState(o.stateDir, o.dryRun));
|
|
11344
11802
|
if (existsSync23(o.configFile)) {
|
|
11345
|
-
if (!o.dryRun)
|
|
11803
|
+
if (!o.dryRun) rmSync16(o.configFile, { force: true });
|
|
11346
11804
|
steps.push({ step: "config", target: o.configFile, action: mark(o.dryRun) });
|
|
11347
11805
|
} else {
|
|
11348
11806
|
steps.push({ step: "config", target: o.configFile, action: "absent" });
|
|
@@ -11358,21 +11816,21 @@ function removeCheckout(dir, dryRun, fx) {
|
|
|
11358
11816
|
if (linkedWorktrees.length) {
|
|
11359
11817
|
return { step: "checkout", target: dir, action: "kept", detail: `has linked worktrees (${linkedWorktrees.join(", ")}); remove them first` };
|
|
11360
11818
|
}
|
|
11361
|
-
if (!dryRun)
|
|
11819
|
+
if (!dryRun) rmSync16(dir, { recursive: true, force: true });
|
|
11362
11820
|
return { step: "checkout", target: dir, action: mark(dryRun) };
|
|
11363
11821
|
}
|
|
11364
11822
|
function removeState(stateDir, dryRun) {
|
|
11365
11823
|
const out = [];
|
|
11366
|
-
const targets = [
|
|
11824
|
+
const targets = [join27(stateDir, "resolved"), join27(stateDir, "keys"), clonedCheckoutsFile(stateDir), join27(stateDir, "connect-session.json"), join27(stateDir, "reload-requested"), join27(stateDir, "daemon.started")];
|
|
11367
11825
|
try {
|
|
11368
11826
|
for (const name of readdirSync9(stateDir)) {
|
|
11369
|
-
if (name.startsWith(".") && statSync8(
|
|
11827
|
+
if (name.startsWith(".") && statSync8(join27(stateDir, name)).isFile()) targets.push(join27(stateDir, name));
|
|
11370
11828
|
}
|
|
11371
11829
|
} catch {
|
|
11372
11830
|
}
|
|
11373
11831
|
for (const t of targets) {
|
|
11374
11832
|
if (!existsSync23(t)) continue;
|
|
11375
|
-
if (!dryRun)
|
|
11833
|
+
if (!dryRun) rmSync16(t, { recursive: true, force: true });
|
|
11376
11834
|
out.push({ step: "state", target: t, action: mark(dryRun) });
|
|
11377
11835
|
}
|
|
11378
11836
|
if (!out.length) out.push({ step: "state", target: stateDir, action: "absent", detail: "nothing to remove" });
|
|
@@ -11386,7 +11844,7 @@ function wipeStateDir(stateDir, dryRun) {
|
|
|
11386
11844
|
return [{ step: "state", target: stateDir, action: "absent", detail: "no state directory" }];
|
|
11387
11845
|
}
|
|
11388
11846
|
if (!names.length) return [{ step: "state", target: stateDir, action: "absent", detail: "already empty" }];
|
|
11389
|
-
if (!dryRun) for (const n of names)
|
|
11847
|
+
if (!dryRun) for (const n of names) rmSync16(join27(stateDir, n), { recursive: true, force: true });
|
|
11390
11848
|
return [{ step: "state", target: stateDir, action: mark(dryRun), detail: `${names.length} entr${names.length === 1 ? "y" : "ies"}` }];
|
|
11391
11849
|
}
|
|
11392
11850
|
function describeResetStep(s) {
|
|
@@ -11410,21 +11868,21 @@ async function runDeprovision(o, fx, deps) {
|
|
|
11410
11868
|
};
|
|
11411
11869
|
const afterScheduler = async () => {
|
|
11412
11870
|
const steps2 = [];
|
|
11413
|
-
for (const
|
|
11414
|
-
const target = `${
|
|
11871
|
+
for (const run3 of deps.liveRuns()) {
|
|
11872
|
+
const target = `${run3.role} run on ${run3.route} (pid ${run3.pid})`;
|
|
11415
11873
|
if (o.dryRun) {
|
|
11416
11874
|
steps2.push({ step: "run", target, action: "would-remove" });
|
|
11417
11875
|
continue;
|
|
11418
11876
|
}
|
|
11419
11877
|
try {
|
|
11420
|
-
await deps.killRun(
|
|
11421
|
-
deps.recordKill(
|
|
11878
|
+
await deps.killRun(run3);
|
|
11879
|
+
deps.recordKill(run3);
|
|
11422
11880
|
} catch (e) {
|
|
11423
11881
|
steps2.push({ step: "run", target, action: "failed", detail: e.message });
|
|
11424
11882
|
continue;
|
|
11425
11883
|
}
|
|
11426
11884
|
try {
|
|
11427
|
-
await deps.handBack(
|
|
11885
|
+
await deps.handBack(run3);
|
|
11428
11886
|
steps2.push({ step: "run", target, action: "removed", detail: "killed" });
|
|
11429
11887
|
} catch (e) {
|
|
11430
11888
|
steps2.push({ step: "run", target, action: "removed", detail: `killed; could not tell its ticket (${e.message})` });
|
|
@@ -11471,8 +11929,8 @@ import { spawnSync as spawnSync6 } from "child_process";
|
|
|
11471
11929
|
import { existsSync as existsSync26, readdirSync as readdirSync12 } from "fs";
|
|
11472
11930
|
|
|
11473
11931
|
// src/interrupted.ts
|
|
11474
|
-
import { existsSync as existsSync24, readdirSync as readdirSync10, readFileSync as
|
|
11475
|
-
import { join as
|
|
11932
|
+
import { existsSync as existsSync24, readdirSync as readdirSync10, readFileSync as readFileSync24 } from "fs";
|
|
11933
|
+
import { join as join28 } from "path";
|
|
11476
11934
|
var INTERRUPTED_MARKER = calloutHeader("NOTE", "Interrupted");
|
|
11477
11935
|
function interruptedComment(b) {
|
|
11478
11936
|
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.";
|
|
@@ -11493,19 +11951,19 @@ function runFinished(streamPath) {
|
|
|
11493
11951
|
const path = `${streamPath}.meta.json`;
|
|
11494
11952
|
if (!existsSync24(path)) return false;
|
|
11495
11953
|
try {
|
|
11496
|
-
return JSON.parse(
|
|
11954
|
+
return JSON.parse(readFileSync24(path, "utf8")).running !== true;
|
|
11497
11955
|
} catch {
|
|
11498
11956
|
return true;
|
|
11499
11957
|
}
|
|
11500
11958
|
}
|
|
11501
11959
|
function orphanedStreams(o) {
|
|
11502
|
-
const dir =
|
|
11960
|
+
const dir = join28(o.stateDir, "streams");
|
|
11503
11961
|
if (!existsSync24(dir)) return [];
|
|
11504
11962
|
const ownerAlive = o.isOwnerAlive ?? pidAlive;
|
|
11505
11963
|
const out = [];
|
|
11506
11964
|
for (const name of readdirSync10(dir)) {
|
|
11507
11965
|
if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
|
|
11508
|
-
const streamPath =
|
|
11966
|
+
const streamPath = join28(dir, name);
|
|
11509
11967
|
if (runFinished(streamPath)) continue;
|
|
11510
11968
|
const sidecar = readRunSidecar(streamPath);
|
|
11511
11969
|
if (!sidecar || sidecar.route !== o.routeLabel) continue;
|
|
@@ -11559,14 +12017,14 @@ async function reconcileRuns(o) {
|
|
|
11559
12017
|
|
|
11560
12018
|
// src/live-runs.ts
|
|
11561
12019
|
import { existsSync as existsSync25, readdirSync as readdirSync11 } from "fs";
|
|
11562
|
-
import { join as
|
|
12020
|
+
import { join as join29 } from "path";
|
|
11563
12021
|
function liveRuns(stateDir, isAlive = runIsAlive) {
|
|
11564
|
-
const dir =
|
|
12022
|
+
const dir = join29(stateDir, "streams");
|
|
11565
12023
|
if (!existsSync25(dir)) return [];
|
|
11566
12024
|
const out = [];
|
|
11567
12025
|
for (const name of readdirSync11(dir)) {
|
|
11568
12026
|
if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
|
|
11569
|
-
const streamPath =
|
|
12027
|
+
const streamPath = join29(dir, name);
|
|
11570
12028
|
if (runFinished(streamPath)) continue;
|
|
11571
12029
|
const sidecar = readRunSidecar(streamPath);
|
|
11572
12030
|
if (!sidecar || !isAlive(sidecar)) continue;
|
|
@@ -11580,8 +12038,8 @@ function siblingRuns(runs, ownTicketRecordId) {
|
|
|
11580
12038
|
async function settleRunEnd(o) {
|
|
11581
12039
|
const { tracker, emit } = o;
|
|
11582
12040
|
const all = o.runs ?? liveRuns(o.stateDir);
|
|
11583
|
-
const
|
|
11584
|
-
const seatSiblings = siblingRuns(all.filter((r) => r.role === o.role && r.route ===
|
|
12041
|
+
const slug2 = routeSlug(o.route.route);
|
|
12042
|
+
const seatSiblings = siblingRuns(all.filter((r) => r.role === o.role && r.route === slug2), o.ownTicketRecordId);
|
|
11585
12043
|
const shipSiblings = siblingRuns(all, o.ownTicketRecordId);
|
|
11586
12044
|
const newest = (rs) => rs[rs.length - 1];
|
|
11587
12045
|
if (o.memberId) {
|
|
@@ -11603,234 +12061,30 @@ async function settleRunEnd(o) {
|
|
|
11603
12061
|
emit.warn(`could not beat ship idle: ${e.message}`, { step: "agent" });
|
|
11604
12062
|
}
|
|
11605
12063
|
}
|
|
11606
|
-
function describeRun(
|
|
12064
|
+
function describeRun(run3, route2, seatIds, worktreeOf = (d, n) => {
|
|
11607
12065
|
try {
|
|
11608
12066
|
return worktreeForNumber(d, n)?.path ?? null;
|
|
11609
12067
|
} catch {
|
|
11610
12068
|
return null;
|
|
11611
12069
|
}
|
|
11612
12070
|
}) {
|
|
11613
|
-
const ownRoute =
|
|
11614
|
-
const repoDir =
|
|
11615
|
-
const num =
|
|
12071
|
+
const ownRoute = run3.route === routeSlug(route2.route);
|
|
12072
|
+
const repoDir = run3.repo ? reposOf(route2).find((t) => t.name === run3.repo)?.dir : void 0;
|
|
12073
|
+
const num = run3.ticket?.replace(/^\D+/, "") ?? "";
|
|
11616
12074
|
return {
|
|
11617
|
-
role:
|
|
11618
|
-
seat: ownRoute ? seatIds[
|
|
11619
|
-
ticket:
|
|
11620
|
-
ticketRecordId:
|
|
11621
|
-
repo:
|
|
12075
|
+
role: run3.role,
|
|
12076
|
+
seat: ownRoute ? seatIds[run3.role] ?? null : null,
|
|
12077
|
+
ticket: run3.ticket,
|
|
12078
|
+
ticketRecordId: run3.ticketRecordId,
|
|
12079
|
+
repo: run3.repo ?? null,
|
|
11622
12080
|
worktree: repoDir && num ? worktreeOf(repoDir, num) : null,
|
|
11623
|
-
stream:
|
|
11624
|
-
pid:
|
|
11625
|
-
startedAt:
|
|
11626
|
-
deadline:
|
|
12081
|
+
stream: run3.streamPath,
|
|
12082
|
+
pid: run3.pid,
|
|
12083
|
+
startedAt: run3.spawnedAt,
|
|
12084
|
+
deadline: run3.deadline ?? null
|
|
11627
12085
|
};
|
|
11628
12086
|
}
|
|
11629
12087
|
|
|
11630
|
-
// src/passenger-containers.ts
|
|
11631
|
-
import { execFileSync as execFileSync6 } from "child_process";
|
|
11632
|
-
import { createHash as createHash5 } from "crypto";
|
|
11633
|
-
var realExec = (cmd, args, opts) => {
|
|
11634
|
-
try {
|
|
11635
|
-
return execFileSync6(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: opts?.timeoutMs });
|
|
11636
|
-
} catch (e) {
|
|
11637
|
-
const err3 = e;
|
|
11638
|
-
const why = [err3.stderr, err3.stdout].map((x) => (x ?? "").trim()).filter(Boolean).join(" / ");
|
|
11639
|
-
throw new Error(`${cmd} ${args.join(" ")}: ${why || err3.message.trim()}`);
|
|
11640
|
-
}
|
|
11641
|
-
};
|
|
11642
|
-
var DOCKER_INFO_TIMEOUT_MS = 1e4;
|
|
11643
|
-
function dockerAvailable(exec = realExec) {
|
|
11644
|
-
try {
|
|
11645
|
-
exec("docker", ["info"], { timeoutMs: DOCKER_INFO_TIMEOUT_MS });
|
|
11646
|
-
return true;
|
|
11647
|
-
} catch {
|
|
11648
|
-
return false;
|
|
11649
|
-
}
|
|
11650
|
-
}
|
|
11651
|
-
var PASSENGER_IMAGE_REPO = "tablation/crew-passenger-mcp";
|
|
11652
|
-
function passengerImageRef(cfg) {
|
|
11653
|
-
return process.env.CREW_PASSENGER_IMAGE || cfg.ship.passengerImage || `${PASSENGER_IMAGE_REPO}:${crewVersion(cfg.crewHome)}`;
|
|
11654
|
-
}
|
|
11655
|
-
function imagePresent(image, exec = realExec) {
|
|
11656
|
-
try {
|
|
11657
|
-
exec("docker", ["image", "inspect", image]);
|
|
11658
|
-
return true;
|
|
11659
|
-
} catch {
|
|
11660
|
-
return false;
|
|
11661
|
-
}
|
|
11662
|
-
}
|
|
11663
|
-
function ensureImage(image, exec = realExec) {
|
|
11664
|
-
if (!imagePresent(image, exec)) exec("docker", ["pull", image]);
|
|
11665
|
-
}
|
|
11666
|
-
function containerNameFor(workspaceId) {
|
|
11667
|
-
return `crew-passenger-${workspaceId}`;
|
|
11668
|
-
}
|
|
11669
|
-
function portFor(workspaceId, base = 28765, span = 1e3) {
|
|
11670
|
-
const digest = createHash5("sha256").update(workspaceId).digest();
|
|
11671
|
-
return base + digest.readUInt32BE(0) % span;
|
|
11672
|
-
}
|
|
11673
|
-
function mountsDigest(mounts, image) {
|
|
11674
|
-
const sorted2 = [...mounts].sort((a, b) => a.containerPath.localeCompare(b.containerPath));
|
|
11675
|
-
return createHash5("sha256").update(JSON.stringify({ mounts: sorted2, image })).digest("hex").slice(0, 16);
|
|
11676
|
-
}
|
|
11677
|
-
function planContainers(cfg) {
|
|
11678
|
-
const byWorkspace = /* @__PURE__ */ new Map();
|
|
11679
|
-
const image = passengerImageRef(cfg);
|
|
11680
|
-
for (const route2 of cfg.routes) {
|
|
11681
|
-
if (!route2.enabled || !route2.hostPassengers || !route2.resolved?.workspaceId) continue;
|
|
11682
|
-
const workspaceId = route2.resolved.workspaceId;
|
|
11683
|
-
const mounts = byWorkspace.get(workspaceId) ?? /* @__PURE__ */ new Map();
|
|
11684
|
-
for (const target of passengerCheckoutTargets(route2)) {
|
|
11685
|
-
mounts.set(target.name, { hostPath: target.dir, containerPath: `/workspace/${target.name}` });
|
|
11686
|
-
}
|
|
11687
|
-
byWorkspace.set(workspaceId, mounts);
|
|
11688
|
-
}
|
|
11689
|
-
return [...byWorkspace.entries()].filter(([, mounts]) => mounts.size > 0).map(([workspaceId, mounts]) => {
|
|
11690
|
-
const mountList = [...mounts.values()];
|
|
11691
|
-
return {
|
|
11692
|
-
workspaceId,
|
|
11693
|
-
containerName: containerNameFor(workspaceId),
|
|
11694
|
-
port: portFor(workspaceId),
|
|
11695
|
-
image,
|
|
11696
|
-
mounts: mountList,
|
|
11697
|
-
mountsHash: mountsDigest(mountList, image)
|
|
11698
|
-
};
|
|
11699
|
-
});
|
|
11700
|
-
}
|
|
11701
|
-
function listPassengerContainers(exec = realExec) {
|
|
11702
|
-
const out = exec("docker", ["ps", "-a", "--filter", "name=crew-passenger-", "--format", "{{.Names}} {{.State}}"]);
|
|
11703
|
-
return out.split("\n").map((l) => l.trim()).filter(Boolean).map((line) => {
|
|
11704
|
-
const [name, state] = line.split(" ");
|
|
11705
|
-
return { name, running: state === "running" };
|
|
11706
|
-
});
|
|
11707
|
-
}
|
|
11708
|
-
function currentMountsHash(name, exec) {
|
|
11709
|
-
try {
|
|
11710
|
-
const out = exec("docker", ["inspect", "--format", '{{ index .Config.Labels "crew.mounts.hash" }}', name]);
|
|
11711
|
-
const hash = out.trim();
|
|
11712
|
-
return hash || void 0;
|
|
11713
|
-
} catch {
|
|
11714
|
-
return void 0;
|
|
11715
|
-
}
|
|
11716
|
-
}
|
|
11717
|
-
function stopContainer(name, exec = realExec) {
|
|
11718
|
-
try {
|
|
11719
|
-
exec("docker", ["rm", "-f", name]);
|
|
11720
|
-
} catch {
|
|
11721
|
-
}
|
|
11722
|
-
}
|
|
11723
|
-
function startContainer(plan, tablationApiBaseUrl, sync, exec = realExec) {
|
|
11724
|
-
const args = [
|
|
11725
|
-
"run",
|
|
11726
|
-
"-d",
|
|
11727
|
-
"--name",
|
|
11728
|
-
plan.containerName,
|
|
11729
|
-
"--restart",
|
|
11730
|
-
"unless-stopped",
|
|
11731
|
-
"-p",
|
|
11732
|
-
`${plan.port}:8765`,
|
|
11733
|
-
"-e",
|
|
11734
|
-
`PASSENGER_MCP_WORKSPACE_ID=${plan.workspaceId}`,
|
|
11735
|
-
"-e",
|
|
11736
|
-
`TABLATION_API_BASE_URL=${tablationApiBaseUrl}`,
|
|
11737
|
-
"--label",
|
|
11738
|
-
`crew.mounts.hash=${plan.mountsHash}`
|
|
11739
|
-
];
|
|
11740
|
-
if (sync) {
|
|
11741
|
-
args.push(
|
|
11742
|
-
"--add-host",
|
|
11743
|
-
"host.docker.internal:host-gateway",
|
|
11744
|
-
"-e",
|
|
11745
|
-
`PASSENGER_SYNC_URL=http://${sync.host}:${sync.port}/sync`,
|
|
11746
|
-
"-e",
|
|
11747
|
-
`PASSENGER_SYNC_SECRET=${sync.secret}`,
|
|
11748
|
-
"-e",
|
|
11749
|
-
`PASSENGER_MCP_SECRET=${sync.secret}`
|
|
11750
|
-
);
|
|
11751
|
-
}
|
|
11752
|
-
for (const m of plan.mounts) args.push("-v", `${m.hostPath}:${m.containerPath}:ro`);
|
|
11753
|
-
args.push(plan.image);
|
|
11754
|
-
exec("docker", args);
|
|
11755
|
-
}
|
|
11756
|
-
function syncPassengerContainers(cfg, tablationApiBaseUrl, exec = realExec, syncEndpoints = /* @__PURE__ */ new Map()) {
|
|
11757
|
-
const plans = planContainers(cfg);
|
|
11758
|
-
const plannedByName = new Map(plans.map((p) => [p.containerName, p]));
|
|
11759
|
-
const existing = listPassengerContainers(exec);
|
|
11760
|
-
const existingByName = new Map(existing.map((c) => [c.name, c]));
|
|
11761
|
-
const started = [];
|
|
11762
|
-
const recreated = [];
|
|
11763
|
-
const stopped = [];
|
|
11764
|
-
const ensured = /* @__PURE__ */ new Set();
|
|
11765
|
-
const start = (plan, sync) => {
|
|
11766
|
-
if (!ensured.has(plan.image)) {
|
|
11767
|
-
ensureImage(plan.image, exec);
|
|
11768
|
-
ensured.add(plan.image);
|
|
11769
|
-
}
|
|
11770
|
-
startContainer(plan, tablationApiBaseUrl, sync, exec);
|
|
11771
|
-
};
|
|
11772
|
-
for (const plan of plans) {
|
|
11773
|
-
const sync = syncEndpoints.get(plan.workspaceId);
|
|
11774
|
-
const current = existingByName.get(plan.containerName);
|
|
11775
|
-
if (!current) {
|
|
11776
|
-
start(plan, sync);
|
|
11777
|
-
started.push(plan.containerName);
|
|
11778
|
-
} else if (!current.running) {
|
|
11779
|
-
stopContainer(plan.containerName, exec);
|
|
11780
|
-
start(plan, sync);
|
|
11781
|
-
started.push(plan.containerName);
|
|
11782
|
-
} else if (currentMountsHash(plan.containerName, exec) !== plan.mountsHash) {
|
|
11783
|
-
stopContainer(plan.containerName, exec);
|
|
11784
|
-
start(plan, sync);
|
|
11785
|
-
recreated.push(plan.containerName);
|
|
11786
|
-
}
|
|
11787
|
-
}
|
|
11788
|
-
for (const { name } of existing) {
|
|
11789
|
-
if (!plannedByName.has(name)) {
|
|
11790
|
-
stopContainer(name, exec);
|
|
11791
|
-
stopped.push(name);
|
|
11792
|
-
}
|
|
11793
|
-
}
|
|
11794
|
-
return { started, recreated, stopped };
|
|
11795
|
-
}
|
|
11796
|
-
function passengerCheckoutSyncSpecs(cfg, workspaceId) {
|
|
11797
|
-
const specs = [];
|
|
11798
|
-
for (const route2 of cfg.routes) {
|
|
11799
|
-
if (!route2.enabled || !route2.hostPassengers || !route2.resolved?.workspaceId) continue;
|
|
11800
|
-
if (workspaceId && route2.resolved.workspaceId !== workspaceId) continue;
|
|
11801
|
-
const routeWorkspaceId = route2.resolved.workspaceId;
|
|
11802
|
-
const ordinaryByName = new Map(reposOf(route2).map((t) => [t.name, t]));
|
|
11803
|
-
for (const target of passengerCheckoutTargets(route2)) {
|
|
11804
|
-
const ordinary = ordinaryByName.get(target.name);
|
|
11805
|
-
const override = route2.repoOverrides[target.name];
|
|
11806
|
-
const effective = resolveRepoConfig(
|
|
11807
|
-
ordinary ? loadRepoConfig(ordinary.dir) : null,
|
|
11808
|
-
{ branch: { ...route2.branch, ...override?.branch } },
|
|
11809
|
-
ordinary?.dir ?? target.dir
|
|
11810
|
-
);
|
|
11811
|
-
specs.push({
|
|
11812
|
-
name: target.name,
|
|
11813
|
-
workspaceId: routeWorkspaceId,
|
|
11814
|
-
dir: target.dir,
|
|
11815
|
-
remote: target.remote,
|
|
11816
|
-
gitRemoteName: effective.branch.remote,
|
|
11817
|
-
base: effective.branch.base,
|
|
11818
|
-
originUrl: ordinary ? originUrlOf(ordinary.dir, effective.branch.remote) : null,
|
|
11819
|
-
cloneScheme: cfg.ship.cloneScheme,
|
|
11820
|
-
...target.forgeHost ? { forgeHost: target.forgeHost } : {}
|
|
11821
|
-
});
|
|
11822
|
-
}
|
|
11823
|
-
}
|
|
11824
|
-
return specs;
|
|
11825
|
-
}
|
|
11826
|
-
function syncAllPassengerCheckouts(cfg) {
|
|
11827
|
-
return passengerCheckoutSyncSpecs(cfg).map((spec) => ({
|
|
11828
|
-
name: spec.name,
|
|
11829
|
-
workspaceId: spec.workspaceId,
|
|
11830
|
-
outcome: syncPassengerCheckout(spec.dir, spec.remote, spec.gitRemoteName, spec.base, { originUrl: spec.originUrl, scheme: spec.cloneScheme, host: spec.forgeHost })
|
|
11831
|
-
}));
|
|
11832
|
-
}
|
|
11833
|
-
|
|
11834
12088
|
// src/deprovision-host.ts
|
|
11835
12089
|
var DOCKER_LABEL_OWNER = "crew.owner=crew";
|
|
11836
12090
|
var DOCKER_LABEL_CACHE = "crew.kind=cache";
|
|
@@ -11862,22 +12116,22 @@ function realDeprovisionDeps(cfg, emit, crewHome, getStore) {
|
|
|
11862
12116
|
};
|
|
11863
12117
|
return {
|
|
11864
12118
|
liveRuns: () => liveRuns(stateDir).map((r) => ({ role: r.role, route: r.route, pid: r.pid, ticketRecordId: r.ticketRecordId ?? void 0, streamPath: r.streamPath })),
|
|
11865
|
-
killRun: (
|
|
11866
|
-
recordKill(
|
|
11867
|
-
emit.emit(`run_killed: ${
|
|
12119
|
+
killRun: (run3) => killRunGroup(run3.pid),
|
|
12120
|
+
recordKill(run3) {
|
|
12121
|
+
emit.emit(`run_killed: ${run3.role} run on ${run3.route} stopped by crew deprovision`, {
|
|
11868
12122
|
step: "agent",
|
|
11869
|
-
role:
|
|
11870
|
-
data: { event: "run_killed", reason: "deprovision", pid:
|
|
12123
|
+
role: run3.role,
|
|
12124
|
+
data: { event: "run_killed", reason: "deprovision", pid: run3.pid }
|
|
11871
12125
|
});
|
|
11872
12126
|
},
|
|
11873
|
-
async handBack(
|
|
11874
|
-
const route2 = routeFor(
|
|
11875
|
-
if (!
|
|
11876
|
-
const memberId = route2.resolved?.seats[
|
|
12127
|
+
async handBack(run3) {
|
|
12128
|
+
const route2 = routeFor(run3.route);
|
|
12129
|
+
if (!run3.ticketRecordId || !route2) return;
|
|
12130
|
+
const memberId = route2.resolved?.seats[run3.role];
|
|
11877
12131
|
if (!memberId) return;
|
|
11878
|
-
const identity = await resolveSeatKey(route2,
|
|
12132
|
+
const identity = await resolveSeatKey(route2, run3.role, getStore);
|
|
11879
12133
|
const tracker = new Tracker(route2, cfg.ship, { apiKey: identity.apiKey });
|
|
11880
|
-
const t = await tracker.ticket(
|
|
12134
|
+
const t = await tracker.ticket(run3.ticketRecordId);
|
|
11881
12135
|
const target = repoTargetFor(route2, t.repo_id);
|
|
11882
12136
|
const num = String(t[tracker.contract.columns.key] ?? "").replace(/^\D+/, "");
|
|
11883
12137
|
const wt = target && num ? worktreeForNumber(target.dir, num) : null;
|
|
@@ -11886,7 +12140,7 @@ function realDeprovisionDeps(cfg, emit, crewHome, getStore) {
|
|
|
11886
12140
|
if (wt) dirty = status(wt.path).length > 0;
|
|
11887
12141
|
} catch {
|
|
11888
12142
|
}
|
|
11889
|
-
await tracker.postEvent(
|
|
12143
|
+
await tracker.postEvent(run3.ticketRecordId, interruptedComment({
|
|
11890
12144
|
branch: wt?.branch ?? null,
|
|
11891
12145
|
dirty,
|
|
11892
12146
|
reason: "the host was released and wiped"
|
|
@@ -11941,9 +12195,9 @@ function realDeprovisionDeps(cfg, emit, crewHome, getStore) {
|
|
|
11941
12195
|
}
|
|
11942
12196
|
|
|
11943
12197
|
// src/pool-member.ts
|
|
11944
|
-
import { mkdirSync as
|
|
12198
|
+
import { mkdirSync as mkdirSync17, readFileSync as readFileSync25, renameSync as renameSync4, writeFileSync as writeFileSync19 } from "fs";
|
|
11945
12199
|
import { homedir as homedir8 } from "os";
|
|
11946
|
-
import { dirname as dirname11, join as
|
|
12200
|
+
import { dirname as dirname11, join as join30 } from "path";
|
|
11947
12201
|
var POOL_EXIT = { ok: 0, failed: 1, credential: 2 };
|
|
11948
12202
|
function backoffMs(attempt) {
|
|
11949
12203
|
return Math.min(6e4, 5e3 * 2 ** Math.max(0, attempt));
|
|
@@ -12132,24 +12386,24 @@ async function runPoolMember(deps) {
|
|
|
12132
12386
|
}
|
|
12133
12387
|
}
|
|
12134
12388
|
function defaultMemberDir(env = process.env) {
|
|
12135
|
-
return env.CREW_POOL_DIR ??
|
|
12389
|
+
return env.CREW_POOL_DIR ?? join30(env.XDG_STATE_HOME ?? join30(homedir8(), ".local", "state"), "crew-pool-member");
|
|
12136
12390
|
}
|
|
12137
12391
|
function fileMemberStore(dir) {
|
|
12138
|
-
const file =
|
|
12392
|
+
const file = join30(dir, "member.json");
|
|
12139
12393
|
return {
|
|
12140
12394
|
read() {
|
|
12141
12395
|
try {
|
|
12142
|
-
return JSON.parse(
|
|
12396
|
+
return JSON.parse(readFileSync25(file, "utf8"));
|
|
12143
12397
|
} catch {
|
|
12144
12398
|
return {};
|
|
12145
12399
|
}
|
|
12146
12400
|
},
|
|
12147
12401
|
write(f) {
|
|
12148
|
-
|
|
12402
|
+
mkdirSync17(dirname11(file), { recursive: true, mode: 448 });
|
|
12149
12403
|
const tmp = `${file}.tmp`;
|
|
12150
|
-
|
|
12404
|
+
writeFileSync19(tmp, `${JSON.stringify(f)}
|
|
12151
12405
|
`, { mode: 384 });
|
|
12152
|
-
|
|
12406
|
+
renameSync4(tmp, file);
|
|
12153
12407
|
}
|
|
12154
12408
|
};
|
|
12155
12409
|
}
|
|
@@ -12204,13 +12458,13 @@ import { hostname as hostname3 } from "os";
|
|
|
12204
12458
|
|
|
12205
12459
|
// src/machine-id.ts
|
|
12206
12460
|
import { execFileSync as execFileSync7 } from "child_process";
|
|
12207
|
-
import { createHash as
|
|
12208
|
-
import { existsSync as existsSync27, mkdirSync as
|
|
12209
|
-
import { dirname as dirname12, join as
|
|
12461
|
+
import { createHash as createHash7 } from "crypto";
|
|
12462
|
+
import { existsSync as existsSync27, mkdirSync as mkdirSync18, readFileSync as readFileSync26, writeFileSync as writeFileSync20 } from "fs";
|
|
12463
|
+
import { dirname as dirname12, join as join31 } from "path";
|
|
12210
12464
|
function machineIdPathFor(stateDir) {
|
|
12211
|
-
return
|
|
12465
|
+
return join31(stateDir, "machine-id");
|
|
12212
12466
|
}
|
|
12213
|
-
var sha256 = (s) =>
|
|
12467
|
+
var sha256 = (s) => createHash7("sha256").update(s).digest("hex");
|
|
12214
12468
|
function readRawMachineId(platform2 = process.platform) {
|
|
12215
12469
|
try {
|
|
12216
12470
|
if (platform2 === "darwin") {
|
|
@@ -12222,7 +12476,7 @@ function readRawMachineId(platform2 = process.platform) {
|
|
|
12222
12476
|
}
|
|
12223
12477
|
for (const p of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
|
|
12224
12478
|
if (existsSync27(p)) {
|
|
12225
|
-
const id =
|
|
12479
|
+
const id = readFileSync26(p, "utf8").trim();
|
|
12226
12480
|
if (id) return id;
|
|
12227
12481
|
}
|
|
12228
12482
|
}
|
|
@@ -12234,14 +12488,14 @@ function ensureMachineId(stateDir, opts = {}) {
|
|
|
12234
12488
|
if (opts.override?.trim()) return sha256(opts.override.trim());
|
|
12235
12489
|
const path = machineIdPathFor(stateDir);
|
|
12236
12490
|
if (existsSync27(path)) {
|
|
12237
|
-
const cached =
|
|
12491
|
+
const cached = readFileSync26(path, "utf8").trim();
|
|
12238
12492
|
if (/^[0-9a-f]{64}$/.test(cached)) return cached;
|
|
12239
12493
|
}
|
|
12240
12494
|
const raw = (opts.read ?? readRawMachineId)();
|
|
12241
12495
|
if (!raw) return void 0;
|
|
12242
12496
|
const hashed = sha256(raw);
|
|
12243
|
-
|
|
12244
|
-
|
|
12497
|
+
mkdirSync18(dirname12(path), { recursive: true });
|
|
12498
|
+
writeFileSync20(path, `${hashed}
|
|
12245
12499
|
`, { mode: 384 });
|
|
12246
12500
|
return hashed;
|
|
12247
12501
|
}
|
|
@@ -12254,7 +12508,7 @@ function matchShipRows(ships, shipName2, machineId) {
|
|
|
12254
12508
|
|
|
12255
12509
|
// src/provision.ts
|
|
12256
12510
|
import { spawnSync as spawnSync7 } from "child_process";
|
|
12257
|
-
import { existsSync as existsSync28, readFileSync as
|
|
12511
|
+
import { existsSync as existsSync28, readFileSync as readFileSync27, writeFileSync as writeFileSync21 } from "fs";
|
|
12258
12512
|
import { hostname as hostname2 } from "os";
|
|
12259
12513
|
import { parseDocument as parseDocument3 } from "yaml";
|
|
12260
12514
|
|
|
@@ -12263,7 +12517,7 @@ import { parseDocument as parseDocument2 } from "yaml";
|
|
|
12263
12517
|
|
|
12264
12518
|
// src/repos-cmd.ts
|
|
12265
12519
|
import { homedir as homedir9 } from "os";
|
|
12266
|
-
import { basename as basename9, join as
|
|
12520
|
+
import { basename as basename9, join as join32, resolve as resolve3 } from "path";
|
|
12267
12521
|
import { isMap, isSeq, parseDocument, stringify } from "yaml";
|
|
12268
12522
|
var ReposError = class extends Error {
|
|
12269
12523
|
};
|
|
@@ -12541,7 +12795,7 @@ function planRepoClone(i) {
|
|
|
12541
12795
|
}
|
|
12542
12796
|
const ship = doc.get("ship", true);
|
|
12543
12797
|
const basePath = String(m.get("reposBasePath") ?? (isMap(ship) ? ship.get("reposBasePath") : void 0) ?? "~/Crew");
|
|
12544
|
-
const target = i.dir ? expandDir(i.dir, i.base) :
|
|
12798
|
+
const target = i.dir ? expandDir(i.dir, i.base) : join32(expandDir(basePath, i.base), i.route.split("/")[0], name);
|
|
12545
12799
|
const add = planRepoAdd({ text: i.text, base: i.base, route: i.route, path: target, origin: remote, resolved: i.resolved, name, isCheckout: i.isCheckout });
|
|
12546
12800
|
const forgeHost = i.resolved?.repoForgeHosts?.[id];
|
|
12547
12801
|
return { ...add, remote, target, ...forgeHost ? { forgeHost } : {} };
|
|
@@ -12814,13 +13068,13 @@ function realProvisionDeps(crewHome) {
|
|
|
12814
13068
|
enableRoute(route2) {
|
|
12815
13069
|
const file = configFile();
|
|
12816
13070
|
if (!file) return false;
|
|
12817
|
-
const text2 =
|
|
13071
|
+
const text2 = readFileSync27(file, "utf8");
|
|
12818
13072
|
const doc = parseDocument3(text2);
|
|
12819
13073
|
const routes = doc.get("routes");
|
|
12820
13074
|
const entry = routes?.items?.find((r) => r.get("route") === route2);
|
|
12821
13075
|
if (!entry) throw new Error(`route ${route2} is not in ${file}`);
|
|
12822
13076
|
if (entry.get("enabled") === true) return false;
|
|
12823
|
-
|
|
13077
|
+
writeFileSync21(file, planEnable(text2, route2));
|
|
12824
13078
|
return true;
|
|
12825
13079
|
},
|
|
12826
13080
|
installState() {
|
|
@@ -12840,7 +13094,7 @@ function realProvisionDeps(crewHome) {
|
|
|
12840
13094
|
continue;
|
|
12841
13095
|
}
|
|
12842
13096
|
for (const p of plan.unitPaths) {
|
|
12843
|
-
if (!existsSync28(p) ||
|
|
13097
|
+
if (!existsSync28(p) || readFileSync27(p, "utf8") !== plan.unitContent[p]) unitsStale = true;
|
|
12844
13098
|
}
|
|
12845
13099
|
}
|
|
12846
13100
|
const label = labelFor(crewHome, "run");
|
|
@@ -12864,10 +13118,10 @@ function realProvisionDeps(crewHome) {
|
|
|
12864
13118
|
setShipSetting(key, value2) {
|
|
12865
13119
|
const file = configFile();
|
|
12866
13120
|
if (!file) return false;
|
|
12867
|
-
const doc = parseDocument3(
|
|
13121
|
+
const doc = parseDocument3(readFileSync27(file, "utf8"));
|
|
12868
13122
|
if (doc.getIn(["ship", key]) === value2) return false;
|
|
12869
13123
|
doc.setIn(["ship", key], value2);
|
|
12870
|
-
|
|
13124
|
+
writeFileSync21(file, doc.toString());
|
|
12871
13125
|
return true;
|
|
12872
13126
|
},
|
|
12873
13127
|
storedToken(host) {
|
|
@@ -12883,12 +13137,12 @@ host=${host}
|
|
|
12883
13137
|
}
|
|
12884
13138
|
|
|
12885
13139
|
// src/tunnel.ts
|
|
12886
|
-
import { spawn as
|
|
12887
|
-
import { existsSync as existsSync29, mkdirSync as
|
|
13140
|
+
import { spawn as spawn6 } from "child_process";
|
|
13141
|
+
import { existsSync as existsSync29, mkdirSync as mkdirSync19, readFileSync as readFileSync28, writeFileSync as writeFileSync22, unlinkSync as unlinkSync3, readdirSync as readdirSync13 } from "fs";
|
|
12888
13142
|
import { randomInt } from "crypto";
|
|
12889
|
-
import { join as
|
|
13143
|
+
import { join as join33 } from "path";
|
|
12890
13144
|
var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
|
|
12891
|
-
var defaultSpawn = (cmd, args) =>
|
|
13145
|
+
var defaultSpawn = (cmd, args) => spawn6(cmd, args, DEFAULT_SPAWN_OPTIONS);
|
|
12892
13146
|
var SLUG_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
12893
13147
|
var SLUG_LENGTH = 12;
|
|
12894
13148
|
function generateTunnelSlug() {
|
|
@@ -12900,12 +13154,12 @@ function bindAddrFor(o, path) {
|
|
|
12900
13154
|
const base = `workspace-${o.workspaceId}-slug-${o.slug}${path ? `-path-${path}` : ""}-key-${o.apiKey}`;
|
|
12901
13155
|
return o.mcpSecret && !path ? `${base}-secret-${o.mcpSecret}` : base;
|
|
12902
13156
|
}
|
|
12903
|
-
function relayBaseUrlFor(
|
|
13157
|
+
function relayBaseUrlFor(slug2, relayHost, relayHttpPort, relayPublicDomain) {
|
|
12904
13158
|
const domain = relayPublicDomain ?? relayHost.replace(/^.*@/, "");
|
|
12905
|
-
return `https://${
|
|
13159
|
+
return `https://${slug2}.${domain}${relayHttpPort === 443 ? "" : `:${relayHttpPort}`}`;
|
|
12906
13160
|
}
|
|
12907
|
-
function publicUrlFor(
|
|
12908
|
-
return `${relayBaseUrlFor(
|
|
13161
|
+
function publicUrlFor(slug2, relayHost, relayHttpPort, relayPublicDomain) {
|
|
13162
|
+
return `${relayBaseUrlFor(slug2, relayHost, relayHttpPort, relayPublicDomain)}/mcp`;
|
|
12909
13163
|
}
|
|
12910
13164
|
function sshArgsFor(o) {
|
|
12911
13165
|
const forwards = [];
|
|
@@ -12958,21 +13212,21 @@ function startTunnel(opts) {
|
|
|
12958
13212
|
};
|
|
12959
13213
|
}
|
|
12960
13214
|
function tunnelStatePath(stateDir, workspaceId) {
|
|
12961
|
-
return
|
|
13215
|
+
return join33(stateDir, "passengers", `${workspaceId}.json`);
|
|
12962
13216
|
}
|
|
12963
13217
|
function readPersistedTunnel(stateDir, workspaceId) {
|
|
12964
13218
|
const path = tunnelStatePath(stateDir, workspaceId);
|
|
12965
13219
|
if (!existsSync29(path)) return void 0;
|
|
12966
13220
|
try {
|
|
12967
|
-
return JSON.parse(
|
|
13221
|
+
return JSON.parse(readFileSync28(path, "utf8"));
|
|
12968
13222
|
} catch {
|
|
12969
13223
|
return void 0;
|
|
12970
13224
|
}
|
|
12971
13225
|
}
|
|
12972
13226
|
function writePersistedTunnel(stateDir, state) {
|
|
12973
13227
|
const path = tunnelStatePath(stateDir, state.workspaceId);
|
|
12974
|
-
|
|
12975
|
-
|
|
13228
|
+
mkdirSync19(join33(stateDir, "passengers"), { recursive: true });
|
|
13229
|
+
writeFileSync22(path, `${JSON.stringify(state, null, 2)}
|
|
12976
13230
|
`);
|
|
12977
13231
|
}
|
|
12978
13232
|
function updatePersistedTunnel(stateDir, workspaceId, patch2) {
|
|
@@ -13006,8 +13260,8 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
|
|
|
13006
13260
|
const controlUnchanged = existing?.controlSocket === deps.controlSocket;
|
|
13007
13261
|
const currentSecret = mcpSecrets.get(plan.workspaceId);
|
|
13008
13262
|
const secretUnchanged = (existing?.mcpSecret ?? void 0) === (currentSecret ?? void 0);
|
|
13009
|
-
const
|
|
13010
|
-
const slugUnchanged = existing?.liveSlug ===
|
|
13263
|
+
const slug2 = existing?.slug ?? generateTunnelSlug();
|
|
13264
|
+
const slugUnchanged = existing?.liveSlug === slug2;
|
|
13011
13265
|
if (existing && alive(existing.pid) && existing.mountsHash === mountsHash && secretUnchanged && slugUnchanged && controlUnchanged) {
|
|
13012
13266
|
if (existing.status === "connecting") {
|
|
13013
13267
|
const settled = { ...existing, status: "connected" };
|
|
@@ -13032,7 +13286,7 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
|
|
|
13032
13286
|
relayHost,
|
|
13033
13287
|
relayPort,
|
|
13034
13288
|
workspaceId: plan.workspaceId,
|
|
13035
|
-
slug,
|
|
13289
|
+
slug: slug2,
|
|
13036
13290
|
apiKey,
|
|
13037
13291
|
localPort: plan.port,
|
|
13038
13292
|
controlSocket: deps.controlSocket,
|
|
@@ -13052,8 +13306,8 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
|
|
|
13052
13306
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
13053
13307
|
status: settledStatus,
|
|
13054
13308
|
mcpSecret: currentSecret,
|
|
13055
|
-
slug,
|
|
13056
|
-
liveSlug:
|
|
13309
|
+
slug: slug2,
|
|
13310
|
+
liveSlug: slug2,
|
|
13057
13311
|
endpointId: existing?.endpointId,
|
|
13058
13312
|
registeredKey: existing?.registeredKey
|
|
13059
13313
|
};
|
|
@@ -13074,7 +13328,7 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
|
|
|
13074
13328
|
}
|
|
13075
13329
|
}
|
|
13076
13330
|
function readAllPersistedWorkspaceIds(stateDir) {
|
|
13077
|
-
const dir =
|
|
13331
|
+
const dir = join33(stateDir, "passengers");
|
|
13078
13332
|
if (!existsSync29(dir)) return [];
|
|
13079
13333
|
return readdirSync13(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
|
|
13080
13334
|
}
|
|
@@ -13157,8 +13411,8 @@ function realPoolMemberDeps(o) {
|
|
|
13157
13411
|
if (!cfg?.ship.relayHost) return null;
|
|
13158
13412
|
for (const r of cfg.routes) {
|
|
13159
13413
|
const ws = r.resolved?.workspaceId;
|
|
13160
|
-
const
|
|
13161
|
-
if (
|
|
13414
|
+
const slug2 = ws ? readPersistedTunnel(cfg.ship.stateDir, ws)?.slug : void 0;
|
|
13415
|
+
if (slug2) return relayBaseUrlFor(slug2, cfg.ship.relayHost, cfg.ship.relayHttpPort, cfg.ship.relayPublicDomain);
|
|
13162
13416
|
}
|
|
13163
13417
|
return null;
|
|
13164
13418
|
},
|
|
@@ -13941,8 +14195,8 @@ ${waiting.map((t) => `| ${displayKey2(t)} | ${t.status} | ${blockers(t, i)} |`).
|
|
|
13941
14195
|
}
|
|
13942
14196
|
|
|
13943
14197
|
// src/poll.ts
|
|
13944
|
-
import { writeFileSync as
|
|
13945
|
-
import { join as
|
|
14198
|
+
import { writeFileSync as writeFileSync23 } from "fs";
|
|
14199
|
+
import { join as join34 } from "path";
|
|
13946
14200
|
|
|
13947
14201
|
// src/stalled.ts
|
|
13948
14202
|
var STALLED_AFTER_CYCLES = 30;
|
|
@@ -14249,7 +14503,7 @@ async function decideCycle(o) {
|
|
|
14249
14503
|
}
|
|
14250
14504
|
return decision;
|
|
14251
14505
|
}
|
|
14252
|
-
var digestPath = (stateDir, route2, role, runId) =>
|
|
14506
|
+
var digestPath = (stateDir, route2, role, runId) => join34(stateDir, `digest-${route2}-${role}${runId ? `-${runId}` : ""}.md`);
|
|
14253
14507
|
function otherRunsRemoved(tickets, own) {
|
|
14254
14508
|
if (!own) return tickets;
|
|
14255
14509
|
return tickets.filter((t) => t.id === own.ticket.id || !own.inFlight(t.id));
|
|
@@ -14327,7 +14581,7 @@ function writeDigest(o, d, role, stateDir, runId, own) {
|
|
|
14327
14581
|
|
|
14328
14582
|
${body}` : body;
|
|
14329
14583
|
const path = digestPath(stateDir, routeSlug(o.route.route), role, runId);
|
|
14330
|
-
|
|
14584
|
+
writeFileSync23(path, text2);
|
|
14331
14585
|
o.emit.emit(`queue digest written for ${role} (${Buffer.byteLength(text2)} bytes)`, {
|
|
14332
14586
|
data: { role, bytes: Buffer.byteLength(text2) }
|
|
14333
14587
|
});
|
|
@@ -14435,7 +14689,7 @@ async function resolveOne(tracker, t, seat, approvedStatus, buildingStatus, affi
|
|
|
14435
14689
|
}
|
|
14436
14690
|
|
|
14437
14691
|
// src/repo-refresh.ts
|
|
14438
|
-
import { existsSync as existsSync31, mkdirSync as
|
|
14692
|
+
import { existsSync as existsSync31, mkdirSync as mkdirSync20, readFileSync as readFileSync29, writeFileSync as writeFileSync24 } from "fs";
|
|
14439
14693
|
import { dirname as dirname14 } from "path";
|
|
14440
14694
|
var REFRESH_COOLDOWN_MS = 2 * 6e4;
|
|
14441
14695
|
var UNSERVABLE_PASSES_BEFORE_ATTENTION = 3;
|
|
@@ -14466,10 +14720,10 @@ function applyRepoRows(resolved, rows) {
|
|
|
14466
14720
|
function persistRepoRows(stateDir, routeName, rows) {
|
|
14467
14721
|
const path = resolvedPathFor(stateDir, routeName);
|
|
14468
14722
|
if (!existsSync31(path)) return;
|
|
14469
|
-
const raw = JSON.parse(
|
|
14723
|
+
const raw = JSON.parse(readFileSync29(path, "utf8"));
|
|
14470
14724
|
applyRepoRows(raw, rows);
|
|
14471
|
-
|
|
14472
|
-
|
|
14725
|
+
mkdirSync20(dirname14(path), { recursive: true });
|
|
14726
|
+
writeFileSync24(path, `${JSON.stringify(raw, null, 2)}
|
|
14473
14727
|
`);
|
|
14474
14728
|
}
|
|
14475
14729
|
async function refreshUnknownRepos(o) {
|
|
@@ -14919,8 +15173,8 @@ Ship ${shipName2} found: ${problem}. The release refuses to squash a ref nobody
|
|
|
14919
15173
|
var localRefSkipReported = (ticket, problem, events) => events.some((e) => e.record_id === ticket.id && e.event_type === "local_ref_skip" && (e.message ?? "").includes(problem));
|
|
14920
15174
|
|
|
14921
15175
|
// src/connect-wizard.ts
|
|
14922
|
-
import { delimiter, dirname as dirname15, join as
|
|
14923
|
-
import { accessSync as accessSync3, constants as constants3, existsSync as existsSync32, mkdirSync as
|
|
15176
|
+
import { delimiter, dirname as dirname15, join as join35 } from "path";
|
|
15177
|
+
import { accessSync as accessSync3, constants as constants3, existsSync as existsSync32, mkdirSync as mkdirSync21, writeFileSync as writeFileSync25 } from "fs";
|
|
14924
15178
|
import { stringify as stringify2 } from "yaml";
|
|
14925
15179
|
var HOST_PASSENGERS_DOCS_URL = "https://crew.tablation.dev/host-passengers";
|
|
14926
15180
|
function shouldRunWizard(opts) {
|
|
@@ -14942,7 +15196,7 @@ function defaultIsExecutable(p) {
|
|
|
14942
15196
|
function findOnPath2(name, pathEnv, isExecutable = defaultIsExecutable) {
|
|
14943
15197
|
for (const dir of pathEnv.split(delimiter)) {
|
|
14944
15198
|
if (!dir) continue;
|
|
14945
|
-
const candidate =
|
|
15199
|
+
const candidate = join35(dir, name);
|
|
14946
15200
|
if (isExecutable(candidate)) return candidate;
|
|
14947
15201
|
}
|
|
14948
15202
|
return void 0;
|
|
@@ -15003,9 +15257,9 @@ routes:
|
|
|
15003
15257
|
${routeBlock}`;
|
|
15004
15258
|
}
|
|
15005
15259
|
function writeNewConfig(path, text2) {
|
|
15006
|
-
|
|
15260
|
+
mkdirSync21(dirname15(path), { recursive: true });
|
|
15007
15261
|
try {
|
|
15008
|
-
|
|
15262
|
+
writeFileSync25(path, text2, { mode: 384, flag: "wx" });
|
|
15009
15263
|
return true;
|
|
15010
15264
|
} catch (e) {
|
|
15011
15265
|
if (e.code === "EEXIST") return false;
|
|
@@ -15039,7 +15293,7 @@ Wrote ${o.configPath}.
|
|
|
15039
15293
|
// src/connect-json.ts
|
|
15040
15294
|
import { accessSync as accessSync4, constants as constants4 } from "fs";
|
|
15041
15295
|
import { homedir as homedir10 } from "os";
|
|
15042
|
-
import { dirname as dirname16, join as
|
|
15296
|
+
import { dirname as dirname16, join as join36 } from "path";
|
|
15043
15297
|
import { delimiter as delimiter2 } from "path";
|
|
15044
15298
|
var CHOICE_EXIT_CODE = 3;
|
|
15045
15299
|
function createEmitter(json2, write) {
|
|
@@ -15078,12 +15332,12 @@ function agentBinSearchDirs(home, pathEnv, extraPath) {
|
|
|
15078
15332
|
const dirs = [
|
|
15079
15333
|
...pathEnv.split(delimiter2),
|
|
15080
15334
|
...(extraPath ?? "").split(delimiter2),
|
|
15081
|
-
|
|
15082
|
-
|
|
15335
|
+
join36(home, ".local/bin"),
|
|
15336
|
+
join36(home, ".claude/local"),
|
|
15083
15337
|
"/opt/homebrew/bin",
|
|
15084
15338
|
"/usr/local/bin",
|
|
15085
|
-
|
|
15086
|
-
|
|
15339
|
+
join36(home, ".npm-global/bin"),
|
|
15340
|
+
join36(home, ".volta/bin")
|
|
15087
15341
|
];
|
|
15088
15342
|
return [...new Set(dirs.filter(Boolean))];
|
|
15089
15343
|
}
|
|
@@ -15119,9 +15373,9 @@ function flagWizardAnswers(f, env) {
|
|
|
15119
15373
|
}
|
|
15120
15374
|
|
|
15121
15375
|
// src/release-run.ts
|
|
15122
|
-
import { readFileSync as
|
|
15123
|
-
import { dirname as dirname17, join as
|
|
15124
|
-
import { tmpdir as
|
|
15376
|
+
import { readFileSync as readFileSync30, writeFileSync as writeFileSync26, existsSync as existsSync33, copyFileSync, mkdirSync as mkdirSync22, mkdtempSync as mkdtempSync3, rmSync as rmSync17 } from "fs";
|
|
15377
|
+
import { dirname as dirname17, join as join37 } from "path";
|
|
15378
|
+
import { tmpdir as tmpdir5 } from "os";
|
|
15125
15379
|
|
|
15126
15380
|
// src/release.ts
|
|
15127
15381
|
function describeUnplaceable(unplaceable, verifiedStatus) {
|
|
@@ -15861,13 +16115,13 @@ async function bump(o, size) {
|
|
|
15861
16115
|
}
|
|
15862
16116
|
return printed;
|
|
15863
16117
|
}
|
|
15864
|
-
const files = o.repo.release.versionFiles.map((f) =>
|
|
16118
|
+
const files = o.repo.release.versionFiles.map((f) => join37(o.cwd, f));
|
|
15865
16119
|
const first = files[0];
|
|
15866
16120
|
if (!existsSync33(first)) {
|
|
15867
16121
|
o.emit.error(`no ${o.repo.release.versionFiles[0]} to read a version from`);
|
|
15868
16122
|
return null;
|
|
15869
16123
|
}
|
|
15870
|
-
const current = JSON.parse(
|
|
16124
|
+
const current = JSON.parse(readFileSync30(first, "utf8")).version;
|
|
15871
16125
|
if (!current) {
|
|
15872
16126
|
o.emit.error(`no version field in ${o.repo.release.versionFiles[0]}`);
|
|
15873
16127
|
return null;
|
|
@@ -15880,27 +16134,27 @@ async function bump(o, size) {
|
|
|
15880
16134
|
return version;
|
|
15881
16135
|
}
|
|
15882
16136
|
for (const f of files) {
|
|
15883
|
-
const src =
|
|
16137
|
+
const src = readFileSync30(f, "utf8");
|
|
15884
16138
|
const out = src.replace(/("version"\s*:\s*")[^"]*(")/, `$1${version}$2`);
|
|
15885
16139
|
if (out === src) {
|
|
15886
16140
|
o.emit.error(`no version field in ${f}`);
|
|
15887
16141
|
return null;
|
|
15888
16142
|
}
|
|
15889
|
-
|
|
16143
|
+
writeFileSync26(f, out);
|
|
15890
16144
|
}
|
|
15891
16145
|
return version;
|
|
15892
16146
|
}
|
|
15893
16147
|
function writeChangelog(o, version, entries) {
|
|
15894
16148
|
const file = o.repo.release.changelog;
|
|
15895
16149
|
if (!file || entries.length === 0) return;
|
|
15896
|
-
const path =
|
|
16150
|
+
const path = join37(o.cwd, file);
|
|
15897
16151
|
const section = renderChangelogSection(version, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), entries);
|
|
15898
16152
|
if (o.dryRun) {
|
|
15899
16153
|
o.emit.emit(`would prepend ${entries.length} entr(ies) to ${file}`);
|
|
15900
16154
|
return;
|
|
15901
16155
|
}
|
|
15902
|
-
const existing = existsSync33(path) ?
|
|
15903
|
-
|
|
16156
|
+
const existing = existsSync33(path) ? readFileSync30(path, "utf8") : "# Changelog\n";
|
|
16157
|
+
writeFileSync26(path, insertChangelogSection(existing, section));
|
|
15904
16158
|
}
|
|
15905
16159
|
async function confirm(o, expected) {
|
|
15906
16160
|
if (!o.repo.hooks.released) return void 0;
|
|
@@ -15978,23 +16232,23 @@ async function confirmLastRelease(o, decision) {
|
|
|
15978
16232
|
if (!confirmed) o.emit.warn(`${decision.lastTag} is still not confirmed live \u2014 tickets stay verified`);
|
|
15979
16233
|
return { confirmed, version };
|
|
15980
16234
|
}
|
|
15981
|
-
function closeReleaseCheckout(cwd, root, dir, branch,
|
|
16235
|
+
function closeReleaseCheckout(cwd, root, dir, branch, run3 = gitOk) {
|
|
15982
16236
|
try {
|
|
15983
|
-
|
|
16237
|
+
run3(cwd, ["worktree", "remove", "--force", dir]);
|
|
15984
16238
|
} finally {
|
|
15985
16239
|
try {
|
|
15986
|
-
|
|
16240
|
+
rmSync17(root, { recursive: true, force: true });
|
|
15987
16241
|
} finally {
|
|
15988
|
-
|
|
15989
|
-
|
|
16242
|
+
run3(cwd, ["worktree", "prune"]);
|
|
16243
|
+
run3(cwd, ["branch", "-D", branch]);
|
|
15990
16244
|
}
|
|
15991
16245
|
}
|
|
15992
16246
|
}
|
|
15993
16247
|
function openReleaseCheckout(o) {
|
|
15994
16248
|
const { remote, base } = o.repo.branch;
|
|
15995
16249
|
const branch = `crew/release-${Date.now()}`;
|
|
15996
|
-
const root = mkdtempSync3(
|
|
15997
|
-
const dir =
|
|
16250
|
+
const root = mkdtempSync3(join37(tmpdir5(), "crew-release-"));
|
|
16251
|
+
const dir = join37(root, "checkout");
|
|
15998
16252
|
o.emit.emit(`release checkout: ${dir}`, { step: "release" });
|
|
15999
16253
|
const close = () => closeReleaseCheckout(o.cwd, root, dir, branch);
|
|
16000
16254
|
try {
|
|
@@ -16004,10 +16258,10 @@ function openReleaseCheckout(o) {
|
|
|
16004
16258
|
throw e;
|
|
16005
16259
|
}
|
|
16006
16260
|
for (const f of o.repo.worktrees.copy) {
|
|
16007
|
-
const src =
|
|
16261
|
+
const src = join37(o.cwd, f);
|
|
16008
16262
|
if (!existsSync33(src)) continue;
|
|
16009
|
-
|
|
16010
|
-
copyFileSync(src,
|
|
16263
|
+
mkdirSync22(dirname17(join37(dir, f)), { recursive: true });
|
|
16264
|
+
copyFileSync(src, join37(dir, f));
|
|
16011
16265
|
}
|
|
16012
16266
|
return { dir, branch, close };
|
|
16013
16267
|
}
|
|
@@ -16152,7 +16406,7 @@ async function runReleaseCycle(o, movedPast2, localRefSkips) {
|
|
|
16152
16406
|
let checkout;
|
|
16153
16407
|
const stranded = recoverStrandedReleaseCheckouts(o.cwd);
|
|
16154
16408
|
if (stranded.length > 0) o.emit.warn(scoped(`dropped release checkout(s) stranded by a crash: ${stranded.join(", ")}`));
|
|
16155
|
-
const swept = sweepStaleReleaseRoots(
|
|
16409
|
+
const swept = sweepStaleReleaseRoots(tmpdir5());
|
|
16156
16410
|
if (swept.length > 0) o.emit.emit(scoped(`removed ${swept.length} stale crew-release-* temp dir(s)`), { step: "release" });
|
|
16157
16411
|
try {
|
|
16158
16412
|
checkout = openReleaseCheckout(o);
|
|
@@ -17325,7 +17579,7 @@ function nextRoles(decision, ran) {
|
|
|
17325
17579
|
}
|
|
17326
17580
|
|
|
17327
17581
|
// src/ship-name.ts
|
|
17328
|
-
import { existsSync as existsSync35, mkdirSync as
|
|
17582
|
+
import { existsSync as existsSync35, mkdirSync as mkdirSync23, readFileSync as readFileSync31, renameSync as renameSync5, writeFileSync as writeFileSync27 } from "fs";
|
|
17329
17583
|
import { dirname as dirname18 } from "path";
|
|
17330
17584
|
import { parseDocument as parseDocument5 } from "yaml";
|
|
17331
17585
|
var ShipNameError = class extends Error {
|
|
@@ -17389,7 +17643,7 @@ async function syncRouteName(i) {
|
|
|
17389
17643
|
function readSyncedName(resolvedFile) {
|
|
17390
17644
|
if (!existsSync35(resolvedFile)) return void 0;
|
|
17391
17645
|
try {
|
|
17392
|
-
const v = JSON.parse(
|
|
17646
|
+
const v = JSON.parse(readFileSync31(resolvedFile, "utf8")).shipName;
|
|
17393
17647
|
return typeof v === "string" ? v : void 0;
|
|
17394
17648
|
} catch {
|
|
17395
17649
|
return void 0;
|
|
@@ -17399,22 +17653,22 @@ function writeSyncedName(resolvedFile, name) {
|
|
|
17399
17653
|
if (!existsSync35(resolvedFile)) return;
|
|
17400
17654
|
let raw;
|
|
17401
17655
|
try {
|
|
17402
|
-
raw = JSON.parse(
|
|
17656
|
+
raw = JSON.parse(readFileSync31(resolvedFile, "utf8"));
|
|
17403
17657
|
} catch {
|
|
17404
17658
|
return;
|
|
17405
17659
|
}
|
|
17406
17660
|
if (raw.shipName === name) return;
|
|
17407
17661
|
raw.shipName = name;
|
|
17408
|
-
|
|
17662
|
+
mkdirSync23(dirname18(resolvedFile), { recursive: true });
|
|
17409
17663
|
const tmp = `${resolvedFile}.tmp${process.pid}`;
|
|
17410
|
-
|
|
17664
|
+
writeFileSync27(tmp, `${JSON.stringify(raw, null, 2)}
|
|
17411
17665
|
`);
|
|
17412
|
-
|
|
17666
|
+
renameSync5(tmp, resolvedFile);
|
|
17413
17667
|
}
|
|
17414
17668
|
async function reconcileShipName(i) {
|
|
17415
17669
|
let local = i.ship.name;
|
|
17416
17670
|
try {
|
|
17417
|
-
local = shipNameOfYaml(
|
|
17671
|
+
local = shipNameOfYaml(readFileSync31(i.configFile, "utf8")) ?? local;
|
|
17418
17672
|
} catch {
|
|
17419
17673
|
}
|
|
17420
17674
|
const resolvedFile = resolvedPathFor(i.stateDir, i.route);
|
|
@@ -17438,8 +17692,8 @@ async function reconcileShipName(i) {
|
|
|
17438
17692
|
}
|
|
17439
17693
|
if (result.status !== "ok") return result;
|
|
17440
17694
|
if (result.action === "adopt" || result.action === "row-wins") {
|
|
17441
|
-
const text2 =
|
|
17442
|
-
|
|
17695
|
+
const text2 = readFileSync31(i.configFile, "utf8");
|
|
17696
|
+
writeFileSync27(i.configFile, setShipNameInYaml(text2, result.name));
|
|
17443
17697
|
i.ship.name = result.name;
|
|
17444
17698
|
if (result.action === "row-wins") {
|
|
17445
17699
|
i.emit?.warn(
|
|
@@ -18098,7 +18352,7 @@ function describeCapability(i) {
|
|
|
18098
18352
|
}
|
|
18099
18353
|
|
|
18100
18354
|
// src/agents-sync-run.ts
|
|
18101
|
-
import { existsSync as existsSync36, mkdirSync as
|
|
18355
|
+
import { existsSync as existsSync36, mkdirSync as mkdirSync24, readFileSync as readFileSync32, writeFileSync as writeFileSync28 } from "fs";
|
|
18102
18356
|
import { dirname as dirname19 } from "path";
|
|
18103
18357
|
async function runAgentsSync(target, o) {
|
|
18104
18358
|
const dryRun = o.dryRun ?? false;
|
|
@@ -18132,12 +18386,12 @@ async function runAgentsSync(target, o) {
|
|
|
18132
18386
|
for (const s of skillResult?.outcomes ?? []) o.say(describeSkillSyncOutcome(s));
|
|
18133
18387
|
if (!dryRun) {
|
|
18134
18388
|
const resolvedPath = resolvedPathFor(o.stateDir, target.route);
|
|
18135
|
-
const raw = existsSync36(resolvedPath) ? JSON.parse(
|
|
18389
|
+
const raw = existsSync36(resolvedPath) ? JSON.parse(readFileSync32(resolvedPath, "utf8")) : {};
|
|
18136
18390
|
raw.agentPersonas = personaResult.agentPersonas;
|
|
18137
18391
|
if (skillResult) raw.agentSkills = skillResult.agentSkills;
|
|
18138
18392
|
if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
|
|
18139
|
-
|
|
18140
|
-
|
|
18393
|
+
mkdirSync24(dirname19(resolvedPath), { recursive: true });
|
|
18394
|
+
writeFileSync28(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
18141
18395
|
`);
|
|
18142
18396
|
if (target.resolved) {
|
|
18143
18397
|
target.resolved.agentPersonas = personaResult.agentPersonas;
|
|
@@ -18152,7 +18406,7 @@ async function runAgentsSync(target, o) {
|
|
|
18152
18406
|
}
|
|
18153
18407
|
|
|
18154
18408
|
// src/logbook.ts
|
|
18155
|
-
import { createHash as
|
|
18409
|
+
import { createHash as createHash8 } from "crypto";
|
|
18156
18410
|
import { TablationClient as TablationClient4 } from "@tablation/client";
|
|
18157
18411
|
var LogbookError = class extends Error {
|
|
18158
18412
|
};
|
|
@@ -18225,7 +18479,7 @@ async function showLogEntry(route2, entryId, opts = {}) {
|
|
|
18225
18479
|
};
|
|
18226
18480
|
}
|
|
18227
18481
|
const prompt = appendAdditions(match.prompt ?? "", match.prompt_additions);
|
|
18228
|
-
const actualSha =
|
|
18482
|
+
const actualSha = createHash8("sha256").update(prompt).digest("hex");
|
|
18229
18483
|
const exact = !!entry.prompt_sha && actualSha === entry.prompt_sha;
|
|
18230
18484
|
return {
|
|
18231
18485
|
entry,
|
|
@@ -18241,27 +18495,27 @@ async function showLogEntry(route2, entryId, opts = {}) {
|
|
|
18241
18495
|
|
|
18242
18496
|
// src/repos-check.ts
|
|
18243
18497
|
import { spawnSync as spawnSync9 } from "child_process";
|
|
18244
|
-
import { readFileSync as
|
|
18498
|
+
import { readFileSync as readFileSync33 } from "fs";
|
|
18245
18499
|
import { basename as basename10 } from "path";
|
|
18246
18500
|
var POSIX_SHELL = /^(?:ba|z|da|k|a|mk)?sh$|^fish$/;
|
|
18247
18501
|
function checkHookSyntax(script, shell) {
|
|
18248
18502
|
const spec = resolveShell(shell);
|
|
18249
18503
|
const name = basename10(spec.bin).replace(/\.exe$/i, "").toLowerCase();
|
|
18250
|
-
let
|
|
18504
|
+
let run3;
|
|
18251
18505
|
if (POSIX_SHELL.test(name)) {
|
|
18252
|
-
|
|
18506
|
+
run3 = spawnSync9(spec.bin, ["-n"], { input: script, encoding: "utf8" });
|
|
18253
18507
|
} else if (/^(pwsh|powershell)$/.test(name)) {
|
|
18254
18508
|
const parse3 = "$e=$null;[void][System.Management.Automation.Language.Parser]::ParseInput($env:CREW_CHECK_SCRIPT,[ref]$null,[ref]$e);if($e){$e|ForEach-Object{[Console]::Error.WriteLine($_.Message)};exit 1}";
|
|
18255
|
-
|
|
18509
|
+
run3 = spawnSync9(spec.bin, ["-NoProfile", "-NonInteractive", "-Command", parse3], {
|
|
18256
18510
|
encoding: "utf8",
|
|
18257
18511
|
env: { ...process.env, CREW_CHECK_SCRIPT: script }
|
|
18258
18512
|
});
|
|
18259
18513
|
} else {
|
|
18260
18514
|
return { status: "unchecked", message: `no syntax check for ${spec.bin}` };
|
|
18261
18515
|
}
|
|
18262
|
-
if (
|
|
18263
|
-
if (
|
|
18264
|
-
const message = `${
|
|
18516
|
+
if (run3.error) return { status: "unchecked", message: `cannot run ${spec.bin}: ${run3.error.message}` };
|
|
18517
|
+
if (run3.status === 0) return { status: "ok" };
|
|
18518
|
+
const message = `${run3.stderr ?? ""}${run3.stdout ?? ""}`.trim() || `${spec.bin} exited ${run3.status}`;
|
|
18265
18519
|
return { status: "error", message };
|
|
18266
18520
|
}
|
|
18267
18521
|
function renderSamples(cfg, dir) {
|
|
@@ -18282,7 +18536,7 @@ function checkRepoSpec(dir, opts = {}) {
|
|
|
18282
18536
|
}
|
|
18283
18537
|
let parsed;
|
|
18284
18538
|
try {
|
|
18285
|
-
parsed = parseRepoConfig(
|
|
18539
|
+
parsed = parseRepoConfig(readFileSync33(file, "utf8"), file);
|
|
18286
18540
|
} catch (e) {
|
|
18287
18541
|
if (!(e instanceof RepoConfigError)) throw e;
|
|
18288
18542
|
return { ...empty, file, error: e.message, exitCode: 1 };
|
|
@@ -18557,7 +18811,7 @@ function existingRepoConfig(f) {
|
|
|
18557
18811
|
}
|
|
18558
18812
|
|
|
18559
18813
|
// src/repos-hooks-agent.ts
|
|
18560
|
-
import { spawn as
|
|
18814
|
+
import { spawn as spawn7, execFileSync as execFileSync8 } from "child_process";
|
|
18561
18815
|
import { realpathSync as realpathSync2 } from "fs";
|
|
18562
18816
|
import { isAbsolute as isAbsolute2, relative } from "path";
|
|
18563
18817
|
var AgentSuggestError = class extends Error {
|
|
@@ -18675,10 +18929,10 @@ async function runAgentSuggest(o) {
|
|
|
18675
18929
|
const before = porcelain(o.dir);
|
|
18676
18930
|
const timeout = AbortSignal.timeout(o.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
18677
18931
|
const signal = o.signal ? AbortSignal.any([o.signal, timeout]) : timeout;
|
|
18678
|
-
const
|
|
18932
|
+
const run3 = await new Promise((resolve7) => {
|
|
18679
18933
|
let out = "";
|
|
18680
18934
|
let err3 = "";
|
|
18681
|
-
const child =
|
|
18935
|
+
const child = spawn7(bin, args, { cwd: o.dir, env, detached: true, stdio: ["pipe", "pipe", "pipe"] });
|
|
18682
18936
|
let settled = false;
|
|
18683
18937
|
const settle = (r) => {
|
|
18684
18938
|
if (settled) return;
|
|
@@ -18710,18 +18964,18 @@ async function runAgentSuggest(o) {
|
|
|
18710
18964
|
});
|
|
18711
18965
|
child.stdin.end(buildAgentPrompt(o.sniff));
|
|
18712
18966
|
});
|
|
18713
|
-
if (
|
|
18967
|
+
if (run3.aborted || signal.aborted) {
|
|
18714
18968
|
throw new AgentSuggestError(o.signal?.aborted ? "cancelled" : "timeout", o.signal?.aborted ? "cancelled" : "the toolchain did not answer in time");
|
|
18715
18969
|
}
|
|
18716
18970
|
const after = porcelain(o.dir);
|
|
18717
18971
|
if (before !== void 0 && after !== before) {
|
|
18718
18972
|
throw new AgentSuggestError("modified_checkout", "the checkout changed during --agent; inspect it with `git status` (the agent result was discarded)");
|
|
18719
18973
|
}
|
|
18720
|
-
const parsed = parseAgentStream(
|
|
18721
|
-
if (
|
|
18722
|
-
const reason = classifyFailure({ code:
|
|
18723
|
-
${
|
|
18724
|
-
throw new AgentSuggestError(reason, reason === "not_installed" ? `${bin} is not installed` : reason === "not_signed_in" ? `${o.spec.name} is not signed in` : (parsed.text ||
|
|
18974
|
+
const parsed = parseAgentStream(run3.out, o.dir);
|
|
18975
|
+
if (run3.spawnError || run3.code !== 0 || parsed.isError) {
|
|
18976
|
+
const reason = classifyFailure({ code: run3.code, ...run3.spawnError ? { spawnError: run3.spawnError } : {}, output: `${parsed.text}
|
|
18977
|
+
${run3.err}` });
|
|
18978
|
+
throw new AgentSuggestError(reason, reason === "not_installed" ? `${bin} is not installed` : reason === "not_signed_in" ? `${o.spec.name} is not signed in` : (parsed.text || run3.err).trim().slice(0, 300) || `exited ${run3.code}`);
|
|
18725
18979
|
}
|
|
18726
18980
|
const { hooks, notes } = parseAgentHooks(parsed.text);
|
|
18727
18981
|
const differsFromSniff = HOOK_KEYS2.filter((k) => hooks[k] !== void 0 && hooks[k] !== o.sniff.hooks[k]);
|
|
@@ -18729,7 +18983,7 @@ ${run2.err}` });
|
|
|
18729
18983
|
}
|
|
18730
18984
|
|
|
18731
18985
|
// src/sample-ticket.ts
|
|
18732
|
-
import { basename as basename11, dirname as dirname20, join as
|
|
18986
|
+
import { basename as basename11, dirname as dirname20, join as join38 } from "path";
|
|
18733
18987
|
var SAMPLE_REPO = "crew-sample";
|
|
18734
18988
|
var SAMPLE_REMOTE = `local/${SAMPLE_REPO}`;
|
|
18735
18989
|
var SAMPLE_MARKER = "sample: true";
|
|
@@ -18752,8 +19006,8 @@ has something safe to work on: its test and build hooks always pass, it has no
|
|
|
18752
19006
|
remote, and \`crew sample-ticket --remove\` deletes it.
|
|
18753
19007
|
`;
|
|
18754
19008
|
function sampleDir(existing, reposBase, workspace) {
|
|
18755
|
-
if (existing.length > 0) return
|
|
18756
|
-
return
|
|
19009
|
+
if (existing.length > 0) return join38(dirname20(existing[0]), SAMPLE_REPO);
|
|
19010
|
+
return join38(reposBase, workspace, SAMPLE_REPO);
|
|
18757
19011
|
}
|
|
18758
19012
|
var SAMPLE_TITLE = "Sample: add a documented `greet` function with a test";
|
|
18759
19013
|
function sampleDescription() {
|
|
@@ -18797,8 +19051,8 @@ function planSampleTicket(i) {
|
|
|
18797
19051
|
}
|
|
18798
19052
|
|
|
18799
19053
|
// src/worktree-sweep.ts
|
|
18800
|
-
import { existsSync as existsSync37, readFileSync as
|
|
18801
|
-
import { basename as basename12, dirname as dirname21, join as
|
|
19054
|
+
import { existsSync as existsSync37, readFileSync as readFileSync34, readdirSync as readdirSync14 } from "fs";
|
|
19055
|
+
import { basename as basename12, dirname as dirname21, join as join39, resolve as resolve4 } from "path";
|
|
18802
19056
|
function planWorktreeSweep(target, tickets, contract) {
|
|
18803
19057
|
const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
|
|
18804
19058
|
const verified = contract.statuses.verified;
|
|
@@ -18875,7 +19129,7 @@ function siblingWorktreeDirs(target) {
|
|
|
18875
19129
|
for (const name of names) {
|
|
18876
19130
|
if (!name.startsWith(stem)) continue;
|
|
18877
19131
|
const m = DIR_NUMBER_SUFFIXED.exec(name);
|
|
18878
|
-
const path =
|
|
19132
|
+
const path = join39(parent, name);
|
|
18879
19133
|
if (m && !registered.has(resolve4(path))) out.push({ path, number: m[1] });
|
|
18880
19134
|
}
|
|
18881
19135
|
return out;
|
|
@@ -18978,7 +19232,7 @@ function isHandBackMerge(path, base) {
|
|
|
18978
19232
|
if (!msgPath) return false;
|
|
18979
19233
|
let first;
|
|
18980
19234
|
try {
|
|
18981
|
-
first =
|
|
19235
|
+
first = readFileSync34(resolve4(path, msgPath), "utf8").split("\n")[0] ?? "";
|
|
18982
19236
|
} catch {
|
|
18983
19237
|
return false;
|
|
18984
19238
|
}
|
|
@@ -19083,14 +19337,14 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
|
|
|
19083
19337
|
}
|
|
19084
19338
|
|
|
19085
19339
|
// src/stream-sweep.ts
|
|
19086
|
-
import { existsSync as existsSync38, readdirSync as readdirSync15, rmSync as
|
|
19087
|
-
import { join as
|
|
19340
|
+
import { existsSync as existsSync38, readdirSync as readdirSync15, rmSync as rmSync18, statSync as statSync10 } from "fs";
|
|
19341
|
+
import { join as join40 } from "path";
|
|
19088
19342
|
var DAY_MS2 = 24 * 60 * 60 * 1e3;
|
|
19089
19343
|
function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
|
|
19090
|
-
const dir =
|
|
19344
|
+
const dir = join40(stateDir, "streams");
|
|
19091
19345
|
if (!existsSync38(dir)) return [];
|
|
19092
19346
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
19093
|
-
return readdirSync15(dir).map((name) =>
|
|
19347
|
+
return readdirSync15(dir).map((name) => join40(dir, name)).filter((path) => {
|
|
19094
19348
|
try {
|
|
19095
19349
|
return statSync10(path).mtimeMs < cutoff;
|
|
19096
19350
|
} catch {
|
|
@@ -19106,7 +19360,7 @@ function applyStreamSweep(paths, dryRun, log) {
|
|
|
19106
19360
|
continue;
|
|
19107
19361
|
}
|
|
19108
19362
|
try {
|
|
19109
|
-
|
|
19363
|
+
rmSync18(path, { force: true });
|
|
19110
19364
|
removed++;
|
|
19111
19365
|
log.emit(`removed stream artifact ${path}`, { step: "worktree" });
|
|
19112
19366
|
} catch (e) {
|
|
@@ -19118,7 +19372,7 @@ function applyStreamSweep(paths, dryRun, log) {
|
|
|
19118
19372
|
function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
19119
19373
|
if (!existsSync38(stateDir)) return [];
|
|
19120
19374
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
19121
|
-
return readdirSync15(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) =>
|
|
19375
|
+
return readdirSync15(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join40(stateDir, name)).filter((path) => {
|
|
19122
19376
|
try {
|
|
19123
19377
|
return statSync10(path).mtimeMs < cutoff;
|
|
19124
19378
|
} catch {
|
|
@@ -19128,12 +19382,12 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
|
19128
19382
|
}
|
|
19129
19383
|
|
|
19130
19384
|
// src/cli.ts
|
|
19131
|
-
import { realpathSync as realpathSync3, readFileSync as
|
|
19385
|
+
import { realpathSync as realpathSync3, readFileSync as readFileSync40, existsSync as existsSync45, mkdirSync as mkdirSync30, writeFileSync as writeFileSync33, rmSync as rmSync23, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync6, statSync as statSync14, readdirSync as readdirSync20 } from "fs";
|
|
19132
19386
|
import { createInterface } from "readline/promises";
|
|
19133
19387
|
|
|
19134
19388
|
// src/periscope-cmd.ts
|
|
19135
|
-
import { existsSync as existsSync39, mkdirSync as
|
|
19136
|
-
import { join as
|
|
19389
|
+
import { existsSync as existsSync39, mkdirSync as mkdirSync25, readFileSync as readFileSync35, rmSync as rmSync19, writeFileSync as writeFileSync29 } from "fs";
|
|
19390
|
+
import { join as join41 } from "path";
|
|
19137
19391
|
var PeriscopeError = class extends Error {
|
|
19138
19392
|
};
|
|
19139
19393
|
var LivePeriscopeError = class extends PeriscopeError {
|
|
@@ -19143,22 +19397,22 @@ var LivePeriscopeError = class extends PeriscopeError {
|
|
|
19143
19397
|
this.data = { refused: "live_periscope_exists", ...live };
|
|
19144
19398
|
}
|
|
19145
19399
|
};
|
|
19146
|
-
var priorDir = (stateDir) =>
|
|
19147
|
-
var priorPath = (stateDir, ticketRecordId) =>
|
|
19400
|
+
var priorDir = (stateDir) => join41(stateDir, "periscope-priors");
|
|
19401
|
+
var priorPath = (stateDir, ticketRecordId) => join41(priorDir(stateDir), `${ticketRecordId}.json`);
|
|
19148
19402
|
function removePeriscopePrior(stateDir, ticketRecordId) {
|
|
19149
|
-
|
|
19403
|
+
rmSync19(priorPath(stateDir, ticketRecordId), { force: true });
|
|
19150
19404
|
}
|
|
19151
19405
|
function writePeriscopePrior(stateDir, ticketRecordId, prior) {
|
|
19152
19406
|
try {
|
|
19153
|
-
|
|
19154
|
-
|
|
19407
|
+
mkdirSync25(priorDir(stateDir), { recursive: true });
|
|
19408
|
+
writeFileSync29(priorPath(stateDir, ticketRecordId), JSON.stringify(prior));
|
|
19155
19409
|
} catch {
|
|
19156
19410
|
}
|
|
19157
19411
|
}
|
|
19158
19412
|
function readPeriscopePrior(stateDir, ticketRecordId) {
|
|
19159
19413
|
try {
|
|
19160
19414
|
const p = priorPath(stateDir, ticketRecordId);
|
|
19161
|
-
return existsSync39(p) ? JSON.parse(
|
|
19415
|
+
return existsSync39(p) ? JSON.parse(readFileSync35(p, "utf8")) : void 0;
|
|
19162
19416
|
} catch {
|
|
19163
19417
|
return void 0;
|
|
19164
19418
|
}
|
|
@@ -19557,7 +19811,7 @@ ${tail2(preview.output)}`);
|
|
|
19557
19811
|
// src/periscope-idle.ts
|
|
19558
19812
|
import { execFileSync as execFileSync9 } from "child_process";
|
|
19559
19813
|
import { statSync as statSync11 } from "fs";
|
|
19560
|
-
import { join as
|
|
19814
|
+
import { join as join42 } from "path";
|
|
19561
19815
|
var MAX_SCANNED_FILES = 2e4;
|
|
19562
19816
|
function newestWorktreeMtime(root) {
|
|
19563
19817
|
let listing;
|
|
@@ -19577,7 +19831,7 @@ function newestWorktreeMtime(root) {
|
|
|
19577
19831
|
if (!rel) continue;
|
|
19578
19832
|
if (++scanned > MAX_SCANNED_FILES) break;
|
|
19579
19833
|
try {
|
|
19580
|
-
const m = statSync11(
|
|
19834
|
+
const m = statSync11(join42(root, rel)).mtimeMs;
|
|
19581
19835
|
if (newest === null || m > newest) newest = m;
|
|
19582
19836
|
} catch {
|
|
19583
19837
|
}
|
|
@@ -19653,18 +19907,18 @@ import { dirname as dirOf2, resolve as resolvePath } from "path";
|
|
|
19653
19907
|
|
|
19654
19908
|
// src/ssh-keys.ts
|
|
19655
19909
|
import { execFileSync as execFileSync10 } from "child_process";
|
|
19656
|
-
import { existsSync as existsSync40, mkdirSync as
|
|
19657
|
-
import { dirname as dirname22, join as
|
|
19910
|
+
import { existsSync as existsSync40, mkdirSync as mkdirSync26, readFileSync as readFileSync36, chmodSync as chmodSync3 } from "fs";
|
|
19911
|
+
import { dirname as dirname22, join as join43 } from "path";
|
|
19658
19912
|
var SshKeyError = class extends Error {
|
|
19659
19913
|
};
|
|
19660
19914
|
function shipSshKeyPathFor(stateDir) {
|
|
19661
|
-
return
|
|
19915
|
+
return join43(stateDir, "ssh", "id_ed25519");
|
|
19662
19916
|
}
|
|
19663
19917
|
function ensureShipSshKeypair(stateDir) {
|
|
19664
19918
|
const privateKeyPath = shipSshKeyPathFor(stateDir);
|
|
19665
19919
|
const publicKeyPath = `${privateKeyPath}.pub`;
|
|
19666
19920
|
if (!existsSync40(privateKeyPath)) {
|
|
19667
|
-
|
|
19921
|
+
mkdirSync26(dirname22(privateKeyPath), { recursive: true });
|
|
19668
19922
|
try {
|
|
19669
19923
|
execFileSync10("ssh-keygen", [
|
|
19670
19924
|
"-t",
|
|
@@ -19687,7 +19941,7 @@ function ensureShipSshKeypair(stateDir) {
|
|
|
19687
19941
|
if (!existsSync40(publicKeyPath)) {
|
|
19688
19942
|
throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
|
|
19689
19943
|
}
|
|
19690
|
-
const publicKey =
|
|
19944
|
+
const publicKey = readFileSync36(publicKeyPath, "utf8").trim();
|
|
19691
19945
|
return { privateKeyPath, publicKeyPath, publicKey };
|
|
19692
19946
|
}
|
|
19693
19947
|
function sshKeygenAvailable() {
|
|
@@ -19730,7 +19984,7 @@ function shipIdentityJson(s) {
|
|
|
19730
19984
|
}
|
|
19731
19985
|
|
|
19732
19986
|
// src/mcp-endpoint-registry.ts
|
|
19733
|
-
import { createHash as
|
|
19987
|
+
import { createHash as createHash9 } from "crypto";
|
|
19734
19988
|
function endpointsUrl(api, workspaceId, id) {
|
|
19735
19989
|
const base = `${api.baseUrl.replace(/\/+$/, "")}/api/workspaces/${workspaceId}/mcp-endpoints`;
|
|
19736
19990
|
return id ? `${base}/${id}` : base;
|
|
@@ -19800,7 +20054,7 @@ async function probeEndpointAccess(api, workspaceId) {
|
|
|
19800
20054
|
}
|
|
19801
20055
|
var REGISTER_MCP_ENDPOINTS_HINT = "give this key's role the REGISTER_MCP_ENDPOINTS capability, e.g. via the group that holds the Crew project role";
|
|
19802
20056
|
function registrationKey(url, mcpSecret, controlUrl) {
|
|
19803
|
-
return
|
|
20057
|
+
return createHash9("sha256").update(`${url}
|
|
19804
20058
|
${mcpSecret ?? ""}
|
|
19805
20059
|
${controlUrl ?? ""}`).digest("hex").slice(0, 16);
|
|
19806
20060
|
}
|
|
@@ -19849,10 +20103,10 @@ async function unregisterEndpoint(workspaceId, info, d) {
|
|
|
19849
20103
|
|
|
19850
20104
|
// src/passenger-sync-daemon.ts
|
|
19851
20105
|
import { createServer as createHttpServer } from "http";
|
|
19852
|
-
import { spawn as
|
|
20106
|
+
import { spawn as spawn8 } from "child_process";
|
|
19853
20107
|
import { randomBytes } from "crypto";
|
|
19854
|
-
import { existsSync as existsSync41, mkdirSync as
|
|
19855
|
-
import { join as
|
|
20108
|
+
import { existsSync as existsSync41, mkdirSync as mkdirSync27, readFileSync as readFileSync37, writeFileSync as writeFileSync30, unlinkSync as unlinkSync4, readdirSync as readdirSync16 } from "fs";
|
|
20109
|
+
import { join as join44 } from "path";
|
|
19856
20110
|
|
|
19857
20111
|
// src/secret-compare.ts
|
|
19858
20112
|
import { timingSafeEqual } from "crypto";
|
|
@@ -19936,26 +20190,26 @@ function runSyncDaemonFromEnv(env = process.env) {
|
|
|
19936
20190
|
return server;
|
|
19937
20191
|
}
|
|
19938
20192
|
var defaultSpawn2 = (cmd, args, env) => {
|
|
19939
|
-
const child =
|
|
20193
|
+
const child = spawn8(cmd, args, { detached: true, stdio: "ignore", env });
|
|
19940
20194
|
child.unref();
|
|
19941
20195
|
return child;
|
|
19942
20196
|
};
|
|
19943
20197
|
function syncDaemonStatePath(stateDir, workspaceId) {
|
|
19944
|
-
return
|
|
20198
|
+
return join44(stateDir, "passengers", `${workspaceId}-sync.json`);
|
|
19945
20199
|
}
|
|
19946
20200
|
function readPersistedSyncDaemon(stateDir, workspaceId) {
|
|
19947
20201
|
const path = syncDaemonStatePath(stateDir, workspaceId);
|
|
19948
20202
|
if (!existsSync41(path)) return void 0;
|
|
19949
20203
|
try {
|
|
19950
|
-
return JSON.parse(
|
|
20204
|
+
return JSON.parse(readFileSync37(path, "utf8"));
|
|
19951
20205
|
} catch {
|
|
19952
20206
|
return void 0;
|
|
19953
20207
|
}
|
|
19954
20208
|
}
|
|
19955
20209
|
function writePersistedSyncDaemon(stateDir, state) {
|
|
19956
20210
|
const path = syncDaemonStatePath(stateDir, state.workspaceId);
|
|
19957
|
-
|
|
19958
|
-
|
|
20211
|
+
mkdirSync27(join44(stateDir, "passengers"), { recursive: true });
|
|
20212
|
+
writeFileSync30(path, `${JSON.stringify(state, null, 2)}
|
|
19959
20213
|
`);
|
|
19960
20214
|
}
|
|
19961
20215
|
function removePersistedSyncDaemon(stateDir, workspaceId) {
|
|
@@ -19975,7 +20229,7 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
|
|
|
19975
20229
|
}
|
|
19976
20230
|
}
|
|
19977
20231
|
function readAllPersistedSyncWorkspaceIds(stateDir) {
|
|
19978
|
-
const dir =
|
|
20232
|
+
const dir = join44(stateDir, "passengers");
|
|
19979
20233
|
if (!existsSync41(dir)) return [];
|
|
19980
20234
|
const suffix = "-sync.json";
|
|
19981
20235
|
return readdirSync16(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
|
|
@@ -20064,8 +20318,8 @@ function describeCause(err3) {
|
|
|
20064
20318
|
}
|
|
20065
20319
|
|
|
20066
20320
|
// src/daemon.ts
|
|
20067
|
-
import { existsSync as existsSync42, mkdirSync as
|
|
20068
|
-
import { join as
|
|
20321
|
+
import { existsSync as existsSync42, mkdirSync as mkdirSync28, readFileSync as readFileSync38, readdirSync as readdirSync17, rmSync as rmSync20, statSync as statSync12, writeFileSync as writeFileSync31 } from "fs";
|
|
20322
|
+
import { join as join45 } from "path";
|
|
20069
20323
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
20070
20324
|
var DEFAULT_FLOOR_MS = 5e3;
|
|
20071
20325
|
var DEFAULT_CEILING_MS = INTERVAL_SECONDS * 1e3;
|
|
@@ -20153,7 +20407,7 @@ function latestMtime(path) {
|
|
|
20153
20407
|
if (!stat2.isDirectory()) return 0;
|
|
20154
20408
|
let latest = stat2.mtimeMs;
|
|
20155
20409
|
for (const entry of readdirSync17(path, { withFileTypes: true })) {
|
|
20156
|
-
latest = Math.max(latest, latestMtime(
|
|
20410
|
+
latest = Math.max(latest, latestMtime(join45(path, entry.name)));
|
|
20157
20411
|
}
|
|
20158
20412
|
return latest;
|
|
20159
20413
|
}
|
|
@@ -20165,7 +20419,7 @@ function makeStaleChecker(paths, opts = {}) {
|
|
|
20165
20419
|
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync42(opts.reloadSentinel);
|
|
20166
20420
|
}
|
|
20167
20421
|
function reloadSentinelFor(stateDir) {
|
|
20168
|
-
return
|
|
20422
|
+
return join45(stateDir, "reload-requested");
|
|
20169
20423
|
}
|
|
20170
20424
|
function readReloadRequestedAt(stateDir) {
|
|
20171
20425
|
try {
|
|
@@ -20175,17 +20429,17 @@ function readReloadRequestedAt(stateDir) {
|
|
|
20175
20429
|
}
|
|
20176
20430
|
}
|
|
20177
20431
|
function daemonStampFor(stateDir) {
|
|
20178
|
-
return
|
|
20432
|
+
return join45(stateDir, "daemon.started");
|
|
20179
20433
|
}
|
|
20180
20434
|
function noteDaemonStarted(stateDir, now = Date.now()) {
|
|
20181
|
-
|
|
20182
|
-
|
|
20183
|
-
|
|
20435
|
+
mkdirSync28(stateDir, { recursive: true });
|
|
20436
|
+
rmSync20(reloadSentinelFor(stateDir), { force: true });
|
|
20437
|
+
writeFileSync31(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
|
|
20184
20438
|
`);
|
|
20185
20439
|
}
|
|
20186
20440
|
function readDaemonStarted(stateDir) {
|
|
20187
20441
|
try {
|
|
20188
|
-
const v = JSON.parse(
|
|
20442
|
+
const v = JSON.parse(readFileSync38(daemonStampFor(stateDir), "utf8")).startedAt;
|
|
20189
20443
|
return typeof v === "number" ? v : null;
|
|
20190
20444
|
} catch {
|
|
20191
20445
|
return null;
|
|
@@ -20250,7 +20504,7 @@ async function runOnePassUnguarded(o) {
|
|
|
20250
20504
|
await hydrateApiKeys([o.route], o.getSessionStore);
|
|
20251
20505
|
const emit = o.newEmitter();
|
|
20252
20506
|
const decide = o.decide ?? decideCycle;
|
|
20253
|
-
const
|
|
20507
|
+
const spawn11 = o.spawnRoleAgent ?? runRoleAgent;
|
|
20254
20508
|
const breaker = o.spawnBreaker ?? new SpawnBreaker();
|
|
20255
20509
|
const ctx = { route: o.route, ship: o.ship, state: o.state, emit, getSessionStore: o.getSessionStore };
|
|
20256
20510
|
if (o.configFile) await reconcileRouteShipName({ route: o.route, ship: o.ship, configFile: o.configFile, emit });
|
|
@@ -20354,7 +20608,7 @@ async function runOnePassUnguarded(o) {
|
|
|
20354
20608
|
settle = resolve7;
|
|
20355
20609
|
});
|
|
20356
20610
|
const ready = (tookTicket2) => settle(tookTicket2);
|
|
20357
|
-
const
|
|
20611
|
+
const run3 = spawn11(role, decision, ctx, { ready }).then(() => {
|
|
20358
20612
|
breaker.recordSuccess(role);
|
|
20359
20613
|
clearShipAttention(o.state, `role_parked:${role}`);
|
|
20360
20614
|
clearShipAttention(o.state, workspaceAuthKey(role));
|
|
@@ -20403,8 +20657,8 @@ async function runOnePassUnguarded(o) {
|
|
|
20403
20657
|
});
|
|
20404
20658
|
if (o.inFlight) {
|
|
20405
20659
|
const set = o.inFlight;
|
|
20406
|
-
set.add(
|
|
20407
|
-
void
|
|
20660
|
+
set.add(run3);
|
|
20661
|
+
void run3.finally(() => set.delete(run3));
|
|
20408
20662
|
}
|
|
20409
20663
|
const reported = TICKET_ROLES.has(role) ? await settled : true;
|
|
20410
20664
|
const tookTicket = reported !== false;
|
|
@@ -20740,7 +20994,7 @@ async function buildEnvironment(route2, ship, ticket) {
|
|
|
20740
20994
|
}
|
|
20741
20995
|
|
|
20742
20996
|
// src/control-api.ts
|
|
20743
|
-
import { createHash as
|
|
20997
|
+
import { createHash as createHash10 } from "crypto";
|
|
20744
20998
|
import {
|
|
20745
20999
|
chmodSync as chmodSync4,
|
|
20746
21000
|
closeSync as closeSync5,
|
|
@@ -20749,17 +21003,17 @@ import {
|
|
|
20749
21003
|
openSync as openSync5,
|
|
20750
21004
|
readSync as readSync3,
|
|
20751
21005
|
readdirSync as readdirSync18,
|
|
20752
|
-
rmSync as
|
|
21006
|
+
rmSync as rmSync21,
|
|
20753
21007
|
statSync as statSync13,
|
|
20754
21008
|
watch
|
|
20755
21009
|
} from "fs";
|
|
20756
21010
|
import { createConnection } from "net";
|
|
20757
21011
|
import { createServer } from "http";
|
|
20758
|
-
import { basename as basename13, dirname as dirname23, join as
|
|
20759
|
-
import { execFile as execFile2, spawn as
|
|
21012
|
+
import { basename as basename13, dirname as dirname23, join as join46 } from "path";
|
|
21013
|
+
import { execFile as execFile2, spawn as spawn10 } from "child_process";
|
|
20760
21014
|
|
|
20761
21015
|
// src/periscope-control.ts
|
|
20762
|
-
import { spawn as
|
|
21016
|
+
import { spawn as spawn9 } from "child_process";
|
|
20763
21017
|
var reply = (status2, body) => ({ status: status2, body });
|
|
20764
21018
|
var err = (status2, message) => reply(status2, { error: message });
|
|
20765
21019
|
var TICKET_RE = /^(?:[A-Za-z]+-)?\d+$/;
|
|
@@ -20950,7 +21204,7 @@ async function handlePeriscopes(h, q) {
|
|
|
20950
21204
|
}
|
|
20951
21205
|
function runCrew(args, onLine, timeoutMs = 20 * 6e4) {
|
|
20952
21206
|
return new Promise((resolve7) => {
|
|
20953
|
-
const child =
|
|
21207
|
+
const child = spawn9(process.execPath, [process.argv[1] ?? "", ...args], { stdio: ["ignore", "pipe", "pipe"] });
|
|
20954
21208
|
let stdout = "";
|
|
20955
21209
|
let stderr = "";
|
|
20956
21210
|
let pending = "";
|
|
@@ -20984,7 +21238,7 @@ function runCrew(args, onLine, timeoutMs = 20 * 6e4) {
|
|
|
20984
21238
|
// src/control-api.ts
|
|
20985
21239
|
var PAUSABLE = ["dev", "design", "qa"];
|
|
20986
21240
|
var CONTROL_SOCKET_NAME = "control.sock";
|
|
20987
|
-
var controlSocketFor = (stateDir) =>
|
|
21241
|
+
var controlSocketFor = (stateDir) => join46(stateDir, CONTROL_SOCKET_NAME);
|
|
20988
21242
|
var MAX_SOCKET_PATH = 100;
|
|
20989
21243
|
var MAX_TAIL_BYTES = 1024 * 1024;
|
|
20990
21244
|
var MAX_WAIT_SECONDS = 30;
|
|
@@ -21000,7 +21254,7 @@ var ControlAuth = class {
|
|
|
21000
21254
|
if (!token) return "unauthorized";
|
|
21001
21255
|
for (const k of this.o.shipKeys()) if (k && secretsMatch(token, k)) return "ok";
|
|
21002
21256
|
const now = (this.o.now ?? Date.now)();
|
|
21003
|
-
const id =
|
|
21257
|
+
const id = createHash10("sha256").update(token).digest("hex");
|
|
21004
21258
|
const hit = this.cache.get(id);
|
|
21005
21259
|
if (hit && now - hit.at < (this.o.cacheMs ?? 6e4)) return hit.verdict;
|
|
21006
21260
|
const verdict = await this.ask(token);
|
|
@@ -21021,7 +21275,7 @@ var ControlAuth = class {
|
|
|
21021
21275
|
for (const k of this.o.shipKeys()) if (k && secretsMatch(token, k)) return "ok";
|
|
21022
21276
|
for (const k of this.o.workspaceSecrets?.(workspaceId) ?? []) if (k && secretsMatch(token, k)) return "ok";
|
|
21023
21277
|
const now = (this.o.now ?? Date.now)();
|
|
21024
|
-
const id = `${workspaceId}:${
|
|
21278
|
+
const id = `${workspaceId}:${createHash10("sha256").update(token).digest("hex")}`;
|
|
21025
21279
|
const hit = this.cache.get(id);
|
|
21026
21280
|
if (hit && now - hit.at < (this.o.cacheMs ?? 6e4)) return hit.verdict;
|
|
21027
21281
|
const verdict = await this.ask(token, [workspaceId]);
|
|
@@ -21071,7 +21325,7 @@ function candidates(live, rotated) {
|
|
|
21071
21325
|
const out = [live];
|
|
21072
21326
|
if (!rotated) return out;
|
|
21073
21327
|
try {
|
|
21074
|
-
for (const n of readdirSync18(dirname23(live))) if (ROTATED_EVENTS_RE.test(n)) out.push(
|
|
21328
|
+
for (const n of readdirSync18(dirname23(live))) if (ROTATED_EVENTS_RE.test(n)) out.push(join46(dirname23(live), n));
|
|
21075
21329
|
} catch {
|
|
21076
21330
|
}
|
|
21077
21331
|
return out;
|
|
@@ -21269,9 +21523,9 @@ async function route(deps, method, url, req, abort) {
|
|
|
21269
21523
|
if (p === "/events" && get2) return tailReply(eventFileFor(deps.stateDir), url.searchParams, true, abort);
|
|
21270
21524
|
if (p === "/streams" && get2) return reply2(200, { streams: liveRuns(deps.stateDir).map(describeStream) });
|
|
21271
21525
|
if (p.startsWith("/streams/") && get2) {
|
|
21272
|
-
const
|
|
21273
|
-
if (!RUN_NAME_RE.test(
|
|
21274
|
-
const file =
|
|
21526
|
+
const run3 = decodeURIComponent(p.slice("/streams/".length));
|
|
21527
|
+
if (!RUN_NAME_RE.test(run3) || run3.includes("..") || /\.(pid|meta\.json|events\.jsonl)$/.test(run3)) return err2(400, "not a run name");
|
|
21528
|
+
const file = join46(deps.stateDir, "streams", run3.endsWith(".jsonl") ? run3 : `${run3}.jsonl`);
|
|
21275
21529
|
if (!existsSync43(file)) return err2(404, "no such run");
|
|
21276
21530
|
return tailReply(file, url.searchParams, false, abort);
|
|
21277
21531
|
}
|
|
@@ -21334,7 +21588,7 @@ async function startControlServer(deps) {
|
|
|
21334
21588
|
log(`control API not started: socket path is too long for a unix socket (${path})`);
|
|
21335
21589
|
return null;
|
|
21336
21590
|
}
|
|
21337
|
-
if (await socketIsStale(path))
|
|
21591
|
+
if (await socketIsStale(path)) rmSync21(path, { force: true });
|
|
21338
21592
|
else if (existsSync43(path)) {
|
|
21339
21593
|
log(`control API not started: ${path} is in use by another process`);
|
|
21340
21594
|
return null;
|
|
@@ -21381,7 +21635,7 @@ async function startControlServer(deps) {
|
|
|
21381
21635
|
path,
|
|
21382
21636
|
close: () => new Promise((done) => {
|
|
21383
21637
|
server.close(() => {
|
|
21384
|
-
|
|
21638
|
+
rmSync21(path, { force: true });
|
|
21385
21639
|
done();
|
|
21386
21640
|
});
|
|
21387
21641
|
})
|
|
@@ -21390,19 +21644,19 @@ async function startControlServer(deps) {
|
|
|
21390
21644
|
});
|
|
21391
21645
|
}
|
|
21392
21646
|
var shq2 = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
|
|
21393
|
-
function runDetached(commands, spawnFn =
|
|
21647
|
+
function runDetached(commands, spawnFn = spawn10) {
|
|
21394
21648
|
const script = `sleep 1; ${commands.map((c) => c.map(shq2).join(" ")).join("; ")}`;
|
|
21395
21649
|
const child = spawnFn("/bin/sh", ["-c", script], { detached: true, stdio: "ignore" });
|
|
21396
21650
|
child.on("error", () => {
|
|
21397
21651
|
});
|
|
21398
21652
|
child.unref();
|
|
21399
21653
|
}
|
|
21400
|
-
function schedulerHandler(control,
|
|
21654
|
+
function schedulerHandler(control, run3 = runDetached) {
|
|
21401
21655
|
return (action) => {
|
|
21402
21656
|
const { startCommand: start, stopCommand: stop } = control;
|
|
21403
21657
|
if (!start || !stop) return { accepted: false, detail: "this host has no service manager for the daemon (cron or none)" };
|
|
21404
21658
|
const commands = action === "start" ? [start] : action === "stop" ? [stop] : [stop, start];
|
|
21405
|
-
|
|
21659
|
+
run3(commands);
|
|
21406
21660
|
return { accepted: true, detail: `${action} requested` };
|
|
21407
21661
|
};
|
|
21408
21662
|
}
|
|
@@ -21421,53 +21675,53 @@ function crewJson(args, timeoutMs = 6e4) {
|
|
|
21421
21675
|
|
|
21422
21676
|
// src/periscope-mcp.ts
|
|
21423
21677
|
import { lstat, mkdir, readdir, readFile, readlink, realpath, rm, stat, writeFile } from "fs/promises";
|
|
21424
|
-
import { basename as basename14, dirname as dirname24, isAbsolute as isAbsolute3, join as
|
|
21678
|
+
import { basename as basename14, dirname as dirname24, isAbsolute as isAbsolute3, join as join48, relative as relative2, resolve as resolve5, sep } from "path";
|
|
21425
21679
|
import { z } from "zod";
|
|
21426
21680
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
21427
21681
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
21428
21682
|
|
|
21429
21683
|
// src/periscope-endpoints.ts
|
|
21430
21684
|
import { randomBytes as randomBytes3 } from "crypto";
|
|
21431
|
-
import { chmodSync as chmodSync5, existsSync as existsSync44, mkdirSync as
|
|
21432
|
-
import { join as
|
|
21433
|
-
var secretDir = (stateDir) =>
|
|
21434
|
-
var secretFile = (stateDir, id) =>
|
|
21435
|
-
var endpointDir = (stateDir) =>
|
|
21436
|
-
var endpointFile = (stateDir, id) =>
|
|
21685
|
+
import { chmodSync as chmodSync5, existsSync as existsSync44, mkdirSync as mkdirSync29, readdirSync as readdirSync19, readFileSync as readFileSync39, rmSync as rmSync22, writeFileSync as writeFileSync32 } from "fs";
|
|
21686
|
+
import { join as join47 } from "path";
|
|
21687
|
+
var secretDir = (stateDir) => join47(stateDir, "periscope-mcp");
|
|
21688
|
+
var secretFile = (stateDir, id) => join47(secretDir(stateDir), `${id}.json`);
|
|
21689
|
+
var endpointDir = (stateDir) => join47(stateDir, "periscope-endpoints");
|
|
21690
|
+
var endpointFile = (stateDir, id) => join47(endpointDir(stateDir), `${id}.json`);
|
|
21437
21691
|
function mintPeriscopeSecret(stateDir, ticketRecordId) {
|
|
21438
|
-
|
|
21692
|
+
mkdirSync29(secretDir(stateDir), { recursive: true, mode: 448 });
|
|
21439
21693
|
const secret = randomBytes3(32).toString("base64url");
|
|
21440
21694
|
const file = secretFile(stateDir, ticketRecordId);
|
|
21441
|
-
|
|
21695
|
+
writeFileSync32(file, `${JSON.stringify({ secret })}
|
|
21442
21696
|
`, { mode: 384 });
|
|
21443
21697
|
chmodSync5(file, 384);
|
|
21444
21698
|
return secret;
|
|
21445
21699
|
}
|
|
21446
21700
|
function readPeriscopeSecret(stateDir, ticketRecordId) {
|
|
21447
21701
|
try {
|
|
21448
|
-
const v = JSON.parse(
|
|
21702
|
+
const v = JSON.parse(readFileSync39(secretFile(stateDir, ticketRecordId), "utf8"));
|
|
21449
21703
|
return typeof v.secret === "string" && v.secret ? v.secret : void 0;
|
|
21450
21704
|
} catch {
|
|
21451
21705
|
return void 0;
|
|
21452
21706
|
}
|
|
21453
21707
|
}
|
|
21454
21708
|
function removePeriscopeSecret(stateDir, ticketRecordId) {
|
|
21455
|
-
|
|
21709
|
+
rmSync22(secretFile(stateDir, ticketRecordId), { force: true });
|
|
21456
21710
|
}
|
|
21457
21711
|
function readPeriscopeEndpoint(stateDir, ticketRecordId) {
|
|
21458
21712
|
try {
|
|
21459
|
-
return JSON.parse(
|
|
21713
|
+
return JSON.parse(readFileSync39(endpointFile(stateDir, ticketRecordId), "utf8"));
|
|
21460
21714
|
} catch {
|
|
21461
21715
|
return void 0;
|
|
21462
21716
|
}
|
|
21463
21717
|
}
|
|
21464
21718
|
function writePeriscopeEndpoint(stateDir, rec) {
|
|
21465
|
-
|
|
21466
|
-
|
|
21719
|
+
mkdirSync29(endpointDir(stateDir), { recursive: true });
|
|
21720
|
+
writeFileSync32(endpointFile(stateDir, rec.ticketRecordId), `${JSON.stringify(rec, null, 2)}
|
|
21467
21721
|
`);
|
|
21468
21722
|
}
|
|
21469
21723
|
function removePeriscopeEndpointRecord(stateDir, ticketRecordId) {
|
|
21470
|
-
|
|
21724
|
+
rmSync22(endpointFile(stateDir, ticketRecordId), { force: true });
|
|
21471
21725
|
}
|
|
21472
21726
|
function listPeriscopeEndpoints(stateDir) {
|
|
21473
21727
|
const dir = endpointDir(stateDir);
|
|
@@ -21574,13 +21828,13 @@ async function resolveInWorktree(root, requested) {
|
|
|
21574
21828
|
let abs;
|
|
21575
21829
|
if (isAbsolute3(requested)) {
|
|
21576
21830
|
abs = resolve5(requested);
|
|
21577
|
-
if (isInside(root, abs)) abs =
|
|
21831
|
+
if (isInside(root, abs)) abs = join48(realRoot, relative2(root, abs));
|
|
21578
21832
|
} else {
|
|
21579
21833
|
abs = resolve5(realRoot, requested);
|
|
21580
21834
|
}
|
|
21581
21835
|
if (!isInside(realRoot, abs)) throw new PathRefusal(`${requested} is outside this periscope's worktree`);
|
|
21582
21836
|
const { real, rest } = await realAncestor(abs);
|
|
21583
|
-
const final = rest ?
|
|
21837
|
+
const final = rest ? join48(real, rest) : real;
|
|
21584
21838
|
if (!isInside(realRoot, final)) throw new PathRefusal(`${requested} is outside this periscope's worktree`);
|
|
21585
21839
|
if (relative2(realRoot, final).split(sep).includes(".git")) throw new PathRefusal(".git is not reachable from a periscope");
|
|
21586
21840
|
return final;
|
|
@@ -21648,7 +21902,7 @@ function createPeriscopeMcpServer(d, sc) {
|
|
|
21648
21902
|
const entries = (await readdir(abs, { withFileTypes: true })).filter((e) => e.name !== ".git");
|
|
21649
21903
|
const lines2 = await Promise.all(entries.map(async (e) => {
|
|
21650
21904
|
if (e.isDirectory()) return `[DIR] ${e.name}`;
|
|
21651
|
-
const size = (await lstat(
|
|
21905
|
+
const size = (await lstat(join48(abs, e.name))).size;
|
|
21652
21906
|
return `[FILE] ${e.name} (${size} bytes)`;
|
|
21653
21907
|
}));
|
|
21654
21908
|
return text(lines2.join("\n") || "(empty)");
|
|
@@ -21714,7 +21968,7 @@ function createPeriscopeMcpServer(d, sc) {
|
|
|
21714
21968
|
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false }
|
|
21715
21969
|
}, async ({ path }) => {
|
|
21716
21970
|
const parent = await inTree(dirname24(path) === "" ? "." : dirname24(path));
|
|
21717
|
-
const abs =
|
|
21971
|
+
const abs = join48(parent, basename14(path));
|
|
21718
21972
|
if (basename14(path) === ".git" || abs === parent) return text("path must name a file", true);
|
|
21719
21973
|
const st = await lstat(abs).catch(() => void 0);
|
|
21720
21974
|
if (!st) return text(`${path} does not exist`, true);
|
|
@@ -22121,6 +22375,7 @@ var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
22121
22375
|
"rotate-passenger-url",
|
|
22122
22376
|
"run",
|
|
22123
22377
|
"sample-ticket",
|
|
22378
|
+
"sandbox",
|
|
22124
22379
|
"selftest",
|
|
22125
22380
|
"shell-init",
|
|
22126
22381
|
"ship",
|
|
@@ -22200,7 +22455,7 @@ if (command === "provision") {
|
|
|
22200
22455
|
process.exit(outcome.code);
|
|
22201
22456
|
}
|
|
22202
22457
|
if (command === "pool-member") {
|
|
22203
|
-
const pool = poolConfigFrom(argv, process.env, () =>
|
|
22458
|
+
const pool = poolConfigFrom(argv, process.env, () => readFileSync40(0, "utf8"));
|
|
22204
22459
|
if (typeof pool === "string") {
|
|
22205
22460
|
process.stderr.write(`crew pool-member: ${pool}
|
|
22206
22461
|
`);
|
|
@@ -22241,9 +22496,9 @@ if (command === "passenger-sync-daemon") {
|
|
|
22241
22496
|
return m ? m[1] : null;
|
|
22242
22497
|
}, routeRelayUrl = function(route3) {
|
|
22243
22498
|
const ws = route3.resolved?.workspaceId;
|
|
22244
|
-
const
|
|
22245
|
-
if (!cfg.ship.relayHost || !
|
|
22246
|
-
return relayBaseUrlFor(
|
|
22499
|
+
const slug2 = ws ? readPersistedTunnel(cfg.ship.stateDir, ws)?.slug : void 0;
|
|
22500
|
+
if (!cfg.ship.relayHost || !slug2) return null;
|
|
22501
|
+
return relayBaseUrlFor(slug2, cfg.ship.relayHost, cfg.ship.relayHttpPort, cfg.ship.relayPublicDomain);
|
|
22247
22502
|
}, passengerApiKeys = function(routes) {
|
|
22248
22503
|
const keys = /* @__PURE__ */ new Map();
|
|
22249
22504
|
for (const r of routes) {
|
|
@@ -22308,8 +22563,8 @@ if (command === "passenger-sync-daemon") {
|
|
|
22308
22563
|
stateDir: cfg.ship.stateDir,
|
|
22309
22564
|
shipName: cfg.ship.name,
|
|
22310
22565
|
apiFor: endpointApiFor(routes),
|
|
22311
|
-
urlFor: (
|
|
22312
|
-
controlUrlFor: (
|
|
22566
|
+
urlFor: (slug2) => publicUrlFor(slug2, relayHost, cfg.ship.relayHttpPort, cfg.ship.relayPublicDomain),
|
|
22567
|
+
controlUrlFor: (slug2) => `${relayBaseUrlFor(slug2, relayHost, cfg.ship.relayHttpPort, cfg.ship.relayPublicDomain)}/control`,
|
|
22313
22568
|
warn: (m) => emit.warn(m, { step: "passengers" })
|
|
22314
22569
|
};
|
|
22315
22570
|
}, releaseRefused = function() {
|
|
@@ -22329,8 +22584,20 @@ if (command === "passenger-sync-daemon") {
|
|
|
22329
22584
|
const out = refreshBaseBranch(r.dir, remote, base, true);
|
|
22330
22585
|
const unsafe = baseBranchUnsafe(out);
|
|
22331
22586
|
return { action: out.action, unsafe, detail: unsafe ? describeUnsafeBase(out, remote, base) : out.detail };
|
|
22587
|
+
}, sandboxTickDeps = function(emit2, c) {
|
|
22588
|
+
return {
|
|
22589
|
+
stateDir: cfg.ship.stateDir,
|
|
22590
|
+
run: spawnBuildRunner(),
|
|
22591
|
+
hasImage: engineHasImage(),
|
|
22592
|
+
removeImage: engineRemoveImage(),
|
|
22593
|
+
raise: (item) => {
|
|
22594
|
+
void raiseShipAttention({ state, emit: emit2, route: c, ship: cfg.ship }, item);
|
|
22595
|
+
},
|
|
22596
|
+
clear: (key) => clearShipAttention(state, key),
|
|
22597
|
+
log: (m) => emit2.emit(m)
|
|
22598
|
+
};
|
|
22332
22599
|
};
|
|
22333
|
-
requireArmed2 = requireArmed, parseTicketNumber2 = parseTicketNumber, routeRelayUrl2 = routeRelayUrl, passengerApiKeys2 = passengerApiKeys, endpointApiFor2 = endpointApiFor, periscopeEndpointDeps2 = periscopeEndpointDeps, periscopeMcpDeps2 = periscopeMcpDeps, endpointReconcileDeps2 = endpointReconcileDeps, releaseRefused2 = releaseRefused, doctorMachineId2 = doctorMachineId, baseHealth2 = baseHealth;
|
|
22600
|
+
requireArmed2 = requireArmed, parseTicketNumber2 = parseTicketNumber, routeRelayUrl2 = routeRelayUrl, passengerApiKeys2 = passengerApiKeys, endpointApiFor2 = endpointApiFor, periscopeEndpointDeps2 = periscopeEndpointDeps, periscopeMcpDeps2 = periscopeMcpDeps, endpointReconcileDeps2 = endpointReconcileDeps, releaseRefused2 = releaseRefused, doctorMachineId2 = doctorMachineId, baseHealth2 = baseHealth, sandboxTickDeps2 = sandboxTickDeps;
|
|
22334
22601
|
let cfg;
|
|
22335
22602
|
let firstRun;
|
|
22336
22603
|
let firstRunFromFlags = false;
|
|
@@ -22401,7 +22668,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
22401
22668
|
throw e;
|
|
22402
22669
|
}
|
|
22403
22670
|
if (command === "credential") {
|
|
22404
|
-
const r = runCredential(positional[1],
|
|
22671
|
+
const r = runCredential(positional[1], readFileSync40(0, "utf8"), cfg.ship.forges, cfg.ship.stateDir);
|
|
22405
22672
|
process.stdout.write(r.out);
|
|
22406
22673
|
process.stderr.write(r.err);
|
|
22407
22674
|
process.exit(r.code);
|
|
@@ -22413,9 +22680,9 @@ if (command === "passenger-sync-daemon") {
|
|
|
22413
22680
|
forges: cfg.ship.forges,
|
|
22414
22681
|
stateDir: cfg.ship.stateDir,
|
|
22415
22682
|
configFile: cfg.configFile,
|
|
22416
|
-
readFile: (p) =>
|
|
22417
|
-
writeFile: (p, t) =>
|
|
22418
|
-
stdin: () =>
|
|
22683
|
+
readFile: (p) => readFileSync40(p, "utf8"),
|
|
22684
|
+
writeFile: (p, t) => writeFileSync33(p, t),
|
|
22685
|
+
stdin: () => readFileSync40(0, "utf8"),
|
|
22419
22686
|
env: process.env
|
|
22420
22687
|
}
|
|
22421
22688
|
);
|
|
@@ -22435,7 +22702,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
22435
22702
|
const DAEMON_SUBCOMMANDS = /* @__PURE__ */ new Set(["start", "stop", "status", "restart"]);
|
|
22436
22703
|
const worktreeArgs = command === "worktree" ? splitWorktreeArgs(positional, cfg.routes.map((r) => r.route)) : void 0;
|
|
22437
22704
|
const named = worktreeArgs ? worktreeArgs.route : command === "daemon" && DAEMON_SUBCOMMANDS.has(positional[1] ?? "") ? positional[2] : positional[1];
|
|
22438
|
-
const fleetWide = command === "inbox" || command === "connect" || command === "disconnect" || command === "deprovision" || command === "agents" || command === "skills" || command === "logbook" || command === "repos" || command === "ship" || command === "projects" || command === "periscope" || command === "whoami" || releaseFleetWide || command === "pause" || command === "resume" || command === "reload" || FLEET_CAPABLE.has(command) && !named && cfg.routes.length > 1 || worktreeArgs !== void 0 && !named && cfg.routes.length > 1;
|
|
22705
|
+
const fleetWide = command === "inbox" || command === "connect" || command === "disconnect" || command === "deprovision" || command === "agents" || command === "skills" || command === "logbook" || command === "repos" || command === "ship" || command === "projects" || command === "periscope" || command === "sandbox" || command === "whoami" || releaseFleetWide || command === "pause" || command === "resume" || command === "reload" || FLEET_CAPABLE.has(command) && !named && cfg.routes.length > 1 || worktreeArgs !== void 0 && !named && cfg.routes.length > 1;
|
|
22439
22706
|
let route2;
|
|
22440
22707
|
try {
|
|
22441
22708
|
route2 = fleetWide ? cfg.routes[0] : findRoute(cfg, named);
|
|
@@ -22698,7 +22965,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
22698
22965
|
}
|
|
22699
22966
|
if (host === "linux" && hasSystemd) {
|
|
22700
22967
|
const label = labelFor(CREW_HOME, "run");
|
|
22701
|
-
const staleTimer =
|
|
22968
|
+
const staleTimer = join49(homedir11(), ".config", "systemd", "user", `${label}.timer`);
|
|
22702
22969
|
if (existsSync45(staleTimer)) {
|
|
22703
22970
|
process.stdout.write(
|
|
22704
22971
|
`removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
|
|
@@ -22761,7 +23028,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
22761
23028
|
for (const a of plan.actions) {
|
|
22762
23029
|
if (a.kind === "enable") {
|
|
22763
23030
|
if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
|
|
22764
|
-
|
|
23031
|
+
writeFileSync33(cfg.configFile, planEnable(readFileSync40(cfg.configFile, "utf8"), a.route));
|
|
22765
23032
|
enabledSound.add(a.route);
|
|
22766
23033
|
changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
|
|
22767
23034
|
} else {
|
|
@@ -22780,6 +23047,14 @@ ${changed2.map((c) => ` - ${c}
|
|
|
22780
23047
|
async function resolvedRepos(c) {
|
|
22781
23048
|
return Promise.all(reposOf(c).map((t) => resolveOneRepo(c, t)));
|
|
22782
23049
|
}
|
|
23050
|
+
async function sandboxReposOf(routes) {
|
|
23051
|
+
const out = /* @__PURE__ */ new Map();
|
|
23052
|
+
for (const c of routes) {
|
|
23053
|
+
const { settled } = await settleRepos(reposOf(c), (t) => t.name, (t) => resolveOneRepo(c, t));
|
|
23054
|
+
for (const r of settled) if (r.config.sandbox && !out.has(r.name)) out.set(r.name, { name: r.name, decl: r.config.sandbox });
|
|
23055
|
+
}
|
|
23056
|
+
return [...out.values()];
|
|
23057
|
+
}
|
|
22783
23058
|
async function resolveOneRepo(c, t) {
|
|
22784
23059
|
{
|
|
22785
23060
|
const id = repoIdForName(c, t.name);
|
|
@@ -23884,7 +24159,7 @@ ${cu.tail.trimEnd()}
|
|
|
23884
24159
|
process.exit(1);
|
|
23885
24160
|
}
|
|
23886
24161
|
requireArmed("run the daemon loop");
|
|
23887
|
-
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [
|
|
24162
|
+
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join49(CREW_HOME, "src"), join49(CREW_HOME, "dist"), join49(CREW_HOME, "bin")];
|
|
23888
24163
|
const configPaths = daemonConfigPaths(cfg);
|
|
23889
24164
|
watchPaths.push(...configPaths);
|
|
23890
24165
|
noteDaemonStarted(cfg.ship.stateDir);
|
|
@@ -23950,6 +24225,7 @@ ${cu.tail.trimEnd()}
|
|
|
23950
24225
|
});
|
|
23951
24226
|
if (controlServer) emit.emit(`control API listening on ${controlServer.path}`);
|
|
23952
24227
|
const inFlight = /* @__PURE__ */ new Set();
|
|
24228
|
+
let sandboxBuilding = false;
|
|
23953
24229
|
const stopReason = await runDaemonLoop({
|
|
23954
24230
|
inFlight,
|
|
23955
24231
|
onStaleDeferred: ({ runs, waitedMs }) => emit.warn(
|
|
@@ -23995,6 +24271,12 @@ ${cu.tail.trimEnd()}
|
|
|
23995
24271
|
),
|
|
23996
24272
|
checkStale: makeStaleChecker(watchPaths, { reloadSentinel: reloadSentinelFor(cfg.ship.stateDir) }),
|
|
23997
24273
|
onPass: (result) => {
|
|
24274
|
+
if (!sandboxBuilding) {
|
|
24275
|
+
sandboxBuilding = true;
|
|
24276
|
+
void sandboxReposOf(cfg.routes.filter((r) => r.enabled)).then((repos) => repos.length ? sandboxBuildTick(repos, sandboxTickDeps(emit, route2)) : null).catch((err3) => emit.warn(`sandbox image build tick failed: ${err3 instanceof Error ? err3.message : String(err3)}`)).finally(() => {
|
|
24277
|
+
sandboxBuilding = false;
|
|
24278
|
+
});
|
|
24279
|
+
}
|
|
23998
24280
|
if (result.started.length) emit.emit(`daemon pass started: ${result.started.join(", ")}`);
|
|
23999
24281
|
for (const s of result.skipped) emit.emit(`daemon pass skipped ${s.role} \u2014 ${s.reason}`);
|
|
24000
24282
|
}
|
|
@@ -24097,9 +24379,9 @@ ${cu.tail.trimEnd()}
|
|
|
24097
24379
|
`);
|
|
24098
24380
|
process.exit(2);
|
|
24099
24381
|
}
|
|
24100
|
-
let
|
|
24382
|
+
let run3;
|
|
24101
24383
|
try {
|
|
24102
|
-
|
|
24384
|
+
run3 = await runAgentsSync(target, {
|
|
24103
24385
|
userAgent: cfg.ship.userAgent,
|
|
24104
24386
|
stateDir: cfg.ship.stateDir,
|
|
24105
24387
|
dryRun,
|
|
@@ -24117,7 +24399,7 @@ ${cu.tail.trimEnd()}
|
|
|
24117
24399
|
}
|
|
24118
24400
|
throw e;
|
|
24119
24401
|
}
|
|
24120
|
-
const { personaResult, skillResult } =
|
|
24402
|
+
const { personaResult, skillResult } = run3;
|
|
24121
24403
|
if (flag("revoke-seat-keys")) {
|
|
24122
24404
|
for (const o of await revokeSeatKeys(target, { userAgent: cfg.ship.userAgent, dryRun })) {
|
|
24123
24405
|
process.stdout.write(`${describeRevokeOutcome(o)}
|
|
@@ -24138,7 +24420,7 @@ ${cu.tail.trimEnd()}
|
|
|
24138
24420
|
}
|
|
24139
24421
|
}
|
|
24140
24422
|
}
|
|
24141
|
-
if (
|
|
24423
|
+
if (run3.diverged) process.exitCode = 1;
|
|
24142
24424
|
break;
|
|
24143
24425
|
}
|
|
24144
24426
|
case "skills": {
|
|
@@ -24180,10 +24462,10 @@ ${cu.tail.trimEnd()}
|
|
|
24180
24462
|
`);
|
|
24181
24463
|
if (!dryRun) {
|
|
24182
24464
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
24183
|
-
const raw = existsSync45(resolvedPath) ? JSON.parse(
|
|
24465
|
+
const raw = existsSync45(resolvedPath) ? JSON.parse(readFileSync40(resolvedPath, "utf8")) : {};
|
|
24184
24466
|
raw.agentSkills = result.agentSkills;
|
|
24185
|
-
|
|
24186
|
-
|
|
24467
|
+
mkdirSync30(dirOf2(resolvedPath), { recursive: true });
|
|
24468
|
+
writeFileSync33(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
24187
24469
|
`);
|
|
24188
24470
|
}
|
|
24189
24471
|
if (result.outcomes.some((o) => o.action === "diverged")) process.exitCode = 1;
|
|
@@ -24206,7 +24488,7 @@ ${cu.tail.trimEnd()}
|
|
|
24206
24488
|
throw e;
|
|
24207
24489
|
}
|
|
24208
24490
|
const oldName = cfg.ship.name;
|
|
24209
|
-
|
|
24491
|
+
writeFileSync33(cfg.configFile, setShipNameInYaml(readFileSync40(cfg.configFile, "utf8"), newName));
|
|
24210
24492
|
cfg.ship.name = newName;
|
|
24211
24493
|
let renameMachineId;
|
|
24212
24494
|
try {
|
|
@@ -24253,7 +24535,7 @@ ${cu.tail.trimEnd()}
|
|
|
24253
24535
|
process.exit(2);
|
|
24254
24536
|
};
|
|
24255
24537
|
if (sub !== "add" && sub !== "list" && sub !== "clone" && sub !== "hooks" && sub !== "check") usage2();
|
|
24256
|
-
const configText =
|
|
24538
|
+
const configText = readFileSync40(cfg.configFile, "utf8");
|
|
24257
24539
|
if (sub === "check") {
|
|
24258
24540
|
try {
|
|
24259
24541
|
const routeArg = value("route") ?? positional[2];
|
|
@@ -24293,7 +24575,7 @@ ${cu.tail.trimEnd()}
|
|
|
24293
24575
|
}
|
|
24294
24576
|
}, read: (r) => {
|
|
24295
24577
|
try {
|
|
24296
|
-
return
|
|
24578
|
+
return readFileSync40(resolvePath(dir, r), "utf8");
|
|
24297
24579
|
} catch {
|
|
24298
24580
|
return void 0;
|
|
24299
24581
|
}
|
|
@@ -24335,19 +24617,19 @@ ${cu.tail.trimEnd()}
|
|
|
24335
24617
|
let cleared;
|
|
24336
24618
|
if (clear2) {
|
|
24337
24619
|
const plan = clearRouteHooks(configText, a.route, a.name);
|
|
24338
|
-
if (plan.removed.length > 0)
|
|
24620
|
+
if (plan.removed.length > 0) writeFileSync33(cfg.configFile, plan.text);
|
|
24339
24621
|
cleared = { file: cfg.configFile, removed: plan.removed };
|
|
24340
24622
|
}
|
|
24341
24623
|
if (write && suggestion.source === null && recipeIds.length === 0 && hookSpecs.length === 0) throw new ReposError(`nothing to write: ${suggestion.notes[0]}`);
|
|
24342
24624
|
if (write === "route") {
|
|
24343
24625
|
const plan = planRouteHooks(configText, a.route, a.name, picked.hooks, { replace: flag("replace") });
|
|
24344
|
-
|
|
24626
|
+
writeFileSync33(cfg.configFile, plan.text);
|
|
24345
24627
|
written = { target: "route", file: cfg.configFile, hooks: plan.written, kept: plan.kept, replaced: plan.replaced };
|
|
24346
24628
|
} else if (write === "repo") {
|
|
24347
24629
|
const have = existingRepoConfig(files);
|
|
24348
24630
|
if (have) throw new ReposError(`${resolvePath(dir, have)} already exists; not overwriting it`);
|
|
24349
24631
|
const file = resolvePath(dir, ".crew.yaml");
|
|
24350
|
-
|
|
24632
|
+
writeFileSync33(file, starterRepoConfig(picked.hooks, picked.release), { flag: "wx" });
|
|
24351
24633
|
written = { target: "repo", file, hooks: Object.keys(picked.hooks), kept: [], replaced: [] };
|
|
24352
24634
|
}
|
|
24353
24635
|
if (flag("json")) {
|
|
@@ -24444,7 +24726,7 @@ ${cu.tail.trimEnd()}
|
|
|
24444
24726
|
let resolvedRemotes;
|
|
24445
24727
|
if (existsSync45(resolvedFile)) {
|
|
24446
24728
|
try {
|
|
24447
|
-
resolvedRemotes = JSON.parse(
|
|
24729
|
+
resolvedRemotes = JSON.parse(readFileSync40(resolvedFile, "utf8"));
|
|
24448
24730
|
} catch {
|
|
24449
24731
|
}
|
|
24450
24732
|
}
|
|
@@ -24465,7 +24747,7 @@ ${cu.tail.trimEnd()}
|
|
|
24465
24747
|
isCheckout,
|
|
24466
24748
|
repoHasHooks: (d) => REPO_CONFIG_FILENAMES.some((f) => {
|
|
24467
24749
|
try {
|
|
24468
|
-
return Object.keys(parseRepoConfig(
|
|
24750
|
+
return Object.keys(parseRepoConfig(readFileSync40(resolvePath(d, f), "utf8"), f).hooks).length > 0;
|
|
24469
24751
|
} catch {
|
|
24470
24752
|
return false;
|
|
24471
24753
|
}
|
|
@@ -24514,7 +24796,7 @@ ${cu.tail.trimEnd()}
|
|
|
24514
24796
|
throw new ReposError(`${e.message}${authHint}
|
|
24515
24797
|
crew.yaml was not changed`);
|
|
24516
24798
|
}
|
|
24517
|
-
|
|
24799
|
+
writeFileSync33(cfg.configFile, plan2.text);
|
|
24518
24800
|
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:
|
|
24519
24801
|
`);
|
|
24520
24802
|
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}`);
|
|
@@ -24574,14 +24856,14 @@ ${cu.tail.trimEnd()}
|
|
|
24574
24856
|
process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
|
|
24575
24857
|
`);
|
|
24576
24858
|
if (existsSync45(resolvedFile)) {
|
|
24577
|
-
const raw = JSON.parse(
|
|
24859
|
+
const raw = JSON.parse(readFileSync40(resolvedFile, "utf8"));
|
|
24578
24860
|
raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
|
|
24579
24861
|
raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
|
|
24580
|
-
|
|
24862
|
+
writeFileSync33(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
24581
24863
|
`);
|
|
24582
24864
|
}
|
|
24583
24865
|
}
|
|
24584
|
-
|
|
24866
|
+
writeFileSync33(cfg.configFile, plan.text);
|
|
24585
24867
|
process.stdout.write(`added ${top} to ${routeName} as ${plan.mode === "dir" ? "dir" : `repos.${plan.name}`} in ${cfg.configFile}
|
|
24586
24868
|
`);
|
|
24587
24869
|
} catch (e) {
|
|
@@ -24950,7 +25232,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
24950
25232
|
process.exit(2);
|
|
24951
25233
|
}
|
|
24952
25234
|
if (found.shipNameAdopted && !dryRun) {
|
|
24953
|
-
|
|
25235
|
+
writeFileSync33(cfg.configFile, setShipNameInYaml(readFileSync40(cfg.configFile, "utf8"), found.shipNameAdopted));
|
|
24954
25236
|
cfg.ship.name = found.shipNameAdopted;
|
|
24955
25237
|
}
|
|
24956
25238
|
if (found.shipRow) {
|
|
@@ -24996,7 +25278,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
24996
25278
|
const hadPreviousFile = existsSync45(resolvedPath);
|
|
24997
25279
|
if (hadPreviousFile) {
|
|
24998
25280
|
try {
|
|
24999
|
-
previous = JSON.parse(
|
|
25281
|
+
previous = JSON.parse(readFileSync40(resolvedPath, "utf8"));
|
|
25000
25282
|
} catch {
|
|
25001
25283
|
}
|
|
25002
25284
|
}
|
|
@@ -25126,12 +25408,12 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
25126
25408
|
process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
|
|
25127
25409
|
`);
|
|
25128
25410
|
} else {
|
|
25129
|
-
|
|
25411
|
+
mkdirSync30(dirname25(resolvedPath), { recursive: true });
|
|
25130
25412
|
if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
|
|
25131
25413
|
const tmpPath = `${resolvedPath}.tmp${process.pid}`;
|
|
25132
|
-
|
|
25414
|
+
writeFileSync33(tmpPath, `${JSON.stringify(resolved, null, 2)}
|
|
25133
25415
|
`);
|
|
25134
|
-
|
|
25416
|
+
renameSync6(tmpPath, resolvedPath);
|
|
25135
25417
|
process.stderr.write(`Resolved ids written to ${resolvedPath}
|
|
25136
25418
|
`);
|
|
25137
25419
|
}
|
|
@@ -25175,8 +25457,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
25175
25457
|
process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
|
|
25176
25458
|
`);
|
|
25177
25459
|
} else {
|
|
25178
|
-
|
|
25179
|
-
|
|
25460
|
+
mkdirSync30(dirname25(apiKeyFile), { recursive: true });
|
|
25461
|
+
writeFileSync33(apiKeyFile, `${apiKeyVar}=${apiKey}
|
|
25180
25462
|
`, { mode: 384 });
|
|
25181
25463
|
process.stderr.write(`API key written to ${apiKeyFile}
|
|
25182
25464
|
`);
|
|
@@ -25309,7 +25591,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
25309
25591
|
if (syncRoute?.resolved) {
|
|
25310
25592
|
try {
|
|
25311
25593
|
process.stderr.write("\n");
|
|
25312
|
-
const
|
|
25594
|
+
const run3 = await runAgentsSync(syncRoute, {
|
|
25313
25595
|
userAgent: cfg.ship.userAgent,
|
|
25314
25596
|
stateDir: cfg.ship.stateDir,
|
|
25315
25597
|
say: (l) => process.stderr.write(` ${l}
|
|
@@ -25324,19 +25606,19 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
25324
25606
|
if (p.state === "forbidden") process.stderr.write(` ${describeSeatForbidden(p.role, p.tables)}
|
|
25325
25607
|
`);
|
|
25326
25608
|
}
|
|
25327
|
-
if (
|
|
25328
|
-
const missing =
|
|
25609
|
+
if (run3.readOnly) {
|
|
25610
|
+
const missing = run3.personaResult.outcomes.some((p) => p.action === "missing");
|
|
25329
25611
|
process.stderr.write(missing ? " A workspace admin has to open the Crew app or run `crew agents sync` once before this ship's seats can run.\n" : " Seats are linked to the workspace's existing personas.\n");
|
|
25330
25612
|
}
|
|
25331
|
-
connectEvents.emit("agents_synced",
|
|
25613
|
+
connectEvents.emit("agents_synced", run3.readOnly ? "agents linked (read-only key)" : "agents synced", {
|
|
25332
25614
|
route: route3,
|
|
25333
|
-
admin: !
|
|
25334
|
-
personas:
|
|
25335
|
-
skills:
|
|
25336
|
-
diverged:
|
|
25615
|
+
admin: !run3.readOnly,
|
|
25616
|
+
personas: run3.personaResult.outcomes,
|
|
25617
|
+
skills: run3.skillResult?.outcomes ?? [],
|
|
25618
|
+
diverged: run3.diverged
|
|
25337
25619
|
});
|
|
25338
25620
|
for (const m of minted) connectEvents.emit("seat_key", describeMintOutcome(m), { route: route3, ...m });
|
|
25339
|
-
if (
|
|
25621
|
+
if (run3.diverged) process.exitCode = 1;
|
|
25340
25622
|
} catch (e) {
|
|
25341
25623
|
if (e instanceof AgentsSyncError || e instanceof SkillSyncError) skip(e.message);
|
|
25342
25624
|
else throw e;
|
|
@@ -25724,6 +26006,70 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
25724
26006
|
if (!planIsUsable(planWt)) process.exitCode = 1;
|
|
25725
26007
|
break;
|
|
25726
26008
|
}
|
|
26009
|
+
case "sandbox": {
|
|
26010
|
+
const subS = positional[1];
|
|
26011
|
+
if (subS !== "build" && subS !== "images") {
|
|
26012
|
+
process.stderr.write("crew sandbox build [REPO] [--route ROUTE] [--json]\ncrew sandbox images [--route ROUTE] [--json]\n");
|
|
26013
|
+
process.exit(2);
|
|
26014
|
+
}
|
|
26015
|
+
const jsonS = flag("json");
|
|
26016
|
+
let routesS;
|
|
26017
|
+
try {
|
|
26018
|
+
routesS = value("route") ? [findRoute(cfg, value("route"))] : cfg.routes.filter((r) => r.enabled);
|
|
26019
|
+
} catch (e) {
|
|
26020
|
+
if (e instanceof ConfigError) {
|
|
26021
|
+
process.stderr.write(`crew: ${e.message}
|
|
26022
|
+
`);
|
|
26023
|
+
process.exit(2);
|
|
26024
|
+
}
|
|
26025
|
+
throw e;
|
|
26026
|
+
}
|
|
26027
|
+
const declared = await sandboxReposOf(routesS);
|
|
26028
|
+
if (subS === "images") {
|
|
26029
|
+
const stateS = readSandboxState(cfg.ship.stateDir);
|
|
26030
|
+
const rows = declared.map((r) => {
|
|
26031
|
+
const plan = planSandbox(r.name, r.decl);
|
|
26032
|
+
return { repo: r.name, wants: plan.tag, tags: stateS[r.name]?.tags ?? [], ...sandboxImageStatus(stateS, plan) };
|
|
26033
|
+
});
|
|
26034
|
+
if (jsonS) {
|
|
26035
|
+
process.stdout.write(`${JSON.stringify(rows, null, 2)}
|
|
26036
|
+
`);
|
|
26037
|
+
break;
|
|
26038
|
+
}
|
|
26039
|
+
if (rows.length === 0) process.stdout.write("no repository declares a sandbox: section in its .crew.yaml\n");
|
|
26040
|
+
for (const r of rows) {
|
|
26041
|
+
process.stdout.write(`${r.repo}: ${r.status} \u2014 ${r.detail}
|
|
26042
|
+
`);
|
|
26043
|
+
for (const t of r.tags) {
|
|
26044
|
+
const ageH = Math.max(0, Math.round((Date.now() - Date.parse(t.builtAt)) / 36e5));
|
|
26045
|
+
process.stdout.write(` ${t.tag} built ${t.builtAt} (${ageH}h ago)
|
|
26046
|
+
`);
|
|
26047
|
+
}
|
|
26048
|
+
}
|
|
26049
|
+
break;
|
|
26050
|
+
}
|
|
26051
|
+
const only = positional[2];
|
|
26052
|
+
const targets = only ? declared.filter((r) => r.name === only) : declared;
|
|
26053
|
+
if (targets.length === 0) {
|
|
26054
|
+
process.stderr.write(only ? `crew: no repository "${only}" declares a sandbox: section
|
|
26055
|
+
` : "crew: no repository declares a sandbox: section in its .crew.yaml\n");
|
|
26056
|
+
process.exit(2);
|
|
26057
|
+
}
|
|
26058
|
+
const emitS = new Emitter({ route: routesS[0].route, eventFile: eventFileFor(cfg.ship.stateDir), rotateBytes: cfg.ship.eventsRotateBytes, logFile: cfg.ship.logFile, console: (l) => process.stderr.write(`${l}
|
|
26059
|
+
`) });
|
|
26060
|
+
const depsS = sandboxTickDeps(emitS, routesS[0]);
|
|
26061
|
+
let failed = false;
|
|
26062
|
+
for (const t of targets) {
|
|
26063
|
+
await sandboxBuildTick([t], depsS, true);
|
|
26064
|
+
const after = readSandboxState(cfg.ship.stateDir);
|
|
26065
|
+
const st = sandboxImageStatus(after, planSandbox(t.name, t.decl));
|
|
26066
|
+
if (st.status === "failed") failed = true;
|
|
26067
|
+
process.stdout.write(`${t.name}: ${st.status} \u2014 ${st.detail}
|
|
26068
|
+
`);
|
|
26069
|
+
}
|
|
26070
|
+
if (failed) process.exit(1);
|
|
26071
|
+
break;
|
|
26072
|
+
}
|
|
25727
26073
|
case "periscope": {
|
|
25728
26074
|
const subP = positional[1];
|
|
25729
26075
|
if (subP !== "up" && subP !== "start" && subP !== "down" && subP !== "end" && subP !== "list") {
|
|
@@ -26006,10 +26352,10 @@ ${where.join("\n")}`);
|
|
|
26006
26352
|
},
|
|
26007
26353
|
prepare: async (path) => {
|
|
26008
26354
|
for (const f of repoP.config.worktrees.copy) {
|
|
26009
|
-
const src =
|
|
26355
|
+
const src = join49(repoP.dir, f);
|
|
26010
26356
|
if (!existsSync45(src)) continue;
|
|
26011
|
-
|
|
26012
|
-
copyFileSync2(src,
|
|
26357
|
+
mkdirSync30(dirOf2(join49(path, f)), { recursive: true });
|
|
26358
|
+
copyFileSync2(src, join49(path, f));
|
|
26013
26359
|
}
|
|
26014
26360
|
const setup = repoP.config.hooks.setup;
|
|
26015
26361
|
return setup ? runScript(setup, { cwd: path, shell: shellP, onLine: (l) => {
|
|
@@ -26283,9 +26629,9 @@ ${where.join("\n")}`);
|
|
|
26283
26629
|
} };
|
|
26284
26630
|
const store = resetStore;
|
|
26285
26631
|
const h = hostFromUrl4(r.baseUrl);
|
|
26286
|
-
const [
|
|
26287
|
-
const session = await store.get(h,
|
|
26288
|
-
return { stored: Boolean(session?.apiKey), clear: () => store.clear(h,
|
|
26632
|
+
const [slug2] = splitRoute(r.route);
|
|
26633
|
+
const session = await store.get(h, slug2);
|
|
26634
|
+
return { stored: Boolean(session?.apiKey), clear: () => store.clear(h, slug2) };
|
|
26289
26635
|
},
|
|
26290
26636
|
async hostSessions(stateDir) {
|
|
26291
26637
|
const store = resetStore;
|
|
@@ -26304,7 +26650,7 @@ ${where.join("\n")}`);
|
|
|
26304
26650
|
};
|
|
26305
26651
|
const droppedRoutes = [];
|
|
26306
26652
|
try {
|
|
26307
|
-
const rawCfg = parseYaml2(
|
|
26653
|
+
const rawCfg = parseYaml2(readFileSync40(cfg.configFile, "utf8"));
|
|
26308
26654
|
for (const r of rawCfg?.routes ?? []) {
|
|
26309
26655
|
if (typeof r?.route !== "string" || cfg.routes.some((x) => x.route === r.route)) continue;
|
|
26310
26656
|
try {
|
|
@@ -26357,8 +26703,8 @@ ${where.join("\n")}`);
|
|
|
26357
26703
|
process.stdout.write("crew reload: no daemon unit is running \u2014 nothing to reload (the next run reads crew.yaml fresh)\n");
|
|
26358
26704
|
break;
|
|
26359
26705
|
}
|
|
26360
|
-
|
|
26361
|
-
|
|
26706
|
+
mkdirSync30(cfg.ship.stateDir, { recursive: true });
|
|
26707
|
+
writeFileSync33(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
|
|
26362
26708
|
`);
|
|
26363
26709
|
process.stdout.write(
|
|
26364
26710
|
"crew reload: requested \u2014 the daemon relaunches with the new config once its running agents end (it starts no new work meanwhile); if it is busy, `crew reload --now` forces the relaunch and interrupts them\n"
|
|
@@ -26411,7 +26757,7 @@ ${where.join("\n")}`);
|
|
|
26411
26757
|
}
|
|
26412
26758
|
case "log": {
|
|
26413
26759
|
try {
|
|
26414
|
-
const text2 =
|
|
26760
|
+
const text2 = readFileSync40(cfg.ship.logFile, "utf8").split("\n");
|
|
26415
26761
|
process.stdout.write(`${text2.slice(-(Number(value("tail")) || 40)).join("\n")}
|
|
26416
26762
|
`);
|
|
26417
26763
|
} catch {
|
|
@@ -26434,7 +26780,7 @@ ${where.join("\n")}`);
|
|
|
26434
26780
|
};
|
|
26435
26781
|
if (!route2.resolved) fail(`route ${route2.route} is not connected \u2014 run \`crew connect\` first`);
|
|
26436
26782
|
const tracker = new Tracker(route2, cfg.ship);
|
|
26437
|
-
const configText =
|
|
26783
|
+
const configText = readFileSync40(cfg.configFile, "utf8");
|
|
26438
26784
|
const cfgBase = dirname25(cfg.configFile);
|
|
26439
26785
|
const configured2 = configuredRepos(configText, route2.route, cfgBase);
|
|
26440
26786
|
const dir = configured2.find((r) => r.name === SAMPLE_REPO)?.dir ?? sampleDir(configured2.map((r) => r.dir), route2.reposBasePath, route2.route.split("/")[0]);
|
|
@@ -26463,7 +26809,7 @@ ${where.join("\n")}`);
|
|
|
26463
26809
|
}
|
|
26464
26810
|
if (configured2.some((r) => r.dir === dir)) {
|
|
26465
26811
|
try {
|
|
26466
|
-
|
|
26812
|
+
writeFileSync33(cfg.configFile, planRepoRemove(configText, cfgBase, route2.route, dir).text);
|
|
26467
26813
|
} catch (e) {
|
|
26468
26814
|
if (e instanceof ReposError) fail(e.message);
|
|
26469
26815
|
throw e;
|
|
@@ -26472,28 +26818,28 @@ ${where.join("\n")}`);
|
|
|
26472
26818
|
if (row) {
|
|
26473
26819
|
await tracker.deleteRepoRow(row.id);
|
|
26474
26820
|
if (existsSync45(resolvedFile)) {
|
|
26475
|
-
const raw = JSON.parse(
|
|
26821
|
+
const raw = JSON.parse(readFileSync40(resolvedFile, "utf8"));
|
|
26476
26822
|
if (raw.repoNames) delete raw.repoNames[row.id];
|
|
26477
26823
|
if (raw.repoRemotes) delete raw.repoRemotes[row.id];
|
|
26478
|
-
|
|
26824
|
+
writeFileSync33(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
26479
26825
|
`);
|
|
26480
26826
|
}
|
|
26481
26827
|
}
|
|
26482
|
-
if (existsSync45(
|
|
26828
|
+
if (existsSync45(join49(dir, ".git")) && basename15(dir) === SAMPLE_REPO) rmSync23(dir, { recursive: true, force: true });
|
|
26483
26829
|
say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
|
|
26484
26830
|
break;
|
|
26485
26831
|
}
|
|
26486
26832
|
const projectId = route2.resolved.areaId ?? resolveRepoProject(await tracker.projectRows(), value("project"));
|
|
26487
|
-
const fresh = !existsSync45(
|
|
26833
|
+
const fresh = !existsSync45(join49(dir, ".git"));
|
|
26488
26834
|
if (existsSync45(dir) && fresh && readdirSync20(dir).length > 0) fail(`${dir} exists and is not a git checkout \u2014 move it aside`);
|
|
26489
26835
|
if (dryRun) {
|
|
26490
26836
|
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`);
|
|
26491
26837
|
break;
|
|
26492
26838
|
}
|
|
26493
26839
|
if (fresh) {
|
|
26494
|
-
|
|
26495
|
-
|
|
26496
|
-
|
|
26840
|
+
mkdirSync30(dir, { recursive: true });
|
|
26841
|
+
writeFileSync33(join49(dir, "README.md"), SAMPLE_README);
|
|
26842
|
+
writeFileSync33(join49(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
|
|
26497
26843
|
const g = (...a) => {
|
|
26498
26844
|
const r = spawnSync10("git", a, { cwd: dir, encoding: "utf8" });
|
|
26499
26845
|
if (r.status !== 0) fail(`git ${a[0]} failed in ${dir}: ${(r.stderr || r.stdout).trim()}`);
|
|
@@ -26512,16 +26858,16 @@ ${where.join("\n")}`);
|
|
|
26512
26858
|
});
|
|
26513
26859
|
repoId = (await tracker.createRepoRow(payload)).id;
|
|
26514
26860
|
if (existsSync45(resolvedFile)) {
|
|
26515
|
-
const raw = JSON.parse(
|
|
26861
|
+
const raw = JSON.parse(readFileSync40(resolvedFile, "utf8"));
|
|
26516
26862
|
raw.repoNames = { ...raw.repoNames, [repoId]: SAMPLE_REPO };
|
|
26517
26863
|
raw.repoRemotes = { ...raw.repoRemotes, [repoId]: SAMPLE_REMOTE };
|
|
26518
|
-
|
|
26864
|
+
writeFileSync33(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
26519
26865
|
`);
|
|
26520
26866
|
}
|
|
26521
26867
|
}
|
|
26522
26868
|
if (!configured2.some((r) => r.dir === dir)) {
|
|
26523
26869
|
try {
|
|
26524
|
-
|
|
26870
|
+
writeFileSync33(cfg.configFile, planRepoAdd({ text: configText, base: cfgBase, route: route2.route, path: dir, name: SAMPLE_REPO }).text);
|
|
26525
26871
|
} catch (e) {
|
|
26526
26872
|
if (e instanceof ReposError) fail(e.message);
|
|
26527
26873
|
throw e;
|
|
@@ -26964,7 +27310,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
26964
27310
|
const unitPlan = planInstall(cfg.ship, CREW_HOME, host, detectSystemd(), "run", installContext());
|
|
26965
27311
|
const abandons = unitAbandonsAgents(unitPlan, (p) => {
|
|
26966
27312
|
try {
|
|
26967
|
-
return
|
|
27313
|
+
return readFileSync40(p, "utf8");
|
|
26968
27314
|
} catch {
|
|
26969
27315
|
return null;
|
|
26970
27316
|
}
|
|
@@ -27386,13 +27732,13 @@ ${foreign.join("\n")}
|
|
|
27386
27732
|
if (cfg.ship.relayHost) {
|
|
27387
27733
|
for (const r of passengerRoutes) {
|
|
27388
27734
|
const ws = r.resolved?.workspaceId;
|
|
27389
|
-
const
|
|
27735
|
+
const slug2 = ws ? readPersistedTunnel(cfg.ship.stateDir, ws)?.slug : void 0;
|
|
27390
27736
|
if (!ws) continue;
|
|
27391
27737
|
process.stdout.write(
|
|
27392
|
-
` ${r.route} url: ${
|
|
27738
|
+
` ${r.route} url: ${slug2 ? publicUrlFor(slug2, cfg.ship.relayHost, cfg.ship.relayHttpPort, cfg.ship.relayPublicDomain) : "not minted yet (first tunnel cycle creates it)"}
|
|
27393
27739
|
`
|
|
27394
27740
|
);
|
|
27395
|
-
const controlUrl =
|
|
27741
|
+
const controlUrl = slug2 && readPersistedTunnel(cfg.ship.stateDir, ws)?.controlSocket ? `${relayBaseUrlFor(slug2, cfg.ship.relayHost, cfg.ship.relayHttpPort, cfg.ship.relayPublicDomain)}/control` : void 0;
|
|
27396
27742
|
process.stdout.write(` ${r.route} control url: ${controlUrl ?? "none (the tunnel carries passengers only)"}
|
|
27397
27743
|
`);
|
|
27398
27744
|
const api = endpointApiFor([r])(ws);
|
|
@@ -27411,6 +27757,15 @@ ${foreign.join("\n")}
|
|
|
27411
27757
|
);
|
|
27412
27758
|
}
|
|
27413
27759
|
}
|
|
27760
|
+
{
|
|
27761
|
+
const declaredS = await sandboxReposOf(cfg.routes.filter((r) => r.enabled));
|
|
27762
|
+
const stateS = readSandboxState(cfg.ship.stateDir);
|
|
27763
|
+
for (const r of declaredS) {
|
|
27764
|
+
const st = sandboxImageStatus(stateS, planSandbox(r.name, r.decl));
|
|
27765
|
+
process.stdout.write(`sandbox image: ${r.name}: ${st.status.toUpperCase()} \u2014 ${st.detail}
|
|
27766
|
+
`);
|
|
27767
|
+
}
|
|
27768
|
+
}
|
|
27414
27769
|
for (const e of listPeriscopeEndpoints(cfg.ship.stateDir)) {
|
|
27415
27770
|
const secret = readPeriscopeSecret(cfg.ship.stateDir, e.ticketRecordId) ? "live" : "ended, awaiting removal";
|
|
27416
27771
|
process.stdout.write(`periscope mcp: ${e.ticket} ${e.route} ${e.url} (${secret}, attached only)
|
|
@@ -27469,3 +27824,4 @@ var endpointReconcileDeps2;
|
|
|
27469
27824
|
var releaseRefused2;
|
|
27470
27825
|
var doctorMachineId2;
|
|
27471
27826
|
var baseHealth2;
|
|
27827
|
+
var sandboxTickDeps2;
|