@dev.fast/whiteboard 0.0.0-stage → 0.2.1-preview.20261005.98
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/LICENSE +21 -0
- package/README.md +146 -2
- package/THIRD_PARTY_NOTICES.md +93 -0
- package/dist/account-alias-DOHN1RSH.js +973 -0
- package/dist/agent-cli-ChOq0ZuO.js +117 -0
- package/dist/agent-cli-DzWfZr6G.js +2 -0
- package/dist/agent-client-CPQo7iTI.js +305 -0
- package/dist/authoring-tools-Cr6Kpjsx.js +234 -0
- package/dist/build-info.json +1 -0
- package/dist/cli-hVgHcdsv.js +47 -0
- package/dist/cli-runner-D8y2_luU.js +5095 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +128 -0
- package/dist/client-DFoh50W3.js +950 -0
- package/dist/desktop-discovery-DBO-PM2V.js +164 -0
- package/dist/error-message-OtiDonty.js +6 -0
- package/dist/fs-utils-BMPLt0cr.js +22 -0
- package/dist/fuzzy-match-BoAmcyak.js +30 -0
- package/dist/headless-host-BJxf4dUU.js +94 -0
- package/dist/input-error-OLgB_h31.js +11 -0
- package/dist/local-data-CxLk75rx.js +4491 -0
- package/dist/mcp-QeF8vkdf.js +140 -0
- package/dist/package-paths-B6-zxvIO.js +47 -0
- package/dist/process-error-telemetry-v9e5h6D7.js +3952 -0
- package/dist/profile-4ry2f3AM.js +76 -0
- package/dist/profile-Bh5FIrmf.js +2 -0
- package/dist/request-origin-D_4QIdoN.js +811 -0
- package/dist/review-agent-traces-DNJFYzc8.js +3866 -0
- package/dist/review-home-paths-6zZH-1c9.js +2560 -0
- package/dist/review-telemetry-DEFnQsQ2.js +1195 -0
- package/dist/runtime-CKiWhGrf.js +52 -0
- package/dist/runtime.d.ts +9 -0
- package/dist/runtime.js +2 -0
- package/dist/s3-SzLpeCG6.js +345 -0
- package/dist/s3-config-BgxkSoOy.js +2 -0
- package/dist/s3-config-CAxhO9u_.js +676 -0
- package/dist/server/desktop-host.d.ts +4 -0
- package/dist/server/desktop-host.js +1702 -0
- package/dist/server-discovery-Dbzn3w6Z.js +66 -0
- package/dist/sharing/index.d.ts +1977 -0
- package/dist/sharing/index.js +2 -0
- package/dist/src-CGP5ytbV.js +283 -0
- package/dist/src-CmdiBL20.js +3676 -0
- package/dist/src-DnwdaQ2r.js +865 -0
- package/dist/src-X9phtB2j.js +68 -0
- package/dist/stored-document-migration-DHeMpLMb.js +2671 -0
- package/dist/tool-failure-C8zB73HV.js +98 -0
- package/dist/tutorial-trace-DrBvkaeL.js +43 -0
- package/instructions/authoring.md +36 -0
- package/instructions/file-lenses.md +13 -0
- package/instructions/scratchpad.md +23 -0
- package/instructions/trace-archaeology.md +113 -0
- package/onboarding.md +8 -0
- package/package.json +106 -3
- package/src/agent-selection.ts +100 -0
- package/src/agent-session-ref.ts +80 -0
- package/src/ask/agents.ts +351 -0
- package/src/ask/checkout-files.ts +47 -0
- package/src/ask/file-refs.ts +111 -0
- package/src/ask/pi-mcp.ts +154 -0
- package/src/ask/protocol.ts +244 -0
- package/src/ask/thread-state.ts +350 -0
- package/src/ask/thread.ts +1400 -0
- package/src/ask/threads.ts +219 -0
- package/src/ask/watch.ts +79 -0
- package/src/cli-install.ts +1041 -0
- package/src/cli-runner.ts +1461 -0
- package/src/cli-runtime-info.ts +35 -0
- package/src/cli.ts +222 -0
- package/src/connect-prompts.ts +248 -0
- package/src/cursor-deeplink.ts +11 -0
- package/src/desktop-discovery.ts +355 -0
- package/src/diff-selection-migration.ts +63 -0
- package/src/embedded-posthog-key.ts +6 -0
- package/src/error-telemetry.ts +247 -0
- package/src/evidence.ts +14 -0
- package/src/exception-telemetry.ts +126 -0
- package/src/fixtures/blocks/call_stack_diff.json +27 -0
- package/src/fixtures/blocks/callout.json +11 -0
- package/src/fixtures/blocks/code.json +9 -0
- package/src/fixtures/blocks/code_peek.json +8 -0
- package/src/fixtures/blocks/database_lens.json +51 -0
- package/src/fixtures/blocks/divider.json +1 -0
- package/src/fixtures/blocks/fixtures.ts +33 -0
- package/src/fixtures/blocks/flow_diagram.json +36 -0
- package/src/fixtures/blocks/ids.ts +8 -0
- package/src/fixtures/blocks/image.json +9 -0
- package/src/fixtures/blocks/markdown.json +7 -0
- package/src/fixtures/blocks/section.json +11 -0
- package/src/fixtures/blocks/sequence.json +31 -0
- package/src/fixtures/blocks/software_map.json +7 -0
- package/src/fixtures/blocks/trace_quote.json +9 -0
- package/src/fixtures/blocks/tutorial.json +51 -0
- package/src/fs-utils.ts +32 -0
- package/src/fuzzy-match.ts +94 -0
- package/src/install.ts +29 -0
- package/src/legacy-skills.ts +133 -0
- package/src/lens-selection.ts +230 -0
- package/src/markdown-latex-math.ts +230 -0
- package/src/markdown.ts +81 -0
- package/src/package-paths.ts +53 -0
- package/src/posthog-capture-client.ts +610 -0
- package/src/review-api/README.md +295 -0
- package/src/review-api/activity.ts +340 -0
- package/src/review-api/agent-cli.ts +219 -0
- package/src/review-api/agent-client.ts +188 -0
- package/src/review-api/anchor-quotes.ts +88 -0
- package/src/review-api/ask-history.ts +242 -0
- package/src/review-api/authoring-tools.ts +211 -0
- package/src/review-api/blocks/call_stack_diff.ts +75 -0
- package/src/review-api/blocks/callout.ts +24 -0
- package/src/review-api/blocks/code.ts +9 -0
- package/src/review-api/blocks/code_peek.ts +13 -0
- package/src/review-api/blocks/database_lens.ts +150 -0
- package/src/review-api/blocks/definition.ts +40 -0
- package/src/review-api/blocks/divider.ts +6 -0
- package/src/review-api/blocks/flow_diagram.ts +111 -0
- package/src/review-api/blocks/image.ts +10 -0
- package/src/review-api/blocks/index.ts +90 -0
- package/src/review-api/blocks/markdown.ts +17 -0
- package/src/review-api/blocks/section.ts +24 -0
- package/src/review-api/blocks/sequence.ts +56 -0
- package/src/review-api/blocks/software_map.ts +9 -0
- package/src/review-api/blocks/trace_quote.ts +10 -0
- package/src/review-api/blocks/tutorial.ts +39 -0
- package/src/review-api/checkout-fs.ts +14 -0
- package/src/review-api/client.ts +1 -0
- package/src/review-api/comparison-coverage.ts +304 -0
- package/src/review-api/component-reference.ts +30 -0
- package/src/review-api/diff-lenses.ts +175 -0
- package/src/review-api/document-headings.ts +51 -0
- package/src/review-api/document-text.ts +200 -0
- package/src/review-api/document.ts +906 -0
- package/src/review-api/file-lenses.ts +100 -0
- package/src/review-api/http.ts +1877 -0
- package/src/review-api/image-decode.ts +29 -0
- package/src/review-api/input-error.ts +9 -0
- package/src/review-api/instructions.ts +89 -0
- package/src/review-api/lens-alignment.ts +52 -0
- package/src/review-api/local-data.ts +1837 -0
- package/src/review-api/map-input.ts +154 -0
- package/src/review-api/mcp-client-agent.ts +32 -0
- package/src/review-api/mcp.ts +240 -0
- package/src/review-api/origin.ts +43 -0
- package/src/review-api/profile.ts +146 -0
- package/src/review-api/public-tools.ts +102 -0
- package/src/review-api/pull-request.ts +389 -0
- package/src/review-api/read-schemas.ts +85 -0
- package/src/review-api/recovery.ts +2 -0
- package/src/review-api/request-origin.ts +33 -0
- package/src/review-api/review-progress.ts +382 -0
- package/src/review-api/status-tool.ts +10 -0
- package/src/review-api/store-schema.ts +22 -0
- package/src/review-api/store.ts +1647 -0
- package/src/review-api/tool-failure.ts +64 -0
- package/src/review-api/trace-schema.ts +13 -0
- package/src/review-api/traces.ts +119 -0
- package/src/review-api/unsupported-files.integration.ts +91 -0
- package/src/review-api/workspaces.ts +692 -0
- package/src/review-api/worktree-source.ts +170 -0
- package/src/review-api/worktree-structural.integration.ts +320 -0
- package/src/review-app-launcher.ts +432 -0
- package/src/review-app-picker.ts +168 -0
- package/src/review-app.ts +134 -0
- package/src/review-bundled-tools.ts +249 -0
- package/src/review-checkout-paths.ts +37 -0
- package/src/review-diff-files.ts +89 -0
- package/src/review-head-checkout.ts +300 -0
- package/src/review-home-paths.ts +73 -0
- package/src/review-info.ts +59 -0
- package/src/review-instances.ts +113 -0
- package/src/review-logger.ts +183 -0
- package/src/review-preferences.ts +88 -0
- package/src/review-prepare.ts +291 -0
- package/src/review-stack.ts +112 -0
- package/src/review-telemetry.ts +1066 -0
- package/src/runtime.ts +74 -0
- package/src/server/account-alias.ts +30 -0
- package/src/server/bounded-stream.ts +34 -0
- package/src/server/bug-report.ts +301 -0
- package/src/server/client-error-budget.ts +45 -0
- package/src/server/crash-report.ts +273 -0
- package/src/server/desktop-host-shutdown.ts +64 -0
- package/src/server/desktop-host.ts +190 -0
- package/src/server/desktop-server.ts +703 -0
- package/src/server/diffr-config.ts +509 -0
- package/src/server/diffr-languages.ts +97 -0
- package/src/server/global-verb-relay.ts +190 -0
- package/src/server/headless-host.ts +149 -0
- package/src/server/hono-http.ts +162 -0
- package/src/server/http-json.ts +24 -0
- package/src/server/json-review-reporting.ts +174 -0
- package/src/server/process-error-telemetry.ts +174 -0
- package/src/server/review-api-parsers.ts +90 -0
- package/src/server/review-lifecycle-telemetry.ts +73 -0
- package/src/server/review-open-watchdog.ts +46 -0
- package/src/server/review-server-core.ts +290 -0
- package/src/server/structural-comparisons.ts +153 -0
- package/src/server/structural-diff.ts +186 -0
- package/src/server/tutorial-service.ts +241 -0
- package/src/server/ui-telemetry.ts +198 -0
- package/src/server-discovery.ts +95 -0
- package/src/session-markers.ts +132 -0
- package/src/sharing/auth.ts +41 -0
- package/src/sharing/cli.ts +83 -0
- package/src/sharing/client.ts +334 -0
- package/src/sharing/export.ts +195 -0
- package/src/sharing/host.ts +316 -0
- package/src/sharing/import.ts +787 -0
- package/src/sharing/index.ts +16 -0
- package/src/sharing/repository.ts +145 -0
- package/src/sharing/routes.ts +34 -0
- package/src/slug.ts +23 -0
- package/src/software-map-diff-counts.ts +517 -0
- package/src/software-map-model.ts +1147 -0
- package/src/software-map-topology-diff.ts +260 -0
- package/src/source.ts +92 -0
- package/src/startup-trace.ts +232 -0
- package/src/stored-document-migration.ts +151 -0
- package/src/telemetry-clean-text.ts +257 -0
- package/src/telemetry-config.ts +298 -0
- package/src/telemetry-debug-sink.ts +38 -0
- package/src/telemetry.ts +13 -0
- package/src/trace-cli.ts +156 -0
- package/src/trace-storage-cli.ts +509 -0
- package/src/tutorial-conversation.ts +18 -0
- package/src/ui-telemetry-events.ts +765 -0
- package/src/unified-diff.ts +71 -0
- package/src/viewed-coverage.ts +259 -0
- package/src/windows-cli.ts +125 -0
- package/tutorial/document.json +275 -0
- package/tutorial/runtime-manifest.json +12 -0
- package/tutorial/sample-service/package.json +9 -0
- package/tutorial/sample-service/src/api/checkout-api.ts +17 -0
- package/tutorial/sample-service/src/app.ts +26 -0
- package/tutorial/sample-service/src/database/schema.ts +10 -0
- package/tutorial/sample-service/src/fulfillment/fulfillment-queue.ts +15 -0
- package/tutorial/sample-service/src/fulfillment/fulfillment-worker.ts +24 -0
- package/tutorial/sample-service/src/inventory/inventory-service.ts +11 -0
- package/tutorial/sample-service/src/orders/order-service.ts +34 -0
- package/tutorial/sample-service/src/orders/order.ts +21 -0
- package/tutorial/sample-service/src/orders/orders-repository.ts +24 -0
- package/tutorial/sample-service/src/payments/payment-gateway.ts +12 -0
- package/tutorial/sample-service/src/shipping/shipping-gateway.ts +15 -0
- package/tutorial/sample-service/tsconfig.json +12 -0
- package/tutorial/software-map.json +139 -0
- package/tutorial/trace.json +20 -0
|
@@ -0,0 +1,676 @@
|
|
|
1
|
+
import { a as jsonNumber, c as jsonString, o as jsonObject, u as parseJsonText } from "./src-X9phtB2j.js";
|
|
2
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { chmodSync, chownSync, closeSync, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeSync } from "node:fs";
|
|
5
|
+
import { chmod, chown, lstat, mkdir, open, readFile, realpath, rename, rm, stat, utimes, writeFile } from "node:fs/promises";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import os from "node:os";
|
|
8
|
+
import { spawnSync } from "node:child_process";
|
|
9
|
+
import { setTimeout } from "node:timers/promises";
|
|
10
|
+
//#region ../trace-core/src/atomic-write.ts
|
|
11
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
12
|
+
function temporaryPath(filePath) {
|
|
13
|
+
return `${filePath}.${process.pid}.${randomBytes(12).toString("hex")}.tmp`;
|
|
14
|
+
}
|
|
15
|
+
function snapshotContents(contents) {
|
|
16
|
+
return contents instanceof Uint8Array ? Buffer.from(contents) : contents;
|
|
17
|
+
}
|
|
18
|
+
function toBuffer(contents, encoding) {
|
|
19
|
+
return contents instanceof Uint8Array ? contents : Buffer.from(contents, encoding ?? "utf8");
|
|
20
|
+
}
|
|
21
|
+
function canIgnoreMetadataError(error) {
|
|
22
|
+
if (!("code" in error)) return false;
|
|
23
|
+
return error.code === "ENOSYS" || (!process.getuid || process.getuid() !== 0) && (error.code === "EINVAL" || error.code === "EPERM");
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Replace a file through a flushed sibling, preserving an existing symlink's
|
|
27
|
+
* target and metadata. Exceptions clean up temporary files; process termination may leave them.
|
|
28
|
+
*/
|
|
29
|
+
function writeFileAtomic(filePath, contents, encoding, options = {}) {
|
|
30
|
+
const snapshot = snapshotContents(contents);
|
|
31
|
+
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
32
|
+
let target = filePath;
|
|
33
|
+
if (!options.replaceSymlink) try {
|
|
34
|
+
target = realpathSync(filePath);
|
|
35
|
+
} catch {}
|
|
36
|
+
let metadata;
|
|
37
|
+
try {
|
|
38
|
+
const candidate = options.replaceSymlink ? lstatSync(target) : statSync(target);
|
|
39
|
+
if (!candidate.isSymbolicLink()) metadata = candidate;
|
|
40
|
+
} catch {}
|
|
41
|
+
const mode = options.mode || metadata?.mode;
|
|
42
|
+
const tmp = temporaryPath(target);
|
|
43
|
+
let fd;
|
|
44
|
+
let created = false;
|
|
45
|
+
try {
|
|
46
|
+
fd = openSync(tmp, "wx", mode ?? 438);
|
|
47
|
+
created = true;
|
|
48
|
+
options.tmpfileCreated?.(tmp);
|
|
49
|
+
const buffer = toBuffer(snapshot, encoding);
|
|
50
|
+
let offset = 0;
|
|
51
|
+
while (offset < buffer.length) {
|
|
52
|
+
const written = writeSync(fd, buffer, offset, buffer.length - offset);
|
|
53
|
+
if (written === 0) throw new Error("Atomic write made no progress");
|
|
54
|
+
offset += written;
|
|
55
|
+
}
|
|
56
|
+
fsyncSync(fd);
|
|
57
|
+
closeSync(fd);
|
|
58
|
+
fd = void 0;
|
|
59
|
+
if (metadata && process.getuid) try {
|
|
60
|
+
chownSync(tmp, metadata.uid, metadata.gid);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
if (!(error instanceof Error) || !canIgnoreMetadataError(error)) throw error;
|
|
63
|
+
}
|
|
64
|
+
if (mode) try {
|
|
65
|
+
chmodSync(tmp, mode);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
if (!(error instanceof Error) || !canIgnoreMetadataError(error)) throw error;
|
|
68
|
+
}
|
|
69
|
+
renameSync(tmp, target);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
if (fd !== void 0) try {
|
|
72
|
+
closeSync(fd);
|
|
73
|
+
} catch {}
|
|
74
|
+
if (created) try {
|
|
75
|
+
rmSync(tmp, { force: true });
|
|
76
|
+
} catch {}
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async function replaceFile(filePath, contents, options) {
|
|
81
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
82
|
+
const target = options.replaceSymlink ? filePath : await realpath(filePath).catch(() => filePath);
|
|
83
|
+
const candidate = options.replaceSymlink ? await lstat(target).catch(() => void 0) : await stat(target).catch(() => void 0);
|
|
84
|
+
const metadata = candidate?.isSymbolicLink() ? void 0 : candidate;
|
|
85
|
+
const mode = options.mode ?? metadata?.mode;
|
|
86
|
+
const tmp = temporaryPath(target);
|
|
87
|
+
let handle;
|
|
88
|
+
let created = false;
|
|
89
|
+
try {
|
|
90
|
+
handle = await open(tmp, "wx", mode ?? 438);
|
|
91
|
+
created = true;
|
|
92
|
+
await options.tmpfileCreated?.(tmp);
|
|
93
|
+
const buffer = toBuffer(contents, options.encoding);
|
|
94
|
+
let offset = 0;
|
|
95
|
+
while (offset < buffer.length) {
|
|
96
|
+
const { bytesWritten } = await handle.write(buffer, offset, buffer.length - offset);
|
|
97
|
+
if (bytesWritten === 0) throw new Error("Atomic write made no progress");
|
|
98
|
+
offset += bytesWritten;
|
|
99
|
+
}
|
|
100
|
+
await handle.sync();
|
|
101
|
+
await handle.close();
|
|
102
|
+
handle = void 0;
|
|
103
|
+
if (metadata && process.getuid) await chown(tmp, metadata.uid, metadata.gid).catch((error) => {
|
|
104
|
+
if (!(error instanceof Error) || !canIgnoreMetadataError(error)) throw error;
|
|
105
|
+
});
|
|
106
|
+
if (mode) await chmod(tmp, mode).catch((error) => {
|
|
107
|
+
if (!(error instanceof Error) || !canIgnoreMetadataError(error)) throw error;
|
|
108
|
+
});
|
|
109
|
+
await rename(tmp, target);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
await handle?.close().catch(() => void 0);
|
|
112
|
+
if (created) await rm(tmp, { force: true }).catch(() => void 0);
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/** Replace a file atomically, awaiting its callback and earlier writes to the same path. */
|
|
117
|
+
async function writeFileAtomicAsync(filePath, contents, options = {}) {
|
|
118
|
+
const snapshot = snapshotContents(contents);
|
|
119
|
+
const key = path.resolve(filePath);
|
|
120
|
+
const pending = (inFlight.get(key) ?? Promise.resolve()).catch(() => void 0).then(() => replaceFile(filePath, snapshot, options));
|
|
121
|
+
inFlight.set(key, pending);
|
|
122
|
+
try {
|
|
123
|
+
await pending;
|
|
124
|
+
} finally {
|
|
125
|
+
if (inFlight.get(key) === pending) inFlight.delete(key);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Write JSON with owner-only permissions on the file and on a directory
|
|
130
|
+
* this call creates. Login tokens and consent files use it.
|
|
131
|
+
*/
|
|
132
|
+
async function writePrivateJsonAtomic(filePath, value) {
|
|
133
|
+
await mkdir(path.dirname(filePath), {
|
|
134
|
+
recursive: true,
|
|
135
|
+
mode: 448
|
|
136
|
+
});
|
|
137
|
+
await writeFileAtomicAsync(filePath, `${JSON.stringify(value, null, 2)}\n`, {
|
|
138
|
+
encoding: "utf8",
|
|
139
|
+
mode: 384
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
//#endregion
|
|
143
|
+
//#region ../trace-core/src/store-origin.ts
|
|
144
|
+
const LOCAL_HOSTS = /* @__PURE__ */ new Set([
|
|
145
|
+
"localhost",
|
|
146
|
+
"127.0.0.1",
|
|
147
|
+
"[::1]"
|
|
148
|
+
]);
|
|
149
|
+
/**
|
|
150
|
+
* The canonical form of one store origin: `https://host[:port]`, or
|
|
151
|
+
* `http://` for a local development store. Any other URL is an error.
|
|
152
|
+
*/
|
|
153
|
+
function normalizeStoreOrigin(value) {
|
|
154
|
+
let url;
|
|
155
|
+
try {
|
|
156
|
+
url = new URL(value.trim());
|
|
157
|
+
} catch {
|
|
158
|
+
throw new Error(`The trace store origin is not a URL: ${value}`);
|
|
159
|
+
}
|
|
160
|
+
const local = LOCAL_HOSTS.has(url.hostname);
|
|
161
|
+
if (!(url.protocol === "https:") && !(local && url.protocol === "http:")) throw new Error(`The trace store origin must use https (http is allowed for localhost): ${value}`);
|
|
162
|
+
if (url.username || url.password || url.search || url.hash || url.pathname !== "/") throw new Error(`The trace store origin must be a bare origin without a path, query, or login: ${value}`);
|
|
163
|
+
return url.origin;
|
|
164
|
+
}
|
|
165
|
+
//#endregion
|
|
166
|
+
//#region ../trace-core/src/trace-home.ts
|
|
167
|
+
const DEV_REVIEW_HOME_ENV = "DEV_REVIEW_HOME";
|
|
168
|
+
/**
|
|
169
|
+
* The one resolver for the Review home directory. Every reader of
|
|
170
|
+
* DEV_REVIEW_HOME calls this, so an untrimmed or empty value cannot make two
|
|
171
|
+
* modules disagree about the directory. An empty value means "use the
|
|
172
|
+
* default".
|
|
173
|
+
*/
|
|
174
|
+
function devReviewHome(env = process.env, homeDir = os.homedir()) {
|
|
175
|
+
const override = env[DEV_REVIEW_HOME_ENV]?.trim();
|
|
176
|
+
return override ? path.resolve(override) : path.join(homeDir, ".dev");
|
|
177
|
+
}
|
|
178
|
+
//#endregion
|
|
179
|
+
//#region ../trace-core/src/with-file-lock.ts
|
|
180
|
+
const LOCK_OWNER_FILE = "owner.json";
|
|
181
|
+
const DEFAULT_HEARTBEAT_MS = 3e4;
|
|
182
|
+
async function withFileLock(lockPath, options, operation) {
|
|
183
|
+
await mkdir(path.dirname(lockPath), { recursive: true });
|
|
184
|
+
const deadline = Date.now() + options.timeoutMs;
|
|
185
|
+
while (true) try {
|
|
186
|
+
await mkdir(lockPath);
|
|
187
|
+
try {
|
|
188
|
+
await writeFile(path.join(lockPath, LOCK_OWNER_FILE), JSON.stringify({
|
|
189
|
+
pid: process.pid,
|
|
190
|
+
...options.identifyOwner && { started: processStartIdentity(process.pid) }
|
|
191
|
+
}), "utf8");
|
|
192
|
+
} catch (error) {
|
|
193
|
+
await rm(lockPath, {
|
|
194
|
+
recursive: true,
|
|
195
|
+
force: true
|
|
196
|
+
});
|
|
197
|
+
throw error;
|
|
198
|
+
}
|
|
199
|
+
break;
|
|
200
|
+
} catch (error) {
|
|
201
|
+
if (!(error instanceof Error && "code" in error && error.code === "EEXIST")) throw error;
|
|
202
|
+
const lockAge = await stat(lockPath).then((metadata) => Date.now() - metadata.mtimeMs).catch(() => 0);
|
|
203
|
+
const owner = await readLockOwner(lockPath);
|
|
204
|
+
const ownerPid = owner?.pid ?? null;
|
|
205
|
+
const ownerDead = ownerPid !== null && (!processIsAlive(ownerPid) || pidReused(ownerPid, owner?.started));
|
|
206
|
+
const neverOwned = ownerPid === null && lockAge > options.unownedGraceMs;
|
|
207
|
+
const heartbeatSilent = lockAge > options.staleMs;
|
|
208
|
+
if (ownerDead || neverOwned || heartbeatSilent) {
|
|
209
|
+
await rm(lockPath, {
|
|
210
|
+
recursive: true,
|
|
211
|
+
force: true
|
|
212
|
+
});
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (Date.now() >= deadline) return { acquired: false };
|
|
216
|
+
await setTimeout(options.retryMs);
|
|
217
|
+
}
|
|
218
|
+
const heartbeat = startHeartbeat(lockPath, options.heartbeatMs ?? DEFAULT_HEARTBEAT_MS);
|
|
219
|
+
try {
|
|
220
|
+
return {
|
|
221
|
+
acquired: true,
|
|
222
|
+
result: await operation()
|
|
223
|
+
};
|
|
224
|
+
} finally {
|
|
225
|
+
clearInterval(heartbeat);
|
|
226
|
+
await rm(lockPath, {
|
|
227
|
+
recursive: true,
|
|
228
|
+
force: true
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
function processIsAlive(pid) {
|
|
233
|
+
try {
|
|
234
|
+
process.kill(pid, 0);
|
|
235
|
+
return true;
|
|
236
|
+
} catch (error) {
|
|
237
|
+
return error instanceof Error && "code" in error && error.code === "EPERM";
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function startHeartbeat(lockPath, intervalMs) {
|
|
241
|
+
const timer = setInterval(() => {
|
|
242
|
+
const now = /* @__PURE__ */ new Date();
|
|
243
|
+
utimes(lockPath, now, now).catch(() => {});
|
|
244
|
+
}, intervalMs);
|
|
245
|
+
timer.unref?.();
|
|
246
|
+
return timer;
|
|
247
|
+
}
|
|
248
|
+
async function readLockOwner(lockPath) {
|
|
249
|
+
return readFile(path.join(lockPath, LOCK_OWNER_FILE), "utf8").then((contents) => {
|
|
250
|
+
const owner = jsonObject(parseJsonText(contents));
|
|
251
|
+
const pid = jsonNumber(owner?.pid);
|
|
252
|
+
return pid === void 0 ? null : {
|
|
253
|
+
pid,
|
|
254
|
+
started: jsonString(owner?.started)
|
|
255
|
+
};
|
|
256
|
+
}).catch(() => null);
|
|
257
|
+
}
|
|
258
|
+
/** Only a recorded start that differs proves reuse; unknown is the same process. */
|
|
259
|
+
function pidReused(pid, started) {
|
|
260
|
+
if (!started) return false;
|
|
261
|
+
const current = processStartIdentity(pid);
|
|
262
|
+
return current !== null && current !== started;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Fixed for a process's life and unaffected by clock changes: the boot id and
|
|
266
|
+
* start ticks on Linux, the kernel's recorded start elsewhere. Null if unknown.
|
|
267
|
+
*/
|
|
268
|
+
function processStartIdentity(pid) {
|
|
269
|
+
try {
|
|
270
|
+
if (process.platform === "linux") {
|
|
271
|
+
const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
|
|
272
|
+
const ticks = stat.slice(stat.lastIndexOf(")") + 2).split(" ")[19];
|
|
273
|
+
const boot = readFileSync("/proc/sys/kernel/random/boot_id", "utf8");
|
|
274
|
+
return ticks ? `${boot.trim()}:${ticks}` : null;
|
|
275
|
+
}
|
|
276
|
+
const ps = spawnSync("ps", [
|
|
277
|
+
"-o",
|
|
278
|
+
"lstart=",
|
|
279
|
+
"-p",
|
|
280
|
+
String(pid)
|
|
281
|
+
], {
|
|
282
|
+
encoding: "utf8",
|
|
283
|
+
env: {
|
|
284
|
+
...process.env,
|
|
285
|
+
LC_ALL: "C",
|
|
286
|
+
TZ: "UTC"
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
return ps.status === 0 && ps.stdout.trim() || null;
|
|
290
|
+
} catch {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
/** The two stores a machine can name. */
|
|
295
|
+
const traceStoreNameSchema = z.enum(["s3", "hosted"]);
|
|
296
|
+
/** The hosted origin used when a file names none. */
|
|
297
|
+
const DEFAULT_HOSTED_ORIGIN = "https://app.dev.fast";
|
|
298
|
+
const s3CaptureSchema = z.object({
|
|
299
|
+
enabled: z.boolean(),
|
|
300
|
+
autoActivateRepositories: z.boolean(),
|
|
301
|
+
verifiedAt: z.string().optional(),
|
|
302
|
+
error: z.string().optional()
|
|
303
|
+
});
|
|
304
|
+
/** Your own S3-compatible bucket. Complete or rejected; never patched from legacy files. */
|
|
305
|
+
const s3ProfileSchema = z.object({
|
|
306
|
+
endpoint: z.string().min(1),
|
|
307
|
+
bucket: z.string().min(1),
|
|
308
|
+
accessKeyId: z.string().min(1),
|
|
309
|
+
secretAccessKey: z.string().min(1),
|
|
310
|
+
region: z.string().min(1).optional(),
|
|
311
|
+
capture: s3CaptureSchema.optional()
|
|
312
|
+
});
|
|
313
|
+
/** The hosted store in use. The login token lives in the auth file. */
|
|
314
|
+
const hostedStoreSchema = z.object({
|
|
315
|
+
origin: z.string().min(1).optional(),
|
|
316
|
+
/** Machine-level capture switch for the hosted store; consent stays. */
|
|
317
|
+
capture: z.object({ enabled: z.boolean() }).optional()
|
|
318
|
+
});
|
|
319
|
+
/**
|
|
320
|
+
* Hosted-only consent: one repository the user allowed to publish complete
|
|
321
|
+
* session transcripts, and the hosted origins it may publish to. Bucket
|
|
322
|
+
* uploads never read this list.
|
|
323
|
+
*/
|
|
324
|
+
const traceRepositoryEntrySchema = z.object({
|
|
325
|
+
repositoryId: z.number().int().positive(),
|
|
326
|
+
name: z.string().min(1),
|
|
327
|
+
enabledOrigins: z.array(z.string().min(1)).optional(),
|
|
328
|
+
allowedAt: z.string().optional()
|
|
329
|
+
});
|
|
330
|
+
const traceConfigSchema = z.object({
|
|
331
|
+
version: z.literal(2),
|
|
332
|
+
"current-store": traceStoreNameSchema.optional(),
|
|
333
|
+
stores: z.object({
|
|
334
|
+
s3: s3ProfileSchema.optional(),
|
|
335
|
+
hosted: hostedStoreSchema.optional()
|
|
336
|
+
}).optional(),
|
|
337
|
+
repositories: z.array(traceRepositoryEntrySchema).optional()
|
|
338
|
+
});
|
|
339
|
+
/** The empty configuration every writer starts from. */
|
|
340
|
+
function emptyTraceConfig() {
|
|
341
|
+
return { version: 2 };
|
|
342
|
+
}
|
|
343
|
+
function currentStore(config) {
|
|
344
|
+
return config?.["current-store"];
|
|
345
|
+
}
|
|
346
|
+
function s3Store(config) {
|
|
347
|
+
return config?.stores?.s3 ?? null;
|
|
348
|
+
}
|
|
349
|
+
/** The hosted origin in effect: the store entry's, or the default. */
|
|
350
|
+
function hostedOrigin(config) {
|
|
351
|
+
const origin = config?.stores?.hosted?.origin;
|
|
352
|
+
if (!origin) return DEFAULT_HOSTED_ORIGIN;
|
|
353
|
+
try {
|
|
354
|
+
return normalizeStoreOrigin(origin);
|
|
355
|
+
} catch {
|
|
356
|
+
return origin;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
/** Whether hosted capture is switched on; consent alone leaves it on. */
|
|
360
|
+
function hostedCaptureEnabled(config) {
|
|
361
|
+
return config?.stores?.hosted?.capture?.enabled ?? true;
|
|
362
|
+
}
|
|
363
|
+
/** Whether the file names a hosted store at all, explicitly or through consent. */
|
|
364
|
+
function hasHostedStore(config) {
|
|
365
|
+
return config?.stores?.hosted !== void 0 || (config?.repositories?.length ?? 0) > 0;
|
|
366
|
+
}
|
|
367
|
+
/** The origins one consent entry allows; absent means the default origin. */
|
|
368
|
+
function enabledOriginsOf(entry) {
|
|
369
|
+
return entry.enabledOrigins ?? ["https://app.dev.fast"];
|
|
370
|
+
}
|
|
371
|
+
const traceConfigV1Schema = z.object({
|
|
372
|
+
version: z.literal(1),
|
|
373
|
+
repositories: z.array(z.unknown()).optional()
|
|
374
|
+
});
|
|
375
|
+
const v1RepositorySchema = z.object({
|
|
376
|
+
repositoryId: z.number().int().positive(),
|
|
377
|
+
name: z.string().min(1),
|
|
378
|
+
store: z.string().min(1),
|
|
379
|
+
allowedAt: z.string().optional()
|
|
380
|
+
});
|
|
381
|
+
var TraceConfigurationError = class extends Error {
|
|
382
|
+
constructor(message) {
|
|
383
|
+
super(message);
|
|
384
|
+
this.name = "TraceConfigurationError";
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
function traceConfigPath(scope = {}) {
|
|
388
|
+
const devHome = scope.devHome ?? devReviewHome(scope.env ?? process.env, scope.homeDir ?? os.homedir());
|
|
389
|
+
return path.join(devHome, "trace", "config.json");
|
|
390
|
+
}
|
|
391
|
+
function readTraceConfigFile(scope = {}) {
|
|
392
|
+
const filePath = traceConfigPath(scope);
|
|
393
|
+
let text;
|
|
394
|
+
let fingerprint;
|
|
395
|
+
try {
|
|
396
|
+
const stats = statSync(filePath);
|
|
397
|
+
fingerprint = `${stats.size}:${stats.mtimeMs}`;
|
|
398
|
+
text = readFileSync(filePath, "utf8");
|
|
399
|
+
} catch {
|
|
400
|
+
return {
|
|
401
|
+
path: filePath,
|
|
402
|
+
source: "absent",
|
|
403
|
+
config: null,
|
|
404
|
+
extra: {},
|
|
405
|
+
fingerprint: null
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
const malformed = (reason) => ({
|
|
409
|
+
path: filePath,
|
|
410
|
+
source: "absent",
|
|
411
|
+
config: null,
|
|
412
|
+
extra: {},
|
|
413
|
+
fingerprint,
|
|
414
|
+
error: `Trace configuration at ${filePath} is invalid: ${reason}`
|
|
415
|
+
});
|
|
416
|
+
let raw;
|
|
417
|
+
try {
|
|
418
|
+
raw = jsonObject(parseJsonText(text));
|
|
419
|
+
} catch {
|
|
420
|
+
return malformed("not valid JSON.");
|
|
421
|
+
}
|
|
422
|
+
if (!raw) return malformed("expected a JSON object.");
|
|
423
|
+
const v2 = traceConfigSchema.safeParse(raw);
|
|
424
|
+
if (v2.success) {
|
|
425
|
+
if (v2.data.stores?.s3 !== void 0 && v2.data.stores?.hosted !== void 0 && v2.data["current-store"] === void 0) return malformed("both stores are configured; set \"current-store\" to \"s3\" or \"hosted\".");
|
|
426
|
+
return {
|
|
427
|
+
path: filePath,
|
|
428
|
+
source: "v2",
|
|
429
|
+
config: v2.data,
|
|
430
|
+
extra: unknownFields(raw, [
|
|
431
|
+
"version",
|
|
432
|
+
"current-store",
|
|
433
|
+
"stores",
|
|
434
|
+
"repositories"
|
|
435
|
+
]),
|
|
436
|
+
fingerprint
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
const v1 = traceConfigV1Schema.safeParse(raw);
|
|
440
|
+
if (v1.success) return {
|
|
441
|
+
path: filePath,
|
|
442
|
+
source: "v1",
|
|
443
|
+
config: {
|
|
444
|
+
version: 2,
|
|
445
|
+
repositories: (v1.data.repositories ?? []).flatMap((entry) => {
|
|
446
|
+
const parsed = v1RepositorySchema.safeParse(entry);
|
|
447
|
+
if (!parsed.success) return [];
|
|
448
|
+
const { store, ...rest } = parsed.data;
|
|
449
|
+
return [{
|
|
450
|
+
...rest,
|
|
451
|
+
enabledOrigins: [store]
|
|
452
|
+
}];
|
|
453
|
+
})
|
|
454
|
+
},
|
|
455
|
+
extra: unknownFields(raw, ["version", "repositories"]),
|
|
456
|
+
fingerprint
|
|
457
|
+
};
|
|
458
|
+
const issue = v2.error.issues[0];
|
|
459
|
+
const where = issue?.path.length ? ` at ${issue.path.join(".")}` : "";
|
|
460
|
+
return malformed(`${issue?.message ?? "unsupported contents"}${where}.`);
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Replaces the configuration atomically with private permissions. The
|
|
464
|
+
* write is refused when the file changed since it was read, so two
|
|
465
|
+
* concurrent editors cannot silently drop each other's changes.
|
|
466
|
+
*/
|
|
467
|
+
async function writeTraceConfigFile(file, config) {
|
|
468
|
+
if (file.error) throw new TraceConfigurationError(file.error);
|
|
469
|
+
await mkdir(path.dirname(file.path), {
|
|
470
|
+
recursive: true,
|
|
471
|
+
mode: 448
|
|
472
|
+
});
|
|
473
|
+
if (!(await withFileLock(`${path.resolve(file.path)}.lock`, {
|
|
474
|
+
retryMs: 20,
|
|
475
|
+
timeoutMs: 1e4,
|
|
476
|
+
staleMs: 12e4,
|
|
477
|
+
heartbeatMs: 5e3,
|
|
478
|
+
unownedGraceMs: 1e3
|
|
479
|
+
}, async () => {
|
|
480
|
+
if (currentFingerprint(file.path) !== file.fingerprint) throw new TraceConfigurationError(`Trace configuration at ${file.path} changed while it was being updated. Re-run the command.`);
|
|
481
|
+
const document = {
|
|
482
|
+
...file.extra,
|
|
483
|
+
version: 2,
|
|
484
|
+
"current-store": config["current-store"],
|
|
485
|
+
stores: config.stores,
|
|
486
|
+
repositories: config.repositories
|
|
487
|
+
};
|
|
488
|
+
await writePrivateJsonAtomic(file.path, document);
|
|
489
|
+
})).acquired) throw new TraceConfigurationError(`Trace configuration at ${file.path} is busy. Re-run the command after the current update finishes.`);
|
|
490
|
+
}
|
|
491
|
+
function currentFingerprint(filePath) {
|
|
492
|
+
try {
|
|
493
|
+
const stats = statSync(filePath);
|
|
494
|
+
return `${stats.size}:${stats.mtimeMs}`;
|
|
495
|
+
} catch {
|
|
496
|
+
return null;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
function unknownFields(raw, known) {
|
|
500
|
+
return Object.fromEntries(Object.entries(raw).filter(([key]) => !known.includes(key)));
|
|
501
|
+
}
|
|
502
|
+
/** Direct profiles compare on destination, credentials, and capture settings. */
|
|
503
|
+
function sameS3Profile(a, b) {
|
|
504
|
+
return a.endpoint === b.endpoint && a.bucket === b.bucket && a.accessKeyId === b.accessKeyId && a.secretAccessKey === b.secretAccessKey && (a.region ?? "auto") === (b.region ?? "auto") && (a.capture?.enabled ?? false) === (b.capture?.enabled ?? false) && (a.capture?.autoActivateRepositories ?? false) === (b.capture?.autoActivateRepositories ?? false);
|
|
505
|
+
}
|
|
506
|
+
//#endregion
|
|
507
|
+
//#region ../trace-core/src/trace-storage/s3-config.ts
|
|
508
|
+
const S3_DEFAULT_REGION = "auto";
|
|
509
|
+
function traceEnvPath(homeDir = os.homedir(), env = process.env) {
|
|
510
|
+
return env.TRACE_ENV_FILE ?? path.join(homeDir, ".config", "dev-trace", "env");
|
|
511
|
+
}
|
|
512
|
+
function traceSettingsPath(homeDir = os.homedir(), env = process.env) {
|
|
513
|
+
return env.TRACE_SETTINGS_FILE ?? path.join(homeDir, ".config", "dev-trace", "settings.json");
|
|
514
|
+
}
|
|
515
|
+
const envFileCache = /* @__PURE__ */ new Map();
|
|
516
|
+
function clearTraceEnvCache() {
|
|
517
|
+
envFileCache.clear();
|
|
518
|
+
}
|
|
519
|
+
/** Parses one `export NAME=value` file; a missing file reads as empty. */
|
|
520
|
+
function readTraceEnvFile(envPath) {
|
|
521
|
+
const cached = envFileCache.get(envPath);
|
|
522
|
+
if (cached) return cached;
|
|
523
|
+
const values = /* @__PURE__ */ new Map();
|
|
524
|
+
try {
|
|
525
|
+
for (const line of readFileSync(envPath, "utf8").split("\n")) {
|
|
526
|
+
const match = /^\s*(?:export\s+)?([A-Z0-9_]+)=(.*)$/.exec(line);
|
|
527
|
+
if (!match) continue;
|
|
528
|
+
values.set(match[1], unquoteEnvValue(match[2]));
|
|
529
|
+
}
|
|
530
|
+
} catch {}
|
|
531
|
+
envFileCache.set(envPath, values);
|
|
532
|
+
return values;
|
|
533
|
+
}
|
|
534
|
+
function unquoteEnvValue(raw) {
|
|
535
|
+
const trimmed = raw.trim();
|
|
536
|
+
try {
|
|
537
|
+
const decoded = jsonString(parseJsonText(trimmed));
|
|
538
|
+
if (decoded !== void 0) return decoded;
|
|
539
|
+
} catch {}
|
|
540
|
+
return trimmed.replace(/^["']|["']$/g, "").trim();
|
|
541
|
+
}
|
|
542
|
+
function traceEnvValue(name, scope = {}) {
|
|
543
|
+
const env = scope.env ?? process.env;
|
|
544
|
+
return env[name] ?? readTraceEnvFile(traceEnvPath(scope.homeDir, env)).get(name);
|
|
545
|
+
}
|
|
546
|
+
const S3_FIELDS = [
|
|
547
|
+
"endpoint",
|
|
548
|
+
"bucket",
|
|
549
|
+
"accessKeyId",
|
|
550
|
+
"secretAccessKey",
|
|
551
|
+
"region"
|
|
552
|
+
];
|
|
553
|
+
/** The variables that supply each field, in precedence order. */
|
|
554
|
+
const S3_FIELD_VARIABLES = {
|
|
555
|
+
endpoint: ["TRACE_R2_ENDPOINT"],
|
|
556
|
+
bucket: ["TRACE_R2_BUCKET"],
|
|
557
|
+
accessKeyId: ["TRACE_R2_ACCESS_KEY_ID", "AWS_ACCESS_KEY_ID"],
|
|
558
|
+
secretAccessKey: ["TRACE_R2_SECRET_ACCESS_KEY", "AWS_SECRET_ACCESS_KEY"],
|
|
559
|
+
region: ["TRACE_R2_REGION"]
|
|
560
|
+
};
|
|
561
|
+
/**
|
|
562
|
+
* Resolves the direct bucket setup. A complete version-2 profile is the
|
|
563
|
+
* base; otherwise the legacy env file is. Process environment variables
|
|
564
|
+
* override either. An incomplete profile is a configuration error, never
|
|
565
|
+
* patched from the legacy file. `ignoreProfile` reads the legacy inputs
|
|
566
|
+
* only, which migration needs to describe what it would persist.
|
|
567
|
+
*/
|
|
568
|
+
function resolveS3Setup(scope = {}) {
|
|
569
|
+
const env = scope.env ?? process.env;
|
|
570
|
+
const envPath = traceEnvPath(scope.homeDir, env);
|
|
571
|
+
const configFile = readTraceConfigFile(scope);
|
|
572
|
+
if (configFile.error) throw new TraceConfigurationError(configFile.error);
|
|
573
|
+
const configPath = configFile.path;
|
|
574
|
+
const profile = scope.ignoreProfile ? null : s3Store(configFile.config);
|
|
575
|
+
const legacy = readTraceEnvFile(envPath);
|
|
576
|
+
const overrides = [];
|
|
577
|
+
const resolved = {
|
|
578
|
+
endpoint: void 0,
|
|
579
|
+
bucket: void 0,
|
|
580
|
+
accessKeyId: void 0,
|
|
581
|
+
secretAccessKey: void 0,
|
|
582
|
+
region: void 0
|
|
583
|
+
};
|
|
584
|
+
let source = profile ? "profile" : "none";
|
|
585
|
+
for (const field of S3_FIELDS) for (const name of S3_FIELD_VARIABLES[field]) {
|
|
586
|
+
if (env[name] !== void 0) {
|
|
587
|
+
overrides.push(name);
|
|
588
|
+
resolved[field] = env[name];
|
|
589
|
+
break;
|
|
590
|
+
}
|
|
591
|
+
if (profile && profile[field] !== void 0) {
|
|
592
|
+
resolved[field] = profile[field];
|
|
593
|
+
break;
|
|
594
|
+
}
|
|
595
|
+
if (legacy.has(name)) {
|
|
596
|
+
resolved[field] = legacy.get(name);
|
|
597
|
+
if (source === "none") source = "legacy-file";
|
|
598
|
+
break;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
if (source === "none" && overrides.length > 0) source = "process-env";
|
|
602
|
+
const { endpoint, bucket, accessKeyId, secretAccessKey } = resolved;
|
|
603
|
+
const credentials = endpoint && bucket && accessKeyId && secretAccessKey ? {
|
|
604
|
+
endpoint,
|
|
605
|
+
bucket,
|
|
606
|
+
accessKeyId,
|
|
607
|
+
secretAccessKey,
|
|
608
|
+
region: resolved.region ?? "auto"
|
|
609
|
+
} : null;
|
|
610
|
+
return {
|
|
611
|
+
credentials,
|
|
612
|
+
source: credentials ? source : "none",
|
|
613
|
+
overrides,
|
|
614
|
+
envPath,
|
|
615
|
+
configPath,
|
|
616
|
+
profile
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
function resolveS3Credentials(scope = {}) {
|
|
620
|
+
return resolveS3Setup(scope).credentials;
|
|
621
|
+
}
|
|
622
|
+
/** The message a machine with no trace configuration reads, in either CLI. */
|
|
623
|
+
function noTraceConfigurationMessage() {
|
|
624
|
+
return `No trace configuration found. Run \`whiteboard trace allow .\` to configure trace capture.`;
|
|
625
|
+
}
|
|
626
|
+
/** Describes the resolved direct-bucket setup without testing the network. */
|
|
627
|
+
function describeS3Setup(env = process.env) {
|
|
628
|
+
const setup = resolveS3Setup({ env });
|
|
629
|
+
const envPath = setup.source === "profile" ? setup.configPath : setup.envPath;
|
|
630
|
+
if (isS3MockMode(env)) return {
|
|
631
|
+
envPath,
|
|
632
|
+
config: {
|
|
633
|
+
endpoint: "mock://endpoint",
|
|
634
|
+
bucket: "mock-bucket",
|
|
635
|
+
accessKeyId: "mock-key"
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
const config = setup.credentials;
|
|
639
|
+
if (!config) return {
|
|
640
|
+
envPath,
|
|
641
|
+
error: setup.profile !== null || existsSync(setup.envPath) || Boolean(env.TRACE_R2_BUCKET) ? "Configuration is missing one or more required S3/R2 values." : noTraceConfigurationMessage()
|
|
642
|
+
};
|
|
643
|
+
return {
|
|
644
|
+
envPath,
|
|
645
|
+
config: {
|
|
646
|
+
endpoint: config.endpoint,
|
|
647
|
+
bucket: config.bucket,
|
|
648
|
+
accessKeyId: config.accessKeyId
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
/** The bucket test double: object keys become files under this directory. */
|
|
653
|
+
function s3MockRoot(env = process.env) {
|
|
654
|
+
if (env.TRACE_R2_MODE !== "mock") return null;
|
|
655
|
+
return env.TRACE_R2_MOCK_DIR || null;
|
|
656
|
+
}
|
|
657
|
+
function isS3MockMode(env = process.env) {
|
|
658
|
+
return env.TRACE_R2_MODE === "mock";
|
|
659
|
+
}
|
|
660
|
+
/** Secret-free identity of the destination; the same bucket keys the same cache. */
|
|
661
|
+
function s3CacheIdentity(config, mockRoot) {
|
|
662
|
+
if (!config) return `s3:mock:${mockRoot ?? ""}`;
|
|
663
|
+
return `s3:${createHash("sha256").update(`${normalizeS3Endpoint(config.endpoint)}\n${config.bucket}`).digest("hex").slice(0, 16)}`;
|
|
664
|
+
}
|
|
665
|
+
function normalizeS3Endpoint(endpoint) {
|
|
666
|
+
try {
|
|
667
|
+
const url = new URL(endpoint);
|
|
668
|
+
url.hash = "";
|
|
669
|
+
url.search = "";
|
|
670
|
+
return url.toString().replace(/\/+$/, "").toLowerCase();
|
|
671
|
+
} catch {
|
|
672
|
+
return endpoint.trim().replace(/\/+$/, "").toLowerCase();
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
//#endregion
|
|
676
|
+
export { devReviewHome as A, s3ProfileSchema as C, processIsAlive as D, writeTraceConfigFile as E, writeFileAtomic as M, writeFileAtomicAsync as N, withFileLock as O, writePrivateJsonAtomic as P, readTraceConfigFile as S, sameS3Profile as T, emptyTraceConfig as _, noTraceConfigurationMessage as a, hostedCaptureEnabled as b, resolveS3Setup as c, traceEnvPath as d, traceEnvValue as f, currentStore as g, TraceConfigurationError as h, isS3MockMode as i, normalizeStoreOrigin as j, DEV_REVIEW_HOME_ENV as k, s3CacheIdentity as l, DEFAULT_HOSTED_ORIGIN as m, clearTraceEnvCache as n, readTraceEnvFile as o, traceSettingsPath as p, describeS3Setup as r, resolveS3Credentials as s, S3_DEFAULT_REGION as t, s3MockRoot as u, enabledOriginsOf as v, s3Store as w, hostedOrigin as x, hasHostedStore as y };
|