lecodes-cli 2.0.2 → 2.0.4

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 (39) hide show
  1. package/dist/index.js +1013 -127
  2. package/package.json +13 -12
  3. package/runtime/sdk-types.json +1 -1
  4. package/runtime/web/VERSION.json +3 -3
  5. package/runtime/web/assets/{inspect-C7x78PNs.js → inspect-DdJ50Kun.js} +2 -2
  6. package/runtime/web/player.js +18 -18
  7. package/runtime/web-lite/assets/canvas-C6G_Pb2Q.js +1 -0
  8. package/runtime/web-lite/assets/pager-O712dZv9.js +1 -0
  9. package/runtime/web-lite/assets/roboto-cyrillic-DVBNKCCH.woff2 +0 -0
  10. package/runtime/web-lite/assets/roboto-cyrillic-ext-1gPz9dK9.woff2 +0 -0
  11. package/runtime/web-lite/assets/roboto-greek-DVLYEgdx.woff2 +0 -0
  12. package/runtime/web-lite/assets/roboto-greek-ext-D98D44EL.woff2 +0 -0
  13. package/runtime/web-lite/assets/roboto-latin-cAJwcdBQ.woff2 +0 -0
  14. package/runtime/web-lite/assets/roboto-latin-ext-t610Sqxq.woff2 +0 -0
  15. package/runtime/web-lite/assets/roboto-rest-C7JkyNHT.woff2 +0 -0
  16. package/runtime/web-lite/assets/roboto-vietnamese-w-p-nKZZ.woff2 +0 -0
  17. package/runtime/web-lite/assets/tween-t_XDy3Nd.js +1 -0
  18. package/runtime/web-lite/assets/vlist-gEB8Sn0S.js +1 -0
  19. package/runtime/web-lite/features.json +51 -0
  20. package/runtime/web-lite/index.html +23 -0
  21. package/runtime/web-lite/lite.js +249 -0
  22. package/src/commands/design/comments.ts +1 -1
  23. package/src/commands/design/context.ts +1 -1
  24. package/src/commands/design/index.ts +1 -1
  25. package/src/commands/dev.ts +17 -53
  26. package/src/commands/index.ts +2 -0
  27. package/src/commands/test.ts +55 -6
  28. package/src/commands/update/index.ts +8 -5
  29. package/src/commands/web.ts +171 -0
  30. package/src/compile/fonts.ts +5 -2
  31. package/src/dev/devServer.ts +20 -1
  32. package/src/dev/watchProject.ts +53 -0
  33. package/src/dev/webRunner.ts +10 -0
  34. package/src/hosts/archives.ts +66 -0
  35. package/src/hosts/liteBrowser.mjs +86 -0
  36. package/src/hosts/liteRunner.ts +386 -0
  37. package/src/hosts/peerInstall.ts +12 -8
  38. package/src/web/build.ts +194 -0
  39. package/src/web/preview.ts +50 -0
package/dist/index.js CHANGED
@@ -52,28 +52,29 @@ var package_default;
52
52
  var init_package = __esm(() => {
53
53
  package_default = {
54
54
  name: "lecodes-cli",
55
- version: "2.0.2",
55
+ version: "2.0.4",
56
56
  dependencies: {
57
57
  "@letary/chisel": "^0.13.0",
58
58
  jimp: "^1.6.1",
59
59
  "lecodes-sdk": "workspace:*"
60
60
  },
61
61
  devDependencies: {
62
- "lecodes-design": "^0.9.0",
62
+ "lecodes-design": "https://storage.yandexcloud.net/le-codes/releases/packages/lecodes-design/lecodes-design-0.11.0.tgz",
63
63
  "lecodes-headless": "workspace:*",
64
64
  "qrcode-terminal": "^0.12.0",
65
- "lecodes-3d-editor": "^0.7.0",
65
+ "lecodes-3d-editor": "https://storage.yandexcloud.net/le-codes/releases/packages/lecodes-3d-editor/lecodes-3d-editor-0.9.0.tgz",
66
66
  "lecodes-assets": "workspace:*",
67
67
  "lecodes-creator-nav": "workspace:*",
68
68
  "lecodes-gl-lite": "workspace:*",
69
69
  "lecodes-web-frame": "workspace:*",
70
70
  "lecodes-web-player": "workspace:*",
71
- "lecodes-web-shared": "workspace:*"
71
+ "lecodes-web-shared": "workspace:*",
72
+ "lecodes-web-lite": "workspace:*"
72
73
  },
73
74
  peerDependencies: {
74
- "lecodes-design": "^0.9.0",
75
- "lecodes-headless": "^2.0.2",
76
- "lecodes-3d-editor": "^0.7.0",
75
+ "lecodes-design": "^0.11.0",
76
+ "lecodes-headless": "^2.0.4",
77
+ "lecodes-3d-editor": "^0.9.0",
77
78
  "lecodes-assets": "^0.2.1"
78
79
  },
79
80
  bin: {
@@ -126,7 +127,7 @@ var init_package = __esm(() => {
126
127
  });
127
128
 
128
129
  // ../../sdk/src/version.ts
129
- var SDK_VERSION = "2.0.2", sdkMajor = (version) => {
130
+ var SDK_VERSION = "2.0.4", sdkMajor = (version) => {
130
131
  const m = version?.match(/^(\d+)\.\d+\.\d+/);
131
132
  return m ? Number(m[1]) : null;
132
133
  }, bundleSdkVersion = (head) => head.slice(0, 2000).match(/^\/\/ sdk: (\d+\.\d+\.\d+\S*)/m)?.[1] ?? null, bundleRunsOn = (bundleVersion, hostVersion) => sdkMajor(bundleVersion) !== null && sdkMajor(bundleVersion) === sdkMajor(hostVersion);
@@ -1349,6 +1350,7 @@ var init_chisel = __esm(() => {
1349
1350
  "remove",
1350
1351
  "setContent",
1351
1352
  "keepAlive",
1353
+ "url",
1352
1354
  "attachTo",
1353
1355
  "detents",
1354
1356
  "dismissible",
@@ -171787,10 +171789,10 @@ var applyProjectFonts = async (entries, opts) => {
171787
171789
  if (faces.length === 0)
171788
171790
  return;
171789
171791
  return buildFontsHeaderValue(faces, (path) => {
171790
- if (!opts.publicUrl || !/^https?:/i.test(opts.publicUrl))
171792
+ if (!opts.relative && (!opts.publicUrl || !/^https?:/i.test(opts.publicUrl)))
171791
171793
  return null;
171792
171794
  const fileSrc = byPath().get(path.replace(/^\//, ""))?.fileSrc;
171793
- return fileSrc ? opts.publicUrl + fileSrc : null;
171795
+ return fileSrc ? (opts.publicUrl ?? "") + fileSrc : null;
171794
171796
  }) || undefined;
171795
171797
  }, isHttp = (s) => /^https?:/i.test(s), sha12 = (b) => createHash2("sha256").update(b).digest("hex").slice(0, 12), registryHome = (registry) => registry.replace(/\/v\d+$/, ""), readRegistry = async (registry, rel) => {
171796
171798
  if (isHttp(registry)) {
@@ -212716,6 +212718,53 @@ var init_peers = __esm(() => {
212716
212718
  ];
212717
212719
  });
212718
212720
 
212721
+ // src/hosts/archives.ts
212722
+ var ARCHIVED_PEERS, BASE = "https://storage.yandexcloud.net/le-codes/releases/packages", packagesBase = () => (process.env.LECODES_PACKAGES_URL || BASE).replace(/\/$/, ""), archiveUrl = (name, version) => `${packagesBase()}/${name}/${name}-${version}.tgz`, numbers = (version) => version.split(/[.-]/).slice(0, 3).map(Number), byVersion = (a2, b) => {
212723
+ const pa = numbers(a2), pb = numbers(b);
212724
+ for (let i2 = 0;i2 < 3; i2++) {
212725
+ const d = (pa[i2] ?? 0) - (pb[i2] ?? 0);
212726
+ if (d)
212727
+ return d;
212728
+ }
212729
+ return 0;
212730
+ }, caretTakes = (floor, version) => {
212731
+ if (!/^\d+\.\d+\.\d+$/.test(version) || byVersion(version, floor) < 0)
212732
+ return false;
212733
+ const f5 = numbers(floor), v = numbers(version);
212734
+ const held = f5[0] ? 1 : f5[1] ? 2 : 3;
212735
+ for (let i2 = 0;i2 < held; i2++)
212736
+ if (f5[i2] !== v[i2])
212737
+ return false;
212738
+ return true;
212739
+ }, asked, versionsOut = (name) => {
212740
+ const url = `${packagesBase()}/${name}/index.json`;
212741
+ let answer = asked.get(url);
212742
+ if (!answer) {
212743
+ answer = fetch(`${url}?t=${Date.now()}`, { signal: AbortSignal.timeout(4000) }).then(async (res) => res.ok ? (await res.json()).versions ?? [] : []).catch(() => []);
212744
+ asked.set(url, answer);
212745
+ }
212746
+ return answer;
212747
+ }, archivedVersion = async (name) => {
212748
+ if (!ARCHIVED_PEERS.has(name))
212749
+ return null;
212750
+ if (name === "lecodes-headless")
212751
+ return package_default.version;
212752
+ const range = package_default.peerDependencies?.[name];
212753
+ const floor = range?.replace(/^[\^~>=\s]+/, "");
212754
+ if (!floor || !/^\d+\.\d+\.\d+/.test(floor))
212755
+ return null;
212756
+ const newest = (await versionsOut(name)).filter((version) => caretTakes(floor, version)).sort(byVersion).pop();
212757
+ return newest ?? floor;
212758
+ }, archiveSpec = async (name) => {
212759
+ const version = await archivedVersion(name);
212760
+ return version ? archiveUrl(name, version) : null;
212761
+ };
212762
+ var init_archives = __esm(() => {
212763
+ init_package();
212764
+ ARCHIVED_PEERS = new Set(["lecodes-headless", "lecodes-design", "lecodes-3d-editor"]);
212765
+ asked = new Map;
212766
+ });
212767
+
212719
212768
  // src/hosts/peerInstall.ts
212720
212769
  import { spawnSync as spawnSync6 } from "node:child_process";
212721
212770
  import { existsSync as existsSync23, mkdirSync as mkdirSync15, readFileSync as readFileSync19, writeFileSync as writeFileSync18 } from "node:fs";
@@ -212734,7 +212783,7 @@ async function loadPeer(name, subpath, opts) {
212734
212783
  if (opts.install === false)
212735
212784
  return null;
212736
212785
  if (peerVersionGlobal(name, spec) !== null)
212737
- throw new CliError(`${name} is installed but "${spec}" can't be loaded from it — reinstall it: npm install -g ${peerSpec(name)}`);
212786
+ throw new CliError(`${name} is installed but "${spec}" can't be loaded from it — reinstall it: npm install -g ${await peerSpec(name)}`);
212738
212787
  await installPeers(companions(name), opts.for);
212739
212788
  }
212740
212789
  return await import(pathToFileURL(resolveEntry(dir, subpath)).href);
@@ -212745,9 +212794,10 @@ var CANVAS_RANGE_FALLBACK = "^0.1.65", readJson2 = (path2) => {
212745
212794
  } catch {
212746
212795
  return null;
212747
212796
  }
212748
- }, peerSpec = (name) => {
212749
- if (name === "lecodes-headless")
212750
- return `lecodes-headless@${package_default.version}`;
212797
+ }, peerSpec = async (name) => {
212798
+ const archive = await archiveSpec(name);
212799
+ if (archive)
212800
+ return archive;
212751
212801
  if (name === "@napi-rs/canvas") {
212752
212802
  const renderer = readJson2(join24(peerDirInPrefix("lecodes-headless"), "package.json"));
212753
212803
  const range2 = renderer?.peerDependencies?.["@napi-rs/canvas"] ?? renderer?.optionalDependencies?.["@napi-rs/canvas"] ?? CANVAS_RANGE_FALLBACK;
@@ -212774,7 +212824,7 @@ var CANVAS_RANGE_FALLBACK = "^0.1.65", readJson2 = (path2) => {
212774
212824
  `);
212775
212825
  }
212776
212826
  const ordered = [...names3].sort((a2, b) => a2 === "lecodes-headless" ? -1 : b === "lecodes-headless" ? 1 : 0);
212777
- const specsNow = ordered.map(peerSpec);
212827
+ const specsNow = await Promise.all(ordered.map(peerSpec));
212778
212828
  const yes = opts.yes === true || !!process.env.LECODES_YES;
212779
212829
  if (!yes) {
212780
212830
  const interactive = process.stdin.isTTY && process.stdout.isTTY;
@@ -212791,7 +212841,7 @@ var CANVAS_RANGE_FALLBACK = "^0.1.65", readJson2 = (path2) => {
212791
212841
  if (!tool)
212792
212842
  throw new CliError(`Neither npm nor bun is on PATH, so ${names3.join(" + ")} can't be installed automatically. Install one, or run on a machine with Node: ${manualCommand(dir, specsNow)}`);
212793
212843
  for (const name of ordered) {
212794
- const spec = peerSpec(name);
212844
+ const spec = await peerSpec(name);
212795
212845
  const argv = tool === "npm" ? ["install", "--prefix", dir, "--no-audit", "--no-fund", "--loglevel=error", spec] : ["add", "--cwd", dir, spec];
212796
212846
  note(`$ ${tool} ${argv.join(" ")}`);
212797
212847
  const r2 = spawnSync6(tool, argv, { stdio: "inherit", shell: process.platform === "win32", timeout: 10 * 60000 });
@@ -212840,6 +212890,7 @@ var CANVAS_RANGE_FALLBACK = "^0.1.65", readJson2 = (path2) => {
212840
212890
  var init_peerInstall = __esm(() => {
212841
212891
  init_package();
212842
212892
  init_peers();
212893
+ init_archives();
212843
212894
  init_cli();
212844
212895
  });
212845
212896
 
@@ -213140,7 +213191,7 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, dirFlag, getContext = (dir) =>
213140
213191
  const mod = await loadPeer("lecodes-design", "server", { for: "lecodes design" });
213141
213192
  const missing = needs.filter((name) => typeof mod[name] !== "function");
213142
213193
  if (missing.length) {
213143
- throw new CliError(`This needs a newer lecodes-design — the installed one has no \`${missing[0]}\`. Run: npm install -g lecodes-design@latest`);
213194
+ throw new CliError(`This needs a newer lecodes-design — the installed one has no \`${missing[0]}\`. Run: lecodes update`);
213144
213195
  }
213145
213196
  return mod;
213146
213197
  }, requirePlatform = (ctx, verb) => {
@@ -213256,7 +213307,7 @@ var aiFlag, cliAuthor = (ai) => ai || process.env.CLAUDECODE ? { author: "Claude
213256
213307
  if (!platform)
213257
213308
  return null;
213258
213309
  if (typeof designServer.mirrorThreads !== "function") {
213259
- note("Comment sync needs a newer lecodes-design (npm install -g lecodes-design@latest) — serving without it.");
213310
+ note("Comment sync needs a newer lecodes-design (run: lecodes update) — serving without it.");
213260
213311
  return null;
213261
213312
  }
213262
213313
  let failures = 0;
@@ -213570,7 +213621,7 @@ var init_snapshot = __esm(() => {
213570
213621
  });
213571
213622
  });
213572
213623
 
213573
- // ../../node_modules/.bun/lecodes-design@0.9.0+a0cdf6553a4c6042/node_modules/lecodes-design/src/core/concept.ts
213624
+ // ../../node_modules/.bun/lecodes-design@https+++storage.yandexcloud.net+le-codes+releases+packages+lecodes-design+lecodes-design-0.11.0.tgz+a0cdf6553a4c6042/node_modules/lecodes-design/src/core/concept.ts
213574
213625
  var SPEC_TEMPLATE = `# Concept
213575
213626
 
213576
213627
  The narrative half of this design. \`meta.json\` + the canvas hold the *visual/flow* half (screens,
@@ -213606,7 +213657,7 @@ Behaviour that isn't visible in a single screen — validation, permissions, not
213606
213657
  cases — and the decisions you've made and why, so they aren't re-litigated later.
213607
213658
  `;
213608
213659
 
213609
- // ../../node_modules/.bun/lecodes-design@0.9.0+a0cdf6553a4c6042/node_modules/lecodes-design/src/templates.ts
213660
+ // ../../node_modules/.bun/lecodes-design@https+++storage.yandexcloud.net+le-codes+releases+packages+lecodes-design+lecodes-design-0.11.0.tgz+a0cdf6553a4c6042/node_modules/lecodes-design/src/templates.ts
213610
213661
  var SPEC_MD, META_JSON = `{
213611
213662
  "screens": {
213612
213663
  "home": { "pos": [0, 0], "desc": "Example screen — replace me", "role": "list" }
@@ -213703,9 +213754,9 @@ export default UIScreen([
213703
213754
  mb: spacing.lg,
213704
213755
  }),
213705
213756
  ]).style({ bgColor: colors.bg, p: spacing.md, pt: "safe-top" })
213706
- `, CLAUDE_MD = `# LeCodes Design — screen prototyping canvas
213757
+ `, CLAUDE_MD = `# le.codes Design — screen prototyping canvas
213707
213758
 
213708
- This folder is a **design canvas** for a LeCodes app: each file in \`screens/\` is one live,
213759
+ This folder is a **design canvas** for a le.codes app: each file in \`screens/\` is one live,
213709
213760
  executable screen shown on a spatial canvas in the browser (\`lecodes design\` runs the dev server).
213710
213761
  You (Claude) write the screens; the developer views them on the canvas and gives feedback.
213711
213762
  This is an intent-capture stage — screens run on inline mock data, before any backend exists.
@@ -213811,7 +213862,7 @@ keep in the chat.
213811
213862
  (entities + fields ARE the future schema — make them meaningful, not "Lorem ipsum").
213812
213863
  - **Reuse via shared/.** Design tokens and reusable components live in \`shared/\`. Use them in
213813
213864
  every screen. Promoting a repeated pattern into \`shared/\` is an explicit, deliberate step.
213814
- - The LeCodes UI API (\`UIScreen\`, \`UIColumn\`, \`UIRow\`, \`UIBox\`, \`UIText\`, \`UIImage\`,
213865
+ - The le.codes UI API (\`UIScreen\`, \`UIColumn\`, \`UIRow\`, \`UIBox\`, \`UIText\`, \`UIImage\`,
213815
213866
  \`UIButton\`, \`UIInput\`, \`UISpacer\`, \`UIScrollable\`, …) is **global** — no imports needed.
213816
213867
  Colors must be hex strings. This folder ships a \`tsconfig.json\` + bundled SDK types
213817
213868
  (\`.lecodes/types/\`), so the editor resolves those globals — and \`UINode\` / \`UINodeChild\` for
@@ -214033,7 +214084,7 @@ declare function assetIcon(id: string, opts?: { color?: string }): { readonly sv
214033
214084
  declare function font(id: string, opts?: { weights?: number[], italic?: boolean }): string
214034
214085
  `, README_HINT = `# design/
214035
214086
 
214036
- LeCodes Design canvas — run \`lecodes design\` here and open the printed URL.
214087
+ le.codes Design canvas — run \`lecodes design\` here and open the printed URL.
214037
214088
  See CLAUDE.md for the conventions (it's written for AI assistants, but it's the spec).
214038
214089
  `;
214039
214090
  var init_templates = __esm(() => {
@@ -214114,7 +214165,7 @@ var firstLine = (body, max = 64) => {
214114
214165
  const sync3 = flags["no-sync"] ? null : startCommentSync(ctx, designServer);
214115
214166
  log(`${c.bold("LeCodes Design")} — ${ctx.name}`);
214116
214167
  log(`Canvas: ${c.bold(server2.url)}`);
214117
- log(`MCP: ${c.bold(server2.url + "/mcp")}${renderer ? "" : c.dim(" (render_screen off — npm install -g lecodes-headless)")}`);
214168
+ log(`MCP: ${c.bold(server2.url + "/mcp")}${renderer ? "" : c.dim(" (render_screen off — lecodes-headless is not installed: `lecodes render` installs it)")}`);
214118
214169
  log(`Watching: ${ctx.dirName}/ (screens hot-reload on save; Ctrl+C to stop)`);
214119
214170
  if (sync3)
214120
214171
  log(`Comments: ${c.dim("in sync with the platform — replies and resolves go back automatically")}`);
@@ -215391,8 +215442,8 @@ var require_main = __commonJS((exports, module) => {
215391
215442
  // src/index.ts
215392
215443
  init_package();
215393
215444
  init_cli();
215394
- import { existsSync as existsSync50, realpathSync as realpathSync2 } from "node:fs";
215395
- import { delimiter as delimiter3, dirname as dirname25, join as join50 } from "node:path";
215445
+ import { existsSync as existsSync53, realpathSync as realpathSync2 } from "node:fs";
215446
+ import { delimiter as delimiter3, dirname as dirname27, join as join53 } from "node:path";
215396
215447
 
215397
215448
  // src/commands/app/index.ts
215398
215449
  init_manifest();
@@ -222047,8 +222098,7 @@ var desktop_default = defineCommand({
222047
222098
 
222048
222099
  // src/commands/dev.ts
222049
222100
  init_manifest();
222050
- init_ignore();
222051
- import { existsSync as existsSync34, mkdirSync as mkdirSync21, readFileSync as readFileSync26, watch, writeFileSync as writeFileSync24 } from "node:fs";
222101
+ import { existsSync as existsSync34, mkdirSync as mkdirSync21, readFileSync as readFileSync26, writeFileSync as writeFileSync24 } from "node:fs";
222052
222102
  import { randomBytes as randomBytes4 } from "node:crypto";
222053
222103
  import { networkInterfaces } from "node:os";
222054
222104
  import { basename as basename13, join as join35 } from "node:path";
@@ -222457,6 +222507,7 @@ var startDevServer = async (options) => {
222457
222507
  ` + renderPrelude({ origin: reqOrigin, base, transport, v: v2 });
222458
222508
  };
222459
222509
  const web = options.web ?? null;
222510
+ const lite = options.lite ?? null;
222460
222511
  const sendBuilt = (res, dir, rel) => {
222461
222512
  if (rel.split("/").some((seg) => seg === ".." || seg === "" || seg === "."))
222462
222513
  return sendText(res, 400, "text/plain", "Bad path");
@@ -222518,6 +222569,18 @@ var startDevServer = async (options) => {
222518
222569
  return redirect(`${base}/web/${query()}`);
222519
222570
  return sendBuilt(res, web.page, rel === "/" ? "index.html" : rel.slice(1));
222520
222571
  }
222572
+ if (route === "/lite" || route.startsWith("/lite/")) {
222573
+ if (!lite)
222574
+ return sendText(res, 404, "text/plain", "No Web Lite page with this CLI (lecodes-web-lite's dist).");
222575
+ const rel = route.slice("/lite".length);
222576
+ if (rel === "" || rel === "/" && !url.searchParams.has("dev")) {
222577
+ url.searchParams.set("dev", "..");
222578
+ res.writeHead(302, { location: `${base}/lite/?${url.searchParams.toString()}${url.hash}` });
222579
+ res.end();
222580
+ return;
222581
+ }
222582
+ return sendBuilt(res, lite, rel === "/" ? "index.html" : rel.slice(1));
222583
+ }
222521
222584
  if (route === "/version") {
222522
222585
  return sendJson(res, 200, { v });
222523
222586
  }
@@ -222601,6 +222664,7 @@ var startDevServer = async (options) => {
222601
222664
  return {
222602
222665
  url: `${origin}${base}/dev.js`,
222603
222666
  webUrl: web ? `${origin}${base}/web/` : null,
222667
+ liteUrl: lite ? `${origin}${base}/lite/` : null,
222604
222668
  base,
222605
222669
  port: boundPort,
222606
222670
  v: () => v,
@@ -222636,6 +222700,15 @@ var PAGE = {
222636
222700
  build: ["-c", "vite.page.config.ts"],
222637
222701
  sources: ["hosts/web-frame/src", "hosts/web-frame/page", "hosts/web-player/src/protocol.ts"]
222638
222702
  };
222703
+ var LITE = {
222704
+ pkg: "lecodes-web-lite",
222705
+ dist: "dist",
222706
+ entry: "lite.js",
222707
+ vendored: "web-lite",
222708
+ build: [],
222709
+ sources: ["hosts/web-lite/src", "hosts/web-lite/index.html", "renderers/web-lite/src", "renderers/web-shared/src", "hosts/web-player/src/dev.ts"]
222710
+ };
222711
+ var resolveLiteRunner = (log2 = () => {}) => resolve16(LITE, log2);
222639
222712
  var resolveWebRunner = (log2 = () => {}) => {
222640
222713
  const player = resolve16(PLAYER, log2);
222641
222714
  return player ? { player, page: resolve16(PAGE, log2) } : null;
@@ -222689,6 +222762,59 @@ var newest = (paths) => {
222689
222762
  return latest;
222690
222763
  };
222691
222764
 
222765
+ // src/dev/watchProject.ts
222766
+ init_ignore();
222767
+ init_paths();
222768
+ import { watch } from "node:fs";
222769
+ var DEBOUNCE_MS = 120;
222770
+ var SKIP_DIRS2 = new Set([".lecodes", ".git", "node_modules", "dist"]);
222771
+ var SKIP_FILES2 = new Set(["tsconfig.json", "jsconfig.json", ".gitignore", ".lecodesignore", ".DS_Store"]);
222772
+ var watchProject = (root, onChange, log2) => {
222773
+ const ignore = loadIgnore(root);
222774
+ const counts = (file) => {
222775
+ if (file === null)
222776
+ return true;
222777
+ const parts = file.replace(/\\/g, "/").split("/");
222778
+ if (parts.some((p2) => SKIP_DIRS2.has(p2)))
222779
+ return false;
222780
+ if (SKIP_FILES2.has(parts[parts.length - 1]))
222781
+ return false;
222782
+ try {
222783
+ if (ignore(toKey(file), false))
222784
+ return false;
222785
+ } catch {
222786
+ return false;
222787
+ }
222788
+ return true;
222789
+ };
222790
+ let timer = null;
222791
+ let watcher = null;
222792
+ try {
222793
+ watcher = watch(root, { recursive: true }, (_type, file) => {
222794
+ const name = file === null ? null : String(file);
222795
+ log2.debug?.(`[watch] event: ${name ?? "(null)"} → ${counts(name) ? "recompile" : "skip"}`);
222796
+ if (!counts(name))
222797
+ return;
222798
+ if (timer)
222799
+ clearTimeout(timer);
222800
+ timer = setTimeout(() => {
222801
+ timer = null;
222802
+ onChange();
222803
+ }, DEBOUNCE_MS);
222804
+ });
222805
+ } catch {
222806
+ log2.warn("fs.watch(recursive) unavailable on this platform — hot reload disabled (restart to pick up changes).");
222807
+ }
222808
+ watcher?.on("error", (e2) => log2.warn(`Watcher error — hot reload may have stopped (restart the command): ${e2.message}`));
222809
+ return {
222810
+ close() {
222811
+ if (timer)
222812
+ clearTimeout(timer);
222813
+ watcher?.close();
222814
+ }
222815
+ };
222816
+ };
222817
+
222692
222818
  // src/commands/dev.ts
222693
222819
  init_browserAuth();
222694
222820
 
@@ -222982,11 +223108,7 @@ var launchOnIos = (opts) => {
222982
223108
  // src/commands/dev.ts
222983
223109
  init_cli();
222984
223110
  init_shared();
222985
- init_paths();
222986
223111
  var DEFAULT_PORT2 = 4499;
222987
- var DEBOUNCE_MS = 120;
222988
- var WATCH_SKIP_DIRS = new Set([".lecodes", ".git", "node_modules"]);
222989
- var WATCH_SKIP_FILES = new Set(["tsconfig.json", "jsconfig.json", ".gitignore", ".lecodesignore", ".DS_Store"]);
222990
223112
  var VIRTUAL_IFACE = /wsl|docker|hyper-v|vethernet|virtualbox|vmware|tap|tun|zerotier|tailscale|loopback/i;
222991
223113
  var lanCandidates = () => {
222992
223114
  const nets = networkInterfaces();
@@ -223106,54 +223228,18 @@ var dev_default = defineCommand({
223106
223228
  warnErr(`Compile failed — fix it and save; devices connect once it compiles:
223107
223229
  ${first.error}`);
223108
223230
  }
223109
- const ignore = loadIgnore(root);
223110
- const shouldRecompile = (file) => {
223111
- if (file === null)
223112
- return true;
223113
- const parts = file.replace(/\\/g, "/").split("/");
223114
- if (parts.some((p2) => WATCH_SKIP_DIRS.has(p2)))
223115
- return false;
223116
- if (WATCH_SKIP_FILES.has(parts[parts.length - 1]))
223117
- return false;
223118
- try {
223119
- const key = toKey(file);
223120
- if (ignore(key, false))
223121
- return false;
223122
- } catch {
223123
- return false;
223124
- }
223125
- return true;
223126
- };
223127
- const debug2 = process.env.LECODES_DEV_DEBUG === "1";
223128
- let timer = null;
223129
- const onChange = (file) => {
223130
- if (debug2)
223131
- note(`[watch] event: ${file ?? "(null)"} → ${shouldRecompile(file) ? "recompile" : "skip"}`);
223132
- if (!shouldRecompile(file))
223133
- return;
223134
- if (timer)
223135
- clearTimeout(timer);
223136
- timer = setTimeout(async () => {
223137
- timer = null;
223138
- const result = await server2.recompile();
223139
- if (!result.ok) {
223140
- warnErr(`Compile failed (devices keep the last good version):
223231
+ const watcher = watchProject(root, async () => {
223232
+ const result = await server2.recompile();
223233
+ if (!result.ok) {
223234
+ warnErr(`Compile failed (devices keep the last good version):
223141
223235
  ${result.error}`);
223142
- } else if (result.changed) {
223143
- const devices = server2.clientCount();
223144
- note(`✓ v${result.v} → ${devices} device${devices === 1 ? "" : "s"}`);
223145
- for (const w of result.warnings)
223146
- warnErr(w);
223147
- }
223148
- }, DEBOUNCE_MS);
223149
- };
223150
- let watcher = null;
223151
- try {
223152
- watcher = watch(root, { recursive: true }, (_type, file) => onChange(file));
223153
- } catch {
223154
- warnErr("fs.watch(recursive) unavailable on this platform — hot reload disabled (restart to pick up changes).");
223155
- }
223156
- watcher?.on("error", (e2) => warnErr(`Watcher error — hot reload may have stopped (restart lecodes dev): ${e2.message}`));
223236
+ } else if (result.changed) {
223237
+ const devices = server2.clientCount();
223238
+ note(`✓ v${result.v} → ${devices} device${devices === 1 ? "" : "s"}`);
223239
+ for (const w of result.warnings)
223240
+ warnErr(w);
223241
+ }
223242
+ }, { warn: warnErr, debug: process.env.LECODES_DEV_DEBUG === "1" ? note : undefined });
223157
223243
  log("");
223158
223244
  log(`${c.bold("LeCodes Dev")} — ${name}`);
223159
223245
  log("");
@@ -223186,14 +223272,14 @@ ${result.error}`);
223186
223272
  session2 = launchOnAndroid({ port, url: androidUrl, serial: flags.serial, apk: flags.apk, packageName: flags.package });
223187
223273
  } catch (e2) {
223188
223274
  server2.close();
223189
- watcher?.close();
223275
+ watcher.close();
223190
223276
  throw e2;
223191
223277
  }
223192
223278
  note(`Android: ${session2.serial} → ${androidUrl} (adb reverse; device logs stream here as [device:log])`);
223193
223279
  const finish = (code) => {
223194
223280
  session2.stop();
223195
223281
  server2.close();
223196
- watcher?.close();
223282
+ watcher.close();
223197
223283
  process.exit(code);
223198
223284
  };
223199
223285
  process.on("SIGINT", () => {
@@ -223228,14 +223314,14 @@ ${crashes.join(`
223228
223314
  session2 = launchOnIos({ simulatorUrl: `http://127.0.0.1:${port}${path2}`, deviceUrl, udid: flags.udid, app: flags.app, bundleId: flags.bundle });
223229
223315
  } catch (e2) {
223230
223316
  server2.close();
223231
- watcher?.close();
223317
+ watcher.close();
223232
223318
  throw e2;
223233
223319
  }
223234
223320
  note(`iOS: ${session2.name} → ${session2.kind === "simulator" ? `http://127.0.0.1:${port}${path2}` : deviceUrl} (device logs stream here as [device:log])`);
223235
223321
  const finish = (code) => {
223236
223322
  session2.stop();
223237
223323
  server2.close();
223238
- watcher?.close();
223324
+ watcher.close();
223239
223325
  process.exit(code);
223240
223326
  };
223241
223327
  process.on("SIGINT", () => {
@@ -230583,15 +230669,15 @@ var parsePlatforms = (raw) => {
230583
230669
  if (raw === undefined)
230584
230670
  return;
230585
230671
  const known = allShaderPlatforms();
230586
- const asked = raw.split(",").map((p2) => p2.trim()).filter(Boolean);
230587
- if (asked.length === 0)
230672
+ const asked2 = raw.split(",").map((p2) => p2.trim()).filter(Boolean);
230673
+ if (asked2.length === 0)
230588
230674
  throw new CliError(`--platform needs at least one target. Known: ${known.join(", ")}`);
230589
- const bad = asked.filter((p2) => !known.includes(p2));
230675
+ const bad = asked2.filter((p2) => !known.includes(p2));
230590
230676
  if (bad.length > 0) {
230591
230677
  throw new CliError(`Unknown shader platform${bad.length > 1 ? "s" : ""}: ${bad.join(", ")}
230592
230678
  Known: ${known.join(", ")}`);
230593
230679
  }
230594
- return asked;
230680
+ return asked2;
230595
230681
  };
230596
230682
  var compileShaders = async (platforms) => {
230597
230683
  const { root, shaders } = collectShaders();
@@ -230703,8 +230789,391 @@ var status_default = defineCommand({
230703
230789
  });
230704
230790
 
230705
230791
  // src/commands/test.ts
230706
- import { existsSync as existsSync47, mkdirSync as mkdirSync31, readFileSync as readFileSync40, readdirSync as readdirSync21 } from "node:fs";
230707
- import { dirname as dirname23, resolve as resolve28 } from "node:path";
230792
+ import { existsSync as existsSync48, mkdirSync as mkdirSync32, readFileSync as readFileSync40, readdirSync as readdirSync21 } from "node:fs";
230793
+ import { dirname as dirname24, resolve as resolve28 } from "node:path";
230794
+ init_manifest();
230795
+
230796
+ // src/hosts/liteRunner.ts
230797
+ init_cli();
230798
+ import { spawn as spawn6 } from "node:child_process";
230799
+ import { existsSync as existsSync47, mkdirSync as mkdirSync31, writeFileSync as writeFileSync34 } from "node:fs";
230800
+ import { createServer as createServer3 } from "node:net";
230801
+ import { dirname as dirname23, join as join48 } from "node:path";
230802
+ import { createInterface as createInterface2 } from "node:readline";
230803
+ init_distRoot();
230804
+ init_peers();
230805
+ var FRAME_MS2 = 16;
230806
+ var LOG_SLACK_MS = 3000;
230807
+ var NO_NODE = "--lite needs Node on the PATH: the browser is driven from a Node process.";
230808
+ var NO_PLAYWRIGHT = "--lite runs the flows in a browser and needs Playwright: `npm install -D playwright` in the project, then `npx playwright install chromium` once.";
230809
+ var startDriver = async () => {
230810
+ const script = join48(distRoot, "src", "hosts", "liteBrowser.mjs");
230811
+ if (!existsSync47(script))
230812
+ throw new CliError("--lite is not available in this build of the CLI (its browser driver ships with the npm package).");
230813
+ const child = spawn6("node", [script], { stdio: ["pipe", "pipe", "inherit"] });
230814
+ const waiting = new Map;
230815
+ let onConsole = () => {};
230816
+ let next = 1;
230817
+ let dead = null;
230818
+ const fail = (error) => {
230819
+ dead = dead ?? error;
230820
+ for (const w of waiting.values())
230821
+ w.reject(dead);
230822
+ waiting.clear();
230823
+ };
230824
+ child.on("error", () => fail(new CliError(NO_NODE)));
230825
+ child.on("exit", () => fail(new Error("the browser's process ended")));
230826
+ createInterface2({ input: child.stdout }).on("line", (line) => {
230827
+ let message;
230828
+ try {
230829
+ message = JSON.parse(line);
230830
+ } catch {
230831
+ return;
230832
+ }
230833
+ if (message.event === "console") {
230834
+ onConsole(message.level ?? "log", message.text ?? "");
230835
+ return;
230836
+ }
230837
+ const w = message.id !== undefined ? waiting.get(message.id) : undefined;
230838
+ if (!w)
230839
+ return;
230840
+ waiting.delete(message.id);
230841
+ if (message.ok)
230842
+ w.resolve(message.value);
230843
+ else
230844
+ w.reject(new Error(message.error ?? "failed"));
230845
+ });
230846
+ const call = (op, args = {}) => new Promise((resolve28, reject) => {
230847
+ if (dead) {
230848
+ reject(dead);
230849
+ return;
230850
+ }
230851
+ const id = next++;
230852
+ waiting.set(id, { resolve: resolve28, reject });
230853
+ child.stdin.write(JSON.stringify({ id, op, args }) + `
230854
+ `);
230855
+ });
230856
+ const from = [
230857
+ join48(process.cwd(), "package.json"),
230858
+ join48(distRoot, "package.json"),
230859
+ join48(peersDir(), "package.json"),
230860
+ join48(distRoot, "..", "..", "engines", "gl-lite", "package.json")
230861
+ ];
230862
+ try {
230863
+ await call("launch", { from });
230864
+ } catch (e2) {
230865
+ child.kill();
230866
+ if (e2 instanceof CliError)
230867
+ throw e2;
230868
+ const message = e2 instanceof Error ? e2.message : String(e2);
230869
+ throw new CliError(message === "NO_PLAYWRIGHT" ? NO_PLAYWRIGHT : `Chromium did not start (${message}) — run "npx playwright install chromium" once.`);
230870
+ }
230871
+ return {
230872
+ open(width, height, listener) {
230873
+ onConsole = listener;
230874
+ return call("open", { width, height });
230875
+ },
230876
+ goto: (url) => call("goto", { url }),
230877
+ waitFor: (expression, timeout) => call("waitFor", { expression, timeout }),
230878
+ evaluate: (expression) => call("evaluate", { expression }),
230879
+ timeout: (ms) => call("timeout", { ms }),
230880
+ mouse: (action, x2, y2) => call("mouse", { action, x: x2, y: y2 }),
230881
+ keyboard: (action, text2) => call("keyboard", { action, text: text2 }),
230882
+ screenshot: (path2, clip) => call("screenshot", { path: path2, clip }),
230883
+ async close() {
230884
+ await call("close").catch(() => {});
230885
+ child.kill();
230886
+ }
230887
+ };
230888
+ };
230889
+ var freePort = () => new Promise((resolve28, reject) => {
230890
+ const probe = createServer3();
230891
+ probe.once("error", reject);
230892
+ probe.listen(0, "127.0.0.1", () => {
230893
+ const address = probe.address();
230894
+ const port = typeof address === "object" && address ? address.port : 0;
230895
+ probe.close(() => resolve28(port));
230896
+ });
230897
+ });
230898
+ var startLiteSession = async (options) => {
230899
+ const port = await freePort();
230900
+ const server2 = await startDevServer({
230901
+ root: options.root,
230902
+ manifest: options.manifest,
230903
+ name: options.name,
230904
+ lite: options.lite,
230905
+ origin: `http://127.0.0.1:${port}`,
230906
+ bind: "127.0.0.1",
230907
+ port,
230908
+ token: "test",
230909
+ entryOverride: options.entry
230910
+ });
230911
+ const compiled = await server2.recompile();
230912
+ if (!compiled.ok) {
230913
+ server2.close();
230914
+ throw new CliError(`Compile failed:
230915
+ ${compiled.error}`);
230916
+ }
230917
+ let driver;
230918
+ try {
230919
+ driver = await startDriver();
230920
+ } catch (e2) {
230921
+ server2.close();
230922
+ throw e2;
230923
+ }
230924
+ return {
230925
+ run: (plan, runOptions) => runPlan(driver, `${server2.liteUrl}?dev=..&test`, plan, runOptions),
230926
+ async close() {
230927
+ await driver.close();
230928
+ server2.close();
230929
+ }
230930
+ };
230931
+ };
230932
+ var parseArgs2 = (rest) => {
230933
+ const out = [];
230934
+ let i2 = 0;
230935
+ while (i2 < rest.length) {
230936
+ if (rest[i2] === " ") {
230937
+ i2++;
230938
+ continue;
230939
+ }
230940
+ if (rest[i2] === '"') {
230941
+ let text2 = "";
230942
+ i2++;
230943
+ while (i2 < rest.length && rest[i2] !== '"') {
230944
+ if (rest[i2] === "\\")
230945
+ i2++;
230946
+ text2 += rest[i2] ?? "";
230947
+ i2++;
230948
+ }
230949
+ i2++;
230950
+ out.push(text2);
230951
+ continue;
230952
+ }
230953
+ const end = rest.indexOf(" ", i2);
230954
+ const token = rest.slice(i2, end < 0 ? rest.length : end);
230955
+ const n2 = Number(token);
230956
+ out.push(Number.isFinite(n2) && token !== "" ? n2 : token);
230957
+ i2 = end < 0 ? rest.length : end;
230958
+ }
230959
+ return out;
230960
+ };
230961
+ var unwire = (text2) => text2.replace(/\\(n|\\)/g, (_, c4) => c4 === "n" ? `
230962
+ ` : "\\");
230963
+ var LEVEL = { log: "DEBUG", debug: "DEBUG", info: "INFO", warning: "WARN", warn: "WARN", error: "ERROR" };
230964
+ var ON_SHOW = ".lc-root > .lc:not(.lc-covered)";
230965
+ var FRAME = "new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(() => r(null))))";
230966
+ var FILL_VALUE = `(f) => f.type === "email" ? "test@example.com" : f.type === "tel" ? "5550100" : f.inputMode === "numeric" || f.inputMode === "decimal" ? "1" : f.type === "date" ? "2026-01-01" : f.type === "time" ? "12:00" : "test"`;
230967
+ var runPlan = async (driver, url, plan, options) => {
230968
+ const output = [];
230969
+ const seen = [];
230970
+ const say = (line) => {
230971
+ output.push(line);
230972
+ };
230973
+ await driver.open(options.width, options.height, (level, text2) => {
230974
+ seen.push(text2);
230975
+ say(`[${LEVEL[level] ?? "DEBUG"}] ${text2}`);
230976
+ options.onConsole?.(level, text2);
230977
+ });
230978
+ const expectOf = new Map;
230979
+ for (const step of plan.steps)
230980
+ if (step.expectLog !== undefined)
230981
+ for (const line of step.lines)
230982
+ expectOf.set(line, step.expectLog);
230983
+ const dump = () => driver.evaluate("window.lecodesTest.dump()");
230984
+ const rectOf = (id) => driver.evaluate(`window.lecodesTest.rect(${id})`);
230985
+ const nodeOf = async (name) => {
230986
+ const node = findDumpTarget(parseTreeDump(await dump()), name);
230987
+ if (!node)
230988
+ throw new Error(`no element "${name}" on the screen`);
230989
+ return node;
230990
+ };
230991
+ const pointOf = async (args, at2) => {
230992
+ const first = args[at2];
230993
+ if (typeof first === "number")
230994
+ return { point: [first, Number(args[at2 + 1])], next: at2 + 2 };
230995
+ const rect = await rectOf((await nodeOf(String(first))).id);
230996
+ if (!rect || rect[2] === 0 || rect[3] === 0)
230997
+ throw new Error(`"${first}" is not laid out`);
230998
+ return { point: [rect[0] + rect[2] / 2, rect[1] + rect[3] / 2], next: at2 + 1 };
230999
+ };
231000
+ const frame = () => driver.evaluate(FRAME);
231001
+ const waitFor = async (ms, line) => {
231002
+ const expected = expectOf.get(line);
231003
+ if (expected === undefined) {
231004
+ await driver.timeout(ms);
231005
+ return;
231006
+ }
231007
+ const until = Date.now() + ms + LOG_SLACK_MS;
231008
+ while (Date.now() < until) {
231009
+ if (seen.some((text2) => text2.includes(expected)))
231010
+ return;
231011
+ await driver.timeout(FRAME_MS2);
231012
+ }
231013
+ };
231014
+ let clip = null;
231015
+ const play = async (op, rest, line) => {
231016
+ const args = parseArgs2(rest);
231017
+ switch (op) {
231018
+ case "frames":
231019
+ await waitFor(Math.max(0, Number(args[0])) * FRAME_MS2, line);
231020
+ return;
231021
+ case "wait":
231022
+ await waitFor(Math.max(0, Number(args[0])), line);
231023
+ return;
231024
+ case "tap": {
231025
+ const { point } = await pointOf(args, 0);
231026
+ await driver.mouse("move", point[0], point[1]);
231027
+ await driver.mouse("down");
231028
+ await driver.mouse("up");
231029
+ await frame();
231030
+ return;
231031
+ }
231032
+ case "hold": {
231033
+ const { point, next } = await pointOf(args, 0);
231034
+ await driver.mouse("move", point[0], point[1]);
231035
+ await driver.mouse("down");
231036
+ await driver.timeout(Number(args[next]) || 0);
231037
+ await driver.mouse("up");
231038
+ await frame();
231039
+ return;
231040
+ }
231041
+ case "drag": {
231042
+ const from = await pointOf(args, 0);
231043
+ const to2 = await pointOf(args, from.next);
231044
+ const ms = Number(args[to2.next]) || 300;
231045
+ const steps = Math.max(2, Math.round(ms / FRAME_MS2));
231046
+ await driver.mouse("move", from.point[0], from.point[1]);
231047
+ await driver.mouse("down");
231048
+ for (let i2 = 1;i2 <= steps; i2++) {
231049
+ const t2 = i2 / steps;
231050
+ await driver.mouse("move", from.point[0] + (to2.point[0] - from.point[0]) * t2, from.point[1] + (to2.point[1] - from.point[1]) * t2);
231051
+ await driver.timeout(ms / steps);
231052
+ }
231053
+ await driver.mouse("up");
231054
+ await frame();
231055
+ return;
231056
+ }
231057
+ case "mouseto": {
231058
+ const { point } = await pointOf(args, 0);
231059
+ await driver.mouse("move", point[0], point[1]);
231060
+ await frame();
231061
+ return;
231062
+ }
231063
+ case "type":
231064
+ await driver.keyboard("type", unwire(rest));
231065
+ await frame();
231066
+ return;
231067
+ case "fill": {
231068
+ const value = rest === "" ? undefined : unwire(rest);
231069
+ const count = await driver.evaluate(`(() => {
231070
+ window.__lecodesFill = [...document.querySelectorAll("${ON_SHOW} input.lc, ${ON_SHOW} textarea.lc")].filter((f) => f.value === "")
231071
+ return window.__lecodesFill.length
231072
+ })()`);
231073
+ for (let i2 = 0;i2 < count; i2++) {
231074
+ const text2 = value ?? await driver.evaluate(`(${FILL_VALUE})(window.__lecodesFill[${i2}])`);
231075
+ await driver.evaluate(`window.__lecodesFill[${i2}].focus()`);
231076
+ await driver.keyboard("type", text2);
231077
+ }
231078
+ await frame();
231079
+ return;
231080
+ }
231081
+ case "scroll": {
231082
+ const by = Number(args[0]);
231083
+ const name = typeof args[1] === "string" ? args[1] : null;
231084
+ const target = name === null ? `document.querySelector("${ON_SHOW} .lc-scrollable, ${ON_SHOW} .lc-vlist")` : `document.querySelector('[data-node="${(await nodeOf(name)).id}"]')`;
231085
+ const found = await driver.evaluate(`(() => {
231086
+ const el = ${target}
231087
+ if (!el) return false
231088
+ const style = getComputedStyle(el)
231089
+ if (style.overflowX !== "hidden" && style.overflowY === "hidden") el.scrollLeft += ${by}
231090
+ else el.scrollTop += ${by}
231091
+ return true
231092
+ })()`);
231093
+ if (!found)
231094
+ throw new Error(name === null ? "no scrollable on the screen" : `"${name}" is not there`);
231095
+ await frame();
231096
+ return;
231097
+ }
231098
+ case "key": {
231099
+ await driver.keyboard("down", String(args[0]));
231100
+ await driver.timeout(Number(args[1]) || 0);
231101
+ await driver.keyboard("up", String(args[0]));
231102
+ return;
231103
+ }
231104
+ case "keydown":
231105
+ await driver.keyboard("down", String(args[0]));
231106
+ return;
231107
+ case "keyup":
231108
+ await driver.keyboard("up", String(args[0]));
231109
+ return;
231110
+ case "wheel":
231111
+ await driver.mouse("wheel", Number(args[0]) * 100, Number(args[1]) * 100);
231112
+ await frame();
231113
+ return;
231114
+ case "dump": {
231115
+ const file = rest.trim();
231116
+ mkdirSync31(dirname23(file), { recursive: true });
231117
+ writeFileSync34(file, await dump());
231118
+ return;
231119
+ }
231120
+ case "clip": {
231121
+ if (rest.trim() === "off") {
231122
+ clip = null;
231123
+ return;
231124
+ }
231125
+ if (typeof args[0] === "number") {
231126
+ clip = { x: args[0], y: Number(args[1]), width: Number(args[2]), height: Number(args[3]) };
231127
+ return;
231128
+ }
231129
+ const rect = await rectOf((await nodeOf(String(args[0]))).id);
231130
+ if (!rect)
231131
+ throw new Error(`"${args[0]}" is not laid out`);
231132
+ clip = { x: rect[0], y: rect[1], width: rect[2], height: rect[3] };
231133
+ return;
231134
+ }
231135
+ case "shot": {
231136
+ const file = rest.trim();
231137
+ mkdirSync31(dirname23(file), { recursive: true });
231138
+ await driver.screenshot(file, clip);
231139
+ return;
231140
+ }
231141
+ default:
231142
+ throw new Error(`"${op}" is not available on Web Lite (it has no 3D scene, no gamepad)`);
231143
+ }
231144
+ };
231145
+ try {
231146
+ await driver.goto(url);
231147
+ await driver.waitFor("window.lecodesTest && window.lecodesTest.ready === true", 20000);
231148
+ say("[script] loaded");
231149
+ const lines = plan.text.split(`
231150
+ `);
231151
+ for (let i2 = 0;i2 < lines.length; i2++) {
231152
+ const text2 = lines[i2].trim();
231153
+ if (text2 === "" || text2.startsWith("#"))
231154
+ continue;
231155
+ const space = text2.indexOf(" ");
231156
+ const op = space < 0 ? text2 : text2.slice(0, space);
231157
+ const rest = space < 0 ? "" : text2.slice(space + 1);
231158
+ try {
231159
+ await play(op, rest, i2 + 1);
231160
+ say(`[script] #${i2 + 1} done ${op}`);
231161
+ } catch (e2) {
231162
+ say(`[script] #${i2 + 1} error ${op}: ${e2 instanceof Error ? e2.message.split(`
231163
+ `)[0] : String(e2)}`);
231164
+ break;
231165
+ }
231166
+ }
231167
+ say("[script] end");
231168
+ } catch (e2) {
231169
+ say(`[ERROR] ${e2 instanceof Error ? e2.message.split(`
231170
+ `)[0] : String(e2)}`);
231171
+ }
231172
+ return output.join(`
231173
+ `);
231174
+ };
231175
+
231176
+ // src/commands/test.ts
230708
231177
  init_manifest();
230709
231178
  init_cli();
230710
231179
  init_shared();
@@ -230782,7 +231251,7 @@ var readFlowFile = (abs, displayName) => {
230782
231251
  name: file.name ?? displayName,
230783
231252
  kind: "file",
230784
231253
  steps: file.steps,
230785
- fixtures: resolveFixtures(file.fixtures, dirname23(abs)),
231254
+ fixtures: resolveFixtures(file.fixtures, dirname24(abs)),
230786
231255
  width: file.width,
230787
231256
  height: file.height,
230788
231257
  settle: file.settle,
@@ -230802,6 +231271,7 @@ var testFlags = {
230802
231271
  ...viewportFlags,
230803
231272
  ...bundleFlags,
230804
231273
  ...desktopRenderFlags,
231274
+ lite: bool("run the file scenarios on Web Lite, in a browser (needs Playwright): the same steps, targets and expects; the clock is the browser's, so a step that expects a log line waits for it; design flows stay headless"),
230805
231275
  desktop: bool("run the file scenarios through the NATIVE host: the same steps, targets and expects as headless (expect.audio and the json output excepted); design flows stay headless. --frames / --fixed-dt as in render --desktop")
230806
231276
  };
230807
231277
  var test_default = defineCommand({
@@ -230817,6 +231287,11 @@ Design flows read tests/fixtures.json automatically (canned fetch responses) so
230817
231287
  });
230818
231288
  var runTests = async (args, flags) => {
230819
231289
  const sim = flags.sim;
231290
+ const lite = flags.lite;
231291
+ if (lite && (flags.desktop || sim))
231292
+ throw new CliError("--lite is a host of its own — drop --desktop / --sim.");
231293
+ if (lite && flags.flows)
231294
+ throw new CliError("--flows needs the headless renderer (design flows read expect.screen and fill inputs) — drop --lite.");
230820
231295
  const desktop = flags.desktop || sim;
230821
231296
  if (desktop && flags.flows)
230822
231297
  throw new CliError(`--flows needs the headless renderer (design flows read expect.screen and fill inputs) — drop --${sim ? "sim" : "desktop"}.`);
@@ -230828,7 +231303,7 @@ var runTests = async (args, flags) => {
230828
231303
  }
230829
231304
  const desktopRenderer = desktop ? resolveDesktopRenderer(desktopRoot, flags.renderer) : undefined;
230830
231305
  const native = desktop ? await buildBundle(bundleOptions(flags), { localAssets: true, desktopRenderer }) : null;
230831
- const headless = desktop ? null : await compileHeadlessBundle({ entry: flags.entry, publicUrl: flags["public-url"], remoteAssets: flags["remote-assets"], waitNetwork: flags["wait-network"] });
231306
+ const headless = desktop || lite ? null : await compileHeadlessBundle({ entry: flags.entry, publicUrl: flags["public-url"], remoteAssets: flags["remote-assets"], waitNetwork: flags["wait-network"] });
230832
231307
  const warnings = headless?.warnings ?? [];
230833
231308
  const useLocalAssets = headless?.useLocalAssets ?? false;
230834
231309
  const js = headless?.js ?? "";
@@ -230842,7 +231317,7 @@ var runTests = async (args, flags) => {
230842
231317
  root = process.cwd();
230843
231318
  }
230844
231319
  }
230845
- const renderer = desktop ? null : await requireRenderer();
231320
+ const renderer = desktop || lite ? null : await requireRenderer();
230846
231321
  const nativeFrames = flags.frames || 60;
230847
231322
  const nativeFixedDt = flags["fixed-dt"] ?? 16;
230848
231323
  if (nativeFixedDt < 0 || nativeFixedDt > 1000)
@@ -230856,28 +231331,28 @@ var runTests = async (args, flags) => {
230856
231331
  if (args.length > 0) {
230857
231332
  for (const given of args) {
230858
231333
  const candidates = [resolve28(given), resolve28(testsDir, given), resolve28(testsDir, `${given}.flow.json`)];
230859
- const abs = candidates.find((p2) => existsSync47(p2));
231334
+ const abs = candidates.find((p2) => existsSync48(p2));
230860
231335
  if (!abs)
230861
231336
  throw new CliError(`No such test: "${given}" (looked in ${testsDir}).`);
230862
231337
  cases.push(readFlowFile(abs, given));
230863
231338
  }
230864
- } else if (existsSync47(testsDir)) {
231339
+ } else if (existsSync48(testsDir)) {
230865
231340
  for (const f5 of readdirSync21(testsDir).filter((f6) => f6.endsWith(".flow.json")).sort()) {
230866
231341
  cases.push(readFlowFile(resolve28(testsDir, f5), f5));
230867
231342
  }
230868
231343
  }
230869
231344
  const defaultPack = resolve28(testsDir, "fixtures.json");
230870
- const flowFixtures = flags.fixtures !== undefined ? resolveFixtures(flags.fixtures, ".") : existsSync47(defaultPack) ? resolveFixtures("fixtures.json", testsDir) : undefined;
231345
+ const flowFixtures = flags.fixtures !== undefined ? resolveFixtures(flags.fixtures, ".") : existsSync48(defaultPack) ? resolveFixtures("fixtures.json", testsDir) : undefined;
230871
231346
  const flowsOnly = flags.flows;
230872
231347
  const wantFlows = !flags["no-flows"] && args.length === 0;
230873
231348
  if (flowsOnly)
230874
231349
  cases.length = 0;
230875
231350
  let skipped = [];
230876
231351
  let entryScreen = null;
230877
- if (desktop && wantFlows && existsSync47(designMetaPath)) {
230878
- note("Design-derived flows skipped with --desktop (they read expect.screen and fill inputs; run `lecodes test` headless for them).");
231352
+ if ((desktop || lite) && wantFlows && existsSync48(designMetaPath)) {
231353
+ note("Design-derived flows skipped with --desktop / --lite (they read expect.screen and fill inputs; run `lecodes test` headless for them).");
230879
231354
  }
230880
- if (!desktop && (wantFlows || flowsOnly) && existsSync47(designMetaPath)) {
231355
+ if (!desktop && !lite && (wantFlows || flowsOnly) && existsSync48(designMetaPath)) {
230881
231356
  let meta;
230882
231357
  try {
230883
231358
  meta = JSON.parse(readFileSync40(designMetaPath, "utf8"));
@@ -230917,7 +231392,7 @@ var runTests = async (args, flags) => {
230917
231392
  const reportCase = (result) => {
230918
231393
  const skippedCount = result.steps.reduce((n2, s2) => n2 + (s2.skipped?.length ?? 0), 0);
230919
231394
  if (result.ok) {
230920
- out(`${c.green("✓")} ${result.name}${skippedCount > 0 ? c.dim(` (${skippedCount} semantic check${skippedCount === 1 ? "" : "s"} skipped on --desktop)`) : ""}`);
231395
+ out(`${c.green("✓")} ${result.name}${skippedCount > 0 ? c.dim(` (${skippedCount} check${skippedCount === 1 ? "" : "s"} skipped on this host)`) : ""}`);
230921
231396
  } else {
230922
231397
  out(`${c.red("✗")} ${result.name}`);
230923
231398
  if (result.error)
@@ -230936,8 +231411,8 @@ var runTests = async (args, flags) => {
230936
231411
  const plan = translateScenario(kase.steps, { outDir: caseDir, headroomFrames: nativeFrames, sim });
230937
231412
  for (const st of plan.steps)
230938
231413
  if (st.screenshot)
230939
- mkdirSync31(dirname23(st.screenshot), { recursive: true });
230940
- mkdirSync31(caseDir, { recursive: true });
231414
+ mkdirSync32(dirname24(st.screenshot), { recursive: true });
231415
+ mkdirSync32(caseDir, { recursive: true });
230941
231416
  const finalPng = resolve28(caseDir, "final.png");
230942
231417
  const { output } = await runDesktopRender(native.js, native.resources, {
230943
231418
  renderer: desktopRenderer,
@@ -230952,7 +231427,7 @@ var runTests = async (args, flags) => {
230952
231427
  script: plan.text,
230953
231428
  fixedDtMs: nativeFixedDt
230954
231429
  });
230955
- const r2 = evaluateDesktopRun(plan, output, (path2) => existsSync47(path2) ? readFileSync40(path2, "utf8") : null);
231430
+ const r2 = evaluateDesktopRun(plan, output, (path2) => existsSync48(path2) ? readFileSync40(path2, "utf8") : null);
230956
231431
  if (!r2.loaded)
230957
231432
  throw new CliError("This desktop host doesn't support scripted input (CREATOR_SCRIPT). Update it: lecodes desktop update (or point LECODES_DESKTOP_EXE at a newer build).");
230958
231433
  return {
@@ -230964,8 +231439,48 @@ var runTests = async (args, flags) => {
230964
231439
  artifacts: r2.ok ? [] : [finalPng]
230965
231440
  };
230966
231441
  };
231442
+ let liteSession = null;
231443
+ if (lite) {
231444
+ const page = resolveLiteRunner(note);
231445
+ if (!page)
231446
+ throw new CliError("No Web Lite page found: this CLI has no runtime/web-lite (a monorepo checkout needs `bun run build` in hosts/web-lite).");
231447
+ let manifest = null;
231448
+ try {
231449
+ manifest = readManifest(root);
231450
+ } catch {}
231451
+ liteSession = await startLiteSession({ root, manifest, name: manifest?.name ?? "app", lite: page, entry: flags.entry });
231452
+ }
231453
+ const runLiteCase = async (kase) => {
231454
+ const caseDir = resolve28(artifactsRoot, slug(kase.name));
231455
+ const plan = translateScenario(kase.steps, { outDir: caseDir, headroomFrames: 0 });
231456
+ mkdirSync32(caseDir, { recursive: true });
231457
+ const output = await liteSession.run(plan, {
231458
+ width: flags.width || kase.width || projectViewport?.width || 390,
231459
+ height: flags.height || kase.height || projectViewport?.height || 844,
231460
+ onConsole
231461
+ });
231462
+ const r2 = evaluateDesktopRun(plan, output, (path2) => existsSync48(path2) ? readFileSync40(path2, "utf8") : null);
231463
+ return {
231464
+ name: kase.name,
231465
+ kind: kase.kind,
231466
+ ok: r2.ok,
231467
+ steps: r2.steps.map((s2) => ({ desc: s2.desc, ok: s2.ok, error: s2.error, skipped: s2.skipped.length > 0 ? s2.skipped : undefined })),
231468
+ error: !r2.loaded ? "the page did not load the app" : r2.ended ? undefined : "the page stopped before the script finished",
231469
+ artifacts: []
231470
+ };
231471
+ };
230967
231472
  for (const kase of cases) {
230968
231473
  let result;
231474
+ if (lite) {
231475
+ try {
231476
+ result = await runLiteCase(kase);
231477
+ } catch (e2) {
231478
+ result = { name: kase.name, kind: kase.kind, ok: false, steps: [], error: e2 instanceof Error ? e2.message : String(e2), artifacts: [] };
231479
+ }
231480
+ results.push(result);
231481
+ reportCase(result);
231482
+ continue;
231483
+ }
230969
231484
  if (desktop) {
230970
231485
  try {
230971
231486
  result = await runNativeCase(kase);
@@ -231039,6 +231554,7 @@ var runTests = async (args, flags) => {
231039
231554
  }, null, 2) + `
231040
231555
  `);
231041
231556
  }
231557
+ await liteSession?.close();
231042
231558
  clearTimeout(killer);
231043
231559
  process.exit(failed > 0 ? 1 : 0);
231044
231560
  };
@@ -231047,9 +231563,9 @@ var runTests = async (args, flags) => {
231047
231563
  init_peerInstall();
231048
231564
  import { createRequire as createRequire8 } from "node:module";
231049
231565
  import { execFileSync as execFileSync8 } from "node:child_process";
231050
- import { existsSync as existsSync48, mkdtempSync as mkdtempSync6, readdirSync as readdirSync22, rmSync as rmSync13, statSync as statSync19 } from "node:fs";
231566
+ import { existsSync as existsSync49, mkdtempSync as mkdtempSync6, readdirSync as readdirSync22, rmSync as rmSync13, statSync as statSync19 } from "node:fs";
231051
231567
  import { tmpdir as tmpdir6 } from "node:os";
231052
- import { extname, join as join48, relative as relative12, resolve as resolve29 } from "node:path";
231568
+ import { extname, join as join49, relative as relative12, resolve as resolve29 } from "node:path";
231053
231569
  init_cli();
231054
231570
  init_paths();
231055
231571
  var DEFAULT_SIZE = 256;
@@ -231061,13 +231577,13 @@ var findKtx = () => {
231061
231577
  process.env.LECODES_KTX,
231062
231578
  process.env.KTX_CLI,
231063
231579
  process.env.KTX_TOKTX?.replace(/toktx(\.exe)?$/i, exe),
231064
- process.platform === "win32" ? join48(process.env.LOCALAPPDATA ?? "", "Programs", "KTX-Software", "bin", exe) : null,
231065
- process.platform === "win32" ? join48(process.env.ProgramFiles ?? "", "KTX-Software", "bin", exe) : null,
231580
+ process.platform === "win32" ? join49(process.env.LOCALAPPDATA ?? "", "Programs", "KTX-Software", "bin", exe) : null,
231581
+ process.platform === "win32" ? join49(process.env.ProgramFiles ?? "", "KTX-Software", "bin", exe) : null,
231066
231582
  "/usr/local/bin/ktx",
231067
231583
  "/opt/homebrew/bin/ktx"
231068
231584
  ].filter((p2) => !!p2);
231069
231585
  for (const path2 of candidates) {
231070
- if (existsSync48(path2))
231586
+ if (existsSync49(path2))
231071
231587
  return path2;
231072
231588
  }
231073
231589
  try {
@@ -231099,7 +231615,7 @@ var ktxToPng = (ktx, file, out) => {
231099
231615
  run4(["extract", "--transcode", "rgba8", "--level", "0", file, out]);
231100
231616
  } catch (e2) {
231101
231617
  run4(["extract", "--level", "0", file, out]);
231102
- if (!existsSync48(out))
231618
+ if (!existsSync49(out))
231103
231619
  throw e2;
231104
231620
  }
231105
231621
  };
@@ -231108,7 +231624,7 @@ var collect = (dir) => {
231108
231624
  for (const name of readdirSync22(dir)) {
231109
231625
  if (name === "node_modules" || name.startsWith("."))
231110
231626
  continue;
231111
- const full = join48(dir, name);
231627
+ const full = join49(dir, name);
231112
231628
  if (statSync19(full).isDirectory())
231113
231629
  out.push(...collect(full));
231114
231630
  else if (IMAGE_EXT.test(name))
@@ -231135,7 +231651,7 @@ var thumbs = async (targets, size, force) => {
231135
231651
  if (targets.length > 0) {
231136
231652
  for (const arg of targets) {
231137
231653
  const abs = resolve29(process.cwd(), arg);
231138
- if (!existsSync48(abs))
231654
+ if (!existsSync49(abs))
231139
231655
  throw new CliError(`No such file or folder: ${arg}`);
231140
231656
  if (statSync19(abs).isDirectory())
231141
231657
  files.push(...collect(abs));
@@ -231157,7 +231673,7 @@ var thumbs = async (targets, size, force) => {
231157
231673
  const ktx = files.some((f5) => f5.toLowerCase().endsWith(".ktx2")) ? findKtx() : null;
231158
231674
  const sharp = sharpResizer();
231159
231675
  const resize = sharp ?? jimpResizer();
231160
- const tmp = mkdtempSync6(join48(tmpdir6(), "lecodes-thumbs-"));
231676
+ const tmp = mkdtempSync6(join49(tmpdir6(), "lecodes-thumbs-"));
231161
231677
  let made = 0, fresh = 0;
231162
231678
  const skipped = [];
231163
231679
  try {
@@ -231183,7 +231699,7 @@ var thumbs = async (targets, size, force) => {
231183
231699
  skipped.push(`${asset} (KTX2 needs KTX-Software's \`ktx\` — set LECODES_KTX or put it on PATH)`);
231184
231700
  continue;
231185
231701
  }
231186
- source = join48(tmp, `${i2}.png`);
231702
+ source = join49(tmp, `${i2}.png`);
231187
231703
  ktxToPng(ktx, file, source);
231188
231704
  }
231189
231705
  sceneServer.writePreviewFile(root, asset, await resize(source, size));
@@ -231203,7 +231719,7 @@ var thumbs = async (targets, size, force) => {
231203
231719
  if (rendered.length > 0) {
231204
231720
  warnErr(`Models, materials and prefabs are RENDERED, not decoded — the scene editor does those: ${rendered.join(", ")}`);
231205
231721
  }
231206
- note(`Cache: ${join48(root, ".lecodes", "preview")} (re-runs skip everything unchanged)`);
231722
+ note(`Cache: ${join49(root, ".lecodes", "preview")} (re-runs skip everything unchanged)`);
231207
231723
  };
231208
231724
 
231209
231725
  // src/commands/update/index.ts
@@ -231211,10 +231727,11 @@ init_package();
231211
231727
  init_cli();
231212
231728
  init_peers();
231213
231729
  init_peerInstall();
231730
+ init_archives();
231214
231731
  import { spawnSync as spawnSync10 } from "node:child_process";
231215
- import { existsSync as existsSync49, readdirSync as readdirSync23 } from "node:fs";
231732
+ import { existsSync as existsSync50, readdirSync as readdirSync23 } from "node:fs";
231216
231733
  import { homedir as homedir13 } from "node:os";
231217
- import { dirname as dirname24, join as join49, resolve as resolve31 } from "node:path";
231734
+ import { dirname as dirname25, join as join50, resolve as resolve31 } from "node:path";
231218
231735
 
231219
231736
  // src/commands/update/types.ts
231220
231737
  init_api();
@@ -231286,9 +231803,9 @@ var findProjectDir = (start) => {
231286
231803
  const home = resolve31(homedir13());
231287
231804
  let dir = resolve31(start);
231288
231805
  while (true) {
231289
- if (dir !== home && (existsSync49(join49(dir, "app.json")) || existsSync49(join49(dir, ".lecodes", "manifest.json")) || existsSync49(join49(dir, ".lecodes", "types"))))
231806
+ if (dir !== home && (existsSync50(join50(dir, "app.json")) || existsSync50(join50(dir, ".lecodes", "manifest.json")) || existsSync50(join50(dir, ".lecodes", "types"))))
231290
231807
  return dir;
231291
- const parent = dirname24(dir);
231808
+ const parent = dirname25(dir);
231292
231809
  if (parent === dir)
231293
231810
  return null;
231294
231811
  dir = parent;
@@ -231323,7 +231840,7 @@ var semverNewer2 = (a2, b) => {
231323
231840
  return 0;
231324
231841
  };
231325
231842
  var FROM_SOURCE = /[\\/]src[\\/]commands[\\/]/.test(import.meta.url);
231326
- var STANDALONE = existsSync49(join49(dirname24(process.execPath), "runtime")) && existsSync49(join49(dirname24(process.execPath), process.platform === "win32" ? "chisel.exe" : "chisel"));
231843
+ var STANDALONE = existsSync50(join50(dirname25(process.execPath), "runtime")) && existsSync50(join50(dirname25(process.execPath), process.platform === "win32" ? "chisel.exe" : "chisel"));
231327
231844
  var update_default = defineCommand({
231328
231845
  name: "update",
231329
231846
  summary: "Move everything the project depends on to its newest published state",
@@ -231383,7 +231900,7 @@ ${c.bold(name)} ${c.dim(what)}`);
231383
231900
  return ["skipped", "not inside a project"];
231384
231901
  if (flags["no-shells"])
231385
231902
  return ["skipped", "--no-shells"];
231386
- const present = ["ios", "android"].filter((d) => existsSync49(join49(root, d)) && readdirSync23(join49(root, d)).length > 0);
231903
+ const present = ["ios", "android"].filter((d) => existsSync50(join50(root, d)) && readdirSync23(join50(root, d)).length > 0);
231387
231904
  if (present.length === 0)
231388
231905
  return ["skipped", "no ios/ or android/ shell in this project (`lecodes app init ios|android`)"];
231389
231906
  await runSubcommand(app_default, "update");
@@ -231398,7 +231915,7 @@ ${c.bold(name)} ${c.dim(what)}`);
231398
231915
  for (const [name, probe] of OPTIONAL_PEERS) {
231399
231916
  const where = peerSource(name, probe);
231400
231917
  const installed = peerVersion(name, probe);
231401
- const latest = !installed ? null : where === "prefix" && name === "lecodes-headless" ? package_default.version : await npmLatest(name);
231918
+ const latest = !installed ? null : await archivedVersion(name) ?? await npmLatest(name);
231402
231919
  rows.push({ name, installed, latest, where });
231403
231920
  }
231404
231921
  rows.push({ name: "lecodes-cli", installed: package_default.version, latest: await npmLatest("lecodes-cli"), where: "global" });
@@ -231412,7 +231929,7 @@ ${c.bold(name)} ${c.dim(what)}`);
231412
231929
  if (behind.length === 0)
231413
231930
  return "ok";
231414
231931
  const prefixBehind = behind.filter((r2) => r2.where === "prefix");
231415
- const globalSpecs = behind.filter((r2) => r2.where !== "prefix").map((r2) => r2.name + "@latest");
231932
+ const globalSpecs = await Promise.all(behind.filter((r2) => r2.where !== "prefix").map(async (r2) => await archiveSpec(r2.name) ?? r2.name + "@latest"));
231416
231933
  if (flags.check) {
231417
231934
  return ["updated", behind.length + " behind — run: lecodes update" + (globalSpecs.length ? " (global: npm install -g " + globalSpecs.join(" ") + ")" : "")];
231418
231935
  }
@@ -231430,7 +231947,7 @@ ${c.bold(name)} ${c.dim(what)}`);
231430
231947
  const r2 = spawnSync10("npm", ["install", "-g", ...toInstall], { stdio: "inherit", shell: true });
231431
231948
  if (r2.status !== 0)
231432
231949
  throw new CliError("npm install -g exited with " + (r2.status ?? "a signal"));
231433
- done.push(...toInstall.map((s2) => s2.replace("@latest", "")));
231950
+ done.push(...behind.filter((r3) => r3.where !== "prefix" && (!STANDALONE || r3.name !== "lecodes-cli")).map((r3) => r3.name));
231434
231951
  }
231435
231952
  if (done.length === 0)
231436
231953
  return ["skipped", "only the standalone CLI itself is behind — download the release"];
@@ -231449,11 +231966,380 @@ ${c.bold("Summary")}`);
231449
231966
  }
231450
231967
  });
231451
231968
 
231969
+ // src/commands/web.ts
231970
+ init_manifest();
231971
+ import { basename as basename25, relative as relative13 } from "node:path";
231972
+ init_browserAuth();
231973
+ init_cli();
231974
+
231975
+ // src/web/build.ts
231976
+ import { createHash as createHash9 } from "node:crypto";
231977
+ import { copyFileSync as copyFileSync2, cpSync as cpSync6, existsSync as existsSync51, mkdirSync as mkdirSync33, readFileSync as readFileSync41, readdirSync as readdirSync24, rmSync as rmSync14, statSync as statSync20, writeFileSync as writeFileSync35 } from "node:fs";
231978
+ import { dirname as dirname26, extname as extname2, join as join51, resolve as resolve32 } from "node:path";
231979
+ init_fonts();
231980
+ init_assetIcons();
231981
+ init_projectCompile();
231982
+ init_cli();
231983
+ var escapeHtml = (text2) => text2.replace(/[&<>"]/g, (c4) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c4]);
231984
+ var normalBase = (base) => {
231985
+ const b = (base ?? "/").trim() || "/";
231986
+ return (b.startsWith("/") ? b : `/${b}`).replace(/\/*$/, "/");
231987
+ };
231988
+ var walk = (dir, prefix = "") => {
231989
+ const out = [];
231990
+ for (const name of readdirSync24(dir)) {
231991
+ const abs = join51(dir, name);
231992
+ const stat = statSync20(abs);
231993
+ if (stat.isDirectory())
231994
+ out.push(...walk(abs, `${prefix}${name}/`));
231995
+ else
231996
+ out.push({ path: prefix + name, bytes: stat.size });
231997
+ }
231998
+ return out;
231999
+ };
232000
+ var buildWeb = async (options) => {
232001
+ const { root, manifest, name, lite } = options;
232002
+ const out = resolve32(root, options.out);
232003
+ if (out === resolve32(root) || resolve32(root).startsWith(out + "\\") || resolve32(root).startsWith(out + "/")) {
232004
+ throw new CliError(`The output folder is cleaned before a build: "${options.out}" holds the project itself.`);
232005
+ }
232006
+ const config = readAppConfigAt(root)?.web ?? {};
232007
+ const mode = config.urls === "path" ? "path" : "hash";
232008
+ const base = normalBase(config.base);
232009
+ const warnings = [];
232010
+ const assetUrl = (path2) => "assets" + encodeURI(path2);
232011
+ const collected = collectLocalEntries(root, manifest, "assets");
232012
+ const entries = collected.entries.filter((e2) => {
232013
+ const abs = e2.absPath ? resolve32(e2.absPath) : null;
232014
+ return !(abs && (abs === out || abs.startsWith(out + "\\") || abs.startsWith(out + "/")));
232015
+ });
232016
+ for (const w of collected.warnings)
232017
+ if (!/shaders compile on the server/.test(w))
232018
+ warnings.push(w);
232019
+ const fontsHeader = await applyProjectFonts(entries, {
232020
+ mode: "vendor",
232021
+ root,
232022
+ vendorDir: root,
232023
+ vendorPrefix: "",
232024
+ publicUrl: "",
232025
+ relative: true,
232026
+ makeResourceEntry: (path2, absPath) => ({ path: path2, type: "resource", fileSrc: assetUrl(path2), absPath })
232027
+ });
232028
+ const iconsHeader = await applyAppAssetIcons(entries);
232029
+ if (!options.entry) {
232030
+ const ambiguous = ambiguousEntryWarning(entries);
232031
+ if (ambiguous)
232032
+ warnings.push(ambiguous);
232033
+ }
232034
+ const compiled = await compileProject2({
232035
+ entries,
232036
+ name,
232037
+ publicUrl: "",
232038
+ entryOverride: options.entry,
232039
+ header: true,
232040
+ fontsHeader,
232041
+ iconsHeader,
232042
+ sourcemap: options.sourcemap === true,
232043
+ minify: options.minify,
232044
+ onWarning: (w) => warnings.push(w)
232045
+ });
232046
+ const js = compiled.replace(/^\/\/ last-updated: .*$/m, "// last-updated: -");
232047
+ const header = js.slice(0, 2000);
232048
+ if (!/^\/\/ gl: disable/m.test(header) || /^\/\/ 2d: enable/m.test(header)) {
232049
+ warnings.push("This project uses 3D or 2D scenes, which Web Lite does not show yet: the page runs its UI, a scene's calls fail. (`lecodes dev --web` runs the whole app on the web player.)");
232050
+ }
232051
+ rmSync14(out, { recursive: true, force: true });
232052
+ mkdirSync33(out, { recursive: true });
232053
+ cpSync6(lite, out, { recursive: true });
232054
+ const table2 = join51(out, "features.json");
232055
+ if (existsSync51(table2)) {
232056
+ const { features, fonts } = JSON.parse(readFileSync41(table2, "utf8"));
232057
+ if (config.font === "system")
232058
+ for (const file of fonts ?? [])
232059
+ rmSync14(join51(out, file), { force: true });
232060
+ for (const feature of features) {
232061
+ if (feature.uses.some((name2) => js.includes(name2)))
232062
+ continue;
232063
+ for (const file of feature.files)
232064
+ rmSync14(join51(out, file), { force: true });
232065
+ }
232066
+ rmSync14(table2);
232067
+ }
232068
+ const bundle = `app.${createHash9("sha256").update(js).digest("hex").slice(0, 10)}.js`;
232069
+ writeFileSync35(join51(out, bundle), js);
232070
+ for (const entry of entries) {
232071
+ if (entry.type !== "resource" || !entry.absPath || !entry.fileSrc)
232072
+ continue;
232073
+ if (!js.includes(entry.fileSrc))
232074
+ continue;
232075
+ const to2 = join51(out, "assets", ...entry.path.split("/").filter(Boolean));
232076
+ if (existsSync51(to2)) {
232077
+ warnings.push(`${entry.path}: a file of the page has this name in the built folder — the project's file was not copied`);
232078
+ continue;
232079
+ }
232080
+ mkdirSync33(dirname26(to2), { recursive: true });
232081
+ copyFileSync2(entry.absPath, to2);
232082
+ }
232083
+ const app = readAppConfigAt(root);
232084
+ let html = readFileSync41(join51(lite, "index.html"), "utf8");
232085
+ const head = [];
232086
+ head.push(`<meta name="theme-color" content="${escapeHtml(config.themeColor ?? "#000000")}" />`);
232087
+ if (app?.icon) {
232088
+ const icon = resolve32(root, app.icon);
232089
+ if (existsSync51(icon)) {
232090
+ const file = `icon${extname2(icon).toLowerCase() || ".png"}`;
232091
+ copyFileSync2(icon, join51(out, file));
232092
+ head.push(`<link rel="icon" href="${file}" />`, `<link rel="apple-touch-icon" href="${file}" />`);
232093
+ } else {
232094
+ warnings.push(`app.json icon "${app.icon}" is not there — the page has no icon`);
232095
+ }
232096
+ }
232097
+ if (mode === "path")
232098
+ head.unshift(`<base href="${escapeHtml(base)}" />`);
232099
+ const attrs = [
232100
+ `data-src="${bundle}"`,
232101
+ ...mode === "path" ? [`data-urls="path"`, `data-base="${escapeHtml(base)}"`] : [],
232102
+ ...config.font === "system" ? [`data-font="system"`] : []
232103
+ ].join(" ");
232104
+ const replace = (from, to2) => {
232105
+ const next = html.replace(from, to2);
232106
+ if (next === html)
232107
+ throw new CliError(`The page of Web Lite has no ${String(from)} to fill — this CLI and its page are of different versions.`);
232108
+ html = next;
232109
+ };
232110
+ replace(/<title>[^<]*<\/title>/, `<title>${escapeHtml(config.title ?? app?.name ?? name)}</title>`);
232111
+ replace(`<div id="app"></div>`, `<div id="app" ${attrs}></div>`);
232112
+ replace("</head>", ` ${head.join(`
232113
+ `)}
232114
+ </head>`);
232115
+ if (config.lang)
232116
+ replace(/<html lang="[^"]*">/, `<html lang="${escapeHtml(config.lang)}">`);
232117
+ writeFileSync35(join51(out, "index.html"), html);
232118
+ if (mode === "path")
232119
+ writeFileSync35(join51(out, "404.html"), html);
232120
+ return { out, bundle, files: walk(out), warnings };
232121
+ };
232122
+
232123
+ // src/web/preview.ts
232124
+ import { createReadStream as createReadStream2, existsSync as existsSync52, statSync as statSync21 } from "node:fs";
232125
+ import { createServer as createServer4 } from "node:http";
232126
+ import { join as join52, resolve as resolve33 } from "node:path";
232127
+ var startPreview = async (dir, port) => {
232128
+ const root = resolve33(dir);
232129
+ const fallback = existsSync52(join52(root, "404.html")) ? join52(root, "index.html") : null;
232130
+ const server2 = createServer4((req, res) => {
232131
+ let path2;
232132
+ try {
232133
+ path2 = decodeURIComponent(new URL(req.url ?? "/", "http://localhost").pathname);
232134
+ } catch {
232135
+ res.writeHead(400);
232136
+ res.end();
232137
+ return;
232138
+ }
232139
+ const parts = path2.split("/").filter(Boolean);
232140
+ if (parts.some((p2) => p2 === ".." || p2 === ".")) {
232141
+ res.writeHead(400);
232142
+ res.end();
232143
+ return;
232144
+ }
232145
+ let file = join52(root, ...parts);
232146
+ if (existsSync52(file) && statSync21(file).isDirectory())
232147
+ file = join52(file, "index.html");
232148
+ let status3 = 200;
232149
+ if (!existsSync52(file) || !statSync21(file).isFile()) {
232150
+ if (!fallback || /\.[a-z0-9]+$/i.test(parts[parts.length - 1] ?? "")) {
232151
+ res.writeHead(404, { "content-type": "text/plain" });
232152
+ res.end("Not found");
232153
+ return;
232154
+ }
232155
+ file = fallback;
232156
+ }
232157
+ res.writeHead(status3, {
232158
+ "content-type": contentType(file),
232159
+ "cache-control": /[.-][0-9a-f]{8,}\.[a-z0-9]+$/i.test(file) ? "max-age=31536000, immutable" : "no-cache"
232160
+ });
232161
+ createReadStream2(file).pipe(res);
232162
+ });
232163
+ await new Promise((done, reject) => {
232164
+ server2.once("error", reject);
232165
+ server2.listen(port, "0.0.0.0", () => {
232166
+ server2.removeListener("error", reject);
232167
+ done();
232168
+ });
232169
+ });
232170
+ const address = server2.address();
232171
+ const bound = typeof address === "object" && address !== null ? address.port : port;
232172
+ return { url: `http://localhost:${bound}/`, port: bound, close: () => server2.close() };
232173
+ };
232174
+
232175
+ // src/commands/web.ts
232176
+ init_shared();
232177
+ var DEFAULT_PORT4 = 4488;
232178
+ var DEFAULT_OUT = "dist/web";
232179
+ var project = () => {
232180
+ let root;
232181
+ let manifest;
232182
+ try {
232183
+ root = findProjectRoot(process.cwd());
232184
+ manifest = readManifest(root);
232185
+ } catch {
232186
+ root = process.cwd();
232187
+ manifest = null;
232188
+ }
232189
+ return { root, manifest, name: manifest?.name ?? basename25(root) };
232190
+ };
232191
+ var litePage = () => {
232192
+ const lite = resolveLiteRunner(note);
232193
+ if (!lite)
232194
+ throw new CliError("No Web Lite page found: this CLI has no runtime/web-lite (a monorepo checkout needs `bun run build` in hosts/web-lite).");
232195
+ return lite;
232196
+ };
232197
+ var kb = (bytes) => bytes < 1024 ? `${bytes} B` : bytes < 1024 * 1024 ? `${(bytes / 1024).toFixed(1)} KB` : `${(bytes / 1024 / 1024).toFixed(2)} MB`;
232198
+ var dev = defineCommand({
232199
+ name: "dev",
232200
+ summary: "Open the project as a web page, reloaded on every save",
232201
+ description: "Serves the project's bundle and assets with the page of Web Lite around them; a save recompiles and runs the app again, the page's console output and errors stream back here. The address's hash is the route the app is opened with (#/product/42 → app.launchUrl).",
232202
+ flags: {
232203
+ port: num("server port", { default: DEFAULT_PORT4 }),
232204
+ entry: bundleFlags.entry,
232205
+ "no-open": bool("don't open the browser")
232206
+ },
232207
+ examples: ["lecodes web dev", "lecodes web dev --port 8080 --no-open"],
232208
+ run: async ({ flags }) => {
232209
+ const { root, manifest, name } = project();
232210
+ const lite = litePage();
232211
+ const port = flags.port || DEFAULT_PORT4;
232212
+ const lan = lanCandidates()[0]?.address;
232213
+ const origin = `http://localhost:${port}`;
232214
+ const server2 = await startDevServer({
232215
+ root,
232216
+ manifest,
232217
+ name,
232218
+ origin,
232219
+ port,
232220
+ token: loadDevToken(root),
232221
+ entryOverride: flags.entry,
232222
+ lite,
232223
+ log: note
232224
+ }).catch((e2) => {
232225
+ const msg = e2 instanceof Error ? e2.message : String(e2);
232226
+ throw new CliError(msg.includes("EADDRINUSE") ? `Port ${port} is already in use — pass --port <n>.` : msg);
232227
+ });
232228
+ note("Compiling…");
232229
+ const first = await server2.recompile();
232230
+ if (first.ok)
232231
+ for (const w of first.warnings)
232232
+ warnErr(w);
232233
+ else
232234
+ warnErr(`Compile failed — fix it and save; the page loads once it compiles:
232235
+ ${first.error}`);
232236
+ const watcher = watchProject(root, async () => {
232237
+ const result = await server2.recompile();
232238
+ if (!result.ok)
232239
+ warnErr(`Compile failed (the page keeps the last good version):
232240
+ ${result.error}`);
232241
+ else if (result.changed) {
232242
+ note(`✓ v${result.v}`);
232243
+ for (const w of result.warnings)
232244
+ warnErr(w);
232245
+ }
232246
+ }, { warn: warnErr, debug: process.env.LECODES_DEV_DEBUG === "1" ? note : undefined });
232247
+ const url = server2.liteUrl;
232248
+ log("");
232249
+ log(`${c.bold("LeCodes Web")} — ${name}`);
232250
+ log("");
232251
+ log(`In the browser: ${c.bold(url)}`);
232252
+ if (lan)
232253
+ note(`On this network: ${url.replace("http://localhost:", `http://${lan}:`)}`);
232254
+ log("");
232255
+ log(`Watching ${root} — save a file to reload. Ctrl+C to stop.`);
232256
+ if (!flags["no-open"])
232257
+ openBrowser(url);
232258
+ process.on("SIGINT", () => {
232259
+ watcher.close();
232260
+ server2.close();
232261
+ process.exit(0);
232262
+ });
232263
+ }
232264
+ });
232265
+ var build2 = defineCommand({
232266
+ name: "build",
232267
+ summary: "Build the project into a folder any static hosting serves",
232268
+ description: 'Writes the page, the bundle, the page\'s own files and the assets the bundle names into one folder (dist/web). Every url in it is relative: the folder works at a domain\'s root and inside a folder of one.\n\napp.json `web` sets the page: `title`, `themeColor`, `lang`, and `urls` — "hash" (default: a route is `#/product/42`, which any hosting serves) or "path" (`/product/42` under `base`: the hosting has to answer every path with the page; a 404.html is written for the ones that do it that way).',
232269
+ flags: {
232270
+ out: str("the folder to write (cleaned first)", { alias: "o", value: "<dir>", default: DEFAULT_OUT }),
232271
+ entry: bundleFlags.entry,
232272
+ sourcemap: bool("inline the source map (errors name the app's own lines; the bundle grows by its sources)"),
232273
+ "no-minify": bundleFlags["no-minify"]
232274
+ },
232275
+ examples: ["lecodes web build", "lecodes web build -o public"],
232276
+ run: async ({ flags }) => {
232277
+ const { root, manifest, name } = project();
232278
+ const lite = litePage();
232279
+ note("Building…");
232280
+ const result = await buildWeb({
232281
+ root,
232282
+ manifest,
232283
+ name,
232284
+ lite,
232285
+ out: flags.out,
232286
+ entry: flags.entry,
232287
+ sourcemap: flags.sourcemap,
232288
+ minify: flags["no-minify"] ? false : undefined
232289
+ });
232290
+ for (const w of result.warnings)
232291
+ warnErr(w);
232292
+ const total = result.files.reduce((sum, f5) => sum + f5.bytes, 0);
232293
+ const shown = relative13(process.cwd(), result.out) || ".";
232294
+ log("");
232295
+ for (const f5 of [...result.files].sort((a2, b) => b.bytes - a2.bytes).slice(0, 8))
232296
+ log(` ${kb(f5.bytes).padStart(10)} ${f5.path}`);
232297
+ if (result.files.length > 8)
232298
+ log(c.dim(` …and ${result.files.length - 8} more`));
232299
+ log("");
232300
+ log(`${c.green("✓")} ${c.bold(shown)} — ${result.files.length} files, ${kb(total)}`);
232301
+ note(`Look at it: lecodes web preview${flags.out === DEFAULT_OUT ? "" : ` ${flags.out}`}`);
232302
+ }
232303
+ });
232304
+ var preview = defineCommand({
232305
+ name: "preview",
232306
+ summary: "Serve a built folder, as a static hosting would",
232307
+ usage: "[dir]",
232308
+ flags: {
232309
+ port: num("server port", { default: DEFAULT_PORT4 + 1 }),
232310
+ "no-open": bool("don't open the browser")
232311
+ },
232312
+ examples: ["lecodes web preview", "lecodes web preview public --port 8080"],
232313
+ run: async ({ args, flags }) => {
232314
+ const dir = args[0] ?? DEFAULT_OUT;
232315
+ const server2 = await startPreview(dir, flags.port || DEFAULT_PORT4 + 1).catch((e2) => {
232316
+ const msg = e2 instanceof Error ? e2.message : String(e2);
232317
+ throw new CliError(msg.includes("EADDRINUSE") ? `Port ${flags.port} is already in use — pass --port <n>.` : msg);
232318
+ });
232319
+ log(`${c.bold("LeCodes Web")} — ${dir}`);
232320
+ log(`In the browser: ${c.bold(server2.url)}`);
232321
+ log("Ctrl+C to stop.");
232322
+ if (!flags["no-open"])
232323
+ openBrowser(server2.url);
232324
+ process.on("SIGINT", () => {
232325
+ server2.close();
232326
+ process.exit(0);
232327
+ });
232328
+ }
232329
+ });
232330
+ var web_default = defineCommand({
232331
+ name: "web",
232332
+ summary: "The project as a web page (Web Lite): serve it with reload on save, build it for a hosting",
232333
+ description: "Web Lite shows the app with the browser's own layout, text and scroll: what a site ships. UI apps for now — a project's 3D and 2D scenes are not shown yet.",
232334
+ commands: [dev, build2, preview]
232335
+ });
232336
+
231452
232337
  // src/commands/index.ts
231453
232338
  var commandGroups = (opts) => [
231454
232339
  { title: "Project", commands: [init_default, create_default, clone_default, link_default, status_default, diff_default, pull_default, push_default, login_default] },
231455
232340
  { title: "Run & test", commands: [
231456
232341
  dev_default,
232342
+ web_default,
231457
232343
  compile_default,
231458
232344
  render_default,
231459
232345
  test_default,
@@ -231485,9 +232371,9 @@ var commandGroups = (opts) => [
231485
232371
  // src/index.ts
231486
232372
  init_peers();
231487
232373
  var STANDALONE2 = (() => {
231488
- const dir = dirname25(process.execPath);
232374
+ const dir = dirname27(process.execPath);
231489
232375
  const chisel = process.platform === "win32" ? "chisel.exe" : "chisel";
231490
- return existsSync50(join50(dir, "runtime")) && existsSync50(join50(dir, chisel));
232376
+ return existsSync53(join53(dir, "runtime")) && existsSync53(join53(dir, chisel));
231491
232377
  })();
231492
232378
  var INSTALLED = STANDALONE2 && (() => {
231493
232379
  const exe = process.platform === "win32" ? "lecodes.exe" : "lecodes";
@@ -231502,8 +232388,8 @@ var INSTALLED = STANDALONE2 && (() => {
231502
232388
  if (!d)
231503
232389
  return false;
231504
232390
  try {
231505
- const cand = join50(d, exe);
231506
- return existsSync50(cand) && realpathSync2(cand) === self2;
232391
+ const cand = join53(d, exe);
232392
+ return existsSync53(cand) && realpathSync2(cand) === self2;
231507
232393
  } catch {
231508
232394
  return false;
231509
232395
  }