@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
package/src/index.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  /** Attach to an existing editor session; this package never launches one. */
2
- import { EditorClient } from '@volter/sdk/client';
2
+ import { EditorClient } from './client.js';
3
3
  import { LiveEditor } from './editor.js';
4
- import { LiveTools } from './tools.js';
5
4
  import { lazyChainProxy } from './lazy-proxy.js';
6
5
  import { resolveSession, type ResolvedSession, type SessionResolutionDeps } from './session.js';
7
6
  import { createLazySession } from './singleton.js';
@@ -10,22 +9,20 @@ import { editorMemberHint } from './member-hints.js';
10
9
  export { LiveEditor, inferAssetKind, type PanelName } from './editor.js';
11
10
  export { LiveEditorDocument } from './editor-document.js';
12
11
  export type { DocumentGestureOptions, DocumentKeyOptions, DocumentPasteOptions } from './editor-document.js';
13
- export { LiveTools } from './tools.js';
14
12
  export { editorMemberHint, withEditorMemberHints } from './member-hints.js';
15
- export { resolveSession, findProjectRootFrom, NO_SESSION_FOR_PROJECT } from './session.js';
13
+ export { resolveSession, findProjectRootFrom, NO_SESSION_FOR_PROJECT, SESSION_MANIFEST_UNREADABLE } from './session.js';
16
14
  export type { ResolvedSession, SessionResolutionDeps, SessionListingTransport, ProjectSessionHint } from './session.js';
17
15
  export type { ActiveDocumentCapture, EditorView, PresentedEditorView } from '@volter/sdk';
18
16
 
19
17
  export interface LiveBindings {
20
18
  editor: LiveEditor;
21
- tools: LiveTools;
22
19
  }
23
20
  export interface LiveSession extends LiveBindings {
24
21
  session: ResolvedSession;
25
22
  }
26
23
  function bindTo(port: number): LiveBindings {
27
24
  const client = new EditorClient({ url: `http://127.0.0.1:${port}` });
28
- return { editor: new LiveEditor(client), tools: new LiveTools(client) };
25
+ return { editor: new LiveEditor(client) };
29
26
  }
30
27
  /** Inspect the callable surface without connecting; calls fail on port zero. */
31
28
  export function unconnectedBindings(): LiveBindings { return bindTo(0); }
@@ -37,4 +34,3 @@ export async function connect(projectDir?: string, deps?: SessionResolutionDeps)
37
34
  }
38
35
  const lazySession = createLazySession(() => connect());
39
36
  export const editor: LiveEditor = lazyChainProxy<LiveEditor>(() => lazySession.ensure().then(s => s.editor), [], editorMemberHint);
40
- export const tools: LiveTools = lazyChainProxy<LiveTools>(() => lazySession.ensure().then(s => s.tools));
@@ -0,0 +1,15 @@
1
+ import { existsSync, realpathSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+
4
+ /**
5
+ * The real directory of installed package `name` as Node finds it from `fromDir`: up the `node_modules` folders, or
6
+ * null when none holds it. It is found by its folder rather than by resolving `<name>/package.json`, which fails for a
7
+ * package whose `exports` does not list that file (`@volter/sdk`).
8
+ */
9
+ export function installedPackageDir(fromDir: string, name: string): string | null {
10
+ for (let dir = fromDir; ; dir = dirname(dir)) {
11
+ const candidate = join(dir, 'node_modules', ...name.split('/'));
12
+ if (existsSync(join(candidate, 'package.json'))) return realpathSync(candidate);
13
+ if (dirname(dir) === dir) return null;
14
+ }
15
+ }
package/src/lazy-proxy.ts CHANGED
@@ -1,21 +1,21 @@
1
1
  /**
2
2
  * `lazyChainProxy` — turns an async "resolve the real object" function into
3
3
  * a synchronously-importable stand-in that supports the SAME call shape as
4
- * the real thing, including nested member access (`game.input.hold(...)`,
5
- * `editor.document.query(...)`), by recording the property-access PATH and only
4
+ * the real thing, including nested member access
5
+ * (`editor.document.query(...)`), by recording the property-access PATH and only
6
6
  * resolving + walking it down at the point of an actual function CALL.
7
7
  *
8
- * This is what makes `index.ts`'s top-level `export const editor = ...` /
9
- * `game` / `page` work as plain values a script can `import { editor, game,
10
- * page } from '@volter/live'` and call immediately — each call transparently
11
- * awaits the memoized `connect()` first.
8
+ * This is what makes `index.ts`'s top-level `export const editor = ...` work
9
+ * as a plain value a script can `import { editor } from '@volter/live'` and
10
+ * call immediately — each call transparently awaits the memoized `connect()`
11
+ * first.
12
12
  *
13
13
  * Limitation (by design, documented on `index.ts`'s exports too): only
14
- * FUNCTION-shaped access resolves through this proxy — `game.input.hold(x)`
15
- * works, but reading a plain data property (e.g. `game.fenceTick`) would
16
- * return another inert proxy, not the real number, since there is no
17
- * function CALL to trigger resolution. Every documented `editor`/`game`/
18
- * `page` member is a method, so this never bites the documented surface.
14
+ * FUNCTION-shaped access resolves through this proxy — `editor.document.query(s)`
15
+ * works, but reading a plain data property would return another inert proxy,
16
+ * not the real value, since there is no function CALL to trigger resolution.
17
+ * Every documented `editor` member is a method, so this never bites the
18
+ * documented surface.
19
19
  */
20
20
 
21
21
  type AnyFn = (...args: unknown[]) => unknown;
@@ -2,13 +2,10 @@
2
2
  * NAMES AN AGENT REACHES FOR THAT THE FAÇADE DOES NOT HAVE, and where the
3
3
  * thing it wanted actually lives.
4
4
  *
5
- * Why this exists (measured 2026-10, an agent building an obby in the Model
6
- * Editor): it called `editor.setCamera(...)`, got the engine's bare
7
- * "editor.setCamera is not a function", searched for a camera verb, and in the
8
- * end improvised a pose from `frame()` and `orbit()`. The camera was reachable
9
- * the whole time — through `editor.present`'s `viewport.camera` — but the
10
- * failure said nothing about it. The refusal is the one moment the caller is
11
- * certainly reading, so that is where the pointer goes.
5
+ * A call to `editor.setCamera(...)` would otherwise fail with the engine's bare
6
+ * "editor.setCamera is not a function", though the camera is reachable through
7
+ * `editor.present`'s `viewport.camera`. The refusal is the one moment the
8
+ * caller is certainly reading, so that is where the pointer goes.
12
9
  *
13
10
  * TARGETED, NOT A CATCH-ALL. Only the names below are intercepted, each with
14
11
  * the door it was looking for. Every other missing member keeps JavaScript's
@@ -19,9 +16,8 @@
19
16
  const CAMERA_HINT =
20
17
  'The viewport camera is posed through a presented view: ' +
21
18
  'await editor.present({ version: 1, viewport: { camera: { position: { x, y, z }, target: { x, y, z }, fov } } }) ' +
22
- '(stage space: metres, Y up; a Blender point (x, y, z) is { x, y: z, z: -y }), ' +
19
+ '(stage space: metres, Y up), ' +
23
20
  'and read back with (await editor.currentView()).viewport.camera. ' +
24
- 'From Cyclotron\'s shell: cyclotron camera --position x,y,z --target x,y,z [--fov n] (Blender coordinates). ' +
25
21
  'editor.frame() and editor.orbit() frame or swing around the subject instead.';
26
22
 
27
23
  /** Missing `editor` member → what to use instead. */
@@ -43,19 +39,19 @@ export function editorMemberHint(member: PropertyKey): string | undefined {
43
39
 
44
40
  /**
45
41
  * `editor` as `eval` binds it: the same object, except that reading one of the
46
- * hinted names it does NOT have throws the hint, naming `binding.member`.
42
+ * hinted names it does NOT have throws the hint, naming `editor.member`.
47
43
  * Thrown on READ, not on call, so `editor.camera.position` is caught as well as
48
44
  * `editor.setCamera(...)`.
49
45
  *
50
46
  * Methods come back bound to the real object: `LiveEditor` keeps its client in
51
47
  * a `#`-private field, which a call whose `this` is the proxy cannot read.
52
48
  */
53
- export function withEditorMemberHints<T extends object>(target: T, binding = 'editor'): T {
49
+ export function withEditorMemberHints<T extends object>(target: T): T {
54
50
  return new Proxy(target, {
55
51
  get(object, key) {
56
52
  if (!(key in object)) {
57
53
  const hint = editorMemberHint(key);
58
- if (hint !== undefined) throw new TypeError(`${binding}.${String(key)} does not exist. ${hint}`);
54
+ if (hint !== undefined) throw new TypeError(`editor.${String(key)} does not exist. ${hint}`);
59
55
  }
60
56
  const value: unknown = Reflect.get(object, key, object);
61
57
  return typeof value === 'function' ? (value as (...args: unknown[]) => unknown).bind(object) : value;
@@ -0,0 +1,38 @@
1
+ /** Project creation is executable product code, shared by its CLI and the host. */
2
+ export interface ProductCreateRequest {
3
+ name: string;
4
+ targetDir: string;
5
+ /** A starter the product offers (its `templates`), or none for its default. */
6
+ template?: string;
7
+ /** One of the product's example projects (its `examples`), in place of a template. */
8
+ example?: string;
9
+ }
10
+ export interface ProductCreateResult {
11
+ targetDir: string;
12
+ manifest: unknown;
13
+ }
14
+ /** A starter or an example a product creates, as a picker lists it. */
15
+ interface ProductCreateChoice {
16
+ readonly id: string;
17
+ readonly name: string;
18
+ readonly description?: string;
19
+ }
20
+ export interface ProductCreateDeclaration {
21
+ readonly product: string;
22
+ /** The starters a new project begins from. */
23
+ readonly templates: readonly ProductCreateChoice[];
24
+ /** Finished projects to learn from, each made whole (`create --example <id>`). */
25
+ readonly examples?: readonly ProductCreateChoice[];
26
+ create(request: ProductCreateRequest): Promise<ProductCreateResult>;
27
+ }
28
+ export function assertProductCreateDeclaration(value: unknown, source: string): ProductCreateDeclaration {
29
+ const declaration = value as Partial<ProductCreateDeclaration> | null;
30
+ const choices = (list: unknown): boolean =>
31
+ Array.isArray(list) && !list.some(t => typeof t?.id !== 'string' || typeof t?.name !== 'string');
32
+ if (!declaration || typeof declaration.product !== 'string' ||
33
+ typeof declaration.create !== 'function' || !choices(declaration.templates) ||
34
+ (declaration.examples !== undefined && !choices(declaration.examples))) {
35
+ throw new Error(`${source}: expected a product, templates (and any examples) and create(request) function`);
36
+ }
37
+ return declaration as ProductCreateDeclaration;
38
+ }
@@ -0,0 +1,327 @@
1
+
2
+
3
+ import { existsSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { installedPackageDir } from './installed-package';
6
+ import { declaredContributions, type ProductDeclaration, readVolterManifest } from './volter-manifest';
7
+
8
+
9
+ export const PRODUCT_DECLARATION_KEY = 'contributes.volter.product';
10
+
11
+ /** Light or dark — the two the workbench's own `ColorScheme` has plain names
12
+ * for, and the only two a product needs to state (a high-contrast scheme is a
13
+ * person's accessibility choice, never a product's declaration). */
14
+ export type ProductColorTheme = 'dark' | 'light';
15
+
16
+
17
+ export interface ProductWorkbench {
18
+ /** The GitHub Release tag on `volter-ai/code-oss`. */
19
+ readonly release: string;
20
+ /** The sha256 of that release's tarball asset, hex. */
21
+ readonly tarballSha256: string;
22
+ }
23
+
24
+ /** A product package, as everything outside it needs to know it. */
25
+ export interface ProductIdentity {
26
+
27
+ readonly name: string;
28
+ /** Its version, reported beside the workbench by the editor's `status` command. */
29
+ readonly version: string;
30
+
31
+ readonly command: string;
32
+ /** The name a person sees — `contributes.volter.product.displayName`. */
33
+ readonly displayName: string;
34
+ /**
35
+ * `contributes.volter.product.upgrade: true` — its command has `upgrade [version]`
36
+ * (the product CLI's own), so a pinned-engine refusal
37
+ * may tell a person to run it. Optional: a product that says nothing has none.
38
+ */
39
+ readonly upgrade?: boolean;
40
+ /**
41
+ * `contributes.volter.product.adapterRequired: true` — a project this product opens must have its own
42
+ * adapter module (`editor/volter.adapter.ts`), because the product's layout and document
43
+ * finders are declared there and the native default shows none of them. Such a product refuses
44
+ * a project without one. Optional: a product that says nothing opens it on the native default.
45
+ */
46
+ readonly adapterRequired?: boolean;
47
+ /** Its package root, absolute. */
48
+ readonly dir: string;
49
+ /**
50
+ * Its ONE source entry, package-relative and POSIX (`./src/index.ts`). That
51
+ * entry IS the product's frame entry: it composes and re-exports `mountVolter`,
52
+ * which is the name the fork's contribution reads off the served module.
53
+ */
54
+ readonly entry: string;
55
+ /**
56
+ * The packages this product mounts, in order — `contributes.volter.product.composes`. The ONE statement of
57
+ * the composition: the build serves it to the entry as `volter:composition`, and the session
58
+ * reads it here for the catalog and the serving doors.
59
+ */
60
+ readonly composes: readonly string[];
61
+
62
+ readonly colorTheme: ProductColorTheme;
63
+ /** WHICH WORKBENCH THIS PRODUCT IS — see {@link ProductWorkbench}. */
64
+ readonly workbench: ProductWorkbench | null;
65
+ /**
66
+ * The ONE LINE that installs this product and starts a project in it —
67
+ * `contributes.volter.product.install` (`npx <name> create my-project`). A limited view quotes
68
+ * it wherever it cannot do what the local editor does (its agent answers with it, and every
69
+ * route it does not carry names it), so it is a declaration read without running the product.
70
+ * An undeclared one is `npx <name> create my-project`, which is what every product's `create`
71
+ * verb accepts.
72
+ */
73
+ readonly install: string;
74
+ }
75
+
76
+ /** What a project without a product is told to add: the declaration this module reads, naming no package. */
77
+ export const PRODUCT_DECLARATION_HINT =
78
+ `a product package as a dev dependency: one whose package.json declares \`${PRODUCT_DECLARATION_KEY}\``;
79
+
80
+ const PRODUCT_COLOR_THEMES: readonly ProductColorTheme[] = ['dark', 'light'];
81
+
82
+ /**
83
+ * Read one product package's declaration, or `null` when the manifest is not a
84
+ * product's. Throws when it declares `contributes.volter.product` and the declaration is
85
+ * unusable — a half-written product must be named, never skipped.
86
+ */
87
+ export function readProductManifest(packageDir: string): ProductIdentity | null {
88
+ const manifestPath = join(packageDir, 'package.json');
89
+ const manifest = readVolterManifest(manifestPath);
90
+ const declared = manifest?.product;
91
+ if (manifest === null || declared === undefined) return null;
92
+ const entry = declared.entry;
93
+ if (typeof entry !== 'string' || !entry.startsWith('./'))
94
+ throw new Error(
95
+ `${manifestPath} declares ${PRODUCT_DECLARATION_KEY} but its "entry" is ` +
96
+ `${JSON.stringify(entry)}. It must be a ./-relative module — the product's ONE source ` +
97
+ 'entry, which is also its frame entry (e.g. "./src/index.ts").',
98
+ );
99
+ const resolved = join(packageDir, entry);
100
+ if (!existsSync(resolved))
101
+ throw new Error(
102
+ `${manifestPath} declares ${PRODUCT_DECLARATION_KEY}.entry ${JSON.stringify(entry)}, ` +
103
+ `which does not exist (${resolved}).`,
104
+ );
105
+ const colorTheme = declared.colorTheme;
106
+ // REQUIRED, like `entry`, and for the same reason: a product that does not
107
+ // say which way round it paints gets the workbench's web default, which is
108
+ // LIGHT, and its cover is white until its real theme loads. There is no
109
+ // defensible default to invent here — a dark product and a light product are
110
+ // both ordinary — so an unstated one is named rather than guessed.
111
+ if (typeof colorTheme !== 'string' || !PRODUCT_COLOR_THEMES.includes(colorTheme as never))
112
+ throw new Error(
113
+ `${manifestPath} declares ${PRODUCT_DECLARATION_KEY} but its "colorTheme" is ` +
114
+ `${JSON.stringify(colorTheme)}. It must be ${PRODUCT_COLOR_THEMES.map((one) =>
115
+ JSON.stringify(one),
116
+ ).join(' or ')} — which way round this product paints, read before any of its code ` +
117
+ "runs so the workbench's first frame is already the product's own background.",
118
+ );
119
+ // REQUIRED, for the same reason as `entry`: the kit names verbs in every
120
+ // message it writes, and there is no command to name but the product's own.
121
+ const composes = productComposes(manifestPath, declared);
122
+ const bin = manifest.bin;
123
+ const commands = bin !== null && typeof bin === 'object' ? Object.keys(bin) : [];
124
+ if (commands.length !== 1)
125
+ throw new Error(
126
+ `${manifestPath} declares ${PRODUCT_DECLARATION_KEY} but its "bin" names ` +
127
+ `${commands.length === 0 ? 'no command' : `${commands.length} commands`}. A product has ` +
128
+ 'exactly one command — the one every session message tells a person to run.',
129
+ );
130
+ const displayName = declared.displayName;
131
+ if (typeof displayName !== 'string' || displayName.trim() === '')
132
+ throw new Error(
133
+ `${manifestPath} declares ${PRODUCT_DECLARATION_KEY} but its "displayName" is ` +
134
+ `${JSON.stringify(displayName)}. It must be the name a person sees (e.g. "Volter Editor").`,
135
+ );
136
+ const name = manifest.name ?? packageDir;
137
+ const install = declared.install;
138
+ if (install !== undefined && (typeof install !== 'string' || install.trim() === ''))
139
+ throw new Error(
140
+ `${manifestPath} declares ${PRODUCT_DECLARATION_KEY} but its "install" is ` +
141
+ `${JSON.stringify(install)}. It is the one line that installs this product and starts a ` +
142
+ `project (e.g. "npx ${name} create my-project"), or absent.`,
143
+ );
144
+ return {
145
+ name,
146
+ version: manifest.version ?? 'unknown',
147
+ command: commands[0]!,
148
+ displayName,
149
+ upgrade: declared.upgrade === true,
150
+ adapterRequired: declared.adapterRequired === true,
151
+ dir: packageDir,
152
+ entry,
153
+ composes,
154
+ colorTheme: colorTheme as ProductColorTheme,
155
+ // A private source product may use an explicitly selected matching checkout.
156
+ // Public packages must always carry a downloadable, checksummed workbench.
157
+ workbench: manifest.private && declared.workbench === undefined
158
+ ? null : readProductWorkbench(manifestPath, declared.workbench),
159
+ install: typeof install === 'string' ? install.trim() : `npx ${name} create my-project`,
160
+ };
161
+ }
162
+
163
+ /** A product declaration's `composes`, checked: the packages it mounts, by name, in order. The
164
+ * build's composition module and the session read it through this one check. */
165
+ export function productComposes(manifestPath: string, declared: ProductDeclaration): string[] {
166
+ const composes = declared.composes;
167
+ if (!Array.isArray(composes) || composes.some((name) => typeof name !== 'string' || name === ''))
168
+ throw new Error(
169
+ `${manifestPath} declares ${PRODUCT_DECLARATION_KEY} but its "composes" is ` +
170
+ `${JSON.stringify(composes)}. It must list the packages this product mounts, by name ` +
171
+ '(e.g. ["<package>"]); each is a dependency of the product package.',
172
+ );
173
+ return composes as string[];
174
+ }
175
+
176
+ /** The sha256 a release pins its tarball with, as `BUILD.json` writes it. */
177
+ const SHA256_HEX = /^[0-9a-f]{64}$/;
178
+
179
+ /**
180
+ * REQUIRED, like `entry` and `colorTheme`, and for the sharpest reason of all
181
+ * three of them: a product that does not say which workbench it is cannot be INSTALLED
182
+ * — `npx @volter/<product> create <name>` on a machine with nothing else on it
183
+ * has no other way to find the bytes it runs in, and the person is left writing
184
+ * a path to a directory they would have to build first. There is no default to
185
+ * invent (the tag names a fork commit nobody can guess), so an unstated one is
186
+ * named rather than guessed.
187
+ */
188
+ function readProductWorkbench(manifestPath: string, declared: unknown): ProductWorkbench {
189
+ const example =
190
+ ' "contributes": { "volter": { "product": { "workbench": { "release": "<product>-<fork sha 12>-<platform>", ' +
191
+ '"tarballSha256": "<64 hex>" } } } }\n' +
192
+ '`node scripts/workbench/build-release.mjs --publish --out <release dir>` cuts the release ' +
193
+ 'and prints exactly that block.';
194
+ if (typeof declared !== 'object' || declared === null || Array.isArray(declared))
195
+ throw new Error(
196
+ `${manifestPath} declares ${PRODUCT_DECLARATION_KEY} but its "workbench" is ` +
197
+ `${JSON.stringify(declared)}. A product IS the Code-OSS workbench it was compiled into, ` +
198
+ `so it names the published release those bytes are:\n${example}`,
199
+ );
200
+ const platforms = declared as Record<string, unknown>;
201
+ if (!('release' in platforms) && !('tarballSha256' in platforms)) {
202
+ const machine = `${process.platform}-${process.arch}`;
203
+ const declaredHere = platforms[machine];
204
+ if (declaredHere === undefined)
205
+ throw new Error(
206
+ `${manifestPath}'s ${PRODUCT_DECLARATION_KEY}.workbench declares releases for ` +
207
+ `${Object.keys(platforms).join(', ') || 'no platform'} and this machine is ${machine}. A ` +
208
+ "Code-OSS server package carries its own platform's node binary and native modules, so " +
209
+ `there is nothing this product can run in here until a ${machine} release is cut:\n` +
210
+ ` node scripts/workbench/build-release.mjs --product <id> --platform ${machine} --checkout <fork dir> --publish`,
211
+ );
212
+ return readPlatformWorkbench(manifestPath, declaredHere, example);
213
+ }
214
+ return readPlatformWorkbench(manifestPath, declared, example);
215
+ }
216
+
217
+ function readPlatformWorkbench(manifestPath: string, declared: unknown, example: string): ProductWorkbench {
218
+ if (typeof declared !== 'object' || declared === null || Array.isArray(declared))
219
+ throw new Error(
220
+ `${manifestPath}'s ${PRODUCT_DECLARATION_KEY}.workbench names ${JSON.stringify(declared)} ` +
221
+ `for this platform, which is not a release:\n${example}`,
222
+ );
223
+ const record = declared as Record<string, unknown>;
224
+ const extra = Object.keys(record).filter((key) => key !== 'release' && key !== 'tarballSha256');
225
+ if (extra.length > 0)
226
+ throw new Error(
227
+ `${manifestPath}'s ${PRODUCT_DECLARATION_KEY}.workbench carries ` +
228
+ `${extra.map((key) => `"${key}"`).join(', ')}, which this declaration has no meaning for. ` +
229
+ `It names the release and the pin, and nothing else:\n${example}`,
230
+ );
231
+ const release = record['release'];
232
+ if (typeof release !== 'string' || release.trim() === '')
233
+ throw new Error(
234
+ `${manifestPath}'s ${PRODUCT_DECLARATION_KEY}.workbench names no "release" — the GitHub ` +
235
+ `Release tag its workbench was published under:\n${example}`,
236
+ );
237
+ const tarballSha256 = record['tarballSha256'];
238
+ if (typeof tarballSha256 !== 'string' || !SHA256_HEX.test(tarballSha256))
239
+ throw new Error(
240
+ `${manifestPath}'s ${PRODUCT_DECLARATION_KEY}.workbench has tarballSha256 ` +
241
+ `${JSON.stringify(tarballSha256)}, which is not 64 hex characters. It is the PIN — the ` +
242
+ 'hash the downloaded tarball is checked against before a byte of it is extracted, so it ' +
243
+ `is never approximate:\n${example}`,
244
+ );
245
+ return { release, tarballSha256 };
246
+ }
247
+
248
+ /**
249
+ * A product's SHORT id — `@<scope>/<name>` → `<name>`.
250
+ *
251
+ * It is the product's own `product({ id })`, the directory its workbench half
252
+ * lives in (`packages/<id>/workbench`), the `--product` flag the overlay and the
253
+ * release build take, and what a release records in `BUILD.json`. One derivation
254
+ * rather than four literals: the package name is the only place the product is
255
+ * NAMED, and everything else is that name's last segment.
256
+ */
257
+ export function workbenchProductId(packageName: string): string {
258
+ return packageName.split('/').pop() ?? packageName;
259
+ }
260
+
261
+ /** A product's entry as a path relative to `fromDir`, POSIX — the spelling a
262
+ * Vite manifest key and a dev-server URL both use. */
263
+ export function productEntryPath(product: ProductIdentity, fromDir: string): string {
264
+ const absolute = join(product.dir, product.entry);
265
+ const prefix = fromDir.endsWith('/') ? fromDir : `${fromDir}/`;
266
+ const relative = absolute.startsWith(prefix) ? absolute.slice(prefix.length) : absolute;
267
+ return relative.split('\\').join('/');
268
+ }
269
+
270
+ /** Every `@volter/*` this project declares, in either section, sorted. */
271
+ function declaredVolterPackages(projectRoot: string): string[] {
272
+ const manifest = readVolterManifest(join(projectRoot, 'package.json'));
273
+ return (manifest?.dependencyNames ?? []).filter((name) => name.startsWith('@volter/')).sort();
274
+ }
275
+
276
+ /**
277
+ * The packages `product` mounts that are installed and contribute: its `contributes.volter.product.composes`,
278
+ * each resolved from the product's own directory and kept when its manifest declares
279
+ * a `contributes.volter` object (its serving half, `contributes.volter.serving`, may be all a package contributes).
280
+ */
281
+ export function productComposedPackages(product: ProductIdentity): string[] {
282
+ return product.composes.filter((name) => {
283
+ const dir = installedPackageDir(product.dir, name);
284
+ if (dir === null) return false;
285
+ const manifest = readVolterManifest(join(dir, 'package.json'));
286
+ return declaredContributions(manifest) !== null;
287
+ });
288
+ }
289
+
290
+ /** The products this project declares AND resolves, in declaration order. */
291
+ export function declaredProducts(projectRoot: string): ProductIdentity[] {
292
+ const found: ProductIdentity[] = [];
293
+ for (const name of declaredVolterPackages(projectRoot)) {
294
+ const dir = installedPackageDir(projectRoot, name);
295
+ if (dir === null) continue;
296
+ const product = readProductManifest(dir);
297
+ if (product !== null) found.push(product);
298
+ }
299
+ return found;
300
+ }
301
+
302
+ /**
303
+ * THE product this project opens in, or a throw naming what is wrong.
304
+ *
305
+ * The refusals are the whole point: a project with none and a project with two
306
+ * are both states a person can be in, and neither has an answer this code may
307
+ * invent.
308
+ */
309
+ export function resolveProductForProject(projectRoot: string): ProductIdentity {
310
+ const products = declaredProducts(projectRoot);
311
+ if (products.length === 1) return products[0] as ProductIdentity;
312
+ if (products.length === 0)
313
+ throw new Error(
314
+ `${projectRoot} declares no editor product, so there is no editor to open it in.\n` +
315
+ 'The installed product supplies the editor,\n' +
316
+ 'and which one runs is what the project installed, never a flag.\n' +
317
+ `For THIS folder, install ${PRODUCT_DECLARATION_HINT},\n` +
318
+ "then run that product's `edit` command again. For a NEW project, the product's own `create` verb makes it.",
319
+ );
320
+ throw new Error(
321
+ `${projectRoot} declares ${products.length} editor products — ` +
322
+ `${products.map((product) => product.name).join(' and ')}.\n` +
323
+ 'One page runs one product, and nothing here may choose between them. Remove the one you\n' +
324
+ 'are not building with:\n' +
325
+ products.map((product) => ` npm uninstall ${product.name}`).join('\n'),
326
+ );
327
+ }