lecodes-cli 0.20.2 → 1.0.0

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 (105) hide show
  1. package/README.md +56 -57
  2. package/dist/index.js +5918 -5020
  3. package/package.json +7 -5
  4. package/runtime/materials/decal-relief.mat +12 -1
  5. package/runtime/materials/decal.mat +17 -3
  6. package/runtime/materials/lightmap-baked-lite.mat +6 -1
  7. package/runtime/materials/lightmap-baked.mat +6 -1
  8. package/runtime/materials/lightmap.mat +3 -0
  9. package/runtime/scene-harness.json +1 -1
  10. package/runtime/sdk-types.json +1 -1
  11. package/runtime/web/assets/createViewerLite-DHqTFhWB.js +866 -0
  12. package/runtime/web/assets/{index-BMt7AnC5.js → index-Q06c6oHx.js} +1 -1
  13. package/runtime/web/embed.js +1 -1
  14. package/src/api.ts +1 -302
  15. package/src/cli/args.ts +37 -0
  16. package/src/cli/command.ts +113 -0
  17. package/src/cli/errors.ts +13 -0
  18. package/src/cli/help.ts +133 -0
  19. package/src/cli/index.ts +7 -0
  20. package/src/cli/output.ts +102 -0
  21. package/src/cli/run.ts +106 -0
  22. package/src/commands/{appAndroid.ts → app/android.ts} +451 -42
  23. package/src/commands/{appDesktop.ts → app/desktop.ts} +63 -32
  24. package/src/commands/{appDesktopMac.ts → app/desktopMac.ts} +24 -18
  25. package/src/commands/{appIcon.ts → app/icon.ts} +2 -2
  26. package/src/commands/{app.ts → app/index.ts} +397 -289
  27. package/src/commands/{appShared.ts → app/shared.ts} +30 -39
  28. package/src/commands/{appTemplatesAndroid.ts → app/templates/android.ts} +62 -28
  29. package/src/commands/{appTemplatesGradlew.ts → app/templates/gradlew.ts} +1 -1
  30. package/src/commands/{appTemplates.ts → app/templates/ios.ts} +1 -1
  31. package/src/commands/assets.ts +23 -21
  32. package/src/commands/clone.ts +48 -51
  33. package/src/commands/compile.ts +62 -88
  34. package/src/commands/create.ts +46 -47
  35. package/src/commands/design/comments.ts +361 -0
  36. package/src/commands/design/context.ts +101 -0
  37. package/src/commands/design/index.ts +391 -0
  38. package/src/commands/design/snapshot.ts +134 -0
  39. package/src/commands/{designTemplates.ts → design/templates.ts} +4 -3
  40. package/src/commands/desktop.ts +90 -82
  41. package/src/commands/dev.ts +201 -176
  42. package/src/commands/diff.ts +39 -51
  43. package/src/commands/index.ts +60 -0
  44. package/src/commands/{init.ts → init/index.ts} +199 -191
  45. package/src/commands/install.ts +80 -67
  46. package/src/commands/lightmap.ts +203 -171
  47. package/src/commands/link.ts +87 -94
  48. package/src/commands/login.ts +25 -23
  49. package/src/commands/navmesh.ts +168 -138
  50. package/src/commands/pn.ts +201 -244
  51. package/src/commands/pull.ts +49 -56
  52. package/src/commands/push.ts +48 -54
  53. package/src/commands/render.ts +158 -99
  54. package/src/commands/scene.ts +58 -55
  55. package/src/commands/{shaders.ts → shaders/index.ts} +61 -42
  56. package/src/commands/{shadersNew.ts → shaders/new.ts} +2 -2
  57. package/src/commands/shared.ts +79 -0
  58. package/src/commands/status.ts +22 -26
  59. package/src/commands/test.ts +398 -371
  60. package/src/commands/thumbs.ts +185 -175
  61. package/src/commands/update/index.ts +226 -0
  62. package/src/commands/{types.ts → update/types.ts} +31 -22
  63. package/src/compile/collect.ts +5 -5
  64. package/src/compile/collectLocal.ts +3 -3
  65. package/src/compile/designCompile.ts +3 -3
  66. package/src/compile/headlessBundle.ts +133 -129
  67. package/src/compile/projectCompile.ts +2 -2
  68. package/src/compile/sceneCompile.ts +10 -8
  69. package/src/compile/screenEntry.ts +123 -127
  70. package/src/compile/shaders.ts +3 -3
  71. package/src/dev/androidDev.ts +1 -1
  72. package/src/dev/devServer.ts +3 -3
  73. package/src/dev/webRunner.ts +1 -1
  74. package/src/{cmgenTool.ts → hosts/cmgenTool.ts} +1 -1
  75. package/src/{desktopRenderer.ts → hosts/desktopRenderer.ts} +6 -5
  76. package/src/{desktopScript.ts → hosts/desktopScript.ts} +1 -1
  77. package/src/{distRoot.ts → hosts/distRoot.ts} +9 -5
  78. package/src/{matcTool.ts → hosts/matcTool.ts} +1 -1
  79. package/src/{peerInstall.ts → hosts/peerInstall.ts} +2 -2
  80. package/src/{releases.ts → hosts/releases.ts} +1 -1
  81. package/src/index.ts +32 -480
  82. package/src/platform/api.ts +302 -0
  83. package/src/{browserAuth.ts → platform/browserAuth.ts} +1 -1
  84. package/src/{config.ts → platform/config.ts} +1 -1
  85. package/src/{serverDiff.ts → platform/serverDiff.ts} +3 -3
  86. package/src/{projectEnv.ts → project/env.ts} +33 -2
  87. package/src/{localFiles.ts → project/localFiles.ts} +1 -1
  88. package/src/{manifest.ts → project/manifest.ts} +1 -1
  89. package/src/{project.ts → project/materialize.ts} +2 -2
  90. package/src/project/paths.ts +20 -0
  91. package/src/{textDiff.ts → project/textDiff.ts} +1 -1
  92. package/src/{types.ts → project/types.ts} +0 -0
  93. package/runtime/web/assets/createViewerLite-Ct_PZdof.js +0 -852
  94. package/src/commands/design.ts +0 -846
  95. package/src/commands/update.ts +0 -211
  96. package/src/util.ts +0 -146
  97. /package/src/commands/{projectTemplates.ts → init/templates.ts} +0 -0
  98. /package/src/{lecodes-3d-editor.d.ts → declarations/lecodes-3d-editor.d.ts} +0 -0
  99. /package/src/{lecodes-assets.d.ts → declarations/lecodes-assets.d.ts} +0 -0
  100. /package/src/{lecodes-design.d.ts → declarations/lecodes-design.d.ts} +0 -0
  101. /package/src/{lecodes-renderer.d.ts → declarations/lecodes-renderer.d.ts} +0 -0
  102. /package/src/{qrcode-terminal.d.ts → declarations/qrcode-terminal.d.ts} +0 -0
  103. /package/src/{peers.ts → hosts/peers.ts} +0 -0
  104. /package/src/{designMeta.ts → project/designMeta.ts} +0 -0
  105. /package/src/{ignore.ts → project/ignore.ts} +0 -0
@@ -1,127 +1,123 @@
1
- import { existsSync, readFileSync, statSync } from "node:fs"
2
- import { isAbsolute, join, relative, resolve } from "node:path"
3
- import { screenFrame } from "../designMeta"
4
- import { CliError, flagStr, toKey, type Args } from "../util"
5
- import type { CompileEntry } from "./projectCompile"
6
-
7
- /*
8
- * Rendering ONE screen module instead of the whole app — `lecodes render <file>` (and
9
- * `--desktop`). A design screen (`design/screens/login.ts`) never boots itself: it default-exports
10
- * a `UIScreen`, or a `(state) => UIScreen` function for its states, and the design board / snapshot
11
- * supply the `.open()`. This builds the same virtual entry for the normal project pipeline, so any
12
- * file written that way — inside design/ or not — renders with the full `lecodes render` flag set
13
- * (--png/--clip/--script/--device/…) against the project's real imports and assets.
14
- *
15
- * Kept in step with compile/designCompile.ts, which does the same wrapping per screen for the board.
16
- */
17
-
18
- /** Extensions a screen module may carry (so `render design/screens/login` works too). */
19
- const SCREEN_EXTS = [".ts", ".tsx", ".js", ".jsx", ".mjs"]
20
-
21
- /** Where the generated entry lives — the project root, so its import specifier is the screen's
22
- * own project key ("./design/screens/login"). Leading dot: never a real project file. */
23
- const SCREEN_ENTRY_PATH = "/.screen-entry.ts"
24
-
25
- /** The design folder a bare screen id (`lecodes render login`) is looked up in. */
26
- const DEFAULT_DESIGN_DIR = "design"
27
-
28
- export type ScreenTarget = {
29
- /** The virtual entry module — hand it to the compiler alongside the project's own entries. */
30
- entry: CompileEntry
31
- /** Path of that entry, for `entryOverride`. */
32
- entryPath: string
33
- /** The board frame from the design folder's meta.json, when this IS a design screen. */
34
- frame?: [number, number]
35
- /** How the screen is named in progress notes ("design/screens/login.ts", "login@empty"). */
36
- label: string
37
- }
38
-
39
- /** `lecodes render <file|screen-id>` — the positional a screen render is asked for by. */
40
- const screenArg = (args: Args): string | undefined => args._[0]
41
-
42
- /** Whether the argument names a path at all (vs a bare screen id). */
43
- const hasSeparator = (file: string) => file.includes("/") || file.includes("\\")
44
-
45
- /** Find the screen file: a path (with or without extension), or a bare id resolved against the
46
- * design folder's screens/ (`lecodes render login`, honoring --dir). */
47
- const findScreenFile = (root: string, file: string, dirFlag: string | undefined): string => {
48
- const candidates = [resolve(process.cwd(), file)]
49
- if (!hasSeparator(file)) {
50
- candidates.push(join(root, (dirFlag ?? DEFAULT_DESIGN_DIR).replace(/[/\\]+$/, ""), "screens", file))
51
- }
52
- for (const base of candidates) {
53
- if (existsSync(base) && statSync(base).isFile()) return base
54
- for (const ext of SCREEN_EXTS) if (existsSync(base + ext)) return base + ext
55
- }
56
- throw new CliError(
57
- `No screen file at "${file}".` +
58
- (hasSeparator(file) ? "" : ` Pass a path, or a screen id that exists in ${dirFlag ?? DEFAULT_DESIGN_DIR}/screens/.`))
59
- }
60
-
61
- /** The design folder a screen file belongs to ("<root>/design" for "/design/screens/login.ts"),
62
- * or null when it isn't laid out as a design screen. */
63
- const designDirOf = (root: string, key: string): { dir: string, id: string } | null => {
64
- const m = /^\/(.*)screens\/([^/]+)\.[^./]+$/.exec(key)
65
- if (!m) return null
66
- const dir = join(root, m[1])
67
- return existsSync(join(dir, "meta.json")) ? { dir, id: m[2] } : null
68
- }
69
-
70
- /**
71
- * Resolve `<file>` into the virtual entry that opens its default export.
72
- * `state` picks the branch of a `(state) => UIScreen` screen (ignored by a value export).
73
- */
74
- const resolveScreenTarget = (
75
- root: string, file: string, opts: { state?: string, dir?: string } = {},
76
- ): ScreenTarget => {
77
- const abs = findScreenFile(root, file, opts.dir)
78
- const rel = relative(root, abs)
79
- if (rel.startsWith("..") || isAbsolute(rel)) {
80
- throw new CliError(
81
- `"${file}" is outside the project (${root}) — a screen renders as part of its project, so its ` +
82
- `imports and asset() files resolve from the same tree.`)
83
- }
84
- const key = toKey(rel)
85
-
86
- // A file with no default export compiles to an empty render; say so here instead.
87
- if (!/^\s*export\s+default\b/m.test(readFileSync(abs, "utf8"))) {
88
- throw new CliError(
89
- `${key.slice(1)} has no default export. A screen file ends with ` +
90
- `\`export default UIScreen([...])\` — or \`export default (state) => UIScreen([...])\` for screen states.`)
91
- }
92
-
93
- // Same wrapper the design board compiles (designCompile.ts): a function export is invoked with the
94
- // chosen state (a bare call uses the parameter's default = the canonical state), a value is used
95
- // as-is, and either way the screen is opened so the runtime has something to render.
96
- const invoke = opts.state !== undefined ? `screen(${JSON.stringify(opts.state)})` : "screen()"
97
- const spec = "." + key.replace(/\.[^./]+$/, "")
98
- const design = designDirOf(root, key)
99
- return {
100
- entryPath: SCREEN_ENTRY_PATH,
101
- entry: {
102
- path: SCREEN_ENTRY_PATH,
103
- type: "text",
104
- text: `import screen from ${JSON.stringify(spec)}\n`
105
- + `const __screen = typeof screen === "function" ? ${invoke} : screen\n`
106
- + `__screen.open()\n`,
107
- },
108
- frame: design ? screenFrame(design.dir, design.id) : undefined,
109
- label: (design ? design.id : key.slice(1)) + (opts.state !== undefined ? `@${opts.state}` : ""),
110
- }
111
- }
112
-
113
- /** Read the screen target off the CLI args, or undefined when no screen was named. Rejects the
114
- * flag combinations that would fight over the entrypoint. */
115
- export const screenTargetFromArgs = (root: string, args: Args): ScreenTarget | undefined => {
116
- const file = screenArg(args)
117
- if (file === undefined) {
118
- if (flagStr(args, "state") !== undefined) {
119
- throw new CliError("--state picks a screen's state — name the screen file too: lecodes render <file> --state <name>")
120
- }
121
- return undefined
122
- }
123
- if (flagStr(args, "entry") !== undefined) {
124
- throw new CliError("Use either a screen file or --entry, not both — each names the module to compile from.")
125
- }
126
- return resolveScreenTarget(root, file, { state: flagStr(args, "state"), dir: flagStr(args, "dir") })
127
- }
1
+ import { existsSync, readFileSync, statSync } from "node:fs"
2
+ import { isAbsolute, join, relative, resolve } from "node:path"
3
+ import { screenFrame } from "../project/designMeta"
4
+ import { CliError } from "../cli"
5
+ import { toKey } from "../project/paths"
6
+ import type { CompileEntry } from "./projectCompile"
7
+
8
+ /*
9
+ * Rendering ONE screen module instead of the whole app — `lecodes render <file>` (and
10
+ * `--desktop`). A design screen (`design/screens/login.ts`) never boots itself: it default-exports
11
+ * a `UIScreen`, or a `(state) => UIScreen` function for its states, and the design board / snapshot
12
+ * supply the `.open()`. This builds the same virtual entry for the normal project pipeline, so any
13
+ * file written that way — inside design/ or not — renders with the full `lecodes render` flag set
14
+ * (--png/--clip/--script/--device/…) against the project's real imports and assets.
15
+ *
16
+ * Kept in step with compile/designCompile.ts, which does the same wrapping per screen for the board.
17
+ */
18
+
19
+ /** Extensions a screen module may carry (so `render design/screens/login` works too). */
20
+ const SCREEN_EXTS = [".ts", ".tsx", ".js", ".jsx", ".mjs"]
21
+
22
+ /** Where the generated entry lives — the project root, so its import specifier is the screen's
23
+ * own project key ("./design/screens/login"). Leading dot: never a real project file. */
24
+ const SCREEN_ENTRY_PATH = "/.screen-entry.ts"
25
+
26
+ /** The design folder a bare screen id (`lecodes render login`) is looked up in. */
27
+ const DEFAULT_DESIGN_DIR = "design"
28
+
29
+ export type ScreenTarget = {
30
+ /** The virtual entry module — hand it to the compiler alongside the project's own entries. */
31
+ entry: CompileEntry
32
+ /** Path of that entry, for `entryOverride`. */
33
+ entryPath: string
34
+ /** The board frame from the design folder's meta.json, when this IS a design screen. */
35
+ frame?: [number, number]
36
+ /** How the screen is named in progress notes ("design/screens/login.ts", "login@empty"). */
37
+ label: string
38
+ }
39
+
40
+ /** `lecodes render <file|screen-id>` — the positional a screen render is asked for by. */
41
+
42
+ /** Whether the argument names a path at all (vs a bare screen id). */
43
+ const hasSeparator = (file: string) => file.includes("/") || file.includes("\\")
44
+
45
+ /** Find the screen file: a path (with or without extension), or a bare id resolved against the
46
+ * design folder's screens/ (`lecodes render login`, honoring --dir). */
47
+ const findScreenFile = (root: string, file: string, dirFlag: string | undefined): string => {
48
+ const candidates = [resolve(process.cwd(), file)]
49
+ if (!hasSeparator(file)) {
50
+ candidates.push(join(root, (dirFlag ?? DEFAULT_DESIGN_DIR).replace(/[/\\]+$/, ""), "screens", file))
51
+ }
52
+ for (const base of candidates) {
53
+ if (existsSync(base) && statSync(base).isFile()) return base
54
+ for (const ext of SCREEN_EXTS) if (existsSync(base + ext)) return base + ext
55
+ }
56
+ throw new CliError(
57
+ `No screen file at "${file}".` +
58
+ (hasSeparator(file) ? "" : ` Pass a path, or a screen id that exists in ${dirFlag ?? DEFAULT_DESIGN_DIR}/screens/.`))
59
+ }
60
+
61
+ /** The design folder a screen file belongs to ("<root>/design" for "/design/screens/login.ts"),
62
+ * or null when it isn't laid out as a design screen. */
63
+ const designDirOf = (root: string, key: string): { dir: string, id: string } | null => {
64
+ const m = /^\/(.*)screens\/([^/]+)\.[^./]+$/.exec(key)
65
+ if (!m) return null
66
+ const dir = join(root, m[1])
67
+ return existsSync(join(dir, "meta.json")) ? { dir, id: m[2] } : null
68
+ }
69
+
70
+ /**
71
+ * Resolve `<file>` into the virtual entry that opens its default export.
72
+ * `state` picks the branch of a `(state) => UIScreen` screen (ignored by a value export).
73
+ */
74
+ const resolveScreenTarget = (
75
+ root: string, file: string, opts: { state?: string, dir?: string } = {},
76
+ ): ScreenTarget => {
77
+ const abs = findScreenFile(root, file, opts.dir)
78
+ const rel = relative(root, abs)
79
+ if (rel.startsWith("..") || isAbsolute(rel)) {
80
+ throw new CliError(
81
+ `"${file}" is outside the project (${root}) — a screen renders as part of its project, so its ` +
82
+ `imports and asset() files resolve from the same tree.`)
83
+ }
84
+ const key = toKey(rel)
85
+
86
+ // A file with no default export compiles to an empty render; say so here instead.
87
+ if (!/^\s*export\s+default\b/m.test(readFileSync(abs, "utf8"))) {
88
+ throw new CliError(
89
+ `${key.slice(1)} has no default export. A screen file ends with ` +
90
+ `\`export default UIScreen([...])\` — or \`export default (state) => UIScreen([...])\` for screen states.`)
91
+ }
92
+
93
+ // Same wrapper the design board compiles (designCompile.ts): a function export is invoked with the
94
+ // chosen state (a bare call uses the parameter's default = the canonical state), a value is used
95
+ // as-is, and either way the screen is opened so the runtime has something to render.
96
+ const invoke = opts.state !== undefined ? `screen(${JSON.stringify(opts.state)})` : "screen()"
97
+ const spec = "." + key.replace(/\.[^./]+$/, "")
98
+ const design = designDirOf(root, key)
99
+ return {
100
+ entryPath: SCREEN_ENTRY_PATH,
101
+ entry: {
102
+ path: SCREEN_ENTRY_PATH,
103
+ type: "text",
104
+ text: `import screen from ${JSON.stringify(spec)}\n`
105
+ + `const __screen = typeof screen === "function" ? ${invoke} : screen\n`
106
+ + `__screen.open()\n`,
107
+ },
108
+ frame: design ? screenFrame(design.dir, design.id) : undefined,
109
+ label: (design ? design.id : key.slice(1)) + (opts.state !== undefined ? `@${opts.state}` : ""),
110
+ }
111
+ }
112
+
113
+ /** The screen target named on the command line (`render <file>`), or undefined when no screen was
114
+ * named. Rejects the flag combinations that would fight over the entrypoint. */
115
+ export type ScreenArgs = { file?: string, state?: string, entry?: string, dir?: string }
116
+ export const screenTargetFrom = (root: string, a: ScreenArgs): ScreenTarget | undefined => {
117
+ if (a.file === undefined) {
118
+ if (a.state !== undefined) throw new CliError("--state picks a screen's state — name the screen file too: lecodes render <file> --state <name>")
119
+ return undefined
120
+ }
121
+ if (a.entry !== undefined) throw new CliError("Use either a screen file or --entry, not both — each names the module to compile from.")
122
+ return resolveScreenTarget(root, a.file, { state: a.state, dir: a.dir })
123
+ }
@@ -8,9 +8,9 @@ import {
8
8
  matcArgs, MATC_PLATFORMS, TARGET_MATRIX_GENERATION,
9
9
  type AnyMatcPlatform, type AnyShaderPlatform,
10
10
  } from "lecodes-sdk/compile"
11
- import { resolveMatcExe, resolveMatcExeCached } from "../matcTool"
12
- import { LECODES_DIR } from "../manifest"
13
- import { CliError } from "../util"
11
+ import { resolveMatcExe, resolveMatcExeCached } from "../hosts/matcTool"
12
+ import { LECODES_DIR } from "../project/manifest"
13
+ import { CliError } from "../cli"
14
14
  import type { CompileEntry } from "./projectCompile"
15
15
 
16
16
  /*
@@ -1,7 +1,7 @@
1
1
  import { existsSync, writeFileSync } from "node:fs"
2
2
  import { join } from "node:path"
3
3
  import { spawnSync } from "node:child_process"
4
- import { CliError, note, warnErr } from "../util"
4
+ import { CliError, note, warnErr } from "../cli"
5
5
 
6
6
  /*
7
7
  * `lecodes dev --android` — open the dev URL in the LeCodes app on the connected phone, through adb.
@@ -1,8 +1,8 @@
1
1
  import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http"
2
2
  import { createReadStream, statSync } from "node:fs"
3
3
  import { join } from "node:path"
4
- import type { Manifest } from "../manifest"
5
- import { resolveLocalPath } from "../localFiles"
4
+ import type { Manifest } from "../project/manifest"
5
+ import { resolveLocalPath } from "../project/localFiles"
6
6
  import { collectLocalEntries } from "../compile/collectLocal"
7
7
  import { applyLocalShaders, isShaderArtifactName, shadersCacheDir } from "../compile/shaders"
8
8
  import { ambiguousEntryWarning, compileProject } from "../compile/projectCompile"
@@ -10,7 +10,7 @@ import { applyProjectFonts } from "../compile/fonts"
10
10
  import { applyAppAssetIcons } from "../compile/assetIcons"
11
11
  import { renderDevScreen, renderPrelude } from "./clientTemplates"
12
12
  import { upgradeWebSocket, type WsConnection } from "./wsServer"
13
- import { toKey } from "../util"
13
+ import { toKey } from "../project/paths"
14
14
  import { makeStackDemangler } from "../compile/nativeStack"
15
15
 
16
16
  /*
@@ -1,7 +1,7 @@
1
1
  import { existsSync } from "node:fs"
2
2
  import { createRequire } from "node:module"
3
3
  import { dirname, join } from "node:path"
4
- import { distRoot } from "../distRoot"
4
+ import { distRoot } from "../hosts/distRoot"
5
5
 
6
6
  /**
7
7
  * Where the browser runner for `lecodes dev --web` lives: viewer-lite's built `dist-embed`
@@ -2,7 +2,7 @@ import { existsSync, readdirSync } from "node:fs"
2
2
  import { homedir } from "node:os"
3
3
  import { join, resolve } from "node:path"
4
4
  import { downloadReleaseArchive, fetchReleaseProduct, newestForPlatform, semverNewer, type ManifestFile, type ManifestVersion } from "./releases"
5
- import { CliError, note } from "./util"
5
+ import { CliError, note } from "../cli"
6
6
 
7
7
  /*
8
8
  * Filament's cmgen (environment tool) — the tool behind `lecodes assets sky`, which turns an
@@ -3,8 +3,8 @@ import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFile
3
3
  import { homedir, tmpdir } from "node:os"
4
4
  import { delimiter, join, resolve } from "node:path"
5
5
  import { cachedArtifactSha, downloadReleaseArchive, fetchReleaseProduct, semverNewer, type ManifestVersion } from "./releases"
6
- import { CliError, note, warnErr } from "./util"
7
- import { makeStackDemangler } from "./compile/nativeStack"
6
+ import { CliError, note, warnErr } from "../cli"
7
+ import { makeStackDemangler } from "../compile/nativeStack"
8
8
 
9
9
  /*
10
10
  * The NATIVE renderer behind `lecodes render --desktop`: the LeCodes Desktop host (tgfx UI + Filament
@@ -38,9 +38,10 @@ export const DESKTOP_RENDERER_VERSION = "1.1.0"
38
38
  export { semverNewer }
39
39
 
40
40
  /** Which desktop host to run. "gl" is the default everywhere; "vulkan" is a separate Windows binary
41
- * (Filament + tgfx on one VkDevice), NOT a runtime switch — it has its own artifact, its own
42
- * shaders, and a smaller feature set (no 2D engine, no native-view plugins). Chosen per project via
43
- * app.json `desktop.renderer`, or per run via `--renderer` / LECODES_DESKTOP_RENDERER. */
41
+ * (Filament + tgfx + creator-2d on one VkDevice), NOT a runtime switch — it has its own artifact, its
42
+ * own shaders, and a smaller feature set (no native-view plugins; the 2D engine joined it 2026-09-08,
43
+ * i.e. desktop releases after 1.4.1). Chosen per project via app.json `desktop.renderer`, or per run via `--renderer` /
44
+ * LECODES_DESKTOP_RENDERER. */
44
45
  export type DesktopRenderer = "gl" | "vulkan"
45
46
 
46
47
  export const DESKTOP_RENDERER_MIN_VERSION: Record<DesktopRenderer, string> = { gl: "0.0.0", vulkan: "1.4.0" }
@@ -1,5 +1,5 @@
1
1
  import { resolve } from "node:path"
2
- import { CliError } from "./util"
2
+ import { CliError } from "../cli"
3
3
 
4
4
  /*
5
5
  * `lecodes render --desktop --script`: the same JSON scenario a headless run takes, driven through
@@ -5,16 +5,20 @@ import { fileURLToPath } from "node:url"
5
5
  /* Root of the CLI's on-disk companion files (runtime/ with the vendored SDK source + type bundle).
6
6
  *
7
7
  * Two distribution shapes:
8
- * - npm package / monorepo dev: this module lives in dist/ (or src/), companions at ../runtime —
9
- * resolve relative to import.meta.url as before.
8
+ * - npm package: the bundle is dist/index.js, companions at ../runtime.
9
+ * - monorepo dev: this module runs from src/hosts/, so the package root is two levels up. The
10
+ * package.json marker matters here — runtime/ is gitignored and absent until vendor-runtime runs,
11
+ * and callers also resolve siblings from the root (../projects, ../creator-gl).
10
12
  * - standalone binary (`bun build --compile`, see scripts/build-bin.ts): import.meta.url points into
11
13
  * the virtual bunfs archive which does NOT exist on disk, so the companions ship in the release
12
14
  * archive NEXT TO the executable and are resolved via process.execPath. */
13
15
  export const distRoot = (() => {
16
+ const here = dirname(fileURLToPath(import.meta.url))
14
17
  const candidates = [
15
- join(dirname(fileURLToPath(import.meta.url)), ".."), // package root (dist/.. or src/..)
16
- dirname(process.execPath), // standalone binary: next to the exe
18
+ join(here, ".."), // npm package: dist/..
19
+ join(here, "..", ".."), // monorepo dev: src/hosts/../..
20
+ dirname(process.execPath), // standalone binary: next to the exe
17
21
  ]
18
- for (const c of candidates) if (existsSync(join(c, "runtime"))) return c
22
+ for (const c of candidates) if (existsSync(join(c, "runtime")) || existsSync(join(c, "package.json"))) return c
19
23
  return candidates[0]
20
24
  })()
@@ -2,7 +2,7 @@ import { existsSync, readdirSync } from "node:fs"
2
2
  import { homedir } from "node:os"
3
3
  import { join, resolve } from "node:path"
4
4
  import { downloadReleaseArchive, fetchReleaseProduct, newestForPlatform, semverNewer, type ManifestFile, type ManifestVersion } from "./releases"
5
- import { CliError, note } from "./util"
5
+ import { CliError, note } from "../cli"
6
6
 
7
7
  /*
8
8
  * Filament's matc (material compiler) — the tool behind LOCAL shader compile: `lecodes dev`
@@ -2,9 +2,9 @@ import { spawnSync } from "node:child_process"
2
2
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
3
3
  import { join } from "node:path"
4
4
  import { pathToFileURL } from "node:url"
5
- import pkg from "../package.json"
5
+ import pkg from "../../package.json"
6
6
  import { peerDirInPrefix, peersDir, peerVersionGlobal } from "./peers"
7
- import { CliError, c, confirm, log, note } from "./util"
7
+ import { CliError, c, confirm, log, note } from "../cli"
8
8
 
9
9
  /*
10
10
  * Peers install themselves on first use. `lecodes render` on a machine with only the CLI asks once,
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto"
2
2
  import { existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs"
3
3
  import { join } from "node:path"
4
4
  import { spawnSync } from "node:child_process"
5
- import { CliError } from "./util"
5
+ import { CliError } from "../cli"
6
6
 
7
7
  /*
8
8
  * The S3 release channel the CLI downloads native tools from: the manifest scripts/release/*