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,415 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One check of a project's recorded remote main, and the build of a new map when it moved (docs/plans/code-map.md §1-2).
|
|
3
|
+
* Toolroll's own code, in the worker: raw blobs from a private depth-1 folder, a static parse in a bounded child, no
|
|
4
|
+
* agent, no model and nothing from the repository run. The remote identity is captured first and carried unchanged to
|
|
5
|
+
* publication. Every write after the lease is fenced; a worker that lost its lease drops its result.
|
|
6
|
+
*/
|
|
7
|
+
import { createRequire } from "node:module";
|
|
8
|
+
import { mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync } from "node:fs";
|
|
9
|
+
import { createServer } from "node:http";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
import { boundedRun, cleanupLeftover } from "./bounded-spawn.js";
|
|
13
|
+
import { buildCodeMap, EXTRACTOR_TAG, externalPackage, isParsedLanguage, LIMITS, manifestEntries, unsafeName, vendoredRoot } from "./code-map-shape.js";
|
|
14
|
+
import { clearOldSpawn, clearRequest, CODE_MAP_ACT, codeMapOn, currentVersion, digestOfUrl, encodeMap, extendLease, publishVersion, readRefresh, recordCheck, recordFailure, recordFolder, recordPeek, recordSpawn, releaseLease, takeLease, tidyVersions } from "./code-map-store.js";
|
|
15
|
+
import { scanForSecrets } from "./evidence.js";
|
|
16
|
+
import { run } from "./exec.js";
|
|
17
|
+
import { pinDestination, lookupAll } from "./remote-destination.js";
|
|
18
|
+
import { initPrivateDir, mapGitArgs, mapGitEnv, peekMain, readShallowMain, remoteUrlProblem } from "./remote-main.js";
|
|
19
|
+
import { trustedRemoteIdentity } from "./store.js";
|
|
20
|
+
export const REFRESH_EVERY_MS = 30 * 60_000;
|
|
21
|
+
let extractorName = null;
|
|
22
|
+
/** Pinned by the parser's own package: a parser bump rebuilds every map. */
|
|
23
|
+
export function currentExtractor() {
|
|
24
|
+
if (extractorName !== null)
|
|
25
|
+
return extractorName;
|
|
26
|
+
const require = createRequire(import.meta.url);
|
|
27
|
+
const { version } = JSON.parse(readFileSync(require.resolve("typescript-parser/package.json"), "utf8"));
|
|
28
|
+
return extractorName = `typescript-parser@${version}/${EXTRACTOR_TAG}`;
|
|
29
|
+
}
|
|
30
|
+
let pinProven = null;
|
|
31
|
+
/** Connect a pinned name that can't resolve to a local listener: only a libcurl that honours the pin reaches it. */
|
|
32
|
+
export async function curlPinWorks() {
|
|
33
|
+
if (pinProven !== null)
|
|
34
|
+
return pinProven;
|
|
35
|
+
let hits = 0;
|
|
36
|
+
const server = createServer((_request, response) => { hits++; response.writeHead(404); response.end(); });
|
|
37
|
+
const port = await new Promise(done => {
|
|
38
|
+
server.once("error", () => done(null));
|
|
39
|
+
server.listen(0, "127.0.0.1", () => { const held = server.address(); done(typeof held === "object" && held !== null ? held.port : null); });
|
|
40
|
+
});
|
|
41
|
+
if (port === null)
|
|
42
|
+
return false;
|
|
43
|
+
try {
|
|
44
|
+
const host = "toolroll-pin-check.invalid";
|
|
45
|
+
await run("git", ["-c", "protocol.allow=always", "-c", `http.curloptResolve=${host}:${port}:127.0.0.1`, "ls-remote", "--", `http://${host}:${port}/x.git`], { cwd: "/", envAllowlist: ["PATH", "HOME"], env: { GIT_CONFIG_NOSYSTEM: "1", GIT_CONFIG_GLOBAL: "/dev/null", GIT_TERMINAL_PROMPT: "0" }, timeoutMs: 10_000 });
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
server.closeAllConnections();
|
|
49
|
+
server.close();
|
|
50
|
+
}
|
|
51
|
+
return pinProven = hits > 0;
|
|
52
|
+
}
|
|
53
|
+
// ---- reading the tree --------------------------------------------------------------------------------------------
|
|
54
|
+
const utf8 = new TextDecoder("utf-8", { fatal: true });
|
|
55
|
+
const textOf = (bytes) => { try {
|
|
56
|
+
return bytes.includes(0) ? null : utf8.decode(bytes);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return null;
|
|
60
|
+
} };
|
|
61
|
+
export function parseTree(output) {
|
|
62
|
+
const entries = [];
|
|
63
|
+
let at = 0;
|
|
64
|
+
while (at < output.length) {
|
|
65
|
+
let end = output.indexOf(0, at);
|
|
66
|
+
if (end < 0)
|
|
67
|
+
end = output.length;
|
|
68
|
+
const tab = output.indexOf(0x09, at);
|
|
69
|
+
if (tab > at && tab < end) {
|
|
70
|
+
const head = /^(\d{6}) (\w+) ([0-9a-f]{40,64}) +(\d+|-)$/.exec(output.subarray(at, tab).toString("latin1"));
|
|
71
|
+
if (head !== null) {
|
|
72
|
+
let path = null;
|
|
73
|
+
try {
|
|
74
|
+
path = utf8.decode(output.subarray(tab + 1, end));
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
path = null;
|
|
78
|
+
}
|
|
79
|
+
entries.push({ path, mode: head[1], kind: head[2], oid: head[3], bytes: head[4] === "-" ? 0 : Number(head[4]) });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
at = end + 1;
|
|
83
|
+
}
|
|
84
|
+
return entries;
|
|
85
|
+
}
|
|
86
|
+
const pathRefused = (path) => {
|
|
87
|
+
if (path === null || unsafeName(path) || Buffer.byteLength(path) > LIMITS.pathBytes)
|
|
88
|
+
return true;
|
|
89
|
+
const parts = path.split("/");
|
|
90
|
+
return parts.length - 1 > LIMITS.pathDepth || parts.some(part => part === "" || part === "." || part === ".." || part.toLowerCase() === ".git");
|
|
91
|
+
};
|
|
92
|
+
/** Which of `names` a secret scan flags, by one scan over all of them. */
|
|
93
|
+
function flagged(names) {
|
|
94
|
+
return new Set(scanForSecrets(names.join("\n")).map(hit => hit.line - 1));
|
|
95
|
+
}
|
|
96
|
+
async function readBlobs(dir, pin, bounded, wanted) {
|
|
97
|
+
const found = new Map();
|
|
98
|
+
const batches = [];
|
|
99
|
+
let batch = [], size = 0;
|
|
100
|
+
for (const one of wanted) {
|
|
101
|
+
if (batch.length > 0 && (batch.length >= 500 || size + one.bytes > 8 * 1024 * 1024)) {
|
|
102
|
+
batches.push(batch);
|
|
103
|
+
batch = [];
|
|
104
|
+
size = 0;
|
|
105
|
+
}
|
|
106
|
+
batch.push(one);
|
|
107
|
+
size += one.bytes;
|
|
108
|
+
}
|
|
109
|
+
if (batch.length > 0)
|
|
110
|
+
batches.push(batch);
|
|
111
|
+
for (const group of batches) {
|
|
112
|
+
const ran = await bounded("git", mapGitArgs(dir, pin, ["cat-file", "--batch"]), { cwd: dir, env: mapGitEnv(pin), stdin: Buffer.from(`${group.map(one => one.oid).join("\n")}\n`),
|
|
113
|
+
timeoutMs: 60_000, maxStdout: 8 * 1024 * 1024 + LIMITS.fileBytes + group.length * 128 + 4096, maxStderr: 4096, label: "code-map-blobs" });
|
|
114
|
+
if (ran.code !== 0)
|
|
115
|
+
throw new MapRefused("couldn't read main: its files couldn't be read", "failed");
|
|
116
|
+
let at = 0;
|
|
117
|
+
for (const one of group) {
|
|
118
|
+
const end = ran.stdout.indexOf(0x0a, at);
|
|
119
|
+
if (end < 0)
|
|
120
|
+
throw new MapRefused("couldn't read main: its files couldn't be read", "failed");
|
|
121
|
+
const head = /^([0-9a-f]{40,64}) (\w+) (\d+)$/.exec(ran.stdout.subarray(at, end).toString("latin1"));
|
|
122
|
+
if (head === null) {
|
|
123
|
+
at = end + 1;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const bytes = Number(head[3]);
|
|
127
|
+
if (head[1] === one.oid && head[2] === "blob" && bytes <= LIMITS.fileBytes)
|
|
128
|
+
found.set(one.path, ran.stdout.subarray(end + 1, end + 1 + bytes));
|
|
129
|
+
at = end + 1 + bytes + 1;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return found;
|
|
133
|
+
}
|
|
134
|
+
/** A failed build's reason, in the words the page shows, and the state it reads as. */
|
|
135
|
+
export class MapRefused extends Error {
|
|
136
|
+
state;
|
|
137
|
+
constructor(message, state) {
|
|
138
|
+
super(message);
|
|
139
|
+
this.state = state;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/** The parser process's entry, from the installed release (the sources under a test). */
|
|
143
|
+
export function parserEntry() {
|
|
144
|
+
const source = import.meta.url.endsWith(".ts");
|
|
145
|
+
const entry = fileURLToPath(new URL(source ? "./code-map-main.ts" : "./code-map-main.js", import.meta.url));
|
|
146
|
+
return { file: process.execPath, args: [`--max-old-space-size=${LIMITS.parserHeapMb}`, ...(source ? ["--import", import.meta.resolve("tsx")] : []), entry] };
|
|
147
|
+
}
|
|
148
|
+
async function parseBlobs(dir, bounded, files, paths, tsconfig, entry) {
|
|
149
|
+
const ran = await bounded(entry.file, entry.args, { cwd: dir, env: { PATH: process.env["PATH"] ?? "/usr/bin:/bin", HOME: dir }, stdin: Buffer.from(JSON.stringify({ files, paths, tsconfig })),
|
|
150
|
+
timeoutMs: LIMITS.parserMs, maxStdout: LIMITS.parserOutput, maxStderr: 4096, memoryBytes: 1024 ** 3, label: "code-map-parse" });
|
|
151
|
+
if (ran.code !== 0 || ran.killed !== null)
|
|
152
|
+
throw new MapRefused("couldn't finish reading", "failed");
|
|
153
|
+
try {
|
|
154
|
+
return JSON.parse(ran.stdout.toString("utf8"));
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
throw new MapRefused("couldn't finish reading", "failed");
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/** Everything between a clean private folder and a map. Throws MapRefused when no new version can come of it. */
|
|
161
|
+
export async function buildFromFolder(dir, pin, bounded, context, entry = parserEntry()) {
|
|
162
|
+
const listed = await bounded("git", mapGitArgs(dir, pin, ["ls-tree", "-r", "-l", "-z", context.commit]), { cwd: dir, env: mapGitEnv(pin), timeoutMs: 60_000, maxStdout: 16 * 1024 * 1024, maxStderr: 4096, label: "code-map-tree" });
|
|
163
|
+
if (listed.killed === "output")
|
|
164
|
+
throw new MapRefused(`too large: more than ${LIMITS.treeEntries.toLocaleString("en-US")} files`, "too-large");
|
|
165
|
+
if (listed.code !== 0)
|
|
166
|
+
throw new MapRefused("couldn't read main: its file list couldn't be read", "failed");
|
|
167
|
+
const raw = parseTree(listed.stdout);
|
|
168
|
+
if (raw.length > LIMITS.treeEntries)
|
|
169
|
+
throw new MapRefused(`too large: ${raw.length.toLocaleString("en-US")} files (the limit is ${LIMITS.treeEntries.toLocaleString("en-US")})`, "too-large");
|
|
170
|
+
let withheld = 0;
|
|
171
|
+
const named = raw.filter((entryRow) => { if (pathRefused(entryRow.path)) {
|
|
172
|
+
withheld++;
|
|
173
|
+
return false;
|
|
174
|
+
} return true; });
|
|
175
|
+
const secret = flagged(named.map(one => one.path));
|
|
176
|
+
const safe = named.filter((_one, index) => { if (secret.has(index)) {
|
|
177
|
+
withheld++;
|
|
178
|
+
return false;
|
|
179
|
+
} return true; });
|
|
180
|
+
const entries = safe.map(one => ({ path: one.path, mode: one.mode, bytes: one.bytes }));
|
|
181
|
+
const warnings = new Set();
|
|
182
|
+
const unread = new Map();
|
|
183
|
+
const toRead = [];
|
|
184
|
+
const manifestsWanted = [];
|
|
185
|
+
let tsconfigEntry = null;
|
|
186
|
+
let filesRead = 0, bytesRead = 0;
|
|
187
|
+
for (const one of safe) {
|
|
188
|
+
if (one.kind !== "blob" || (one.mode !== "100644" && one.mode !== "100755") || vendoredRoot(one.path) !== null)
|
|
189
|
+
continue;
|
|
190
|
+
const blob = { path: one.path, oid: one.oid, bytes: one.bytes };
|
|
191
|
+
if (one.path === "tsconfig.json" && one.bytes <= LIMITS.manifestBytes)
|
|
192
|
+
tsconfigEntry = blob;
|
|
193
|
+
if (one.path.endsWith("package.json") && (one.path === "package.json" || one.path.endsWith("/package.json")) && one.bytes <= LIMITS.manifestBytes)
|
|
194
|
+
manifestsWanted.push(blob);
|
|
195
|
+
if (!isParsedLanguage(one.path))
|
|
196
|
+
continue;
|
|
197
|
+
if (one.bytes > LIMITS.fileBytes) {
|
|
198
|
+
unread.set(one.path, "too-large");
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (filesRead >= LIMITS.filesRead || bytesRead + one.bytes > LIMITS.bytesRead) {
|
|
202
|
+
unread.set(one.path, "path-only");
|
|
203
|
+
warnings.add("read-limit");
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
toRead.push(blob);
|
|
207
|
+
filesRead++;
|
|
208
|
+
bytesRead += one.bytes;
|
|
209
|
+
}
|
|
210
|
+
const blobs = await readBlobs(dir, pin, bounded, [...toRead, ...manifestsWanted, ...(tsconfigEntry === null ? [] : [tsconfigEntry])]);
|
|
211
|
+
const files = [];
|
|
212
|
+
for (const one of toRead) {
|
|
213
|
+
const bytes = blobs.get(one.path);
|
|
214
|
+
const text = bytes === undefined ? null : textOf(bytes);
|
|
215
|
+
if (text === null)
|
|
216
|
+
unread.set(one.path, "path-only"); // not text
|
|
217
|
+
else
|
|
218
|
+
files.push({ path: one.path, text });
|
|
219
|
+
}
|
|
220
|
+
const tsconfigBytes = tsconfigEntry === null ? undefined : blobs.get(tsconfigEntry.path);
|
|
221
|
+
const tsconfig = tsconfigBytes === undefined ? null : textOf(tsconfigBytes);
|
|
222
|
+
const parsed = await parseBlobs(dir, bounded, files, entries.filter(one => vendoredRoot(one.path) === null).map(one => one.path), tsconfig, entry);
|
|
223
|
+
if (parsed.tsconfig === "extends")
|
|
224
|
+
warnings.add("tsconfig-extends");
|
|
225
|
+
if (parsed.tsconfig === "unreadable")
|
|
226
|
+
warnings.add("tsconfig-unreadable");
|
|
227
|
+
// The names the parser found: control characters and secrets are withheld here, on the parent's side.
|
|
228
|
+
const allNames = [];
|
|
229
|
+
for (const file of parsed.facts) {
|
|
230
|
+
for (const one of file.exports)
|
|
231
|
+
allNames.push(one.name);
|
|
232
|
+
for (const one of file.specifiers)
|
|
233
|
+
allNames.push(one.specifier);
|
|
234
|
+
}
|
|
235
|
+
const safeNames = allNames.map(name => name.replace(/[\r\n]/g, " "));
|
|
236
|
+
const secrets = flagged(safeNames);
|
|
237
|
+
let index = 0;
|
|
238
|
+
const facts = new Map();
|
|
239
|
+
for (const file of parsed.facts) {
|
|
240
|
+
const exports = [], imports = [], external = new Set();
|
|
241
|
+
for (const one of file.exports) {
|
|
242
|
+
const bad = secrets.has(index++) || unsafeName(one.name);
|
|
243
|
+
if (bad) {
|
|
244
|
+
withheld++;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (one.name.length > LIMITS.symbolChars)
|
|
248
|
+
warnings.add("symbols-truncated");
|
|
249
|
+
exports.push({ name: one.name.slice(0, LIMITS.symbolChars), kind: one.kind, line: one.line });
|
|
250
|
+
}
|
|
251
|
+
for (const one of file.specifiers) {
|
|
252
|
+
const bad = secrets.has(index++) || unsafeName(one.specifier);
|
|
253
|
+
if (bad) {
|
|
254
|
+
withheld++;
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
if (one.to !== null) {
|
|
258
|
+
imports.push({ to: one.to, kind: one.kind, line: one.line });
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
const name = externalPackage(one.specifier);
|
|
262
|
+
if (name !== null)
|
|
263
|
+
external.add(name);
|
|
264
|
+
}
|
|
265
|
+
facts.set(file.path, { exports, imports, external: [...external], parseFailed: file.parseFailed });
|
|
266
|
+
}
|
|
267
|
+
const manifests = new Map();
|
|
268
|
+
for (const one of manifestsWanted) {
|
|
269
|
+
const bytes = blobs.get(one.path);
|
|
270
|
+
const text = bytes === undefined ? null : textOf(bytes);
|
|
271
|
+
const read = text === null ? null : manifestEntries(text);
|
|
272
|
+
if (read === null)
|
|
273
|
+
continue;
|
|
274
|
+
const named2 = read.name !== null && !unsafeName(read.name) && flagged([read.name]).size === 0 ? read.name : null;
|
|
275
|
+
if (read.name !== null && named2 === null)
|
|
276
|
+
withheld++;
|
|
277
|
+
manifests.set(one.path, { name: named2, entries: read.entries.filter(entryName => !unsafeName(entryName)) });
|
|
278
|
+
}
|
|
279
|
+
return buildCodeMap({ repo: context.repo, commit: context.commit, remote: { urlDigest: context.urlDigest, record: context.record }, builtAt: context.builtAt, extractor: context.extractor,
|
|
280
|
+
entries, entryCount: raw.length, withheld, facts, unread, manifests, warnings: [...warnings] });
|
|
281
|
+
}
|
|
282
|
+
/** Claim the instance's one build, in the transaction that records the folder for crash cleanup. */
|
|
283
|
+
function claimBuild(store, lease, folder, now) {
|
|
284
|
+
return store.transact(() => {
|
|
285
|
+
const other = store.handle.prepare("SELECT 1 FROM code_map_refresh WHERE repo <> ? AND spawn_folder IS NOT NULL AND lease_owner IS NOT NULL AND lease_until > ? LIMIT 1").get(lease.repo, now.toISOString());
|
|
286
|
+
return other === undefined && recordFolder(store, lease, folder);
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
/** Check one project; build and publish when main moved. Never throws for a remote's sake. */
|
|
290
|
+
export async function checkCodeMap(store, repo, deps) {
|
|
291
|
+
const clock = deps.clock ?? (() => new Date());
|
|
292
|
+
const bounded = deps.bounded ?? boundedRun;
|
|
293
|
+
const owner = deps.owner ?? `worker-${process.pid}`;
|
|
294
|
+
// Identity first: everything after this is judged against what is captured here.
|
|
295
|
+
const identity = trustedRemoteIdentity(store, repo);
|
|
296
|
+
if (identity === null || remoteUrlProblem(identity.url) !== null || !codeMapOn(store, repo))
|
|
297
|
+
return "unavailable";
|
|
298
|
+
const urlDigest = digestOfUrl(identity.url);
|
|
299
|
+
const lease = takeLease(store, repo, owner, clock());
|
|
300
|
+
if (lease === null)
|
|
301
|
+
return "held";
|
|
302
|
+
mkdirSync(deps.scratch, { recursive: true });
|
|
303
|
+
// What a crashed build left, before anything else.
|
|
304
|
+
cleanupLeftover(lease.previous, realpathSync(deps.scratch));
|
|
305
|
+
if (lease.previous.pid !== null || lease.previous.folder !== null)
|
|
306
|
+
clearOldSpawn(store, lease);
|
|
307
|
+
const started = currentVersion(store, repo);
|
|
308
|
+
const extractor = currentExtractor();
|
|
309
|
+
const folder = mkdtempSync(join(realpathSync(deps.scratch), ".code-map-"));
|
|
310
|
+
const ledger = (outcome, detail) => { store.recordAction({ at: clock().toISOString(), actor: "toolroll", repo, taskId: null, runId: null, action: CODE_MAP_ACT, outcome, source: "work", detail }); };
|
|
311
|
+
const finishCheck = (state, commit, extra = {}) => {
|
|
312
|
+
if (!recordCheck(store, lease, { digest: urlDigest, record: identity.record, commit, extractor, state, ...extra }, clock()))
|
|
313
|
+
return false;
|
|
314
|
+
clearRequest(store, lease);
|
|
315
|
+
ledger(commit ?? "unread", state);
|
|
316
|
+
return true;
|
|
317
|
+
};
|
|
318
|
+
const fail = (why, state) => {
|
|
319
|
+
if (!recordFailure(store, lease, why, state, clock()))
|
|
320
|
+
return "lease-lost";
|
|
321
|
+
clearRequest(store, lease);
|
|
322
|
+
ledger("unread", why.slice(0, 300));
|
|
323
|
+
return state;
|
|
324
|
+
};
|
|
325
|
+
try {
|
|
326
|
+
const pinned = await pinDestination(identity.url, deps.env ?? process.env, deps.lookup ?? lookupAll);
|
|
327
|
+
if (!pinned.ok)
|
|
328
|
+
return fail(`couldn't read main: ${pinned.why}`, "failed");
|
|
329
|
+
const pin = pinned.pin;
|
|
330
|
+
if (pin.env["GIT_SSH_COMMAND"] === undefined && !(await (deps.pinWorks ?? curlPinWorks)()))
|
|
331
|
+
return fail("couldn't read main: this git can't pin its connection", "failed");
|
|
332
|
+
await initPrivateDir(folder, bounded);
|
|
333
|
+
const peek = await peekMain(identity.url, folder, pin, bounded, deps.helper);
|
|
334
|
+
if (!peek.ok)
|
|
335
|
+
return fail(peek.reason === "moved" ? peek.why : `couldn't read main: ${peek.why}`, "failed");
|
|
336
|
+
if (peek.commit === null)
|
|
337
|
+
return finishCheck("no-main", null) ? "no-main" : "lease-lost";
|
|
338
|
+
if (!recordPeek(store, lease, peek.commit))
|
|
339
|
+
return "lease-lost";
|
|
340
|
+
if (started !== null && started.remoteDigest === urlDigest && started.remoteRecord === identity.record && started.commit === peek.commit && started.extractor === extractor) {
|
|
341
|
+
return finishCheck("current", peek.commit, { proxy: pin.proxy }) ? "fresh" : "lease-lost";
|
|
342
|
+
}
|
|
343
|
+
if (!claimBuild(store, lease, folder, clock())) {
|
|
344
|
+
if (readRefresh(store, repo)?.leaseToken !== lease.token)
|
|
345
|
+
return "lease-lost";
|
|
346
|
+
return "waits";
|
|
347
|
+
}
|
|
348
|
+
await deps.atStage?.("before-fetch");
|
|
349
|
+
const fetched = await readShallowMain(identity.url, folder, pin, bounded, spawn => { recordSpawn(store, lease, spawn); }, deps.helper);
|
|
350
|
+
if (!fetched.ok)
|
|
351
|
+
return fail(fetched.reason === "moved" ? fetched.why : `couldn't read main: ${fetched.why}`, "failed");
|
|
352
|
+
await deps.atStage?.("after-fetch");
|
|
353
|
+
if (!extendLease(store, lease, clock()))
|
|
354
|
+
return "lease-lost";
|
|
355
|
+
const builtAt = clock().toISOString();
|
|
356
|
+
let map;
|
|
357
|
+
try {
|
|
358
|
+
map = await buildFromFolder(folder, pin, bounded, { repo, commit: fetched.commit, urlDigest, record: identity.record, builtAt, extractor });
|
|
359
|
+
}
|
|
360
|
+
catch (error) {
|
|
361
|
+
if (error instanceof MapRefused)
|
|
362
|
+
return fail(error.message, error.state);
|
|
363
|
+
return fail("couldn't finish reading", "failed");
|
|
364
|
+
}
|
|
365
|
+
if (!extendLease(store, lease, clock()))
|
|
366
|
+
return "lease-lost";
|
|
367
|
+
const encoded = encodeMap(map);
|
|
368
|
+
if (encoded.bytes > LIMITS.storedBytes)
|
|
369
|
+
return fail("too large to store", "too-large");
|
|
370
|
+
await deps.atStage?.("before-publish");
|
|
371
|
+
const report = JSON.stringify({ enforced: fetched.enforced, proxy: pin.proxy });
|
|
372
|
+
const published = publishVersion(store, lease, { map, encoded, identity, startedFrom: started?.id ?? null, report }, clock());
|
|
373
|
+
if (published === null)
|
|
374
|
+
return "lease-lost";
|
|
375
|
+
const state = map.counts.entries === 0 ? "empty" : published.current ? "current" : published.why;
|
|
376
|
+
if (!finishCheck(state, fetched.commit, { enforced: fetched.enforced, proxy: pin.proxy }))
|
|
377
|
+
return "lease-lost";
|
|
378
|
+
if (published.current)
|
|
379
|
+
tidyVersions(store, repo);
|
|
380
|
+
return published.current ? (map.counts.entries === 0 ? "empty" : "published") : "stored";
|
|
381
|
+
}
|
|
382
|
+
finally {
|
|
383
|
+
try {
|
|
384
|
+
rmSync(folder, { recursive: true, force: true });
|
|
385
|
+
}
|
|
386
|
+
catch { /* a later takeover removes it */ }
|
|
387
|
+
releaseLease(store, lease);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
/** Projects due a check: the map on, a remote recorded, past any back-off, and a request queued or 30 minutes since the
|
|
391
|
+
* last completed check. */
|
|
392
|
+
export function dueProjects(store, now) {
|
|
393
|
+
const rows = store.handle.prepare(`SELECT p.path, r.requested_at, r.retry_after, r.last_checked_at FROM project p LEFT JOIN code_map_refresh r ON r.repo = p.path
|
|
394
|
+
WHERE p.code_map = 1 AND p.remote_url IS NOT NULL AND p.remote_url <> '' ORDER BY p.path`).all();
|
|
395
|
+
return rows.filter(row => {
|
|
396
|
+
const retry = row["retry_after"];
|
|
397
|
+
if (typeof retry === "string" && Date.parse(retry) > now.getTime())
|
|
398
|
+
return false;
|
|
399
|
+
const last = row["last_checked_at"];
|
|
400
|
+
return row["requested_at"] !== null && row["requested_at"] !== undefined || typeof last !== "string" || now.getTime() - Date.parse(last) >= REFRESH_EVERY_MS;
|
|
401
|
+
}).map(row => String(row["path"]));
|
|
402
|
+
}
|
|
403
|
+
/** The watch loop's step: every due project in turn, one build at a time. */
|
|
404
|
+
export async function refreshCodeMaps(store, deps) {
|
|
405
|
+
const clock = deps.clock ?? (() => new Date());
|
|
406
|
+
let checked = 0;
|
|
407
|
+
for (const repo of dueProjects(store, clock())) {
|
|
408
|
+
try {
|
|
409
|
+
await checkCodeMap(store, repo, deps);
|
|
410
|
+
checked++;
|
|
411
|
+
}
|
|
412
|
+
catch { /* the next pass tries again */ }
|
|
413
|
+
}
|
|
414
|
+
return checked;
|
|
415
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { type MapDoor } from "./code-map-acts.js";
|
|
2
|
+
import type { Store } from "./store.js";
|
|
3
|
+
export declare const MAP_DESCRIPTORS: readonly [{
|
|
4
|
+
readonly action: "";
|
|
5
|
+
readonly synopsis: "a project's code map: its areas, and how they use each other";
|
|
6
|
+
readonly mutation: "none";
|
|
7
|
+
readonly positional: null;
|
|
8
|
+
readonly flags: readonly [Readonly<{
|
|
9
|
+
[x: string]: unknown;
|
|
10
|
+
name: string;
|
|
11
|
+
takesValue: boolean;
|
|
12
|
+
meaning: string;
|
|
13
|
+
}>, Readonly<{
|
|
14
|
+
[x: string]: unknown;
|
|
15
|
+
name: string;
|
|
16
|
+
takesValue: boolean;
|
|
17
|
+
meaning: string;
|
|
18
|
+
}>, Readonly<{
|
|
19
|
+
[x: string]: unknown;
|
|
20
|
+
name: string;
|
|
21
|
+
takesValue: boolean;
|
|
22
|
+
meaning: string;
|
|
23
|
+
}>];
|
|
24
|
+
}, {
|
|
25
|
+
readonly action: "area";
|
|
26
|
+
readonly synopsis: "one area's files, exported names, tests and connections: map area <id>";
|
|
27
|
+
readonly mutation: "none";
|
|
28
|
+
readonly positional: "the area's id";
|
|
29
|
+
readonly flags: readonly Readonly<{
|
|
30
|
+
[x: string]: unknown;
|
|
31
|
+
name: string;
|
|
32
|
+
takesValue: boolean;
|
|
33
|
+
meaning: string;
|
|
34
|
+
}>[];
|
|
35
|
+
}, {
|
|
36
|
+
readonly action: "file";
|
|
37
|
+
readonly synopsis: "one file's exported names, imports and who imports it: map file <path>";
|
|
38
|
+
readonly mutation: "none";
|
|
39
|
+
readonly positional: "the file's exact path";
|
|
40
|
+
readonly flags: readonly Readonly<{
|
|
41
|
+
[x: string]: unknown;
|
|
42
|
+
name: string;
|
|
43
|
+
takesValue: boolean;
|
|
44
|
+
meaning: string;
|
|
45
|
+
}>[];
|
|
46
|
+
}, {
|
|
47
|
+
readonly action: "search";
|
|
48
|
+
readonly synopsis: "search files and exported names: map search <words>";
|
|
49
|
+
readonly mutation: "none";
|
|
50
|
+
readonly positional: "words to search for";
|
|
51
|
+
readonly flags: readonly Readonly<{
|
|
52
|
+
[x: string]: unknown;
|
|
53
|
+
name: string;
|
|
54
|
+
takesValue: boolean;
|
|
55
|
+
meaning: string;
|
|
56
|
+
}>[];
|
|
57
|
+
}, {
|
|
58
|
+
readonly action: "refresh";
|
|
59
|
+
readonly synopsis: "ask for a check of main now; it runs on the next pass";
|
|
60
|
+
readonly mutation: "unkeyed";
|
|
61
|
+
readonly positional: null;
|
|
62
|
+
readonly flags: readonly [Readonly<{
|
|
63
|
+
[x: string]: unknown;
|
|
64
|
+
name: string;
|
|
65
|
+
takesValue: boolean;
|
|
66
|
+
meaning: string;
|
|
67
|
+
}>, Readonly<{
|
|
68
|
+
[x: string]: unknown;
|
|
69
|
+
name: string;
|
|
70
|
+
takesValue: boolean;
|
|
71
|
+
meaning: string;
|
|
72
|
+
}>];
|
|
73
|
+
}, {
|
|
74
|
+
readonly action: "report";
|
|
75
|
+
readonly synopsis: "builds with and without the map, by assigned arm";
|
|
76
|
+
readonly mutation: "none";
|
|
77
|
+
readonly positional: null;
|
|
78
|
+
readonly flags: readonly [Readonly<{
|
|
79
|
+
[x: string]: unknown;
|
|
80
|
+
name: string;
|
|
81
|
+
takesValue: boolean;
|
|
82
|
+
meaning: string;
|
|
83
|
+
}>, Readonly<{
|
|
84
|
+
[x: string]: unknown;
|
|
85
|
+
name: string;
|
|
86
|
+
takesValue: boolean;
|
|
87
|
+
meaning: string;
|
|
88
|
+
}>];
|
|
89
|
+
}, {
|
|
90
|
+
readonly action: "on";
|
|
91
|
+
readonly synopsis: "turn a project's code map on (a person, or this computer's owner)";
|
|
92
|
+
readonly mutation: "unkeyed";
|
|
93
|
+
readonly positional: null;
|
|
94
|
+
readonly flags: readonly [Readonly<{
|
|
95
|
+
[x: string]: unknown;
|
|
96
|
+
name: string;
|
|
97
|
+
takesValue: boolean;
|
|
98
|
+
meaning: string;
|
|
99
|
+
}>, Readonly<{
|
|
100
|
+
[x: string]: unknown;
|
|
101
|
+
name: string;
|
|
102
|
+
takesValue: boolean;
|
|
103
|
+
meaning: string;
|
|
104
|
+
}>];
|
|
105
|
+
readonly audience: "operator";
|
|
106
|
+
}, {
|
|
107
|
+
readonly action: "off";
|
|
108
|
+
readonly synopsis: "turn a project's code map off (a person, or this computer's owner)";
|
|
109
|
+
readonly mutation: "unkeyed";
|
|
110
|
+
readonly positional: null;
|
|
111
|
+
readonly flags: readonly [Readonly<{
|
|
112
|
+
[x: string]: unknown;
|
|
113
|
+
name: string;
|
|
114
|
+
takesValue: boolean;
|
|
115
|
+
meaning: string;
|
|
116
|
+
}>, Readonly<{
|
|
117
|
+
[x: string]: unknown;
|
|
118
|
+
name: string;
|
|
119
|
+
takesValue: boolean;
|
|
120
|
+
meaning: string;
|
|
121
|
+
}>];
|
|
122
|
+
readonly audience: "operator";
|
|
123
|
+
}, {
|
|
124
|
+
readonly action: "holdout end";
|
|
125
|
+
readonly synopsis: "stop enrolling new builds in the holdout; existing ones keep their arm (a person)";
|
|
126
|
+
readonly mutation: "unkeyed";
|
|
127
|
+
readonly positional: null;
|
|
128
|
+
readonly flags: readonly [Readonly<{
|
|
129
|
+
[x: string]: unknown;
|
|
130
|
+
name: string;
|
|
131
|
+
takesValue: boolean;
|
|
132
|
+
meaning: string;
|
|
133
|
+
}>, Readonly<{
|
|
134
|
+
[x: string]: unknown;
|
|
135
|
+
name: string;
|
|
136
|
+
takesValue: boolean;
|
|
137
|
+
meaning: string;
|
|
138
|
+
}>];
|
|
139
|
+
readonly audience: "operator";
|
|
140
|
+
}];
|
|
141
|
+
export type MapCliContext = {
|
|
142
|
+
store: Store;
|
|
143
|
+
write: (line: string) => void;
|
|
144
|
+
json: boolean;
|
|
145
|
+
now: Date;
|
|
146
|
+
actor: string | null;
|
|
147
|
+
repos: readonly string[];
|
|
148
|
+
/** Whether the caller stands as an approver of this project, proved now (a lead token does; a local owner doesn't). */
|
|
149
|
+
approver: (repo: string) => boolean;
|
|
150
|
+
/** Who may change this project's map, as proved for this command; null when nobody was. */
|
|
151
|
+
door: (repo: string) => MapDoor | null;
|
|
152
|
+
};
|
|
153
|
+
export declare function runMapCommand(positional: readonly string[], options: Map<string, string | true>, context: MapCliContext): Promise<number>;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `toolroll map` (docs/plans/code-map.md §5-7): the same reader and acts as the page. A read needs project access;
|
|
3
|
+
* `refresh` any approver, the lead included; `on`/`off` and `holdout end` a person, re-proved inside the write.
|
|
4
|
+
*/
|
|
5
|
+
import { resolve } from "node:path";
|
|
6
|
+
import { answerMap, areaRows, codeMapStatus, openMap } from "./code-map-read.js";
|
|
7
|
+
import { endHoldout, setCodeMapSwitch } from "./code-map-acts.js";
|
|
8
|
+
import { codeMapReport, NOT_ENOUGH, reportRows } from "./code-map-report.js";
|
|
9
|
+
import { requestRefresh } from "./code-map-store.js";
|
|
10
|
+
import { canonicalProject } from "./project.js";
|
|
11
|
+
import { envelopeJson } from "./envelope.js";
|
|
12
|
+
const jsonFlag = { name: "json", takesValue: false, meaning: "answer with one machine envelope on stdout" };
|
|
13
|
+
const repoFlag = { name: "repo", takesValue: true, meaning: "which project" };
|
|
14
|
+
const pageFlags = [jsonFlag, repoFlag, { name: "cursor", takesValue: true, meaning: "the next page, from the answer before it" }];
|
|
15
|
+
export const MAP_DESCRIPTORS = [
|
|
16
|
+
{ action: "", synopsis: "a project's code map: its areas, and how they use each other", mutation: "none", positional: null, flags: [jsonFlag, repoFlag, pageFlags[2]] },
|
|
17
|
+
{ action: "area", synopsis: "one area's files, exported names, tests and connections: map area <id>", mutation: "none", positional: "the area's id", flags: pageFlags },
|
|
18
|
+
{ action: "file", synopsis: "one file's exported names, imports and who imports it: map file <path>", mutation: "none", positional: "the file's exact path", flags: pageFlags },
|
|
19
|
+
{ action: "search", synopsis: "search files and exported names: map search <words>", mutation: "none", positional: "words to search for", flags: pageFlags },
|
|
20
|
+
{ action: "refresh", synopsis: "ask for a check of main now; it runs on the next pass", mutation: "unkeyed", positional: null, flags: [jsonFlag, repoFlag] },
|
|
21
|
+
{ action: "report", synopsis: "builds with and without the map, by assigned arm", mutation: "none", positional: null, flags: [jsonFlag, repoFlag] },
|
|
22
|
+
{ action: "on", synopsis: "turn a project's code map on (a person, or this computer's owner)", mutation: "unkeyed", positional: null, flags: [jsonFlag, repoFlag], audience: "operator" },
|
|
23
|
+
{ action: "off", synopsis: "turn a project's code map off (a person, or this computer's owner)", mutation: "unkeyed", positional: null, flags: [jsonFlag, repoFlag], audience: "operator" },
|
|
24
|
+
{ action: "holdout end", synopsis: "stop enrolling new builds in the holdout; existing ones keep their arm (a person)", mutation: "unkeyed", positional: null, flags: [jsonFlag, repoFlag], audience: "operator" },
|
|
25
|
+
];
|
|
26
|
+
const printable = (text) => [...text].map(char => (/[\p{C}]/u.test(char) ? `\\u{${char.codePointAt(0).toString(16)}}` : char)).join("");
|
|
27
|
+
function linesOf(answer) {
|
|
28
|
+
const lines = [`Code map · main at ${answer.commit.slice(0, 7)}`, answer.label, ""];
|
|
29
|
+
for (const item of answer.items)
|
|
30
|
+
lines.push(printable(JSON.stringify(item)));
|
|
31
|
+
if (answer.omitted > 0)
|
|
32
|
+
lines.push(`${answer.omitted} too long to show.`);
|
|
33
|
+
if (answer.cursor !== null)
|
|
34
|
+
lines.push(`More: --cursor ${answer.cursor}`);
|
|
35
|
+
return lines;
|
|
36
|
+
}
|
|
37
|
+
export async function runMapCommand(positional, options, context) {
|
|
38
|
+
const { store, now } = context;
|
|
39
|
+
const first = positional[0];
|
|
40
|
+
const action = first === undefined || first.startsWith("/") || first.startsWith(".") ? "" : first;
|
|
41
|
+
const named = action === "" ? first : undefined;
|
|
42
|
+
const command = action === "" ? "map" : `map ${action}`;
|
|
43
|
+
const fail = (reason, message) => { context.write(context.json ? envelopeJson({ ok: false, command, reason, message }) : printable(message)); return reason === "usage" ? 2 : 1; };
|
|
44
|
+
const ok = (result, lines) => { context.write(context.json ? envelopeJson({ ok: true, command, ...result }) : lines.join("\n")); return 0; };
|
|
45
|
+
if (!["", "area", "file", "search", "refresh", "report", "on", "off", "holdout"].includes(action))
|
|
46
|
+
return fail("usage", "Use map [<project>] | area <id> | file <path> | search <words> | refresh | report | on | off | holdout end.");
|
|
47
|
+
const open = context.repos;
|
|
48
|
+
const asked = options.get("repo") ?? named;
|
|
49
|
+
if (asked === true)
|
|
50
|
+
return fail("usage", "--repo takes a project path.");
|
|
51
|
+
const wanted = asked === undefined ? (open.length === 1 ? open[0] : null) : canonicalProject(asked) ?? resolve(asked);
|
|
52
|
+
if (wanted === null)
|
|
53
|
+
return fail("usage", "Name the project: --repo <path>.");
|
|
54
|
+
if (!open.includes(wanted))
|
|
55
|
+
return fail("refused", "That project isn't one you can open here.");
|
|
56
|
+
if (action === "refresh") {
|
|
57
|
+
if (context.actor === null)
|
|
58
|
+
return fail("unauthenticated", "Sign in first: an approver asks for a check.");
|
|
59
|
+
if (!context.approver(wanted))
|
|
60
|
+
return fail("refused", "Only an approver asks for a check.");
|
|
61
|
+
const said = requestRefresh(store, wanted, context.actor, now);
|
|
62
|
+
return ok({ repo: wanted, request: said }, [said === "queued" ? "Queued: main is checked on the next pass." : "Already queued."]);
|
|
63
|
+
}
|
|
64
|
+
if (action === "on" || action === "off" || action === "holdout") {
|
|
65
|
+
if (action === "holdout" && (positional[1] !== "end" || positional.length !== 2))
|
|
66
|
+
return fail("usage", "Use map holdout end.");
|
|
67
|
+
const door = context.door(wanted);
|
|
68
|
+
if (door === null)
|
|
69
|
+
return fail("unauthenticated", "This takes your own sign-in: --as <you> --token <password>.");
|
|
70
|
+
const done = action === "holdout" ? endHoldout(store, door, wanted, now) : setCodeMapSwitch(store, door, wanted, action === "on", now);
|
|
71
|
+
if (!done.ok)
|
|
72
|
+
return fail("refused", done.problem);
|
|
73
|
+
return ok({ repo: wanted, ...(action === "holdout" ? { holdout: "ended" } : { codeMap: action }) }, [action === "holdout" ? "The holdout has ended: no new builds are enrolled, and existing ones keep their arm." : `The code map is ${action} for this project.`]);
|
|
74
|
+
}
|
|
75
|
+
if (action === "report") {
|
|
76
|
+
const report = codeMapReport(store, wanted, now);
|
|
77
|
+
return ok({ report }, ["Builds with and without the map", ...reportRows(report).map(row => `${row.label}: with ${row.map} · without ${row.holdout}`), ...(report.enough ? [] : [NOT_ENOUGH])]);
|
|
78
|
+
}
|
|
79
|
+
const cursor = options.get("cursor");
|
|
80
|
+
if (cursor === true)
|
|
81
|
+
return fail("usage", "--cursor takes the value the last answer gave.");
|
|
82
|
+
if (action === "") {
|
|
83
|
+
const status = codeMapStatus(store, wanted, now);
|
|
84
|
+
if (status.version === null)
|
|
85
|
+
return ok({ map: { v: 1, ok: false, state: status.state, message: status.words }, state: status.state }, [status.words]);
|
|
86
|
+
const answer = answerMap(store, { repo: wanted, view: "overview", ...(cursor === undefined ? {} : { cursor }) });
|
|
87
|
+
if (!answer.ok)
|
|
88
|
+
return ok({ map: answer, state: status.state }, [answer.message]);
|
|
89
|
+
if (context.json)
|
|
90
|
+
return ok({ map: answer, state: status.state }, []);
|
|
91
|
+
const opened = openMap(store, wanted, answer.version);
|
|
92
|
+
const rows = opened.ok ? areaRows(opened.map) : [];
|
|
93
|
+
return ok({}, [`Code map · main at ${answer.commit.slice(0, 7)} · ${status.words}`, answer.label, "", ...rows.slice(0, 60).map(one => printable(`${one.id} · ${one.files} files · ${one.languages.join(", ") || "no code"}${one.uses.length === 0 ? "" : ` · uses ${one.uses.slice(0, 3).map(use => use.id).join(", ")}`}`)), ...(rows.length > 60 ? [`${rows.length - 60} more: use --json with --cursor.`] : [])]);
|
|
94
|
+
}
|
|
95
|
+
const view = action;
|
|
96
|
+
const query = positional.slice(1).join(" ");
|
|
97
|
+
if (query === "")
|
|
98
|
+
return fail("usage", action === "search" ? "Say what to search for." : `Name it: map ${action} <${action === "area" ? "id" : "path"}>.`);
|
|
99
|
+
const answer = answerMap(store, { repo: wanted, view, ...(view === "search" ? { query } : { id: positional[1] }), ...(cursor === undefined ? {} : { cursor }) });
|
|
100
|
+
if (!answer.ok)
|
|
101
|
+
return answer.state === "not-found" || answer.state === "usage" ? fail(answer.state === "usage" ? "usage" : "not-found", answer.message) : ok({ map: answer }, [answer.message]);
|
|
102
|
+
return ok({ map: answer }, linesOf(answer));
|
|
103
|
+
}
|