@tablation/crew 0.117.1 → 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 +600 -57
- 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,
|
|
@@ -6932,6 +6939,69 @@ var PERISCOPE_COLUMNS = [
|
|
|
6932
6939
|
"expires_at"
|
|
6933
6940
|
];
|
|
6934
6941
|
|
|
6942
|
+
// src/tool-classify.ts
|
|
6943
|
+
var INTEL_SERVER_NAME2 = "code";
|
|
6944
|
+
var SEARCH_TOOLS = /* @__PURE__ */ new Set([
|
|
6945
|
+
"Grep",
|
|
6946
|
+
"Glob",
|
|
6947
|
+
"LS",
|
|
6948
|
+
// Claude
|
|
6949
|
+
"search_file_content",
|
|
6950
|
+
"glob",
|
|
6951
|
+
"list_directory",
|
|
6952
|
+
// Gemini
|
|
6953
|
+
"grep_search",
|
|
6954
|
+
"file_search"
|
|
6955
|
+
]);
|
|
6956
|
+
var EDIT_TOOLS = /* @__PURE__ */ new Set([
|
|
6957
|
+
"Edit",
|
|
6958
|
+
"Write",
|
|
6959
|
+
"MultiEdit",
|
|
6960
|
+
"NotebookEdit",
|
|
6961
|
+
// Claude
|
|
6962
|
+
"file_change",
|
|
6963
|
+
// Codex
|
|
6964
|
+
"replace",
|
|
6965
|
+
"write_file"
|
|
6966
|
+
// Gemini
|
|
6967
|
+
]);
|
|
6968
|
+
var SHELL_TOOLS = /* @__PURE__ */ new Set(["Bash", "command", "run_shell_command"]);
|
|
6969
|
+
var SEARCH_COMMANDS = /* @__PURE__ */ new Set(["grep", "egrep", "fgrep", "rg", "find", "ls", "fd", "ag"]);
|
|
6970
|
+
function classifyTool(tool, target) {
|
|
6971
|
+
if (!tool) return "other";
|
|
6972
|
+
if (tool.startsWith(`mcp__${INTEL_SERVER_NAME2}__`)) return "symbol";
|
|
6973
|
+
if (EDIT_TOOLS.has(tool)) return "edit";
|
|
6974
|
+
if (SEARCH_TOOLS.has(tool)) return "search";
|
|
6975
|
+
if (SHELL_TOOLS.has(tool)) {
|
|
6976
|
+
const first = (target ?? "").split("/").pop();
|
|
6977
|
+
return first && SEARCH_COMMANDS.has(first) ? "search" : "other";
|
|
6978
|
+
}
|
|
6979
|
+
return "other";
|
|
6980
|
+
}
|
|
6981
|
+
var BeforeFirstEditTracker = class {
|
|
6982
|
+
calls = { search: 0, symbol: 0, other: 0 };
|
|
6983
|
+
edited = false;
|
|
6984
|
+
seconds;
|
|
6985
|
+
/** `elapsedMs` is the time since session start, when known (not on a replayed stream). */
|
|
6986
|
+
note(tool, target, elapsedMs) {
|
|
6987
|
+
if (this.edited) return;
|
|
6988
|
+
const kind = classifyTool(tool, target);
|
|
6989
|
+
if (kind === "edit") {
|
|
6990
|
+
this.edited = true;
|
|
6991
|
+
if (elapsedMs !== void 0) this.seconds = Math.max(0, Math.round(elapsedMs / 1e3));
|
|
6992
|
+
} else {
|
|
6993
|
+
this.calls[kind]++;
|
|
6994
|
+
}
|
|
6995
|
+
}
|
|
6996
|
+
result() {
|
|
6997
|
+
return {
|
|
6998
|
+
calls: { ...this.calls },
|
|
6999
|
+
edited: this.edited,
|
|
7000
|
+
...this.seconds !== void 0 ? { secondsToFirstEdit: this.seconds } : {}
|
|
7001
|
+
};
|
|
7002
|
+
}
|
|
7003
|
+
};
|
|
7004
|
+
|
|
6935
7005
|
// src/agent-log.ts
|
|
6936
7006
|
import { existsSync as existsSync17, mkdirSync as mkdirSync12, readdirSync as readdirSync6, readFileSync as readFileSync17, rmSync as rmSync10, writeFileSync as writeFileSync13 } from "fs";
|
|
6937
7007
|
import { join as join18 } from "path";
|
|
@@ -7006,6 +7076,36 @@ function counterIncrements(run3, cols, have) {
|
|
|
7006
7076
|
}
|
|
7007
7077
|
return out;
|
|
7008
7078
|
}
|
|
7079
|
+
var BEFORE_EDIT_COLUMNS = {
|
|
7080
|
+
search: "calls_before_edit_search",
|
|
7081
|
+
symbol: "calls_before_edit_symbol",
|
|
7082
|
+
other: "calls_before_edit_other",
|
|
7083
|
+
seconds: "seconds_to_first_edit",
|
|
7084
|
+
edited: "edited"
|
|
7085
|
+
};
|
|
7086
|
+
function beforeFirstEditBody(b) {
|
|
7087
|
+
return {
|
|
7088
|
+
[BEFORE_EDIT_COLUMNS.search]: b.calls.search,
|
|
7089
|
+
[BEFORE_EDIT_COLUMNS.symbol]: b.calls.symbol,
|
|
7090
|
+
[BEFORE_EDIT_COLUMNS.other]: b.calls.other,
|
|
7091
|
+
[BEFORE_EDIT_COLUMNS.edited]: b.edited,
|
|
7092
|
+
...b.secondsToFirstEdit !== void 0 ? { [BEFORE_EDIT_COLUMNS.seconds]: b.secondsToFirstEdit } : {}
|
|
7093
|
+
};
|
|
7094
|
+
}
|
|
7095
|
+
async function writeBeforeFirstEdit(target, rowId, b) {
|
|
7096
|
+
try {
|
|
7097
|
+
const model = await request(
|
|
7098
|
+
target,
|
|
7099
|
+
"GET",
|
|
7100
|
+
`/data-models/agent_log?workspaceId=${encodeURIComponent(target.workspaceId)}`
|
|
7101
|
+
);
|
|
7102
|
+
const have = await columnsOf(target, model.id);
|
|
7103
|
+
const body = Object.fromEntries(Object.entries(beforeFirstEditBody(b)).filter(([column]) => have.has(column)));
|
|
7104
|
+
if (Object.keys(body).length === 0) return;
|
|
7105
|
+
await request(target, "PATCH", `/data-models/${model.id}/records/${rowId}`, body);
|
|
7106
|
+
} catch {
|
|
7107
|
+
}
|
|
7108
|
+
}
|
|
7009
7109
|
async function post(target, path, body) {
|
|
7010
7110
|
const url = `${target.baseUrl.replace(/\/+$/, "")}/api/workspaces/${target.workspaceId}${path}`;
|
|
7011
7111
|
const res = await fetch(url, {
|
|
@@ -7074,6 +7174,7 @@ async function reportAgentRun(target, run3) {
|
|
|
7074
7174
|
...cycle.role ? { role: cycle.role } : {}
|
|
7075
7175
|
});
|
|
7076
7176
|
}
|
|
7177
|
+
if (run3.beforeFirstEdit) await writeBeforeFirstEdit(target, entry.id, run3.beforeFirstEdit);
|
|
7077
7178
|
const counters = target.tickets && ticketRecordId ? await addRunToTicket(target, target.tickets, ticketRecordId, run3) : void 0;
|
|
7078
7179
|
return {
|
|
7079
7180
|
agentLogId: entry.id,
|
|
@@ -8604,12 +8705,14 @@ function listeners() {
|
|
|
8604
8705
|
return out;
|
|
8605
8706
|
}
|
|
8606
8707
|
var pidsOnPort = (port) => listeners().filter((l) => l.port === port).map((l) => l.pid);
|
|
8607
|
-
var DEFAULT_PORTS = { backendBase: 3e4, frontendBase: 4e4, span: 1e3 };
|
|
8708
|
+
var DEFAULT_PORTS = { backendBase: 3e4, frontendBase: 4e4, proxyBase: 5e4, span: 1e3 };
|
|
8608
8709
|
function ticketForPort(port, s = DEFAULT_PORTS) {
|
|
8609
8710
|
if (port >= s.backendBase && port < s.backendBase + s.span) return { n: port - s.backendBase, role: "backend" };
|
|
8610
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" };
|
|
8611
8713
|
return null;
|
|
8612
8714
|
}
|
|
8715
|
+
var proxyPortFor = (n, s = DEFAULT_PORTS) => (s.proxyBase ?? DEFAULT_PORTS.proxyBase) + n;
|
|
8613
8716
|
function worktreeExists(parent, prefix, n) {
|
|
8614
8717
|
for (const name of [`${prefix}${n}`, `${prefix}${String(n).padStart(3, "0")}`]) {
|
|
8615
8718
|
if (existsSync18(join19(parent, name, ".git"))) return true;
|
|
@@ -8645,7 +8748,7 @@ async function killGently(pids, graceMs = 2e3) {
|
|
|
8645
8748
|
}
|
|
8646
8749
|
function pidsInWorktree(path, n, s = DEFAULT_PORTS) {
|
|
8647
8750
|
const pids = /* @__PURE__ */ new Set();
|
|
8648
|
-
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);
|
|
8649
8752
|
const matched = run("pgrep", ["-f", `${path}/`]);
|
|
8650
8753
|
for (const line of matched.split("\n")) {
|
|
8651
8754
|
const pid = Number(line.trim());
|
|
@@ -10588,6 +10691,8 @@ var RunObserver = class {
|
|
|
10588
10691
|
result;
|
|
10589
10692
|
/** Prefix tokens of the first assistant call that carried usage; kept once. */
|
|
10590
10693
|
prefixTokens;
|
|
10694
|
+
/** Tool calls before the run's first edit (CREW-2879). */
|
|
10695
|
+
beforeFirstEdit = new BeforeFirstEditTracker();
|
|
10591
10696
|
/** Set once the stream carries the harness's final result (turn count or cost). */
|
|
10592
10697
|
sawResult = false;
|
|
10593
10698
|
muted = false;
|
|
@@ -10599,6 +10704,7 @@ var RunObserver = class {
|
|
|
10599
10704
|
/** Called after `liveTicket` changes, so the sidecar can be rewritten. */
|
|
10600
10705
|
onClaim;
|
|
10601
10706
|
buf = "";
|
|
10707
|
+
startMs = Date.now();
|
|
10602
10708
|
mapper;
|
|
10603
10709
|
id;
|
|
10604
10710
|
adapter;
|
|
@@ -10654,6 +10760,7 @@ var RunObserver = class {
|
|
|
10654
10760
|
for (const ev of events) if ((ev.kind === "thought" || ev.kind === "text") && ev.text) this.attribution.note(ev.text);
|
|
10655
10761
|
for (const ev of events) {
|
|
10656
10762
|
if (ev.kind === "tool") this.attribution.noteWrite(ev.tool, ev.input);
|
|
10763
|
+
if (ev.kind === "tool") this.beforeFirstEdit.note(ev.tool, ev.target, this.muted ? void 0 : Date.now() - this.startMs);
|
|
10657
10764
|
if (ev.kind === "tool" && isClaimWrite(ev.tool, ev.input)) {
|
|
10658
10765
|
const claimed = this.attribution.current;
|
|
10659
10766
|
if (claimed && claimed !== this.liveTicket) {
|
|
@@ -10768,6 +10875,7 @@ async function finishRun(id, obs, emit, startedAt, end, agentLogKey, hintMoved)
|
|
|
10768
10875
|
emit.warn(`could not save the pending hand-off: ${e.message}`, { step: "agent" });
|
|
10769
10876
|
}
|
|
10770
10877
|
}
|
|
10878
|
+
const before = obs.beforeFirstEdit.result();
|
|
10771
10879
|
try {
|
|
10772
10880
|
writeRunMeta(id, startedAt, obs.liveTicket, {
|
|
10773
10881
|
attributedTicket: attributedTicket ?? null,
|
|
@@ -10779,6 +10887,7 @@ async function finishRun(id, obs, emit, startedAt, end, agentLogKey, hintMoved)
|
|
|
10779
10887
|
sessionId: result?.sessionId ?? null,
|
|
10780
10888
|
numTurns: result?.numTurns ?? null,
|
|
10781
10889
|
prefixTokens: obs.prefixTokens ?? null,
|
|
10890
|
+
beforeFirstEdit: before,
|
|
10782
10891
|
totalCostUsd: result?.costUsd ?? null
|
|
10783
10892
|
});
|
|
10784
10893
|
} catch {
|
|
@@ -10794,6 +10903,9 @@ async function finishRun(id, obs, emit, startedAt, end, agentLogKey, hintMoved)
|
|
|
10794
10903
|
attributedTicket: attributedTicket ?? null,
|
|
10795
10904
|
attributedBy,
|
|
10796
10905
|
...result?.sessionId ? { sessionId: result.sessionId } : {},
|
|
10906
|
+
callsBeforeFirstEdit: before.calls,
|
|
10907
|
+
edited: before.edited,
|
|
10908
|
+
...before.secondsToFirstEdit !== void 0 ? { secondsToFirstEdit: before.secondsToFirstEdit } : {},
|
|
10797
10909
|
...result?.numTurns !== void 0 ? { numTurns: result.numTurns } : {},
|
|
10798
10910
|
...result?.costUsd !== void 0 ? { totalCostUsd: result.costUsd } : {}
|
|
10799
10911
|
}
|
|
@@ -10814,6 +10926,7 @@ async function finishRun(id, obs, emit, startedAt, end, agentLogKey, hintMoved)
|
|
|
10814
10926
|
// Both come from a finished run: without the result line (interrupted) neither is trustworthy.
|
|
10815
10927
|
...result?.numTurns !== void 0 ? { numTurns: result.numTurns } : {},
|
|
10816
10928
|
...result?.numTurns !== void 0 && obs.prefixTokens !== void 0 ? { prefixTokens: obs.prefixTokens } : {},
|
|
10929
|
+
beforeFirstEdit: before,
|
|
10817
10930
|
harness: identity.harness,
|
|
10818
10931
|
provider: identity.provider,
|
|
10819
10932
|
promptVersion: id.promptVersion,
|
|
@@ -12669,7 +12782,7 @@ function doctorSelftestLines(stored, toolchains) {
|
|
|
12669
12782
|
}
|
|
12670
12783
|
|
|
12671
12784
|
// src/cli.ts
|
|
12672
|
-
import { resolve as resolve7, dirname as
|
|
12785
|
+
import { resolve as resolve7, dirname as dirname31, join as join51, basename as basename16 } from "path";
|
|
12673
12786
|
import { fileURLToPath } from "url";
|
|
12674
12787
|
import { hostname as hostname4, homedir as homedir11 } from "os";
|
|
12675
12788
|
import { spawnSync as spawnSync10 } from "child_process";
|
|
@@ -19591,7 +19704,20 @@ async function listLogEntries(route2, opts = {}) {
|
|
|
19591
19704
|
limit: opts.limit ?? 20,
|
|
19592
19705
|
...filters.length > 0 ? { filters: JSON.stringify(filters) } : {}
|
|
19593
19706
|
});
|
|
19594
|
-
|
|
19707
|
+
const newestFirst = [...entries].sort((a, b) => String(b.started_at ?? "").localeCompare(String(a.started_at ?? "")));
|
|
19708
|
+
if (!opts.beforeEdit) return newestFirst;
|
|
19709
|
+
const searches = (e) => e.calls_before_edit_search ?? -1;
|
|
19710
|
+
return newestFirst.sort((a, b) => searches(b) - searches(a));
|
|
19711
|
+
}
|
|
19712
|
+
function formatBeforeEdit(e) {
|
|
19713
|
+
if (e.calls_before_edit_search == null && e.calls_before_edit_symbol == null && e.calls_before_edit_other == null) return void 0;
|
|
19714
|
+
const parts = [
|
|
19715
|
+
`search ${e.calls_before_edit_search ?? 0}`,
|
|
19716
|
+
`symbol ${e.calls_before_edit_symbol ?? 0}`,
|
|
19717
|
+
`other ${e.calls_before_edit_other ?? 0}`
|
|
19718
|
+
];
|
|
19719
|
+
const when = e.edited === false ? "no edit" : e.seconds_to_first_edit != null ? `${e.seconds_to_first_edit}s to first edit` : void 0;
|
|
19720
|
+
return `${parts.join(", ")}${when ? ` (${when})` : ""}`;
|
|
19595
19721
|
}
|
|
19596
19722
|
async function showLogEntry(route2, entryId, opts = {}) {
|
|
19597
19723
|
requireWorkspace(route2);
|
|
@@ -20307,7 +20433,7 @@ function warnOrphanedWorktreeDirs(paths, log, held) {
|
|
|
20307
20433
|
}
|
|
20308
20434
|
async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin", base, held) {
|
|
20309
20435
|
const refused = [];
|
|
20310
|
-
const
|
|
20436
|
+
const refuse2 = (a, reason, detail) => {
|
|
20311
20437
|
refused.push(a.path);
|
|
20312
20438
|
const when = held ? noteHeldWorktree(held.state, { repo: held.repo, path: a.path, ticket: a.ticket.issue_id, status: a.ticket.status, reason }) : "first";
|
|
20313
20439
|
if (when) {
|
|
@@ -20335,12 +20461,12 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
|
|
|
20335
20461
|
} catch {
|
|
20336
20462
|
}
|
|
20337
20463
|
if (dirty) {
|
|
20338
|
-
|
|
20464
|
+
refuse2(a, "uncommitted changes", "it has uncommitted changes");
|
|
20339
20465
|
continue;
|
|
20340
20466
|
}
|
|
20341
20467
|
}
|
|
20342
20468
|
if (!dryRun && a.requirePushed && a.branch && existsSync39(a.path) && remoteConfigured(cwd, remote) && worktreeHoldsUnpushed(a.path, remote)) {
|
|
20343
|
-
|
|
20469
|
+
refuse2(a, "unpushed commits", `${a.branch} has commits that are not on ${remote}`);
|
|
20344
20470
|
continue;
|
|
20345
20471
|
}
|
|
20346
20472
|
const n = Number(a.ticket.issue_id.replace(/^\D+/, ""));
|
|
@@ -20364,7 +20490,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
|
|
|
20364
20490
|
try {
|
|
20365
20491
|
git(cwd, ["worktree", "remove", "--force", a.path]);
|
|
20366
20492
|
} catch (e) {
|
|
20367
|
-
|
|
20493
|
+
refuse2(a, "removal failed", `could not remove it: ${e.message}`);
|
|
20368
20494
|
continue;
|
|
20369
20495
|
}
|
|
20370
20496
|
if (!a.keepBranch && a.branch) {
|
|
@@ -20538,7 +20664,7 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
|
20538
20664
|
}
|
|
20539
20665
|
|
|
20540
20666
|
// src/cli.ts
|
|
20541
|
-
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";
|
|
20542
20668
|
import { createInterface } from "readline/promises";
|
|
20543
20669
|
|
|
20544
20670
|
// src/periscope-cmd.ts
|
|
@@ -20744,9 +20870,18 @@ ${tail(handoff.output)}`);
|
|
|
20744
20870
|
return abandon(`preview exited ${preview?.code ?? "without running"}
|
|
20745
20871
|
${tail(preview?.output ?? "")}`);
|
|
20746
20872
|
}
|
|
20747
|
-
const
|
|
20748
|
-
if (!
|
|
20873
|
+
const previewUrl = previewUrlFrom(preview.output);
|
|
20874
|
+
if (!previewUrl) return abandon(`preview printed no URL as its last line
|
|
20749
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
|
+
}
|
|
20750
20885
|
d.say("preview", `preview at ${url}`, { url });
|
|
20751
20886
|
const intel = await probeIntel(
|
|
20752
20887
|
{ runHook: (e) => d.runHook("intel", wt.path, e) },
|
|
@@ -20935,10 +21070,17 @@ ${section}`;
|
|
|
20935
21070
|
forgetLocal(d, row.ticketRecordId);
|
|
20936
21071
|
return { ticket: row.ticket, mode: "keep", outcome: "done", state: "kept", sha: pushed.sha, ...existing ? { existing: true, ...prior ? { status: prior.status } : {} } : {} };
|
|
20937
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
|
+
}
|
|
20938
21080
|
function periscopeListEntries(rows, now, isListening) {
|
|
20939
21081
|
return rows.filter((r) => r.state === "live").map((r) => {
|
|
20940
21082
|
const at = r.lastActivityAt ? new Date(r.lastActivityAt).getTime() : NaN;
|
|
20941
|
-
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) };
|
|
20942
21084
|
});
|
|
20943
21085
|
}
|
|
20944
21086
|
function formatPeriscopeList(entries) {
|
|
@@ -20947,7 +21089,7 @@ function formatPeriscopeList(entries) {
|
|
|
20947
21089
|
e.ticket,
|
|
20948
21090
|
e.repo,
|
|
20949
21091
|
e.worktree,
|
|
20950
|
-
e.url,
|
|
21092
|
+
e.proxied ? `${e.url} (proxy)` : e.url,
|
|
20951
21093
|
e.state,
|
|
20952
21094
|
e.idleMinutes === null ? "idle ?" : `idle ${e.idleMinutes} min`,
|
|
20953
21095
|
e.listening ? "listening" : "not listening"
|
|
@@ -20979,9 +21121,16 @@ ${tail2(iso.output)}`);
|
|
|
20979
21121
|
throw new PeriscopeError(`preview exited ${preview?.code ?? "without running"}
|
|
20980
21122
|
${tail2(preview?.output ?? "")}`);
|
|
20981
21123
|
}
|
|
20982
|
-
|
|
21124
|
+
let url = previewUrlFrom(preview.output);
|
|
20983
21125
|
if (!url) throw new PeriscopeError(`preview printed no URL as its last line
|
|
20984
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
|
+
}
|
|
20985
21134
|
const now = d.now();
|
|
20986
21135
|
await d.update(row.id, {
|
|
20987
21136
|
url,
|
|
@@ -21517,6 +21666,388 @@ function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
|
|
|
21517
21666
|
return endpoints;
|
|
21518
21667
|
}
|
|
21519
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
|
+
|
|
21520
22051
|
// src/outage.ts
|
|
21521
22052
|
var TRANSIENT_BACKOFF_MS = [15e3, 3e4, 6e4];
|
|
21522
22053
|
var OUTAGE_ESCALATE_AFTER = 5;
|
|
@@ -21549,7 +22080,7 @@ function describeCause(err3) {
|
|
|
21549
22080
|
}
|
|
21550
22081
|
|
|
21551
22082
|
// src/daemon.ts
|
|
21552
|
-
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";
|
|
21553
22084
|
import { join as join47 } from "path";
|
|
21554
22085
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
21555
22086
|
var DEFAULT_FLOOR_MS = 5e3;
|
|
@@ -21663,7 +22194,7 @@ function daemonStampFor(stateDir) {
|
|
|
21663
22194
|
return join47(stateDir, "daemon.started");
|
|
21664
22195
|
}
|
|
21665
22196
|
function noteDaemonStarted(stateDir, now = Date.now()) {
|
|
21666
|
-
|
|
22197
|
+
mkdirSync33(stateDir, { recursive: true });
|
|
21667
22198
|
rmSync20(reloadSentinelFor(stateDir), { force: true });
|
|
21668
22199
|
writeFileSync33(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
|
|
21669
22200
|
`);
|
|
@@ -21735,7 +22266,7 @@ async function runOnePassUnguarded(o) {
|
|
|
21735
22266
|
await hydrateApiKeys([o.route], o.getSessionStore);
|
|
21736
22267
|
const emit = o.newEmitter();
|
|
21737
22268
|
const decide = o.decide ?? decideCycle;
|
|
21738
|
-
const
|
|
22269
|
+
const spawn12 = o.spawnRoleAgent ?? runRoleAgent;
|
|
21739
22270
|
const breaker = o.spawnBreaker ?? new SpawnBreaker();
|
|
21740
22271
|
const ctx = { route: o.route, ship: o.ship, state: o.state, emit, getSessionStore: o.getSessionStore };
|
|
21741
22272
|
if (o.sandboxCache) {
|
|
@@ -21846,7 +22377,7 @@ async function runOnePassUnguarded(o) {
|
|
|
21846
22377
|
settle = resolve8;
|
|
21847
22378
|
});
|
|
21848
22379
|
const ready = (tookTicket2) => settle(tookTicket2);
|
|
21849
|
-
const run3 =
|
|
22380
|
+
const run3 = spawn12(role, decision, ctx, { ready }).then(() => {
|
|
21850
22381
|
breaker.recordSuccess(role);
|
|
21851
22382
|
clearShipAttention(o.state, `role_parked:${role}`);
|
|
21852
22383
|
clearShipAttention(o.state, workspaceAuthKey(role));
|
|
@@ -22246,12 +22777,12 @@ import {
|
|
|
22246
22777
|
watch
|
|
22247
22778
|
} from "fs";
|
|
22248
22779
|
import { createConnection } from "net";
|
|
22249
|
-
import { createServer } from "http";
|
|
22250
|
-
import { basename as basename14, dirname as
|
|
22251
|
-
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";
|
|
22252
22783
|
|
|
22253
22784
|
// src/periscope-control.ts
|
|
22254
|
-
import { spawn as
|
|
22785
|
+
import { spawn as spawn10 } from "child_process";
|
|
22255
22786
|
var reply = (status2, body) => ({ status: status2, body });
|
|
22256
22787
|
var err = (status2, message) => reply(status2, { error: message });
|
|
22257
22788
|
var TICKET_RE = /^(?:[A-Za-z]+-)?\d+$/;
|
|
@@ -22442,7 +22973,7 @@ async function handlePeriscopes(h, q2) {
|
|
|
22442
22973
|
}
|
|
22443
22974
|
function runCrew(args, onLine, timeoutMs = 20 * 6e4) {
|
|
22444
22975
|
return new Promise((resolve8) => {
|
|
22445
|
-
const child =
|
|
22976
|
+
const child = spawn10(process.execPath, [process.argv[1] ?? "", ...args], { stdio: ["ignore", "pipe", "pipe"] });
|
|
22446
22977
|
let stdout = "";
|
|
22447
22978
|
let stderr = "";
|
|
22448
22979
|
let pending = "";
|
|
@@ -22481,7 +23012,7 @@ var MAX_SOCKET_PATH = 100;
|
|
|
22481
23012
|
var MAX_TAIL_BYTES = 1024 * 1024;
|
|
22482
23013
|
var MAX_WAIT_SECONDS = 30;
|
|
22483
23014
|
var DEFAULT_TAIL_LINES = 100;
|
|
22484
|
-
var
|
|
23015
|
+
var MAX_BODY_BYTES2 = 4096;
|
|
22485
23016
|
var ControlAuth = class {
|
|
22486
23017
|
cache = /* @__PURE__ */ new Map();
|
|
22487
23018
|
o;
|
|
@@ -22563,7 +23094,7 @@ function candidates(live, rotated) {
|
|
|
22563
23094
|
const out = [live];
|
|
22564
23095
|
if (!rotated) return out;
|
|
22565
23096
|
try {
|
|
22566
|
-
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));
|
|
22567
23098
|
} catch {
|
|
22568
23099
|
}
|
|
22569
23100
|
return out;
|
|
@@ -22702,7 +23233,7 @@ async function readJsonBody(req) {
|
|
|
22702
23233
|
let n = 0;
|
|
22703
23234
|
for await (const c of req) {
|
|
22704
23235
|
n += c.length;
|
|
22705
|
-
if (n >
|
|
23236
|
+
if (n > MAX_BODY_BYTES2) return null;
|
|
22706
23237
|
chunks.push(c);
|
|
22707
23238
|
}
|
|
22708
23239
|
if (!n) return {};
|
|
@@ -22723,7 +23254,7 @@ async function tailReply(path, q2, rotated, abort) {
|
|
|
22723
23254
|
const deadline = Date.now() + Math.min(wait, MAX_WAIT_SECONDS) * 1e3;
|
|
22724
23255
|
let r = readTail(path, { after, tail: tail3, rotated });
|
|
22725
23256
|
while (after && !r.lines.length && !r.reset && Date.now() < deadline && !abort.aborted) {
|
|
22726
|
-
await waitForChange(
|
|
23257
|
+
await waitForChange(dirname29(path), deadline - Date.now(), abort);
|
|
22727
23258
|
r = readTail(path, { after: r.cursor, rotated });
|
|
22728
23259
|
}
|
|
22729
23260
|
return reply2(200, r);
|
|
@@ -22831,7 +23362,7 @@ async function startControlServer(deps) {
|
|
|
22831
23362
|
log(`control API not started: ${path} is in use by another process`);
|
|
22832
23363
|
return null;
|
|
22833
23364
|
}
|
|
22834
|
-
const server =
|
|
23365
|
+
const server = createServer2((req, res) => {
|
|
22835
23366
|
const abort = new AbortController();
|
|
22836
23367
|
res.on("close", () => abort.abort());
|
|
22837
23368
|
void (async () => {
|
|
@@ -22882,7 +23413,7 @@ async function startControlServer(deps) {
|
|
|
22882
23413
|
});
|
|
22883
23414
|
}
|
|
22884
23415
|
var shq2 = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
|
|
22885
|
-
function runDetached(commands, spawnFn =
|
|
23416
|
+
function runDetached(commands, spawnFn = spawn11) {
|
|
22886
23417
|
const script = `sleep 1; ${commands.map((c) => c.map(shq2).join(" ")).join("; ")}`;
|
|
22887
23418
|
const child = spawnFn("/bin/sh", ["-c", script], { detached: true, stdio: "ignore" });
|
|
22888
23419
|
child.on("error", () => {
|
|
@@ -22913,21 +23444,21 @@ function crewJson(args, timeoutMs = 6e4) {
|
|
|
22913
23444
|
|
|
22914
23445
|
// src/periscope-mcp.ts
|
|
22915
23446
|
import { lstat, mkdir, readdir, readFile, readlink, realpath, rm, stat, writeFile } from "fs/promises";
|
|
22916
|
-
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";
|
|
22917
23448
|
import { z } from "zod";
|
|
22918
23449
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
22919
23450
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
22920
23451
|
|
|
22921
23452
|
// src/periscope-endpoints.ts
|
|
22922
23453
|
import { randomBytes as randomBytes3 } from "crypto";
|
|
22923
|
-
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";
|
|
22924
23455
|
import { join as join49 } from "path";
|
|
22925
23456
|
var secretDir = (stateDir) => join49(stateDir, "periscope-mcp");
|
|
22926
23457
|
var secretFile = (stateDir, id) => join49(secretDir(stateDir), `${id}.json`);
|
|
22927
23458
|
var endpointDir = (stateDir) => join49(stateDir, "periscope-endpoints");
|
|
22928
23459
|
var endpointFile = (stateDir, id) => join49(endpointDir(stateDir), `${id}.json`);
|
|
22929
23460
|
function mintPeriscopeSecret(stateDir, ticketRecordId) {
|
|
22930
|
-
|
|
23461
|
+
mkdirSync34(secretDir(stateDir), { recursive: true, mode: 448 });
|
|
22931
23462
|
const secret = randomBytes3(32).toString("base64url");
|
|
22932
23463
|
const file = secretFile(stateDir, ticketRecordId);
|
|
22933
23464
|
writeFileSync34(file, `${JSON.stringify({ secret })}
|
|
@@ -22954,7 +23485,7 @@ function readPeriscopeEndpoint(stateDir, ticketRecordId) {
|
|
|
22954
23485
|
}
|
|
22955
23486
|
}
|
|
22956
23487
|
function writePeriscopeEndpoint(stateDir, rec) {
|
|
22957
|
-
|
|
23488
|
+
mkdirSync34(endpointDir(stateDir), { recursive: true });
|
|
22958
23489
|
writeFileSync34(endpointFile(stateDir, rec.ticketRecordId), `${JSON.stringify(rec, null, 2)}
|
|
22959
23490
|
`);
|
|
22960
23491
|
}
|
|
@@ -23024,7 +23555,7 @@ async function removeEndedPeriscopeEndpoints(d) {
|
|
|
23024
23555
|
var PERISCOPE_HOOKS = ["test", "build", "ports", "handoff", "preview", "isolate"];
|
|
23025
23556
|
var MAX_READ_BYTES = 1024 * 1024;
|
|
23026
23557
|
var MAX_WRITE_BYTES = 4 * 1024 * 1024;
|
|
23027
|
-
var
|
|
23558
|
+
var MAX_BODY_BYTES3 = 8 * 1024 * 1024;
|
|
23028
23559
|
var MAX_HOOK_OUTPUT = 2e4;
|
|
23029
23560
|
var MAX_DIFF = 2e5;
|
|
23030
23561
|
var MAX_SEARCH_RESULTS = 500;
|
|
@@ -23032,7 +23563,7 @@ var PathRefusal = class extends Error {
|
|
|
23032
23563
|
};
|
|
23033
23564
|
var isInside = (root, p) => {
|
|
23034
23565
|
const rel = relative2(root, p);
|
|
23035
|
-
return rel === "" || !rel.startsWith(`..${sep}`) && rel !== ".." && !
|
|
23566
|
+
return rel === "" || !rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute5(rel);
|
|
23036
23567
|
};
|
|
23037
23568
|
async function realAncestor(p) {
|
|
23038
23569
|
let cur = p;
|
|
@@ -23051,10 +23582,10 @@ async function realAncestor(p) {
|
|
|
23051
23582
|
}
|
|
23052
23583
|
if (isLink) {
|
|
23053
23584
|
if (++hops > 40) throw new PathRefusal(`too many symbolic links resolving ${p}`);
|
|
23054
|
-
cur = resolve6(
|
|
23585
|
+
cur = resolve6(dirname30(cur), await readlink(cur));
|
|
23055
23586
|
continue;
|
|
23056
23587
|
}
|
|
23057
|
-
const parent =
|
|
23588
|
+
const parent = dirname30(cur);
|
|
23058
23589
|
if (parent === cur) throw new PathRefusal(`cannot resolve ${p}`);
|
|
23059
23590
|
tail3.push(basename15(cur));
|
|
23060
23591
|
cur = parent;
|
|
@@ -23064,7 +23595,7 @@ async function resolveInWorktree(root, requested) {
|
|
|
23064
23595
|
if (!requested || requested.includes("\0")) throw new PathRefusal("path is required");
|
|
23065
23596
|
const realRoot = await realpath(root);
|
|
23066
23597
|
let abs;
|
|
23067
|
-
if (
|
|
23598
|
+
if (isAbsolute5(requested)) {
|
|
23068
23599
|
abs = resolve6(requested);
|
|
23069
23600
|
if (isInside(root, abs)) abs = join50(realRoot, relative2(root, abs));
|
|
23070
23601
|
} else {
|
|
@@ -23165,7 +23696,7 @@ function createPeriscopeMcpServer(d, sc) {
|
|
|
23165
23696
|
if (Buffer.byteLength(content) > MAX_WRITE_BYTES) return text(`content exceeds ${MAX_WRITE_BYTES} bytes`, true);
|
|
23166
23697
|
const abs = await inTree(path);
|
|
23167
23698
|
if (abs === await realpath(root)) return text("path must name a file", true);
|
|
23168
|
-
await mkdir(
|
|
23699
|
+
await mkdir(dirname30(abs), { recursive: true });
|
|
23169
23700
|
await inTree(path);
|
|
23170
23701
|
await writeFile(abs, content);
|
|
23171
23702
|
return text(`Wrote ${Buffer.byteLength(content)} bytes to ${await rel(abs)}`);
|
|
@@ -23205,7 +23736,7 @@ function createPeriscopeMcpServer(d, sc) {
|
|
|
23205
23736
|
inputSchema: { path: z.string() },
|
|
23206
23737
|
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false }
|
|
23207
23738
|
}, async ({ path }) => {
|
|
23208
|
-
const parent = await inTree(
|
|
23739
|
+
const parent = await inTree(dirname30(path) === "" ? "." : dirname30(path));
|
|
23209
23740
|
const abs = join50(parent, basename15(path));
|
|
23210
23741
|
if (basename15(path) === ".git" || abs === parent) return text("path must name a file", true);
|
|
23211
23742
|
const st = await lstat(abs).catch(() => void 0);
|
|
@@ -23278,7 +23809,7 @@ async function readBody(req) {
|
|
|
23278
23809
|
let n = 0;
|
|
23279
23810
|
for await (const c of req) {
|
|
23280
23811
|
n += c.length;
|
|
23281
|
-
if (n >
|
|
23812
|
+
if (n > MAX_BODY_BYTES3) return null;
|
|
23282
23813
|
chunks.push(c);
|
|
23283
23814
|
}
|
|
23284
23815
|
return Buffer.concat(chunks).toString("utf8");
|
|
@@ -23327,7 +23858,7 @@ function findPeriscope(sidecars, segment) {
|
|
|
23327
23858
|
}
|
|
23328
23859
|
|
|
23329
23860
|
// src/cli.ts
|
|
23330
|
-
var CREW_HOME = isCompiledBinary(import.meta.url) ?
|
|
23861
|
+
var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname31(process.execPath) : resolve7(dirname31(fileURLToPath(import.meta.url)), "..");
|
|
23331
23862
|
var RELEASE_LOCK_TTL_MS = 20 * 60 * 1e3;
|
|
23332
23863
|
var CYCLE_SECONDS2 = 120;
|
|
23333
23864
|
var STALE_CYCLES = 3;
|
|
@@ -23526,7 +24057,7 @@ function usage(problem, requested = false) {
|
|
|
23526
24057
|
crew repos clone [ROUTE] NAME clone a Repos row's remote into the route directory and attach it (--dir PATH, --dry-run, --json)
|
|
23527
24058
|
crew repos hooks [ROUTE] NAME suggest test/build hooks for a checkout (--agent, --write route|repo, --recipes, --json)
|
|
23528
24059
|
crew repos list ROUTE the checkouts a route has, with the tracker's Repos row each matches (--json: every Repos row too)
|
|
23529
|
-
crew logbook list [route] recent Agent Log entries, filterable by --role/--ticket
|
|
24060
|
+
crew logbook list [route] recent Agent Log entries, filterable by --role/--ticket; --before-edit sorts by searches before the first edit
|
|
23530
24061
|
crew logbook show [route] ID [--prompt] one entry; --prompt reconstructs and verifies its prompt
|
|
23531
24062
|
crew passengers [route] sync Host Passengers containers/tunnels once; what the passengers unit invokes
|
|
23532
24063
|
crew install write and load this platform's scheduler unit
|
|
@@ -23584,6 +24115,7 @@ var connectChoices = parseChoices(argv);
|
|
|
23584
24115
|
if (!command) usage(argv.length ? `unknown option "${argv[0]}"` : void 0);
|
|
23585
24116
|
var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
|
|
23586
24117
|
"passenger-sync-daemon",
|
|
24118
|
+
"periscope-proxy",
|
|
23587
24119
|
"agents",
|
|
23588
24120
|
"connect",
|
|
23589
24121
|
"credential",
|
|
@@ -23722,6 +24254,13 @@ if (command === "pool-member") {
|
|
|
23722
24254
|
}
|
|
23723
24255
|
if (command === "passenger-sync-daemon") {
|
|
23724
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);
|
|
23725
24264
|
} else {
|
|
23726
24265
|
let requireArmed = function(what) {
|
|
23727
24266
|
if (dryRun || route2.enabled) return;
|
|
@@ -25763,7 +26302,7 @@ ${cu.tail.trimEnd()}
|
|
|
25763
26302
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
25764
26303
|
const raw = existsSync47(resolvedPath) ? JSON.parse(readFileSync42(resolvedPath, "utf8")) : {};
|
|
25765
26304
|
raw.agentSkills = result.agentSkills;
|
|
25766
|
-
|
|
26305
|
+
mkdirSync35(dirOf2(resolvedPath), { recursive: true });
|
|
25767
26306
|
writeFileSync35(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
25768
26307
|
`);
|
|
25769
26308
|
}
|
|
@@ -25841,7 +26380,7 @@ ${cu.tail.trimEnd()}
|
|
|
25841
26380
|
const found = cfg.routes.find((r) => r.route === routeArg);
|
|
25842
26381
|
if (!found) throw new ReposError(`no route "${routeArg}" in the config (have: ${cfg.routes.map((r) => r.route).join(", ") || "none"})`);
|
|
25843
26382
|
const named2 = value("route") ? void 0 : positional[3];
|
|
25844
|
-
const dir = named2 ? repoDirOf(configText,
|
|
26383
|
+
const dir = named2 ? repoDirOf(configText, dirname31(cfg.configFile), found.route, named2) : resolvePath(positional[value("route") ? 2 : 3] ?? ".");
|
|
25845
26384
|
const target = reposOf(found).find((t) => resolvePath(t.dir) === dir || t.name === named2);
|
|
25846
26385
|
const o = target ? found.repoOverrides[target.name] : void 0;
|
|
25847
26386
|
runReposCheck(dir, {
|
|
@@ -25864,7 +26403,7 @@ ${cu.tail.trimEnd()}
|
|
|
25864
26403
|
if (sub === "hooks") {
|
|
25865
26404
|
try {
|
|
25866
26405
|
const a = parseHooksArgs(positional.slice(2), routeNamesOf(configText));
|
|
25867
|
-
const dir = repoDirOf(configText,
|
|
26406
|
+
const dir = repoDirOf(configText, dirname31(cfg.configFile), a.route, a.name);
|
|
25868
26407
|
if (!existsSync47(dir)) throw new ReposError(`${dir} does not exist on disk`);
|
|
25869
26408
|
const files = { exists: (r) => existsSync47(resolvePath(dir, r)), list: (r) => {
|
|
25870
26409
|
try {
|
|
@@ -26031,7 +26570,7 @@ ${cu.tail.trimEnd()}
|
|
|
26031
26570
|
}
|
|
26032
26571
|
const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
|
|
26033
26572
|
const text2 = configText;
|
|
26034
|
-
const base =
|
|
26573
|
+
const base = dirname31(cfg.configFile);
|
|
26035
26574
|
const isCheckout = (d) => existsSync47(`${d}/.git`);
|
|
26036
26575
|
try {
|
|
26037
26576
|
if (sub === "list") {
|
|
@@ -26179,7 +26718,7 @@ ${cu.tail.trimEnd()}
|
|
|
26179
26718
|
const sub = positional[1];
|
|
26180
26719
|
if (sub !== "list" && sub !== "show") {
|
|
26181
26720
|
process.stderr.write(
|
|
26182
|
-
"crew logbook list [route] [--role R] [--ticket T] [--limit N]\ncrew logbook show [route] <entryId> [--prompt]\n"
|
|
26721
|
+
"crew logbook list [route] [--role R] [--ticket T] [--limit N] [--before-edit]\ncrew logbook show [route] <entryId> [--prompt]\n"
|
|
26183
26722
|
);
|
|
26184
26723
|
process.exit(2);
|
|
26185
26724
|
}
|
|
@@ -26206,7 +26745,8 @@ ${cu.tail.trimEnd()}
|
|
|
26206
26745
|
userAgent: cfg.ship.userAgent,
|
|
26207
26746
|
role: value("role"),
|
|
26208
26747
|
ticket: value("ticket"),
|
|
26209
|
-
limit: value("limit") ? Number(value("limit")) : void 0
|
|
26748
|
+
limit: value("limit") ? Number(value("limit")) : void 0,
|
|
26749
|
+
beforeEdit: flag("before-edit")
|
|
26210
26750
|
});
|
|
26211
26751
|
if (entries.length === 0) {
|
|
26212
26752
|
process.stdout.write("(no Agent Log entries match)\n");
|
|
@@ -26214,7 +26754,7 @@ ${cu.tail.trimEnd()}
|
|
|
26214
26754
|
}
|
|
26215
26755
|
for (const e of entries) {
|
|
26216
26756
|
process.stdout.write(
|
|
26217
|
-
`${e.id} ${e.started_at ?? "?"} ${e.outcome ?? "?"}${e.ticket_reference ? ` ${e.ticket_reference}` : ""}${e.model ? ` ${e.model}` : ""}${e.harness || e.provider ? ` (${[e.harness, e.provider].filter(Boolean).join(" \xB7 ")})` : ""}${e.num_turns != null ? ` turns ${e.num_turns}` : ""}${e.prefix_tokens != null ? ` prefix ${e.prefix_tokens}` : ""}
|
|
26757
|
+
`${e.id} ${e.started_at ?? "?"} ${e.outcome ?? "?"}${e.ticket_reference ? ` ${e.ticket_reference}` : ""}${e.model ? ` ${e.model}` : ""}${e.harness || e.provider ? ` (${[e.harness, e.provider].filter(Boolean).join(" \xB7 ")})` : ""}${e.num_turns != null ? ` turns ${e.num_turns}` : ""}${e.prefix_tokens != null ? ` prefix ${e.prefix_tokens}` : ""}${formatBeforeEdit(e) ? ` before edit: ${formatBeforeEdit(e)}` : ""}
|
|
26218
26758
|
`
|
|
26219
26759
|
);
|
|
26220
26760
|
}
|
|
@@ -26234,6 +26774,7 @@ harness: ${entry.harness ?? "?"}
|
|
|
26234
26774
|
provider: ${entry.provider ?? "?"}
|
|
26235
26775
|
` + (entry.num_turns != null ? `turns: ${entry.num_turns}
|
|
26236
26776
|
` : "") + (entry.prefix_tokens != null ? `prefix: ${entry.prefix_tokens} tokens
|
|
26777
|
+
` : "") + (formatBeforeEdit(entry) ? `before edit: ${formatBeforeEdit(entry)}
|
|
26237
26778
|
` : "")
|
|
26238
26779
|
);
|
|
26239
26780
|
if (flag("prompt")) {
|
|
@@ -26707,7 +27248,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
26707
27248
|
process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
|
|
26708
27249
|
`);
|
|
26709
27250
|
} else {
|
|
26710
|
-
|
|
27251
|
+
mkdirSync35(dirname31(resolvedPath), { recursive: true });
|
|
26711
27252
|
if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
|
|
26712
27253
|
const tmpPath = `${resolvedPath}.tmp${process.pid}`;
|
|
26713
27254
|
writeFileSync35(tmpPath, `${JSON.stringify(resolved, null, 2)}
|
|
@@ -26756,7 +27297,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
26756
27297
|
process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
|
|
26757
27298
|
`);
|
|
26758
27299
|
} else {
|
|
26759
|
-
|
|
27300
|
+
mkdirSync35(dirname31(apiKeyFile), { recursive: true });
|
|
26760
27301
|
writeFileSync35(apiKeyFile, `${apiKeyVar}=${apiKey}
|
|
26761
27302
|
`, { mode: 384 });
|
|
26762
27303
|
process.stderr.write(`API key written to ${apiKeyFile}
|
|
@@ -27634,6 +28175,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
27634
28175
|
find: async () => hit && { row: hit.row, tag: hit.tag },
|
|
27635
28176
|
worktreeExists: (p) => existsSync47(p),
|
|
27636
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" }),
|
|
27637
28179
|
caps: periscopeCaps(cfg.ship),
|
|
27638
28180
|
runHook: async (name, cwd, env) => {
|
|
27639
28181
|
const script = hit?.repo.config.hooks[name];
|
|
@@ -27729,7 +28271,7 @@ ${where.join("\n")}`);
|
|
|
27729
28271
|
for (const f of repoP.config.worktrees.copy) {
|
|
27730
28272
|
const src = join51(repoP.dir, f);
|
|
27731
28273
|
if (!existsSync47(src)) continue;
|
|
27732
|
-
|
|
28274
|
+
mkdirSync35(dirOf2(join51(path, f)), { recursive: true });
|
|
27733
28275
|
copyFileSync2(src, join51(path, f));
|
|
27734
28276
|
}
|
|
27735
28277
|
const setup = repoP.config.hooks.setup;
|
|
@@ -27743,6 +28285,7 @@ ${where.join("\n")}`);
|
|
|
27743
28285
|
},
|
|
27744
28286
|
createRow: (fields) => trackerP.createPeriscope(fields),
|
|
27745
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" }),
|
|
27746
28289
|
registerCode: registerCodeServer,
|
|
27747
28290
|
recordLocal: (sc) => {
|
|
27748
28291
|
writePeriscopeSidecar(cfg.ship.stateDir, { ...sc, route: routeP.route });
|
|
@@ -28081,7 +28624,7 @@ ${where.join("\n")}`);
|
|
|
28081
28624
|
process.stdout.write("crew reload: no daemon unit is running \u2014 nothing to reload (the next run reads crew.yaml fresh)\n");
|
|
28082
28625
|
break;
|
|
28083
28626
|
}
|
|
28084
|
-
|
|
28627
|
+
mkdirSync35(cfg.ship.stateDir, { recursive: true });
|
|
28085
28628
|
writeFileSync35(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
|
|
28086
28629
|
`);
|
|
28087
28630
|
process.stdout.write(
|
|
@@ -28171,7 +28714,7 @@ ${where.join("\n")}`);
|
|
|
28171
28714
|
if (!route2.resolved) fail(`route ${route2.route} is not connected \u2014 run \`crew connect\` first`);
|
|
28172
28715
|
const tracker = new Tracker(route2, cfg.ship);
|
|
28173
28716
|
const configText = readFileSync42(cfg.configFile, "utf8");
|
|
28174
|
-
const cfgBase =
|
|
28717
|
+
const cfgBase = dirname31(cfg.configFile);
|
|
28175
28718
|
const configured2 = configuredRepos(configText, route2.route, cfgBase);
|
|
28176
28719
|
const dir = configured2.find((r) => r.name === SAMPLE_REPO)?.dir ?? sampleDir(configured2.map((r) => r.dir), route2.reposBasePath, route2.route.split("/")[0]);
|
|
28177
28720
|
const resolvedFile = resolvedPathFor(cfg.ship.stateDir, route2.route);
|
|
@@ -28227,7 +28770,7 @@ ${where.join("\n")}`);
|
|
|
28227
28770
|
break;
|
|
28228
28771
|
}
|
|
28229
28772
|
if (fresh) {
|
|
28230
|
-
|
|
28773
|
+
mkdirSync35(dir, { recursive: true });
|
|
28231
28774
|
writeFileSync35(join51(dir, "README.md"), SAMPLE_README);
|
|
28232
28775
|
writeFileSync35(join51(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
|
|
28233
28776
|
const g = (...a) => {
|
package/package.json
CHANGED