@volter/live 0.5.211 → 0.5.212

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.
Files changed (67) hide show
  1. package/BUNDLED_NOTICES +13 -13
  2. package/README.md +7 -11
  3. package/dist/chunk-7SZ3C4CC.js +768 -0
  4. package/dist/chunk-7SZ3C4CC.js.map +7 -0
  5. package/dist/chunk-7WKVJGHZ.js +15 -0
  6. package/dist/chunk-7WKVJGHZ.js.map +7 -0
  7. package/dist/chunk-SDIWRX5X.js +189 -0
  8. package/dist/chunk-SDIWRX5X.js.map +7 -0
  9. package/dist/chunk-USXOIBVX.js +81 -0
  10. package/dist/chunk-USXOIBVX.js.map +7 -0
  11. package/dist/chunk-Y5W77HYT.js +123 -0
  12. package/dist/chunk-Y5W77HYT.js.map +7 -0
  13. package/dist/chunk-YPAOTPIF.js +202 -0
  14. package/dist/chunk-YPAOTPIF.js.map +7 -0
  15. package/dist/client.d.ts +370 -0
  16. package/dist/client.js +9 -0
  17. package/dist/client.js.map +7 -0
  18. package/dist/discovery.d.ts +32 -0
  19. package/dist/discovery.js +19 -0
  20. package/dist/discovery.js.map +7 -0
  21. package/dist/editor-document.d.ts +12 -47
  22. package/dist/editor.d.ts +34 -181
  23. package/dist/http-transport.d.ts +6 -0
  24. package/dist/index.d.ts +1 -5
  25. package/dist/index.js +76 -1722
  26. package/dist/index.js.map +4 -4
  27. package/dist/installed-package.d.ts +6 -0
  28. package/dist/installed-package.js +7 -0
  29. package/dist/installed-package.js.map +7 -0
  30. package/dist/lazy-proxy.d.ts +11 -11
  31. package/dist/member-hints.d.ts +6 -9
  32. package/dist/product-create.d.ts +29 -0
  33. package/dist/product-create.js +13 -0
  34. package/dist/product-create.js.map +7 -0
  35. package/dist/product-locator.d.ts +100 -0
  36. package/dist/product-locator.js +25 -0
  37. package/dist/product-locator.js.map +7 -0
  38. package/dist/registry-format.d.ts +44 -0
  39. package/dist/registry-format.js +21 -0
  40. package/dist/registry-format.js.map +7 -0
  41. package/dist/session.d.ts +10 -3
  42. package/dist/singleton.d.ts +1 -13
  43. package/dist/volter-manifest.d.ts +55 -0
  44. package/dist/volter-manifest.js +9 -0
  45. package/dist/volter-manifest.js.map +7 -0
  46. package/dist/workbench-locator.d.ts +84 -0
  47. package/dist/workbench-locator.js +416 -0
  48. package/dist/workbench-locator.js.map +7 -0
  49. package/package.json +35 -3
  50. package/src/client.ts +1052 -0
  51. package/src/discovery.ts +105 -0
  52. package/src/editor-document.ts +12 -49
  53. package/src/editor.ts +45 -212
  54. package/src/http-transport.ts +19 -0
  55. package/src/index.ts +3 -7
  56. package/src/installed-package.ts +15 -0
  57. package/src/lazy-proxy.ts +11 -11
  58. package/src/member-hints.ts +8 -12
  59. package/src/product-create.ts +38 -0
  60. package/src/product-locator.ts +327 -0
  61. package/src/registry-format.ts +185 -0
  62. package/src/session.ts +38 -66
  63. package/src/singleton.ts +1 -16
  64. package/src/volter-manifest.ts +97 -0
  65. package/src/workbench-locator.ts +625 -0
  66. package/dist/tools.d.ts +0 -23
  67. package/src/tools.ts +0 -45
@@ -0,0 +1,625 @@
1
+
2
+
3
+ import { execFileSync } from 'node:child_process';
4
+ import { createHash } from 'node:crypto';
5
+ import {
6
+ createWriteStream,
7
+ existsSync,
8
+ mkdirSync,
9
+ readFileSync,
10
+ renameSync,
11
+ rmSync,
12
+ writeFileSync,
13
+ } from 'node:fs';
14
+ import { homedir } from 'node:os';
15
+ import { basename, isAbsolute, join, resolve, sep } from 'node:path';
16
+ import { Readable } from 'node:stream';
17
+ import { pipeline } from 'node:stream/promises';
18
+ import {
19
+ PRODUCT_DECLARATION_KEY,
20
+ type ProductIdentity,
21
+ workbenchProductId,
22
+ } from './product-locator';
23
+
24
+ /** Where a project declares its workbench, relative to the project root. */
25
+ export const WORKBENCH_LOCATOR_PATH = '.volter/workbench.json';
26
+
27
+ /** The release's own record, written beside the tarball by
28
+ * `scripts/workbench/build-release.mjs`. */
29
+ const RELEASE_RECORD = 'BUILD.json';
30
+ /** The REH launcher inside a fork checkout — the marker that says "sources". */
31
+ const SOURCES_LAUNCHER = join('scripts', 'code-server.sh');
32
+ /** What `scripts/workbench/overlay.mjs` leaves in a checkout it has overlaid. */
33
+ const OVERLAY_RECORD = '.volter-overlay.json';
34
+
35
+ /** What the session reports about the workbench it is running. */
36
+ export interface WorkbenchIdentity {
37
+ readonly kind: 'release' | 'sources';
38
+ /** The directory named by the declaration or the flag, resolved absolute. */
39
+ readonly dir: string;
40
+ /** The Code-OSS fork commit these bytes are. */
41
+ readonly commit: string;
42
+
43
+ readonly product: string;
44
+ }
45
+
46
+ /** Everything the session needs to run it, identity included. */
47
+ export interface ResolvedWorkbench extends WorkbenchIdentity {
48
+ /** The executable to spawn, already known to exist. */
49
+ readonly serverBin: string;
50
+ /** Its working directory — the package root, or the checkout. */
51
+ readonly cwd: string;
52
+ }
53
+
54
+ /** The exact file to write, quoted in every refusal that wants one. */
55
+ export function workbenchDeclarationExample(projectRoot: string): string {
56
+ return (
57
+ `${join(projectRoot, WORKBENCH_LOCATOR_PATH)}:\n` +
58
+ ' { "workbench": "<path to an extracted Code-OSS release, or to a fork checkout>" }'
59
+ );
60
+ }
61
+
62
+ /**
63
+ * The workbench directory this project declares, or `null` when it declares
64
+ * none. Throws when the file exists and is not exactly `{ "workbench": <path> }`
65
+ * — unrecognized keys are refused.
66
+ */
67
+ export function readWorkbenchDeclaration(projectRoot: string): string | null {
68
+ const file = join(projectRoot, WORKBENCH_LOCATOR_PATH);
69
+ if (!existsSync(file)) return null;
70
+ let parsed: unknown;
71
+ try {
72
+ parsed = JSON.parse(readFileSync(file, 'utf8'));
73
+ } catch (error) {
74
+ throw new Error(
75
+ `${file} is not valid JSON (${error instanceof Error ? error.message : String(error)}). It says ` +
76
+ `which Code-OSS workbench this project opens in, and nothing else:\n${workbenchDeclarationExample(projectRoot)}`,
77
+ );
78
+ }
79
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
80
+ throw new Error(
81
+ `${file} must be a JSON object naming one workbench:\n${workbenchDeclarationExample(projectRoot)}`,
82
+ );
83
+ }
84
+ const record = parsed as Record<string, unknown>;
85
+ const extra = Object.keys(record).filter((key) => key !== 'workbench');
86
+ if (extra.length > 0) {
87
+ throw new Error(
88
+ `${file} carries ${extra.map((key) => `"${key}"`).join(', ')}, which this document has no meaning for. ` +
89
+ `It names ONE key:\n${workbenchDeclarationExample(projectRoot)}`,
90
+ );
91
+ }
92
+ const declared = record['workbench'];
93
+ if (typeof declared !== 'string' || declared.trim() === '') {
94
+ throw new Error(
95
+ `${file} names no workbench directory:\n${workbenchDeclarationExample(projectRoot)}`,
96
+ );
97
+ }
98
+ return isAbsolute(declared) ? resolve(declared) : resolve(projectRoot, declared);
99
+ }
100
+
101
+ /**
102
+ * WRITE THE DECLARATION — the one record of which workbench THIS machine opens
103
+ * this project in. Written by `<product> create --workbench <dir>`, and written
104
+ * by the fetch below, so the two paths cannot leave two different shapes of
105
+ * file behind.
106
+ */
107
+ export function writeWorkbenchDeclaration(projectRoot: string, workbenchDir: string): void {
108
+ mkdirSync(join(projectRoot, '.volter'), { recursive: true });
109
+ writeFileSync(
110
+ join(projectRoot, WORKBENCH_LOCATOR_PATH),
111
+ `${JSON.stringify({ workbench: resolve(workbenchDir) }, null, 2)}\n`,
112
+ );
113
+ }
114
+
115
+ /**
116
+ * The url the workbench answers on — the ONE url a person opens under the
117
+ * frame. Spelled here so the CLI's launch line and the session's tab bijection
118
+ * cannot disagree about it.
119
+ *
120
+ * `?project=<id>` is the one boot param the Volter editor carries; the session's
121
+ * one-origin proxy redirects it to the workbench's own `?folder=`, and
122
+ * `frame-proxy.ts` says why it must be a redirect.
123
+ */
124
+ export function workbenchUrl(proxyPort: number, projectRoot: string): string {
125
+ const id = basename(resolve(projectRoot));
126
+ // Cookies ignore ports. A shared loopback hostname can inherit another
127
+ // local VS Code app's secret-storage cookie and select its absent key server.
128
+ // Give each project a stable host; browsers resolve *.localhost to loopback.
129
+ return `http://editor-${projectKey(projectRoot)}.localhost:${proxyPort}/?project=${encodeURIComponent(id)}`;
130
+ }
131
+
132
+ /** A project's stable key on this machine: its page's host, and its workbench extensions folder. */
133
+ export function projectKey(projectRoot: string): string {
134
+ return createHash('sha256').update(resolve(projectRoot)).digest('hex').slice(0, 16);
135
+ }
136
+
137
+ /** This machine in the build script's own platform vocabulary. */
138
+ function machinePlatform(): string {
139
+ return `${process.platform}-${process.arch}`;
140
+ }
141
+
142
+
143
+ /** The workbench at `dir`, refused where it is built for another product than `expectedProduct`; none named (a session
144
+ * whose project's product cannot be resolved, which `/__editor/served-modules` explains) is not judged. */
145
+ export function resolveWorkbench(dir: string, expectedProduct?: string): ResolvedWorkbench {
146
+ const root = resolve(dir);
147
+ const resolved = existsSync(join(root, RELEASE_RECORD))
148
+ ? resolveRelease(root)
149
+ : existsSync(join(root, SOURCES_LAUNCHER))
150
+ ? resolveSources(root)
151
+ : null;
152
+ if (resolved === null) {
153
+ throw new Error(
154
+ `${root} is neither a Code-OSS release nor a fork checkout: it carries no ${RELEASE_RECORD} (the record ` +
155
+ `scripts/workbench/build-release.mjs writes beside its tarball) and no ${SOURCES_LAUNCHER}. Name the ` +
156
+ 'directory the release was extracted into, or the fork checkout itself.',
157
+ );
158
+ }
159
+ if (expectedProduct !== undefined && resolved.product !== expectedProduct) {
160
+ throw new Error(
161
+ `${root} is a ${resolved.product} workbench and this project opens in ${expectedProduct}. A workbench is ` +
162
+ "built for ONE product — the editor kit and that product's workbench half are overlaid on the fork " +
163
+ "together — so its title, its look and its commands are the other product's, whatever this session " +
164
+ `serves into it. ${
165
+ resolved.kind === 'sources'
166
+ ? `Re-overlay the checkout:\n node scripts/workbench/dev.mjs --checkout ${root} --product ${expectedProduct}`
167
+ : `Name a ${expectedProduct} release in .volter/workbench.json, or cut one:\n node scripts/workbench/build-release.mjs --product ${expectedProduct} --platform ${machinePlatform()} --checkout <fork dir>`
168
+ }`,
169
+ );
170
+ }
171
+ return resolved;
172
+ }
173
+
174
+ /**
175
+ * The five fields the launch depends on — `target`, `platform`, `commit`,
176
+ * `serverBin` and `product`. The rest of `BUILD.json` is the build's own
177
+ * provenance (sizes, sha256, the node it was built with) and is read by a
178
+ * person, not by this.
179
+ */
180
+ function resolveRelease(dir: string): ResolvedWorkbench {
181
+ const record = join(dir, RELEASE_RECORD);
182
+ const cut = `node scripts/workbench/build-release.mjs --product <id> --platform ${machinePlatform()} --checkout <fork dir> --out ${dir}`;
183
+ let parsed: Record<string, unknown>;
184
+ try {
185
+ parsed = JSON.parse(readFileSync(record, 'utf8')) as Record<string, unknown>;
186
+ } catch (error) {
187
+ throw new Error(`${record} could not be read: ${String(error)}`);
188
+ }
189
+ const needed = ['target', 'platform', 'commit', 'serverBin', 'product'] as const;
190
+ const missing = needed.filter((key) => typeof parsed[key] !== 'string');
191
+ if (missing.length > 0) {
192
+ throw new Error(
193
+ `${record} is not a release record this understands: ${missing.join(', ')} ` +
194
+ `${missing.length === 1 ? 'is' : 'are'} missing or not a string. A release names ${needed.join(', ')} — ` +
195
+ 'the five fields scripts/workbench/build-release.mjs writes and this one spawns from. ' +
196
+ 'The record must name the product compiled into the workbench. Re-cut it:\n ' +
197
+ cut,
198
+ );
199
+ }
200
+ const target = parsed['target'] as string;
201
+ const platform = parsed['platform'] as string;
202
+ const commit = parsed['commit'] as string;
203
+ const serverBin = parsed['serverBin'] as string;
204
+ const product = parsed['product'] as string;
205
+
206
+ const machine = machinePlatform();
207
+ if (platform !== machine) {
208
+ throw new Error(
209
+ `${dir} was built for ${platform} and this machine is ${machine}. A Code-OSS server package carries its ` +
210
+ "own platform's node binary and native modules, so it cannot run here — and this never falls back to a " +
211
+ `source checkout, because a boot that silently was not the release measures nothing. Cut the ${machine} ` +
212
+ `release:\n node scripts/workbench/build-release.mjs --product ${product} --platform ${machine} --checkout <fork dir>`,
213
+ );
214
+ }
215
+
216
+ const packageRoot = join(dir, target);
217
+ const serverBinPath = join(packageRoot, serverBin);
218
+ if (!existsSync(serverBinPath)) {
219
+ const tarball =
220
+ typeof parsed['tarball'] === 'string'
221
+ ? (parsed['tarball'] as string)
222
+ : `${target}-<commit>.tar.gz`;
223
+ throw new Error(
224
+ `${dir} records ${target}, but ${serverBinPath} is not there — the tarball has not been extracted beside ` +
225
+ `${RELEASE_RECORD}. Run:\n tar -xzf ${join(dir, tarball)} -C ${dir}`,
226
+ );
227
+ }
228
+ return { kind: 'release', dir, commit, product, serverBin: serverBinPath, cwd: packageRoot };
229
+ }
230
+
231
+ /**
232
+ * A fork checkout. The commit is the working tree's, read with git: a checkout
233
+ * has no file that states it, and a stale one would name bytes nobody is
234
+ * running. The PRODUCT is the overlay's own record — a checkout with none has
235
+ * no volter tier in it at all, and that is refused here rather than left to
236
+ * surface as a workbench where `Volter: Open Workspace` does not exist.
237
+ */
238
+ function resolveSources(dir: string): ResolvedWorkbench {
239
+ let commit: string;
240
+ try {
241
+ commit = execFileSync('git', ['rev-parse', 'HEAD'], { windowsHide: true, cwd: dir, encoding: 'utf8' }).trim();
242
+ } catch (error) {
243
+ throw new Error(
244
+ `${dir} carries ${SOURCES_LAUNCHER}, so it reads as a Code-OSS fork checkout, but \`git rev-parse HEAD\` ` +
245
+ `there failed (${error instanceof Error ? error.message : String(error)}). A checkout's commit is the ` +
246
+ 'only record of which fork this workbench is.',
247
+ );
248
+ }
249
+ const marker = join(dir, OVERLAY_RECORD);
250
+ if (!existsSync(marker)) {
251
+ throw new Error(
252
+ `${dir} is a Code-OSS checkout with no volter overlay: ${OVERLAY_RECORD} is not there, so nothing of the ` +
253
+ 'editor is compiled into it and the workbench would come up as plain Code-OSS. Overlay and compile it:\n' +
254
+ ` node scripts/workbench/dev.mjs --checkout ${dir} --product <product>`,
255
+ );
256
+ }
257
+ let product: unknown;
258
+ try {
259
+ product = (JSON.parse(readFileSync(marker, 'utf8')) as Record<string, unknown>)['product'];
260
+ } catch (error) {
261
+ throw new Error(`${marker} could not be read: ${String(error)}`);
262
+ }
263
+ if (typeof product !== 'string' || product === '') {
264
+ throw new Error(
265
+ `${marker} names no product. It is written by scripts/workbench/overlay.mjs and says which product's ` +
266
+ 'workbench half is in this checkout; re-run the overlay rather than editing it.',
267
+ );
268
+ }
269
+ return {
270
+ kind: 'sources',
271
+ dir,
272
+ commit,
273
+ product,
274
+ serverBin: join(dir, SOURCES_LAUNCHER),
275
+ cwd: dir,
276
+ };
277
+ }
278
+
279
+ // ---------------------------------------------------------------------------
280
+ // THE PRODUCT'S OWN WORKBENCH — the third resolution step, and the only one
281
+ // that needs nothing on the machine but the product.
282
+ // ---------------------------------------------------------------------------
283
+
284
+ /** The fork's own repository, where a cut release is published. Private today;
285
+ * the fetch below is the same call either way, with a token or without one. */
286
+ const RELEASE_REPO = 'volter-ai/code-oss';
287
+
288
+ /**
289
+ * WHERE FETCHED BYTES LIVE — one directory per release tag, per user (`~/.volter/workbenches`).
290
+ *
291
+ * Keyed by the TAG and nothing else, because that is what makes it a cache and
292
+ * not a decision: two projects on the same release share one directory,
293
+ * a second `create` consults no network, and a release is immutable, so a tag
294
+ * that is here is the tag that was published. Which workbench a project opens
295
+ * in is still recorded in the PROJECT (`.volter/workbench.json`, written the
296
+ * moment this resolves) — nothing ever searches this directory.
297
+ */
298
+ export const WORKBENCH_CACHE_ROOT = join(homedir(), '.volter', 'workbenches');
299
+
300
+ /** Where the fetch talks. `log` is a line; `progress` is ONE line rewritten in
301
+ * place (216 MB is a wait, and a wait with no number is a hang). */
302
+ export interface WorkbenchFetchIO {
303
+ readonly log: (line: string) => void;
304
+ readonly progress?: (line: string) => void;
305
+ }
306
+
307
+ /**
308
+ * THE WORKBENCH THIS PROJECT OPENS IN, resolved in three steps:
309
+ *
310
+ * 1. `--workbench <dir>` — one launch, writes nothing.
311
+ * 2. `<project>/.volter/workbench.json` — this machine's record for this project.
312
+ * 3. the PRODUCT's declared release — fetched once, then written into (2).
313
+ *
314
+ * Step 3 is what makes the product's `create` open something
315
+ * on a machine that has never built anything, and it is why the CLI resolves
316
+ * the product BEFORE the workbench.
317
+ */
318
+ export async function resolveWorkbenchForProject(options: {
319
+ projectRoot: string;
320
+ product: ProductIdentity;
321
+ /** `--workbench <dir>`, when the person passed one. */
322
+ override?: string | undefined;
323
+ io: WorkbenchFetchIO;
324
+ }): Promise<ResolvedWorkbench> {
325
+ const { projectRoot, product, override, io } = options;
326
+ const productId = workbenchProductId(product.name);
327
+ if (override !== undefined) return resolveWorkbench(resolve(override), productId);
328
+ // A declaration under the cache root is the one the fetch below wrote: the
329
+ // product's pin decides it, so a pin that moved, or a cache that was
330
+ // cleared, fetches again instead of opening a stale release or refusing a
331
+ // directory that is gone.
332
+ const declared = readWorkbenchDeclaration(projectRoot);
333
+ const fetchRecord = declared !== null && product.workbench !== null && declared.startsWith(`${WORKBENCH_CACHE_ROOT}${sep}`);
334
+ if (declared !== null && !fetchRecord) return resolveWorkbench(declared, productId);
335
+
336
+ if (product.workbench === null) throw new Error(
337
+ `${product.name} is a private source product with no released workbench. ` +
338
+ `Pass --workbench <compiled checkout> or declare it in ${WORKBENCH_LOCATOR_PATH}. ` +
339
+ 'Public distribution requires a pinned workbench release and checksum.',
340
+ );
341
+ const tag = product.workbench.release;
342
+ const dir = join(WORKBENCH_CACHE_ROOT, tag);
343
+ // A CACHED TAG IS NEVER RE-FETCHED. The marker is the release's own record,
344
+ // so a directory that lost its extraction reads as absent and is fetched
345
+ // again rather than resolving into a refusal a person cannot act on.
346
+ const fetched = !existsSync(join(dir, RELEASE_RECORD));
347
+ if (fetched) await fetchDeclaredRelease(product, tag, dir, io);
348
+ else io.log(`Workbench ${tag} is already installed (${dir}).`);
349
+ const workbench = resolveWorkbench(dir, productId);
350
+ writeWorkbenchDeclaration(projectRoot, dir);
351
+ io.log(`Declared in ${WORKBENCH_LOCATOR_PATH}: ${dir}`);
352
+ return workbench;
353
+ }
354
+
355
+ /** A GitHub release asset, as much of one as this reads. */
356
+ interface ReleaseAsset {
357
+ readonly id: number;
358
+ readonly name: string;
359
+ readonly size: number;
360
+ }
361
+
362
+ /**
363
+ * The token, from the two doors a person has: the environment, or the `gh` they
364
+ * are already logged into. Deliberately not a file of ours, not a setting and
365
+ * not a prompt — a credential this reads is one somebody else's tool already
366
+ * owns.
367
+ */
368
+ function githubToken(): string | null {
369
+ const fromEnv = process.env['GITHUB_TOKEN'];
370
+ if (typeof fromEnv === 'string' && fromEnv.trim() !== '') return fromEnv.trim();
371
+ try {
372
+ const out = execFileSync('gh', ['auth', 'token'], {
373
+ windowsHide: true,
374
+ encoding: 'utf8',
375
+ stdio: ['ignore', 'pipe', 'ignore'],
376
+ }).trim();
377
+ return out === '' ? null : out;
378
+ } catch {
379
+ return null;
380
+ }
381
+ }
382
+
383
+ function apiHeaders(token: string | null): Record<string, string> {
384
+ return {
385
+ Accept: 'application/vnd.github+json',
386
+ 'X-GitHub-Api-Version': '2022-11-28',
387
+ 'User-Agent': 'volter-workbench-locator',
388
+ ...(token === null ? {} : { Authorization: `Bearer ${token}` }),
389
+ };
390
+ }
391
+
392
+ function megabytes(bytes: number): string {
393
+ return (bytes / 1024 ** 2).toFixed(0);
394
+ }
395
+
396
+ /** The line a refusal prints when the release is not reachable — both doors,
397
+ * named, because "no credentials" with no names is the same as no message. */
398
+ function tokenDoors(): string {
399
+ return (
400
+ ' GITHUB_TOKEN=<a token with repo scope> — in this shell, or\n' +
401
+ ' gh auth login — so `gh auth token` answers'
402
+ );
403
+ }
404
+
405
+ /** The CLI prints only Error.message. Keep the public request/stage and Node's
406
+ * nested transport code there, without printing auth headers or signed redirects.
407
+ * The original thrown value is retained as cause for callers that can inspect it. */
408
+ async function downloadOperation<T>(
409
+ stage: string,
410
+ url: string,
411
+ token: string | null,
412
+ operation: () => Promise<T>,
413
+ ): Promise<T> {
414
+ try {
415
+ return await operation();
416
+ } catch (cause) {
417
+ const seen = new Set<unknown>();
418
+ const redact = (text: string): string => {
419
+ const withoutToken = token === null ? text : text.replaceAll(token, '[redacted]');
420
+ return withoutToken.replace(/https?:\/\/[^\s"<>]+/gi, (raw) => {
421
+ try {
422
+ const parsed = new URL(raw);
423
+ parsed.username = '';
424
+ parsed.password = '';
425
+ parsed.search = '';
426
+ parsed.hash = '';
427
+ return parsed.href;
428
+ } catch { return '[redacted URL]'; }
429
+ }).slice(0, 1000);
430
+ };
431
+ const describe = (error: unknown, depth: number): string => {
432
+ if (seen.has(error) || depth > 4) return '[cause omitted]';
433
+ seen.add(error);
434
+ if (typeof error !== 'object' || error === null) return redact(String(error));
435
+ const fields = error as { name?: unknown; message?: unknown; code?: unknown; cause?: unknown; errors?: unknown };
436
+ const name = typeof fields.name === 'string' ? fields.name : 'Error';
437
+ const message = typeof fields.message === 'string' ? fields.message : 'Unknown transport error';
438
+ const code = typeof fields.code === 'string' ? ` [${fields.code}]` : '';
439
+ let detail = redact(`${name}${code}: ${message}`);
440
+ if (fields.cause !== undefined) detail += `; caused by ${describe(fields.cause, depth + 1)}`;
441
+ if (Array.isArray(fields.errors))
442
+ detail += `; ${fields.errors.slice(0, 4).map((error) => describe(error, depth + 1)).join('; ')}`;
443
+ return detail;
444
+ };
445
+ throw new Error(`Workbench download failed during ${stage} at ${url}: ${describe(cause, 0)}`, { cause });
446
+ }
447
+ }
448
+
449
+ function assetUrl(asset: ReleaseAsset): string {
450
+ return `https://api.github.com/repos/${RELEASE_REPO}/releases/assets/${asset.id}`;
451
+ }
452
+
453
+ /**
454
+ * FETCH ONE PUBLISHED RELEASE into `dir`, atomically: everything lands in a
455
+ * sibling `.partial` directory and is renamed into place at the end, so a
456
+ * directory under `~/.volter/workbenches` either is a whole workbench or is not
457
+ * there. Any failure — a refused download, a sha that does not match the
458
+ * product's pin, a tar that dies — deletes the partial and refuses.
459
+ */
460
+ async function fetchDeclaredRelease(
461
+ product: ProductIdentity,
462
+ tag: string,
463
+ dir: string,
464
+ io: WorkbenchFetchIO,
465
+ ): Promise<void> {
466
+ if (product.workbench === null) throw new Error('Cannot download an unpinned workbench.');
467
+ const productId = workbenchProductId(product.name);
468
+ const machine = machinePlatform();
469
+ const token = githubToken();
470
+ const partial = join(WORKBENCH_CACHE_ROOT, `.${tag}.${process.pid}.partial`);
471
+ const cutLine =
472
+ ` node scripts/workbench/build-release.mjs --product ${productId} --platform ${machine} ` +
473
+ '--checkout <fork dir> --publish';
474
+
475
+ const releaseUrl = `https://api.github.com/repos/${RELEASE_REPO}/releases/tags/${encodeURIComponent(tag)}`;
476
+ const response = await downloadOperation('release metadata request', releaseUrl, token,
477
+ () => fetch(releaseUrl, { headers: apiHeaders(token) }));
478
+ if (response.status === 404 && token === null)
479
+ throw new Error(
480
+ `${product.name} opens in the Code-OSS workbench published as ${tag} on ${RELEASE_REPO}, and ` +
481
+ 'that repository answers 404 to this machine — it is private, and nothing here has a ' +
482
+ 'GitHub token to read it with. Two doors, either one:\n' +
483
+ `${tokenDoors()}\n` +
484
+ 'Or name a workbench you already have, which needs no network at all:\n' +
485
+ ` ${product.command} edit --workbench <dir>`,
486
+ );
487
+ if (response.status === 404)
488
+ throw new Error(
489
+ `${RELEASE_REPO} has no release tagged ${tag}, and that is the workbench ${product.name} ` +
490
+ `declares (package.json#${PRODUCT_DECLARATION_KEY}.workbench). Either the declaration names ` +
491
+ 'a tag that was never published, or this token cannot see the repository. Cut and publish ' +
492
+ `it:\n${cutLine}`,
493
+ );
494
+ if (!response.ok)
495
+ throw new Error(
496
+ `${RELEASE_REPO}'s release ${tag} answered ${response.status} ${response.statusText}. That is ` +
497
+ `the workbench ${product.name} declares; ` +
498
+ `${token === null ? 'no token was found' : 'the token this machine has was used'}.` +
499
+ (token === null ? `\n${tokenDoors()}` : ''),
500
+ );
501
+ const metadata = await downloadOperation('release metadata response', releaseUrl, token, () => response.json());
502
+ const assets = (metadata as { assets?: ReleaseAsset[] }).assets ?? [];
503
+ const record = assets.find((asset) => asset.name === RELEASE_RECORD);
504
+ const tarball = assets.find((asset) => asset.name.endsWith('.tar.gz'));
505
+ if (record === undefined || tarball === undefined)
506
+ throw new Error(
507
+ `Release ${tag} on ${RELEASE_REPO} carries ` +
508
+ `${assets.length === 0 ? 'no assets' : assets.map((asset) => asset.name).join(', ')}. ` +
509
+ `A workbench release is a tarball and its ${RELEASE_RECORD}, both uploaded by ` +
510
+ `\`build-release.mjs --publish\`; re-publish it:\n${cutLine}`,
511
+ );
512
+
513
+ rmSync(partial, { recursive: true, force: true });
514
+ mkdirSync(partial, { recursive: true });
515
+ try {
516
+ // Read the compatibility record before downloading the workbench artifact.
517
+ const recordText = await downloadAssetText(record, token);
518
+ const built = await downloadOperation('BUILD.json parse', assetUrl(record), token,
519
+ async () => JSON.parse(recordText) as Record<string, unknown>);
520
+ if (built['platform'] !== machine)
521
+ throw new Error(
522
+ `Release ${tag} was built for ${String(built['platform'])} and this machine is ${machine}. A ` +
523
+ "Code-OSS server package carries its own platform's node binary and native modules, so it " +
524
+ `cannot run here, and nothing falls back. Cut and publish the ${machine} release:\n${cutLine}`,
525
+ );
526
+ if (built['tarballSha256'] !== product.workbench.tarballSha256)
527
+ throw new Error(
528
+ `Release ${tag}'s own ${RELEASE_RECORD} records tarballSha256 ${String(built['tarballSha256'])} ` +
529
+ `and ${product.name} pins ${product.workbench.tarballSha256}. A release is immutable, so these ` +
530
+ 'two cannot disagree about one tag: either the product declares the wrong release, or the ' +
531
+ 'release was replaced. Nothing was downloaded.',
532
+ );
533
+ writeFileSync(join(partial, RELEASE_RECORD), recordText);
534
+
535
+ io.log(
536
+ `Fetching the ${productId} workbench ${tag} from ${RELEASE_REPO} — ` +
537
+ `${megabytes(tarball.size)} MB, once for this user on this computer.`,
538
+ );
539
+ const tarballPath = join(partial, tarball.name);
540
+ let announced = 0;
541
+ const sha = await downloadAsset(tarball, token, tarballPath, (received) => {
542
+ if (received - announced < 4 * 1024 ** 2 && received !== tarball.size) return;
543
+ announced = received;
544
+ io.progress?.(
545
+ ` ${megabytes(received)} / ${megabytes(tarball.size)} MB ` +
546
+ `(${Math.floor((received / tarball.size) * 100)}%)`,
547
+ );
548
+ });
549
+ if (sha !== product.workbench.tarballSha256)
550
+ throw new Error(
551
+ `The download of ${tag} hashes to ${sha} and ${product.name} pins ` +
552
+ `${product.workbench.tarballSha256}. These are not the bytes this product runs in; the ` +
553
+ 'partial download was deleted and nothing was extracted.',
554
+ );
555
+ io.log(` sha256 ${sha.slice(0, 12)}… matches the pin. Extracting…`);
556
+ // Windows' own tar (bsdtar, System32) — Git's GNU tar, often first on PATH, reads the
557
+ // "C:" of a path as a remote host.
558
+ const tar = process.platform === 'win32'
559
+ ? join(process.env['SystemRoot'] ?? 'C:\\Windows', 'System32', 'tar.exe') : 'tar';
560
+ execFileSync(tar, ['-xzf', tarballPath, '-C', partial], {
561
+ windowsHide: true,
562
+ stdio: ['ignore', 'ignore', 'pipe'],
563
+ });
564
+ // The tarball has done its job and is 216 MB; the extraction is what runs.
565
+ rmSync(tarballPath, { force: true });
566
+ if (existsSync(join(dir, RELEASE_RECORD))) {
567
+ // Another process won the same race. A release is immutable, so its
568
+ // directory is ours too — take it and drop this copy.
569
+ rmSync(partial, { recursive: true, force: true });
570
+ } else {
571
+ rmSync(dir, { recursive: true, force: true });
572
+ renameSync(partial, dir);
573
+ }
574
+ io.log(`Workbench installed at ${dir}.`);
575
+ } catch (error) {
576
+ rmSync(partial, { recursive: true, force: true });
577
+ throw error;
578
+ }
579
+ }
580
+
581
+ /** One asset's bytes as text — for `BUILD.json`, which is a kilobyte. */
582
+ async function downloadAssetText(asset: ReleaseAsset, token: string | null): Promise<string> {
583
+ const url = assetUrl(asset);
584
+ const response = await downloadOperation('BUILD.json request', url, token,
585
+ () => fetch(url, { headers: { ...apiHeaders(token), Accept: 'application/octet-stream' } }));
586
+ if (!response.ok)
587
+ throw new Error(
588
+ `${asset.name} of ${RELEASE_REPO} answered ${response.status} ${response.statusText}.`,
589
+ );
590
+ return await downloadOperation('BUILD.json response', url, token, () => response.text());
591
+ }
592
+
593
+ /**
594
+ * One asset's bytes to a file, hashing them ON THE WAY THROUGH — a 216 MB
595
+ * tarball is not read twice, and it is never held in memory.
596
+ */
597
+ async function downloadAsset(
598
+ asset: ReleaseAsset,
599
+ token: string | null,
600
+ destination: string,
601
+ onProgress: (received: number) => void,
602
+ ): Promise<string> {
603
+ const url = assetUrl(asset);
604
+ const response = await downloadOperation('archive request', url, token,
605
+ () => fetch(url, { headers: { ...apiHeaders(token), Accept: 'application/octet-stream' } }));
606
+ if (!response.ok || response.body === null)
607
+ throw new Error(
608
+ `${asset.name} of ${RELEASE_REPO} answered ${response.status} ${response.statusText}.`,
609
+ );
610
+ const hash = createHash('sha256');
611
+ let received = 0;
612
+ await downloadOperation('archive transfer', url, token, () => pipeline(
613
+ Readable.fromWeb(response.body as never),
614
+ async function* (source: AsyncIterable<Buffer>) {
615
+ for await (const chunk of source) {
616
+ hash.update(chunk);
617
+ received += chunk.length;
618
+ onProgress(received);
619
+ yield chunk;
620
+ }
621
+ },
622
+ createWriteStream(destination),
623
+ ));
624
+ return hash.digest('hex');
625
+ }
package/dist/tools.d.ts DELETED
@@ -1,23 +0,0 @@
1
- /** Registered project tools over the current shared editor session. */
2
- import type { EditorClient, ProjectToolCatalog, ProjectToolCatalogEntry, ProjectToolOutcome } from '@volter/sdk';
3
- export declare class LiveTools {
4
- #private;
5
- constructor(client: EditorClient);
6
- /** Enumerate the exact `package.json#volter.tools` catalog without executing it. */
7
- list(): Promise<ProjectToolCatalog>;
8
- /** Return one tool's discoverable metadata, or `null` when it is not registered. */
9
- describe(name: string): Promise<ProjectToolCatalogEntry | null>;
10
- /**
11
- * Invoke the same validated callable used by the editor and CLI.
12
- *
13
- * `instance` names WHICH mounted instance the tool should drive when several
14
- * are live (multiplayer authoring) — it reaches the tool as `ctx.instance`,
15
- * and a tool that drives the game binds
16
- * `game.instance(ctx.instance)` from it. Omitted is the single-instance case;
17
- * the tool then targets the sole live instance, exactly as before.
18
- */
19
- run(name: string, input?: unknown, options?: {
20
- confirm?: boolean;
21
- instance?: string;
22
- }): Promise<ProjectToolOutcome>;
23
- }