@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.
- package/dist/cli.js +476 -52
- 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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
20865
|
-
if (!
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
22368
|
-
import { execFile as execFile3, spawn as
|
|
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
|
|
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 =
|
|
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
|
|
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(
|
|
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 >
|
|
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(
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 !== ".." && !
|
|
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(
|
|
23585
|
+
cur = resolve6(dirname30(cur), await readlink(cur));
|
|
23172
23586
|
continue;
|
|
23173
23587
|
}
|
|
23174
|
-
const parent =
|
|
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 (
|
|
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(
|
|
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(
|
|
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 >
|
|
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) ?
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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