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 +159 -73
- package/package.json +3 -3
- package/runtime/sdk/compile/aspectMacro.ts +42 -0
- package/runtime/sdk/compile/assetIconMacro.ts +384 -0
- package/runtime/sdk/compile/assetMacro.ts +45 -0
- package/runtime/sdk/compile/assetName.ts +50 -0
- package/runtime/sdk/compile/bundler.ts +244 -0
- package/runtime/sdk/compile/compileProject.ts +106 -0
- package/runtime/sdk/compile/detectEntry.ts +125 -0
- package/runtime/sdk/compile/fontMacro.ts +459 -0
- package/runtime/sdk/compile/fontRegistry.ts +78 -0
- package/runtime/sdk/compile/header.ts +55 -0
- package/runtime/sdk/compile/index.ts +53 -0
- package/runtime/sdk/compile/libraryImports.ts +52 -0
- package/runtime/sdk/compile/sceneEditor.ts +78 -0
- package/runtime/sdk/compile/sfnt.ts +98 -0
- package/runtime/sdk/compile/sourcemap.ts +25 -0
- package/runtime/sdk/inject.ts +1 -0
- package/runtime/sdk/ui/UI.ts +1 -1
- package/runtime/sdk/ui/UITabs.ts +65 -4
- package/runtime/sdk-types.json +1 -1
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
43131
|
-
//
|
|
43132
|
-
//
|
|
43133
|
-
//
|
|
43134
|
-
|
|
43135
|
-
|
|
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
|
-
- \`
|
|
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
|
|
43344
|
-
declaration and \`.screen("<tab>", …)\` call shapes intact (the canvas
|
|
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
|
-
},
|
|
43593
|
+
}, PHONE_SIZE, readMetaFrames = (designDir) => {
|
|
43610
43594
|
try {
|
|
43611
43595
|
const meta = JSON.parse(readFileSync18(join19(designDir, "meta.json"), "utf8"));
|
|
43612
|
-
|
|
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
|
|
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] =
|
|
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
|
|
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.
|
|
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.
|
|
45237
|
-
"lecodes-renderer": "^0.8.
|
|
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, {
|
|
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
|
-
|
|
50129
|
-
|
|
50130
|
-
|
|
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
|
-
|
|
50550
|
-
|
|
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
|
|
50581
|
-
const
|
|
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'
|
|
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 (
|
|
51842
|
-
|
|
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.
|
|
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.
|
|
17
|
-
"lecodes-renderer": "^0.8.
|
|
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
|
+
}
|