lecodes-cli 0.8.2 → 0.9.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 +432 -98
- package/package.json +3 -3
- package/runtime/sdk/inject.ts +1 -1
- package/runtime/sdk/ui/UI.ts +1 -1
- package/runtime/sdk/ui/UINode.ts +7 -6
- package/runtime/sdk/ui/UIPager.ts +71 -3
- package/runtime/sdk/ui/UIScreen.ts +7 -57
- package/runtime/sdk/ui/UIScrollable.ts +19 -6
- package/runtime/sdk/ui/UIVirtualizedList.ts +12 -2
- package/runtime/sdk/ui/presentable.ts +2 -1
- package/runtime/sdk/ui/router.ts +4 -2
- package/runtime/sdk/ui/theme.ts +34 -7
- package/runtime/sdk-types.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2007,7 +2007,7 @@ var require_main = __commonJS((exports, module) => {
|
|
|
2007
2007
|
// package.json
|
|
2008
2008
|
var package_default = {
|
|
2009
2009
|
name: "lecodes-cli",
|
|
2010
|
-
version: "0.
|
|
2010
|
+
version: "0.9.0",
|
|
2011
2011
|
description: "Clone, edit and push LeCodes projects from your machine.",
|
|
2012
2012
|
type: "module",
|
|
2013
2013
|
license: "MIT",
|
|
@@ -2037,11 +2037,11 @@ var package_default = {
|
|
|
2037
2037
|
prepublishOnly: "bun run sync-peers && bun run build"
|
|
2038
2038
|
},
|
|
2039
2039
|
dependencies: {
|
|
2040
|
-
"@letary/chisel": "^0.4.
|
|
2040
|
+
"@letary/chisel": "^0.4.2"
|
|
2041
2041
|
},
|
|
2042
2042
|
peerDependencies: {
|
|
2043
2043
|
"lecodes-design": "^0.3.2",
|
|
2044
|
-
"lecodes-renderer": "^0.
|
|
2044
|
+
"lecodes-renderer": "^0.7.0",
|
|
2045
2045
|
"lecodes-3d-editor": "^0.1.0"
|
|
2046
2046
|
},
|
|
2047
2047
|
peerDependenciesMeta: {
|
|
@@ -4866,15 +4866,8 @@ var browserLogin = async (webBase, timeoutMs = 5 * 60 * 1000, open = openBrowser
|
|
|
4866
4866
|
}
|
|
4867
4867
|
};
|
|
4868
4868
|
|
|
4869
|
-
// src/
|
|
4870
|
-
var
|
|
4871
|
-
"screens": {
|
|
4872
|
-
"home": { "pos": [0, 0], "desc": "Example screen — replace me", "role": "list" }
|
|
4873
|
-
},
|
|
4874
|
-
"edges": []
|
|
4875
|
-
}
|
|
4876
|
-
`;
|
|
4877
|
-
var SPEC_MD = `# Concept
|
|
4869
|
+
// ../lecodes-design/src/core/concept.ts
|
|
4870
|
+
var SPEC_TEMPLATE = `# Concept
|
|
4878
4871
|
|
|
4879
4872
|
The narrative half of this design. \`meta.json\` + the canvas hold the *visual/flow* half (screens,
|
|
4880
4873
|
positions, edges); this file holds what never renders as a screen — the data model, the actors, and
|
|
@@ -4908,6 +4901,16 @@ which is the screen's *kind* — list, detail, form…)
|
|
|
4908
4901
|
Behaviour that isn't visible in a single screen — validation, permissions, notifications, edge
|
|
4909
4902
|
cases — and the decisions you've made and why, so they aren't re-litigated later.
|
|
4910
4903
|
`;
|
|
4904
|
+
|
|
4905
|
+
// ../lecodes-design/src/templates.ts
|
|
4906
|
+
var SPEC_MD = SPEC_TEMPLATE;
|
|
4907
|
+
var META_JSON = `{
|
|
4908
|
+
"screens": {
|
|
4909
|
+
"home": { "pos": [0, 0], "desc": "Example screen — replace me", "role": "list" }
|
|
4910
|
+
},
|
|
4911
|
+
"edges": []
|
|
4912
|
+
}
|
|
4913
|
+
`;
|
|
4911
4914
|
var TOKENS_TS = `// Design tokens shared by every screen. Colors are hex strings (the engine parses hex only).
|
|
4912
4915
|
// App font: one theme() call here sets the default text font everywhere. font("id") installs a
|
|
4913
4916
|
// registry family (vendored at compile time) and returns its name — no loading code needed.
|
|
@@ -5070,6 +5073,26 @@ positions or descriptions.
|
|
|
5070
5073
|
\`lecodes design check\` lists screens still untagged (never blocking). *(A screen's **states** are
|
|
5071
5074
|
a separate thing — declared in the source, not meta; see "Screen states" below.)*
|
|
5072
5075
|
|
|
5076
|
+
## The design is testable — names carry into the app
|
|
5077
|
+
|
|
5078
|
+
Every edge with an \`activator\` doubles as a **flow test** for the real app: \`lecodes test\` (run
|
|
5079
|
+
at the project root once the app exists) walks the graph from the start screen, taps each edge's
|
|
5080
|
+
activator, and expects to land on a screen named like the edge's target. The design stays a living
|
|
5081
|
+
contract instead of a discarded sketch — but only if the app build keeps the wiring:
|
|
5082
|
+
|
|
5083
|
+
- **Keep the design's \`name\`s** when translating a screen to app code — every element an edge's
|
|
5084
|
+
\`activator\` references keeps its \`name\` (\`.style({ name: "submit" })\`).
|
|
5085
|
+
- **Name each app screen's root with its design screen id** — \`UIScreen([ … ]).style({ name:
|
|
5086
|
+
"login" })\` — that's how the runner knows where a tap landed.
|
|
5087
|
+
- Edges with no activator (backend/system-driven) and \`@state\` edges are reported as *not
|
|
5088
|
+
machine-verifiable* — cover the important ones with hand-written \`tests/*.flow.json\` scenarios
|
|
5089
|
+
(same step schema as \`lecodes render --script\`; tab-bar navigation needs these too, since the
|
|
5090
|
+
bar draws no edges).
|
|
5091
|
+
|
|
5092
|
+
Names also appear in every headless render (JSON snapshots, the board's live elements), so they
|
|
5093
|
+
double as stable selectors for any scripted interaction — naming interactive elements is never
|
|
5094
|
+
wasted work.
|
|
5095
|
+
|
|
5073
5096
|
## The concept — spec.md
|
|
5074
5097
|
|
|
5075
5098
|
\`meta.json\` + the canvas are the *visual/flow* half of the design; \`spec.md\` is the *narrative*
|
|
@@ -5291,7 +5314,6 @@ var README_HINT = `# design/
|
|
|
5291
5314
|
LeCodes Design canvas — run \`lecodes design\` here and open the printed URL.
|
|
5292
5315
|
See CLAUDE.md for the conventions (it's written for AI assistants, but it's the spec).
|
|
5293
5316
|
`;
|
|
5294
|
-
|
|
5295
5317
|
// src/commands/design.ts
|
|
5296
5318
|
var DEFAULT_DIR = "design";
|
|
5297
5319
|
var DEFAULT_PORT = 4477;
|
|
@@ -6564,50 +6586,47 @@ var login = async (args) => {
|
|
|
6564
6586
|
};
|
|
6565
6587
|
|
|
6566
6588
|
// src/commands/render.ts
|
|
6567
|
-
import { writeFileSync as writeFileSync14 } from "node:fs";
|
|
6568
|
-
import { resolve as resolve6 } from "node:path";
|
|
6589
|
+
import { mkdirSync as mkdirSync13, readFileSync as readFileSync17, writeFileSync as writeFileSync14 } from "node:fs";
|
|
6590
|
+
import { dirname as dirname11, resolve as resolve6 } from "node:path";
|
|
6591
|
+
|
|
6592
|
+
// src/compile/headlessBundle.ts
|
|
6569
6593
|
var LOCAL_ASSET_SCHEME2 = "lecodesfile://";
|
|
6570
|
-
var
|
|
6571
|
-
|
|
6572
|
-
|
|
6573
|
-
|
|
6574
|
-
|
|
6575
|
-
|
|
6576
|
-
|
|
6577
|
-
|
|
6578
|
-
|
|
6579
|
-
if (n.length === 4 && n.every((v) => Number.isFinite(v)))
|
|
6580
|
-
return { safeArea: n, safeAreaBars: [] };
|
|
6581
|
-
throw new CliError(`--safe-area expects "top,right,bottom,left" (4 numbers), got "${explicit}".`);
|
|
6582
|
-
}
|
|
6583
|
-
const device = flagStr(args, "device");
|
|
6584
|
-
if (device) {
|
|
6585
|
-
const preset = DEVICE_SAFE_AREA[device];
|
|
6586
|
-
if (!preset)
|
|
6587
|
-
throw new CliError(`Unknown --device "${device}". Options: ${Object.keys(DEVICE_SAFE_AREA).join(", ")}.`);
|
|
6588
|
-
return { safeArea: preset.insets, safeAreaBars: preset.bars };
|
|
6594
|
+
var compileHeadlessBundle = async (args) => {
|
|
6595
|
+
let root;
|
|
6596
|
+
let manifest;
|
|
6597
|
+
try {
|
|
6598
|
+
root = findProjectRoot(process.cwd());
|
|
6599
|
+
manifest = readManifest(root);
|
|
6600
|
+
} catch {
|
|
6601
|
+
root = process.cwd();
|
|
6602
|
+
manifest = null;
|
|
6589
6603
|
}
|
|
6590
|
-
return;
|
|
6591
|
-
};
|
|
6592
|
-
var render = async (args) => {
|
|
6593
|
-
const timeoutMs = Number(flagStr(args, "timeout")) || 15000;
|
|
6594
|
-
const killer = setTimeout(() => {
|
|
6595
|
-
warnErr(`Render timed out after ${timeoutMs}ms (try --settle / --wait-network, or raise --timeout).`);
|
|
6596
|
-
process.exit(1);
|
|
6597
|
-
}, timeoutMs);
|
|
6598
|
-
killer.unref?.();
|
|
6599
|
-
const root = findProjectRoot(process.cwd());
|
|
6600
|
-
const manifest = readManifest(root);
|
|
6601
|
-
const config = loadConfig();
|
|
6602
|
-
const apiUrl = requireApiUrl(config);
|
|
6603
|
-
const token = requireToken(config);
|
|
6604
|
-
note("Resolving project assets…");
|
|
6605
|
-
const project = await getProject(apiUrl, token, manifest.uuid);
|
|
6606
|
-
const { entries, warnings: collectWarnings } = collectEntries(root, manifest, project);
|
|
6607
6604
|
const publicUrlFlag = flagStr(args, "public-url");
|
|
6608
|
-
const useLocalAssets = !publicUrlFlag && !flagBool(args, "remote-assets");
|
|
6605
|
+
const useLocalAssets = manifest ? !publicUrlFlag && !flagBool(args, "remote-assets") : true;
|
|
6606
|
+
let entries;
|
|
6607
|
+
let warnings;
|
|
6608
|
+
let name;
|
|
6609
|
+
let publicUrl;
|
|
6610
|
+
if (manifest) {
|
|
6611
|
+
const config = loadConfig();
|
|
6612
|
+
const apiUrl = requireApiUrl(config);
|
|
6613
|
+
const token = requireToken(config);
|
|
6614
|
+
note("Resolving project assets…");
|
|
6615
|
+
const project = await getProject(apiUrl, token, manifest.uuid);
|
|
6616
|
+
({ entries, warnings } = collectEntries(root, manifest, project));
|
|
6617
|
+
name = manifest.name;
|
|
6618
|
+
publicUrl = useLocalAssets ? LOCAL_ASSET_SCHEME2 : publicUrlFlag ?? apiUrl;
|
|
6619
|
+
if (useLocalAssets)
|
|
6620
|
+
warnings = warnings.filter((w) => !w.includes("No server URL"));
|
|
6621
|
+
} else {
|
|
6622
|
+
({ entries, warnings } = collectLocalEntries(root, null, ""));
|
|
6623
|
+
name = root.split(/[\\/]/).pop() ?? "project";
|
|
6624
|
+
publicUrl = LOCAL_ASSET_SCHEME2;
|
|
6625
|
+
if (flagBool(args, "remote-assets") || publicUrlFlag) {
|
|
6626
|
+
warnings.push("This is a local project (no server) — assets always load from the working tree.");
|
|
6627
|
+
}
|
|
6628
|
+
}
|
|
6609
6629
|
const compileEntries = useLocalAssets ? entries.map((e) => e.type === "resource" && e.absPath ? { ...e, fileSrc: encodeURIComponent(e.absPath) } : e) : entries;
|
|
6610
|
-
const warnings = useLocalAssets ? collectWarnings.filter((w) => !w.includes("No server URL")) : collectWarnings;
|
|
6611
6630
|
await applyProjectFonts(compileEntries, useLocalAssets ? {
|
|
6612
6631
|
mode: "vendor",
|
|
6613
6632
|
root,
|
|
@@ -6618,8 +6637,8 @@ var render = async (args) => {
|
|
|
6618
6637
|
note("Compiling…");
|
|
6619
6638
|
const js = await compileProject2({
|
|
6620
6639
|
entries: compileEntries,
|
|
6621
|
-
name
|
|
6622
|
-
publicUrl
|
|
6640
|
+
name,
|
|
6641
|
+
publicUrl,
|
|
6623
6642
|
entryOverride: flagStr(args, "entry"),
|
|
6624
6643
|
header: false,
|
|
6625
6644
|
format: "iife"
|
|
@@ -6630,15 +6649,75 @@ var render = async (args) => {
|
|
|
6630
6649
|
if (js.includes("_creator2d") && !useLocalAssets && !flagBool(args, "wait-network")) {
|
|
6631
6650
|
warnings.push("2D scene detected: sprite textures load over the network — add --wait-network to include them (else sprites render as flat placeholders).");
|
|
6632
6651
|
}
|
|
6633
|
-
|
|
6652
|
+
return { js, root, warnings, useLocalAssets };
|
|
6653
|
+
};
|
|
6654
|
+
|
|
6655
|
+
// src/commands/render.ts
|
|
6656
|
+
var DEVICE_SAFE_AREA = {
|
|
6657
|
+
none: { insets: [0, 0, 0, 0], bars: [] },
|
|
6658
|
+
iphone: { insets: [59, 0, 34, 0], bars: [] },
|
|
6659
|
+
android: { insets: [24, 0, 48, 0], bars: ["top", "bottom"] }
|
|
6660
|
+
};
|
|
6661
|
+
var resolveSafeAreaChoice = (explicit, device) => {
|
|
6662
|
+
if (explicit) {
|
|
6663
|
+
const n = explicit.split(",").map((s) => Number(s.trim()));
|
|
6664
|
+
if (n.length === 4 && n.every((v) => Number.isFinite(v)))
|
|
6665
|
+
return { safeArea: n, safeAreaBars: [] };
|
|
6666
|
+
throw new CliError(`--safe-area expects "top,right,bottom,left" (4 numbers), got "${explicit}".`);
|
|
6667
|
+
}
|
|
6668
|
+
if (device) {
|
|
6669
|
+
const preset = DEVICE_SAFE_AREA[device];
|
|
6670
|
+
if (!preset)
|
|
6671
|
+
throw new CliError(`Unknown --device "${device}". Options: ${Object.keys(DEVICE_SAFE_AREA).join(", ")}.`);
|
|
6672
|
+
return { safeArea: preset.insets, safeAreaBars: preset.bars };
|
|
6673
|
+
}
|
|
6674
|
+
return;
|
|
6675
|
+
};
|
|
6676
|
+
var requireRenderer = async () => {
|
|
6634
6677
|
try {
|
|
6635
|
-
|
|
6678
|
+
return await import("lecodes-renderer/headless");
|
|
6636
6679
|
} catch {
|
|
6637
6680
|
throw new CliError("The renderer isn't installed. Run: npm i -D lecodes-renderer");
|
|
6638
6681
|
}
|
|
6639
|
-
|
|
6640
|
-
|
|
6641
|
-
|
|
6682
|
+
};
|
|
6683
|
+
var fileScenarioIO = (dir) => ({
|
|
6684
|
+
write: (relPath, data) => {
|
|
6685
|
+
const abs = resolve6(dir, relPath);
|
|
6686
|
+
mkdirSync13(dirname11(abs), { recursive: true });
|
|
6687
|
+
writeFileSync14(abs, data);
|
|
6688
|
+
}
|
|
6689
|
+
});
|
|
6690
|
+
var readScenarioFile = (path) => {
|
|
6691
|
+
let raw;
|
|
6692
|
+
try {
|
|
6693
|
+
raw = JSON.parse(readFileSync17(resolve6(path), "utf8"));
|
|
6694
|
+
} catch (e) {
|
|
6695
|
+
throw new CliError(`Can't read scenario "${path}": ${e instanceof Error ? e.message : String(e)}`);
|
|
6696
|
+
}
|
|
6697
|
+
if (Array.isArray(raw))
|
|
6698
|
+
return { steps: raw };
|
|
6699
|
+
if (typeof raw !== "object" || raw === null)
|
|
6700
|
+
throw new CliError(`Scenario "${path}" must be a JSON object with "steps" (or a bare step array).`);
|
|
6701
|
+
const file = raw;
|
|
6702
|
+
if (file.steps === undefined)
|
|
6703
|
+
throw new CliError(`Scenario "${path}" has no "steps".`);
|
|
6704
|
+
return file;
|
|
6705
|
+
};
|
|
6706
|
+
var render = async (args) => {
|
|
6707
|
+
const scriptFlag = flagStr(args, "script");
|
|
6708
|
+
const scenario = scriptFlag !== undefined ? readScenarioFile(scriptFlag) : null;
|
|
6709
|
+
const stepCount = scenario && Array.isArray(scenario.steps) ? scenario.steps.length : 0;
|
|
6710
|
+
const timeoutMs = Number(flagStr(args, "timeout")) || 15000 + stepCount * 3000;
|
|
6711
|
+
const killer = setTimeout(() => {
|
|
6712
|
+
warnErr(`Render timed out after ${timeoutMs}ms (try --settle / --wait-network, or raise --timeout).`);
|
|
6713
|
+
process.exit(1);
|
|
6714
|
+
}, timeoutMs);
|
|
6715
|
+
killer.unref?.();
|
|
6716
|
+
const { js, warnings, useLocalAssets } = await compileHeadlessBundle(args);
|
|
6717
|
+
const renderer = await requireRenderer();
|
|
6718
|
+
const width = Number(flagStr(args, "width")) || scenario?.width || 390;
|
|
6719
|
+
const height = Number(flagStr(args, "height")) || scenario?.height || 844;
|
|
6720
|
+
const settleStr = flagStr(args, "settle") ?? (scenario?.settle !== undefined ? String(scenario.settle) : undefined);
|
|
6642
6721
|
const timeStr = flagStr(args, "time");
|
|
6643
6722
|
const onConsole = flagBool(args, "logs") ? (level, text2) => logErr(`${c.dim(`[${level}]`)} ${text2}`) : undefined;
|
|
6644
6723
|
const timing = {
|
|
@@ -6647,13 +6726,36 @@ var render = async (args) => {
|
|
|
6647
6726
|
localAssets: useLocalAssets,
|
|
6648
6727
|
timeMs: timeStr !== undefined ? Number(timeStr) : undefined,
|
|
6649
6728
|
afterAnimations: !flagBool(args, "no-after-animations"),
|
|
6650
|
-
...
|
|
6729
|
+
...resolveSafeAreaChoice(flagStr(args, "safe-area") ?? scenario?.safeArea, flagStr(args, "device") ?? scenario?.device),
|
|
6651
6730
|
onConsole
|
|
6652
6731
|
};
|
|
6653
|
-
const done = (flush) => flush(() => {
|
|
6732
|
+
const done = (flush, code = 0) => flush(() => {
|
|
6654
6733
|
clearTimeout(killer);
|
|
6655
|
-
process.exit(
|
|
6734
|
+
process.exit(code);
|
|
6656
6735
|
});
|
|
6736
|
+
if (scenario) {
|
|
6737
|
+
const steps = renderer.parseScenarioSteps(scenario.steps);
|
|
6738
|
+
const outDir = flagStr(args, "out-dir") ?? ".";
|
|
6739
|
+
const needsRaster = steps.some((s) => s.screenshot !== undefined);
|
|
6740
|
+
note(`Running ${steps.length} step${steps.length === 1 ? "" : "s"}…`);
|
|
6741
|
+
const session = await renderer.createSession(js, { width, height, ...timing, raster: needsRaster ? true : "auto" });
|
|
6742
|
+
try {
|
|
6743
|
+
const result2 = await renderer.runScenario(session, steps, fileScenarioIO(outDir));
|
|
6744
|
+
for (const w of [...warnings, ...session.warnings])
|
|
6745
|
+
warnErr(w);
|
|
6746
|
+
for (const st of result2.steps) {
|
|
6747
|
+
logErr(st.ok ? `${c.green("✓")} ${st.desc}` : `${c.red("✗")} ${st.desc}
|
|
6748
|
+
${c.red(st.error ?? "failed")}`);
|
|
6749
|
+
}
|
|
6750
|
+
if (!result2.ok && result2.failureArtifacts.length > 0) {
|
|
6751
|
+
logErr(c.dim(` failing frame: ${result2.failureArtifacts.map((p) => resolve6(outDir, p)).join(", ")}`));
|
|
6752
|
+
}
|
|
6753
|
+
logErr(result2.ok ? c.green(`✓ ${result2.steps.length}/${steps.length} steps passed`) : c.red(`✗ failed at step ${result2.steps.length} of ${steps.length}`));
|
|
6754
|
+
return done((cb) => cb(), result2.ok ? 0 : 1);
|
|
6755
|
+
} finally {
|
|
6756
|
+
session.dispose();
|
|
6757
|
+
}
|
|
6758
|
+
}
|
|
6657
6759
|
const pngFlag = flagStr(args, "png") ?? (flagBool(args, "png") ? "screenshot.png" : undefined);
|
|
6658
6760
|
if (pngFlag !== undefined) {
|
|
6659
6761
|
note("Rendering PNG…");
|
|
@@ -6694,8 +6796,8 @@ var render = async (args) => {
|
|
|
6694
6796
|
};
|
|
6695
6797
|
|
|
6696
6798
|
// src/commands/pull.ts
|
|
6697
|
-
import { existsSync as existsSync17, mkdirSync as
|
|
6698
|
-
import { dirname as
|
|
6799
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync14, rmSync as rmSync2, writeFileSync as writeFileSync15 } from "node:fs";
|
|
6800
|
+
import { dirname as dirname12, join as join19 } from "node:path";
|
|
6699
6801
|
var pull = async (args) => {
|
|
6700
6802
|
const root = findProjectRoot(process.cwd());
|
|
6701
6803
|
const manifest = readManifest(root);
|
|
@@ -6755,7 +6857,7 @@ var pullPaths = async (root, manifest, project, paths, apiUrl, token) => {
|
|
|
6755
6857
|
for (const asset of targets) {
|
|
6756
6858
|
const key = pathById.get(asset.id);
|
|
6757
6859
|
const abs = join19(root, ...key.replace(/^\//, "").split("/"));
|
|
6758
|
-
|
|
6860
|
+
mkdirSync14(dirname12(abs), { recursive: true });
|
|
6759
6861
|
let sha;
|
|
6760
6862
|
if (asset.type === "file") {
|
|
6761
6863
|
if (!asset.file?.src)
|
|
@@ -6778,7 +6880,7 @@ var pullPaths = async (root, manifest, project, paths, apiUrl, token) => {
|
|
|
6778
6880
|
|
|
6779
6881
|
// src/commands/push.ts
|
|
6780
6882
|
import { basename as basename10 } from "node:path";
|
|
6781
|
-
import { readFileSync as
|
|
6883
|
+
import { readFileSync as readFileSync18 } from "node:fs";
|
|
6782
6884
|
var push = async (args) => {
|
|
6783
6885
|
const root = findProjectRoot(process.cwd());
|
|
6784
6886
|
const manifest = readManifest(root);
|
|
@@ -6837,9 +6939,9 @@ var push = async (args) => {
|
|
|
6837
6939
|
if (source)
|
|
6838
6940
|
entry.assetId = source.id;
|
|
6839
6941
|
if (isTextKind(kind)) {
|
|
6840
|
-
entry.text =
|
|
6942
|
+
entry.text = readFileSync18(file.absPath, "utf8");
|
|
6841
6943
|
} else if (!source || source.sha !== file.sha) {
|
|
6842
|
-
const blob = await uploadBlob(apiUrl, token, manifest.uuid, basename10(file.path),
|
|
6944
|
+
const blob = await uploadBlob(apiUrl, token, manifest.uuid, basename10(file.path), readFileSync18(file.absPath));
|
|
6843
6945
|
entry.blobFileId = blob.id;
|
|
6844
6946
|
uploaded++;
|
|
6845
6947
|
}
|
|
@@ -6892,11 +6994,11 @@ var reportPublish = (res) => {
|
|
|
6892
6994
|
};
|
|
6893
6995
|
|
|
6894
6996
|
// src/commands/scene.ts
|
|
6895
|
-
import { existsSync as existsSync19, readFileSync as
|
|
6896
|
-
import { basename as basename11, dirname as
|
|
6997
|
+
import { existsSync as existsSync19, readFileSync as readFileSync20, readdirSync as readdirSync8, statSync as statSync4, writeFileSync as writeFileSync16 } from "node:fs";
|
|
6998
|
+
import { basename as basename11, dirname as dirname13, join as join21, resolve as resolve7 } from "node:path";
|
|
6897
6999
|
|
|
6898
7000
|
// src/compile/sceneCompile.ts
|
|
6899
|
-
import { existsSync as existsSync18, readFileSync as
|
|
7001
|
+
import { existsSync as existsSync18, readFileSync as readFileSync19 } from "node:fs";
|
|
6900
7002
|
import { join as join20 } from "node:path";
|
|
6901
7003
|
var TEXT_EXT = /\.(ts|js|json|svg|txt|md)$/i;
|
|
6902
7004
|
var rel = (key) => key.replace(/^\//, "");
|
|
@@ -6907,7 +7009,7 @@ var resolveHarness = (loadSceneHarness2) => {
|
|
|
6907
7009
|
} catch {
|
|
6908
7010
|
const vendored = join20(distRoot, "runtime", "scene-harness.json");
|
|
6909
7011
|
if (existsSync18(vendored))
|
|
6910
|
-
return JSON.parse(
|
|
7012
|
+
return JSON.parse(readFileSync19(vendored, "utf8"));
|
|
6911
7013
|
throw new CliError("scene-editor harness sources not found — reinstall the CLI or set LECODES_SCENE_HARNESS_DIR.");
|
|
6912
7014
|
}
|
|
6913
7015
|
};
|
|
@@ -6919,7 +7021,7 @@ var compileSceneEditor = async (opts) => {
|
|
|
6919
7021
|
for (const file of scan.files) {
|
|
6920
7022
|
const path = "./" + rel(file.path);
|
|
6921
7023
|
if (TEXT_EXT.test(path)) {
|
|
6922
|
-
entries.push({ path, type: "text", text:
|
|
7024
|
+
entries.push({ path, type: "text", text: readFileSync19(file.absPath, "utf8") });
|
|
6923
7025
|
} else {
|
|
6924
7026
|
entries.push({ path, type: "resource", fileSrc: "/project/" + rel(file.path) });
|
|
6925
7027
|
}
|
|
@@ -6963,7 +7065,7 @@ var getContext2 = (args) => {
|
|
|
6963
7065
|
if (existsSync19(abs) && statSync4(abs).isDirectory())
|
|
6964
7066
|
root = abs;
|
|
6965
7067
|
else if (abs.endsWith(".scene.ts") && existsSync19(abs))
|
|
6966
|
-
root =
|
|
7068
|
+
root = dirname13(abs);
|
|
6967
7069
|
else
|
|
6968
7070
|
throw new CliError(`No such folder or .scene.ts: ${pathArg}`);
|
|
6969
7071
|
}
|
|
@@ -6976,7 +7078,7 @@ var writeMcpConfig2 = (root, port) => {
|
|
|
6976
7078
|
let config = {};
|
|
6977
7079
|
if (existsSync19(path)) {
|
|
6978
7080
|
try {
|
|
6979
|
-
config = JSON.parse(
|
|
7081
|
+
config = JSON.parse(readFileSync20(path, "utf8"));
|
|
6980
7082
|
} catch {
|
|
6981
7083
|
warnErr(".mcp.json exists but isn't valid JSON — left it untouched. Add the lecodes-scene server by hand.");
|
|
6982
7084
|
return;
|
|
@@ -7068,23 +7170,239 @@ var status = async (_args) => {
|
|
|
7068
7170
|
info(c.dim(` ${scan.ignored.length} ignored (.lecodesignore)`));
|
|
7069
7171
|
};
|
|
7070
7172
|
|
|
7173
|
+
// src/commands/test.ts
|
|
7174
|
+
import { existsSync as existsSync20, readFileSync as readFileSync21, readdirSync as readdirSync9 } from "node:fs";
|
|
7175
|
+
import { resolve as resolve8 } from "node:path";
|
|
7176
|
+
var deriveDesignFlows = (meta, entryScreen) => {
|
|
7177
|
+
const edges = meta.edges ?? [];
|
|
7178
|
+
const isState = (id) => id.includes("@");
|
|
7179
|
+
const verifiable = edges.filter((e) => e.activator && !isState(e.from) && !isState(e.to));
|
|
7180
|
+
const prev = new Map;
|
|
7181
|
+
const visited = new Set([entryScreen]);
|
|
7182
|
+
const queue = [entryScreen];
|
|
7183
|
+
while (queue.length > 0) {
|
|
7184
|
+
const at = queue.shift();
|
|
7185
|
+
for (const e of verifiable) {
|
|
7186
|
+
if (e.from !== at || visited.has(e.to))
|
|
7187
|
+
continue;
|
|
7188
|
+
visited.add(e.to);
|
|
7189
|
+
prev.set(e.to, e);
|
|
7190
|
+
queue.push(e.to);
|
|
7191
|
+
}
|
|
7192
|
+
}
|
|
7193
|
+
const pathTo = (screen) => {
|
|
7194
|
+
if (screen === entryScreen)
|
|
7195
|
+
return [];
|
|
7196
|
+
if (!visited.has(screen))
|
|
7197
|
+
return null;
|
|
7198
|
+
const hops = [];
|
|
7199
|
+
for (let at = screen;at !== entryScreen; at = hops[0].from)
|
|
7200
|
+
hops.unshift(prev.get(at));
|
|
7201
|
+
return hops;
|
|
7202
|
+
};
|
|
7203
|
+
const flows = [];
|
|
7204
|
+
const skipped = [];
|
|
7205
|
+
for (const e of edges) {
|
|
7206
|
+
const name = `${e.from} —${e.activator ?? "?"}→ ${e.to}`;
|
|
7207
|
+
if (!e.activator) {
|
|
7208
|
+
skipped.push({ name, reason: "no activator (backend/gesture-driven) — cover it with a tests/*.flow.json" });
|
|
7209
|
+
continue;
|
|
7210
|
+
}
|
|
7211
|
+
if (isState(e.from) || isState(e.to)) {
|
|
7212
|
+
skipped.push({ name, reason: "screen-state edge (@state) — states are runtime conditions, not screens" });
|
|
7213
|
+
continue;
|
|
7214
|
+
}
|
|
7215
|
+
const path = pathTo(e.from);
|
|
7216
|
+
if (path === null) {
|
|
7217
|
+
skipped.push({ name, reason: `"${e.from}" has no activator path from "${entryScreen}" (tab-bar navigation?) — cover it with a tests/*.flow.json` });
|
|
7218
|
+
continue;
|
|
7219
|
+
}
|
|
7220
|
+
flows.push({
|
|
7221
|
+
name,
|
|
7222
|
+
kind: "flow",
|
|
7223
|
+
steps: [
|
|
7224
|
+
...path.map((hop) => ({ tap: hop.activator, expect: { screen: hop.to } })),
|
|
7225
|
+
{ tap: e.activator, expect: { screen: e.to } }
|
|
7226
|
+
]
|
|
7227
|
+
});
|
|
7228
|
+
}
|
|
7229
|
+
return { flows, skipped };
|
|
7230
|
+
};
|
|
7231
|
+
var slug3 = (s) => s.replace(/[^a-zA-Z0-9-_]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64) || "case";
|
|
7232
|
+
var readFlowFile = (abs, displayName) => {
|
|
7233
|
+
let raw;
|
|
7234
|
+
try {
|
|
7235
|
+
raw = JSON.parse(readFileSync21(abs, "utf8"));
|
|
7236
|
+
} catch (e) {
|
|
7237
|
+
throw new CliError(`Can't read "${displayName}": ${e instanceof Error ? e.message : String(e)}`);
|
|
7238
|
+
}
|
|
7239
|
+
if (Array.isArray(raw))
|
|
7240
|
+
return { name: displayName, kind: "file", steps: raw };
|
|
7241
|
+
if (typeof raw !== "object" || raw === null || raw.steps === undefined) {
|
|
7242
|
+
throw new CliError(`"${displayName}" must be a JSON object with "steps" (or a bare step array).`);
|
|
7243
|
+
}
|
|
7244
|
+
const file = raw;
|
|
7245
|
+
return { name: file.name ?? displayName, kind: "file", steps: file.steps, width: file.width, height: file.height, settle: file.settle, device: file.device, safeArea: file.safeArea };
|
|
7246
|
+
};
|
|
7247
|
+
var test = async (args) => {
|
|
7248
|
+
const { js, warnings, useLocalAssets, root } = await compileHeadlessBundle(args);
|
|
7249
|
+
const renderer = await requireRenderer();
|
|
7250
|
+
const testsDir = resolve8(root, flagStr(args, "dir") ?? "tests");
|
|
7251
|
+
const designMetaPath = resolve8(root, flagStr(args, "design-dir") ?? "design", "meta.json");
|
|
7252
|
+
const asJson = flagBool(args, "json");
|
|
7253
|
+
const out = asJson ? logErr : log;
|
|
7254
|
+
const cases = [];
|
|
7255
|
+
if (args._.length > 0) {
|
|
7256
|
+
for (const given of args._) {
|
|
7257
|
+
const candidates = [resolve8(given), resolve8(testsDir, given), resolve8(testsDir, `${given}.flow.json`)];
|
|
7258
|
+
const abs = candidates.find((p) => existsSync20(p));
|
|
7259
|
+
if (!abs)
|
|
7260
|
+
throw new CliError(`No such test: "${given}" (looked in ${testsDir}).`);
|
|
7261
|
+
cases.push(readFlowFile(abs, given));
|
|
7262
|
+
}
|
|
7263
|
+
} else if (existsSync20(testsDir)) {
|
|
7264
|
+
for (const f2 of readdirSync9(testsDir).filter((f3) => f3.endsWith(".flow.json")).sort()) {
|
|
7265
|
+
cases.push(readFlowFile(resolve8(testsDir, f2), f2));
|
|
7266
|
+
}
|
|
7267
|
+
}
|
|
7268
|
+
const flowsOnly = flagBool(args, "flows");
|
|
7269
|
+
const wantFlows = !flagBool(args, "no-flows") && args._.length === 0;
|
|
7270
|
+
if (flowsOnly)
|
|
7271
|
+
cases.length = 0;
|
|
7272
|
+
let skipped = [];
|
|
7273
|
+
let entryScreen = null;
|
|
7274
|
+
if ((wantFlows || flowsOnly) && existsSync20(designMetaPath)) {
|
|
7275
|
+
let meta;
|
|
7276
|
+
try {
|
|
7277
|
+
meta = JSON.parse(readFileSync21(designMetaPath, "utf8"));
|
|
7278
|
+
} catch (e) {
|
|
7279
|
+
throw new CliError(`Can't read ${designMetaPath}: ${e instanceof Error ? e.message : String(e)}`);
|
|
7280
|
+
}
|
|
7281
|
+
if ((meta.edges ?? []).length > 0) {
|
|
7282
|
+
note("Probing the start screen…");
|
|
7283
|
+
const probe = await renderer.createSession(js, { raster: false, localAssets: useLocalAssets });
|
|
7284
|
+
entryScreen = probe.screenName();
|
|
7285
|
+
probe.dispose();
|
|
7286
|
+
if (entryScreen === null) {
|
|
7287
|
+
warnErr('Design flows skipped: the app\'s start screen root has no `name`. Name each screen root with its design screen id (e.g. UIScreen([...], { name: "login" })).');
|
|
7288
|
+
} else {
|
|
7289
|
+
const derived = deriveDesignFlows(meta, entryScreen);
|
|
7290
|
+
cases.push(...derived.flows);
|
|
7291
|
+
skipped = derived.skipped;
|
|
7292
|
+
}
|
|
7293
|
+
}
|
|
7294
|
+
} else if (flowsOnly) {
|
|
7295
|
+
throw new CliError(`--flows needs a design graph (${designMetaPath} not found).`);
|
|
7296
|
+
}
|
|
7297
|
+
if (cases.length === 0) {
|
|
7298
|
+
for (const w of warnings)
|
|
7299
|
+
warnErr(w);
|
|
7300
|
+
throw new CliError(`No tests found. Write tests/*.flow.json scenarios, or draw activator edges in the design board (design/meta.json).`);
|
|
7301
|
+
}
|
|
7302
|
+
const timeoutMs = Number(flagStr(args, "timeout")) || 20000 + cases.length * 1e4;
|
|
7303
|
+
const killer = setTimeout(() => {
|
|
7304
|
+
warnErr(`Test run timed out after ${timeoutMs}ms (raise --timeout or lower --settle).`);
|
|
7305
|
+
process.exit(1);
|
|
7306
|
+
}, timeoutMs);
|
|
7307
|
+
killer.unref?.();
|
|
7308
|
+
const onConsole = flagBool(args, "logs") ? (level, text) => logErr(`${c.dim(`[${level}]`)} ${text}`) : undefined;
|
|
7309
|
+
const artifactsRoot = resolve8(root, flagStr(args, "artifacts") ?? resolve8(testsDir, ".artifacts"));
|
|
7310
|
+
const results = [];
|
|
7311
|
+
for (const kase of cases) {
|
|
7312
|
+
const flagSettle = flagStr(args, "settle");
|
|
7313
|
+
let result;
|
|
7314
|
+
try {
|
|
7315
|
+
const steps = renderer.parseScenarioSteps(kase.steps);
|
|
7316
|
+
const session = await renderer.createSession(js, {
|
|
7317
|
+
width: Number(flagStr(args, "width")) || kase.width || 390,
|
|
7318
|
+
height: Number(flagStr(args, "height")) || kase.height || 844,
|
|
7319
|
+
settleMs: flagSettle !== undefined ? Number(flagSettle) : kase.settle,
|
|
7320
|
+
waitNetwork: flagBool(args, "wait-network"),
|
|
7321
|
+
localAssets: useLocalAssets,
|
|
7322
|
+
...resolveSafeAreaChoice(flagStr(args, "safe-area") ?? kase.safeArea, flagStr(args, "device") ?? kase.device),
|
|
7323
|
+
onConsole
|
|
7324
|
+
});
|
|
7325
|
+
try {
|
|
7326
|
+
const io = fileScenarioIO(resolve8(artifactsRoot, slug3(kase.name)));
|
|
7327
|
+
const outcome = await renderer.runScenario(session, steps, io);
|
|
7328
|
+
result = {
|
|
7329
|
+
name: kase.name,
|
|
7330
|
+
kind: kase.kind,
|
|
7331
|
+
ok: outcome.ok,
|
|
7332
|
+
steps: outcome.steps.map((s) => ({ desc: s.desc, ok: s.ok, error: s.error })),
|
|
7333
|
+
artifacts: outcome.failureArtifacts.map((p) => resolve8(artifactsRoot, slug3(kase.name), p))
|
|
7334
|
+
};
|
|
7335
|
+
} finally {
|
|
7336
|
+
session.dispose();
|
|
7337
|
+
}
|
|
7338
|
+
} catch (e) {
|
|
7339
|
+
result = { name: kase.name, kind: kase.kind, ok: false, steps: [], error: e instanceof Error ? e.message : String(e), artifacts: [] };
|
|
7340
|
+
}
|
|
7341
|
+
results.push(result);
|
|
7342
|
+
if (result.ok) {
|
|
7343
|
+
out(`${c.green("✓")} ${result.name}`);
|
|
7344
|
+
} else {
|
|
7345
|
+
out(`${c.red("✗")} ${result.name}`);
|
|
7346
|
+
if (result.error)
|
|
7347
|
+
out(` ${c.red(result.error)}`);
|
|
7348
|
+
for (const s of result.steps) {
|
|
7349
|
+
out(s.ok ? c.dim(` ✓ ${s.desc}`) : ` ${c.red(`✗ ${s.desc} — ${s.error}`)}`);
|
|
7350
|
+
}
|
|
7351
|
+
if (result.artifacts.length > 0)
|
|
7352
|
+
out(c.dim(` failing frame: ${result.artifacts.join(", ")}`));
|
|
7353
|
+
}
|
|
7354
|
+
}
|
|
7355
|
+
for (const w of warnings)
|
|
7356
|
+
warnErr(w);
|
|
7357
|
+
const passed = results.filter((r) => r.ok).length;
|
|
7358
|
+
const failed = results.length - passed;
|
|
7359
|
+
if (skipped.length > 0) {
|
|
7360
|
+
out("");
|
|
7361
|
+
out(c.dim(`Not machine-verifiable (${skipped.length}):`));
|
|
7362
|
+
for (const s of skipped)
|
|
7363
|
+
out(c.dim(` - ${s.name}: ${s.reason}`));
|
|
7364
|
+
}
|
|
7365
|
+
out("");
|
|
7366
|
+
const flowCount = results.filter((r) => r.kind === "flow").length;
|
|
7367
|
+
const summary = [
|
|
7368
|
+
`${passed} passed`,
|
|
7369
|
+
failed > 0 ? c.red(`${failed} failed`) : undefined,
|
|
7370
|
+
skipped.length > 0 ? `${skipped.length} unverifiable` : undefined,
|
|
7371
|
+
flowCount > 0 ? c.dim(`(${flowCount} from the design graph${entryScreen ? `, start: ${entryScreen}` : ""})`) : undefined
|
|
7372
|
+
].filter(Boolean).join(", ");
|
|
7373
|
+
out(failed > 0 ? `${c.red("✗")} ${summary}` : `${c.green("✓")} ${summary}`);
|
|
7374
|
+
if (asJson) {
|
|
7375
|
+
process.stdout.write(JSON.stringify({
|
|
7376
|
+
ok: failed === 0,
|
|
7377
|
+
passed,
|
|
7378
|
+
failed,
|
|
7379
|
+
entryScreen,
|
|
7380
|
+
cases: results,
|
|
7381
|
+
unverifiable: skipped
|
|
7382
|
+
}, null, 2) + `
|
|
7383
|
+
`);
|
|
7384
|
+
}
|
|
7385
|
+
clearTimeout(killer);
|
|
7386
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
7387
|
+
};
|
|
7388
|
+
|
|
7071
7389
|
// src/index.ts
|
|
7072
|
-
import { existsSync as
|
|
7390
|
+
import { existsSync as existsSync22, readFileSync as readFileSync23, realpathSync as realpathSync2 } from "node:fs";
|
|
7073
7391
|
import { createRequire as createRequire4 } from "node:module";
|
|
7074
|
-
import { delimiter as delimiter2, dirname as
|
|
7392
|
+
import { delimiter as delimiter2, dirname as dirname15, join as join23 } from "node:path";
|
|
7075
7393
|
|
|
7076
7394
|
// src/commands/install.ts
|
|
7077
7395
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
7078
|
-
import { appendFileSync as appendFileSync2, chmodSync, cpSync as cpSync2, existsSync as
|
|
7396
|
+
import { appendFileSync as appendFileSync2, chmodSync, cpSync as cpSync2, existsSync as existsSync21, mkdirSync as mkdirSync15, readFileSync as readFileSync22, realpathSync, rmSync as rmSync3, symlinkSync } from "node:fs";
|
|
7079
7397
|
import { homedir as homedir3 } from "node:os";
|
|
7080
|
-
import { basename as basename12, delimiter, dirname as
|
|
7398
|
+
import { basename as basename12, delimiter, dirname as dirname14, join as join22, resolve as resolve9, sep } from "node:path";
|
|
7081
7399
|
var isWin = process.platform === "win32";
|
|
7082
7400
|
var HOME = homedir3();
|
|
7083
7401
|
var safeRealpath = (p) => {
|
|
7084
7402
|
try {
|
|
7085
7403
|
return realpathSync(p);
|
|
7086
7404
|
} catch {
|
|
7087
|
-
return
|
|
7405
|
+
return resolve9(p);
|
|
7088
7406
|
}
|
|
7089
7407
|
};
|
|
7090
7408
|
var onPath = (dir) => {
|
|
@@ -7100,19 +7418,19 @@ var onPath = (dir) => {
|
|
|
7100
7418
|
});
|
|
7101
7419
|
};
|
|
7102
7420
|
var install = async (args) => {
|
|
7103
|
-
const bundle =
|
|
7421
|
+
const bundle = dirname14(process.execPath);
|
|
7104
7422
|
const exeName = basename12(process.execPath);
|
|
7105
7423
|
const chiselName = isWin ? "chisel.exe" : "chisel";
|
|
7106
|
-
if (!
|
|
7424
|
+
if (!existsSync21(join22(bundle, "runtime")) || !existsSync21(join22(bundle, chiselName))) {
|
|
7107
7425
|
throw new CliError("`lecodes install` only works from the packaged release build (the archive containing lecodes + " + "chisel + runtime/). You appear to be running from source or an npm install — install globally " + "with your package manager there instead.");
|
|
7108
7426
|
}
|
|
7109
|
-
const companions = [exeName, chiselName, "runtime", "README.txt"].filter((f2) =>
|
|
7427
|
+
const companions = [exeName, chiselName, "runtime", "README.txt"].filter((f2) => existsSync21(join22(bundle, f2)));
|
|
7110
7428
|
if (isWin)
|
|
7111
7429
|
return installWindows(args, bundle, companions);
|
|
7112
7430
|
return installUnix(args, bundle, companions);
|
|
7113
7431
|
};
|
|
7114
7432
|
var copyBundle = (bundle, target, companions) => {
|
|
7115
|
-
|
|
7433
|
+
mkdirSync15(target, { recursive: true });
|
|
7116
7434
|
if (safeRealpath(target) === safeRealpath(bundle))
|
|
7117
7435
|
return;
|
|
7118
7436
|
for (const f2 of companions) {
|
|
@@ -7130,8 +7448,8 @@ var permError = (e, dir, system) => {
|
|
|
7130
7448
|
};
|
|
7131
7449
|
var installUnix = (args, bundle, companions) => {
|
|
7132
7450
|
const system = flagBool(args, "system");
|
|
7133
|
-
const libDir =
|
|
7134
|
-
const binDir =
|
|
7451
|
+
const libDir = resolve9(flagStr(args, "dir") ?? (system ? "/usr/local/lib/lecodes-cli" : join22(HOME, ".local", "lib", "lecodes-cli")));
|
|
7452
|
+
const binDir = resolve9(flagStr(args, "bin") ?? (system ? "/usr/local/bin" : join22(HOME, ".local", "bin")));
|
|
7135
7453
|
const link = join22(binDir, "lecodes");
|
|
7136
7454
|
try {
|
|
7137
7455
|
copyBundle(bundle, libDir, companions);
|
|
@@ -7141,7 +7459,7 @@ var installUnix = (args, bundle, companions) => {
|
|
|
7141
7459
|
throw permError(e, libDir, system);
|
|
7142
7460
|
}
|
|
7143
7461
|
try {
|
|
7144
|
-
|
|
7462
|
+
mkdirSync15(binDir, { recursive: true });
|
|
7145
7463
|
rmSync3(link, { force: true });
|
|
7146
7464
|
symlinkSync(join22(libDir, "lecodes"), link);
|
|
7147
7465
|
} catch (e) {
|
|
@@ -7164,7 +7482,7 @@ var appendPathToShellRc = (binDir) => {
|
|
|
7164
7482
|
const { file, name } = shellRc();
|
|
7165
7483
|
const line = `export PATH="${binDir}:$PATH"`;
|
|
7166
7484
|
try {
|
|
7167
|
-
const existing =
|
|
7485
|
+
const existing = existsSync21(file) ? readFileSync22(file, "utf8") : "";
|
|
7168
7486
|
if (existing.includes(line))
|
|
7169
7487
|
return name;
|
|
7170
7488
|
appendFileSync2(file, `${existing && !existing.endsWith(`
|
|
@@ -7202,7 +7520,7 @@ var finishPathUnix = (args, binDir) => {
|
|
|
7202
7520
|
log(` • or install system-wide (no PATH edit): ${c.bold("sudo lecodes install --system")}`);
|
|
7203
7521
|
};
|
|
7204
7522
|
var installWindows = (args, bundle, companions) => {
|
|
7205
|
-
const target =
|
|
7523
|
+
const target = resolve9(flagStr(args, "dir") ?? join22(process.env.LOCALAPPDATA ?? join22(HOME, "AppData", "Local"), "Programs", "lecodes-cli"));
|
|
7206
7524
|
try {
|
|
7207
7525
|
copyBundle(bundle, target, companions);
|
|
7208
7526
|
} catch (e) {
|
|
@@ -7239,9 +7557,9 @@ var addToUserPathWindows = (dir) => {
|
|
|
7239
7557
|
|
|
7240
7558
|
// src/index.ts
|
|
7241
7559
|
var STANDALONE = (() => {
|
|
7242
|
-
const dir =
|
|
7560
|
+
const dir = dirname15(process.execPath);
|
|
7243
7561
|
const chisel = process.platform === "win32" ? "chisel.exe" : "chisel";
|
|
7244
|
-
return
|
|
7562
|
+
return existsSync22(join23(dir, "runtime")) && existsSync22(join23(dir, chisel));
|
|
7245
7563
|
})();
|
|
7246
7564
|
var INSTALLED = STANDALONE && (() => {
|
|
7247
7565
|
const exe = process.platform === "win32" ? "lecodes.exe" : "lecodes";
|
|
@@ -7257,7 +7575,7 @@ var INSTALLED = STANDALONE && (() => {
|
|
|
7257
7575
|
return false;
|
|
7258
7576
|
try {
|
|
7259
7577
|
const cand = join23(d, exe);
|
|
7260
|
-
return
|
|
7578
|
+
return existsSync22(cand) && realpathSync2(cand) === self;
|
|
7261
7579
|
} catch {
|
|
7262
7580
|
return false;
|
|
7263
7581
|
}
|
|
@@ -7320,12 +7638,28 @@ ${c.bold("Commands:")}
|
|
|
7320
7638
|
-o, --out <file> write JSON to a file instead of stdout
|
|
7321
7639
|
--png [file] rasterize a PNG (default screenshot.png;
|
|
7322
7640
|
needs @napi-rs/canvas)
|
|
7641
|
+
--script <file> drive the app through a step list (tap/type/
|
|
7642
|
+
scroll/wait/expect/screenshot/json steps)
|
|
7643
|
+
--out-dir <dir> where --script step outputs land (default .)
|
|
7323
7644
|
--scale <n> PNG device-pixel scale (default 2)
|
|
7324
7645
|
--width/--height viewport size (default 390x844)
|
|
7325
7646
|
--entry <path> override the entrypoint
|
|
7326
7647
|
--settle <ms> wait for promises/timers/fetch before snapshot
|
|
7327
7648
|
--wait-network let data fetches hit the real network
|
|
7328
7649
|
--logs forward the app's console output (to stderr)
|
|
7650
|
+
test [name…] Run headless flow tests: tests/*.flow.json scenarios, plus flows
|
|
7651
|
+
derived from the design graph (design/meta.json activator edges
|
|
7652
|
+
— checks the built app against the designed navigation; needs
|
|
7653
|
+
named elements + screen roots). Failing steps dump the frame
|
|
7654
|
+
(JSON + PNG) under tests/.artifacts/<flow>/.
|
|
7655
|
+
--flows run ONLY the design-derived flows
|
|
7656
|
+
--no-flows run ONLY the tests/*.flow.json files
|
|
7657
|
+
--json machine-readable report on stdout
|
|
7658
|
+
--dir <folder> tests folder (default tests/)
|
|
7659
|
+
--design-dir <dir> design folder (default design/)
|
|
7660
|
+
--artifacts <dir> failure-artifact folder override
|
|
7661
|
+
(+ the render timing flags: --width/--height, --settle,
|
|
7662
|
+
--device, --safe-area, --wait-network, --logs, --entry)
|
|
7329
7663
|
design [serve] LeCodes Design — a live canvas of screens for AI-driven prototyping
|
|
7330
7664
|
(needs the optional 'lecodes-design' package; works inside a
|
|
7331
7665
|
cloned project or standalone in any folder)
|
|
@@ -7374,7 +7708,7 @@ ${c.bold("Commands:")}
|
|
|
7374
7708
|
ASC API key; stored in ~/.lecodes/asc.json
|
|
7375
7709
|
|
|
7376
7710
|
Config lives in ~/.lecodes/config.json (override with LECODES_API / LECODES_TOKEN).`;
|
|
7377
|
-
var commands = { init: init2, login, create, clone, status, diff, pull, push, compile: compile2, preview: previewLegacy, render, design, scene, dev, app };
|
|
7711
|
+
var commands = { init: init2, login, create, clone, status, diff, pull, push, compile: compile2, preview: previewLegacy, render, test, design, scene, dev, app };
|
|
7378
7712
|
if (STANDALONE)
|
|
7379
7713
|
commands.install = install;
|
|
7380
7714
|
var readVersion = () => package_default.version ?? "unknown";
|
|
@@ -7386,15 +7720,15 @@ var OPTIONAL_PEERS = [
|
|
|
7386
7720
|
var peerVersion = (name, probe) => {
|
|
7387
7721
|
try {
|
|
7388
7722
|
const require2 = createRequire4(import.meta.url);
|
|
7389
|
-
let dir =
|
|
7723
|
+
let dir = dirname15(require2.resolve(probe));
|
|
7390
7724
|
for (let depth = 0;depth < 16; depth++) {
|
|
7391
7725
|
const manifest = join23(dir, "package.json");
|
|
7392
|
-
if (
|
|
7393
|
-
const parsed = JSON.parse(
|
|
7726
|
+
if (existsSync22(manifest)) {
|
|
7727
|
+
const parsed = JSON.parse(readFileSync23(manifest, "utf8"));
|
|
7394
7728
|
if (parsed.name === name)
|
|
7395
7729
|
return parsed.version ?? "unknown";
|
|
7396
7730
|
}
|
|
7397
|
-
const parent =
|
|
7731
|
+
const parent = dirname15(dir);
|
|
7398
7732
|
if (parent === dir)
|
|
7399
7733
|
break;
|
|
7400
7734
|
dir = parent;
|