@tablation/crew 0.118.0 → 0.119.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 +476 -52
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -2547,7 +2547,8 @@ var TOP_LEVEL = /* @__PURE__ */ new Set([
2547
2547
  "hooks",
2548
2548
  "labels",
2549
2549
  "release",
2550
- "sandbox"
2550
+ "sandbox",
2551
+ "periscope"
2551
2552
  ]);
2552
2553
  var BRANCH_KEYS = /* @__PURE__ */ new Set(["base", "name", "push", "remote", "lowercase"]);
2553
2554
  var PLACEHOLDER = /\{(key|number|slug|role|prefix|tag|issue)\}/g;
@@ -2668,6 +2669,7 @@ function parseRepoConfig(text2, file) {
2668
2669
  rejectUnknown(raw.release?.isolateFailures, ISOLATE_KEYS, "release.isolateFailures", file);
2669
2670
  rejectUnknown(raw.branch, BRANCH_KEYS, "branch", file);
2670
2671
  rejectUnknown(raw.worktrees, /* @__PURE__ */ new Set(["copy", "prefix", "name"]), "worktrees", file);
2672
+ rejectUnknown(raw.periscope, /* @__PURE__ */ new Set(["inspector"]), "periscope", file);
2671
2673
  rejectUnknown(raw.docs, /* @__PURE__ */ new Set(["triagePolicy", "designGuide"]), "docs", file);
2672
2674
  rejectUnknown(raw.sandbox, /* @__PURE__ */ new Set(["image", "packages", "setup", "warm", "hosts", "toolchains"]), "sandbox", file);
2673
2675
  const sandboxNetwork = parseSandboxNetwork(raw.sandbox, file, (m) => new RepoConfigError(m));
@@ -2851,10 +2853,15 @@ function parseRepoConfig(text2, file) {
2851
2853
  if (versionFiles.length === 0) {
2852
2854
  throw new RepoConfigError(`${file}: release.versionFiles cannot be empty`);
2853
2855
  }
2856
+ const inspectorRaw = raw.periscope?.inspector;
2857
+ if (inspectorRaw !== void 0 && inspectorRaw !== "proxy" && inspectorRaw !== "repo") {
2858
+ throw new RepoConfigError(`${file}: periscope.inspector must be "proxy" or "repo"`);
2859
+ }
2854
2860
  return {
2855
2861
  version: SPEC_VERSION,
2856
2862
  branch,
2857
2863
  worktrees: worktrees2,
2864
+ ...inspectorRaw ? { periscope: { inspector: inspectorRaw } } : {},
2858
2865
  docs,
2859
2866
  ...sandbox ? { sandbox } : {},
2860
2867
  platform: platform2,
@@ -8698,12 +8705,14 @@ function listeners() {
8698
8705
  return out;
8699
8706
  }
8700
8707
  var pidsOnPort = (port) => listeners().filter((l) => l.port === port).map((l) => l.pid);
8701
- var DEFAULT_PORTS = { backendBase: 3e4, frontendBase: 4e4, span: 1e3 };
8708
+ var DEFAULT_PORTS = { backendBase: 3e4, frontendBase: 4e4, proxyBase: 5e4, span: 1e3 };
8702
8709
  function ticketForPort(port, s = DEFAULT_PORTS) {
8703
8710
  if (port >= s.backendBase && port < s.backendBase + s.span) return { n: port - s.backendBase, role: "backend" };
8704
8711
  if (port >= s.frontendBase && port < s.frontendBase + s.span) return { n: port - s.frontendBase, role: "frontend" };
8712
+ if (s.proxyBase !== void 0 && port >= s.proxyBase && port < s.proxyBase + s.span) return { n: port - s.proxyBase, role: "proxy" };
8705
8713
  return null;
8706
8714
  }
8715
+ var proxyPortFor = (n, s = DEFAULT_PORTS) => (s.proxyBase ?? DEFAULT_PORTS.proxyBase) + n;
8707
8716
  function worktreeExists(parent, prefix, n) {
8708
8717
  for (const name of [`${prefix}${n}`, `${prefix}${String(n).padStart(3, "0")}`]) {
8709
8718
  if (existsSync18(join19(parent, name, ".git"))) return true;
@@ -8739,7 +8748,7 @@ async function killGently(pids, graceMs = 2e3) {
8739
8748
  }
8740
8749
  function pidsInWorktree(path, n, s = DEFAULT_PORTS) {
8741
8750
  const pids = /* @__PURE__ */ new Set();
8742
- for (const port of [s.backendBase + n, s.frontendBase + n]) for (const p of pidsOnPort(port)) pids.add(p);
8751
+ for (const port of [s.backendBase + n, s.frontendBase + n, ...s.proxyBase !== void 0 ? [s.proxyBase + n] : []]) for (const p of pidsOnPort(port)) pids.add(p);
8743
8752
  const matched = run("pgrep", ["-f", `${path}/`]);
8744
8753
  for (const line of matched.split("\n")) {
8745
8754
  const pid = Number(line.trim());
@@ -12773,7 +12782,7 @@ function doctorSelftestLines(stored, toolchains) {
12773
12782
  }
12774
12783
 
12775
12784
  // src/cli.ts
12776
- import { resolve as resolve7, dirname as dirname30, join as join51, basename as basename16 } from "path";
12785
+ import { resolve as resolve7, dirname as dirname31, join as join51, basename as basename16 } from "path";
12777
12786
  import { fileURLToPath } from "url";
12778
12787
  import { hostname as hostname4, homedir as homedir11 } from "os";
12779
12788
  import { spawnSync as spawnSync10 } from "child_process";
@@ -20424,7 +20433,7 @@ function warnOrphanedWorktreeDirs(paths, log, held) {
20424
20433
  }
20425
20434
  async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin", base, held) {
20426
20435
  const refused = [];
20427
- const refuse = (a, reason, detail) => {
20436
+ const refuse2 = (a, reason, detail) => {
20428
20437
  refused.push(a.path);
20429
20438
  const when = held ? noteHeldWorktree(held.state, { repo: held.repo, path: a.path, ticket: a.ticket.issue_id, status: a.ticket.status, reason }) : "first";
20430
20439
  if (when) {
@@ -20452,12 +20461,12 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
20452
20461
  } catch {
20453
20462
  }
20454
20463
  if (dirty) {
20455
- refuse(a, "uncommitted changes", "it has uncommitted changes");
20464
+ refuse2(a, "uncommitted changes", "it has uncommitted changes");
20456
20465
  continue;
20457
20466
  }
20458
20467
  }
20459
20468
  if (!dryRun && a.requirePushed && a.branch && existsSync39(a.path) && remoteConfigured(cwd, remote) && worktreeHoldsUnpushed(a.path, remote)) {
20460
- refuse(a, "unpushed commits", `${a.branch} has commits that are not on ${remote}`);
20469
+ refuse2(a, "unpushed commits", `${a.branch} has commits that are not on ${remote}`);
20461
20470
  continue;
20462
20471
  }
20463
20472
  const n = Number(a.ticket.issue_id.replace(/^\D+/, ""));
@@ -20481,7 +20490,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
20481
20490
  try {
20482
20491
  git(cwd, ["worktree", "remove", "--force", a.path]);
20483
20492
  } catch (e) {
20484
- refuse(a, "removal failed", `could not remove it: ${e.message}`);
20493
+ refuse2(a, "removal failed", `could not remove it: ${e.message}`);
20485
20494
  continue;
20486
20495
  }
20487
20496
  if (!a.keepBranch && a.branch) {
@@ -20655,7 +20664,7 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
20655
20664
  }
20656
20665
 
20657
20666
  // src/cli.ts
20658
- import { realpathSync as realpathSync3, readFileSync as readFileSync42, existsSync as existsSync47, mkdirSync as mkdirSync34, writeFileSync as writeFileSync35, rmSync as rmSync23, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync6, statSync as statSync16, readdirSync as readdirSync20 } from "fs";
20667
+ import { realpathSync as realpathSync3, readFileSync as readFileSync42, existsSync as existsSync47, mkdirSync as mkdirSync35, writeFileSync as writeFileSync35, rmSync as rmSync23, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync6, statSync as statSync16, readdirSync as readdirSync20 } from "fs";
20659
20668
  import { createInterface } from "readline/promises";
20660
20669
 
20661
20670
  // src/periscope-cmd.ts
@@ -20861,9 +20870,18 @@ ${tail(handoff.output)}`);
20861
20870
  return abandon(`preview exited ${preview?.code ?? "without running"}
20862
20871
  ${tail(preview?.output ?? "")}`);
20863
20872
  }
20864
- const url = previewUrlFrom(preview.output);
20865
- if (!url) return abandon(`preview printed no URL as its last line
20873
+ const previewUrl = previewUrlFrom(preview.output);
20874
+ if (!previewUrl) return abandon(`preview printed no URL as its last line
20866
20875
  ${tail(preview.output)}`);
20876
+ let url = previewUrl;
20877
+ if (d.startProxy) {
20878
+ try {
20879
+ url = await d.startProxy(wt.path, previewUrl, /(\d+)$/.exec(draft.issueId)?.[1] ?? "0");
20880
+ } catch (e) {
20881
+ return abandon(`starting the periscope proxy failed: ${e.message}`);
20882
+ }
20883
+ d.say("proxy", `proxy at ${url} in front of ${previewUrl}`, { url, previewUrl });
20884
+ }
20867
20885
  d.say("preview", `preview at ${url}`, { url });
20868
20886
  const intel = await probeIntel(
20869
20887
  { runHook: (e) => d.runHook("intel", wt.path, e) },
@@ -21052,10 +21070,17 @@ ${section}`;
21052
21070
  forgetLocal(d, row.ticketRecordId);
21053
21071
  return { ticket: row.ticket, mode: "keep", outcome: "done", state: "kept", sha: pushed.sha, ...existing ? { existing: true, ...prior ? { status: prior.status } : {} } : {} };
21054
21072
  }
21073
+ function isProxyUrl(url, number) {
21074
+ try {
21075
+ return Number(new URL(url).port) === proxyPortFor(Number(number));
21076
+ } catch {
21077
+ return false;
21078
+ }
21079
+ }
21055
21080
  function periscopeListEntries(rows, now, isListening) {
21056
21081
  return rows.filter((r) => r.state === "live").map((r) => {
21057
21082
  const at = r.lastActivityAt ? new Date(r.lastActivityAt).getTime() : NaN;
21058
- return { ...r, idleMinutes: Number.isNaN(at) ? null : Math.max(0, Math.floor((now.getTime() - at) / 6e4)), listening: isListening(r) };
21083
+ return { ...r, idleMinutes: Number.isNaN(at) ? null : Math.max(0, Math.floor((now.getTime() - at) / 6e4)), listening: isListening(r), proxied: isProxyUrl(r.url, r.number) };
21059
21084
  });
21060
21085
  }
21061
21086
  function formatPeriscopeList(entries) {
@@ -21064,7 +21089,7 @@ function formatPeriscopeList(entries) {
21064
21089
  e.ticket,
21065
21090
  e.repo,
21066
21091
  e.worktree,
21067
- e.url,
21092
+ e.proxied ? `${e.url} (proxy)` : e.url,
21068
21093
  e.state,
21069
21094
  e.idleMinutes === null ? "idle ?" : `idle ${e.idleMinutes} min`,
21070
21095
  e.listening ? "listening" : "not listening"
@@ -21096,9 +21121,16 @@ ${tail2(iso.output)}`);
21096
21121
  throw new PeriscopeError(`preview exited ${preview?.code ?? "without running"}
21097
21122
  ${tail2(preview?.output ?? "")}`);
21098
21123
  }
21099
- const url = previewUrlFrom(preview.output);
21124
+ let url = previewUrlFrom(preview.output);
21100
21125
  if (!url) throw new PeriscopeError(`preview printed no URL as its last line
21101
21126
  ${tail2(preview.output)}`);
21127
+ if (d.startProxy) {
21128
+ try {
21129
+ url = await d.startProxy(row.worktree, url, /(\d+)$/.exec(tag)?.[1] ?? "0");
21130
+ } catch (e) {
21131
+ throw new PeriscopeError(`starting the periscope proxy failed: ${e.message}`);
21132
+ }
21133
+ }
21102
21134
  const now = d.now();
21103
21135
  await d.update(row.id, {
21104
21136
  url,
@@ -21634,6 +21666,388 @@ function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
21634
21666
  return endpoints;
21635
21667
  }
21636
21668
 
21669
+ // src/periscope-proxy.ts
21670
+ import { spawn as spawn9 } from "child_process";
21671
+ import { appendFileSync as appendFileSync3, mkdirSync as mkdirSync32 } from "fs";
21672
+ import { createServer, request as httpRequest } from "http";
21673
+ import { request as httpsRequest } from "https";
21674
+ import { connect as netConnect } from "net";
21675
+ import { connect as tlsConnect } from "tls";
21676
+ import { dirname as dirname28, isAbsolute as isAbsolute4 } from "path";
21677
+ import { brotliDecompressSync, gunzipSync, inflateSync } from "zlib";
21678
+
21679
+ // src/periscope-inspector.ts
21680
+ var INSPECTOR_ROUTE = "/__periscope/inspector.js";
21681
+ var POINTED_ROUTE = "/__periscope/pointed";
21682
+ var INSPECTOR_SOURCE = `
21683
+ const ROUTE = ${JSON.stringify(POINTED_ROUTE)};
21684
+ let armed = false;
21685
+ let off = false;
21686
+ let current = null;
21687
+ let toast = null;
21688
+ const outline = document.createElement('div');
21689
+ const label = document.createElement('div');
21690
+ Object.assign(outline.style, { position: 'fixed', zIndex: '2147483646', pointerEvents: 'none', outline: '2px solid #e11d8f', outlineOffset: '-1px', display: 'none' });
21691
+ Object.assign(label.style, { position: 'fixed', zIndex: '2147483647', pointerEvents: 'none', font: '11px/1.3 ui-monospace, monospace', background: '#1f1f23', color: '#fff', padding: '2px 6px', borderRadius: '3px', display: 'none', maxWidth: '60vw', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' });
21692
+ document.documentElement.append(outline, label);
21693
+
21694
+ function componentName(el) {
21695
+ const key = Object.keys(el).find((k) => k.startsWith('__reactFiber$'));
21696
+ let fiber = key ? el[key] : null;
21697
+ while (fiber) {
21698
+ const t = fiber.type;
21699
+ if (typeof t === 'function' || (t && typeof t === 'object')) {
21700
+ const n = (typeof t === 'function' ? t.displayName || t.name : t.displayName || (t.render && (t.render.displayName || t.render.name)) || (t.type && (t.type.displayName || t.type.name)));
21701
+ if (n) return n;
21702
+ }
21703
+ fiber = fiber.return;
21704
+ }
21705
+ return undefined;
21706
+ }
21707
+
21708
+ function hide() {
21709
+ current = null;
21710
+ outline.style.display = 'none';
21711
+ label.style.display = 'none';
21712
+ }
21713
+
21714
+ function sync() {
21715
+ if (!armed || off) { hide(); document.documentElement.style.cursor = ''; return; }
21716
+ document.documentElement.style.cursor = 'crosshair';
21717
+ }
21718
+
21719
+ function show(el) {
21720
+ current = el;
21721
+ const r = el.getBoundingClientRect();
21722
+ Object.assign(outline.style, { display: 'block', left: r.left + 'px', top: r.top + 'px', width: r.width + 'px', height: r.height + 'px' });
21723
+ label.textContent = el.getAttribute('data-source') + (componentName(el) ? ' <' + componentName(el) + '>' : '');
21724
+ label.style.display = 'block';
21725
+ label.style.left = Math.max(0, r.left) + 'px';
21726
+ label.style.top = (r.top > 20 ? r.top - 20 : r.bottom + 2) + 'px';
21727
+ }
21728
+
21729
+ function flash(text) {
21730
+ if (toast) toast.remove();
21731
+ toast = document.createElement('div');
21732
+ toast.textContent = text;
21733
+ Object.assign(toast.style, { position: 'fixed', zIndex: '2147483647', right: '12px', bottom: '12px', font: '12px ui-monospace, monospace', background: '#1f1f23', color: '#fff', padding: '6px 10px', borderRadius: '4px', pointerEvents: 'none' });
21734
+ document.documentElement.append(toast);
21735
+ setTimeout(() => { if (toast) { toast.remove(); toast = null; } }, 1500);
21736
+ }
21737
+
21738
+ window.addEventListener('keydown', (e) => {
21739
+ if (e.key === 'Alt') { armed = true; off = false; sync(); }
21740
+ else if (e.key === 'Escape' && armed) { off = true; sync(); }
21741
+ }, true);
21742
+ window.addEventListener('keyup', (e) => { if (e.key === 'Alt') { armed = false; off = false; sync(); } }, true);
21743
+ window.addEventListener('blur', () => { armed = false; off = false; sync(); });
21744
+
21745
+ window.addEventListener('mousemove', (e) => {
21746
+ if (!armed || off) return;
21747
+ const target = e.target instanceof Element ? e.target.closest('[data-source]') : null;
21748
+ if (target) show(target); else hide();
21749
+ }, true);
21750
+
21751
+ window.addEventListener('click', (e) => {
21752
+ if (!armed || off || !current) return;
21753
+ e.preventDefault();
21754
+ e.stopPropagation();
21755
+ const src = current.getAttribute('data-source');
21756
+ const i = src.lastIndexOf(':');
21757
+ const entry = { path: src.slice(0, i), line: Number(src.slice(i + 1)), component: componentName(current), url: location.href };
21758
+ fetch(ROUTE, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(entry) })
21759
+ .then((r) => { if (!r.ok) throw new Error(String(r.status)); })
21760
+ .catch(() => flash('point failed'));
21761
+ if (navigator.clipboard) navigator.clipboard.writeText(src).catch(() => {});
21762
+ flash('pointed at ' + src);
21763
+ }, true);
21764
+ `;
21765
+
21766
+ // src/periscope-proxy.ts
21767
+ var MAX_BODY_BYTES = 8 * 1024;
21768
+ var SCRIPT_TAG = `<script type="module" src="${INSPECTOR_ROUTE}"></script>`;
21769
+ function parsePointedBody(raw) {
21770
+ let body;
21771
+ try {
21772
+ body = JSON.parse(raw);
21773
+ } catch {
21774
+ return null;
21775
+ }
21776
+ if (!body || typeof body !== "object") return null;
21777
+ const b = body;
21778
+ const p = b.path;
21779
+ const line = b.line;
21780
+ if (typeof p !== "string" || p.length === 0 || p.length > 500) return null;
21781
+ if (isAbsolute4(p) || p.split(/[\\/]/).includes("..")) return null;
21782
+ if (typeof line !== "number" || !Number.isInteger(line) || line < 1) return null;
21783
+ const entry = { path: p, line };
21784
+ if (typeof b.column === "number" && Number.isInteger(b.column) && b.column >= 0) entry.column = b.column;
21785
+ if (typeof b.component === "string" && b.component.length <= 200) entry.component = b.component;
21786
+ if (typeof b.url === "string") entry.url = b.url.slice(0, 2e3);
21787
+ return entry;
21788
+ }
21789
+ function appendPointed(file, entry, now = /* @__PURE__ */ new Date()) {
21790
+ mkdirSync32(dirname28(file), { recursive: true });
21791
+ appendFileSync3(file, `${JSON.stringify({ ...entry, at: now.toISOString() })}
21792
+ `);
21793
+ }
21794
+ var CSP_DIRECTIVES = /* @__PURE__ */ new Set(["default-src", "script-src", "script-src-elem", "connect-src"]);
21795
+ function relaxCsp(policy) {
21796
+ return policy.split(";").map((part) => {
21797
+ const tokens = part.trim().split(/\s+/).filter(Boolean);
21798
+ const name = tokens[0]?.toLowerCase();
21799
+ if (!name || !CSP_DIRECTIVES.has(name)) return part.trim();
21800
+ const sources = tokens.slice(1).filter((t) => t.toLowerCase() !== "'none'");
21801
+ if (!sources.some((t) => t.toLowerCase() === "'self'")) sources.push("'self'");
21802
+ return [tokens[0], ...sources].join(" ");
21803
+ }).filter(Boolean).join("; ");
21804
+ }
21805
+ function injectScript(html) {
21806
+ const i = html.search(/<\/body\s*>(?![\s\S]*<\/body\s*>)/i);
21807
+ return i === -1 ? html + SCRIPT_TAG : html.slice(0, i) + SCRIPT_TAG + html.slice(i);
21808
+ }
21809
+ function decode(body, encoding) {
21810
+ switch ((encoding ?? "").toLowerCase().trim()) {
21811
+ case "":
21812
+ case "identity":
21813
+ return body;
21814
+ case "gzip":
21815
+ case "x-gzip":
21816
+ return gunzipSync(body);
21817
+ case "deflate":
21818
+ return inflateSync(body);
21819
+ case "br":
21820
+ return brotliDecompressSync(body);
21821
+ default:
21822
+ return void 0;
21823
+ }
21824
+ }
21825
+ function forwardHeaders(req, target) {
21826
+ const h = { ...req.headers, host: target.host };
21827
+ if (typeof h.origin === "string") h.origin = target.origin;
21828
+ if (typeof h.referer === "string") {
21829
+ try {
21830
+ const r = new URL(h.referer);
21831
+ h.referer = `${target.origin}${r.pathname}${r.search}`;
21832
+ } catch {
21833
+ }
21834
+ }
21835
+ return h;
21836
+ }
21837
+ function refuse(res, status2, headers = {}) {
21838
+ res.writeHead(status2, headers);
21839
+ res.end();
21840
+ }
21841
+ function handlePointed(req, res, o) {
21842
+ if (req.method !== "POST") return refuse(res, 405, { Allow: "POST" });
21843
+ const chunks = [];
21844
+ let size = 0;
21845
+ let aborted = false;
21846
+ req.on("data", (c) => {
21847
+ size += c.length;
21848
+ if (size > MAX_BODY_BYTES) {
21849
+ aborted = true;
21850
+ refuse(res, 413);
21851
+ req.destroy();
21852
+ return;
21853
+ }
21854
+ chunks.push(c);
21855
+ });
21856
+ req.on("end", () => {
21857
+ if (aborted) return;
21858
+ const entry = parsePointedBody(Buffer.concat(chunks).toString("utf8"));
21859
+ if (!entry) return refuse(res, 400);
21860
+ try {
21861
+ appendPointed(o.dropFile, entry, o.now?.());
21862
+ refuse(res, 204);
21863
+ } catch {
21864
+ refuse(res, 500);
21865
+ }
21866
+ });
21867
+ }
21868
+ function proxyRequest(req, res, o) {
21869
+ const { target } = o;
21870
+ const secure = target.protocol === "https:";
21871
+ const upstream = (secure ? httpsRequest : httpRequest)({
21872
+ hostname: target.hostname,
21873
+ port: target.port || (secure ? 443 : 80),
21874
+ method: req.method,
21875
+ path: req.url,
21876
+ headers: forwardHeaders(req, target)
21877
+ }, (up) => {
21878
+ const headers = { ...up.headers };
21879
+ if (typeof headers.location === "string" && headers.location.startsWith(target.origin)) {
21880
+ headers.location = headers.location.slice(target.origin.length) || "/";
21881
+ }
21882
+ const csp = headers["content-security-policy"];
21883
+ if (o.inject && typeof csp === "string") headers["content-security-policy"] = relaxCsp(csp);
21884
+ const isHtml = /^text\/html\b/i.test(String(up.headers["content-type"] ?? ""));
21885
+ if (!o.inject || !isHtml || req.method === "HEAD") {
21886
+ res.writeHead(up.statusCode ?? 502, headers);
21887
+ up.pipe(res);
21888
+ return;
21889
+ }
21890
+ const chunks = [];
21891
+ up.on("data", (c) => chunks.push(c));
21892
+ up.on("end", () => {
21893
+ let html;
21894
+ try {
21895
+ html = decode(Buffer.concat(chunks), String(up.headers["content-encoding"] ?? ""));
21896
+ } catch {
21897
+ html = void 0;
21898
+ }
21899
+ if (!html) {
21900
+ res.writeHead(up.statusCode ?? 502, headers);
21901
+ res.end(Buffer.concat(chunks));
21902
+ return;
21903
+ }
21904
+ const out = Buffer.from(injectScript(html.toString("utf8")), "utf8");
21905
+ delete headers["content-encoding"];
21906
+ delete headers.etag;
21907
+ delete headers["transfer-encoding"];
21908
+ headers["content-length"] = String(out.length);
21909
+ res.writeHead(up.statusCode ?? 502, headers);
21910
+ res.end(out);
21911
+ });
21912
+ up.on("error", () => res.destroy());
21913
+ });
21914
+ upstream.on("error", (e) => {
21915
+ if (res.headersSent) {
21916
+ res.destroy();
21917
+ return;
21918
+ }
21919
+ res.writeHead(502, { "Content-Type": "text/plain; charset=utf-8" });
21920
+ res.end(`periscope proxy: ${target.origin} is not answering (${e.message})
21921
+ `);
21922
+ });
21923
+ req.pipe(upstream);
21924
+ }
21925
+ function proxyUpgrade(req, client2, head, o) {
21926
+ const { target } = o;
21927
+ const secure = target.protocol === "https:";
21928
+ const port = Number(target.port) || (secure ? 443 : 80);
21929
+ const up = secure ? tlsConnect({ host: target.hostname, port, servername: target.hostname }) : netConnect({ host: target.hostname, port });
21930
+ up.on("error", () => client2.destroy());
21931
+ client2.on("error", () => up.destroy());
21932
+ const hangUp = () => {
21933
+ up.destroy();
21934
+ client2.destroy();
21935
+ };
21936
+ client2.on("end", hangUp);
21937
+ client2.on("close", hangUp);
21938
+ up.on("end", hangUp);
21939
+ up.on("close", hangUp);
21940
+ up.on(secure ? "secureConnect" : "connect", () => {
21941
+ const headers = forwardHeaders(req, target);
21942
+ const lines2 = [`${req.method} ${req.url} HTTP/${req.httpVersion}`];
21943
+ for (const [k, v] of Object.entries(headers)) {
21944
+ if (v === void 0) continue;
21945
+ for (const one of Array.isArray(v) ? v : [v]) lines2.push(`${k}: ${one}`);
21946
+ }
21947
+ up.write(`${lines2.join("\r\n")}\r
21948
+ \r
21949
+ `);
21950
+ if (head.length) up.write(head);
21951
+ up.pipe(client2);
21952
+ client2.pipe(up);
21953
+ });
21954
+ }
21955
+ function createPeriscopeProxy(o) {
21956
+ const server = createServer(async (req, res) => {
21957
+ try {
21958
+ if (o.requestFilter && !await o.requestFilter(req, res)) return;
21959
+ const path = (req.url ?? "/").split("?")[0];
21960
+ if (o.inject && path === INSPECTOR_ROUTE && (req.method === "GET" || req.method === "HEAD")) {
21961
+ res.writeHead(200, { "Content-Type": "text/javascript; charset=utf-8", "Cache-Control": "no-store" });
21962
+ res.end(req.method === "HEAD" ? void 0 : INSPECTOR_SOURCE);
21963
+ return;
21964
+ }
21965
+ if (o.inject && path === POINTED_ROUTE) return handlePointed(req, res, o);
21966
+ proxyRequest(req, res, o);
21967
+ } catch {
21968
+ if (!res.headersSent) refuse(res, 500);
21969
+ else res.destroy();
21970
+ }
21971
+ });
21972
+ server.on("upgrade", async (req, socket, head) => {
21973
+ try {
21974
+ if (o.requestFilter && !await o.requestFilter(req, socket)) return;
21975
+ proxyUpgrade(req, socket, head, o);
21976
+ } catch {
21977
+ socket.destroy();
21978
+ }
21979
+ });
21980
+ return server;
21981
+ }
21982
+ function proxyEnv(l) {
21983
+ return {
21984
+ CREW_PROXY_TARGET: l.target,
21985
+ CREW_PROXY_HOST: l.host,
21986
+ CREW_PROXY_PORT: String(l.port),
21987
+ CREW_PROXY_INJECT: l.inject ? "1" : "0"
21988
+ };
21989
+ }
21990
+ function proxiedUrl(previewUrl, host, port) {
21991
+ const u = new URL(previewUrl);
21992
+ return `http://${host.includes(":") ? `[${host}]` : host}:${port}${u.pathname === "/" && !u.search ? "/" : u.pathname}${u.search}`;
21993
+ }
21994
+ function runProxyFromEnv(dropFile, env = process.env) {
21995
+ const target = env.CREW_PROXY_TARGET;
21996
+ const port = Number(env.CREW_PROXY_PORT);
21997
+ if (!target || !Number.isInteger(port) || port <= 0) throw new Error("periscope-proxy: CREW_PROXY_TARGET and CREW_PROXY_PORT are required");
21998
+ const server = createPeriscopeProxy({ target: new URL(target), dropFile, inject: env.CREW_PROXY_INJECT !== "0" });
21999
+ server.on("error", (e) => {
22000
+ process.stderr.write(`periscope-proxy: ${e.message}
22001
+ `);
22002
+ process.exit(1);
22003
+ });
22004
+ server.listen(port, env.CREW_PROXY_HOST || "localhost");
22005
+ return server;
22006
+ }
22007
+ function accepts(host, port, timeoutMs = 500) {
22008
+ return new Promise((resolve8) => {
22009
+ const s = netConnect({ host, port });
22010
+ const done = (ok) => {
22011
+ s.destroy();
22012
+ resolve8(ok);
22013
+ };
22014
+ s.setTimeout(timeoutMs, () => done(false));
22015
+ s.once("connect", () => done(true));
22016
+ s.once("error", () => done(false));
22017
+ });
22018
+ }
22019
+ async function launchProxy(invocation, worktree, previewUrl, number, o) {
22020
+ const port = proxyPortFor(number);
22021
+ const stale = pidsOnPort(port);
22022
+ if (stale.length) await killGently(stale);
22023
+ const launch = { worktree, target: previewUrl, host: o.host, port, inject: o.inject };
22024
+ const [cmd, ...args] = invocation;
22025
+ const child = spawn9(cmd, [...args, "periscope-proxy", pointedAtFile(worktree)], {
22026
+ detached: true,
22027
+ stdio: "ignore",
22028
+ cwd: worktree,
22029
+ env: { ...process.env, ...proxyEnv(launch) }
22030
+ });
22031
+ child.unref();
22032
+ let exited = false;
22033
+ child.on("exit", () => {
22034
+ exited = true;
22035
+ });
22036
+ const deadline = Date.now() + (o.waitMs ?? 1e4);
22037
+ while (Date.now() < deadline) {
22038
+ if (exited) throw new Error(`the proxy process exited before listening on ${o.host}:${port}`);
22039
+ if (await accepts(o.host, port)) return proxiedUrl(previewUrl, o.host, port);
22040
+ await new Promise((r) => setTimeout(r, 100));
22041
+ }
22042
+ if (child.pid) {
22043
+ try {
22044
+ process.kill(child.pid, "SIGTERM");
22045
+ } catch {
22046
+ }
22047
+ }
22048
+ throw new Error(`the proxy did not start listening on ${o.host}:${port}`);
22049
+ }
22050
+
21637
22051
  // src/outage.ts
21638
22052
  var TRANSIENT_BACKOFF_MS = [15e3, 3e4, 6e4];
21639
22053
  var OUTAGE_ESCALATE_AFTER = 5;
@@ -21666,7 +22080,7 @@ function describeCause(err3) {
21666
22080
  }
21667
22081
 
21668
22082
  // src/daemon.ts
21669
- import { existsSync as existsSync44, mkdirSync as mkdirSync32, readFileSync as readFileSync40, readdirSync as readdirSync17, rmSync as rmSync20, statSync as statSync14, writeFileSync as writeFileSync33 } from "fs";
22083
+ import { existsSync as existsSync44, mkdirSync as mkdirSync33, readFileSync as readFileSync40, readdirSync as readdirSync17, rmSync as rmSync20, statSync as statSync14, writeFileSync as writeFileSync33 } from "fs";
21670
22084
  import { join as join47 } from "path";
21671
22085
  import { randomBytes as randomBytes2 } from "crypto";
21672
22086
  var DEFAULT_FLOOR_MS = 5e3;
@@ -21780,7 +22194,7 @@ function daemonStampFor(stateDir) {
21780
22194
  return join47(stateDir, "daemon.started");
21781
22195
  }
21782
22196
  function noteDaemonStarted(stateDir, now = Date.now()) {
21783
- mkdirSync32(stateDir, { recursive: true });
22197
+ mkdirSync33(stateDir, { recursive: true });
21784
22198
  rmSync20(reloadSentinelFor(stateDir), { force: true });
21785
22199
  writeFileSync33(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
21786
22200
  `);
@@ -21852,7 +22266,7 @@ async function runOnePassUnguarded(o) {
21852
22266
  await hydrateApiKeys([o.route], o.getSessionStore);
21853
22267
  const emit = o.newEmitter();
21854
22268
  const decide = o.decide ?? decideCycle;
21855
- const spawn11 = o.spawnRoleAgent ?? runRoleAgent;
22269
+ const spawn12 = o.spawnRoleAgent ?? runRoleAgent;
21856
22270
  const breaker = o.spawnBreaker ?? new SpawnBreaker();
21857
22271
  const ctx = { route: o.route, ship: o.ship, state: o.state, emit, getSessionStore: o.getSessionStore };
21858
22272
  if (o.sandboxCache) {
@@ -21963,7 +22377,7 @@ async function runOnePassUnguarded(o) {
21963
22377
  settle = resolve8;
21964
22378
  });
21965
22379
  const ready = (tookTicket2) => settle(tookTicket2);
21966
- const run3 = spawn11(role, decision, ctx, { ready }).then(() => {
22380
+ const run3 = spawn12(role, decision, ctx, { ready }).then(() => {
21967
22381
  breaker.recordSuccess(role);
21968
22382
  clearShipAttention(o.state, `role_parked:${role}`);
21969
22383
  clearShipAttention(o.state, workspaceAuthKey(role));
@@ -22363,12 +22777,12 @@ import {
22363
22777
  watch
22364
22778
  } from "fs";
22365
22779
  import { createConnection } from "net";
22366
- import { createServer } from "http";
22367
- import { basename as basename14, dirname as dirname28, join as join48 } from "path";
22368
- import { execFile as execFile3, spawn as spawn10 } from "child_process";
22780
+ import { createServer as createServer2 } from "http";
22781
+ import { basename as basename14, dirname as dirname29, join as join48 } from "path";
22782
+ import { execFile as execFile3, spawn as spawn11 } from "child_process";
22369
22783
 
22370
22784
  // src/periscope-control.ts
22371
- import { spawn as spawn9 } from "child_process";
22785
+ import { spawn as spawn10 } from "child_process";
22372
22786
  var reply = (status2, body) => ({ status: status2, body });
22373
22787
  var err = (status2, message) => reply(status2, { error: message });
22374
22788
  var TICKET_RE = /^(?:[A-Za-z]+-)?\d+$/;
@@ -22559,7 +22973,7 @@ async function handlePeriscopes(h, q2) {
22559
22973
  }
22560
22974
  function runCrew(args, onLine, timeoutMs = 20 * 6e4) {
22561
22975
  return new Promise((resolve8) => {
22562
- const child = spawn9(process.execPath, [process.argv[1] ?? "", ...args], { stdio: ["ignore", "pipe", "pipe"] });
22976
+ const child = spawn10(process.execPath, [process.argv[1] ?? "", ...args], { stdio: ["ignore", "pipe", "pipe"] });
22563
22977
  let stdout = "";
22564
22978
  let stderr = "";
22565
22979
  let pending = "";
@@ -22598,7 +23012,7 @@ var MAX_SOCKET_PATH = 100;
22598
23012
  var MAX_TAIL_BYTES = 1024 * 1024;
22599
23013
  var MAX_WAIT_SECONDS = 30;
22600
23014
  var DEFAULT_TAIL_LINES = 100;
22601
- var MAX_BODY_BYTES = 4096;
23015
+ var MAX_BODY_BYTES2 = 4096;
22602
23016
  var ControlAuth = class {
22603
23017
  cache = /* @__PURE__ */ new Map();
22604
23018
  o;
@@ -22680,7 +23094,7 @@ function candidates(live, rotated) {
22680
23094
  const out = [live];
22681
23095
  if (!rotated) return out;
22682
23096
  try {
22683
- for (const n of readdirSync18(dirname28(live))) if (ROTATED_EVENTS_RE.test(n)) out.push(join48(dirname28(live), n));
23097
+ for (const n of readdirSync18(dirname29(live))) if (ROTATED_EVENTS_RE.test(n)) out.push(join48(dirname29(live), n));
22684
23098
  } catch {
22685
23099
  }
22686
23100
  return out;
@@ -22819,7 +23233,7 @@ async function readJsonBody(req) {
22819
23233
  let n = 0;
22820
23234
  for await (const c of req) {
22821
23235
  n += c.length;
22822
- if (n > MAX_BODY_BYTES) return null;
23236
+ if (n > MAX_BODY_BYTES2) return null;
22823
23237
  chunks.push(c);
22824
23238
  }
22825
23239
  if (!n) return {};
@@ -22840,7 +23254,7 @@ async function tailReply(path, q2, rotated, abort) {
22840
23254
  const deadline = Date.now() + Math.min(wait, MAX_WAIT_SECONDS) * 1e3;
22841
23255
  let r = readTail(path, { after, tail: tail3, rotated });
22842
23256
  while (after && !r.lines.length && !r.reset && Date.now() < deadline && !abort.aborted) {
22843
- await waitForChange(dirname28(path), deadline - Date.now(), abort);
23257
+ await waitForChange(dirname29(path), deadline - Date.now(), abort);
22844
23258
  r = readTail(path, { after: r.cursor, rotated });
22845
23259
  }
22846
23260
  return reply2(200, r);
@@ -22948,7 +23362,7 @@ async function startControlServer(deps) {
22948
23362
  log(`control API not started: ${path} is in use by another process`);
22949
23363
  return null;
22950
23364
  }
22951
- const server = createServer((req, res) => {
23365
+ const server = createServer2((req, res) => {
22952
23366
  const abort = new AbortController();
22953
23367
  res.on("close", () => abort.abort());
22954
23368
  void (async () => {
@@ -22999,7 +23413,7 @@ async function startControlServer(deps) {
22999
23413
  });
23000
23414
  }
23001
23415
  var shq2 = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
23002
- function runDetached(commands, spawnFn = spawn10) {
23416
+ function runDetached(commands, spawnFn = spawn11) {
23003
23417
  const script = `sleep 1; ${commands.map((c) => c.map(shq2).join(" ")).join("; ")}`;
23004
23418
  const child = spawnFn("/bin/sh", ["-c", script], { detached: true, stdio: "ignore" });
23005
23419
  child.on("error", () => {
@@ -23030,21 +23444,21 @@ function crewJson(args, timeoutMs = 6e4) {
23030
23444
 
23031
23445
  // src/periscope-mcp.ts
23032
23446
  import { lstat, mkdir, readdir, readFile, readlink, realpath, rm, stat, writeFile } from "fs/promises";
23033
- import { basename as basename15, dirname as dirname29, isAbsolute as isAbsolute4, join as join50, relative as relative2, resolve as resolve6, sep } from "path";
23447
+ import { basename as basename15, dirname as dirname30, isAbsolute as isAbsolute5, join as join50, relative as relative2, resolve as resolve6, sep } from "path";
23034
23448
  import { z } from "zod";
23035
23449
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
23036
23450
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
23037
23451
 
23038
23452
  // src/periscope-endpoints.ts
23039
23453
  import { randomBytes as randomBytes3 } from "crypto";
23040
- import { chmodSync as chmodSync5, existsSync as existsSync46, mkdirSync as mkdirSync33, readdirSync as readdirSync19, readFileSync as readFileSync41, rmSync as rmSync22, writeFileSync as writeFileSync34 } from "fs";
23454
+ import { chmodSync as chmodSync5, existsSync as existsSync46, mkdirSync as mkdirSync34, readdirSync as readdirSync19, readFileSync as readFileSync41, rmSync as rmSync22, writeFileSync as writeFileSync34 } from "fs";
23041
23455
  import { join as join49 } from "path";
23042
23456
  var secretDir = (stateDir) => join49(stateDir, "periscope-mcp");
23043
23457
  var secretFile = (stateDir, id) => join49(secretDir(stateDir), `${id}.json`);
23044
23458
  var endpointDir = (stateDir) => join49(stateDir, "periscope-endpoints");
23045
23459
  var endpointFile = (stateDir, id) => join49(endpointDir(stateDir), `${id}.json`);
23046
23460
  function mintPeriscopeSecret(stateDir, ticketRecordId) {
23047
- mkdirSync33(secretDir(stateDir), { recursive: true, mode: 448 });
23461
+ mkdirSync34(secretDir(stateDir), { recursive: true, mode: 448 });
23048
23462
  const secret = randomBytes3(32).toString("base64url");
23049
23463
  const file = secretFile(stateDir, ticketRecordId);
23050
23464
  writeFileSync34(file, `${JSON.stringify({ secret })}
@@ -23071,7 +23485,7 @@ function readPeriscopeEndpoint(stateDir, ticketRecordId) {
23071
23485
  }
23072
23486
  }
23073
23487
  function writePeriscopeEndpoint(stateDir, rec) {
23074
- mkdirSync33(endpointDir(stateDir), { recursive: true });
23488
+ mkdirSync34(endpointDir(stateDir), { recursive: true });
23075
23489
  writeFileSync34(endpointFile(stateDir, rec.ticketRecordId), `${JSON.stringify(rec, null, 2)}
23076
23490
  `);
23077
23491
  }
@@ -23141,7 +23555,7 @@ async function removeEndedPeriscopeEndpoints(d) {
23141
23555
  var PERISCOPE_HOOKS = ["test", "build", "ports", "handoff", "preview", "isolate"];
23142
23556
  var MAX_READ_BYTES = 1024 * 1024;
23143
23557
  var MAX_WRITE_BYTES = 4 * 1024 * 1024;
23144
- var MAX_BODY_BYTES2 = 8 * 1024 * 1024;
23558
+ var MAX_BODY_BYTES3 = 8 * 1024 * 1024;
23145
23559
  var MAX_HOOK_OUTPUT = 2e4;
23146
23560
  var MAX_DIFF = 2e5;
23147
23561
  var MAX_SEARCH_RESULTS = 500;
@@ -23149,7 +23563,7 @@ var PathRefusal = class extends Error {
23149
23563
  };
23150
23564
  var isInside = (root, p) => {
23151
23565
  const rel = relative2(root, p);
23152
- return rel === "" || !rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute4(rel);
23566
+ return rel === "" || !rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute5(rel);
23153
23567
  };
23154
23568
  async function realAncestor(p) {
23155
23569
  let cur = p;
@@ -23168,10 +23582,10 @@ async function realAncestor(p) {
23168
23582
  }
23169
23583
  if (isLink) {
23170
23584
  if (++hops > 40) throw new PathRefusal(`too many symbolic links resolving ${p}`);
23171
- cur = resolve6(dirname29(cur), await readlink(cur));
23585
+ cur = resolve6(dirname30(cur), await readlink(cur));
23172
23586
  continue;
23173
23587
  }
23174
- const parent = dirname29(cur);
23588
+ const parent = dirname30(cur);
23175
23589
  if (parent === cur) throw new PathRefusal(`cannot resolve ${p}`);
23176
23590
  tail3.push(basename15(cur));
23177
23591
  cur = parent;
@@ -23181,7 +23595,7 @@ async function resolveInWorktree(root, requested) {
23181
23595
  if (!requested || requested.includes("\0")) throw new PathRefusal("path is required");
23182
23596
  const realRoot = await realpath(root);
23183
23597
  let abs;
23184
- if (isAbsolute4(requested)) {
23598
+ if (isAbsolute5(requested)) {
23185
23599
  abs = resolve6(requested);
23186
23600
  if (isInside(root, abs)) abs = join50(realRoot, relative2(root, abs));
23187
23601
  } else {
@@ -23282,7 +23696,7 @@ function createPeriscopeMcpServer(d, sc) {
23282
23696
  if (Buffer.byteLength(content) > MAX_WRITE_BYTES) return text(`content exceeds ${MAX_WRITE_BYTES} bytes`, true);
23283
23697
  const abs = await inTree(path);
23284
23698
  if (abs === await realpath(root)) return text("path must name a file", true);
23285
- await mkdir(dirname29(abs), { recursive: true });
23699
+ await mkdir(dirname30(abs), { recursive: true });
23286
23700
  await inTree(path);
23287
23701
  await writeFile(abs, content);
23288
23702
  return text(`Wrote ${Buffer.byteLength(content)} bytes to ${await rel(abs)}`);
@@ -23322,7 +23736,7 @@ function createPeriscopeMcpServer(d, sc) {
23322
23736
  inputSchema: { path: z.string() },
23323
23737
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false }
23324
23738
  }, async ({ path }) => {
23325
- const parent = await inTree(dirname29(path) === "" ? "." : dirname29(path));
23739
+ const parent = await inTree(dirname30(path) === "" ? "." : dirname30(path));
23326
23740
  const abs = join50(parent, basename15(path));
23327
23741
  if (basename15(path) === ".git" || abs === parent) return text("path must name a file", true);
23328
23742
  const st = await lstat(abs).catch(() => void 0);
@@ -23395,7 +23809,7 @@ async function readBody(req) {
23395
23809
  let n = 0;
23396
23810
  for await (const c of req) {
23397
23811
  n += c.length;
23398
- if (n > MAX_BODY_BYTES2) return null;
23812
+ if (n > MAX_BODY_BYTES3) return null;
23399
23813
  chunks.push(c);
23400
23814
  }
23401
23815
  return Buffer.concat(chunks).toString("utf8");
@@ -23444,7 +23858,7 @@ function findPeriscope(sidecars, segment) {
23444
23858
  }
23445
23859
 
23446
23860
  // src/cli.ts
23447
- var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname30(process.execPath) : resolve7(dirname30(fileURLToPath(import.meta.url)), "..");
23861
+ var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname31(process.execPath) : resolve7(dirname31(fileURLToPath(import.meta.url)), "..");
23448
23862
  var RELEASE_LOCK_TTL_MS = 20 * 60 * 1e3;
23449
23863
  var CYCLE_SECONDS2 = 120;
23450
23864
  var STALE_CYCLES = 3;
@@ -23701,6 +24115,7 @@ var connectChoices = parseChoices(argv);
23701
24115
  if (!command) usage(argv.length ? `unknown option "${argv[0]}"` : void 0);
23702
24116
  var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
23703
24117
  "passenger-sync-daemon",
24118
+ "periscope-proxy",
23704
24119
  "agents",
23705
24120
  "connect",
23706
24121
  "credential",
@@ -23839,6 +24254,13 @@ if (command === "pool-member") {
23839
24254
  }
23840
24255
  if (command === "passenger-sync-daemon") {
23841
24256
  runSyncDaemonFromEnv();
24257
+ } else if (command === "periscope-proxy") {
24258
+ const dropFile = positional[1];
24259
+ if (!dropFile) {
24260
+ process.stderr.write("crew periscope-proxy is started by `crew periscope up`, not run by hand\n");
24261
+ process.exit(2);
24262
+ }
24263
+ runProxyFromEnv(dropFile);
23842
24264
  } else {
23843
24265
  let requireArmed = function(what) {
23844
24266
  if (dryRun || route2.enabled) return;
@@ -25880,7 +26302,7 @@ ${cu.tail.trimEnd()}
25880
26302
  const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
25881
26303
  const raw = existsSync47(resolvedPath) ? JSON.parse(readFileSync42(resolvedPath, "utf8")) : {};
25882
26304
  raw.agentSkills = result.agentSkills;
25883
- mkdirSync34(dirOf2(resolvedPath), { recursive: true });
26305
+ mkdirSync35(dirOf2(resolvedPath), { recursive: true });
25884
26306
  writeFileSync35(resolvedPath, `${JSON.stringify(raw, null, 2)}
25885
26307
  `);
25886
26308
  }
@@ -25958,7 +26380,7 @@ ${cu.tail.trimEnd()}
25958
26380
  const found = cfg.routes.find((r) => r.route === routeArg);
25959
26381
  if (!found) throw new ReposError(`no route "${routeArg}" in the config (have: ${cfg.routes.map((r) => r.route).join(", ") || "none"})`);
25960
26382
  const named2 = value("route") ? void 0 : positional[3];
25961
- const dir = named2 ? repoDirOf(configText, dirname30(cfg.configFile), found.route, named2) : resolvePath(positional[value("route") ? 2 : 3] ?? ".");
26383
+ const dir = named2 ? repoDirOf(configText, dirname31(cfg.configFile), found.route, named2) : resolvePath(positional[value("route") ? 2 : 3] ?? ".");
25962
26384
  const target = reposOf(found).find((t) => resolvePath(t.dir) === dir || t.name === named2);
25963
26385
  const o = target ? found.repoOverrides[target.name] : void 0;
25964
26386
  runReposCheck(dir, {
@@ -25981,7 +26403,7 @@ ${cu.tail.trimEnd()}
25981
26403
  if (sub === "hooks") {
25982
26404
  try {
25983
26405
  const a = parseHooksArgs(positional.slice(2), routeNamesOf(configText));
25984
- const dir = repoDirOf(configText, dirname30(cfg.configFile), a.route, a.name);
26406
+ const dir = repoDirOf(configText, dirname31(cfg.configFile), a.route, a.name);
25985
26407
  if (!existsSync47(dir)) throw new ReposError(`${dir} does not exist on disk`);
25986
26408
  const files = { exists: (r) => existsSync47(resolvePath(dir, r)), list: (r) => {
25987
26409
  try {
@@ -26148,7 +26570,7 @@ ${cu.tail.trimEnd()}
26148
26570
  }
26149
26571
  const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
26150
26572
  const text2 = configText;
26151
- const base = dirname30(cfg.configFile);
26573
+ const base = dirname31(cfg.configFile);
26152
26574
  const isCheckout = (d) => existsSync47(`${d}/.git`);
26153
26575
  try {
26154
26576
  if (sub === "list") {
@@ -26826,7 +27248,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
26826
27248
  process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
26827
27249
  `);
26828
27250
  } else {
26829
- mkdirSync34(dirname30(resolvedPath), { recursive: true });
27251
+ mkdirSync35(dirname31(resolvedPath), { recursive: true });
26830
27252
  if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
26831
27253
  const tmpPath = `${resolvedPath}.tmp${process.pid}`;
26832
27254
  writeFileSync35(tmpPath, `${JSON.stringify(resolved, null, 2)}
@@ -26875,7 +27297,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
26875
27297
  process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
26876
27298
  `);
26877
27299
  } else {
26878
- mkdirSync34(dirname30(apiKeyFile), { recursive: true });
27300
+ mkdirSync35(dirname31(apiKeyFile), { recursive: true });
26879
27301
  writeFileSync35(apiKeyFile, `${apiKeyVar}=${apiKey}
26880
27302
  `, { mode: 384 });
26881
27303
  process.stderr.write(`API key written to ${apiKeyFile}
@@ -27753,6 +28175,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
27753
28175
  find: async () => hit && { row: hit.row, tag: hit.tag },
27754
28176
  worktreeExists: (p) => existsSync47(p),
27755
28177
  prepareDropFile,
28178
+ startProxy: (wt, url, n) => launchProxy(crewInvocation(CREW_HOME), wt, url, Number(n), { host: hit.row.host || "localhost", inject: hit.repo.config.periscope?.inspector !== "repo" }),
27756
28179
  caps: periscopeCaps(cfg.ship),
27757
28180
  runHook: async (name, cwd, env) => {
27758
28181
  const script = hit?.repo.config.hooks[name];
@@ -27848,7 +28271,7 @@ ${where.join("\n")}`);
27848
28271
  for (const f of repoP.config.worktrees.copy) {
27849
28272
  const src = join51(repoP.dir, f);
27850
28273
  if (!existsSync47(src)) continue;
27851
- mkdirSync34(dirOf2(join51(path, f)), { recursive: true });
28274
+ mkdirSync35(dirOf2(join51(path, f)), { recursive: true });
27852
28275
  copyFileSync2(src, join51(path, f));
27853
28276
  }
27854
28277
  const setup = repoP.config.hooks.setup;
@@ -27862,6 +28285,7 @@ ${where.join("\n")}`);
27862
28285
  },
27863
28286
  createRow: (fields) => trackerP.createPeriscope(fields),
27864
28287
  prepareDropFile,
28288
+ startProxy: (wt, url, n) => launchProxy(crewInvocation(CREW_HOME), wt, url, Number(n), { host: a.host ?? "localhost", inject: repoP.config.periscope?.inspector !== "repo" }),
27865
28289
  registerCode: registerCodeServer,
27866
28290
  recordLocal: (sc) => {
27867
28291
  writePeriscopeSidecar(cfg.ship.stateDir, { ...sc, route: routeP.route });
@@ -28200,7 +28624,7 @@ ${where.join("\n")}`);
28200
28624
  process.stdout.write("crew reload: no daemon unit is running \u2014 nothing to reload (the next run reads crew.yaml fresh)\n");
28201
28625
  break;
28202
28626
  }
28203
- mkdirSync34(cfg.ship.stateDir, { recursive: true });
28627
+ mkdirSync35(cfg.ship.stateDir, { recursive: true });
28204
28628
  writeFileSync35(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
28205
28629
  `);
28206
28630
  process.stdout.write(
@@ -28290,7 +28714,7 @@ ${where.join("\n")}`);
28290
28714
  if (!route2.resolved) fail(`route ${route2.route} is not connected \u2014 run \`crew connect\` first`);
28291
28715
  const tracker = new Tracker(route2, cfg.ship);
28292
28716
  const configText = readFileSync42(cfg.configFile, "utf8");
28293
- const cfgBase = dirname30(cfg.configFile);
28717
+ const cfgBase = dirname31(cfg.configFile);
28294
28718
  const configured2 = configuredRepos(configText, route2.route, cfgBase);
28295
28719
  const dir = configured2.find((r) => r.name === SAMPLE_REPO)?.dir ?? sampleDir(configured2.map((r) => r.dir), route2.reposBasePath, route2.route.split("/")[0]);
28296
28720
  const resolvedFile = resolvedPathFor(cfg.ship.stateDir, route2.route);
@@ -28346,7 +28770,7 @@ ${where.join("\n")}`);
28346
28770
  break;
28347
28771
  }
28348
28772
  if (fresh) {
28349
- mkdirSync34(dir, { recursive: true });
28773
+ mkdirSync35(dir, { recursive: true });
28350
28774
  writeFileSync35(join51(dir, "README.md"), SAMPLE_README);
28351
28775
  writeFileSync35(join51(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
28352
28776
  const g = (...a) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.118.0",
3
+ "version": "0.119.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",