lecodes-cli 2.0.1 → 2.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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.1",
55
+ version: "2.0.3",
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.1",
76
- "lecodes-3d-editor": "^0.7.0",
75
+ "lecodes-design": "^0.11.0",
76
+ "lecodes-headless": "^2.0.3",
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.1", sdkMajor = (version) => {
130
+ var SDK_VERSION = "2.0.3", 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,24 @@ 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`, archivedVersion = (name) => {
212723
+ if (!ARCHIVED_PEERS.has(name))
212724
+ return null;
212725
+ if (name === "lecodes-headless")
212726
+ return package_default.version;
212727
+ const range = package_default.peerDependencies?.[name];
212728
+ const version = range?.replace(/^[\^~>=\s]+/, "");
212729
+ return version && /^\d+\.\d+\.\d+/.test(version) ? version : null;
212730
+ }, archiveSpec = (name) => {
212731
+ const version = archivedVersion(name);
212732
+ return version ? archiveUrl(name, version) : null;
212733
+ };
212734
+ var init_archives = __esm(() => {
212735
+ init_package();
212736
+ ARCHIVED_PEERS = new Set(["lecodes-headless", "lecodes-design", "lecodes-3d-editor"]);
212737
+ });
212738
+
212719
212739
  // src/hosts/peerInstall.ts
212720
212740
  import { spawnSync as spawnSync6 } from "node:child_process";
212721
212741
  import { existsSync as existsSync23, mkdirSync as mkdirSync15, readFileSync as readFileSync19, writeFileSync as writeFileSync18 } from "node:fs";
@@ -212746,8 +212766,9 @@ var CANVAS_RANGE_FALLBACK = "^0.1.65", readJson2 = (path2) => {
212746
212766
  return null;
212747
212767
  }
212748
212768
  }, peerSpec = (name) => {
212749
- if (name === "lecodes-headless")
212750
- return `lecodes-headless@${package_default.version}`;
212769
+ const archive = archiveSpec(name);
212770
+ if (archive)
212771
+ return archive;
212751
212772
  if (name === "@napi-rs/canvas") {
212752
212773
  const renderer = readJson2(join24(peerDirInPrefix("lecodes-headless"), "package.json"));
212753
212774
  const range2 = renderer?.peerDependencies?.["@napi-rs/canvas"] ?? renderer?.optionalDependencies?.["@napi-rs/canvas"] ?? CANVAS_RANGE_FALLBACK;
@@ -212840,6 +212861,7 @@ var CANVAS_RANGE_FALLBACK = "^0.1.65", readJson2 = (path2) => {
212840
212861
  var init_peerInstall = __esm(() => {
212841
212862
  init_package();
212842
212863
  init_peers();
212864
+ init_archives();
212843
212865
  init_cli();
212844
212866
  });
212845
212867
 
@@ -213140,7 +213162,7 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, dirFlag, getContext = (dir) =>
213140
213162
  const mod = await loadPeer("lecodes-design", "server", { for: "lecodes design" });
213141
213163
  const missing = needs.filter((name) => typeof mod[name] !== "function");
213142
213164
  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`);
213165
+ throw new CliError(`This needs a newer lecodes-design — the installed one has no \`${missing[0]}\`. Run: lecodes update`);
213144
213166
  }
213145
213167
  return mod;
213146
213168
  }, requirePlatform = (ctx, verb) => {
@@ -213256,7 +213278,7 @@ var aiFlag, cliAuthor = (ai) => ai || process.env.CLAUDECODE ? { author: "Claude
213256
213278
  if (!platform)
213257
213279
  return null;
213258
213280
  if (typeof designServer.mirrorThreads !== "function") {
213259
- note("Comment sync needs a newer lecodes-design (npm install -g lecodes-design@latest) — serving without it.");
213281
+ note("Comment sync needs a newer lecodes-design (run: lecodes update) — serving without it.");
213260
213282
  return null;
213261
213283
  }
213262
213284
  let failures = 0;
@@ -213570,7 +213592,7 @@ var init_snapshot = __esm(() => {
213570
213592
  });
213571
213593
  });
213572
213594
 
213573
- // ../../node_modules/.bun/lecodes-design@0.9.0+a0cdf6553a4c6042/node_modules/lecodes-design/src/core/concept.ts
213595
+ // ../../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
213596
  var SPEC_TEMPLATE = `# Concept
213575
213597
 
213576
213598
  The narrative half of this design. \`meta.json\` + the canvas hold the *visual/flow* half (screens,
@@ -213606,7 +213628,7 @@ Behaviour that isn't visible in a single screen — validation, permissions, not
213606
213628
  cases — and the decisions you've made and why, so they aren't re-litigated later.
213607
213629
  `;
213608
213630
 
213609
- // ../../node_modules/.bun/lecodes-design@0.9.0+a0cdf6553a4c6042/node_modules/lecodes-design/src/templates.ts
213631
+ // ../../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
213632
  var SPEC_MD, META_JSON = `{
213611
213633
  "screens": {
213612
213634
  "home": { "pos": [0, 0], "desc": "Example screen — replace me", "role": "list" }
@@ -213703,9 +213725,9 @@ export default UIScreen([
213703
213725
  mb: spacing.lg,
213704
213726
  }),
213705
213727
  ]).style({ bgColor: colors.bg, p: spacing.md, pt: "safe-top" })
213706
- `, CLAUDE_MD = `# LeCodes Design — screen prototyping canvas
213728
+ `, CLAUDE_MD = `# le.codes Design — screen prototyping canvas
213707
213729
 
213708
- This folder is a **design canvas** for a LeCodes app: each file in \`screens/\` is one live,
213730
+ This folder is a **design canvas** for a le.codes app: each file in \`screens/\` is one live,
213709
213731
  executable screen shown on a spatial canvas in the browser (\`lecodes design\` runs the dev server).
213710
213732
  You (Claude) write the screens; the developer views them on the canvas and gives feedback.
213711
213733
  This is an intent-capture stage — screens run on inline mock data, before any backend exists.
@@ -213811,7 +213833,7 @@ keep in the chat.
213811
213833
  (entities + fields ARE the future schema — make them meaningful, not "Lorem ipsum").
213812
213834
  - **Reuse via shared/.** Design tokens and reusable components live in \`shared/\`. Use them in
213813
213835
  every screen. Promoting a repeated pattern into \`shared/\` is an explicit, deliberate step.
213814
- - The LeCodes UI API (\`UIScreen\`, \`UIColumn\`, \`UIRow\`, \`UIBox\`, \`UIText\`, \`UIImage\`,
213836
+ - The le.codes UI API (\`UIScreen\`, \`UIColumn\`, \`UIRow\`, \`UIBox\`, \`UIText\`, \`UIImage\`,
213815
213837
  \`UIButton\`, \`UIInput\`, \`UISpacer\`, \`UIScrollable\`, …) is **global** — no imports needed.
213816
213838
  Colors must be hex strings. This folder ships a \`tsconfig.json\` + bundled SDK types
213817
213839
  (\`.lecodes/types/\`), so the editor resolves those globals — and \`UINode\` / \`UINodeChild\` for
@@ -214033,7 +214055,7 @@ declare function assetIcon(id: string, opts?: { color?: string }): { readonly sv
214033
214055
  declare function font(id: string, opts?: { weights?: number[], italic?: boolean }): string
214034
214056
  `, README_HINT = `# design/
214035
214057
 
214036
- LeCodes Design canvas — run \`lecodes design\` here and open the printed URL.
214058
+ le.codes Design canvas — run \`lecodes design\` here and open the printed URL.
214037
214059
  See CLAUDE.md for the conventions (it's written for AI assistants, but it's the spec).
214038
214060
  `;
214039
214061
  var init_templates = __esm(() => {
@@ -214114,7 +214136,7 @@ var firstLine = (body, max = 64) => {
214114
214136
  const sync3 = flags["no-sync"] ? null : startCommentSync(ctx, designServer);
214115
214137
  log(`${c.bold("LeCodes Design")} — ${ctx.name}`);
214116
214138
  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)")}`);
214139
+ log(`MCP: ${c.bold(server2.url + "/mcp")}${renderer ? "" : c.dim(" (render_screen off — lecodes-headless is not installed: `lecodes render` installs it)")}`);
214118
214140
  log(`Watching: ${ctx.dirName}/ (screens hot-reload on save; Ctrl+C to stop)`);
214119
214141
  if (sync3)
214120
214142
  log(`Comments: ${c.dim("in sync with the platform — replies and resolves go back automatically")}`);
@@ -215391,8 +215413,8 @@ var require_main = __commonJS((exports, module) => {
215391
215413
  // src/index.ts
215392
215414
  init_package();
215393
215415
  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";
215416
+ import { existsSync as existsSync53, realpathSync as realpathSync2 } from "node:fs";
215417
+ import { delimiter as delimiter3, dirname as dirname27, join as join53 } from "node:path";
215396
215418
 
215397
215419
  // src/commands/app/index.ts
215398
215420
  init_manifest();
@@ -222047,8 +222069,7 @@ var desktop_default = defineCommand({
222047
222069
 
222048
222070
  // src/commands/dev.ts
222049
222071
  init_manifest();
222050
- init_ignore();
222051
- import { existsSync as existsSync34, mkdirSync as mkdirSync21, readFileSync as readFileSync26, watch, writeFileSync as writeFileSync24 } from "node:fs";
222072
+ import { existsSync as existsSync34, mkdirSync as mkdirSync21, readFileSync as readFileSync26, writeFileSync as writeFileSync24 } from "node:fs";
222052
222073
  import { randomBytes as randomBytes4 } from "node:crypto";
222053
222074
  import { networkInterfaces } from "node:os";
222054
222075
  import { basename as basename13, join as join35 } from "node:path";
@@ -222457,6 +222478,7 @@ var startDevServer = async (options) => {
222457
222478
  ` + renderPrelude({ origin: reqOrigin, base, transport, v: v2 });
222458
222479
  };
222459
222480
  const web = options.web ?? null;
222481
+ const lite = options.lite ?? null;
222460
222482
  const sendBuilt = (res, dir, rel) => {
222461
222483
  if (rel.split("/").some((seg) => seg === ".." || seg === "" || seg === "."))
222462
222484
  return sendText(res, 400, "text/plain", "Bad path");
@@ -222518,6 +222540,18 @@ var startDevServer = async (options) => {
222518
222540
  return redirect(`${base}/web/${query()}`);
222519
222541
  return sendBuilt(res, web.page, rel === "/" ? "index.html" : rel.slice(1));
222520
222542
  }
222543
+ if (route === "/lite" || route.startsWith("/lite/")) {
222544
+ if (!lite)
222545
+ return sendText(res, 404, "text/plain", "No Web Lite page with this CLI (lecodes-web-lite's dist).");
222546
+ const rel = route.slice("/lite".length);
222547
+ if (rel === "" || rel === "/" && !url.searchParams.has("dev")) {
222548
+ url.searchParams.set("dev", "..");
222549
+ res.writeHead(302, { location: `${base}/lite/?${url.searchParams.toString()}${url.hash}` });
222550
+ res.end();
222551
+ return;
222552
+ }
222553
+ return sendBuilt(res, lite, rel === "/" ? "index.html" : rel.slice(1));
222554
+ }
222521
222555
  if (route === "/version") {
222522
222556
  return sendJson(res, 200, { v });
222523
222557
  }
@@ -222601,6 +222635,7 @@ var startDevServer = async (options) => {
222601
222635
  return {
222602
222636
  url: `${origin}${base}/dev.js`,
222603
222637
  webUrl: web ? `${origin}${base}/web/` : null,
222638
+ liteUrl: lite ? `${origin}${base}/lite/` : null,
222604
222639
  base,
222605
222640
  port: boundPort,
222606
222641
  v: () => v,
@@ -222636,6 +222671,15 @@ var PAGE = {
222636
222671
  build: ["-c", "vite.page.config.ts"],
222637
222672
  sources: ["hosts/web-frame/src", "hosts/web-frame/page", "hosts/web-player/src/protocol.ts"]
222638
222673
  };
222674
+ var LITE = {
222675
+ pkg: "lecodes-web-lite",
222676
+ dist: "dist",
222677
+ entry: "lite.js",
222678
+ vendored: "web-lite",
222679
+ build: [],
222680
+ sources: ["hosts/web-lite/src", "hosts/web-lite/index.html", "renderers/web-lite/src", "renderers/web-shared/src", "hosts/web-player/src/dev.ts"]
222681
+ };
222682
+ var resolveLiteRunner = (log2 = () => {}) => resolve16(LITE, log2);
222639
222683
  var resolveWebRunner = (log2 = () => {}) => {
222640
222684
  const player = resolve16(PLAYER, log2);
222641
222685
  return player ? { player, page: resolve16(PAGE, log2) } : null;
@@ -222689,6 +222733,59 @@ var newest = (paths) => {
222689
222733
  return latest;
222690
222734
  };
222691
222735
 
222736
+ // src/dev/watchProject.ts
222737
+ init_ignore();
222738
+ init_paths();
222739
+ import { watch } from "node:fs";
222740
+ var DEBOUNCE_MS = 120;
222741
+ var SKIP_DIRS2 = new Set([".lecodes", ".git", "node_modules", "dist"]);
222742
+ var SKIP_FILES2 = new Set(["tsconfig.json", "jsconfig.json", ".gitignore", ".lecodesignore", ".DS_Store"]);
222743
+ var watchProject = (root, onChange, log2) => {
222744
+ const ignore = loadIgnore(root);
222745
+ const counts = (file) => {
222746
+ if (file === null)
222747
+ return true;
222748
+ const parts = file.replace(/\\/g, "/").split("/");
222749
+ if (parts.some((p2) => SKIP_DIRS2.has(p2)))
222750
+ return false;
222751
+ if (SKIP_FILES2.has(parts[parts.length - 1]))
222752
+ return false;
222753
+ try {
222754
+ if (ignore(toKey(file), false))
222755
+ return false;
222756
+ } catch {
222757
+ return false;
222758
+ }
222759
+ return true;
222760
+ };
222761
+ let timer = null;
222762
+ let watcher = null;
222763
+ try {
222764
+ watcher = watch(root, { recursive: true }, (_type, file) => {
222765
+ const name = file === null ? null : String(file);
222766
+ log2.debug?.(`[watch] event: ${name ?? "(null)"} → ${counts(name) ? "recompile" : "skip"}`);
222767
+ if (!counts(name))
222768
+ return;
222769
+ if (timer)
222770
+ clearTimeout(timer);
222771
+ timer = setTimeout(() => {
222772
+ timer = null;
222773
+ onChange();
222774
+ }, DEBOUNCE_MS);
222775
+ });
222776
+ } catch {
222777
+ log2.warn("fs.watch(recursive) unavailable on this platform — hot reload disabled (restart to pick up changes).");
222778
+ }
222779
+ watcher?.on("error", (e2) => log2.warn(`Watcher error — hot reload may have stopped (restart the command): ${e2.message}`));
222780
+ return {
222781
+ close() {
222782
+ if (timer)
222783
+ clearTimeout(timer);
222784
+ watcher?.close();
222785
+ }
222786
+ };
222787
+ };
222788
+
222692
222789
  // src/commands/dev.ts
222693
222790
  init_browserAuth();
222694
222791
 
@@ -222982,11 +223079,7 @@ var launchOnIos = (opts) => {
222982
223079
  // src/commands/dev.ts
222983
223080
  init_cli();
222984
223081
  init_shared();
222985
- init_paths();
222986
223082
  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
223083
  var VIRTUAL_IFACE = /wsl|docker|hyper-v|vethernet|virtualbox|vmware|tap|tun|zerotier|tailscale|loopback/i;
222991
223084
  var lanCandidates = () => {
222992
223085
  const nets = networkInterfaces();
@@ -223106,54 +223199,18 @@ var dev_default = defineCommand({
223106
223199
  warnErr(`Compile failed — fix it and save; devices connect once it compiles:
223107
223200
  ${first.error}`);
223108
223201
  }
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):
223202
+ const watcher = watchProject(root, async () => {
223203
+ const result = await server2.recompile();
223204
+ if (!result.ok) {
223205
+ warnErr(`Compile failed (devices keep the last good version):
223141
223206
  ${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}`));
223207
+ } else if (result.changed) {
223208
+ const devices = server2.clientCount();
223209
+ note(`✓ v${result.v} → ${devices} device${devices === 1 ? "" : "s"}`);
223210
+ for (const w of result.warnings)
223211
+ warnErr(w);
223212
+ }
223213
+ }, { warn: warnErr, debug: process.env.LECODES_DEV_DEBUG === "1" ? note : undefined });
223157
223214
  log("");
223158
223215
  log(`${c.bold("LeCodes Dev")} — ${name}`);
223159
223216
  log("");
@@ -223186,14 +223243,14 @@ ${result.error}`);
223186
223243
  session2 = launchOnAndroid({ port, url: androidUrl, serial: flags.serial, apk: flags.apk, packageName: flags.package });
223187
223244
  } catch (e2) {
223188
223245
  server2.close();
223189
- watcher?.close();
223246
+ watcher.close();
223190
223247
  throw e2;
223191
223248
  }
223192
223249
  note(`Android: ${session2.serial} → ${androidUrl} (adb reverse; device logs stream here as [device:log])`);
223193
223250
  const finish = (code) => {
223194
223251
  session2.stop();
223195
223252
  server2.close();
223196
- watcher?.close();
223253
+ watcher.close();
223197
223254
  process.exit(code);
223198
223255
  };
223199
223256
  process.on("SIGINT", () => {
@@ -223228,14 +223285,14 @@ ${crashes.join(`
223228
223285
  session2 = launchOnIos({ simulatorUrl: `http://127.0.0.1:${port}${path2}`, deviceUrl, udid: flags.udid, app: flags.app, bundleId: flags.bundle });
223229
223286
  } catch (e2) {
223230
223287
  server2.close();
223231
- watcher?.close();
223288
+ watcher.close();
223232
223289
  throw e2;
223233
223290
  }
223234
223291
  note(`iOS: ${session2.name} → ${session2.kind === "simulator" ? `http://127.0.0.1:${port}${path2}` : deviceUrl} (device logs stream here as [device:log])`);
223235
223292
  const finish = (code) => {
223236
223293
  session2.stop();
223237
223294
  server2.close();
223238
- watcher?.close();
223295
+ watcher.close();
223239
223296
  process.exit(code);
223240
223297
  };
223241
223298
  process.on("SIGINT", () => {
@@ -230703,8 +230760,391 @@ var status_default = defineCommand({
230703
230760
  });
230704
230761
 
230705
230762
  // 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";
230763
+ import { existsSync as existsSync48, mkdirSync as mkdirSync32, readFileSync as readFileSync40, readdirSync as readdirSync21 } from "node:fs";
230764
+ import { dirname as dirname24, resolve as resolve28 } from "node:path";
230765
+ init_manifest();
230766
+
230767
+ // src/hosts/liteRunner.ts
230768
+ init_cli();
230769
+ import { spawn as spawn6 } from "node:child_process";
230770
+ import { existsSync as existsSync47, mkdirSync as mkdirSync31, writeFileSync as writeFileSync34 } from "node:fs";
230771
+ import { createServer as createServer3 } from "node:net";
230772
+ import { dirname as dirname23, join as join48 } from "node:path";
230773
+ import { createInterface as createInterface2 } from "node:readline";
230774
+ init_distRoot();
230775
+ init_peers();
230776
+ var FRAME_MS2 = 16;
230777
+ var LOG_SLACK_MS = 3000;
230778
+ var NO_NODE = "--lite needs Node on the PATH: the browser is driven from a Node process.";
230779
+ 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.";
230780
+ var startDriver = async () => {
230781
+ const script = join48(distRoot, "src", "hosts", "liteBrowser.mjs");
230782
+ if (!existsSync47(script))
230783
+ throw new CliError("--lite is not available in this build of the CLI (its browser driver ships with the npm package).");
230784
+ const child = spawn6("node", [script], { stdio: ["pipe", "pipe", "inherit"] });
230785
+ const waiting = new Map;
230786
+ let onConsole = () => {};
230787
+ let next = 1;
230788
+ let dead = null;
230789
+ const fail = (error) => {
230790
+ dead = dead ?? error;
230791
+ for (const w of waiting.values())
230792
+ w.reject(dead);
230793
+ waiting.clear();
230794
+ };
230795
+ child.on("error", () => fail(new CliError(NO_NODE)));
230796
+ child.on("exit", () => fail(new Error("the browser's process ended")));
230797
+ createInterface2({ input: child.stdout }).on("line", (line) => {
230798
+ let message;
230799
+ try {
230800
+ message = JSON.parse(line);
230801
+ } catch {
230802
+ return;
230803
+ }
230804
+ if (message.event === "console") {
230805
+ onConsole(message.level ?? "log", message.text ?? "");
230806
+ return;
230807
+ }
230808
+ const w = message.id !== undefined ? waiting.get(message.id) : undefined;
230809
+ if (!w)
230810
+ return;
230811
+ waiting.delete(message.id);
230812
+ if (message.ok)
230813
+ w.resolve(message.value);
230814
+ else
230815
+ w.reject(new Error(message.error ?? "failed"));
230816
+ });
230817
+ const call = (op, args = {}) => new Promise((resolve28, reject) => {
230818
+ if (dead) {
230819
+ reject(dead);
230820
+ return;
230821
+ }
230822
+ const id = next++;
230823
+ waiting.set(id, { resolve: resolve28, reject });
230824
+ child.stdin.write(JSON.stringify({ id, op, args }) + `
230825
+ `);
230826
+ });
230827
+ const from = [
230828
+ join48(process.cwd(), "package.json"),
230829
+ join48(distRoot, "package.json"),
230830
+ join48(peersDir(), "package.json"),
230831
+ join48(distRoot, "..", "..", "engines", "gl-lite", "package.json")
230832
+ ];
230833
+ try {
230834
+ await call("launch", { from });
230835
+ } catch (e2) {
230836
+ child.kill();
230837
+ if (e2 instanceof CliError)
230838
+ throw e2;
230839
+ const message = e2 instanceof Error ? e2.message : String(e2);
230840
+ throw new CliError(message === "NO_PLAYWRIGHT" ? NO_PLAYWRIGHT : `Chromium did not start (${message}) — run "npx playwright install chromium" once.`);
230841
+ }
230842
+ return {
230843
+ open(width, height, listener) {
230844
+ onConsole = listener;
230845
+ return call("open", { width, height });
230846
+ },
230847
+ goto: (url) => call("goto", { url }),
230848
+ waitFor: (expression, timeout) => call("waitFor", { expression, timeout }),
230849
+ evaluate: (expression) => call("evaluate", { expression }),
230850
+ timeout: (ms) => call("timeout", { ms }),
230851
+ mouse: (action, x2, y2) => call("mouse", { action, x: x2, y: y2 }),
230852
+ keyboard: (action, text2) => call("keyboard", { action, text: text2 }),
230853
+ screenshot: (path2, clip) => call("screenshot", { path: path2, clip }),
230854
+ async close() {
230855
+ await call("close").catch(() => {});
230856
+ child.kill();
230857
+ }
230858
+ };
230859
+ };
230860
+ var freePort = () => new Promise((resolve28, reject) => {
230861
+ const probe = createServer3();
230862
+ probe.once("error", reject);
230863
+ probe.listen(0, "127.0.0.1", () => {
230864
+ const address = probe.address();
230865
+ const port = typeof address === "object" && address ? address.port : 0;
230866
+ probe.close(() => resolve28(port));
230867
+ });
230868
+ });
230869
+ var startLiteSession = async (options) => {
230870
+ const port = await freePort();
230871
+ const server2 = await startDevServer({
230872
+ root: options.root,
230873
+ manifest: options.manifest,
230874
+ name: options.name,
230875
+ lite: options.lite,
230876
+ origin: `http://127.0.0.1:${port}`,
230877
+ bind: "127.0.0.1",
230878
+ port,
230879
+ token: "test",
230880
+ entryOverride: options.entry
230881
+ });
230882
+ const compiled = await server2.recompile();
230883
+ if (!compiled.ok) {
230884
+ server2.close();
230885
+ throw new CliError(`Compile failed:
230886
+ ${compiled.error}`);
230887
+ }
230888
+ let driver;
230889
+ try {
230890
+ driver = await startDriver();
230891
+ } catch (e2) {
230892
+ server2.close();
230893
+ throw e2;
230894
+ }
230895
+ return {
230896
+ run: (plan, runOptions) => runPlan(driver, `${server2.liteUrl}?dev=..&test`, plan, runOptions),
230897
+ async close() {
230898
+ await driver.close();
230899
+ server2.close();
230900
+ }
230901
+ };
230902
+ };
230903
+ var parseArgs2 = (rest) => {
230904
+ const out = [];
230905
+ let i2 = 0;
230906
+ while (i2 < rest.length) {
230907
+ if (rest[i2] === " ") {
230908
+ i2++;
230909
+ continue;
230910
+ }
230911
+ if (rest[i2] === '"') {
230912
+ let text2 = "";
230913
+ i2++;
230914
+ while (i2 < rest.length && rest[i2] !== '"') {
230915
+ if (rest[i2] === "\\")
230916
+ i2++;
230917
+ text2 += rest[i2] ?? "";
230918
+ i2++;
230919
+ }
230920
+ i2++;
230921
+ out.push(text2);
230922
+ continue;
230923
+ }
230924
+ const end = rest.indexOf(" ", i2);
230925
+ const token = rest.slice(i2, end < 0 ? rest.length : end);
230926
+ const n2 = Number(token);
230927
+ out.push(Number.isFinite(n2) && token !== "" ? n2 : token);
230928
+ i2 = end < 0 ? rest.length : end;
230929
+ }
230930
+ return out;
230931
+ };
230932
+ var unwire = (text2) => text2.replace(/\\(n|\\)/g, (_, c4) => c4 === "n" ? `
230933
+ ` : "\\");
230934
+ var LEVEL = { log: "DEBUG", debug: "DEBUG", info: "INFO", warning: "WARN", warn: "WARN", error: "ERROR" };
230935
+ var ON_SHOW = ".lc-root > .lc:not(.lc-covered)";
230936
+ var FRAME = "new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(() => r(null))))";
230937
+ 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"`;
230938
+ var runPlan = async (driver, url, plan, options) => {
230939
+ const output = [];
230940
+ const seen = [];
230941
+ const say = (line) => {
230942
+ output.push(line);
230943
+ };
230944
+ await driver.open(options.width, options.height, (level, text2) => {
230945
+ seen.push(text2);
230946
+ say(`[${LEVEL[level] ?? "DEBUG"}] ${text2}`);
230947
+ options.onConsole?.(level, text2);
230948
+ });
230949
+ const expectOf = new Map;
230950
+ for (const step of plan.steps)
230951
+ if (step.expectLog !== undefined)
230952
+ for (const line of step.lines)
230953
+ expectOf.set(line, step.expectLog);
230954
+ const dump = () => driver.evaluate("window.lecodesTest.dump()");
230955
+ const rectOf = (id) => driver.evaluate(`window.lecodesTest.rect(${id})`);
230956
+ const nodeOf = async (name) => {
230957
+ const node = findDumpTarget(parseTreeDump(await dump()), name);
230958
+ if (!node)
230959
+ throw new Error(`no element "${name}" on the screen`);
230960
+ return node;
230961
+ };
230962
+ const pointOf = async (args, at2) => {
230963
+ const first = args[at2];
230964
+ if (typeof first === "number")
230965
+ return { point: [first, Number(args[at2 + 1])], next: at2 + 2 };
230966
+ const rect = await rectOf((await nodeOf(String(first))).id);
230967
+ if (!rect || rect[2] === 0 || rect[3] === 0)
230968
+ throw new Error(`"${first}" is not laid out`);
230969
+ return { point: [rect[0] + rect[2] / 2, rect[1] + rect[3] / 2], next: at2 + 1 };
230970
+ };
230971
+ const frame = () => driver.evaluate(FRAME);
230972
+ const waitFor = async (ms, line) => {
230973
+ const expected = expectOf.get(line);
230974
+ if (expected === undefined) {
230975
+ await driver.timeout(ms);
230976
+ return;
230977
+ }
230978
+ const until = Date.now() + ms + LOG_SLACK_MS;
230979
+ while (Date.now() < until) {
230980
+ if (seen.some((text2) => text2.includes(expected)))
230981
+ return;
230982
+ await driver.timeout(FRAME_MS2);
230983
+ }
230984
+ };
230985
+ let clip = null;
230986
+ const play = async (op, rest, line) => {
230987
+ const args = parseArgs2(rest);
230988
+ switch (op) {
230989
+ case "frames":
230990
+ await waitFor(Math.max(0, Number(args[0])) * FRAME_MS2, line);
230991
+ return;
230992
+ case "wait":
230993
+ await waitFor(Math.max(0, Number(args[0])), line);
230994
+ return;
230995
+ case "tap": {
230996
+ const { point } = await pointOf(args, 0);
230997
+ await driver.mouse("move", point[0], point[1]);
230998
+ await driver.mouse("down");
230999
+ await driver.mouse("up");
231000
+ await frame();
231001
+ return;
231002
+ }
231003
+ case "hold": {
231004
+ const { point, next } = await pointOf(args, 0);
231005
+ await driver.mouse("move", point[0], point[1]);
231006
+ await driver.mouse("down");
231007
+ await driver.timeout(Number(args[next]) || 0);
231008
+ await driver.mouse("up");
231009
+ await frame();
231010
+ return;
231011
+ }
231012
+ case "drag": {
231013
+ const from = await pointOf(args, 0);
231014
+ const to2 = await pointOf(args, from.next);
231015
+ const ms = Number(args[to2.next]) || 300;
231016
+ const steps = Math.max(2, Math.round(ms / FRAME_MS2));
231017
+ await driver.mouse("move", from.point[0], from.point[1]);
231018
+ await driver.mouse("down");
231019
+ for (let i2 = 1;i2 <= steps; i2++) {
231020
+ const t2 = i2 / steps;
231021
+ await driver.mouse("move", from.point[0] + (to2.point[0] - from.point[0]) * t2, from.point[1] + (to2.point[1] - from.point[1]) * t2);
231022
+ await driver.timeout(ms / steps);
231023
+ }
231024
+ await driver.mouse("up");
231025
+ await frame();
231026
+ return;
231027
+ }
231028
+ case "mouseto": {
231029
+ const { point } = await pointOf(args, 0);
231030
+ await driver.mouse("move", point[0], point[1]);
231031
+ await frame();
231032
+ return;
231033
+ }
231034
+ case "type":
231035
+ await driver.keyboard("type", unwire(rest));
231036
+ await frame();
231037
+ return;
231038
+ case "fill": {
231039
+ const value = rest === "" ? undefined : unwire(rest);
231040
+ const count = await driver.evaluate(`(() => {
231041
+ window.__lecodesFill = [...document.querySelectorAll("${ON_SHOW} input.lc, ${ON_SHOW} textarea.lc")].filter((f) => f.value === "")
231042
+ return window.__lecodesFill.length
231043
+ })()`);
231044
+ for (let i2 = 0;i2 < count; i2++) {
231045
+ const text2 = value ?? await driver.evaluate(`(${FILL_VALUE})(window.__lecodesFill[${i2}])`);
231046
+ await driver.evaluate(`window.__lecodesFill[${i2}].focus()`);
231047
+ await driver.keyboard("type", text2);
231048
+ }
231049
+ await frame();
231050
+ return;
231051
+ }
231052
+ case "scroll": {
231053
+ const by = Number(args[0]);
231054
+ const name = typeof args[1] === "string" ? args[1] : null;
231055
+ const target = name === null ? `document.querySelector("${ON_SHOW} .lc-scrollable, ${ON_SHOW} .lc-vlist")` : `document.querySelector('[data-node="${(await nodeOf(name)).id}"]')`;
231056
+ const found = await driver.evaluate(`(() => {
231057
+ const el = ${target}
231058
+ if (!el) return false
231059
+ const style = getComputedStyle(el)
231060
+ if (style.overflowX !== "hidden" && style.overflowY === "hidden") el.scrollLeft += ${by}
231061
+ else el.scrollTop += ${by}
231062
+ return true
231063
+ })()`);
231064
+ if (!found)
231065
+ throw new Error(name === null ? "no scrollable on the screen" : `"${name}" is not there`);
231066
+ await frame();
231067
+ return;
231068
+ }
231069
+ case "key": {
231070
+ await driver.keyboard("down", String(args[0]));
231071
+ await driver.timeout(Number(args[1]) || 0);
231072
+ await driver.keyboard("up", String(args[0]));
231073
+ return;
231074
+ }
231075
+ case "keydown":
231076
+ await driver.keyboard("down", String(args[0]));
231077
+ return;
231078
+ case "keyup":
231079
+ await driver.keyboard("up", String(args[0]));
231080
+ return;
231081
+ case "wheel":
231082
+ await driver.mouse("wheel", Number(args[0]) * 100, Number(args[1]) * 100);
231083
+ await frame();
231084
+ return;
231085
+ case "dump": {
231086
+ const file = rest.trim();
231087
+ mkdirSync31(dirname23(file), { recursive: true });
231088
+ writeFileSync34(file, await dump());
231089
+ return;
231090
+ }
231091
+ case "clip": {
231092
+ if (rest.trim() === "off") {
231093
+ clip = null;
231094
+ return;
231095
+ }
231096
+ if (typeof args[0] === "number") {
231097
+ clip = { x: args[0], y: Number(args[1]), width: Number(args[2]), height: Number(args[3]) };
231098
+ return;
231099
+ }
231100
+ const rect = await rectOf((await nodeOf(String(args[0]))).id);
231101
+ if (!rect)
231102
+ throw new Error(`"${args[0]}" is not laid out`);
231103
+ clip = { x: rect[0], y: rect[1], width: rect[2], height: rect[3] };
231104
+ return;
231105
+ }
231106
+ case "shot": {
231107
+ const file = rest.trim();
231108
+ mkdirSync31(dirname23(file), { recursive: true });
231109
+ await driver.screenshot(file, clip);
231110
+ return;
231111
+ }
231112
+ default:
231113
+ throw new Error(`"${op}" is not available on Web Lite (it has no 3D scene, no gamepad)`);
231114
+ }
231115
+ };
231116
+ try {
231117
+ await driver.goto(url);
231118
+ await driver.waitFor("window.lecodesTest && window.lecodesTest.ready === true", 20000);
231119
+ say("[script] loaded");
231120
+ const lines = plan.text.split(`
231121
+ `);
231122
+ for (let i2 = 0;i2 < lines.length; i2++) {
231123
+ const text2 = lines[i2].trim();
231124
+ if (text2 === "" || text2.startsWith("#"))
231125
+ continue;
231126
+ const space = text2.indexOf(" ");
231127
+ const op = space < 0 ? text2 : text2.slice(0, space);
231128
+ const rest = space < 0 ? "" : text2.slice(space + 1);
231129
+ try {
231130
+ await play(op, rest, i2 + 1);
231131
+ say(`[script] #${i2 + 1} done ${op}`);
231132
+ } catch (e2) {
231133
+ say(`[script] #${i2 + 1} error ${op}: ${e2 instanceof Error ? e2.message.split(`
231134
+ `)[0] : String(e2)}`);
231135
+ break;
231136
+ }
231137
+ }
231138
+ say("[script] end");
231139
+ } catch (e2) {
231140
+ say(`[ERROR] ${e2 instanceof Error ? e2.message.split(`
231141
+ `)[0] : String(e2)}`);
231142
+ }
231143
+ return output.join(`
231144
+ `);
231145
+ };
231146
+
231147
+ // src/commands/test.ts
230708
231148
  init_manifest();
230709
231149
  init_cli();
230710
231150
  init_shared();
@@ -230782,7 +231222,7 @@ var readFlowFile = (abs, displayName) => {
230782
231222
  name: file.name ?? displayName,
230783
231223
  kind: "file",
230784
231224
  steps: file.steps,
230785
- fixtures: resolveFixtures(file.fixtures, dirname23(abs)),
231225
+ fixtures: resolveFixtures(file.fixtures, dirname24(abs)),
230786
231226
  width: file.width,
230787
231227
  height: file.height,
230788
231228
  settle: file.settle,
@@ -230802,6 +231242,7 @@ var testFlags = {
230802
231242
  ...viewportFlags,
230803
231243
  ...bundleFlags,
230804
231244
  ...desktopRenderFlags,
231245
+ 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
231246
  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
231247
  };
230807
231248
  var test_default = defineCommand({
@@ -230817,6 +231258,11 @@ Design flows read tests/fixtures.json automatically (canned fetch responses) so
230817
231258
  });
230818
231259
  var runTests = async (args, flags) => {
230819
231260
  const sim = flags.sim;
231261
+ const lite = flags.lite;
231262
+ if (lite && (flags.desktop || sim))
231263
+ throw new CliError("--lite is a host of its own — drop --desktop / --sim.");
231264
+ if (lite && flags.flows)
231265
+ throw new CliError("--flows needs the headless renderer (design flows read expect.screen and fill inputs) — drop --lite.");
230820
231266
  const desktop = flags.desktop || sim;
230821
231267
  if (desktop && flags.flows)
230822
231268
  throw new CliError(`--flows needs the headless renderer (design flows read expect.screen and fill inputs) — drop --${sim ? "sim" : "desktop"}.`);
@@ -230828,7 +231274,7 @@ var runTests = async (args, flags) => {
230828
231274
  }
230829
231275
  const desktopRenderer = desktop ? resolveDesktopRenderer(desktopRoot, flags.renderer) : undefined;
230830
231276
  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"] });
231277
+ const headless = desktop || lite ? null : await compileHeadlessBundle({ entry: flags.entry, publicUrl: flags["public-url"], remoteAssets: flags["remote-assets"], waitNetwork: flags["wait-network"] });
230832
231278
  const warnings = headless?.warnings ?? [];
230833
231279
  const useLocalAssets = headless?.useLocalAssets ?? false;
230834
231280
  const js = headless?.js ?? "";
@@ -230842,7 +231288,7 @@ var runTests = async (args, flags) => {
230842
231288
  root = process.cwd();
230843
231289
  }
230844
231290
  }
230845
- const renderer = desktop ? null : await requireRenderer();
231291
+ const renderer = desktop || lite ? null : await requireRenderer();
230846
231292
  const nativeFrames = flags.frames || 60;
230847
231293
  const nativeFixedDt = flags["fixed-dt"] ?? 16;
230848
231294
  if (nativeFixedDt < 0 || nativeFixedDt > 1000)
@@ -230856,28 +231302,28 @@ var runTests = async (args, flags) => {
230856
231302
  if (args.length > 0) {
230857
231303
  for (const given of args) {
230858
231304
  const candidates = [resolve28(given), resolve28(testsDir, given), resolve28(testsDir, `${given}.flow.json`)];
230859
- const abs = candidates.find((p2) => existsSync47(p2));
231305
+ const abs = candidates.find((p2) => existsSync48(p2));
230860
231306
  if (!abs)
230861
231307
  throw new CliError(`No such test: "${given}" (looked in ${testsDir}).`);
230862
231308
  cases.push(readFlowFile(abs, given));
230863
231309
  }
230864
- } else if (existsSync47(testsDir)) {
231310
+ } else if (existsSync48(testsDir)) {
230865
231311
  for (const f5 of readdirSync21(testsDir).filter((f6) => f6.endsWith(".flow.json")).sort()) {
230866
231312
  cases.push(readFlowFile(resolve28(testsDir, f5), f5));
230867
231313
  }
230868
231314
  }
230869
231315
  const defaultPack = resolve28(testsDir, "fixtures.json");
230870
- const flowFixtures = flags.fixtures !== undefined ? resolveFixtures(flags.fixtures, ".") : existsSync47(defaultPack) ? resolveFixtures("fixtures.json", testsDir) : undefined;
231316
+ const flowFixtures = flags.fixtures !== undefined ? resolveFixtures(flags.fixtures, ".") : existsSync48(defaultPack) ? resolveFixtures("fixtures.json", testsDir) : undefined;
230871
231317
  const flowsOnly = flags.flows;
230872
231318
  const wantFlows = !flags["no-flows"] && args.length === 0;
230873
231319
  if (flowsOnly)
230874
231320
  cases.length = 0;
230875
231321
  let skipped = [];
230876
231322
  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).");
231323
+ if ((desktop || lite) && wantFlows && existsSync48(designMetaPath)) {
231324
+ note("Design-derived flows skipped with --desktop / --lite (they read expect.screen and fill inputs; run `lecodes test` headless for them).");
230879
231325
  }
230880
- if (!desktop && (wantFlows || flowsOnly) && existsSync47(designMetaPath)) {
231326
+ if (!desktop && !lite && (wantFlows || flowsOnly) && existsSync48(designMetaPath)) {
230881
231327
  let meta;
230882
231328
  try {
230883
231329
  meta = JSON.parse(readFileSync40(designMetaPath, "utf8"));
@@ -230917,7 +231363,7 @@ var runTests = async (args, flags) => {
230917
231363
  const reportCase = (result) => {
230918
231364
  const skippedCount = result.steps.reduce((n2, s2) => n2 + (s2.skipped?.length ?? 0), 0);
230919
231365
  if (result.ok) {
230920
- out(`${c.green("✓")} ${result.name}${skippedCount > 0 ? c.dim(` (${skippedCount} semantic check${skippedCount === 1 ? "" : "s"} skipped on --desktop)`) : ""}`);
231366
+ out(`${c.green("✓")} ${result.name}${skippedCount > 0 ? c.dim(` (${skippedCount} check${skippedCount === 1 ? "" : "s"} skipped on this host)`) : ""}`);
230921
231367
  } else {
230922
231368
  out(`${c.red("✗")} ${result.name}`);
230923
231369
  if (result.error)
@@ -230936,8 +231382,8 @@ var runTests = async (args, flags) => {
230936
231382
  const plan = translateScenario(kase.steps, { outDir: caseDir, headroomFrames: nativeFrames, sim });
230937
231383
  for (const st of plan.steps)
230938
231384
  if (st.screenshot)
230939
- mkdirSync31(dirname23(st.screenshot), { recursive: true });
230940
- mkdirSync31(caseDir, { recursive: true });
231385
+ mkdirSync32(dirname24(st.screenshot), { recursive: true });
231386
+ mkdirSync32(caseDir, { recursive: true });
230941
231387
  const finalPng = resolve28(caseDir, "final.png");
230942
231388
  const { output } = await runDesktopRender(native.js, native.resources, {
230943
231389
  renderer: desktopRenderer,
@@ -230952,7 +231398,7 @@ var runTests = async (args, flags) => {
230952
231398
  script: plan.text,
230953
231399
  fixedDtMs: nativeFixedDt
230954
231400
  });
230955
- const r2 = evaluateDesktopRun(plan, output, (path2) => existsSync47(path2) ? readFileSync40(path2, "utf8") : null);
231401
+ const r2 = evaluateDesktopRun(plan, output, (path2) => existsSync48(path2) ? readFileSync40(path2, "utf8") : null);
230956
231402
  if (!r2.loaded)
230957
231403
  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
231404
  return {
@@ -230964,8 +231410,48 @@ var runTests = async (args, flags) => {
230964
231410
  artifacts: r2.ok ? [] : [finalPng]
230965
231411
  };
230966
231412
  };
231413
+ let liteSession = null;
231414
+ if (lite) {
231415
+ const page = resolveLiteRunner(note);
231416
+ if (!page)
231417
+ 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).");
231418
+ let manifest = null;
231419
+ try {
231420
+ manifest = readManifest(root);
231421
+ } catch {}
231422
+ liteSession = await startLiteSession({ root, manifest, name: manifest?.name ?? "app", lite: page, entry: flags.entry });
231423
+ }
231424
+ const runLiteCase = async (kase) => {
231425
+ const caseDir = resolve28(artifactsRoot, slug(kase.name));
231426
+ const plan = translateScenario(kase.steps, { outDir: caseDir, headroomFrames: 0 });
231427
+ mkdirSync32(caseDir, { recursive: true });
231428
+ const output = await liteSession.run(plan, {
231429
+ width: flags.width || kase.width || projectViewport?.width || 390,
231430
+ height: flags.height || kase.height || projectViewport?.height || 844,
231431
+ onConsole
231432
+ });
231433
+ const r2 = evaluateDesktopRun(plan, output, (path2) => existsSync48(path2) ? readFileSync40(path2, "utf8") : null);
231434
+ return {
231435
+ name: kase.name,
231436
+ kind: kase.kind,
231437
+ ok: r2.ok,
231438
+ steps: r2.steps.map((s2) => ({ desc: s2.desc, ok: s2.ok, error: s2.error, skipped: s2.skipped.length > 0 ? s2.skipped : undefined })),
231439
+ error: !r2.loaded ? "the page did not load the app" : r2.ended ? undefined : "the page stopped before the script finished",
231440
+ artifacts: []
231441
+ };
231442
+ };
230967
231443
  for (const kase of cases) {
230968
231444
  let result;
231445
+ if (lite) {
231446
+ try {
231447
+ result = await runLiteCase(kase);
231448
+ } catch (e2) {
231449
+ result = { name: kase.name, kind: kase.kind, ok: false, steps: [], error: e2 instanceof Error ? e2.message : String(e2), artifacts: [] };
231450
+ }
231451
+ results.push(result);
231452
+ reportCase(result);
231453
+ continue;
231454
+ }
230969
231455
  if (desktop) {
230970
231456
  try {
230971
231457
  result = await runNativeCase(kase);
@@ -231039,6 +231525,7 @@ var runTests = async (args, flags) => {
231039
231525
  }, null, 2) + `
231040
231526
  `);
231041
231527
  }
231528
+ await liteSession?.close();
231042
231529
  clearTimeout(killer);
231043
231530
  process.exit(failed > 0 ? 1 : 0);
231044
231531
  };
@@ -231047,9 +231534,9 @@ var runTests = async (args, flags) => {
231047
231534
  init_peerInstall();
231048
231535
  import { createRequire as createRequire8 } from "node:module";
231049
231536
  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";
231537
+ import { existsSync as existsSync49, mkdtempSync as mkdtempSync6, readdirSync as readdirSync22, rmSync as rmSync13, statSync as statSync19 } from "node:fs";
231051
231538
  import { tmpdir as tmpdir6 } from "node:os";
231052
- import { extname, join as join48, relative as relative12, resolve as resolve29 } from "node:path";
231539
+ import { extname, join as join49, relative as relative12, resolve as resolve29 } from "node:path";
231053
231540
  init_cli();
231054
231541
  init_paths();
231055
231542
  var DEFAULT_SIZE = 256;
@@ -231061,13 +231548,13 @@ var findKtx = () => {
231061
231548
  process.env.LECODES_KTX,
231062
231549
  process.env.KTX_CLI,
231063
231550
  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,
231551
+ process.platform === "win32" ? join49(process.env.LOCALAPPDATA ?? "", "Programs", "KTX-Software", "bin", exe) : null,
231552
+ process.platform === "win32" ? join49(process.env.ProgramFiles ?? "", "KTX-Software", "bin", exe) : null,
231066
231553
  "/usr/local/bin/ktx",
231067
231554
  "/opt/homebrew/bin/ktx"
231068
231555
  ].filter((p2) => !!p2);
231069
231556
  for (const path2 of candidates) {
231070
- if (existsSync48(path2))
231557
+ if (existsSync49(path2))
231071
231558
  return path2;
231072
231559
  }
231073
231560
  try {
@@ -231099,7 +231586,7 @@ var ktxToPng = (ktx, file, out) => {
231099
231586
  run4(["extract", "--transcode", "rgba8", "--level", "0", file, out]);
231100
231587
  } catch (e2) {
231101
231588
  run4(["extract", "--level", "0", file, out]);
231102
- if (!existsSync48(out))
231589
+ if (!existsSync49(out))
231103
231590
  throw e2;
231104
231591
  }
231105
231592
  };
@@ -231108,7 +231595,7 @@ var collect = (dir) => {
231108
231595
  for (const name of readdirSync22(dir)) {
231109
231596
  if (name === "node_modules" || name.startsWith("."))
231110
231597
  continue;
231111
- const full = join48(dir, name);
231598
+ const full = join49(dir, name);
231112
231599
  if (statSync19(full).isDirectory())
231113
231600
  out.push(...collect(full));
231114
231601
  else if (IMAGE_EXT.test(name))
@@ -231135,7 +231622,7 @@ var thumbs = async (targets, size, force) => {
231135
231622
  if (targets.length > 0) {
231136
231623
  for (const arg of targets) {
231137
231624
  const abs = resolve29(process.cwd(), arg);
231138
- if (!existsSync48(abs))
231625
+ if (!existsSync49(abs))
231139
231626
  throw new CliError(`No such file or folder: ${arg}`);
231140
231627
  if (statSync19(abs).isDirectory())
231141
231628
  files.push(...collect(abs));
@@ -231157,7 +231644,7 @@ var thumbs = async (targets, size, force) => {
231157
231644
  const ktx = files.some((f5) => f5.toLowerCase().endsWith(".ktx2")) ? findKtx() : null;
231158
231645
  const sharp = sharpResizer();
231159
231646
  const resize = sharp ?? jimpResizer();
231160
- const tmp = mkdtempSync6(join48(tmpdir6(), "lecodes-thumbs-"));
231647
+ const tmp = mkdtempSync6(join49(tmpdir6(), "lecodes-thumbs-"));
231161
231648
  let made = 0, fresh = 0;
231162
231649
  const skipped = [];
231163
231650
  try {
@@ -231183,7 +231670,7 @@ var thumbs = async (targets, size, force) => {
231183
231670
  skipped.push(`${asset} (KTX2 needs KTX-Software's \`ktx\` — set LECODES_KTX or put it on PATH)`);
231184
231671
  continue;
231185
231672
  }
231186
- source = join48(tmp, `${i2}.png`);
231673
+ source = join49(tmp, `${i2}.png`);
231187
231674
  ktxToPng(ktx, file, source);
231188
231675
  }
231189
231676
  sceneServer.writePreviewFile(root, asset, await resize(source, size));
@@ -231203,7 +231690,7 @@ var thumbs = async (targets, size, force) => {
231203
231690
  if (rendered.length > 0) {
231204
231691
  warnErr(`Models, materials and prefabs are RENDERED, not decoded — the scene editor does those: ${rendered.join(", ")}`);
231205
231692
  }
231206
- note(`Cache: ${join48(root, ".lecodes", "preview")} (re-runs skip everything unchanged)`);
231693
+ note(`Cache: ${join49(root, ".lecodes", "preview")} (re-runs skip everything unchanged)`);
231207
231694
  };
231208
231695
 
231209
231696
  // src/commands/update/index.ts
@@ -231211,10 +231698,11 @@ init_package();
231211
231698
  init_cli();
231212
231699
  init_peers();
231213
231700
  init_peerInstall();
231701
+ init_archives();
231214
231702
  import { spawnSync as spawnSync10 } from "node:child_process";
231215
- import { existsSync as existsSync49, readdirSync as readdirSync23 } from "node:fs";
231703
+ import { existsSync as existsSync50, readdirSync as readdirSync23 } from "node:fs";
231216
231704
  import { homedir as homedir13 } from "node:os";
231217
- import { dirname as dirname24, join as join49, resolve as resolve31 } from "node:path";
231705
+ import { dirname as dirname25, join as join50, resolve as resolve31 } from "node:path";
231218
231706
 
231219
231707
  // src/commands/update/types.ts
231220
231708
  init_api();
@@ -231286,9 +231774,9 @@ var findProjectDir = (start) => {
231286
231774
  const home = resolve31(homedir13());
231287
231775
  let dir = resolve31(start);
231288
231776
  while (true) {
231289
- if (dir !== home && (existsSync49(join49(dir, "app.json")) || existsSync49(join49(dir, ".lecodes", "manifest.json")) || existsSync49(join49(dir, ".lecodes", "types"))))
231777
+ if (dir !== home && (existsSync50(join50(dir, "app.json")) || existsSync50(join50(dir, ".lecodes", "manifest.json")) || existsSync50(join50(dir, ".lecodes", "types"))))
231290
231778
  return dir;
231291
- const parent = dirname24(dir);
231779
+ const parent = dirname25(dir);
231292
231780
  if (parent === dir)
231293
231781
  return null;
231294
231782
  dir = parent;
@@ -231323,7 +231811,7 @@ var semverNewer2 = (a2, b) => {
231323
231811
  return 0;
231324
231812
  };
231325
231813
  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"));
231814
+ var STANDALONE = existsSync50(join50(dirname25(process.execPath), "runtime")) && existsSync50(join50(dirname25(process.execPath), process.platform === "win32" ? "chisel.exe" : "chisel"));
231327
231815
  var update_default = defineCommand({
231328
231816
  name: "update",
231329
231817
  summary: "Move everything the project depends on to its newest published state",
@@ -231383,7 +231871,7 @@ ${c.bold(name)} ${c.dim(what)}`);
231383
231871
  return ["skipped", "not inside a project"];
231384
231872
  if (flags["no-shells"])
231385
231873
  return ["skipped", "--no-shells"];
231386
- const present = ["ios", "android"].filter((d) => existsSync49(join49(root, d)) && readdirSync23(join49(root, d)).length > 0);
231874
+ const present = ["ios", "android"].filter((d) => existsSync50(join50(root, d)) && readdirSync23(join50(root, d)).length > 0);
231387
231875
  if (present.length === 0)
231388
231876
  return ["skipped", "no ios/ or android/ shell in this project (`lecodes app init ios|android`)"];
231389
231877
  await runSubcommand(app_default, "update");
@@ -231398,7 +231886,7 @@ ${c.bold(name)} ${c.dim(what)}`);
231398
231886
  for (const [name, probe] of OPTIONAL_PEERS) {
231399
231887
  const where = peerSource(name, probe);
231400
231888
  const installed = peerVersion(name, probe);
231401
- const latest = !installed ? null : where === "prefix" && name === "lecodes-headless" ? package_default.version : await npmLatest(name);
231889
+ const latest = !installed ? null : archivedVersion(name) ?? await npmLatest(name);
231402
231890
  rows.push({ name, installed, latest, where });
231403
231891
  }
231404
231892
  rows.push({ name: "lecodes-cli", installed: package_default.version, latest: await npmLatest("lecodes-cli"), where: "global" });
@@ -231412,7 +231900,7 @@ ${c.bold(name)} ${c.dim(what)}`);
231412
231900
  if (behind.length === 0)
231413
231901
  return "ok";
231414
231902
  const prefixBehind = behind.filter((r2) => r2.where === "prefix");
231415
- const globalSpecs = behind.filter((r2) => r2.where !== "prefix").map((r2) => r2.name + "@latest");
231903
+ const globalSpecs = behind.filter((r2) => r2.where !== "prefix").map((r2) => archiveSpec(r2.name) ?? r2.name + "@latest");
231416
231904
  if (flags.check) {
231417
231905
  return ["updated", behind.length + " behind — run: lecodes update" + (globalSpecs.length ? " (global: npm install -g " + globalSpecs.join(" ") + ")" : "")];
231418
231906
  }
@@ -231430,7 +231918,7 @@ ${c.bold(name)} ${c.dim(what)}`);
231430
231918
  const r2 = spawnSync10("npm", ["install", "-g", ...toInstall], { stdio: "inherit", shell: true });
231431
231919
  if (r2.status !== 0)
231432
231920
  throw new CliError("npm install -g exited with " + (r2.status ?? "a signal"));
231433
- done.push(...toInstall.map((s2) => s2.replace("@latest", "")));
231921
+ done.push(...behind.filter((r3) => r3.where !== "prefix" && (!STANDALONE || r3.name !== "lecodes-cli")).map((r3) => r3.name));
231434
231922
  }
231435
231923
  if (done.length === 0)
231436
231924
  return ["skipped", "only the standalone CLI itself is behind — download the release"];
@@ -231449,11 +231937,362 @@ ${c.bold("Summary")}`);
231449
231937
  }
231450
231938
  });
231451
231939
 
231940
+ // src/commands/web.ts
231941
+ init_manifest();
231942
+ import { basename as basename25, relative as relative13 } from "node:path";
231943
+ init_browserAuth();
231944
+ init_cli();
231945
+
231946
+ // src/web/build.ts
231947
+ import { createHash as createHash9 } from "node:crypto";
231948
+ 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";
231949
+ import { dirname as dirname26, extname as extname2, join as join51, resolve as resolve32 } from "node:path";
231950
+ init_fonts();
231951
+ init_assetIcons();
231952
+ init_projectCompile();
231953
+ init_cli();
231954
+ var escapeHtml = (text2) => text2.replace(/[&<>"]/g, (c4) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c4]);
231955
+ var normalBase = (base) => {
231956
+ const b = (base ?? "/").trim() || "/";
231957
+ return (b.startsWith("/") ? b : `/${b}`).replace(/\/*$/, "/");
231958
+ };
231959
+ var walk = (dir, prefix = "") => {
231960
+ const out = [];
231961
+ for (const name of readdirSync24(dir)) {
231962
+ const abs = join51(dir, name);
231963
+ const stat = statSync20(abs);
231964
+ if (stat.isDirectory())
231965
+ out.push(...walk(abs, `${prefix}${name}/`));
231966
+ else
231967
+ out.push({ path: prefix + name, bytes: stat.size });
231968
+ }
231969
+ return out;
231970
+ };
231971
+ var buildWeb = async (options) => {
231972
+ const { root, manifest, name, lite } = options;
231973
+ const out = resolve32(root, options.out);
231974
+ if (out === resolve32(root) || resolve32(root).startsWith(out + "\\") || resolve32(root).startsWith(out + "/")) {
231975
+ throw new CliError(`The output folder is cleaned before a build: "${options.out}" holds the project itself.`);
231976
+ }
231977
+ const config = readAppConfigAt(root)?.web ?? {};
231978
+ const mode = config.urls === "path" ? "path" : "hash";
231979
+ const base = normalBase(config.base);
231980
+ const warnings = [];
231981
+ const assetUrl = (path2) => "assets" + encodeURI(path2);
231982
+ const collected = collectLocalEntries(root, manifest, "assets");
231983
+ const entries = collected.entries.filter((e2) => {
231984
+ const abs = e2.absPath ? resolve32(e2.absPath) : null;
231985
+ return !(abs && (abs === out || abs.startsWith(out + "\\") || abs.startsWith(out + "/")));
231986
+ });
231987
+ for (const w of collected.warnings)
231988
+ if (!/shaders compile on the server/.test(w))
231989
+ warnings.push(w);
231990
+ const fontsHeader = await applyProjectFonts(entries, {
231991
+ mode: "vendor",
231992
+ root,
231993
+ vendorDir: root,
231994
+ vendorPrefix: "",
231995
+ publicUrl: "",
231996
+ relative: true,
231997
+ makeResourceEntry: (path2, absPath) => ({ path: path2, type: "resource", fileSrc: assetUrl(path2), absPath })
231998
+ });
231999
+ const iconsHeader = await applyAppAssetIcons(entries);
232000
+ if (!options.entry) {
232001
+ const ambiguous = ambiguousEntryWarning(entries);
232002
+ if (ambiguous)
232003
+ warnings.push(ambiguous);
232004
+ }
232005
+ const compiled = await compileProject2({
232006
+ entries,
232007
+ name,
232008
+ publicUrl: "",
232009
+ entryOverride: options.entry,
232010
+ header: true,
232011
+ fontsHeader,
232012
+ iconsHeader,
232013
+ sourcemap: options.sourcemap === true,
232014
+ minify: options.minify,
232015
+ onWarning: (w) => warnings.push(w)
232016
+ });
232017
+ const js = compiled.replace(/^\/\/ last-updated: .*$/m, "// last-updated: -");
232018
+ const header = js.slice(0, 2000);
232019
+ if (!/^\/\/ gl: disable/m.test(header) || /^\/\/ 2d: enable/m.test(header)) {
232020
+ 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.)");
232021
+ }
232022
+ rmSync14(out, { recursive: true, force: true });
232023
+ mkdirSync33(out, { recursive: true });
232024
+ cpSync6(lite, out, { recursive: true });
232025
+ const bundle = `app.${createHash9("sha256").update(js).digest("hex").slice(0, 10)}.js`;
232026
+ writeFileSync35(join51(out, bundle), js);
232027
+ for (const entry of entries) {
232028
+ if (entry.type !== "resource" || !entry.absPath || !entry.fileSrc)
232029
+ continue;
232030
+ if (!js.includes(entry.fileSrc))
232031
+ continue;
232032
+ const to2 = join51(out, "assets", ...entry.path.split("/").filter(Boolean));
232033
+ if (existsSync51(to2)) {
232034
+ warnings.push(`${entry.path}: a file of the page has this name in the built folder — the project's file was not copied`);
232035
+ continue;
232036
+ }
232037
+ mkdirSync33(dirname26(to2), { recursive: true });
232038
+ copyFileSync2(entry.absPath, to2);
232039
+ }
232040
+ const app = readAppConfigAt(root);
232041
+ let html = readFileSync41(join51(lite, "index.html"), "utf8");
232042
+ const head = [];
232043
+ head.push(`<meta name="theme-color" content="${escapeHtml(config.themeColor ?? "#000000")}" />`);
232044
+ if (app?.icon) {
232045
+ const icon = resolve32(root, app.icon);
232046
+ if (existsSync51(icon)) {
232047
+ const file = `icon${extname2(icon).toLowerCase() || ".png"}`;
232048
+ copyFileSync2(icon, join51(out, file));
232049
+ head.push(`<link rel="icon" href="${file}" />`, `<link rel="apple-touch-icon" href="${file}" />`);
232050
+ } else {
232051
+ warnings.push(`app.json icon "${app.icon}" is not there — the page has no icon`);
232052
+ }
232053
+ }
232054
+ if (mode === "path")
232055
+ head.unshift(`<base href="${escapeHtml(base)}" />`);
232056
+ const attrs = [`data-src="${bundle}"`, ...mode === "path" ? [`data-urls="path"`, `data-base="${escapeHtml(base)}"`] : []].join(" ");
232057
+ const replace = (from, to2) => {
232058
+ const next = html.replace(from, to2);
232059
+ if (next === html)
232060
+ throw new CliError(`The page of Web Lite has no ${String(from)} to fill — this CLI and its page are of different versions.`);
232061
+ html = next;
232062
+ };
232063
+ replace(/<title>[^<]*<\/title>/, `<title>${escapeHtml(config.title ?? app?.name ?? name)}</title>`);
232064
+ replace(`<div id="app"></div>`, `<div id="app" ${attrs}></div>`);
232065
+ replace("</head>", ` ${head.join(`
232066
+ `)}
232067
+ </head>`);
232068
+ if (config.lang)
232069
+ replace(/<html lang="[^"]*">/, `<html lang="${escapeHtml(config.lang)}">`);
232070
+ writeFileSync35(join51(out, "index.html"), html);
232071
+ if (mode === "path")
232072
+ writeFileSync35(join51(out, "404.html"), html);
232073
+ return { out, bundle, files: walk(out), warnings };
232074
+ };
232075
+
232076
+ // src/web/preview.ts
232077
+ import { createReadStream as createReadStream2, existsSync as existsSync52, statSync as statSync21 } from "node:fs";
232078
+ import { createServer as createServer4 } from "node:http";
232079
+ import { join as join52, resolve as resolve33 } from "node:path";
232080
+ var startPreview = async (dir, port) => {
232081
+ const root = resolve33(dir);
232082
+ const fallback = existsSync52(join52(root, "404.html")) ? join52(root, "index.html") : null;
232083
+ const server2 = createServer4((req, res) => {
232084
+ let path2;
232085
+ try {
232086
+ path2 = decodeURIComponent(new URL(req.url ?? "/", "http://localhost").pathname);
232087
+ } catch {
232088
+ res.writeHead(400);
232089
+ res.end();
232090
+ return;
232091
+ }
232092
+ const parts = path2.split("/").filter(Boolean);
232093
+ if (parts.some((p2) => p2 === ".." || p2 === ".")) {
232094
+ res.writeHead(400);
232095
+ res.end();
232096
+ return;
232097
+ }
232098
+ let file = join52(root, ...parts);
232099
+ if (existsSync52(file) && statSync21(file).isDirectory())
232100
+ file = join52(file, "index.html");
232101
+ let status3 = 200;
232102
+ if (!existsSync52(file) || !statSync21(file).isFile()) {
232103
+ if (!fallback || /\.[a-z0-9]+$/i.test(parts[parts.length - 1] ?? "")) {
232104
+ res.writeHead(404, { "content-type": "text/plain" });
232105
+ res.end("Not found");
232106
+ return;
232107
+ }
232108
+ file = fallback;
232109
+ }
232110
+ res.writeHead(status3, {
232111
+ "content-type": contentType(file),
232112
+ "cache-control": /[.-][0-9a-f]{8,}\.[a-z0-9]+$/i.test(file) ? "max-age=31536000, immutable" : "no-cache"
232113
+ });
232114
+ createReadStream2(file).pipe(res);
232115
+ });
232116
+ await new Promise((done, reject) => {
232117
+ server2.once("error", reject);
232118
+ server2.listen(port, "0.0.0.0", () => {
232119
+ server2.removeListener("error", reject);
232120
+ done();
232121
+ });
232122
+ });
232123
+ const address = server2.address();
232124
+ const bound = typeof address === "object" && address !== null ? address.port : port;
232125
+ return { url: `http://localhost:${bound}/`, port: bound, close: () => server2.close() };
232126
+ };
232127
+
232128
+ // src/commands/web.ts
232129
+ init_shared();
232130
+ var DEFAULT_PORT4 = 4488;
232131
+ var DEFAULT_OUT = "dist/web";
232132
+ var project = () => {
232133
+ let root;
232134
+ let manifest;
232135
+ try {
232136
+ root = findProjectRoot(process.cwd());
232137
+ manifest = readManifest(root);
232138
+ } catch {
232139
+ root = process.cwd();
232140
+ manifest = null;
232141
+ }
232142
+ return { root, manifest, name: manifest?.name ?? basename25(root) };
232143
+ };
232144
+ var litePage = () => {
232145
+ const lite = resolveLiteRunner(note);
232146
+ if (!lite)
232147
+ 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).");
232148
+ return lite;
232149
+ };
232150
+ var kb = (bytes) => bytes < 1024 ? `${bytes} B` : bytes < 1024 * 1024 ? `${(bytes / 1024).toFixed(1)} KB` : `${(bytes / 1024 / 1024).toFixed(2)} MB`;
232151
+ var dev = defineCommand({
232152
+ name: "dev",
232153
+ summary: "Open the project as a web page, reloaded on every save",
232154
+ 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).",
232155
+ flags: {
232156
+ port: num("server port", { default: DEFAULT_PORT4 }),
232157
+ entry: bundleFlags.entry,
232158
+ "no-open": bool("don't open the browser")
232159
+ },
232160
+ examples: ["lecodes web dev", "lecodes web dev --port 8080 --no-open"],
232161
+ run: async ({ flags }) => {
232162
+ const { root, manifest, name } = project();
232163
+ const lite = litePage();
232164
+ const port = flags.port || DEFAULT_PORT4;
232165
+ const lan = lanCandidates()[0]?.address;
232166
+ const origin = `http://localhost:${port}`;
232167
+ const server2 = await startDevServer({
232168
+ root,
232169
+ manifest,
232170
+ name,
232171
+ origin,
232172
+ port,
232173
+ token: loadDevToken(root),
232174
+ entryOverride: flags.entry,
232175
+ lite,
232176
+ log: note
232177
+ }).catch((e2) => {
232178
+ const msg = e2 instanceof Error ? e2.message : String(e2);
232179
+ throw new CliError(msg.includes("EADDRINUSE") ? `Port ${port} is already in use — pass --port <n>.` : msg);
232180
+ });
232181
+ note("Compiling…");
232182
+ const first = await server2.recompile();
232183
+ if (first.ok)
232184
+ for (const w of first.warnings)
232185
+ warnErr(w);
232186
+ else
232187
+ warnErr(`Compile failed — fix it and save; the page loads once it compiles:
232188
+ ${first.error}`);
232189
+ const watcher = watchProject(root, async () => {
232190
+ const result = await server2.recompile();
232191
+ if (!result.ok)
232192
+ warnErr(`Compile failed (the page keeps the last good version):
232193
+ ${result.error}`);
232194
+ else if (result.changed) {
232195
+ note(`✓ v${result.v}`);
232196
+ for (const w of result.warnings)
232197
+ warnErr(w);
232198
+ }
232199
+ }, { warn: warnErr, debug: process.env.LECODES_DEV_DEBUG === "1" ? note : undefined });
232200
+ const url = server2.liteUrl;
232201
+ log("");
232202
+ log(`${c.bold("LeCodes Web")} — ${name}`);
232203
+ log("");
232204
+ log(`In the browser: ${c.bold(url)}`);
232205
+ if (lan)
232206
+ note(`On this network: ${url.replace("http://localhost:", `http://${lan}:`)}`);
232207
+ log("");
232208
+ log(`Watching ${root} — save a file to reload. Ctrl+C to stop.`);
232209
+ if (!flags["no-open"])
232210
+ openBrowser(url);
232211
+ process.on("SIGINT", () => {
232212
+ watcher.close();
232213
+ server2.close();
232214
+ process.exit(0);
232215
+ });
232216
+ }
232217
+ });
232218
+ var build2 = defineCommand({
232219
+ name: "build",
232220
+ summary: "Build the project into a folder any static hosting serves",
232221
+ 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).',
232222
+ flags: {
232223
+ out: str("the folder to write (cleaned first)", { alias: "o", value: "<dir>", default: DEFAULT_OUT }),
232224
+ entry: bundleFlags.entry,
232225
+ sourcemap: bool("inline the source map (errors name the app's own lines; the bundle grows by its sources)"),
232226
+ "no-minify": bundleFlags["no-minify"]
232227
+ },
232228
+ examples: ["lecodes web build", "lecodes web build -o public"],
232229
+ run: async ({ flags }) => {
232230
+ const { root, manifest, name } = project();
232231
+ const lite = litePage();
232232
+ note("Building…");
232233
+ const result = await buildWeb({
232234
+ root,
232235
+ manifest,
232236
+ name,
232237
+ lite,
232238
+ out: flags.out,
232239
+ entry: flags.entry,
232240
+ sourcemap: flags.sourcemap,
232241
+ minify: flags["no-minify"] ? false : undefined
232242
+ });
232243
+ for (const w of result.warnings)
232244
+ warnErr(w);
232245
+ const total = result.files.reduce((sum, f5) => sum + f5.bytes, 0);
232246
+ const shown = relative13(process.cwd(), result.out) || ".";
232247
+ log("");
232248
+ for (const f5 of [...result.files].sort((a2, b) => b.bytes - a2.bytes).slice(0, 8))
232249
+ log(` ${kb(f5.bytes).padStart(10)} ${f5.path}`);
232250
+ if (result.files.length > 8)
232251
+ log(c.dim(` …and ${result.files.length - 8} more`));
232252
+ log("");
232253
+ log(`${c.green("✓")} ${c.bold(shown)} — ${result.files.length} files, ${kb(total)}`);
232254
+ note(`Look at it: lecodes web preview${flags.out === DEFAULT_OUT ? "" : ` ${flags.out}`}`);
232255
+ }
232256
+ });
232257
+ var preview = defineCommand({
232258
+ name: "preview",
232259
+ summary: "Serve a built folder, as a static hosting would",
232260
+ usage: "[dir]",
232261
+ flags: {
232262
+ port: num("server port", { default: DEFAULT_PORT4 + 1 }),
232263
+ "no-open": bool("don't open the browser")
232264
+ },
232265
+ examples: ["lecodes web preview", "lecodes web preview public --port 8080"],
232266
+ run: async ({ args, flags }) => {
232267
+ const dir = args[0] ?? DEFAULT_OUT;
232268
+ const server2 = await startPreview(dir, flags.port || DEFAULT_PORT4 + 1).catch((e2) => {
232269
+ const msg = e2 instanceof Error ? e2.message : String(e2);
232270
+ throw new CliError(msg.includes("EADDRINUSE") ? `Port ${flags.port} is already in use — pass --port <n>.` : msg);
232271
+ });
232272
+ log(`${c.bold("LeCodes Web")} — ${dir}`);
232273
+ log(`In the browser: ${c.bold(server2.url)}`);
232274
+ log("Ctrl+C to stop.");
232275
+ if (!flags["no-open"])
232276
+ openBrowser(server2.url);
232277
+ process.on("SIGINT", () => {
232278
+ server2.close();
232279
+ process.exit(0);
232280
+ });
232281
+ }
232282
+ });
232283
+ var web_default = defineCommand({
232284
+ name: "web",
232285
+ summary: "The project as a web page (Web Lite): serve it with reload on save, build it for a hosting",
232286
+ 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.",
232287
+ commands: [dev, build2, preview]
232288
+ });
232289
+
231452
232290
  // src/commands/index.ts
231453
232291
  var commandGroups = (opts) => [
231454
232292
  { title: "Project", commands: [init_default, create_default, clone_default, link_default, status_default, diff_default, pull_default, push_default, login_default] },
231455
232293
  { title: "Run & test", commands: [
231456
232294
  dev_default,
232295
+ web_default,
231457
232296
  compile_default,
231458
232297
  render_default,
231459
232298
  test_default,
@@ -231485,9 +232324,9 @@ var commandGroups = (opts) => [
231485
232324
  // src/index.ts
231486
232325
  init_peers();
231487
232326
  var STANDALONE2 = (() => {
231488
- const dir = dirname25(process.execPath);
232327
+ const dir = dirname27(process.execPath);
231489
232328
  const chisel = process.platform === "win32" ? "chisel.exe" : "chisel";
231490
- return existsSync50(join50(dir, "runtime")) && existsSync50(join50(dir, chisel));
232329
+ return existsSync53(join53(dir, "runtime")) && existsSync53(join53(dir, chisel));
231491
232330
  })();
231492
232331
  var INSTALLED = STANDALONE2 && (() => {
231493
232332
  const exe = process.platform === "win32" ? "lecodes.exe" : "lecodes";
@@ -231502,8 +232341,8 @@ var INSTALLED = STANDALONE2 && (() => {
231502
232341
  if (!d)
231503
232342
  return false;
231504
232343
  try {
231505
- const cand = join50(d, exe);
231506
- return existsSync50(cand) && realpathSync2(cand) === self2;
232344
+ const cand = join53(d, exe);
232345
+ return existsSync53(cand) && realpathSync2(cand) === self2;
231507
232346
  } catch {
231508
232347
  return false;
231509
232348
  }