@tablation/crew 0.126.5 → 0.126.7
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 +214 -125
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -690,9 +690,9 @@ function urlPort(url) {
|
|
|
690
690
|
return void 0;
|
|
691
691
|
}
|
|
692
692
|
}
|
|
693
|
-
function
|
|
693
|
+
function hostListening(port, host, timeoutMs) {
|
|
694
694
|
return new Promise((resolve8) => {
|
|
695
|
-
const s = connect({ port, host
|
|
695
|
+
const s = connect({ port, host });
|
|
696
696
|
const done = (ok) => {
|
|
697
697
|
s.destroy();
|
|
698
698
|
resolve8(ok);
|
|
@@ -702,6 +702,10 @@ function portListening(port, timeoutMs = 500) {
|
|
|
702
702
|
s.once("error", () => done(false));
|
|
703
703
|
});
|
|
704
704
|
}
|
|
705
|
+
async function portListening(port, timeoutMs = 500) {
|
|
706
|
+
if (await hostListening(port, "127.0.0.1", timeoutMs)) return true;
|
|
707
|
+
return hostListening(port, "::1", timeoutMs);
|
|
708
|
+
}
|
|
705
709
|
async function periscopesForStatus(stateDir, probe = portListening) {
|
|
706
710
|
return Promise.all(readPeriscopeSidecars(stateDir).map(async (sc) => {
|
|
707
711
|
const port = urlPort(sc.url);
|
|
@@ -12000,6 +12004,21 @@ function slugFor(route2, agentId) {
|
|
|
12000
12004
|
const [workspaceSlug] = splitRoute(route2.route);
|
|
12001
12005
|
return seatKeySlug(workspaceSlug, agentId);
|
|
12002
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
|
+
}
|
|
12003
12022
|
function seatAgentId(route2, role) {
|
|
12004
12023
|
return route2.resolved?.agentPersonas?.[role]?.agentId;
|
|
12005
12024
|
}
|
|
@@ -12492,6 +12511,15 @@ printf '%s
|
|
|
12492
12511
|
' "$tip" > "$out/$name.tip"
|
|
12493
12512
|
echo "updated $tip"
|
|
12494
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
|
+
`;
|
|
12495
12523
|
var SandboxCache = class {
|
|
12496
12524
|
s;
|
|
12497
12525
|
exec;
|
|
@@ -12578,6 +12606,20 @@ var SandboxCache = class {
|
|
|
12578
12606
|
if (!m) throw new Error(`unexpected output materializing ${repo.host}/${repo.path}: ${stdout.trim().slice(0, 200)}`);
|
|
12579
12607
|
return { tip: m[2], changed: m[1] === "updated" };
|
|
12580
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
|
+
}
|
|
12581
12623
|
/** Mirrors in the volume with their last refresh; empty when the volume does not exist. */
|
|
12582
12624
|
async list() {
|
|
12583
12625
|
try {
|
|
@@ -14051,7 +14093,7 @@ async function runPoolMember(deps) {
|
|
|
14051
14093
|
await deps.sleep(backoffMs(failures++));
|
|
14052
14094
|
}
|
|
14053
14095
|
};
|
|
14054
|
-
let
|
|
14096
|
+
let reported2;
|
|
14055
14097
|
let busy = false;
|
|
14056
14098
|
const report = async (state, o = {}) => {
|
|
14057
14099
|
const body = {
|
|
@@ -14074,7 +14116,7 @@ async function runPoolMember(deps) {
|
|
|
14074
14116
|
if (r.status === 401) return "credential";
|
|
14075
14117
|
if (r.status === 409) deps.log(`status ${state}: the platform refused the transition (409); not retrying`);
|
|
14076
14118
|
else if (r.status >= 400) deps.log(`status ${state}: platform answered ${r.status}; not retrying`);
|
|
14077
|
-
|
|
14119
|
+
reported2 = body;
|
|
14078
14120
|
deps.store.write({ ...deps.store.read(), lastState: state });
|
|
14079
14121
|
return "ok";
|
|
14080
14122
|
};
|
|
@@ -14157,7 +14199,7 @@ async function runPoolMember(deps) {
|
|
|
14157
14199
|
if (r === "stopped") return POOL_EXIT.ok;
|
|
14158
14200
|
}
|
|
14159
14201
|
}
|
|
14160
|
-
let relayReported =
|
|
14202
|
+
let relayReported = reported2?.state === "running" ? reported2.relayUrl : null;
|
|
14161
14203
|
let reregister = false;
|
|
14162
14204
|
while (!reregister) {
|
|
14163
14205
|
if (deps.stopping()) return POOL_EXIT.ok;
|
|
@@ -19072,6 +19114,73 @@ function runForge(a, io) {
|
|
|
19072
19114
|
`, err: "" };
|
|
19073
19115
|
}
|
|
19074
19116
|
|
|
19117
|
+
// src/mirror-contract.ts
|
|
19118
|
+
import { existsSync as existsSync40 } from "fs";
|
|
19119
|
+
|
|
19120
|
+
// src/passenger-mirror.ts
|
|
19121
|
+
import { mkdirSync as mkdirSync29 } from "fs";
|
|
19122
|
+
import { dirname as dirname25 } from "path";
|
|
19123
|
+
function mirrorRepoOf(spec) {
|
|
19124
|
+
const fromOrigin = spec.originUrl ? parseRemote(spec.originUrl) : null;
|
|
19125
|
+
if (fromOrigin) return fromOrigin;
|
|
19126
|
+
if (!spec.remote) return null;
|
|
19127
|
+
return { host: normalizeForgeHost(spec.forgeHost) || DEFAULT_FORGE_HOST, path: spec.remote };
|
|
19128
|
+
}
|
|
19129
|
+
async function materializeMirrorCheckouts(specs, cacheFor = (s) => new SandboxCache(s.mirror.cache)) {
|
|
19130
|
+
const out = [];
|
|
19131
|
+
for (const spec of specs) {
|
|
19132
|
+
if (!spec.mirror) continue;
|
|
19133
|
+
const base = { name: spec.name, workspaceId: spec.workspaceId };
|
|
19134
|
+
const repo = mirrorRepoOf(spec);
|
|
19135
|
+
if (!repo) {
|
|
19136
|
+
out.push({ ...base, action: "failed", detail: `no remote known for ${spec.name}, so its mirror cannot be found` });
|
|
19137
|
+
continue;
|
|
19138
|
+
}
|
|
19139
|
+
try {
|
|
19140
|
+
const parent = dirname25(spec.dir);
|
|
19141
|
+
mkdirSync29(parent, { recursive: true });
|
|
19142
|
+
mkdirSync29(spec.dir, { recursive: true });
|
|
19143
|
+
const r = await cacheFor(spec).materialize(repo, spec.base, parent, spec.name);
|
|
19144
|
+
out.push({ ...base, action: r.changed ? "updated" : "unchanged", detail: r.tip });
|
|
19145
|
+
} catch (e) {
|
|
19146
|
+
const err3 = e;
|
|
19147
|
+
out.push({ ...base, action: "failed", detail: (err3.stderr?.trim() || err3.message || String(e)).split("\n").slice(-3).join(" / ") });
|
|
19148
|
+
}
|
|
19149
|
+
}
|
|
19150
|
+
return out;
|
|
19151
|
+
}
|
|
19152
|
+
function mirrorOutcome(m) {
|
|
19153
|
+
if (m.action === "failed") return { action: "clone-failed", detail: m.detail, repeat: false };
|
|
19154
|
+
return { action: "cloned", detail: `${m.action} ${m.detail}` };
|
|
19155
|
+
}
|
|
19156
|
+
|
|
19157
|
+
// src/mirror-contract.ts
|
|
19158
|
+
function lacksCheckout(ship, dir) {
|
|
19159
|
+
return ship.runner === "sandbox" && !existsSync40(dir);
|
|
19160
|
+
}
|
|
19161
|
+
function mirrorRepoOfTarget(t) {
|
|
19162
|
+
return mirrorRepoOf({ remote: t.remote, forgeHost: t.forgeHost });
|
|
19163
|
+
}
|
|
19164
|
+
async function readMirrorContract(cache, repo, base) {
|
|
19165
|
+
for (const name of REPO_CONFIG_FILENAMES) {
|
|
19166
|
+
const text2 = await cache.showFile(repo, base, name);
|
|
19167
|
+
if (text2 !== null) return parseRepoConfig(text2, `${repo.host}/${repo.path}:${base}:${name}`);
|
|
19168
|
+
}
|
|
19169
|
+
return null;
|
|
19170
|
+
}
|
|
19171
|
+
function noCheckoutReleaseNote(repo, mode) {
|
|
19172
|
+
if (mode === "local" || mode === null) {
|
|
19173
|
+
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`;
|
|
19174
|
+
}
|
|
19175
|
+
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)`;
|
|
19176
|
+
}
|
|
19177
|
+
var reported = /* @__PURE__ */ new Set();
|
|
19178
|
+
function firstReport(key) {
|
|
19179
|
+
if (reported.has(key)) return false;
|
|
19180
|
+
reported.add(key);
|
|
19181
|
+
return true;
|
|
19182
|
+
}
|
|
19183
|
+
|
|
19075
19184
|
// src/whoami.ts
|
|
19076
19185
|
async function buildWhoami(routes, deps = {}) {
|
|
19077
19186
|
const doFetch = deps.fetch ?? fetch;
|
|
@@ -19243,7 +19352,7 @@ function missingReleaserWarning(repo, release, ships, now = Date.now()) {
|
|
|
19243
19352
|
}
|
|
19244
19353
|
|
|
19245
19354
|
// src/watch.ts
|
|
19246
|
-
import { watch as watchFile, existsSync as
|
|
19355
|
+
import { watch as watchFile, existsSync as existsSync41, statSync as statSync11, openSync as openSync5, readSync as readSync2, closeSync as closeSync5 } from "fs";
|
|
19247
19356
|
var E = "\x1B[";
|
|
19248
19357
|
var dim = (s) => `${E}2m${s}${E}0m`;
|
|
19249
19358
|
var bold = (s) => `${E}1m${s}${E}0m`;
|
|
@@ -19344,7 +19453,7 @@ function renderHeaders(states, now = Date.now()) {
|
|
|
19344
19453
|
return states.map((s) => renderHeader(s, now)).join("");
|
|
19345
19454
|
}
|
|
19346
19455
|
function readFrom(file, from) {
|
|
19347
|
-
if (!
|
|
19456
|
+
if (!existsSync41(file)) return { events: [], offset: from };
|
|
19348
19457
|
const size = statSync11(file).size;
|
|
19349
19458
|
if (size < from) return readFrom(file, 0);
|
|
19350
19459
|
if (size === from) return { events: [], offset: from };
|
|
@@ -19587,8 +19696,8 @@ function nextRoles(decision, ran) {
|
|
|
19587
19696
|
}
|
|
19588
19697
|
|
|
19589
19698
|
// src/ship-name.ts
|
|
19590
|
-
import { existsSync as
|
|
19591
|
-
import { dirname as
|
|
19699
|
+
import { existsSync as existsSync42, mkdirSync as mkdirSync30, readFileSync as readFileSync37, renameSync as renameSync5, writeFileSync as writeFileSync32 } from "fs";
|
|
19700
|
+
import { dirname as dirname26 } from "path";
|
|
19592
19701
|
import { parseDocument as parseDocument5 } from "yaml";
|
|
19593
19702
|
var ShipNameError = class extends Error {
|
|
19594
19703
|
};
|
|
@@ -19649,7 +19758,7 @@ async function syncRouteName(i) {
|
|
|
19649
19758
|
return { status: "ok", ...base, action, name: i.local };
|
|
19650
19759
|
}
|
|
19651
19760
|
function readSyncedName(resolvedFile) {
|
|
19652
|
-
if (!
|
|
19761
|
+
if (!existsSync42(resolvedFile)) return void 0;
|
|
19653
19762
|
try {
|
|
19654
19763
|
const v = JSON.parse(readFileSync37(resolvedFile, "utf8")).shipName;
|
|
19655
19764
|
return typeof v === "string" ? v : void 0;
|
|
@@ -19658,7 +19767,7 @@ function readSyncedName(resolvedFile) {
|
|
|
19658
19767
|
}
|
|
19659
19768
|
}
|
|
19660
19769
|
function writeSyncedName(resolvedFile, name) {
|
|
19661
|
-
if (!
|
|
19770
|
+
if (!existsSync42(resolvedFile)) return;
|
|
19662
19771
|
let raw;
|
|
19663
19772
|
try {
|
|
19664
19773
|
raw = JSON.parse(readFileSync37(resolvedFile, "utf8"));
|
|
@@ -19667,7 +19776,7 @@ function writeSyncedName(resolvedFile, name) {
|
|
|
19667
19776
|
}
|
|
19668
19777
|
if (raw.shipName === name) return;
|
|
19669
19778
|
raw.shipName = name;
|
|
19670
|
-
|
|
19779
|
+
mkdirSync30(dirname26(resolvedFile), { recursive: true });
|
|
19671
19780
|
const tmp = `${resolvedFile}.tmp${process.pid}`;
|
|
19672
19781
|
writeFileSync32(tmp, `${JSON.stringify(raw, null, 2)}
|
|
19673
19782
|
`);
|
|
@@ -20360,8 +20469,8 @@ function describeCapability(i) {
|
|
|
20360
20469
|
}
|
|
20361
20470
|
|
|
20362
20471
|
// src/agents-sync-run.ts
|
|
20363
|
-
import { existsSync as
|
|
20364
|
-
import { dirname as
|
|
20472
|
+
import { existsSync as existsSync43, mkdirSync as mkdirSync31, readFileSync as readFileSync38, writeFileSync as writeFileSync33 } from "fs";
|
|
20473
|
+
import { dirname as dirname27 } from "path";
|
|
20365
20474
|
async function runAgentsSync(target, o) {
|
|
20366
20475
|
const dryRun = o.dryRun ?? false;
|
|
20367
20476
|
const force = o.force ?? false;
|
|
@@ -20394,11 +20503,11 @@ async function runAgentsSync(target, o) {
|
|
|
20394
20503
|
for (const s of skillResult?.outcomes ?? []) o.say(describeSkillSyncOutcome(s));
|
|
20395
20504
|
if (!dryRun) {
|
|
20396
20505
|
const resolvedPath = resolvedPathFor(o.stateDir, target.route);
|
|
20397
|
-
const raw =
|
|
20506
|
+
const raw = existsSync43(resolvedPath) ? JSON.parse(readFileSync38(resolvedPath, "utf8")) : {};
|
|
20398
20507
|
raw.agentPersonas = personaResult.agentPersonas;
|
|
20399
20508
|
if (skillResult) raw.agentSkills = skillResult.agentSkills;
|
|
20400
20509
|
if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
|
|
20401
|
-
|
|
20510
|
+
mkdirSync31(dirname27(resolvedPath), { recursive: true });
|
|
20402
20511
|
writeFileSync33(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
20403
20512
|
`);
|
|
20404
20513
|
if (target.resolved) {
|
|
@@ -21021,7 +21130,7 @@ ${run3.err}` });
|
|
|
21021
21130
|
}
|
|
21022
21131
|
|
|
21023
21132
|
// src/sample-ticket.ts
|
|
21024
|
-
import { basename as basename12, dirname as
|
|
21133
|
+
import { basename as basename12, dirname as dirname28, join as join44 } from "path";
|
|
21025
21134
|
var SAMPLE_REPO = "crew-sample";
|
|
21026
21135
|
var SAMPLE_REMOTE = `local/${SAMPLE_REPO}`;
|
|
21027
21136
|
var SAMPLE_MARKER = "sample: true";
|
|
@@ -21044,7 +21153,7 @@ has something safe to work on: its test and build hooks always pass, it has no
|
|
|
21044
21153
|
remote, and \`crew sample-ticket --remove\` deletes it.
|
|
21045
21154
|
`;
|
|
21046
21155
|
function sampleDir(existing, reposBase, workspace) {
|
|
21047
|
-
if (existing.length > 0) return join44(
|
|
21156
|
+
if (existing.length > 0) return join44(dirname28(existing[0]), SAMPLE_REPO);
|
|
21048
21157
|
return join44(reposBase, workspace, SAMPLE_REPO);
|
|
21049
21158
|
}
|
|
21050
21159
|
var SAMPLE_TITLE = "Sample: add a documented `greet` function with a test";
|
|
@@ -21089,8 +21198,8 @@ function planSampleTicket(i) {
|
|
|
21089
21198
|
}
|
|
21090
21199
|
|
|
21091
21200
|
// src/worktree-sweep.ts
|
|
21092
|
-
import { existsSync as
|
|
21093
|
-
import { basename as basename13, dirname as
|
|
21201
|
+
import { existsSync as existsSync44, readFileSync as readFileSync40, readdirSync as readdirSync14 } from "fs";
|
|
21202
|
+
import { basename as basename13, dirname as dirname29, join as join45, resolve as resolve5 } from "path";
|
|
21094
21203
|
function planWorktreeSweep(target, tickets, contract) {
|
|
21095
21204
|
const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
|
|
21096
21205
|
const verified = contract.statuses.verified;
|
|
@@ -21154,7 +21263,7 @@ function worktreeTicketNumbers(target) {
|
|
|
21154
21263
|
return [...nums];
|
|
21155
21264
|
}
|
|
21156
21265
|
function siblingWorktreeDirs(target) {
|
|
21157
|
-
const parent =
|
|
21266
|
+
const parent = dirname29(target.dir.replace(/[/\\]+$/, ""));
|
|
21158
21267
|
const stem = `${basename13(target.dir.replace(/[/\\]+$/, ""))}-`;
|
|
21159
21268
|
const registered = new Set(worktrees(target.dir).map((w) => resolve5(w.path)));
|
|
21160
21269
|
let names;
|
|
@@ -21202,7 +21311,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
|
|
|
21202
21311
|
let removed = 0;
|
|
21203
21312
|
let keptBranches = 0;
|
|
21204
21313
|
for (const a of actions) {
|
|
21205
|
-
if (!dryRun && base && !a.keepBranch &&
|
|
21314
|
+
if (!dryRun && base && !a.keepBranch && existsSync44(a.path) && isHandBackMerge(a.path, base)) {
|
|
21206
21315
|
if (gitOk(a.path, ["merge", "--abort"]) !== null) {
|
|
21207
21316
|
log.emit(
|
|
21208
21317
|
`abandoned the hand-back merge of ${base} in ${a.path}: ${a.ticket.issue_id} deployed without it`,
|
|
@@ -21210,7 +21319,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
|
|
|
21210
21319
|
);
|
|
21211
21320
|
}
|
|
21212
21321
|
}
|
|
21213
|
-
if (!dryRun &&
|
|
21322
|
+
if (!dryRun && existsSync44(a.path)) {
|
|
21214
21323
|
let dirty = false;
|
|
21215
21324
|
try {
|
|
21216
21325
|
dirty = status(a.path).length > 0;
|
|
@@ -21221,7 +21330,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
|
|
|
21221
21330
|
continue;
|
|
21222
21331
|
}
|
|
21223
21332
|
}
|
|
21224
|
-
if (!dryRun && a.requirePushed && a.branch &&
|
|
21333
|
+
if (!dryRun && a.requirePushed && a.branch && existsSync44(a.path) && remoteConfigured(cwd, remote) && worktreeHoldsUnpushed(a.path, remote)) {
|
|
21225
21334
|
refuse2(a, "unpushed commits", `${a.branch} has commits that are not on ${remote}`);
|
|
21226
21335
|
continue;
|
|
21227
21336
|
}
|
|
@@ -21375,12 +21484,12 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
|
|
|
21375
21484
|
}
|
|
21376
21485
|
|
|
21377
21486
|
// src/stream-sweep.ts
|
|
21378
|
-
import { existsSync as
|
|
21487
|
+
import { existsSync as existsSync45, readdirSync as readdirSync15, rmSync as rmSync20, statSync as statSync12 } from "fs";
|
|
21379
21488
|
import { join as join46 } from "path";
|
|
21380
21489
|
var DAY_MS2 = 24 * 60 * 60 * 1e3;
|
|
21381
21490
|
function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
|
|
21382
21491
|
const dir = join46(stateDir, "streams");
|
|
21383
|
-
if (!
|
|
21492
|
+
if (!existsSync45(dir)) return [];
|
|
21384
21493
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
21385
21494
|
return readdirSync15(dir).map((name) => join46(dir, name)).filter((path) => {
|
|
21386
21495
|
try {
|
|
@@ -21408,7 +21517,7 @@ function applyStreamSweep(paths, dryRun, log) {
|
|
|
21408
21517
|
return removed;
|
|
21409
21518
|
}
|
|
21410
21519
|
function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
21411
|
-
if (!
|
|
21520
|
+
if (!existsSync45(stateDir)) return [];
|
|
21412
21521
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
21413
21522
|
return readdirSync15(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join46(stateDir, name)).filter((path) => {
|
|
21414
21523
|
try {
|
|
@@ -21420,11 +21529,11 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
|
21420
21529
|
}
|
|
21421
21530
|
|
|
21422
21531
|
// src/cli.ts
|
|
21423
|
-
import { realpathSync as realpathSync3, readFileSync as readFileSync46, existsSync as
|
|
21532
|
+
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";
|
|
21424
21533
|
import { createInterface } from "readline/promises";
|
|
21425
21534
|
|
|
21426
21535
|
// src/periscope-cmd.ts
|
|
21427
|
-
import { existsSync as
|
|
21536
|
+
import { existsSync as existsSync46, mkdirSync as mkdirSync32, readFileSync as readFileSync41, rmSync as rmSync21, writeFileSync as writeFileSync34 } from "fs";
|
|
21428
21537
|
import { join as join47 } from "path";
|
|
21429
21538
|
var PeriscopeError = class extends Error {
|
|
21430
21539
|
};
|
|
@@ -21442,7 +21551,7 @@ function removePeriscopePrior(stateDir, ticketRecordId) {
|
|
|
21442
21551
|
}
|
|
21443
21552
|
function writePeriscopePrior(stateDir, ticketRecordId, prior) {
|
|
21444
21553
|
try {
|
|
21445
|
-
|
|
21554
|
+
mkdirSync32(priorDir(stateDir), { recursive: true });
|
|
21446
21555
|
writeFileSync34(priorPath(stateDir, ticketRecordId), JSON.stringify(prior));
|
|
21447
21556
|
} catch {
|
|
21448
21557
|
}
|
|
@@ -21450,7 +21559,7 @@ function writePeriscopePrior(stateDir, ticketRecordId, prior) {
|
|
|
21450
21559
|
function readPeriscopePrior(stateDir, ticketRecordId) {
|
|
21451
21560
|
try {
|
|
21452
21561
|
const p = priorPath(stateDir, ticketRecordId);
|
|
21453
|
-
return
|
|
21562
|
+
return existsSync46(p) ? JSON.parse(readFileSync41(p, "utf8")) : void 0;
|
|
21454
21563
|
} catch {
|
|
21455
21564
|
return void 0;
|
|
21456
21565
|
}
|
|
@@ -22005,8 +22114,8 @@ import { dirname as dirOf2, resolve as resolvePath } from "path";
|
|
|
22005
22114
|
|
|
22006
22115
|
// src/ssh-keys.ts
|
|
22007
22116
|
import { execFileSync as execFileSync13 } from "child_process";
|
|
22008
|
-
import { existsSync as
|
|
22009
|
-
import { dirname as
|
|
22117
|
+
import { existsSync as existsSync47, mkdirSync as mkdirSync33, readFileSync as readFileSync42, chmodSync as chmodSync3 } from "fs";
|
|
22118
|
+
import { dirname as dirname30, join as join49 } from "path";
|
|
22010
22119
|
var SshKeyError = class extends Error {
|
|
22011
22120
|
};
|
|
22012
22121
|
function shipSshKeyPathFor(stateDir) {
|
|
@@ -22015,8 +22124,8 @@ function shipSshKeyPathFor(stateDir) {
|
|
|
22015
22124
|
function ensureShipSshKeypair(stateDir) {
|
|
22016
22125
|
const privateKeyPath = shipSshKeyPathFor(stateDir);
|
|
22017
22126
|
const publicKeyPath = `${privateKeyPath}.pub`;
|
|
22018
|
-
if (!
|
|
22019
|
-
|
|
22127
|
+
if (!existsSync47(privateKeyPath)) {
|
|
22128
|
+
mkdirSync33(dirname30(privateKeyPath), { recursive: true });
|
|
22020
22129
|
try {
|
|
22021
22130
|
execFileSync13("ssh-keygen", [
|
|
22022
22131
|
"-t",
|
|
@@ -22036,7 +22145,7 @@ function ensureShipSshKeypair(stateDir) {
|
|
|
22036
22145
|
}
|
|
22037
22146
|
chmodSync3(privateKeyPath, 384);
|
|
22038
22147
|
}
|
|
22039
|
-
if (!
|
|
22148
|
+
if (!existsSync47(publicKeyPath)) {
|
|
22040
22149
|
throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
|
|
22041
22150
|
}
|
|
22042
22151
|
const publicKey = readFileSync42(publicKeyPath, "utf8").trim();
|
|
@@ -22203,46 +22312,9 @@ async function unregisterEndpoint(workspaceId, info, d) {
|
|
|
22203
22312
|
import { createServer as createHttpServer } from "http";
|
|
22204
22313
|
import { spawn as spawn9 } from "child_process";
|
|
22205
22314
|
import { randomBytes } from "crypto";
|
|
22206
|
-
import { existsSync as
|
|
22315
|
+
import { existsSync as existsSync48, mkdirSync as mkdirSync34, readFileSync as readFileSync43, writeFileSync as writeFileSync35, unlinkSync as unlinkSync4, readdirSync as readdirSync16 } from "fs";
|
|
22207
22316
|
import { join as join50 } from "path";
|
|
22208
22317
|
|
|
22209
|
-
// src/passenger-mirror.ts
|
|
22210
|
-
import { mkdirSync as mkdirSync33 } from "fs";
|
|
22211
|
-
import { dirname as dirname30 } from "path";
|
|
22212
|
-
function mirrorRepoOf(spec) {
|
|
22213
|
-
const fromOrigin = spec.originUrl ? parseRemote(spec.originUrl) : null;
|
|
22214
|
-
if (fromOrigin) return fromOrigin;
|
|
22215
|
-
if (!spec.remote) return null;
|
|
22216
|
-
return { host: normalizeForgeHost(spec.forgeHost) || DEFAULT_FORGE_HOST, path: spec.remote };
|
|
22217
|
-
}
|
|
22218
|
-
async function materializeMirrorCheckouts(specs, cacheFor = (s) => new SandboxCache(s.mirror.cache)) {
|
|
22219
|
-
const out = [];
|
|
22220
|
-
for (const spec of specs) {
|
|
22221
|
-
if (!spec.mirror) continue;
|
|
22222
|
-
const base = { name: spec.name, workspaceId: spec.workspaceId };
|
|
22223
|
-
const repo = mirrorRepoOf(spec);
|
|
22224
|
-
if (!repo) {
|
|
22225
|
-
out.push({ ...base, action: "failed", detail: `no remote known for ${spec.name}, so its mirror cannot be found` });
|
|
22226
|
-
continue;
|
|
22227
|
-
}
|
|
22228
|
-
try {
|
|
22229
|
-
const parent = dirname30(spec.dir);
|
|
22230
|
-
mkdirSync33(parent, { recursive: true });
|
|
22231
|
-
mkdirSync33(spec.dir, { recursive: true });
|
|
22232
|
-
const r = await cacheFor(spec).materialize(repo, spec.base, parent, spec.name);
|
|
22233
|
-
out.push({ ...base, action: r.changed ? "updated" : "unchanged", detail: r.tip });
|
|
22234
|
-
} catch (e) {
|
|
22235
|
-
const err3 = e;
|
|
22236
|
-
out.push({ ...base, action: "failed", detail: (err3.stderr?.trim() || err3.message || String(e)).split("\n").slice(-3).join(" / ") });
|
|
22237
|
-
}
|
|
22238
|
-
}
|
|
22239
|
-
return out;
|
|
22240
|
-
}
|
|
22241
|
-
function mirrorOutcome(m) {
|
|
22242
|
-
if (m.action === "failed") return { action: "clone-failed", detail: m.detail, repeat: false };
|
|
22243
|
-
return { action: "cloned", detail: `${m.action} ${m.detail}` };
|
|
22244
|
-
}
|
|
22245
|
-
|
|
22246
22318
|
// src/secret-compare.ts
|
|
22247
22319
|
import { timingSafeEqual } from "crypto";
|
|
22248
22320
|
function secretsMatch(a, b) {
|
|
@@ -22338,7 +22410,7 @@ function syncDaemonStatePath(stateDir, workspaceId) {
|
|
|
22338
22410
|
}
|
|
22339
22411
|
function readPersistedSyncDaemon(stateDir, workspaceId) {
|
|
22340
22412
|
const path = syncDaemonStatePath(stateDir, workspaceId);
|
|
22341
|
-
if (!
|
|
22413
|
+
if (!existsSync48(path)) return void 0;
|
|
22342
22414
|
try {
|
|
22343
22415
|
return JSON.parse(readFileSync43(path, "utf8"));
|
|
22344
22416
|
} catch {
|
|
@@ -22369,7 +22441,7 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
|
|
|
22369
22441
|
}
|
|
22370
22442
|
function readAllPersistedSyncWorkspaceIds(stateDir) {
|
|
22371
22443
|
const dir = join50(stateDir, "passengers");
|
|
22372
|
-
if (!
|
|
22444
|
+
if (!existsSync48(dir)) return [];
|
|
22373
22445
|
const suffix = "-sync.json";
|
|
22374
22446
|
return readdirSync16(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
|
|
22375
22447
|
}
|
|
@@ -22889,7 +22961,7 @@ function contractFixers(dir, baseRef, tickets, locate) {
|
|
|
22889
22961
|
}
|
|
22890
22962
|
|
|
22891
22963
|
// src/daemon.ts
|
|
22892
|
-
import { existsSync as
|
|
22964
|
+
import { existsSync as existsSync49, mkdirSync as mkdirSync36, readFileSync as readFileSync44, readdirSync as readdirSync17, rmSync as rmSync22, statSync as statSync14, writeFileSync as writeFileSync36 } from "fs";
|
|
22893
22965
|
import { join as join52 } from "path";
|
|
22894
22966
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
22895
22967
|
var DEFAULT_FLOOR_MS = 5e3;
|
|
@@ -22987,7 +23059,7 @@ function computeTreeSignature(paths) {
|
|
|
22987
23059
|
}
|
|
22988
23060
|
function makeStaleChecker(paths, opts = {}) {
|
|
22989
23061
|
const initial = computeTreeSignature(paths);
|
|
22990
|
-
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 &&
|
|
23062
|
+
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync49(opts.reloadSentinel);
|
|
22991
23063
|
}
|
|
22992
23064
|
function reloadSentinelFor(stateDir) {
|
|
22993
23065
|
return join52(stateDir, "reload-requested");
|
|
@@ -23238,8 +23310,8 @@ async function runOnePassUnguarded(o) {
|
|
|
23238
23310
|
set.add(run3);
|
|
23239
23311
|
void run3.finally(() => set.delete(run3));
|
|
23240
23312
|
}
|
|
23241
|
-
const
|
|
23242
|
-
const tookTicket =
|
|
23313
|
+
const reported2 = TICKET_ROLES.has(role) ? await settled : true;
|
|
23314
|
+
const tookTicket = reported2 !== false;
|
|
23243
23315
|
if (tookTicket) started.push(role);
|
|
23244
23316
|
if (roleCap(role) <= 1) {
|
|
23245
23317
|
live.delete(role);
|
|
@@ -23576,7 +23648,7 @@ import { createHash as createHash10 } from "crypto";
|
|
|
23576
23648
|
import {
|
|
23577
23649
|
chmodSync as chmodSync4,
|
|
23578
23650
|
closeSync as closeSync6,
|
|
23579
|
-
existsSync as
|
|
23651
|
+
existsSync as existsSync50,
|
|
23580
23652
|
fstatSync,
|
|
23581
23653
|
openSync as openSync6,
|
|
23582
23654
|
readSync as readSync3,
|
|
@@ -23947,7 +24019,7 @@ function lastLines2(path, n) {
|
|
|
23947
24019
|
}
|
|
23948
24020
|
function readTail(live, opts) {
|
|
23949
24021
|
const max = opts.maxBytes ?? MAX_TAIL_BYTES;
|
|
23950
|
-
if (!
|
|
24022
|
+
if (!existsSync50(live)) {
|
|
23951
24023
|
return { lines: [], cursor: opts.after ?? encodeCursor({ dev: 0, ino: 0, off: 0 }), reset: false };
|
|
23952
24024
|
}
|
|
23953
24025
|
const cur = opts.after ? decodeCursor(opts.after) : null;
|
|
@@ -24104,7 +24176,7 @@ async function route(deps, method, url, req, abort) {
|
|
|
24104
24176
|
const run3 = decodeURIComponent(p.slice("/streams/".length));
|
|
24105
24177
|
if (!RUN_NAME_RE.test(run3) || run3.includes("..") || /\.(pid|meta\.json|events\.jsonl)$/.test(run3)) return err2(400, "not a run name");
|
|
24106
24178
|
const file = join53(deps.stateDir, "streams", run3.endsWith(".jsonl") ? run3 : `${run3}.jsonl`);
|
|
24107
|
-
if (!
|
|
24179
|
+
if (!existsSync50(file)) return err2(404, "no such run");
|
|
24108
24180
|
return tailReply(file, url.searchParams, false, abort);
|
|
24109
24181
|
}
|
|
24110
24182
|
if ((p === "/pause" || p === "/resume") && post3) {
|
|
@@ -24148,7 +24220,7 @@ function send(res, r) {
|
|
|
24148
24220
|
res.end(body);
|
|
24149
24221
|
}
|
|
24150
24222
|
async function socketIsStale(path) {
|
|
24151
|
-
if (!
|
|
24223
|
+
if (!existsSync50(path)) return false;
|
|
24152
24224
|
return new Promise((resolve8) => {
|
|
24153
24225
|
const c = createConnection(path);
|
|
24154
24226
|
c.once("connect", () => {
|
|
@@ -24167,7 +24239,7 @@ async function startControlServer(deps) {
|
|
|
24167
24239
|
return null;
|
|
24168
24240
|
}
|
|
24169
24241
|
if (await socketIsStale(path)) rmSync23(path, { force: true });
|
|
24170
|
-
else if (
|
|
24242
|
+
else if (existsSync50(path)) {
|
|
24171
24243
|
log(`control API not started: ${path} is in use by another process`);
|
|
24172
24244
|
return null;
|
|
24173
24245
|
}
|
|
@@ -24260,7 +24332,7 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/
|
|
|
24260
24332
|
|
|
24261
24333
|
// src/periscope-endpoints.ts
|
|
24262
24334
|
import { randomBytes as randomBytes3 } from "crypto";
|
|
24263
|
-
import { chmodSync as chmodSync5, existsSync as
|
|
24335
|
+
import { chmodSync as chmodSync5, existsSync as existsSync51, mkdirSync as mkdirSync37, readdirSync as readdirSync19, readFileSync as readFileSync45, rmSync as rmSync24, writeFileSync as writeFileSync37 } from "fs";
|
|
24264
24336
|
import { join as join54 } from "path";
|
|
24265
24337
|
var secretDir = (stateDir) => join54(stateDir, "periscope-mcp");
|
|
24266
24338
|
var secretFile = (stateDir, id) => join54(secretDir(stateDir), `${id}.json`);
|
|
@@ -24303,7 +24375,7 @@ function removePeriscopeEndpointRecord(stateDir, ticketRecordId) {
|
|
|
24303
24375
|
}
|
|
24304
24376
|
function listPeriscopeEndpoints(stateDir) {
|
|
24305
24377
|
const dir = endpointDir(stateDir);
|
|
24306
|
-
if (!
|
|
24378
|
+
if (!existsSync51(dir)) return [];
|
|
24307
24379
|
const out = [];
|
|
24308
24380
|
for (const name of readdirSync19(dir)) {
|
|
24309
24381
|
if (!name.endsWith(".json")) continue;
|
|
@@ -25286,7 +25358,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
25286
25358
|
} else {
|
|
25287
25359
|
const toleratesNoRoutes = command === "credential" || command === "forge" || command === "repos" || command === "ship" || command === "disconnect" && flag("reset") || command === "deprovision" || command === "connect" && flag("sign-out");
|
|
25288
25360
|
const configless = command === "connect" && flag("sign-out") || command === "disconnect" && flag("reset") || command === "deprovision";
|
|
25289
|
-
if (configless && !
|
|
25361
|
+
if (configless && !existsSync52(findConfigFile(CREW_HOME))) {
|
|
25290
25362
|
cfg = loadConfig(CREW_HOME, firstRunConfigPath(), { text: "ship: {}\n", allowNoRoutes: true });
|
|
25291
25363
|
} else {
|
|
25292
25364
|
cfg = loadConfig(CREW_HOME, void 0, toleratesNoRoutes ? { allowNoRoutes: true } : {});
|
|
@@ -25630,7 +25702,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
25630
25702
|
if (host === "linux" && hasSystemd) {
|
|
25631
25703
|
const label = labelFor(CREW_HOME, "run");
|
|
25632
25704
|
const staleTimer = join56(homedir11(), ".config", "systemd", "user", `${label}.timer`);
|
|
25633
|
-
if (
|
|
25705
|
+
if (existsSync52(staleTimer)) {
|
|
25634
25706
|
process.stdout.write(
|
|
25635
25707
|
`removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
|
|
25636
25708
|
`
|
|
@@ -25659,7 +25731,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
25659
25731
|
const { settled: doctorRepos, errors: refusedRepos } = await settleRepos(reposOf(c), (t) => t.name, (t) => resolveOneRepo(c, t));
|
|
25660
25732
|
for (const e of refusedRepos) problems.push(`repo ${e.repo}: ${e.detail}`);
|
|
25661
25733
|
for (const r of doctorRepos) {
|
|
25662
|
-
if (!
|
|
25734
|
+
if (!existsSync52(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
|
|
25663
25735
|
if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
|
|
25664
25736
|
readiness.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
|
|
25665
25737
|
}
|
|
@@ -25686,7 +25758,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
25686
25758
|
}
|
|
25687
25759
|
async function doctorFix(o) {
|
|
25688
25760
|
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))));
|
|
25689
|
-
const unitExists = (p) =>
|
|
25761
|
+
const unitExists = (p) => existsSync52(p);
|
|
25690
25762
|
const crontab = o.host === "linux" ? spawnSync11("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
|
|
25691
25763
|
const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run", installContext()), unitExists, crontab) : true;
|
|
25692
25764
|
const plan = planFix({ routes, schedulerAbsent, host: o.host });
|
|
@@ -25838,9 +25910,25 @@ ${changed2.map((c) => ` - ${c}
|
|
|
25838
25910
|
}
|
|
25839
25911
|
}
|
|
25840
25912
|
const precheckTickets = /* @__PURE__ */ new Map();
|
|
25913
|
+
const scope0 = (c, t) => releaseLockScope(c.route, t.name);
|
|
25841
25914
|
async function releasePhase(c, target, opts = {}) {
|
|
25842
25915
|
const scope = releaseLockScope(c.route, target.name);
|
|
25843
25916
|
const remit = emit.forRoute(c.route);
|
|
25917
|
+
if (lacksCheckout(cfg.ship, target.dir)) {
|
|
25918
|
+
let mode = null;
|
|
25919
|
+
const settings = cacheSettingsOf(cfg.ship.sandbox);
|
|
25920
|
+
const mirror = mirrorRepoOfTarget(target);
|
|
25921
|
+
if (settings && mirror) {
|
|
25922
|
+
try {
|
|
25923
|
+
mode = (await readMirrorContract(new SandboxCache(settings), mirror, c.branch?.base ?? "main"))?.release.mode ?? null;
|
|
25924
|
+
} catch (e) {
|
|
25925
|
+
if (firstReport(`${scope0(c, target)}:contract`)) remit.warn(`${target.name}: could not read the contract from the mirror: ${e.message}`, { step: "release" });
|
|
25926
|
+
}
|
|
25927
|
+
}
|
|
25928
|
+
const note = noCheckoutReleaseNote(target.name, mode);
|
|
25929
|
+
if (firstReport(`${scope0(c, target)}:release`)) remit.emit(note, { step: "release" });
|
|
25930
|
+
return { scope: releaseLockScope(c.route, target.name), tests: "skipped", outcome: "skipped", detail: note };
|
|
25931
|
+
}
|
|
25844
25932
|
const relLock = dryRun ? void 0 : state.acquire(`release-${scope}`);
|
|
25845
25933
|
if (relLock && !relLock.ok) {
|
|
25846
25934
|
remit.emit(`a previous release (pid ${relLock.heldBy}) is still running \u2014 skipping`, { step: "release" });
|
|
@@ -26100,10 +26188,10 @@ ${changed2.map((c) => ` - ${c}
|
|
|
26100
26188
|
if (outcome.gateRed && !dryRun) {
|
|
26101
26189
|
try {
|
|
26102
26190
|
const g = outcome.gateRed;
|
|
26103
|
-
const
|
|
26191
|
+
const reported2 = await tracker.events("gate_red");
|
|
26104
26192
|
const seat = releaseSeat(c);
|
|
26105
26193
|
for (const t of gateRedTickets(outcome.merged, outcome.decision.merges)) {
|
|
26106
|
-
if (alreadyReported(t, g,
|
|
26194
|
+
if (alreadyReported(t, g, reported2)) continue;
|
|
26107
26195
|
await tracker.postEvent(t.id, gateRedComment(g), seat, "gate_red");
|
|
26108
26196
|
remit.warn(`test gate red for ${g.count} cycles \u2014 noted on the ticket`, { ticket: t.issue_id, step: "merge" });
|
|
26109
26197
|
}
|
|
@@ -26114,10 +26202,10 @@ ${changed2.map((c) => ` - ${c}
|
|
|
26114
26202
|
}
|
|
26115
26203
|
if (outcome.localRefSkips?.length && !dryRun) {
|
|
26116
26204
|
try {
|
|
26117
|
-
const
|
|
26205
|
+
const reported2 = await tracker.events("local_ref_skip");
|
|
26118
26206
|
const seat = releaseSeat(c);
|
|
26119
26207
|
for (const s of outcome.localRefSkips) {
|
|
26120
|
-
if (localRefSkipReported(s.ticket, s.problem,
|
|
26208
|
+
if (localRefSkipReported(s.ticket, s.problem, reported2)) continue;
|
|
26121
26209
|
await tracker.postEvent(s.ticket.id, localRefSkipComment(cfg.ship.name, s.problem), seat, "local_ref_skip");
|
|
26122
26210
|
}
|
|
26123
26211
|
} catch (e) {
|
|
@@ -27187,7 +27275,7 @@ ${cu.tail.trimEnd()}
|
|
|
27187
27275
|
`);
|
|
27188
27276
|
if (!dryRun) {
|
|
27189
27277
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
27190
|
-
const raw =
|
|
27278
|
+
const raw = existsSync52(resolvedPath) ? JSON.parse(readFileSync46(resolvedPath, "utf8")) : {};
|
|
27191
27279
|
raw.agentSkills = result.agentSkills;
|
|
27192
27280
|
mkdirSync38(dirOf2(resolvedPath), { recursive: true });
|
|
27193
27281
|
writeFileSync38(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
@@ -27292,8 +27380,8 @@ ${cu.tail.trimEnd()}
|
|
|
27292
27380
|
try {
|
|
27293
27381
|
const a = parseHooksArgs(positional.slice(2), routeNamesOf(configText));
|
|
27294
27382
|
const dir = repoDirOf(configText, dirname34(cfg.configFile), a.route, a.name);
|
|
27295
|
-
if (!
|
|
27296
|
-
const files = { exists: (r) =>
|
|
27383
|
+
if (!existsSync52(dir)) throw new ReposError(`${dir} does not exist on disk`);
|
|
27384
|
+
const files = { exists: (r) => existsSync52(resolvePath(dir, r)), list: (r) => {
|
|
27297
27385
|
try {
|
|
27298
27386
|
return readdirSync20(resolvePath(dir, r));
|
|
27299
27387
|
} catch {
|
|
@@ -27411,7 +27499,7 @@ ${cu.tail.trimEnd()}
|
|
|
27411
27499
|
if (sub === "list" && !routeName) usage2();
|
|
27412
27500
|
if (sub === "add") {
|
|
27413
27501
|
try {
|
|
27414
|
-
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) =>
|
|
27502
|
+
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync52(resolvePath(p.replace(/^~(?=\/|$)/, homedir11()))));
|
|
27415
27503
|
routeName = a.route;
|
|
27416
27504
|
addPath = a.path;
|
|
27417
27505
|
addName = a.name;
|
|
@@ -27441,7 +27529,7 @@ ${cu.tail.trimEnd()}
|
|
|
27441
27529
|
const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
|
|
27442
27530
|
if (sub === "list" || sub === "add") {
|
|
27443
27531
|
const liveRoute = cfg.routes.find((r) => r.route === routeName);
|
|
27444
|
-
if (liveRoute?.resolved?.reposModelId &&
|
|
27532
|
+
if (liveRoute?.resolved?.reposModelId && existsSync52(resolvedFile) && !dryRun) {
|
|
27445
27533
|
try {
|
|
27446
27534
|
await hydrateApiKeys([liveRoute]);
|
|
27447
27535
|
persistRepoRows(cfg.ship.stateDir, liveRoute.route, await new Tracker(liveRoute, cfg.ship).repoRowsForMap());
|
|
@@ -27450,7 +27538,7 @@ ${cu.tail.trimEnd()}
|
|
|
27450
27538
|
}
|
|
27451
27539
|
}
|
|
27452
27540
|
let resolvedRemotes;
|
|
27453
|
-
if (
|
|
27541
|
+
if (existsSync52(resolvedFile)) {
|
|
27454
27542
|
try {
|
|
27455
27543
|
resolvedRemotes = JSON.parse(readFileSync46(resolvedFile, "utf8"));
|
|
27456
27544
|
} catch {
|
|
@@ -27459,7 +27547,7 @@ ${cu.tail.trimEnd()}
|
|
|
27459
27547
|
const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
|
|
27460
27548
|
const text2 = configText;
|
|
27461
27549
|
const base = dirname34(cfg.configFile);
|
|
27462
|
-
const isCheckout = (d) =>
|
|
27550
|
+
const isCheckout = (d) => existsSync52(`${d}/.git`);
|
|
27463
27551
|
try {
|
|
27464
27552
|
if (sub === "list") {
|
|
27465
27553
|
if (flag("json")) {
|
|
@@ -27469,7 +27557,7 @@ ${cu.tail.trimEnd()}
|
|
|
27469
27557
|
base,
|
|
27470
27558
|
resolved: resolvedRemotes,
|
|
27471
27559
|
originOf,
|
|
27472
|
-
exists: (d) =>
|
|
27560
|
+
exists: (d) => existsSync52(d),
|
|
27473
27561
|
isCheckout,
|
|
27474
27562
|
repoHasHooks: (d) => REPO_CONFIG_FILENAMES.some((f) => {
|
|
27475
27563
|
try {
|
|
@@ -27491,7 +27579,7 @@ ${cu.tail.trimEnd()}
|
|
|
27491
27579
|
for (const r of repos) {
|
|
27492
27580
|
const origin2 = originOf(r.dir);
|
|
27493
27581
|
const row = matchRepoName(origin2, resolvedRemotes);
|
|
27494
|
-
const note = !
|
|
27582
|
+
const note = !existsSync52(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
|
|
27495
27583
|
process.stdout.write(`${r.name} ${r.dir} ${note}
|
|
27496
27584
|
`);
|
|
27497
27585
|
}
|
|
@@ -27503,9 +27591,9 @@ ${cu.tail.trimEnd()}
|
|
|
27503
27591
|
` : `${human}
|
|
27504
27592
|
`);
|
|
27505
27593
|
const plan2 = planRepoClone({ text: text2, base, route: routeName, name: addName, resolved: resolvedRemotes, dir: value("dir"), isCheckout });
|
|
27506
|
-
if (
|
|
27594
|
+
if (existsSync52(plan2.target) && (existsSync52(`${plan2.target}/.git`) || readdirSync20(plan2.target).length > 0)) {
|
|
27507
27595
|
throw new ReposError(
|
|
27508
|
-
|
|
27596
|
+
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`
|
|
27509
27597
|
);
|
|
27510
27598
|
}
|
|
27511
27599
|
if (dryRun) {
|
|
@@ -27530,7 +27618,7 @@ ${cu.tail.trimEnd()}
|
|
|
27530
27618
|
break;
|
|
27531
27619
|
}
|
|
27532
27620
|
const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir11()));
|
|
27533
|
-
const top =
|
|
27621
|
+
const top = existsSync52(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
|
|
27534
27622
|
if (!top) throw new ReposError(`${target} is not a git checkout`);
|
|
27535
27623
|
const origin = originOf(top);
|
|
27536
27624
|
const plan = planRepoAdd({
|
|
@@ -27582,7 +27670,7 @@ ${cu.tail.trimEnd()}
|
|
|
27582
27670
|
const created = await rowTracker.createRepoRow(rowPayload);
|
|
27583
27671
|
process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
|
|
27584
27672
|
`);
|
|
27585
|
-
if (
|
|
27673
|
+
if (existsSync52(resolvedFile)) {
|
|
27586
27674
|
const raw = JSON.parse(readFileSync46(resolvedFile, "utf8"));
|
|
27587
27675
|
raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
|
|
27588
27676
|
raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
|
|
@@ -28004,7 +28092,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
28004
28092
|
}
|
|
28005
28093
|
}
|
|
28006
28094
|
let previous;
|
|
28007
|
-
const hadPreviousFile =
|
|
28095
|
+
const hadPreviousFile = existsSync52(resolvedPath);
|
|
28008
28096
|
if (hadPreviousFile) {
|
|
28009
28097
|
try {
|
|
28010
28098
|
previous = JSON.parse(readFileSync46(resolvedPath, "utf8"));
|
|
@@ -28149,7 +28237,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
28149
28237
|
let mintOutcomes = [];
|
|
28150
28238
|
if (!dryRun && previous?.agentPersonas && existingRoute?.resolved) {
|
|
28151
28239
|
try {
|
|
28152
|
-
mintOutcomes = await mintMissingSeatKeys(
|
|
28240
|
+
mintOutcomes = await mintMissingSeatKeys(withFoundSeats(existingRoute, found, previous.releaseAgentId), { userAgent: cfg.ship.userAgent, shipName: cfg.ship.name });
|
|
28153
28241
|
for (const o of mintOutcomes) process.stderr.write(`${describeMintOutcome(o)}
|
|
28154
28242
|
`);
|
|
28155
28243
|
for (const p of await probeSeatReads({ ...existingRoute, resolved: { ...existingRoute.resolved, seats: found.seats } }, { userAgent: cfg.ship.userAgent })) {
|
|
@@ -28884,7 +28972,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
28884
28972
|
prepare: async (path) => {
|
|
28885
28973
|
for (const f of repoWt.config.worktrees.copy) {
|
|
28886
28974
|
const src = join56(repoWt.dir, f);
|
|
28887
|
-
if (!
|
|
28975
|
+
if (!existsSync52(src) || existsSync52(join56(path, f))) continue;
|
|
28888
28976
|
mkdirSync38(dirOf2(join56(path, f)), { recursive: true });
|
|
28889
28977
|
copyFileSync2(src, join56(path, f));
|
|
28890
28978
|
}
|
|
@@ -28951,11 +29039,12 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
28951
29039
|
});
|
|
28952
29040
|
};
|
|
28953
29041
|
if (subP === "list") {
|
|
28954
|
-
const
|
|
28955
|
-
|
|
28956
|
-
|
|
28957
|
-
|
|
28958
|
-
);
|
|
29042
|
+
const listRows = (await Promise.all(routesP.map(gather))).flat().map((r) => ({ ...r, pointedAt: readPointedAt(r.worktree) }));
|
|
29043
|
+
const serving = new Map(await Promise.all(listRows.map(async (r) => {
|
|
29044
|
+
const port = urlPort(r.url);
|
|
29045
|
+
return [r.id, port === void 0 ? false : await portListening(port)];
|
|
29046
|
+
})));
|
|
29047
|
+
const entries = periscopeListEntries(listRows, /* @__PURE__ */ new Date(), (r) => serving.get(r.id) ?? false);
|
|
28959
29048
|
process.stdout.write(jsonP ? `${JSON.stringify(entries)}
|
|
28960
29049
|
` : `${formatPeriscopeList(entries)}
|
|
28961
29050
|
`);
|
|
@@ -29106,7 +29195,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
29106
29195
|
const shellR = hit && resolveShell(hit.repo.config.shell ?? cfg.ship.shell);
|
|
29107
29196
|
const resumed = await runPeriscopeResume({
|
|
29108
29197
|
find: async () => hit && { row: hit.row, tag: hit.tag },
|
|
29109
|
-
worktreeExists: (p) =>
|
|
29198
|
+
worktreeExists: (p) => existsSync52(p),
|
|
29110
29199
|
prepareDropFile,
|
|
29111
29200
|
startProxy: (wt, url, n) => launchProxy(crewInvocation(CREW_HOME), wt, url, Number(n), { host: hit.row.host || "localhost", inject: hit.repo.config.periscope?.inspector !== "repo" }),
|
|
29112
29201
|
caps: periscopeCaps(cfg.ship),
|
|
@@ -29203,7 +29292,7 @@ ${where.join("\n")}`);
|
|
|
29203
29292
|
prepare: async (path) => {
|
|
29204
29293
|
for (const f of repoP.config.worktrees.copy) {
|
|
29205
29294
|
const src = join56(repoP.dir, f);
|
|
29206
|
-
if (!
|
|
29295
|
+
if (!existsSync52(src)) continue;
|
|
29207
29296
|
mkdirSync38(dirOf2(join56(path, f)), { recursive: true });
|
|
29208
29297
|
copyFileSync2(src, join56(path, f));
|
|
29209
29298
|
}
|
|
@@ -29695,7 +29784,7 @@ ${where.join("\n")}`);
|
|
|
29695
29784
|
}
|
|
29696
29785
|
if (row) {
|
|
29697
29786
|
await tracker.deleteRepoRow(row.id);
|
|
29698
|
-
if (
|
|
29787
|
+
if (existsSync52(resolvedFile)) {
|
|
29699
29788
|
const raw = JSON.parse(readFileSync46(resolvedFile, "utf8"));
|
|
29700
29789
|
if (raw.repoNames) delete raw.repoNames[row.id];
|
|
29701
29790
|
if (raw.repoRemotes) delete raw.repoRemotes[row.id];
|
|
@@ -29703,13 +29792,13 @@ ${where.join("\n")}`);
|
|
|
29703
29792
|
`);
|
|
29704
29793
|
}
|
|
29705
29794
|
}
|
|
29706
|
-
if (
|
|
29795
|
+
if (existsSync52(join56(dir, ".git")) && basename16(dir) === SAMPLE_REPO) rmSync25(dir, { recursive: true, force: true });
|
|
29707
29796
|
say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
|
|
29708
29797
|
break;
|
|
29709
29798
|
}
|
|
29710
29799
|
const projectId = route2.resolved.areaId ?? resolveRepoProject(await tracker.projectRows(), value("project"));
|
|
29711
|
-
const fresh = !
|
|
29712
|
-
if (
|
|
29800
|
+
const fresh = !existsSync52(join56(dir, ".git"));
|
|
29801
|
+
if (existsSync52(dir) && fresh && readdirSync20(dir).length > 0) fail2(`${dir} exists and is not a git checkout \u2014 move it aside`);
|
|
29713
29802
|
if (dryRun) {
|
|
29714
29803
|
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`);
|
|
29715
29804
|
break;
|
|
@@ -29735,7 +29824,7 @@ ${where.join("\n")}`);
|
|
|
29735
29824
|
spec: { platform: "unix", releaseMode: "integrate", ciProvider: "none" }
|
|
29736
29825
|
});
|
|
29737
29826
|
repoId = (await tracker.createRepoRow(payload)).id;
|
|
29738
|
-
if (
|
|
29827
|
+
if (existsSync52(resolvedFile)) {
|
|
29739
29828
|
const raw = JSON.parse(readFileSync46(resolvedFile, "utf8"));
|
|
29740
29829
|
raw.repoNames = { ...raw.repoNames, [repoId]: SAMPLE_REPO };
|
|
29741
29830
|
raw.repoRemotes = { ...raw.repoRemotes, [repoId]: SAMPLE_REMOTE };
|
|
@@ -30091,7 +30180,7 @@ watermark: ${state.watermark()}
|
|
|
30091
30180
|
}
|
|
30092
30181
|
for (const r of repos) {
|
|
30093
30182
|
const remote = r.config.branch.remote;
|
|
30094
|
-
if (
|
|
30183
|
+
if (existsSync52(r.dir)) {
|
|
30095
30184
|
pushAuth.push({
|
|
30096
30185
|
route: c.route,
|
|
30097
30186
|
repo: r.name,
|
|
@@ -30192,7 +30281,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
30192
30281
|
}
|
|
30193
30282
|
try {
|
|
30194
30283
|
const evFile = eventFileFor(cfg.ship.stateDir);
|
|
30195
|
-
const evSize =
|
|
30284
|
+
const evSize = existsSync52(evFile) ? statSync16(evFile).size : 0;
|
|
30196
30285
|
process.stdout.write(
|
|
30197
30286
|
evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
|
|
30198
30287
|
` : "events log: ok\n"
|
package/package.json
CHANGED