@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.
Files changed (2) hide show
  1. package/dist/cli.js +363 -110
  2. 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 (err2) {
367
- this.error(`${message} \u2014 ${err2.message}`, {
366
+ } catch (err3) {
367
+ this.error(`${message} \u2014 ${err3.message}`, {
368
368
  data: { ms: this.now().getTime() - started, ok: false }
369
369
  });
370
- throw err2;
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", (err2) => {
3552
+ child.on("error", (err3) => {
3553
3553
  if (timer) clearTimeout(timer);
3554
3554
  resolvePromise({ code: 127, output: `${output}
3555
- crew: cannot run ${shell.bin}: ${err2.message}` });
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 err2 = e;
4252
- const why = [err2.stderr, err2.stdout].map(streamText).filter(Boolean).join(" / ");
4253
- throw new GitError(`git ${args.join(" ")}: ${why || err2.message.trim()}`);
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 err2 = e;
4261
- const why = [err2.stderr, err2.stdout].map(streamText).filter(Boolean).join(" / ");
4262
- throw new GitError(`git ${args.join(" ")}: ${why || err2.message.trim()}`);
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 err2 = e;
4705
- const why = [err2.stderr, err2.stdout].map(streamText).filter(Boolean).join(" / ");
4706
- throw new GitError(`git clone ${url} ${dir}: ${why || err2.message.trim()}`);
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 err2 = e;
4740
- if (err2.code !== "EEXIST") throw err2;
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 err2 = e;
5133
- const raw = (err2.stderr ? String(err2.stderr) : err2.message).trim().split("\n").filter(Boolean).slice(-2).join(" / ");
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 (err2) {
7363
- this.onReadFailure?.(`reading ${type} events failed: ${err2 instanceof Error ? err2.message : String(err2)}`);
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", (err2) => {
9782
+ child.on("error", (err3) => {
9783
9783
  spawnFailed = true;
9784
9784
  clearInterval(timer);
9785
- reject(new AgentSpawnError(`cannot run ${plan.bin}: ${err2.message}`));
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", (err2) => {
9797
- emit.warn(`agent closed stdin before the prompt was delivered: ${err2.message}`, { step: "agent" });
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 err2 = e;
9885
- const raw = typeof err2?.stderr === "string" && err2.stderr.trim() ? err2.stderr : typeof err2?.message === "string" ? err2.message : "";
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(err2) {
10810
- return err2 instanceof TablationApiError3 && err2.status === 426;
10810
+ function isUpgradeRequired(err3) {
10811
+ return err3 instanceof TablationApiError3 && err3.status === 426;
10811
10812
  }
10812
- async function raiseFromUpgradeRequired(o, err2) {
10813
+ async function raiseFromUpgradeRequired(o, err3) {
10813
10814
  const have = sentVersion(o.ship) ?? "unknown";
10814
- const floor = /(\d+\.\d+\.\d+)/.exec(err2.message.replace(have, ""))?.[1] ?? "a newer version";
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 (err2) {
11630
- emit.warn(`reading ${what} failed: ${err2 instanceof Error ? err2.message : String(err2)}`);
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 err2 = "";
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: err2, aborted: true });
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
- err2 += d.toString();
16662
+ err3 += d.toString();
16662
16663
  });
16663
- child.on("error", (e) => settle({ code: null, out, err: err2, spawnError: e, aborted: false }));
16664
- child.on("close", (code) => settle({ code, out, err: err2, aborted: false }));
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 err2 = e;
17674
- throw new SshKeyError(`ssh-keygen failed: ${err2.stderr?.toString().trim() || err2.message}`);
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 err2 = e;
17701
- const why = [err2.stderr, err2.stdout].map((x) => (x ?? "").trim()).filter(Boolean).join(" / ");
17702
- throw new Error(`${cmd} ${args.join(" ")}: ${why || err2.message.trim()}`);
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", (err2) => {
18276
- console.error(`Passenger sync daemon failed to bind 0.0.0.0:${port}: ${err2 instanceof Error ? err2.message : String(err2)}`);
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(err2) {
18394
- if (!(err2 instanceof Error)) return false;
18395
- const msg = err2.message;
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 = err2, depth = 0; e && depth < 4; e = e.cause, depth++) {
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 (err2 instanceof SyntaxError && /Unexpected token '?<|is not valid JSON|Unexpected end of JSON/.test(msg)) return true;
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(err2) {
18407
- const first = (err2 instanceof Error ? err2.message : String(err2)).split("\n")[0];
18408
- const code = codeOf(err2?.cause);
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 (err2) {
18438
- if (isTransientError(err2)) {
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: err2, consecutive: transientRun, backoffMs: wait, downMs });
18445
- if (transientRun === OUTAGE_ESCALATE_AFTER) o.onOutage?.({ err: err2, consecutive: transientRun, downMs });
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?.(err2);
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 spawn8 = o.spawnRoleAgent ?? runRoleAgent;
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 = spawn8(role, decision, ctx, { ready }).then(() => {
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 spawn7 } from "child_process";
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
- async ask(token) {
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 this.o.workspaceIds) {
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 reply = (status2, body) => ({ status: status2, body });
19210
- var err = (status2, message) => reply(status2, { error: message });
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 err(400, "tail must be an integer from 0 to 1000");
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 err(400, "wait must be a number of seconds");
19253
- if (after !== void 0 && !decodeCursor(after)) return err(400, "after is not a cursor this API issued");
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 reply(200, r);
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 err(502, "could not produce the status document");
19269
- return reply(200, {
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 reply(200, { streams: liveRuns(deps.stateDir).map(describeStream) });
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 err(400, "not a run name");
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 err(404, "no such run");
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 err(400, "body must be a small JSON object");
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 err(400, `role must be one of ${PAUSABLE.join(", ")}`);
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 reply(200, { ok: true, pause: pauseDoc(deps.state) });
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 reply(200, { ok: true, pause: pauseDoc(deps.state) });
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 ? reply(202, { accepted: true, detail: r.detail }) : err(409, r.detail);
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 ? reply(200, doc) : err(502, "could not produce the doctor report");
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 err(405, "method not allowed");
19313
- return err(404, "not found");
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, err(401, "invalid or revoked key"));
19352
- if (verdict === "forbidden") return send(res, err(403, "this key does not belong to a workspace this ship serves"));
19353
- if (verdict === "unavailable") return send(res, err(503, "could not verify the key right now"));
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, err(500, "internal error"));
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 = spawn7) {
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 (err2) {
20794
- remit.warn(`could not sweep ${e.route.route}: ${err2.message}`, { step: "sweep" });
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: new ControlAuth({
21314
- baseUrl: route2.baseUrl || DEFAULT_BASE_URL,
21315
- workspaceIds: [...new Set(controlRoutes.map((r) => r.resolved.workspaceId))],
21316
- shipKeys: () => cfg.routes.flatMap((r) => {
21317
- try {
21318
- return [resolveApiKey(r)];
21319
- } catch {
21320
- return [];
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: err2, consecutive, backoffMs }) => emit.warn(
21350
- `tracker unavailable (${describeCause(err2)}); attempt ${consecutive}, retrying in ${Math.round(backoffMs / 1e3)}s`
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: err2, downMs }) => {
21353
- const msg = `tracker unreachable for ${Math.max(1, Math.round(downMs / 6e4))} minutes: ${describeCause(err2)}`;
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: (err2) => emit.error(
21367
- `daemon pass failed, continuing: ${err2 instanceof Error ? err2.stack ?? err2.message : String(err2)}`
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.93.0",
3
+ "version": "0.94.0",
4
4
  "description": "A standing team of headless agents that picks work off a Tablation board, does it on a machine you control, and reports back.",
5
5
  "type": "module",
6
6
  "license": "MIT",