lecodes-cli 0.13.1 → 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.
@@ -43217,13 +43198,13 @@ This is an intent-capture stage — screens run on inline mock data, before any
43217
43198
  ## Read this first, then meta.json
43218
43199
 
43219
43200
  \`meta.json\` is the app map: every screen (position, description) and the edges between them
43220
- (navigation intent). **Read it before opening any screen file.** It is the human's file — the
43221
- canvas and the developer own positions and descriptions. When you create a screen, add its meta
43222
- entry **without a \`pos\`** and add edges — the board **auto-places it next to its flow neighbours**,
43223
- so you never hand-pick coordinates (you can't see the board, and positions are the human's to own).
43224
- \`lecodes design arrange --new\` optionally writes those auto-placements into meta.json; the developer's
43225
- ⇥ Arrange button re-tidies the whole board. Never reposition or rewrite existing human-authored
43226
- positions or descriptions.
43201
+ (navigation intent). **Read it before opening any screen file.** When you create a screen, add its
43202
+ meta entry **without a \`pos\`** and add edges — the board places it from its structure (its tab
43203
+ lane, its group region, its flow neighbours) and **persists that position itself**; you never
43204
+ hand-pick coordinates (you can't see the board). To tidy after a batch of changes, call the
43205
+ \`arrange_screens\` MCP tool (or \`lecodes design arrange\`) — the layout engine knows tab rails,
43206
+ group regions and flow direction. Never reposition or rewrite existing \`pos\` values or
43207
+ human-authored descriptions — positions the developer dragged are theirs.
43227
43208
 
43228
43209
  \`\`\`json
43229
43210
  {
@@ -43247,11 +43228,15 @@ positions or descriptions.
43247
43228
  - **An edge from a screen to itself** (\`"from": "home", "to": "home@empty"\`) is a *state link*: the
43248
43229
  board draws it as a chip on the activator element (clicking it switches the tile's state), never
43249
43230
  as an arrow. Use it to record "tapping X puts this screen into state Y".
43250
- - **Group each screen** you create by its user-facing area — \`"group": "Onboarding"\` — 2–6 screens
43251
- per group (Onboarding, Checkout, Settings…). Groups render as labeled regions and \`arrange\` lays
43252
- each out as its own sub-flow, so grouping keeps big boards legible. Semantic clustering is your
43253
- job (you're good at it); coordinates are \`arrange\`'s (it can see the board, you can't). Don't
43254
- regroup existing screens unless asked — groups are part of the human's map.
43231
+ - **Group only what the tab bar doesn't already section.** Screens on a tab bar are sectioned by
43232
+ their lanes — never set a \`group\` on a tab root or any screen that mounts the bar (a group that
43233
+ duplicates a lane draws nothing and gets flagged for deletion). Group the screens *outside* the
43234
+ bar by user-facing area — \`"group": "Onboarding"\` — 2–6 screens per group (Onboarding,
43235
+ Recovery, Pickers…; on an app with no tab bar that means every screen). Groups render as labeled
43236
+ regions and \`arrange\` lays each out as its own sub-flow, so grouping keeps big boards legible.
43237
+ Semantic clustering is your job (you're good at it); coordinates are \`arrange\`'s (it can see
43238
+ the board, you can't). Don't regroup existing screens unless asked — groups are part of the
43239
+ human's map.
43255
43240
  - **Tag a screen's \`role\`** in its meta entry — its *kind* (\`list\`, \`detail\`, \`form\`, \`auth\`,
43256
43241
  \`modal\`…). Optional, renders on the board, captures concept the screen source can't;
43257
43242
  \`lecodes design check\` lists screens still untagged (never blocking). *(A screen's **states** are
@@ -43322,9 +43307,11 @@ keep in the chat.
43322
43307
 
43323
43308
  Most apps have one — declare it ONCE, in source, and never draw edges for it:
43324
43309
 
43325
- - \`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\`:
43326
43312
  \`export const mainTabs = defineTabs({ home: { label: "Home", icon: assetIcon("lucide:house") }, … })\`
43327
- — 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.
43328
43315
  - Every screen that shows the bar mounts it with a **literal** tab id:
43329
43316
  \`export default () => mainTabs.screen("news", [ …content ])\` — tab roots AND pushed detail
43330
43317
  screens that keep the bar (the news lane's article screen mounts \`"news"\` too). A screen that
@@ -43332,10 +43319,13 @@ Most apps have one — declare it ONCE, in source, and never draw edges for it:
43332
43319
  - The canvas discovers all of this from the source (like screen states) and draws the bar as a
43333
43320
  **rail + lane bands** — tab switching needs NO edges, and \`design_state\` serves the discovered
43334
43321
  \`hubs\`. In the live prototype, tapping a tab button jumps the board to that tab's root screen.
43322
+ - The lane bands replace groups for bar screens: don't \`group\` anything that mounts the bar
43323
+ (see the meta.json section) — groups are for the flows *outside* the bar.
43335
43324
  - Style through theme variables: the bar reads \`var(--primaryColor)\`, \`var(--mutedColor)\`,
43336
43325
  \`var(--tabbarBg)\`, \`var(--screenBg)\` (with fallbacks) — one \`theme({ primaryColor: … })\` call
43337
- restyles it app-wide. You may restyle \`shared/tabs.ts\` itself; keep the \`defineTabs({ … })\`
43338
- 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).
43339
43329
 
43340
43330
  ## Screen states (variants)
43341
43331
 
@@ -43497,8 +43487,8 @@ JSON by hand (a message missing its \`pending\` flag is never delivered, and not
43497
43487
  1. \`board_activity\` (if the server is up), then \`design_comments\` for anything open, then read
43498
43488
  \`meta.json\`, the \`shared/\` signatures, and only the screens you need.
43499
43489
  2. Create/edit screen files; register new screens in \`meta.json\` **without \`pos\`** and add edges —
43500
- they auto-place next to their neighbours on the board. (\`lecodes design arrange --new\` commits
43501
- those positions to meta.json if you want them written.)
43490
+ the board places them from their lane/group/flow structure and persists the result. After a
43491
+ larger batch, \`arrange_screens\` (MCP) or \`lecodes design arrange\` re-tidies the whole board.
43502
43492
  3. \`check_screens\` → \`render_screen\` (or \`lecodes design snapshot\`) → verify → fix.
43503
43493
  4. When the developer edits a description, text, or the flow on the canvas — visible in
43504
43494
  \`board_activity\` and in \`meta.json\`/the screen source — treat it as the updated spec.
@@ -43600,12 +43590,17 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
43600
43590
  if (!existsSync19(dir))
43601
43591
  return [];
43602
43592
  return readdirSync10(dir).filter((f3) => f3.endsWith(".ts") && !f3.startsWith(".")).map((f3) => f3.slice(0, -3)).sort();
43603
- }, readMetaSizes = (designDir) => {
43593
+ }, PHONE_SIZE, readMetaFrames = (designDir) => {
43604
43594
  try {
43605
43595
  const meta = JSON.parse(readFileSync18(join19(designDir, "meta.json"), "utf8"));
43606
- 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
+ };
43607
43602
  } catch {
43608
- return {};
43603
+ return { screens: {}, defaultSize: PHONE_SIZE };
43609
43604
  }
43610
43605
  }, writeMcpConfig = (ctx, port) => {
43611
43606
  const path2 = join19(ctx.root, ".mcp.json");
@@ -43631,7 +43626,7 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
43631
43626
  log(`${c.green(existing ? "updated" : "created")} .mcp.json (lecodes-design MCP → ${entry.url})`);
43632
43627
  }, init = async (ctx, args) => {
43633
43628
  const files = [
43634
- ["meta.json", META_JSON],
43629
+ ["meta.json", flagBool(args, "desktop") ? META_JSON_DESKTOP : META_JSON],
43635
43630
  ["spec.md", SPEC_MD],
43636
43631
  ["README.md", README_HINT],
43637
43632
  ["CLAUDE.md", CLAUDE_MD],
@@ -43717,14 +43712,14 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
43717
43712
  }, 120000);
43718
43713
  killer.unref?.();
43719
43714
  const png2 = flagBool(args, "png") || flagStr(args, "png") !== undefined;
43720
- const sizes = readMetaSizes(ctx.designDir);
43715
+ const frames = readMetaFrames(ctx.designDir);
43721
43716
  const outDir = flagStr(args, "out-dir") ?? join19(ctx.designDir, ".snapshots");
43722
43717
  const single = targets.length === 1 && requested.length === 1 && !png2 && !allStates && !flagStr(args, "out-dir");
43723
43718
  const onConsole = flagBool(args, "logs") ? (level, text) => logErr(`${c.dim(`[${level}]`)} ${text}`) : undefined;
43724
43719
  if (!single)
43725
43720
  mkdirSync14(outDir, { recursive: true });
43726
43721
  for (const id of targets) {
43727
- const [width, height] = sizes[id]?.size ?? [390, 844];
43722
+ const [width, height] = frames.screens[id]?.size ?? frames.defaultSize;
43728
43723
  let plan = [{ state: undefined, bases: [id] }];
43729
43724
  if (discover) {
43730
43725
  const { states, canonical } = discover(readFileSync18(join19(ctx.designDir, "screens", `${id}.ts`), "utf8"));
@@ -43798,7 +43793,8 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
43798
43793
  state,
43799
43794
  assets: "disk"
43800
43795
  });
43801
- 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;
43802
43798
  const warnings = [];
43803
43799
  let json;
43804
43800
  let png2;
@@ -43849,11 +43845,12 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, getContext = (args) => {
43849
43845
  const designServer = await loadDesignServer(ctx);
43850
43846
  const onlyNew = flagBool(args, "new");
43851
43847
  const summary = designServer.arrangeDesign(ctx.designDir, { onlyNew });
43848
+ const score = summary.score ? ` (score ${summary.score.before.toFixed(0)} → ${summary.score.after.toFixed(0)}, lower is better)` : "";
43852
43849
  if (!summary.changed) {
43853
- log(onlyNew ? "No unplaced screens — nothing to arrange." : "Already tidy — the current layout wins, nothing moved.");
43850
+ log(onlyNew ? "No unplaced screens — nothing to arrange." : `Already in its arranged shape — nothing moved${score}.`);
43854
43851
  return;
43855
43852
  }
43856
- log(`${c.green("✓")} ${onlyNew ? "Placed" : "Arranged"} ${summary.moved} screen${summary.moved === 1 ? "" : "s"} in ${ctx.dirName}/meta.json.`);
43853
+ log(`${c.green("✓")} ${onlyNew ? "Placed" : "Arranged"} ${summary.moved} screen${summary.moved === 1 ? "" : "s"} in ${ctx.dirName}/meta.json${score}.`);
43857
43854
  }, share = async (ctx, args) => {
43858
43855
  if (!ctx.manifest) {
43859
43856
  throw new CliError("`lecodes design share` needs a cloned project. Run it inside one, and push the design first (lecodes push).");
@@ -44192,6 +44189,7 @@ var init_design = __esm(() => {
44192
44189
  init_config();
44193
44190
  init_api();
44194
44191
  init_types4();
44192
+ PHONE_SIZE = [390, 844];
44195
44193
  });
44196
44194
 
44197
44195
  // ../../node_modules/.bun/qrcode-terminal@0.12.0/node_modules/qrcode-terminal/vendor/QRCode/QRMode.js
@@ -45213,7 +45211,7 @@ var require_main = __commonJS((exports, module) => {
45213
45211
  // package.json
45214
45212
  var package_default = {
45215
45213
  name: "lecodes-cli",
45216
- version: "0.13.1",
45214
+ version: "0.14.0",
45217
45215
  dependencies: {
45218
45216
  "@letary/chisel": "^0.6.0",
45219
45217
  jimp: "^1.6.1"
@@ -45226,8 +45224,8 @@ var package_default = {
45226
45224
  sdk: "workspace:*"
45227
45225
  },
45228
45226
  peerDependencies: {
45229
- "lecodes-design": "^0.5.0",
45230
- "lecodes-renderer": "^0.8.0",
45227
+ "lecodes-design": "^0.6.1",
45228
+ "lecodes-renderer": "^0.8.2",
45231
45229
  "lecodes-3d-editor": "^0.2.0"
45232
45230
  },
45233
45231
  bin: {
@@ -46346,6 +46344,17 @@ var FIRST_PARTY_PLUGINS = {
46346
46344
  geolocation: "letary/lecodes-plugins",
46347
46345
  push: "letary/lecodes-plugins"
46348
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
+ };
46349
46358
  var readJson = (path) => {
46350
46359
  if (!existsSync10(path))
46351
46360
  return null;
@@ -48907,6 +48916,7 @@ var create = async (args) => {
48907
48916
 
48908
48917
  // src/commands/desktop.ts
48909
48918
  init_util();
48919
+ init_manifest();
48910
48920
 
48911
48921
  // src/desktopRenderer.ts
48912
48922
  init_util();
@@ -49057,6 +49067,20 @@ var updateDesktop = async () => {
49057
49067
  else
49058
49068
  note(`Desktop renderer set to ${latest} (was ${before}).`);
49059
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
+ };
49060
49084
  var stageBundle = (js, resources) => {
49061
49085
  const runDir = mkdtempSync(join18(tmpdir(), "lecodes-render-"));
49062
49086
  writeFileSync14(join18(runDir, "app.js"), js);
@@ -49071,9 +49095,7 @@ var runDesktopApp = async (js, resources, opts = {}) => {
49071
49095
  const exe = await resolveDesktopExe();
49072
49096
  const runDir = stageBundle(js, resources);
49073
49097
  try {
49074
- const env = { ...process.env };
49075
- if (opts.width && opts.height)
49076
- env.CREATOR_WINDOW_SIZE = `${opts.width}x${opts.height}`;
49098
+ const env = { ...process.env, ...windowEnv(opts) };
49077
49099
  await new Promise((done, fail) => {
49078
49100
  const child = spawn2(exe, [join18(runDir, "app.js")], { cwd: runDir, env, stdio: ["ignore", "inherit", "inherit"] });
49079
49101
  child.on("error", (e2) => fail(new CliError(`Can't run the desktop host: ${e2.message}`)));
@@ -49088,6 +49110,15 @@ var runDesktopApp = async (js, resources, opts = {}) => {
49088
49110
  rmSync4(runDir, { recursive: true, force: true });
49089
49111
  }
49090
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
+ };
49091
49122
  var runDesktopRender = async (js, resources, opts) => {
49092
49123
  const exe = await resolveDesktopExe();
49093
49124
  const runDir = stageBundle(js, resources);
@@ -49141,11 +49172,24 @@ ${tail.join("").trimEnd()}` : "";
49141
49172
 
49142
49173
  // src/commands/desktop.ts
49143
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;
49144
49183
  const { js, resources } = await buildBundle(args);
49145
- const width = Number(flagStr(args, "width")) || undefined;
49146
- const height = Number(flagStr(args, "height")) || undefined;
49147
49184
  note("Launching LeCodes Desktop… (close the window to return)");
49148
- 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
+ });
49149
49193
  };
49150
49194
  var status = async () => {
49151
49195
  const selected = selectedDesktopVersion();
@@ -49787,6 +49831,19 @@ ${result.error}`);
49787
49831
  note(`Token stored in ${LECODES_DIR}/dev.json (delete it to rotate the URL).`);
49788
49832
  log("");
49789
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
+ }
49790
49847
  };
49791
49848
 
49792
49849
  // src/commands/diff.ts
@@ -50030,6 +50087,13 @@ const screen = UIScreen(
50030
50087
 
50031
50088
  screen.open()
50032
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
+ `;
50033
50097
  var readmeTemplate = (name) => `# ${name}
50034
50098
 
50035
50099
  A LeCodes app. The whole SDK is available as globals (\`UIScreen\`, \`UIText\`, \`Scene2D\`, …) —
@@ -50104,8 +50168,11 @@ var init2 = async (args) => {
50104
50168
  log(`${c.green("created")} ${rel}`);
50105
50169
  created++;
50106
50170
  };
50171
+ const forDesktop = flagBool(args, "desktop");
50107
50172
  write("main.ts", STARTER2);
50108
50173
  write("README.md", readmeTemplate(name));
50174
+ if (forDesktop)
50175
+ write("app.json", appJsonDesktop(name));
50109
50176
  write("CLAUDE.md", appClaudeMd(name));
50110
50177
  write("tests/counter.flow.json", STARTER_FLOW);
50111
50178
  write(".lecodesignore", DEFAULT_IGNORE_FILE);
@@ -50118,9 +50185,15 @@ var init2 = async (args) => {
50118
50185
  log("");
50119
50186
  success(created ? `Local project ${c.bold(name)} ready${dirArg === "." ? "" : ` in ${c.bold(dirArg)}/`}.` : `Project ${c.bold(name)} already initialized.`);
50120
50187
  const cd = dirArg === "." ? "" : `cd ${dirArg} && `;
50121
- log(` ${c.bold(cd + "lecodes dev")} run it on a device with hot reload`);
50122
- log(` ${c.bold(cd + "lecodes test")} run the flow tests headless (no device)`);
50123
- 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
+ }
50124
50197
  };
50125
50198
 
50126
50199
  // src/commands/link.ts
@@ -50470,6 +50543,7 @@ var compileHeadlessBundle = async (args) => {
50470
50543
  };
50471
50544
 
50472
50545
  // src/commands/render.ts
50546
+ init_manifest();
50473
50547
  init_util();
50474
50548
  var DEVICE_SAFE_AREA = {
50475
50549
  none: { insets: [0, 0, 0, 0], bars: [] },
@@ -50539,8 +50613,15 @@ var readScenarioFile = (path2) => {
50539
50613
  return file;
50540
50614
  };
50541
50615
  var renderDesktop = async (args) => {
50542
- const width = Number(flagStr(args, "width")) || 390;
50543
- 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;
50544
50625
  const frames = Number(flagStr(args, "frames")) || 60;
50545
50626
  const timeoutMs = Number(flagStr(args, "timeout")) || 60000;
50546
50627
  const pngFlag = flagStr(args, "png") ?? "screenshot.png";
@@ -50568,10 +50649,11 @@ var render = async (args) => {
50568
50649
  process.exit(1);
50569
50650
  }, timeoutMs);
50570
50651
  killer.unref?.();
50571
- const { js, warnings, useLocalAssets } = await compileHeadlessBundle(args);
50652
+ const { js, warnings, useLocalAssets, root } = await compileHeadlessBundle(args);
50572
50653
  const renderer = await requireRenderer();
50573
- const width = Number(flagStr(args, "width")) || scenario?.width || 390;
50574
- 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;
50575
50657
  const settleStr = flagStr(args, "settle") ?? (scenario?.settle !== undefined ? String(scenario.settle) : undefined);
50576
50658
  const timeStr = flagStr(args, "time");
50577
50659
  const onConsole = flagBool(args, "logs") ? (level, text2) => logErr(`${c.dim(`[${level}]`)} ${text2}`) : undefined;
@@ -51286,6 +51368,7 @@ var readFlowFile = (abs, displayName) => {
51286
51368
  var test = async (args) => {
51287
51369
  const { js, warnings, useLocalAssets, root } = await compileHeadlessBundle(args);
51288
51370
  const renderer = await requireRenderer();
51371
+ const projectViewport = desktopDefaultViewport(root);
51289
51372
  const testsDir = resolve12(root, flagStr(args, "dir") ?? "tests");
51290
51373
  const designMetaPath = resolve12(root, flagStr(args, "design-dir") ?? "design", "meta.json");
51291
51374
  const asJson = flagBool(args, "json");
@@ -51356,8 +51439,8 @@ var test = async (args) => {
51356
51439
  try {
51357
51440
  const steps = renderer.parseScenarioSteps(kase.steps);
51358
51441
  const session = await renderer.createSession(js, {
51359
- width: Number(flagStr(args, "width")) || kase.width || 390,
51360
- 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,
51361
51444
  settleMs: flagSettle !== undefined ? Number(flagSettle) : kase.settle,
51362
51445
  waitNetwork: flagBool(args, "wait-network"),
51363
51446
  localAssets: useLocalAssets,
@@ -51709,7 +51792,11 @@ ${c.bold("Commands:")}
51709
51792
  init [dir] Scaffold a new LOCAL project (no login/server): a starter
51710
51793
  main.ts, a CLAUDE.md + passing flow test (the headless
51711
51794
  render/test loop), IDE types + tsconfig, a .lecodesignore.
51712
- 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}
51713
51800
  login Authorize in the browser and store an access token
51714
51801
  --token <pat> store a token from the web UI instead
51715
51802
  --api <url> API origin (default https://le.codes)
@@ -51752,6 +51839,9 @@ ${c.bold("Commands:")}
51752
51839
  --host <ip> LAN address to advertise (default: auto-detect)
51753
51840
  --entry <path> override the entrypoint
51754
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)
51755
51845
  compile Compile the local project to JS (prints to stdout)
51756
51846
  (was 'lecodes preview' — the old name still works)
51757
51847
  -o, --out <file> write to a file instead of stdout
@@ -51817,6 +51907,8 @@ ${c.bold("Commands:")}
51817
51907
  --dir <folder> design folder (default design/)
51818
51908
  --no-open don't open the browser
51819
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)
51820
51912
  design snapshot [screen…] Render design screens headless (needs 'lecodes-renderer')
51821
51913
  one screen → JSON to stdout; else files in design/.snapshots/
51822
51914
  --png also rasterize PNGs (needs @napi-rs/canvas)
@@ -51831,8 +51923,9 @@ ${c.bold("Commands:")}
51831
51923
  no args shows the version status (selected / downloaded /
51832
51924
  newest published)
51833
51925
  run compile the current project and open it in a real
51834
- desktop window (--width/--height set the window size;
51835
- --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)
51836
51929
  update move to the newest published release
51837
51930
  (cached in ~/.lecodes/renderer)
51838
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.1",
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.5.0",
17
- "lecodes-renderer": "^0.8.0",
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": {
@@ -16,9 +16,9 @@
16
16
  *
17
17
  * WHERE the SVG bodies come from is the caller's business, passed in as a resolver:
18
18
  * - CLI design compile: vendored into the design folder from the icon registry (lock + notices);
19
- * - backend design board: the already-pushed `design/assets/icons/**` text assets, no registry;
20
19
  * - CLI app compiles: the user-level cache (~/.lecodes/cache/icons) + registry on miss;
21
- * - backend app compile: a server-side cache + registry on miss.
20
+ * - backend app compile + design board: vendored `assets/icons/**` text assets first, then a
21
+ * server-side cache + registry on miss.
22
22
  * App compiles never write icons into the project — attribution for platform-deployed apps rides
23
23
  * the `// icons:` bundle header (buildIconsHeaderValue) and the platform, not an in-repo notice.
24
24
  */
@@ -358,10 +358,12 @@ export const iconNotFoundError = async (
358
358
  return new Error(`assetIcon: unknown icon pack "${pack}" (in "${pack}:${name}"). Available packs: ${packs.join(", ")}.`)
359
359
  } catch { /* corrupt licenses → fall through to the connectivity error */ }
360
360
  }
361
- return new Error(
361
+ // `transient` lets callers that cache compile results skip caching this one: unlike a missing
362
+ // icon, an unreachable registry is not a property of the source being compiled.
363
+ return Object.assign(new Error(
362
364
  `assetIcon: can't reach the icon registry${registry ? ` (${registry})` : ""} to resolve "${pack}:${name}". `
363
365
  + `Check your connection, or set LECODES_ICON_REGISTRY.`,
364
- )
366
+ ), { transient: true })
365
367
  }
366
368
 
367
369
  /** `…assets/icons/<pack>/<name>.svg` — an icon the design flow vendored into the project tree. */
@@ -81,6 +81,7 @@ export {
81
81
  UIScrollable, type UIScrollableStyle,
82
82
  UIPager, type UIPagerStyle,
83
83
  UITabs, type UITabDef,
84
+ defineTabs, type TabDef, type TabsHandle,
84
85
  UIWidget, type UIWidgetStyle,
85
86
  UIModal, type UIModalStyle,
86
87
  UIBottomSheet, type UIBottomSheetStyle,
@@ -17,7 +17,7 @@ export { UIPopover, type UIPopoverStyle, type UIPopoverAnchor } from './UIPopove
17
17
  export { UIRow, UIColumn, UIBox, type UIContainerStyle } from './UIContainer'
18
18
  export { UIScrollable, type UIScrollableStyle } from './UIScrollable'
19
19
  export { UIPager, type UIPagerStyle } from './UIPager'
20
- export { UITabs, type UITabDef } from './UITabs'
20
+ export { UITabs, type UITabDef, defineTabs, type TabDef, type TabsHandle } from './UITabs'
21
21
 
22
22
  export { UIVirtualizedList } from './UIVirtualizedList'
23
23