lecodes-cli 0.6.4 → 0.7.1

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 (48) hide show
  1. package/README.md +1 -0
  2. package/dist/index.js +920 -636
  3. package/package.json +9 -4
  4. package/runtime/scene-harness.json +1 -0
  5. package/runtime/sdk/compile/assetMacro.ts +4 -3
  6. package/runtime/sdk/compile/bundler.ts +35 -3
  7. package/runtime/sdk/compile/compileProject.ts +14 -2
  8. package/runtime/sdk/compile/libraryImports.ts +47 -0
  9. package/runtime/sdk/compile/sceneEditor.ts +16 -4
  10. package/runtime/sdk/core/Aspect.ts +37 -0
  11. package/runtime/sdk/core/InspectorUI.ts +212 -0
  12. package/runtime/sdk/core/fields.ts +15 -3
  13. package/runtime/sdk/g2/Scene2D.ts +51 -5
  14. package/runtime/sdk/gl/Model.ts +9 -0
  15. package/runtime/sdk/gl/Node.ts +4 -1
  16. package/runtime/sdk/gl/Scene.ts +76 -11
  17. package/runtime/sdk/gl/scenarios.ts +349 -0
  18. package/runtime/sdk/inject.ts +28 -8
  19. package/runtime/sdk/kit/UITabs.ts +105 -0
  20. package/runtime/sdk/plugins/camera.ts +81 -0
  21. package/runtime/sdk/plugins/geolocation.ts +123 -0
  22. package/runtime/sdk/plugins/permission.ts +7 -0
  23. package/runtime/sdk/plugins/qr.ts +61 -24
  24. package/runtime/sdk/runtime/app.ts +37 -0
  25. package/runtime/sdk/runtime/appEvents.ts +29 -0
  26. package/runtime/sdk/runtime/channel.ts +50 -0
  27. package/runtime/sdk/runtime/clipboard.ts +20 -0
  28. package/runtime/sdk/runtime/datetime.ts +2 -4
  29. package/runtime/sdk/runtime/device.ts +137 -4
  30. package/runtime/sdk/runtime/misc.ts +4 -0
  31. package/runtime/sdk/runtime/service.ts +82 -0
  32. package/runtime/sdk/runtime/touch.ts +26 -0
  33. package/runtime/sdk/scene/defineScene.ts +651 -17
  34. package/runtime/sdk/scene/editorPlugins.ts +86 -0
  35. package/runtime/sdk/ui/NativeView.ts +144 -0
  36. package/runtime/sdk/ui/UI.ts +6 -1
  37. package/runtime/sdk/ui/UIButton.ts +26 -3
  38. package/runtime/sdk/ui/UIInput.ts +2 -2
  39. package/runtime/sdk/ui/UINode.ts +24 -4
  40. package/runtime/sdk/ui/UIScreen.ts +86 -29
  41. package/runtime/sdk/ui/UIScreenHost.ts +213 -0
  42. package/runtime/sdk/ui/UIText.ts +1 -1
  43. package/runtime/sdk/ui/UIVideo.ts +48 -2
  44. package/runtime/sdk/ui/UIWidget.ts +34 -1
  45. package/runtime/sdk/ui/presentable.ts +116 -0
  46. package/runtime/sdk/ui/router.ts +73 -29
  47. package/runtime/sdk-types.json +1 -1
  48. package/runtime/sdk/runtime/camera.ts +0 -31
package/dist/index.js CHANGED
@@ -31,133 +31,538 @@ var __toESM = (mod, isNodeMode, target) => {
31
31
  return to;
32
32
  };
33
33
  var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
34
+ var __returnValue = (v) => v;
35
+ function __exportSetter(name, newValue) {
36
+ this[name] = __returnValue.bind(null, newValue);
37
+ }
38
+ var __export = (target, all) => {
39
+ for (var name in all)
40
+ __defProp(target, name, {
41
+ get: all[name],
42
+ enumerable: true,
43
+ configurable: true,
44
+ set: __exportSetter.bind(all, name)
45
+ });
46
+ };
47
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
34
48
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
35
49
 
36
- // ../../node_modules/.bun/qrcode-terminal@0.12.0/node_modules/qrcode-terminal/vendor/QRCode/QRMode.js
37
- var require_QRMode = __commonJS((exports, module) => {
38
- module.exports = {
39
- MODE_NUMBER: 1 << 0,
40
- MODE_ALPHA_NUM: 1 << 1,
41
- MODE_8BIT_BYTE: 1 << 2,
42
- MODE_KANJI: 1 << 3
43
- };
50
+ // ../sdk/src/compile/assetMacro.ts
51
+ var ASSET_RE, hasAssetMacro = (src) => src.includes("asset("), desugarAssetMacros = (src, filePath) => {
52
+ const byPath = new Map;
53
+ let i = 0;
54
+ const out = src.replace(ASSET_RE, (_match, _quote, path) => {
55
+ let name = byPath.get(path);
56
+ if (!name) {
57
+ name = `__asset_${i++}`;
58
+ byPath.set(path, name);
59
+ }
60
+ return name;
61
+ });
62
+ if (/(?<![.\w$])asset\(/.test(out)) {
63
+ throw new Error(`asset() expects a string literal, found a dynamic argument in: ${filePath}`);
64
+ }
65
+ if (byPath.size === 0)
66
+ return src;
67
+ const imports = [...byPath].map(([path, name]) => `import ${name} from ${JSON.stringify(path)};`).join(`
68
+ `);
69
+ return out + `
70
+ ` + imports;
71
+ };
72
+ var init_assetMacro = __esm(() => {
73
+ ASSET_RE = /(?<![.\w$])asset\(\s*(["'])((?:[^"'\\]|\\.)*)\1\s*\)/g;
44
74
  });
45
75
 
46
- // ../../node_modules/.bun/qrcode-terminal@0.12.0/node_modules/qrcode-terminal/vendor/QRCode/QR8bitByte.js
47
- var require_QR8bitByte = __commonJS((exports, module) => {
48
- var QRMode = require_QRMode();
49
- function QR8bitByte(data) {
50
- this.mode = QRMode.MODE_8BIT_BYTE;
51
- this.data = data;
76
+ // ../sdk/src/compile/aspectMacro.ts
77
+ var ASPECT_RE, hasAspectExtends = (src) => /\bextends\s+Aspect\s*</.test(src), injectAspectNames = (src) => {
78
+ let out = "";
79
+ let last = 0;
80
+ ASPECT_RE.lastIndex = 0;
81
+ let m;
82
+ while ((m = ASPECT_RE.exec(src)) !== null) {
83
+ const braceEnd = m.index + m[0].length;
84
+ if (!classBodyDeclaresAspect(src, braceEnd)) {
85
+ out += src.slice(last, braceEnd) + ` static aspect = ${JSON.stringify(m[2])};`;
86
+ last = braceEnd;
87
+ }
52
88
  }
53
- QR8bitByte.prototype = {
54
- getLength: function() {
55
- return this.data.length;
56
- },
57
- write: function(buffer) {
58
- for (var i = 0;i < this.data.length; i++) {
59
- buffer.put(this.data.charCodeAt(i), 8);
60
- }
89
+ return last === 0 ? src : out + src.slice(last);
90
+ }, classBodyDeclaresAspect = (src, from) => {
91
+ let depth = 1;
92
+ let i = from;
93
+ for (;i < src.length && depth > 0; i++) {
94
+ const c2 = src[i];
95
+ if (c2 === "{")
96
+ depth++;
97
+ else if (c2 === "}")
98
+ depth--;
99
+ }
100
+ return /\bstatic\b[^;{}()=]*\baspect\b/.test(src.slice(from, i));
101
+ };
102
+ var init_aspectMacro = __esm(() => {
103
+ ASPECT_RE = /\bclass\s+\w+\s+extends\s+Aspect\s*<\s*(["'])((?:[^"'\\]|\\.)*)\1[^{};]*>\s*\{/g;
104
+ });
105
+
106
+ // ../sdk/src/compile/libraryImports.ts
107
+ var hasImportStatement = (src) => src.includes("import") || src.includes("from"), IMPORT_FROM_RE, SIDE_EFFECT_RE, isRelativeOrAbsolute = (spec) => spec.startsWith("/") || spec.startsWith("./") || spec.startsWith("../"), rewriteLibraryImports = (src, slugs) => {
108
+ const rewriteSpec = (spec) => {
109
+ if (isRelativeOrAbsolute(spec))
110
+ return spec;
111
+ const slash = spec.indexOf("/");
112
+ const slug3 = slash === -1 ? spec : spec.slice(0, slash);
113
+ if (slugs.has(slug3)) {
114
+ const sub = slash === -1 ? "/index" : spec.slice(slash);
115
+ return `/__lib/${slug3}${sub}`;
61
116
  }
117
+ throw new Error(`Unknown library "${slug3}" — add it in Settings → Libraries, or use a relative path like "./${slug3}".`);
62
118
  };
63
- module.exports = QR8bitByte;
119
+ let out = src.replace(IMPORT_FROM_RE, (_m, boundary, head, quote, spec) => `${boundary}${head}${quote}${rewriteSpec(spec)}${quote}`);
120
+ out = out.replace(SIDE_EFFECT_RE, (m, boundary, head, quote, spec) => isRelativeOrAbsolute(spec) ? m : `${boundary}${head}${quote}${rewriteSpec(spec)}${quote}`);
121
+ return out;
122
+ };
123
+ var init_libraryImports = __esm(() => {
124
+ IMPORT_FROM_RE = /(^|[;\n}])(\s*(?:import|export)\b[^"'`;]*?\bfrom\s*)(["'])([^"'\n]+)\3/g;
125
+ SIDE_EFFECT_RE = /(^|[;\n}])(\s*import\s*)(["'])([^"'\n]+)\3/g;
64
126
  });
65
127
 
66
- // ../../node_modules/.bun/qrcode-terminal@0.12.0/node_modules/qrcode-terminal/vendor/QRCode/QRMath.js
67
- var require_QRMath = __commonJS((exports, module) => {
68
- var QRMath = {
69
- glog: function(n) {
70
- if (n < 1) {
71
- throw new Error("glog(" + n + ")");
72
- }
73
- return QRMath.LOG_TABLE[n];
74
- },
75
- gexp: function(n) {
76
- while (n < 0) {
77
- n += 255;
78
- }
79
- while (n >= 256) {
80
- n -= 255;
81
- }
82
- return QRMath.EXP_TABLE[n];
83
- },
84
- EXP_TABLE: new Array(256),
85
- LOG_TABLE: new Array(256)
86
- };
87
- for (i = 0;i < 8; i++) {
88
- QRMath.EXP_TABLE[i] = 1 << i;
89
- }
90
- var i;
91
- for (i = 8;i < 256; i++) {
92
- QRMath.EXP_TABLE[i] = QRMath.EXP_TABLE[i - 4] ^ QRMath.EXP_TABLE[i - 5] ^ QRMath.EXP_TABLE[i - 6] ^ QRMath.EXP_TABLE[i - 8];
128
+ // ../sdk/src/compile/bundler.ts
129
+ import { existsSync as existsSync7, readdirSync as readdirSync4, readFileSync as readFileSync6, statSync } from "node:fs";
130
+ import { spawnSync } from "node:child_process";
131
+ import { createRequire as createRequire2 } from "node:module";
132
+ import { dirname as dirname5, join as join9, relative as relative2 } from "node:path";
133
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
134
+ var resolveInject = () => {
135
+ const override = process.env.LECODES_SDK_INJECT;
136
+ if (override && existsSync7(override))
137
+ return override;
138
+ const here = dirname5(fileURLToPath2(import.meta.url));
139
+ const src = join9(here, "..", "inject.ts");
140
+ if (existsSync7(src))
141
+ return src;
142
+ throw new Error("sdk: could not locate the inject source (LECODES_SDK_INJECT / src/inject.ts)");
143
+ }, normalize = (path) => {
144
+ const parts = [];
145
+ for (const part of path.split("/")) {
146
+ if (!part || part === ".")
147
+ continue;
148
+ if (part === "..")
149
+ parts.pop();
150
+ else
151
+ parts.push(part);
93
152
  }
94
- var i;
95
- for (i = 0;i < 255; i++) {
96
- QRMath.LOG_TABLE[QRMath.EXP_TABLE[i]] = i;
153
+ return "/" + parts.join("/");
154
+ }, CHISEL_DEFINE, CHISEL_KEEP, resolveSdkSrc = () => {
155
+ const inject = resolveInject();
156
+ if (!inject.endsWith("inject.ts")) {
157
+ throw new Error(`chisel needs the SDK source tree (src/inject.ts); got ${inject}. Set LECODES_SDK_INJECT to the source.`);
97
158
  }
98
- var i;
99
- module.exports = QRMath;
100
- });
101
-
102
- // ../../node_modules/.bun/qrcode-terminal@0.12.0/node_modules/qrcode-terminal/vendor/QRCode/QRPolynomial.js
103
- var require_QRPolynomial = __commonJS((exports, module) => {
104
- var QRMath = require_QRMath();
105
- function QRPolynomial(num, shift) {
106
- if (num.length === undefined) {
107
- throw new Error(num.length + "/" + shift);
108
- }
109
- var offset = 0;
110
- while (offset < num.length && num[offset] === 0) {
111
- offset++;
159
+ return dirname5(inject);
160
+ }, isSdkSource = (path) => /\.[tj]s$/.test(path) && !path.endsWith(".d.ts") && !path.includes("/compile/") && !path.includes("/tests/") && !path.includes("/__tests__/"), sdkFilesCache, collectSdkFiles = (sdkSrc) => {
161
+ const cached = sdkFilesCache.get(sdkSrc);
162
+ if (cached)
163
+ return cached;
164
+ const out = {};
165
+ const walk = (dir) => {
166
+ for (const name of readdirSync4(dir)) {
167
+ const p = join9(dir, name);
168
+ if (statSync(p).isDirectory())
169
+ walk(p);
170
+ else if (isSdkSource(p))
171
+ out["/__sdk/" + relative2(sdkSrc, p).split("\\").join("/")] = readFileSync6(p, "utf8");
112
172
  }
113
- this.num = new Array(num.length - offset + shift);
114
- for (var i = 0;i < num.length - offset; i++) {
115
- this.num[i] = num[i + offset];
173
+ };
174
+ walk(sdkSrc);
175
+ sdkFilesCache.set(sdkSrc, out);
176
+ return out;
177
+ }, resolveChiselBin = () => {
178
+ const override = process.env.LECODES_CHISEL_BIN;
179
+ if (override) {
180
+ if (existsSync7(override))
181
+ return override;
182
+ throw new Error(`LECODES_CHISEL_BIN points at a missing file: ${override}`);
183
+ }
184
+ try {
185
+ const bin = createRequire2(import.meta.url)("@letary/chisel").binaryPath();
186
+ if (bin && existsSync7(bin))
187
+ return bin;
188
+ } catch {}
189
+ throw new Error("chisel binary not found. Install @letary/chisel, or set LECODES_CHISEL_BIN.");
190
+ }, bundleProjectChisel = async (entrypoint, files, format, sourcemap, minifyOverride, editor, libraries) => {
191
+ const entry = normalize(entrypoint);
192
+ const slugs = new Set(Object.keys(libraries ?? {}));
193
+ const projFiles = {};
194
+ for (const [k, v] of Object.entries(files)) {
195
+ const path = normalize(k);
196
+ let src = v;
197
+ if (/\.[tj]sx?$/.test(path)) {
198
+ if (hasImportStatement(src))
199
+ src = rewriteLibraryImports(src, slugs);
200
+ if (hasAssetMacro(src))
201
+ src = desugarAssetMacros(src, path);
202
+ if (hasAspectExtends(src))
203
+ src = injectAspectNames(src);
116
204
  }
205
+ projFiles[path] = src;
117
206
  }
118
- QRPolynomial.prototype = {
119
- get: function(index) {
120
- return this.num[index];
121
- },
122
- getLength: function() {
123
- return this.num.length;
124
- },
125
- multiply: function(e) {
126
- var num = new Array(this.getLength() + e.getLength() - 1);
127
- for (var i = 0;i < this.getLength(); i++) {
128
- for (var j = 0;j < e.getLength(); j++) {
129
- num[i + j] ^= QRMath.gexp(QRMath.glog(this.get(i)) + QRMath.glog(e.get(j)));
130
- }
131
- }
132
- return new QRPolynomial(num, 0);
133
- },
134
- mod: function(e) {
135
- if (this.getLength() - e.getLength() < 0) {
136
- return this;
137
- }
138
- var ratio = QRMath.glog(this.get(0)) - QRMath.glog(e.get(0));
139
- var num = new Array(this.getLength());
140
- for (var i = 0;i < this.getLength(); i++) {
141
- num[i] = this.get(i);
142
- }
143
- for (var x = 0;x < e.getLength(); x++) {
144
- num[x] ^= QRMath.gexp(QRMath.glog(e.get(x)) + ratio);
207
+ if (!projFiles[entry])
208
+ throw new Error(`Entrypoint not found: ${entry}`);
209
+ for (const [slug3, libFiles] of Object.entries(libraries ?? {})) {
210
+ for (const [k, v] of Object.entries(libFiles)) {
211
+ const path = normalize(`/__lib/${slug3}/${k}`);
212
+ let src = v;
213
+ if (/\.[tj]sx?$/.test(path)) {
214
+ if (hasAssetMacro(src))
215
+ src = desugarAssetMacros(src, path);
216
+ if (hasAspectExtends(src))
217
+ src = injectAspectNames(src);
145
218
  }
146
- return new QRPolynomial(num, 0).mod(e);
219
+ projFiles[path] = src;
147
220
  }
148
- };
149
- module.exports = QRPolynomial;
221
+ }
222
+ const sdkFiles = collectSdkFiles(resolveSdkSrc());
223
+ const minify = minifyOverride ?? process.env.LECODES_CHISEL_MINIFY !== "0";
224
+ const input = JSON.stringify({
225
+ files: { ...sdkFiles, ...projFiles },
226
+ entry,
227
+ inject: ["/__sdk/inject.ts"],
228
+ format,
229
+ minify,
230
+ fuse: process.env.LECODES_CHISEL_FUSE === "1",
231
+ define: { ...CHISEL_DEFINE, EDITOR: editor ? "true" : "false" },
232
+ keep: CHISEL_KEEP,
233
+ sourcemap,
234
+ reactive_ui: process.env.LECODES_CHISEL_REACTIVE_UI !== "0"
235
+ });
236
+ const bin = resolveChiselBin();
237
+ const r = spawnSync(bin, [], { input, encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
238
+ if (r.status === 2 || r.error) {
239
+ throw new Error(`chisel failed: ${r.error?.message ?? r.stderr ?? "unknown error"}`);
240
+ }
241
+ let parsed;
242
+ try {
243
+ parsed = JSON.parse(r.stdout || "{}");
244
+ } catch {
245
+ throw new Error(`chisel produced invalid output: ${(r.stdout || r.stderr || "").slice(0, 200)}`);
246
+ }
247
+ if (parsed.error)
248
+ throw new Error(`chisel: ${parsed.error}`);
249
+ if (parsed.code == null)
250
+ throw new Error("chisel produced no code");
251
+ return { code: parsed.code, map: parsed.map ?? null };
252
+ }, bundleProject = async (entrypoint, files, format = "esm") => (await bundleProjectChisel(entrypoint, files, format, false)).code, bundleProjectWithMap = async (entrypoint, files, format = "esm", minify, editor, libraries) => bundleProjectChisel(entrypoint, files, format, true, minify, editor, libraries);
253
+ var init_bundler = __esm(() => {
254
+ init_assetMacro();
255
+ init_aspectMacro();
256
+ init_libraryImports();
257
+ CHISEL_DEFINE = { RAD2DEG: "57.29577951308232", DEG2RAD: "0.017453292519943295" };
258
+ CHISEL_KEEP = ["_*"];
259
+ sdkFilesCache = new Map;
150
260
  });
151
261
 
152
- // ../../node_modules/.bun/qrcode-terminal@0.12.0/node_modules/qrcode-terminal/vendor/QRCode/QRMaskPattern.js
153
- var require_QRMaskPattern = __commonJS((exports, module) => {
154
- module.exports = {
155
- PATTERN000: 0,
156
- PATTERN001: 1,
157
- PATTERN010: 2,
158
- PATTERN011: 3,
159
- PATTERN100: 4,
160
- PATTERN101: 5,
262
+ // ../sdk/src/compile/sourcemap.ts
263
+ var offsetSourceMap = (mapJson, lines) => {
264
+ if (lines <= 0)
265
+ return mapJson;
266
+ const map = JSON.parse(mapJson);
267
+ map.mappings = ";".repeat(lines) + map.mappings;
268
+ return JSON.stringify(map);
269
+ }, inlineSourceMapComment = (mapJson) => {
270
+ const b64 = Buffer.from(mapJson, "utf8").toString("base64");
271
+ return `//# lecodesMap=data:application/json;charset=utf-8;base64,${b64}`;
272
+ };
273
+
274
+ // ../sdk/src/compile/header.ts
275
+ var buildHeader = (name, code, opts = {}) => {
276
+ let header = `// name: ${name.slice(0, 20)}
277
+ `;
278
+ if (opts.icon)
279
+ header += `// icon: ${opts.icon}
280
+ `;
281
+ header += `// last-updated: ${new Date().toISOString()}
282
+ `;
283
+ const localAssets = new Set;
284
+ for (const m of code.matchAll(/_creatorUtils\.fetchLocal\((.+?)\)/g)) {
285
+ localAssets.add(m[1].replaceAll(/['"]/g, "").trim());
286
+ }
287
+ if (/_creator\.createGlb/.test(code))
288
+ localAssets.add("uberarchive");
289
+ if (/_creator\.captureImage/.test(code))
290
+ localAssets.add("capture");
291
+ if (localAssets.size > 0)
292
+ header += `// preload: ${[...localAssets].join(";")}
293
+ `;
294
+ if (!code.includes("_creator.createScene") && !code.includes("_creator.createEntity"))
295
+ header += `// gl: disable
296
+ `;
297
+ else if (code.includes("_creator.createEngine"))
298
+ header += `// gl: defer
299
+ `;
300
+ if (code.includes("_creator2d."))
301
+ header += `// 2d: enable
302
+ `;
303
+ if (code.includes("_creator.createARController"))
304
+ header += "// ar: enable";
305
+ header += `
306
+ `;
307
+ return header;
308
+ }, detectProjectKind = (code) => {
309
+ if (code.includes("_creator.createARController"))
310
+ return "ar";
311
+ if (code.includes("_creator2d."))
312
+ return "2d";
313
+ if (!code.includes("_creator."))
314
+ return "ui";
315
+ return "3d";
316
+ };
317
+
318
+ // ../sdk/src/compile/compileProject.ts
319
+ var detectEntry = (entries) => {
320
+ let entry = "";
321
+ let max = -1;
322
+ for (const e of entries) {
323
+ const ext2 = e.path.split(".").at(-1)?.toLowerCase();
324
+ if (e.path.endsWith(".editor.ts"))
325
+ continue;
326
+ if (e.type === "text" && e.text !== undefined && !e.text.includes("export") && (ext2 === "ts" || ext2 === "js")) {
327
+ if (e.text.length > max) {
328
+ entry = e.path;
329
+ max = e.text.length;
330
+ }
331
+ }
332
+ }
333
+ return entry;
334
+ }, compileProject = async (opts) => {
335
+ const entry = opts.entryOverride ?? detectEntry(opts.entries);
336
+ if (!entry) {
337
+ throw new Error("Couldn't find an entrypoint (a top-level .ts/.js file with no `export`). Pass entryOverride.");
338
+ }
339
+ const files = {};
340
+ for (const e of opts.entries) {
341
+ if (e.type === "resource") {
342
+ if (opts.localAssets) {
343
+ const name = e.path.split(/[\\/]/).pop() ?? e.path;
344
+ files[e.path] = `export default ("id:" + _creatorUtils.fetchLocal(${JSON.stringify(name)}))`;
345
+ } else {
346
+ files[e.path] = `export default "${opts.publicUrl}${e.fileSrc ?? ""}"`;
347
+ }
348
+ } else if (e.type === "shader") {
349
+ const shaders = e.shaders ?? [];
350
+ if (shaders.length > 2) {
351
+ files[e.path] = `export default \`${opts.publicUrl}${shaders[0].src.replace("_opengl", "_${_creator.subBackend ?? _creator.backend}")}\``;
352
+ } else if (shaders.length > 0) {
353
+ files[e.path] = `export default \`${opts.publicUrl}${shaders[0].src.replace("_opengl", "_${_creator.backend}")}\``;
354
+ } else {
355
+ files[e.path] = `export default ""`;
356
+ }
357
+ } else if (e.path.toLowerCase().endsWith(".svg")) {
358
+ files[e.path] = `export default SvgSource(\`${e.text ?? ""}\`)`;
359
+ } else {
360
+ files[e.path] = e.text ?? "";
361
+ }
362
+ }
363
+ const { code, map } = await bundleProjectWithMap(entry, files, opts.format ?? "esm", opts.minify, opts.editor, opts.libraries);
364
+ if (opts.header === false) {
365
+ return map ? `${code}
366
+ ${inlineSourceMapComment(map)}` : code;
367
+ }
368
+ const header = buildHeader(opts.name, code, { icon: opts.icon });
369
+ if (!map)
370
+ return header + code;
371
+ const headerLines = (header.match(/\n/g) ?? []).length;
372
+ return `${header}${code}
373
+ ${inlineSourceMapComment(offsetSourceMap(map, headerLines))}`;
374
+ };
375
+ var init_compileProject = __esm(() => {
376
+ init_bundler();
377
+ });
378
+
379
+ // ../sdk/src/compile/sceneEditor.ts
380
+ import { existsSync as existsSync8, readdirSync as readdirSync5, readFileSync as readFileSync7 } from "node:fs";
381
+ import { join as join10 } from "node:path";
382
+ var SCENE_HARNESS_DIR = "./.scene-editor", SCENE_ENTRY_PATH = "./.scene-entry.ts", loadSceneHarness = (fallbackDir) => {
383
+ const override = process.env.LECODES_SCENE_HARNESS_DIR;
384
+ const dir = override && existsSync8(override) ? override : fallbackDir && existsSync8(fallbackDir) ? fallbackDir : null;
385
+ if (!dir)
386
+ throw new Error("scene-editor harness sources not found (set LECODES_SCENE_HARNESS_DIR)");
387
+ const out = {};
388
+ for (const f of readdirSync5(dir)) {
389
+ if (f.endsWith(".ts"))
390
+ out[f] = readFileSync7(join10(dir, f), "utf8");
391
+ }
392
+ return out;
393
+ }, sceneEditorEntries = (scenePath, harness, projectPaths = []) => {
394
+ const entries = Object.entries(harness).map(([name, text]) => ({
395
+ path: `${SCENE_HARNESS_DIR}/${name}`,
396
+ type: "text",
397
+ text
398
+ }));
399
+ entries.push({
400
+ path: `${SCENE_HARNESS_DIR}/.edit-mode.ts`,
401
+ type: "text",
402
+ text: `;(globalThis as any).__lecodesSceneEdit = true
403
+ export {}
404
+ `
405
+ });
406
+ const editorImports = projectPaths.filter((p) => p.endsWith(".editor.ts")).map((p) => `import "${p}"
407
+ `).join("");
408
+ entries.push({
409
+ path: SCENE_ENTRY_PATH,
410
+ type: "text",
411
+ text: `import "${SCENE_HARNESS_DIR}/.edit-mode.ts"
412
+ ` + `import "${scenePath}"
413
+ ` + editorImports + `import "${SCENE_HARNESS_DIR}/main.ts"
414
+ `
415
+ });
416
+ return entries;
417
+ };
418
+ var init_sceneEditor = () => {};
419
+
420
+ // ../sdk/src/compile/index.ts
421
+ var exports_compile = {};
422
+ __export(exports_compile, {
423
+ sceneEditorEntries: () => sceneEditorEntries,
424
+ offsetSourceMap: () => offsetSourceMap,
425
+ loadSceneHarness: () => loadSceneHarness,
426
+ inlineSourceMapComment: () => inlineSourceMapComment,
427
+ detectProjectKind: () => detectProjectKind,
428
+ compileProject: () => compileProject,
429
+ bundleProjectWithMap: () => bundleProjectWithMap,
430
+ bundleProject: () => bundleProject,
431
+ buildHeader: () => buildHeader,
432
+ SCENE_HARNESS_DIR: () => SCENE_HARNESS_DIR,
433
+ SCENE_ENTRY_PATH: () => SCENE_ENTRY_PATH
434
+ });
435
+ var init_compile = __esm(() => {
436
+ init_bundler();
437
+ init_compileProject();
438
+ init_sceneEditor();
439
+ });
440
+
441
+ // ../../node_modules/.bun/qrcode-terminal@0.12.0/node_modules/qrcode-terminal/vendor/QRCode/QRMode.js
442
+ var require_QRMode = __commonJS((exports, module) => {
443
+ module.exports = {
444
+ MODE_NUMBER: 1 << 0,
445
+ MODE_ALPHA_NUM: 1 << 1,
446
+ MODE_8BIT_BYTE: 1 << 2,
447
+ MODE_KANJI: 1 << 3
448
+ };
449
+ });
450
+
451
+ // ../../node_modules/.bun/qrcode-terminal@0.12.0/node_modules/qrcode-terminal/vendor/QRCode/QR8bitByte.js
452
+ var require_QR8bitByte = __commonJS((exports, module) => {
453
+ var QRMode = require_QRMode();
454
+ function QR8bitByte(data) {
455
+ this.mode = QRMode.MODE_8BIT_BYTE;
456
+ this.data = data;
457
+ }
458
+ QR8bitByte.prototype = {
459
+ getLength: function() {
460
+ return this.data.length;
461
+ },
462
+ write: function(buffer) {
463
+ for (var i = 0;i < this.data.length; i++) {
464
+ buffer.put(this.data.charCodeAt(i), 8);
465
+ }
466
+ }
467
+ };
468
+ module.exports = QR8bitByte;
469
+ });
470
+
471
+ // ../../node_modules/.bun/qrcode-terminal@0.12.0/node_modules/qrcode-terminal/vendor/QRCode/QRMath.js
472
+ var require_QRMath = __commonJS((exports, module) => {
473
+ var QRMath = {
474
+ glog: function(n) {
475
+ if (n < 1) {
476
+ throw new Error("glog(" + n + ")");
477
+ }
478
+ return QRMath.LOG_TABLE[n];
479
+ },
480
+ gexp: function(n) {
481
+ while (n < 0) {
482
+ n += 255;
483
+ }
484
+ while (n >= 256) {
485
+ n -= 255;
486
+ }
487
+ return QRMath.EXP_TABLE[n];
488
+ },
489
+ EXP_TABLE: new Array(256),
490
+ LOG_TABLE: new Array(256)
491
+ };
492
+ for (i = 0;i < 8; i++) {
493
+ QRMath.EXP_TABLE[i] = 1 << i;
494
+ }
495
+ var i;
496
+ for (i = 8;i < 256; i++) {
497
+ QRMath.EXP_TABLE[i] = QRMath.EXP_TABLE[i - 4] ^ QRMath.EXP_TABLE[i - 5] ^ QRMath.EXP_TABLE[i - 6] ^ QRMath.EXP_TABLE[i - 8];
498
+ }
499
+ var i;
500
+ for (i = 0;i < 255; i++) {
501
+ QRMath.LOG_TABLE[QRMath.EXP_TABLE[i]] = i;
502
+ }
503
+ var i;
504
+ module.exports = QRMath;
505
+ });
506
+
507
+ // ../../node_modules/.bun/qrcode-terminal@0.12.0/node_modules/qrcode-terminal/vendor/QRCode/QRPolynomial.js
508
+ var require_QRPolynomial = __commonJS((exports, module) => {
509
+ var QRMath = require_QRMath();
510
+ function QRPolynomial(num, shift) {
511
+ if (num.length === undefined) {
512
+ throw new Error(num.length + "/" + shift);
513
+ }
514
+ var offset = 0;
515
+ while (offset < num.length && num[offset] === 0) {
516
+ offset++;
517
+ }
518
+ this.num = new Array(num.length - offset + shift);
519
+ for (var i = 0;i < num.length - offset; i++) {
520
+ this.num[i] = num[i + offset];
521
+ }
522
+ }
523
+ QRPolynomial.prototype = {
524
+ get: function(index) {
525
+ return this.num[index];
526
+ },
527
+ getLength: function() {
528
+ return this.num.length;
529
+ },
530
+ multiply: function(e) {
531
+ var num = new Array(this.getLength() + e.getLength() - 1);
532
+ for (var i = 0;i < this.getLength(); i++) {
533
+ for (var j = 0;j < e.getLength(); j++) {
534
+ num[i + j] ^= QRMath.gexp(QRMath.glog(this.get(i)) + QRMath.glog(e.get(j)));
535
+ }
536
+ }
537
+ return new QRPolynomial(num, 0);
538
+ },
539
+ mod: function(e) {
540
+ if (this.getLength() - e.getLength() < 0) {
541
+ return this;
542
+ }
543
+ var ratio = QRMath.glog(this.get(0)) - QRMath.glog(e.get(0));
544
+ var num = new Array(this.getLength());
545
+ for (var i = 0;i < this.getLength(); i++) {
546
+ num[i] = this.get(i);
547
+ }
548
+ for (var x = 0;x < e.getLength(); x++) {
549
+ num[x] ^= QRMath.gexp(QRMath.glog(e.get(x)) + ratio);
550
+ }
551
+ return new QRPolynomial(num, 0).mod(e);
552
+ }
553
+ };
554
+ module.exports = QRPolynomial;
555
+ });
556
+
557
+ // ../../node_modules/.bun/qrcode-terminal@0.12.0/node_modules/qrcode-terminal/vendor/QRCode/QRMaskPattern.js
558
+ var require_QRMaskPattern = __commonJS((exports, module) => {
559
+ module.exports = {
560
+ PATTERN000: 0,
561
+ PATTERN001: 1,
562
+ PATTERN010: 2,
563
+ PATTERN011: 3,
564
+ PATTERN100: 4,
565
+ PATTERN101: 5,
161
566
  PATTERN110: 6,
162
567
  PATTERN111: 7
163
568
  };
@@ -1052,7 +1457,7 @@ var require_main = __commonJS((exports, module) => {
1052
1457
  // package.json
1053
1458
  var package_default = {
1054
1459
  name: "lecodes-cli",
1055
- version: "0.6.4",
1460
+ version: "0.7.1",
1056
1461
  description: "Clone, edit and push LeCodes projects from your machine.",
1057
1462
  type: "module",
1058
1463
  license: "MIT",
@@ -1075,18 +1480,19 @@ var package_default = {
1075
1480
  icon: "bun scripts/make-icon.ts",
1076
1481
  "vendor-runtime": "bun scripts/vendor-runtime.ts",
1077
1482
  "sync-peers": "bun scripts/sync-peer-versions.ts",
1078
- build: 'bun scripts/vendor-runtime.ts && bun build src/index.ts --target=node --outfile dist/index.js --external @letary/chisel --external lecodes-renderer --external lecodes-design --banner="#!/usr/bin/env node"',
1483
+ build: 'bun scripts/vendor-runtime.ts && bun build src/index.ts --target=node --outfile dist/index.js --external @letary/chisel --external lecodes-renderer --external lecodes-design --external lecodes-3d-editor --banner="#!/usr/bin/env node"',
1079
1484
  "build:bin": "bun scripts/build-bin.ts",
1080
1485
  typecheck: "tsc --noEmit",
1081
1486
  dev: "bun run src/index.ts",
1082
1487
  prepublishOnly: "bun run sync-peers && bun run build"
1083
1488
  },
1084
1489
  dependencies: {
1085
- "@letary/chisel": "^0.3.0"
1490
+ "@letary/chisel": "^0.4.1"
1086
1491
  },
1087
1492
  peerDependencies: {
1088
1493
  "lecodes-design": "^0.3.1",
1089
- "lecodes-renderer": "^0.5.0"
1494
+ "lecodes-renderer": "^0.6.1",
1495
+ "lecodes-3d-editor": "^0.1.0"
1090
1496
  },
1091
1497
  peerDependenciesMeta: {
1092
1498
  "lecodes-design": {
@@ -1094,12 +1500,16 @@ var package_default = {
1094
1500
  },
1095
1501
  "lecodes-renderer": {
1096
1502
  optional: true
1503
+ },
1504
+ "lecodes-3d-editor": {
1505
+ optional: true
1097
1506
  }
1098
1507
  },
1099
1508
  devDependencies: {
1100
1509
  "lecodes-design": "workspace:*",
1101
1510
  "lecodes-renderer": "^0.4.1",
1102
1511
  "qrcode-terminal": "^0.12.0",
1512
+ "lecodes-3d-editor": "workspace:*",
1103
1513
  sdk: "workspace:*"
1104
1514
  }
1105
1515
  };
@@ -1593,441 +2003,159 @@ const scene = new Scene2D({ background: '#10131a' })
1593
2003
  scene.open()
1594
2004
 
1595
2005
  setLoop((dt) => {
1596
- // your per-frame logic — dt is seconds since the previous frame
1597
- })
1598
- `;
1599
- var create = async (args) => {
1600
- const config = loadConfig();
1601
- const apiUrl = requireApiUrl(config);
1602
- const token = requireToken(config);
1603
- const name = args._[0];
1604
- if (!name)
1605
- throw new CliError("Usage: lecodes create <name> [directory]");
1606
- const dir = args._[1] ?? slug2(name);
1607
- const root = resolve2(dir);
1608
- if (existsSync6(root) && readdirSync2(root).length > 0) {
1609
- throw new CliError(`Directory "${dir}" already exists and is not empty.`);
1610
- }
1611
- mkdirSync6(root, { recursive: true });
1612
- info(c.dim(`Creating project ${c.bold(name)}…`));
1613
- const { uuid } = await createProject(apiUrl, token, name, flagStr(args, "team"));
1614
- info(c.dim("Fetching type definitions…"));
1615
- await materializeTypes(root, apiUrl);
1616
- writeTsconfig(root);
1617
- writeDefaultIgnore(root);
1618
- writeFileSync6(join7(root, "main.ts"), STARTER);
1619
- const commits = await getCommits(apiUrl, token, uuid);
1620
- writeManifest(root, { uuid, apiUrl, name, baseHeadId: commits.headId, files: {} });
1621
- success(`Created ${c.bold(name)} in ${c.bold(dir)}/`);
1622
- info(c.dim(` cd ${dir} — edit main.ts, then \`lecodes push -m "first commit"\` to save it`));
1623
- };
1624
-
1625
- // src/commands/design.ts
1626
- import { existsSync as existsSync10, mkdirSync as mkdirSync8, readFileSync as readFileSync9, readdirSync as readdirSync5, writeFileSync as writeFileSync8, appendFileSync } from "node:fs";
1627
- import { basename as basename2, join as join12 } from "node:path";
1628
-
1629
- // src/compile/designCompile.ts
1630
- import { readFileSync as readFileSync8 } from "node:fs";
1631
- import { basename } from "node:path";
1632
-
1633
- // src/localFiles.ts
1634
- import { readdirSync as readdirSync3, readFileSync as readFileSync5 } from "node:fs";
1635
- import { join as join8, relative } from "node:path";
1636
- var SKIP_DIRS = new Set([".lecodes", ".git", "node_modules"]);
1637
- var SKIP_FILES = new Set(["tsconfig.json", "jsconfig.json", ".gitignore", ".lecodesignore", ".DS_Store"]);
1638
- var TEXT_EXTS = new Set([
1639
- "ts",
1640
- "tsx",
1641
- "js",
1642
- "jsx",
1643
- "mjs",
1644
- "cjs",
1645
- "json",
1646
- "svg",
1647
- "glsl",
1648
- "vert",
1649
- "frag",
1650
- "txt",
1651
- "md",
1652
- "css",
1653
- "html",
1654
- "xml",
1655
- "yml",
1656
- "yaml",
1657
- "csv"
1658
- ]);
1659
- var ext = (name) => {
1660
- const i = name.lastIndexOf(".");
1661
- return i < 0 ? "" : name.slice(i + 1).toLowerCase();
1662
- };
1663
- var classifyKind = (name) => {
1664
- const e = ext(name);
1665
- if (e === "mat")
1666
- return "shader";
1667
- if (e === "scene")
1668
- return "scene";
1669
- return TEXT_EXTS.has(e) ? "text" : "resource";
1670
- };
1671
- var isTextKind = (kind) => kind !== "resource";
1672
- var scanLocal = (root, opts = {}) => {
1673
- const { ignore, tracked } = opts;
1674
- const files = [];
1675
- const dirs = [];
1676
- const ignored = [];
1677
- const isTracked = (key) => tracked?.has(key) ?? false;
1678
- const hasTrackedUnder = (key) => {
1679
- if (!tracked)
1680
- return false;
1681
- const prefix = key + "/";
1682
- for (const t of tracked)
1683
- if (t.startsWith(prefix))
1684
- return true;
1685
- return false;
1686
- };
1687
- const walk = (dir, parentIgnored) => {
1688
- for (const entry of readdirSync3(dir, { withFileTypes: true })) {
1689
- const abs = join8(dir, entry.name);
1690
- const key = toKey(relative(root, abs));
1691
- if (entry.isDirectory()) {
1692
- if (SKIP_DIRS.has(entry.name))
1693
- continue;
1694
- const dirIgnored = parentIgnored || !!ignore && ignore(key, true);
1695
- if (dirIgnored && !isTracked(key) && !hasTrackedUnder(key)) {
1696
- ignored.push(key + "/");
1697
- continue;
1698
- }
1699
- dirs.push(key);
1700
- walk(abs, dirIgnored);
1701
- } else if (entry.isFile()) {
1702
- if (SKIP_FILES.has(entry.name))
1703
- continue;
1704
- const fileIgnored = parentIgnored || !!ignore && ignore(key, false);
1705
- if (fileIgnored && !isTracked(key)) {
1706
- ignored.push(key);
1707
- continue;
1708
- }
1709
- files.push({ path: key, absPath: abs, sha: sha256(readFileSync5(abs)) });
1710
- }
1711
- }
1712
- };
1713
- walk(root, false);
1714
- return { files, dirs, ignored };
1715
- };
1716
- var scanProject = (root, manifest) => scanLocal(root, { ignore: loadIgnore(root), tracked: new Set(Object.keys(manifest.files)) });
1717
- var diffLocal = (manifest, scan) => {
1718
- const added = [];
1719
- const modified = [];
1720
- const localByPath = new Map(scan.files.map((f) => [f.path, f]));
1721
- for (const file of scan.files) {
1722
- const known = manifest.files[file.path];
1723
- if (!known)
1724
- added.push(file);
1725
- else if (known.sha !== file.sha)
1726
- modified.push(file);
1727
- }
1728
- const deleted = Object.keys(manifest.files).filter((path) => !localByPath.has(path));
1729
- const moves = new Map;
1730
- const claimed = new Set;
1731
- for (const file of added) {
1732
- const from = deleted.find((path) => !claimed.has(path) && manifest.files[path].sha === file.sha);
1733
- if (from) {
1734
- moves.set(file.path, from);
1735
- claimed.add(from);
1736
- }
1737
- }
1738
- return { added, modified, deleted, moves };
1739
- };
1740
- var hasChanges = (diff) => diff.added.length + diff.modified.length + diff.deleted.length > 0;
1741
-
1742
- // src/compile/projectCompile.ts
1743
- import { existsSync as existsSync8 } from "node:fs";
1744
- import { createRequire as createRequire3 } from "node:module";
1745
- import { dirname as dirname6, join as join10 } from "node:path";
1746
- // ../sdk/src/compile/bundler.ts
1747
- import { existsSync as existsSync7, readdirSync as readdirSync4, readFileSync as readFileSync6, statSync } from "node:fs";
1748
- import { spawnSync } from "node:child_process";
1749
- import { createRequire as createRequire2 } from "node:module";
1750
- import { dirname as dirname5, join as join9, relative as relative2 } from "node:path";
1751
- import { fileURLToPath as fileURLToPath2 } from "node:url";
1752
-
1753
- // ../sdk/src/compile/assetMacro.ts
1754
- var ASSET_RE = /\basset\(\s*(["'])((?:[^"'\\]|\\.)*)\1\s*\)/g;
1755
- var hasAssetMacro = (src) => src.includes("asset(");
1756
- var desugarAssetMacros = (src, filePath) => {
1757
- const byPath = new Map;
1758
- let i = 0;
1759
- const out = src.replace(ASSET_RE, (_match, _quote, path) => {
1760
- let name = byPath.get(path);
1761
- if (!name) {
1762
- name = `__asset_${i++}`;
1763
- byPath.set(path, name);
1764
- }
1765
- return name;
1766
- });
1767
- if (/\basset\(/.test(out)) {
1768
- throw new Error(`asset() expects a string literal, found a dynamic argument in: ${filePath}`);
1769
- }
1770
- if (byPath.size === 0)
1771
- return src;
1772
- const imports = [...byPath].map(([path, name]) => `import ${name} from ${JSON.stringify(path)};`).join(`
1773
- `);
1774
- return out + `
1775
- ` + imports;
1776
- };
1777
-
1778
- // ../sdk/src/compile/aspectMacro.ts
1779
- var ASPECT_RE = /\bclass\s+\w+\s+extends\s+Aspect\s*<\s*(["'])((?:[^"'\\]|\\.)*)\1[^{};]*>\s*\{/g;
1780
- var hasAspectExtends = (src) => /\bextends\s+Aspect\s*</.test(src);
1781
- var injectAspectNames = (src) => {
1782
- let out = "";
1783
- let last = 0;
1784
- ASPECT_RE.lastIndex = 0;
1785
- let m;
1786
- while ((m = ASPECT_RE.exec(src)) !== null) {
1787
- const braceEnd = m.index + m[0].length;
1788
- if (!classBodyDeclaresAspect(src, braceEnd)) {
1789
- out += src.slice(last, braceEnd) + ` static aspect = ${JSON.stringify(m[2])};`;
1790
- last = braceEnd;
1791
- }
1792
- }
1793
- return last === 0 ? src : out + src.slice(last);
1794
- };
1795
- var classBodyDeclaresAspect = (src, from) => {
1796
- let depth = 1;
1797
- let i = from;
1798
- for (;i < src.length && depth > 0; i++) {
1799
- const c2 = src[i];
1800
- if (c2 === "{")
1801
- depth++;
1802
- else if (c2 === "}")
1803
- depth--;
1804
- }
1805
- return /\bstatic\b[^;{}()=]*\baspect\b/.test(src.slice(from, i));
1806
- };
1807
-
1808
- // ../sdk/src/compile/bundler.ts
1809
- var resolveInject = () => {
1810
- const override = process.env.LECODES_SDK_INJECT;
1811
- if (override && existsSync7(override))
1812
- return override;
1813
- const here = dirname5(fileURLToPath2(import.meta.url));
1814
- const src = join9(here, "..", "inject.ts");
1815
- if (existsSync7(src))
1816
- return src;
1817
- throw new Error("sdk: could not locate the inject source (LECODES_SDK_INJECT / src/inject.ts)");
1818
- };
1819
- var normalize = (path) => {
1820
- const parts = [];
1821
- for (const part of path.split("/")) {
1822
- if (!part || part === ".")
1823
- continue;
1824
- if (part === "..")
1825
- parts.pop();
1826
- else
1827
- parts.push(part);
1828
- }
1829
- return "/" + parts.join("/");
1830
- };
1831
- var CHISEL_DEFINE = { RAD2DEG: "57.29577951308232", DEG2RAD: "0.017453292519943295" };
1832
- var CHISEL_KEEP = ["_*"];
1833
- var resolveSdkSrc = () => {
1834
- const inject = resolveInject();
1835
- if (!inject.endsWith("inject.ts")) {
1836
- throw new Error(`chisel needs the SDK source tree (src/inject.ts); got ${inject}. Set LECODES_SDK_INJECT to the source.`);
1837
- }
1838
- return dirname5(inject);
1839
- };
1840
- var isSdkSource = (path) => /\.[tj]s$/.test(path) && !path.endsWith(".d.ts") && !path.includes("/compile/") && !path.includes("/tests/") && !path.includes("/__tests__/");
1841
- var sdkFilesCache = new Map;
1842
- var collectSdkFiles = (sdkSrc) => {
1843
- const cached = sdkFilesCache.get(sdkSrc);
1844
- if (cached)
1845
- return cached;
1846
- const out = {};
1847
- const walk = (dir) => {
1848
- for (const name of readdirSync4(dir)) {
1849
- const p = join9(dir, name);
1850
- if (statSync(p).isDirectory())
1851
- walk(p);
1852
- else if (isSdkSource(p))
1853
- out["/__sdk/" + relative2(sdkSrc, p).split("\\").join("/")] = readFileSync6(p, "utf8");
1854
- }
1855
- };
1856
- walk(sdkSrc);
1857
- sdkFilesCache.set(sdkSrc, out);
1858
- return out;
1859
- };
1860
- var resolveChiselBin = () => {
1861
- const override = process.env.LECODES_CHISEL_BIN;
1862
- if (override) {
1863
- if (existsSync7(override))
1864
- return override;
1865
- throw new Error(`LECODES_CHISEL_BIN points at a missing file: ${override}`);
1866
- }
1867
- try {
1868
- const bin = createRequire2(import.meta.url)("@letary/chisel").binaryPath();
1869
- if (bin && existsSync7(bin))
1870
- return bin;
1871
- } catch {}
1872
- throw new Error("chisel binary not found. Install @letary/chisel, or set LECODES_CHISEL_BIN.");
1873
- };
1874
- var bundleProjectChisel = async (entrypoint, files, format, sourcemap) => {
1875
- const entry = normalize(entrypoint);
1876
- const projFiles = {};
1877
- for (const [k, v] of Object.entries(files)) {
1878
- const path = normalize(k);
1879
- let src = v;
1880
- if (/\.[tj]sx?$/.test(path)) {
1881
- if (hasAssetMacro(src))
1882
- src = desugarAssetMacros(src, path);
1883
- if (hasAspectExtends(src))
1884
- src = injectAspectNames(src);
1885
- }
1886
- projFiles[path] = src;
1887
- }
1888
- if (!projFiles[entry])
1889
- throw new Error(`Entrypoint not found: ${entry}`);
1890
- const sdkFiles = collectSdkFiles(resolveSdkSrc());
1891
- const minify = process.env.LECODES_CHISEL_MINIFY !== "0";
1892
- const input = JSON.stringify({
1893
- files: { ...sdkFiles, ...projFiles },
1894
- entry,
1895
- inject: ["/__sdk/inject.ts"],
1896
- format,
1897
- minify,
1898
- fuse: process.env.LECODES_CHISEL_FUSE === "1",
1899
- define: CHISEL_DEFINE,
1900
- keep: CHISEL_KEEP,
1901
- sourcemap,
1902
- reactive_ui: process.env.LECODES_CHISEL_REACTIVE_UI !== "0"
1903
- });
1904
- const bin = resolveChiselBin();
1905
- const r = spawnSync(bin, [], { input, encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
1906
- if (r.status === 2 || r.error) {
1907
- throw new Error(`chisel failed: ${r.error?.message ?? r.stderr ?? "unknown error"}`);
1908
- }
1909
- let parsed;
1910
- try {
1911
- parsed = JSON.parse(r.stdout || "{}");
1912
- } catch {
1913
- throw new Error(`chisel produced invalid output: ${(r.stdout || r.stderr || "").slice(0, 200)}`);
1914
- }
1915
- if (parsed.error)
1916
- throw new Error(`chisel: ${parsed.error}`);
1917
- if (parsed.code == null)
1918
- throw new Error("chisel produced no code");
1919
- return { code: parsed.code, map: parsed.map ?? null };
1920
- };
1921
- var bundleProjectWithMap = async (entrypoint, files, format = "esm") => bundleProjectChisel(entrypoint, files, format, true);
1922
-
1923
- // ../sdk/src/compile/header.ts
1924
- var buildHeader = (name, code, opts = {}) => {
1925
- let header = `// name: ${name.slice(0, 20)}
1926
- `;
1927
- if (opts.icon)
1928
- header += `// icon: ${opts.icon}
1929
- `;
1930
- header += `// last-updated: ${new Date().toISOString()}
1931
- `;
1932
- const localAssets = new Set;
1933
- for (const m of code.matchAll(/_creatorUtils\.fetchLocal\((.+?)\)/g)) {
1934
- localAssets.add(m[1].replaceAll(/['"]/g, "").trim());
1935
- }
1936
- if (/_creator\.createGlb/.test(code))
1937
- localAssets.add("uberarchive");
1938
- if (/_creator\.captureImage/.test(code))
1939
- localAssets.add("capture");
1940
- if (localAssets.size > 0)
1941
- header += `// preload: ${[...localAssets].join(";")}
1942
- `;
1943
- if (!code.includes("_creator.createScene") && !code.includes("_creator.createEntity"))
1944
- header += `// gl: disable
1945
- `;
1946
- else if (code.includes("_creator.createEngine"))
1947
- header += `// gl: defer
1948
- `;
1949
- if (code.includes("_creator2d."))
1950
- header += `// 2d: enable
1951
- `;
1952
- if (code.includes("_creator.createARController"))
1953
- header += "// ar: enable";
1954
- header += `
2006
+ // your per-frame logic — dt is seconds since the previous frame
2007
+ })
1955
2008
  `;
1956
- return header;
2009
+ var create = async (args) => {
2010
+ const config = loadConfig();
2011
+ const apiUrl = requireApiUrl(config);
2012
+ const token = requireToken(config);
2013
+ const name = args._[0];
2014
+ if (!name)
2015
+ throw new CliError("Usage: lecodes create <name> [directory]");
2016
+ const dir = args._[1] ?? slug2(name);
2017
+ const root = resolve2(dir);
2018
+ if (existsSync6(root) && readdirSync2(root).length > 0) {
2019
+ throw new CliError(`Directory "${dir}" already exists and is not empty.`);
2020
+ }
2021
+ mkdirSync6(root, { recursive: true });
2022
+ info(c.dim(`Creating project ${c.bold(name)}…`));
2023
+ const { uuid } = await createProject(apiUrl, token, name, flagStr(args, "team"));
2024
+ info(c.dim("Fetching type definitions…"));
2025
+ await materializeTypes(root, apiUrl);
2026
+ writeTsconfig(root);
2027
+ writeDefaultIgnore(root);
2028
+ writeFileSync6(join7(root, "main.ts"), STARTER);
2029
+ const commits = await getCommits(apiUrl, token, uuid);
2030
+ writeManifest(root, { uuid, apiUrl, name, baseHeadId: commits.headId, files: {} });
2031
+ success(`Created ${c.bold(name)} in ${c.bold(dir)}/`);
2032
+ info(c.dim(` cd ${dir} — edit main.ts, then \`lecodes push -m "first commit"\` to save it`));
1957
2033
  };
1958
2034
 
1959
- // ../sdk/src/compile/sourcemap.ts
1960
- var offsetSourceMap = (mapJson, lines) => {
1961
- if (lines <= 0)
1962
- return mapJson;
1963
- const map = JSON.parse(mapJson);
1964
- map.mappings = ";".repeat(lines) + map.mappings;
1965
- return JSON.stringify(map);
2035
+ // src/commands/design.ts
2036
+ import { existsSync as existsSync11, mkdirSync as mkdirSync8, readFileSync as readFileSync10, readdirSync as readdirSync6, writeFileSync as writeFileSync8, appendFileSync } from "node:fs";
2037
+ import { basename as basename2, join as join13 } from "node:path";
2038
+
2039
+ // src/compile/designCompile.ts
2040
+ import { readFileSync as readFileSync9 } from "node:fs";
2041
+ import { basename } from "node:path";
2042
+
2043
+ // src/localFiles.ts
2044
+ import { readdirSync as readdirSync3, readFileSync as readFileSync5 } from "node:fs";
2045
+ import { join as join8, relative } from "node:path";
2046
+ var SKIP_DIRS = new Set([".lecodes", ".git", "node_modules"]);
2047
+ var SKIP_FILES = new Set(["tsconfig.json", "jsconfig.json", ".gitignore", ".lecodesignore", ".DS_Store"]);
2048
+ var TEXT_EXTS = new Set([
2049
+ "ts",
2050
+ "tsx",
2051
+ "js",
2052
+ "jsx",
2053
+ "mjs",
2054
+ "cjs",
2055
+ "json",
2056
+ "svg",
2057
+ "glsl",
2058
+ "vert",
2059
+ "frag",
2060
+ "txt",
2061
+ "md",
2062
+ "css",
2063
+ "html",
2064
+ "xml",
2065
+ "yml",
2066
+ "yaml",
2067
+ "csv"
2068
+ ]);
2069
+ var ext = (name) => {
2070
+ const i = name.lastIndexOf(".");
2071
+ return i < 0 ? "" : name.slice(i + 1).toLowerCase();
1966
2072
  };
1967
- var inlineSourceMapComment = (mapJson) => {
1968
- const b64 = Buffer.from(mapJson, "utf8").toString("base64");
1969
- return `//# lecodesMap=data:application/json;charset=utf-8;base64,${b64}`;
2073
+ var classifyKind = (name) => {
2074
+ const e = ext(name);
2075
+ if (e === "mat")
2076
+ return "shader";
2077
+ if (e === "scene")
2078
+ return "scene";
2079
+ return TEXT_EXTS.has(e) ? "text" : "resource";
1970
2080
  };
1971
-
1972
- // ../sdk/src/compile/compileProject.ts
1973
- var detectEntry = (entries) => {
1974
- let entry = "";
1975
- let max = -1;
1976
- for (const e of entries) {
1977
- const ext2 = e.path.split(".").at(-1)?.toLowerCase();
1978
- if (e.type === "text" && e.text !== undefined && !e.text.includes("export") && (ext2 === "ts" || ext2 === "js")) {
1979
- if (e.text.length > max) {
1980
- entry = e.path;
1981
- max = e.text.length;
2081
+ var isTextKind = (kind) => kind !== "resource";
2082
+ var scanLocal = (root, opts = {}) => {
2083
+ const { ignore, tracked } = opts;
2084
+ const files = [];
2085
+ const dirs = [];
2086
+ const ignored = [];
2087
+ const isTracked = (key) => tracked?.has(key) ?? false;
2088
+ const hasTrackedUnder = (key) => {
2089
+ if (!tracked)
2090
+ return false;
2091
+ const prefix = key + "/";
2092
+ for (const t of tracked)
2093
+ if (t.startsWith(prefix))
2094
+ return true;
2095
+ return false;
2096
+ };
2097
+ const walk = (dir, parentIgnored) => {
2098
+ for (const entry of readdirSync3(dir, { withFileTypes: true })) {
2099
+ const abs = join8(dir, entry.name);
2100
+ const key = toKey(relative(root, abs));
2101
+ if (entry.isDirectory()) {
2102
+ if (SKIP_DIRS.has(entry.name))
2103
+ continue;
2104
+ const dirIgnored = parentIgnored || !!ignore && ignore(key, true);
2105
+ if (dirIgnored && !isTracked(key) && !hasTrackedUnder(key)) {
2106
+ ignored.push(key + "/");
2107
+ continue;
2108
+ }
2109
+ dirs.push(key);
2110
+ walk(abs, dirIgnored);
2111
+ } else if (entry.isFile()) {
2112
+ if (SKIP_FILES.has(entry.name))
2113
+ continue;
2114
+ const fileIgnored = parentIgnored || !!ignore && ignore(key, false);
2115
+ if (fileIgnored && !isTracked(key)) {
2116
+ ignored.push(key);
2117
+ continue;
2118
+ }
2119
+ files.push({ path: key, absPath: abs, sha: sha256(readFileSync5(abs)) });
1982
2120
  }
1983
2121
  }
1984
- }
1985
- return entry;
2122
+ };
2123
+ walk(root, false);
2124
+ return { files, dirs, ignored };
1986
2125
  };
1987
- var compileProject = async (opts) => {
1988
- const entry = opts.entryOverride ?? detectEntry(opts.entries);
1989
- if (!entry) {
1990
- throw new Error("Couldn't find an entrypoint (a top-level .ts/.js file with no `export`). Pass entryOverride.");
2126
+ var scanProject = (root, manifest) => scanLocal(root, { ignore: loadIgnore(root), tracked: new Set(Object.keys(manifest.files)) });
2127
+ var diffLocal = (manifest, scan) => {
2128
+ const added = [];
2129
+ const modified = [];
2130
+ const localByPath = new Map(scan.files.map((f) => [f.path, f]));
2131
+ for (const file of scan.files) {
2132
+ const known = manifest.files[file.path];
2133
+ if (!known)
2134
+ added.push(file);
2135
+ else if (known.sha !== file.sha)
2136
+ modified.push(file);
1991
2137
  }
1992
- const files = {};
1993
- for (const e of opts.entries) {
1994
- if (e.type === "resource") {
1995
- if (opts.localAssets) {
1996
- const name = e.path.split(/[\\/]/).pop() ?? e.path;
1997
- files[e.path] = `export default ("id:" + _creatorUtils.fetchLocal(${JSON.stringify(name)}))`;
1998
- } else {
1999
- files[e.path] = `export default "${opts.publicUrl}${e.fileSrc ?? ""}"`;
2000
- }
2001
- } else if (e.type === "shader") {
2002
- const shaders = e.shaders ?? [];
2003
- if (shaders.length > 2) {
2004
- files[e.path] = `export default \`${opts.publicUrl}${shaders[0].src.replace("_opengl", "_${_creator.subBackend ?? _creator.backend}")}\``;
2005
- } else if (shaders.length > 0) {
2006
- files[e.path] = `export default \`${opts.publicUrl}${shaders[0].src.replace("_opengl", "_${_creator.backend}")}\``;
2007
- } else {
2008
- files[e.path] = `export default ""`;
2009
- }
2010
- } else if (e.path.toLowerCase().endsWith(".svg")) {
2011
- files[e.path] = `export default SvgSource(\`${e.text ?? ""}\`)`;
2012
- } else {
2013
- files[e.path] = e.text ?? "";
2138
+ const deleted = Object.keys(manifest.files).filter((path) => !localByPath.has(path));
2139
+ const moves = new Map;
2140
+ const claimed = new Set;
2141
+ for (const file of added) {
2142
+ const from = deleted.find((path) => !claimed.has(path) && manifest.files[path].sha === file.sha);
2143
+ if (from) {
2144
+ moves.set(file.path, from);
2145
+ claimed.add(from);
2014
2146
  }
2015
2147
  }
2016
- const { code, map } = await bundleProjectWithMap(entry, files, opts.format ?? "esm");
2017
- if (opts.header === false) {
2018
- return map ? `${code}
2019
- ${inlineSourceMapComment(map)}` : code;
2020
- }
2021
- const header = buildHeader(opts.name, code, { icon: opts.icon });
2022
- if (!map)
2023
- return header + code;
2024
- const headerLines = (header.match(/\n/g) ?? []).length;
2025
- return `${header}${code}
2026
- ${inlineSourceMapComment(offsetSourceMap(map, headerLines))}`;
2148
+ return { added, modified, deleted, moves };
2027
2149
  };
2150
+ var hasChanges = (diff) => diff.added.length + diff.modified.length + diff.deleted.length > 0;
2151
+
2028
2152
  // src/compile/projectCompile.ts
2029
- var vendoredInject = join10(distRoot, "runtime", "sdk", "inject.ts");
2030
- if (existsSync8(vendoredInject))
2153
+ import { existsSync as existsSync9 } from "node:fs";
2154
+ import { createRequire as createRequire3 } from "node:module";
2155
+ import { dirname as dirname6, join as join11 } from "node:path";
2156
+ init_compile();
2157
+ var vendoredInject = join11(distRoot, "runtime", "sdk", "inject.ts");
2158
+ if (existsSync9(vendoredInject))
2031
2159
  process.env.LECODES_SDK_INJECT = vendoredInject;
2032
2160
  var resolveChiselBin2 = () => {
2033
2161
  if (process.env.LECODES_CHISEL_BIN)
@@ -2037,8 +2165,8 @@ var resolveChiselBin2 = () => {
2037
2165
  if (fromPkg)
2038
2166
  return fromPkg;
2039
2167
  } catch {}
2040
- const local = join10(dirname6(process.execPath), process.platform === "win32" ? "chisel.exe" : "chisel");
2041
- return existsSync8(local) ? local : null;
2168
+ const local = join11(dirname6(process.execPath), process.platform === "win32" ? "chisel.exe" : "chisel");
2169
+ return existsSync9(local) ? local : null;
2042
2170
  };
2043
2171
  var chiselBin = resolveChiselBin2();
2044
2172
  if (chiselBin)
@@ -2050,13 +2178,15 @@ var compileProject2 = (opts) => compileProject({
2050
2178
  entryOverride: opts.entryOverride,
2051
2179
  header: opts.header,
2052
2180
  format: opts.format,
2053
- localAssets: opts.localAssets
2181
+ localAssets: opts.localAssets,
2182
+ minify: opts.minify,
2183
+ editor: opts.editor
2054
2184
  });
2055
2185
 
2056
2186
  // src/compile/assetIcons.ts
2057
2187
  import { createHash as createHash2 } from "node:crypto";
2058
- import { existsSync as existsSync9, mkdirSync as mkdirSync7, readFileSync as readFileSync7, writeFileSync as writeFileSync7 } from "node:fs";
2059
- import { dirname as dirname7, join as join11 } from "node:path";
2188
+ import { existsSync as existsSync10, mkdirSync as mkdirSync7, readFileSync as readFileSync8, writeFileSync as writeFileSync7 } from "node:fs";
2189
+ import { dirname as dirname7, join as join12 } from "node:path";
2060
2190
  var DEFAULT_REGISTRY = process.env.LECODES_ICON_REGISTRY?.trim() || "https://icon-registry.jt3.ru/v1";
2061
2191
  var ICON_ID = /^([a-z0-9]+(?:-[a-z0-9]+)*):([a-z0-9]+(?:-[a-z0-9]+)*)$/;
2062
2192
  var isIdentStart = (c2) => c2 >= "A" && c2 <= "Z" || c2 >= "a" && c2 <= "z" || c2 === "_" || c2 === "$";
@@ -2308,14 +2438,14 @@ var readText = async (registry, rel) => {
2308
2438
  return res.ok ? await res.text() : null;
2309
2439
  }
2310
2440
  try {
2311
- return readFileSync7(join11(registry, rel), "utf8");
2441
+ return readFileSync8(join12(registry, rel), "utf8");
2312
2442
  } catch {
2313
2443
  return null;
2314
2444
  }
2315
2445
  };
2316
2446
  var readLock = (path) => {
2317
2447
  try {
2318
- const lock = JSON.parse(readFileSync7(path, "utf8"));
2448
+ const lock = JSON.parse(readFileSync8(path, "utf8"));
2319
2449
  if (lock && typeof lock === "object" && lock.icons && lock.packs)
2320
2450
  return lock;
2321
2451
  } catch {}
@@ -2352,7 +2482,7 @@ var renderNotices = (lock) => {
2352
2482
  `);
2353
2483
  };
2354
2484
  var writeIfChanged = (path, text) => {
2355
- const current = existsSync9(path) ? safeRead(path) : null;
2485
+ const current = existsSync10(path) ? safeRead(path) : null;
2356
2486
  if (current === text)
2357
2487
  return false;
2358
2488
  writeFileSync7(path, text);
@@ -2360,20 +2490,20 @@ var writeIfChanged = (path, text) => {
2360
2490
  };
2361
2491
  var safeRead = (path) => {
2362
2492
  try {
2363
- return readFileSync7(path, "utf8");
2493
+ return readFileSync8(path, "utf8");
2364
2494
  } catch {
2365
2495
  return null;
2366
2496
  }
2367
2497
  };
2368
2498
  var vendorIcons = async (designDir, ids, opts = {}) => {
2369
2499
  const registry = (opts.registry ?? DEFAULT_REGISTRY).replace(/\/+$/, "");
2370
- const lockPath = join11(designDir, "icons.lock.json");
2500
+ const lockPath = join12(designDir, "icons.lock.json");
2371
2501
  const lock = readLock(lockPath) ?? { registry, icons: {}, packs: {} };
2372
2502
  const svgById = new Map;
2373
2503
  const missing = [];
2374
2504
  for (const id of ids) {
2375
2505
  const li = lock.icons[id];
2376
- const cached = li && safeRead(join11(designDir, li.path));
2506
+ const cached = li && safeRead(join12(designDir, li.path));
2377
2507
  if (cached)
2378
2508
  svgById.set(id, cached);
2379
2509
  else
@@ -2394,7 +2524,7 @@ var vendorIcons = async (designDir, ids, opts = {}) => {
2394
2524
  if (svg === null)
2395
2525
  throw new Error(`assetIcon: icon "${id}" not found in the registry. Check the name at ${registryHome(registry)}.`);
2396
2526
  const rel = `assets/icons/${pack}/${name}.svg`;
2397
- const abs = join11(designDir, rel);
2527
+ const abs = join12(designDir, rel);
2398
2528
  mkdirSync7(dirname7(abs), { recursive: true });
2399
2529
  writeFileSync7(abs, svg);
2400
2530
  lock.icons[id] = { pack, name, hash: sha12(svg), spdx: lic.spdx, path: rel };
@@ -2405,7 +2535,7 @@ var vendorIcons = async (designDir, ids, opts = {}) => {
2405
2535
  writeIfChanged(lockPath, serializeLock(lock));
2406
2536
  }
2407
2537
  if (Object.keys(lock.icons).length)
2408
- writeIfChanged(join11(designDir, "ICON-NOTICES.md"), renderNotices(lock));
2538
+ writeIfChanged(join12(designDir, "ICON-NOTICES.md"), renderNotices(lock));
2409
2539
  return svgById;
2410
2540
  };
2411
2541
  var applyAssetIcons = async (entries, designDir, opts = {}) => {
@@ -2440,7 +2570,7 @@ var compileDesignScreen = async (opts) => {
2440
2570
  } else if (type === "shader") {
2441
2571
  entries.push({ path: file.path, type, shaders: [] });
2442
2572
  } else {
2443
- entries.push({ path: file.path, type, text: readFileSync8(file.absPath, "utf8") });
2573
+ entries.push({ path: file.path, type, text: readFileSync9(file.absPath, "utf8") });
2444
2574
  }
2445
2575
  }
2446
2576
  const invoke = state !== undefined ? `screen(${JSON.stringify(state)})` : "screen()";
@@ -2849,31 +2979,31 @@ var getContext = (args) => {
2849
2979
  manifest,
2850
2980
  name: manifest?.name ?? basename2(root),
2851
2981
  dirName,
2852
- designDir: join12(root, dirName),
2982
+ designDir: join13(root, dirName),
2853
2983
  designKey: "/" + dirName.replace(/\\/g, "/")
2854
2984
  };
2855
2985
  };
2856
2986
  var listScreens = (designDir) => {
2857
- const dir = join12(designDir, "screens");
2858
- if (!existsSync10(dir))
2987
+ const dir = join13(designDir, "screens");
2988
+ if (!existsSync11(dir))
2859
2989
  return [];
2860
- return readdirSync5(dir).filter((f) => f.endsWith(".ts") && !f.startsWith(".")).map((f) => f.slice(0, -3)).sort();
2990
+ return readdirSync6(dir).filter((f) => f.endsWith(".ts") && !f.startsWith(".")).map((f) => f.slice(0, -3)).sort();
2861
2991
  };
2862
2992
  var readMetaSizes = (designDir) => {
2863
2993
  try {
2864
- const meta = JSON.parse(readFileSync9(join12(designDir, "meta.json"), "utf8"));
2994
+ const meta = JSON.parse(readFileSync10(join13(designDir, "meta.json"), "utf8"));
2865
2995
  return meta?.screens ?? {};
2866
2996
  } catch {
2867
2997
  return {};
2868
2998
  }
2869
2999
  };
2870
3000
  var writeMcpConfig = (ctx, port) => {
2871
- const path = join12(ctx.root, ".mcp.json");
3001
+ const path = join13(ctx.root, ".mcp.json");
2872
3002
  const entry = { type: "http", url: `http://localhost:${port}/mcp` };
2873
3003
  let config = {};
2874
- if (existsSync10(path)) {
3004
+ if (existsSync11(path)) {
2875
3005
  try {
2876
- config = JSON.parse(readFileSync9(path, "utf8"));
3006
+ config = JSON.parse(readFileSync10(path, "utf8"));
2877
3007
  } catch {
2878
3008
  warnErr(".mcp.json exists but isn't valid JSON — left it untouched. Add the lecodes-design server by hand.");
2879
3009
  return;
@@ -2896,15 +3026,15 @@ var init = async (ctx, args) => {
2896
3026
  ["spec.md", SPEC_MD],
2897
3027
  ["README.md", README_HINT],
2898
3028
  ["CLAUDE.md", CLAUDE_MD],
2899
- [join12("shared", "tokens.ts"), TOKENS_TS],
2900
- [join12("screens", "home.ts"), HOME_SCREEN_TS]
3029
+ [join13("shared", "tokens.ts"), TOKENS_TS],
3030
+ [join13("screens", "home.ts"), HOME_SCREEN_TS]
2901
3031
  ];
2902
- mkdirSync8(join12(ctx.designDir, "screens"), { recursive: true });
2903
- mkdirSync8(join12(ctx.designDir, "shared"), { recursive: true });
3032
+ mkdirSync8(join13(ctx.designDir, "screens"), { recursive: true });
3033
+ mkdirSync8(join13(ctx.designDir, "shared"), { recursive: true });
2904
3034
  let created = 0;
2905
3035
  for (const [rel, content] of files) {
2906
- const abs = join12(ctx.designDir, rel);
2907
- if (existsSync10(abs)) {
3036
+ const abs = join13(ctx.designDir, rel);
3037
+ if (existsSync11(abs)) {
2908
3038
  note(`kept ${ctx.dirName}/${rel.replace(/\\/g, "/")}`);
2909
3039
  continue;
2910
3040
  }
@@ -2912,9 +3042,9 @@ var init = async (ctx, args) => {
2912
3042
  log(`${c.green("created")} ${ctx.dirName}/${rel.replace(/\\/g, "/")}`);
2913
3043
  created++;
2914
3044
  }
2915
- const ignorePath = join12(ctx.root, ".lecodesignore");
3045
+ const ignorePath = join13(ctx.root, ".lecodesignore");
2916
3046
  const ignoreLine = `${ctx.dirName}/.snapshots/`;
2917
- const current = existsSync10(ignorePath) ? readFileSync9(ignorePath, "utf8") : "";
3047
+ const current = existsSync11(ignorePath) ? readFileSync10(ignorePath, "utf8") : "";
2918
3048
  if (!current.split(/\r?\n/).includes(ignoreLine)) {
2919
3049
  appendFileSync(ignorePath, (current && !current.endsWith(`
2920
3050
  `) ? `
@@ -2925,8 +3055,8 @@ var init = async (ctx, args) => {
2925
3055
  writeMcpConfig(ctx, Number(flagStr(args, "port")) || DEFAULT_PORT);
2926
3056
  const wroteTypes = materializeTypesLocal(ctx.designDir);
2927
3057
  writeTsconfig(ctx.designDir);
2928
- mkdirSync8(join12(ctx.designDir, ".lecodes", "types"), { recursive: true });
2929
- writeFileSync8(join12(ctx.designDir, ".lecodes", "types", "design-macros.d.ts"), DESIGN_MACROS_DTS);
3058
+ mkdirSync8(join13(ctx.designDir, ".lecodes", "types"), { recursive: true });
3059
+ writeFileSync8(join13(ctx.designDir, ".lecodes", "types", "design-macros.d.ts"), DESIGN_MACROS_DTS);
2930
3060
  log(`${c.green("created")} ${ctx.dirName}/tsconfig.json${wroteTypes ? ` + ${ctx.dirName}/.lecodes/types/` : ""} (IDE types)`);
2931
3061
  if (!wroteTypes)
2932
3062
  warnErr("Bundled SDK types not found — tsconfig written, but globals may show unresolved until the CLI is rebuilt with vendored types.");
@@ -2975,7 +3105,7 @@ var snapshot = async (ctx, args) => {
2975
3105
  killer.unref?.();
2976
3106
  const png = flagBool(args, "png") || flagStr(args, "png") !== undefined;
2977
3107
  const sizes = readMetaSizes(ctx.designDir);
2978
- const outDir = flagStr(args, "out-dir") ?? join12(ctx.designDir, ".snapshots");
3108
+ const outDir = flagStr(args, "out-dir") ?? join13(ctx.designDir, ".snapshots");
2979
3109
  const single = targets.length === 1 && requested.length === 1 && !png && !allStates && !flagStr(args, "out-dir");
2980
3110
  const onConsole = flagBool(args, "logs") ? (level, text) => logErr(`${c.dim(`[${level}]`)} ${text}`) : undefined;
2981
3111
  if (!single)
@@ -2984,7 +3114,7 @@ var snapshot = async (ctx, args) => {
2984
3114
  const [width, height] = sizes[id]?.size ?? [390, 844];
2985
3115
  let plan = [{ state: undefined, bases: [id] }];
2986
3116
  if (discover) {
2987
- const { states, canonical } = discover(readFileSync9(join12(ctx.designDir, "screens", `${id}.ts`), "utf8"));
3117
+ const { states, canonical } = discover(readFileSync10(join13(ctx.designDir, "screens", `${id}.ts`), "utf8"));
2988
3118
  if (stateFlag !== undefined) {
2989
3119
  if (!states.includes(stateFlag)) {
2990
3120
  if (requested.length === 1)
@@ -3025,18 +3155,18 @@ var snapshot = async (ctx, args) => {
3025
3155
  for (const w of pngResult.warnings)
3026
3156
  warnErr(`${tag}: ${w}`);
3027
3157
  for (const base of bases) {
3028
- writeFileSync8(join12(outDir, `${base}.json`), result.text);
3029
- log(`${c.green("✓")} ${tag} → ${join12(outDir, `${base}.json`)}`);
3158
+ writeFileSync8(join13(outDir, `${base}.json`), result.text);
3159
+ log(`${c.green("✓")} ${tag} → ${join13(outDir, `${base}.json`)}`);
3030
3160
  if (pngResult) {
3031
- writeFileSync8(join12(outDir, `${base}.png`), pngResult.png);
3032
- log(`${c.green("✓")} ${tag} → ${join12(outDir, `${base}.png`)}`);
3161
+ writeFileSync8(join13(outDir, `${base}.png`), pngResult.png);
3162
+ log(`${c.green("✓")} ${tag} → ${join13(outDir, `${base}.png`)}`);
3033
3163
  }
3034
3164
  }
3035
3165
  }
3036
3166
  }
3037
3167
  };
3038
3168
  var serve = async (ctx, args) => {
3039
- if (!existsSync10(ctx.designDir)) {
3169
+ if (!existsSync11(ctx.designDir)) {
3040
3170
  throw new CliError(`No ${ctx.dirName}/ folder here. Run "lecodes design init" to scaffold it.`);
3041
3171
  }
3042
3172
  let designServer;
@@ -3109,7 +3239,7 @@ var serve = async (ctx, args) => {
3109
3239
  openBrowser(server.url);
3110
3240
  };
3111
3241
  var arrange = async (ctx, args) => {
3112
- if (!existsSync10(ctx.designDir)) {
3242
+ if (!existsSync11(ctx.designDir)) {
3113
3243
  throw new CliError(`No ${ctx.dirName}/ folder here. Run "lecodes design init" to scaffold it.`);
3114
3244
  }
3115
3245
  let designServer;
@@ -3145,7 +3275,7 @@ var share = async (ctx, args) => {
3145
3275
  note("Turn it off with: lecodes design share --off");
3146
3276
  };
3147
3277
  var check = async (ctx, args) => {
3148
- if (!existsSync10(ctx.designDir)) {
3278
+ if (!existsSync11(ctx.designDir)) {
3149
3279
  throw new CliError(`No ${ctx.dirName}/ folder here. Run "lecodes design init" to scaffold it.`);
3150
3280
  }
3151
3281
  let designServer;
@@ -3210,18 +3340,18 @@ var design = async (args) => {
3210
3340
  };
3211
3341
 
3212
3342
  // src/commands/dev.ts
3213
- import { existsSync as existsSync12, mkdirSync as mkdirSync9, readFileSync as readFileSync11, watch, writeFileSync as writeFileSync9 } from "node:fs";
3343
+ import { existsSync as existsSync13, mkdirSync as mkdirSync9, readFileSync as readFileSync12, watch, writeFileSync as writeFileSync9 } from "node:fs";
3214
3344
  import { randomBytes as randomBytes2 } from "node:crypto";
3215
3345
  import { networkInterfaces } from "node:os";
3216
- import { basename as basename4, join as join14 } from "node:path";
3346
+ import { basename as basename4, join as join15 } from "node:path";
3217
3347
 
3218
3348
  // src/dev/devServer.ts
3219
3349
  import { createServer as createServer2 } from "node:http";
3220
- import { createReadStream, existsSync as existsSync11, statSync as statSync2 } from "node:fs";
3221
- import { join as join13 } from "node:path";
3350
+ import { createReadStream, existsSync as existsSync12, statSync as statSync2 } from "node:fs";
3351
+ import { join as join14 } from "node:path";
3222
3352
 
3223
3353
  // src/compile/collectLocal.ts
3224
- import { readFileSync as readFileSync10 } from "node:fs";
3354
+ import { readFileSync as readFileSync11 } from "node:fs";
3225
3355
  import { basename as basename3 } from "node:path";
3226
3356
  var collectLocalEntries = (root, manifest, assetBase) => {
3227
3357
  const entries = [];
@@ -3235,7 +3365,7 @@ var collectLocalEntries = (root, manifest, assetBase) => {
3235
3365
  warnings.push(`${file.path}: shaders compile on the server — push the project once for shaders to work in dev mode.`);
3236
3366
  entries.push({ path: file.path, type, shaders: [] });
3237
3367
  } else {
3238
- entries.push({ path: file.path, type, text: readFileSync10(file.absPath, "utf8") });
3368
+ entries.push({ path: file.path, type, text: readFileSync11(file.absPath, "utf8") });
3239
3369
  }
3240
3370
  }
3241
3371
  return { entries, warnings };
@@ -3595,8 +3725,8 @@ var startDevServer = async (options) => {
3595
3725
  } catch {
3596
3726
  return sendText(res, 400, "text/plain", "Bad path");
3597
3727
  }
3598
- const abs = join13(root, key.slice(1));
3599
- if (!existsSync11(abs) || !statSync2(abs).isFile())
3728
+ const abs = join14(root, key.slice(1));
3729
+ if (!existsSync12(abs) || !statSync2(abs).isFile())
3600
3730
  return sendText(res, 404, "text/plain", "Not found");
3601
3731
  res.writeHead(200, { "content-type": contentType(key), "cache-control": "no-store" });
3602
3732
  createReadStream(abs).pipe(res);
@@ -3679,14 +3809,14 @@ var pickLanAddress = () => {
3679
3809
  return null;
3680
3810
  };
3681
3811
  var loadDevToken = (root) => {
3682
- const path = join14(root, LECODES_DIR, "dev.json");
3812
+ const path = join15(root, LECODES_DIR, "dev.json");
3683
3813
  try {
3684
- const token2 = JSON.parse(readFileSync11(path, "utf8")).token;
3814
+ const token2 = JSON.parse(readFileSync12(path, "utf8")).token;
3685
3815
  if (token2 && /^[a-z0-9]{8,}$/.test(token2))
3686
3816
  return token2;
3687
3817
  } catch {}
3688
3818
  const token = randomBytes2(6).toString("hex");
3689
- mkdirSync9(join14(root, LECODES_DIR), { recursive: true });
3819
+ mkdirSync9(join15(root, LECODES_DIR), { recursive: true });
3690
3820
  writeFileSync9(path, JSON.stringify({ token }, null, 2) + `
3691
3821
  `);
3692
3822
  return token;
@@ -3799,14 +3929,14 @@ ${result.error}`);
3799
3929
  await printQr(server.url);
3800
3930
  log(`Scan the QR with the LeCodes app, or open: ${c.bold(server.url)}`);
3801
3931
  note(`Older desktop builds (no websocket bridge): ${server.url}?t=poll`);
3802
- if (existsSync12(join14(root, LECODES_DIR, "manifest.json")))
3932
+ if (existsSync13(join15(root, LECODES_DIR, "manifest.json")))
3803
3933
  note(`Token stored in ${LECODES_DIR}/dev.json (delete it to rotate the URL).`);
3804
3934
  log("");
3805
3935
  log(`Watching ${root} — save a file to hot-reload. Ctrl+C to stop.`);
3806
3936
  };
3807
3937
 
3808
3938
  // src/serverDiff.ts
3809
- import { readFileSync as readFileSync12 } from "node:fs";
3939
+ import { readFileSync as readFileSync13 } from "node:fs";
3810
3940
  import { basename as basename5 } from "node:path";
3811
3941
  var isTextAsset = (t) => t === "text" || t === "shader" || t === "scene";
3812
3942
  var computeServerDiff = (project, scan) => {
@@ -3825,7 +3955,7 @@ var computeServerDiff = (project, scan) => {
3825
3955
  const local = localByPath.get(path);
3826
3956
  if (local && !server) {
3827
3957
  const text = isTextKind(classifyKind(basename5(path)));
3828
- entries.push({ path, status: "added", binary: !text, serverText: "", localText: text ? readFileSync12(local.absPath, "utf8") : undefined });
3958
+ entries.push({ path, status: "added", binary: !text, serverText: "", localText: text ? readFileSync13(local.absPath, "utf8") : undefined });
3829
3959
  continue;
3830
3960
  }
3831
3961
  if (server && !local) {
@@ -3836,12 +3966,12 @@ var computeServerDiff = (project, scan) => {
3836
3966
  if (server && local) {
3837
3967
  if (isTextAsset(server.type)) {
3838
3968
  const serverText = server.text ?? "";
3839
- const localText = readFileSync12(local.absPath, "utf8");
3969
+ const localText = readFileSync13(local.absPath, "utf8");
3840
3970
  if (serverText !== localText)
3841
3971
  entries.push({ path, status: "modified", binary: false, serverText, localText });
3842
3972
  } else {
3843
3973
  const serverSize = server.file?.size;
3844
- const localSize = Buffer.byteLength(readFileSync12(local.absPath));
3974
+ const localSize = Buffer.byteLength(readFileSync13(local.absPath));
3845
3975
  if (serverSize !== undefined && serverSize !== localSize) {
3846
3976
  entries.push({ path, status: "modified", binary: true });
3847
3977
  }
@@ -3994,8 +4124,8 @@ var diff = async (args) => {
3994
4124
  var statusLabel = (e) => e.status === "added" ? c.green(" added ") : e.status === "removed" ? c.red(" removed ") : c.yellow(" modified ");
3995
4125
 
3996
4126
  // src/commands/init.ts
3997
- import { existsSync as existsSync13, mkdirSync as mkdirSync10, writeFileSync as writeFileSync10 } from "node:fs";
3998
- import { basename as basename6, dirname as dirname8, join as join15, resolve as resolve3 } from "node:path";
4127
+ import { existsSync as existsSync14, mkdirSync as mkdirSync10, writeFileSync as writeFileSync10 } from "node:fs";
4128
+ import { basename as basename6, dirname as dirname8, join as join16, resolve as resolve3 } from "node:path";
3999
4129
  var STARTER2 = `// Welcome to LeCodes — the whole SDK is available as globals, no imports needed.
4000
4130
  // Run it live on a device with \`lecodes dev\`, or bundle it with \`lecodes preview -o app.js\`.
4001
4131
 
@@ -4038,8 +4168,8 @@ var init2 = async (args) => {
4038
4168
  mkdirSync10(root, { recursive: true });
4039
4169
  let created = 0;
4040
4170
  const write = (rel, content) => {
4041
- const abs = join15(root, rel);
4042
- if (existsSync13(abs)) {
4171
+ const abs = join16(root, rel);
4172
+ if (existsSync14(abs)) {
4043
4173
  note(`kept ${rel}`);
4044
4174
  return;
4045
4175
  }
@@ -4079,7 +4209,7 @@ import { writeFileSync as writeFileSync11 } from "node:fs";
4079
4209
  import { basename as basename8, resolve as resolve4 } from "node:path";
4080
4210
 
4081
4211
  // src/compile/collect.ts
4082
- import { readFileSync as readFileSync13 } from "node:fs";
4212
+ import { readFileSync as readFileSync14 } from "node:fs";
4083
4213
  import { basename as basename7 } from "node:path";
4084
4214
  var collectEntries = (root, manifest, project) => {
4085
4215
  const pathById = buildPaths(project.assets);
@@ -4104,7 +4234,7 @@ var collectEntries = (root, manifest, project) => {
4104
4234
  warnings.push(`No compiled shader for ${file.path} — push it to compile the shader.`);
4105
4235
  entries.push({ path: file.path, type, shaders });
4106
4236
  } else {
4107
- entries.push({ path: file.path, type, text: readFileSync13(file.absPath, "utf8") });
4237
+ entries.push({ path: file.path, type, text: readFileSync14(file.absPath, "utf8") });
4108
4238
  }
4109
4239
  }
4110
4240
  return { entries, warnings };
@@ -4223,7 +4353,7 @@ var render = async (args) => {
4223
4353
  format: "iife"
4224
4354
  });
4225
4355
  if (js.includes("_creator.createScene") || js.includes("_creator.createEntity")) {
4226
- warnings.push("This looks like a 3D/GL project — only the UI layer is rendered.");
4356
+ warnings.push("3D project: the scene graph is captured as `scene3d` in the JSON output; 3D isn't rasterized into PNGs.");
4227
4357
  }
4228
4358
  if (js.includes("_creator2d") && !useLocalAssets && !flagBool(args, "wait-network")) {
4229
4359
  warnings.push("2D scene detected: sprite textures load over the network — add --wait-network to include them (else sprites render as flat placeholders).");
@@ -4292,8 +4422,8 @@ var render = async (args) => {
4292
4422
  };
4293
4423
 
4294
4424
  // src/commands/pull.ts
4295
- import { existsSync as existsSync14, mkdirSync as mkdirSync11, rmSync, writeFileSync as writeFileSync13 } from "node:fs";
4296
- import { dirname as dirname9, join as join16 } from "node:path";
4425
+ import { existsSync as existsSync15, mkdirSync as mkdirSync11, rmSync, writeFileSync as writeFileSync13 } from "node:fs";
4426
+ import { dirname as dirname9, join as join17 } from "node:path";
4297
4427
  var pull = async (args) => {
4298
4428
  const root = findProjectRoot(process.cwd());
4299
4429
  const manifest = readManifest(root);
@@ -4313,8 +4443,8 @@ var pull = async (args) => {
4313
4443
  const remotePaths = new Set([...buildPaths(project.assets).entries()].filter(([id]) => project.assets.find((a) => a.id === id)?.type !== "folder").map(([, path]) => path));
4314
4444
  for (const path of Object.keys(manifest.files)) {
4315
4445
  if (!remotePaths.has(path)) {
4316
- const abs = join16(root, ...path.replace(/^\//, "").split("/"));
4317
- if (existsSync14(abs))
4446
+ const abs = join17(root, ...path.replace(/^\//, "").split("/"));
4447
+ if (existsSync15(abs))
4318
4448
  rmSync(abs);
4319
4449
  }
4320
4450
  }
@@ -4352,7 +4482,7 @@ var pullPaths = async (root, manifest, project, paths, apiUrl, token) => {
4352
4482
  const files = { ...manifest.files };
4353
4483
  for (const asset of targets) {
4354
4484
  const key = pathById.get(asset.id);
4355
- const abs = join16(root, ...key.replace(/^\//, "").split("/"));
4485
+ const abs = join17(root, ...key.replace(/^\//, "").split("/"));
4356
4486
  mkdirSync11(dirname9(abs), { recursive: true });
4357
4487
  let sha;
4358
4488
  if (asset.type === "file") {
@@ -4376,7 +4506,7 @@ var pullPaths = async (root, manifest, project, paths, apiUrl, token) => {
4376
4506
 
4377
4507
  // src/commands/push.ts
4378
4508
  import { basename as basename9 } from "node:path";
4379
- import { readFileSync as readFileSync14 } from "node:fs";
4509
+ import { readFileSync as readFileSync15 } from "node:fs";
4380
4510
  var push = async (args) => {
4381
4511
  const root = findProjectRoot(process.cwd());
4382
4512
  const manifest = readManifest(root);
@@ -4435,9 +4565,9 @@ var push = async (args) => {
4435
4565
  if (source)
4436
4566
  entry.assetId = source.id;
4437
4567
  if (isTextKind(kind)) {
4438
- entry.text = readFileSync14(file.absPath, "utf8");
4568
+ entry.text = readFileSync15(file.absPath, "utf8");
4439
4569
  } else if (!source || source.sha !== file.sha) {
4440
- const blob = await uploadBlob(apiUrl, token, manifest.uuid, basename9(file.path), readFileSync14(file.absPath));
4570
+ const blob = await uploadBlob(apiUrl, token, manifest.uuid, basename9(file.path), readFileSync15(file.absPath));
4441
4571
  entry.blobFileId = blob.id;
4442
4572
  uploaded++;
4443
4573
  }
@@ -4489,6 +4619,154 @@ var reportPublish = (res) => {
4489
4619
  warn(`Push landed, but publish failed: ${res.publishError ?? "unknown error"}`);
4490
4620
  };
4491
4621
 
4622
+ // src/commands/scene.ts
4623
+ import { existsSync as existsSync17, readFileSync as readFileSync17, readdirSync as readdirSync7, statSync as statSync3, writeFileSync as writeFileSync14 } from "node:fs";
4624
+ import { basename as basename10, dirname as dirname10, join as join19, resolve as resolve6 } from "node:path";
4625
+
4626
+ // src/compile/sceneCompile.ts
4627
+ import { existsSync as existsSync16, readFileSync as readFileSync16 } from "node:fs";
4628
+ import { join as join18 } from "node:path";
4629
+ var TEXT_EXT = /\.(ts|js|json|svg|txt|md)$/i;
4630
+ var rel = (key) => key.replace(/^\//, "");
4631
+ var resolveHarness = (loadSceneHarness2) => {
4632
+ const inRepo = join18(distRoot, "..", "projects", "src", "scene-editor");
4633
+ try {
4634
+ return loadSceneHarness2(existsSync16(inRepo) ? inRepo : undefined);
4635
+ } catch {
4636
+ const vendored = join18(distRoot, "runtime", "scene-harness.json");
4637
+ if (existsSync16(vendored))
4638
+ return JSON.parse(readFileSync16(vendored, "utf8"));
4639
+ throw new CliError("scene-editor harness sources not found — reinstall the CLI or set LECODES_SCENE_HARNESS_DIR.");
4640
+ }
4641
+ };
4642
+ var compileSceneEditor = async (opts) => {
4643
+ const { root, scenePath } = opts;
4644
+ const { loadSceneHarness: loadSceneHarness2, sceneEditorEntries: sceneEditorEntries2, SCENE_ENTRY_PATH: SCENE_ENTRY_PATH2 } = await Promise.resolve().then(() => (init_compile(), exports_compile));
4645
+ const entries = [];
4646
+ const scan = scanLocal(root, { ignore: loadIgnore(root), tracked: new Set(Object.keys(opts.manifest?.files ?? {})) });
4647
+ for (const file of scan.files) {
4648
+ const path = "./" + rel(file.path);
4649
+ if (TEXT_EXT.test(path)) {
4650
+ entries.push({ path, type: "text", text: readFileSync16(file.absPath, "utf8") });
4651
+ } else {
4652
+ entries.push({ path, type: "resource", fileSrc: "/project/" + rel(file.path) });
4653
+ }
4654
+ }
4655
+ const scene = scenePath.startsWith("./") ? scenePath : "./" + rel(scenePath);
4656
+ if (!entries.some((e) => e.path === scene && e.type === "text")) {
4657
+ return { js: null, error: `Scene file not found: ${scene}` };
4658
+ }
4659
+ entries.push(...sceneEditorEntries2(scene, resolveHarness(loadSceneHarness2), entries.map((e) => e.path)));
4660
+ try {
4661
+ const js = await compileProject2({
4662
+ entries,
4663
+ name: opts.name,
4664
+ publicUrl: "",
4665
+ entryOverride: SCENE_ENTRY_PATH2,
4666
+ header: false,
4667
+ format: "iife",
4668
+ minify: false,
4669
+ editor: true
4670
+ });
4671
+ return { js };
4672
+ } catch (e) {
4673
+ return { js: null, error: e?.message?.replace(/mem:/g, "") ?? String(e) };
4674
+ }
4675
+ };
4676
+
4677
+ // src/commands/scene.ts
4678
+ var DEFAULT_PORT3 = 4488;
4679
+ var getContext2 = (args) => {
4680
+ let root;
4681
+ let manifest;
4682
+ try {
4683
+ root = findProjectRoot(process.cwd());
4684
+ manifest = readManifest(root);
4685
+ } catch {
4686
+ root = process.cwd();
4687
+ manifest = null;
4688
+ const pathArg = args._[0];
4689
+ if (pathArg) {
4690
+ const abs = resolve6(process.cwd(), pathArg);
4691
+ if (existsSync17(abs) && statSync3(abs).isDirectory())
4692
+ root = abs;
4693
+ else if (abs.endsWith(".scene.ts") && existsSync17(abs))
4694
+ root = dirname10(abs);
4695
+ else
4696
+ throw new CliError(`No such folder or .scene.ts: ${pathArg}`);
4697
+ }
4698
+ }
4699
+ return { root, manifest, name: manifest?.name ?? basename10(root) };
4700
+ };
4701
+ var writeMcpConfig2 = (root, port) => {
4702
+ const path = join19(root, ".mcp.json");
4703
+ const entry = { type: "http", url: `http://localhost:${port}/mcp` };
4704
+ let config = {};
4705
+ if (existsSync17(path)) {
4706
+ try {
4707
+ config = JSON.parse(readFileSync17(path, "utf8"));
4708
+ } catch {
4709
+ warnErr(".mcp.json exists but isn't valid JSON — left it untouched. Add the lecodes-scene server by hand.");
4710
+ return;
4711
+ }
4712
+ }
4713
+ config.mcpServers ??= {};
4714
+ const existing = config.mcpServers["lecodes-scene"];
4715
+ if (existing?.url === entry.url) {
4716
+ note("kept .mcp.json (lecodes-scene MCP server)");
4717
+ return;
4718
+ }
4719
+ config.mcpServers["lecodes-scene"] = entry;
4720
+ writeFileSync14(path, JSON.stringify(config, null, 2) + `
4721
+ `);
4722
+ note(`${existing ? "updated" : "created"} .mcp.json (lecodes-scene MCP → ${entry.url})`);
4723
+ };
4724
+ var countScenes = (dir) => {
4725
+ let n = 0;
4726
+ for (const name of readdirSync7(dir)) {
4727
+ if (name === "node_modules" || name.startsWith("."))
4728
+ continue;
4729
+ const full = join19(dir, name);
4730
+ if (statSync3(full).isDirectory())
4731
+ n += countScenes(full);
4732
+ else if (name.endsWith(".scene.ts"))
4733
+ n++;
4734
+ }
4735
+ return n;
4736
+ };
4737
+ var scene = async (args) => {
4738
+ const ctx = getContext2(args);
4739
+ let sceneServer;
4740
+ try {
4741
+ sceneServer = await import("lecodes-3d-editor/server");
4742
+ } catch {
4743
+ throw new CliError("The scene editor isn't installed. Run: npm i -D lecodes-3d-editor");
4744
+ }
4745
+ const port = Number(flagStr(args, "port")) || DEFAULT_PORT3;
4746
+ if (countScenes(ctx.root) === 0) {
4747
+ warnErr(`No .scene.ts file under ${ctx.root} yet — add one and the editor will pick it up.`);
4748
+ }
4749
+ const server = await sceneServer.startSceneServer({
4750
+ root: ctx.root,
4751
+ port,
4752
+ log: note,
4753
+ compile: (scenePath) => compileSceneEditor({
4754
+ root: ctx.root,
4755
+ manifest: ctx.manifest,
4756
+ name: ctx.name,
4757
+ scenePath
4758
+ })
4759
+ });
4760
+ if (!flagBool(args, "no-mcp"))
4761
+ writeMcpConfig2(ctx.root, port);
4762
+ log(`${c.bold("LeCodes Scene Editor")} — ${ctx.name}`);
4763
+ log(`Editor: ${c.bold(server.url)}`);
4764
+ log(`MCP: ${c.bold(server.url + "/mcp")}`);
4765
+ log(`Watching: ${ctx.root} (scenes hot-reload on save; Ctrl+C to stop)`);
4766
+ if (!flagBool(args, "no-open"))
4767
+ openBrowser(server.url);
4768
+ };
4769
+
4492
4770
  // src/commands/status.ts
4493
4771
  var status = async (_args) => {
4494
4772
  const root = findProjectRoot(process.cwd());
@@ -4519,21 +4797,21 @@ var status = async (_args) => {
4519
4797
  };
4520
4798
 
4521
4799
  // src/index.ts
4522
- import { existsSync as existsSync16, realpathSync as realpathSync2 } from "node:fs";
4523
- import { delimiter as delimiter2, dirname as dirname11, join as join18 } from "node:path";
4800
+ import { existsSync as existsSync19, realpathSync as realpathSync2 } from "node:fs";
4801
+ import { delimiter as delimiter2, dirname as dirname12, join as join21 } from "node:path";
4524
4802
 
4525
4803
  // src/commands/install.ts
4526
4804
  import { execFileSync } from "node:child_process";
4527
- import { appendFileSync as appendFileSync2, chmodSync, cpSync, existsSync as existsSync15, mkdirSync as mkdirSync12, readFileSync as readFileSync15, realpathSync, rmSync as rmSync2, symlinkSync } from "node:fs";
4805
+ import { appendFileSync as appendFileSync2, chmodSync, cpSync, existsSync as existsSync18, mkdirSync as mkdirSync12, readFileSync as readFileSync18, realpathSync, rmSync as rmSync2, symlinkSync } from "node:fs";
4528
4806
  import { homedir as homedir2 } from "node:os";
4529
- import { basename as basename10, delimiter, dirname as dirname10, join as join17, resolve as resolve6, sep } from "node:path";
4807
+ import { basename as basename11, delimiter, dirname as dirname11, join as join20, resolve as resolve7, sep } from "node:path";
4530
4808
  var isWin = process.platform === "win32";
4531
4809
  var HOME = homedir2();
4532
4810
  var safeRealpath = (p) => {
4533
4811
  try {
4534
4812
  return realpathSync(p);
4535
4813
  } catch {
4536
- return resolve6(p);
4814
+ return resolve7(p);
4537
4815
  }
4538
4816
  };
4539
4817
  var onPath = (dir) => {
@@ -4549,13 +4827,13 @@ var onPath = (dir) => {
4549
4827
  });
4550
4828
  };
4551
4829
  var install = async (args) => {
4552
- const bundle = dirname10(process.execPath);
4553
- const exeName = basename10(process.execPath);
4830
+ const bundle = dirname11(process.execPath);
4831
+ const exeName = basename11(process.execPath);
4554
4832
  const chiselName = isWin ? "chisel.exe" : "chisel";
4555
- if (!existsSync15(join17(bundle, "runtime")) || !existsSync15(join17(bundle, chiselName))) {
4833
+ if (!existsSync18(join20(bundle, "runtime")) || !existsSync18(join20(bundle, chiselName))) {
4556
4834
  throw new CliError("`lecodes install` only works from the packaged release build (the archive containing lecodes + " + "chisel + runtime/). You appear to be running from source or an npm install — install globally " + "with your package manager there instead.");
4557
4835
  }
4558
- const companions = [exeName, chiselName, "runtime", "README.txt"].filter((f) => existsSync15(join17(bundle, f)));
4836
+ const companions = [exeName, chiselName, "runtime", "README.txt"].filter((f) => existsSync18(join20(bundle, f)));
4559
4837
  if (isWin)
4560
4838
  return installWindows(args, bundle, companions);
4561
4839
  return installUnix(args, bundle, companions);
@@ -4565,9 +4843,9 @@ var copyBundle = (bundle, target, companions) => {
4565
4843
  if (safeRealpath(target) === safeRealpath(bundle))
4566
4844
  return;
4567
4845
  for (const f of companions) {
4568
- const dest = join17(target, f);
4846
+ const dest = join20(target, f);
4569
4847
  rmSync2(dest, { recursive: true, force: true });
4570
- cpSync(join17(bundle, f), dest, { recursive: true });
4848
+ cpSync(join20(bundle, f), dest, { recursive: true });
4571
4849
  }
4572
4850
  };
4573
4851
  var permError = (e, dir, system) => {
@@ -4579,41 +4857,41 @@ var permError = (e, dir, system) => {
4579
4857
  };
4580
4858
  var installUnix = (args, bundle, companions) => {
4581
4859
  const system = flagBool(args, "system");
4582
- const libDir = resolve6(flagStr(args, "dir") ?? (system ? "/usr/local/lib/lecodes-cli" : join17(HOME, ".local", "lib", "lecodes-cli")));
4583
- const binDir = resolve6(flagStr(args, "bin") ?? (system ? "/usr/local/bin" : join17(HOME, ".local", "bin")));
4584
- const link = join17(binDir, "lecodes");
4860
+ const libDir = resolve7(flagStr(args, "dir") ?? (system ? "/usr/local/lib/lecodes-cli" : join20(HOME, ".local", "lib", "lecodes-cli")));
4861
+ const binDir = resolve7(flagStr(args, "bin") ?? (system ? "/usr/local/bin" : join20(HOME, ".local", "bin")));
4862
+ const link = join20(binDir, "lecodes");
4585
4863
  try {
4586
4864
  copyBundle(bundle, libDir, companions);
4587
- chmodSync(join17(libDir, "lecodes"), 493);
4588
- chmodSync(join17(libDir, "chisel"), 493);
4865
+ chmodSync(join20(libDir, "lecodes"), 493);
4866
+ chmodSync(join20(libDir, "chisel"), 493);
4589
4867
  } catch (e) {
4590
4868
  throw permError(e, libDir, system);
4591
4869
  }
4592
4870
  try {
4593
4871
  mkdirSync12(binDir, { recursive: true });
4594
4872
  rmSync2(link, { force: true });
4595
- symlinkSync(join17(libDir, "lecodes"), link);
4873
+ symlinkSync(join20(libDir, "lecodes"), link);
4596
4874
  } catch (e) {
4597
4875
  throw permError(e, binDir, system);
4598
4876
  }
4599
4877
  success(`Installed to ${c.bold(libDir)}`);
4600
- log(` ${c.dim(link + " → " + join17(libDir, "lecodes"))}`);
4878
+ log(` ${c.dim(link + " → " + join20(libDir, "lecodes"))}`);
4601
4879
  finishPathUnix(args, binDir);
4602
4880
  };
4603
4881
  var shellRc = () => {
4604
- const shell = basename10(process.env.SHELL ?? "");
4882
+ const shell = basename11(process.env.SHELL ?? "");
4605
4883
  if (shell === "zsh")
4606
- return { file: join17(HOME, ".zshrc"), name: "~/.zshrc" };
4884
+ return { file: join20(HOME, ".zshrc"), name: "~/.zshrc" };
4607
4885
  if (shell === "bash")
4608
- return { file: join17(HOME, ".bashrc"), name: "~/.bashrc" };
4609
- return { file: join17(HOME, ".profile"), name: "~/.profile" };
4886
+ return { file: join20(HOME, ".bashrc"), name: "~/.bashrc" };
4887
+ return { file: join20(HOME, ".profile"), name: "~/.profile" };
4610
4888
  };
4611
4889
  var PATH_MARKER = "# Added by `lecodes install`";
4612
4890
  var appendPathToShellRc = (binDir) => {
4613
4891
  const { file, name } = shellRc();
4614
4892
  const line = `export PATH="${binDir}:$PATH"`;
4615
4893
  try {
4616
- const existing = existsSync15(file) ? readFileSync15(file, "utf8") : "";
4894
+ const existing = existsSync18(file) ? readFileSync18(file, "utf8") : "";
4617
4895
  if (existing.includes(line))
4618
4896
  return name;
4619
4897
  appendFileSync2(file, `${existing && !existing.endsWith(`
@@ -4651,7 +4929,7 @@ var finishPathUnix = (args, binDir) => {
4651
4929
  log(` • or install system-wide (no PATH edit): ${c.bold("sudo lecodes install --system")}`);
4652
4930
  };
4653
4931
  var installWindows = (args, bundle, companions) => {
4654
- const target = resolve6(flagStr(args, "dir") ?? join17(process.env.LOCALAPPDATA ?? join17(HOME, "AppData", "Local"), "Programs", "lecodes-cli"));
4932
+ const target = resolve7(flagStr(args, "dir") ?? join20(process.env.LOCALAPPDATA ?? join20(HOME, "AppData", "Local"), "Programs", "lecodes-cli"));
4655
4933
  try {
4656
4934
  copyBundle(bundle, target, companions);
4657
4935
  } catch (e) {
@@ -4688,9 +4966,9 @@ var addToUserPathWindows = (dir) => {
4688
4966
 
4689
4967
  // src/index.ts
4690
4968
  var STANDALONE = (() => {
4691
- const dir = dirname11(process.execPath);
4969
+ const dir = dirname12(process.execPath);
4692
4970
  const chisel = process.platform === "win32" ? "chisel.exe" : "chisel";
4693
- return existsSync16(join18(dir, "runtime")) && existsSync16(join18(dir, chisel));
4971
+ return existsSync19(join21(dir, "runtime")) && existsSync19(join21(dir, chisel));
4694
4972
  })();
4695
4973
  var INSTALLED = STANDALONE && (() => {
4696
4974
  const exe = process.platform === "win32" ? "lecodes.exe" : "lecodes";
@@ -4705,8 +4983,8 @@ var INSTALLED = STANDALONE && (() => {
4705
4983
  if (!d)
4706
4984
  return false;
4707
4985
  try {
4708
- const cand = join18(d, exe);
4709
- return existsSync16(cand) && realpathSync2(cand) === self;
4986
+ const cand = join21(d, exe);
4987
+ return existsSync19(cand) && realpathSync2(cand) === self;
4710
4988
  } catch {
4711
4989
  return false;
4712
4990
  }
@@ -4784,9 +5062,15 @@ ${c.bold("Commands:")}
4784
5062
  --png also rasterize PNGs (needs @napi-rs/canvas)
4785
5063
  --out-dir <dir> output folder override
4786
5064
  --logs forward the screens' console output (to stderr)
5065
+ scene [path] LeCodes Scene Editor — visually edit a .scene.ts (tree, inspector,
5066
+ gizmo) on local files (needs the optional 'lecodes-3d-editor' package;
5067
+ works inside a cloned project or standalone in any folder)
5068
+ --port <n> editor-server port (default 4488)
5069
+ --no-open don't open the browser
5070
+ --no-mcp don't register the MCP server in .mcp.json
4787
5071
 
4788
5072
  Config lives in ~/.lecodes/config.json (override with LECODES_API / LECODES_TOKEN).`;
4789
- var commands = { init: init2, login, create, clone, status, diff, pull, push, preview, render, design, dev };
5073
+ var commands = { init: init2, login, create, clone, status, diff, pull, push, preview, render, design, scene, dev };
4790
5074
  if (STANDALONE)
4791
5075
  commands.install = install;
4792
5076
  var readVersion = () => package_default.version ?? "unknown";