lecodes-cli 0.20.2 → 2.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 (154) hide show
  1. package/README.md +56 -57
  2. package/dist/index.js +11052 -7592
  3. package/package.json +14 -8
  4. package/runtime/materials/decal-relief.mat +12 -1
  5. package/runtime/materials/decal.mat +17 -3
  6. package/runtime/materials/lightmap.mat +117 -138
  7. package/runtime/materials/particles-quad.mat +6 -2
  8. package/runtime/materials/terrain-lightmap.mat +123 -0
  9. package/runtime/materials/terrain.mat +36 -55
  10. package/runtime/sdk-types.json +1 -1
  11. package/runtime/web/VERSION.json +5 -0
  12. package/runtime/web/assets/camera-5XOM8eHl.js +1 -0
  13. package/runtime/web/assets/creator-2d-D9WKYM6h.js +1 -0
  14. package/runtime/web/assets/creator-2d-ukvEmNEp.wasm +0 -0
  15. package/runtime/web/assets/creator-full-BBXzQlhq.js +1 -0
  16. package/runtime/web/assets/creator-full-D2dSDdDj.wasm +0 -0
  17. package/runtime/web/assets/creator-full.js-BYswGsqR.symbols +8124 -0
  18. package/runtime/web/assets/creator-ui-DIc92ion.wasm +0 -0
  19. package/runtime/web/assets/creator-ui-_1Z_AX2M.js +1 -0
  20. package/runtime/web/assets/geolocation-BjTKN9_P.js +1 -0
  21. package/runtime/web/assets/inspect-C7x78PNs.js +2 -0
  22. package/runtime/web/assets/map-CyBxOSV0.js +1 -0
  23. package/runtime/web/assets/map-XzoOFt0E.js +2 -0
  24. package/runtime/web/assets/mapImpl-bxvQ2h1Z.js +823 -0
  25. package/runtime/web/assets/maplibre-gl-worker-B7LXXUkR.js +8 -0
  26. package/runtime/web/assets/materials-uri4FvPE.bin +0 -0
  27. package/runtime/web/assets/materials-world-CQWt8TAm.bin +0 -0
  28. package/runtime/web/assets/qr-scanner-CK_cYoJR.js +1 -0
  29. package/runtime/web/assets/uberarchive-DLH-kBhM.bin +0 -0
  30. package/runtime/web/index.html +36 -0
  31. package/runtime/web/player.js +21 -0
  32. package/runtime/web-page/index.html +41 -0
  33. package/runtime/web-page/page.css +1 -0
  34. package/runtime/web-page/page.js +31 -0
  35. package/src/api.ts +1 -302
  36. package/src/cli/args.ts +37 -0
  37. package/src/cli/command.ts +113 -0
  38. package/src/cli/errors.ts +13 -0
  39. package/src/cli/help.ts +158 -0
  40. package/src/cli/index.ts +7 -0
  41. package/src/cli/output.ts +102 -0
  42. package/src/cli/run.ts +106 -0
  43. package/src/commands/{appAndroid.ts → app/android.ts} +454 -43
  44. package/src/commands/{appDesktop.ts → app/desktop.ts} +64 -33
  45. package/src/commands/{appDesktopMac.ts → app/desktopMac.ts} +34 -27
  46. package/src/commands/{appIcon.ts → app/icon.ts} +11 -8
  47. package/src/commands/{app.ts → app/index.ts} +400 -290
  48. package/src/commands/{appShared.ts → app/shared.ts} +43 -44
  49. package/src/commands/{appTemplatesAndroid.ts → app/templates/android.ts} +72 -39
  50. package/src/commands/{appTemplatesGradlew.ts → app/templates/gradlew.ts} +1 -1
  51. package/src/commands/{appTemplates.ts → app/templates/ios.ts} +2 -2
  52. package/src/commands/assets.ts +23 -21
  53. package/src/commands/clone.ts +48 -51
  54. package/src/commands/compile.ts +76 -89
  55. package/src/commands/create.ts +46 -47
  56. package/src/commands/design/comments.ts +361 -0
  57. package/src/commands/design/context.ts +101 -0
  58. package/src/commands/design/index.ts +391 -0
  59. package/src/commands/design/snapshot.ts +134 -0
  60. package/src/commands/{designTemplates.ts → design/templates.ts} +4 -3
  61. package/src/commands/desktop.ts +94 -82
  62. package/src/commands/dev.ts +260 -182
  63. package/src/commands/diff.ts +39 -51
  64. package/src/commands/index.ts +64 -0
  65. package/src/commands/{init.ts → init/index.ts} +195 -191
  66. package/src/commands/{projectTemplates.ts → init/templates.ts} +64 -21
  67. package/src/commands/install.ts +80 -67
  68. package/src/commands/lightmap.ts +543 -252
  69. package/src/commands/link.ts +87 -94
  70. package/src/commands/login.ts +25 -23
  71. package/src/commands/navmesh.ts +169 -139
  72. package/src/commands/plugin.ts +46 -0
  73. package/src/commands/pn.ts +201 -244
  74. package/src/commands/pull.ts +49 -56
  75. package/src/commands/push.ts +48 -54
  76. package/src/commands/render.ts +176 -111
  77. package/src/commands/scene.ts +63 -55
  78. package/src/commands/serve.ts +58 -0
  79. package/src/commands/{shaders.ts → shaders/index.ts} +61 -42
  80. package/src/commands/{shadersNew.ts → shaders/new.ts} +3 -3
  81. package/src/commands/shared.ts +79 -0
  82. package/src/commands/status.ts +22 -26
  83. package/src/commands/test.ts +94 -63
  84. package/src/commands/thumbs.ts +185 -175
  85. package/src/commands/update/index.ts +226 -0
  86. package/src/commands/{types.ts → update/types.ts} +31 -22
  87. package/src/compile/collect.ts +5 -5
  88. package/src/compile/collectLocal.ts +3 -3
  89. package/src/compile/designCompile.ts +4 -4
  90. package/src/compile/headlessBundle.ts +16 -12
  91. package/src/compile/nativeStack.ts +1 -1
  92. package/src/compile/projectCompile.ts +12 -4
  93. package/src/compile/sceneCompile.ts +14 -26
  94. package/src/compile/screenEntry.ts +123 -127
  95. package/src/compile/shaders.ts +3 -3
  96. package/src/{lecodes-3d-editor.d.ts → declarations/lecodes-3d-editor.d.ts} +4 -0
  97. package/src/{lecodes-assets.d.ts → declarations/lecodes-assets.d.ts} +1 -1
  98. package/src/{lecodes-renderer.d.ts → declarations/lecodes-headless.d.ts} +3 -3
  99. package/src/dev/androidDev.ts +1 -1
  100. package/src/dev/clientTemplates.ts +18 -14
  101. package/src/dev/devServer.ts +78 -41
  102. package/src/dev/iosDev.ts +180 -0
  103. package/src/dev/webRunner.ts +83 -15
  104. package/src/{cmgenTool.ts → hosts/cmgenTool.ts} +2 -2
  105. package/src/{desktopRenderer.ts → hosts/desktopRenderer.ts} +104 -32
  106. package/src/{desktopScript.ts → hosts/desktopScript.ts} +151 -36
  107. package/src/{distRoot.ts → hosts/distRoot.ts} +9 -5
  108. package/src/{matcTool.ts → hosts/matcTool.ts} +2 -2
  109. package/src/{peerInstall.ts → hosts/peerInstall.ts} +7 -7
  110. package/src/{peers.ts → hosts/peers.ts} +2 -2
  111. package/src/{releases.ts → hosts/releases.ts} +1 -1
  112. package/src/index.ts +35 -480
  113. package/src/platform/api.ts +302 -0
  114. package/src/{browserAuth.ts → platform/browserAuth.ts} +1 -1
  115. package/src/{config.ts → platform/config.ts} +1 -1
  116. package/src/{serverDiff.ts → platform/serverDiff.ts} +3 -3
  117. package/src/plugin/contract.ts +438 -0
  118. package/src/plugin/emitKotlin.ts +357 -0
  119. package/src/plugin/emitSdk.ts +259 -0
  120. package/src/plugin/emitSwift.ts +336 -0
  121. package/src/plugin/emitWeb.ts +383 -0
  122. package/src/plugin/generate.ts +157 -0
  123. package/src/plugin/lines.ts +28 -0
  124. package/src/plugin/lower.ts +231 -0
  125. package/src/plugin/manifest.ts +192 -0
  126. package/src/{projectEnv.ts → project/env.ts} +33 -2
  127. package/src/{ignore.ts → project/ignore.ts} +164 -163
  128. package/src/{localFiles.ts → project/localFiles.ts} +1 -1
  129. package/src/{manifest.ts → project/manifest.ts} +1 -1
  130. package/src/{project.ts → project/materialize.ts} +2 -2
  131. package/src/project/paths.ts +20 -0
  132. package/src/{textDiff.ts → project/textDiff.ts} +1 -1
  133. package/src/{types.ts → project/types.ts} +0 -0
  134. package/runtime/materials/lightmap-baked-lite.mat +0 -175
  135. package/runtime/materials/lightmap-baked.mat +0 -176
  136. package/runtime/scene-harness.json +0 -1
  137. package/runtime/web/assets/__vite-browser-external-BIHI7g3E.js +0 -1
  138. package/runtime/web/assets/basis-C64VHDVD.js +0 -1
  139. package/runtime/web/assets/basis_transcoder-VXdx5NbI.wasm +0 -0
  140. package/runtime/web/assets/createViewerLite-Ct_PZdof.js +0 -852
  141. package/runtime/web/assets/draco-BiISTFcR.js +0 -118
  142. package/runtime/web/assets/draco_decoder-DsQ12WqX.wasm +0 -0
  143. package/runtime/web/assets/index-BMt7AnC5.js +0 -2
  144. package/runtime/web/assets/mapViewImpl-B2JcES8l.js +0 -810
  145. package/runtime/web/assets/maplibre-gl-worker-CJfwIrte.js +0 -8
  146. package/runtime/web/assets/worker-Caf-yYEI.js +0 -2
  147. package/runtime/web/embed.html +0 -32
  148. package/runtime/web/embed.js +0 -171
  149. package/src/commands/design.ts +0 -846
  150. package/src/commands/update.ts +0 -211
  151. package/src/util.ts +0 -146
  152. /package/src/{lecodes-design.d.ts → declarations/lecodes-design.d.ts} +0 -0
  153. /package/src/{qrcode-terminal.d.ts → declarations/qrcode-terminal.d.ts} +0 -0
  154. /package/src/{designMeta.ts → project/designMeta.ts} +0 -0
@@ -1,12 +1,13 @@
1
1
  import { execFileSync, spawn } from "node:child_process"
2
2
  import { cpSync, existsSync, mkdirSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs"
3
3
  import { basename, dirname, join, resolve } from "node:path"
4
- import { buildBundle } from "./compile"
5
- import { resolveDesktopExe, selectedDesktopVersion } from "../desktopRenderer"
6
- import { CliError, c, flagBool, flagStr, log, note, success, warn, type Args } from "../util"
7
- import { assertRendererSupportsBundle, desktopPluginLibraries, projectRoot, readJson, resolveDesktopRenderer, writeJson, type AppConfig } from "./appShared"
8
- import { loadIconMaster } from "./appIcon"
9
- import { buildMacApp, macHostProvidedNames } from "./appDesktopMac"
4
+ import { buildBundle } from "../compile"
5
+ import { resolveDesktopExe, selectedDesktopVersion } from "../../hosts/desktopRenderer"
6
+ import { CliError, bool, c, defineCommand, log, note, num, str, success, warn, type FlagValues } from "../../cli"
7
+ import { bundleFlags, bundleOptions } from "../shared"
8
+ import { desktopPluginLibraries, projectRoot, readJson, resolveDesktopRenderer, writeJson, type AppConfig } from "./shared"
9
+ import { loadIconMaster } from "./icon"
10
+ import { buildMacApp, macHostProvidedNames } from "./desktopMac"
10
11
 
11
12
  /*
12
13
  * `lecodes app init|build desktop` — the desktop arm of the native-app commands
@@ -14,7 +15,8 @@ import { buildMacApp, macHostProvidedNames } from "./appDesktopMac"
14
15
  * `build` stages the verified cached host + the compiled bundle + its assets into one portable
15
16
  * folder that runs on a double-click. On macOS the same command produces a signed (and, with a
16
17
  * notarytool profile, notarized) .app instead — the staging half is shared, the bundle half lives
17
- * in appDesktopMac.ts.
18
+ * in desktopMac.ts. The `build` node is defined here and mounted twice: `lecodes desktop build`
19
+ * (desktop.ts) and `lecodes app build desktop` (index.ts) are the same command.
18
20
  *
19
21
  * The staged folder is SELF-DESCRIBING: a pruned app.json (name + desktop.window) ships next to
20
22
  * app.js, and the host (≥1.3.0) reads it for title/size/min-size/resizable/fullscreen — no
@@ -32,16 +34,19 @@ const dirSize = (dir: string): number =>
32
34
 
33
35
  /* --------------------------------------- init ---------------------------------------- */
34
36
 
37
+ /** The `app init desktop` flags (index.ts declares the shared init node). */
38
+ export interface DesktopInitOptions { name?: string, fullscreen?: boolean }
39
+
35
40
  /** `lecodes app init desktop` — declare the desktop target in app.json (mirrors
36
41
  * `lecodes init --desktop` for existing projects). Purely additive; re-running keeps edits. */
37
- export const initDesktop = async (args: Args) => {
42
+ export const initDesktop = async (opts: DesktopInitOptions): Promise<void> => {
38
43
  const root = projectRoot()
39
44
  const existing = readJson<AppConfig>(join(root, "app.json")) ?? {}
40
- const name = flagStr(args, "name") ?? existing.name ?? basename(root)
45
+ const name = opts.name ?? existing.name ?? basename(root)
41
46
  const window = {
42
47
  width: 1280, height: 800, minWidth: 640, minHeight: 480,
43
48
  ...existing.desktop?.window,
44
- ...(flagBool(args, "fullscreen") ? { fullscreen: true } : {}),
49
+ ...(opts.fullscreen ? { fullscreen: true } : {}),
45
50
  }
46
51
  const config: AppConfig = { ...existing, name, desktop: { ...existing.desktop, window } }
47
52
  writeJson(join(root, "app.json"), config)
@@ -60,12 +65,27 @@ const cleanFileName = (name: string) =>
60
65
  const ASSETS_DIR = "assets" // the project's own files
61
66
  const RUNTIME_DIR = "runtime" // the host's engine files (initial.js, built-in materials, IBL)
62
67
 
63
- export const buildDesktop = async (args: Args) => {
68
+ const desktopBuildFlags = {
69
+ out: str("output folder (default dist/<name>)", { alias: "o", value: "<dir>" }),
70
+ slug: str("folder / zip name override (Latin-only)", { value: "<name>" }),
71
+ renderer: str("host + shader variant: gl | vulkan (default: app.json desktop.renderer, else gl)", { value: "<gl|vulkan>" }),
72
+ probe: bool("headless-render the staged folder to a PNG next to it (macOS: a brief windowed run + snapshot)"),
73
+ frames: num("(--probe) frames to render before the snapshot", { default: 90 }),
74
+ after: num("(--probe, macOS) seconds the app runs before the snapshot", { default: 8 }),
75
+ zip: bool("also pack the result for sending: <out>.zip with UTF-8 / forward-slash entries (macOS: a ditto archive of the .app)"),
76
+ dmg: bool("macOS: also build a drag-to-Applications disk image (signed + notarized too)"),
77
+ sign: str("macOS codesign identity — a \"Developer ID Application: …\" cert, or adhoc (local only)", { value: "<identity|adhoc>" }),
78
+ notarize: str("macOS: notarize + staple with this `xcrun notarytool store-credentials` keychain profile", { value: "<profile>" }),
79
+ "no-notarize": bool("macOS: skip notarization even when app.json / the environment names a profile"),
80
+ ...bundleFlags,
81
+ }
82
+
83
+ const buildDesktop = async (flags: FlagValues<typeof desktopBuildFlags>): Promise<void> => {
64
84
  const root = projectRoot()
65
85
  const config = readJson<AppConfig>(join(root, "app.json")) ?? {}
66
86
  const name = (config.name ?? basename(root)).trim() || "app"
67
- const slug = flagStr(args, "slug") ?? cleanFileName(name)
68
- const out = resolve(root, flagStr(args, "o", "out") ?? join(root, "dist", slug))
87
+ const slug = flags.slug ?? cleanFileName(name)
88
+ const out = resolve(root, flags.out ?? join(root, "dist", slug))
69
89
 
70
90
  // Only a previous build output (or an empty/absent dir) may be replaced — never wipe a
71
91
  // directory this command didn't produce.
@@ -78,24 +98,21 @@ export const buildDesktop = async (args: Args) => {
78
98
  }
79
99
 
80
100
  // Per-target entry (app.json desktop.entry); an explicit --entry wins.
81
- if (config.desktop?.entry && flagStr(args, "entry") === undefined) args.flags.entry = config.desktop.entry
101
+ const bundle = bundleOptions(flags)
102
+ if (config.desktop?.entry && bundle.entry === undefined) bundle.entry = config.desktop.entry
82
103
 
83
104
  // ONE renderer decision: it selects the host binary staged into the folder AND the single
84
105
  // .filamat variant compiled beside the bundle. Two independent reads of the config is how a
85
106
  // build ships a Vulkan exe next to an OpenGL shader (a Filament abort at the first material).
86
- const renderer = resolveDesktopRenderer(root, args)
107
+ const renderer = resolveDesktopRenderer(root, flags.renderer)
87
108
 
88
109
  const mac = process.platform === "darwin"
89
110
  note(`Building "${name}" for ${mac ? "macOS" : "desktop"}${renderer === "vulkan" ? " (Vulkan)" : ""}…`)
90
- const { js, resources } = await buildBundle(
91
- args,
92
- { remotePatterns: config.resources?.remote ?? [] },
93
- { localShaders: true, desktopRenderer: renderer },
94
- )
95
-
96
- // The Vulkan host has no 2D engine — refuse now rather than shipping a folder that opens a blank
97
- // window (see assertRendererSupportsBundle).
98
- assertRendererSupportsBundle(renderer, js)
111
+ const { js, resources } = await buildBundle(bundle, {
112
+ shell: { remotePatterns: config.resources?.remote ?? [] },
113
+ localShaders: true,
114
+ desktopRenderer: renderer,
115
+ })
99
116
 
100
117
  // Only what the bundle actually references ships — the compile inventory lists EVERY project
101
118
  // resource (design/ references included), which would multiply the build size. asset() compiles
@@ -145,9 +162,14 @@ export const buildDesktop = async (args: Args) => {
145
162
  throw new CliError(`The bundle references assets with no local file: ${missing.join(", ")}`)
146
163
  }
147
164
 
148
- // macOS: one signed .app rather than a folder — see appDesktopMac.ts.
149
- if (mac) return buildMacApp({ root, config, name, slug, out, js, shipped, hostExe: exe, args })
150
- if (flagBool(args, "dmg")) warn("--dmg is a macOS format — ignored on this platform.")
165
+ // macOS: one signed .app rather than a folder — see desktopMac.ts.
166
+ if (mac) {
167
+ return buildMacApp({
168
+ root, config, name, slug, out, js, shipped, hostExe: exe,
169
+ opts: { sign: flags.sign, notarize: flags.notarize, noNotarize: flags["no-notarize"], dmg: flags.dmg, zip: flags.zip, probe: flags.probe, after: flags.after },
170
+ })
171
+ }
172
+ if (flags.dmg) warn("--dmg is a macOS format — ignored on this platform.")
151
173
 
152
174
  const version = process.env.LECODES_DESKTOP_EXE ? "local build" : selectedDesktopVersion()
153
175
 
@@ -218,7 +240,7 @@ export const buildDesktop = async (args: Args) => {
218
240
  note(" app.ico rendered from " + config.icon)
219
241
  }
220
242
 
221
- // 5) Native plugins (packages/desktop/plugin-sdk/lecodes_plugin.h): the project's plugins/ dir +
243
+ // 5) Native plugins (hosts/desktop/plugin-sdk/lecodes_plugin.h): the project's plugins/ dir +
222
244
  // every declared plugin's desktop library → plugins/ next to the exe, where a host WITH the plugin
223
245
  // loader (added after 1.4.0) scans at startup; an older host ignores the folder.
224
246
  // A declared library that's missing is a build error (strict).
@@ -236,11 +258,21 @@ export const buildDesktop = async (args: Args) => {
236
258
 
237
259
  success(`Portable build: ${out} (${mb(dirSize(out))})`)
238
260
 
239
- if (flagBool(args, "probe")) await probeBuild(out, exeName, config, args)
240
- if (flagBool(args, "zip")) zipBuild(out)
241
- if (!flagBool(args, "probe")) note(`Tip: --probe renders the staged folder headless to verify exactly what ships.`)
261
+ if (flags.probe) await probeBuild(out, exeName, config, flags.frames)
262
+ if (flags.zip) zipBuild(out)
263
+ if (!flags.probe) note(`Tip: --probe renders the staged folder headless to verify exactly what ships.`)
242
264
  }
243
265
 
266
+ /** `lecodes desktop build` and `lecodes app build desktop` — the one node, mounted in both trees. */
267
+ export const desktopBuild = defineCommand({
268
+ name: "build",
269
+ summary: "Stage a portable desktop app folder (dist/<name>/): host exe + app.js + assets + runtime",
270
+ description: "The cached host renamed to <Name>.exe + app.js + assets/ (the project's files) + runtime/ (the engine's) + app.json (the host reads title/size/fullscreen from it) + app.ico from app.json \"icon\". Duplicate asset basenames are a build error (they silently alias in the bundle). Also spelled `lecodes app build desktop`.\n\nOn macOS: a signed .app instead (the downloaded LeCodes Desktop.app + the project inside it); app.json desktop.macos = bundleId / buildNumber / category / signing.identity / notarize.profile (a notarytool keychain profile — notarized + stapled, so it opens on any Mac without \"Open Anyway\"). --sign / --notarize / --no-notarize override it per build; LECODES_MACOS_SIGN_ID / LECODES_NOTARY_PROFILE sit between the flags and app.json.",
271
+ flags: desktopBuildFlags,
272
+ examples: ["lecodes desktop build", "lecodes app build desktop --zip --probe", "lecodes desktop build -o out/kiosk --renderer vulkan", "lecodes app build desktop --dmg --notarize MyProfile"],
273
+ run: ({ flags }) => buildDesktop(flags),
274
+ })
275
+
244
276
  /** app.ico from the app.json icon master: PNG-compressed frames (fine on the Win10+ the
245
277
  * distribution requires) at the sizes Windows asks for — no image tooling needed. */
246
278
  const writeIco = async (root: string, config: AppConfig, dest: string) => {
@@ -272,9 +304,8 @@ const writeIco = async (root: string, config: AppConfig, dest: string) => {
272
304
 
273
305
  /** `--probe`: run the STAGED folder hidden and dump a PNG next to it — verifies exactly what
274
306
  * ships (the staged host, its assets, the bundle), not the dev environment. */
275
- const probeBuild = async (out: string, exeName: string, config: AppConfig, args: Args) => {
307
+ const probeBuild = async (out: string, exeName: string, config: AppConfig, frames: number) => {
276
308
  const win = config.desktop?.window
277
- const frames = Number(flagStr(args, "frames")) || 90
278
309
  const shot = join(dirname(out), `${basename(out)}-probe.png`)
279
310
  note(`Probe: hidden render of the staged folder (${frames} frames)…`)
280
311
  await new Promise<void>((done, fail) => {
@@ -2,12 +2,12 @@ import { execFileSync, spawn, spawnSync } from "node:child_process"
2
2
  import { cpSync, existsSync, mkdirSync, mkdtempSync, readdirSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs"
3
3
  import { tmpdir } from "node:os"
4
4
  import { basename, join, resolve } from "node:path"
5
- import { CliError, c, flagBool, flagStr, log, note, success, warn, type Args } from "../util"
6
- import { writeJson, type AppConfig, type MacDesktopConfig } from "./appShared"
7
- import { loadIconMaster } from "./appIcon"
5
+ import { CliError, c, log, note, success, warn } from "../../cli"
6
+ import { writeJson, type AppConfig, type MacDesktopConfig } from "./shared"
7
+ import { loadIconMaster } from "./icon"
8
8
 
9
9
  /*
10
- * `lecodes app build desktop` on macOS — the .app arm of appDesktop.ts.
10
+ * `lecodes app build desktop` on macOS — the .app arm of desktop.ts.
11
11
  *
12
12
  * Windows/Linux stage a portable FOLDER (exe + app.js + assets/ + runtime/). macOS has no such
13
13
  * thing: Gatekeeper judges one signed bundle as a whole, and a Finder launch runs with cwd "/", so
@@ -53,14 +53,14 @@ const cleanBundleName = (name: string) => name.replace(/[/:]/g, "").trim() || "A
53
53
 
54
54
  /* ------------------------------------- host bundle ------------------------------------- */
55
55
 
56
- /** The .app that owns a host executable (…/X.app/Contents/MacOS/lecodes-app → …/X.app). */
56
+ /** The .app that owns a host executable (…/X.app/Contents/MacOS/lecodes-desktop → …/X.app). */
57
57
  export const macHostAppOf = (exe: string): string => {
58
58
  const app = resolve(exe, "..", "..", "..")
59
59
  if (!app.endsWith(".app") || !existsSync(join(app, "Contents", "Info.plist"))) {
60
60
  throw new CliError(
61
61
  `The macOS host must be an .app bundle (got ${exe}). Install LeCodes Desktop.app, or point ` +
62
- `LECODES_DESKTOP_EXE at <Build>.app/Contents/MacOS/lecodes-app — a bare SwiftPM binary has no ` +
63
- `resource bundles and can't be re-signed into an app.`,
62
+ `LECODES_DESKTOP_EXE at <Build>.app/Contents/MacOS/lecodes-desktop — a bare build-tree binary has no ` +
63
+ `Resources/runtime and can't be re-signed into an app (hosts/desktop/package-macos.sh makes the bundle).`,
64
64
  )
65
65
  }
66
66
  return app
@@ -120,7 +120,7 @@ export const assertMacHostCanBundle = (exe: string) => {
120
120
  `This LeCodes Desktop host (${macHostAppOf(exe)}) predates project bundles — an app built from it would ` +
121
121
  "open the host's home screen instead of the project.\n" +
122
122
  " Run `lecodes desktop update` (needs a published macOS host with project-bundle support), or point " +
123
- "LECODES_DESKTOP_EXE at a current build (…/LeCodes Desktop.app/Contents/MacOS/lecodes-app).",
123
+ "LECODES_DESKTOP_EXE at a current build (…/LeCodes Desktop.app/Contents/MacOS/lecodes-desktop).",
124
124
  )
125
125
  }
126
126
 
@@ -318,17 +318,17 @@ const writeIcns = async (root: string, config: AppConfig, dest: string, tmp: str
318
318
 
319
319
  /* ---------------------------------------- probe ---------------------------------------- */
320
320
 
321
- /** `--probe`: launch the built app for a few seconds and grab the presented 3D frame through the
322
- * host's LECODES_SNAPSHOT hook. The window shows briefly — the macOS host has no hidden mode. */
323
- const probeMacApp = async (app: string, out: string, args: Args) => {
324
- const after = Number(flagStr(args, "after")) || 8
321
+ /** `--probe`: launch the built app for a few seconds and grab its last frame through the host's
322
+ * headless render contract (CREATOR_MAX_FRAMES + CREATOR_DUMP_PNG, the same as render --desktop).
323
+ * The window shows: the probe proves the SHIPPED bundle launches from cwd "/", not the hidden path. */
324
+ const probeMacApp = async (app: string, out: string, after: number) => {
325
325
  const shot = join(out, `${basename(app, ".app")}-probe.png`)
326
326
  rmSync(shot, { force: true })
327
- note(`Probe: launching the built app for ${after + 2}s (the window will show)…`)
327
+ note(`Probe: launching the built app for ~${after + 2}s (the window will show)…`)
328
328
  await new Promise<void>((done, fail) => {
329
- const child = spawn(join(app, "Contents", "MacOS", "lecodes-app"), [], {
329
+ const child = spawn(join(app, "Contents", "MacOS", "lecodes-desktop"), [], {
330
330
  cwd: "/", // what Finder gives a double-clicked app — proves nothing leans on the cwd
331
- env: { ...process.env, LECODES_SNAPSHOT: shot, LECODES_SNAPSHOT_AFTER: String(after), LECODES_EXIT_AFTER: String(after + 2) },
331
+ env: { ...process.env, CREATOR_DUMP_PNG: shot, CREATOR_MAX_FRAMES: String((after + 2) * 60) },
332
332
  stdio: ["ignore", "pipe", "pipe"],
333
333
  })
334
334
  const tail: string[] = []
@@ -348,6 +348,18 @@ const probeMacApp = async (app: string, out: string, args: Args) => {
348
348
 
349
349
  /* ---------------------------------------- build ---------------------------------------- */
350
350
 
351
+ /** The macOS-relevant `desktop build` flags (desktop.ts declares them; this is the typed subset). */
352
+ export interface MacBuildOptions {
353
+ sign?: string
354
+ notarize?: string
355
+ noNotarize: boolean
356
+ dmg: boolean
357
+ zip: boolean
358
+ probe: boolean
359
+ /** `--probe`: seconds the app runs before the snapshot. */
360
+ after: number
361
+ }
362
+
351
363
  export interface MacBuildInput {
352
364
  root: string
353
365
  config: AppConfig
@@ -357,11 +369,11 @@ export interface MacBuildInput {
357
369
  js: string
358
370
  shipped: { name: string, path: string, absPath?: string }[]
359
371
  hostExe: string
360
- args: Args
372
+ opts: MacBuildOptions
361
373
  }
362
374
 
363
375
  export const buildMacApp = async (input: MacBuildInput) => {
364
- const { root, config, name, slug, out, js, shipped, hostExe, args } = input
376
+ const { root, config, name, slug, out, js, shipped, hostExe, opts } = input
365
377
  const hostApp = macHostAppOf(hostExe)
366
378
  assertMacHostCanBundle(hostExe)
367
379
  const mac = config.desktop?.macos ?? {}
@@ -416,12 +428,7 @@ export const buildMacApp = async (input: MacBuildInput) => {
416
428
  note(` bundle: ${bundleId} ${config.version ?? "1.0"} (${mac.buildNumber ?? 1})`)
417
429
 
418
430
  // 4) Sign the whole bundle (the project files are now sealed too), then notarize + staple.
419
- const signing = resolveMacSigning(mac, {
420
- sign: flagStr(args, "sign"),
421
- notarize: flagStr(args, "notarize"),
422
- noNotarize: flagBool(args, "no-notarize"),
423
- env: process.env,
424
- }, developerIdInKeychain)
431
+ const signing = resolveMacSigning(mac, { sign: opts.sign, notarize: opts.notarize, noNotarize: opts.noNotarize, env: process.env }, developerIdInKeychain)
425
432
  await signApp(app, signing, hostApp, tmp)
426
433
  if (signing.notaryProfile) {
427
434
  notarize(app, signing.notaryProfile, tmp, "app")
@@ -439,16 +446,16 @@ export const buildMacApp = async (input: MacBuildInput) => {
439
446
  success(`App: ${app} (${mb(dirSize(app))})`)
440
447
 
441
448
  // The DMG goes before --probe/--zip so a failure there costs nothing already earned.
442
- if (flagBool(args, "dmg")) await buildDmg(app, out, appName, signing, tmp)
443
- if (flagBool(args, "probe")) await probeMacApp(app, out, args)
444
- if (flagBool(args, "zip")) {
449
+ if (opts.dmg) await buildDmg(app, out, appName, signing, tmp)
450
+ if (opts.probe) await probeMacApp(app, out, opts.after)
451
+ if (opts.zip) {
445
452
  // ditto, not zip(1): it keeps the bundle's structure/xattrs (the stapled ticket rides in
446
453
  // Contents/CodeResources) exactly as Finder's "Compress" would.
447
454
  const zip = join(out, `${appName}.zip`)
448
455
  rmSync(zip, { force: true })
449
456
  execFileSync("ditto", ["-c", "-k", "--keepParent", app, zip])
450
457
  success(`Zip: ${zip} (${mb(statSync(zip).size)})`)
451
- } else if (!flagBool(args, "dmg")) {
458
+ } else if (!opts.dmg) {
452
459
  note("Tip: --zip packs the .app for sending; --dmg wraps it in a drag-to-Applications image.")
453
460
  }
454
461
  } finally {
@@ -1,8 +1,9 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
2
2
  import { dirname, join, resolve } from "node:path"
3
3
  import type { JimpInstance } from "jimp"
4
- import { CliError, c, log, warn } from "../util"
5
- import type { AppConfig } from "./appShared"
4
+ import { Color } from "lecodes-sdk/color"
5
+ import { CliError, c, log, warn } from "../../cli"
6
+ import type { AppConfig } from "./shared"
6
7
 
7
8
  /* App icons rendered from the one master image app.json "icon" names (a square ≥1024px
8
9
  * PNG/JPEG, project-root-relative). Strictly additive: without the key, sync never touches an
@@ -36,12 +37,14 @@ export const loadIconMaster = async (root: string, config: AppConfig): Promise<I
36
37
  const path = resolve(root, rel)
37
38
  if (!existsSync(path)) throw new CliError(`app.json "icon": ${rel} not found`)
38
39
 
39
- const raw = (config.iconBackground ?? "#FFFFFF").trim()
40
- const expanded = /^#[0-9a-fA-F]{3}$/.test(raw) ? "#" + [...raw.slice(1)].map(ch => ch + ch).join("") : raw
41
- const m = /^#([0-9a-fA-F]{6})$/.exec(expanded)
42
- if (!m) throw new CliError(`app.json "iconBackground" must be #RGB or #RRGGBB (got "${config.iconBackground}")`)
43
- const backgroundHex = "#" + m[1].toUpperCase()
44
- const background = (parseInt(m[1], 16) * 0x100 + 0xff) >>> 0
40
+ // Any CSS color, opaque (an icon backdrop has no alpha): the SDK's one parser.
41
+ const rgba = Color.tryParse(config.iconBackground ?? "#FFFFFF")
42
+ if (!rgba || (Color.toPackedRgba(rgba) & 0xff) !== 0xff) {
43
+ throw new CliError(`app.json "iconBackground" must be an opaque CSS color — #RRGGBB, a name, rgb() / hsl() (got ${JSON.stringify(config.iconBackground)})`)
44
+ }
45
+ const rgb = Color.toPackedRgb(rgba)
46
+ const backgroundHex = "#" + rgb.toString(16).padStart(6, "0").toUpperCase()
47
+ const background = (rgb * 0x100 + 0xff) >>> 0
45
48
 
46
49
  const { Jimp } = await import("jimp")
47
50
  let image: JimpInstance