lecodes-cli 2.4.8 → 2.4.10
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/README.md +2 -3
- package/dist/index.js +1072 -832
- package/package.json +10 -10
- package/runtime/materials/lightmap.mat +4 -0
- package/runtime/materials/lit.mat +27 -0
- package/runtime/sdk-types.json +1 -1
- package/runtime/web/VERSION.json +3 -3
- package/runtime/web/assets/materials-CJzxzdUK.bin +0 -0
- package/runtime/web/assets/materials-world-BNQb0Z1l.bin +0 -0
- package/runtime/web/player.js +3 -3
- package/runtime/web-lite/assets/{index-Bd7pdF7u.js → index-e-m97P8Y.js} +1 -1
- package/runtime/web-lite/assets/{scene3d-DXpVYhGy.js → scene3d-NxtMMpHd.js} +23 -16
- package/runtime/web-lite/features.json +2 -2
- package/runtime/web-lite/lite.js +2 -2
- package/src/commands/compile.ts +13 -4
- package/src/commands/init/templates.ts +3 -2
- package/src/commands/lightmap.ts +101 -83
- package/src/compile/projectCompile.ts +23 -5
- package/src/compile/sceneCompile.ts +3 -2
- package/src/hosts/peerInstall.ts +19 -3
- package/runtime/web/assets/materials-BzTsWTWO.bin +0 -0
- package/runtime/web/assets/materials-world-aKwAWYRd.bin +0 -0
package/src/commands/lightmap.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { closeSync, existsSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, rmSync,
|
|
1
|
+
import { closeSync, existsSync, mkdirSync, mkdtempSync, openSync, readdirSync, readFileSync, readSync, rmSync, writeFileSync } from "node:fs"
|
|
2
2
|
import { tmpdir } from "node:os"
|
|
3
|
-
import { basename, dirname, join, relative, resolve } from "node:path"
|
|
3
|
+
import { basename, dirname, join, relative, resolve, sep } from "node:path"
|
|
4
4
|
import { deflateSync } from "node:zlib"
|
|
5
|
-
import { buildBundle, type BundleResource } from "./compile"
|
|
5
|
+
import { buildBundle, type BundleBakeTarget, type BundleResource } from "./compile"
|
|
6
6
|
import { readAppConfigAt } from "./app/shared"
|
|
7
7
|
import { runDesktopRender } from "../hosts/desktopRenderer"
|
|
8
8
|
import { loadPeer } from "../hosts/peerInstall"
|
|
@@ -17,8 +17,11 @@ import { bundleFlags, bundleOptions, localProject } from "./shared"
|
|
|
17
17
|
* scene is complete — the SDK calls `_creator.lightmapBake` instead of loading: the host describes
|
|
18
18
|
* every registered static to creator-bake, which traces the sun, the sky and the lamps on the
|
|
19
19
|
* GPU (--tracer: OptiX on NVIDIA, else Vulkan ray query on any card with hardware ray tracing) and
|
|
20
|
-
* writes
|
|
21
|
-
*
|
|
20
|
+
* writes the files itself (no toktx, no PNG intermediates). "[lightmap] done" is the success marker.
|
|
21
|
+
* THE TARGET is the level's own `bake('<path>.bake')` (the SDK's bakeMacro): the compile reports every
|
|
22
|
+
* bake() the entry reaches, the files land next to that `.bake` — the manifest that names them, which the
|
|
23
|
+
* next compile reads. Nothing has to exist before the first bake; `--out` writes elsewhere, `--name`
|
|
24
|
+
* picks the level when the entry reaches several.
|
|
22
25
|
* stats the per-instance numbers a .bake carries (mean lux, sun / sky visibility, texels, rect)
|
|
23
26
|
* diff the noise of a bake per instance: two bakes with different --seed compared texel by texel
|
|
24
27
|
* inspect PNGs from the float layers a `bake --debug-dir` kept: whole pages or one instance's rect
|
|
@@ -75,38 +78,44 @@ const cubePatternsOf = (argv: string[]): string[] => {
|
|
|
75
78
|
return out.filter((p) => p !== "")
|
|
76
79
|
}
|
|
77
80
|
|
|
78
|
-
/**
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
b.writeBigUInt64LE(BigInt(dfdOffset + dfdSize), o); b.writeBigUInt64LE(BigInt(raw.length), o + 8); b.writeBigUInt64LE(BigInt(raw.length), o + 16); o += 24
|
|
94
|
-
b.writeUInt32LE(dfdSize, o)
|
|
95
|
-
raw.copy(b, dfdOffset + dfdSize)
|
|
96
|
-
return b
|
|
81
|
+
/** The stem of a `.bake` path: `assets/lightmap/level.bake` → `level`. */
|
|
82
|
+
const stemOf = (path: string): string => basename(path).replace(/\.bake$/i, "")
|
|
83
|
+
|
|
84
|
+
/** The bake target among the bake() calls the bundle reaches: the only one, or the one `--name` names. */
|
|
85
|
+
const pickTarget = (bakes: BundleBakeTarget[], name: string | undefined, entry: string): BundleBakeTarget => {
|
|
86
|
+
if (bakes.length === 0) throw new CliError(`${entry} reaches no bake(): the level's scene file says where its bake goes — add \`env: { lightmap: bake('./assets/lightmap/level.bake') }\` (a hand-built scene: \`Lightmap.load(scene, bake('./assets/lightmap/level.bake'))\`).`)
|
|
87
|
+
const listed = bakes.map((b) => `${stemOf(b.path)} (${b.path})`).join(", ")
|
|
88
|
+
if (name === undefined) {
|
|
89
|
+
if (bakes.length > 1) throw new CliError(`${entry} reaches ${bakes.length} bake() calls: ${listed} — pass --name <stem> to pick the level to bake.`)
|
|
90
|
+
return bakes[0]
|
|
91
|
+
}
|
|
92
|
+
const named = bakes.filter((b) => stemOf(b.path) === name)
|
|
93
|
+
if (named.length === 0) throw new CliError(`--name ${name}: ${entry} reaches no bake() of that stem — it reaches ${listed}.`)
|
|
94
|
+
if (named.length > 1) throw new CliError(`--name ${name}: two bake() targets share that stem (${named.map((b) => b.path).join(", ")}) — rename one.`)
|
|
95
|
+
return named[0]
|
|
97
96
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
97
|
+
|
|
98
|
+
/** Every `.bake` under the project (node_modules and the `.lightmap` debug dirs aside), project-relative. */
|
|
99
|
+
const findBakes = (root: string): string[] => {
|
|
100
|
+
const SKIP = new Set([ "node_modules", ".lightmap", ".git", ".lecodes" ])
|
|
101
|
+
const out: string[] = []
|
|
102
|
+
const walk = (dir: string): void => {
|
|
103
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
104
|
+
if (e.isDirectory()) { if (!SKIP.has(e.name)) walk(join(dir, e.name)) }
|
|
105
|
+
else if (/\.bake$/i.test(e.name)) out.push(relative(root, join(dir, e.name)).replace(/\\/g, "/"))
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
walk(root)
|
|
109
|
+
return out.sort()
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** `--bake` of stats / diff / inspect: the flag, else the project's one `.bake`. */
|
|
113
|
+
const bakeFileOf = (root: string, flag: string | undefined): string => {
|
|
114
|
+
if (flag) return resolve(root, flag)
|
|
115
|
+
const found = findBakes(root)
|
|
116
|
+
if (found.length === 0) throw new CliError("No .bake in the project — run `lecodes lightmap bake` first.")
|
|
117
|
+
if (found.length > 1) throw new CliError(`The project has ${found.length} bakes (${found.join(", ")}) — pass --bake <file>.`)
|
|
118
|
+
return resolve(root, found[0])
|
|
110
119
|
}
|
|
111
120
|
|
|
112
121
|
// ---- the unwrap pre-step --------------------------------------------------------------------------
|
|
@@ -163,6 +172,9 @@ const unwrapReferenced = async (js: string, resources: BundleResource[], root: s
|
|
|
163
172
|
if (raw.length === 0) { note(`Lightmap uvs: the ${glbs.length} GLB(s) the app references carry them`); return }
|
|
164
173
|
note(`Unwrapping ${raw.length} of the ${glbs.length} GLB(s) the app references — no lightmap uvs; written in place (--no-unwrap skips)…`)
|
|
165
174
|
const assets = await loadPeer<typeof import("lecodes-assets/cli")>("lecodes-assets", "cli", { for: "lecodes lightmap bake (lightmap uvs)" })
|
|
175
|
+
if (typeof assets.unwrapGlbFiles !== "function") {
|
|
176
|
+
throw new CliError("The lecodes-assets this CLI loaded has no unwrapGlbFiles (it predates the bake's unwrap) — `lecodes update` moves it forward; --no-unwrap bakes the GLBs as they are.")
|
|
177
|
+
}
|
|
166
178
|
const t0 = performance.now()
|
|
167
179
|
const rel = (f: string): string => relative(root, f).replace(/\\/g, "/")
|
|
168
180
|
const reports = await assets.unwrapGlbFiles(raw, {
|
|
@@ -179,23 +191,23 @@ const unwrapReferenced = async (js: string, resources: BundleResource[], root: s
|
|
|
179
191
|
const bake = defineCommand({
|
|
180
192
|
name: "bake",
|
|
181
193
|
summary: "Bake the sun, the sky and the lamps into a lux atlas for the level's statics",
|
|
182
|
-
description: "Runs the app in bake mode in lecodes-bake — the windowless host with the bake built in (no physics, 2D, audio, net or navmesh), downloaded on first use like the desktop host; LECODES_BAKE_EXE points at a local build (hosts/desktop build.ps1 -Bake). The statics are what the app registers with Lightmap.add (a scene file does it for every node no Physics aspect moves), the bake fires at its Lightmap.load call.
|
|
194
|
+
description: "Runs the app in bake mode in lecodes-bake — the windowless host with the bake built in (no physics, 2D, audio, net or navmesh), downloaded on first use like the desktop host; LECODES_BAKE_EXE points at a local build (hosts/desktop build.ps1 -Bake). The statics are what the app registers with Lightmap.add (a scene file does it for every node no Physics aspect moves), the bake fires at its Lightmap.load call. Needs a GPU with hardware ray tracing (--tracer).\n\nWhere: the level's own bake('<dir>/<stem>.bake') — env: { lightmap: bake('./assets/lightmap/level.bake') } in a scene file, the path relative to that file — names the target; nothing has to exist before the first bake. Writes into <dir>: <stem>.bake (the manifest: it names every file below, and the next compile reads it — nothing to paste into the scene) + <stem>-light[_n].ktx2 (irradiance, BC6H, a page per _n) + <stem>-aux[_n].ktx2 (sky visibility, the directional share and direction of the light) + <stem>-probes.ktx2 (the reflection probes: a cubemap of the level's radiance per probe, prefiltered per roughness; --probe-spacing 0 = none) + <stem>.lgrid (the light grid the movers read; --volume 0 = none) + <stem>-uv1.bin (the packed lightmap uv1 the rects map) + with --split <stem>-direct[_n].ktx2 / <stem>-indirect[_n].ktx2.\n\nLightmap UVs: the GLBs the app references that have none (TEXCOORD_1) are unwrapped first, IN PLACE \u2014 the same as lecodes assets unwrap, run on just those files; a file that has them is left alone, --no-unwrap skips the step. A whole level in one GLB that wants --groups / --zone is unwrapped by hand first (lecodes assets unwrap --help).",
|
|
183
195
|
flags: {
|
|
184
|
-
out: str("
|
|
185
|
-
name: str("
|
|
196
|
+
out: str("write the files into this folder instead of next to the level's .bake (a second seed for `lightmap diff`)", { value: "<dir>" }),
|
|
197
|
+
name: str("bake the level whose bake() names <stem>.bake — when the entry reaches several", { value: "<stem>" }),
|
|
186
198
|
...bundleFlags,
|
|
187
|
-
entry: str("the file that
|
|
199
|
+
entry: str("the file that opens the level (it reaches the bake() that names the target)", { value: "<file>", default: "main.ts" }),
|
|
188
200
|
size: num("atlas page edge (texels)", { default: 4096 }),
|
|
189
201
|
texel: num("texel size (m); the bake coarsens only when the pages are full", { default: 0.05 }),
|
|
190
202
|
pages: num("atlas pages to fill before the bake coarsens the texel", { default: 4 }),
|
|
191
203
|
"sun-rays": num("shadow rays into the sun's disc per texel", { default: 64 }),
|
|
192
|
-
"sky-rays": num("hemisphere rays per texel (the sky's irradiance and visibility). 2048 (2026-09-16): shadow rays are cheap (
|
|
193
|
-
"light-rays": num("shadow rays per lamp per texel (64: at 32
|
|
194
|
-
"bounce-rays": num("bounce paths per texel; 0 = direct light only. 128 (2026-09-16): a dark room's floor
|
|
204
|
+
"sky-rays": num("hemisphere rays per texel (the sky's irradiance and visibility). 2048 (2026-09-16): shadow rays are cheap (an interior level's direct pass: 2 s a page) and a texel deep in a corridor sees the sky through a far window with 1 ray in 256 — every hit was 410 lux", { default: 2048 }),
|
|
205
|
+
"light-rays": num("shadow rays per lamp per texel (64: at 32 an interior level's lamp layer read 43 % noise between two seeds, the ceilings and doors next to a lamp 48 %)", { default: 64 }),
|
|
206
|
+
"bounce-rays": num("bounce paths per texel; 0 = direct light only. 128 (2026-09-16): a dark room's floor of an interior level reads 240 % noise at 64, 160 % at 128, 130 % at 256 — the last doubling costs 2x the bake time for little", { default: 128 }),
|
|
195
207
|
bounces: num("path vertices at most (Russian roulette from the third); 0 = direct light only", { default: 3 }),
|
|
196
208
|
"patch-samples": num("next-event samples per texel over the BRIGHT PATCHES of a first direct pass (sunlit floors, the ceiling around a lamp) — what keeps the bounce in dark rooms from arriving as impulses (every path vertex takes one more); 0 = the cosine paths alone", { default: 64 }),
|
|
197
|
-
split: bool("also write <
|
|
198
|
-
denoise: str("the AI denoiser (--denoiser) over: indirect = the bounce alone (
|
|
209
|
+
split: bool("also write <stem>-direct[_n].ktx2 / <stem>-indirect[_n].ktx2 — the pages the \"direct\" / \"indirect\" views bind (the manifest names them)"),
|
|
210
|
+
denoise: str("the AI denoiser (--denoiser) over: indirect = the bounce alone (an interior level, 2026-09-16: bounce noise 57 % -> 23 % of the mean, per-instance means unchanged), no-sun = + the sky and the lamps (the sky through a small opening: 0..3 hits of 256 rays, a texel is one of four values), all = + the sun (its shadow edges too: a sunlit pillar's mean moved 6 %), none. An interior level, two seeds: no-sun took the sky 1.8 % -> 0.5 %, the lamps 43 % -> 21 %", { value: "<none|indirect|no-sun|all>", default: "no-sun" }),
|
|
199
211
|
tracer: str("which GPU API traces: auto = OptiX where an NVIDIA driver with OptiX opens, else Vulkan ray query — any card with hardware ray tracing (RTX, Radeon RX 6000+, Arc) with its driver alone; optix / vulkan by name (the bake fails when it cannot open)", { value: "<auto|optix|vulkan>", default: "auto" }),
|
|
200
212
|
denoiser: str("which AI denoiser --denoise runs: auto = OptiX's on an NVIDIA card, else Intel Open Image Denoise (its CUDA / HIP / CPU device, the DLLs next to the host); optix / oidn by name. Independent of --tracer; one that cannot open is a warning and the pages stay as traced", { value: "<auto|optix|oidn>", default: "auto" }),
|
|
201
213
|
"light-radius": num("lamps: the emitter's radius (m) — soft shadows, no 1/d² singularity", { default: 0.05 }),
|
|
@@ -209,13 +221,13 @@ const bake = defineCommand({
|
|
|
209
221
|
"probe-indoor": bool("keep only the probes under a roof — those whose open share of the sphere (rays from the point that meet nothing) is at most 15 %, holes in the geometry or not: outdoor statics reflect the sky through their baked sky visibility instead — an environment map without the ground, but no outdoor probe can reach indoors and the atlas shrinks"),
|
|
210
222
|
"probe-layout": str("rooms = every indoor point of the grid slides to the middle of its room and the points of one room merge: one probe per room, far from the walls, its box the room (stations every ~8 m along a room over 12 m), outdoor points stay on the grid; grid = everything stays on the grid. Either way the pages carry which probes each block of texels sees (rays at bake time)", { value: "<rooms|grid>", default: "rooms" }),
|
|
211
223
|
"min-texels": num("instances smaller than this many ATLAS TEXELS along every axis (x --texel: 12 at 5 cm = 0.6 m) get no place in the atlas: they still cast their baked shadow and bounce, their own light is baked where they stand (six sides, no atlas texels) — cartridges, boxes, pipe elbows, a fire extinguisher: a baked shadow does not read on them and their tight charts are where the seams are. 0 = everything bakes; --dry-run lists what falls under it", { default: 12 }),
|
|
212
|
-
"thin-texels": num("ROUND AND NARROW objects leave the atlas like --min-texels: an instance none of whose lightmap charts is wider than this many texels across its short side (at --texel) — a railing's tubes, a cable, a pipe run, blinds, a gun barrel. A strip of 1-3 texels around a tube cannot hold the light's change around it and its uv seam shows as a step (
|
|
213
|
-
cube: str("BAKE AS CUBES BY NAME: a regular expression (ECMAScript, case-sensitive) searched in every record's key — <node path>@<ordinal>, one record per mesh of a model, so SM_pipe takes every pipe node and @1$ the second mesh of each. A record that matches leaves the atlas like a --min-texels / --thin-texels one, whatever its size or charts: an occluder whose own light is baked where it stands as ambient cubes (in slabs along a long one, --object-slab) — a pipe run, a fence, a prop whose lightmap seams show. Repeat the flag (--cube SM_pipe --cube SM_fence) or join with | in one (--cube \"SM_pipe|SM_fence\", quoted for the shell); a pattern that starts with - goes as --cube=<regex>. The
|
|
224
|
+
"thin-texels": num("ROUND AND NARROW objects leave the atlas like --min-texels: an instance none of whose lightmap charts is wider than this many texels across its short side (at --texel) — a railing's tubes, a cable, a pipe run, blinds, a gun barrel. A strip of 1-3 texels around a tube cannot hold the light's change around it and its uv seam shows as a step (a railing: two texels 5 mm apart on the seam, one lit, one shadowed); the ambient cube baked where it stands is smooth. A flat object has a wide chart somewhere and stays. 0 = off; --dry-run lists what falls under it", { default: 4 }),
|
|
225
|
+
cube: str("BAKE AS CUBES BY NAME: a regular expression (ECMAScript, case-sensitive) searched in every record's key — <node path>@<ordinal>, one record per mesh of a model, so SM_pipe takes every pipe node and @1$ the second mesh of each. A record that matches leaves the atlas like a --min-texels / --thin-texels one, whatever its size or charts: an occluder whose own light is baked where it stands as ambient cubes (in slabs along a long one, --object-slab) — a pipe run, a fence, a prop whose lightmap seams show. Repeat the flag (--cube SM_pipe --cube SM_fence) or join with | in one (--cube \"SM_pipe|SM_fence\", quoted for the shell); a pattern that starts with - goes as --cube=<regex>. The level's bake() option cube adds its own patterns, a node's lightmap: 'cube' marks one node by hand; --dry-run lists what matches", { value: "<regex>" }),
|
|
214
226
|
"object-slab": num("A LONG object the atlas left out (--min-texels, --thin-texels, --cube) takes its light in SLABS of this many metres along the longest axis of its bounds — round(length / this) ambient cubes, 16 at most — a 6 m pipe run, a railing along a wall: one end under a lamp, the other in the dark, where one cube gave all of it the mean. The shader blends the two slabs around each pixel along that axis. 0 = one cube an object", { value: "<m>", default: 1 }),
|
|
215
227
|
"emissive-nits": num("EMISSIVE SURFACES LIGHT THE LEVEL: cd/m² of emission 1.0 (emissive factor x strength x map) — every lit region of an emissive surface (a panel drawn in a ceiling's texture, a screen, a sign) bakes as a rectangle lamp. Default: what the level's camera makes of 1.0, 1.2 x 2^EV100, so a panel lights as brightly as it looks. 0 = emissive surfaces light nothing"),
|
|
216
228
|
"detail-texels": num("A SMALL SURFACE GETS A FINER TEXEL: an instance whose surface is under this many atlas texels (area / texel²) is laid out denser by sqrt(this / its texels), 3x at most — a pipe of 1 m is ~300 texels at 5 cm, its charts a few texels wide and every chart edge a step; at 2000 it bakes at ~2 cm. 0 = off", { default: 2000 }),
|
|
217
|
-
volume: num("THE LIGHT GRID for movers: the cell size in metres (0.75 of it tall) — per cell in free space the baked ambient light (sky, lamps, every bounce; not the direct sun, which stays real-time on movers), links between cells a ray connects so nothing blends through a wall; written as <
|
|
218
|
-
"probe-box": str("only grid cells whose centre lies inside this world box get a probe: x0,y0,z0,x1,y1,z1 in metres — keeps probes off a roof top the player never sees (
|
|
229
|
+
volume: num("THE LIGHT GRID for movers: the cell size in metres (0.75 of it tall) — per cell in free space the baked ambient light (sky, lamps, every bounce; not the direct sun, which stays real-time on movers), links between cells a ray connects so nothing blends through a wall; written as <stem>.lgrid. 0 = none", { default: 1 }),
|
|
230
|
+
"probe-box": str("only grid cells whose centre lies inside this world box get a probe: x0,y0,z0,x1,y1,z1 in metres — keeps probes off a roof top the player never sees (79 probes above a ceiling with holes where the fixtures were bled the sky onto the walls of a level). Default: the receivers' bounds", { value: "<x0,y0,z0,x1,y1,z1>" }),
|
|
219
231
|
"debug-dir": str("keep the float layers per page (PFM) here for `lightmap inspect` — gigabytes per level; a `.lightmap/` dir inside the project is skipped by the compile scan, anywhere else must be outside the project", { value: "<dir>" }),
|
|
220
232
|
"dry-run": bool("only the atlas LAYOUT: pages, texel size, the largest rects — no tracing, nothing written (seconds, not minutes; the host still loads the level)"),
|
|
221
233
|
frames: num("host frames to run before giving up — load headroom", { default: 900 }),
|
|
@@ -226,13 +238,15 @@ const bake = defineCommand({
|
|
|
226
238
|
logs: bool("forward the host's output"),
|
|
227
239
|
"no-unwrap": bool("leave the referenced GLBs as they are: no lightmap unwrap of those without TEXCOORD_1 (they bake without lightmap uvs)"),
|
|
228
240
|
},
|
|
229
|
-
examples: [
|
|
241
|
+
examples: [
|
|
242
|
+
"lecodes lightmap bake",
|
|
243
|
+
"lecodes lightmap bake --dry-run --logs",
|
|
244
|
+
"lecodes lightmap bake --entry level2.ts --texel 0.04 --debug-dir .lightmap/level2 --logs",
|
|
245
|
+
],
|
|
230
246
|
run: async ({ flags, argv }) => {
|
|
231
247
|
const { root } = localProject()
|
|
232
|
-
const outDir = resolve(root, flags.out)
|
|
233
|
-
const stem = flags.name
|
|
234
248
|
const cube = cubePatternsOf(argv)
|
|
235
|
-
if (!/^[A-Za-z0-9_
|
|
249
|
+
if (flags.name !== undefined && !/^[A-Za-z0-9_.-]+$/.test(flags.name)) throw new CliError(`--name wants the stem of a .bake (its file name without .bake), got "${flags.name}".`)
|
|
236
250
|
const entryFile = flags.entry
|
|
237
251
|
if (!existsSync(resolve(root, entryFile))) {
|
|
238
252
|
throw new CliError(`No ${entryFile} in ${root} — pass --entry <file> (the file that builds the scene and calls Lightmap.load).`)
|
|
@@ -241,9 +255,12 @@ const bake = defineCommand({
|
|
|
241
255
|
if (!["none", "indirect", "no-sun", "all"].includes(flags.denoise)) throw new CliError(`--denoise wants none, indirect, no-sun or all, got "${flags.denoise}".`)
|
|
242
256
|
if (!["auto", "optix", "vulkan"].includes(flags.tracer)) throw new CliError(`--tracer wants auto, optix or vulkan, got "${flags.tracer}".`)
|
|
243
257
|
if (!["auto", "optix", "oidn"].includes(flags.denoiser)) throw new CliError(`--denoiser wants auto, optix or oidn, got "${flags.denoiser}".`)
|
|
258
|
+
// where the files go is the level's bake() (the SDK resolves `<root>/<its path>` at the bake call); `--out` overrides
|
|
259
|
+
// the folder, `--name` picks the level
|
|
244
260
|
const cfg = {
|
|
245
|
-
|
|
246
|
-
|
|
261
|
+
root: root.replace(/\\/g, "/"),
|
|
262
|
+
...(flags.out ? { outDir: resolve(root, flags.out).replace(/\\/g, "/") } : {}),
|
|
263
|
+
...(flags.name !== undefined ? { stem: flags.name } : {}),
|
|
247
264
|
size: range("size", flags.size, 256, 16384),
|
|
248
265
|
texel: range("texel", flags.texel, 0.001, 2),
|
|
249
266
|
pages: range("pages", flags.pages, 1, 16),
|
|
@@ -288,25 +305,26 @@ const bake = defineCommand({
|
|
|
288
305
|
const entryPath = "/.lightmap-entry.ts"
|
|
289
306
|
const entryText = `globalThis.__lecodesLightmap = ${JSON.stringify(cfg)}\nimport ${JSON.stringify(importPath)}\n`
|
|
290
307
|
|
|
291
|
-
// The bake files must exist BEFORE the compile: the scene references them through asset(), and a
|
|
292
|
-
// missing asset is a broken bundle. The first bake starts from placeholders the real files overwrite.
|
|
293
|
-
mkdirSync(outDir, { recursive: true })
|
|
294
|
-
ensurePlaceholders(outDir, stem)
|
|
295
|
-
if (debugDir) mkdirSync(debugDir, { recursive: true })
|
|
296
|
-
|
|
297
308
|
const desktopRenderer = "gl" as const // lecodes-bake is the GL tree on Filament's NOOP backend: `_creator.backend` "desktop" fetches the GL shaders
|
|
298
|
-
const { js, resources } = await buildBundle(bundleOptions(flags), {
|
|
309
|
+
const { js, resources, bakes } = await buildBundle(bundleOptions(flags), {
|
|
299
310
|
localAssets: true, desktopRenderer,
|
|
300
311
|
extraEntries: [{ path: entryPath, type: "text", text: entryText }],
|
|
301
312
|
entryOverride: entryPath,
|
|
302
313
|
})
|
|
314
|
+
// the target: the bake() the entry reaches (the compile reported it; before the first bake it compiled unbaked)
|
|
315
|
+
const target = pickTarget(bakes, flags.name, entryFile)
|
|
316
|
+
const stem = stemOf(target.path)
|
|
317
|
+
const outDir = flags.out ? resolve(root, flags.out) : dirname(target.absPath)
|
|
318
|
+
mkdirSync(outDir, { recursive: true })
|
|
319
|
+
if (debugDir) mkdirSync(debugDir, { recursive: true })
|
|
320
|
+
note(`Target: ${relative(root, join(outDir, `${stem}.bake`)).replace(/\\/g, "/")}${target.exists ? "" : " (the first bake: nothing there yet)"}${flags.out ? " (--out)" : ""}`)
|
|
303
321
|
if (!flags["no-unwrap"]) await unwrapReferenced(js, resources, root, cfg.texel)
|
|
304
322
|
|
|
305
323
|
// The host writes the bake itself; the PNG the render driver insists on goes to a scratch dir.
|
|
306
324
|
const scratch = mkdtempSync(join(tmpdir(), "lecodes-lightmap-"))
|
|
307
325
|
const outPng = join(scratch, "frame.png")
|
|
308
326
|
const bounce = cfg.bounceRays > 0 && cfg.bounces > 0 ? `, ${cfg.bounceRays} bounce paths of ${cfg.bounces}${cfg.patchSamples > 0 ? ` + ${cfg.patchSamples} patch samples` : ""}${cfg.denoise !== "none" ? `, denoised (${cfg.denoise})` : ", raw"}` : ", no bounce"
|
|
309
|
-
note(`Baking up to ${cfg.pages} × ${cfg.size}² at ${(cfg.texel * 100).toFixed(1)} cm texels (${cfg.sunRays} sun + ${cfg.skyRays} sky + ${cfg.lightRays} lamp rays${bounce}) → ${relative(root, outDir) || "."}/${stem}.*…`)
|
|
327
|
+
note(`Baking up to ${cfg.pages} × ${cfg.size}² at ${(cfg.texel * 100).toFixed(1)} cm texels (${cfg.sunRays} sun + ${cfg.skyRays} sky + ${cfg.lightRays} lamp rays${bounce}) → ${relative(root, outDir).split(sep).join("/") || "."}/${stem}.*…`)
|
|
310
328
|
let output = ""
|
|
311
329
|
if (flags["dry-run"]) process.env.CBK_LAYOUT_ONLY = "1" // the host inherits it: creator-bake stops after the layout
|
|
312
330
|
try {
|
|
@@ -337,17 +355,14 @@ const bake = defineCommand({
|
|
|
337
355
|
}
|
|
338
356
|
if (/\[lightmap\] done/.test(output)) {
|
|
339
357
|
const data = readBake(join(outDir, `${stem}.bake`))
|
|
340
|
-
const rel = (f: string): string => relative(root, join(outDir, f)).replace(/\\/g, "/")
|
|
341
|
-
const assets = (files: string[]): string => files.length === 1 ? `asset('<rel>/${rel(files[0])}')` : `[${files.map((f) => `asset('<rel>/${rel(f)}')`).join(", ")}]`
|
|
342
358
|
const splitFiles = data.direct && data.indirect ? ` + ${data.direct.join(", ")} + ${data.indirect.join(", ")}` : ""
|
|
343
|
-
const splitDef = data.direct && data.indirect ? `, direct: ${assets(data.direct)}, indirect: ${assets(data.indirect)}` : ""
|
|
344
359
|
const probeFiles = data.probes ? ` + ${data.probes.file} (${data.probes.count} probe(s) of ${data.probes.size}² at ${data.probes.spacing} m)` : ""
|
|
345
360
|
const gridFile = data.volume ? ` + ${data.volume.file} (light grid ${data.volume.n.join(" x ")}, ${data.volume.valid} cell(s) in free space)` : ""
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
361
|
+
const uv1File = data.uv1 ? ` + ${data.uv1} (the packed lightmap uv1)` : ""
|
|
362
|
+
logErr(c.green(`✓ baked → ${relative(root, outDir).replace(/\\/g, "/") || "."}/${stem}.bake + ${data.light.join(", ")} + ${data.aux.join(", ")}${splitFiles}${probeFiles}${gridFile}${uv1File}`))
|
|
363
|
+
logErr(c.dim(flags.out
|
|
364
|
+
? ` written to --out: the level's bake() still reads ${target.path}`
|
|
365
|
+
: ` the manifest names them: the next compile of ${entryFile} stages them (bake('…') in the scene reads it)`))
|
|
351
366
|
if (debugDir) logErr(c.dim(` debug layers: ${relative(root, debugDir)} (lecodes lightmap inspect --bake ${relative(root, join(outDir, `${stem}.bake`)).replace(/\\/g, "/")} --debug-dir ${relative(root, debugDir).replace(/\\/g, "/")})`))
|
|
352
367
|
return
|
|
353
368
|
}
|
|
@@ -369,15 +384,16 @@ const stats = defineCommand({
|
|
|
369
384
|
summary: "The per-instance numbers a .bake carries: mean lux, sun / sky visibility, texels, rect",
|
|
370
385
|
description: "Reads the .bake alone (no debug layers needed). One row per receiver, sorted by mean lux unless --sort says otherwise; --key filters by a regular expression over the keys.",
|
|
371
386
|
flags: {
|
|
372
|
-
bake: str("the .bake file", { value: "<file>"
|
|
387
|
+
bake: str("the .bake file (default: the project's one .bake)", { value: "<file>" }),
|
|
373
388
|
key: str("only instances whose key matches this regular expression", { value: "<regex>" }),
|
|
374
389
|
sort: str("row order", { value: "<lux|key|texels|sun|sky>", default: "lux" }),
|
|
375
390
|
limit: num("rows to print (0 = all)", { default: 60 }),
|
|
376
391
|
},
|
|
377
|
-
examples: [ "lecodes lightmap stats --
|
|
392
|
+
examples: [ "lecodes lightmap stats --key Wall", "lecodes lightmap stats --bake assets/level2/lightmap/level2.bake --sort sky --limit 0" ],
|
|
378
393
|
run: ({ flags }) => {
|
|
379
394
|
const { root } = localProject()
|
|
380
|
-
const
|
|
395
|
+
const bakeFile = bakeFileOf(root, flags.bake)
|
|
396
|
+
const data = readBake(bakeFile)
|
|
381
397
|
const re = flags.key ? new RegExp(flags.key) : null
|
|
382
398
|
let rows = data.instances.filter((i) => i.receiver && (!re || re.test(i.key)))
|
|
383
399
|
const sorters: Record<string, (a: BakeInstance, b: BakeInstance) => number> = {
|
|
@@ -392,7 +408,7 @@ const stats = defineCommand({
|
|
|
392
408
|
rows = rows.sort(sorter)
|
|
393
409
|
const total = rows.length
|
|
394
410
|
if (flags.limit > 0) rows = rows.slice(0, flags.limit)
|
|
395
|
-
log(`${basename(
|
|
411
|
+
log(`${basename(bakeFile)}: ${data.pages} page(s) of ${data.size}, ${(data.texel * 100).toFixed(2)} cm texels, ${data.texels ?? "?"} texels, lightScale ${data.lightScale}`)
|
|
396
412
|
log(`${"key".padEnd(48)} ${"page".padStart(4)} ${"rect".padStart(20)} ${"texels".padStart(8)} ${"lux".padStart(9)} ${"sun".padStart(6)} ${"sky".padStart(6)} ${"dir".padStart(6)}`)
|
|
397
413
|
for (const r of rows) {
|
|
398
414
|
const rect = r.rect ? `${r.rect[0]},${r.rect[1]} ${r.rect[2]}x${r.rect[3]}` : ""
|
|
@@ -412,7 +428,7 @@ const diff = defineCommand({
|
|
|
412
428
|
summary: "The noise of a bake, per instance: two bakes that differ only by --seed, compared texel by texel",
|
|
413
429
|
description: "Bake the level twice with different --seed (and --out / --debug-dir, so the second does not overwrite the first — a `.lightmap/` dir inside the project is skipped by the compile scan). Both must share the layout (same instances, rects and pages — a seed changes nothing else). Per instance: the mean of the layer, the noise as the RMS of (a − b) / √2 over the mean, in %, and the share of texels that differ by more than half their mean. Sorted by noise unless --sort says otherwise.",
|
|
414
430
|
flags: {
|
|
415
|
-
bake: str("the first .bake", { value: "<file>"
|
|
431
|
+
bake: str("the first .bake (default: the project's one .bake)", { value: "<file>" }),
|
|
416
432
|
"debug-dir": str("where the first bake kept its PFM layers", { value: "<dir>", default: ".lightmap" }),
|
|
417
433
|
bake2: str("the second .bake (another --seed)", { value: "<file>" }),
|
|
418
434
|
"debug-dir2": str("where the second bake kept its PFM layers", { value: "<dir>" }),
|
|
@@ -423,19 +439,21 @@ const diff = defineCommand({
|
|
|
423
439
|
limit: num("rows to print (0 = all)", { default: 40 }),
|
|
424
440
|
},
|
|
425
441
|
examples: [
|
|
426
|
-
"lecodes lightmap bake --
|
|
427
|
-
"lecodes lightmap
|
|
442
|
+
"lecodes lightmap bake --debug-dir .lightmap/seed1 --logs",
|
|
443
|
+
"lecodes lightmap bake --seed 2 --out .lightmap/seed2/bake --debug-dir .lightmap/seed2 --logs",
|
|
444
|
+
"lecodes lightmap diff --debug-dir .lightmap/seed1 --bake2 .lightmap/seed2/bake/level.bake --debug-dir2 .lightmap/seed2 --layer bounce",
|
|
428
445
|
],
|
|
429
446
|
run: ({ flags }) => {
|
|
430
447
|
const { root } = localProject()
|
|
431
448
|
if (!flags.bake2 || !flags["debug-dir2"]) throw new CliError("--bake2 and --debug-dir2 name the second bake (another --seed).")
|
|
432
|
-
const
|
|
449
|
+
const bakeFile = bakeFileOf(root, flags.bake)
|
|
450
|
+
const a = readBake(bakeFile), b = readBake(resolve(root, flags.bake2))
|
|
433
451
|
if (a.size !== b.size || a.pages !== b.pages) throw new CliError(`The bakes differ in layout (${a.pages} × ${a.size} vs ${b.pages} × ${b.size}) — a diff needs the same scene baked twice with another --seed.`)
|
|
434
452
|
const ra = a.instances.filter((i) => i.receiver), rb = b.instances.filter((i) => i.receiver)
|
|
435
453
|
if (ra.length !== rb.length || ra.some((i, k) => i.key !== rb[k].key || i.page !== rb[k].page || String(i.rect) !== String(rb[k].rect))) {
|
|
436
454
|
throw new CliError("The bakes list different instances or rects — a diff needs the same scene baked twice with another --seed.")
|
|
437
455
|
}
|
|
438
|
-
const stemA = basename(
|
|
456
|
+
const stemA = basename(bakeFile, ".bake"), stemB = basename(flags.bake2, ".bake")
|
|
439
457
|
const dirA = resolve(root, flags["debug-dir"]), dirB = resolve(root, flags["debug-dir2"])
|
|
440
458
|
const layer = flags.layer
|
|
441
459
|
if (![ "irradiance", "bounce", "sun", "sky", "lamps" ].includes(layer)) throw new CliError(`--layer: expected irradiance, bounce, sun, sky or lamps, got ${JSON.stringify(layer)}`)
|
|
@@ -559,7 +577,7 @@ const inspect = defineCommand({
|
|
|
559
577
|
summary: "PNGs from a bake's debug layers: whole pages, or one instance's rect enlarged",
|
|
560
578
|
description: "Reads the PFM float layers `bake --debug-dir` kept next to the .bake's rects. Without --key: one PNG per page and layer with every rect outlined. With --key: the matching instances' rects (plus a margin) enlarged by --scale, one PNG per instance and layer. Lux layers are shown as log10 over --lux-range (or linear over --white); vectors as x0.5+0.5.",
|
|
561
579
|
flags: {
|
|
562
|
-
bake: str("the .bake file", { value: "<file>"
|
|
580
|
+
bake: str("the .bake file (default: the project's one .bake)", { value: "<file>" }),
|
|
563
581
|
"debug-dir": str("where the bake kept its PFM layers", { value: "<dir>", default: ".lightmap" }),
|
|
564
582
|
out: str("where the PNGs go", { value: "<dir>", default: ".lightmap/png" }),
|
|
565
583
|
key: str("instances whose key matches this regular expression (default: whole pages)", { value: "<regex>" }),
|
|
@@ -570,10 +588,10 @@ const inspect = defineCommand({
|
|
|
570
588
|
white: num("linear mode: the lux shown as white (0 = log mode)", { default: 0 }),
|
|
571
589
|
limit: num("instances to render with --key (0 = all)", { default: 24 }),
|
|
572
590
|
},
|
|
573
|
-
examples: [ "lecodes lightmap inspect --
|
|
591
|
+
examples: [ "lecodes lightmap inspect --debug-dir .lightmap/level", "lecodes lightmap inspect --key 'Wall_05' --layer irradiance,sun,sky,lamps --scale 6" ],
|
|
574
592
|
run: ({ flags }) => {
|
|
575
593
|
const { root } = localProject()
|
|
576
|
-
const bakeFile =
|
|
594
|
+
const bakeFile = bakeFileOf(root, flags.bake)
|
|
577
595
|
const data = readBake(bakeFile)
|
|
578
596
|
const stem = basename(bakeFile).replace(/\.bake$/, "")
|
|
579
597
|
const dbg = resolve(root, flags["debug-dir"])
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync } from "node:fs"
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs"
|
|
2
2
|
import { createRequire } from "node:module"
|
|
3
3
|
import { dirname, join } from "node:path"
|
|
4
4
|
import type { FileKind } from "../project/manifest"
|
|
@@ -6,8 +6,8 @@ import { distRoot } from "../hosts/distRoot"
|
|
|
6
6
|
import { installPeers } from "../hosts/peerInstall"
|
|
7
7
|
import { peerDirInPrefix } from "../hosts/peers"
|
|
8
8
|
import {
|
|
9
|
-
|
|
10
|
-
type CompileServerOptions,
|
|
9
|
+
compileProjectWithMap as sdkCompileProjectWithMap, compileServerBundle, detectEntry, inlineSourceMapComment, isServerPath, scanModulesChisel,
|
|
10
|
+
localAssetNames, ENTRY_CONVENTION, type BundleBake, type CompileServerOptions,
|
|
11
11
|
} from "lecodes-sdk/compile"
|
|
12
12
|
|
|
13
13
|
// Every project compiles through the SDK's one import-free compiler, which bundles with chisel (the
|
|
@@ -106,9 +106,23 @@ export const ambiguousEntryWarning = (entries: { path: string, type?: string, te
|
|
|
106
106
|
return `Ambiguous entrypoint: nothing imports ${roots.join(", ")} — picked ${entry}. Rename your entry to main.ts (or pass --entry).`
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
|
|
110
|
-
|
|
109
|
+
/** A file of the compile by its root path (`/assets/lightmap/level.bake`) — how `bake()` reads the manifest it names:
|
|
110
|
+
* the entries' own files on disk. */
|
|
111
|
+
const resourceReader = (entries: CompileEntry[]) => {
|
|
112
|
+
const byRoot = new Map(entries.map((e) => [ "/" + e.path.replace(/\\/g, "/").replace(/^[./]+/, ""), e ]))
|
|
113
|
+
return (rootPath: string): string | null => {
|
|
114
|
+
const e = byRoot.get(rootPath)
|
|
115
|
+
if (e?.text !== undefined) return e.text
|
|
116
|
+
try { return e?.absPath ? readFileSync(e.absPath, "utf8") : null } catch { return null }
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** The bundle (its map inlined, the shipped / preview form) and the `bake()` targets it reaches — the target of
|
|
121
|
+
* `lecodes lightmap bake`. */
|
|
122
|
+
export const compileProjectBundle = async (opts: CompileOptions): Promise<{ js: string, bakes: BundleBake[] }> => {
|
|
123
|
+
const { code, map, bakes } = await sdkCompileProjectWithMap({
|
|
111
124
|
entries: opts.entries.map((e) => ({ path: e.path, type: e.type as any, text: e.text, fileSrc: e.fileSrc, remote: e.remote, shaders: e.shaders, shadersLocal: e.shadersLocal })),
|
|
125
|
+
readResource: resourceReader(opts.entries),
|
|
112
126
|
name: opts.name,
|
|
113
127
|
publicUrl: opts.publicUrl,
|
|
114
128
|
entryOverride: opts.entryOverride,
|
|
@@ -123,6 +137,10 @@ export const compileProject = (opts: CompileOptions): Promise<string> =>
|
|
|
123
137
|
onWarning: opts.onWarning,
|
|
124
138
|
serverUrl: opts.serverUrl,
|
|
125
139
|
})
|
|
140
|
+
return { js: map ? `${code}\n${inlineSourceMapComment(map)}` : code, bakes }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export const compileProject = async (opts: CompileOptions): Promise<string> => (await compileProjectBundle(opts)).js
|
|
126
144
|
|
|
127
145
|
/** Does the project have a server side (`*.server.ts` files)? */
|
|
128
146
|
export const hasServerFiles = (entries: CompileEntry[]): boolean =>
|
|
@@ -54,8 +54,9 @@ export const compileSceneEditor = async (opts: {
|
|
|
54
54
|
// the same /project/* mount — see the fallback below when matc isn't available
|
|
55
55
|
entries.push({ path, type: "shader", text: readFileSync(file.absPath, "utf8"), absPath: file.absPath })
|
|
56
56
|
} else {
|
|
57
|
-
// Binary assets resolve to URLs the scene server serves from the project folder (/project/*)
|
|
58
|
-
|
|
57
|
+
// Binary assets resolve to URLs the scene server serves from the project folder (/project/*);
|
|
58
|
+
// absPath lets a bake() read its manifest
|
|
59
|
+
entries.push({ path, type: "resource", fileSrc: "/project/" + rel(file.path), absPath: file.absPath })
|
|
59
60
|
}
|
|
60
61
|
}
|
|
61
62
|
|
package/src/hosts/peerInstall.ts
CHANGED
|
@@ -3,7 +3,8 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
|
|
|
3
3
|
import { join } from "node:path"
|
|
4
4
|
import { pathToFileURL } from "node:url"
|
|
5
5
|
import pkg from "../../package.json"
|
|
6
|
-
import { peerDirInPrefix, peersDir, peerVersionGlobal } from "./peers"
|
|
6
|
+
import { peerDirInPrefix, peersDir, peerVersionGlobal, peerVersionPrefix } from "./peers"
|
|
7
|
+
import { semverNewer } from "./releases"
|
|
7
8
|
import { archiveSpec } from "./archives"
|
|
8
9
|
import { commandLine, installGlobalArgv, installIntoArgv, ownManager, pickManager } from "./packageManager"
|
|
9
10
|
import { CliError, c, confirm, log, note } from "../cli"
|
|
@@ -124,6 +125,13 @@ const resolveEntry = (pkgDir: string, subpath?: string): string => {
|
|
|
124
125
|
return join(pkgDir, rel)
|
|
125
126
|
}
|
|
126
127
|
|
|
128
|
+
/** The runtime set's peers that are npm's: the CLI calls their API (lightmap bake's unwrapGlbFiles), so one
|
|
129
|
+
* OLDER than this CLI is a "not a function" deep inside a command — re-installed at this CLI's range, never
|
|
130
|
+
* loaded. (The set's archive, lecodes-headless, is installed by its url at exactly this version.) */
|
|
131
|
+
const SET_ON_NPM = new Set(["lecodes-assets"])
|
|
132
|
+
const olderThanCli = (name: string, version: string | null): boolean =>
|
|
133
|
+
SET_ON_NPM.has(name) && version !== null && semverNewer(version, pkg.version) > 0
|
|
134
|
+
|
|
127
135
|
const isNotFound = (e: unknown, spec: string): boolean => {
|
|
128
136
|
const code = (e as { code?: string })?.code
|
|
129
137
|
const msg = e instanceof Error ? e.message : String(e)
|
|
@@ -146,17 +154,25 @@ export async function loadPeer<T>(name: string, subpath: string | undefined, opt
|
|
|
146
154
|
export async function loadPeer<T>(name: string, subpath: string | undefined, opts: LoadPeerOptions & { install: false }): Promise<T | null>
|
|
147
155
|
export async function loadPeer<T>(name: string, subpath: string | undefined, opts: LoadPeerOptions): Promise<T | null> {
|
|
148
156
|
const spec = subpath ? `${name}/${subpath}` : name
|
|
157
|
+
// what the user installed by hand wins — unless it is older than the API this CLI calls
|
|
158
|
+
const byHand = peerVersionGlobal(name, spec)
|
|
159
|
+
if (olderThanCli(name, byHand)) {
|
|
160
|
+
throw new CliError(`${name} ${byHand} (installed by hand) is older than this CLI (${pkg.version}) needs — update it: ${commandLine(ownManager(), installGlobalArgv(ownManager(), [await peerSpec(name)]))}`)
|
|
161
|
+
}
|
|
149
162
|
try {
|
|
150
163
|
return (await import(spec)) as T
|
|
151
164
|
} catch (e) {
|
|
152
165
|
if (!isNotFound(e, spec)) throw e
|
|
153
166
|
}
|
|
154
167
|
const dir = peerDirInPrefix(name)
|
|
155
|
-
|
|
168
|
+
const installed = peerVersionPrefix(name)
|
|
169
|
+
const stale = olderThanCli(name, installed)
|
|
170
|
+
if (installed === null || stale) {
|
|
156
171
|
if (opts.install === false) return null
|
|
157
172
|
// A global install of a DIFFERENT version than the probe expects would have resolved above; here
|
|
158
173
|
// the peer is nowhere. (Globally installed but broken → the error above was not "not found".)
|
|
159
|
-
if (peerVersionGlobal(name, spec) !== null) throw new CliError(`${name} is installed but "${spec}" can't be loaded from it — reinstall it: ${commandLine(ownManager(), installGlobalArgv(ownManager(), [await peerSpec(name)]))}`)
|
|
174
|
+
if (installed === null && peerVersionGlobal(name, spec) !== null) throw new CliError(`${name} is installed but "${spec}" can't be loaded from it — reinstall it: ${commandLine(ownManager(), installGlobalArgv(ownManager(), [await peerSpec(name)]))}`)
|
|
175
|
+
if (stale) note(`${name} ${installed} in ${peersDir()} is older than this CLI (${pkg.version}) — installing the one it needs`)
|
|
160
176
|
await installPeers(companions(name), opts.for)
|
|
161
177
|
}
|
|
162
178
|
return (await import(pathToFileURL(resolveEntry(dir, subpath)).href)) as T
|
|
Binary file
|
|
Binary file
|