toolroll 0.9.92 → 0.9.94
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/CHANGELOG.md +87 -0
- package/dist/activity-line.d.ts +7 -4
- package/dist/activity-line.js +9 -7
- package/dist/agent-fence.d.ts +4 -0
- package/dist/agent-fence.js +4 -1
- package/dist/approval-authority.d.ts +7 -1
- package/dist/approval-authority.js +11 -2
- package/dist/approval-password.d.ts +31 -0
- package/dist/approval-password.js +26 -0
- package/dist/assignment-status.d.ts +1 -1
- package/dist/assignment-ui.js +3 -3
- package/dist/assignment.js +16 -3
- package/dist/boot-identity.d.ts +2 -1
- package/dist/boot-identity.js +6 -4
- package/dist/bounded-spawn.d.ts +57 -0
- package/dist/bounded-spawn.js +220 -0
- package/dist/browser/terminal.js +42 -42
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +26 -26
- package/dist/browser-crew.js +2 -2
- package/dist/browser-shell.d.ts +1 -1
- package/dist/browser-shell.js +2 -2
- package/dist/browser-workspace.d.ts +48 -7
- package/dist/browser-workspace.js +3 -3
- package/dist/builder-restart.d.ts +21 -0
- package/dist/builder-restart.js +50 -0
- package/dist/builder.d.ts +4 -0
- package/dist/builder.js +73 -25
- package/dist/chat-acceptance.js +1 -1
- package/dist/chat-actions.d.ts +12 -2
- package/dist/chat-actions.js +25 -2
- package/dist/chat-channel.js +11 -0
- package/dist/chat-decide.d.ts +7 -2
- package/dist/chat-decide.js +37 -9
- package/dist/chat-quiet.js +3 -3
- package/dist/chat-voice.d.ts +1 -1
- package/dist/chat-voice.js +2 -2
- package/dist/check-facts.d.ts +126 -0
- package/dist/check-facts.js +318 -0
- package/dist/check-failure.d.ts +22 -3
- package/dist/check-failure.js +57 -13
- package/dist/check-levels-ui.d.ts +4 -0
- package/dist/check-levels-ui.js +10 -1
- package/dist/check-load.d.ts +85 -0
- package/dist/check-load.js +147 -0
- package/dist/check-only-standing.d.ts +36 -0
- package/dist/check-only-standing.js +39 -0
- package/dist/check-only.d.ts +15 -0
- package/dist/check-only.js +99 -0
- package/dist/check-report.d.ts +7 -0
- package/dist/check-report.js +4 -3
- package/dist/check-stamp.d.ts +46 -0
- package/dist/check-stamp.js +93 -0
- package/dist/checkout-cleanup.d.ts +36 -5
- package/dist/checkout-cleanup.js +166 -14
- package/dist/cli-help.d.ts +4 -1
- package/dist/cli-help.js +47 -30
- package/dist/cli.js +16 -1
- package/dist/code-extract.d.ts +44 -0
- package/dist/code-extract.js +88 -0
- package/dist/code-map-acts.d.ts +24 -0
- package/dist/code-map-acts.js +53 -0
- package/dist/code-map-admission.d.ts +52 -0
- package/dist/code-map-admission.js +295 -0
- package/dist/code-map-build.d.ts +63 -0
- package/dist/code-map-build.js +415 -0
- package/dist/code-map-cli.d.ts +153 -0
- package/dist/code-map-cli.js +103 -0
- package/dist/code-map-main.d.ts +1 -0
- package/dist/code-map-main.js +48 -0
- package/dist/code-map-read.d.ts +106 -0
- package/dist/code-map-read.js +171 -0
- package/dist/code-map-report.d.ts +37 -0
- package/dist/code-map-report.js +95 -0
- package/dist/code-map-shape.d.ts +172 -0
- package/dist/code-map-shape.js +327 -0
- package/dist/code-map-store.d.ts +136 -0
- package/dist/code-map-store.js +233 -0
- package/dist/coding-provider.d.ts +2 -0
- package/dist/coding-provider.js +3 -2
- package/dist/coding-update.js +3 -2
- package/dist/coding-workspace.js +4 -4
- package/dist/console-chrome.js +2 -2
- package/dist/containment.d.ts +5 -0
- package/dist/containment.js +14 -1
- package/dist/contracts/chat-actions.d.ts +9 -0
- package/dist/contracts/chat-actions.js +2 -0
- package/dist/contracts/cli.d.ts +52 -0
- package/dist/contracts/cli.js +4 -0
- package/dist/contracts/console-api.d.ts +12 -5
- package/dist/contracts/console-api.js +12 -5
- package/dist/contracts/gateway-tools.d.ts +17 -0
- package/dist/contracts/gateway-tools.js +3 -1
- package/dist/contracts/lead-tools.d.ts +61 -0
- package/dist/contracts/lead-tools.js +10 -1
- package/dist/contracts/project-knowledge.d.ts +38 -5
- package/dist/contracts/project-knowledge.js +11 -5
- package/dist/contracts/project-memory.d.ts +3 -1
- package/dist/contracts/project-memory.js +3 -2
- package/dist/contracts/release-check.d.ts +25 -1
- package/dist/contracts/release-check.js +19 -0
- package/dist/contracts/storage.d.ts +14 -0
- package/dist/contracts/storage.js +3 -1
- package/dist/contracts/store-json.d.ts +2 -0
- package/dist/contracts/youiyoux-request.d.ts +1 -1
- package/dist/contracts/youiyoux-request.js +1 -1
- package/dist/desktop-access.js +6 -2
- package/dist/desktop-update.d.ts +2 -2
- package/dist/desktop-update.js +13 -9
- package/dist/dispatch.js +18 -15
- package/dist/dispose.js +8 -2
- package/dist/easy-start.js +2 -3
- package/dist/eval-hooks.d.ts +55 -0
- package/dist/eval-hooks.js +227 -4
- package/dist/evals.js +2 -1
- package/dist/fast-start.d.ts +136 -0
- package/dist/fast-start.js +431 -0
- package/dist/fast-terms.d.ts +9 -0
- package/dist/fast-terms.js +23 -0
- package/dist/fenced-check-coverage.d.ts +62 -0
- package/dist/fenced-check-coverage.js +54 -0
- package/dist/first-run.js +1 -1
- package/dist/flaky-ci.d.ts +31 -0
- package/dist/flaky-ci.js +149 -0
- package/dist/flaky-cli.d.ts +60 -0
- package/dist/flaky-cli.js +60 -0
- package/dist/flaky.d.ts +83 -0
- package/dist/flaky.js +264 -0
- package/dist/flow-card-task.js +3 -0
- package/dist/flow-triggers.d.ts +1 -1
- package/dist/flow-triggers.js +1 -1
- package/dist/github-import.d.ts +62 -0
- package/dist/github-import.js +146 -0
- package/dist/guides.js +15 -10
- package/dist/invoke.d.ts +5 -1
- package/dist/invoke.js +49 -14
- package/dist/job-object-helper.ps1 +10 -3
- package/dist/keymap.d.ts +41 -0
- package/dist/keymap.js +51 -0
- package/dist/knowledge-cli.js +1 -1
- package/dist/knowledge-ui.d.ts +21 -4
- package/dist/knowledge-ui.js +75 -7
- package/dist/laptop-background.d.ts +16 -0
- package/dist/laptop-background.js +108 -2
- package/dist/lead-context.js +1 -1
- package/dist/lead-contract.d.ts +1 -1
- package/dist/lead-contract.js +2 -2
- package/dist/lead-progress.js +3 -0
- package/dist/lead-status.d.ts +2 -0
- package/dist/lead-status.js +6 -2
- package/dist/lead-tools.js +51 -2
- package/dist/lead.js +1 -1
- package/dist/lesson-notes.d.ts +81 -0
- package/dist/lesson-notes.js +336 -0
- package/dist/limits-ui.js +1 -1
- package/dist/local-owner.d.ts +1 -1
- package/dist/local-owner.js +11 -0
- package/dist/machine-check.d.ts +16 -0
- package/dist/machine-check.js +26 -0
- package/dist/machine-identity.d.ts +75 -0
- package/dist/machine-identity.js +170 -0
- package/dist/mcp-core.js +11 -1
- package/dist/memory-cli.d.ts +300 -1
- package/dist/memory-cli.js +62 -9
- package/dist/memory-pass.d.ts +16 -0
- package/dist/memory-pass.js +67 -3
- package/dist/memory-tidy.d.ts +181 -0
- package/dist/memory-tidy.js +523 -0
- package/dist/memory-use.d.ts +35 -0
- package/dist/memory-use.js +109 -0
- package/dist/merge-checklist.d.ts +55 -0
- package/dist/merge-checklist.js +182 -0
- package/dist/needs-you.d.ts +32 -13
- package/dist/needs-you.js +108 -49
- package/dist/nightly-github.d.ts +2 -0
- package/dist/nightly-github.js +4 -4
- package/dist/operate-remote-arguments.js +6 -1
- package/dist/operate-remote.js +3 -1
- package/dist/operate.d.ts +6 -1
- package/dist/operate.js +540 -70
- package/dist/phase-routing.js +2 -2
- package/dist/plain-schema.d.ts +23 -0
- package/dist/plain-schema.js +94 -0
- package/dist/plain-words.d.ts +20 -0
- package/dist/plain-words.js +66 -0
- package/dist/planner.js +2 -1
- package/dist/process-liveness.d.ts +2 -0
- package/dist/process-liveness.js +61 -17
- package/dist/project-delete-ui.d.ts +7 -2
- package/dist/project-delete-ui.js +12 -1
- package/dist/project-delete.js +13 -2
- package/dist/project-knowledge.d.ts +18 -11
- package/dist/project-knowledge.js +39 -9
- package/dist/project-learning.d.ts +2 -1
- package/dist/project-learning.js +15 -45
- package/dist/project-memory.d.ts +5 -0
- package/dist/project-memory.js +22 -2
- package/dist/proof.js +30 -5
- package/dist/provider-auth.js +1 -1
- package/dist/publish.js +9 -1
- package/dist/pull-request-flow.js +5 -0
- package/dist/push.d.ts +26 -3
- package/dist/push.js +55 -15
- package/dist/release-adapters.d.ts +9 -0
- package/dist/release-adapters.js +87 -1
- package/dist/release-approval.d.ts +191 -0
- package/dist/release-approval.js +382 -0
- package/dist/release-auto.js +26 -17
- package/dist/release-card.d.ts +74 -6
- package/dist/release-card.js +179 -23
- package/dist/release-carry.d.ts +7 -2
- package/dist/release-carry.js +39 -9
- package/dist/release-cli.d.ts +4 -0
- package/dist/release-cli.js +38 -23
- package/dist/release-coverage.js +1 -1
- package/dist/release-next.d.ts +48 -0
- package/dist/release-next.js +161 -0
- package/dist/release.d.ts +50 -1
- package/dist/release.js +118 -11
- package/dist/remote-destination.d.ts +36 -0
- package/dist/remote-destination.js +217 -0
- package/dist/remote-main.d.ts +48 -0
- package/dist/remote-main.js +84 -5
- package/dist/repository-context.d.ts +3 -2
- package/dist/repository-context.js +112 -70
- package/dist/restart-certification.d.ts +1 -0
- package/dist/restart-certification.js +8 -6
- package/dist/result-acts.d.ts +29 -21
- package/dist/result-acts.js +36 -25
- package/dist/result-follow-ups.d.ts +2 -2
- package/dist/result-follow-ups.js +18 -4
- package/dist/result-review.d.ts +1 -1
- package/dist/route-change-check.d.ts +4 -0
- package/dist/route-change-check.js +13 -0
- package/dist/route-change.d.ts +22 -0
- package/dist/route-change.js +89 -0
- package/dist/route-hooks.js +3 -2
- package/dist/route-results.d.ts +6 -1
- package/dist/route-results.js +14 -5
- package/dist/run-closeout.d.ts +1 -1
- package/dist/run-closeout.js +7 -5
- package/dist/runner-process.js +3 -2
- package/dist/runner.js +2 -1
- package/dist/scope.d.ts +6 -1
- package/dist/scope.js +22 -9
- package/dist/serve.d.ts +1 -1
- package/dist/serve.js +24 -18
- package/dist/server/chat.js +14 -12
- package/dist/server/chrome.d.ts +1 -1
- package/dist/server/chrome.js +26 -35
- package/dist/server/console.css +23 -0
- package/dist/server/guards.js +4 -0
- package/dist/server/home.d.ts +6 -0
- package/dist/server/home.js +21 -13
- package/dist/server/http.js +1 -1
- package/dist/server/kit.js +1 -1
- package/dist/server/merge-ready-view.d.ts +10 -0
- package/dist/server/merge-ready-view.js +30 -0
- package/dist/server/pages-assets.d.ts +1 -0
- package/dist/server/pages-assets.js +8 -5
- package/dist/server/pages.js +103 -82
- package/dist/server/render-chat.js +2 -2
- package/dist/server/render-code-map.d.ts +30 -0
- package/dist/server/render-code-map.js +137 -0
- package/dist/server/render-flaky.d.ts +20 -0
- package/dist/server/render-flaky.js +59 -0
- package/dist/server/render-history.js +1 -1
- package/dist/server/render-new-task.d.ts +26 -1
- package/dist/server/render-new-task.js +30 -5
- package/dist/server/render-pages.d.ts +1 -175
- package/dist/server/render-pages.js +38 -313
- package/dist/server/render-projects.d.ts +2 -1
- package/dist/server/render-projects.js +9 -7
- package/dist/server/render-release.d.ts +19 -0
- package/dist/server/render-release.js +84 -0
- package/dist/server/render-results.d.ts +2 -2
- package/dist/server/render-results.js +71 -73
- package/dist/server/render-tasks.d.ts +13 -2
- package/dist/server/render-tasks.js +156 -139
- package/dist/server/retired-routes.d.ts +17 -0
- package/dist/server/retired-routes.js +53 -0
- package/dist/server/route-table.d.ts +4 -1
- package/dist/server/route-table.js +52 -13
- package/dist/server/settings.js +129 -22
- package/dist/server/task-views.js +61 -26
- package/dist/server/tasks.d.ts +17 -6
- package/dist/server/tasks.js +273 -338
- package/dist/server/welcome.js +1 -1
- package/dist/settings-groups-ui.js +1 -1
- package/dist/setup-guide.d.ts +2 -2
- package/dist/stop-proof.d.ts +67 -0
- package/dist/stop-proof.js +295 -0
- package/dist/storage-sweep-process.js +1 -1
- package/dist/storage-sweep.d.ts +5 -3
- package/dist/storage-sweep.js +30 -9
- package/dist/storage-ui.js +28 -17
- package/dist/storage.d.ts +27 -2
- package/dist/storage.js +39 -2
- package/dist/store.d.ts +148 -34
- package/dist/store.js +626 -177
- package/dist/surface.js +46 -5
- package/dist/sweep-quarantine.d.ts +1 -0
- package/dist/sweep-quarantine.js +6 -4
- package/dist/tamper-scan.d.ts +4 -0
- package/dist/tamper-scan.js +90 -0
- package/dist/task-aliases.d.ts +5 -0
- package/dist/task-aliases.js +9 -0
- package/dist/task-control.js +4 -1
- package/dist/task-outcome-cli.d.ts +2 -0
- package/dist/task-outcome-cli.js +36 -17
- package/dist/task-sizing.d.ts +5 -0
- package/dist/task-sizing.js +2 -0
- package/dist/task-source.d.ts +7 -0
- package/dist/task-source.js +14 -0
- package/dist/task-status.d.ts +9 -0
- package/dist/task-status.js +40 -49
- package/dist/teardown-git.d.ts +16 -0
- package/dist/teardown-git.js +63 -5
- package/dist/telegram-progress.js +5 -3
- package/dist/telegram-status.js +6 -4
- package/dist/test-infra.d.ts +8 -0
- package/dist/test-infra.js +18 -0
- package/dist/tidy.js +1 -1
- package/dist/toolroll-update-ui.js +2 -2
- package/dist/toolroll-update.js +18 -5
- package/dist/tui-contract.d.ts +24 -0
- package/dist/tui-source.js +67 -8
- package/dist/tui.js +51 -51
- package/dist/vanished-retry.d.ts +30 -0
- package/dist/vanished-retry.js +174 -0
- package/dist/work-index.d.ts +2 -0
- package/dist/work-index.js +82 -70
- package/dist/work-summary.d.ts +2 -2
- package/dist/work-summary.js +2 -0
- package/dist/workspace-ui.d.ts +4 -2
- package/dist/workspace-ui.js +28 -22
- package/dist/worktree.d.ts +12 -3
- package/dist/worktree.js +68 -13
- package/package.json +1 -1
- package/dist/server/render-next.d.ts +0 -63
- package/dist/server/render-next.js +0 -109
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a read of a recorded remote really connects, worked out and pinned before git runs (docs/plans/code-map.md §1).
|
|
3
|
+
* remote-main.ts's URL rules look at a host's spelling; a name can still resolve to this machine. Here the name is
|
|
4
|
+
* resolved once, every address is checked, and git is told to use exactly the checked address. Routing is decided from
|
|
5
|
+
* the same environment git gets. Anything that can't be decided or pinned refuses with a reason: there is no fallback to
|
|
6
|
+
* an unpinned read.
|
|
7
|
+
*/
|
|
8
|
+
import { lookup as dnsLookup } from "node:dns/promises";
|
|
9
|
+
import { isIP } from "node:net";
|
|
10
|
+
export const lookupAll = async (host) => (await dnsLookup(host, { all: true, verbatim: true })).map(one => one.address);
|
|
11
|
+
const UNDECIDED = "Toolroll can't tell how this connection would be routed";
|
|
12
|
+
const NOT_ALLOWED = "its address isn't one Toolroll connects to";
|
|
13
|
+
/** The eight 16-bit groups of an IPv6 address; null when it isn't one. */
|
|
14
|
+
function hextets(address) {
|
|
15
|
+
let text = address.toLowerCase().replace(/%.*$/, "");
|
|
16
|
+
const dotted = /(\d+\.\d+\.\d+\.\d+)$/.exec(text);
|
|
17
|
+
if (dotted !== null) {
|
|
18
|
+
const parts = dotted[1].split(".").map(Number);
|
|
19
|
+
if (parts.some(part => part > 255))
|
|
20
|
+
return null;
|
|
21
|
+
text = `${text.slice(0, dotted.index)}${((parts[0] << 8) | parts[1]).toString(16)}:${((parts[2] << 8) | parts[3]).toString(16)}`;
|
|
22
|
+
}
|
|
23
|
+
const halves = text.split("::");
|
|
24
|
+
if (halves.length > 2)
|
|
25
|
+
return null;
|
|
26
|
+
const left = halves[0] === "" ? [] : halves[0].split(":");
|
|
27
|
+
const right = halves.length === 1 || halves[1] === "" ? [] : halves[1].split(":");
|
|
28
|
+
const gap = 8 - left.length - right.length;
|
|
29
|
+
if (halves.length === 1 ? gap !== 0 : gap < 1)
|
|
30
|
+
return null;
|
|
31
|
+
const groups = [...left, ...Array(halves.length === 1 ? 0 : gap).fill("0"), ...right].map(one => parseInt(one, 16));
|
|
32
|
+
return groups.length === 8 && groups.every(one => Number.isInteger(one) && one >= 0 && one <= 0xffff) ? groups : null;
|
|
33
|
+
}
|
|
34
|
+
const refusedV4 = (address) => {
|
|
35
|
+
const [a, b] = address.split(".").map(Number);
|
|
36
|
+
return a === 127 || a === 0 || (a === 169 && b === 254);
|
|
37
|
+
};
|
|
38
|
+
const v4Of = (high, low) => `${high >> 8}.${high & 255}.${low >> 8}.${low & 255}`;
|
|
39
|
+
/** Whether an address is loopback, unspecified, link-local, or an IPv4-mapped or NAT64 form of these. */
|
|
40
|
+
export function addressRefused(address) {
|
|
41
|
+
const family = isIP(address);
|
|
42
|
+
if (family === 4)
|
|
43
|
+
return refusedV4(address);
|
|
44
|
+
if (family !== 6)
|
|
45
|
+
return true;
|
|
46
|
+
const g = hextets(address);
|
|
47
|
+
if (g === null)
|
|
48
|
+
return true;
|
|
49
|
+
const top = g.slice(0, 5).every(one => one === 0);
|
|
50
|
+
if (top && g[5] === 0 && g[6] === 0 && (g[7] === 0 || g[7] === 1))
|
|
51
|
+
return true; // :: and ::1
|
|
52
|
+
if ((g[0] & 0xffc0) === 0xfe80)
|
|
53
|
+
return true; // fe80::/10
|
|
54
|
+
if (top && g[5] === 0xffff)
|
|
55
|
+
return refusedV4(v4Of(g[6], g[7])); // ::ffff:a.b.c.d
|
|
56
|
+
if (top && g[5] === 0)
|
|
57
|
+
return refusedV4(v4Of(g[6], g[7])); // ::a.b.c.d (deprecated compatible form)
|
|
58
|
+
if (g[0] === 0x64 && g[1] === 0xff9b && g.slice(2, 6).every(one => one === 0))
|
|
59
|
+
return refusedV4(v4Of(g[6], g[7])); // 64:ff9b::/96
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
/** The host, effective port and scheme a recorded URL connects to; null when it can't be read. The caller has already
|
|
63
|
+
* passed remoteUrlProblem. Node's URL parser normalizes every spelling of a literal host. */
|
|
64
|
+
export function destinationOf(url) {
|
|
65
|
+
const scheme = /^([A-Za-z][A-Za-z0-9+.-]*):\/\//.exec(url)?.[1]?.toLowerCase();
|
|
66
|
+
let host, port;
|
|
67
|
+
if (scheme === "https" || scheme === "ssh") {
|
|
68
|
+
let parsed;
|
|
69
|
+
try {
|
|
70
|
+
parsed = new URL(url);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
host = parsed.hostname.replace(/^\[(.*)\]$/, "$1");
|
|
76
|
+
port = parsed.port === "" ? (scheme === "https" ? 443 : 22) : Number(parsed.port);
|
|
77
|
+
return validDestination(scheme, host, port);
|
|
78
|
+
}
|
|
79
|
+
if (scheme !== undefined)
|
|
80
|
+
return null;
|
|
81
|
+
const colon = url.indexOf(":");
|
|
82
|
+
if (colon <= 0)
|
|
83
|
+
return null;
|
|
84
|
+
host = url.slice(0, colon).replace(/^.*@/, "");
|
|
85
|
+
return validDestination("ssh", host, 22);
|
|
86
|
+
}
|
|
87
|
+
function validDestination(scheme, host, port) {
|
|
88
|
+
const literal = isIP(host) !== 0;
|
|
89
|
+
if (!literal && !/^[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?\.?$/.test(host))
|
|
90
|
+
return null;
|
|
91
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
92
|
+
return null;
|
|
93
|
+
return { scheme, host, port, literal };
|
|
94
|
+
}
|
|
95
|
+
async function checkedAddress(host, literal, lookup) {
|
|
96
|
+
if (literal)
|
|
97
|
+
return addressRefused(host) ? { ok: false, why: NOT_ALLOWED } : { ok: true, address: host };
|
|
98
|
+
let found;
|
|
99
|
+
try {
|
|
100
|
+
found = await lookup(host);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return { ok: false, why: "its host name couldn't be resolved" };
|
|
104
|
+
}
|
|
105
|
+
if (found.length === 0)
|
|
106
|
+
return { ok: false, why: "its host name couldn't be resolved" };
|
|
107
|
+
// Every address, not just the first: a name that also answers with this machine isn't connected to.
|
|
108
|
+
if (found.some(addressRefused))
|
|
109
|
+
return { ok: false, why: NOT_ALLOWED };
|
|
110
|
+
return { ok: true, address: found[0] };
|
|
111
|
+
}
|
|
112
|
+
const curlPin = (host, port, address) => ["-c", `http.curloptResolve=${host}:${port}:${isIP(address) === 6 ? `[${address}]` : address}`];
|
|
113
|
+
/** One NO_PROXY entry against a host, as curl compares them; "unknown" for anything Toolroll won't second-guess. */
|
|
114
|
+
function noProxyMatch(entry, host) {
|
|
115
|
+
const raw = entry.replace(/^\[(.*)\]$/, "$1");
|
|
116
|
+
if (raw === "*")
|
|
117
|
+
return true;
|
|
118
|
+
const hostIp = isIP(host);
|
|
119
|
+
const cidr = /^(.+)\/(\d+)$/.exec(raw);
|
|
120
|
+
if (cidr !== null) {
|
|
121
|
+
if (isIP(cidr[1]) === 0)
|
|
122
|
+
return "unknown";
|
|
123
|
+
if (hostIp === 0)
|
|
124
|
+
return false;
|
|
125
|
+
if (hostIp !== isIP(cidr[1]))
|
|
126
|
+
return false;
|
|
127
|
+
const bits = Number(cidr[2]);
|
|
128
|
+
if (hostIp === 4) {
|
|
129
|
+
if (bits > 32)
|
|
130
|
+
return "unknown";
|
|
131
|
+
const value = (ip) => ip.split(".").reduce((n, part) => n * 256 + Number(part), 0);
|
|
132
|
+
const size = 2 ** (32 - bits);
|
|
133
|
+
return Math.floor(value(host) / size) === Math.floor(value(cidr[1]) / size);
|
|
134
|
+
}
|
|
135
|
+
return "unknown";
|
|
136
|
+
}
|
|
137
|
+
if (isIP(raw) !== 0)
|
|
138
|
+
return hostIp !== 0 && (hextets(raw)?.join() ?? raw) === (hextets(host)?.join() ?? host);
|
|
139
|
+
if (/[^A-Za-z0-9.-]/.test(raw))
|
|
140
|
+
return "unknown";
|
|
141
|
+
if (hostIp !== 0)
|
|
142
|
+
return false;
|
|
143
|
+
const name = raw.replace(/^\.+/, "").replace(/\.+$/, "").toLowerCase();
|
|
144
|
+
const wanted = host.replace(/\.+$/, "").toLowerCase();
|
|
145
|
+
return name !== "" && (wanted === name || wanted.endsWith(`.${name}`));
|
|
146
|
+
}
|
|
147
|
+
/** Whether curl would skip the proxy for this host; "unknown" when an entry can't be decided. */
|
|
148
|
+
export function noProxyApplies(noProxy, host) {
|
|
149
|
+
let unknown = false;
|
|
150
|
+
for (const entry of noProxy.split(/[,\s]+/).filter(Boolean)) {
|
|
151
|
+
const hit = noProxyMatch(entry, host);
|
|
152
|
+
if (hit === true)
|
|
153
|
+
return true;
|
|
154
|
+
if (hit === "unknown")
|
|
155
|
+
unknown = true;
|
|
156
|
+
}
|
|
157
|
+
return unknown ? "unknown" : false;
|
|
158
|
+
}
|
|
159
|
+
const envOf = (env, lower, upper) => {
|
|
160
|
+
const low = env[lower];
|
|
161
|
+
return low !== undefined && low !== "" ? low : env[upper] ?? "";
|
|
162
|
+
};
|
|
163
|
+
/** The proxy curl would use from `https_proxy`/`HTTPS_PROXY`: http or https schemes only (a SOCKS proxy resolves names
|
|
164
|
+
* on its side, in ways this can't see). */
|
|
165
|
+
function proxyOf(value) {
|
|
166
|
+
const text = /^[A-Za-z][A-Za-z0-9+.-]*:\/\//.test(value) ? value : `http://${value}`;
|
|
167
|
+
let parsed;
|
|
168
|
+
try {
|
|
169
|
+
parsed = new URL(text);
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
|
|
175
|
+
return null;
|
|
176
|
+
const host = parsed.hostname.replace(/^\[(.*)\]$/, "$1");
|
|
177
|
+
const port = parsed.port === "" ? (parsed.protocol === "https:" ? 443 : 1080) : Number(parsed.port);
|
|
178
|
+
const literal = isIP(host) !== 0;
|
|
179
|
+
if (host === "" || (!literal && !/^[A-Za-z0-9.-]+$/.test(host)))
|
|
180
|
+
return null;
|
|
181
|
+
return { host, port, literal };
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Decide the route and pin it. `env` is what git will see (the names remote-main.ts's ENV_ALLOWLIST passes). ssh never
|
|
185
|
+
* uses a proxy. Through a proxy, the proxy's own address is checked and pinned; the operator's proxy then chooses the
|
|
186
|
+
* final destination, and the caller says so.
|
|
187
|
+
*/
|
|
188
|
+
export async function pinDestination(url, env, lookup = lookupAll) {
|
|
189
|
+
const target = destinationOf(url);
|
|
190
|
+
if (target === null)
|
|
191
|
+
return { ok: false, why: UNDECIDED };
|
|
192
|
+
if (target.scheme === "ssh") {
|
|
193
|
+
const checked = await checkedAddress(target.host, target.literal, lookup);
|
|
194
|
+
if (!checked.ok)
|
|
195
|
+
return checked;
|
|
196
|
+
return { ok: true, pin: { gitArgs: [], proxy: null, env: { GIT_SSH_COMMAND: `ssh -o BatchMode=yes -o ProxyCommand=none -o ProxyJump=none -o HostName=${checked.address} -o Port=${target.port} -o HostKeyAlias=${target.host} -o CheckHostIP=no` } } };
|
|
197
|
+
}
|
|
198
|
+
const proxyValue = envOf(env, "https_proxy", "HTTPS_PROXY");
|
|
199
|
+
if (proxyValue !== "") {
|
|
200
|
+
const skip = noProxyApplies(envOf(env, "no_proxy", "NO_PROXY"), target.host);
|
|
201
|
+
if (skip === "unknown")
|
|
202
|
+
return { ok: false, why: UNDECIDED };
|
|
203
|
+
if (!skip) {
|
|
204
|
+
const proxy = proxyOf(proxyValue);
|
|
205
|
+
if (proxy === null)
|
|
206
|
+
return { ok: false, why: UNDECIDED };
|
|
207
|
+
const checked = await checkedAddress(proxy.host, proxy.literal, lookup);
|
|
208
|
+
if (!checked.ok)
|
|
209
|
+
return { ok: false, why: `your proxy's ${checked.why.replace(/^its /, "")}` };
|
|
210
|
+
return { ok: true, pin: { gitArgs: curlPin(proxy.host, proxy.port, checked.address), env: {}, proxy: proxy.host } };
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
const checked = await checkedAddress(target.host, target.literal, lookup);
|
|
214
|
+
if (!checked.ok)
|
|
215
|
+
return checked;
|
|
216
|
+
return { ok: true, pin: { gitArgs: curlPin(target.host, target.port, checked.address), env: {}, proxy: null } };
|
|
217
|
+
}
|
package/dist/remote-main.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { type ExecResult } from "./exec.js";
|
|
2
|
+
import type { BoundedOptions, BoundedResult, BoundedRunner } from "./bounded-spawn.js";
|
|
3
|
+
import type { Pin } from "./remote-destination.js";
|
|
2
4
|
import type { Store } from "./store.js";
|
|
3
5
|
import type { Runner } from "./worktree.js";
|
|
4
6
|
/** What the private directory's git may see of this process's environment: nothing that names a repository, an
|
|
@@ -40,7 +42,13 @@ export type RemoteMainRead = {
|
|
|
40
42
|
} | {
|
|
41
43
|
ok: false;
|
|
42
44
|
why: string;
|
|
45
|
+
reason?: "moved";
|
|
43
46
|
};
|
|
47
|
+
/** What every reader of a remote main shows when its address redirects. The target is remote-supplied text: it is never
|
|
48
|
+
* shown, stored or followed, and the new remote is never adopted automatically. */
|
|
49
|
+
export declare const REMOTE_MOVED = "The repository moved: its address now redirects. Update the project's remote in Toolroll (Settings \u2192 Projects), then try again.";
|
|
50
|
+
/** git's words for a refused redirect (http.followRedirects=false), read for the status code alone. */
|
|
51
|
+
export declare function redirected(stderr: string): boolean;
|
|
44
52
|
/**
|
|
45
53
|
* One pass's reads of recorded remotes. Every `read` fetches again (the pre-teardown re-check is never older than
|
|
46
54
|
* the teardown); a project's private directory lasts until `close`, so a fetch after the first only brings what
|
|
@@ -61,3 +69,43 @@ export declare class RemoteMains {
|
|
|
61
69
|
read(repo: string): Promise<RemoteMainRead>;
|
|
62
70
|
close(): void;
|
|
63
71
|
}
|
|
72
|
+
export declare const MAP_FETCH_LIMITS: {
|
|
73
|
+
readonly timeoutMs: 120000;
|
|
74
|
+
readonly memoryBytes: number;
|
|
75
|
+
readonly diskBytes: number;
|
|
76
|
+
readonly fileBytes: number;
|
|
77
|
+
readonly maxStderr: number;
|
|
78
|
+
};
|
|
79
|
+
export type MapGitOptions = {
|
|
80
|
+
helper?: string;
|
|
81
|
+
extra?: readonly string[];
|
|
82
|
+
};
|
|
83
|
+
/** Git in a private directory under the map's rules: the same hardening as `RemoteMains`, plus the pin. */
|
|
84
|
+
export declare function mapGitArgs(dir: string, pin: Pin, args: readonly string[], options?: MapGitOptions): string[];
|
|
85
|
+
/** The environment for it: exactly the allowed names, the fixed settings, and the pin's. */
|
|
86
|
+
export declare function mapGitEnv(pin: Pin, from?: Readonly<Record<string, string | undefined>>): Record<string, string>;
|
|
87
|
+
export declare function initPrivateDir(dir: string, bounded: BoundedRunner): Promise<BoundedResult>;
|
|
88
|
+
export type MapPeek = {
|
|
89
|
+
ok: true;
|
|
90
|
+
commit: string | null;
|
|
91
|
+
} | {
|
|
92
|
+
ok: false;
|
|
93
|
+
why: string;
|
|
94
|
+
reason?: "moved";
|
|
95
|
+
};
|
|
96
|
+
/** The commit the recorded remote's main is at, without fetching: null when the remote has no `main`. */
|
|
97
|
+
export declare function peekMain(url: string, dir: string, pin: Pin, bounded: BoundedRunner, helper?: string): Promise<MapPeek>;
|
|
98
|
+
/** A depth-1 private folder is clean when it holds only what the fetch made: a `shallow` file naming exactly the fetched
|
|
99
|
+
* commit, and no grafts or alternates. */
|
|
100
|
+
export declare function shallowClean(dir: string, commit: string): boolean;
|
|
101
|
+
export type MapFetch = {
|
|
102
|
+
ok: true;
|
|
103
|
+
commit: string;
|
|
104
|
+
enforced: "system" | "checked";
|
|
105
|
+
} | {
|
|
106
|
+
ok: false;
|
|
107
|
+
why: string;
|
|
108
|
+
reason?: "moved";
|
|
109
|
+
};
|
|
110
|
+
/** Fetch main at depth 1 into `dir`, under the map's limits. `onStart` records the spawn for crash cleanup. */
|
|
111
|
+
export declare function readShallowMain(url: string, dir: string, pin: Pin, bounded: BoundedRunner, onStart?: BoundedOptions["onStart"], helper?: string): Promise<MapFetch>;
|
package/dist/remote-main.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* Every doubt fails closed: no recorded remote, a URL that isn't allowed, a failed fetch or a remote without main all
|
|
14
14
|
* read as "not proven", and the task stays for a person.
|
|
15
15
|
*/
|
|
16
|
-
import { mkdirSync, mkdtempSync, rmSync, existsSync } from "node:fs";
|
|
16
|
+
import { lstatSync, mkdirSync, mkdtempSync, readFileSync, rmSync, existsSync } from "node:fs";
|
|
17
17
|
import { isAbsolute, join } from "node:path";
|
|
18
18
|
import { run } from "./exec.js";
|
|
19
19
|
/** The ref a fetched remote main is kept under, inside the private directory only. */
|
|
@@ -35,7 +35,9 @@ export const CREDENTIAL_HELPER = process.platform === "darwin" ? "osxkeychain" :
|
|
|
35
35
|
function hardened(helper, file = "never") {
|
|
36
36
|
return ["-c", "core.hooksPath=/dev/null", "-c", "protocol.allow=never", "-c", "protocol.https.allow=always", "-c", "protocol.ssh.allow=always",
|
|
37
37
|
"-c", `protocol.file.allow=${file}`, "-c", "protocol.ext.allow=never", "-c", "credential.helper=", ...(helper === "" ? [] : ["-c", `credential.helper=${helper}`]),
|
|
38
|
-
"-c", "gc.auto=0", "-c", "maintenance.auto=false", "-c", "fetch.recurseSubmodules=false", "-c", "core.fsmonitor=false"
|
|
38
|
+
"-c", "gc.auto=0", "-c", "maintenance.auto=false", "-c", "fetch.recurseSubmodules=false", "-c", "core.fsmonitor=false",
|
|
39
|
+
// Never to an address the recorded URL doesn't name: a moved repository is a person's choice to record (MOVED).
|
|
40
|
+
"-c", "http.followRedirects=false"];
|
|
39
41
|
}
|
|
40
42
|
/**
|
|
41
43
|
* A URL Toolroll may fetch remote main from: https or ssh (either form). Local paths, file URLs and loopback hosts
|
|
@@ -96,6 +98,14 @@ export function remoteUrlProblem(url) {
|
|
|
96
98
|
const parsed = parseRemoteUrl(url);
|
|
97
99
|
return "why" in parsed ? parsed.why : null;
|
|
98
100
|
}
|
|
101
|
+
/** What every reader of a remote main shows when its address redirects. The target is remote-supplied text: it is never
|
|
102
|
+
* shown, stored or followed, and the new remote is never adopted automatically. */
|
|
103
|
+
export const REMOTE_MOVED = "The repository moved: its address now redirects. Update the project's remote in Toolroll (Settings → Projects), then try again.";
|
|
104
|
+
/** git's words for a refused redirect (http.followRedirects=false), read for the status code alone. */
|
|
105
|
+
export function redirected(stderr) {
|
|
106
|
+
return /returned error: 30[12378]\b/.test(stderr);
|
|
107
|
+
}
|
|
108
|
+
const UNREAD = "Toolroll couldn't read the project's remote main";
|
|
99
109
|
/**
|
|
100
110
|
* One pass's reads of recorded remotes. Every `read` fetches again (the pre-teardown re-check is never older than
|
|
101
111
|
* the teardown); a project's private directory lasts until `close`, so a fetch after the first only brings what
|
|
@@ -145,14 +155,16 @@ export class RemoteMains {
|
|
|
145
155
|
return { ok: false, why: problem };
|
|
146
156
|
const dir = await this.dirFor(repo);
|
|
147
157
|
if (dir === null)
|
|
148
|
-
return { ok: false, why:
|
|
158
|
+
return { ok: false, why: UNREAD };
|
|
149
159
|
const fetched = await this.exec(dir, ["fetch", "--quiet", "--no-tags", "--no-write-fetch-head", "--", url, `+refs/heads/main:${REMOTE_MAIN}`]);
|
|
160
|
+
if (fetched.code !== 0 && redirected(fetched.stderr))
|
|
161
|
+
return { ok: false, why: REMOTE_MOVED, reason: "moved" };
|
|
150
162
|
if (fetched.code !== 0 || !this.clean(dir))
|
|
151
|
-
return { ok: false, why:
|
|
163
|
+
return { ok: false, why: UNREAD };
|
|
152
164
|
const resolved = await this.exec(dir, ["rev-parse", "--verify", "--quiet", `${REMOTE_MAIN}^{commit}`]);
|
|
153
165
|
const main = resolved.stdout.trim();
|
|
154
166
|
if (resolved.code !== 0 || !SHA.test(main))
|
|
155
|
-
return { ok: false, why:
|
|
167
|
+
return { ok: false, why: UNREAD };
|
|
156
168
|
const git = args => this.exec(dir, args);
|
|
157
169
|
const bring = async (from, head) => {
|
|
158
170
|
if (!SHA.test(head))
|
|
@@ -177,3 +189,70 @@ export class RemoteMains {
|
|
|
177
189
|
this.dirs.clear();
|
|
178
190
|
}
|
|
179
191
|
}
|
|
192
|
+
// ---- the code map's own read: pinned, depth 1, bounded (docs/plans/code-map.md §1) ---------------------------------
|
|
193
|
+
const MAP_MAIN = "refs/toolroll/map-main";
|
|
194
|
+
/** What keeps git's own memory down. index-pack still materializes delta bases, so these reduce; they don't bound. */
|
|
195
|
+
const LOW_MEMORY = ["-c", "pack.threads=1", "-c", "core.deltaBaseCacheLimit=32m", "-c", "core.bigFileThreshold=1m"];
|
|
196
|
+
export const MAP_FETCH_LIMITS = { timeoutMs: 120_000, memoryBytes: 1024 ** 3, diskBytes: 1024 ** 3, fileBytes: 512 * 1024 ** 2, maxStderr: 64 * 1024 };
|
|
197
|
+
/** Git in a private directory under the map's rules: the same hardening as `RemoteMains`, plus the pin. */
|
|
198
|
+
export function mapGitArgs(dir, pin, args, options = {}) {
|
|
199
|
+
return ["--git-dir", dir, ...hardened(options.helper ?? CREDENTIAL_HELPER), ...pin.gitArgs, ...(options.extra ?? []), ...args];
|
|
200
|
+
}
|
|
201
|
+
/** The environment for it: exactly the allowed names, the fixed settings, and the pin's. */
|
|
202
|
+
export function mapGitEnv(pin, from = process.env) {
|
|
203
|
+
const env = {};
|
|
204
|
+
for (const name of ENV_ALLOWLIST) {
|
|
205
|
+
const value = from[name];
|
|
206
|
+
if (value !== undefined)
|
|
207
|
+
env[name] = value;
|
|
208
|
+
}
|
|
209
|
+
return { ...env, ...ENV, ...pin.env };
|
|
210
|
+
}
|
|
211
|
+
export function initPrivateDir(dir, bounded) {
|
|
212
|
+
return bounded("git", ["init", "--quiet", "--bare", "--template=", dir], { cwd: dir, env: mapGitEnv({ gitArgs: [], env: {}, proxy: null }), timeoutMs: 30_000, maxStdout: 4096, maxStderr: 4096, label: "code-map-init" });
|
|
213
|
+
}
|
|
214
|
+
/** The commit the recorded remote's main is at, without fetching: null when the remote has no `main`. */
|
|
215
|
+
export async function peekMain(url, dir, pin, bounded, helper) {
|
|
216
|
+
const ran = await bounded("git", mapGitArgs(dir, pin, ["ls-remote", "--", url, "refs/heads/main"], helper === undefined ? {} : { helper }), { cwd: dir, env: mapGitEnv(pin), timeoutMs: 30_000, maxStdout: 64 * 1024, maxStderr: MAP_FETCH_LIMITS.maxStderr, label: "code-map-peek" });
|
|
217
|
+
if (ran.code !== 0 && redirected(ran.stderr))
|
|
218
|
+
return { ok: false, why: REMOTE_MOVED, reason: "moved" };
|
|
219
|
+
if (ran.code !== 0 || ran.killed !== null)
|
|
220
|
+
return { ok: false, why: UNREAD };
|
|
221
|
+
const line = ran.stdout.toString("utf8").split("\n").find(one => one.endsWith("\trefs/heads/main"));
|
|
222
|
+
if (line === undefined)
|
|
223
|
+
return ran.stdout.length === 0 ? { ok: true, commit: null } : { ok: false, why: UNREAD };
|
|
224
|
+
const commit = line.slice(0, line.indexOf("\t"));
|
|
225
|
+
return SHA.test(commit) ? { ok: true, commit } : { ok: false, why: UNREAD };
|
|
226
|
+
}
|
|
227
|
+
/** A depth-1 private folder is clean when it holds only what the fetch made: a `shallow` file naming exactly the fetched
|
|
228
|
+
* commit, and no grafts or alternates. */
|
|
229
|
+
export function shallowClean(dir, commit) {
|
|
230
|
+
if ([join("info", "grafts"), join("objects", "info", "alternates"), join("objects", "info", "http-alternates")].some(name => existsSync(join(dir, name))))
|
|
231
|
+
return false;
|
|
232
|
+
try {
|
|
233
|
+
const held = lstatSync(join(dir, "shallow"));
|
|
234
|
+
if (!held.isFile() || held.size > 128)
|
|
235
|
+
return false;
|
|
236
|
+
return readFileSync(join(dir, "shallow"), "utf8").trim() === commit;
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
// A history of one commit needs no shallow boundary, and git writes none.
|
|
240
|
+
return error.code === "ENOENT";
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/** Fetch main at depth 1 into `dir`, under the map's limits. `onStart` records the spawn for crash cleanup. */
|
|
244
|
+
export async function readShallowMain(url, dir, pin, bounded, onStart, helper) {
|
|
245
|
+
const fetched = await bounded("git", mapGitArgs(dir, pin, ["fetch", "--quiet", "--depth=1", "--no-tags", "--no-write-fetch-head", "--", url, `+refs/heads/main:${MAP_MAIN}`], { extra: LOW_MEMORY, ...(helper === undefined ? {} : { helper }) }), { cwd: dir, env: mapGitEnv(pin), timeoutMs: MAP_FETCH_LIMITS.timeoutMs, maxStdout: 4096, maxStderr: MAP_FETCH_LIMITS.maxStderr, memoryBytes: MAP_FETCH_LIMITS.memoryBytes,
|
|
246
|
+
diskBytes: MAP_FETCH_LIMITS.diskBytes, fileBytes: MAP_FETCH_LIMITS.fileBytes, folder: dir, label: "code-map-fetch", ...(onStart === undefined ? {} : { onStart }) });
|
|
247
|
+
if (fetched.code !== 0 && redirected(fetched.stderr))
|
|
248
|
+
return { ok: false, why: REMOTE_MOVED, reason: "moved" };
|
|
249
|
+
if (fetched.killed !== null)
|
|
250
|
+
return { ok: false, why: `the fetch went past its ${fetched.killed} limit` };
|
|
251
|
+
if (fetched.code !== 0)
|
|
252
|
+
return { ok: false, why: UNREAD };
|
|
253
|
+
const resolved = await bounded("git", mapGitArgs(dir, pin, ["rev-parse", "--verify", "--quiet", `${MAP_MAIN}^{commit}`]), { cwd: dir, env: mapGitEnv(pin), timeoutMs: 30_000, maxStdout: 4096, maxStderr: 4096, label: "code-map-rev" });
|
|
254
|
+
const commit = resolved.stdout.toString("utf8").trim();
|
|
255
|
+
if (resolved.code !== 0 || !SHA.test(commit) || !shallowClean(dir, commit))
|
|
256
|
+
return { ok: false, why: UNREAD };
|
|
257
|
+
return { ok: true, commit, enforced: fetched.enforced };
|
|
258
|
+
}
|
|
@@ -3,7 +3,8 @@ export type RepositoryContextRequest = {
|
|
|
3
3
|
query: string;
|
|
4
4
|
mode?: 'search' | 'impact';
|
|
5
5
|
audience?: 'lead' | 'crew';
|
|
6
|
-
/**
|
|
6
|
+
/** The admitted project from Toolroll's records: repo itself, or the project a crew worktree belongs to. Every read is
|
|
7
|
+
* bound to its git folder. */
|
|
7
8
|
project?: string;
|
|
8
9
|
baseRevision?: string;
|
|
9
10
|
cacheRoot?: string;
|
|
@@ -16,7 +17,7 @@ export type RepositoryCheckout = {
|
|
|
16
17
|
head: string;
|
|
17
18
|
baseRevision: string;
|
|
18
19
|
sourceHash: string;
|
|
19
|
-
source: 'working-tree';
|
|
20
|
+
source: 'working-tree' | 'base-commit';
|
|
20
21
|
};
|
|
21
22
|
export type RepositoryRelationship = {
|
|
22
23
|
from: string;
|