@dev.fast/whiteboard 0.2.5-preview.20261013.112 → 0.2.5-preview.20261015.115
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/THIRD_PARTY_NOTICES.md +29 -0
- package/dist/{account-alias-D0hF0sKk.js → account-alias-ChZR3RIU.js} +94 -8
- package/dist/{agent-cli-BBPxwUOd.js → agent-cli-CR2PnA9G.js} +9 -12
- package/dist/agent-cli-CRPPmS3l.js +2 -0
- package/dist/{agent-client-P3J4y8Zn.js → agent-client-DVd-prfp.js} +31 -14
- package/dist/{authoring-tools-D8WXsm_g.js → authoring-tools-BXKEwhfi.js} +5 -5
- package/dist/background-server-Cw7CcfaZ.js +156 -0
- package/dist/background-server-D2Rrr_e2.js +2 -0
- package/dist/build-info.json +1 -1
- package/dist/{cli-C0ziP6hg.js → cli-CSrz_wGk.js} +1 -1
- package/dist/{cli-runner-C1gY-HAI.js → cli-runner-PlGqBeMw.js} +368 -252
- package/dist/cli.js +3 -2
- package/dist/{client-DzcSt6pV.js → client-DhZzPePs.js} +5 -3
- package/dist/{process-error-telemetry-C0swByfp.js → headless-host-CVXTSIlx.js} +3285 -2585
- package/dist/headless-host-Cp37C-v0.js +2 -0
- package/dist/input-error-OLgB_h31.js +11 -0
- package/dist/{local-data-BlERtVm2.js → local-data-yfLp5PiJ.js} +237 -157
- package/dist/{mcp-t7VN9R3k.js → mcp-DBbDwG4y.js} +4 -4
- package/dist/{package-paths-B9PDoAl6.js → package-paths-BsuI4WUf.js} +1 -1
- package/dist/profile-BSOkA6A-.js +2 -0
- package/dist/{profile-BsYDBr7t.js → profile-pAzg7fxC.js} +3 -3
- package/dist/remote-attach-BAt2eZx3.js +137 -0
- package/dist/remote-extensions-2Cxf3hMo.js +2 -0
- package/dist/remote-extensions-BiNx6NQK.js +323 -0
- package/dist/remote-language-server-DQgWUNeV.js +252 -0
- package/dist/remote-uninstall-DLtDG0o7.js +153 -0
- package/dist/{request-origin-DA4K1qWY.js → request-origin-DaRIV3cB.js} +5 -2
- package/dist/review-app-launcher-D7I4DGoH.js +222 -0
- package/dist/{review-home-paths-Y9mYWkCI.js → review-home-paths-zwevIq4I.js} +7 -7
- package/dist/{review-telemetry-CmM6tCbj.js → review-telemetry-BqB4CiTD.js} +5 -5
- package/dist/{s3-SzLpeCG6.js → s3-77-qkEhs.js} +1 -1
- package/dist/{s3-config-BgxkSoOy.js → s3-config-0xuCh-F_.js} +1 -1
- package/dist/{s3-config-CAxhO9u_.js → s3-config-DDWWMh5x.js} +5 -1
- package/dist/server/desktop-host.js +1663 -266
- package/dist/{desktop-discovery-Ba3dD5Cn.js → server-discovery-CifQ3yQS.js} +88 -3
- package/dist/sharing/index.d.ts +88 -32
- package/dist/sharing/index.js +1 -1
- package/dist/{src-DiHP11l3.js → src-BMPuxWv4.js} +6 -11
- package/dist/{src-WCbabvdS.js → src-Bgvf1sEN.js} +128 -8
- package/dist/{stored-document-migration-u4L_1VZa.js → stored-document-migration-CFOvjRwW.js} +5 -18
- package/dist/{tool-failure-CJHBCHMh.js → tool-failure-BXWzQG8T.js} +2 -2
- package/instructions/authoring.md +16 -12
- package/instructions/file-lenses.md +4 -2
- package/package.json +9 -14
- package/src/agent-connect.ts +507 -0
- package/src/agent-selection.ts +4 -0
- package/src/ask/agents.ts +59 -13
- package/src/ask/pi-mcp.ts +7 -3
- package/src/ask/thread-state.ts +9 -4
- package/src/ask/thread.ts +88 -50
- package/src/ask/threads.ts +53 -7
- package/src/cli-install.ts +3 -1
- package/src/cli-runner.ts +472 -81
- package/src/cli.ts +1 -1
- package/src/connect-agents.ts +285 -0
- package/src/connect-prompts.ts +60 -6
- package/src/desktop-discovery.ts +24 -0
- package/src/pinned-download.ts +118 -0
- package/src/remote-attach.ts +130 -0
- package/src/remote-extensions.ts +359 -0
- package/src/remote-language-server.ts +428 -0
- package/src/remote-toolchains.ts +107 -0
- package/src/remote-uninstall.ts +280 -0
- package/src/review-api/README.md +1 -3
- package/src/review-api/agent-cli.ts +4 -4
- package/src/review-api/agent-client.ts +23 -0
- package/src/review-api/ask-history.ts +31 -16
- package/src/review-api/authoring-tools.ts +1 -1
- package/src/review-api/blocks/index.ts +2 -4
- package/src/review-api/document.ts +1 -4
- package/src/review-api/github-provider.ts +154 -0
- package/src/review-api/http.ts +265 -237
- package/src/review-api/local-data.ts +34 -17
- package/src/review-api/onboarding.ts +478 -0
- package/src/review-api/origin.ts +10 -6
- package/src/review-api/provider.ts +44 -0
- package/src/review-api/pull-request.ts +15 -146
- package/src/review-api/recent-folders.ts +173 -0
- package/src/review-api/store.ts +56 -10
- package/src/review-api/workspaces.ts +56 -11
- package/src/review-app-launcher.ts +61 -3
- package/src/review-stack.ts +162 -36
- package/src/server/background-server.ts +250 -0
- package/src/server/desktop-host-shutdown.ts +24 -0
- package/src/server/desktop-host.ts +39 -24
- package/src/server/desktop-server.ts +68 -118
- package/src/server/global-verb-relay.ts +16 -0
- package/src/server/headless-host.ts +128 -3
- package/src/server/hono-http.ts +6 -1
- package/src/server/legacy-tutorial-cleanup.ts +38 -0
- package/src/server/review-gateway-hosts.ts +379 -0
- package/src/server/review-gateway-list.ts +109 -0
- package/src/server/review-gateway-memory.ts +130 -0
- package/src/server/review-gateway-pushes.ts +142 -0
- package/src/server/review-gateway-streams.ts +415 -0
- package/src/server/review-gateway-transport.ts +204 -0
- package/src/server/review-gateway.ts +433 -0
- package/src/server/review-server-core.ts +36 -6
- package/src/server-discovery.ts +11 -0
- package/src/sharing/import.ts +1 -0
- package/src/ui-telemetry-events.ts +5 -1
- package/src/vsix.ts +122 -0
- package/dist/agent-cli-YSuhE5X8.js +0 -2
- package/dist/headless-host-BHD_Vg5F.js +0 -95
- package/dist/profile-j97aRTS7.js +0 -2
- package/dist/server-discovery-ZVi8Ay8J.js +0 -66
- package/src/fixtures/blocks/tutorial.json +0 -14
- package/src/review-api/blocks/tutorial.ts +0 -23
- package/src/server/tutorial-service.ts +0 -217
- package/tutorial/document.json +0 -216
- package/tutorial/lenses.json +0 -10
- package/tutorial/runtime-manifest.json +0 -11
- package/tutorial/sample-service/package.json +0 -9
- package/tutorial/sample-service/src/api/checkout-api.ts +0 -17
- package/tutorial/sample-service/src/app.ts +0 -26
- package/tutorial/sample-service/src/database/drizzle-orm.d.ts +0 -16
- package/tutorial/sample-service/src/database/schema.ts +0 -9
- package/tutorial/sample-service/src/fulfillment/fulfillment-queue.ts +0 -15
- package/tutorial/sample-service/src/fulfillment/fulfillment-worker.ts +0 -24
- package/tutorial/sample-service/src/inventory/forklift-mood.ts +0 -4
- package/tutorial/sample-service/src/inventory/inventory-service.ts +0 -28
- package/tutorial/sample-service/src/inventory/warehouse-lore.ts +0 -11
- package/tutorial/sample-service/src/orders/order-service.ts +0 -35
- package/tutorial/sample-service/src/orders/order.ts +0 -43
- package/tutorial/sample-service/src/orders/orders-repository.ts +0 -24
- package/tutorial/sample-service/src/payments/fraud-vibes.ts +0 -4
- package/tutorial/sample-service/src/payments/payment-gateway.ts +0 -29
- package/tutorial/sample-service/src/payments/raccoon-ledger.ts +0 -9
- package/tutorial/sample-service/src/shipping/shipping-gateway.ts +0 -15
- package/tutorial/sample-service/tsconfig.json +0 -12
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { n as REVIEW_STATUS_TOOL, t as authoringTools } from "./authoring-tools-
|
|
4
|
-
import { i as publicTool, n as RECOVERY, r as callPublicTool, t as toolFailure } from "./tool-failure-
|
|
1
|
+
import { a as toolResultText, s as ReviewApiError } from "./agent-client-DVd-prfp.js";
|
|
2
|
+
import { t as ReviewInputError } from "./input-error-OLgB_h31.js";
|
|
3
|
+
import { n as REVIEW_STATUS_TOOL, t as authoringTools } from "./authoring-tools-BXKEwhfi.js";
|
|
4
|
+
import { i as publicTool, n as RECOVERY, r as callPublicTool, t as toolFailure } from "./tool-failure-BXWzQG8T.js";
|
|
5
5
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
6
6
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7
7
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { c as jsonString, o as jsonObject, u as parseJsonText } from "./src-X9phtB2j.js";
|
|
2
|
-
import "./review-home-paths-
|
|
2
|
+
import "./review-home-paths-zwevIq4I.js";
|
|
3
3
|
import { readFileSync } from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { o as initializeReviewStoreSchema, t as openLocalReviewStore } from "./local-data-
|
|
2
|
-
import "./review-home-paths-
|
|
3
|
-
import {
|
|
1
|
+
import { o as initializeReviewStoreSchema, t as openLocalReviewStore } from "./local-data-yfLp5PiJ.js";
|
|
2
|
+
import "./review-home-paths-zwevIq4I.js";
|
|
3
|
+
import { A as withFileLock } from "./s3-config-DDWWMh5x.js";
|
|
4
4
|
import { access, mkdir } from "node:fs/promises";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { DatabaseSync } from "node:sqlite";
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { i as readReviewPackageVersion } from "./package-paths-BsuI4WUf.js";
|
|
2
|
+
import { a as readReviewServerHealth, l as serverNotReady } from "./server-discovery-CifQ3yQS.js";
|
|
3
|
+
import { c as stopBackgroundServer, o as recordedBackgroundServer, r as ensureBackgroundServer } from "./background-server-Cw7CcfaZ.js";
|
|
4
|
+
import { t as ensureRemoteLanguageServer } from "./remote-language-server-DQgWUNeV.js";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { spawn } from "node:child_process";
|
|
7
|
+
import { gt, valid } from "semver";
|
|
8
|
+
//#region src/remote-toolchains.ts
|
|
9
|
+
const PROBE_TIMEOUT_MS = 1e4;
|
|
10
|
+
const TOOLCHAINS = {
|
|
11
|
+
rust: [["cargo", "--version"], ["rustc", "--version"]],
|
|
12
|
+
swift: [["swift", "--version"]],
|
|
13
|
+
csharp: [["dotnet", "--version"]],
|
|
14
|
+
go: [["go", "version"]]
|
|
15
|
+
};
|
|
16
|
+
function probe(command, env, timeoutMs) {
|
|
17
|
+
const shell = env.SHELL || "/bin/sh";
|
|
18
|
+
const flags = ["csh", "tcsh"].includes(path.basename(shell)) ? ["-ic"] : [
|
|
19
|
+
"-i",
|
|
20
|
+
"-l",
|
|
21
|
+
"-c"
|
|
22
|
+
];
|
|
23
|
+
return new Promise((resolve) => {
|
|
24
|
+
const child = spawn(shell, [...flags, command.join(" ")], {
|
|
25
|
+
env,
|
|
26
|
+
detached: true,
|
|
27
|
+
stdio: "ignore"
|
|
28
|
+
});
|
|
29
|
+
const timer = setTimeout(() => {
|
|
30
|
+
child.removeAllListeners("exit");
|
|
31
|
+
try {
|
|
32
|
+
process.kill(-child.pid, "SIGKILL");
|
|
33
|
+
} catch {}
|
|
34
|
+
resolve(`${command.join(" ")} did not answer within ${timeoutMs / 1e3} s`);
|
|
35
|
+
}, timeoutMs);
|
|
36
|
+
child.once("error", (error) => {
|
|
37
|
+
clearTimeout(timer);
|
|
38
|
+
resolve(`the login shell ${shell} did not run: ${error.message}`);
|
|
39
|
+
});
|
|
40
|
+
child.once("exit", (code) => {
|
|
41
|
+
clearTimeout(timer);
|
|
42
|
+
resolve(code === 0 ? "found" : code === 127 ? "missing" : `${command.join(" ")} failed with exit code ${code ?? "none"}`);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
async function missingToolchains(groups, env, { timeoutMs = PROBE_TIMEOUT_MS, toolchains = TOOLCHAINS } = {}) {
|
|
47
|
+
const entries = await Promise.all(groups.map(async (group) => {
|
|
48
|
+
const commands = Object.hasOwn(toolchains, group) ? toolchains[group] : [];
|
|
49
|
+
const outcomes = await Promise.all(commands.map((command) => probe(command, env, timeoutMs)));
|
|
50
|
+
const missing = commands.filter((_, i) => outcomes[i] === "missing").map(([tool]) => tool);
|
|
51
|
+
const failures = outcomes.filter((outcome) => outcome !== "found" && outcome !== "missing");
|
|
52
|
+
const details = [...missing.length > 0 ? [`${missing.join(" and ")} ${missing.length > 1 ? "were" : "was"} not found on the login shell's PATH`] : [], ...failures];
|
|
53
|
+
return details.length > 0 ? [[group, details.join("; ")]] : [];
|
|
54
|
+
}));
|
|
55
|
+
return new Map(entries.flat());
|
|
56
|
+
}
|
|
57
|
+
//#endregion
|
|
58
|
+
//#region src/remote-attach.ts
|
|
59
|
+
async function remoteAttach(input) {
|
|
60
|
+
const extensions = new AbortController();
|
|
61
|
+
const language = ensureRemoteLanguageServer({
|
|
62
|
+
env: input.env,
|
|
63
|
+
packageRoot: input.packageRoot,
|
|
64
|
+
groups: input.groups,
|
|
65
|
+
signal: extensions.signal,
|
|
66
|
+
ensure: input.ensureExtensions,
|
|
67
|
+
installTimeoutMs: input.installTimeoutMs,
|
|
68
|
+
cli: input.cli
|
|
69
|
+
});
|
|
70
|
+
const toolchains = missingToolchains(input.groups ?? [], input.env);
|
|
71
|
+
let server;
|
|
72
|
+
let previousVersion;
|
|
73
|
+
let incompatibleRunning;
|
|
74
|
+
try {
|
|
75
|
+
const version = input.version ?? readReviewPackageVersion(import.meta.url);
|
|
76
|
+
const other = input.replace ? await otherVersionRunning(input.stateDir, version) : void 0;
|
|
77
|
+
if (other && (other.startedBy === "user" || newer(other.version, version))) incompatibleRunning = other;
|
|
78
|
+
else if (other) {
|
|
79
|
+
await stopBackgroundServer({ serverPid: other.pid });
|
|
80
|
+
previousVersion = other.version;
|
|
81
|
+
}
|
|
82
|
+
server = await ensureBackgroundServer({
|
|
83
|
+
stateDir: input.stateDir,
|
|
84
|
+
env: input.env,
|
|
85
|
+
startedBy: "desktop",
|
|
86
|
+
cli: input.cli
|
|
87
|
+
});
|
|
88
|
+
} catch (error) {
|
|
89
|
+
extensions.abort();
|
|
90
|
+
await Promise.all([language, toolchains]);
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
const { discovery, started } = server;
|
|
94
|
+
const { languageServer, languageServerDetail, languageServerPending, languageGroups } = await language;
|
|
95
|
+
const missing = await toolchains;
|
|
96
|
+
const health = await readReviewServerHealth(discovery);
|
|
97
|
+
if (!health) throw serverNotReady(input.stateDir);
|
|
98
|
+
return {
|
|
99
|
+
event: "remote.attach",
|
|
100
|
+
version: health.version ?? null,
|
|
101
|
+
commit: health.commit ?? null,
|
|
102
|
+
serverId: health.serverId ?? null,
|
|
103
|
+
url: discovery.url,
|
|
104
|
+
token: discovery.token,
|
|
105
|
+
startedServer: started,
|
|
106
|
+
languageServer,
|
|
107
|
+
...languageServerDetail !== void 0 && { languageServerDetail },
|
|
108
|
+
...languageServerPending && { languageServerPending },
|
|
109
|
+
languageGroups: languageGroups.map(({ group, installed, detail }) => {
|
|
110
|
+
const details = [detail, missing.get(group)].filter(Boolean).join("; ");
|
|
111
|
+
return {
|
|
112
|
+
group,
|
|
113
|
+
installed,
|
|
114
|
+
...details && { detail: details }
|
|
115
|
+
};
|
|
116
|
+
}),
|
|
117
|
+
...previousVersion !== void 0 && {
|
|
118
|
+
replaced: true,
|
|
119
|
+
previousVersion
|
|
120
|
+
},
|
|
121
|
+
...incompatibleRunning && { incompatibleRunning }
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const newer = (running, own) => valid(running) !== null && valid(own) !== null && gt(running, own);
|
|
125
|
+
async function otherVersionRunning(stateDir, version) {
|
|
126
|
+
const recorded = await recordedBackgroundServer(stateDir);
|
|
127
|
+
if (!recorded) return void 0;
|
|
128
|
+
const { discovery, health } = recorded;
|
|
129
|
+
const running = health.version ?? "unknown";
|
|
130
|
+
return running === version ? void 0 : {
|
|
131
|
+
version: running,
|
|
132
|
+
pid: discovery.serverPid,
|
|
133
|
+
startedBy: discovery.startedBy
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
//#endregion
|
|
137
|
+
export { remoteAttach };
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { u as parseJsonText } from "./src-X9phtB2j.js";
|
|
2
|
+
import "./review-home-paths-zwevIq4I.js";
|
|
3
|
+
import { M as devReviewHome } from "./s3-config-DDWWMh5x.js";
|
|
4
|
+
import { t as findReviewPackageRoot } from "./package-paths-BsuI4WUf.js";
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { createReadStream, createWriteStream } from "node:fs";
|
|
8
|
+
import { chmod, mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { execFile } from "node:child_process";
|
|
11
|
+
import { promisify } from "node:util";
|
|
12
|
+
import { Readable } from "node:stream";
|
|
13
|
+
import { pipeline } from "node:stream/promises";
|
|
14
|
+
import yauzl from "yauzl";
|
|
15
|
+
//#region src/pinned-download.ts
|
|
16
|
+
const DOWNLOAD_TIMEOUT_MS = 12e4;
|
|
17
|
+
/** Streams `url` into `file` through a temporary file and returns its sha256. */
|
|
18
|
+
async function downloadFile(url, file, { size, timeoutMs = DOWNLOAD_TIMEOUT_MS, signal } = {}) {
|
|
19
|
+
const { host } = new URL(url);
|
|
20
|
+
const partial = `${file}.${process.pid}.part`;
|
|
21
|
+
let response;
|
|
22
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
23
|
+
try {
|
|
24
|
+
response = await fetch(url, { signal: AbortSignal.any([AbortSignal.timeout(timeoutMs), ...signal ? [signal] : []]) });
|
|
25
|
+
} catch (error) {
|
|
26
|
+
throw new Error(`Network error reaching ${host}: ${error instanceof Error ? networkCause(error) : String(error)}`);
|
|
27
|
+
}
|
|
28
|
+
if (!response.ok || !response.body) throw new Error(`${host} answered ${response.status} for ${url}`);
|
|
29
|
+
const hash = createHash("sha256");
|
|
30
|
+
let received = 0;
|
|
31
|
+
const tooLarge = /* @__PURE__ */ new Error(`${url} is larger than its pinned ${size} bytes`);
|
|
32
|
+
try {
|
|
33
|
+
await pipeline(Readable.fromWeb(response.body), async function* (chunks) {
|
|
34
|
+
for await (const chunk of chunks) {
|
|
35
|
+
received += chunk.length;
|
|
36
|
+
if (size !== void 0 && received > size) throw tooLarge;
|
|
37
|
+
hash.update(chunk);
|
|
38
|
+
yield chunk;
|
|
39
|
+
}
|
|
40
|
+
}, createWriteStream(partial));
|
|
41
|
+
await rename(partial, file);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
await rm(partial, { force: true });
|
|
44
|
+
if (error === tooLarge) throw error;
|
|
45
|
+
throw new Error(`Network error downloading from ${host}: ${error instanceof Error ? networkCause(error) : String(error)}`);
|
|
46
|
+
}
|
|
47
|
+
return hash.digest("hex");
|
|
48
|
+
}
|
|
49
|
+
/** Leaves a matching `file` in place; otherwise downloads it and checks the pin. */
|
|
50
|
+
async function downloadPinned(pin, file, options = {}) {
|
|
51
|
+
if (await sha256File(file).catch(() => void 0) === pin.sha256) return;
|
|
52
|
+
const actual = await downloadFile(pin.url, file, {
|
|
53
|
+
...options,
|
|
54
|
+
size: pin.size
|
|
55
|
+
});
|
|
56
|
+
if (actual !== pin.sha256) {
|
|
57
|
+
await rm(file, { force: true });
|
|
58
|
+
throw new Error(`Checksum mismatch for ${pin.url}: expected ${pin.sha256}, got ${actual}. The download was deleted.`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async function sha256File(file) {
|
|
62
|
+
const hash = createHash("sha256");
|
|
63
|
+
for await (const chunk of createReadStream(file)) hash.update(chunk);
|
|
64
|
+
return hash.digest("hex");
|
|
65
|
+
}
|
|
66
|
+
const networkCauseSchema = z.object({ code: z.string() });
|
|
67
|
+
const networkCause = (error) => error.name === "TimeoutError" ? "timed out" : networkCauseSchema.safeParse(error.cause).data?.code ?? error.message;
|
|
68
|
+
//#endregion
|
|
69
|
+
//#region src/vsix.ts
|
|
70
|
+
const manifestSchema = z.looseObject({
|
|
71
|
+
publisher: z.string(),
|
|
72
|
+
name: z.string(),
|
|
73
|
+
engines: z.looseObject({ vscode: z.string().optional() }).optional()
|
|
74
|
+
});
|
|
75
|
+
/** Unpacks a VSIX's `extension/` payload into `destination`. */
|
|
76
|
+
async function extractVsix(vsix, destination, maxBytes = Number.POSITIVE_INFINITY) {
|
|
77
|
+
let unpacked = 0;
|
|
78
|
+
const zip = await new Promise((resolve, reject) => yauzl.open(vsix, { lazyEntries: true }, (error, opened) => error ? reject(error) : resolve(opened)));
|
|
79
|
+
const openReadStream = (entry) => new Promise((resolve, reject) => zip.openReadStream(entry, (error, stream) => error ? reject(error) : resolve(stream)));
|
|
80
|
+
try {
|
|
81
|
+
await new Promise((resolve, reject) => {
|
|
82
|
+
zip.on("error", reject);
|
|
83
|
+
zip.on("end", () => resolve());
|
|
84
|
+
zip.on("entry", (entry) => {
|
|
85
|
+
(async () => {
|
|
86
|
+
const name = entry.fileName.replaceAll("\\", "/");
|
|
87
|
+
if (name.endsWith("/") || !name.startsWith("extension/")) return;
|
|
88
|
+
const relative = name.slice(10);
|
|
89
|
+
const parts = relative.split("/");
|
|
90
|
+
const mode = entry.externalFileAttributes >>> 16 & 65535;
|
|
91
|
+
if (parts.some((part) => part === ".." || part === "." || part === "" || part.includes(":")) || path.posix.isAbsolute(relative)) throw new Error(`VSIX contains an unsafe path: ${name}`);
|
|
92
|
+
if ((mode & 61440) === 40960) throw new Error(`VSIX contains a symlink: ${name}`);
|
|
93
|
+
unpacked += entry.uncompressedSize;
|
|
94
|
+
if (unpacked > maxBytes) throw new Error(`VSIX unpacks to more than ${maxBytes} bytes`);
|
|
95
|
+
const output = path.join(destination, ...parts);
|
|
96
|
+
await mkdir(path.dirname(output), { recursive: true });
|
|
97
|
+
await pipeline(await openReadStream(entry), createWriteStream(output));
|
|
98
|
+
if (process.platform !== "win32" && mode & 73) await chmod(output, mode & 511);
|
|
99
|
+
})().then(() => zip.readEntry(), reject);
|
|
100
|
+
});
|
|
101
|
+
zip.readEntry();
|
|
102
|
+
});
|
|
103
|
+
} finally {
|
|
104
|
+
zip.close();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Drops the `scripts` and `dependencies` a payload ships prebundled, and
|
|
109
|
+
* `extensionPack` when Review does not ship the pack's members.
|
|
110
|
+
*/
|
|
111
|
+
async function sanitizeVsixManifest(directory, extension) {
|
|
112
|
+
const file = path.join(directory, "package.json");
|
|
113
|
+
const manifest = manifestSchema.parse(JSON.parse(await readFile(file, "utf8")));
|
|
114
|
+
const declared = `${manifest.publisher}.${manifest.name}`;
|
|
115
|
+
if (declared.toLowerCase() !== extension.id.toLowerCase()) throw new Error(`${extension.id}: the VSIX declares ${declared}`);
|
|
116
|
+
const { scripts, dependencies, devDependencies, extensionPack, ...kept } = manifest;
|
|
117
|
+
await writeFile(file, `${JSON.stringify(extension.stripExtensionPack ? kept : {
|
|
118
|
+
...kept,
|
|
119
|
+
extensionPack
|
|
120
|
+
}, void 0, 2)}\n`);
|
|
121
|
+
return manifest.engines?.vscode;
|
|
122
|
+
}
|
|
123
|
+
//#endregion
|
|
124
|
+
//#region src/remote-extensions.ts
|
|
125
|
+
const EXECUTABLE_TIMEOUT_MS = 15e3;
|
|
126
|
+
const STAMP_FILE = ".curated.json";
|
|
127
|
+
const curatedDownloadSchema = z.object({
|
|
128
|
+
universal: z.boolean(),
|
|
129
|
+
url: z.string(),
|
|
130
|
+
sha256: z.string(),
|
|
131
|
+
size: z.number()
|
|
132
|
+
});
|
|
133
|
+
const curatedExtensionSchema = z.object({
|
|
134
|
+
id: z.string(),
|
|
135
|
+
version: z.string(),
|
|
136
|
+
tier: z.enum(["bundled", "optional"]),
|
|
137
|
+
group: z.string(),
|
|
138
|
+
executables: z.array(z.string()),
|
|
139
|
+
stripExtensionPack: z.boolean(),
|
|
140
|
+
targets: z.object({
|
|
141
|
+
"linux-x64": curatedDownloadSchema,
|
|
142
|
+
"linux-arm64": curatedDownloadSchema
|
|
143
|
+
})
|
|
144
|
+
});
|
|
145
|
+
const stampSchema = z.object({
|
|
146
|
+
id: z.string(),
|
|
147
|
+
version: z.string(),
|
|
148
|
+
target: z.string(),
|
|
149
|
+
sha256: z.string(),
|
|
150
|
+
installedTimestamp: z.number()
|
|
151
|
+
});
|
|
152
|
+
function whiteboardRemoteHome(env = process.env) {
|
|
153
|
+
return path.join(devReviewHome(env), "whiteboard-remote");
|
|
154
|
+
}
|
|
155
|
+
function remoteServerPaths(env = process.env) {
|
|
156
|
+
const home = whiteboardRemoteHome(env);
|
|
157
|
+
return {
|
|
158
|
+
extensionsDir: path.join(home, "extensions"),
|
|
159
|
+
serverDataDir: path.join(home, "server")
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function remoteExtensionTarget(platform = process.platform, arch = process.arch) {
|
|
163
|
+
if (platform === "linux" && (arch === "x64" || arch === "arm64")) return `linux-${arch}`;
|
|
164
|
+
throw new Error(`Language features on a remote need Linux on x64 or arm64; this machine is ${platform}-${arch}.`);
|
|
165
|
+
}
|
|
166
|
+
async function ensureRemoteExtensions(input = {}) {
|
|
167
|
+
const target = input.target ?? remoteExtensionTarget();
|
|
168
|
+
const curated = input.curated ?? await readPackagedCurated();
|
|
169
|
+
const { extensionsDir, serverDataDir } = remoteServerPaths(input.env);
|
|
170
|
+
const installed = [];
|
|
171
|
+
const skipped = [];
|
|
172
|
+
const failed = [];
|
|
173
|
+
const listed = [];
|
|
174
|
+
await mkdir(extensionsDir, { recursive: true });
|
|
175
|
+
for (const extension of curated) {
|
|
176
|
+
if (extension.tier === "optional" && !input.groups?.includes(extension.group)) {
|
|
177
|
+
skipped.push({
|
|
178
|
+
id: extension.id,
|
|
179
|
+
reason: `optional group "${extension.group}" not requested`
|
|
180
|
+
});
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
const download = extension.targets[target];
|
|
184
|
+
const folder = `${extension.id}-${extension.version}`;
|
|
185
|
+
const directory = path.join(extensionsDir, folder);
|
|
186
|
+
const stamp = {
|
|
187
|
+
id: extension.id,
|
|
188
|
+
version: extension.version,
|
|
189
|
+
target,
|
|
190
|
+
sha256: download.sha256
|
|
191
|
+
};
|
|
192
|
+
try {
|
|
193
|
+
const current = await readStamp(directory);
|
|
194
|
+
if (current && sameStamp(current, stamp) && await checkExecutables(directory, extension).then(() => true, () => false)) {
|
|
195
|
+
skipped.push({
|
|
196
|
+
id: extension.id,
|
|
197
|
+
reason: "up to date"
|
|
198
|
+
});
|
|
199
|
+
listed.push(storedExtension(extension, directory, folder, target, current.installedTimestamp));
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
const installedTimestamp = Date.now();
|
|
203
|
+
await install(extension, download, directory, {
|
|
204
|
+
...stamp,
|
|
205
|
+
installedTimestamp
|
|
206
|
+
}, input.timeoutMs, input.signal);
|
|
207
|
+
installed.push(extension.id);
|
|
208
|
+
listed.push(storedExtension(extension, directory, folder, target, installedTimestamp));
|
|
209
|
+
} catch (error) {
|
|
210
|
+
failed.push({
|
|
211
|
+
id: extension.id,
|
|
212
|
+
error: error instanceof Error ? error.message : String(error)
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const list = `${JSON.stringify(listed)}\n`;
|
|
217
|
+
const listFile = path.join(extensionsDir, "extensions.json");
|
|
218
|
+
if (await readFile(listFile, "utf8").catch(() => void 0) !== list) {
|
|
219
|
+
const temporary = `${listFile}.${process.pid}.tmp`;
|
|
220
|
+
await writeFile(temporary, list);
|
|
221
|
+
await rename(temporary, listFile);
|
|
222
|
+
await rm(path.join(serverDataDir, "data", "CachedProfilesData"), {
|
|
223
|
+
recursive: true,
|
|
224
|
+
force: true
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
const kept = new Set(listed.map((entry) => entry.relativeLocation));
|
|
228
|
+
for (const folder of await readdir(extensionsDir)) if (!kept.has(folder) && curated.some(({ id }) => folder.startsWith(`${id}-`) && /^\d/.test(folder.slice(id.length + 1)))) await rm(path.join(extensionsDir, folder), {
|
|
229
|
+
recursive: true,
|
|
230
|
+
force: true
|
|
231
|
+
});
|
|
232
|
+
return {
|
|
233
|
+
event: "remote.extensions",
|
|
234
|
+
target,
|
|
235
|
+
installed,
|
|
236
|
+
skipped,
|
|
237
|
+
failed,
|
|
238
|
+
groups: requestedGroups(curated, input.groups ?? [], failed)
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
function requestedGroups(curated, groups, failed) {
|
|
242
|
+
return groups.flatMap((group) => {
|
|
243
|
+
const members = curated.filter((extension) => extension.tier === "optional" && extension.group === group);
|
|
244
|
+
if (members.length === 0) return [];
|
|
245
|
+
const errors = failed.filter(({ id }) => members.some((extension) => extension.id === id));
|
|
246
|
+
return [errors.length === 0 ? {
|
|
247
|
+
group,
|
|
248
|
+
installed: true
|
|
249
|
+
} : {
|
|
250
|
+
group,
|
|
251
|
+
installed: false,
|
|
252
|
+
detail: errors.map(({ id, error }) => `${id}: ${error}`).join("; ")
|
|
253
|
+
}];
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
async function readPackagedCurated() {
|
|
257
|
+
const file = path.join(findReviewPackageRoot(import.meta.url), "vscode-server", "curated.json");
|
|
258
|
+
const text = await readFile(file, "utf8").catch(() => {
|
|
259
|
+
throw new Error(`This package has no VS Code server (${file} is missing).`);
|
|
260
|
+
});
|
|
261
|
+
return z.object({ extensions: z.array(curatedExtensionSchema) }).parse(JSON.parse(text)).extensions;
|
|
262
|
+
}
|
|
263
|
+
async function readStamp(directory) {
|
|
264
|
+
return readFile(path.join(directory, STAMP_FILE), "utf8").then((text) => stampSchema.safeParse(parseJsonText(text)).data).catch(() => void 0);
|
|
265
|
+
}
|
|
266
|
+
function sameStamp(current, expected) {
|
|
267
|
+
return current.id === expected.id && current.version === expected.version && current.target === expected.target && current.sha256 === expected.sha256;
|
|
268
|
+
}
|
|
269
|
+
async function install(extension, download, directory, stamp, timeoutMs, signal) {
|
|
270
|
+
const part = `${directory}.${process.pid}.vsix`;
|
|
271
|
+
const staging = `${directory}.${process.pid}.staging`;
|
|
272
|
+
try {
|
|
273
|
+
await downloadPinned(download, part, {
|
|
274
|
+
timeoutMs,
|
|
275
|
+
signal
|
|
276
|
+
});
|
|
277
|
+
await rm(staging, {
|
|
278
|
+
recursive: true,
|
|
279
|
+
force: true
|
|
280
|
+
});
|
|
281
|
+
await extractVsix(part, staging, download.size * 8);
|
|
282
|
+
await sanitizeVsixManifest(staging, extension);
|
|
283
|
+
await checkExecutables(staging, extension);
|
|
284
|
+
await writeFile(path.join(staging, STAMP_FILE), `${JSON.stringify(stamp)}\n`);
|
|
285
|
+
await rm(directory, {
|
|
286
|
+
recursive: true,
|
|
287
|
+
force: true
|
|
288
|
+
});
|
|
289
|
+
await rename(staging, directory);
|
|
290
|
+
} finally {
|
|
291
|
+
await rm(part, { force: true });
|
|
292
|
+
await rm(staging, {
|
|
293
|
+
recursive: true,
|
|
294
|
+
force: true
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
async function checkExecutables(directory, extension) {
|
|
299
|
+
for (const relative of extension.executables) await promisify(execFile)(path.join(directory, relative), ["--version"], {
|
|
300
|
+
timeout: EXECUTABLE_TIMEOUT_MS,
|
|
301
|
+
killSignal: "SIGKILL"
|
|
302
|
+
}).catch((error) => {
|
|
303
|
+
throw new Error(`${extension.id}: ${relative} does not run here: ${error.message}`);
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
function storedExtension(extension, directory, folder, target, installedTimestamp) {
|
|
307
|
+
return {
|
|
308
|
+
identifier: { id: extension.id },
|
|
309
|
+
version: extension.version,
|
|
310
|
+
location: {
|
|
311
|
+
$mid: 1,
|
|
312
|
+
scheme: "file",
|
|
313
|
+
path: directory
|
|
314
|
+
},
|
|
315
|
+
relativeLocation: folder,
|
|
316
|
+
metadata: {
|
|
317
|
+
installedTimestamp,
|
|
318
|
+
targetPlatform: extension.targets[target].universal ? "universal" : target
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
//#endregion
|
|
323
|
+
export { whiteboardRemoteHome as i, remoteExtensionTarget as n, remoteServerPaths as r, ensureRemoteExtensions as t };
|