lecodes-cli 0.13.2 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1826,7 +1826,7 @@ var ICON_ID, isIdentStart2 = (c2) => c2 >= "A" && c2 <= "Z" || c2 >= "a" && c2 <
1826
1826
  return new Error(`assetIcon: unknown icon pack "${pack}" (in "${pack}:${name}"). Available packs: ${packs.join(", ")}.`);
1827
1827
  } catch {}
1828
1828
  }
1829
- return new Error(`assetIcon: can't reach the icon registry${registry ? ` (${registry})` : ""} to resolve "${pack}:${name}". ` + `Check your connection, or set LECODES_ICON_REGISTRY.`);
1829
+ return Object.assign(new Error(`assetIcon: can't reach the icon registry${registry ? ` (${registry})` : ""} to resolve "${pack}:${name}". ` + `Check your connection, or set LECODES_ICON_REGISTRY.`), { transient: true });
1830
1830
  }, VENDORED_ICON_PATH, collectVendoredIcons = (entries) => {
1831
1831
  const map = new Map;
1832
1832
  for (const e of entries) {
@@ -43098,6 +43098,13 @@ var SPEC_MD, META_JSON = `{
43098
43098
  },
43099
43099
  "edges": []
43100
43100
  }
43101
+ `, META_JSON_DESKTOP = `{
43102
+ "defaultSize": [1280, 800],
43103
+ "screens": {
43104
+ "home": { "pos": [0, 0], "desc": "Example screen — replace me", "role": "list" }
43105
+ },
43106
+ "edges": []
43107
+ }
43101
43108
  `, TOKENS_TS = `// Design tokens shared by every screen. Colors are hex strings (the engine parses hex only).
43102
43109
  // App font: one theme() call here sets the default text font everywhere. font("id") installs a
43103
43110
  // registry family (vendored at compile time) and returns its name — no loading code needed.
@@ -43114,56 +43121,30 @@ export const colors = {
43114
43121
  export const spacing = { xs: 4, sm: 8, md: 16, lg: 24 }
43115
43122
 
43116
43123
  export const radius = { sm: 8, md: 12, lg: 20 }
43117
- `, TABS_TS = `// The app's tab bar, declared ONCE — the design canvas discovers it from this file.
43124
+ `, TABS_TS = `// The app's tab bar — declare it here ONCE; the design canvas discovers it from this file:
43118
43125
  //
43119
43126
  // export const mainTabs = defineTabs({
43120
43127
  // home: { label: "Home", icon: assetIcon("lucide:house") },
43121
43128
  // news: { label: "News", icon: assetIcon("lucide:newspaper") },
43122
43129
  // })
43123
43130
  //
43124
- // The KEYS are screen ids — each tab's root screen (screens/<key>.ts), in tab order, literals only.
43131
+ // \`defineTabs\` is an SDK global — the standard themed bar, the same look the built app's UITabs
43132
+ // renders. The KEYS are screen ids — each tab's root screen (screens/<key>.ts), in tab order,
43133
+ // literals only. Icons are always \`assetIcon("pack:name")\` calls — never hand-written
43134
+ // \`{ svg }\` objects (those silently render blank).
43135
+ //
43125
43136
  // Every screen that shows the bar mounts it with a literal tab id — tab roots AND pushed details
43126
43137
  // that keep the bar:
43127
43138
  //
43128
43139
  // export default () => mainTabs.screen("news", [ ...content ])
43129
43140
  //
43130
- // A screen that doesn't mount it is a full-screen push (bar hidden). Never draw meta.json edges
43131
- // between tab screens — the bar already navigates; the board renders it as a rail + lanes.
43132
- // Restyle the bar below freely: only the defineTabs({ keys }) declaration and the
43133
- // .screen("<tab>", …) / .bar("<tab>") call shapes are parsed.
43134
-
43135
- type TabDef = { label: string, icon?: { svg: string, tintColor: string | null } }
43136
-
43137
- // Theme-variable driven — theme({ primaryColor, mutedColor, tabbarBg, screenBg }) anywhere
43138
- // restyles every screen's bar at once; the fallbacks keep an untouched scaffold looking right.
43139
- const ACTIVE = "var(--primaryColor, #5b8cff)"
43140
- const INACTIVE = "var(--mutedColor, #8a919e)"
43141
- const BAR_BG = "var(--tabbarBg, #15171c)"
43142
- const SCREEN_BG = "var(--screenBg, #101114)"
43143
-
43144
- export const defineTabs = <T extends Record<string, TabDef>>(tabs: T) => {
43145
- const ids = Object.keys(tabs) as (keyof T & string)[]
43146
-
43147
- /** The bar alone — mount it yourself when the screen owns its whole layout. */
43148
- const bar = (active: keyof T & string) =>
43149
- UIRow(ids.map((id) => {
43150
- const color = id === active ? ACTIVE : INACTIVE
43151
- return UIButton([
43152
- ...(tabs[id].icon ? [UIImage(tabs[id].icon!).style({ width: 22, height: 22, tintColor: color })] : []),
43153
- UIText(tabs[id].label).style({ fontSize: 10, color }),
43154
- ]).style({ name: \`tab-\${id}\`, flexDirection: "column", gap: 3, flexGrow: 1, flexBasis: 0, pt: 8, pb: 6 })
43155
- })).style({ bgColor: BAR_BG, pb: "safe-bottom" })
43156
-
43157
- /** A screen that sits ON the bar (the common case): [ content, bar ] — the same column the real
43158
- * app composes as [ UIPager, bar ]. */
43159
- const screen = (active: keyof T & string, children: UINodeChild[]) =>
43160
- UIScreen([
43161
- UIColumn(children).style({ flexGrow: 1, p: 16, pt: "safe-top" }),
43162
- bar(active),
43163
- ]).style({ bgColor: SCREEN_BG })
43164
-
43165
- return { ids, bar, screen }
43166
- }
43141
+ // …or \`mainTabs.bar("news")\` when the screen owns its whole layout. A screen that doesn't mount
43142
+ // it is a full-screen push (bar hidden). Never draw meta.json edges between tab screens — the bar
43143
+ // already navigates; the board renders it as a rail + lanes.
43144
+ //
43145
+ // Restyle via the theme: theme({ primaryColor, mutedColor, tabbarBg, screenBg }) in shared/tokens.ts
43146
+ // recolors every screen's bar (and the built app's UITabs) at once. Keep the defineTabs({...})
43147
+ // declaration and the .screen("<tab>", …) / .bar("<tab>") call shapes intact — the canvas parses them.
43167
43148
  `, HOME_SCREEN_TS = `import { colors, spacing, radius } from "../shared/tokens"
43168
43149
 
43169
43150
  // Mock data: the domain shown on realistic values. Inline, per screen.
@@ -43326,9 +43307,11 @@ keep in the chat.
43326
43307
 
43327
43308
  Most apps have one — declare it ONCE, in source, and never draw edges for it:
43328
43309
 
43329
- - \`shared/tabs.ts\` ships a \`defineTabs\` helper. Declare the bar there:
43310
+ - \`defineTabs\` is an SDK global (the standard themed bar — the same look the built app's
43311
+ \`UITabs\` renders). Declare the bar in \`shared/tabs.ts\`:
43330
43312
  \`export const mainTabs = defineTabs({ home: { label: "Home", icon: assetIcon("lucide:house") }, … })\`
43331
- — the **keys are screen ids** (each tab's root screen, in tab order), literals only.
43313
+ — the **keys are screen ids** (each tab's root screen, in tab order), literals only, and icons
43314
+ are always \`assetIcon(…)\` calls.
43332
43315
  - Every screen that shows the bar mounts it with a **literal** tab id:
43333
43316
  \`export default () => mainTabs.screen("news", [ …content ])\` — tab roots AND pushed detail
43334
43317
  screens that keep the bar (the news lane's article screen mounts \`"news"\` too). A screen that
@@ -43340,8 +43323,9 @@ Most apps have one — declare it ONCE, in source, and never draw edges for it:
43340
43323
  (see the meta.json section) — groups are for the flows *outside* the bar.
43341
43324
  - Style through theme variables: the bar reads \`var(--primaryColor)\`, \`var(--mutedColor)\`,
43342
43325
  \`var(--tabbarBg)\`, \`var(--screenBg)\` (with fallbacks) — one \`theme({ primaryColor: … })\` call
43343
- restyles it app-wide. You may restyle \`shared/tabs.ts\` itself; keep the \`defineTabs({ … })\`
43344
- declaration and \`.screen("<tab>", …)\` call shapes intact (the canvas parses them).
43326
+ in \`shared/tokens.ts\` restyles it app-wide (and the built app's bar with it). Keep the
43327
+ \`defineTabs({ … })\` declaration and \`.screen("<tab>", …)\` call shapes intact (the canvas
43328
+ parses them).
43345
43329
 
43346
43330
  ## Screen states (variants)
43347
43331
 
@@ -43606,12 +43590,17 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
43606
43590
  if (!existsSync19(dir))
43607
43591
  return [];
43608
43592
  return readdirSync10(dir).filter((f3) => f3.endsWith(".ts") && !f3.startsWith(".")).map((f3) => f3.slice(0, -3)).sort();
43609
- }, readMetaSizes = (designDir) => {
43593
+ }, PHONE_SIZE, readMetaFrames = (designDir) => {
43610
43594
  try {
43611
43595
  const meta = JSON.parse(readFileSync18(join19(designDir, "meta.json"), "utf8"));
43612
- return meta?.screens ?? {};
43596
+ const d = meta?.defaultSize;
43597
+ const valid = Array.isArray(d) && d.length === 2 && d.every((n2) => typeof n2 === "number" && Number.isFinite(n2) && n2 >= 64 && n2 <= 8192);
43598
+ return {
43599
+ screens: meta?.screens ?? {},
43600
+ defaultSize: valid ? [Math.round(d[0]), Math.round(d[1])] : PHONE_SIZE
43601
+ };
43613
43602
  } catch {
43614
- return {};
43603
+ return { screens: {}, defaultSize: PHONE_SIZE };
43615
43604
  }
43616
43605
  }, writeMcpConfig = (ctx, port) => {
43617
43606
  const path2 = join19(ctx.root, ".mcp.json");
@@ -43637,7 +43626,7 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
43637
43626
  log(`${c.green(existing ? "updated" : "created")} .mcp.json (lecodes-design MCP → ${entry.url})`);
43638
43627
  }, init = async (ctx, args) => {
43639
43628
  const files = [
43640
- ["meta.json", META_JSON],
43629
+ ["meta.json", flagBool(args, "desktop") ? META_JSON_DESKTOP : META_JSON],
43641
43630
  ["spec.md", SPEC_MD],
43642
43631
  ["README.md", README_HINT],
43643
43632
  ["CLAUDE.md", CLAUDE_MD],
@@ -43723,14 +43712,14 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
43723
43712
  }, 120000);
43724
43713
  killer.unref?.();
43725
43714
  const png2 = flagBool(args, "png") || flagStr(args, "png") !== undefined;
43726
- const sizes = readMetaSizes(ctx.designDir);
43715
+ const frames = readMetaFrames(ctx.designDir);
43727
43716
  const outDir = flagStr(args, "out-dir") ?? join19(ctx.designDir, ".snapshots");
43728
43717
  const single = targets.length === 1 && requested.length === 1 && !png2 && !allStates && !flagStr(args, "out-dir");
43729
43718
  const onConsole = flagBool(args, "logs") ? (level, text) => logErr(`${c.dim(`[${level}]`)} ${text}`) : undefined;
43730
43719
  if (!single)
43731
43720
  mkdirSync14(outDir, { recursive: true });
43732
43721
  for (const id of targets) {
43733
- const [width, height] = sizes[id]?.size ?? [390, 844];
43722
+ const [width, height] = frames.screens[id]?.size ?? frames.defaultSize;
43734
43723
  let plan = [{ state: undefined, bases: [id] }];
43735
43724
  if (discover) {
43736
43725
  const { states, canonical } = discover(readFileSync18(join19(ctx.designDir, "screens", `${id}.ts`), "utf8"));
@@ -43804,7 +43793,8 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
43804
43793
  state,
43805
43794
  assets: "disk"
43806
43795
  });
43807
- const [width, height] = readMetaSizes(ctx.designDir)[id]?.size ?? [390, 844];
43796
+ const frames = readMetaFrames(ctx.designDir);
43797
+ const [width, height] = frames.screens[id]?.size ?? frames.defaultSize;
43808
43798
  const warnings = [];
43809
43799
  let json;
43810
43800
  let png2;
@@ -44199,6 +44189,7 @@ var init_design = __esm(() => {
44199
44189
  init_config();
44200
44190
  init_api();
44201
44191
  init_types4();
44192
+ PHONE_SIZE = [390, 844];
44202
44193
  });
44203
44194
 
44204
44195
  // ../../node_modules/.bun/qrcode-terminal@0.12.0/node_modules/qrcode-terminal/vendor/QRCode/QRMode.js
@@ -45220,7 +45211,7 @@ var require_main = __commonJS((exports, module) => {
45220
45211
  // package.json
45221
45212
  var package_default = {
45222
45213
  name: "lecodes-cli",
45223
- version: "0.13.2",
45214
+ version: "0.14.0",
45224
45215
  dependencies: {
45225
45216
  "@letary/chisel": "^0.6.0",
45226
45217
  jimp: "^1.6.1"
@@ -45233,8 +45224,8 @@ var package_default = {
45233
45224
  sdk: "workspace:*"
45234
45225
  },
45235
45226
  peerDependencies: {
45236
- "lecodes-design": "^0.6.0",
45237
- "lecodes-renderer": "^0.8.1",
45227
+ "lecodes-design": "^0.6.1",
45228
+ "lecodes-renderer": "^0.8.2",
45238
45229
  "lecodes-3d-editor": "^0.2.0"
45239
45230
  },
45240
45231
  bin: {
@@ -46353,6 +46344,17 @@ var FIRST_PARTY_PLUGINS = {
46353
46344
  geolocation: "letary/lecodes-plugins",
46354
46345
  push: "letary/lecodes-plugins"
46355
46346
  };
46347
+ var readAppConfigAt = (root) => readJson(join12(root, "app.json"));
46348
+ var DESKTOP_DEFAULT_WINDOW = { width: 1280, height: 720 };
46349
+ var desktopDefaultViewport = (root) => {
46350
+ const cfg = readAppConfigAt(root);
46351
+ if (!cfg?.desktop || cfg.ios || cfg.android)
46352
+ return null;
46353
+ return {
46354
+ width: cfg.desktop.window?.width ?? DESKTOP_DEFAULT_WINDOW.width,
46355
+ height: cfg.desktop.window?.height ?? DESKTOP_DEFAULT_WINDOW.height
46356
+ };
46357
+ };
46356
46358
  var readJson = (path) => {
46357
46359
  if (!existsSync10(path))
46358
46360
  return null;
@@ -48914,6 +48916,7 @@ var create = async (args) => {
48914
48916
 
48915
48917
  // src/commands/desktop.ts
48916
48918
  init_util();
48919
+ init_manifest();
48917
48920
 
48918
48921
  // src/desktopRenderer.ts
48919
48922
  init_util();
@@ -49064,6 +49067,20 @@ var updateDesktop = async () => {
49064
49067
  else
49065
49068
  note(`Desktop renderer set to ${latest} (was ${before}).`);
49066
49069
  };
49070
+ var windowEnv = (opts) => {
49071
+ const env = {};
49072
+ const width = opts.width ?? (opts.height !== undefined ? 1280 : undefined);
49073
+ const height = opts.height ?? (opts.width !== undefined ? 720 : undefined);
49074
+ if (width && height)
49075
+ env.CREATOR_WINDOW_SIZE = `${width}x${height}`;
49076
+ if (opts.minWidth && opts.minHeight)
49077
+ env.CREATOR_MIN_WINDOW_SIZE = `${opts.minWidth}x${opts.minHeight}`;
49078
+ if (opts.title)
49079
+ env.CREATOR_WINDOW_TITLE = opts.title;
49080
+ if (opts.resizable === false)
49081
+ env.CREATOR_WINDOW_FIXED = "1";
49082
+ return env;
49083
+ };
49067
49084
  var stageBundle = (js, resources) => {
49068
49085
  const runDir = mkdtempSync(join18(tmpdir(), "lecodes-render-"));
49069
49086
  writeFileSync14(join18(runDir, "app.js"), js);
@@ -49078,9 +49095,7 @@ var runDesktopApp = async (js, resources, opts = {}) => {
49078
49095
  const exe = await resolveDesktopExe();
49079
49096
  const runDir = stageBundle(js, resources);
49080
49097
  try {
49081
- const env = { ...process.env };
49082
- if (opts.width && opts.height)
49083
- env.CREATOR_WINDOW_SIZE = `${opts.width}x${opts.height}`;
49098
+ const env = { ...process.env, ...windowEnv(opts) };
49084
49099
  await new Promise((done, fail) => {
49085
49100
  const child = spawn2(exe, [join18(runDir, "app.js")], { cwd: runDir, env, stdio: ["ignore", "inherit", "inherit"] });
49086
49101
  child.on("error", (e2) => fail(new CliError(`Can't run the desktop host: ${e2.message}`)));
@@ -49095,6 +49110,15 @@ var runDesktopApp = async (js, resources, opts = {}) => {
49095
49110
  rmSync4(runDir, { recursive: true, force: true });
49096
49111
  }
49097
49112
  };
49113
+ var runDesktopDev = async (url, opts = {}) => {
49114
+ const exe = await resolveDesktopExe();
49115
+ const env = { ...process.env, ...windowEnv(opts) };
49116
+ await new Promise((done, fail) => {
49117
+ const child = spawn2(exe, [url], { env, stdio: ["ignore", "ignore", "inherit"] });
49118
+ child.on("error", (e2) => fail(new CliError(`Can't run the desktop host: ${e2.message}`)));
49119
+ child.on("exit", () => done());
49120
+ });
49121
+ };
49098
49122
  var runDesktopRender = async (js, resources, opts) => {
49099
49123
  const exe = await resolveDesktopExe();
49100
49124
  const runDir = stageBundle(js, resources);
@@ -49148,11 +49172,24 @@ ${tail.join("").trimEnd()}` : "";
49148
49172
 
49149
49173
  // src/commands/desktop.ts
49150
49174
  var run = async (args) => {
49175
+ let root;
49176
+ try {
49177
+ root = findProjectRoot(process.cwd());
49178
+ } catch {
49179
+ root = process.cwd();
49180
+ }
49181
+ const appCfg = readAppConfigAt(root);
49182
+ const win = appCfg?.desktop?.window;
49151
49183
  const { js, resources } = await buildBundle(args);
49152
- const width = Number(flagStr(args, "width")) || undefined;
49153
- const height = Number(flagStr(args, "height")) || undefined;
49154
49184
  note("Launching LeCodes Desktop… (close the window to return)");
49155
- await runDesktopApp(js, resources, { width, height });
49185
+ await runDesktopApp(js, resources, {
49186
+ width: Number(flagStr(args, "width")) || win?.width,
49187
+ height: Number(flagStr(args, "height")) || win?.height,
49188
+ minWidth: win?.minWidth,
49189
+ minHeight: win?.minHeight,
49190
+ resizable: win?.resizable,
49191
+ title: flagStr(args, "title") ?? win?.title ?? appCfg?.name
49192
+ });
49156
49193
  };
49157
49194
  var status = async () => {
49158
49195
  const selected = selectedDesktopVersion();
@@ -49794,6 +49831,19 @@ ${result.error}`);
49794
49831
  note(`Token stored in ${LECODES_DIR}/dev.json (delete it to rotate the URL).`);
49795
49832
  log("");
49796
49833
  log(`Watching ${root} — save a file to hot-reload. Ctrl+C to stop.`);
49834
+ if (flagBool(args, "desktop")) {
49835
+ const appCfg = readAppConfigAt(root);
49836
+ const win = appCfg?.desktop?.window;
49837
+ note("Opening LeCodes Desktop against the dev server…");
49838
+ runDesktopDev(server.url, {
49839
+ width: win?.width,
49840
+ height: win?.height,
49841
+ minWidth: win?.minWidth,
49842
+ minHeight: win?.minHeight,
49843
+ resizable: win?.resizable,
49844
+ title: win?.title ?? appCfg?.name ?? name
49845
+ }).then(() => note("Desktop window closed — the dev server keeps running (Ctrl+C to stop)."), (e2) => warnErr(`Desktop host: ${e2 instanceof Error ? e2.message : String(e2)}`));
49846
+ }
49797
49847
  };
49798
49848
 
49799
49849
  // src/commands/diff.ts
@@ -50037,6 +50087,13 @@ const screen = UIScreen(
50037
50087
 
50038
50088
  screen.open()
50039
50089
  `;
50090
+ var appJsonDesktop = (name) => JSON.stringify({
50091
+ name,
50092
+ desktop: {
50093
+ window: { width: 1280, height: 800, minWidth: 640, minHeight: 480 }
50094
+ }
50095
+ }, null, 2) + `
50096
+ `;
50040
50097
  var readmeTemplate = (name) => `# ${name}
50041
50098
 
50042
50099
  A LeCodes app. The whole SDK is available as globals (\`UIScreen\`, \`UIText\`, \`Scene2D\`, …) —
@@ -50111,8 +50168,11 @@ var init2 = async (args) => {
50111
50168
  log(`${c.green("created")} ${rel}`);
50112
50169
  created++;
50113
50170
  };
50171
+ const forDesktop = flagBool(args, "desktop");
50114
50172
  write("main.ts", STARTER2);
50115
50173
  write("README.md", readmeTemplate(name));
50174
+ if (forDesktop)
50175
+ write("app.json", appJsonDesktop(name));
50116
50176
  write("CLAUDE.md", appClaudeMd(name));
50117
50177
  write("tests/counter.flow.json", STARTER_FLOW);
50118
50178
  write(".lecodesignore", DEFAULT_IGNORE_FILE);
@@ -50125,9 +50185,15 @@ var init2 = async (args) => {
50125
50185
  log("");
50126
50186
  success(created ? `Local project ${c.bold(name)} ready${dirArg === "." ? "" : ` in ${c.bold(dirArg)}/`}.` : `Project ${c.bold(name)} already initialized.`);
50127
50187
  const cd = dirArg === "." ? "" : `cd ${dirArg} && `;
50128
- log(` ${c.bold(cd + "lecodes dev")} run it on a device with hot reload`);
50129
- log(` ${c.bold(cd + "lecodes test")} run the flow tests headless (no device)`);
50130
- log(` ${c.bold(cd + "lecodes compile -o app.js")} bundle it for LeCodes Desktop`);
50188
+ if (forDesktop) {
50189
+ log(` ${c.bold(cd + "lecodes dev --desktop")} run it in a desktop window with hot reload`);
50190
+ log(` ${c.bold(cd + "lecodes test")} run the flow tests headless (desktop-sized)`);
50191
+ log(` ${c.bold(cd + "lecodes desktop run")} open the compiled app in the desktop host`);
50192
+ } else {
50193
+ log(` ${c.bold(cd + "lecodes dev")} run it on a device with hot reload`);
50194
+ log(` ${c.bold(cd + "lecodes test")} run the flow tests headless (no device)`);
50195
+ log(` ${c.bold(cd + "lecodes compile -o app.js")} bundle it for LeCodes Desktop`);
50196
+ }
50131
50197
  };
50132
50198
 
50133
50199
  // src/commands/link.ts
@@ -50477,6 +50543,7 @@ var compileHeadlessBundle = async (args) => {
50477
50543
  };
50478
50544
 
50479
50545
  // src/commands/render.ts
50546
+ init_manifest();
50480
50547
  init_util();
50481
50548
  var DEVICE_SAFE_AREA = {
50482
50549
  none: { insets: [0, 0, 0, 0], bars: [] },
@@ -50546,8 +50613,15 @@ var readScenarioFile = (path2) => {
50546
50613
  return file;
50547
50614
  };
50548
50615
  var renderDesktop = async (args) => {
50549
- const width = Number(flagStr(args, "width")) || 390;
50550
- const height = Number(flagStr(args, "height")) || 844;
50616
+ let root;
50617
+ try {
50618
+ root = findProjectRoot(process.cwd());
50619
+ } catch {
50620
+ root = process.cwd();
50621
+ }
50622
+ const win = readAppConfigAt(root)?.desktop?.window;
50623
+ const width = Number(flagStr(args, "width")) || win?.width || 390;
50624
+ const height = Number(flagStr(args, "height")) || win?.height || 844;
50551
50625
  const frames = Number(flagStr(args, "frames")) || 60;
50552
50626
  const timeoutMs = Number(flagStr(args, "timeout")) || 60000;
50553
50627
  const pngFlag = flagStr(args, "png") ?? "screenshot.png";
@@ -50575,10 +50649,11 @@ var render = async (args) => {
50575
50649
  process.exit(1);
50576
50650
  }, timeoutMs);
50577
50651
  killer.unref?.();
50578
- const { js, warnings, useLocalAssets } = await compileHeadlessBundle(args);
50652
+ const { js, warnings, useLocalAssets, root } = await compileHeadlessBundle(args);
50579
50653
  const renderer = await requireRenderer();
50580
- const width = Number(flagStr(args, "width")) || scenario?.width || 390;
50581
- const height = Number(flagStr(args, "height")) || scenario?.height || 844;
50654
+ const projectViewport = desktopDefaultViewport(root);
50655
+ const width = Number(flagStr(args, "width")) || scenario?.width || projectViewport?.width || 390;
50656
+ const height = Number(flagStr(args, "height")) || scenario?.height || projectViewport?.height || 844;
50582
50657
  const settleStr = flagStr(args, "settle") ?? (scenario?.settle !== undefined ? String(scenario.settle) : undefined);
50583
50658
  const timeStr = flagStr(args, "time");
50584
50659
  const onConsole = flagBool(args, "logs") ? (level, text2) => logErr(`${c.dim(`[${level}]`)} ${text2}`) : undefined;
@@ -51293,6 +51368,7 @@ var readFlowFile = (abs, displayName) => {
51293
51368
  var test = async (args) => {
51294
51369
  const { js, warnings, useLocalAssets, root } = await compileHeadlessBundle(args);
51295
51370
  const renderer = await requireRenderer();
51371
+ const projectViewport = desktopDefaultViewport(root);
51296
51372
  const testsDir = resolve12(root, flagStr(args, "dir") ?? "tests");
51297
51373
  const designMetaPath = resolve12(root, flagStr(args, "design-dir") ?? "design", "meta.json");
51298
51374
  const asJson = flagBool(args, "json");
@@ -51363,8 +51439,8 @@ var test = async (args) => {
51363
51439
  try {
51364
51440
  const steps = renderer.parseScenarioSteps(kase.steps);
51365
51441
  const session = await renderer.createSession(js, {
51366
- width: Number(flagStr(args, "width")) || kase.width || 390,
51367
- height: Number(flagStr(args, "height")) || kase.height || 844,
51442
+ width: Number(flagStr(args, "width")) || kase.width || projectViewport?.width || 390,
51443
+ height: Number(flagStr(args, "height")) || kase.height || projectViewport?.height || 844,
51368
51444
  settleMs: flagSettle !== undefined ? Number(flagSettle) : kase.settle,
51369
51445
  waitNetwork: flagBool(args, "wait-network"),
51370
51446
  localAssets: useLocalAssets,
@@ -51716,7 +51792,11 @@ ${c.bold("Commands:")}
51716
51792
  init [dir] Scaffold a new LOCAL project (no login/server): a starter
51717
51793
  main.ts, a CLAUDE.md + passing flow test (the headless
51718
51794
  render/test loop), IDE types + tsconfig, a .lecodesignore.
51719
- Run it with 'lecodes dev' or 'lecodes compile'.${INSTALL_HELP}
51795
+ Run it with 'lecodes dev' or 'lecodes compile'.
51796
+ --desktop also write an app.json declaring the desktop
51797
+ target (window size; 'dev --desktop',
51798
+ 'desktop run' and headless render/test
51799
+ pick it up)${INSTALL_HELP}
51720
51800
  login Authorize in the browser and store an access token
51721
51801
  --token <pat> store a token from the web UI instead
51722
51802
  --api <url> API origin (default https://le.codes)
@@ -51759,6 +51839,9 @@ ${c.bold("Commands:")}
51759
51839
  --host <ip> LAN address to advertise (default: auto-detect)
51760
51840
  --entry <path> override the entrypoint
51761
51841
  --no-qr don't render the QR code
51842
+ --desktop also open the app in the native desktop host
51843
+ (same hot reload; window from app.json's
51844
+ desktop block; closing it keeps the server)
51762
51845
  compile Compile the local project to JS (prints to stdout)
51763
51846
  (was 'lecodes preview' — the old name still works)
51764
51847
  -o, --out <file> write to a file instead of stdout
@@ -51824,6 +51907,8 @@ ${c.bold("Commands:")}
51824
51907
  --dir <folder> design folder (default design/)
51825
51908
  --no-open don't open the browser
51826
51909
  design init Scaffold the design folder (screens/, shared/, meta.json, CLAUDE.md)
51910
+ --desktop desktop-frame board (meta.json defaultSize
51911
+ [1280, 800] instead of the phone frame)
51827
51912
  design snapshot [screen…] Render design screens headless (needs 'lecodes-renderer')
51828
51913
  one screen → JSON to stdout; else files in design/.snapshots/
51829
51914
  --png also rasterize PNGs (needs @napi-rs/canvas)
@@ -51838,8 +51923,9 @@ ${c.bold("Commands:")}
51838
51923
  no args shows the version status (selected / downloaded /
51839
51924
  newest published)
51840
51925
  run compile the current project and open it in a real
51841
- desktop window (--width/--height set the window size;
51842
- --entry overrides the entrypoint)
51926
+ desktop window (size/min-size/title default from
51927
+ app.json's desktop block; --width/--height/--title
51928
+ override; --entry overrides the entrypoint)
51843
51929
  update move to the newest published release
51844
51930
  (cached in ~/.lecodes/renderer)
51845
51931
  scene [path] LeCodes Scene Editor — visually edit a .scene.ts (tree, inspector,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lecodes-cli",
3
- "version": "0.13.2",
3
+ "version": "0.14.0",
4
4
  "dependencies": {
5
5
  "@letary/chisel": "^0.6.0",
6
6
  "jimp": "^1.6.1"
@@ -13,8 +13,8 @@
13
13
  "sdk": "workspace:*"
14
14
  },
15
15
  "peerDependencies": {
16
- "lecodes-design": "^0.6.0",
17
- "lecodes-renderer": "^0.8.1",
16
+ "lecodes-design": "^0.6.1",
17
+ "lecodes-renderer": "^0.8.2",
18
18
  "lecodes-3d-editor": "^0.2.0"
19
19
  },
20
20
  "bin": {
@@ -0,0 +1,42 @@
1
+ // Compile-time injection of an aspect's runtime name. A user aspect carries its accessor name in the
2
+ // (type-erased) generic — `class Health extends Aspect<'health', Sprite> {…}` — so neither the
3
+ // bundler nor the runtime can see it after types are stripped. This pass injects the matching
4
+ // `static aspect = "health"` so node.aspect()/node.health work at runtime, exactly like the SDK's
5
+ // built-in aspects declare it by hand.
6
+ //
7
+ // Runs on PROJECT files only (the SDK's own aspects already declare `static aspect`). Idempotent:
8
+ // skips a class that already declares one. String-based, like assetMacro — no parser dependency.
9
+
10
+ // `class X extends Aspect<'name', …>` up to the class body's opening brace. Group 3 = the name.
11
+ const ASPECT_RE = /\bclass\s+\w+\s+extends\s+Aspect\s*<\s*(["'])((?:[^"'\\]|\\.)*)\1[^{};]*>\s*\{/g
12
+
13
+ /** Cheap pre-check so we don't scan files that define no aspects. */
14
+ export const hasAspectExtends = (src: string): boolean => /\bextends\s+Aspect\s*</.test(src)
15
+
16
+ /** Inject `static aspect = "<name>"` into each `class … extends Aspect<'<name>', …>`. */
17
+ export const injectAspectNames = (src: string): string => {
18
+ let out = ""
19
+ let last = 0
20
+ ASPECT_RE.lastIndex = 0
21
+ let m: RegExpExecArray | null
22
+ while ((m = ASPECT_RE.exec(src)) !== null) {
23
+ const braceEnd = m.index + m[0].length // index just past the opening `{`
24
+ if (!classBodyDeclaresAspect(src, braceEnd)) {
25
+ out += src.slice(last, braceEnd) + ` static aspect = ${JSON.stringify(m[2])};`
26
+ last = braceEnd
27
+ }
28
+ }
29
+ return last === 0 ? src : out + src.slice(last)
30
+ }
31
+
32
+ /** Does the class body starting just after `from` (its `{`) already declare a `static aspect`? */
33
+ const classBodyDeclaresAspect = (src: string, from: number): boolean => {
34
+ let depth = 1
35
+ let i = from
36
+ for (; i < src.length && depth > 0; i++) {
37
+ const c = src[i]
38
+ if (c === "{") depth++
39
+ else if (c === "}") depth--
40
+ }
41
+ return /\bstatic\b[^;{}()=]*\baspect\b/.test(src.slice(from, i))
42
+ }