@volter/editor-live 0.5.64 → 0.5.66

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/BUNDLED_NOTICES CHANGED
@@ -3,8 +3,9 @@ Bundled license notices
3
3
  These notices accompany code incorporated into this package. Separate npm dependencies retain their own licenses.
4
4
 
5
5
 
6
- @volter/editor-project@0.5.64
6
+ @volter/editor-project@0.5.66
7
7
  Declared license: Apache-2.0
8
+ Source: the unmodified npm package @volter/editor-project@0.5.66, from https://github.com/volter-ai/editor.git
8
9
 
9
10
  --- packages/editor-project/LICENSE ---
10
11
 
@@ -212,8 +213,9 @@ Declared license: Apache-2.0
212
213
  limitations under the License.
213
214
 
214
215
 
215
- @volter/editor-sdk@0.5.64
216
+ @volter/editor-sdk@0.5.66
216
217
  Declared license: Apache-2.0
218
+ Source: the unmodified npm package @volter/editor-sdk@0.5.66, from https://github.com/volter-ai/editor.git
217
219
 
218
220
  --- packages/editor-sdk/LICENSE ---
219
221
 
@@ -50,6 +50,8 @@ export interface DocumentKeyOptions extends DocumentGestureOptions {
50
50
  metaKey?: boolean;
51
51
  shiftKey?: boolean;
52
52
  altKey?: boolean;
53
+ /** Hold the key this long before releasing it (a person holding W). */
54
+ holdMs?: number;
53
55
  }
54
56
  export interface DocumentPasteOptions extends DocumentGestureOptions {
55
57
  selector?: string;
package/dist/editor.d.ts CHANGED
@@ -28,7 +28,7 @@ export declare class LiveEditor {
28
28
  readonly document: LiveEditorDocument;
29
29
  constructor(client: EditorClient);
30
30
  /**
31
- * THE BLENDER LANE'S VERBS, from `volter-editor eval`.
31
+ * THE BLENDER LANE'S VERBS, from `volter-model-editor eval`.
32
32
  *
33
33
  * Blender runs headless in the editor tab's worker (ARCHITECTURE-CORE, "THE
34
34
  * BLENDER IN THE TAB IS BLENDER") and answers `blender-start`,
@@ -40,7 +40,7 @@ export declare class LiveEditor {
40
40
  * per question — the same discovery failure `eval-surface.ts`'s header
41
41
  * records, in a lane that had not noticed it yet.
42
42
  *
43
- * volter-editor eval "await editor.blender('blender-execute', { code: 'import bpy; print(len(bpy.data.objects))' })"
43
+ * volter-model-editor eval "await editor.blender('blender-execute', { code: 'import bpy; print(len(bpy.data.objects))' })"
44
44
  *
45
45
  * `blender-status` is the only verb that creates nothing: it answers whether
46
46
  * this tab already has a session without starting one.
@@ -70,6 +70,9 @@ export declare class LiveEditor {
70
70
  * `@volter/editor-sdk`'s `CaptureDimensions`.
71
71
  * Supply a view to present and photograph it in one editor request.
72
72
  */
73
+ /** The active document's RENDER alone, at the size asked for: what the document draws,
74
+ * without what the viewport draws over it (its navigation gizmo, its readouts). To see a
75
+ * stage as the person does, use `captureEditorChrome({ region: 'document' })`. */
73
76
  captureActiveDocument(size?: CaptureDimensions, view?: EditorView): Promise<ActiveDocumentCapture>;
74
77
  /**
75
78
  * Photograph the editor PAGE — every panel, tab strip and viewport as the
@@ -159,12 +162,21 @@ export declare class LiveEditor {
159
162
  */
160
163
  workspace(id: EditorWorkspaceName): Promise<void>;
161
164
  /**
162
- * Apply a STYLE BUNDLE by id — the chrome's palette, material, icon set and
163
- * region defaults in one gesture, the session door beside
165
+ * Apply a STYLE BUNDLE by id — the chrome's palette, material and icon set in
166
+ * one gesture, the session door beside
164
167
  * `View → <Style> Style`. A bundle the open project does not offer refuses
165
168
  * and names the vocabulary; `currentView().style` reports the one worn.
166
169
  */
167
170
  style(id: string): Promise<void>;
171
+ /**
172
+ * A document stage's viewport PRESENTATION — its draw mode, lighting, backdrop and overlays
173
+ * (`@volter/editor-sdk/kit/viewport-presentation`) — resolved. With `layer`, that choice is
174
+ * recorded for the view first, as a person's toolbar change would be, e.g.
175
+ * `presentation('model:src/models/cube.blend', { all: { lighting: { studioPreset: 'kit' } } })`.
176
+ * With a STRING, the named view of that id (`*.view.ts`, e.g. `'godot'`) is put on the view
177
+ * whole; the answer's `viewPresets` lists the ones registered.
178
+ */
179
+ presentation(documentId: string, layer?: Parameters<EditorClient['viewportPresentation']>[1]): ReturnType<EditorClient['viewportPresentation']>;
168
180
  /**
169
181
  * Set the MATERIAL apart from the bundle that usually carries it.
170
182
  * Appearance is palette × material, independent axes by ruling, so
@@ -392,6 +404,8 @@ export declare class LiveEditor {
392
404
  * queue — the same one the keyboard shortcut drives. */
393
405
  undo(): Promise<HistoryStep>;
394
406
  redo(): Promise<HistoryStep>;
407
+ /** Reload the editor's page and resolve once the new document answers. */
408
+ reloadPage(): Promise<void>;
395
409
  /** Mirrors `vgai status` — the full live editor state as JSON. */
396
410
  status(): Promise<EditorState>;
397
411
  /** A live viewport PNG (`EditorClient.captureViewport`) — no direct CLI verb exists; this is the closest wire read. */
package/dist/index.js CHANGED
@@ -168,7 +168,7 @@ var EditorClient = class {
168
168
  commandDispatcher = void 0;
169
169
  if (error?.name === "TimeoutError") {
170
170
  throw new EditorCommandError(
171
- `The editor at ${this.baseUrl} never answered "${type}" within ${Math.round(deadlineMs / 1e3)}s \u2014 past every server-side budget, so the server itself is not answering. Check the terminal running \`volter-editor edit\`.`,
171
+ `The editor at ${this.baseUrl} never answered "${type}" within ${Math.round(deadlineMs / 1e3)}s \u2014 past every server-side budget, so the server itself is not answering. Check the terminal that started this editor session.`,
172
172
  void 0,
173
173
  true
174
174
  );
@@ -180,7 +180,7 @@ var EditorClient = class {
180
180
  continue;
181
181
  }
182
182
  throw new EditorCommandError(
183
- `POST ${url} ("${type}") never reached the editor: ${code}${detail ? ` (${detail})` : ""}.` + (retried ? ` Retried once after ${TRANSPORT_RETRY_DELAY_MS}ms; it failed the same way.` : "") + (options?.retryTransport === true ? "" : " Not retried automatically: this command can change editor state, and a socket that died after the request was written cannot prove the editor did not already run it.") + " A transport failure means the port stopped answering, not that the editor refused \u2014 the dev server restarts on any watched source edit, and it shuts itself down after an idle window. Check the terminal running `volter-editor edit` and confirm the port this client resolved.",
183
+ `POST ${url} ("${type}") never reached the editor: ${code}${detail ? ` (${detail})` : ""}.` + (retried ? ` Retried once after ${TRANSPORT_RETRY_DELAY_MS}ms; it failed the same way.` : "") + (options?.retryTransport === true ? "" : " Not retried automatically: this command can change editor state, and a socket that died after the request was written cannot prove the editor did not already run it.") + " A transport failure means the port stopped answering, not that the editor refused \u2014 the dev server restarts on any watched source edit, and it shuts itself down after an idle window. Check the terminal that started this editor session and confirm the port this client resolved.",
184
184
  code,
185
185
  false
186
186
  );
@@ -558,10 +558,21 @@ var EditorClient = class {
558
558
  }
559
559
  /** Apply a STYLE BUNDLE — palette, material, icon set and region defaults
560
560
  * in one gesture (`classic`/`glass`/`maya`/`substance`, or one a package
561
- * the project declares carries, `blender`). */
561
+ * the project or product composes carries, such as `blender`). */
562
562
  async setStyle(style) {
563
563
  await this.command({ type: "set-style", style });
564
564
  }
565
+ /** A document stage's viewport PRESENTATION (`kit/viewport-presentation`), resolved; with a
566
+ * `layer`, that choice is recorded for the view first, as the toolbar records it. */
567
+ /** A view's presentation, answered resolved. With a LAYER the person's choice is recorded
568
+ * first; with a STRING, the named view (`*.view.ts`) of that id is put on the view whole. */
569
+ async viewportPresentation(documentId, layer) {
570
+ return this.command({
571
+ type: "viewport-presentation",
572
+ documentId,
573
+ ...typeof layer === "string" ? { preset: layer } : layer ? { layer } : {}
574
+ });
575
+ }
565
576
  /** Set the MATERIAL apart from the bundle that usually carries it.
566
577
  * Answers with what the chrome wears afterwards. */
567
578
  async setAppearance(appearance) {
@@ -772,7 +783,8 @@ var EditorClient = class {
772
783
  async captureEditorChrome(options) {
773
784
  return this.command({
774
785
  type: "capture-editor-chrome",
775
- ...options?.scale === void 0 ? {} : { scale: options.scale }
786
+ ...options?.scale === void 0 ? {} : { scale: options.scale },
787
+ ...options?.region === void 0 ? {} : { region: options.region }
776
788
  });
777
789
  }
778
790
  /** With a view, present and capture it in one request so document discovery
@@ -960,6 +972,50 @@ var EditorClient = class {
960
972
  async documentTable() {
961
973
  return this.command({ type: "document-table" });
962
974
  }
975
+ /**
976
+ * Reload the editor's page and wait until the new document answers — the
977
+ * host's `page-reload`, for a product with no game client (`@volter/game-live`'s
978
+ * `page.reload()` is the same order with the same two witnesses). A tab's
979
+ * epoch count rising proves a new document loaded; a host command answering
980
+ * proves it can be driven. It is sent only once that tab reports its command
981
+ * listener ready, because a command sent before is filed as a stall.
982
+ */
983
+ async reloadPage(timeoutMs = 6e4) {
984
+ const epochs = async () => {
985
+ const tabs = (await this.getState().catch(() => ({ tabs: [] }))).tabs ?? [];
986
+ const byTab = tabs;
987
+ return new Map(byTab.map((tab) => [tab.tabId8, tab]));
988
+ };
989
+ const pause = () => new Promise((settle) => setTimeout(settle, 250));
990
+ const before = await epochs();
991
+ const deadline = Date.now() + timeoutMs;
992
+ await this.command({ type: "page-reload" });
993
+ let reloaded = [];
994
+ while (reloaded.length === 0) {
995
+ if (Date.now() > deadline) throw new Error("The reload was ordered and no tab of this session reported a new page load.");
996
+ await pause();
997
+ const now = await epochs();
998
+ reloaded = [...now].filter(([tabId, tab]) => tab.epochCount > (before.get(tabId)?.epochCount ?? 0)).map(([tabId]) => tabId);
999
+ }
1000
+ for (; ; ) {
1001
+ if (Date.now() > deadline) throw new Error("A new page loaded, but its command listener did not become ready.");
1002
+ const now = await epochs();
1003
+ if (reloaded.some((tabId) => {
1004
+ const listener = now.get(tabId)?.commandListener;
1005
+ return typeof listener !== "string" || listener === "ready";
1006
+ })) break;
1007
+ await pause();
1008
+ }
1009
+ for (; ; ) {
1010
+ try {
1011
+ await this.command({ type: "current-view" });
1012
+ return;
1013
+ } catch (error) {
1014
+ if (Date.now() > deadline) throw error;
1015
+ await pause();
1016
+ }
1017
+ }
1018
+ }
963
1019
  async getState() {
964
1020
  const res = await this.httpFetch(`${this.baseUrl}/__editor/state`);
965
1021
  const state = await this.readJson(res);
@@ -1017,7 +1073,7 @@ var EditorClient = class {
1017
1073
  if (error?.name === "TimeoutError") throw error;
1018
1074
  const { code, detail } = describeFetchFailure(error);
1019
1075
  throw new EditorCommandError(
1020
- `${init?.method ?? "GET"} ${url} never reached the editor: ${code}${detail ? ` (${detail})` : ""}. Nothing answered on that port \u2014 check the terminal running \`volter-editor edit\` and confirm the port this client resolved.`,
1076
+ `${init?.method ?? "GET"} ${url} never reached the editor: ${code}${detail ? ` (${detail})` : ""}. Nothing answered on that port \u2014 check the terminal that started this editor session and confirm the port this client resolved.`,
1021
1077
  code,
1022
1078
  false
1023
1079
  );
@@ -1033,7 +1089,7 @@ var EditorClient = class {
1033
1089
  const contentType = res.headers.get("content-type") ?? "";
1034
1090
  if (!contentType.includes("application/json")) {
1035
1091
  throw new Error(
1036
- `The editor at ${this.baseUrl} answered with its page fallback (${contentType || "no content-type"}) rather than JSON, so no editor server handled the request. Check that this URL is a running \`volter-editor edit\` session.`
1092
+ `The editor at ${this.baseUrl} answered with its page fallback (${contentType || "no content-type"}) rather than JSON, so no editor server handled the request. Check that this URL is a running editor session.`
1037
1093
  );
1038
1094
  }
1039
1095
  const body = await res.json();
@@ -1252,7 +1308,7 @@ function inferAssetKind(path) {
1252
1308
  return EXTENSION_KIND[ext] ?? "json";
1253
1309
  }
1254
1310
  var LiveEditor = class {
1255
- /** `#`-private, not `private`: `volter-editor eval --list` enumerates this object's
1311
+ /** `#`-private, not `private`: `volter-model-editor eval --list` enumerates this object's
1256
1312
  * real runtime members, and TypeScript's erased `private` would leave the
1257
1313
  * raw `EditorClient` advertised beside them. */
1258
1314
  #client;
@@ -1270,7 +1326,7 @@ var LiveEditor = class {
1270
1326
  this.document = new LiveEditorDocument(client);
1271
1327
  }
1272
1328
  /**
1273
- * THE BLENDER LANE'S VERBS, from `volter-editor eval`.
1329
+ * THE BLENDER LANE'S VERBS, from `volter-model-editor eval`.
1274
1330
  *
1275
1331
  * Blender runs headless in the editor tab's worker (ARCHITECTURE-CORE, "THE
1276
1332
  * BLENDER IN THE TAB IS BLENDER") and answers `blender-start`,
@@ -1282,7 +1338,7 @@ var LiveEditor = class {
1282
1338
  * per question — the same discovery failure `eval-surface.ts`'s header
1283
1339
  * records, in a lane that had not noticed it yet.
1284
1340
  *
1285
- * volter-editor eval "await editor.blender('blender-execute', { code: 'import bpy; print(len(bpy.data.objects))' })"
1341
+ * volter-model-editor eval "await editor.blender('blender-execute', { code: 'import bpy; print(len(bpy.data.objects))' })"
1286
1342
  *
1287
1343
  * `blender-status` is the only verb that creates nothing: it answers whether
1288
1344
  * this tab already has a session without starting one.
@@ -1321,6 +1377,9 @@ var LiveEditor = class {
1321
1377
  * `@volter/editor-sdk`'s `CaptureDimensions`.
1322
1378
  * Supply a view to present and photograph it in one editor request.
1323
1379
  */
1380
+ /** The active document's RENDER alone, at the size asked for: what the document draws,
1381
+ * without what the viewport draws over it (its navigation gizmo, its readouts). To see a
1382
+ * stage as the person does, use `captureEditorChrome({ region: 'document' })`. */
1324
1383
  async captureActiveDocument(size, view) {
1325
1384
  return this.#client.captureActiveDocument(size, view);
1326
1385
  }
@@ -1420,14 +1479,25 @@ var LiveEditor = class {
1420
1479
  await this.#client.setWorkspace(id);
1421
1480
  }
1422
1481
  /**
1423
- * Apply a STYLE BUNDLE by id — the chrome's palette, material, icon set and
1424
- * region defaults in one gesture, the session door beside
1482
+ * Apply a STYLE BUNDLE by id — the chrome's palette, material and icon set in
1483
+ * one gesture, the session door beside
1425
1484
  * `View → <Style> Style`. A bundle the open project does not offer refuses
1426
1485
  * and names the vocabulary; `currentView().style` reports the one worn.
1427
1486
  */
1428
1487
  async style(id) {
1429
1488
  await this.#client.setStyle(id);
1430
1489
  }
1490
+ /**
1491
+ * A document stage's viewport PRESENTATION — its draw mode, lighting, backdrop and overlays
1492
+ * (`@volter/editor-sdk/kit/viewport-presentation`) — resolved. With `layer`, that choice is
1493
+ * recorded for the view first, as a person's toolbar change would be, e.g.
1494
+ * `presentation('model:src/models/cube.blend', { all: { lighting: { studioPreset: 'kit' } } })`.
1495
+ * With a STRING, the named view of that id (`*.view.ts`, e.g. `'godot'`) is put on the view
1496
+ * whole; the answer's `viewPresets` lists the ones registered.
1497
+ */
1498
+ async presentation(documentId, layer) {
1499
+ return this.#client.viewportPresentation(documentId, layer);
1500
+ }
1431
1501
  /**
1432
1502
  * Set the MATERIAL apart from the bundle that usually carries it.
1433
1503
  * Appearance is palette × material, independent axes by ruling, so
@@ -1710,6 +1780,10 @@ var LiveEditor = class {
1710
1780
  async redo() {
1711
1781
  return this.#client.redo();
1712
1782
  }
1783
+ /** Reload the editor's page and resolve once the new document answers. */
1784
+ async reloadPage() {
1785
+ await this.#client.reloadPage();
1786
+ }
1713
1787
  /** Mirrors `vgai status` — the full live editor state as JSON. */
1714
1788
  async status() {
1715
1789
  return this.#client.getState();
@@ -1786,8 +1860,8 @@ function lazyChainProxy(resolveRoot, path = []) {
1786
1860
  }
1787
1861
 
1788
1862
  // src/session.ts
1789
- import { existsSync as existsSync2, readFileSync as readFileSync2, realpathSync as realpathSync2 } from "node:fs";
1790
- import { dirname, join as join3, resolve as resolve2 } from "node:path";
1863
+ import { existsSync as existsSync3, readFileSync as readFileSync3, realpathSync as realpathSync2 } from "node:fs";
1864
+ import { dirname as dirname2, join as join4, resolve as resolve2 } from "node:path";
1791
1865
 
1792
1866
  // ../editor-project/src/manifest/locate.ts
1793
1867
  import { existsSync } from "node:fs";
@@ -1812,18 +1886,175 @@ function resolveManifestPath(dir) {
1812
1886
  return join(dir, MANIFEST_FILENAME);
1813
1887
  }
1814
1888
 
1889
+ // ../editor-sdk/src/session/product-locator.ts
1890
+ import { existsSync as existsSync2, readFileSync } from "node:fs";
1891
+ import { createRequire } from "node:module";
1892
+ import { dirname, join as join2 } from "node:path";
1893
+ import { pathToFileURL } from "node:url";
1894
+ var PRODUCT_DECLARATION_KEY = "vgai.product";
1895
+ var PRODUCT_INSTALL_LINES = [
1896
+ " npm install --save-dev @volter/model-editor",
1897
+ " npm install --save-dev @volter/game-editor"
1898
+ ];
1899
+ var PRODUCT_CREATE_LINES = [" npx @volter/editor create <name>"];
1900
+ var PRODUCT_COLOR_THEMES = ["dark", "light"];
1901
+ function readProductManifest(packageDir) {
1902
+ const manifestPath = join2(packageDir, "package.json");
1903
+ let manifest;
1904
+ try {
1905
+ manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
1906
+ } catch {
1907
+ return null;
1908
+ }
1909
+ const declared = manifest.vgai?.product;
1910
+ if (declared === void 0) return null;
1911
+ const entry = declared.entry;
1912
+ if (typeof entry !== "string" || !entry.startsWith("./"))
1913
+ throw new Error(
1914
+ `${manifestPath} declares ${PRODUCT_DECLARATION_KEY} but its "entry" is ${JSON.stringify(entry)}. It must be a ./-relative module \u2014 the product's ONE source entry, which is also its frame entry (e.g. "./src/index.ts").`
1915
+ );
1916
+ const resolved = join2(packageDir, entry);
1917
+ if (!existsSync2(resolved))
1918
+ throw new Error(
1919
+ `${manifestPath} declares ${PRODUCT_DECLARATION_KEY}.entry ${JSON.stringify(entry)}, which does not exist (${resolved}).`
1920
+ );
1921
+ const colorTheme = declared.colorTheme;
1922
+ if (typeof colorTheme !== "string" || !PRODUCT_COLOR_THEMES.includes(colorTheme))
1923
+ throw new Error(
1924
+ `${manifestPath} declares ${PRODUCT_DECLARATION_KEY} but its "colorTheme" is ${JSON.stringify(colorTheme)}. It must be ${PRODUCT_COLOR_THEMES.map(
1925
+ (one) => JSON.stringify(one)
1926
+ ).join(" or ")} \u2014 which way round this product paints, read before any of its code runs so the workbench's first frame is already the product's own background.`
1927
+ );
1928
+ const bin = manifest.bin;
1929
+ const commands = bin !== null && typeof bin === "object" ? Object.keys(bin) : [];
1930
+ if (commands.length !== 1)
1931
+ throw new Error(
1932
+ `${manifestPath} declares ${PRODUCT_DECLARATION_KEY} but its "bin" names ${commands.length === 0 ? "no command" : `${commands.length} commands`}. A product has exactly one command \u2014 the one every session message tells a person to run.`
1933
+ );
1934
+ const displayName = declared.displayName;
1935
+ if (typeof displayName !== "string" || displayName.trim() === "")
1936
+ throw new Error(
1937
+ `${manifestPath} declares ${PRODUCT_DECLARATION_KEY} but its "displayName" is ${JSON.stringify(displayName)}. It must be the name a person sees (e.g. "Volter Editor").`
1938
+ );
1939
+ return {
1940
+ name: typeof manifest.name === "string" ? manifest.name : packageDir,
1941
+ version: typeof manifest.version === "string" ? manifest.version : "unknown",
1942
+ command: commands[0],
1943
+ displayName,
1944
+ dir: packageDir,
1945
+ entry,
1946
+ colorTheme,
1947
+ // A private source product may use an explicitly selected matching checkout.
1948
+ // Public packages must always carry a downloadable, checksummed workbench.
1949
+ workbench: manifest.private === true && declared.workbench === void 0 ? null : readProductWorkbench(manifestPath, declared.workbench)
1950
+ };
1951
+ }
1952
+ var SHA256_HEX = /^[0-9a-f]{64}$/;
1953
+ function readProductWorkbench(manifestPath, declared) {
1954
+ const example = ' "vgai": { "product": { "workbench": { "release": "<product>-<fork sha 12>-<platform>", "tarballSha256": "<64 hex>" } } }\n`node scripts/workbench/build-release.mjs --publish --out <release dir>` cuts the release and prints exactly that block.';
1955
+ if (typeof declared !== "object" || declared === null || Array.isArray(declared))
1956
+ throw new Error(
1957
+ `${manifestPath} declares ${PRODUCT_DECLARATION_KEY} but its "workbench" is ${JSON.stringify(declared)}. A product IS the Code-OSS workbench it was compiled into, so it names the published release those bytes are:
1958
+ ${example}`
1959
+ );
1960
+ const record = declared;
1961
+ const extra = Object.keys(record).filter((key) => key !== "release" && key !== "tarballSha256");
1962
+ if (extra.length > 0)
1963
+ throw new Error(
1964
+ `${manifestPath}'s ${PRODUCT_DECLARATION_KEY}.workbench carries ${extra.map((key) => `"${key}"`).join(", ")}, which this declaration has no meaning for. It names the release and the pin, and nothing else:
1965
+ ${example}`
1966
+ );
1967
+ const release = record["release"];
1968
+ if (typeof release !== "string" || release.trim() === "")
1969
+ throw new Error(
1970
+ `${manifestPath}'s ${PRODUCT_DECLARATION_KEY}.workbench names no "release" \u2014 the GitHub Release tag its workbench was published under:
1971
+ ${example}`
1972
+ );
1973
+ const tarballSha256 = record["tarballSha256"];
1974
+ if (typeof tarballSha256 !== "string" || !SHA256_HEX.test(tarballSha256))
1975
+ throw new Error(
1976
+ `${manifestPath}'s ${PRODUCT_DECLARATION_KEY}.workbench has tarballSha256 ${JSON.stringify(tarballSha256)}, which is not 64 hex characters. It is the PIN \u2014 the hash the downloaded tarball is checked against before a byte of it is extracted, so it is never approximate:
1977
+ ${example}`
1978
+ );
1979
+ return { release, tarballSha256 };
1980
+ }
1981
+ function declaredVgaiPackages(projectRoot) {
1982
+ let manifest;
1983
+ try {
1984
+ manifest = JSON.parse(
1985
+ readFileSync(join2(projectRoot, "package.json"), "utf8")
1986
+ );
1987
+ } catch {
1988
+ return [];
1989
+ }
1990
+ return Object.keys({ ...manifest.dependencies ?? {}, ...manifest.devDependencies ?? {} }).filter((name) => name.startsWith("@volter/")).sort();
1991
+ }
1992
+ function locatePackage(projectRoot, name) {
1993
+ const require2 = createRequire(pathToFileURL(join2(projectRoot, "package.json")));
1994
+ try {
1995
+ return dirname(require2.resolve(`${name}/package.json`));
1996
+ } catch {
1997
+ try {
1998
+ let dir = dirname(require2.resolve(name));
1999
+ for (; ; ) {
2000
+ const candidate = join2(dir, "package.json");
2001
+ try {
2002
+ if (JSON.parse(readFileSync(candidate, "utf8")).name === name)
2003
+ return dir;
2004
+ } catch {
2005
+ }
2006
+ const parent = dirname(dir);
2007
+ if (parent === dir) return null;
2008
+ dir = parent;
2009
+ }
2010
+ } catch {
2011
+ return null;
2012
+ }
2013
+ }
2014
+ }
2015
+ function declaredProducts(projectRoot) {
2016
+ const found = [];
2017
+ for (const name of declaredVgaiPackages(projectRoot)) {
2018
+ const dir = locatePackage(projectRoot, name);
2019
+ if (dir === null) continue;
2020
+ const product = readProductManifest(dir);
2021
+ if (product !== null) found.push(product);
2022
+ }
2023
+ return found;
2024
+ }
2025
+ function resolveProductForProject(projectRoot) {
2026
+ const products = declaredProducts(projectRoot);
2027
+ if (products.length === 1) return products[0];
2028
+ if (products.length === 0)
2029
+ throw new Error(
2030
+ `${projectRoot} declares no editor product, so there is no editor to open it in.
2031
+ The installed product supplies the editor,
2032
+ and which one runs is what the project installed, never a flag.
2033
+ For THIS folder, install the one you are building with:
2034
+ ${PRODUCT_INSTALL_LINES.join("\n")}
2035
+ then run that product's \`edit\` command again. For a NEW project, the product creates it:
2036
+ ${PRODUCT_CREATE_LINES.join("\n")}`
2037
+ );
2038
+ throw new Error(
2039
+ `${projectRoot} declares ${products.length} editor products \u2014 ${products.map((product) => product.name).join(" and ")}.
2040
+ One page runs one product, and nothing here may choose between them. Remove the one you
2041
+ are not building with:
2042
+ ` + products.map((product) => ` npm uninstall ${product.name}`).join("\n")
2043
+ );
2044
+ }
2045
+
1815
2046
  // ../editor-sdk/src/session/registry-format.ts
1816
2047
  import {
1817
2048
  mkdirSync,
1818
2049
  readdirSync,
1819
- readFileSync,
2050
+ readFileSync as readFileSync2,
1820
2051
  realpathSync,
1821
2052
  unlinkSync,
1822
2053
  writeFileSync
1823
2054
  } from "node:fs";
1824
2055
  import { homedir } from "node:os";
1825
- import { join as join2, resolve } from "node:path";
1826
- var EDITOR_SESSIONS_REGISTRY_FILE = join2(homedir(), ".vgai", "editor-sessions.json");
2056
+ import { join as join3, resolve } from "node:path";
2057
+ var EDITOR_SESSIONS_REGISTRY_FILE = join3(homedir(), ".vgai", "editor-sessions.json");
1827
2058
  function isEditorSessionEntry(v) {
1828
2059
  if (typeof v !== "object" || v === null) return false;
1829
2060
  const s = v;
@@ -1854,7 +2085,7 @@ function pidAlive(pid) {
1854
2085
  }
1855
2086
  function readLiveRegisteredSessions() {
1856
2087
  try {
1857
- const raw = JSON.parse(readFileSync(EDITOR_SESSIONS_REGISTRY_FILE, "utf8"));
2088
+ const raw = JSON.parse(readFileSync2(EDITOR_SESSIONS_REGISTRY_FILE, "utf8"));
1858
2089
  return Array.isArray(raw) ? raw.filter(isEditorSessionEntry).map(normalizeEditorSessionEntry).filter((s) => pidAlive(s.pid)) : [];
1859
2090
  } catch {
1860
2091
  return [];
@@ -1933,8 +2164,8 @@ var HttpSessionDiscovery = class {
1933
2164
  function findProjectRootFrom(dir) {
1934
2165
  let cur = resolve2(dir);
1935
2166
  for (; ; ) {
1936
- if (existsSync2(resolveManifestPath(cur))) return cur;
1937
- const parent = dirname(cur);
2167
+ if (existsSync3(resolveManifestPath(cur))) return cur;
2168
+ const parent = dirname2(cur);
1938
2169
  if (parent === cur) return null;
1939
2170
  cur = parent;
1940
2171
  }
@@ -1949,7 +2180,7 @@ function canonicalPath(path) {
1949
2180
  function readProjectSession(projectRoot) {
1950
2181
  try {
1951
2182
  const value = JSON.parse(
1952
- readFileSync2(join3(projectRoot, ".vgai", "session.json"), "utf8")
2183
+ readFileSync3(join4(projectRoot, ".vgai", "session.json"), "utf8")
1953
2184
  );
1954
2185
  if (typeof value !== "object" || value === null) return null;
1955
2186
  const hint = value;
@@ -1981,12 +2212,12 @@ ${manifestError}`
1981
2212
  function noMatchingSessionRefusal(projectRoot, canon, sessions, discoveryFailure) {
1982
2213
  if (discoveryFailure !== null) {
1983
2214
  return new Error(
1984
- `@volter/editor-live: could not READ the editor session registry while looking for ${projectRoot} \u2014 ${discoveryFailure}. This is not the answer "no editor is running": the question went unanswered, so nothing is known about what is live. Retry (a probe can time out while the box is loaded); if it keeps failing, \`vgai sessions\` asks the same question directly.`
2215
+ `@volter/editor-live: could not READ the editor session registry while looking for ${projectRoot} \u2014 ${discoveryFailure}. This is not the answer "no editor is running": the question went unanswered, so nothing is known about what is live. Retry (a probe can time out while the box is loaded); if it keeps failing, ${sessionsCommandFor(projectRoot)} asks the same question directly.`
1985
2216
  );
1986
2217
  }
1987
2218
  if (sessions.length === 0) {
1988
2219
  return new Error(
1989
- `@volter/editor-live: the editor session registry is readable and lists NO live sessions, so none covers ${projectRoot}. @volter/editor-live only attaches to an already-running session \u2014 it never starts one \u2014 so run \`volter-editor edit\` in that project first, then retry.`
2220
+ `@volter/editor-live: the editor session registry is readable and lists NO live sessions, so none covers ${projectRoot}. @volter/editor-live only attaches to an already-running session \u2014 it never starts one \u2014 so run ${editCommandFor(projectRoot)} in that project first, then retry.`
1990
2221
  );
1991
2222
  }
1992
2223
  const listed = sessions.map((s) => ` port ${s.port} \u2192 ${s.project === null ? "(no project)" : s.project}`).join("\n");
@@ -1995,7 +2226,7 @@ function noMatchingSessionRefusal(projectRoot, canon, sessions, discoveryFailure
1995
2226
  looking for (resolved): ${canon}
1996
2227
  live sessions:
1997
2228
  ${listed}
1998
- If one of those is meant to be this project, the two paths differ after resolution \u2014 the usual cause is a git worktree or a symlink, where the session was opened through a different path to the same files. Run \`volter-editor edit\` from THIS path, or use the path the session lists.`
2229
+ If one of those is meant to be this project, the two paths differ after resolution \u2014 the usual cause is a git worktree or a symlink, where the session was opened through a different path to the same files. Run ${editCommandFor(projectRoot)} from THIS path, or use the path the session lists.`
1999
2230
  );
2000
2231
  }
2001
2232
  async function resolveSession(projectDir = process.cwd(), deps = {}) {
@@ -2004,7 +2235,7 @@ async function resolveSession(projectDir = process.cwd(), deps = {}) {
2004
2235
  const projectRoot = findRoot(projectDir);
2005
2236
  if (projectRoot === null) {
2006
2237
  throw new Error(
2007
- `@volter/editor-live: no vgai.project.json found in ${projectDir} or any parent directory \u2014 is this a vgai project?`
2238
+ `@volter/editor-live: no vgai.project.json found in ${projectDir} or any parent directory \u2014 is this a project?`
2008
2239
  );
2009
2240
  }
2010
2241
  const localHint = (deps.readProjectSession ?? readProjectSession)(projectRoot);
@@ -2039,6 +2270,19 @@ async function resolveSession(projectDir = process.cwd(), deps = {}) {
2039
2270
  if (match.manifestError != null) throw manifestRefusal(projectRoot, match.manifestError);
2040
2271
  return { port: match.port, projectRoot };
2041
2272
  }
2273
+ function editCommandFor(projectRoot) {
2274
+ return verbFor(projectRoot, "edit");
2275
+ }
2276
+ function sessionsCommandFor(projectRoot) {
2277
+ return verbFor(projectRoot, "sessions");
2278
+ }
2279
+ function verbFor(projectRoot, verb) {
2280
+ try {
2281
+ return `\`${resolveProductForProject(projectRoot).command} ${verb}\``;
2282
+ } catch {
2283
+ return `its product's \`${verb}\` command`;
2284
+ }
2285
+ }
2042
2286
 
2043
2287
  // src/singleton.ts
2044
2288
  function createLazySession(factory) {