lecodes-cli 0.1.2 → 0.1.5
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/README.md +1 -1
- package/dist/index.js +643 -169
- package/package.json +5 -5
- package/runtime/worker/ui.js +2 -2
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ lecodes preview > app.js # compile the local project to JS
|
|
|
35
35
|
| `pull` | Overwrite local files with the project's current remote state. `--force` to discard local edits. |
|
|
36
36
|
| `push` | Push all local changes as **one** new checkpoint. `-m`/`--message` for the message, `--force` to override the "server moved on" guard. |
|
|
37
37
|
| `preview` | Compile the local project to a single JS bundle, printed to **stdout** (pipeable). `-o`/`--out <file>` to write a file, `--entry <path>` to override the entrypoint, `--no-header` to omit the metadata header, `--public-url <url>` for the asset URL prefix. Diagnostics go to stderr. |
|
|
38
|
-
| `render` | Render the local **UI** project. By default emits a semantic JSON description (great for AI/automated checks) to stdout or `-o`/`--out <file>`; `--png [file]` rasterizes a PNG (default `screenshot.png`, `--scale <n>` for DPR). `--width`/`--height` set the viewport (default 390×844). Requires the optional `lecodes-
|
|
38
|
+
| `render` | Render the local **UI** project. By default emits a semantic JSON description (great for AI/automated checks) to stdout or `-o`/`--out <file>`; `--png [file]` rasterizes a PNG (default `screenshot.png`, `--scale <n>` for DPR). `--width`/`--height` set the viewport (default 390×844). Requires the optional `lecodes-renderer` package (`npm i -D lecodes-renderer`); PNG also needs `@napi-rs/canvas`. 3D scenes aren't rendered. |
|
|
39
39
|
|
|
40
40
|
## How it works
|
|
41
41
|
|
package/dist/index.js
CHANGED
|
@@ -2,8 +2,13 @@
|
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
4
4
|
|
|
5
|
+
// src/index.ts
|
|
6
|
+
import { readFileSync as readFileSync8 } from "node:fs";
|
|
7
|
+
import { dirname as dirname5, join as join9 } from "node:path";
|
|
8
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
9
|
+
|
|
5
10
|
// src/commands/clone.ts
|
|
6
|
-
import { existsSync as
|
|
11
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync5, readdirSync } from "node:fs";
|
|
7
12
|
import { resolve } from "node:path";
|
|
8
13
|
|
|
9
14
|
// src/util.ts
|
|
@@ -23,6 +28,7 @@ var log = (msg = "") => process.stdout.write(msg + `
|
|
|
23
28
|
`);
|
|
24
29
|
var info = (msg) => log(msg);
|
|
25
30
|
var success = (msg) => log(c.green("✓ ") + msg);
|
|
31
|
+
var warn = (msg) => log(c.yellow("! ") + msg);
|
|
26
32
|
var logErr = (msg = "") => process.stderr.write(msg + `
|
|
27
33
|
`);
|
|
28
34
|
var note = (msg) => logErr(c.dim(msg));
|
|
@@ -44,6 +50,8 @@ var endProgress = () => {
|
|
|
44
50
|
|
|
45
51
|
class CliError extends Error {
|
|
46
52
|
}
|
|
53
|
+
var NULL_SINKS = new Set(["/dev/null", "nul", "//./nul"]);
|
|
54
|
+
var isNullSink = (path) => NULL_SINKS.has(path.trim().replace(/\\/g, "/").toLowerCase());
|
|
47
55
|
var sha256 = (data) => createHash("sha256").update(data).digest("hex");
|
|
48
56
|
var toKey = (path) => {
|
|
49
57
|
const parts = path.replace(/\\/g, "/").split("/").map((p) => p.trim()).filter((p) => p && p !== ".");
|
|
@@ -59,6 +67,12 @@ var prompt = (question, fallback) => new Promise((resolve) => {
|
|
|
59
67
|
resolve(answer.trim() || fallback || "");
|
|
60
68
|
});
|
|
61
69
|
});
|
|
70
|
+
var confirm = async (question, fallback = false) => {
|
|
71
|
+
const answer = (await prompt(`${question} ${fallback ? "[Y/n]" : "[y/N]"}`)).toLowerCase();
|
|
72
|
+
if (!answer)
|
|
73
|
+
return fallback;
|
|
74
|
+
return answer === "y" || answer === "yes";
|
|
75
|
+
};
|
|
62
76
|
var parseArgs = (argv) => {
|
|
63
77
|
const out = { _: [], flags: {} };
|
|
64
78
|
for (let i = 0;i < argv.length; i++) {
|
|
@@ -154,6 +168,7 @@ var uploadBlob = (apiUrl, token, uuid, name, bytes) => {
|
|
|
154
168
|
return apiRequest(apiUrl, `/projects/${uuid}/blobs`, { token, body: form });
|
|
155
169
|
};
|
|
156
170
|
var sync = (apiUrl, token, uuid, payload) => apiRequest(apiUrl, `/projects/${uuid}/sync`, { token, body: payload });
|
|
171
|
+
var publishProject = (apiUrl, token, uuid) => apiRequest(apiUrl, `/projects/${uuid}/publish`, { token, body: {} });
|
|
157
172
|
var downloadBlob = async (apiUrl, src) => {
|
|
158
173
|
const resp = await fetch(`${apiUrl}${src}`);
|
|
159
174
|
if (!resp.ok)
|
|
@@ -328,6 +343,96 @@ var writeTsconfig = (root) => {
|
|
|
328
343
|
writeFileSync4(join4(root, "tsconfig.json"), JSON.stringify(TSCONFIG, null, 2));
|
|
329
344
|
};
|
|
330
345
|
|
|
346
|
+
// src/ignore.ts
|
|
347
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync5 } from "node:fs";
|
|
348
|
+
import { join as join5 } from "node:path";
|
|
349
|
+
var BUILTIN_PATTERNS = ["node_modules/", ".git/", "_*", "*.log", ".DS_Store"];
|
|
350
|
+
var DEFAULT_IGNORE_FILE = `# Files matched here are NOT pushed to LeCodes.
|
|
351
|
+
# Only untracked files are affected — assets already in the project stay tracked
|
|
352
|
+
# even if they match a pattern. Built-in defaults (node_modules, _*, *.log) always apply.
|
|
353
|
+
|
|
354
|
+
# Render / preview outputs
|
|
355
|
+
screenshot.png
|
|
356
|
+
shots/
|
|
357
|
+
*.png
|
|
358
|
+
|
|
359
|
+
# Local notes & dumps (uncomment to ignore)
|
|
360
|
+
# *.json
|
|
361
|
+
# notes.md
|
|
362
|
+
`;
|
|
363
|
+
var translate = (glob) => {
|
|
364
|
+
let re = "";
|
|
365
|
+
for (let i = 0;i < glob.length; i++) {
|
|
366
|
+
const ch = glob[i];
|
|
367
|
+
if (ch === "*") {
|
|
368
|
+
if (glob[i + 1] === "*") {
|
|
369
|
+
re += ".*";
|
|
370
|
+
i++;
|
|
371
|
+
if (glob[i + 1] === "/")
|
|
372
|
+
i++;
|
|
373
|
+
} else
|
|
374
|
+
re += "[^/]*";
|
|
375
|
+
} else if (ch === "?")
|
|
376
|
+
re += "[^/]";
|
|
377
|
+
else if (ch === "/")
|
|
378
|
+
re += "/";
|
|
379
|
+
else if ("\\^$.|+()[]{}".includes(ch))
|
|
380
|
+
re += "\\" + ch;
|
|
381
|
+
else
|
|
382
|
+
re += ch;
|
|
383
|
+
}
|
|
384
|
+
return re;
|
|
385
|
+
};
|
|
386
|
+
var compile = (raw) => {
|
|
387
|
+
let p = raw.trim();
|
|
388
|
+
if (!p || p.startsWith("#"))
|
|
389
|
+
return null;
|
|
390
|
+
let negate = false;
|
|
391
|
+
if (p.startsWith("!")) {
|
|
392
|
+
negate = true;
|
|
393
|
+
p = p.slice(1);
|
|
394
|
+
}
|
|
395
|
+
let dirOnly = false;
|
|
396
|
+
if (p.endsWith("/")) {
|
|
397
|
+
dirOnly = true;
|
|
398
|
+
p = p.slice(0, -1);
|
|
399
|
+
}
|
|
400
|
+
const anchored = p.startsWith("/") || p.includes("/");
|
|
401
|
+
if (p.startsWith("/"))
|
|
402
|
+
p = p.slice(1);
|
|
403
|
+
if (!p)
|
|
404
|
+
return null;
|
|
405
|
+
const body = translate(p);
|
|
406
|
+
const re = anchored ? new RegExp("^/" + body + "(/.*)?$") : new RegExp("(^|/)" + body + "(/.*)?$");
|
|
407
|
+
return { re, negate, dirOnly };
|
|
408
|
+
};
|
|
409
|
+
var buildMatcher = (rules) => (key, isDir) => {
|
|
410
|
+
let ignored = false;
|
|
411
|
+
for (const r of rules) {
|
|
412
|
+
if (r.dirOnly && !isDir)
|
|
413
|
+
continue;
|
|
414
|
+
if (r.re.test(key))
|
|
415
|
+
ignored = !r.negate;
|
|
416
|
+
}
|
|
417
|
+
return ignored;
|
|
418
|
+
};
|
|
419
|
+
var loadIgnore = (root) => {
|
|
420
|
+
const lines = [...BUILTIN_PATTERNS];
|
|
421
|
+
const file = join5(root, ".lecodesignore");
|
|
422
|
+
if (existsSync2(file)) {
|
|
423
|
+
try {
|
|
424
|
+
lines.push(...readFileSync3(file, "utf8").split(/\r?\n/));
|
|
425
|
+
} catch {}
|
|
426
|
+
}
|
|
427
|
+
const rules = lines.map(compile).filter((r) => r !== null);
|
|
428
|
+
return buildMatcher(rules);
|
|
429
|
+
};
|
|
430
|
+
var writeDefaultIgnore = (root) => {
|
|
431
|
+
const file = join5(root, ".lecodesignore");
|
|
432
|
+
if (!existsSync2(file))
|
|
433
|
+
writeFileSync5(file, DEFAULT_IGNORE_FILE);
|
|
434
|
+
};
|
|
435
|
+
|
|
331
436
|
// src/commands/clone.ts
|
|
332
437
|
var parseUuid = (input) => {
|
|
333
438
|
if (!input)
|
|
@@ -347,7 +452,7 @@ var clone = async (args) => {
|
|
|
347
452
|
const project = await getProject(apiUrl, token, uuid);
|
|
348
453
|
const dir = args._[1] ?? slug(project.name);
|
|
349
454
|
const root = resolve(dir);
|
|
350
|
-
if (
|
|
455
|
+
if (existsSync3(root) && readdirSync(root).length > 0) {
|
|
351
456
|
throw new CliError(`Directory "${dir}" already exists and is not empty.`);
|
|
352
457
|
}
|
|
353
458
|
mkdirSync5(root, { recursive: true });
|
|
@@ -359,12 +464,310 @@ var clone = async (args) => {
|
|
|
359
464
|
await materializeTypes(root, apiUrl);
|
|
360
465
|
writeTsconfig(root);
|
|
361
466
|
}
|
|
467
|
+
writeDefaultIgnore(root);
|
|
362
468
|
const commits = await getCommits(apiUrl, token, uuid);
|
|
363
469
|
writeManifest(root, { uuid, apiUrl, name: project.name, baseHeadId: commits.headId, files });
|
|
364
470
|
success(`Cloned ${c.bold(project.name)} into ${c.bold(dir)}/ (${Object.keys(files).length} files)`);
|
|
365
471
|
info(c.dim(` cd ${dir} — edit, then \`lecodes push -m "your message"\``));
|
|
366
472
|
};
|
|
367
473
|
|
|
474
|
+
// src/localFiles.ts
|
|
475
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync4 } from "node:fs";
|
|
476
|
+
import { join as join6, relative } from "node:path";
|
|
477
|
+
var SKIP_DIRS = new Set([".lecodes", ".git", "node_modules"]);
|
|
478
|
+
var SKIP_FILES = new Set(["tsconfig.json", "jsconfig.json", ".gitignore", ".lecodesignore", ".DS_Store"]);
|
|
479
|
+
var TEXT_EXTS = new Set([
|
|
480
|
+
"ts",
|
|
481
|
+
"tsx",
|
|
482
|
+
"js",
|
|
483
|
+
"jsx",
|
|
484
|
+
"mjs",
|
|
485
|
+
"cjs",
|
|
486
|
+
"json",
|
|
487
|
+
"svg",
|
|
488
|
+
"glsl",
|
|
489
|
+
"vert",
|
|
490
|
+
"frag",
|
|
491
|
+
"txt",
|
|
492
|
+
"md",
|
|
493
|
+
"css",
|
|
494
|
+
"html",
|
|
495
|
+
"xml",
|
|
496
|
+
"yml",
|
|
497
|
+
"yaml",
|
|
498
|
+
"csv"
|
|
499
|
+
]);
|
|
500
|
+
var ext = (name) => {
|
|
501
|
+
const i = name.lastIndexOf(".");
|
|
502
|
+
return i < 0 ? "" : name.slice(i + 1).toLowerCase();
|
|
503
|
+
};
|
|
504
|
+
var classifyKind = (name) => {
|
|
505
|
+
const e = ext(name);
|
|
506
|
+
if (e === "mat")
|
|
507
|
+
return "shader";
|
|
508
|
+
if (e === "scene")
|
|
509
|
+
return "scene";
|
|
510
|
+
return TEXT_EXTS.has(e) ? "text" : "resource";
|
|
511
|
+
};
|
|
512
|
+
var isTextKind = (kind) => kind !== "resource";
|
|
513
|
+
var scanLocal = (root, opts = {}) => {
|
|
514
|
+
const { ignore, tracked } = opts;
|
|
515
|
+
const files = [];
|
|
516
|
+
const dirs = [];
|
|
517
|
+
const ignored = [];
|
|
518
|
+
const isTracked = (key) => tracked?.has(key) ?? false;
|
|
519
|
+
const hasTrackedUnder = (key) => {
|
|
520
|
+
if (!tracked)
|
|
521
|
+
return false;
|
|
522
|
+
const prefix = key + "/";
|
|
523
|
+
for (const t of tracked)
|
|
524
|
+
if (t.startsWith(prefix))
|
|
525
|
+
return true;
|
|
526
|
+
return false;
|
|
527
|
+
};
|
|
528
|
+
const walk = (dir, parentIgnored) => {
|
|
529
|
+
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
530
|
+
const abs = join6(dir, entry.name);
|
|
531
|
+
const key = toKey(relative(root, abs));
|
|
532
|
+
if (entry.isDirectory()) {
|
|
533
|
+
if (SKIP_DIRS.has(entry.name))
|
|
534
|
+
continue;
|
|
535
|
+
const dirIgnored = parentIgnored || !!ignore && ignore(key, true);
|
|
536
|
+
if (dirIgnored && !isTracked(key) && !hasTrackedUnder(key)) {
|
|
537
|
+
ignored.push(key + "/");
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
dirs.push(key);
|
|
541
|
+
walk(abs, dirIgnored);
|
|
542
|
+
} else if (entry.isFile()) {
|
|
543
|
+
if (SKIP_FILES.has(entry.name))
|
|
544
|
+
continue;
|
|
545
|
+
const fileIgnored = parentIgnored || !!ignore && ignore(key, false);
|
|
546
|
+
if (fileIgnored && !isTracked(key)) {
|
|
547
|
+
ignored.push(key);
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
files.push({ path: key, absPath: abs, sha: sha256(readFileSync4(abs)) });
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
walk(root, false);
|
|
555
|
+
return { files, dirs, ignored };
|
|
556
|
+
};
|
|
557
|
+
var scanProject = (root, manifest) => scanLocal(root, { ignore: loadIgnore(root), tracked: new Set(Object.keys(manifest.files)) });
|
|
558
|
+
var diffLocal = (manifest, scan) => {
|
|
559
|
+
const added = [];
|
|
560
|
+
const modified = [];
|
|
561
|
+
const localByPath = new Map(scan.files.map((f) => [f.path, f]));
|
|
562
|
+
for (const file of scan.files) {
|
|
563
|
+
const known = manifest.files[file.path];
|
|
564
|
+
if (!known)
|
|
565
|
+
added.push(file);
|
|
566
|
+
else if (known.sha !== file.sha)
|
|
567
|
+
modified.push(file);
|
|
568
|
+
}
|
|
569
|
+
const deleted = Object.keys(manifest.files).filter((path) => !localByPath.has(path));
|
|
570
|
+
const moves = new Map;
|
|
571
|
+
const claimed = new Set;
|
|
572
|
+
for (const file of added) {
|
|
573
|
+
const from = deleted.find((path) => !claimed.has(path) && manifest.files[path].sha === file.sha);
|
|
574
|
+
if (from) {
|
|
575
|
+
moves.set(file.path, from);
|
|
576
|
+
claimed.add(from);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return { added, modified, deleted, moves };
|
|
580
|
+
};
|
|
581
|
+
var hasChanges = (diff) => diff.added.length + diff.modified.length + diff.deleted.length > 0;
|
|
582
|
+
|
|
583
|
+
// src/serverDiff.ts
|
|
584
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
585
|
+
import { basename } from "node:path";
|
|
586
|
+
var isTextAsset = (t) => t === "text" || t === "shader" || t === "scene";
|
|
587
|
+
var computeServerDiff = (project, scan) => {
|
|
588
|
+
const pathById = buildPaths(project.assets);
|
|
589
|
+
const serverByPath = new Map;
|
|
590
|
+
for (const asset of project.assets) {
|
|
591
|
+
if (asset.type === "folder")
|
|
592
|
+
continue;
|
|
593
|
+
serverByPath.set(pathById.get(asset.id), asset);
|
|
594
|
+
}
|
|
595
|
+
const localByPath = new Map(scan.files.map((f) => [f.path, f]));
|
|
596
|
+
const entries = [];
|
|
597
|
+
const paths = new Set([...serverByPath.keys(), ...localByPath.keys()]);
|
|
598
|
+
for (const path of [...paths].sort()) {
|
|
599
|
+
const server = serverByPath.get(path);
|
|
600
|
+
const local = localByPath.get(path);
|
|
601
|
+
if (local && !server) {
|
|
602
|
+
const text = isTextKind(classifyKind(basename(path)));
|
|
603
|
+
entries.push({ path, status: "added", binary: !text, serverText: "", localText: text ? readFileSync5(local.absPath, "utf8") : undefined });
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
if (server && !local) {
|
|
607
|
+
const text = isTextAsset(server.type);
|
|
608
|
+
entries.push({ path, status: "removed", binary: !text, serverText: text ? server.text ?? "" : undefined, localText: "" });
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
if (server && local) {
|
|
612
|
+
if (isTextAsset(server.type)) {
|
|
613
|
+
const serverText = server.text ?? "";
|
|
614
|
+
const localText = readFileSync5(local.absPath, "utf8");
|
|
615
|
+
if (serverText !== localText)
|
|
616
|
+
entries.push({ path, status: "modified", binary: false, serverText, localText });
|
|
617
|
+
} else {
|
|
618
|
+
const serverSize = server.file?.size;
|
|
619
|
+
const localSize = Buffer.byteLength(readFileSync5(local.absPath));
|
|
620
|
+
if (serverSize !== undefined && serverSize !== localSize) {
|
|
621
|
+
entries.push({ path, status: "modified", binary: true });
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
return entries;
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
// src/textDiff.ts
|
|
630
|
+
var MAX_LINES = 4000;
|
|
631
|
+
var lcsDiff = (a, b) => {
|
|
632
|
+
const n = a.length, m = b.length;
|
|
633
|
+
const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
|
|
634
|
+
for (let i2 = n - 1;i2 >= 0; i2--) {
|
|
635
|
+
for (let j2 = m - 1;j2 >= 0; j2--) {
|
|
636
|
+
dp[i2][j2] = a[i2] === b[j2] ? dp[i2 + 1][j2 + 1] + 1 : Math.max(dp[i2 + 1][j2], dp[i2][j2 + 1]);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
const ops = [];
|
|
640
|
+
let i = 0, j = 0;
|
|
641
|
+
while (i < n && j < m) {
|
|
642
|
+
if (a[i] === b[j]) {
|
|
643
|
+
ops.push({ tag: " ", line: a[i] });
|
|
644
|
+
i++;
|
|
645
|
+
j++;
|
|
646
|
+
} else if (dp[i + 1][j] >= dp[i][j + 1]) {
|
|
647
|
+
ops.push({ tag: "-", line: a[i] });
|
|
648
|
+
i++;
|
|
649
|
+
} else {
|
|
650
|
+
ops.push({ tag: "+", line: b[j] });
|
|
651
|
+
j++;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
while (i < n)
|
|
655
|
+
ops.push({ tag: "-", line: a[i++] });
|
|
656
|
+
while (j < m)
|
|
657
|
+
ops.push({ tag: "+", line: b[j++] });
|
|
658
|
+
return ops;
|
|
659
|
+
};
|
|
660
|
+
var splitLines = (text) => {
|
|
661
|
+
if (text === "")
|
|
662
|
+
return [];
|
|
663
|
+
const lines = text.split(`
|
|
664
|
+
`);
|
|
665
|
+
if (lines[lines.length - 1] === "")
|
|
666
|
+
lines.pop();
|
|
667
|
+
return lines;
|
|
668
|
+
};
|
|
669
|
+
var unifiedDiff = (oldText, newText, oldLabel, newLabel, context = 3) => {
|
|
670
|
+
if (oldText === newText)
|
|
671
|
+
return "";
|
|
672
|
+
const a = splitLines(oldText);
|
|
673
|
+
const b = splitLines(newText);
|
|
674
|
+
if (a.length + b.length > MAX_LINES) {
|
|
675
|
+
return c.dim(` (large file — ${a.length} → ${b.length} lines, diff omitted)`);
|
|
676
|
+
}
|
|
677
|
+
const ops = lcsDiff(a, b);
|
|
678
|
+
const hunks = [];
|
|
679
|
+
let aLine = 0, bLine = 0;
|
|
680
|
+
let cur = null;
|
|
681
|
+
let trailingContext = 0;
|
|
682
|
+
for (let k = 0;k < ops.length; k++) {
|
|
683
|
+
const op = ops[k];
|
|
684
|
+
const isChange = op.tag !== " ";
|
|
685
|
+
if (isChange) {
|
|
686
|
+
if (!cur) {
|
|
687
|
+
const back = [];
|
|
688
|
+
let bi = k - 1;
|
|
689
|
+
while (bi >= 0 && ops[bi].tag === " " && back.length < context) {
|
|
690
|
+
back.unshift(ops[bi]);
|
|
691
|
+
bi--;
|
|
692
|
+
}
|
|
693
|
+
cur = { aStart: aLine - back.length, bStart: bLine - back.length, lines: [...back] };
|
|
694
|
+
}
|
|
695
|
+
cur.lines.push(op);
|
|
696
|
+
trailingContext = 0;
|
|
697
|
+
} else if (cur) {
|
|
698
|
+
cur.lines.push(op);
|
|
699
|
+
if (++trailingContext >= context * 2) {
|
|
700
|
+
cur.lines.length -= trailingContext - context;
|
|
701
|
+
hunks.push(cur);
|
|
702
|
+
cur = null;
|
|
703
|
+
trailingContext = 0;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
if (op.tag !== "+")
|
|
707
|
+
aLine++;
|
|
708
|
+
if (op.tag !== "-")
|
|
709
|
+
bLine++;
|
|
710
|
+
}
|
|
711
|
+
if (cur) {
|
|
712
|
+
if (trailingContext > context)
|
|
713
|
+
cur.lines.length -= trailingContext - context;
|
|
714
|
+
hunks.push(cur);
|
|
715
|
+
}
|
|
716
|
+
const out = [c.bold(`--- ${oldLabel}`), c.bold(`+++ ${newLabel}`)];
|
|
717
|
+
for (const h of hunks) {
|
|
718
|
+
const aCount = h.lines.filter((o) => o.tag !== "+").length;
|
|
719
|
+
const bCount = h.lines.filter((o) => o.tag !== "-").length;
|
|
720
|
+
out.push(c.cyan(`@@ -${h.aStart + 1},${aCount} +${h.bStart + 1},${bCount} @@`));
|
|
721
|
+
for (const o of h.lines) {
|
|
722
|
+
if (o.tag === "+")
|
|
723
|
+
out.push(c.green("+" + o.line));
|
|
724
|
+
else if (o.tag === "-")
|
|
725
|
+
out.push(c.red("-" + o.line));
|
|
726
|
+
else
|
|
727
|
+
out.push(c.dim(" " + o.line));
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
return out.join(`
|
|
731
|
+
`);
|
|
732
|
+
};
|
|
733
|
+
|
|
734
|
+
// src/commands/diff.ts
|
|
735
|
+
var diff = async (args) => {
|
|
736
|
+
const root = findProjectRoot(process.cwd());
|
|
737
|
+
const manifest = readManifest(root);
|
|
738
|
+
const config = loadConfig();
|
|
739
|
+
const apiUrl = requireApiUrl(config);
|
|
740
|
+
const token = requireToken(config);
|
|
741
|
+
info(c.dim("Comparing with the server…"));
|
|
742
|
+
const project = await getProject(apiUrl, token, manifest.uuid);
|
|
743
|
+
const scan = scanProject(root, manifest);
|
|
744
|
+
let entries = computeServerDiff(project, scan);
|
|
745
|
+
if (args._.length > 0) {
|
|
746
|
+
const keys = args._.map(toKey);
|
|
747
|
+
entries = entries.filter((e) => keys.some((k) => e.path === k || e.path.startsWith(k + "/")));
|
|
748
|
+
}
|
|
749
|
+
if (entries.length === 0) {
|
|
750
|
+
info(c.dim("No differences between local and server."));
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
if (flagBool(args, "name-only")) {
|
|
754
|
+
for (const e of entries)
|
|
755
|
+
log(statusLabel(e) + e.path + (e.binary ? c.dim(" (binary)") : ""));
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
for (const e of entries) {
|
|
759
|
+
if (e.binary) {
|
|
760
|
+
log(statusLabel(e) + e.path + c.dim(" (binary — content not shown)"));
|
|
761
|
+
continue;
|
|
762
|
+
}
|
|
763
|
+
const d = unifiedDiff(e.serverText ?? "", e.localText ?? "", `server${e.path}`, `local${e.path}`);
|
|
764
|
+
if (d)
|
|
765
|
+
log(`
|
|
766
|
+
` + d);
|
|
767
|
+
}
|
|
768
|
+
};
|
|
769
|
+
var statusLabel = (e) => e.status === "added" ? c.green(" added ") : e.status === "removed" ? c.red(" removed ") : c.yellow(" modified ");
|
|
770
|
+
|
|
368
771
|
// src/browserAuth.ts
|
|
369
772
|
import { createServer } from "node:http";
|
|
370
773
|
import { spawn } from "node:child_process";
|
|
@@ -455,101 +858,12 @@ var login = async (args) => {
|
|
|
455
858
|
};
|
|
456
859
|
|
|
457
860
|
// src/commands/preview.ts
|
|
458
|
-
import { writeFileSync as
|
|
861
|
+
import { writeFileSync as writeFileSync6 } from "node:fs";
|
|
459
862
|
import { resolve as resolve2 } from "node:path";
|
|
460
863
|
|
|
461
864
|
// src/compile/collect.ts
|
|
462
|
-
import { readFileSync as
|
|
463
|
-
import { basename } from "node:path";
|
|
464
|
-
|
|
465
|
-
// src/localFiles.ts
|
|
466
|
-
import { readdirSync as readdirSync2, readFileSync as readFileSync3 } from "node:fs";
|
|
467
|
-
import { join as join5, relative } from "node:path";
|
|
468
|
-
var SKIP_DIRS = new Set([".lecodes", ".git", "node_modules"]);
|
|
469
|
-
var SKIP_FILES = new Set(["tsconfig.json", "jsconfig.json", ".gitignore", ".DS_Store"]);
|
|
470
|
-
var TEXT_EXTS = new Set([
|
|
471
|
-
"ts",
|
|
472
|
-
"tsx",
|
|
473
|
-
"js",
|
|
474
|
-
"jsx",
|
|
475
|
-
"mjs",
|
|
476
|
-
"cjs",
|
|
477
|
-
"json",
|
|
478
|
-
"svg",
|
|
479
|
-
"glsl",
|
|
480
|
-
"vert",
|
|
481
|
-
"frag",
|
|
482
|
-
"txt",
|
|
483
|
-
"md",
|
|
484
|
-
"css",
|
|
485
|
-
"html",
|
|
486
|
-
"xml",
|
|
487
|
-
"yml",
|
|
488
|
-
"yaml",
|
|
489
|
-
"csv"
|
|
490
|
-
]);
|
|
491
|
-
var ext = (name) => {
|
|
492
|
-
const i = name.lastIndexOf(".");
|
|
493
|
-
return i < 0 ? "" : name.slice(i + 1).toLowerCase();
|
|
494
|
-
};
|
|
495
|
-
var classifyKind = (name) => {
|
|
496
|
-
const e = ext(name);
|
|
497
|
-
if (e === "mat")
|
|
498
|
-
return "shader";
|
|
499
|
-
if (e === "scene")
|
|
500
|
-
return "scene";
|
|
501
|
-
return TEXT_EXTS.has(e) ? "text" : "resource";
|
|
502
|
-
};
|
|
503
|
-
var isTextKind = (kind) => kind !== "resource";
|
|
504
|
-
var scanLocal = (root) => {
|
|
505
|
-
const files = [];
|
|
506
|
-
const dirs = [];
|
|
507
|
-
const walk = (dir) => {
|
|
508
|
-
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
509
|
-
if (entry.isDirectory()) {
|
|
510
|
-
if (SKIP_DIRS.has(entry.name))
|
|
511
|
-
continue;
|
|
512
|
-
const abs = join5(dir, entry.name);
|
|
513
|
-
dirs.push(toKey(relative(root, abs)));
|
|
514
|
-
walk(abs);
|
|
515
|
-
} else if (entry.isFile()) {
|
|
516
|
-
if (SKIP_FILES.has(entry.name))
|
|
517
|
-
continue;
|
|
518
|
-
const abs = join5(dir, entry.name);
|
|
519
|
-
const bytes = readFileSync3(abs);
|
|
520
|
-
files.push({ path: toKey(relative(root, abs)), absPath: abs, sha: sha256(bytes) });
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
};
|
|
524
|
-
walk(root);
|
|
525
|
-
return { files, dirs };
|
|
526
|
-
};
|
|
527
|
-
var diffLocal = (manifest, scan) => {
|
|
528
|
-
const added = [];
|
|
529
|
-
const modified = [];
|
|
530
|
-
const localByPath = new Map(scan.files.map((f) => [f.path, f]));
|
|
531
|
-
for (const file of scan.files) {
|
|
532
|
-
const known = manifest.files[file.path];
|
|
533
|
-
if (!known)
|
|
534
|
-
added.push(file);
|
|
535
|
-
else if (known.sha !== file.sha)
|
|
536
|
-
modified.push(file);
|
|
537
|
-
}
|
|
538
|
-
const deleted = Object.keys(manifest.files).filter((path) => !localByPath.has(path));
|
|
539
|
-
const moves = new Map;
|
|
540
|
-
const claimed = new Set;
|
|
541
|
-
for (const file of added) {
|
|
542
|
-
const from = deleted.find((path) => !claimed.has(path) && manifest.files[path].sha === file.sha);
|
|
543
|
-
if (from) {
|
|
544
|
-
moves.set(file.path, from);
|
|
545
|
-
claimed.add(from);
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
return { added, modified, deleted, moves };
|
|
549
|
-
};
|
|
550
|
-
var hasChanges = (diff) => diff.added.length + diff.modified.length + diff.deleted.length > 0;
|
|
551
|
-
|
|
552
|
-
// src/compile/collect.ts
|
|
865
|
+
import { readFileSync as readFileSync6 } from "node:fs";
|
|
866
|
+
import { basename as basename2 } from "node:path";
|
|
553
867
|
var collectEntries = (root, manifest, project) => {
|
|
554
868
|
const pathById = buildPaths(project.assets);
|
|
555
869
|
const serverByPath = new Map;
|
|
@@ -561,7 +875,7 @@ var collectEntries = (root, manifest, project) => {
|
|
|
561
875
|
const entries = [];
|
|
562
876
|
const warnings = [];
|
|
563
877
|
for (const file of scanLocal(root).files) {
|
|
564
|
-
const type = manifest.files[file.path]?.type ?? classifyKind(
|
|
878
|
+
const type = manifest.files[file.path]?.type ?? classifyKind(basename2(file.path));
|
|
565
879
|
if (type === "resource") {
|
|
566
880
|
const src = serverByPath.get(file.path)?.fileSrc;
|
|
567
881
|
if (!src)
|
|
@@ -573,7 +887,7 @@ var collectEntries = (root, manifest, project) => {
|
|
|
573
887
|
warnings.push(`No compiled shader for ${file.path} — push it to compile the shader.`);
|
|
574
888
|
entries.push({ path: file.path, type, shaders });
|
|
575
889
|
} else {
|
|
576
|
-
entries.push({ path: file.path, type, text:
|
|
890
|
+
entries.push({ path: file.path, type, text: readFileSync6(file.absPath, "utf8") });
|
|
577
891
|
}
|
|
578
892
|
}
|
|
579
893
|
return { entries, warnings };
|
|
@@ -581,8 +895,8 @@ var collectEntries = (root, manifest, project) => {
|
|
|
581
895
|
|
|
582
896
|
// src/compile/bundler.ts
|
|
583
897
|
import * as esbuild from "esbuild";
|
|
584
|
-
import { existsSync as
|
|
585
|
-
import { dirname as dirname3, join as
|
|
898
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
899
|
+
import { dirname as dirname3, join as join7 } from "node:path";
|
|
586
900
|
import { fileURLToPath } from "node:url";
|
|
587
901
|
|
|
588
902
|
// src/compile/assetMacro.ts
|
|
@@ -625,32 +939,32 @@ import * as vec4 from "gl-matrix/vec4";
|
|
|
625
939
|
`;
|
|
626
940
|
var resolveRuntime = () => {
|
|
627
941
|
const here = dirname3(fileURLToPath(import.meta.url));
|
|
628
|
-
const vendored =
|
|
629
|
-
if (
|
|
630
|
-
const w =
|
|
942
|
+
const vendored = join7(here, "..", "runtime");
|
|
943
|
+
if (existsSync4(join7(vendored, "worker", "creator.js"))) {
|
|
944
|
+
const w = join7(vendored, "worker");
|
|
631
945
|
return {
|
|
632
946
|
alias: {
|
|
633
|
-
creator:
|
|
634
|
-
ui:
|
|
635
|
-
animate:
|
|
636
|
-
utils:
|
|
637
|
-
"gl-matrix":
|
|
947
|
+
creator: join7(w, "creator.js"),
|
|
948
|
+
ui: join7(w, "ui.js"),
|
|
949
|
+
animate: join7(w, "animate.js"),
|
|
950
|
+
utils: join7(w, "utils.js"),
|
|
951
|
+
"gl-matrix": join7(vendored, "gl-matrix")
|
|
638
952
|
},
|
|
639
|
-
inject: [
|
|
953
|
+
inject: [join7(w, "globals.js")]
|
|
640
954
|
};
|
|
641
955
|
}
|
|
642
|
-
const repoRoot =
|
|
643
|
-
const ws =
|
|
644
|
-
if (
|
|
956
|
+
const repoRoot = join7(here, "..", "..", "..", "..");
|
|
957
|
+
const ws = join7(repoRoot, "packages", "worker", "src");
|
|
958
|
+
if (existsSync4(join7(ws, "index.ts"))) {
|
|
645
959
|
return {
|
|
646
960
|
alias: {
|
|
647
|
-
creator:
|
|
648
|
-
ui:
|
|
649
|
-
animate:
|
|
650
|
-
utils:
|
|
651
|
-
"gl-matrix":
|
|
961
|
+
creator: join7(ws, "index.ts"),
|
|
962
|
+
ui: join7(ws, "ui.ts"),
|
|
963
|
+
animate: join7(ws, "animate.ts"),
|
|
964
|
+
utils: join7(ws, "utils.ts"),
|
|
965
|
+
"gl-matrix": join7(repoRoot, "packages", "backend", "src", "assets", "dependencies", "gl-matrix")
|
|
652
966
|
},
|
|
653
|
-
inject: [
|
|
967
|
+
inject: [join7(ws, "globals.ts")]
|
|
654
968
|
};
|
|
655
969
|
}
|
|
656
970
|
throw new CliError("Could not locate the compile runtime — try reinstalling lecodes-cli.");
|
|
@@ -833,8 +1147,10 @@ var preview = async (args) => {
|
|
|
833
1147
|
for (const w of warnings)
|
|
834
1148
|
warnErr(w);
|
|
835
1149
|
const out = flagStr(args, "o", "out");
|
|
836
|
-
if (out) {
|
|
837
|
-
|
|
1150
|
+
if (out && isNullSink(out)) {
|
|
1151
|
+
note(`Discarded output (${out})`);
|
|
1152
|
+
} else if (out) {
|
|
1153
|
+
writeFileSync6(resolve2(out), js);
|
|
838
1154
|
note(`Wrote ${out} (${js.length} bytes)`);
|
|
839
1155
|
} else {
|
|
840
1156
|
process.stdout.write(js);
|
|
@@ -846,9 +1162,37 @@ var preview = async (args) => {
|
|
|
846
1162
|
};
|
|
847
1163
|
|
|
848
1164
|
// src/commands/render.ts
|
|
849
|
-
import { writeFileSync as
|
|
1165
|
+
import { writeFileSync as writeFileSync7 } from "node:fs";
|
|
850
1166
|
import { resolve as resolve3 } from "node:path";
|
|
1167
|
+
var DEVICE_SAFE_AREA = {
|
|
1168
|
+
none: [0, 0, 0, 0],
|
|
1169
|
+
iphone: [47, 0, 34, 0],
|
|
1170
|
+
android: [24, 0, 24, 0]
|
|
1171
|
+
};
|
|
1172
|
+
var resolveSafeArea = (args) => {
|
|
1173
|
+
const explicit = flagStr(args, "safe-area");
|
|
1174
|
+
if (explicit) {
|
|
1175
|
+
const n = explicit.split(",").map((s) => Number(s.trim()));
|
|
1176
|
+
if (n.length === 4 && n.every((v) => Number.isFinite(v)))
|
|
1177
|
+
return n;
|
|
1178
|
+
throw new CliError(`--safe-area expects "top,right,bottom,left" (4 numbers), got "${explicit}".`);
|
|
1179
|
+
}
|
|
1180
|
+
const device = flagStr(args, "device");
|
|
1181
|
+
if (device) {
|
|
1182
|
+
const preset = DEVICE_SAFE_AREA[device];
|
|
1183
|
+
if (!preset)
|
|
1184
|
+
throw new CliError(`Unknown --device "${device}". Options: ${Object.keys(DEVICE_SAFE_AREA).join(", ")}.`);
|
|
1185
|
+
return preset;
|
|
1186
|
+
}
|
|
1187
|
+
return;
|
|
1188
|
+
};
|
|
851
1189
|
var render = async (args) => {
|
|
1190
|
+
const timeoutMs = Number(flagStr(args, "timeout")) || 15000;
|
|
1191
|
+
const killer = setTimeout(() => {
|
|
1192
|
+
warnErr(`Render timed out after ${timeoutMs}ms (try --settle / --wait-network, or raise --timeout).`);
|
|
1193
|
+
process.exit(1);
|
|
1194
|
+
}, timeoutMs);
|
|
1195
|
+
killer.unref?.();
|
|
852
1196
|
const root = findProjectRoot(process.cwd());
|
|
853
1197
|
const manifest = readManifest(root);
|
|
854
1198
|
const config = loadConfig();
|
|
@@ -871,54 +1215,81 @@ var render = async (args) => {
|
|
|
871
1215
|
}
|
|
872
1216
|
let renderer;
|
|
873
1217
|
try {
|
|
874
|
-
renderer = await import("lecodes-
|
|
1218
|
+
renderer = await import("lecodes-renderer/headless");
|
|
875
1219
|
} catch {
|
|
876
|
-
throw new CliError("The renderer isn't installed. Run: npm i -D lecodes-
|
|
1220
|
+
throw new CliError("The renderer isn't installed. Run: npm i -D lecodes-renderer");
|
|
877
1221
|
}
|
|
878
1222
|
const width = Number(flagStr(args, "width")) || 390;
|
|
879
1223
|
const height = Number(flagStr(args, "height")) || 844;
|
|
1224
|
+
const settleStr = flagStr(args, "settle");
|
|
1225
|
+
const timeStr = flagStr(args, "time");
|
|
1226
|
+
const onConsole = flagBool(args, "logs") ? (level, text2) => logErr(`${c.dim(`[${level}]`)} ${text2}`) : undefined;
|
|
1227
|
+
const timing = {
|
|
1228
|
+
settleMs: settleStr !== undefined ? Number(settleStr) : undefined,
|
|
1229
|
+
waitNetwork: flagBool(args, "wait-network"),
|
|
1230
|
+
timeMs: timeStr !== undefined ? Number(timeStr) : undefined,
|
|
1231
|
+
afterAnimations: !flagBool(args, "no-after-animations"),
|
|
1232
|
+
safeArea: resolveSafeArea(args),
|
|
1233
|
+
onConsole
|
|
1234
|
+
};
|
|
1235
|
+
const done = (flush) => flush(() => {
|
|
1236
|
+
clearTimeout(killer);
|
|
1237
|
+
process.exit(0);
|
|
1238
|
+
});
|
|
880
1239
|
const pngFlag = flagStr(args, "png") ?? (flagBool(args, "png") ? "screenshot.png" : undefined);
|
|
881
1240
|
if (pngFlag !== undefined) {
|
|
882
1241
|
note("Rendering PNG…");
|
|
883
1242
|
const { png, warnings: pngWarnings } = await renderer.renderToPng(js, {
|
|
884
1243
|
width,
|
|
885
1244
|
height,
|
|
886
|
-
scale: Number(flagStr(args, "scale")) || 2
|
|
1245
|
+
scale: Number(flagStr(args, "scale")) || 2,
|
|
1246
|
+
...timing
|
|
887
1247
|
});
|
|
888
|
-
const pngPath = resolve3(pngFlag);
|
|
889
|
-
writeFileSync6(pngPath, png);
|
|
890
1248
|
for (const w of [...warnings, ...pngWarnings])
|
|
891
1249
|
warnErr(w);
|
|
892
|
-
|
|
893
|
-
|
|
1250
|
+
if (isNullSink(pngFlag)) {
|
|
1251
|
+
note(`Discarded PNG (${pngFlag})`);
|
|
1252
|
+
} else {
|
|
1253
|
+
writeFileSync7(resolve3(pngFlag), png);
|
|
1254
|
+
note(`Wrote ${pngFlag} (${png.length} bytes, ${width}x${height})`);
|
|
1255
|
+
}
|
|
1256
|
+
return done((cb) => cb());
|
|
894
1257
|
}
|
|
895
1258
|
note("Rendering…");
|
|
896
|
-
const result = await renderer.renderToJson(js, { width, height });
|
|
1259
|
+
const result = await renderer.renderToJson(js, { width, height, ...timing });
|
|
897
1260
|
for (const w of [...warnings, ...result.warnings])
|
|
898
1261
|
warnErr(w);
|
|
899
1262
|
const out = flagStr(args, "o", "out");
|
|
1263
|
+
if (out && isNullSink(out)) {
|
|
1264
|
+
note(`Discarded output (${out})`);
|
|
1265
|
+
return done((cb) => cb());
|
|
1266
|
+
}
|
|
900
1267
|
if (out) {
|
|
901
|
-
|
|
1268
|
+
writeFileSync7(resolve3(out), result.text);
|
|
902
1269
|
note(`Wrote ${out} (${result.text.length} bytes)`);
|
|
903
|
-
|
|
904
|
-
process.stdout.write(result.text);
|
|
905
|
-
if (!result.text.endsWith(`
|
|
906
|
-
`))
|
|
907
|
-
process.stdout.write(`
|
|
908
|
-
`);
|
|
1270
|
+
return done((cb) => cb());
|
|
909
1271
|
}
|
|
1272
|
+
const text = result.text.endsWith(`
|
|
1273
|
+
`) ? result.text : result.text + `
|
|
1274
|
+
`;
|
|
1275
|
+
return done((cb) => process.stdout.write(text, cb));
|
|
910
1276
|
};
|
|
911
1277
|
|
|
912
1278
|
// src/commands/pull.ts
|
|
913
|
-
import { existsSync as
|
|
914
|
-
import { join as
|
|
1279
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync6, rmSync, writeFileSync as writeFileSync8 } from "node:fs";
|
|
1280
|
+
import { dirname as dirname4, join as join8 } from "node:path";
|
|
915
1281
|
var pull = async (args) => {
|
|
916
1282
|
const root = findProjectRoot(process.cwd());
|
|
917
1283
|
const manifest = readManifest(root);
|
|
918
1284
|
const config = loadConfig();
|
|
919
1285
|
const apiUrl = requireApiUrl(config);
|
|
920
1286
|
const token = requireToken(config);
|
|
921
|
-
if (
|
|
1287
|
+
if (args._.length > 0) {
|
|
1288
|
+
const project2 = await getProject(apiUrl, token, manifest.uuid);
|
|
1289
|
+
await pullPaths(root, manifest, project2, args._.map(toKey), apiUrl, token);
|
|
1290
|
+
return;
|
|
1291
|
+
}
|
|
1292
|
+
if (!flagBool(args, "force") && hasChanges(diffLocal(manifest, scanProject(root, manifest)))) {
|
|
922
1293
|
throw new CliError("You have local changes. Push them first, or run `lecodes pull --force` to discard them.");
|
|
923
1294
|
}
|
|
924
1295
|
info(c.dim("Fetching project…"));
|
|
@@ -926,8 +1297,8 @@ var pull = async (args) => {
|
|
|
926
1297
|
const remotePaths = new Set([...buildPaths(project.assets).entries()].filter(([id]) => project.assets.find((a) => a.id === id)?.type !== "folder").map(([, path]) => path));
|
|
927
1298
|
for (const path of Object.keys(manifest.files)) {
|
|
928
1299
|
if (!remotePaths.has(path)) {
|
|
929
|
-
const abs =
|
|
930
|
-
if (
|
|
1300
|
+
const abs = join8(root, ...path.replace(/^\//, "").split("/"));
|
|
1301
|
+
if (existsSync5(abs))
|
|
931
1302
|
rmSync(abs);
|
|
932
1303
|
}
|
|
933
1304
|
}
|
|
@@ -941,42 +1312,110 @@ var pull = async (args) => {
|
|
|
941
1312
|
writeManifest(root, { ...manifest, name: project.name, baseHeadId: commits.headId, files });
|
|
942
1313
|
success(`Pulled latest into ${c.bold(manifest.name)}.`);
|
|
943
1314
|
};
|
|
1315
|
+
var kindOf2 = (type) => type === "file" ? "resource" : type;
|
|
1316
|
+
var pullPaths = async (root, manifest, project, paths, apiUrl, token) => {
|
|
1317
|
+
const pathById = buildPaths(project.assets);
|
|
1318
|
+
const byPath = new Map;
|
|
1319
|
+
for (const asset of project.assets)
|
|
1320
|
+
byPath.set(pathById.get(asset.id), asset);
|
|
1321
|
+
const targets = [];
|
|
1322
|
+
for (const path of paths) {
|
|
1323
|
+
const asset = byPath.get(path);
|
|
1324
|
+
if (!asset)
|
|
1325
|
+
throw new CliError(`Not found on the server: ${path}`);
|
|
1326
|
+
if (asset.type === "folder") {
|
|
1327
|
+
const prefix = path + "/";
|
|
1328
|
+
const under = project.assets.filter((a) => a.type !== "folder" && pathById.get(a.id).startsWith(prefix));
|
|
1329
|
+
if (under.length === 0)
|
|
1330
|
+
throw new CliError(`Folder is empty on the server: ${path}`);
|
|
1331
|
+
targets.push(...under);
|
|
1332
|
+
} else {
|
|
1333
|
+
targets.push(asset);
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
const files = { ...manifest.files };
|
|
1337
|
+
for (const asset of targets) {
|
|
1338
|
+
const key = pathById.get(asset.id);
|
|
1339
|
+
const abs = join8(root, ...key.replace(/^\//, "").split("/"));
|
|
1340
|
+
mkdirSync6(dirname4(abs), { recursive: true });
|
|
1341
|
+
let sha;
|
|
1342
|
+
if (asset.type === "file") {
|
|
1343
|
+
if (!asset.file?.src)
|
|
1344
|
+
continue;
|
|
1345
|
+
const bytes = await downloadBlob(apiUrl, asset.file.src);
|
|
1346
|
+
writeFileSync8(abs, bytes);
|
|
1347
|
+
sha = sha256(bytes);
|
|
1348
|
+
} else {
|
|
1349
|
+
const text = asset.text ?? "";
|
|
1350
|
+
writeFileSync8(abs, text);
|
|
1351
|
+
sha = sha256(text);
|
|
1352
|
+
}
|
|
1353
|
+
files[key] = { id: asset.id, type: kindOf2(asset.type), sha };
|
|
1354
|
+
info(c.green(` pulled ${key}`));
|
|
1355
|
+
}
|
|
1356
|
+
writeManifest(root, { ...manifest, files });
|
|
1357
|
+
success(`Pulled ${targets.length} file${targets.length === 1 ? "" : "s"} from the server.`);
|
|
1358
|
+
info(c.dim("Local edits to other files are untouched. Run `lecodes diff` to review, then `lecodes push --force`."));
|
|
1359
|
+
};
|
|
944
1360
|
|
|
945
1361
|
// src/commands/push.ts
|
|
946
|
-
import { basename as
|
|
947
|
-
import { readFileSync as
|
|
1362
|
+
import { basename as basename3 } from "node:path";
|
|
1363
|
+
import { readFileSync as readFileSync7 } from "node:fs";
|
|
948
1364
|
var push = async (args) => {
|
|
949
1365
|
const root = findProjectRoot(process.cwd());
|
|
950
1366
|
const manifest = readManifest(root);
|
|
951
1367
|
const config = loadConfig();
|
|
952
1368
|
const apiUrl = requireApiUrl(config);
|
|
953
1369
|
const token = requireToken(config);
|
|
954
|
-
const
|
|
955
|
-
const
|
|
956
|
-
|
|
957
|
-
|
|
1370
|
+
const publish = flagBool(args, "publish");
|
|
1371
|
+
const scan = scanProject(root, manifest);
|
|
1372
|
+
const diff2 = diffLocal(manifest, scan);
|
|
1373
|
+
if (!hasChanges(diff2)) {
|
|
1374
|
+
if (publish) {
|
|
1375
|
+
await publishProject(apiUrl, token, manifest.uuid);
|
|
1376
|
+
success("No file changes — published the current project state.");
|
|
1377
|
+
} else {
|
|
1378
|
+
info(c.dim("Working tree clean — nothing to push."));
|
|
1379
|
+
}
|
|
958
1380
|
return;
|
|
959
1381
|
}
|
|
960
|
-
|
|
1382
|
+
const force = flagBool(args, "force");
|
|
1383
|
+
if (!force && manifest.baseHeadId !== null) {
|
|
961
1384
|
const commits = await getCommits(apiUrl, token, manifest.uuid);
|
|
962
1385
|
if (commits.headId !== manifest.baseHeadId) {
|
|
963
|
-
throw new CliError("The project changed on the server since your last sync. Run `lecodes pull`
|
|
1386
|
+
throw new CliError("The project changed on the server since your last sync. Run `lecodes diff` to compare, `lecodes pull` to take the server's version, or `lecodes push --force` to overwrite.");
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
if (force) {
|
|
1390
|
+
const project = await getProject(apiUrl, token, manifest.uuid);
|
|
1391
|
+
const serverDiff = computeServerDiff(project, scan);
|
|
1392
|
+
if (serverDiff.length) {
|
|
1393
|
+
info(c.yellow(`--force will overwrite the server. ${serverDiff.length} file${serverDiff.length === 1 ? "" : "s"} change:`));
|
|
1394
|
+
const label = { added: c.green(" create "), removed: c.red(" delete "), modified: c.yellow(" update ") };
|
|
1395
|
+
for (const e of serverDiff)
|
|
1396
|
+
log(label[e.status] + e.path + (e.binary ? c.dim(" (binary)") : ""));
|
|
1397
|
+
if (process.stdin.isTTY && !flagBool(args, "y", "yes")) {
|
|
1398
|
+
if (!await confirm("Overwrite the server with your local copy?", false)) {
|
|
1399
|
+
info(c.dim("Aborted."));
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
964
1403
|
}
|
|
965
1404
|
}
|
|
966
1405
|
const message = (flagStr(args, "m", "message") ?? await prompt("Checkpoint message")) || "Update from lecodes CLI";
|
|
967
|
-
const sourceFor = (path) => manifest.files[path] ?? (
|
|
1406
|
+
const sourceFor = (path) => manifest.files[path] ?? (diff2.moves.has(path) ? manifest.files[diff2.moves.get(path)] : undefined);
|
|
968
1407
|
const files = [];
|
|
969
1408
|
let uploaded = 0;
|
|
970
1409
|
for (const file of scan.files) {
|
|
971
1410
|
const source = sourceFor(file.path);
|
|
972
|
-
const kind = source?.type ?? classifyKind(
|
|
1411
|
+
const kind = source?.type ?? classifyKind(basename3(file.path));
|
|
973
1412
|
const entry = { path: file.path, type: kind };
|
|
974
1413
|
if (source)
|
|
975
1414
|
entry.assetId = source.id;
|
|
976
1415
|
if (isTextKind(kind)) {
|
|
977
|
-
entry.text =
|
|
1416
|
+
entry.text = readFileSync7(file.absPath, "utf8");
|
|
978
1417
|
} else if (!source || source.sha !== file.sha) {
|
|
979
|
-
const blob = await uploadBlob(apiUrl, token, manifest.uuid,
|
|
1418
|
+
const blob = await uploadBlob(apiUrl, token, manifest.uuid, basename3(file.path), readFileSync7(file.absPath));
|
|
980
1419
|
entry.blobFileId = blob.id;
|
|
981
1420
|
uploaded++;
|
|
982
1421
|
}
|
|
@@ -985,9 +1424,11 @@ var push = async (args) => {
|
|
|
985
1424
|
const folders = scan.dirs.map((path) => ({ path }));
|
|
986
1425
|
if (uploaded > 0)
|
|
987
1426
|
info(c.dim(`Uploaded ${uploaded} binary file${uploaded === 1 ? "" : "s"}.`));
|
|
988
|
-
const res = await sync(apiUrl, token, manifest.uuid, { message, files, folders });
|
|
1427
|
+
const res = await sync(apiUrl, token, manifest.uuid, { message, files, folders, publish });
|
|
989
1428
|
if (!res.created) {
|
|
990
1429
|
info(c.dim("Server reported no changes."));
|
|
1430
|
+
if (publish)
|
|
1431
|
+
reportPublish(res);
|
|
991
1432
|
return;
|
|
992
1433
|
}
|
|
993
1434
|
const shaByPath = new Map(scan.files.map((f) => [f.path, f.sha]));
|
|
@@ -997,37 +1438,49 @@ var push = async (args) => {
|
|
|
997
1438
|
}
|
|
998
1439
|
writeManifest(root, { ...manifest, baseHeadId: res.headId, files: newFiles });
|
|
999
1440
|
const counts = [
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1441
|
+
diff2.added.length - diff2.moves.size && `${diff2.added.length - diff2.moves.size} added`,
|
|
1442
|
+
diff2.modified.length && `${diff2.modified.length} modified`,
|
|
1443
|
+
diff2.moves.size && `${diff2.moves.size} moved`,
|
|
1444
|
+
diff2.deleted.length - diff2.moves.size && `${diff2.deleted.length - diff2.moves.size} deleted`
|
|
1004
1445
|
].filter(Boolean).join(", ");
|
|
1005
1446
|
success(`Pushed checkpoint${res.seq ? ` #${res.seq}` : ""}${counts ? ` (${counts})` : ""}: ${message}`);
|
|
1447
|
+
if (publish)
|
|
1448
|
+
reportPublish(res);
|
|
1449
|
+
};
|
|
1450
|
+
var reportPublish = (res) => {
|
|
1451
|
+
if (res.published)
|
|
1452
|
+
success("Published — the project is now live.");
|
|
1453
|
+
else
|
|
1454
|
+
warn(`Push landed, but publish failed: ${res.publishError ?? "unknown error"}`);
|
|
1006
1455
|
};
|
|
1007
1456
|
|
|
1008
1457
|
// src/commands/status.ts
|
|
1009
1458
|
var status = async (_args) => {
|
|
1010
1459
|
const root = findProjectRoot(process.cwd());
|
|
1011
1460
|
const manifest = readManifest(root);
|
|
1012
|
-
const
|
|
1461
|
+
const scan = scanProject(root, manifest);
|
|
1462
|
+
const diff2 = diffLocal(manifest, scan);
|
|
1013
1463
|
info(`Project ${c.bold(manifest.name)} (${manifest.uuid})`);
|
|
1014
|
-
|
|
1015
|
-
|
|
1464
|
+
const ignoredNote = scan.ignored.length ? c.dim(` (${scan.ignored.length} ignored)`) : "";
|
|
1465
|
+
if (!hasChanges(diff2)) {
|
|
1466
|
+
info(c.dim("Working tree clean — nothing to push.") + ignoredNote);
|
|
1016
1467
|
return;
|
|
1017
1468
|
}
|
|
1018
|
-
for (const f of
|
|
1019
|
-
if (!
|
|
1469
|
+
for (const f of diff2.added) {
|
|
1470
|
+
if (!diff2.moves.has(f.path))
|
|
1020
1471
|
log(c.green(` added ${f.path}`));
|
|
1021
1472
|
}
|
|
1022
|
-
for (const f of
|
|
1473
|
+
for (const f of diff2.modified)
|
|
1023
1474
|
log(c.yellow(` modified ${f.path}`));
|
|
1024
|
-
for (const [to, from] of
|
|
1475
|
+
for (const [to, from] of diff2.moves)
|
|
1025
1476
|
log(c.cyan(` moved ${from} → ${to}`));
|
|
1026
|
-
const movedFrom = new Set(
|
|
1027
|
-
for (const path of
|
|
1477
|
+
const movedFrom = new Set(diff2.moves.values());
|
|
1478
|
+
for (const path of diff2.deleted) {
|
|
1028
1479
|
if (!movedFrom.has(path))
|
|
1029
1480
|
log(c.red(` deleted ${path}`));
|
|
1030
1481
|
}
|
|
1482
|
+
if (scan.ignored.length)
|
|
1483
|
+
info(c.dim(` ${scan.ignored.length} ignored (.lecodesignore)`));
|
|
1031
1484
|
};
|
|
1032
1485
|
|
|
1033
1486
|
// src/index.ts
|
|
@@ -1043,27 +1496,44 @@ ${c.bold("Commands:")}
|
|
|
1043
1496
|
clone <uuid|url> [dir] Download a project's files + types into a new folder
|
|
1044
1497
|
--no-types skip the .d.ts / tsconfig generation
|
|
1045
1498
|
status Show local changes vs the last sync
|
|
1046
|
-
|
|
1499
|
+
diff [path…] Show a unified diff of local vs the current server state
|
|
1500
|
+
--name-only list changed paths + status only
|
|
1501
|
+
pull [path…] Overwrite local files with the project's current state
|
|
1502
|
+
(with path… : pull only those files/folders, keep other edits)
|
|
1047
1503
|
--force discard uncommitted local changes
|
|
1048
1504
|
push Push local changes as one new checkpoint
|
|
1049
1505
|
-m, --message checkpoint message
|
|
1050
|
-
--
|
|
1506
|
+
--publish also deploy the project live (compile) with the push
|
|
1507
|
+
--force push even if the server moved on (shows + confirms
|
|
1508
|
+
which server files get overwritten)
|
|
1509
|
+
-y, --yes skip the --force overwrite confirmation
|
|
1051
1510
|
preview Compile the local project to JS (prints to stdout)
|
|
1052
1511
|
-o, --out <file> write to a file instead of stdout
|
|
1053
1512
|
--entry <path> override the entrypoint
|
|
1054
1513
|
--no-header omit the metadata header
|
|
1055
1514
|
--public-url <url> asset URL prefix (default: the API origin)
|
|
1056
1515
|
render Render the local UI project to a semantic JSON description
|
|
1057
|
-
(needs the optional 'lecodes-
|
|
1516
|
+
(needs the optional 'lecodes-renderer' package; UI projects only)
|
|
1058
1517
|
-o, --out <file> write JSON to a file instead of stdout
|
|
1059
1518
|
--png [file] rasterize a PNG (default screenshot.png;
|
|
1060
1519
|
needs @napi-rs/canvas)
|
|
1061
1520
|
--scale <n> PNG device-pixel scale (default 2)
|
|
1062
1521
|
--width/--height viewport size (default 390x844)
|
|
1063
1522
|
--entry <path> override the entrypoint
|
|
1523
|
+
--settle <ms> wait for promises/timers/fetch before snapshot
|
|
1524
|
+
--wait-network let data fetches hit the real network
|
|
1525
|
+
--logs forward the app's console output (to stderr)
|
|
1064
1526
|
|
|
1065
1527
|
Config lives in ~/.lecodes/config.json (override with LECODES_API / LECODES_TOKEN).`;
|
|
1066
|
-
var commands = { login, clone, status, pull, push, preview, render };
|
|
1528
|
+
var commands = { login, clone, status, diff, pull, push, preview, render };
|
|
1529
|
+
var readVersion = () => {
|
|
1530
|
+
try {
|
|
1531
|
+
const here = dirname5(fileURLToPath2(import.meta.url));
|
|
1532
|
+
return JSON.parse(readFileSync8(join9(here, "..", "package.json"), "utf8")).version ?? "unknown";
|
|
1533
|
+
} catch {
|
|
1534
|
+
return "unknown";
|
|
1535
|
+
}
|
|
1536
|
+
};
|
|
1067
1537
|
var main = async () => {
|
|
1068
1538
|
const argv = process.argv.slice(2);
|
|
1069
1539
|
const command = argv[0];
|
|
@@ -1071,6 +1541,10 @@ var main = async () => {
|
|
|
1071
1541
|
log(HELP);
|
|
1072
1542
|
return;
|
|
1073
1543
|
}
|
|
1544
|
+
if (command === "--version" || command === "-v" || command === "version") {
|
|
1545
|
+
log(readVersion());
|
|
1546
|
+
return;
|
|
1547
|
+
}
|
|
1074
1548
|
const run = commands[command];
|
|
1075
1549
|
if (!run) {
|
|
1076
1550
|
log(c.red(`Unknown command: ${command}`));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lecodes-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Clone, edit and push LeCodes projects from your machine.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
},
|
|
22
22
|
"scripts": {
|
|
23
23
|
"vendor-runtime": "bun scripts/vendor-runtime.ts",
|
|
24
|
-
"build": "bun scripts/vendor-runtime.ts && bun build src/index.ts --target=node --outfile dist/index.js --external esbuild --external lecodes-
|
|
24
|
+
"build": "bun scripts/vendor-runtime.ts && bun build src/index.ts --target=node --outfile dist/index.js --external esbuild --external lecodes-renderer --banner=\"#!/usr/bin/env node\"",
|
|
25
25
|
"typecheck": "tsc --noEmit",
|
|
26
26
|
"dev": "bun run src/index.ts",
|
|
27
27
|
"prepublishOnly": "bun run build"
|
|
@@ -30,14 +30,14 @@
|
|
|
30
30
|
"esbuild": "^0.27.3"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
|
-
"lecodes-
|
|
33
|
+
"lecodes-renderer": "^0.1.1"
|
|
34
34
|
},
|
|
35
35
|
"peerDependenciesMeta": {
|
|
36
|
-
"lecodes-
|
|
36
|
+
"lecodes-renderer": {
|
|
37
37
|
"optional": true
|
|
38
38
|
}
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"lecodes-
|
|
41
|
+
"lecodes-renderer": "0.1.1"
|
|
42
42
|
}
|
|
43
43
|
}
|
package/runtime/worker/ui.js
CHANGED
|
@@ -713,8 +713,8 @@ var Router = /* @__PURE__ */ Object.freeze({
|
|
|
713
713
|
push(screen) {
|
|
714
714
|
_creatorUI.routerPush(screen);
|
|
715
715
|
},
|
|
716
|
-
replace(screen) {
|
|
717
|
-
_creatorUI.routerReplace(screen);
|
|
716
|
+
replace(screen, opts) {
|
|
717
|
+
_creatorUI.routerReplace(screen, opts?.transition ?? "fade");
|
|
718
718
|
},
|
|
719
719
|
pop(to) {
|
|
720
720
|
_creatorUI.routerPop(to ?? -1);
|