@tablation/crew 0.126.6 → 0.126.8
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 +243 -122
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -12004,6 +12004,21 @@ function slugFor(route2, agentId) {
|
|
|
12004
12004
|
const [workspaceSlug] = splitRoute(route2.route);
|
|
12005
12005
|
return seatKeySlug(workspaceSlug, agentId);
|
|
12006
12006
|
}
|
|
12007
|
+
function withFoundSeats(route2, found, previousReleaseAgentId) {
|
|
12008
|
+
const resolved = route2.resolved;
|
|
12009
|
+
if (!resolved) return route2;
|
|
12010
|
+
const releaseSeat2 = found.releaseSeat ?? resolved.releaseSeat;
|
|
12011
|
+
const releaseAgentId = resolved.releaseAgentId ?? previousReleaseAgentId;
|
|
12012
|
+
return {
|
|
12013
|
+
...route2,
|
|
12014
|
+
resolved: {
|
|
12015
|
+
...resolved,
|
|
12016
|
+
seats: found.seats,
|
|
12017
|
+
...releaseSeat2 ? { releaseSeat: releaseSeat2 } : {},
|
|
12018
|
+
...releaseAgentId ? { releaseAgentId } : {}
|
|
12019
|
+
}
|
|
12020
|
+
};
|
|
12021
|
+
}
|
|
12007
12022
|
function seatAgentId(route2, role) {
|
|
12008
12023
|
return route2.resolved?.agentPersonas?.[role]?.agentId;
|
|
12009
12024
|
}
|
|
@@ -12496,6 +12511,15 @@ printf '%s
|
|
|
12496
12511
|
' "$tip" > "$out/$name.tip"
|
|
12497
12512
|
echo "updated $tip"
|
|
12498
12513
|
`;
|
|
12514
|
+
var SHOW_SCRIPT = `
|
|
12515
|
+
set -e
|
|
12516
|
+
export GIT_TERMINAL_PROMPT=0
|
|
12517
|
+
m=${VOLUME_ROOT}/"$1"; base="$2"; path="$3"
|
|
12518
|
+
git config --global --add safe.directory '*'
|
|
12519
|
+
git -C "$m" rev-parse --verify --quiet "refs/heads/$base^{commit}" >/dev/null || { echo "the mirror has no branch $base" >&2; exit 3; }
|
|
12520
|
+
git -C "$m" cat-file -e "refs/heads/$base:$path" 2>/dev/null || exit 4
|
|
12521
|
+
git -C "$m" show "refs/heads/$base:$path"
|
|
12522
|
+
`;
|
|
12499
12523
|
var SandboxCache = class {
|
|
12500
12524
|
s;
|
|
12501
12525
|
exec;
|
|
@@ -12582,6 +12606,20 @@ var SandboxCache = class {
|
|
|
12582
12606
|
if (!m) throw new Error(`unexpected output materializing ${repo.host}/${repo.path}: ${stdout.trim().slice(0, 200)}`);
|
|
12583
12607
|
return { tip: m[2], changed: m[1] === "updated" };
|
|
12584
12608
|
}
|
|
12609
|
+
/**
|
|
12610
|
+
* The text of `path` at the tip of `base` in the repository's mirror, or
|
|
12611
|
+
* null when the mirror has no such file. Throws when the mirror or the
|
|
12612
|
+
* branch is missing, so the caller can tell "no contract" from "no mirror".
|
|
12613
|
+
*/
|
|
12614
|
+
async showFile(repo, base, path) {
|
|
12615
|
+
try {
|
|
12616
|
+
const { stdout } = await this.helper(this.s.helperImage, SHOW_SCRIPT, [mirrorRelPath(repo), base, path]);
|
|
12617
|
+
return stdout;
|
|
12618
|
+
} catch (e) {
|
|
12619
|
+
if (e.code === 4) return null;
|
|
12620
|
+
throw e;
|
|
12621
|
+
}
|
|
12622
|
+
}
|
|
12585
12623
|
/** Mirrors in the volume with their last refresh; empty when the volume does not exist. */
|
|
12586
12624
|
async list() {
|
|
12587
12625
|
try {
|
|
@@ -14055,7 +14093,7 @@ async function runPoolMember(deps) {
|
|
|
14055
14093
|
await deps.sleep(backoffMs(failures++));
|
|
14056
14094
|
}
|
|
14057
14095
|
};
|
|
14058
|
-
let
|
|
14096
|
+
let reported2;
|
|
14059
14097
|
let busy = false;
|
|
14060
14098
|
const report = async (state, o = {}) => {
|
|
14061
14099
|
const body = {
|
|
@@ -14078,7 +14116,7 @@ async function runPoolMember(deps) {
|
|
|
14078
14116
|
if (r.status === 401) return "credential";
|
|
14079
14117
|
if (r.status === 409) deps.log(`status ${state}: the platform refused the transition (409); not retrying`);
|
|
14080
14118
|
else if (r.status >= 400) deps.log(`status ${state}: platform answered ${r.status}; not retrying`);
|
|
14081
|
-
|
|
14119
|
+
reported2 = body;
|
|
14082
14120
|
deps.store.write({ ...deps.store.read(), lastState: state });
|
|
14083
14121
|
return "ok";
|
|
14084
14122
|
};
|
|
@@ -14161,7 +14199,7 @@ async function runPoolMember(deps) {
|
|
|
14161
14199
|
if (r === "stopped") return POOL_EXIT.ok;
|
|
14162
14200
|
}
|
|
14163
14201
|
}
|
|
14164
|
-
let relayReported =
|
|
14202
|
+
let relayReported = reported2?.state === "running" ? reported2.relayUrl : null;
|
|
14165
14203
|
let reregister = false;
|
|
14166
14204
|
while (!reregister) {
|
|
14167
14205
|
if (deps.stopping()) return POOL_EXIT.ok;
|
|
@@ -15184,13 +15222,30 @@ function isPidAlive(pid) {
|
|
|
15184
15222
|
}
|
|
15185
15223
|
var CONTROL_ONLY_HASH = "control-only";
|
|
15186
15224
|
var TUNNEL_REJECTED_AFTER = 2;
|
|
15225
|
+
var CONTROL_RETRY_MS = 60 * 60 * 1e3;
|
|
15226
|
+
var RELAY_CONTROL_UNSUPPORTED_KEY = "relay_control_unsupported";
|
|
15227
|
+
function relayControlUnsupportedMessage(workspaceId) {
|
|
15228
|
+
return `the relay refused the control-path forward for workspace ${workspaceId}, so the passengers tunnel runs without the control API (remote ship transport and periscopes will not reach this ship); the relay is too old for \`-path-control\` (crew-relay 0.2.0+) \u2014 deploy it with scripts/deploy-relay.sh`;
|
|
15229
|
+
}
|
|
15230
|
+
function controlRefusedTunnels(stateDir, now = Date.now()) {
|
|
15231
|
+
const out = [];
|
|
15232
|
+
for (const workspaceId of readAllPersistedWorkspaceIds(stateDir)) {
|
|
15233
|
+
const at = readPersistedTunnel(stateDir, workspaceId)?.controlRefusedAt;
|
|
15234
|
+
if (at && now - Date.parse(at) < CONTROL_RETRY_MS) out.push(workspaceId);
|
|
15235
|
+
}
|
|
15236
|
+
return out;
|
|
15237
|
+
}
|
|
15187
15238
|
function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, stateDir, mcpSecrets = /* @__PURE__ */ new Map(), deps = {}) {
|
|
15188
15239
|
const alive = deps.isPidAlive ?? isPidAlive;
|
|
15189
15240
|
const plannedIds = new Set(plans.map((p) => p.workspaceId));
|
|
15190
15241
|
for (const plan of plans) {
|
|
15191
15242
|
const existing = readPersistedTunnel(stateDir, plan.workspaceId);
|
|
15192
15243
|
const mountsHash = plan.mountsHash ?? CONTROL_ONLY_HASH;
|
|
15193
|
-
const
|
|
15244
|
+
const refusedAt = existing?.controlRefusedAt;
|
|
15245
|
+
const refusalFresh = refusedAt !== void 0 && Date.now() - Date.parse(refusedAt) < CONTROL_RETRY_MS;
|
|
15246
|
+
let controlSocket = deps.controlSocket && refusalFresh ? void 0 : deps.controlSocket;
|
|
15247
|
+
let controlRefusedAt = refusalFresh ? refusedAt : void 0;
|
|
15248
|
+
const controlUnchanged = existing?.controlSocket === controlSocket;
|
|
15194
15249
|
const currentSecret = mcpSecrets.get(plan.workspaceId);
|
|
15195
15250
|
const secretUnchanged = (existing?.mcpSecret ?? void 0) === (currentSecret ?? void 0);
|
|
15196
15251
|
const slug2 = existing?.slug ?? generateTunnelSlug();
|
|
@@ -15215,7 +15270,12 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
|
|
|
15215
15270
|
continue;
|
|
15216
15271
|
}
|
|
15217
15272
|
const diedQuickly = existing !== void 0 && existing.status === "connecting" && !alive(existing.pid) && controlUnchanged && secretUnchanged && slugUnchanged && existing.mountsHash === mountsHash;
|
|
15218
|
-
|
|
15273
|
+
let quickExits = diedQuickly ? (existing.quickExits ?? 0) + 1 : 0;
|
|
15274
|
+
if (diedQuickly && controlSocket && plan.port !== void 0) {
|
|
15275
|
+
controlSocket = void 0;
|
|
15276
|
+
controlRefusedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
15277
|
+
quickExits = 0;
|
|
15278
|
+
}
|
|
15219
15279
|
let settledStatus = "connecting";
|
|
15220
15280
|
const tunnel = startTunnel({
|
|
15221
15281
|
relayHost,
|
|
@@ -15224,7 +15284,7 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
|
|
|
15224
15284
|
slug: slug2,
|
|
15225
15285
|
apiKey,
|
|
15226
15286
|
localPort: plan.port,
|
|
15227
|
-
controlSocket
|
|
15287
|
+
controlSocket,
|
|
15228
15288
|
privateKeyPath,
|
|
15229
15289
|
mcpSecret: currentSecret,
|
|
15230
15290
|
spawnFn: deps.spawnFn,
|
|
@@ -15237,7 +15297,8 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
|
|
|
15237
15297
|
containerName: plan.containerName ?? "",
|
|
15238
15298
|
workspaceId: plan.workspaceId,
|
|
15239
15299
|
mountsHash,
|
|
15240
|
-
controlSocket
|
|
15300
|
+
controlSocket,
|
|
15301
|
+
controlRefusedAt,
|
|
15241
15302
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
15242
15303
|
status: settledStatus,
|
|
15243
15304
|
mcpSecret: currentSecret,
|
|
@@ -19076,6 +19137,73 @@ function runForge(a, io) {
|
|
|
19076
19137
|
`, err: "" };
|
|
19077
19138
|
}
|
|
19078
19139
|
|
|
19140
|
+
// src/mirror-contract.ts
|
|
19141
|
+
import { existsSync as existsSync40 } from "fs";
|
|
19142
|
+
|
|
19143
|
+
// src/passenger-mirror.ts
|
|
19144
|
+
import { mkdirSync as mkdirSync29 } from "fs";
|
|
19145
|
+
import { dirname as dirname25 } from "path";
|
|
19146
|
+
function mirrorRepoOf(spec) {
|
|
19147
|
+
const fromOrigin = spec.originUrl ? parseRemote(spec.originUrl) : null;
|
|
19148
|
+
if (fromOrigin) return fromOrigin;
|
|
19149
|
+
if (!spec.remote) return null;
|
|
19150
|
+
return { host: normalizeForgeHost(spec.forgeHost) || DEFAULT_FORGE_HOST, path: spec.remote };
|
|
19151
|
+
}
|
|
19152
|
+
async function materializeMirrorCheckouts(specs, cacheFor = (s) => new SandboxCache(s.mirror.cache)) {
|
|
19153
|
+
const out = [];
|
|
19154
|
+
for (const spec of specs) {
|
|
19155
|
+
if (!spec.mirror) continue;
|
|
19156
|
+
const base = { name: spec.name, workspaceId: spec.workspaceId };
|
|
19157
|
+
const repo = mirrorRepoOf(spec);
|
|
19158
|
+
if (!repo) {
|
|
19159
|
+
out.push({ ...base, action: "failed", detail: `no remote known for ${spec.name}, so its mirror cannot be found` });
|
|
19160
|
+
continue;
|
|
19161
|
+
}
|
|
19162
|
+
try {
|
|
19163
|
+
const parent = dirname25(spec.dir);
|
|
19164
|
+
mkdirSync29(parent, { recursive: true });
|
|
19165
|
+
mkdirSync29(spec.dir, { recursive: true });
|
|
19166
|
+
const r = await cacheFor(spec).materialize(repo, spec.base, parent, spec.name);
|
|
19167
|
+
out.push({ ...base, action: r.changed ? "updated" : "unchanged", detail: r.tip });
|
|
19168
|
+
} catch (e) {
|
|
19169
|
+
const err3 = e;
|
|
19170
|
+
out.push({ ...base, action: "failed", detail: (err3.stderr?.trim() || err3.message || String(e)).split("\n").slice(-3).join(" / ") });
|
|
19171
|
+
}
|
|
19172
|
+
}
|
|
19173
|
+
return out;
|
|
19174
|
+
}
|
|
19175
|
+
function mirrorOutcome(m) {
|
|
19176
|
+
if (m.action === "failed") return { action: "clone-failed", detail: m.detail, repeat: false };
|
|
19177
|
+
return { action: "cloned", detail: `${m.action} ${m.detail}` };
|
|
19178
|
+
}
|
|
19179
|
+
|
|
19180
|
+
// src/mirror-contract.ts
|
|
19181
|
+
function lacksCheckout(ship, dir) {
|
|
19182
|
+
return ship.runner === "sandbox" && !existsSync40(dir);
|
|
19183
|
+
}
|
|
19184
|
+
function mirrorRepoOfTarget(t) {
|
|
19185
|
+
return mirrorRepoOf({ remote: t.remote, forgeHost: t.forgeHost });
|
|
19186
|
+
}
|
|
19187
|
+
async function readMirrorContract(cache, repo, base) {
|
|
19188
|
+
for (const name of REPO_CONFIG_FILENAMES) {
|
|
19189
|
+
const text2 = await cache.showFile(repo, base, name);
|
|
19190
|
+
if (text2 !== null) return parseRepoConfig(text2, `${repo.host}/${repo.path}:${base}:${name}`);
|
|
19191
|
+
}
|
|
19192
|
+
return null;
|
|
19193
|
+
}
|
|
19194
|
+
function noCheckoutReleaseNote(repo, mode) {
|
|
19195
|
+
if (mode === "local" || mode === null) {
|
|
19196
|
+
return `${repo}: no checkout on this ship; release.mode "local" repositories are released by the host that owns the deploy hook, which takes a ticket at verified`;
|
|
19197
|
+
}
|
|
19198
|
+
return `${repo}: no checkout on this ship; release.mode "${mode}" needs the sandboxed release over a mirror-fed clone, which is not built (CREW-2940)`;
|
|
19199
|
+
}
|
|
19200
|
+
var reported = /* @__PURE__ */ new Set();
|
|
19201
|
+
function firstReport(key) {
|
|
19202
|
+
if (reported.has(key)) return false;
|
|
19203
|
+
reported.add(key);
|
|
19204
|
+
return true;
|
|
19205
|
+
}
|
|
19206
|
+
|
|
19079
19207
|
// src/whoami.ts
|
|
19080
19208
|
async function buildWhoami(routes, deps = {}) {
|
|
19081
19209
|
const doFetch = deps.fetch ?? fetch;
|
|
@@ -19247,7 +19375,7 @@ function missingReleaserWarning(repo, release, ships, now = Date.now()) {
|
|
|
19247
19375
|
}
|
|
19248
19376
|
|
|
19249
19377
|
// src/watch.ts
|
|
19250
|
-
import { watch as watchFile, existsSync as
|
|
19378
|
+
import { watch as watchFile, existsSync as existsSync41, statSync as statSync11, openSync as openSync5, readSync as readSync2, closeSync as closeSync5 } from "fs";
|
|
19251
19379
|
var E = "\x1B[";
|
|
19252
19380
|
var dim = (s) => `${E}2m${s}${E}0m`;
|
|
19253
19381
|
var bold = (s) => `${E}1m${s}${E}0m`;
|
|
@@ -19348,7 +19476,7 @@ function renderHeaders(states, now = Date.now()) {
|
|
|
19348
19476
|
return states.map((s) => renderHeader(s, now)).join("");
|
|
19349
19477
|
}
|
|
19350
19478
|
function readFrom(file, from) {
|
|
19351
|
-
if (!
|
|
19479
|
+
if (!existsSync41(file)) return { events: [], offset: from };
|
|
19352
19480
|
const size = statSync11(file).size;
|
|
19353
19481
|
if (size < from) return readFrom(file, 0);
|
|
19354
19482
|
if (size === from) return { events: [], offset: from };
|
|
@@ -19591,8 +19719,8 @@ function nextRoles(decision, ran) {
|
|
|
19591
19719
|
}
|
|
19592
19720
|
|
|
19593
19721
|
// src/ship-name.ts
|
|
19594
|
-
import { existsSync as
|
|
19595
|
-
import { dirname as
|
|
19722
|
+
import { existsSync as existsSync42, mkdirSync as mkdirSync30, readFileSync as readFileSync37, renameSync as renameSync5, writeFileSync as writeFileSync32 } from "fs";
|
|
19723
|
+
import { dirname as dirname26 } from "path";
|
|
19596
19724
|
import { parseDocument as parseDocument5 } from "yaml";
|
|
19597
19725
|
var ShipNameError = class extends Error {
|
|
19598
19726
|
};
|
|
@@ -19653,7 +19781,7 @@ async function syncRouteName(i) {
|
|
|
19653
19781
|
return { status: "ok", ...base, action, name: i.local };
|
|
19654
19782
|
}
|
|
19655
19783
|
function readSyncedName(resolvedFile) {
|
|
19656
|
-
if (!
|
|
19784
|
+
if (!existsSync42(resolvedFile)) return void 0;
|
|
19657
19785
|
try {
|
|
19658
19786
|
const v = JSON.parse(readFileSync37(resolvedFile, "utf8")).shipName;
|
|
19659
19787
|
return typeof v === "string" ? v : void 0;
|
|
@@ -19662,7 +19790,7 @@ function readSyncedName(resolvedFile) {
|
|
|
19662
19790
|
}
|
|
19663
19791
|
}
|
|
19664
19792
|
function writeSyncedName(resolvedFile, name) {
|
|
19665
|
-
if (!
|
|
19793
|
+
if (!existsSync42(resolvedFile)) return;
|
|
19666
19794
|
let raw;
|
|
19667
19795
|
try {
|
|
19668
19796
|
raw = JSON.parse(readFileSync37(resolvedFile, "utf8"));
|
|
@@ -19671,7 +19799,7 @@ function writeSyncedName(resolvedFile, name) {
|
|
|
19671
19799
|
}
|
|
19672
19800
|
if (raw.shipName === name) return;
|
|
19673
19801
|
raw.shipName = name;
|
|
19674
|
-
|
|
19802
|
+
mkdirSync30(dirname26(resolvedFile), { recursive: true });
|
|
19675
19803
|
const tmp = `${resolvedFile}.tmp${process.pid}`;
|
|
19676
19804
|
writeFileSync32(tmp, `${JSON.stringify(raw, null, 2)}
|
|
19677
19805
|
`);
|
|
@@ -20364,8 +20492,8 @@ function describeCapability(i) {
|
|
|
20364
20492
|
}
|
|
20365
20493
|
|
|
20366
20494
|
// src/agents-sync-run.ts
|
|
20367
|
-
import { existsSync as
|
|
20368
|
-
import { dirname as
|
|
20495
|
+
import { existsSync as existsSync43, mkdirSync as mkdirSync31, readFileSync as readFileSync38, writeFileSync as writeFileSync33 } from "fs";
|
|
20496
|
+
import { dirname as dirname27 } from "path";
|
|
20369
20497
|
async function runAgentsSync(target, o) {
|
|
20370
20498
|
const dryRun = o.dryRun ?? false;
|
|
20371
20499
|
const force = o.force ?? false;
|
|
@@ -20398,11 +20526,11 @@ async function runAgentsSync(target, o) {
|
|
|
20398
20526
|
for (const s of skillResult?.outcomes ?? []) o.say(describeSkillSyncOutcome(s));
|
|
20399
20527
|
if (!dryRun) {
|
|
20400
20528
|
const resolvedPath = resolvedPathFor(o.stateDir, target.route);
|
|
20401
|
-
const raw =
|
|
20529
|
+
const raw = existsSync43(resolvedPath) ? JSON.parse(readFileSync38(resolvedPath, "utf8")) : {};
|
|
20402
20530
|
raw.agentPersonas = personaResult.agentPersonas;
|
|
20403
20531
|
if (skillResult) raw.agentSkills = skillResult.agentSkills;
|
|
20404
20532
|
if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
|
|
20405
|
-
|
|
20533
|
+
mkdirSync31(dirname27(resolvedPath), { recursive: true });
|
|
20406
20534
|
writeFileSync33(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
20407
20535
|
`);
|
|
20408
20536
|
if (target.resolved) {
|
|
@@ -21025,7 +21153,7 @@ ${run3.err}` });
|
|
|
21025
21153
|
}
|
|
21026
21154
|
|
|
21027
21155
|
// src/sample-ticket.ts
|
|
21028
|
-
import { basename as basename12, dirname as
|
|
21156
|
+
import { basename as basename12, dirname as dirname28, join as join44 } from "path";
|
|
21029
21157
|
var SAMPLE_REPO = "crew-sample";
|
|
21030
21158
|
var SAMPLE_REMOTE = `local/${SAMPLE_REPO}`;
|
|
21031
21159
|
var SAMPLE_MARKER = "sample: true";
|
|
@@ -21048,7 +21176,7 @@ has something safe to work on: its test and build hooks always pass, it has no
|
|
|
21048
21176
|
remote, and \`crew sample-ticket --remove\` deletes it.
|
|
21049
21177
|
`;
|
|
21050
21178
|
function sampleDir(existing, reposBase, workspace) {
|
|
21051
|
-
if (existing.length > 0) return join44(
|
|
21179
|
+
if (existing.length > 0) return join44(dirname28(existing[0]), SAMPLE_REPO);
|
|
21052
21180
|
return join44(reposBase, workspace, SAMPLE_REPO);
|
|
21053
21181
|
}
|
|
21054
21182
|
var SAMPLE_TITLE = "Sample: add a documented `greet` function with a test";
|
|
@@ -21093,8 +21221,8 @@ function planSampleTicket(i) {
|
|
|
21093
21221
|
}
|
|
21094
21222
|
|
|
21095
21223
|
// src/worktree-sweep.ts
|
|
21096
|
-
import { existsSync as
|
|
21097
|
-
import { basename as basename13, dirname as
|
|
21224
|
+
import { existsSync as existsSync44, readFileSync as readFileSync40, readdirSync as readdirSync14 } from "fs";
|
|
21225
|
+
import { basename as basename13, dirname as dirname29, join as join45, resolve as resolve5 } from "path";
|
|
21098
21226
|
function planWorktreeSweep(target, tickets, contract) {
|
|
21099
21227
|
const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
|
|
21100
21228
|
const verified = contract.statuses.verified;
|
|
@@ -21158,7 +21286,7 @@ function worktreeTicketNumbers(target) {
|
|
|
21158
21286
|
return [...nums];
|
|
21159
21287
|
}
|
|
21160
21288
|
function siblingWorktreeDirs(target) {
|
|
21161
|
-
const parent =
|
|
21289
|
+
const parent = dirname29(target.dir.replace(/[/\\]+$/, ""));
|
|
21162
21290
|
const stem = `${basename13(target.dir.replace(/[/\\]+$/, ""))}-`;
|
|
21163
21291
|
const registered = new Set(worktrees(target.dir).map((w) => resolve5(w.path)));
|
|
21164
21292
|
let names;
|
|
@@ -21206,7 +21334,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
|
|
|
21206
21334
|
let removed = 0;
|
|
21207
21335
|
let keptBranches = 0;
|
|
21208
21336
|
for (const a of actions) {
|
|
21209
|
-
if (!dryRun && base && !a.keepBranch &&
|
|
21337
|
+
if (!dryRun && base && !a.keepBranch && existsSync44(a.path) && isHandBackMerge(a.path, base)) {
|
|
21210
21338
|
if (gitOk(a.path, ["merge", "--abort"]) !== null) {
|
|
21211
21339
|
log.emit(
|
|
21212
21340
|
`abandoned the hand-back merge of ${base} in ${a.path}: ${a.ticket.issue_id} deployed without it`,
|
|
@@ -21214,7 +21342,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
|
|
|
21214
21342
|
);
|
|
21215
21343
|
}
|
|
21216
21344
|
}
|
|
21217
|
-
if (!dryRun &&
|
|
21345
|
+
if (!dryRun && existsSync44(a.path)) {
|
|
21218
21346
|
let dirty = false;
|
|
21219
21347
|
try {
|
|
21220
21348
|
dirty = status(a.path).length > 0;
|
|
@@ -21225,7 +21353,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
|
|
|
21225
21353
|
continue;
|
|
21226
21354
|
}
|
|
21227
21355
|
}
|
|
21228
|
-
if (!dryRun && a.requirePushed && a.branch &&
|
|
21356
|
+
if (!dryRun && a.requirePushed && a.branch && existsSync44(a.path) && remoteConfigured(cwd, remote) && worktreeHoldsUnpushed(a.path, remote)) {
|
|
21229
21357
|
refuse2(a, "unpushed commits", `${a.branch} has commits that are not on ${remote}`);
|
|
21230
21358
|
continue;
|
|
21231
21359
|
}
|
|
@@ -21379,12 +21507,12 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
|
|
|
21379
21507
|
}
|
|
21380
21508
|
|
|
21381
21509
|
// src/stream-sweep.ts
|
|
21382
|
-
import { existsSync as
|
|
21510
|
+
import { existsSync as existsSync45, readdirSync as readdirSync15, rmSync as rmSync20, statSync as statSync12 } from "fs";
|
|
21383
21511
|
import { join as join46 } from "path";
|
|
21384
21512
|
var DAY_MS2 = 24 * 60 * 60 * 1e3;
|
|
21385
21513
|
function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
|
|
21386
21514
|
const dir = join46(stateDir, "streams");
|
|
21387
|
-
if (!
|
|
21515
|
+
if (!existsSync45(dir)) return [];
|
|
21388
21516
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
21389
21517
|
return readdirSync15(dir).map((name) => join46(dir, name)).filter((path) => {
|
|
21390
21518
|
try {
|
|
@@ -21412,7 +21540,7 @@ function applyStreamSweep(paths, dryRun, log) {
|
|
|
21412
21540
|
return removed;
|
|
21413
21541
|
}
|
|
21414
21542
|
function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
21415
|
-
if (!
|
|
21543
|
+
if (!existsSync45(stateDir)) return [];
|
|
21416
21544
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
21417
21545
|
return readdirSync15(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join46(stateDir, name)).filter((path) => {
|
|
21418
21546
|
try {
|
|
@@ -21424,11 +21552,11 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
|
21424
21552
|
}
|
|
21425
21553
|
|
|
21426
21554
|
// src/cli.ts
|
|
21427
|
-
import { realpathSync as realpathSync3, readFileSync as readFileSync46, existsSync as
|
|
21555
|
+
import { realpathSync as realpathSync3, readFileSync as readFileSync46, existsSync as existsSync52, mkdirSync as mkdirSync38, writeFileSync as writeFileSync38, rmSync as rmSync25, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync6, statSync as statSync16, readdirSync as readdirSync20 } from "fs";
|
|
21428
21556
|
import { createInterface } from "readline/promises";
|
|
21429
21557
|
|
|
21430
21558
|
// src/periscope-cmd.ts
|
|
21431
|
-
import { existsSync as
|
|
21559
|
+
import { existsSync as existsSync46, mkdirSync as mkdirSync32, readFileSync as readFileSync41, rmSync as rmSync21, writeFileSync as writeFileSync34 } from "fs";
|
|
21432
21560
|
import { join as join47 } from "path";
|
|
21433
21561
|
var PeriscopeError = class extends Error {
|
|
21434
21562
|
};
|
|
@@ -21446,7 +21574,7 @@ function removePeriscopePrior(stateDir, ticketRecordId) {
|
|
|
21446
21574
|
}
|
|
21447
21575
|
function writePeriscopePrior(stateDir, ticketRecordId, prior) {
|
|
21448
21576
|
try {
|
|
21449
|
-
|
|
21577
|
+
mkdirSync32(priorDir(stateDir), { recursive: true });
|
|
21450
21578
|
writeFileSync34(priorPath(stateDir, ticketRecordId), JSON.stringify(prior));
|
|
21451
21579
|
} catch {
|
|
21452
21580
|
}
|
|
@@ -21454,7 +21582,7 @@ function writePeriscopePrior(stateDir, ticketRecordId, prior) {
|
|
|
21454
21582
|
function readPeriscopePrior(stateDir, ticketRecordId) {
|
|
21455
21583
|
try {
|
|
21456
21584
|
const p = priorPath(stateDir, ticketRecordId);
|
|
21457
|
-
return
|
|
21585
|
+
return existsSync46(p) ? JSON.parse(readFileSync41(p, "utf8")) : void 0;
|
|
21458
21586
|
} catch {
|
|
21459
21587
|
return void 0;
|
|
21460
21588
|
}
|
|
@@ -22009,8 +22137,8 @@ import { dirname as dirOf2, resolve as resolvePath } from "path";
|
|
|
22009
22137
|
|
|
22010
22138
|
// src/ssh-keys.ts
|
|
22011
22139
|
import { execFileSync as execFileSync13 } from "child_process";
|
|
22012
|
-
import { existsSync as
|
|
22013
|
-
import { dirname as
|
|
22140
|
+
import { existsSync as existsSync47, mkdirSync as mkdirSync33, readFileSync as readFileSync42, chmodSync as chmodSync3 } from "fs";
|
|
22141
|
+
import { dirname as dirname30, join as join49 } from "path";
|
|
22014
22142
|
var SshKeyError = class extends Error {
|
|
22015
22143
|
};
|
|
22016
22144
|
function shipSshKeyPathFor(stateDir) {
|
|
@@ -22019,8 +22147,8 @@ function shipSshKeyPathFor(stateDir) {
|
|
|
22019
22147
|
function ensureShipSshKeypair(stateDir) {
|
|
22020
22148
|
const privateKeyPath = shipSshKeyPathFor(stateDir);
|
|
22021
22149
|
const publicKeyPath = `${privateKeyPath}.pub`;
|
|
22022
|
-
if (!
|
|
22023
|
-
|
|
22150
|
+
if (!existsSync47(privateKeyPath)) {
|
|
22151
|
+
mkdirSync33(dirname30(privateKeyPath), { recursive: true });
|
|
22024
22152
|
try {
|
|
22025
22153
|
execFileSync13("ssh-keygen", [
|
|
22026
22154
|
"-t",
|
|
@@ -22040,7 +22168,7 @@ function ensureShipSshKeypair(stateDir) {
|
|
|
22040
22168
|
}
|
|
22041
22169
|
chmodSync3(privateKeyPath, 384);
|
|
22042
22170
|
}
|
|
22043
|
-
if (!
|
|
22171
|
+
if (!existsSync47(publicKeyPath)) {
|
|
22044
22172
|
throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
|
|
22045
22173
|
}
|
|
22046
22174
|
const publicKey = readFileSync42(publicKeyPath, "utf8").trim();
|
|
@@ -22207,46 +22335,9 @@ async function unregisterEndpoint(workspaceId, info, d) {
|
|
|
22207
22335
|
import { createServer as createHttpServer } from "http";
|
|
22208
22336
|
import { spawn as spawn9 } from "child_process";
|
|
22209
22337
|
import { randomBytes } from "crypto";
|
|
22210
|
-
import { existsSync as
|
|
22338
|
+
import { existsSync as existsSync48, mkdirSync as mkdirSync34, readFileSync as readFileSync43, writeFileSync as writeFileSync35, unlinkSync as unlinkSync4, readdirSync as readdirSync16 } from "fs";
|
|
22211
22339
|
import { join as join50 } from "path";
|
|
22212
22340
|
|
|
22213
|
-
// src/passenger-mirror.ts
|
|
22214
|
-
import { mkdirSync as mkdirSync33 } from "fs";
|
|
22215
|
-
import { dirname as dirname30 } from "path";
|
|
22216
|
-
function mirrorRepoOf(spec) {
|
|
22217
|
-
const fromOrigin = spec.originUrl ? parseRemote(spec.originUrl) : null;
|
|
22218
|
-
if (fromOrigin) return fromOrigin;
|
|
22219
|
-
if (!spec.remote) return null;
|
|
22220
|
-
return { host: normalizeForgeHost(spec.forgeHost) || DEFAULT_FORGE_HOST, path: spec.remote };
|
|
22221
|
-
}
|
|
22222
|
-
async function materializeMirrorCheckouts(specs, cacheFor = (s) => new SandboxCache(s.mirror.cache)) {
|
|
22223
|
-
const out = [];
|
|
22224
|
-
for (const spec of specs) {
|
|
22225
|
-
if (!spec.mirror) continue;
|
|
22226
|
-
const base = { name: spec.name, workspaceId: spec.workspaceId };
|
|
22227
|
-
const repo = mirrorRepoOf(spec);
|
|
22228
|
-
if (!repo) {
|
|
22229
|
-
out.push({ ...base, action: "failed", detail: `no remote known for ${spec.name}, so its mirror cannot be found` });
|
|
22230
|
-
continue;
|
|
22231
|
-
}
|
|
22232
|
-
try {
|
|
22233
|
-
const parent = dirname30(spec.dir);
|
|
22234
|
-
mkdirSync33(parent, { recursive: true });
|
|
22235
|
-
mkdirSync33(spec.dir, { recursive: true });
|
|
22236
|
-
const r = await cacheFor(spec).materialize(repo, spec.base, parent, spec.name);
|
|
22237
|
-
out.push({ ...base, action: r.changed ? "updated" : "unchanged", detail: r.tip });
|
|
22238
|
-
} catch (e) {
|
|
22239
|
-
const err3 = e;
|
|
22240
|
-
out.push({ ...base, action: "failed", detail: (err3.stderr?.trim() || err3.message || String(e)).split("\n").slice(-3).join(" / ") });
|
|
22241
|
-
}
|
|
22242
|
-
}
|
|
22243
|
-
return out;
|
|
22244
|
-
}
|
|
22245
|
-
function mirrorOutcome(m) {
|
|
22246
|
-
if (m.action === "failed") return { action: "clone-failed", detail: m.detail, repeat: false };
|
|
22247
|
-
return { action: "cloned", detail: `${m.action} ${m.detail}` };
|
|
22248
|
-
}
|
|
22249
|
-
|
|
22250
22341
|
// src/secret-compare.ts
|
|
22251
22342
|
import { timingSafeEqual } from "crypto";
|
|
22252
22343
|
function secretsMatch(a, b) {
|
|
@@ -22342,7 +22433,7 @@ function syncDaemonStatePath(stateDir, workspaceId) {
|
|
|
22342
22433
|
}
|
|
22343
22434
|
function readPersistedSyncDaemon(stateDir, workspaceId) {
|
|
22344
22435
|
const path = syncDaemonStatePath(stateDir, workspaceId);
|
|
22345
|
-
if (!
|
|
22436
|
+
if (!existsSync48(path)) return void 0;
|
|
22346
22437
|
try {
|
|
22347
22438
|
return JSON.parse(readFileSync43(path, "utf8"));
|
|
22348
22439
|
} catch {
|
|
@@ -22373,7 +22464,7 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
|
|
|
22373
22464
|
}
|
|
22374
22465
|
function readAllPersistedSyncWorkspaceIds(stateDir) {
|
|
22375
22466
|
const dir = join50(stateDir, "passengers");
|
|
22376
|
-
if (!
|
|
22467
|
+
if (!existsSync48(dir)) return [];
|
|
22377
22468
|
const suffix = "-sync.json";
|
|
22378
22469
|
return readdirSync16(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
|
|
22379
22470
|
}
|
|
@@ -22893,7 +22984,7 @@ function contractFixers(dir, baseRef, tickets, locate) {
|
|
|
22893
22984
|
}
|
|
22894
22985
|
|
|
22895
22986
|
// src/daemon.ts
|
|
22896
|
-
import { existsSync as
|
|
22987
|
+
import { existsSync as existsSync49, mkdirSync as mkdirSync36, readFileSync as readFileSync44, readdirSync as readdirSync17, rmSync as rmSync22, statSync as statSync14, writeFileSync as writeFileSync36 } from "fs";
|
|
22897
22988
|
import { join as join52 } from "path";
|
|
22898
22989
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
22899
22990
|
var DEFAULT_FLOOR_MS = 5e3;
|
|
@@ -22991,7 +23082,7 @@ function computeTreeSignature(paths) {
|
|
|
22991
23082
|
}
|
|
22992
23083
|
function makeStaleChecker(paths, opts = {}) {
|
|
22993
23084
|
const initial = computeTreeSignature(paths);
|
|
22994
|
-
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 &&
|
|
23085
|
+
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync49(opts.reloadSentinel);
|
|
22995
23086
|
}
|
|
22996
23087
|
function reloadSentinelFor(stateDir) {
|
|
22997
23088
|
return join52(stateDir, "reload-requested");
|
|
@@ -23242,8 +23333,8 @@ async function runOnePassUnguarded(o) {
|
|
|
23242
23333
|
set.add(run3);
|
|
23243
23334
|
void run3.finally(() => set.delete(run3));
|
|
23244
23335
|
}
|
|
23245
|
-
const
|
|
23246
|
-
const tookTicket =
|
|
23336
|
+
const reported2 = TICKET_ROLES.has(role) ? await settled : true;
|
|
23337
|
+
const tookTicket = reported2 !== false;
|
|
23247
23338
|
if (tookTicket) started.push(role);
|
|
23248
23339
|
if (roleCap(role) <= 1) {
|
|
23249
23340
|
live.delete(role);
|
|
@@ -23580,7 +23671,7 @@ import { createHash as createHash10 } from "crypto";
|
|
|
23580
23671
|
import {
|
|
23581
23672
|
chmodSync as chmodSync4,
|
|
23582
23673
|
closeSync as closeSync6,
|
|
23583
|
-
existsSync as
|
|
23674
|
+
existsSync as existsSync50,
|
|
23584
23675
|
fstatSync,
|
|
23585
23676
|
openSync as openSync6,
|
|
23586
23677
|
readSync as readSync3,
|
|
@@ -23951,7 +24042,7 @@ function lastLines2(path, n) {
|
|
|
23951
24042
|
}
|
|
23952
24043
|
function readTail(live, opts) {
|
|
23953
24044
|
const max = opts.maxBytes ?? MAX_TAIL_BYTES;
|
|
23954
|
-
if (!
|
|
24045
|
+
if (!existsSync50(live)) {
|
|
23955
24046
|
return { lines: [], cursor: opts.after ?? encodeCursor({ dev: 0, ino: 0, off: 0 }), reset: false };
|
|
23956
24047
|
}
|
|
23957
24048
|
const cur = opts.after ? decodeCursor(opts.after) : null;
|
|
@@ -24108,7 +24199,7 @@ async function route(deps, method, url, req, abort) {
|
|
|
24108
24199
|
const run3 = decodeURIComponent(p.slice("/streams/".length));
|
|
24109
24200
|
if (!RUN_NAME_RE.test(run3) || run3.includes("..") || /\.(pid|meta\.json|events\.jsonl)$/.test(run3)) return err2(400, "not a run name");
|
|
24110
24201
|
const file = join53(deps.stateDir, "streams", run3.endsWith(".jsonl") ? run3 : `${run3}.jsonl`);
|
|
24111
|
-
if (!
|
|
24202
|
+
if (!existsSync50(file)) return err2(404, "no such run");
|
|
24112
24203
|
return tailReply(file, url.searchParams, false, abort);
|
|
24113
24204
|
}
|
|
24114
24205
|
if ((p === "/pause" || p === "/resume") && post3) {
|
|
@@ -24152,7 +24243,7 @@ function send(res, r) {
|
|
|
24152
24243
|
res.end(body);
|
|
24153
24244
|
}
|
|
24154
24245
|
async function socketIsStale(path) {
|
|
24155
|
-
if (!
|
|
24246
|
+
if (!existsSync50(path)) return false;
|
|
24156
24247
|
return new Promise((resolve8) => {
|
|
24157
24248
|
const c = createConnection(path);
|
|
24158
24249
|
c.once("connect", () => {
|
|
@@ -24171,7 +24262,7 @@ async function startControlServer(deps) {
|
|
|
24171
24262
|
return null;
|
|
24172
24263
|
}
|
|
24173
24264
|
if (await socketIsStale(path)) rmSync23(path, { force: true });
|
|
24174
|
-
else if (
|
|
24265
|
+
else if (existsSync50(path)) {
|
|
24175
24266
|
log(`control API not started: ${path} is in use by another process`);
|
|
24176
24267
|
return null;
|
|
24177
24268
|
}
|
|
@@ -24264,7 +24355,7 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/
|
|
|
24264
24355
|
|
|
24265
24356
|
// src/periscope-endpoints.ts
|
|
24266
24357
|
import { randomBytes as randomBytes3 } from "crypto";
|
|
24267
|
-
import { chmodSync as chmodSync5, existsSync as
|
|
24358
|
+
import { chmodSync as chmodSync5, existsSync as existsSync51, mkdirSync as mkdirSync37, readdirSync as readdirSync19, readFileSync as readFileSync45, rmSync as rmSync24, writeFileSync as writeFileSync37 } from "fs";
|
|
24268
24359
|
import { join as join54 } from "path";
|
|
24269
24360
|
var secretDir = (stateDir) => join54(stateDir, "periscope-mcp");
|
|
24270
24361
|
var secretFile = (stateDir, id) => join54(secretDir(stateDir), `${id}.json`);
|
|
@@ -24307,7 +24398,7 @@ function removePeriscopeEndpointRecord(stateDir, ticketRecordId) {
|
|
|
24307
24398
|
}
|
|
24308
24399
|
function listPeriscopeEndpoints(stateDir) {
|
|
24309
24400
|
const dir = endpointDir(stateDir);
|
|
24310
|
-
if (!
|
|
24401
|
+
if (!existsSync51(dir)) return [];
|
|
24311
24402
|
const out = [];
|
|
24312
24403
|
for (const name of readdirSync19(dir)) {
|
|
24313
24404
|
if (!name.endsWith(".json")) continue;
|
|
@@ -25290,7 +25381,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
25290
25381
|
} else {
|
|
25291
25382
|
const toleratesNoRoutes = command === "credential" || command === "forge" || command === "repos" || command === "ship" || command === "disconnect" && flag("reset") || command === "deprovision" || command === "connect" && flag("sign-out");
|
|
25292
25383
|
const configless = command === "connect" && flag("sign-out") || command === "disconnect" && flag("reset") || command === "deprovision";
|
|
25293
|
-
if (configless && !
|
|
25384
|
+
if (configless && !existsSync52(findConfigFile(CREW_HOME))) {
|
|
25294
25385
|
cfg = loadConfig(CREW_HOME, firstRunConfigPath(), { text: "ship: {}\n", allowNoRoutes: true });
|
|
25295
25386
|
} else {
|
|
25296
25387
|
cfg = loadConfig(CREW_HOME, void 0, toleratesNoRoutes ? { allowNoRoutes: true } : {});
|
|
@@ -25477,6 +25568,15 @@ if (command === "passenger-sync-daemon") {
|
|
|
25477
25568
|
since: (/* @__PURE__ */ new Date()).toISOString()
|
|
25478
25569
|
}).then(() => void 0).catch(() => void 0));
|
|
25479
25570
|
} else if (status2 === "connected") clearShipAttention(state, rejectedKey);
|
|
25571
|
+
const controlKey = `${RELAY_CONTROL_UNSUPPORTED_KEY}:${workspaceId}`;
|
|
25572
|
+
if (info?.controlRefusedAt) {
|
|
25573
|
+
pending.push(raiseShipAttention({ state, emit, route: owningRoute, ship: cfg.ship }, {
|
|
25574
|
+
kind: RELAY_CONTROL_UNSUPPORTED_KEY,
|
|
25575
|
+
key: controlKey,
|
|
25576
|
+
message: relayControlUnsupportedMessage(workspaceId),
|
|
25577
|
+
since: (/* @__PURE__ */ new Date()).toISOString()
|
|
25578
|
+
}).then(() => void 0).catch(() => void 0));
|
|
25579
|
+
} else if (status2 === "connected") clearShipAttention(state, controlKey);
|
|
25480
25580
|
}
|
|
25481
25581
|
});
|
|
25482
25582
|
for (const plan of containerPlans) pending.push(reconcileEndpoint(plan.workspaceId, reconcile));
|
|
@@ -25634,7 +25734,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
25634
25734
|
if (host === "linux" && hasSystemd) {
|
|
25635
25735
|
const label = labelFor(CREW_HOME, "run");
|
|
25636
25736
|
const staleTimer = join56(homedir11(), ".config", "systemd", "user", `${label}.timer`);
|
|
25637
|
-
if (
|
|
25737
|
+
if (existsSync52(staleTimer)) {
|
|
25638
25738
|
process.stdout.write(
|
|
25639
25739
|
`removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
|
|
25640
25740
|
`
|
|
@@ -25663,7 +25763,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
25663
25763
|
const { settled: doctorRepos, errors: refusedRepos } = await settleRepos(reposOf(c), (t) => t.name, (t) => resolveOneRepo(c, t));
|
|
25664
25764
|
for (const e of refusedRepos) problems.push(`repo ${e.repo}: ${e.detail}`);
|
|
25665
25765
|
for (const r of doctorRepos) {
|
|
25666
|
-
if (!
|
|
25766
|
+
if (!existsSync52(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
|
|
25667
25767
|
if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
|
|
25668
25768
|
readiness.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
|
|
25669
25769
|
}
|
|
@@ -25690,7 +25790,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
25690
25790
|
}
|
|
25691
25791
|
async function doctorFix(o) {
|
|
25692
25792
|
const routes = await Promise.all(routesInScope(cfg.routes, named).map((c) => gatherHealth({ route: c.route, enabled: c.enabled, hasRepos: reposOf(c).length > 0 }, () => routeProblems(c))));
|
|
25693
|
-
const unitExists = (p) =>
|
|
25793
|
+
const unitExists = (p) => existsSync52(p);
|
|
25694
25794
|
const crontab = o.host === "linux" ? spawnSync11("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
|
|
25695
25795
|
const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run", installContext()), unitExists, crontab) : true;
|
|
25696
25796
|
const plan = planFix({ routes, schedulerAbsent, host: o.host });
|
|
@@ -25842,9 +25942,25 @@ ${changed2.map((c) => ` - ${c}
|
|
|
25842
25942
|
}
|
|
25843
25943
|
}
|
|
25844
25944
|
const precheckTickets = /* @__PURE__ */ new Map();
|
|
25945
|
+
const scope0 = (c, t) => releaseLockScope(c.route, t.name);
|
|
25845
25946
|
async function releasePhase(c, target, opts = {}) {
|
|
25846
25947
|
const scope = releaseLockScope(c.route, target.name);
|
|
25847
25948
|
const remit = emit.forRoute(c.route);
|
|
25949
|
+
if (lacksCheckout(cfg.ship, target.dir)) {
|
|
25950
|
+
let mode = null;
|
|
25951
|
+
const settings = cacheSettingsOf(cfg.ship.sandbox);
|
|
25952
|
+
const mirror = mirrorRepoOfTarget(target);
|
|
25953
|
+
if (settings && mirror) {
|
|
25954
|
+
try {
|
|
25955
|
+
mode = (await readMirrorContract(new SandboxCache(settings), mirror, c.branch?.base ?? "main"))?.release.mode ?? null;
|
|
25956
|
+
} catch (e) {
|
|
25957
|
+
if (firstReport(`${scope0(c, target)}:contract`)) remit.warn(`${target.name}: could not read the contract from the mirror: ${e.message}`, { step: "release" });
|
|
25958
|
+
}
|
|
25959
|
+
}
|
|
25960
|
+
const note = noCheckoutReleaseNote(target.name, mode);
|
|
25961
|
+
if (firstReport(`${scope0(c, target)}:release`)) remit.emit(note, { step: "release" });
|
|
25962
|
+
return { scope: releaseLockScope(c.route, target.name), tests: "skipped", outcome: "skipped", detail: note };
|
|
25963
|
+
}
|
|
25848
25964
|
const relLock = dryRun ? void 0 : state.acquire(`release-${scope}`);
|
|
25849
25965
|
if (relLock && !relLock.ok) {
|
|
25850
25966
|
remit.emit(`a previous release (pid ${relLock.heldBy}) is still running \u2014 skipping`, { step: "release" });
|
|
@@ -26104,10 +26220,10 @@ ${changed2.map((c) => ` - ${c}
|
|
|
26104
26220
|
if (outcome.gateRed && !dryRun) {
|
|
26105
26221
|
try {
|
|
26106
26222
|
const g = outcome.gateRed;
|
|
26107
|
-
const
|
|
26223
|
+
const reported2 = await tracker.events("gate_red");
|
|
26108
26224
|
const seat = releaseSeat(c);
|
|
26109
26225
|
for (const t of gateRedTickets(outcome.merged, outcome.decision.merges)) {
|
|
26110
|
-
if (alreadyReported(t, g,
|
|
26226
|
+
if (alreadyReported(t, g, reported2)) continue;
|
|
26111
26227
|
await tracker.postEvent(t.id, gateRedComment(g), seat, "gate_red");
|
|
26112
26228
|
remit.warn(`test gate red for ${g.count} cycles \u2014 noted on the ticket`, { ticket: t.issue_id, step: "merge" });
|
|
26113
26229
|
}
|
|
@@ -26118,10 +26234,10 @@ ${changed2.map((c) => ` - ${c}
|
|
|
26118
26234
|
}
|
|
26119
26235
|
if (outcome.localRefSkips?.length && !dryRun) {
|
|
26120
26236
|
try {
|
|
26121
|
-
const
|
|
26237
|
+
const reported2 = await tracker.events("local_ref_skip");
|
|
26122
26238
|
const seat = releaseSeat(c);
|
|
26123
26239
|
for (const s of outcome.localRefSkips) {
|
|
26124
|
-
if (localRefSkipReported(s.ticket, s.problem,
|
|
26240
|
+
if (localRefSkipReported(s.ticket, s.problem, reported2)) continue;
|
|
26125
26241
|
await tracker.postEvent(s.ticket.id, localRefSkipComment(cfg.ship.name, s.problem), seat, "local_ref_skip");
|
|
26126
26242
|
}
|
|
26127
26243
|
} catch (e) {
|
|
@@ -27191,7 +27307,7 @@ ${cu.tail.trimEnd()}
|
|
|
27191
27307
|
`);
|
|
27192
27308
|
if (!dryRun) {
|
|
27193
27309
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
27194
|
-
const raw =
|
|
27310
|
+
const raw = existsSync52(resolvedPath) ? JSON.parse(readFileSync46(resolvedPath, "utf8")) : {};
|
|
27195
27311
|
raw.agentSkills = result.agentSkills;
|
|
27196
27312
|
mkdirSync38(dirOf2(resolvedPath), { recursive: true });
|
|
27197
27313
|
writeFileSync38(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
@@ -27296,8 +27412,8 @@ ${cu.tail.trimEnd()}
|
|
|
27296
27412
|
try {
|
|
27297
27413
|
const a = parseHooksArgs(positional.slice(2), routeNamesOf(configText));
|
|
27298
27414
|
const dir = repoDirOf(configText, dirname34(cfg.configFile), a.route, a.name);
|
|
27299
|
-
if (!
|
|
27300
|
-
const files = { exists: (r) =>
|
|
27415
|
+
if (!existsSync52(dir)) throw new ReposError(`${dir} does not exist on disk`);
|
|
27416
|
+
const files = { exists: (r) => existsSync52(resolvePath(dir, r)), list: (r) => {
|
|
27301
27417
|
try {
|
|
27302
27418
|
return readdirSync20(resolvePath(dir, r));
|
|
27303
27419
|
} catch {
|
|
@@ -27415,7 +27531,7 @@ ${cu.tail.trimEnd()}
|
|
|
27415
27531
|
if (sub === "list" && !routeName) usage2();
|
|
27416
27532
|
if (sub === "add") {
|
|
27417
27533
|
try {
|
|
27418
|
-
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) =>
|
|
27534
|
+
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync52(resolvePath(p.replace(/^~(?=\/|$)/, homedir11()))));
|
|
27419
27535
|
routeName = a.route;
|
|
27420
27536
|
addPath = a.path;
|
|
27421
27537
|
addName = a.name;
|
|
@@ -27445,7 +27561,7 @@ ${cu.tail.trimEnd()}
|
|
|
27445
27561
|
const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
|
|
27446
27562
|
if (sub === "list" || sub === "add") {
|
|
27447
27563
|
const liveRoute = cfg.routes.find((r) => r.route === routeName);
|
|
27448
|
-
if (liveRoute?.resolved?.reposModelId &&
|
|
27564
|
+
if (liveRoute?.resolved?.reposModelId && existsSync52(resolvedFile) && !dryRun) {
|
|
27449
27565
|
try {
|
|
27450
27566
|
await hydrateApiKeys([liveRoute]);
|
|
27451
27567
|
persistRepoRows(cfg.ship.stateDir, liveRoute.route, await new Tracker(liveRoute, cfg.ship).repoRowsForMap());
|
|
@@ -27454,7 +27570,7 @@ ${cu.tail.trimEnd()}
|
|
|
27454
27570
|
}
|
|
27455
27571
|
}
|
|
27456
27572
|
let resolvedRemotes;
|
|
27457
|
-
if (
|
|
27573
|
+
if (existsSync52(resolvedFile)) {
|
|
27458
27574
|
try {
|
|
27459
27575
|
resolvedRemotes = JSON.parse(readFileSync46(resolvedFile, "utf8"));
|
|
27460
27576
|
} catch {
|
|
@@ -27463,7 +27579,7 @@ ${cu.tail.trimEnd()}
|
|
|
27463
27579
|
const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
|
|
27464
27580
|
const text2 = configText;
|
|
27465
27581
|
const base = dirname34(cfg.configFile);
|
|
27466
|
-
const isCheckout = (d) =>
|
|
27582
|
+
const isCheckout = (d) => existsSync52(`${d}/.git`);
|
|
27467
27583
|
try {
|
|
27468
27584
|
if (sub === "list") {
|
|
27469
27585
|
if (flag("json")) {
|
|
@@ -27473,7 +27589,7 @@ ${cu.tail.trimEnd()}
|
|
|
27473
27589
|
base,
|
|
27474
27590
|
resolved: resolvedRemotes,
|
|
27475
27591
|
originOf,
|
|
27476
|
-
exists: (d) =>
|
|
27592
|
+
exists: (d) => existsSync52(d),
|
|
27477
27593
|
isCheckout,
|
|
27478
27594
|
repoHasHooks: (d) => REPO_CONFIG_FILENAMES.some((f) => {
|
|
27479
27595
|
try {
|
|
@@ -27495,7 +27611,7 @@ ${cu.tail.trimEnd()}
|
|
|
27495
27611
|
for (const r of repos) {
|
|
27496
27612
|
const origin2 = originOf(r.dir);
|
|
27497
27613
|
const row = matchRepoName(origin2, resolvedRemotes);
|
|
27498
|
-
const note = !
|
|
27614
|
+
const note = !existsSync52(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
|
|
27499
27615
|
process.stdout.write(`${r.name} ${r.dir} ${note}
|
|
27500
27616
|
`);
|
|
27501
27617
|
}
|
|
@@ -27507,9 +27623,9 @@ ${cu.tail.trimEnd()}
|
|
|
27507
27623
|
` : `${human}
|
|
27508
27624
|
`);
|
|
27509
27625
|
const plan2 = planRepoClone({ text: text2, base, route: routeName, name: addName, resolved: resolvedRemotes, dir: value("dir"), isCheckout });
|
|
27510
|
-
if (
|
|
27626
|
+
if (existsSync52(plan2.target) && (existsSync52(`${plan2.target}/.git`) || readdirSync20(plan2.target).length > 0)) {
|
|
27511
27627
|
throw new ReposError(
|
|
27512
|
-
|
|
27628
|
+
existsSync52(`${plan2.target}/.git`) ? `${plan2.target} is already a checkout \u2014 attach it with \`crew repos add ${routeName} ${plan2.name} ${plan2.target}\`` : `${plan2.target} exists and is not empty \u2014 pass --dir to clone somewhere else`
|
|
27513
27629
|
);
|
|
27514
27630
|
}
|
|
27515
27631
|
if (dryRun) {
|
|
@@ -27534,7 +27650,7 @@ ${cu.tail.trimEnd()}
|
|
|
27534
27650
|
break;
|
|
27535
27651
|
}
|
|
27536
27652
|
const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir11()));
|
|
27537
|
-
const top =
|
|
27653
|
+
const top = existsSync52(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
|
|
27538
27654
|
if (!top) throw new ReposError(`${target} is not a git checkout`);
|
|
27539
27655
|
const origin = originOf(top);
|
|
27540
27656
|
const plan = planRepoAdd({
|
|
@@ -27586,7 +27702,7 @@ ${cu.tail.trimEnd()}
|
|
|
27586
27702
|
const created = await rowTracker.createRepoRow(rowPayload);
|
|
27587
27703
|
process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
|
|
27588
27704
|
`);
|
|
27589
|
-
if (
|
|
27705
|
+
if (existsSync52(resolvedFile)) {
|
|
27590
27706
|
const raw = JSON.parse(readFileSync46(resolvedFile, "utf8"));
|
|
27591
27707
|
raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
|
|
27592
27708
|
raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
|
|
@@ -28008,7 +28124,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
28008
28124
|
}
|
|
28009
28125
|
}
|
|
28010
28126
|
let previous;
|
|
28011
|
-
const hadPreviousFile =
|
|
28127
|
+
const hadPreviousFile = existsSync52(resolvedPath);
|
|
28012
28128
|
if (hadPreviousFile) {
|
|
28013
28129
|
try {
|
|
28014
28130
|
previous = JSON.parse(readFileSync46(resolvedPath, "utf8"));
|
|
@@ -28153,7 +28269,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
28153
28269
|
let mintOutcomes = [];
|
|
28154
28270
|
if (!dryRun && previous?.agentPersonas && existingRoute?.resolved) {
|
|
28155
28271
|
try {
|
|
28156
|
-
mintOutcomes = await mintMissingSeatKeys(
|
|
28272
|
+
mintOutcomes = await mintMissingSeatKeys(withFoundSeats(existingRoute, found, previous.releaseAgentId), { userAgent: cfg.ship.userAgent, shipName: cfg.ship.name });
|
|
28157
28273
|
for (const o of mintOutcomes) process.stderr.write(`${describeMintOutcome(o)}
|
|
28158
28274
|
`);
|
|
28159
28275
|
for (const p of await probeSeatReads({ ...existingRoute, resolved: { ...existingRoute.resolved, seats: found.seats } }, { userAgent: cfg.ship.userAgent })) {
|
|
@@ -28888,7 +29004,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
28888
29004
|
prepare: async (path) => {
|
|
28889
29005
|
for (const f of repoWt.config.worktrees.copy) {
|
|
28890
29006
|
const src = join56(repoWt.dir, f);
|
|
28891
|
-
if (!
|
|
29007
|
+
if (!existsSync52(src) || existsSync52(join56(path, f))) continue;
|
|
28892
29008
|
mkdirSync38(dirOf2(join56(path, f)), { recursive: true });
|
|
28893
29009
|
copyFileSync2(src, join56(path, f));
|
|
28894
29010
|
}
|
|
@@ -29111,7 +29227,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
29111
29227
|
const shellR = hit && resolveShell(hit.repo.config.shell ?? cfg.ship.shell);
|
|
29112
29228
|
const resumed = await runPeriscopeResume({
|
|
29113
29229
|
find: async () => hit && { row: hit.row, tag: hit.tag },
|
|
29114
|
-
worktreeExists: (p) =>
|
|
29230
|
+
worktreeExists: (p) => existsSync52(p),
|
|
29115
29231
|
prepareDropFile,
|
|
29116
29232
|
startProxy: (wt, url, n) => launchProxy(crewInvocation(CREW_HOME), wt, url, Number(n), { host: hit.row.host || "localhost", inject: hit.repo.config.periscope?.inspector !== "repo" }),
|
|
29117
29233
|
caps: periscopeCaps(cfg.ship),
|
|
@@ -29208,7 +29324,7 @@ ${where.join("\n")}`);
|
|
|
29208
29324
|
prepare: async (path) => {
|
|
29209
29325
|
for (const f of repoP.config.worktrees.copy) {
|
|
29210
29326
|
const src = join56(repoP.dir, f);
|
|
29211
|
-
if (!
|
|
29327
|
+
if (!existsSync52(src)) continue;
|
|
29212
29328
|
mkdirSync38(dirOf2(join56(path, f)), { recursive: true });
|
|
29213
29329
|
copyFileSync2(src, join56(path, f));
|
|
29214
29330
|
}
|
|
@@ -29700,7 +29816,7 @@ ${where.join("\n")}`);
|
|
|
29700
29816
|
}
|
|
29701
29817
|
if (row) {
|
|
29702
29818
|
await tracker.deleteRepoRow(row.id);
|
|
29703
|
-
if (
|
|
29819
|
+
if (existsSync52(resolvedFile)) {
|
|
29704
29820
|
const raw = JSON.parse(readFileSync46(resolvedFile, "utf8"));
|
|
29705
29821
|
if (raw.repoNames) delete raw.repoNames[row.id];
|
|
29706
29822
|
if (raw.repoRemotes) delete raw.repoRemotes[row.id];
|
|
@@ -29708,13 +29824,13 @@ ${where.join("\n")}`);
|
|
|
29708
29824
|
`);
|
|
29709
29825
|
}
|
|
29710
29826
|
}
|
|
29711
|
-
if (
|
|
29827
|
+
if (existsSync52(join56(dir, ".git")) && basename16(dir) === SAMPLE_REPO) rmSync25(dir, { recursive: true, force: true });
|
|
29712
29828
|
say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
|
|
29713
29829
|
break;
|
|
29714
29830
|
}
|
|
29715
29831
|
const projectId = route2.resolved.areaId ?? resolveRepoProject(await tracker.projectRows(), value("project"));
|
|
29716
|
-
const fresh = !
|
|
29717
|
-
if (
|
|
29832
|
+
const fresh = !existsSync52(join56(dir, ".git"));
|
|
29833
|
+
if (existsSync52(dir) && fresh && readdirSync20(dir).length > 0) fail2(`${dir} exists and is not a git checkout \u2014 move it aside`);
|
|
29718
29834
|
if (dryRun) {
|
|
29719
29835
|
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`);
|
|
29720
29836
|
break;
|
|
@@ -29740,7 +29856,7 @@ ${where.join("\n")}`);
|
|
|
29740
29856
|
spec: { platform: "unix", releaseMode: "integrate", ciProvider: "none" }
|
|
29741
29857
|
});
|
|
29742
29858
|
repoId = (await tracker.createRepoRow(payload)).id;
|
|
29743
|
-
if (
|
|
29859
|
+
if (existsSync52(resolvedFile)) {
|
|
29744
29860
|
const raw = JSON.parse(readFileSync46(resolvedFile, "utf8"));
|
|
29745
29861
|
raw.repoNames = { ...raw.repoNames, [repoId]: SAMPLE_REPO };
|
|
29746
29862
|
raw.repoRemotes = { ...raw.repoRemotes, [repoId]: SAMPLE_REMOTE };
|
|
@@ -30096,7 +30212,7 @@ watermark: ${state.watermark()}
|
|
|
30096
30212
|
}
|
|
30097
30213
|
for (const r of repos) {
|
|
30098
30214
|
const remote = r.config.branch.remote;
|
|
30099
|
-
if (
|
|
30215
|
+
if (existsSync52(r.dir)) {
|
|
30100
30216
|
pushAuth.push({
|
|
30101
30217
|
route: c.route,
|
|
30102
30218
|
repo: r.name,
|
|
@@ -30161,6 +30277,9 @@ watermark: ${state.watermark()}
|
|
|
30161
30277
|
for (const t of rejectedTunnels(cfg.ship.stateDir)) {
|
|
30162
30278
|
problems.push({ severity: "attention", kind: TUNNEL_REJECTED_KEY, message: tunnelRejectedMessage(t.workspaceId, t.quickExits), remedy: "rotate the slug with `crew rotate-passenger-url`" });
|
|
30163
30279
|
}
|
|
30280
|
+
for (const workspaceId of controlRefusedTunnels(cfg.ship.stateDir)) {
|
|
30281
|
+
problems.push({ severity: "attention", kind: RELAY_CONTROL_UNSUPPORTED_KEY, message: relayControlUnsupportedMessage(workspaceId), remedy: "deploy crew-relay 0.2.0 or later (scripts/deploy-relay.sh in the crew-relay repository)" });
|
|
30282
|
+
}
|
|
30164
30283
|
const previewProblem2 = await previewHostProblem(cfg);
|
|
30165
30284
|
if (previewProblem2) problems.push(previewHostFinding(previewProblem2));
|
|
30166
30285
|
if (cfg.routes.some((x) => x.route === route2.route && x.enabled && x.hostPeriscopes)) {
|
|
@@ -30183,6 +30302,8 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
30183
30302
|
for (const w of roleCapWarnings(cfg.ship)) process.stdout.write(`role concurrency: WARN ${w}
|
|
30184
30303
|
`);
|
|
30185
30304
|
for (const t of rejectedTunnels(cfg.ship.stateDir)) process.stdout.write(`relay tunnel: ATTENTION ${tunnelRejectedMessage(t.workspaceId, t.quickExits)}
|
|
30305
|
+
`);
|
|
30306
|
+
for (const workspaceId of controlRefusedTunnels(cfg.ship.stateDir)) process.stdout.write(`relay tunnel: ATTENTION ${relayControlUnsupportedMessage(workspaceId)}
|
|
30186
30307
|
`);
|
|
30187
30308
|
process.stdout.write(`periscopes: ${periscopeSummary(cfg)}
|
|
30188
30309
|
`);
|
|
@@ -30197,7 +30318,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
30197
30318
|
}
|
|
30198
30319
|
try {
|
|
30199
30320
|
const evFile = eventFileFor(cfg.ship.stateDir);
|
|
30200
|
-
const evSize =
|
|
30321
|
+
const evSize = existsSync52(evFile) ? statSync16(evFile).size : 0;
|
|
30201
30322
|
process.stdout.write(
|
|
30202
30323
|
evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
|
|
30203
30324
|
` : "events log: ok\n"
|
package/package.json
CHANGED