@tablation/crew 0.93.0 → 0.94.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +363 -110
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -363,11 +363,11 @@ var Emitter = class _Emitter {
|
|
|
363
363
|
const result = await fn();
|
|
364
364
|
this.emit(message, { data: { ms: this.now().getTime() - started, ok: true } });
|
|
365
365
|
return result;
|
|
366
|
-
} catch (
|
|
367
|
-
this.error(`${message} \u2014 ${
|
|
366
|
+
} catch (err3) {
|
|
367
|
+
this.error(`${message} \u2014 ${err3.message}`, {
|
|
368
368
|
data: { ms: this.now().getTime() - started, ok: false }
|
|
369
369
|
});
|
|
370
|
-
throw
|
|
370
|
+
throw err3;
|
|
371
371
|
}
|
|
372
372
|
}
|
|
373
373
|
};
|
|
@@ -3549,10 +3549,10 @@ function runScript(script, opts) {
|
|
|
3549
3549
|
child.stdout.on("data", absorb);
|
|
3550
3550
|
child.stderr.on("data", absorb);
|
|
3551
3551
|
const timer = opts.timeoutMs ? setTimeout(() => child.kill("SIGKILL"), opts.timeoutMs) : void 0;
|
|
3552
|
-
child.on("error", (
|
|
3552
|
+
child.on("error", (err3) => {
|
|
3553
3553
|
if (timer) clearTimeout(timer);
|
|
3554
3554
|
resolvePromise({ code: 127, output: `${output}
|
|
3555
|
-
crew: cannot run ${shell.bin}: ${
|
|
3555
|
+
crew: cannot run ${shell.bin}: ${err3.message}` });
|
|
3556
3556
|
});
|
|
3557
3557
|
child.on("close", (code) => {
|
|
3558
3558
|
if (timer) clearTimeout(timer);
|
|
@@ -4248,18 +4248,18 @@ function gitRaw(cwd, args) {
|
|
|
4248
4248
|
try {
|
|
4249
4249
|
return execFileSync2("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
4250
4250
|
} catch (e) {
|
|
4251
|
-
const
|
|
4252
|
-
const why = [
|
|
4253
|
-
throw new GitError(`git ${args.join(" ")}: ${why ||
|
|
4251
|
+
const err3 = e;
|
|
4252
|
+
const why = [err3.stderr, err3.stdout].map(streamText).filter(Boolean).join(" / ");
|
|
4253
|
+
throw new GitError(`git ${args.join(" ")}: ${why || err3.message.trim()}`);
|
|
4254
4254
|
}
|
|
4255
4255
|
}
|
|
4256
4256
|
function git(cwd, args) {
|
|
4257
4257
|
try {
|
|
4258
4258
|
return execFileSync2("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
4259
4259
|
} catch (e) {
|
|
4260
|
-
const
|
|
4261
|
-
const why = [
|
|
4262
|
-
throw new GitError(`git ${args.join(" ")}: ${why ||
|
|
4260
|
+
const err3 = e;
|
|
4261
|
+
const why = [err3.stderr, err3.stdout].map(streamText).filter(Boolean).join(" / ");
|
|
4262
|
+
throw new GitError(`git ${args.join(" ")}: ${why || err3.message.trim()}`);
|
|
4263
4263
|
}
|
|
4264
4264
|
}
|
|
4265
4265
|
function gitOk(cwd, args) {
|
|
@@ -4701,9 +4701,9 @@ function ensureRepoCheckout(dir, remote, opts = {}) {
|
|
|
4701
4701
|
try {
|
|
4702
4702
|
execFileSync2("git", ["clone", url, dir], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
4703
4703
|
} catch (e) {
|
|
4704
|
-
const
|
|
4705
|
-
const why = [
|
|
4706
|
-
throw new GitError(`git clone ${url} ${dir}: ${why ||
|
|
4704
|
+
const err3 = e;
|
|
4705
|
+
const why = [err3.stderr, err3.stdout].map(streamText).filter(Boolean).join(" / ");
|
|
4706
|
+
throw new GitError(`git clone ${url} ${dir}: ${why || err3.message.trim()}`);
|
|
4707
4707
|
}
|
|
4708
4708
|
cloneRecorder?.(dir);
|
|
4709
4709
|
return true;
|
|
@@ -4736,8 +4736,8 @@ function acquireCheckoutLock(dir, timeoutMs = 3e4) {
|
|
|
4736
4736
|
}
|
|
4737
4737
|
};
|
|
4738
4738
|
} catch (e) {
|
|
4739
|
-
const
|
|
4740
|
-
if (
|
|
4739
|
+
const err3 = e;
|
|
4740
|
+
if (err3.code !== "EEXIST") throw err3;
|
|
4741
4741
|
try {
|
|
4742
4742
|
if (Date.now() - statSync5(lockPath).mtimeMs > CHECKOUT_LOCK_STALE_MS) {
|
|
4743
4743
|
try {
|
|
@@ -5129,8 +5129,8 @@ function probePushAuth(o, run2 = realGit, baseEnv = process.env) {
|
|
|
5129
5129
|
run2(o.dir, ["push", "--dry-run", o.remote, `HEAD:refs/heads/${PROBE_BRANCH}`], env);
|
|
5130
5130
|
return { state: "ok" };
|
|
5131
5131
|
} catch (e) {
|
|
5132
|
-
const
|
|
5133
|
-
const raw = (
|
|
5132
|
+
const err3 = e;
|
|
5133
|
+
const raw = (err3.stderr ? String(err3.stderr) : err3.message).trim().split("\n").filter(Boolean).slice(-2).join(" / ");
|
|
5134
5134
|
const detail = raw || "push dry-run failed";
|
|
5135
5135
|
const f = o.forge;
|
|
5136
5136
|
if (!f) return { state: "failed", detail, hint: pushAuthHint(o.remoteUrl) };
|
|
@@ -7359,8 +7359,8 @@ var Tracker = class {
|
|
|
7359
7359
|
if (res.status === 404) return [];
|
|
7360
7360
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
7361
7361
|
rows = await res.json();
|
|
7362
|
-
} catch (
|
|
7363
|
-
this.onReadFailure?.(`reading ${type} events failed: ${
|
|
7362
|
+
} catch (err3) {
|
|
7363
|
+
this.onReadFailure?.(`reading ${type} events failed: ${err3 instanceof Error ? err3.message : String(err3)}`);
|
|
7364
7364
|
return [];
|
|
7365
7365
|
}
|
|
7366
7366
|
return rows.filter((e) => e.event_type === type).sort((a, b) => Date.parse(b.occurred_at ?? b.created_at ?? "") - Date.parse(a.occurred_at ?? a.created_at ?? ""));
|
|
@@ -9779,10 +9779,10 @@ function spawnAgent(plan, emit, hintMoved) {
|
|
|
9779
9779
|
const tail3 = new FileTail(plan.streamPath);
|
|
9780
9780
|
const timer = setInterval(() => obs.feed(tail3.poll()), TAIL_POLL_MS);
|
|
9781
9781
|
let spawnFailed = false;
|
|
9782
|
-
child.on("error", (
|
|
9782
|
+
child.on("error", (err3) => {
|
|
9783
9783
|
spawnFailed = true;
|
|
9784
9784
|
clearInterval(timer);
|
|
9785
|
-
reject(new AgentSpawnError(`cannot run ${plan.bin}: ${
|
|
9785
|
+
reject(new AgentSpawnError(`cannot run ${plan.bin}: ${err3.message}`));
|
|
9786
9786
|
});
|
|
9787
9787
|
child.on("close", async (code) => {
|
|
9788
9788
|
clearInterval(timer);
|
|
@@ -9793,8 +9793,8 @@ function spawnAgent(plan, emit, hintMoved) {
|
|
|
9793
9793
|
const { ms: ms3, handoffPending } = await finishRun(id, obs, emit, startedAt, { code: code ?? 1 }, plan.agentLog?.apiKey, hintMoved);
|
|
9794
9794
|
resolve6({ code: code ?? 1, ms: ms3, ...handoffPending ? { handoffPending } : {} });
|
|
9795
9795
|
});
|
|
9796
|
-
child.stdin?.on("error", (
|
|
9797
|
-
emit.warn(`agent closed stdin before the prompt was delivered: ${
|
|
9796
|
+
child.stdin?.on("error", (err3) => {
|
|
9797
|
+
emit.warn(`agent closed stdin before the prompt was delivered: ${err3.message}`, { step: "agent" });
|
|
9798
9798
|
});
|
|
9799
9799
|
child.stdin?.end(plan.prompt);
|
|
9800
9800
|
});
|
|
@@ -9881,8 +9881,8 @@ async function resolveSeatKey(route2, role, getStore = getSessionStore) {
|
|
|
9881
9881
|
}
|
|
9882
9882
|
}
|
|
9883
9883
|
function safeStderrLine(e) {
|
|
9884
|
-
const
|
|
9885
|
-
const raw = typeof
|
|
9884
|
+
const err3 = e;
|
|
9885
|
+
const raw = typeof err3?.stderr === "string" && err3.stderr.trim() ? err3.stderr : typeof err3?.message === "string" ? err3.message : "";
|
|
9886
9886
|
for (const line of raw.split("\n")) {
|
|
9887
9887
|
const t = line.trim();
|
|
9888
9888
|
if (!t || /^Command failed:/i.test(t) || /(^|\s)-w(\s|$)/.test(t)) continue;
|
|
@@ -10378,6 +10378,7 @@ var VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
|
10378
10378
|
"service",
|
|
10379
10379
|
"topic",
|
|
10380
10380
|
"assignee",
|
|
10381
|
+
"requested-by",
|
|
10381
10382
|
"issue",
|
|
10382
10383
|
"shell",
|
|
10383
10384
|
"resume"
|
|
@@ -10806,12 +10807,12 @@ async function checkCrewFloor(o, deps = {}) {
|
|
|
10806
10807
|
function openOutdated(state) {
|
|
10807
10808
|
return state.shipAttention().previous().find((i) => i.key === CREW_OUTDATED_KEY)?.message ?? null;
|
|
10808
10809
|
}
|
|
10809
|
-
function isUpgradeRequired(
|
|
10810
|
-
return
|
|
10810
|
+
function isUpgradeRequired(err3) {
|
|
10811
|
+
return err3 instanceof TablationApiError3 && err3.status === 426;
|
|
10811
10812
|
}
|
|
10812
|
-
async function raiseFromUpgradeRequired(o,
|
|
10813
|
+
async function raiseFromUpgradeRequired(o, err3) {
|
|
10813
10814
|
const have = sentVersion(o.ship) ?? "unknown";
|
|
10814
|
-
const floor = /(\d+\.\d+\.\d+)/.exec(
|
|
10815
|
+
const floor = /(\d+\.\d+\.\d+)/.exec(err3.message.replace(have, ""))?.[1] ?? "a newer version";
|
|
10815
10816
|
await raiseShipAttention(o, outdatedItem(have, floor));
|
|
10816
10817
|
}
|
|
10817
10818
|
|
|
@@ -11626,8 +11627,8 @@ async function decideCycle(o) {
|
|
|
11626
11627
|
const optional = async (what, read, fallback) => {
|
|
11627
11628
|
try {
|
|
11628
11629
|
return await read;
|
|
11629
|
-
} catch (
|
|
11630
|
-
emit.warn(`reading ${what} failed: ${
|
|
11630
|
+
} catch (err3) {
|
|
11631
|
+
emit.warn(`reading ${what} failed: ${err3 instanceof Error ? err3.message : String(err3)}`);
|
|
11631
11632
|
return fallback;
|
|
11632
11633
|
}
|
|
11633
11634
|
};
|
|
@@ -16633,7 +16634,7 @@ async function runAgentSuggest(o) {
|
|
|
16633
16634
|
const signal = o.signal ? AbortSignal.any([o.signal, timeout]) : timeout;
|
|
16634
16635
|
const run2 = await new Promise((resolve6) => {
|
|
16635
16636
|
let out = "";
|
|
16636
|
-
let
|
|
16637
|
+
let err3 = "";
|
|
16637
16638
|
const child = spawn4(bin, args, { cwd: o.dir, env, detached: true, stdio: ["pipe", "pipe", "pipe"] });
|
|
16638
16639
|
let settled = false;
|
|
16639
16640
|
const settle = (r) => {
|
|
@@ -16650,7 +16651,7 @@ async function runAgentSuggest(o) {
|
|
|
16650
16651
|
}
|
|
16651
16652
|
child.stdout.destroy();
|
|
16652
16653
|
child.stderr.destroy();
|
|
16653
|
-
settle({ code: null, out, err:
|
|
16654
|
+
settle({ code: null, out, err: err3, aborted: true });
|
|
16654
16655
|
};
|
|
16655
16656
|
if (signal.aborted) onAbort();
|
|
16656
16657
|
else signal.addEventListener("abort", onAbort, { once: true });
|
|
@@ -16658,10 +16659,10 @@ async function runAgentSuggest(o) {
|
|
|
16658
16659
|
out += d.toString();
|
|
16659
16660
|
});
|
|
16660
16661
|
child.stderr.on("data", (d) => {
|
|
16661
|
-
|
|
16662
|
+
err3 += d.toString();
|
|
16662
16663
|
});
|
|
16663
|
-
child.on("error", (e) => settle({ code: null, out, err:
|
|
16664
|
-
child.on("close", (code) => settle({ code, out, err:
|
|
16664
|
+
child.on("error", (e) => settle({ code: null, out, err: err3, spawnError: e, aborted: false }));
|
|
16665
|
+
child.on("close", (code) => settle({ code, out, err: err3, aborted: false }));
|
|
16665
16666
|
child.stdin.on("error", () => {
|
|
16666
16667
|
});
|
|
16667
16668
|
child.stdin.end(buildAgentPrompt(o.sniff));
|
|
@@ -17163,7 +17164,7 @@ function readPeriscopePrior(stateDir, ticketRecordId) {
|
|
|
17163
17164
|
var DEFAULT_MAX_PERISCOPES2 = 3;
|
|
17164
17165
|
var DEFAULT_PERISCOPE_IDLE_MINUTES2 = 120;
|
|
17165
17166
|
function parsePeriscopeUpArgs(positional2, flags) {
|
|
17166
|
-
const usage2 = "usage: crew periscope up [[route] REPO] [--topic TEXT | --issue NNN] [--isolate] [--host HOST] [--assignee CREW_ROW_ID] [--no-open] [--json]";
|
|
17167
|
+
const usage2 = "usage: crew periscope up [[route] REPO] [--topic TEXT | --issue NNN] [--isolate] [--host HOST] [--assignee CREW_ROW_ID] [--requested-by ID] [--no-open] [--json]";
|
|
17167
17168
|
const rest = positional2.slice(2);
|
|
17168
17169
|
if (rest.length > 2) throw new PeriscopeError(usage2);
|
|
17169
17170
|
const topic = flags.topic?.trim();
|
|
@@ -17184,6 +17185,7 @@ ${usage2}`);
|
|
|
17184
17185
|
isolate: flags.isolate,
|
|
17185
17186
|
...flags.host ? { host: flags.host } : {},
|
|
17186
17187
|
...flags.assignee ? { assignee: flags.assignee } : {},
|
|
17188
|
+
...flags.requestedBy?.trim() ? { requestedBy: flags.requestedBy.trim() } : {},
|
|
17187
17189
|
json: flags.json
|
|
17188
17190
|
};
|
|
17189
17191
|
}
|
|
@@ -17670,8 +17672,8 @@ function ensureShipSshKeypair(stateDir) {
|
|
|
17670
17672
|
"-q"
|
|
17671
17673
|
], { stdio: ["ignore", "ignore", "pipe"] });
|
|
17672
17674
|
} catch (e) {
|
|
17673
|
-
const
|
|
17674
|
-
throw new SshKeyError(`ssh-keygen failed: ${
|
|
17675
|
+
const err3 = e;
|
|
17676
|
+
throw new SshKeyError(`ssh-keygen failed: ${err3.stderr?.toString().trim() || err3.message}`);
|
|
17675
17677
|
}
|
|
17676
17678
|
chmodSync3(privateKeyPath, 384);
|
|
17677
17679
|
}
|
|
@@ -17697,9 +17699,9 @@ var realExec = (cmd, args, opts) => {
|
|
|
17697
17699
|
try {
|
|
17698
17700
|
return execFileSync9(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: opts?.timeoutMs });
|
|
17699
17701
|
} catch (e) {
|
|
17700
|
-
const
|
|
17701
|
-
const why = [
|
|
17702
|
-
throw new Error(`${cmd} ${args.join(" ")}: ${why ||
|
|
17702
|
+
const err3 = e;
|
|
17703
|
+
const why = [err3.stderr, err3.stdout].map((x) => (x ?? "").trim()).filter(Boolean).join(" / ");
|
|
17704
|
+
throw new Error(`${cmd} ${args.join(" ")}: ${why || err3.message.trim()}`);
|
|
17703
17705
|
}
|
|
17704
17706
|
};
|
|
17705
17707
|
var DOCKER_INFO_TIMEOUT_MS = 1e4;
|
|
@@ -18272,8 +18274,8 @@ function runSyncDaemonFromEnv(env = process.env) {
|
|
|
18272
18274
|
const secret = requireEnv(env, "PASSENGER_SYNC_SECRET");
|
|
18273
18275
|
const targets = JSON.parse(requireEnv(env, "PASSENGER_SYNC_TARGETS"));
|
|
18274
18276
|
const server = createSyncDaemonServer(targets, secret);
|
|
18275
|
-
server.on("error", (
|
|
18276
|
-
console.error(`Passenger sync daemon failed to bind 0.0.0.0:${port}: ${
|
|
18277
|
+
server.on("error", (err3) => {
|
|
18278
|
+
console.error(`Passenger sync daemon failed to bind 0.0.0.0:${port}: ${err3 instanceof Error ? err3.message : String(err3)}`);
|
|
18277
18279
|
process.exit(1);
|
|
18278
18280
|
});
|
|
18279
18281
|
server.listen(port, "0.0.0.0", () => {
|
|
@@ -18390,22 +18392,22 @@ function codeOf(e) {
|
|
|
18390
18392
|
const code = e?.code;
|
|
18391
18393
|
return typeof code === "string" ? code : void 0;
|
|
18392
18394
|
}
|
|
18393
|
-
function isTransientError(
|
|
18394
|
-
if (!(
|
|
18395
|
-
const msg =
|
|
18395
|
+
function isTransientError(err3) {
|
|
18396
|
+
if (!(err3 instanceof Error)) return false;
|
|
18397
|
+
const msg = err3.message;
|
|
18396
18398
|
if (msg === "fetch failed" || msg.startsWith("fetch failed")) return true;
|
|
18397
|
-
for (let e =
|
|
18399
|
+
for (let e = err3, depth = 0; e && depth < 4; e = e.cause, depth++) {
|
|
18398
18400
|
const code = codeOf(e);
|
|
18399
18401
|
if (code && NETWORK_CODES.includes(code)) return true;
|
|
18400
18402
|
}
|
|
18401
18403
|
if (NETWORK_CODES.some((c) => msg.includes(c))) return true;
|
|
18402
18404
|
if (/\b(?:HTTP|status)\s*:?\s*(502|503|504)\b/i.test(msg) || /^(502|503|504)\b/.test(msg)) return true;
|
|
18403
|
-
if (
|
|
18405
|
+
if (err3 instanceof SyntaxError && /Unexpected token '?<|is not valid JSON|Unexpected end of JSON/.test(msg)) return true;
|
|
18404
18406
|
return false;
|
|
18405
18407
|
}
|
|
18406
|
-
function describeCause(
|
|
18407
|
-
const first = (
|
|
18408
|
-
const code = codeOf(
|
|
18408
|
+
function describeCause(err3) {
|
|
18409
|
+
const first = (err3 instanceof Error ? err3.message : String(err3)).split("\n")[0];
|
|
18410
|
+
const code = codeOf(err3?.cause);
|
|
18409
18411
|
return code && !first.includes(code) ? `${first} (${code})` : first;
|
|
18410
18412
|
}
|
|
18411
18413
|
|
|
@@ -18434,22 +18436,22 @@ async function runDaemonLoop(o) {
|
|
|
18434
18436
|
o.onRecovered?.({ downMs: now() - downSince });
|
|
18435
18437
|
transientRun = 0;
|
|
18436
18438
|
}
|
|
18437
|
-
} catch (
|
|
18438
|
-
if (isTransientError(
|
|
18439
|
+
} catch (err3) {
|
|
18440
|
+
if (isTransientError(err3)) {
|
|
18439
18441
|
passes++;
|
|
18440
18442
|
if (transientRun === 0) downSince = now();
|
|
18441
18443
|
transientRun++;
|
|
18442
18444
|
const wait = transientBackoffMs(transientRun);
|
|
18443
18445
|
const downMs = now() - downSince;
|
|
18444
|
-
o.onTransient?.({ err:
|
|
18445
|
-
if (transientRun === OUTAGE_ESCALATE_AFTER) o.onOutage?.({ err:
|
|
18446
|
+
o.onTransient?.({ err: err3, consecutive: transientRun, backoffMs: wait, downMs });
|
|
18447
|
+
if (transientRun === OUTAGE_ESCALATE_AFTER) o.onOutage?.({ err: err3, consecutive: transientRun, downMs });
|
|
18446
18448
|
if (o.maxPasses !== void 0 && passes >= o.maxPasses) return "maxPasses";
|
|
18447
18449
|
if (o.signal?.aborted) return "aborted";
|
|
18448
18450
|
await sleep(wait);
|
|
18449
18451
|
continue;
|
|
18450
18452
|
}
|
|
18451
18453
|
passes++;
|
|
18452
|
-
o.onPassError?.(
|
|
18454
|
+
o.onPassError?.(err3);
|
|
18453
18455
|
if (o.maxPasses !== void 0 && passes >= o.maxPasses) return "maxPasses";
|
|
18454
18456
|
if (o.signal?.aborted) return "aborted";
|
|
18455
18457
|
await sleep(backoff);
|
|
@@ -18568,7 +18570,7 @@ async function runOnePassUnguarded(o) {
|
|
|
18568
18570
|
await hydrateApiKeys([o.route], o.getSessionStore);
|
|
18569
18571
|
const emit = o.newEmitter();
|
|
18570
18572
|
const decide = o.decide ?? decideCycle;
|
|
18571
|
-
const
|
|
18573
|
+
const spawn9 = o.spawnRoleAgent ?? runRoleAgent;
|
|
18572
18574
|
const breaker = o.spawnBreaker ?? new SpawnBreaker();
|
|
18573
18575
|
const ctx = { route: o.route, ship: o.ship, state: o.state, emit, getSessionStore: o.getSessionStore };
|
|
18574
18576
|
if (o.configFile) await reconcileRouteShipName({ route: o.route, ship: o.ship, configFile: o.configFile, emit });
|
|
@@ -18672,7 +18674,7 @@ async function runOnePassUnguarded(o) {
|
|
|
18672
18674
|
const settled = new Promise((resolve6) => {
|
|
18673
18675
|
ready = resolve6;
|
|
18674
18676
|
});
|
|
18675
|
-
const run2 =
|
|
18677
|
+
const run2 = spawn9(role, decision, ctx, { ready }).then(() => {
|
|
18676
18678
|
breaker.recordSuccess(role);
|
|
18677
18679
|
clearShipAttention(o.state, `role_parked:${role}`);
|
|
18678
18680
|
}).catch(async (e) => {
|
|
@@ -19020,7 +19022,213 @@ import {
|
|
|
19020
19022
|
import { createConnection } from "net";
|
|
19021
19023
|
import { createServer } from "http";
|
|
19022
19024
|
import { basename as basename12, dirname as dirname21, join as join43 } from "path";
|
|
19023
|
-
import { execFile, spawn as
|
|
19025
|
+
import { execFile, spawn as spawn8 } from "child_process";
|
|
19026
|
+
|
|
19027
|
+
// src/periscope-control.ts
|
|
19028
|
+
import { spawn as spawn7 } from "child_process";
|
|
19029
|
+
var reply = (status2, body) => ({ status: status2, body });
|
|
19030
|
+
var err = (status2, message) => reply(status2, { error: message });
|
|
19031
|
+
var TICKET_RE = /^(?:[A-Za-z]+-)?\d+$/;
|
|
19032
|
+
var MAX_FIELD = 200;
|
|
19033
|
+
function plainText(v, field, required) {
|
|
19034
|
+
if (v === void 0 || v === null || v === "") return required ? { error: `${field} is required` } : void 0;
|
|
19035
|
+
if (typeof v !== "string") return { error: `${field} must be a string` };
|
|
19036
|
+
const t = v.trim();
|
|
19037
|
+
if (!t) return required ? { error: `${field} is required` } : void 0;
|
|
19038
|
+
if (t.length > MAX_FIELD || /[\r\n\0]/.test(t)) return { error: `${field} must be one line of at most ${MAX_FIELD} characters` };
|
|
19039
|
+
if (t.startsWith("-")) return { error: `${field} must not start with "-"` };
|
|
19040
|
+
return t;
|
|
19041
|
+
}
|
|
19042
|
+
var isErr = (v) => typeof v === "object" && v !== null && "error" in v;
|
|
19043
|
+
function pickRoute(h, named) {
|
|
19044
|
+
const hosting = h.routes.filter((r) => r.hostPeriscopes);
|
|
19045
|
+
if (named !== void 0) {
|
|
19046
|
+
const hit = hosting.find((r) => r.route === named);
|
|
19047
|
+
return hit ?? err(404, "not found");
|
|
19048
|
+
}
|
|
19049
|
+
if (hosting.length === 1) return hosting[0];
|
|
19050
|
+
return err(400, `route is required (this ship hosts periscopes on: ${hosting.map((r) => r.route).join(", ")})`);
|
|
19051
|
+
}
|
|
19052
|
+
function cliMessage(r) {
|
|
19053
|
+
const lines = r.stderr.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
19054
|
+
const plain = lines.filter((l) => !l.startsWith("{"));
|
|
19055
|
+
const last = plain[plain.length - 1] ?? lines[lines.length - 1] ?? `crew exited with status ${r.code}`;
|
|
19056
|
+
return last.replace(/^crew periscope:\s*/, "");
|
|
19057
|
+
}
|
|
19058
|
+
function failureStatus(r) {
|
|
19059
|
+
const m = cliMessage(r);
|
|
19060
|
+
if (/^no periscope for ticket /.test(m)) return 404;
|
|
19061
|
+
if (/already hosts \d+ live periscope/.test(m)) return 409;
|
|
19062
|
+
return r.code === 1 ? 422 : 500;
|
|
19063
|
+
}
|
|
19064
|
+
function lastJsonLine(stdout) {
|
|
19065
|
+
const lines = stdout.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
19066
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
19067
|
+
try {
|
|
19068
|
+
const v = JSON.parse(lines[i]);
|
|
19069
|
+
if (v && typeof v === "object" && !Array.isArray(v)) return v;
|
|
19070
|
+
} catch {
|
|
19071
|
+
}
|
|
19072
|
+
}
|
|
19073
|
+
return null;
|
|
19074
|
+
}
|
|
19075
|
+
function parseLine(line) {
|
|
19076
|
+
try {
|
|
19077
|
+
const v = JSON.parse(line);
|
|
19078
|
+
return v && typeof v === "object" && !Array.isArray(v) ? v : null;
|
|
19079
|
+
} catch {
|
|
19080
|
+
return null;
|
|
19081
|
+
}
|
|
19082
|
+
}
|
|
19083
|
+
async function execute(h, args, streaming) {
|
|
19084
|
+
const finish = (r) => {
|
|
19085
|
+
if (r.code !== 0) return { status: failureStatus(r), body: { error: cliMessage(r) } };
|
|
19086
|
+
const done = lastJsonLine(r.stdout);
|
|
19087
|
+
return done ? { status: 200, body: done } : { status: 502, body: { error: "the command finished without a result" } };
|
|
19088
|
+
};
|
|
19089
|
+
if (!streaming) {
|
|
19090
|
+
const f = finish(await h.run(args, () => {
|
|
19091
|
+
}));
|
|
19092
|
+
return reply(f.status, f.body);
|
|
19093
|
+
}
|
|
19094
|
+
return {
|
|
19095
|
+
status: 200,
|
|
19096
|
+
body: null,
|
|
19097
|
+
stream: async (write) => {
|
|
19098
|
+
const r = await h.run(args, (line) => {
|
|
19099
|
+
const ev = parseLine(line);
|
|
19100
|
+
if (ev) write(ev);
|
|
19101
|
+
});
|
|
19102
|
+
const f = finish(r);
|
|
19103
|
+
write(f.status === 200 ? f.body : { event: "error", status: f.status, message: f.body.error });
|
|
19104
|
+
}
|
|
19105
|
+
};
|
|
19106
|
+
}
|
|
19107
|
+
var isPeriscopePath = (p) => p === "/periscopes" || p.startsWith("/periscopes/");
|
|
19108
|
+
async function handlePeriscopes(h, q) {
|
|
19109
|
+
if (!h || !h.routes.some((r) => r.hostPeriscopes)) return err(404, "not found");
|
|
19110
|
+
const down = /^\/periscopes\/([^/]+)\/down$/.exec(q.path);
|
|
19111
|
+
const isUp = q.path === "/periscopes" && q.method === "POST";
|
|
19112
|
+
const isList = q.path === "/periscopes" && q.method === "GET";
|
|
19113
|
+
if (q.path === "/periscopes" && !isUp && !isList) return err(405, "method not allowed");
|
|
19114
|
+
if (down && q.method !== "POST") return err(405, "method not allowed");
|
|
19115
|
+
if (q.path !== "/periscopes" && !down) return err(404, "not found");
|
|
19116
|
+
const member = async (workspaceId) => {
|
|
19117
|
+
const v = await h.verifyIn(q.token, workspaceId);
|
|
19118
|
+
if (v === "ok") return void 0;
|
|
19119
|
+
if (v === "unavailable") return err(503, "could not verify the key right now");
|
|
19120
|
+
return err(403, "this key does not belong to the workspace of this route");
|
|
19121
|
+
};
|
|
19122
|
+
if (isList) {
|
|
19123
|
+
const named = q.query.get("route") ?? void 0;
|
|
19124
|
+
const candidates2 = named === void 0 ? h.routes.filter((r) => r.hostPeriscopes) : [pickRoute(h, named)];
|
|
19125
|
+
const rows = [];
|
|
19126
|
+
for (const c of candidates2) {
|
|
19127
|
+
if ("status" in c) return c;
|
|
19128
|
+
const denied2 = await member(c.workspaceId);
|
|
19129
|
+
if (denied2) {
|
|
19130
|
+
if (named !== void 0) return denied2;
|
|
19131
|
+
continue;
|
|
19132
|
+
}
|
|
19133
|
+
const r = await h.run(["periscope", "list", c.route, "--json"], () => {
|
|
19134
|
+
});
|
|
19135
|
+
if (r.code !== 0) return err(failureStatus(r), cliMessage(r));
|
|
19136
|
+
try {
|
|
19137
|
+
const list = JSON.parse(r.stdout);
|
|
19138
|
+
if (Array.isArray(list)) for (const e of list) rows.push({ route: c.route, ...e });
|
|
19139
|
+
} catch {
|
|
19140
|
+
return err(502, "the list command did not answer JSON");
|
|
19141
|
+
}
|
|
19142
|
+
}
|
|
19143
|
+
return reply(200, { periscopes: rows });
|
|
19144
|
+
}
|
|
19145
|
+
const b = q.body;
|
|
19146
|
+
if (!b) return err(400, "body must be a small JSON object");
|
|
19147
|
+
const routeName = plainText(b.route, "route", false);
|
|
19148
|
+
if (isErr(routeName)) return err(400, routeName.error);
|
|
19149
|
+
const picked = pickRoute(h, routeName);
|
|
19150
|
+
if ("status" in picked) return picked;
|
|
19151
|
+
const denied = await member(picked.workspaceId);
|
|
19152
|
+
if (denied) return denied;
|
|
19153
|
+
if (down) {
|
|
19154
|
+
const ticket = decodeURIComponent(down[1]);
|
|
19155
|
+
if (!TICKET_RE.test(ticket)) return err(400, "ticket must be a ticket number such as 123 or CREW-123");
|
|
19156
|
+
if (b.outcome !== "keep" && b.outcome !== "discard") return err(400, 'outcome must be "keep" or "discard"');
|
|
19157
|
+
if (b.allowEmpty !== void 0 && typeof b.allowEmpty !== "boolean") return err(400, "allowEmpty must be a boolean");
|
|
19158
|
+
return execute(h, [
|
|
19159
|
+
"periscope",
|
|
19160
|
+
"down",
|
|
19161
|
+
picked.route,
|
|
19162
|
+
ticket,
|
|
19163
|
+
`--${b.outcome}`,
|
|
19164
|
+
...b.allowEmpty === true ? ["--allow-empty"] : [],
|
|
19165
|
+
"--json"
|
|
19166
|
+
], q.wantsStream);
|
|
19167
|
+
}
|
|
19168
|
+
const repo = plainText(b.repo, "repo", true);
|
|
19169
|
+
const topic = plainText(b.topic, "topic", true);
|
|
19170
|
+
const requestedBy = plainText(b.requestedBy, "requestedBy", false);
|
|
19171
|
+
for (const v of [repo, topic, requestedBy]) if (isErr(v)) return err(400, v.error);
|
|
19172
|
+
if (b.isolate !== void 0 && typeof b.isolate !== "boolean") return err(400, "isolate must be a boolean");
|
|
19173
|
+
if (!h.previewHost) return err(409, "this ship has no ship.previewHost, so a Passenger could not reach a periscope");
|
|
19174
|
+
const live = await h.run(["periscope", "list", picked.route, "--json"], () => {
|
|
19175
|
+
});
|
|
19176
|
+
if (live.code === 0) {
|
|
19177
|
+
try {
|
|
19178
|
+
const n = JSON.parse(live.stdout).length;
|
|
19179
|
+
if (n >= h.maxPeriscopes) return err(409, `this ship already hosts ${n} live periscope(s) (ship.maxPeriscopes is ${h.maxPeriscopes}) \u2014 discard one first`);
|
|
19180
|
+
} catch {
|
|
19181
|
+
}
|
|
19182
|
+
}
|
|
19183
|
+
return execute(h, [
|
|
19184
|
+
"periscope",
|
|
19185
|
+
"up",
|
|
19186
|
+
picked.route,
|
|
19187
|
+
repo,
|
|
19188
|
+
"--topic",
|
|
19189
|
+
topic,
|
|
19190
|
+
"--host",
|
|
19191
|
+
h.previewHost,
|
|
19192
|
+
...requestedBy ? ["--requested-by", requestedBy] : [],
|
|
19193
|
+
...b.isolate === true ? ["--isolate"] : [],
|
|
19194
|
+
"--no-open",
|
|
19195
|
+
"--json"
|
|
19196
|
+
], q.wantsStream);
|
|
19197
|
+
}
|
|
19198
|
+
function runCrew(args, onLine, timeoutMs = 20 * 6e4) {
|
|
19199
|
+
return new Promise((resolve6) => {
|
|
19200
|
+
const child = spawn7(process.execPath, [process.argv[1] ?? "", ...args], { stdio: ["ignore", "pipe", "pipe"] });
|
|
19201
|
+
let stdout = "";
|
|
19202
|
+
let stderr = "";
|
|
19203
|
+
let pending = "";
|
|
19204
|
+
child.stdout.on("data", (d) => {
|
|
19205
|
+
stdout += d.toString("utf8");
|
|
19206
|
+
});
|
|
19207
|
+
child.stderr.on("data", (d) => {
|
|
19208
|
+
const text = d.toString("utf8");
|
|
19209
|
+
stderr += text;
|
|
19210
|
+
pending += text;
|
|
19211
|
+
let i;
|
|
19212
|
+
while ((i = pending.indexOf("\n")) >= 0) {
|
|
19213
|
+
const line = pending.slice(0, i).trim();
|
|
19214
|
+
pending = pending.slice(i + 1);
|
|
19215
|
+
if (line) onLine(line);
|
|
19216
|
+
}
|
|
19217
|
+
});
|
|
19218
|
+
const t = setTimeout(() => child.kill("SIGTERM"), timeoutMs);
|
|
19219
|
+
child.on("error", (e) => {
|
|
19220
|
+
clearTimeout(t);
|
|
19221
|
+
resolve6({ code: 127, stdout, stderr: `${stderr}${e.message}
|
|
19222
|
+
` });
|
|
19223
|
+
});
|
|
19224
|
+
child.on("close", (code) => {
|
|
19225
|
+
clearTimeout(t);
|
|
19226
|
+
resolve6({ code: code ?? 1, stdout, stderr });
|
|
19227
|
+
});
|
|
19228
|
+
});
|
|
19229
|
+
}
|
|
19230
|
+
|
|
19231
|
+
// src/control-api.ts
|
|
19024
19232
|
var PAUSABLE = ["dev", "design", "qa"];
|
|
19025
19233
|
var CONTROL_SOCKET_NAME = "control.sock";
|
|
19026
19234
|
var controlSocketFor = (stateDir) => join43(stateDir, CONTROL_SOCKET_NAME);
|
|
@@ -19051,11 +19259,26 @@ var ControlAuth = class {
|
|
|
19051
19259
|
}
|
|
19052
19260
|
return verdict;
|
|
19053
19261
|
}
|
|
19054
|
-
|
|
19262
|
+
/**
|
|
19263
|
+
* Like `verify`, but the token must belong to `workspaceId` itself, not just
|
|
19264
|
+
* to some workspace this ship serves. The ship's own keys pass.
|
|
19265
|
+
*/
|
|
19266
|
+
async verifyIn(token, workspaceId) {
|
|
19267
|
+
if (!token) return "unauthorized";
|
|
19268
|
+
for (const k of this.o.shipKeys()) if (k && secretsMatch(token, k)) return "ok";
|
|
19269
|
+
const now = (this.o.now ?? Date.now)();
|
|
19270
|
+
const id = `${workspaceId}:${createHash9("sha256").update(token).digest("hex")}`;
|
|
19271
|
+
const hit = this.cache.get(id);
|
|
19272
|
+
if (hit && now - hit.at < (this.o.cacheMs ?? 6e4)) return hit.verdict;
|
|
19273
|
+
const verdict = await this.ask(token, [workspaceId]);
|
|
19274
|
+
if (verdict !== "unavailable") this.cache.set(id, { verdict, at: now });
|
|
19275
|
+
return verdict;
|
|
19276
|
+
}
|
|
19277
|
+
async ask(token, workspaceIds = this.o.workspaceIds) {
|
|
19055
19278
|
const doFetch = this.o.fetch ?? fetch;
|
|
19056
19279
|
const base = this.o.baseUrl.replace(/\/+$/, "");
|
|
19057
19280
|
let forbidden = false;
|
|
19058
|
-
for (const ws of
|
|
19281
|
+
for (const ws of workspaceIds) {
|
|
19059
19282
|
let res;
|
|
19060
19283
|
try {
|
|
19061
19284
|
res = await doFetch(`${base}/api/auth/me?workspaceId=${encodeURIComponent(ws)}`, {
|
|
@@ -19206,8 +19429,8 @@ function waitForChange(dir, ms3, signal) {
|
|
|
19206
19429
|
}
|
|
19207
19430
|
});
|
|
19208
19431
|
}
|
|
19209
|
-
var
|
|
19210
|
-
var
|
|
19432
|
+
var reply2 = (status2, body) => ({ status: status2, body });
|
|
19433
|
+
var err2 = (status2, message) => reply2(status2, { error: message });
|
|
19211
19434
|
function pauseDoc(state) {
|
|
19212
19435
|
return {
|
|
19213
19436
|
crew: state.isPaused(),
|
|
@@ -19247,26 +19470,38 @@ async function readJsonBody(req) {
|
|
|
19247
19470
|
async function tailReply(path, q, rotated, abort) {
|
|
19248
19471
|
const after = q.get("after") ?? void 0;
|
|
19249
19472
|
const tail3 = q.has("tail") ? Number(q.get("tail")) : void 0;
|
|
19250
|
-
if (tail3 !== void 0 && !(Number.isInteger(tail3) && tail3 >= 0 && tail3 <= 1e3)) return
|
|
19473
|
+
if (tail3 !== void 0 && !(Number.isInteger(tail3) && tail3 >= 0 && tail3 <= 1e3)) return err2(400, "tail must be an integer from 0 to 1000");
|
|
19251
19474
|
const wait = q.has("wait") ? Number(q.get("wait")) : 0;
|
|
19252
|
-
if (!(Number.isFinite(wait) && wait >= 0)) return
|
|
19253
|
-
if (after !== void 0 && !decodeCursor(after)) return
|
|
19475
|
+
if (!(Number.isFinite(wait) && wait >= 0)) return err2(400, "wait must be a number of seconds");
|
|
19476
|
+
if (after !== void 0 && !decodeCursor(after)) return err2(400, "after is not a cursor this API issued");
|
|
19254
19477
|
const deadline = Date.now() + Math.min(wait, MAX_WAIT_SECONDS) * 1e3;
|
|
19255
19478
|
let r = readTail(path, { after, tail: tail3, rotated });
|
|
19256
19479
|
while (after && !r.lines.length && !r.reset && Date.now() < deadline && !abort.aborted) {
|
|
19257
19480
|
await waitForChange(dirname21(path), deadline - Date.now(), abort);
|
|
19258
19481
|
r = readTail(path, { after: r.cursor, rotated });
|
|
19259
19482
|
}
|
|
19260
|
-
return
|
|
19483
|
+
return reply2(200, r);
|
|
19261
19484
|
}
|
|
19262
19485
|
async function route(deps, method, url, req, abort) {
|
|
19263
19486
|
const p = url.pathname.replace(/\/+$/, "") || "/";
|
|
19264
19487
|
const get2 = method === "GET";
|
|
19265
19488
|
const post3 = method === "POST";
|
|
19489
|
+
if (isPeriscopePath(p)) {
|
|
19490
|
+
const token = /^Bearer (.+)$/.exec(req.headers.authorization ?? "")?.[1]?.trim() ?? "";
|
|
19491
|
+
const body = method === "POST" ? await readJsonBody(req) : {};
|
|
19492
|
+
return handlePeriscopes(deps.periscopes, {
|
|
19493
|
+
method,
|
|
19494
|
+
path: p,
|
|
19495
|
+
query: url.searchParams,
|
|
19496
|
+
token,
|
|
19497
|
+
body,
|
|
19498
|
+
wantsStream: url.searchParams.get("stream") === "1" || (req.headers.accept ?? "").includes("application/x-ndjson")
|
|
19499
|
+
});
|
|
19500
|
+
}
|
|
19266
19501
|
if (p === "/status" && get2) {
|
|
19267
19502
|
const doc = await deps.status();
|
|
19268
|
-
if (!doc) return
|
|
19269
|
-
return
|
|
19503
|
+
if (!doc) return err2(502, "could not produce the status document");
|
|
19504
|
+
return reply2(200, {
|
|
19270
19505
|
...doc,
|
|
19271
19506
|
daemon: {
|
|
19272
19507
|
alive: true,
|
|
@@ -19278,41 +19513,50 @@ async function route(deps, method, url, req, abort) {
|
|
|
19278
19513
|
});
|
|
19279
19514
|
}
|
|
19280
19515
|
if (p === "/events" && get2) return tailReply(eventFileFor(deps.stateDir), url.searchParams, true, abort);
|
|
19281
|
-
if (p === "/streams" && get2) return
|
|
19516
|
+
if (p === "/streams" && get2) return reply2(200, { streams: liveRuns(deps.stateDir).map(describeStream) });
|
|
19282
19517
|
if (p.startsWith("/streams/") && get2) {
|
|
19283
19518
|
const run2 = decodeURIComponent(p.slice("/streams/".length));
|
|
19284
|
-
if (!RUN_NAME_RE.test(run2) || run2.includes("..") || /\.(pid|meta\.json|events\.jsonl)$/.test(run2)) return
|
|
19519
|
+
if (!RUN_NAME_RE.test(run2) || run2.includes("..") || /\.(pid|meta\.json|events\.jsonl)$/.test(run2)) return err2(400, "not a run name");
|
|
19285
19520
|
const file = join43(deps.stateDir, "streams", run2.endsWith(".jsonl") ? run2 : `${run2}.jsonl`);
|
|
19286
|
-
if (!existsSync39(file)) return
|
|
19521
|
+
if (!existsSync39(file)) return err2(404, "no such run");
|
|
19287
19522
|
return tailReply(file, url.searchParams, false, abort);
|
|
19288
19523
|
}
|
|
19289
19524
|
if ((p === "/pause" || p === "/resume") && post3) {
|
|
19290
19525
|
const body = await readJsonBody(req);
|
|
19291
|
-
if (!body) return
|
|
19526
|
+
if (!body) return err2(400, "body must be a small JSON object");
|
|
19292
19527
|
const role = body.role;
|
|
19293
|
-
if (role !== void 0 && !PAUSABLE.includes(role)) return
|
|
19528
|
+
if (role !== void 0 && !PAUSABLE.includes(role)) return err2(400, `role must be one of ${PAUSABLE.join(", ")}`);
|
|
19294
19529
|
if (p === "/pause") deps.state.pause(role);
|
|
19295
19530
|
else deps.state.resume(role);
|
|
19296
|
-
return
|
|
19531
|
+
return reply2(200, { ok: true, pause: pauseDoc(deps.state) });
|
|
19297
19532
|
}
|
|
19298
19533
|
if ((p === "/release/pause" || p === "/release/resume") && post3) {
|
|
19299
19534
|
deps.state.setReleasePaused(p === "/release/pause");
|
|
19300
|
-
return
|
|
19535
|
+
return reply2(200, { ok: true, pause: pauseDoc(deps.state) });
|
|
19301
19536
|
}
|
|
19302
19537
|
const sched = /^\/scheduler\/(start|stop|restart)$/.exec(p);
|
|
19303
19538
|
if (sched && post3) {
|
|
19304
19539
|
const r = deps.scheduler(sched[1]);
|
|
19305
|
-
return r.accepted ?
|
|
19540
|
+
return r.accepted ? reply2(202, { accepted: true, detail: r.detail }) : err2(409, r.detail);
|
|
19306
19541
|
}
|
|
19307
19542
|
if (p === "/doctor" && get2) {
|
|
19308
19543
|
const doc = await deps.doctor();
|
|
19309
|
-
return doc ?
|
|
19544
|
+
return doc ? reply2(200, doc) : err2(502, "could not produce the doctor report");
|
|
19310
19545
|
}
|
|
19311
19546
|
const known = ["/status", "/events", "/streams", "/pause", "/resume", "/release/pause", "/release/resume", "/doctor"];
|
|
19312
|
-
if (known.includes(p) || p.startsWith("/streams/") || sched) return
|
|
19313
|
-
return
|
|
19547
|
+
if (known.includes(p) || p.startsWith("/streams/") || sched) return err2(405, "method not allowed");
|
|
19548
|
+
return err2(404, "not found");
|
|
19314
19549
|
}
|
|
19315
19550
|
function send(res, r) {
|
|
19551
|
+
if (r.stream) {
|
|
19552
|
+
res.writeHead(200, { "Content-Type": "application/x-ndjson", "Cache-Control": "no-store" });
|
|
19553
|
+
return r.stream((ev) => {
|
|
19554
|
+
if (!res.writableEnded) res.write(`${JSON.stringify(ev)}
|
|
19555
|
+
`);
|
|
19556
|
+
}).then(() => {
|
|
19557
|
+
res.end();
|
|
19558
|
+
});
|
|
19559
|
+
}
|
|
19316
19560
|
const body = JSON.stringify(r.body);
|
|
19317
19561
|
res.writeHead(r.status, { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body), "Cache-Control": "no-store" });
|
|
19318
19562
|
res.end(body);
|
|
@@ -19348,13 +19592,13 @@ async function startControlServer(deps) {
|
|
|
19348
19592
|
try {
|
|
19349
19593
|
const m = /^Bearer (.+)$/.exec(req.headers.authorization ?? "");
|
|
19350
19594
|
const verdict = await deps.auth.verify(m?.[1]?.trim() ?? "");
|
|
19351
|
-
if (verdict === "unauthorized") return send(res,
|
|
19352
|
-
if (verdict === "forbidden") return send(res,
|
|
19353
|
-
if (verdict === "unavailable") return send(res,
|
|
19354
|
-
send(res, await route(deps, req.method ?? "GET", new URL(req.url ?? "/", "http://ship"), req, abort.signal));
|
|
19595
|
+
if (verdict === "unauthorized") return send(res, err2(401, "invalid or revoked key"));
|
|
19596
|
+
if (verdict === "forbidden") return send(res, err2(403, "this key does not belong to a workspace this ship serves"));
|
|
19597
|
+
if (verdict === "unavailable") return send(res, err2(503, "could not verify the key right now"));
|
|
19598
|
+
await send(res, await route(deps, req.method ?? "GET", new URL(req.url ?? "/", "http://ship"), req, abort.signal));
|
|
19355
19599
|
} catch (e) {
|
|
19356
19600
|
log(`control API request failed: ${e.message}`);
|
|
19357
|
-
if (!res.headersSent) send(res,
|
|
19601
|
+
if (!res.headersSent) send(res, err2(500, "internal error"));
|
|
19358
19602
|
else res.end();
|
|
19359
19603
|
}
|
|
19360
19604
|
})();
|
|
@@ -19384,7 +19628,7 @@ async function startControlServer(deps) {
|
|
|
19384
19628
|
});
|
|
19385
19629
|
}
|
|
19386
19630
|
var shq2 = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
|
|
19387
|
-
function runDetached(commands, spawnFn =
|
|
19631
|
+
function runDetached(commands, spawnFn = spawn8) {
|
|
19388
19632
|
const script = `sleep 1; ${commands.map((c) => c.map(shq2).join(" ")).join("; ")}`;
|
|
19389
19633
|
const child = spawnFn("/bin/sh", ["-c", script], { detached: true, stdio: "ignore" });
|
|
19390
19634
|
child.on("error", () => {
|
|
@@ -20790,8 +21034,8 @@ ${cu.tail.trimEnd()}
|
|
|
20790
21034
|
`swept ${r.parked} parked, ${r.restored} restored${r.failed ? `, ${r.failed} failed` : ""}${r.contended ? `, ${r.contended} contended` : ""} (${e.route.route})`,
|
|
20791
21035
|
{ step: "sweep", data: r }
|
|
20792
21036
|
);
|
|
20793
|
-
} catch (
|
|
20794
|
-
remit.warn(`could not sweep ${e.route.route}: ${
|
|
21037
|
+
} catch (err3) {
|
|
21038
|
+
remit.warn(`could not sweep ${e.route.route}: ${err3.message}`, { step: "sweep" });
|
|
20795
21039
|
}
|
|
20796
21040
|
}
|
|
20797
21041
|
process.stdout.write(renderFleet(fleet, state));
|
|
@@ -21307,21 +21551,30 @@ ${cu.tail.trimEnd()}
|
|
|
21307
21551
|
noteDaemonStarted(cfg.ship.stateDir);
|
|
21308
21552
|
const spawnBreaker = new SpawnBreaker();
|
|
21309
21553
|
const controlRoutes = cfg.routes.filter((r) => r.resolved?.workspaceId);
|
|
21554
|
+
const controlAuth = new ControlAuth({
|
|
21555
|
+
baseUrl: route2.baseUrl || DEFAULT_BASE_URL,
|
|
21556
|
+
workspaceIds: [...new Set(controlRoutes.map((r) => r.resolved.workspaceId))],
|
|
21557
|
+
shipKeys: () => cfg.routes.flatMap((r) => {
|
|
21558
|
+
try {
|
|
21559
|
+
return [resolveApiKey(r)];
|
|
21560
|
+
} catch {
|
|
21561
|
+
return [];
|
|
21562
|
+
}
|
|
21563
|
+
}),
|
|
21564
|
+
userAgent: cfg.ship.userAgent
|
|
21565
|
+
});
|
|
21310
21566
|
const controlServer = await startControlServer({
|
|
21311
21567
|
stateDir: cfg.ship.stateDir,
|
|
21312
21568
|
state,
|
|
21313
|
-
auth:
|
|
21314
|
-
|
|
21315
|
-
|
|
21316
|
-
|
|
21317
|
-
|
|
21318
|
-
|
|
21319
|
-
|
|
21320
|
-
|
|
21321
|
-
|
|
21322
|
-
}),
|
|
21323
|
-
userAgent: cfg.ship.userAgent
|
|
21324
|
-
}),
|
|
21569
|
+
auth: controlAuth,
|
|
21570
|
+
// CREW-2859: Passenger periscopes, only on a route that sets hostPeriscopes.
|
|
21571
|
+
periscopes: {
|
|
21572
|
+
routes: controlRoutes.filter((r) => r.enabled).map((r) => ({ route: r.route, workspaceId: r.resolved.workspaceId, hostPeriscopes: r.hostPeriscopes === true })),
|
|
21573
|
+
previewHost: cfg.ship.previewHost,
|
|
21574
|
+
maxPeriscopes: cfg.ship.maxPeriscopes,
|
|
21575
|
+
run: runCrew,
|
|
21576
|
+
verifyIn: (token, ws) => controlAuth.verifyIn(token, ws)
|
|
21577
|
+
},
|
|
21325
21578
|
status: () => crewJson(["status", route2.route, "--json"]),
|
|
21326
21579
|
doctor: () => crewJson(["doctor", route2.route, "--json", "--offline"]),
|
|
21327
21580
|
scheduler: schedulerHandler(planDaemonControl(CREW_HOME, hostPlatform(), detectSystemd())),
|
|
@@ -21346,11 +21599,11 @@ ${cu.tail.trimEnd()}
|
|
|
21346
21599
|
`)
|
|
21347
21600
|
})
|
|
21348
21601
|
}),
|
|
21349
|
-
onTransient: ({ err:
|
|
21350
|
-
`tracker unavailable (${describeCause(
|
|
21602
|
+
onTransient: ({ err: err3, consecutive, backoffMs }) => emit.warn(
|
|
21603
|
+
`tracker unavailable (${describeCause(err3)}); attempt ${consecutive}, retrying in ${Math.round(backoffMs / 1e3)}s`
|
|
21351
21604
|
),
|
|
21352
|
-
onOutage: ({ err:
|
|
21353
|
-
const msg = `tracker unreachable for ${Math.max(1, Math.round(downMs / 6e4))} minutes: ${describeCause(
|
|
21605
|
+
onOutage: ({ err: err3, downMs }) => {
|
|
21606
|
+
const msg = `tracker unreachable for ${Math.max(1, Math.round(downMs / 6e4))} minutes: ${describeCause(err3)}`;
|
|
21354
21607
|
emit.error(msg);
|
|
21355
21608
|
void raiseShipAttention({ state, emit, route: route2, ship: cfg.ship }, {
|
|
21356
21609
|
kind: "tracker_unreachable",
|
|
@@ -21363,8 +21616,8 @@ ${cu.tail.trimEnd()}
|
|
|
21363
21616
|
clearShipAttention(state, TRACKER_OUTAGE_KEY);
|
|
21364
21617
|
emit.emit(`tracker is back after ${Math.max(1, Math.round(downMs / 1e3))}s`);
|
|
21365
21618
|
},
|
|
21366
|
-
onPassError: (
|
|
21367
|
-
`daemon pass failed, continuing: ${
|
|
21619
|
+
onPassError: (err3) => emit.error(
|
|
21620
|
+
`daemon pass failed, continuing: ${err3 instanceof Error ? err3.stack ?? err3.message : String(err3)}`
|
|
21368
21621
|
),
|
|
21369
21622
|
checkStale: makeStaleChecker(watchPaths, { reloadSentinel: reloadSentinelFor(cfg.ship.stateDir) }),
|
|
21370
21623
|
onPass: (result) => {
|
|
@@ -23289,7 +23542,7 @@ ${resumed.url}
|
|
|
23289
23542
|
`);
|
|
23290
23543
|
break;
|
|
23291
23544
|
}
|
|
23292
|
-
const parsedUp = parsePeriscopeUpArgs(positional, { topic: value("topic"), issue: value("issue"), host: value("host"), assignee: value("assignee"), isolate: flag("isolate"), json: jsonP });
|
|
23545
|
+
const parsedUp = parsePeriscopeUpArgs(positional, { topic: value("topic"), issue: value("issue"), host: value("host"), assignee: value("assignee"), requestedBy: value("requested-by"), isolate: flag("isolate"), json: jsonP });
|
|
23293
23546
|
let a;
|
|
23294
23547
|
if (parsedUp.repo !== void 0) a = { ...parsedUp, repo: parsedUp.repo };
|
|
23295
23548
|
else {
|
|
@@ -23347,7 +23600,7 @@ ${where.join("\n")}`);
|
|
|
23347
23600
|
title,
|
|
23348
23601
|
description: `A periscope: a throwaway preview of ${a.repo} for a viewer, opened by \`crew periscope up\`.`,
|
|
23349
23602
|
report_type: "feature",
|
|
23350
|
-
reporter_name: operatorName,
|
|
23603
|
+
reporter_name: a.requestedBy ?? operatorName,
|
|
23351
23604
|
[cP.columns.status]: cP.statuses.draft,
|
|
23352
23605
|
[cP.columns.assignee]: pairId,
|
|
23353
23606
|
...routeP.resolved.areaId ? { [cP.columns.slice]: routeP.resolved.areaId } : {},
|
package/package.json
CHANGED