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,54 +1,63 @@
1
1
  import { resolve } from "node:path"
2
- import { getTypes } from "../api"
3
- import { DEFAULT_API, loadConfig, normalizeApiUrl } from "../config"
4
- import { findProjectRoot, LECODES_DIR } from "../manifest"
2
+ import { getTypes } from "../../platform/api"
3
+ import { DEFAULT_API, loadConfig, normalizeApiUrl } from "../../platform/config"
4
+ import { findProjectRoot, LECODES_DIR } from "../../project/manifest"
5
5
  import {
6
6
  bundleHash, readLocalTypeBundle, readProjectTypeBundle, writeTsconfig, writeTypeBundle,
7
7
  type TypeBundle,
8
- } from "../types"
9
- import { CliError, c, flagBool, flagStr, log, note, success, warn, type Args } from "../util"
8
+ } from "../../project/types"
9
+ import { CliError, c, log, note, success, warn } from "../../cli"
10
10
 
11
11
  /*
12
- * The types step of `lecodes update` (formerly `lecodes types [dir]`) — re-materialize the IDE type surface (<root>/.lecodes/types/ + tsconfig).
12
+ * The types step of `lecodes update` (formerly `lecodes types [dir]`) — re-materialize the IDE type
13
+ * surface (<root>/.lecodes/types/ + tsconfig).
13
14
  *
14
15
  * Every project gets these types once, at scaffold time: `clone`/`create`/`pull` fetch them from
15
16
  * GET /api/types (the deployed runtime's surface), while `init` and `design` — which run with no
16
17
  * server and no login — write the bundle vendored into the CLI at publish time. Neither source
17
- * refreshes on its own, so a local project's types age with the CLI binary that made it. This
18
- * command is the refresh: server by default (newest SDK the platform actually runs), `--local` for
19
- * the CLI's own copy, `--check` to only report drift (non-zero exit when stale).
18
+ * refreshes on its own, so a local project's types age with the CLI binary that made it. This step
19
+ * is the refresh: server by default (newest SDK the platform actually runs), `local` for the CLI's
20
+ * own copy, `check` to only report drift (a CliError when stale).
20
21
  *
21
22
  * IDE-only: the compiler reads the SDK sources vendored in runtime/sdk, never .lecodes/types. When
22
- * the two sources disagree the command says so — types promising APIs this CLI can't compile is
23
+ * the two sources disagree the step says so — types promising APIs this CLI can't compile is
23
24
  * exactly the confusing case worth naming out loud.
24
25
  */
25
26
 
27
+ export type RefreshTypesOptions = {
28
+ /** The project folder (default: the project root above the cwd, else the cwd). */
29
+ dir?: string
30
+ /** The bundle vendored in this CLI instead of the server's. */
31
+ local: boolean
32
+ /** Only report drift — write nothing, throw when stale. */
33
+ check: boolean
34
+ /** API origin for the server bundle (default: the configured one). */
35
+ api?: string
36
+ }
37
+
26
38
  const label = (bundle: TypeBundle) => `${Object.keys(bundle).length} file(s)`
27
39
 
28
- export const types = async (args: Args) => {
29
- const dirArg = args._[0]
30
- const root = dirArg ? resolve(dirArg) : (() => {
40
+ export const refreshTypes = async (opts: RefreshTypesOptions): Promise<void> => {
41
+ const root = opts.dir ? resolve(opts.dir) : (() => {
31
42
  try { return findProjectRoot(process.cwd()) } catch { return process.cwd() }
32
43
  })()
33
44
 
34
- const useLocal = flagBool(args, "local")
35
- const checkOnly = flagBool(args, "check")
36
45
  const local = readLocalTypeBundle()
37
46
 
38
47
  // ---- pick the source ----
39
48
  let bundle: TypeBundle | null = null
40
49
  let source = ""
41
- if (useLocal) {
50
+ if (opts.local) {
42
51
  if (!local) throw new CliError("This CLI ships no vendored type bundle — drop --local to fetch them from the server.")
43
52
  bundle = local
44
53
  source = "the CLI's bundled SDK"
45
54
  } else {
46
- const apiUrl = normalizeApiUrl(flagStr(args, "api") ?? loadConfig().apiUrl ?? DEFAULT_API)
55
+ const apiUrl = normalizeApiUrl(opts.api ?? loadConfig().apiUrl ?? DEFAULT_API)
47
56
  try {
48
57
  bundle = await getTypes(apiUrl)
49
58
  source = apiUrl
50
59
  } catch (e) {
51
- if (!local || checkOnly) throw e
60
+ if (!local || opts.check) throw e
52
61
  warn(`${(e as Error).message}`)
53
62
  bundle = local
54
63
  source = "the CLI's bundled SDK (server unreachable)"
@@ -56,10 +65,10 @@ export const types = async (args: Args) => {
56
65
  }
57
66
  log(`${c.green("source")} ${source} — ${label(bundle)}`)
58
67
 
59
- // ---- report drift, write unless --check ----
68
+ // ---- report drift, write unless check ----
60
69
  const onDisk = readProjectTypeBundle(root)
61
70
  const same = bundleHash(onDisk) === bundleHash(bundle)
62
- if (checkOnly) {
71
+ if (opts.check) {
63
72
  if (Object.keys(onDisk).length === 0) throw new CliError(`No types in ${LECODES_DIR}/types/ — run \`lecodes update\`.`)
64
73
  if (!same) throw new CliError(`Types are out of date (${label(onDisk)} on disk) — run \`lecodes update\`.`)
65
74
  success(`Types are up to date (${label(onDisk)}).`)
@@ -77,11 +86,11 @@ export const types = async (args: Args) => {
77
86
 
78
87
  // The compiler uses the SDK vendored in this CLI, not these declarations — worth saying when the
79
88
  // server's surface is a different one, since that gap shows up as "autocompletes but won't build".
80
- if (!useLocal && local && bundleHash(local) !== bundleHash(bundle)) {
89
+ if (!opts.local && local && bundleHash(local) !== bundleHash(bundle)) {
81
90
  warn("The server's SDK surface differs from the one this CLI compiles with — if something autocompletes but fails to compile, update the CLI (`npm install -g lecodes-cli@latest`) or re-run with --local.")
82
91
  }
83
92
 
84
93
  log("")
85
94
  success(stats.added + stats.updated + stats.removed > 0 ? "Types synced." : "Types already current.")
86
- if (!dirArg) note("Reload the TypeScript server in your editor to pick them up.")
95
+ if (!opts.dir) note("Reload the TypeScript server in your editor to pick them up.")
87
96
  }
@@ -1,10 +1,10 @@
1
1
  import { readFileSync } from "node:fs"
2
2
  import { basename } from "node:path"
3
- import type { Project } from "../api"
4
- import type { Manifest } from "../manifest"
5
- import { classifyKind, scanLocal } from "../localFiles"
6
- import { loadIgnore } from "../ignore"
7
- import { buildPaths } from "../project"
3
+ import type { Project } from "../platform/api"
4
+ import type { Manifest } from "../project/manifest"
5
+ import { classifyKind, scanLocal } from "../project/localFiles"
6
+ import { loadIgnore } from "../project/ignore"
7
+ import { buildPaths } from "../project/materialize"
8
8
  import type { CompileEntry } from "./projectCompile"
9
9
 
10
10
  /*
@@ -1,8 +1,8 @@
1
1
  import { readFileSync } from "node:fs"
2
2
  import { basename } from "node:path"
3
- import type { Manifest } from "../manifest"
4
- import { classifyKind, scanLocal } from "../localFiles"
5
- import { loadIgnore } from "../ignore"
3
+ import type { Manifest } from "../project/manifest"
4
+ import { classifyKind, scanLocal } from "../project/localFiles"
5
+ import { loadIgnore } from "../project/ignore"
6
6
  import type { CompileEntry } from "./projectCompile"
7
7
 
8
8
  /*
@@ -1,8 +1,8 @@
1
1
  import { readFileSync } from "node:fs"
2
2
  import { basename } from "node:path"
3
- import type { Manifest } from "../manifest"
4
- import { classifyKind, scanLocal } from "../localFiles"
5
- import { loadIgnore } from "../ignore"
3
+ import type { Manifest } from "../project/manifest"
4
+ import { classifyKind, scanLocal } from "../project/localFiles"
5
+ import { loadIgnore } from "../project/ignore"
6
6
  import { compileProject, type CompileEntry } from "./projectCompile"
7
7
  import { applyAssetIcons } from "./assetIcons"
8
8
  import { applyProjectFonts } from "./fonts"
@@ -1,129 +1,133 @@
1
- import { getProject } from "../api"
2
- import { loadConfig, requireApiUrl, requireToken } from "../config"
3
- import { findProjectRoot, readManifest, type Manifest } from "../manifest"
4
- import { collectEntries } from "./collect"
5
- import { collectLocalEntries } from "./collectLocal"
6
- import { ambiguousEntryWarning, compileProject } from "./projectCompile"
7
- import { applyProjectFonts } from "./fonts"
8
- import { applyAppAssetIcons } from "./assetIcons"
9
- import type { ScreenTarget } from "./screenEntry"
10
- import { flagBool, flagStr, note, type Args } from "../util"
11
-
12
- /*
13
- * Compile the project into the iife bundle the headless renderer runs — shared by `lecodes render`
14
- * and `lecodes test`. Two modes, picked automatically (same split as `lecodes compile`):
15
- * - Server-backed project (.lecodes/manifest.json): entries resolve from local files + the
16
- * project's server metadata (needs login).
17
- * - Local project (from `lecodes init` — no manifest): compiles straight from the working tree,
18
- * no login.
19
- * In both, resource assets default to LOCAL disk reads: each asset('./x.png') URL becomes
20
- * `lecodesfile://<abs-path>` which the renderer reads off disk — unpushed/edited files render with
21
- * no network. `--remote-assets` / an explicit `--public-url` switch to server URLs instead
22
- * (server-backed projects only; then data needs --wait-network).
23
- */
24
-
25
- // URL scheme emitted for a local project's resources so the renderer reads them off disk. Must match
26
- // lecodes-renderer's LOCAL_ASSET_SCHEME (headless/runProject.ts).
27
- export const LOCAL_ASSET_SCHEME = "lecodesfile://"
28
-
29
- export type HeadlessBundle = {
30
- js: string
31
- /** The project root the bundle was compiled from. */
32
- root: string
33
- /** Non-fatal compile-time notices (missing URLs, engine hints) — print via warnErr. */
34
- warnings: string[]
35
- /** Whether asset URLs resolve from the local disk (`lecodesfile://`). */
36
- useLocalAssets: boolean
37
- }
38
-
39
- /** Compile the local project for a headless run (honors --entry / --public-url / --remote-assets). */
40
- export const compileHeadlessBundle = async (args: Args, opts: { screen?: ScreenTarget } = {}): Promise<HeadlessBundle> => {
41
- let root: string
42
- let manifest: Manifest | null
43
- try {
44
- root = findProjectRoot(process.cwd())
45
- manifest = readManifest(root)
46
- } catch {
47
- root = process.cwd()
48
- manifest = null
49
- }
50
-
51
- const publicUrlFlag = flagStr(args, "public-url")
52
- // A local (server-less) project has no remote asset URLs — it always reads from disk.
53
- const useLocalAssets = manifest ? (!publicUrlFlag && !flagBool(args, "remote-assets")) : true
54
-
55
- let entries
56
- let warnings: string[]
57
- let name: string
58
- let publicUrl: string
59
-
60
- if (manifest) {
61
- const config = loadConfig()
62
- const apiUrl = requireApiUrl(config)
63
- const token = requireToken(config)
64
-
65
- note("Resolving project assets…")
66
- const project = await getProject(apiUrl, token, manifest.uuid)
67
- ;({ entries, warnings } = collectEntries(root, manifest, project))
68
- name = manifest.name
69
- publicUrl = useLocalAssets ? LOCAL_ASSET_SCHEME : (publicUrlFlag ?? apiUrl)
70
- // In local mode the "push it so its URL resolves" notices are moot (we read from disk).
71
- if (useLocalAssets) warnings = warnings.filter((w) => !w.includes("No server URL"))
72
- } else {
73
- ;({ entries, warnings } = collectLocalEntries(root, null, ""))
74
- name = root.split(/[\\/]/).pop() ?? "project"
75
- publicUrl = LOCAL_ASSET_SCHEME
76
- if (flagBool(args, "remote-assets") || publicUrlFlag) {
77
- warnings.push("This is a local project (no server) — assets always load from the working tree.")
78
- }
79
- }
80
-
81
- // Local assets (default): resolve each resource (asset('./x.png')) from the working tree instead of
82
- // the server, so unpushed / edited images show up with no --wait-network. Each resource's URL becomes
83
- // `lecodesfile://<abs-path>`, which the renderer reads off disk.
84
- const compileEntries = useLocalAssets
85
- ? entries.map((e) => (e.type === "resource" && e.absPath)
86
- ? { ...e, fileSrc: encodeURIComponent(e.absPath) }
87
- : e)
88
- : [...entries]
89
- // Rendering one screen: the generated entry that imports it and calls .open() (see screenEntry.ts).
90
- if (opts.screen) compileEntries.push(opts.screen.entry)
91
-
92
- // font() macro: vendor registry faces into assets/fonts/ so headless renders read them off disk
93
- // (lecodesfile://); with --remote-assets they stay CDN URLs like every other remote resource.
94
- await applyProjectFonts(compileEntries, useLocalAssets
95
- ? {
96
- mode: "vendor", root, vendorDir: root, vendorPrefix: "",
97
- makeResourceEntry: (path, absPath) => ({ path, type: "resource", fileSrc: encodeURIComponent(absPath), absPath }),
98
- }
99
- : { mode: "cdn", root })
100
-
101
- // assetIcon() macro: resolve from the user-level cache (~/.lecodes/cache/icons) and inline, so
102
- // headless renders stay deterministic and offline after the first fetch (like local assets).
103
- await applyAppAssetIcons(compileEntries)
104
-
105
- note("Compiling…")
106
- const entryFlag = opts.screen?.entryPath ?? flagStr(args, "entry")
107
- if (!entryFlag) {
108
- const ambiguous = ambiguousEntryWarning(compileEntries)
109
- if (ambiguous) warnings.push(ambiguous)
110
- }
111
- const js = await compileProject({
112
- entries: compileEntries,
113
- name,
114
- publicUrl,
115
- entryOverride: entryFlag,
116
- header: false,
117
- format: "iife",
118
- })
119
- if (js.includes("_creator.createScene") || js.includes("_creator.createEntity")) {
120
- warnings.push("3D project: the scene graph is captured as `scene3d` in the JSON output; 3D isn't rasterized into PNGs.")
121
- }
122
- // With --remote-assets, 2D textures come from the network; without --wait-network they resolve empty
123
- // and sprites render as flat placeholders. Local assets (default) load from disk, so no hint needed.
124
- if (js.includes("_creator2d") && !useLocalAssets && !flagBool(args, "wait-network")) {
125
- warnings.push("2D scene detected: sprite textures load over the network — add --wait-network to include them (else sprites render as flat placeholders).")
126
- }
127
-
128
- return { js, root, warnings, useLocalAssets }
129
- }
1
+ import { getProject } from "../platform/api"
2
+ import { loadConfig, requireApiUrl, requireToken } from "../platform/config"
3
+ import { findProjectRoot, readManifest, type Manifest } from "../project/manifest"
4
+ import { collectEntries } from "./collect"
5
+ import { collectLocalEntries } from "./collectLocal"
6
+ import { ambiguousEntryWarning, compileProject } from "./projectCompile"
7
+ import { applyProjectFonts } from "./fonts"
8
+ import { applyAppAssetIcons } from "./assetIcons"
9
+ import type { ScreenTarget } from "./screenEntry"
10
+ import { note } from "../cli"
11
+
12
+ /*
13
+ * Compile the project into the iife bundle the headless renderer runs — shared by `lecodes render`
14
+ * and `lecodes test`. Two modes, picked automatically (same split as `lecodes compile`):
15
+ * - Server-backed project (.lecodes/manifest.json): entries resolve from local files + the
16
+ * project's server metadata (needs login).
17
+ * - Local project (from `lecodes init` — no manifest): compiles straight from the working tree,
18
+ * no login.
19
+ * In both, resource assets default to LOCAL disk reads: each asset('./x.png') URL becomes
20
+ * `lecodesfile://<abs-path>` which the renderer reads off disk — unpushed/edited files render with
21
+ * no network. `--remote-assets` / an explicit `--public-url` switch to server URLs instead
22
+ * (server-backed projects only; then data needs --wait-network).
23
+ */
24
+
25
+ // URL scheme emitted for a local project's resources so the renderer reads them off disk. Must match
26
+ // lecodes-renderer's LOCAL_ASSET_SCHEME (headless/runProject.ts).
27
+ export const LOCAL_ASSET_SCHEME = "lecodesfile://"
28
+
29
+ export type HeadlessBundle = {
30
+ js: string
31
+ /** The project root the bundle was compiled from. */
32
+ root: string
33
+ /** Non-fatal compile-time notices (missing URLs, engine hints) — print via warnErr. */
34
+ warnings: string[]
35
+ /** Whether asset URLs resolve from the local disk (`lecodesfile://`). */
36
+ useLocalAssets: boolean
37
+ }
38
+
39
+ /** What a headless compile honours: `--entry` / `--public-url` / `--remote-assets` / `--wait-network`,
40
+ * and the screen module a `render <file>` named. */
41
+ export type HeadlessOptions = { entry?: string, publicUrl?: string, remoteAssets?: boolean, waitNetwork?: boolean, screen?: ScreenTarget }
42
+
43
+ /** Compile the local project for a headless run. */
44
+ export const compileHeadlessBundle = async (opts: HeadlessOptions = {}): Promise<HeadlessBundle> => {
45
+ let root: string
46
+ let manifest: Manifest | null
47
+ try {
48
+ root = findProjectRoot(process.cwd())
49
+ manifest = readManifest(root)
50
+ } catch {
51
+ root = process.cwd()
52
+ manifest = null
53
+ }
54
+
55
+ const publicUrlFlag = opts.publicUrl
56
+ // A local (server-less) project has no remote asset URLs — it always reads from disk.
57
+ const useLocalAssets = manifest ? (!publicUrlFlag && !opts.remoteAssets) : true
58
+
59
+ let entries
60
+ let warnings: string[]
61
+ let name: string
62
+ let publicUrl: string
63
+
64
+ if (manifest) {
65
+ const config = loadConfig()
66
+ const apiUrl = requireApiUrl(config)
67
+ const token = requireToken(config)
68
+
69
+ note("Resolving project assets…")
70
+ const project = await getProject(apiUrl, token, manifest.uuid)
71
+ ;({ entries, warnings } = collectEntries(root, manifest, project))
72
+ name = manifest.name
73
+ publicUrl = useLocalAssets ? LOCAL_ASSET_SCHEME : (publicUrlFlag ?? apiUrl)
74
+ // In local mode the "push it so its URL resolves" notices are moot (we read from disk).
75
+ if (useLocalAssets) warnings = warnings.filter((w) => !w.includes("No server URL"))
76
+ } else {
77
+ ;({ entries, warnings } = collectLocalEntries(root, null, ""))
78
+ name = root.split(/[\\/]/).pop() ?? "project"
79
+ publicUrl = LOCAL_ASSET_SCHEME
80
+ if (opts.remoteAssets || publicUrlFlag) {
81
+ warnings.push("This is a local project (no server) — assets always load from the working tree.")
82
+ }
83
+ }
84
+
85
+ // Local assets (default): resolve each resource (asset('./x.png')) from the working tree instead of
86
+ // the server, so unpushed / edited images show up with no --wait-network. Each resource's URL becomes
87
+ // `lecodesfile://<abs-path>`, which the renderer reads off disk.
88
+ const compileEntries = useLocalAssets
89
+ ? entries.map((e) => (e.type === "resource" && e.absPath)
90
+ ? { ...e, fileSrc: encodeURIComponent(e.absPath) }
91
+ : e)
92
+ : [...entries]
93
+ // Rendering one screen: the generated entry that imports it and calls .open() (see screenEntry.ts).
94
+ if (opts.screen) compileEntries.push(opts.screen.entry)
95
+
96
+ // font() macro: vendor registry faces into assets/fonts/ so headless renders read them off disk
97
+ // (lecodesfile://); with --remote-assets they stay CDN URLs like every other remote resource.
98
+ await applyProjectFonts(compileEntries, useLocalAssets
99
+ ? {
100
+ mode: "vendor", root, vendorDir: root, vendorPrefix: "",
101
+ makeResourceEntry: (path, absPath) => ({ path, type: "resource", fileSrc: encodeURIComponent(absPath), absPath }),
102
+ }
103
+ : { mode: "cdn", root })
104
+
105
+ // assetIcon() macro: resolve from the user-level cache (~/.lecodes/cache/icons) and inline, so
106
+ // headless renders stay deterministic and offline after the first fetch (like local assets).
107
+ await applyAppAssetIcons(compileEntries)
108
+
109
+ note("Compiling…")
110
+ const entryFlag = opts.screen?.entryPath ?? opts.entry
111
+ if (!entryFlag) {
112
+ const ambiguous = ambiguousEntryWarning(compileEntries)
113
+ if (ambiguous) warnings.push(ambiguous)
114
+ }
115
+ const js = await compileProject({
116
+ entries: compileEntries,
117
+ name,
118
+ publicUrl,
119
+ entryOverride: entryFlag,
120
+ header: false,
121
+ format: "iife",
122
+ })
123
+ if (js.includes("_creator.createScene") || js.includes("_creator.createEntity")) {
124
+ warnings.push("3D project: the scene graph is captured as `scene3d` in the JSON output; 3D isn't rasterized into PNGs.")
125
+ }
126
+ // With --remote-assets, 2D textures come from the network; without --wait-network they resolve empty
127
+ // and sprites render as flat placeholders. Local assets (default) load from disk, so no hint needed.
128
+ if (js.includes("_creator2d") && !useLocalAssets && !opts.waitNetwork) {
129
+ warnings.push("2D scene detected: sprite textures load over the network — add --wait-network to include them (else sprites render as flat placeholders).")
130
+ }
131
+
132
+ return { js, root, warnings, useLocalAssets }
133
+ }
@@ -1,8 +1,8 @@
1
1
  import { existsSync } from "node:fs"
2
2
  import { createRequire } from "node:module"
3
3
  import { dirname, join } from "node:path"
4
- import type { FileKind } from "../manifest"
5
- import { distRoot } from "../distRoot"
4
+ import type { FileKind } from "../project/manifest"
5
+ import { distRoot } from "../hosts/distRoot"
6
6
  import { compileProject as sdkCompileProject, detectEntry, scanModulesChisel, ENTRY_CONVENTION } from "lecodes-sdk/compile"
7
7
 
8
8
  // Every project compiles through the SDK's one import-free compiler, which bundles with chisel (the
@@ -1,11 +1,11 @@
1
1
  import { createHash } from "node:crypto"
2
2
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
3
3
  import { join } from "node:path"
4
- import type { Manifest } from "../manifest"
5
- import { scanLocal } from "../localFiles"
6
- import { loadIgnore } from "../ignore"
7
- import { distRoot } from "../distRoot"
8
- import { CliError, warnErr } from "../util"
4
+ import type { Manifest } from "../project/manifest"
5
+ import { scanLocal } from "../project/localFiles"
6
+ import { loadIgnore } from "../project/ignore"
7
+ import { distRoot } from "../hosts/distRoot"
8
+ import { CliError, warnErr } from "../cli"
9
9
  import { compileProject, type CompileEntry } from "./projectCompile"
10
10
  import { applyLocalShaders, shadersCacheDir } from "./shaders"
11
11
 
@@ -55,7 +55,7 @@ export const compileSceneEditor = async (opts: {
55
55
  scenePath: string
56
56
  }): Promise<{ js: string | null, error?: string }> => {
57
57
  const { root, scenePath } = opts
58
- const { loadSceneHarness, sceneEditorEntries, SCENE_ENTRY_PATH } = await import("lecodes-sdk/compile")
58
+ const { loadSceneHarness, sceneEditorEntries, sceneImportsOf, SCENE_ENTRY_PATH } = await import("lecodes-sdk/compile")
59
59
 
60
60
  const entries: CompileEntry[] = []
61
61
  const scan = scanLocal(root, { ignore: loadIgnore(root), tracked: new Set(Object.keys(opts.manifest?.files ?? {})) })
@@ -96,10 +96,12 @@ export const compileSceneEditor = async (opts: {
96
96
  }
97
97
 
98
98
  const scene = scenePath.startsWith("./") ? scenePath : "./" + rel(scenePath)
99
- if (!entries.some((e) => e.path === scene && e.type === "text")) {
99
+ const sceneEntry = entries.find((e) => e.path === scene && e.type === "text")
100
+ if (!sceneEntry) {
100
101
  return { js: null, error: `Scene file not found: ${scene}` }
101
102
  }
102
- entries.push(...sceneEditorEntries(scene, resolveHarness(loadSceneHarness), entries.map((e) => e.path)))
103
+ const paths = entries.map((e) => e.path)
104
+ entries.push(...sceneEditorEntries(scene, resolveHarness(loadSceneHarness), paths, sceneImportsOf(sceneEntry.text ?? "", scene, paths)))
103
105
 
104
106
  try {
105
107
  const js = await compileProject({