lecodes-cli 0.16.2 → 0.17.1
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 +983 -320
- package/package.json +4 -3
- package/runtime/sdk/gl/Animator.ts +10 -2
- package/runtime/sdk/gl/IK.ts +174 -170
- package/runtime/sdk/gl/InstancedMesh.ts +132 -0
- package/runtime/sdk/gl/Node.ts +285 -267
- package/runtime/sdk/gl/Particles.ts +625 -546
- package/runtime/sdk/gl/Scene.ts +291 -289
- package/runtime/sdk/gl/state.ts +6 -6
- package/runtime/sdk/inject.ts +162 -160
- package/runtime/sdk-types.json +1 -1
package/dist/index.js
CHANGED
|
@@ -120,7 +120,7 @@ var useColor, paint = (code, s) => useColor ? `\x1B[${code}m${s}\x1B[0m` : s, c,
|
|
|
120
120
|
if (!answer)
|
|
121
121
|
return fallback;
|
|
122
122
|
return answer === "y" || answer === "yes";
|
|
123
|
-
}, parseArgs = (argv) => {
|
|
123
|
+
}, isNegativeNumber = (s) => /^-(\d|\.\d)/.test(s), isValue = (s) => !s.startsWith("-") || isNegativeNumber(s), parseArgs = (argv) => {
|
|
124
124
|
const out = { _: [], flags: {} };
|
|
125
125
|
for (let i = 0;i < argv.length; i++) {
|
|
126
126
|
const arg = argv[i];
|
|
@@ -132,15 +132,15 @@ var useColor, paint = (code, s) => useColor ? `\x1B[${code}m${s}\x1B[0m` : s, c,
|
|
|
132
132
|
}
|
|
133
133
|
const key = arg.slice(2);
|
|
134
134
|
const next = argv[i + 1];
|
|
135
|
-
if (next !== undefined &&
|
|
135
|
+
if (next !== undefined && isValue(next)) {
|
|
136
136
|
out.flags[key] = next;
|
|
137
137
|
i++;
|
|
138
138
|
} else
|
|
139
139
|
out.flags[key] = true;
|
|
140
|
-
} else if (arg.startsWith("-") && arg.length > 1) {
|
|
140
|
+
} else if (arg.startsWith("-") && arg.length > 1 && !isNegativeNumber(arg)) {
|
|
141
141
|
const key = arg.slice(1);
|
|
142
142
|
const next = argv[i + 1];
|
|
143
|
-
if (next !== undefined &&
|
|
143
|
+
if (next !== undefined && isValue(next)) {
|
|
144
144
|
out.flags[key] = next;
|
|
145
145
|
i++;
|
|
146
146
|
} else
|
|
@@ -388,6 +388,34 @@ design/comments/
|
|
|
388
388
|
const file = join3(root, ".lecodesignore");
|
|
389
389
|
if (!existsSync2(file))
|
|
390
390
|
writeFileSync3(file, DEFAULT_IGNORE_FILE);
|
|
391
|
+
}, DEFAULT_GITIGNORE = `# LeCodes per-checkout state: generated IDE types + this machine's sync cursor
|
|
392
|
+
# (.lecodes/manifest.json). Regenerate with \`lecodes types\` / re-attach with \`lecodes link\`.
|
|
393
|
+
.lecodes/
|
|
394
|
+
|
|
395
|
+
# Render / preview outputs and test failure frames
|
|
396
|
+
screenshot.png
|
|
397
|
+
shots/
|
|
398
|
+
tests/.artifacts/
|
|
399
|
+
|
|
400
|
+
node_modules/
|
|
401
|
+
`, ensureGitignore = (root) => {
|
|
402
|
+
const file = join3(root, ".gitignore");
|
|
403
|
+
if (!existsSync2(file)) {
|
|
404
|
+
writeFileSync3(file, DEFAULT_GITIGNORE);
|
|
405
|
+
return "created";
|
|
406
|
+
}
|
|
407
|
+
const text = readFileSync3(file, "utf8");
|
|
408
|
+
const covered = text.split(/\r?\n/).some((l) => /^\/?\.lecodes\/?\s*$/.test(l.trim()));
|
|
409
|
+
if (covered)
|
|
410
|
+
return null;
|
|
411
|
+
const sep = text.length === 0 || text.endsWith(`
|
|
412
|
+
`) ? "" : `
|
|
413
|
+
`;
|
|
414
|
+
writeFileSync3(file, `${text}${sep}
|
|
415
|
+
# LeCodes per-checkout state (generated types + sync cursor) — not source
|
|
416
|
+
.lecodes/
|
|
417
|
+
`);
|
|
418
|
+
return "updated";
|
|
391
419
|
};
|
|
392
420
|
var init_ignore = __esm(() => {
|
|
393
421
|
BUILTIN_PATTERNS = ["node_modules/", ".git/", "_*", "*.log", ".DS_Store", ".artifacts/"];
|
|
@@ -84194,8 +84222,8 @@ ${lanes.join(`
|
|
|
84194
84222
|
if (node === undefined || node.parent.kind !== 307 || !isInternalModuleImportEqualsDeclaration(node)) {
|
|
84195
84223
|
return false;
|
|
84196
84224
|
}
|
|
84197
|
-
const
|
|
84198
|
-
return
|
|
84225
|
+
const isValue2 = isAliasResolvedToValue(getSymbolOfDeclaration(node));
|
|
84226
|
+
return isValue2 && node.moduleReference && !nodeIsMissing(node.moduleReference);
|
|
84199
84227
|
}
|
|
84200
84228
|
function isAliasResolvedToValue(symbol, excludeTypeOnlyValues) {
|
|
84201
84229
|
if (!symbol) {
|
|
@@ -215816,7 +215844,7 @@ var require_main = __commonJS((exports, module) => {
|
|
|
215816
215844
|
// package.json
|
|
215817
215845
|
var package_default = {
|
|
215818
215846
|
name: "lecodes-cli",
|
|
215819
|
-
version: "0.
|
|
215847
|
+
version: "0.17.1",
|
|
215820
215848
|
dependencies: {
|
|
215821
215849
|
"@letary/chisel": "^0.8.0",
|
|
215822
215850
|
jimp: "^1.6.1"
|
|
@@ -215827,11 +215855,12 @@ var package_default = {
|
|
|
215827
215855
|
"qrcode-terminal": "^0.12.0",
|
|
215828
215856
|
"lecodes-3d-editor": "workspace:*",
|
|
215829
215857
|
"lecodes-assets": "workspace:*",
|
|
215830
|
-
sdk: "workspace:*"
|
|
215858
|
+
sdk: "workspace:*",
|
|
215859
|
+
"viewer-lite": "workspace:*"
|
|
215831
215860
|
},
|
|
215832
215861
|
peerDependencies: {
|
|
215833
215862
|
"lecodes-design": "^0.8.1",
|
|
215834
|
-
"lecodes-renderer": "^0.8.
|
|
215863
|
+
"lecodes-renderer": "^0.8.6",
|
|
215835
215864
|
"lecodes-3d-editor": "^0.3.0",
|
|
215836
215865
|
"lecodes-assets": "^0.1.0"
|
|
215837
215866
|
},
|
|
@@ -215891,8 +215920,8 @@ import { basename as basename6, dirname as dirname12, join as join17, resolve as
|
|
|
215891
215920
|
init_api();
|
|
215892
215921
|
init_config();
|
|
215893
215922
|
init_manifest();
|
|
215894
|
-
import { writeFileSync as
|
|
215895
|
-
import { basename as basename3, resolve } from "node:path";
|
|
215923
|
+
import { writeFileSync as writeFileSync8 } from "node:fs";
|
|
215924
|
+
import { basename as basename3, resolve as resolve2 } from "node:path";
|
|
215896
215925
|
|
|
215897
215926
|
// src/compile/collect.ts
|
|
215898
215927
|
init_localFiles();
|
|
@@ -216005,6 +216034,226 @@ init_projectCompile();
|
|
|
216005
216034
|
init_fonts();
|
|
216006
216035
|
init_assetIcons();
|
|
216007
216036
|
init_ignore();
|
|
216037
|
+
|
|
216038
|
+
// src/commands/appShared.ts
|
|
216039
|
+
init_manifest();
|
|
216040
|
+
init_ignore();
|
|
216041
|
+
init_util();
|
|
216042
|
+
import { execFileSync } from "node:child_process";
|
|
216043
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync11, writeFileSync as writeFileSync7 } from "node:fs";
|
|
216044
|
+
import { homedir as homedir3 } from "node:os";
|
|
216045
|
+
import { dirname as dirname8, join as join12, resolve } from "node:path";
|
|
216046
|
+
var FIRST_PARTY_PLUGINS = {
|
|
216047
|
+
camera: "letary/lecodes-plugins",
|
|
216048
|
+
"qr-scanner": "letary/lecodes-plugins",
|
|
216049
|
+
geolocation: "letary/lecodes-plugins",
|
|
216050
|
+
push: "letary/lecodes-plugins"
|
|
216051
|
+
};
|
|
216052
|
+
var readAppConfigAt = (root) => readJson(join12(root, "app.json"));
|
|
216053
|
+
var DESKTOP_DEFAULT_WINDOW = { width: 1280, height: 720 };
|
|
216054
|
+
var desktopDefaultViewport = (root) => {
|
|
216055
|
+
const cfg = readAppConfigAt(root);
|
|
216056
|
+
if (!cfg?.desktop || cfg.ios || cfg.android)
|
|
216057
|
+
return null;
|
|
216058
|
+
return {
|
|
216059
|
+
width: cfg.desktop.window?.width ?? DESKTOP_DEFAULT_WINDOW.width,
|
|
216060
|
+
height: cfg.desktop.window?.height ?? DESKTOP_DEFAULT_WINDOW.height
|
|
216061
|
+
};
|
|
216062
|
+
};
|
|
216063
|
+
var readJson = (path) => {
|
|
216064
|
+
if (!existsSync10(path))
|
|
216065
|
+
return null;
|
|
216066
|
+
try {
|
|
216067
|
+
return JSON.parse(readFileSync11(path, "utf8").replace(/^\uFEFF/, ""));
|
|
216068
|
+
} catch {
|
|
216069
|
+
throw new CliError(`Invalid JSON in ${path}`);
|
|
216070
|
+
}
|
|
216071
|
+
};
|
|
216072
|
+
var writeJson = (path, value) => writeFileSync7(path, JSON.stringify(value, null, 2) + `
|
|
216073
|
+
`);
|
|
216074
|
+
var projectRoot = () => {
|
|
216075
|
+
try {
|
|
216076
|
+
return findProjectRoot(process.cwd());
|
|
216077
|
+
} catch {
|
|
216078
|
+
return process.cwd();
|
|
216079
|
+
}
|
|
216080
|
+
};
|
|
216081
|
+
var git = (cwd, ...args) => execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
216082
|
+
var slugify = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "app";
|
|
216083
|
+
var runTool = (cwd, cmd, args) => {
|
|
216084
|
+
try {
|
|
216085
|
+
return execFileSync(cmd, args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], maxBuffer: 128 * 1024 * 1024 });
|
|
216086
|
+
} catch (e) {
|
|
216087
|
+
const err = e;
|
|
216088
|
+
const tail = ((err.stdout ?? "") + `
|
|
216089
|
+
` + (err.stderr ?? "")).split(`
|
|
216090
|
+
`).filter((l) => l.trim()).slice(-25).join(`
|
|
216091
|
+
`);
|
|
216092
|
+
throw new CliError(`${cmd} ${args[0] ?? ""} failed:
|
|
216093
|
+
${tail}`);
|
|
216094
|
+
}
|
|
216095
|
+
};
|
|
216096
|
+
var ensureShellIgnored = (root, dir) => {
|
|
216097
|
+
const ignorePath = join12(root, ".lecodesignore");
|
|
216098
|
+
const ignore = existsSync10(ignorePath) ? readFileSync11(ignorePath, "utf8") : DEFAULT_IGNORE_FILE;
|
|
216099
|
+
if (new RegExp(`^${dir}\\/$`, "m").test(ignore))
|
|
216100
|
+
return;
|
|
216101
|
+
const comment = ignore.includes("# Native app shells") ? "" : "# Native app shells — generated by `lecodes app init`, never pushed to LeCodes.\n";
|
|
216102
|
+
writeFileSync7(ignorePath, ignore.replace(/\n*$/, `
|
|
216103
|
+
`) + `
|
|
216104
|
+
` + comment + `${dir}/
|
|
216105
|
+
`);
|
|
216106
|
+
log(`${c.green("updated")} .lecodesignore (+ ${dir}/)`);
|
|
216107
|
+
};
|
|
216108
|
+
var SDK_VARIANTS = ["core", "2d", "3d", "full"];
|
|
216109
|
+
var detectEngines = (bundleJs) => {
|
|
216110
|
+
const lines = [];
|
|
216111
|
+
for (const line of bundleJs.split(`
|
|
216112
|
+
`)) {
|
|
216113
|
+
if (!line.startsWith("//"))
|
|
216114
|
+
break;
|
|
216115
|
+
lines.push(line);
|
|
216116
|
+
}
|
|
216117
|
+
const header = lines.join(`
|
|
216118
|
+
`);
|
|
216119
|
+
return { gl: !/^\/\/ gl: disable$/m.test(header), g2: /^\/\/ 2d: enable$/m.test(header) };
|
|
216120
|
+
};
|
|
216121
|
+
var resolveVariant = (requested, bundleJs, configKey = "ios.variant") => {
|
|
216122
|
+
const uses = detectEngines(bundleJs);
|
|
216123
|
+
const detected = uses.gl && uses.g2 ? "full" : uses.gl ? "3d" : uses.g2 ? "2d" : "core";
|
|
216124
|
+
if (requested === "auto")
|
|
216125
|
+
return { variant: detected, detected };
|
|
216126
|
+
const variant = requested;
|
|
216127
|
+
const missing = [
|
|
216128
|
+
uses.gl && variant !== "3d" && variant !== "full" ? "3D" : null,
|
|
216129
|
+
uses.g2 && variant !== "2d" && variant !== "full" ? "2D" : null
|
|
216130
|
+
].filter(Boolean);
|
|
216131
|
+
if (missing.length > 0) {
|
|
216132
|
+
warn(`project uses the ${missing.join(" + ")} engine but ${configKey} forces "${variant}" — those scenes won't run on device; remove ${configKey} to auto-select "${detected}".`);
|
|
216133
|
+
}
|
|
216134
|
+
return { variant, detected };
|
|
216135
|
+
};
|
|
216136
|
+
var VERSION_RE = /^\d+\.\d+\.\d+$/;
|
|
216137
|
+
var cmpVersion = (a, b) => {
|
|
216138
|
+
const pa = a.split(".").map(Number);
|
|
216139
|
+
const pb = b.split(".").map(Number);
|
|
216140
|
+
for (let i = 0;i < 3; i++) {
|
|
216141
|
+
if ((pa[i] ?? 0) !== (pb[i] ?? 0))
|
|
216142
|
+
return (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
216143
|
+
}
|
|
216144
|
+
return 0;
|
|
216145
|
+
};
|
|
216146
|
+
var fetchLatestSdkVersion = async (repo) => {
|
|
216147
|
+
const url = `https://api.github.com/repos/${repo}/releases/latest`;
|
|
216148
|
+
const res = await fetch(url, { headers: { accept: "application/vnd.github+json", "user-agent": "lecodes-cli" } });
|
|
216149
|
+
if (!res.ok)
|
|
216150
|
+
throw new CliError(`Could not read the latest ${repo} release (HTTP ${res.status}) — ${url}`);
|
|
216151
|
+
const tag = (await res.json()).tag_name?.replace(/^v/, "") ?? "";
|
|
216152
|
+
if (!VERSION_RE.test(tag))
|
|
216153
|
+
throw new CliError(`${repo} latest release has no x.y.z tag (got "${tag}")`);
|
|
216154
|
+
return tag;
|
|
216155
|
+
};
|
|
216156
|
+
var resolveSdkVersion = async (pin, fallback, opts) => {
|
|
216157
|
+
if (pin !== "latest") {
|
|
216158
|
+
if (!VERSION_RE.test(pin)) {
|
|
216159
|
+
throw new CliError(`app.json ${opts.configKey} must be "latest" or an exact x.y.z release (got "${pin}").`);
|
|
216160
|
+
}
|
|
216161
|
+
if (cmpVersion(pin, opts.minVersion) < 0) {
|
|
216162
|
+
throw new CliError(`app.json ${opts.configKey} ${pin} is older than the ${opts.minVersion} these generated shells need.`);
|
|
216163
|
+
}
|
|
216164
|
+
return pin;
|
|
216165
|
+
}
|
|
216166
|
+
try {
|
|
216167
|
+
return await fetchLatestSdkVersion(opts.repo);
|
|
216168
|
+
} catch (e) {
|
|
216169
|
+
const kept = fallback ?? opts.minVersion;
|
|
216170
|
+
warn(`Could not resolve the latest SDK (${e.message}) — staying on ${kept}.`);
|
|
216171
|
+
return kept;
|
|
216172
|
+
}
|
|
216173
|
+
};
|
|
216174
|
+
var ensureRepo = (repo, wantTag, lock) => {
|
|
216175
|
+
const override = process.env.LECODES_PLUGINS_DIR;
|
|
216176
|
+
if (override) {
|
|
216177
|
+
note(`Using local plugin checkout ${override} (LECODES_PLUGINS_DIR)`);
|
|
216178
|
+
return { dir: resolve(override), tag: "local", commit: "local" };
|
|
216179
|
+
}
|
|
216180
|
+
const cache = join12(homedir3(), ".lecodes", "plugins", repo.replace("/", "__"));
|
|
216181
|
+
const url = `https://github.com/${repo}.git`;
|
|
216182
|
+
if (!existsSync10(join12(cache, ".git"))) {
|
|
216183
|
+
mkdirSync6(dirname8(cache), { recursive: true });
|
|
216184
|
+
note(`Cloning ${url}…`);
|
|
216185
|
+
git(dirname8(cache), "clone", "--quiet", url, cache);
|
|
216186
|
+
}
|
|
216187
|
+
let tag = lock?.tag ?? wantTag;
|
|
216188
|
+
let commit = lock?.commit;
|
|
216189
|
+
if (commit) {
|
|
216190
|
+
try {
|
|
216191
|
+
git(cache, "cat-file", "-e", `${commit}^{commit}`);
|
|
216192
|
+
} catch {
|
|
216193
|
+
git(cache, "fetch", "--quiet", "--tags", "origin");
|
|
216194
|
+
}
|
|
216195
|
+
} else {
|
|
216196
|
+
git(cache, "fetch", "--quiet", "--tags", "origin");
|
|
216197
|
+
if (!tag) {
|
|
216198
|
+
tag = git(cache, "tag", "--list", "v*", "--sort=-version:refname").split(`
|
|
216199
|
+
`)[0] ?? "";
|
|
216200
|
+
if (!tag)
|
|
216201
|
+
throw new CliError(`${repo} has no v* version tags to install from`);
|
|
216202
|
+
}
|
|
216203
|
+
commit = git(cache, "rev-list", "-n", "1", tag);
|
|
216204
|
+
}
|
|
216205
|
+
git(cache, "checkout", "--quiet", "--detach", commit);
|
|
216206
|
+
return { dir: cache, tag: tag ?? "unknown", commit };
|
|
216207
|
+
};
|
|
216208
|
+
var resolvePlugin = (id, spec, lock, platform) => {
|
|
216209
|
+
let repo;
|
|
216210
|
+
let subdir;
|
|
216211
|
+
let wantTag = null;
|
|
216212
|
+
if (spec.startsWith("github:")) {
|
|
216213
|
+
const [repoPart, tagPart] = spec.slice("github:".length).split("@");
|
|
216214
|
+
repo = repoPart;
|
|
216215
|
+
wantTag = tagPart ?? null;
|
|
216216
|
+
subdir = "";
|
|
216217
|
+
} else {
|
|
216218
|
+
repo = FIRST_PARTY_PLUGINS[id];
|
|
216219
|
+
if (!repo) {
|
|
216220
|
+
throw new CliError(`Unknown plugin "${id}" — known: ${Object.keys(FIRST_PARTY_PLUGINS).join(", ")}, or use a github:owner/repo spec.`);
|
|
216221
|
+
}
|
|
216222
|
+
subdir = `plugins/${id}`;
|
|
216223
|
+
}
|
|
216224
|
+
const checkout = ensureRepo(repo, wantTag, lock?.repo === repo ? lock : undefined);
|
|
216225
|
+
const dir = subdir ? join12(checkout.dir, subdir) : checkout.dir;
|
|
216226
|
+
const manifest = readJson(join12(dir, "lecodes-plugin.json"));
|
|
216227
|
+
if (!manifest)
|
|
216228
|
+
throw new CliError(`Plugin "${id}": no lecodes-plugin.json in ${dir}`);
|
|
216229
|
+
if (manifest.id !== id)
|
|
216230
|
+
throw new CliError(`Plugin "${id}": manifest declares id "${manifest.id}"`);
|
|
216231
|
+
if (!manifest[platform]) {
|
|
216232
|
+
throw new CliError(`Plugin "${id}" does not support ${platform === "ios" ? "iOS" : "Android"} (manifest "${platform}" is null)`);
|
|
216233
|
+
}
|
|
216234
|
+
if (spec.startsWith("github:")) {
|
|
216235
|
+
const names = [...manifest.provides?.views ?? [], ...manifest.provides?.services ?? []];
|
|
216236
|
+
for (const name of names) {
|
|
216237
|
+
if (!name.includes("."))
|
|
216238
|
+
warn(`Plugin "${id}" provides bare channel name "${name}" — third-party names should be vendor-prefixed (e.g. "acme.${name}").`);
|
|
216239
|
+
}
|
|
216240
|
+
}
|
|
216241
|
+
return {
|
|
216242
|
+
id,
|
|
216243
|
+
dir,
|
|
216244
|
+
manifest,
|
|
216245
|
+
lock: { repo, tag: checkout.tag, commit: checkout.commit, version: manifest.version ?? "unknown" }
|
|
216246
|
+
};
|
|
216247
|
+
};
|
|
216248
|
+
var latestPluginTag = (repo) => {
|
|
216249
|
+
try {
|
|
216250
|
+
return ensureRepo(repo, null, undefined).tag;
|
|
216251
|
+
} catch {
|
|
216252
|
+
return null;
|
|
216253
|
+
}
|
|
216254
|
+
};
|
|
216255
|
+
|
|
216256
|
+
// src/commands/compile.ts
|
|
216008
216257
|
init_util();
|
|
216009
216258
|
var compile2 = async (args) => {
|
|
216010
216259
|
await compileImpl(args);
|
|
@@ -216016,7 +216265,7 @@ var compileImpl = async (args, shell) => {
|
|
|
216016
216265
|
if (out && isNullSink(out)) {
|
|
216017
216266
|
note(`Discarded output (${out})`);
|
|
216018
216267
|
} else if (out) {
|
|
216019
|
-
|
|
216268
|
+
writeFileSync8(resolve2(out), js);
|
|
216020
216269
|
note(`Wrote ${out} (${js.length} bytes)`);
|
|
216021
216270
|
} else {
|
|
216022
216271
|
process.stdout.write(js);
|
|
@@ -216027,7 +216276,7 @@ var compileImpl = async (args, shell) => {
|
|
|
216027
216276
|
}
|
|
216028
216277
|
return resources;
|
|
216029
216278
|
};
|
|
216030
|
-
var buildBundle = async (args, shell) => {
|
|
216279
|
+
var buildBundle = async (args, shell, opts = {}) => {
|
|
216031
216280
|
let root;
|
|
216032
216281
|
let manifest;
|
|
216033
216282
|
try {
|
|
@@ -216046,19 +216295,21 @@ var buildBundle = async (args, shell) => {
|
|
|
216046
216295
|
const config = loadConfig();
|
|
216047
216296
|
const apiUrl = requireApiUrl(config);
|
|
216048
216297
|
const token = requireToken(config);
|
|
216298
|
+
const publicUrlFlag = flagStr(args, "public-url");
|
|
216299
|
+
localAssets = !!shell || !!opts.localAssets && publicUrlFlag === undefined && !flagBool(args, "remote-assets");
|
|
216049
216300
|
note("Resolving project assets…");
|
|
216050
216301
|
const project = await getProject(apiUrl, token, manifest.uuid);
|
|
216051
|
-
({ entries, warnings } = collectEntries(root, manifest, project, { warnMissingUrl: !
|
|
216302
|
+
({ entries, warnings } = collectEntries(root, manifest, project, { warnMissingUrl: !localAssets }));
|
|
216052
216303
|
name = manifest.name;
|
|
216053
|
-
publicUrl =
|
|
216054
|
-
|
|
216055
|
-
if (
|
|
216056
|
-
const isRemote = buildGlobMatcher(
|
|
216304
|
+
publicUrl = publicUrlFlag ?? apiUrl;
|
|
216305
|
+
const remotePatterns = shell?.remotePatterns ?? (localAssets ? readAppConfigAt(root)?.resources?.remote ?? [] : []);
|
|
216306
|
+
if (localAssets && remotePatterns.length > 0) {
|
|
216307
|
+
const isRemote = buildGlobMatcher(remotePatterns);
|
|
216057
216308
|
for (const e of entries) {
|
|
216058
216309
|
if (e.type !== "resource" || !isRemote(e.path))
|
|
216059
216310
|
continue;
|
|
216060
216311
|
if (!e.fileSrc) {
|
|
216061
|
-
warnErr(`${e.path} matches resources.remote but has no server URL yet — bundling it (push, then re-sync).`);
|
|
216312
|
+
warnErr(`${e.path} matches resources.remote but has no server URL yet — ${shell ? "bundling it (push, then re-sync)" : "reading it from the working tree"}.`);
|
|
216062
216313
|
continue;
|
|
216063
216314
|
}
|
|
216064
216315
|
e.remote = true;
|
|
@@ -216068,13 +216319,16 @@ var buildBundle = async (args, shell) => {
|
|
|
216068
216319
|
const publicUrlFlag = flagStr(args, "public-url");
|
|
216069
216320
|
localAssets = shell ? true : publicUrlFlag === undefined;
|
|
216070
216321
|
publicUrl = publicUrlFlag ?? "";
|
|
216322
|
+
if (opts.localAssets && flagBool(args, "remote-assets")) {
|
|
216323
|
+
warnErr("This is a local project (no server) — assets always load from the working tree.");
|
|
216324
|
+
}
|
|
216071
216325
|
({ entries, warnings } = collectLocalEntries(root, null, ""));
|
|
216072
216326
|
name = basename3(root);
|
|
216073
216327
|
if (shell && shell.remotePatterns.length > 0) {
|
|
216074
216328
|
warnErr("resources.remote needs a server-backed project (a streamed asset needs a server URL) — bundling everything.");
|
|
216075
216329
|
}
|
|
216076
216330
|
}
|
|
216077
|
-
const fontsHeader = await applyProjectFonts(entries, manifest && !
|
|
216331
|
+
const fontsHeader = await applyProjectFonts(entries, manifest && !localAssets ? { mode: "cdn", root, publicUrl } : {
|
|
216078
216332
|
mode: "vendor",
|
|
216079
216333
|
root,
|
|
216080
216334
|
vendorDir: root,
|
|
@@ -216957,224 +217211,6 @@ buildNumber) — edit it there, then re-run \`lecodes app sync\`. Plugins are ma
|
|
|
216957
217211
|
\`lecodes app plugin add <id | github:owner/repo[@tag]>\` and pinned in \`../plugins.lock.json\`.
|
|
216958
217212
|
`;
|
|
216959
217213
|
|
|
216960
|
-
// src/commands/appShared.ts
|
|
216961
|
-
init_manifest();
|
|
216962
|
-
init_ignore();
|
|
216963
|
-
init_util();
|
|
216964
|
-
import { execFileSync } from "node:child_process";
|
|
216965
|
-
import { existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync11, writeFileSync as writeFileSync8 } from "node:fs";
|
|
216966
|
-
import { homedir as homedir3 } from "node:os";
|
|
216967
|
-
import { dirname as dirname8, join as join12, resolve as resolve2 } from "node:path";
|
|
216968
|
-
var FIRST_PARTY_PLUGINS = {
|
|
216969
|
-
camera: "letary/lecodes-plugins",
|
|
216970
|
-
"qr-scanner": "letary/lecodes-plugins",
|
|
216971
|
-
geolocation: "letary/lecodes-plugins",
|
|
216972
|
-
push: "letary/lecodes-plugins"
|
|
216973
|
-
};
|
|
216974
|
-
var readAppConfigAt = (root) => readJson(join12(root, "app.json"));
|
|
216975
|
-
var DESKTOP_DEFAULT_WINDOW = { width: 1280, height: 720 };
|
|
216976
|
-
var desktopDefaultViewport = (root) => {
|
|
216977
|
-
const cfg = readAppConfigAt(root);
|
|
216978
|
-
if (!cfg?.desktop || cfg.ios || cfg.android)
|
|
216979
|
-
return null;
|
|
216980
|
-
return {
|
|
216981
|
-
width: cfg.desktop.window?.width ?? DESKTOP_DEFAULT_WINDOW.width,
|
|
216982
|
-
height: cfg.desktop.window?.height ?? DESKTOP_DEFAULT_WINDOW.height
|
|
216983
|
-
};
|
|
216984
|
-
};
|
|
216985
|
-
var readJson = (path) => {
|
|
216986
|
-
if (!existsSync10(path))
|
|
216987
|
-
return null;
|
|
216988
|
-
try {
|
|
216989
|
-
return JSON.parse(readFileSync11(path, "utf8").replace(/^\uFEFF/, ""));
|
|
216990
|
-
} catch {
|
|
216991
|
-
throw new CliError(`Invalid JSON in ${path}`);
|
|
216992
|
-
}
|
|
216993
|
-
};
|
|
216994
|
-
var writeJson = (path, value) => writeFileSync8(path, JSON.stringify(value, null, 2) + `
|
|
216995
|
-
`);
|
|
216996
|
-
var projectRoot = () => {
|
|
216997
|
-
try {
|
|
216998
|
-
return findProjectRoot(process.cwd());
|
|
216999
|
-
} catch {
|
|
217000
|
-
return process.cwd();
|
|
217001
|
-
}
|
|
217002
|
-
};
|
|
217003
|
-
var git = (cwd, ...args) => execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
217004
|
-
var slugify = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "app";
|
|
217005
|
-
var runTool = (cwd, cmd, args) => {
|
|
217006
|
-
try {
|
|
217007
|
-
return execFileSync(cmd, args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], maxBuffer: 128 * 1024 * 1024 });
|
|
217008
|
-
} catch (e) {
|
|
217009
|
-
const err = e;
|
|
217010
|
-
const tail = ((err.stdout ?? "") + `
|
|
217011
|
-
` + (err.stderr ?? "")).split(`
|
|
217012
|
-
`).filter((l) => l.trim()).slice(-25).join(`
|
|
217013
|
-
`);
|
|
217014
|
-
throw new CliError(`${cmd} ${args[0] ?? ""} failed:
|
|
217015
|
-
${tail}`);
|
|
217016
|
-
}
|
|
217017
|
-
};
|
|
217018
|
-
var ensureShellIgnored = (root, dir) => {
|
|
217019
|
-
const ignorePath = join12(root, ".lecodesignore");
|
|
217020
|
-
const ignore = existsSync10(ignorePath) ? readFileSync11(ignorePath, "utf8") : DEFAULT_IGNORE_FILE;
|
|
217021
|
-
if (new RegExp(`^${dir}\\/$`, "m").test(ignore))
|
|
217022
|
-
return;
|
|
217023
|
-
const comment = ignore.includes("# Native app shells") ? "" : "# Native app shells — generated by `lecodes app init`, never pushed to LeCodes.\n";
|
|
217024
|
-
writeFileSync8(ignorePath, ignore.replace(/\n*$/, `
|
|
217025
|
-
`) + `
|
|
217026
|
-
` + comment + `${dir}/
|
|
217027
|
-
`);
|
|
217028
|
-
log(`${c.green("updated")} .lecodesignore (+ ${dir}/)`);
|
|
217029
|
-
};
|
|
217030
|
-
var SDK_VARIANTS = ["core", "2d", "3d", "full"];
|
|
217031
|
-
var detectEngines = (bundleJs) => {
|
|
217032
|
-
const lines = [];
|
|
217033
|
-
for (const line of bundleJs.split(`
|
|
217034
|
-
`)) {
|
|
217035
|
-
if (!line.startsWith("//"))
|
|
217036
|
-
break;
|
|
217037
|
-
lines.push(line);
|
|
217038
|
-
}
|
|
217039
|
-
const header = lines.join(`
|
|
217040
|
-
`);
|
|
217041
|
-
return { gl: !/^\/\/ gl: disable$/m.test(header), g2: /^\/\/ 2d: enable$/m.test(header) };
|
|
217042
|
-
};
|
|
217043
|
-
var resolveVariant = (requested, bundleJs, configKey = "ios.variant") => {
|
|
217044
|
-
const uses = detectEngines(bundleJs);
|
|
217045
|
-
const detected = uses.gl && uses.g2 ? "full" : uses.gl ? "3d" : uses.g2 ? "2d" : "core";
|
|
217046
|
-
if (requested === "auto")
|
|
217047
|
-
return { variant: detected, detected };
|
|
217048
|
-
const variant = requested;
|
|
217049
|
-
const missing = [
|
|
217050
|
-
uses.gl && variant !== "3d" && variant !== "full" ? "3D" : null,
|
|
217051
|
-
uses.g2 && variant !== "2d" && variant !== "full" ? "2D" : null
|
|
217052
|
-
].filter(Boolean);
|
|
217053
|
-
if (missing.length > 0) {
|
|
217054
|
-
warn(`project uses the ${missing.join(" + ")} engine but ${configKey} forces "${variant}" — those scenes won't run on device; remove ${configKey} to auto-select "${detected}".`);
|
|
217055
|
-
}
|
|
217056
|
-
return { variant, detected };
|
|
217057
|
-
};
|
|
217058
|
-
var VERSION_RE = /^\d+\.\d+\.\d+$/;
|
|
217059
|
-
var cmpVersion = (a, b) => {
|
|
217060
|
-
const pa = a.split(".").map(Number);
|
|
217061
|
-
const pb = b.split(".").map(Number);
|
|
217062
|
-
for (let i = 0;i < 3; i++) {
|
|
217063
|
-
if ((pa[i] ?? 0) !== (pb[i] ?? 0))
|
|
217064
|
-
return (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
217065
|
-
}
|
|
217066
|
-
return 0;
|
|
217067
|
-
};
|
|
217068
|
-
var fetchLatestSdkVersion = async (repo) => {
|
|
217069
|
-
const url = `https://api.github.com/repos/${repo}/releases/latest`;
|
|
217070
|
-
const res = await fetch(url, { headers: { accept: "application/vnd.github+json", "user-agent": "lecodes-cli" } });
|
|
217071
|
-
if (!res.ok)
|
|
217072
|
-
throw new CliError(`Could not read the latest ${repo} release (HTTP ${res.status}) — ${url}`);
|
|
217073
|
-
const tag = (await res.json()).tag_name?.replace(/^v/, "") ?? "";
|
|
217074
|
-
if (!VERSION_RE.test(tag))
|
|
217075
|
-
throw new CliError(`${repo} latest release has no x.y.z tag (got "${tag}")`);
|
|
217076
|
-
return tag;
|
|
217077
|
-
};
|
|
217078
|
-
var resolveSdkVersion = async (pin, fallback, opts) => {
|
|
217079
|
-
if (pin !== "latest") {
|
|
217080
|
-
if (!VERSION_RE.test(pin)) {
|
|
217081
|
-
throw new CliError(`app.json ${opts.configKey} must be "latest" or an exact x.y.z release (got "${pin}").`);
|
|
217082
|
-
}
|
|
217083
|
-
if (cmpVersion(pin, opts.minVersion) < 0) {
|
|
217084
|
-
throw new CliError(`app.json ${opts.configKey} ${pin} is older than the ${opts.minVersion} these generated shells need.`);
|
|
217085
|
-
}
|
|
217086
|
-
return pin;
|
|
217087
|
-
}
|
|
217088
|
-
try {
|
|
217089
|
-
return await fetchLatestSdkVersion(opts.repo);
|
|
217090
|
-
} catch (e) {
|
|
217091
|
-
const kept = fallback ?? opts.minVersion;
|
|
217092
|
-
warn(`Could not resolve the latest SDK (${e.message}) — staying on ${kept}.`);
|
|
217093
|
-
return kept;
|
|
217094
|
-
}
|
|
217095
|
-
};
|
|
217096
|
-
var ensureRepo = (repo, wantTag, lock) => {
|
|
217097
|
-
const override = process.env.LECODES_PLUGINS_DIR;
|
|
217098
|
-
if (override) {
|
|
217099
|
-
note(`Using local plugin checkout ${override} (LECODES_PLUGINS_DIR)`);
|
|
217100
|
-
return { dir: resolve2(override), tag: "local", commit: "local" };
|
|
217101
|
-
}
|
|
217102
|
-
const cache = join12(homedir3(), ".lecodes", "plugins", repo.replace("/", "__"));
|
|
217103
|
-
const url = `https://github.com/${repo}.git`;
|
|
217104
|
-
if (!existsSync10(join12(cache, ".git"))) {
|
|
217105
|
-
mkdirSync6(dirname8(cache), { recursive: true });
|
|
217106
|
-
note(`Cloning ${url}…`);
|
|
217107
|
-
git(dirname8(cache), "clone", "--quiet", url, cache);
|
|
217108
|
-
}
|
|
217109
|
-
let tag = lock?.tag ?? wantTag;
|
|
217110
|
-
let commit = lock?.commit;
|
|
217111
|
-
if (commit) {
|
|
217112
|
-
try {
|
|
217113
|
-
git(cache, "cat-file", "-e", `${commit}^{commit}`);
|
|
217114
|
-
} catch {
|
|
217115
|
-
git(cache, "fetch", "--quiet", "--tags", "origin");
|
|
217116
|
-
}
|
|
217117
|
-
} else {
|
|
217118
|
-
git(cache, "fetch", "--quiet", "--tags", "origin");
|
|
217119
|
-
if (!tag) {
|
|
217120
|
-
tag = git(cache, "tag", "--list", "v*", "--sort=-version:refname").split(`
|
|
217121
|
-
`)[0] ?? "";
|
|
217122
|
-
if (!tag)
|
|
217123
|
-
throw new CliError(`${repo} has no v* version tags to install from`);
|
|
217124
|
-
}
|
|
217125
|
-
commit = git(cache, "rev-list", "-n", "1", tag);
|
|
217126
|
-
}
|
|
217127
|
-
git(cache, "checkout", "--quiet", "--detach", commit);
|
|
217128
|
-
return { dir: cache, tag: tag ?? "unknown", commit };
|
|
217129
|
-
};
|
|
217130
|
-
var resolvePlugin = (id, spec, lock, platform) => {
|
|
217131
|
-
let repo;
|
|
217132
|
-
let subdir;
|
|
217133
|
-
let wantTag = null;
|
|
217134
|
-
if (spec.startsWith("github:")) {
|
|
217135
|
-
const [repoPart, tagPart] = spec.slice("github:".length).split("@");
|
|
217136
|
-
repo = repoPart;
|
|
217137
|
-
wantTag = tagPart ?? null;
|
|
217138
|
-
subdir = "";
|
|
217139
|
-
} else {
|
|
217140
|
-
repo = FIRST_PARTY_PLUGINS[id];
|
|
217141
|
-
if (!repo) {
|
|
217142
|
-
throw new CliError(`Unknown plugin "${id}" — known: ${Object.keys(FIRST_PARTY_PLUGINS).join(", ")}, or use a github:owner/repo spec.`);
|
|
217143
|
-
}
|
|
217144
|
-
subdir = `plugins/${id}`;
|
|
217145
|
-
}
|
|
217146
|
-
const checkout = ensureRepo(repo, wantTag, lock?.repo === repo ? lock : undefined);
|
|
217147
|
-
const dir = subdir ? join12(checkout.dir, subdir) : checkout.dir;
|
|
217148
|
-
const manifest = readJson(join12(dir, "lecodes-plugin.json"));
|
|
217149
|
-
if (!manifest)
|
|
217150
|
-
throw new CliError(`Plugin "${id}": no lecodes-plugin.json in ${dir}`);
|
|
217151
|
-
if (manifest.id !== id)
|
|
217152
|
-
throw new CliError(`Plugin "${id}": manifest declares id "${manifest.id}"`);
|
|
217153
|
-
if (!manifest[platform]) {
|
|
217154
|
-
throw new CliError(`Plugin "${id}" does not support ${platform === "ios" ? "iOS" : "Android"} (manifest "${platform}" is null)`);
|
|
217155
|
-
}
|
|
217156
|
-
if (spec.startsWith("github:")) {
|
|
217157
|
-
const names = [...manifest.provides?.views ?? [], ...manifest.provides?.services ?? []];
|
|
217158
|
-
for (const name of names) {
|
|
217159
|
-
if (!name.includes("."))
|
|
217160
|
-
warn(`Plugin "${id}" provides bare channel name "${name}" — third-party names should be vendor-prefixed (e.g. "acme.${name}").`);
|
|
217161
|
-
}
|
|
217162
|
-
}
|
|
217163
|
-
return {
|
|
217164
|
-
id,
|
|
217165
|
-
dir,
|
|
217166
|
-
manifest,
|
|
217167
|
-
lock: { repo, tag: checkout.tag, commit: checkout.commit, version: manifest.version ?? "unknown" }
|
|
217168
|
-
};
|
|
217169
|
-
};
|
|
217170
|
-
var latestPluginTag = (repo) => {
|
|
217171
|
-
try {
|
|
217172
|
-
return ensureRepo(repo, null, undefined).tag;
|
|
217173
|
-
} catch {
|
|
217174
|
-
return null;
|
|
217175
|
-
}
|
|
217176
|
-
};
|
|
217177
|
-
|
|
217178
217214
|
// src/commands/appAndroid.ts
|
|
217179
217215
|
init_manifest();
|
|
217180
217216
|
import { execFileSync as execFileSync2, spawn, spawnSync as spawnSync2 } from "node:child_process";
|
|
@@ -218692,6 +218728,106 @@ import { spawn as spawn2, spawnSync as spawnSync3 } from "node:child_process";
|
|
|
218692
218728
|
import { copyFileSync, existsSync as existsSync14, mkdirSync as mkdirSync9, mkdtempSync, readdirSync as readdirSync5, readFileSync as readFileSync14, renameSync as renameSync2, rmSync as rmSync2, statSync as statSync3, writeFileSync as writeFileSync11 } from "node:fs";
|
|
218693
218729
|
import { homedir as homedir5, tmpdir } from "node:os";
|
|
218694
218730
|
import { join as join15, resolve as resolve4 } from "node:path";
|
|
218731
|
+
|
|
218732
|
+
// ../viewer-lite/src/sourcemapError.ts
|
|
218733
|
+
var B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
218734
|
+
var B64_INDEX = {};
|
|
218735
|
+
for (let i2 = 0;i2 < B64.length; i2++)
|
|
218736
|
+
B64_INDEX[B64[i2]] = i2;
|
|
218737
|
+
var decodeVlq = (segment) => {
|
|
218738
|
+
const out = [];
|
|
218739
|
+
let shift = 0, value = 0;
|
|
218740
|
+
for (const ch of segment) {
|
|
218741
|
+
const digit = B64_INDEX[ch];
|
|
218742
|
+
if (digit === undefined)
|
|
218743
|
+
break;
|
|
218744
|
+
const more = digit & 32;
|
|
218745
|
+
value += (digit & 31) << shift;
|
|
218746
|
+
if (more) {
|
|
218747
|
+
shift += 5;
|
|
218748
|
+
} else {
|
|
218749
|
+
const negative = value & 1;
|
|
218750
|
+
value >>= 1;
|
|
218751
|
+
out.push(negative ? -value : value);
|
|
218752
|
+
value = 0;
|
|
218753
|
+
shift = 0;
|
|
218754
|
+
}
|
|
218755
|
+
}
|
|
218756
|
+
return out;
|
|
218757
|
+
};
|
|
218758
|
+
|
|
218759
|
+
class SourceMap {
|
|
218760
|
+
sources;
|
|
218761
|
+
lines;
|
|
218762
|
+
constructor(raw) {
|
|
218763
|
+
this.sources = raw.sources;
|
|
218764
|
+
this.lines = [];
|
|
218765
|
+
let srcIdx = 0, srcLine = 0, srcCol = 0;
|
|
218766
|
+
for (const lineStr of raw.mappings.split(";")) {
|
|
218767
|
+
const segs = [];
|
|
218768
|
+
let genCol = 0;
|
|
218769
|
+
if (lineStr) {
|
|
218770
|
+
for (const segStr of lineStr.split(",")) {
|
|
218771
|
+
const v = decodeVlq(segStr);
|
|
218772
|
+
if (v.length === 0)
|
|
218773
|
+
continue;
|
|
218774
|
+
genCol += v[0];
|
|
218775
|
+
if (v.length >= 4) {
|
|
218776
|
+
srcIdx += v[1];
|
|
218777
|
+
srcLine += v[2];
|
|
218778
|
+
srcCol += v[3];
|
|
218779
|
+
segs.push([genCol, srcIdx, srcLine, srcCol]);
|
|
218780
|
+
}
|
|
218781
|
+
}
|
|
218782
|
+
}
|
|
218783
|
+
this.lines.push(segs);
|
|
218784
|
+
}
|
|
218785
|
+
}
|
|
218786
|
+
originalPositionFor(line, column) {
|
|
218787
|
+
const segs = this.lines[line - 1];
|
|
218788
|
+
if (!segs || segs.length === 0)
|
|
218789
|
+
return null;
|
|
218790
|
+
let best = null;
|
|
218791
|
+
for (const seg of segs) {
|
|
218792
|
+
if (seg[0] <= column)
|
|
218793
|
+
best = seg;
|
|
218794
|
+
else
|
|
218795
|
+
break;
|
|
218796
|
+
}
|
|
218797
|
+
best ??= segs[0];
|
|
218798
|
+
const source = this.sources[best[1]];
|
|
218799
|
+
if (source === undefined)
|
|
218800
|
+
return null;
|
|
218801
|
+
return { source, line: best[2] + 1, column: best[3] };
|
|
218802
|
+
}
|
|
218803
|
+
}
|
|
218804
|
+
var extractInlineSourceMap = (code) => {
|
|
218805
|
+
const m = code.match(/\/\/# lecodesMap=data:application\/json;(?:charset=[^;]+;)?base64,([A-Za-z0-9+/=]+)\s*$/);
|
|
218806
|
+
if (!m)
|
|
218807
|
+
return null;
|
|
218808
|
+
try {
|
|
218809
|
+
const json = typeof atob === "function" ? atob(m[1]) : Buffer.from(m[1], "base64").toString("utf8");
|
|
218810
|
+
return new SourceMap(JSON.parse(json));
|
|
218811
|
+
} catch {
|
|
218812
|
+
return null;
|
|
218813
|
+
}
|
|
218814
|
+
};
|
|
218815
|
+
var mapNamedFileStack = (text, map, filename) => {
|
|
218816
|
+
const escaped = filename.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
218817
|
+
return text.replace(new RegExp(`\\b${escaped}:(\\d+):(\\d+)`, "g"), (whole, l2, c4) => {
|
|
218818
|
+
const orig = map.originalPositionFor(parseInt(l2, 10), Math.max(0, parseInt(c4, 10) - 1));
|
|
218819
|
+
return orig ? `${orig.source.replace(/^(\.\/|\/)/, "")}:${orig.line}:${orig.column}` : whole;
|
|
218820
|
+
});
|
|
218821
|
+
};
|
|
218822
|
+
|
|
218823
|
+
// src/compile/nativeStack.ts
|
|
218824
|
+
var BUNDLE_FILENAME = "module.js";
|
|
218825
|
+
var makeStackDemangler = (bundleJs) => {
|
|
218826
|
+
const map = extractInlineSourceMap(bundleJs);
|
|
218827
|
+
return map ? (chunk) => mapNamedFileStack(chunk, map, BUNDLE_FILENAME) : null;
|
|
218828
|
+
};
|
|
218829
|
+
|
|
218830
|
+
// src/desktopRenderer.ts
|
|
218695
218831
|
var MANIFEST_URL = "https://storage.yandexcloud.net/le-codes/releases/versions.json";
|
|
218696
218832
|
var DESKTOP_RENDERER_VERSION = "1.1.0";
|
|
218697
218833
|
var semverNewer = (a2, b) => {
|
|
@@ -218864,13 +219000,34 @@ var stageBundle = (js, resources) => {
|
|
|
218864
219000
|
}
|
|
218865
219001
|
return runDir;
|
|
218866
219002
|
};
|
|
219003
|
+
var pipeThroughDemangler = (src, dst, map) => {
|
|
219004
|
+
let rest = "";
|
|
219005
|
+
src.on("data", (d) => {
|
|
219006
|
+
rest += String(d);
|
|
219007
|
+
const nl = rest.lastIndexOf(`
|
|
219008
|
+
`);
|
|
219009
|
+
if (nl < 0)
|
|
219010
|
+
return;
|
|
219011
|
+
dst.write(map(rest.slice(0, nl + 1)));
|
|
219012
|
+
rest = rest.slice(nl + 1);
|
|
219013
|
+
});
|
|
219014
|
+
src.on("end", () => {
|
|
219015
|
+
if (rest)
|
|
219016
|
+
dst.write(map(rest));
|
|
219017
|
+
});
|
|
219018
|
+
};
|
|
218867
219019
|
var runDesktopApp = async (js, resources, opts = {}) => {
|
|
218868
219020
|
const exe = await resolveDesktopExe();
|
|
218869
219021
|
const runDir = stageBundle(js, resources);
|
|
218870
219022
|
try {
|
|
218871
219023
|
const env = { ...process.env, ...windowEnv(opts) };
|
|
219024
|
+
const demangle = makeStackDemangler(js);
|
|
218872
219025
|
await new Promise((done, fail) => {
|
|
218873
|
-
const child = spawn2(exe, [join15(runDir, "app.js")], { cwd: runDir, env, stdio: ["ignore", "inherit", "inherit"] });
|
|
219026
|
+
const child = demangle ? spawn2(exe, [join15(runDir, "app.js")], { cwd: runDir, env, stdio: ["ignore", "pipe", "pipe"] }) : spawn2(exe, [join15(runDir, "app.js")], { cwd: runDir, env, stdio: ["ignore", "inherit", "inherit"] });
|
|
219027
|
+
if (demangle) {
|
|
219028
|
+
pipeThroughDemangler(child.stdout, process.stdout, demangle);
|
|
219029
|
+
pipeThroughDemangler(child.stderr, process.stderr, demangle);
|
|
219030
|
+
}
|
|
218874
219031
|
child.on("error", (e2) => fail(new CliError(`Can't run the desktop host: ${e2.message}`)));
|
|
218875
219032
|
child.on("exit", (code) => {
|
|
218876
219033
|
if (code === 0 || code === null)
|
|
@@ -218898,26 +219055,33 @@ var runDesktopRender = async (js, resources, opts) => {
|
|
|
218898
219055
|
try {
|
|
218899
219056
|
const appJs = join15(runDir, "app.js");
|
|
218900
219057
|
const outAbs = resolve4(opts.outPng);
|
|
218901
|
-
|
|
218902
|
-
|
|
218903
|
-
|
|
218904
|
-
|
|
218905
|
-
|
|
218906
|
-
|
|
218907
|
-
|
|
218908
|
-
|
|
218909
|
-
|
|
218910
|
-
|
|
218911
|
-
|
|
218912
|
-
|
|
218913
|
-
|
|
219058
|
+
const env = {
|
|
219059
|
+
...process.env,
|
|
219060
|
+
CREATOR_DUMP_PNG: outAbs,
|
|
219061
|
+
CREATOR_MAX_FRAMES: String(opts.frames),
|
|
219062
|
+
CREATOR_WINDOW_SIZE: `${opts.width}x${opts.height}`,
|
|
219063
|
+
CREATOR_HIDDEN: "1"
|
|
219064
|
+
};
|
|
219065
|
+
if (opts.script !== undefined) {
|
|
219066
|
+
const scriptPath = join15(runDir, "script.txt");
|
|
219067
|
+
writeFileSync11(scriptPath, opts.script);
|
|
219068
|
+
env.CREATOR_SCRIPT = scriptPath;
|
|
219069
|
+
}
|
|
219070
|
+
if (opts.fixedDtMs)
|
|
219071
|
+
env.CREATOR_FIXED_DT = String(opts.fixedDtMs);
|
|
219072
|
+
if (opts.camera)
|
|
219073
|
+
env.CREATOR_CAMERA = opts.camera;
|
|
219074
|
+
if (opts.view)
|
|
219075
|
+
env.CREATOR_VIEW = opts.view;
|
|
219076
|
+
const demangle = makeStackDemangler(js) ?? ((text) => text);
|
|
219077
|
+
return await new Promise((done, fail) => {
|
|
219078
|
+
const child = spawn2(exe, [appJs], { cwd: runDir, env, stdio: ["ignore", "pipe", "pipe"] });
|
|
219079
|
+
const chunks = [];
|
|
218914
219080
|
const sink = (d) => {
|
|
218915
219081
|
const text = String(d);
|
|
218916
|
-
|
|
218917
|
-
while (tail.length > 80)
|
|
218918
|
-
tail.shift();
|
|
219082
|
+
chunks.push(text);
|
|
218919
219083
|
if (opts.logs)
|
|
218920
|
-
process.stderr.write(text);
|
|
219084
|
+
process.stderr.write(demangle(text));
|
|
218921
219085
|
};
|
|
218922
219086
|
child.stdout.on("data", sink);
|
|
218923
219087
|
child.stderr.on("data", sink);
|
|
@@ -218931,10 +219095,12 @@ var runDesktopRender = async (js, resources, opts) => {
|
|
|
218931
219095
|
});
|
|
218932
219096
|
child.on("exit", (code) => {
|
|
218933
219097
|
clearTimeout(killer);
|
|
219098
|
+
const output = demangle(chunks.join(""));
|
|
218934
219099
|
if (code === 0 && existsSync14(outAbs))
|
|
218935
|
-
return done();
|
|
219100
|
+
return done({ output, exitCode: code });
|
|
219101
|
+
const tail = demangle(chunks.slice(-80).join("")).trimEnd();
|
|
218936
219102
|
const detail = tail.length > 0 ? `
|
|
218937
|
-
${tail
|
|
219103
|
+
${tail}` : "";
|
|
218938
219104
|
fail(new CliError(code === 0 ? `The renderer exited cleanly but produced no PNG — did the app open a screen?${detail}` : `The renderer exited with code ${code}.${detail}`));
|
|
218939
219105
|
});
|
|
218940
219106
|
});
|
|
@@ -219951,6 +220117,7 @@ var clone3 = async (args) => {
|
|
|
219951
220117
|
writeTsconfig(root);
|
|
219952
220118
|
}
|
|
219953
220119
|
writeDefaultIgnore(root);
|
|
220120
|
+
ensureGitignore(root);
|
|
219954
220121
|
const commits = await getCommits(apiUrl, token, uuid);
|
|
219955
220122
|
writeManifest(root, { uuid, apiUrl, name: project.name, baseHeadId: commits.headId, files });
|
|
219956
220123
|
success(`Cloned ${c.bold(project.name)} into ${c.bold(dir)}/ (${Object.keys(files).length} files)`);
|
|
@@ -219980,12 +220147,22 @@ from \`main.ts\`.
|
|
|
219980
220147
|
\`\`\`
|
|
219981
220148
|
lecodes render # semantic JSON of the launched UI (structure, text, names, values)
|
|
219982
220149
|
lecodes render --png shot.png # pixels (needs @napi-rs/canvas installed)
|
|
220150
|
+
lecodes render --png card.png --clip card # ONE component/region: a node name or x,y,w,h
|
|
219983
220151
|
lecodes render --desktop --png shot.png # NATIVE render: real Filament 3D + UI + 2D composited —
|
|
219984
220152
|
# use this to SEE 3D scenes / procedural geometry (headless
|
|
219985
220153
|
# renders record 3D as JSON only). Windows/Linux; downloads
|
|
219986
220154
|
# the renderer on first use. --frames <n> for slow loads.
|
|
219987
220155
|
lecodes render --script <file> # drive the app through a scenario (format below)
|
|
220156
|
+
lecodes render --desktop --view iso --png iso.png # 3D: inspection camera fitted to the WHOLE
|
|
220157
|
+
# scene (iso|top|front|side|back [--fov N]) — or --camera x,y,z
|
|
220158
|
+
# --look-at x,y,z; the app's camera is untouched. In scripts:
|
|
220159
|
+
# { "view": "iso", "screenshot": "iso.png" } then { "view": "off" }
|
|
220160
|
+
lecodes render --desktop --script <file> # the same scenario on the NATIVE host (3D/Jolt, real
|
|
220161
|
+
# GPU): coordinates only ({ x, y }), expect.log checked,
|
|
220162
|
+
# text/node/screen expects skipped; deterministic 16 ms frames
|
|
219988
220163
|
lecodes test # all tests/*.flow.json + design-derived flows; run before "done"
|
|
220164
|
+
lecodes test --desktop # the file flows on the NATIVE host (3D/Jolt/real GPU; coordinate
|
|
220165
|
+
# flows only — design flows stay headless)
|
|
219989
220166
|
\`\`\`
|
|
219990
220167
|
|
|
219991
220168
|
Iterate with \`--script\` scenarios and keep the useful ones as \`tests/<name>.flow.json\` — they
|
|
@@ -219994,19 +220171,32 @@ are the regression suite. A failing step dumps the failing frame (JSON + PNG) un
|
|
|
219994
220171
|
|
|
219995
220172
|
## Scenario format (tests/*.flow.json and --script files)
|
|
219996
220173
|
|
|
219997
|
-
One action per step (\`tap\`, \`type\`+\`text\`, \`fill\`, \`scroll\` [+\`by\`], \`wait
|
|
220174
|
+
One action per step (\`tap\`, \`type\`+\`text\`, \`fill\`, \`scroll\` [+\`by\`], \`wait\`, and for
|
|
220175
|
+
games: \`frames\` (advance N fixed 1/60 steps, no sleeping), \`drag\`+\`to\` [+\`ms\`], \`hold\`+\`ms\`,
|
|
220176
|
+
\`key\`+\`ms\` / \`keyDown\` / \`keyUp\` (KeyboardEvent.code: "ArrowRight", "KeyW", "Space")), then an
|
|
219998
220177
|
optional \`expect\` (\`screen\`, \`node\`/\`notNode\`, \`text\`/\`notText\`,
|
|
219999
|
-
\`value: { target, equals }\`
|
|
220000
|
-
\`
|
|
220178
|
+
\`value: { target, equals }\`, \`log\` — a \`console.log\` line contains it), then outputs
|
|
220179
|
+
(\`screenshot\` [+\`clip\`: a node name or \`{ x, y, width, height }\`], \`json\`). Targets resolve by
|
|
220180
|
+
\`name\` → exact visible text → substring → input placeholder; a tap/drag on empty space reaches
|
|
220181
|
+
the 2D scene (sprite \`click\`/\`touchstart\`, \`ev.track()\`). Envelope keys: \`name\`,
|
|
220001
220182
|
\`width\`/\`height\`, \`settle\`, \`fixtures\`, \`steps\`.
|
|
220002
220183
|
|
|
220003
220184
|
\`\`\`json
|
|
220004
220185
|
{ "steps": [
|
|
220005
220186
|
{ "expect": { "screen": "home", "text": "Taps: 0" } },
|
|
220006
|
-
{ "tap": "tap", "expect": { "text": "Taps: 1" }, "screenshot": "after.png" }
|
|
220187
|
+
{ "tap": "tap", "expect": { "text": "Taps: 1" }, "screenshot": "after.png" },
|
|
220188
|
+
{ "key": "ArrowRight", "ms": 500, "expect": { "log": "hero x=" } },
|
|
220189
|
+
{ "frames": 60, "screenshot": "hud.png", "clip": "hud" }
|
|
220007
220190
|
] }
|
|
220008
220191
|
\`\`\`
|
|
220009
220192
|
|
|
220193
|
+
Game time is deterministic headless: \`setLoop\`, 2D physics/animation and aspect \`update(dt)\`
|
|
220194
|
+
advance in fixed 1/60 steps driven by \`wait\`/\`frames\`/\`drag\`/\`key\` — never by a wall clock —
|
|
220195
|
+
so a flow that passes once passes every time. \`Input.key(code)\` reads the keys a flow holds.
|
|
220196
|
+
Headless draws UI + the 2D scene (sprites, tilemaps, Box2D); 3D is captured as JSON and Jolt does
|
|
220197
|
+
not simulate — use \`--desktop\` for 3D pixels. \`console.log\` + \`expect.log\` is the assertion
|
|
220198
|
+
channel for game state that has no UI.
|
|
220199
|
+
|
|
220010
220200
|
## Make the app testable
|
|
220011
220201
|
|
|
220012
220202
|
- Give interactive elements a \`name\`: \`UIButton(...).style({ name: "login" })\`.
|
|
@@ -220022,7 +220212,8 @@ optional \`expect\` (\`screen\`, \`node\`/\`notNode\`, \`text\`/\`notText\`,
|
|
|
220022
220212
|
|
|
220023
220213
|
## SDK documentation
|
|
220024
220214
|
|
|
220025
|
-
\`.lecodes/types/\` is the exact API surface (signatures only
|
|
220215
|
+
\`.lecodes/types/\` is the exact API surface (signatures only; generated and gitignored — run
|
|
220216
|
+
\`lecodes types\` if it's missing, never commit \`.lecodes/\`). For semantics and examples,
|
|
220026
220217
|
fetch the docs — they are served as raw markdown for AI tools:
|
|
220027
220218
|
|
|
220028
220219
|
- https://le.codes/llms.txt — the docs map; fetch it first to find the right page
|
|
@@ -220116,7 +220307,7 @@ var run = async (args) => {
|
|
|
220116
220307
|
}
|
|
220117
220308
|
const appCfg = readAppConfigAt(root);
|
|
220118
220309
|
const win = appCfg?.desktop?.window;
|
|
220119
|
-
const { js, resources } = await buildBundle(args);
|
|
220310
|
+
const { js, resources } = await buildBundle(args, undefined, { localAssets: true });
|
|
220120
220311
|
note("Launching LeCodes Desktop… (close the window to return)");
|
|
220121
220312
|
await runDesktopApp(js, resources, {
|
|
220122
220313
|
width: Number(flagStr(args, "width")) || win?.width,
|
|
@@ -220207,23 +220398,29 @@ var DEV_PRELUDE_JS = `
|
|
|
220207
220398
|
} catch (e) {}
|
|
220208
220399
|
}
|
|
220209
220400
|
|
|
220401
|
+
// An Error carries its stack (JSON.stringify would send a useless "{}"); the dev server maps the
|
|
220402
|
+
// frames back to project positions before printing.
|
|
220403
|
+
function describe(a) {
|
|
220404
|
+
if (typeof a === "string") return a
|
|
220405
|
+
if (a && typeof a.stack === "string" && typeof a.message === "string") {
|
|
220406
|
+
return a.stack ? String(a) + "\\n" + a.stack : String(a)
|
|
220407
|
+
}
|
|
220408
|
+
try { return JSON.stringify(a) } catch (e) { return String(a) }
|
|
220409
|
+
}
|
|
220410
|
+
|
|
220210
220411
|
// Mirror console output to the dev terminal ('silent' guards recursion through wrapped calls).
|
|
220211
220412
|
var silent = false
|
|
220212
220413
|
;["log", "warn", "error"].forEach(function (level) {
|
|
220213
220414
|
var orig = console[level]
|
|
220214
220415
|
console[level] = function () {
|
|
220215
220416
|
var parts = []
|
|
220216
|
-
for (var i = 0; i < arguments.length; i++)
|
|
220217
|
-
var a = arguments[i]
|
|
220218
|
-
if (typeof a === "string") { parts.push(a); continue }
|
|
220219
|
-
try { parts.push(JSON.stringify(a)) } catch (e) { parts.push(String(a)) }
|
|
220220
|
-
}
|
|
220417
|
+
for (var i = 0; i < arguments.length; i++) parts.push(describe(arguments[i]))
|
|
220221
220418
|
try { orig.apply(console, arguments) } catch (e) {}
|
|
220222
220419
|
if (!silent) { silent = true; try { post({ type: "log", level: level, text: parts.join(" ") }) } finally { silent = false } }
|
|
220223
220420
|
}
|
|
220224
220421
|
})
|
|
220225
220422
|
|
|
220226
|
-
function report(err) { post({ type: "error", text:
|
|
220423
|
+
function report(err) { post({ type: "error", text: describe(err) }) }
|
|
220227
220424
|
|
|
220228
220425
|
function load(v) {
|
|
220229
220426
|
if (busy) { queued = v; return }
|
|
@@ -220501,6 +220698,14 @@ var startDevServer = async (options) => {
|
|
|
220501
220698
|
for (const client of clients)
|
|
220502
220699
|
client.send(text);
|
|
220503
220700
|
};
|
|
220701
|
+
let demangleFor = null;
|
|
220702
|
+
const demangle = (text) => {
|
|
220703
|
+
if (bundleJs === null)
|
|
220704
|
+
return text;
|
|
220705
|
+
if (demangleFor?.js !== bundleJs)
|
|
220706
|
+
demangleFor = { js: bundleJs, map: makeStackDemangler(bundleJs) };
|
|
220707
|
+
return demangleFor.map ? demangleFor.map(text) : text;
|
|
220708
|
+
};
|
|
220504
220709
|
const printDeviceMessage = (raw) => {
|
|
220505
220710
|
let m = {};
|
|
220506
220711
|
try {
|
|
@@ -220509,9 +220714,9 @@ var startDevServer = async (options) => {
|
|
|
220509
220714
|
return;
|
|
220510
220715
|
}
|
|
220511
220716
|
if (m.type === "log")
|
|
220512
|
-
log2(`[device:${m.level ?? "log"}] ${m.text ?? ""}`);
|
|
220717
|
+
log2(`[device:${m.level ?? "log"}] ${demangle(m.text ?? "")}`);
|
|
220513
220718
|
else if (m.type === "error")
|
|
220514
|
-
log2(`[device:error] ${m.text ?? ""}`);
|
|
220719
|
+
log2(`[device:error] ${demangle(m.text ?? "")}`);
|
|
220515
220720
|
};
|
|
220516
220721
|
const transportOf = (url) => url.searchParams.get("t") === "poll" ? "poll" : "ws";
|
|
220517
220722
|
const handle = async (req, res) => {
|
|
@@ -221086,6 +221291,9 @@ lecodes app open # open it in Xcode, build & run
|
|
|
221086
221291
|
- \`assets/\` — images/fonts the app references (created on demand)
|
|
221087
221292
|
- \`tsconfig.json\` + \`.lecodes/types/\` — generated IDE types (rewritten by the CLI; don't edit).
|
|
221088
221293
|
They come from the SDK bundled in the CLI; \`lecodes types\` refreshes them from the platform.
|
|
221294
|
+
- \`.lecodes/\` is gitignored — per-checkout state (those types + \`manifest.json\`, this machine's
|
|
221295
|
+
sync cursor once linked). After a fresh git clone: \`lecodes types\`, then
|
|
221296
|
+
\`lecodes link --project <uuid>\` to re-attach.
|
|
221089
221297
|
- \`.lecodesignore\` — files a future \`lecodes push\` would skip
|
|
221090
221298
|
|
|
221091
221299
|
Full command list: \`lecodes --help\`.
|
|
@@ -221116,6 +221324,14 @@ var init2 = async (args) => {
|
|
|
221116
221324
|
write(".claude/settings.json", CLAUDE_SETTINGS);
|
|
221117
221325
|
write("tests/counter.flow.json", STARTER_FLOW);
|
|
221118
221326
|
write(".lecodesignore", DEFAULT_IGNORE_FILE);
|
|
221327
|
+
const gi = ensureGitignore(root);
|
|
221328
|
+
if (gi === "created") {
|
|
221329
|
+
log(`${c.green("created")} .gitignore`);
|
|
221330
|
+
created++;
|
|
221331
|
+
} else if (gi === "updated")
|
|
221332
|
+
log(`${c.green("updated")} .gitignore ${c.dim("(+ .lecodes/)")}`);
|
|
221333
|
+
else
|
|
221334
|
+
note("kept .gitignore");
|
|
221119
221335
|
const wroteTypes = materializeTypesLocal(root);
|
|
221120
221336
|
writeTsconfig(root);
|
|
221121
221337
|
log(`${c.green("written")} tsconfig.json${wroteTypes ? ` + ${LECODES_DIR}/types/` : ""} (IDE types)`);
|
|
@@ -221367,6 +221583,8 @@ var link = async (args) => {
|
|
|
221367
221583
|
log(`${c.green("written")} ${LECODES_DIR}/types/ + tsconfig.json ${c.dim("(from the server)")}`);
|
|
221368
221584
|
}
|
|
221369
221585
|
writeDefaultIgnore(root);
|
|
221586
|
+
if (ensureGitignore(root))
|
|
221587
|
+
log(`${c.green("written")} .gitignore ${c.dim("(.lecodes/ is per-checkout state)")}`);
|
|
221370
221588
|
if (flagBool(args, "push")) {
|
|
221371
221589
|
log("");
|
|
221372
221590
|
if (root !== process.cwd())
|
|
@@ -221403,7 +221621,7 @@ var login = async (args) => {
|
|
|
221403
221621
|
|
|
221404
221622
|
// src/commands/render.ts
|
|
221405
221623
|
import { mkdirSync as mkdirSync18, readFileSync as readFileSync22, writeFileSync as writeFileSync19 } from "node:fs";
|
|
221406
|
-
import { dirname as dirname15, resolve as
|
|
221624
|
+
import { dirname as dirname15, resolve as resolve12 } from "node:path";
|
|
221407
221625
|
|
|
221408
221626
|
// src/compile/headlessBundle.ts
|
|
221409
221627
|
init_api();
|
|
@@ -221484,6 +221702,240 @@ var compileHeadlessBundle = async (args) => {
|
|
|
221484
221702
|
|
|
221485
221703
|
// src/commands/render.ts
|
|
221486
221704
|
init_manifest();
|
|
221705
|
+
|
|
221706
|
+
// src/desktopScript.ts
|
|
221707
|
+
init_util();
|
|
221708
|
+
import { resolve as resolve11 } from "node:path";
|
|
221709
|
+
var isPoint = (v) => typeof v === "object" && v !== null && typeof v.x === "number" && typeof v.y === "number";
|
|
221710
|
+
var FRAME_MS = 16;
|
|
221711
|
+
var ACTIONS = ["tap", "type", "fill", "scroll", "wait", "frames", "drag", "hold", "key", "keyDown", "keyUp", "camera", "view"];
|
|
221712
|
+
var SEMANTIC_EXPECT = ["text", "notText", "node", "notNode", "screen", "value"];
|
|
221713
|
+
var VIEW_NAMES = ["iso", "top", "front", "side", "back"];
|
|
221714
|
+
var isVec3 = (v) => Array.isArray(v) && v.length === 3 && v.every((n2) => typeof n2 === "number");
|
|
221715
|
+
var pt = (p2) => `(${p2.x},${p2.y})`;
|
|
221716
|
+
var translateScenario = (rawSteps, opts) => {
|
|
221717
|
+
if (!Array.isArray(rawSteps))
|
|
221718
|
+
throw new CliError(`"steps" must be an array of step objects.`);
|
|
221719
|
+
const lines = [];
|
|
221720
|
+
const steps = [];
|
|
221721
|
+
let budgetMs = 0;
|
|
221722
|
+
const emit = (line) => {
|
|
221723
|
+
lines.push(line);
|
|
221724
|
+
return lines.length;
|
|
221725
|
+
};
|
|
221726
|
+
emit(`# generated by lecodes render --desktop --script`);
|
|
221727
|
+
emit(`frames ${Math.max(0, opts.headroomFrames)}`);
|
|
221728
|
+
const coord = (at, key, v) => {
|
|
221729
|
+
if (isPoint(v))
|
|
221730
|
+
return v;
|
|
221731
|
+
if (typeof v === "string") {
|
|
221732
|
+
throw new CliError(`${at}: "${key}": "${v}" — the native host can't resolve names; give --desktop scripts { "x", "y" } coordinates (logical px), or run the scenario headless (without --desktop).`);
|
|
221733
|
+
}
|
|
221734
|
+
throw new CliError(`${at}: "${key}" must be { "x", "y" }.`);
|
|
221735
|
+
};
|
|
221736
|
+
const num = (at, key, v, def) => {
|
|
221737
|
+
if (v === undefined && def !== undefined)
|
|
221738
|
+
return def;
|
|
221739
|
+
if (typeof v !== "number")
|
|
221740
|
+
throw new CliError(`${at}: "${key}" must be a number.`);
|
|
221741
|
+
return v;
|
|
221742
|
+
};
|
|
221743
|
+
let inspecting = false;
|
|
221744
|
+
rawSteps.forEach((step, i2) => {
|
|
221745
|
+
const at = `steps[${i2}]`;
|
|
221746
|
+
if (typeof step !== "object" || step === null || Array.isArray(step))
|
|
221747
|
+
throw new CliError(`${at}: must be an object.`);
|
|
221748
|
+
const s2 = step;
|
|
221749
|
+
const actions = ACTIONS.filter((k) => s2[k] !== undefined);
|
|
221750
|
+
if (actions.length > 1)
|
|
221751
|
+
throw new CliError(`${at}: one action per step (found ${actions.join(" + ")}).`);
|
|
221752
|
+
const plan = { index: i2, desc: "", lines: [], skippedExpect: [], skippedOutputs: [] };
|
|
221753
|
+
const parts = [];
|
|
221754
|
+
const action = actions[0];
|
|
221755
|
+
if (action === "type" || action === "fill" || action === "scroll") {
|
|
221756
|
+
throw new CliError(`${at}: "${action}" isn't available with --desktop (the native host has no semantic tree / soft keyboard) — run this scenario headless, or drive the gesture with "drag" coordinates.`);
|
|
221757
|
+
}
|
|
221758
|
+
if ((action === "tap" || action === "drag" || action === "hold") && inspecting) {
|
|
221759
|
+
throw new CliError(`${at}: "${action}" while an inspection camera is active — the app's hit-testing uses its own camera, not the one in the shot. Add { "view": "off" } (or { "camera": "off" }) first.`);
|
|
221760
|
+
}
|
|
221761
|
+
if (action === "view") {
|
|
221762
|
+
const v = s2.view;
|
|
221763
|
+
const fov = s2.fov !== undefined ? num(at, "fov", s2.fov) : undefined;
|
|
221764
|
+
if (v === "off") {
|
|
221765
|
+
plan.lines.push(emit("view off"));
|
|
221766
|
+
inspecting = false;
|
|
221767
|
+
parts.push("view off");
|
|
221768
|
+
} else if (typeof v === "string" && VIEW_NAMES.includes(v)) {
|
|
221769
|
+
plan.lines.push(emit(`view ${v}${fov !== undefined ? ` ${fov}` : ""}`));
|
|
221770
|
+
inspecting = true;
|
|
221771
|
+
parts.push(`view ${v}${fov !== undefined ? ` (fov ${fov})` : ""}`);
|
|
221772
|
+
} else
|
|
221773
|
+
throw new CliError(`${at}: "view" wants one of ${VIEW_NAMES.join(", ")} or "off".`);
|
|
221774
|
+
budgetMs += 2 * FRAME_MS;
|
|
221775
|
+
} else if (action === "camera") {
|
|
221776
|
+
const cam = s2.camera;
|
|
221777
|
+
if (cam === "off") {
|
|
221778
|
+
plan.lines.push(emit("camera off"));
|
|
221779
|
+
inspecting = false;
|
|
221780
|
+
parts.push("camera off");
|
|
221781
|
+
} else if (typeof cam === "object" && cam !== null && isVec3(cam.at) && isVec3(cam.lookAt)) {
|
|
221782
|
+
const { at: eye, lookAt, fov } = cam;
|
|
221783
|
+
const fovN = fov !== undefined ? num(at, "camera.fov", fov) : undefined;
|
|
221784
|
+
plan.lines.push(emit(`camera ${eye.join(" ")} ${lookAt.join(" ")}${fovN !== undefined ? ` ${fovN}` : ""}`));
|
|
221785
|
+
inspecting = true;
|
|
221786
|
+
parts.push(`camera at (${eye.join(",")}) → (${lookAt.join(",")})`);
|
|
221787
|
+
} else
|
|
221788
|
+
throw new CliError(`${at}: "camera" wants { "at": [x,y,z], "lookAt": [x,y,z], "fov"? } or "off".`);
|
|
221789
|
+
budgetMs += 2 * FRAME_MS;
|
|
221790
|
+
}
|
|
221791
|
+
if (s2.fov !== undefined && action !== "view")
|
|
221792
|
+
throw new CliError(`${at}: "fov" only goes with "view" (inside "camera" use camera.fov).`);
|
|
221793
|
+
if (action === "tap") {
|
|
221794
|
+
const p2 = coord(at, "tap", s2.tap);
|
|
221795
|
+
plan.lines.push(emit(`tap ${p2.x} ${p2.y}`));
|
|
221796
|
+
budgetMs += 4 * FRAME_MS;
|
|
221797
|
+
parts.push(`tap ${pt(p2)}`);
|
|
221798
|
+
} else if (action === "drag") {
|
|
221799
|
+
const a2 = coord(at, "drag", s2.drag), b = coord(at, "to", s2.to);
|
|
221800
|
+
const ms = num(at, "ms", s2.ms, 300);
|
|
221801
|
+
plan.lines.push(emit(`drag ${a2.x} ${a2.y} ${b.x} ${b.y} ${Math.round(ms)}`));
|
|
221802
|
+
budgetMs += ms + 4 * FRAME_MS;
|
|
221803
|
+
parts.push(`drag ${pt(a2)} → ${pt(b)} over ${ms}ms`);
|
|
221804
|
+
} else if (action === "hold") {
|
|
221805
|
+
const p2 = coord(at, "hold", s2.hold);
|
|
221806
|
+
const ms = num(at, "ms", s2.ms);
|
|
221807
|
+
plan.lines.push(emit(`hold ${p2.x} ${p2.y} ${Math.round(ms)}`));
|
|
221808
|
+
budgetMs += ms + 4 * FRAME_MS;
|
|
221809
|
+
parts.push(`hold ${pt(p2)} ${ms}ms`);
|
|
221810
|
+
} else if (action === "key") {
|
|
221811
|
+
if (typeof s2.key !== "string")
|
|
221812
|
+
throw new CliError(`${at}: "key" must be a KeyboardEvent.code string.`);
|
|
221813
|
+
const ms = num(at, "ms", s2.ms);
|
|
221814
|
+
plan.lines.push(emit(`key ${s2.key} ${Math.round(ms)}`));
|
|
221815
|
+
budgetMs += ms + 2 * FRAME_MS;
|
|
221816
|
+
parts.push(`key ${s2.key} ${ms}ms`);
|
|
221817
|
+
} else if (action === "keyDown" || action === "keyUp") {
|
|
221818
|
+
const code = s2[action];
|
|
221819
|
+
if (typeof code !== "string")
|
|
221820
|
+
throw new CliError(`${at}: "${action}" must be a code string.`);
|
|
221821
|
+
plan.lines.push(emit(`${action.toLowerCase()} ${code}`));
|
|
221822
|
+
budgetMs += 2 * FRAME_MS;
|
|
221823
|
+
parts.push(`${action} ${code}`);
|
|
221824
|
+
} else if (action === "wait") {
|
|
221825
|
+
const ms = num(at, "wait", s2.wait);
|
|
221826
|
+
plan.lines.push(emit(`wait ${Math.round(ms)}`));
|
|
221827
|
+
budgetMs += ms + FRAME_MS;
|
|
221828
|
+
parts.push(`wait ${ms}ms`);
|
|
221829
|
+
} else if (action === "frames") {
|
|
221830
|
+
const n2 = num(at, "frames", s2.frames);
|
|
221831
|
+
plan.lines.push(emit(`frames ${Math.round(n2)}`));
|
|
221832
|
+
budgetMs += n2 * FRAME_MS + FRAME_MS;
|
|
221833
|
+
parts.push(`advance ${n2} frames`);
|
|
221834
|
+
}
|
|
221835
|
+
if (s2.expect !== undefined) {
|
|
221836
|
+
if (typeof s2.expect !== "object" || s2.expect === null)
|
|
221837
|
+
throw new CliError(`${at}: "expect" must be an object.`);
|
|
221838
|
+
const e2 = s2.expect;
|
|
221839
|
+
const bits = [];
|
|
221840
|
+
for (const k of Object.keys(e2)) {
|
|
221841
|
+
if (k === "log") {
|
|
221842
|
+
if (typeof e2.log !== "string")
|
|
221843
|
+
throw new CliError(`${at}: expect.log must be a string.`);
|
|
221844
|
+
plan.expectLog = e2.log;
|
|
221845
|
+
bits.push(`log~"${e2.log}"`);
|
|
221846
|
+
} else if (SEMANTIC_EXPECT.includes(k)) {
|
|
221847
|
+
plan.skippedExpect.push(k);
|
|
221848
|
+
} else {
|
|
221849
|
+
throw new CliError(`${at}: unknown expect key "${k}".`);
|
|
221850
|
+
}
|
|
221851
|
+
}
|
|
221852
|
+
if (bits.length > 0)
|
|
221853
|
+
parts.push(`expect ${bits.join(" · ")}`);
|
|
221854
|
+
}
|
|
221855
|
+
if (s2.clip !== undefined)
|
|
221856
|
+
throw new CliError(`${at}: "clip" isn't available with --desktop (the native host dumps the whole window).`);
|
|
221857
|
+
if (s2.screenshot !== undefined) {
|
|
221858
|
+
if (typeof s2.screenshot !== "string")
|
|
221859
|
+
throw new CliError(`${at}: "screenshot" must be a path.`);
|
|
221860
|
+
plan.screenshot = resolve11(opts.outDir, s2.screenshot);
|
|
221861
|
+
plan.lines.push(emit(`frames 2`));
|
|
221862
|
+
plan.lines.push(emit(`shot ${plan.screenshot}`));
|
|
221863
|
+
budgetMs += 3 * FRAME_MS;
|
|
221864
|
+
parts.push(`→ ${s2.screenshot}`);
|
|
221865
|
+
}
|
|
221866
|
+
if (s2.json !== undefined)
|
|
221867
|
+
plan.skippedOutputs.push("json");
|
|
221868
|
+
if (plan.lines.length === 0) {
|
|
221869
|
+
plan.lines.push(emit(`frames 1`));
|
|
221870
|
+
budgetMs += FRAME_MS;
|
|
221871
|
+
}
|
|
221872
|
+
plan.desc = typeof s2.name === "string" ? s2.name : parts.join(", ") || "(empty step)";
|
|
221873
|
+
steps.push(plan);
|
|
221874
|
+
});
|
|
221875
|
+
const frameCap = Math.max(0, opts.headroomFrames) + Math.ceil(budgetMs / FRAME_MS) + 600;
|
|
221876
|
+
return { text: lines.join(`
|
|
221877
|
+
`) + `
|
|
221878
|
+
`, steps, frameCap };
|
|
221879
|
+
};
|
|
221880
|
+
var evaluateDesktopRun = (plan, output) => {
|
|
221881
|
+
const lines = output.split(/\r?\n/);
|
|
221882
|
+
const doneAt = new Map;
|
|
221883
|
+
const consoleLines = [];
|
|
221884
|
+
let loaded = false, ended = false;
|
|
221885
|
+
for (const line of lines) {
|
|
221886
|
+
const m = /^\[script\] #(\d+) (done|error) (\S+)(?:: (.*))?$/.exec(line);
|
|
221887
|
+
if (m) {
|
|
221888
|
+
doneAt.set(Number(m[1]), { at: consoleLines.length, error: m[2] === "error" ? `${m[3]}: ${m[4] ?? "failed"}` : undefined });
|
|
221889
|
+
continue;
|
|
221890
|
+
}
|
|
221891
|
+
if (line.startsWith("[script] loaded")) {
|
|
221892
|
+
loaded = true;
|
|
221893
|
+
continue;
|
|
221894
|
+
}
|
|
221895
|
+
if (line === "[script] end") {
|
|
221896
|
+
ended = true;
|
|
221897
|
+
continue;
|
|
221898
|
+
}
|
|
221899
|
+
if (line.startsWith("[script]"))
|
|
221900
|
+
continue;
|
|
221901
|
+
consoleLines.push(line);
|
|
221902
|
+
}
|
|
221903
|
+
const steps = [];
|
|
221904
|
+
let ok = true;
|
|
221905
|
+
for (const st of plan.steps) {
|
|
221906
|
+
const skipped = [
|
|
221907
|
+
...st.skippedExpect.map((k) => `expect.${k} (no semantic tree on --desktop)`),
|
|
221908
|
+
...st.skippedOutputs.map((k) => `${k} output (no semantic tree on --desktop)`)
|
|
221909
|
+
];
|
|
221910
|
+
let error;
|
|
221911
|
+
let cutoff = consoleLines.length;
|
|
221912
|
+
for (const ln of st.lines) {
|
|
221913
|
+
const d = doneAt.get(ln);
|
|
221914
|
+
if (!d) {
|
|
221915
|
+
error = error ?? "the host never completed this step (crashed, closed, or hit the frame cap before it)";
|
|
221916
|
+
break;
|
|
221917
|
+
}
|
|
221918
|
+
if (d.error) {
|
|
221919
|
+
error = error ?? d.error;
|
|
221920
|
+
break;
|
|
221921
|
+
}
|
|
221922
|
+
cutoff = d.at;
|
|
221923
|
+
}
|
|
221924
|
+
if (!error && st.expectLog !== undefined) {
|
|
221925
|
+
const seen = consoleLines.slice(0, cutoff);
|
|
221926
|
+
if (!seen.some((l2) => l2.includes(st.expectLog))) {
|
|
221927
|
+
const tail = seen.filter((l2) => l2.startsWith("[DEBUG]") || l2.startsWith("[INFO]") || l2.startsWith("[WARN]") || l2.startsWith("[ERROR]")).slice(-3);
|
|
221928
|
+
error = `expected a console line containing "${st.expectLog}" — ${tail.length === 0 ? "the app logged nothing" : `last lines: ${tail.map((l2) => `"${l2}"`).join(", ")}`}`;
|
|
221929
|
+
}
|
|
221930
|
+
}
|
|
221931
|
+
if (error)
|
|
221932
|
+
ok = false;
|
|
221933
|
+
steps.push({ desc: st.desc, ok: !error, error, skipped });
|
|
221934
|
+
}
|
|
221935
|
+
return { loaded, ended, ok: ok && loaded, steps };
|
|
221936
|
+
};
|
|
221937
|
+
|
|
221938
|
+
// src/commands/render.ts
|
|
221487
221939
|
init_util();
|
|
221488
221940
|
var DEVICE_SAFE_AREA = {
|
|
221489
221941
|
none: { insets: [0, 0, 0, 0], bars: [] },
|
|
@@ -221514,7 +221966,7 @@ var requireRenderer = async () => {
|
|
|
221514
221966
|
};
|
|
221515
221967
|
var fileScenarioIO = (dir) => ({
|
|
221516
221968
|
write: (relPath, data) => {
|
|
221517
|
-
const abs =
|
|
221969
|
+
const abs = resolve12(dir, relPath);
|
|
221518
221970
|
mkdirSync18(dirname15(abs), { recursive: true });
|
|
221519
221971
|
writeFileSync19(abs, data);
|
|
221520
221972
|
}
|
|
@@ -221524,7 +221976,7 @@ var resolveFixtures = (value, baseDir) => {
|
|
|
221524
221976
|
return;
|
|
221525
221977
|
let map = value;
|
|
221526
221978
|
if (typeof value === "string") {
|
|
221527
|
-
const abs =
|
|
221979
|
+
const abs = resolve12(baseDir, value);
|
|
221528
221980
|
try {
|
|
221529
221981
|
map = JSON.parse(readFileSync22(abs, "utf8"));
|
|
221530
221982
|
} catch (e2) {
|
|
@@ -221539,7 +221991,7 @@ var resolveFixtures = (value, baseDir) => {
|
|
|
221539
221991
|
var readScenarioFile = (path2) => {
|
|
221540
221992
|
let raw;
|
|
221541
221993
|
try {
|
|
221542
|
-
raw = JSON.parse(readFileSync22(
|
|
221994
|
+
raw = JSON.parse(readFileSync22(resolve12(path2), "utf8"));
|
|
221543
221995
|
} catch (e2) {
|
|
221544
221996
|
throw new CliError(`Can't read scenario "${path2}": ${e2 instanceof Error ? e2.message : String(e2)}`);
|
|
221545
221997
|
}
|
|
@@ -221552,6 +222004,20 @@ var readScenarioFile = (path2) => {
|
|
|
221552
222004
|
throw new CliError(`Scenario "${path2}" has no "steps".`);
|
|
221553
222005
|
return file;
|
|
221554
222006
|
};
|
|
222007
|
+
var parseClip = (raw) => {
|
|
222008
|
+
if (raw === undefined)
|
|
222009
|
+
return;
|
|
222010
|
+
const parts = raw.split(",").map((p2) => p2.trim());
|
|
222011
|
+
if (parts.length === 4 && parts.every((p2) => /^-?\d+(\.\d+)?$/.test(p2))) {
|
|
222012
|
+
const [x2, y2, width, height] = parts.map(Number);
|
|
222013
|
+
if (width <= 0 || height <= 0)
|
|
222014
|
+
throw new CliError(`--clip: width and height must be positive (got "${raw}").`);
|
|
222015
|
+
return { x: x2, y: y2, width, height };
|
|
222016
|
+
}
|
|
222017
|
+
if (parts.length === 1 && raw.trim() !== "")
|
|
222018
|
+
return raw.trim();
|
|
222019
|
+
throw new CliError(`--clip: expected a node name or "x,y,width,height" (got "${raw}").`);
|
|
222020
|
+
};
|
|
221555
222021
|
var renderDesktop = async (args) => {
|
|
221556
222022
|
let root;
|
|
221557
222023
|
try {
|
|
@@ -221560,22 +222026,101 @@ var renderDesktop = async (args) => {
|
|
|
221560
222026
|
root = process.cwd();
|
|
221561
222027
|
}
|
|
221562
222028
|
const win = readAppConfigAt(root)?.desktop?.window;
|
|
221563
|
-
const
|
|
221564
|
-
const
|
|
222029
|
+
const scriptFlag = flagStr(args, "script");
|
|
222030
|
+
const scenario = scriptFlag !== undefined ? readScenarioFile(scriptFlag) : null;
|
|
222031
|
+
const width = Number(flagStr(args, "width")) || scenario?.width || win?.width || 390;
|
|
222032
|
+
const height = Number(flagStr(args, "height")) || scenario?.height || win?.height || 844;
|
|
221565
222033
|
const frames = Number(flagStr(args, "frames")) || 60;
|
|
221566
|
-
const timeoutMs = Number(flagStr(args, "timeout")) || 60000;
|
|
221567
222034
|
const pngFlag = flagStr(args, "png") ?? "screenshot.png";
|
|
221568
|
-
const
|
|
222035
|
+
const logs = flagBool(args, "logs");
|
|
222036
|
+
const fixedDtFlag = flagStr(args, "fixed-dt");
|
|
222037
|
+
const fixedDtMs = fixedDtFlag !== undefined ? Number(fixedDtFlag) : scenario ? 16 : 0;
|
|
222038
|
+
if (!Number.isFinite(fixedDtMs) || fixedDtMs < 0 || fixedDtMs > 1000)
|
|
222039
|
+
throw new CliError(`--fixed-dt wants a number of ms (0 = wall clock, max 1000), got "${fixedDtFlag}".`);
|
|
222040
|
+
const { camera, view } = resolveInspectionCamera(args);
|
|
222041
|
+
if ((camera || view) && scenario)
|
|
222042
|
+
throw new CliError('--camera/--view apply to a plain render; in a --script run use a { "view": "iso" } or { "camera": { "at", "lookAt" } } step before the screenshot.');
|
|
222043
|
+
const { js, resources } = await buildBundle(args, undefined, { localAssets: true });
|
|
222044
|
+
mkdirSync18(dirname15(resolve12(pngFlag)), { recursive: true });
|
|
222045
|
+
if (scenario) {
|
|
222046
|
+
const outDir = flagStr(args, "out-dir") ?? ".";
|
|
222047
|
+
const plan = translateScenario(scenario.steps, { outDir, headroomFrames: frames });
|
|
222048
|
+
for (const st of plan.steps)
|
|
222049
|
+
if (st.screenshot)
|
|
222050
|
+
mkdirSync18(dirname15(st.screenshot), { recursive: true });
|
|
222051
|
+
const timeoutMs2 = Number(flagStr(args, "timeout")) || 60000 + plan.steps.length * 5000;
|
|
222052
|
+
note(`Running ${plan.steps.length} step${plan.steps.length === 1 ? "" : "s"} (native)…`);
|
|
222053
|
+
const { output } = await runDesktopRender(js, resources, {
|
|
222054
|
+
width,
|
|
222055
|
+
height,
|
|
222056
|
+
frames: plan.frameCap,
|
|
222057
|
+
outPng: pngFlag,
|
|
222058
|
+
logs,
|
|
222059
|
+
timeoutMs: timeoutMs2,
|
|
222060
|
+
script: plan.text,
|
|
222061
|
+
fixedDtMs
|
|
222062
|
+
});
|
|
222063
|
+
const result = evaluateDesktopRun(plan, output);
|
|
222064
|
+
if (!result.loaded) {
|
|
222065
|
+
throw new CliError("This desktop host doesn't support scripted input (CREATOR_SCRIPT) — it ran the frames and ignored the steps. Update it: lecodes desktop update (or point LECODES_DESKTOP_EXE at a newer build).");
|
|
222066
|
+
}
|
|
222067
|
+
for (const st of result.steps) {
|
|
222068
|
+
logErr(st.ok ? `${c.green("✓")} ${st.desc}` : `${c.red("✗")} ${st.desc}
|
|
222069
|
+
${c.red(st.error ?? "failed")}`);
|
|
222070
|
+
for (const sk of st.skipped)
|
|
222071
|
+
logErr(c.dim(` – skipped: ${sk}`));
|
|
222072
|
+
}
|
|
222073
|
+
const skippedCount = result.steps.reduce((n2, s2) => n2 + s2.skipped.length, 0);
|
|
222074
|
+
if (skippedCount > 0)
|
|
222075
|
+
logErr(c.dim(` ${skippedCount} semantic check${skippedCount === 1 ? "" : "s"} skipped — the native host has no UI tree; run the same scenario headless to verify them.`));
|
|
222076
|
+
if (!result.ended)
|
|
222077
|
+
warnErr("The host stopped before the script finished (frame cap / crash) — later steps are reported as incomplete.");
|
|
222078
|
+
logErr(result.ok ? c.green(`✓ ${result.steps.length}/${result.steps.length} steps passed (native)`) : c.red(`✗ ${result.steps.filter((s2) => !s2.ok).length} of ${result.steps.length} steps failed (native)`));
|
|
222079
|
+
note(`Wrote ${pngFlag} (${width}x${height}, native render, final frame)`);
|
|
222080
|
+
process.exit(result.ok ? 0 : 1);
|
|
222081
|
+
}
|
|
222082
|
+
const timeoutMs = Number(flagStr(args, "timeout")) || 60000;
|
|
221569
222083
|
note("Rendering (native)…");
|
|
221570
222084
|
await runDesktopRender(js, resources, {
|
|
221571
222085
|
width,
|
|
221572
222086
|
height,
|
|
221573
222087
|
frames,
|
|
221574
222088
|
outPng: pngFlag,
|
|
221575
|
-
logs
|
|
221576
|
-
timeoutMs
|
|
222089
|
+
logs,
|
|
222090
|
+
timeoutMs,
|
|
222091
|
+
fixedDtMs,
|
|
222092
|
+
camera,
|
|
222093
|
+
view
|
|
221577
222094
|
});
|
|
221578
|
-
note(`Wrote ${pngFlag} (${width}x${height}, native render)`);
|
|
222095
|
+
note(`Wrote ${pngFlag} (${width}x${height}, native render${view ? `, ${view.split(";")[0]} view` : camera ? ", custom camera" : ""})`);
|
|
222096
|
+
};
|
|
222097
|
+
var VIEW_NAMES2 = ["iso", "top", "front", "side", "back"];
|
|
222098
|
+
var parseVec3Flag = (name, raw) => {
|
|
222099
|
+
const parts = raw.split(",").map((p2) => p2.trim());
|
|
222100
|
+
if (parts.length !== 3 || parts.some((p2) => !/^-?\d+(\.\d+)?$/.test(p2)))
|
|
222101
|
+
throw new CliError(`--${name} wants "x,y,z" (got "${raw}").`);
|
|
222102
|
+
return parts.join(",");
|
|
222103
|
+
};
|
|
222104
|
+
var resolveInspectionCamera = (args) => {
|
|
222105
|
+
const cam = flagStr(args, "camera"), look = flagStr(args, "look-at"), view = flagStr(args, "view"), fov = flagStr(args, "fov");
|
|
222106
|
+
if (fov !== undefined && !(Number(fov) > 0 && Number(fov) < 180))
|
|
222107
|
+
throw new CliError(`--fov wants degrees in (0, 180), got "${fov}".`);
|
|
222108
|
+
const fovSuffix = fov !== undefined ? `;${Number(fov)}` : "";
|
|
222109
|
+
if (cam !== undefined || look !== undefined) {
|
|
222110
|
+
if (view !== undefined)
|
|
222111
|
+
throw new CliError("Use either --camera/--look-at or --view, not both.");
|
|
222112
|
+
if (cam === undefined || look === undefined)
|
|
222113
|
+
throw new CliError("--camera and --look-at go together: --camera x,y,z --look-at x,y,z");
|
|
222114
|
+
return { camera: `${parseVec3Flag("camera", cam)};${parseVec3Flag("look-at", look)}${fovSuffix}` };
|
|
222115
|
+
}
|
|
222116
|
+
if (view !== undefined) {
|
|
222117
|
+
if (!VIEW_NAMES2.includes(view))
|
|
222118
|
+
throw new CliError(`--view wants one of ${VIEW_NAMES2.join(", ")} (got "${view}").`);
|
|
222119
|
+
return { view: `${view}${fovSuffix}` };
|
|
222120
|
+
}
|
|
222121
|
+
if (fov !== undefined)
|
|
222122
|
+
throw new CliError("--fov goes with --camera/--look-at or --view.");
|
|
222123
|
+
return {};
|
|
221579
222124
|
};
|
|
221580
222125
|
var render = async (args) => {
|
|
221581
222126
|
if (flagBool(args, "desktop"))
|
|
@@ -221597,7 +222142,7 @@ var render = async (args) => {
|
|
|
221597
222142
|
const settleStr = flagStr(args, "settle") ?? (scenario?.settle !== undefined ? String(scenario.settle) : undefined);
|
|
221598
222143
|
const timeStr = flagStr(args, "time");
|
|
221599
222144
|
const onConsole = flagBool(args, "logs") ? (level, text2) => logErr(`${c.dim(`[${level}]`)} ${text2}`) : undefined;
|
|
221600
|
-
const envelopeFixtures = resolveFixtures(scenario?.fixtures, scriptFlag !== undefined ? dirname15(
|
|
222145
|
+
const envelopeFixtures = resolveFixtures(scenario?.fixtures, scriptFlag !== undefined ? dirname15(resolve12(scriptFlag)) : ".");
|
|
221601
222146
|
const flagFixtures = resolveFixtures(flagStr(args, "fixtures"), ".");
|
|
221602
222147
|
const fixtures = envelopeFixtures || flagFixtures ? { ...envelopeFixtures, ...flagFixtures } : undefined;
|
|
221603
222148
|
if (fixtures) {
|
|
@@ -221636,7 +222181,7 @@ var render = async (args) => {
|
|
|
221636
222181
|
${c.red(st.error ?? "failed")}`);
|
|
221637
222182
|
}
|
|
221638
222183
|
if (!result2.ok && result2.failureArtifacts.length > 0) {
|
|
221639
|
-
logErr(c.dim(` failing frame: ${result2.failureArtifacts.map((p2) =>
|
|
222184
|
+
logErr(c.dim(` failing frame: ${result2.failureArtifacts.map((p2) => resolve12(outDir, p2)).join(", ")}`));
|
|
221640
222185
|
}
|
|
221641
222186
|
logErr(result2.ok ? c.green(`✓ ${result2.steps.length}/${steps.length} steps passed`) : c.red(`✗ failed at step ${result2.steps.length} of ${steps.length}`));
|
|
221642
222187
|
return done((cb) => cb(), result2.ok ? 0 : 1);
|
|
@@ -221647,19 +222192,21 @@ var render = async (args) => {
|
|
|
221647
222192
|
const pngFlag = flagStr(args, "png") ?? (flagBool(args, "png") ? "screenshot.png" : undefined);
|
|
221648
222193
|
if (pngFlag !== undefined) {
|
|
221649
222194
|
note("Rendering PNG…");
|
|
221650
|
-
const
|
|
222195
|
+
const clip = parseClip(flagStr(args, "clip"));
|
|
222196
|
+
const { png: png2, width: outW, height: outH, warnings: pngWarnings } = await renderer.renderToPng(js, {
|
|
221651
222197
|
width,
|
|
221652
222198
|
height,
|
|
221653
222199
|
scale: Number(flagStr(args, "scale")) || 2,
|
|
221654
|
-
...timing
|
|
222200
|
+
...timing,
|
|
222201
|
+
clip
|
|
221655
222202
|
});
|
|
221656
222203
|
for (const w of [...warnings, ...pngWarnings])
|
|
221657
222204
|
warnErr(w);
|
|
221658
222205
|
if (isNullSink(pngFlag)) {
|
|
221659
222206
|
note(`Discarded PNG (${pngFlag})`);
|
|
221660
222207
|
} else {
|
|
221661
|
-
writeFileSync19(
|
|
221662
|
-
note(`Wrote ${pngFlag} (${png2.length} bytes, ${
|
|
222208
|
+
writeFileSync19(resolve12(pngFlag), png2);
|
|
222209
|
+
note(`Wrote ${pngFlag} (${png2.length} bytes, ${outW}x${outH}${clip !== undefined ? ", clipped" : ""})`);
|
|
221663
222210
|
}
|
|
221664
222211
|
return done((cb) => cb());
|
|
221665
222212
|
}
|
|
@@ -221673,7 +222220,7 @@ var render = async (args) => {
|
|
|
221673
222220
|
return done((cb) => cb());
|
|
221674
222221
|
}
|
|
221675
222222
|
if (out) {
|
|
221676
|
-
writeFileSync19(
|
|
222223
|
+
writeFileSync19(resolve12(out), result.text);
|
|
221677
222224
|
note(`Wrote ${out} (${result.text.length} bytes)`);
|
|
221678
222225
|
return done((cb) => cb());
|
|
221679
222226
|
}
|
|
@@ -222034,7 +222581,7 @@ var pullPaths = async (root, manifest, project, paths, apiUrl, token) => {
|
|
|
222034
222581
|
// src/commands/scene.ts
|
|
222035
222582
|
init_manifest();
|
|
222036
222583
|
import { existsSync as existsSync27, readFileSync as readFileSync25, readdirSync as readdirSync12, statSync as statSync8, writeFileSync as writeFileSync22 } from "node:fs";
|
|
222037
|
-
import { basename as basename14, dirname as dirname17, join as join27, resolve as
|
|
222584
|
+
import { basename as basename14, dirname as dirname17, join as join27, resolve as resolve13 } from "node:path";
|
|
222038
222585
|
|
|
222039
222586
|
// src/compile/sceneCompile.ts
|
|
222040
222587
|
init_localFiles();
|
|
@@ -222107,7 +222654,7 @@ var getContext2 = (args) => {
|
|
|
222107
222654
|
manifest = null;
|
|
222108
222655
|
const pathArg = args._[0];
|
|
222109
222656
|
if (pathArg) {
|
|
222110
|
-
const abs =
|
|
222657
|
+
const abs = resolve13(process.cwd(), pathArg);
|
|
222111
222658
|
if (existsSync27(abs) && statSync8(abs).isDirectory())
|
|
222112
222659
|
root = abs;
|
|
222113
222660
|
else if (abs.endsWith(".scene.ts") && existsSync27(abs))
|
|
@@ -222235,8 +222782,9 @@ var status2 = async (_args) => {
|
|
|
222235
222782
|
};
|
|
222236
222783
|
|
|
222237
222784
|
// src/commands/test.ts
|
|
222238
|
-
import { existsSync as existsSync28, readFileSync as readFileSync26, readdirSync as readdirSync13 } from "node:fs";
|
|
222239
|
-
import { dirname as dirname18, resolve as
|
|
222785
|
+
import { existsSync as existsSync28, mkdirSync as mkdirSync20, readFileSync as readFileSync26, readdirSync as readdirSync13 } from "node:fs";
|
|
222786
|
+
import { dirname as dirname18, resolve as resolve14 } from "node:path";
|
|
222787
|
+
init_manifest();
|
|
222240
222788
|
init_util();
|
|
222241
222789
|
var deriveDesignFlows = (meta, entryScreen) => {
|
|
222242
222790
|
const edges = meta.edges ?? [];
|
|
@@ -222321,17 +222869,39 @@ var readFlowFile = (abs, displayName) => {
|
|
|
222321
222869
|
};
|
|
222322
222870
|
};
|
|
222323
222871
|
var test = async (args) => {
|
|
222324
|
-
const
|
|
222325
|
-
|
|
222872
|
+
const desktop2 = flagBool(args, "desktop");
|
|
222873
|
+
if (desktop2 && flagBool(args, "flows"))
|
|
222874
|
+
throw new CliError("--flows needs the headless renderer (design flows tap by name) — drop --desktop.");
|
|
222875
|
+
const native = desktop2 ? await buildBundle(args, undefined, { localAssets: true }) : null;
|
|
222876
|
+
const headless = desktop2 ? null : await compileHeadlessBundle(args);
|
|
222877
|
+
const warnings = headless?.warnings ?? [];
|
|
222878
|
+
const useLocalAssets = headless?.useLocalAssets ?? false;
|
|
222879
|
+
const js = headless?.js ?? "";
|
|
222880
|
+
let root;
|
|
222881
|
+
if (headless)
|
|
222882
|
+
root = headless.root;
|
|
222883
|
+
else {
|
|
222884
|
+
try {
|
|
222885
|
+
root = findProjectRoot(process.cwd());
|
|
222886
|
+
} catch {
|
|
222887
|
+
root = process.cwd();
|
|
222888
|
+
}
|
|
222889
|
+
}
|
|
222890
|
+
const renderer = desktop2 ? null : await requireRenderer();
|
|
222891
|
+
const nativeFrames = Number(flagStr(args, "frames")) || 60;
|
|
222892
|
+
const fixedDtFlag = flagStr(args, "fixed-dt");
|
|
222893
|
+
const nativeFixedDt = fixedDtFlag !== undefined ? Number(fixedDtFlag) : 16;
|
|
222894
|
+
if (!Number.isFinite(nativeFixedDt) || nativeFixedDt < 0 || nativeFixedDt > 1000)
|
|
222895
|
+
throw new CliError(`--fixed-dt wants a number of ms (0 = wall clock, max 1000), got "${fixedDtFlag}".`);
|
|
222326
222896
|
const projectViewport = desktopDefaultViewport(root);
|
|
222327
|
-
const testsDir =
|
|
222328
|
-
const designMetaPath =
|
|
222897
|
+
const testsDir = resolve14(root, flagStr(args, "dir") ?? "tests");
|
|
222898
|
+
const designMetaPath = resolve14(root, flagStr(args, "design-dir") ?? "design", "meta.json");
|
|
222329
222899
|
const asJson = flagBool(args, "json");
|
|
222330
222900
|
const out = asJson ? logErr : log;
|
|
222331
222901
|
const cases = [];
|
|
222332
222902
|
if (args._.length > 0) {
|
|
222333
222903
|
for (const given of args._) {
|
|
222334
|
-
const candidates = [
|
|
222904
|
+
const candidates = [resolve14(given), resolve14(testsDir, given), resolve14(testsDir, `${given}.flow.json`)];
|
|
222335
222905
|
const abs = candidates.find((p2) => existsSync28(p2));
|
|
222336
222906
|
if (!abs)
|
|
222337
222907
|
throw new CliError(`No such test: "${given}" (looked in ${testsDir}).`);
|
|
@@ -222339,11 +222909,11 @@ var test = async (args) => {
|
|
|
222339
222909
|
}
|
|
222340
222910
|
} else if (existsSync28(testsDir)) {
|
|
222341
222911
|
for (const f3 of readdirSync13(testsDir).filter((f4) => f4.endsWith(".flow.json")).sort()) {
|
|
222342
|
-
cases.push(readFlowFile(
|
|
222912
|
+
cases.push(readFlowFile(resolve14(testsDir, f3), f3));
|
|
222343
222913
|
}
|
|
222344
222914
|
}
|
|
222345
222915
|
const fixturesFlag = flagStr(args, "fixtures");
|
|
222346
|
-
const defaultPack =
|
|
222916
|
+
const defaultPack = resolve14(testsDir, "fixtures.json");
|
|
222347
222917
|
const flowFixtures = fixturesFlag !== undefined ? resolveFixtures(fixturesFlag, ".") : existsSync28(defaultPack) ? resolveFixtures("fixtures.json", testsDir) : undefined;
|
|
222348
222918
|
const flowsOnly = flagBool(args, "flows");
|
|
222349
222919
|
const wantFlows = !flagBool(args, "no-flows") && args._.length === 0;
|
|
@@ -222351,7 +222921,10 @@ var test = async (args) => {
|
|
|
222351
222921
|
cases.length = 0;
|
|
222352
222922
|
let skipped = [];
|
|
222353
222923
|
let entryScreen = null;
|
|
222354
|
-
if (
|
|
222924
|
+
if (desktop2 && wantFlows && existsSync28(designMetaPath)) {
|
|
222925
|
+
note("Design-derived flows skipped with --desktop (they tap by name; run `lecodes test` headless for them).");
|
|
222926
|
+
}
|
|
222927
|
+
if (!desktop2 && (wantFlows || flowsOnly) && existsSync28(designMetaPath)) {
|
|
222355
222928
|
let meta;
|
|
222356
222929
|
try {
|
|
222357
222930
|
meta = JSON.parse(readFileSync26(designMetaPath, "utf8"));
|
|
@@ -222379,18 +222952,60 @@ var test = async (args) => {
|
|
|
222379
222952
|
warnErr(w);
|
|
222380
222953
|
throw new CliError(`No tests found. Write tests/*.flow.json scenarios, or draw activator edges in the design board (design/meta.json).`);
|
|
222381
222954
|
}
|
|
222382
|
-
const timeoutMs = Number(flagStr(args, "timeout")) || 20000 + cases.length * 1e4;
|
|
222955
|
+
const timeoutMs = Number(flagStr(args, "timeout")) || (desktop2 ? 60000 + cases.length * 60000 : 20000 + cases.length * 1e4);
|
|
222383
222956
|
const killer = setTimeout(() => {
|
|
222384
222957
|
warnErr(`Test run timed out after ${timeoutMs}ms (raise --timeout or lower --settle).`);
|
|
222385
222958
|
process.exit(1);
|
|
222386
222959
|
}, timeoutMs);
|
|
222387
222960
|
killer.unref?.();
|
|
222388
222961
|
const onConsole = flagBool(args, "logs") ? (level, text) => logErr(`${c.dim(`[${level}]`)} ${text}`) : undefined;
|
|
222389
|
-
const artifactsRoot =
|
|
222962
|
+
const artifactsRoot = resolve14(root, flagStr(args, "artifacts") ?? resolve14(testsDir, ".artifacts"));
|
|
222390
222963
|
const results = [];
|
|
222964
|
+
const runNativeCase = async (kase) => {
|
|
222965
|
+
const caseDir = resolve14(artifactsRoot, slug3(kase.name));
|
|
222966
|
+
const plan = translateScenario(kase.steps, { outDir: caseDir, headroomFrames: nativeFrames });
|
|
222967
|
+
for (const st of plan.steps)
|
|
222968
|
+
if (st.screenshot)
|
|
222969
|
+
mkdirSync20(dirname18(st.screenshot), { recursive: true });
|
|
222970
|
+
mkdirSync20(caseDir, { recursive: true });
|
|
222971
|
+
const finalPng = resolve14(caseDir, "final.png");
|
|
222972
|
+
const { output } = await runDesktopRender(native.js, native.resources, {
|
|
222973
|
+
width: Number(flagStr(args, "width")) || kase.width || projectViewport?.width || 390,
|
|
222974
|
+
height: Number(flagStr(args, "height")) || kase.height || projectViewport?.height || 844,
|
|
222975
|
+
frames: plan.frameCap,
|
|
222976
|
+
outPng: finalPng,
|
|
222977
|
+
logs: flagBool(args, "logs"),
|
|
222978
|
+
timeoutMs: 60000 + plan.steps.length * 5000,
|
|
222979
|
+
script: plan.text,
|
|
222980
|
+
fixedDtMs: nativeFixedDt
|
|
222981
|
+
});
|
|
222982
|
+
const r2 = evaluateDesktopRun(plan, output);
|
|
222983
|
+
if (!r2.loaded)
|
|
222984
|
+
throw new CliError("This desktop host doesn't support scripted input (CREATOR_SCRIPT). Update it: lecodes desktop update (or point LECODES_DESKTOP_EXE at a newer build).");
|
|
222985
|
+
return {
|
|
222986
|
+
name: kase.name,
|
|
222987
|
+
kind: kase.kind,
|
|
222988
|
+
ok: r2.ok,
|
|
222989
|
+
steps: r2.steps.map((s2) => ({ desc: s2.desc, ok: s2.ok, error: s2.error, skipped: s2.skipped.length > 0 ? s2.skipped : undefined })),
|
|
222990
|
+
error: r2.ended ? undefined : "the host stopped before the script finished (frame cap / crash)",
|
|
222991
|
+
artifacts: r2.ok ? [] : [finalPng]
|
|
222992
|
+
};
|
|
222993
|
+
};
|
|
222391
222994
|
for (const kase of cases) {
|
|
222392
222995
|
const flagSettle = flagStr(args, "settle");
|
|
222393
222996
|
let result;
|
|
222997
|
+
if (desktop2) {
|
|
222998
|
+
try {
|
|
222999
|
+
result = await runNativeCase(kase);
|
|
223000
|
+
} catch (e2) {
|
|
223001
|
+
if (e2 instanceof CliError && /doesn't support scripted input/.test(e2.message))
|
|
223002
|
+
throw e2;
|
|
223003
|
+
result = { name: kase.name, kind: kase.kind, ok: false, steps: [], error: e2 instanceof Error ? e2.message : String(e2), artifacts: [] };
|
|
223004
|
+
}
|
|
223005
|
+
results.push(result);
|
|
223006
|
+
reportCase(result);
|
|
223007
|
+
continue;
|
|
223008
|
+
}
|
|
222394
223009
|
try {
|
|
222395
223010
|
const steps = renderer.parseScenarioSteps(kase.steps);
|
|
222396
223011
|
const session = await renderer.createSession(js, {
|
|
@@ -222404,14 +223019,14 @@ var test = async (args) => {
|
|
|
222404
223019
|
onConsole
|
|
222405
223020
|
});
|
|
222406
223021
|
try {
|
|
222407
|
-
const io = fileScenarioIO(
|
|
223022
|
+
const io = fileScenarioIO(resolve14(artifactsRoot, slug3(kase.name)));
|
|
222408
223023
|
const outcome = await renderer.runScenario(session, steps, io);
|
|
222409
223024
|
result = {
|
|
222410
223025
|
name: kase.name,
|
|
222411
223026
|
kind: kase.kind,
|
|
222412
223027
|
ok: outcome.ok,
|
|
222413
223028
|
steps: outcome.steps.map((s2) => ({ desc: s2.desc, ok: s2.ok, error: s2.error })),
|
|
222414
|
-
artifacts: outcome.failureArtifacts.map((p2) =>
|
|
223029
|
+
artifacts: outcome.failureArtifacts.map((p2) => resolve14(artifactsRoot, slug3(kase.name), p2))
|
|
222415
223030
|
};
|
|
222416
223031
|
} finally {
|
|
222417
223032
|
session.dispose();
|
|
@@ -222420,14 +223035,20 @@ var test = async (args) => {
|
|
|
222420
223035
|
result = { name: kase.name, kind: kase.kind, ok: false, steps: [], error: e2 instanceof Error ? e2.message : String(e2), artifacts: [] };
|
|
222421
223036
|
}
|
|
222422
223037
|
results.push(result);
|
|
223038
|
+
reportCase(result);
|
|
223039
|
+
}
|
|
223040
|
+
function reportCase(result) {
|
|
223041
|
+
const skippedCount = result.steps.reduce((n2, s2) => n2 + (s2.skipped?.length ?? 0), 0);
|
|
222423
223042
|
if (result.ok) {
|
|
222424
|
-
out(`${c.green("✓")} ${result.name}`);
|
|
223043
|
+
out(`${c.green("✓")} ${result.name}${skippedCount > 0 ? c.dim(` (${skippedCount} semantic check${skippedCount === 1 ? "" : "s"} skipped on --desktop)`) : ""}`);
|
|
222425
223044
|
} else {
|
|
222426
223045
|
out(`${c.red("✗")} ${result.name}`);
|
|
222427
223046
|
if (result.error)
|
|
222428
223047
|
out(` ${c.red(result.error)}`);
|
|
222429
223048
|
for (const s2 of result.steps) {
|
|
222430
223049
|
out(s2.ok ? c.dim(` ✓ ${s2.desc}`) : ` ${c.red(`✗ ${s2.desc} — ${s2.error}`)}`);
|
|
223050
|
+
for (const sk of s2.skipped ?? [])
|
|
223051
|
+
out(c.dim(` – skipped: ${sk}`));
|
|
222431
223052
|
}
|
|
222432
223053
|
if (result.artifacts.length > 0)
|
|
222433
223054
|
out(c.dim(` failing frame: ${result.artifacts.join(", ")}`));
|
|
@@ -222473,11 +223094,11 @@ init_config();
|
|
|
222473
223094
|
init_manifest();
|
|
222474
223095
|
init_types4();
|
|
222475
223096
|
init_util();
|
|
222476
|
-
import { resolve as
|
|
223097
|
+
import { resolve as resolve15 } from "node:path";
|
|
222477
223098
|
var label = (bundle) => `${Object.keys(bundle).length} file(s)`;
|
|
222478
223099
|
var types4 = async (args) => {
|
|
222479
223100
|
const dirArg = args._[0];
|
|
222480
|
-
const root = dirArg ?
|
|
223101
|
+
const root = dirArg ? resolve15(dirArg) : (() => {
|
|
222481
223102
|
try {
|
|
222482
223103
|
return findProjectRoot(process.cwd());
|
|
222483
223104
|
} catch {
|
|
@@ -222543,16 +223164,16 @@ import { delimiter as delimiter2, dirname as dirname20, join as join29 } from "n
|
|
|
222543
223164
|
// src/commands/install.ts
|
|
222544
223165
|
init_util();
|
|
222545
223166
|
import { execFileSync as execFileSync5 } from "node:child_process";
|
|
222546
|
-
import { appendFileSync as appendFileSync3, chmodSync as chmodSync2, cpSync as cpSync4, existsSync as existsSync29, mkdirSync as
|
|
223167
|
+
import { appendFileSync as appendFileSync3, chmodSync as chmodSync2, cpSync as cpSync4, existsSync as existsSync29, mkdirSync as mkdirSync21, readFileSync as readFileSync27, realpathSync, rmSync as rmSync7, symlinkSync } from "node:fs";
|
|
222547
223168
|
import { homedir as homedir7 } from "node:os";
|
|
222548
|
-
import { basename as basename15, delimiter, dirname as dirname19, join as join28, resolve as
|
|
223169
|
+
import { basename as basename15, delimiter, dirname as dirname19, join as join28, resolve as resolve16, sep as sep2 } from "node:path";
|
|
222549
223170
|
var isWin = process.platform === "win32";
|
|
222550
223171
|
var HOME = homedir7();
|
|
222551
223172
|
var safeRealpath = (p2) => {
|
|
222552
223173
|
try {
|
|
222553
223174
|
return realpathSync(p2);
|
|
222554
223175
|
} catch {
|
|
222555
|
-
return
|
|
223176
|
+
return resolve16(p2);
|
|
222556
223177
|
}
|
|
222557
223178
|
};
|
|
222558
223179
|
var onPath = (dir) => {
|
|
@@ -222580,7 +223201,7 @@ var install = async (args) => {
|
|
|
222580
223201
|
return installUnix(args, bundle, companions);
|
|
222581
223202
|
};
|
|
222582
223203
|
var copyBundle = (bundle, target, companions) => {
|
|
222583
|
-
|
|
223204
|
+
mkdirSync21(target, { recursive: true });
|
|
222584
223205
|
if (safeRealpath(target) === safeRealpath(bundle))
|
|
222585
223206
|
return;
|
|
222586
223207
|
for (const f3 of companions) {
|
|
@@ -222598,8 +223219,8 @@ var permError = (e2, dir, system) => {
|
|
|
222598
223219
|
};
|
|
222599
223220
|
var installUnix = (args, bundle, companions) => {
|
|
222600
223221
|
const system = flagBool(args, "system");
|
|
222601
|
-
const libDir =
|
|
222602
|
-
const binDir =
|
|
223222
|
+
const libDir = resolve16(flagStr(args, "dir") ?? (system ? "/usr/local/lib/lecodes-cli" : join28(HOME, ".local", "lib", "lecodes-cli")));
|
|
223223
|
+
const binDir = resolve16(flagStr(args, "bin") ?? (system ? "/usr/local/bin" : join28(HOME, ".local", "bin")));
|
|
222603
223224
|
const link2 = join28(binDir, "lecodes");
|
|
222604
223225
|
try {
|
|
222605
223226
|
copyBundle(bundle, libDir, companions);
|
|
@@ -222609,7 +223230,7 @@ var installUnix = (args, bundle, companions) => {
|
|
|
222609
223230
|
throw permError(e2, libDir, system);
|
|
222610
223231
|
}
|
|
222611
223232
|
try {
|
|
222612
|
-
|
|
223233
|
+
mkdirSync21(binDir, { recursive: true });
|
|
222613
223234
|
rmSync7(link2, { force: true });
|
|
222614
223235
|
symlinkSync(join28(libDir, "lecodes"), link2);
|
|
222615
223236
|
} catch (e2) {
|
|
@@ -222670,7 +223291,7 @@ var finishPathUnix = (args, binDir) => {
|
|
|
222670
223291
|
log(` • or install system-wide (no PATH edit): ${c.bold("sudo lecodes install --system")}`);
|
|
222671
223292
|
};
|
|
222672
223293
|
var installWindows = (args, bundle, companions) => {
|
|
222673
|
-
const target =
|
|
223294
|
+
const target = resolve16(flagStr(args, "dir") ?? join28(process.env.LOCALAPPDATA ?? join28(HOME, "AppData", "Local"), "Programs", "lecodes-cli"));
|
|
222674
223295
|
try {
|
|
222675
223296
|
copyBundle(bundle, target, companions);
|
|
222676
223297
|
} catch (e2) {
|
|
@@ -222811,8 +223432,12 @@ ${c.bold("Commands:")}
|
|
|
222811
223432
|
--png [file] rasterize a PNG (default screenshot.png;
|
|
222812
223433
|
needs @napi-rs/canvas)
|
|
222813
223434
|
--script <file> drive the app through a step list (tap/type/
|
|
222814
|
-
scroll/wait/expect/screenshot/json steps
|
|
223435
|
+
scroll/wait/expect/screenshot/json steps;
|
|
223436
|
+
games: frames/drag/hold/key/keyDown/keyUp,
|
|
223437
|
+
expect.log, screenshot clip)
|
|
222815
223438
|
--out-dir <dir> where --script step outputs land (default .)
|
|
223439
|
+
--clip <name|x,y,w,h> (--png) crop to one node's rect or a
|
|
223440
|
+
logical-px rect — a component/region render
|
|
222816
223441
|
--fixtures <file> canned fetch responses (URL pattern →
|
|
222817
223442
|
{ status?, json?, text? }) — make
|
|
222818
223443
|
backend-gated screens reachable headless
|
|
@@ -222830,6 +223455,22 @@ ${c.bold("Commands:")}
|
|
|
222830
223455
|
LECODES_DESKTOP_EXE overrides)
|
|
222831
223456
|
--frames <n> (--desktop) frames to run before the shot —
|
|
222832
223457
|
headroom for async loads (default 60)
|
|
223458
|
+
--desktop --script <file> drive the scenario through the native
|
|
223459
|
+
host: tap/drag/hold take { x, y } coordinates
|
|
223460
|
+
(no names — the host has no UI tree; type/
|
|
223461
|
+
scroll/clip unavailable), expect.log is
|
|
223462
|
+
checked, text/node/screen/value are skipped;
|
|
223463
|
+
per-step screenshots + final --png
|
|
223464
|
+
--view <name> (--desktop) inspection camera auto-fitted to the
|
|
223465
|
+
whole 3D scene: iso | top | front | side | back
|
|
223466
|
+
[--fov N]; the app's camera is untouched
|
|
223467
|
+
--camera x,y,z --look-at x,y,z [--fov N]
|
|
223468
|
+
(--desktop) inspection camera at a fixed eye /
|
|
223469
|
+
look-at point (world units, Y up). In a
|
|
223470
|
+
--script run use { "view" } / { "camera" } steps
|
|
223471
|
+
--fixed-dt <ms> (--desktop) virtual app clock, ms per frame —
|
|
223472
|
+
deterministic game time (scripted runs
|
|
223473
|
+
default to 16; plain renders to wall clock)
|
|
222833
223474
|
test [name…] Run headless flow tests: tests/*.flow.json scenarios, plus flows
|
|
222834
223475
|
derived from the design graph (design/meta.json activator edges
|
|
222835
223476
|
— checks the built app against the designed navigation; needs
|
|
@@ -222846,6 +223487,11 @@ ${c.bold("Commands:")}
|
|
|
222846
223487
|
--dir <folder> tests folder (default tests/)
|
|
222847
223488
|
--design-dir <dir> design folder (default design/)
|
|
222848
223489
|
--artifacts <dir> failure-artifact folder override
|
|
223490
|
+
--desktop run the file scenarios through the NATIVE host
|
|
223491
|
+
(coordinates only; expect.log checked,
|
|
223492
|
+
text/node/screen/value skipped; design flows
|
|
223493
|
+
stay headless). --frames / --fixed-dt as in
|
|
223494
|
+
render --desktop
|
|
222849
223495
|
(+ the render timing flags: --width/--height, --settle,
|
|
222850
223496
|
--device, --safe-area, --wait-network, --logs, --entry)
|
|
222851
223497
|
types [dir] Refresh the IDE types (.lecodes/types/ + tsconfig.json) — by
|
|
@@ -222884,7 +223530,9 @@ ${c.bold("Commands:")}
|
|
|
222884
223530
|
app.json fullscreen; --scale <0.5..4|fit> overrides
|
|
222885
223531
|
the display scale — "fit" sizes a 1080p-designed app
|
|
222886
223532
|
to the monitor with the density raised to match;
|
|
222887
|
-
--entry overrides the entrypoint)
|
|
223533
|
+
--entry overrides the entrypoint; asset() resources
|
|
223534
|
+
load from the working tree — --remote-assets or
|
|
223535
|
+
--public-url <url> uses the server's copies instead)
|
|
222888
223536
|
update move to the newest published release
|
|
222889
223537
|
(cached in ~/.lecodes/renderer)
|
|
222890
223538
|
scene [path] LeCodes Scene Editor — visually edit a .scene.ts (tree, inspector,
|
|
@@ -222898,7 +223546,22 @@ ${c.bold("Commands:")}
|
|
|
222898
223546
|
FBX → LeCodes-ready GLB: meters/Y-up, textures embedded, clips from
|
|
222899
223547
|
other FBX files merged by bone name (Mixamo: one FBX per animation);
|
|
222900
223548
|
--fix/--ktx2 run the doctor passes (needs 'lecodes-assets')
|
|
222901
|
-
|
|
223549
|
+
Unity .mat files next to the FBX are read for the real textures
|
|
223550
|
+
(metallic/roughness/ORM/emission packed the glTF way); --no-unity
|
|
223551
|
+
keeps the FBX references; --unity-index <unity-index.txt> resolves GUIDs
|
|
223552
|
+
--list-nodes / --keep <node> … / --split [dir]: carve one variant out of a
|
|
223553
|
+
multi-variant prop file, re-origined on the ground (or one GLB per node)
|
|
223554
|
+
--merge-skins b.fbx …: modular character pieces onto ONE skeleton
|
|
223555
|
+
assets unpack <pkg.unitypackage> [-o dir] [--only re …] [--list]
|
|
223556
|
+
Stream a .unitypackage to a folder: writes unity-index.txt (GUID → path,
|
|
223557
|
+
what convert resolves textures with) and the matching assets + .meta
|
|
223558
|
+
assets scene <map.unity> [-o level.json] [--filter re] [--group g] [--region x0,z0,x1,z1]
|
|
223559
|
+
Unity scene → placed prefabs / mesh objects with world TRS in LeCodes'
|
|
223560
|
+
frame + a prefab table (FBX + --keep node per prefab)
|
|
223561
|
+
assets probe <file.fbx|.glb> [--filmstrip [out.png]]
|
|
223562
|
+
Skeleton, clips (+ root motion / first-frame-vs-rest per clip), meshes,
|
|
223563
|
+
materials, textures, bind sanity, duplicate names; --filmstrip renders
|
|
223564
|
+
a CPU-skinned PNG strip per clip (no GPU)
|
|
222902
223565
|
assets doctor <in.glb|dir> [--fix] [--ktx2] [--dump]
|
|
222903
223566
|
Analyze / fix a GLB against engine limits (bone cap 256, texture
|
|
222904
223567
|
formats/size, KTX2 via toktx)
|