lecodes-cli 0.1.3 → 0.1.6
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 +740 -267
- package/package.json +4 -3
- package/runtime/inject.js +4629 -0
- package/runtime/gl-matrix/common.js +0 -54
- package/runtime/gl-matrix/header.js +0 -12
- package/runtime/gl-matrix/index.js +0 -11
- package/runtime/gl-matrix/mat2.js +0 -425
- package/runtime/gl-matrix/mat2d.js +0 -476
- package/runtime/gl-matrix/mat3.js +0 -765
- package/runtime/gl-matrix/mat4.js +0 -1947
- package/runtime/gl-matrix/quat.js +0 -737
- package/runtime/gl-matrix/quat2.js +0 -822
- package/runtime/gl-matrix/vec2.js +0 -620
- package/runtime/gl-matrix/vec3.js +0 -791
- package/runtime/gl-matrix/vec4.js +0 -652
- package/runtime/worker/animate.js +0 -304
- package/runtime/worker/chunk-4ZN32GYS.js +0 -34
- package/runtime/worker/chunk-G5ZA3LKB.js +0 -52
- package/runtime/worker/chunk-MI3UXE5H.js +0 -1395
- package/runtime/worker/creator.js +0 -930
- package/runtime/worker/globals.js +0 -208
- package/runtime/worker/ui.js +0 -763
- package/runtime/worker/utils.js +0 -16
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 dirname7, join as join11 } from "node:path";
|
|
8
|
+
import { fileURLToPath as fileURLToPath3 } 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++) {
|
|
@@ -146,6 +160,7 @@ var apiRequest = async (apiUrl, path, opts = {}) => {
|
|
|
146
160
|
};
|
|
147
161
|
var getAccount = (apiUrl, token) => apiRequest(apiUrl, "/account", { token });
|
|
148
162
|
var getProject = (apiUrl, token, uuid) => apiRequest(apiUrl, `/projects/${uuid}`, { token });
|
|
163
|
+
var createProject = (apiUrl, token, name, teamUuid) => apiRequest(apiUrl, "/projects", { token, body: { name, teamUuid } });
|
|
149
164
|
var getCommits = (apiUrl, token, uuid) => apiRequest(apiUrl, `/projects/${uuid}/commits`, { token });
|
|
150
165
|
var getTypes = (apiUrl) => apiRequest(apiUrl, "/types");
|
|
151
166
|
var uploadBlob = (apiUrl, token, uuid, name, bytes) => {
|
|
@@ -154,6 +169,7 @@ var uploadBlob = (apiUrl, token, uuid, name, bytes) => {
|
|
|
154
169
|
return apiRequest(apiUrl, `/projects/${uuid}/blobs`, { token, body: form });
|
|
155
170
|
};
|
|
156
171
|
var sync = (apiUrl, token, uuid, payload) => apiRequest(apiUrl, `/projects/${uuid}/sync`, { token, body: payload });
|
|
172
|
+
var publishProject = (apiUrl, token, uuid) => apiRequest(apiUrl, `/projects/${uuid}/publish`, { token, body: {} });
|
|
157
173
|
var downloadBlob = async (apiUrl, src) => {
|
|
158
174
|
const resp = await fetch(`${apiUrl}${src}`);
|
|
159
175
|
if (!resp.ok)
|
|
@@ -267,38 +283,8 @@ var materializeAssets = async (root, project, apiUrl, token, onProgress) => {
|
|
|
267
283
|
};
|
|
268
284
|
|
|
269
285
|
// src/types.ts
|
|
270
|
-
import { join as join4 } from "node:path";
|
|
286
|
+
import { dirname as dirname3, join as join4 } from "node:path";
|
|
271
287
|
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "node:fs";
|
|
272
|
-
var wrapGlobal = (text) => {
|
|
273
|
-
const marker = text.indexOf("declare global");
|
|
274
|
-
if (marker < 0)
|
|
275
|
-
return text;
|
|
276
|
-
const importLines = [];
|
|
277
|
-
const bodyLines = [];
|
|
278
|
-
for (const line of text.split(`
|
|
279
|
-
`)) {
|
|
280
|
-
if (/^\s*import\s.*\sfrom\s/.test(line))
|
|
281
|
-
importLines.push(line);
|
|
282
|
-
else
|
|
283
|
-
bodyLines.push(line);
|
|
284
|
-
}
|
|
285
|
-
const rest = bodyLines.join(`
|
|
286
|
-
`);
|
|
287
|
-
const open = rest.indexOf("{", rest.indexOf("declare global"));
|
|
288
|
-
const close = rest.lastIndexOf("}");
|
|
289
|
-
const preamble = rest.slice(0, rest.indexOf("declare global"));
|
|
290
|
-
const inner = rest.slice(open + 1, close);
|
|
291
|
-
const stripDeclare = (s) => s.replace(/(^|\n)([ \t]*)declare\s+/g, "$1$2");
|
|
292
|
-
const merged = stripDeclare(preamble) + `
|
|
293
|
-
` + stripDeclare(inner);
|
|
294
|
-
return `${importLines.join(`
|
|
295
|
-
`)}
|
|
296
|
-
|
|
297
|
-
declare global {
|
|
298
|
-
${merged}
|
|
299
|
-
}
|
|
300
|
-
`;
|
|
301
|
-
};
|
|
302
288
|
var TSCONFIG = {
|
|
303
289
|
compilerOptions: {
|
|
304
290
|
target: "ES2020",
|
|
@@ -318,16 +304,106 @@ var TSCONFIG = {
|
|
|
318
304
|
var materializeTypes = async (root, apiUrl) => {
|
|
319
305
|
const bundle = await getTypes(apiUrl);
|
|
320
306
|
const typesDir = join4(root, LECODES_DIR, "types");
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
writeFileSync4(
|
|
307
|
+
for (const [path, text] of Object.entries(bundle)) {
|
|
308
|
+
const dest = join4(typesDir, path);
|
|
309
|
+
mkdirSync4(dirname3(dest), { recursive: true });
|
|
310
|
+
writeFileSync4(dest, text);
|
|
325
311
|
}
|
|
326
312
|
};
|
|
327
313
|
var writeTsconfig = (root) => {
|
|
328
314
|
writeFileSync4(join4(root, "tsconfig.json"), JSON.stringify(TSCONFIG, null, 2));
|
|
329
315
|
};
|
|
330
316
|
|
|
317
|
+
// src/ignore.ts
|
|
318
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync5 } from "node:fs";
|
|
319
|
+
import { join as join5 } from "node:path";
|
|
320
|
+
var BUILTIN_PATTERNS = ["node_modules/", ".git/", "_*", "*.log", ".DS_Store"];
|
|
321
|
+
var DEFAULT_IGNORE_FILE = `# Files matched here are NOT pushed to LeCodes.
|
|
322
|
+
# Only untracked files are affected — assets already in the project stay tracked
|
|
323
|
+
# even if they match a pattern. Built-in defaults (node_modules, _*, *.log) always apply.
|
|
324
|
+
|
|
325
|
+
# Render / preview outputs
|
|
326
|
+
screenshot.png
|
|
327
|
+
shots/
|
|
328
|
+
*.png
|
|
329
|
+
|
|
330
|
+
# Local notes & dumps (uncomment to ignore)
|
|
331
|
+
# *.json
|
|
332
|
+
# notes.md
|
|
333
|
+
`;
|
|
334
|
+
var translate = (glob) => {
|
|
335
|
+
let re = "";
|
|
336
|
+
for (let i = 0;i < glob.length; i++) {
|
|
337
|
+
const ch = glob[i];
|
|
338
|
+
if (ch === "*") {
|
|
339
|
+
if (glob[i + 1] === "*") {
|
|
340
|
+
re += ".*";
|
|
341
|
+
i++;
|
|
342
|
+
if (glob[i + 1] === "/")
|
|
343
|
+
i++;
|
|
344
|
+
} else
|
|
345
|
+
re += "[^/]*";
|
|
346
|
+
} else if (ch === "?")
|
|
347
|
+
re += "[^/]";
|
|
348
|
+
else if (ch === "/")
|
|
349
|
+
re += "/";
|
|
350
|
+
else if ("\\^$.|+()[]{}".includes(ch))
|
|
351
|
+
re += "\\" + ch;
|
|
352
|
+
else
|
|
353
|
+
re += ch;
|
|
354
|
+
}
|
|
355
|
+
return re;
|
|
356
|
+
};
|
|
357
|
+
var compile = (raw) => {
|
|
358
|
+
let p = raw.trim();
|
|
359
|
+
if (!p || p.startsWith("#"))
|
|
360
|
+
return null;
|
|
361
|
+
let negate = false;
|
|
362
|
+
if (p.startsWith("!")) {
|
|
363
|
+
negate = true;
|
|
364
|
+
p = p.slice(1);
|
|
365
|
+
}
|
|
366
|
+
let dirOnly = false;
|
|
367
|
+
if (p.endsWith("/")) {
|
|
368
|
+
dirOnly = true;
|
|
369
|
+
p = p.slice(0, -1);
|
|
370
|
+
}
|
|
371
|
+
const anchored = p.startsWith("/") || p.includes("/");
|
|
372
|
+
if (p.startsWith("/"))
|
|
373
|
+
p = p.slice(1);
|
|
374
|
+
if (!p)
|
|
375
|
+
return null;
|
|
376
|
+
const body = translate(p);
|
|
377
|
+
const re = anchored ? new RegExp("^/" + body + "(/.*)?$") : new RegExp("(^|/)" + body + "(/.*)?$");
|
|
378
|
+
return { re, negate, dirOnly };
|
|
379
|
+
};
|
|
380
|
+
var buildMatcher = (rules) => (key, isDir) => {
|
|
381
|
+
let ignored = false;
|
|
382
|
+
for (const r of rules) {
|
|
383
|
+
if (r.dirOnly && !isDir)
|
|
384
|
+
continue;
|
|
385
|
+
if (r.re.test(key))
|
|
386
|
+
ignored = !r.negate;
|
|
387
|
+
}
|
|
388
|
+
return ignored;
|
|
389
|
+
};
|
|
390
|
+
var loadIgnore = (root) => {
|
|
391
|
+
const lines = [...BUILTIN_PATTERNS];
|
|
392
|
+
const file = join5(root, ".lecodesignore");
|
|
393
|
+
if (existsSync2(file)) {
|
|
394
|
+
try {
|
|
395
|
+
lines.push(...readFileSync3(file, "utf8").split(/\r?\n/));
|
|
396
|
+
} catch {}
|
|
397
|
+
}
|
|
398
|
+
const rules = lines.map(compile).filter((r) => r !== null);
|
|
399
|
+
return buildMatcher(rules);
|
|
400
|
+
};
|
|
401
|
+
var writeDefaultIgnore = (root) => {
|
|
402
|
+
const file = join5(root, ".lecodesignore");
|
|
403
|
+
if (!existsSync2(file))
|
|
404
|
+
writeFileSync5(file, DEFAULT_IGNORE_FILE);
|
|
405
|
+
};
|
|
406
|
+
|
|
331
407
|
// src/commands/clone.ts
|
|
332
408
|
var parseUuid = (input) => {
|
|
333
409
|
if (!input)
|
|
@@ -347,7 +423,7 @@ var clone = async (args) => {
|
|
|
347
423
|
const project = await getProject(apiUrl, token, uuid);
|
|
348
424
|
const dir = args._[1] ?? slug(project.name);
|
|
349
425
|
const root = resolve(dir);
|
|
350
|
-
if (
|
|
426
|
+
if (existsSync3(root) && readdirSync(root).length > 0) {
|
|
351
427
|
throw new CliError(`Directory "${dir}" already exists and is not empty.`);
|
|
352
428
|
}
|
|
353
429
|
mkdirSync5(root, { recursive: true });
|
|
@@ -359,12 +435,348 @@ var clone = async (args) => {
|
|
|
359
435
|
await materializeTypes(root, apiUrl);
|
|
360
436
|
writeTsconfig(root);
|
|
361
437
|
}
|
|
438
|
+
writeDefaultIgnore(root);
|
|
362
439
|
const commits = await getCommits(apiUrl, token, uuid);
|
|
363
440
|
writeManifest(root, { uuid, apiUrl, name: project.name, baseHeadId: commits.headId, files });
|
|
364
441
|
success(`Cloned ${c.bold(project.name)} into ${c.bold(dir)}/ (${Object.keys(files).length} files)`);
|
|
365
442
|
info(c.dim(` cd ${dir} — edit, then \`lecodes push -m "your message"\``));
|
|
366
443
|
};
|
|
367
444
|
|
|
445
|
+
// src/commands/create.ts
|
|
446
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync6, readdirSync as readdirSync2, writeFileSync as writeFileSync6 } from "node:fs";
|
|
447
|
+
import { join as join6, resolve as resolve2 } from "node:path";
|
|
448
|
+
var slug2 = (name) => name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "project";
|
|
449
|
+
var STARTER = `// Welcome to le.codes — no imports needed, the whole SDK is available as globals.
|
|
450
|
+
const scene = new Scene2D({ background: '#10131a' })
|
|
451
|
+
scene.open()
|
|
452
|
+
|
|
453
|
+
setLoop((dt) => {
|
|
454
|
+
// your per-frame logic — dt is seconds since the previous frame
|
|
455
|
+
})
|
|
456
|
+
`;
|
|
457
|
+
var create = async (args) => {
|
|
458
|
+
const config = loadConfig();
|
|
459
|
+
const apiUrl = requireApiUrl(config);
|
|
460
|
+
const token = requireToken(config);
|
|
461
|
+
const name = args._[0];
|
|
462
|
+
if (!name)
|
|
463
|
+
throw new CliError("Usage: lecodes create <name> [directory]");
|
|
464
|
+
const dir = args._[1] ?? slug2(name);
|
|
465
|
+
const root = resolve2(dir);
|
|
466
|
+
if (existsSync4(root) && readdirSync2(root).length > 0) {
|
|
467
|
+
throw new CliError(`Directory "${dir}" already exists and is not empty.`);
|
|
468
|
+
}
|
|
469
|
+
mkdirSync6(root, { recursive: true });
|
|
470
|
+
info(c.dim(`Creating project ${c.bold(name)}…`));
|
|
471
|
+
const { uuid } = await createProject(apiUrl, token, name, flagStr(args, "team"));
|
|
472
|
+
info(c.dim("Fetching type definitions…"));
|
|
473
|
+
await materializeTypes(root, apiUrl);
|
|
474
|
+
writeTsconfig(root);
|
|
475
|
+
writeDefaultIgnore(root);
|
|
476
|
+
writeFileSync6(join6(root, "main.ts"), STARTER);
|
|
477
|
+
const commits = await getCommits(apiUrl, token, uuid);
|
|
478
|
+
writeManifest(root, { uuid, apiUrl, name, baseHeadId: commits.headId, files: {} });
|
|
479
|
+
success(`Created ${c.bold(name)} in ${c.bold(dir)}/`);
|
|
480
|
+
info(c.dim(` cd ${dir} — edit main.ts, then \`lecodes push -m "first commit"\` to save it`));
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
// src/localFiles.ts
|
|
484
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync4 } from "node:fs";
|
|
485
|
+
import { join as join7, relative } from "node:path";
|
|
486
|
+
var SKIP_DIRS = new Set([".lecodes", ".git", "node_modules"]);
|
|
487
|
+
var SKIP_FILES = new Set(["tsconfig.json", "jsconfig.json", ".gitignore", ".lecodesignore", ".DS_Store"]);
|
|
488
|
+
var TEXT_EXTS = new Set([
|
|
489
|
+
"ts",
|
|
490
|
+
"tsx",
|
|
491
|
+
"js",
|
|
492
|
+
"jsx",
|
|
493
|
+
"mjs",
|
|
494
|
+
"cjs",
|
|
495
|
+
"json",
|
|
496
|
+
"svg",
|
|
497
|
+
"glsl",
|
|
498
|
+
"vert",
|
|
499
|
+
"frag",
|
|
500
|
+
"txt",
|
|
501
|
+
"md",
|
|
502
|
+
"css",
|
|
503
|
+
"html",
|
|
504
|
+
"xml",
|
|
505
|
+
"yml",
|
|
506
|
+
"yaml",
|
|
507
|
+
"csv"
|
|
508
|
+
]);
|
|
509
|
+
var ext = (name) => {
|
|
510
|
+
const i = name.lastIndexOf(".");
|
|
511
|
+
return i < 0 ? "" : name.slice(i + 1).toLowerCase();
|
|
512
|
+
};
|
|
513
|
+
var classifyKind = (name) => {
|
|
514
|
+
const e = ext(name);
|
|
515
|
+
if (e === "mat")
|
|
516
|
+
return "shader";
|
|
517
|
+
if (e === "scene")
|
|
518
|
+
return "scene";
|
|
519
|
+
return TEXT_EXTS.has(e) ? "text" : "resource";
|
|
520
|
+
};
|
|
521
|
+
var isTextKind = (kind) => kind !== "resource";
|
|
522
|
+
var scanLocal = (root, opts = {}) => {
|
|
523
|
+
const { ignore, tracked } = opts;
|
|
524
|
+
const files = [];
|
|
525
|
+
const dirs = [];
|
|
526
|
+
const ignored = [];
|
|
527
|
+
const isTracked = (key) => tracked?.has(key) ?? false;
|
|
528
|
+
const hasTrackedUnder = (key) => {
|
|
529
|
+
if (!tracked)
|
|
530
|
+
return false;
|
|
531
|
+
const prefix = key + "/";
|
|
532
|
+
for (const t of tracked)
|
|
533
|
+
if (t.startsWith(prefix))
|
|
534
|
+
return true;
|
|
535
|
+
return false;
|
|
536
|
+
};
|
|
537
|
+
const walk = (dir, parentIgnored) => {
|
|
538
|
+
for (const entry of readdirSync3(dir, { withFileTypes: true })) {
|
|
539
|
+
const abs = join7(dir, entry.name);
|
|
540
|
+
const key = toKey(relative(root, abs));
|
|
541
|
+
if (entry.isDirectory()) {
|
|
542
|
+
if (SKIP_DIRS.has(entry.name))
|
|
543
|
+
continue;
|
|
544
|
+
const dirIgnored = parentIgnored || !!ignore && ignore(key, true);
|
|
545
|
+
if (dirIgnored && !isTracked(key) && !hasTrackedUnder(key)) {
|
|
546
|
+
ignored.push(key + "/");
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
dirs.push(key);
|
|
550
|
+
walk(abs, dirIgnored);
|
|
551
|
+
} else if (entry.isFile()) {
|
|
552
|
+
if (SKIP_FILES.has(entry.name))
|
|
553
|
+
continue;
|
|
554
|
+
const fileIgnored = parentIgnored || !!ignore && ignore(key, false);
|
|
555
|
+
if (fileIgnored && !isTracked(key)) {
|
|
556
|
+
ignored.push(key);
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
files.push({ path: key, absPath: abs, sha: sha256(readFileSync4(abs)) });
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
};
|
|
563
|
+
walk(root, false);
|
|
564
|
+
return { files, dirs, ignored };
|
|
565
|
+
};
|
|
566
|
+
var scanProject = (root, manifest) => scanLocal(root, { ignore: loadIgnore(root), tracked: new Set(Object.keys(manifest.files)) });
|
|
567
|
+
var diffLocal = (manifest, scan) => {
|
|
568
|
+
const added = [];
|
|
569
|
+
const modified = [];
|
|
570
|
+
const localByPath = new Map(scan.files.map((f) => [f.path, f]));
|
|
571
|
+
for (const file of scan.files) {
|
|
572
|
+
const known = manifest.files[file.path];
|
|
573
|
+
if (!known)
|
|
574
|
+
added.push(file);
|
|
575
|
+
else if (known.sha !== file.sha)
|
|
576
|
+
modified.push(file);
|
|
577
|
+
}
|
|
578
|
+
const deleted = Object.keys(manifest.files).filter((path) => !localByPath.has(path));
|
|
579
|
+
const moves = new Map;
|
|
580
|
+
const claimed = new Set;
|
|
581
|
+
for (const file of added) {
|
|
582
|
+
const from = deleted.find((path) => !claimed.has(path) && manifest.files[path].sha === file.sha);
|
|
583
|
+
if (from) {
|
|
584
|
+
moves.set(file.path, from);
|
|
585
|
+
claimed.add(from);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
return { added, modified, deleted, moves };
|
|
589
|
+
};
|
|
590
|
+
var hasChanges = (diff) => diff.added.length + diff.modified.length + diff.deleted.length > 0;
|
|
591
|
+
|
|
592
|
+
// src/serverDiff.ts
|
|
593
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
594
|
+
import { basename } from "node:path";
|
|
595
|
+
var isTextAsset = (t) => t === "text" || t === "shader" || t === "scene";
|
|
596
|
+
var computeServerDiff = (project, scan) => {
|
|
597
|
+
const pathById = buildPaths(project.assets);
|
|
598
|
+
const serverByPath = new Map;
|
|
599
|
+
for (const asset of project.assets) {
|
|
600
|
+
if (asset.type === "folder")
|
|
601
|
+
continue;
|
|
602
|
+
serverByPath.set(pathById.get(asset.id), asset);
|
|
603
|
+
}
|
|
604
|
+
const localByPath = new Map(scan.files.map((f) => [f.path, f]));
|
|
605
|
+
const entries = [];
|
|
606
|
+
const paths = new Set([...serverByPath.keys(), ...localByPath.keys()]);
|
|
607
|
+
for (const path of [...paths].sort()) {
|
|
608
|
+
const server = serverByPath.get(path);
|
|
609
|
+
const local = localByPath.get(path);
|
|
610
|
+
if (local && !server) {
|
|
611
|
+
const text = isTextKind(classifyKind(basename(path)));
|
|
612
|
+
entries.push({ path, status: "added", binary: !text, serverText: "", localText: text ? readFileSync5(local.absPath, "utf8") : undefined });
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
if (server && !local) {
|
|
616
|
+
const text = isTextAsset(server.type);
|
|
617
|
+
entries.push({ path, status: "removed", binary: !text, serverText: text ? server.text ?? "" : undefined, localText: "" });
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
if (server && local) {
|
|
621
|
+
if (isTextAsset(server.type)) {
|
|
622
|
+
const serverText = server.text ?? "";
|
|
623
|
+
const localText = readFileSync5(local.absPath, "utf8");
|
|
624
|
+
if (serverText !== localText)
|
|
625
|
+
entries.push({ path, status: "modified", binary: false, serverText, localText });
|
|
626
|
+
} else {
|
|
627
|
+
const serverSize = server.file?.size;
|
|
628
|
+
const localSize = Buffer.byteLength(readFileSync5(local.absPath));
|
|
629
|
+
if (serverSize !== undefined && serverSize !== localSize) {
|
|
630
|
+
entries.push({ path, status: "modified", binary: true });
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
return entries;
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
// src/textDiff.ts
|
|
639
|
+
var MAX_LINES = 4000;
|
|
640
|
+
var lcsDiff = (a, b) => {
|
|
641
|
+
const n = a.length, m = b.length;
|
|
642
|
+
const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
|
|
643
|
+
for (let i2 = n - 1;i2 >= 0; i2--) {
|
|
644
|
+
for (let j2 = m - 1;j2 >= 0; j2--) {
|
|
645
|
+
dp[i2][j2] = a[i2] === b[j2] ? dp[i2 + 1][j2 + 1] + 1 : Math.max(dp[i2 + 1][j2], dp[i2][j2 + 1]);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
const ops = [];
|
|
649
|
+
let i = 0, j = 0;
|
|
650
|
+
while (i < n && j < m) {
|
|
651
|
+
if (a[i] === b[j]) {
|
|
652
|
+
ops.push({ tag: " ", line: a[i] });
|
|
653
|
+
i++;
|
|
654
|
+
j++;
|
|
655
|
+
} else if (dp[i + 1][j] >= dp[i][j + 1]) {
|
|
656
|
+
ops.push({ tag: "-", line: a[i] });
|
|
657
|
+
i++;
|
|
658
|
+
} else {
|
|
659
|
+
ops.push({ tag: "+", line: b[j] });
|
|
660
|
+
j++;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
while (i < n)
|
|
664
|
+
ops.push({ tag: "-", line: a[i++] });
|
|
665
|
+
while (j < m)
|
|
666
|
+
ops.push({ tag: "+", line: b[j++] });
|
|
667
|
+
return ops;
|
|
668
|
+
};
|
|
669
|
+
var splitLines = (text) => {
|
|
670
|
+
if (text === "")
|
|
671
|
+
return [];
|
|
672
|
+
const lines = text.split(`
|
|
673
|
+
`);
|
|
674
|
+
if (lines[lines.length - 1] === "")
|
|
675
|
+
lines.pop();
|
|
676
|
+
return lines;
|
|
677
|
+
};
|
|
678
|
+
var unifiedDiff = (oldText, newText, oldLabel, newLabel, context = 3) => {
|
|
679
|
+
if (oldText === newText)
|
|
680
|
+
return "";
|
|
681
|
+
const a = splitLines(oldText);
|
|
682
|
+
const b = splitLines(newText);
|
|
683
|
+
if (a.length + b.length > MAX_LINES) {
|
|
684
|
+
return c.dim(` (large file — ${a.length} → ${b.length} lines, diff omitted)`);
|
|
685
|
+
}
|
|
686
|
+
const ops = lcsDiff(a, b);
|
|
687
|
+
const hunks = [];
|
|
688
|
+
let aLine = 0, bLine = 0;
|
|
689
|
+
let cur = null;
|
|
690
|
+
let trailingContext = 0;
|
|
691
|
+
for (let k = 0;k < ops.length; k++) {
|
|
692
|
+
const op = ops[k];
|
|
693
|
+
const isChange = op.tag !== " ";
|
|
694
|
+
if (isChange) {
|
|
695
|
+
if (!cur) {
|
|
696
|
+
const back = [];
|
|
697
|
+
let bi = k - 1;
|
|
698
|
+
while (bi >= 0 && ops[bi].tag === " " && back.length < context) {
|
|
699
|
+
back.unshift(ops[bi]);
|
|
700
|
+
bi--;
|
|
701
|
+
}
|
|
702
|
+
cur = { aStart: aLine - back.length, bStart: bLine - back.length, lines: [...back] };
|
|
703
|
+
}
|
|
704
|
+
cur.lines.push(op);
|
|
705
|
+
trailingContext = 0;
|
|
706
|
+
} else if (cur) {
|
|
707
|
+
cur.lines.push(op);
|
|
708
|
+
if (++trailingContext >= context * 2) {
|
|
709
|
+
cur.lines.length -= trailingContext - context;
|
|
710
|
+
hunks.push(cur);
|
|
711
|
+
cur = null;
|
|
712
|
+
trailingContext = 0;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
if (op.tag !== "+")
|
|
716
|
+
aLine++;
|
|
717
|
+
if (op.tag !== "-")
|
|
718
|
+
bLine++;
|
|
719
|
+
}
|
|
720
|
+
if (cur) {
|
|
721
|
+
if (trailingContext > context)
|
|
722
|
+
cur.lines.length -= trailingContext - context;
|
|
723
|
+
hunks.push(cur);
|
|
724
|
+
}
|
|
725
|
+
const out = [c.bold(`--- ${oldLabel}`), c.bold(`+++ ${newLabel}`)];
|
|
726
|
+
for (const h of hunks) {
|
|
727
|
+
const aCount = h.lines.filter((o) => o.tag !== "+").length;
|
|
728
|
+
const bCount = h.lines.filter((o) => o.tag !== "-").length;
|
|
729
|
+
out.push(c.cyan(`@@ -${h.aStart + 1},${aCount} +${h.bStart + 1},${bCount} @@`));
|
|
730
|
+
for (const o of h.lines) {
|
|
731
|
+
if (o.tag === "+")
|
|
732
|
+
out.push(c.green("+" + o.line));
|
|
733
|
+
else if (o.tag === "-")
|
|
734
|
+
out.push(c.red("-" + o.line));
|
|
735
|
+
else
|
|
736
|
+
out.push(c.dim(" " + o.line));
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
return out.join(`
|
|
740
|
+
`);
|
|
741
|
+
};
|
|
742
|
+
|
|
743
|
+
// src/commands/diff.ts
|
|
744
|
+
var diff = async (args) => {
|
|
745
|
+
const root = findProjectRoot(process.cwd());
|
|
746
|
+
const manifest = readManifest(root);
|
|
747
|
+
const config = loadConfig();
|
|
748
|
+
const apiUrl = requireApiUrl(config);
|
|
749
|
+
const token = requireToken(config);
|
|
750
|
+
info(c.dim("Comparing with the server…"));
|
|
751
|
+
const project = await getProject(apiUrl, token, manifest.uuid);
|
|
752
|
+
const scan = scanProject(root, manifest);
|
|
753
|
+
let entries = computeServerDiff(project, scan);
|
|
754
|
+
if (args._.length > 0) {
|
|
755
|
+
const keys = args._.map(toKey);
|
|
756
|
+
entries = entries.filter((e) => keys.some((k) => e.path === k || e.path.startsWith(k + "/")));
|
|
757
|
+
}
|
|
758
|
+
if (entries.length === 0) {
|
|
759
|
+
info(c.dim("No differences between local and server."));
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
if (flagBool(args, "name-only")) {
|
|
763
|
+
for (const e of entries)
|
|
764
|
+
log(statusLabel(e) + e.path + (e.binary ? c.dim(" (binary)") : ""));
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
for (const e of entries) {
|
|
768
|
+
if (e.binary) {
|
|
769
|
+
log(statusLabel(e) + e.path + c.dim(" (binary — content not shown)"));
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
const d = unifiedDiff(e.serverText ?? "", e.localText ?? "", `server${e.path}`, `local${e.path}`);
|
|
773
|
+
if (d)
|
|
774
|
+
log(`
|
|
775
|
+
` + d);
|
|
776
|
+
}
|
|
777
|
+
};
|
|
778
|
+
var statusLabel = (e) => e.status === "added" ? c.green(" added ") : e.status === "removed" ? c.red(" removed ") : c.yellow(" modified ");
|
|
779
|
+
|
|
368
780
|
// src/browserAuth.ts
|
|
369
781
|
import { createServer } from "node:http";
|
|
370
782
|
import { spawn } from "node:child_process";
|
|
@@ -401,10 +813,10 @@ var openBrowser = (url) => {
|
|
|
401
813
|
};
|
|
402
814
|
var browserLogin = async (webBase, timeoutMs = 5 * 60 * 1000, open = openBrowser) => {
|
|
403
815
|
const state = randomBytes(16).toString("hex");
|
|
404
|
-
let
|
|
816
|
+
let resolve3;
|
|
405
817
|
let reject;
|
|
406
818
|
const tokenPromise = new Promise((res, rej) => {
|
|
407
|
-
|
|
819
|
+
resolve3 = res;
|
|
408
820
|
reject = rej;
|
|
409
821
|
});
|
|
410
822
|
const server = createServer((req, res) => {
|
|
@@ -422,7 +834,7 @@ var browserLogin = async (webBase, timeoutMs = 5 * 60 * 1000, open = openBrowser
|
|
|
422
834
|
}
|
|
423
835
|
res.writeHead(200, { "content-type": "text/html" });
|
|
424
836
|
res.end(SUCCESS_HTML);
|
|
425
|
-
|
|
837
|
+
resolve3(token);
|
|
426
838
|
});
|
|
427
839
|
await new Promise((res, rej) => {
|
|
428
840
|
server.once("error", rej);
|
|
@@ -455,101 +867,12 @@ var login = async (args) => {
|
|
|
455
867
|
};
|
|
456
868
|
|
|
457
869
|
// src/commands/preview.ts
|
|
458
|
-
import { writeFileSync as
|
|
459
|
-
import { resolve as
|
|
460
|
-
|
|
461
|
-
// src/compile/collect.ts
|
|
462
|
-
import { readFileSync as readFileSync4 } from "node:fs";
|
|
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;
|
|
870
|
+
import { writeFileSync as writeFileSync7 } from "node:fs";
|
|
871
|
+
import { resolve as resolve3 } from "node:path";
|
|
551
872
|
|
|
552
873
|
// src/compile/collect.ts
|
|
874
|
+
import { readFileSync as readFileSync6 } from "node:fs";
|
|
875
|
+
import { basename as basename2 } from "node:path";
|
|
553
876
|
var collectEntries = (root, manifest, project) => {
|
|
554
877
|
const pathById = buildPaths(project.assets);
|
|
555
878
|
const serverByPath = new Map;
|
|
@@ -561,7 +884,7 @@ var collectEntries = (root, manifest, project) => {
|
|
|
561
884
|
const entries = [];
|
|
562
885
|
const warnings = [];
|
|
563
886
|
for (const file of scanLocal(root).files) {
|
|
564
|
-
const type = manifest.files[file.path]?.type ?? classifyKind(
|
|
887
|
+
const type = manifest.files[file.path]?.type ?? classifyKind(basename2(file.path));
|
|
565
888
|
if (type === "resource") {
|
|
566
889
|
const src = serverByPath.get(file.path)?.fileSrc;
|
|
567
890
|
if (!src)
|
|
@@ -573,19 +896,23 @@ var collectEntries = (root, manifest, project) => {
|
|
|
573
896
|
warnings.push(`No compiled shader for ${file.path} — push it to compile the shader.`);
|
|
574
897
|
entries.push({ path: file.path, type, shaders });
|
|
575
898
|
} else {
|
|
576
|
-
entries.push({ path: file.path, type, text:
|
|
899
|
+
entries.push({ path: file.path, type, text: readFileSync6(file.absPath, "utf8") });
|
|
577
900
|
}
|
|
578
901
|
}
|
|
579
902
|
return { entries, warnings };
|
|
580
903
|
};
|
|
581
904
|
|
|
582
|
-
// src/compile/
|
|
905
|
+
// src/compile/projectCompile.ts
|
|
906
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
907
|
+
import { dirname as dirname5, join as join9 } from "node:path";
|
|
908
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
909
|
+
// ../sdk/src/compile/bundler.ts
|
|
583
910
|
import * as esbuild from "esbuild";
|
|
584
|
-
import { existsSync as
|
|
585
|
-
import { dirname as
|
|
911
|
+
import { existsSync as existsSync5 } from "node:fs";
|
|
912
|
+
import { dirname as dirname4, join as join8 } from "node:path";
|
|
586
913
|
import { fileURLToPath } from "node:url";
|
|
587
914
|
|
|
588
|
-
// src/compile/assetMacro.ts
|
|
915
|
+
// ../sdk/src/compile/assetMacro.ts
|
|
589
916
|
var ASSET_RE = /\basset\(\s*(["'])((?:[^"'\\]|\\.)*)\1\s*\)/g;
|
|
590
917
|
var hasAssetMacro = (src) => src.includes("asset(");
|
|
591
918
|
var desugarAssetMacros = (src, filePath) => {
|
|
@@ -610,52 +937,21 @@ var desugarAssetMacros = (src, filePath) => {
|
|
|
610
937
|
` + out;
|
|
611
938
|
};
|
|
612
939
|
|
|
613
|
-
// src/compile/bundler.ts
|
|
614
|
-
var
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
const vendored = join6(here, "..", "runtime");
|
|
629
|
-
if (existsSync3(join6(vendored, "worker", "creator.js"))) {
|
|
630
|
-
const w = join6(vendored, "worker");
|
|
631
|
-
return {
|
|
632
|
-
alias: {
|
|
633
|
-
creator: join6(w, "creator.js"),
|
|
634
|
-
ui: join6(w, "ui.js"),
|
|
635
|
-
animate: join6(w, "animate.js"),
|
|
636
|
-
utils: join6(w, "utils.js"),
|
|
637
|
-
"gl-matrix": join6(vendored, "gl-matrix")
|
|
638
|
-
},
|
|
639
|
-
inject: [join6(w, "globals.js")]
|
|
640
|
-
};
|
|
641
|
-
}
|
|
642
|
-
const repoRoot = join6(here, "..", "..", "..", "..");
|
|
643
|
-
const ws = join6(repoRoot, "packages", "worker", "src");
|
|
644
|
-
if (existsSync3(join6(ws, "index.ts"))) {
|
|
645
|
-
return {
|
|
646
|
-
alias: {
|
|
647
|
-
creator: join6(ws, "index.ts"),
|
|
648
|
-
ui: join6(ws, "ui.ts"),
|
|
649
|
-
animate: join6(ws, "animate.ts"),
|
|
650
|
-
utils: join6(ws, "utils.ts"),
|
|
651
|
-
"gl-matrix": join6(repoRoot, "packages", "backend", "src", "assets", "dependencies", "gl-matrix")
|
|
652
|
-
},
|
|
653
|
-
inject: [join6(ws, "globals.ts")]
|
|
654
|
-
};
|
|
655
|
-
}
|
|
656
|
-
throw new CliError("Could not locate the compile runtime — try reinstalling lecodes-cli.");
|
|
657
|
-
};
|
|
658
|
-
function normalize(path) {
|
|
940
|
+
// ../sdk/src/compile/bundler.ts
|
|
941
|
+
var resolveInject = () => {
|
|
942
|
+
const override = process.env.LECODES_SDK_INJECT;
|
|
943
|
+
if (override && existsSync5(override))
|
|
944
|
+
return override;
|
|
945
|
+
const here = dirname4(fileURLToPath(import.meta.url));
|
|
946
|
+
const src = join8(here, "..", "inject.ts");
|
|
947
|
+
if (existsSync5(src))
|
|
948
|
+
return src;
|
|
949
|
+
const dist = join8(here, "..", "..", "dist", "inject.js");
|
|
950
|
+
if (existsSync5(dist))
|
|
951
|
+
return dist;
|
|
952
|
+
throw new Error("sdk: could not locate the inject entry (LECODES_SDK_INJECT / src/inject.ts / dist/inject.js)");
|
|
953
|
+
};
|
|
954
|
+
var normalize = (path) => {
|
|
659
955
|
const parts = [];
|
|
660
956
|
for (const part of path.split("/")) {
|
|
661
957
|
if (!part || part === ".")
|
|
@@ -666,20 +962,20 @@ function normalize(path) {
|
|
|
666
962
|
parts.push(part);
|
|
667
963
|
}
|
|
668
964
|
return "/" + parts.join("/");
|
|
669
|
-
}
|
|
670
|
-
|
|
965
|
+
};
|
|
966
|
+
var dirOf = (path) => {
|
|
671
967
|
const i = path.lastIndexOf("/");
|
|
672
968
|
return i <= 0 ? "/" : path.slice(0, i);
|
|
673
|
-
}
|
|
969
|
+
};
|
|
674
970
|
var hasExtension = (path) => /\.[a-zA-Z0-9]+$/.test(path);
|
|
675
|
-
|
|
971
|
+
var resolveBase = (importer, specifier) => {
|
|
676
972
|
if (specifier.startsWith("/"))
|
|
677
973
|
return normalize(specifier);
|
|
678
974
|
if (specifier.startsWith("./") || specifier.startsWith("../"))
|
|
679
975
|
return normalize(dirOf(importer) + "/" + specifier);
|
|
680
|
-
throw new Error(`Bare
|
|
681
|
-
}
|
|
682
|
-
|
|
976
|
+
throw new Error(`Bare imports are not allowed — the whole SDK is available as globals (no imports): ${specifier}`);
|
|
977
|
+
};
|
|
978
|
+
var resolveWithExtensions = (files, importer, specifier) => {
|
|
683
979
|
const base = resolveBase(importer, specifier);
|
|
684
980
|
if (hasExtension(base)) {
|
|
685
981
|
if (files.has(base))
|
|
@@ -691,31 +987,26 @@ function resolveWithExtensions(files, importer, specifier) {
|
|
|
691
987
|
return candidate;
|
|
692
988
|
}
|
|
693
989
|
throw new Error(`Module not found: ${specifier} from ${importer}`);
|
|
694
|
-
}
|
|
990
|
+
};
|
|
695
991
|
var getLoader = (path) => path.endsWith(".ts") ? "ts" : "js";
|
|
696
|
-
async
|
|
697
|
-
const
|
|
698
|
-
const files = new Map(Object.entries(inputFiles).map(([k, v]) => [normalize(k), v]));
|
|
992
|
+
var bundleProject = async (entrypoint, files, format = "esm") => {
|
|
993
|
+
const fileMap = new Map(Object.entries(files).map(([k, v]) => [normalize(k), v]));
|
|
699
994
|
const entry = normalize(entrypoint);
|
|
700
|
-
if (!
|
|
701
|
-
throw new
|
|
995
|
+
if (!fileMap.has(entry))
|
|
996
|
+
throw new Error(`Entrypoint not found: ${entry}`);
|
|
702
997
|
const plugin = {
|
|
703
998
|
name: "memfs",
|
|
704
999
|
setup(build2) {
|
|
705
1000
|
build2.onResolve({ filter: /^entry$/ }, () => ({ path: entry, namespace: "mem" }));
|
|
706
1001
|
build2.onResolve({ filter: /.*/, namespace: "mem" }, (args) => {
|
|
707
|
-
const isRelative = args.path.startsWith("./") || args.path.startsWith("../");
|
|
708
|
-
const isAbsolute = args.path.startsWith("/");
|
|
709
|
-
if (!isRelative && !isAbsolute)
|
|
710
|
-
return null;
|
|
711
1002
|
try {
|
|
712
|
-
return { path: resolveWithExtensions(
|
|
1003
|
+
return { path: resolveWithExtensions(fileMap, args.importer || entry, args.path), namespace: "mem" };
|
|
713
1004
|
} catch (error) {
|
|
714
1005
|
return { errors: [{ text: error instanceof Error ? error.message : String(error) }] };
|
|
715
1006
|
}
|
|
716
1007
|
});
|
|
717
1008
|
build2.onLoad({ filter: /.*/, namespace: "mem" }, (args) => {
|
|
718
|
-
const contents =
|
|
1009
|
+
const contents = fileMap.get(args.path);
|
|
719
1010
|
if (contents == null)
|
|
720
1011
|
return { errors: [{ text: `Module not found: ${args.path}` }] };
|
|
721
1012
|
const finalContents = /\.[tj]sx?$/.test(args.path) && hasAssetMacro(contents) ? desugarAssetMacros(contents, args.path) : contents;
|
|
@@ -725,9 +1016,8 @@ async function buildInMemory(entrypoint, inputFiles, format = "esm") {
|
|
|
725
1016
|
};
|
|
726
1017
|
const result = await esbuild.build({
|
|
727
1018
|
stdin: { contents: `import "entry";`, loader: "js", sourcefile: "bootstrap.js" },
|
|
728
|
-
|
|
1019
|
+
inject: [resolveInject()],
|
|
729
1020
|
define: { RAD2DEG: "57.29577951308232", DEG2RAD: "0.017453292519943295" },
|
|
730
|
-
inject: runtime.inject,
|
|
731
1021
|
bundle: true,
|
|
732
1022
|
write: false,
|
|
733
1023
|
format,
|
|
@@ -737,13 +1027,16 @@ async function buildInMemory(entrypoint, inputFiles, format = "esm") {
|
|
|
737
1027
|
});
|
|
738
1028
|
const output = result.outputFiles?.[0];
|
|
739
1029
|
if (!output)
|
|
740
|
-
throw new
|
|
1030
|
+
throw new Error("Compilation produced no output");
|
|
741
1031
|
return output.text;
|
|
742
|
-
}
|
|
1032
|
+
};
|
|
743
1033
|
|
|
744
|
-
// src/compile/
|
|
745
|
-
var buildHeader = (name, code) => {
|
|
1034
|
+
// ../sdk/src/compile/header.ts
|
|
1035
|
+
var buildHeader = (name, code, opts = {}) => {
|
|
746
1036
|
let header = `// name: ${name.slice(0, 20)}
|
|
1037
|
+
`;
|
|
1038
|
+
if (opts.icon)
|
|
1039
|
+
header += `// icon: ${opts.icon}
|
|
747
1040
|
`;
|
|
748
1041
|
header += `// last-updated: ${new Date().toISOString()}
|
|
749
1042
|
`;
|
|
@@ -763,6 +1056,9 @@ var buildHeader = (name, code) => {
|
|
|
763
1056
|
`;
|
|
764
1057
|
else if (code.includes("_creator.createEngine"))
|
|
765
1058
|
header += `// gl: defer
|
|
1059
|
+
`;
|
|
1060
|
+
if (code.includes("_creator2d."))
|
|
1061
|
+
header += `// 2d: enable
|
|
766
1062
|
`;
|
|
767
1063
|
if (code.includes("_creator.createARController"))
|
|
768
1064
|
header += "// ar: enable";
|
|
@@ -770,6 +1066,8 @@ var buildHeader = (name, code) => {
|
|
|
770
1066
|
`;
|
|
771
1067
|
return header;
|
|
772
1068
|
};
|
|
1069
|
+
|
|
1070
|
+
// ../sdk/src/compile/compileProject.ts
|
|
773
1071
|
var detectEntry = (entries) => {
|
|
774
1072
|
let entry = "";
|
|
775
1073
|
let max = -1;
|
|
@@ -787,7 +1085,7 @@ var detectEntry = (entries) => {
|
|
|
787
1085
|
var compileProject = async (opts) => {
|
|
788
1086
|
const entry = opts.entryOverride ?? detectEntry(opts.entries);
|
|
789
1087
|
if (!entry) {
|
|
790
|
-
throw new
|
|
1088
|
+
throw new Error("Couldn't find an entrypoint (a top-level .ts/.js file with no `export`). Pass entryOverride.");
|
|
791
1089
|
}
|
|
792
1090
|
const files = {};
|
|
793
1091
|
for (const e of opts.entries) {
|
|
@@ -805,12 +1103,24 @@ var compileProject = async (opts) => {
|
|
|
805
1103
|
} else if (e.path.toLowerCase().endsWith(".svg")) {
|
|
806
1104
|
files[e.path] = `export default SvgSource(\`${e.text ?? ""}\`)`;
|
|
807
1105
|
} else {
|
|
808
|
-
files[e.path] =
|
|
1106
|
+
files[e.path] = e.text ?? "";
|
|
809
1107
|
}
|
|
810
1108
|
}
|
|
811
|
-
const code = await
|
|
812
|
-
return opts.header === false ? code : buildHeader(opts.name, code) + code;
|
|
1109
|
+
const code = await bundleProject(entry, files, opts.format ?? "esm");
|
|
1110
|
+
return opts.header === false ? code : buildHeader(opts.name, code, { icon: opts.icon }) + code;
|
|
813
1111
|
};
|
|
1112
|
+
// src/compile/projectCompile.ts
|
|
1113
|
+
var vendoredInject = join9(dirname5(fileURLToPath2(import.meta.url)), "..", "runtime", "inject.js");
|
|
1114
|
+
if (existsSync6(vendoredInject))
|
|
1115
|
+
process.env.LECODES_SDK_INJECT = vendoredInject;
|
|
1116
|
+
var compileProject2 = (opts) => compileProject({
|
|
1117
|
+
entries: opts.entries.map((e) => ({ path: e.path, type: e.type, text: e.text, fileSrc: e.fileSrc, shaders: e.shaders })),
|
|
1118
|
+
name: opts.name,
|
|
1119
|
+
publicUrl: opts.publicUrl,
|
|
1120
|
+
entryOverride: opts.entryOverride,
|
|
1121
|
+
header: opts.header,
|
|
1122
|
+
format: opts.format
|
|
1123
|
+
});
|
|
814
1124
|
|
|
815
1125
|
// src/commands/preview.ts
|
|
816
1126
|
var preview = async (args) => {
|
|
@@ -823,7 +1133,7 @@ var preview = async (args) => {
|
|
|
823
1133
|
const project = await getProject(apiUrl, token, manifest.uuid);
|
|
824
1134
|
const { entries, warnings } = collectEntries(root, manifest, project);
|
|
825
1135
|
note("Compiling…");
|
|
826
|
-
const js = await
|
|
1136
|
+
const js = await compileProject2({
|
|
827
1137
|
entries,
|
|
828
1138
|
name: manifest.name,
|
|
829
1139
|
publicUrl: flagStr(args, "public-url") ?? apiUrl,
|
|
@@ -833,8 +1143,10 @@ var preview = async (args) => {
|
|
|
833
1143
|
for (const w of warnings)
|
|
834
1144
|
warnErr(w);
|
|
835
1145
|
const out = flagStr(args, "o", "out");
|
|
836
|
-
if (out) {
|
|
837
|
-
|
|
1146
|
+
if (out && isNullSink(out)) {
|
|
1147
|
+
note(`Discarded output (${out})`);
|
|
1148
|
+
} else if (out) {
|
|
1149
|
+
writeFileSync7(resolve3(out), js);
|
|
838
1150
|
note(`Wrote ${out} (${js.length} bytes)`);
|
|
839
1151
|
} else {
|
|
840
1152
|
process.stdout.write(js);
|
|
@@ -846,9 +1158,37 @@ var preview = async (args) => {
|
|
|
846
1158
|
};
|
|
847
1159
|
|
|
848
1160
|
// src/commands/render.ts
|
|
849
|
-
import { writeFileSync as
|
|
850
|
-
import { resolve as
|
|
1161
|
+
import { writeFileSync as writeFileSync8 } from "node:fs";
|
|
1162
|
+
import { resolve as resolve4 } from "node:path";
|
|
1163
|
+
var DEVICE_SAFE_AREA = {
|
|
1164
|
+
none: [0, 0, 0, 0],
|
|
1165
|
+
iphone: [47, 0, 34, 0],
|
|
1166
|
+
android: [24, 0, 24, 0]
|
|
1167
|
+
};
|
|
1168
|
+
var resolveSafeArea = (args) => {
|
|
1169
|
+
const explicit = flagStr(args, "safe-area");
|
|
1170
|
+
if (explicit) {
|
|
1171
|
+
const n = explicit.split(",").map((s) => Number(s.trim()));
|
|
1172
|
+
if (n.length === 4 && n.every((v) => Number.isFinite(v)))
|
|
1173
|
+
return n;
|
|
1174
|
+
throw new CliError(`--safe-area expects "top,right,bottom,left" (4 numbers), got "${explicit}".`);
|
|
1175
|
+
}
|
|
1176
|
+
const device = flagStr(args, "device");
|
|
1177
|
+
if (device) {
|
|
1178
|
+
const preset = DEVICE_SAFE_AREA[device];
|
|
1179
|
+
if (!preset)
|
|
1180
|
+
throw new CliError(`Unknown --device "${device}". Options: ${Object.keys(DEVICE_SAFE_AREA).join(", ")}.`);
|
|
1181
|
+
return preset;
|
|
1182
|
+
}
|
|
1183
|
+
return;
|
|
1184
|
+
};
|
|
851
1185
|
var render = async (args) => {
|
|
1186
|
+
const timeoutMs = Number(flagStr(args, "timeout")) || 15000;
|
|
1187
|
+
const killer = setTimeout(() => {
|
|
1188
|
+
warnErr(`Render timed out after ${timeoutMs}ms (try --settle / --wait-network, or raise --timeout).`);
|
|
1189
|
+
process.exit(1);
|
|
1190
|
+
}, timeoutMs);
|
|
1191
|
+
killer.unref?.();
|
|
852
1192
|
const root = findProjectRoot(process.cwd());
|
|
853
1193
|
const manifest = readManifest(root);
|
|
854
1194
|
const config = loadConfig();
|
|
@@ -858,7 +1198,7 @@ var render = async (args) => {
|
|
|
858
1198
|
const project = await getProject(apiUrl, token, manifest.uuid);
|
|
859
1199
|
const { entries, warnings } = collectEntries(root, manifest, project);
|
|
860
1200
|
note("Compiling…");
|
|
861
|
-
const js = await
|
|
1201
|
+
const js = await compileProject2({
|
|
862
1202
|
entries,
|
|
863
1203
|
name: manifest.name,
|
|
864
1204
|
publicUrl: flagStr(args, "public-url") ?? apiUrl,
|
|
@@ -877,48 +1217,75 @@ var render = async (args) => {
|
|
|
877
1217
|
}
|
|
878
1218
|
const width = Number(flagStr(args, "width")) || 390;
|
|
879
1219
|
const height = Number(flagStr(args, "height")) || 844;
|
|
1220
|
+
const settleStr = flagStr(args, "settle");
|
|
1221
|
+
const timeStr = flagStr(args, "time");
|
|
1222
|
+
const onConsole = flagBool(args, "logs") ? (level, text2) => logErr(`${c.dim(`[${level}]`)} ${text2}`) : undefined;
|
|
1223
|
+
const timing = {
|
|
1224
|
+
settleMs: settleStr !== undefined ? Number(settleStr) : undefined,
|
|
1225
|
+
waitNetwork: flagBool(args, "wait-network"),
|
|
1226
|
+
timeMs: timeStr !== undefined ? Number(timeStr) : undefined,
|
|
1227
|
+
afterAnimations: !flagBool(args, "no-after-animations"),
|
|
1228
|
+
safeArea: resolveSafeArea(args),
|
|
1229
|
+
onConsole
|
|
1230
|
+
};
|
|
1231
|
+
const done = (flush) => flush(() => {
|
|
1232
|
+
clearTimeout(killer);
|
|
1233
|
+
process.exit(0);
|
|
1234
|
+
});
|
|
880
1235
|
const pngFlag = flagStr(args, "png") ?? (flagBool(args, "png") ? "screenshot.png" : undefined);
|
|
881
1236
|
if (pngFlag !== undefined) {
|
|
882
1237
|
note("Rendering PNG…");
|
|
883
1238
|
const { png, warnings: pngWarnings } = await renderer.renderToPng(js, {
|
|
884
1239
|
width,
|
|
885
1240
|
height,
|
|
886
|
-
scale: Number(flagStr(args, "scale")) || 2
|
|
1241
|
+
scale: Number(flagStr(args, "scale")) || 2,
|
|
1242
|
+
...timing
|
|
887
1243
|
});
|
|
888
|
-
const pngPath = resolve3(pngFlag);
|
|
889
|
-
writeFileSync6(pngPath, png);
|
|
890
1244
|
for (const w of [...warnings, ...pngWarnings])
|
|
891
1245
|
warnErr(w);
|
|
892
|
-
|
|
893
|
-
|
|
1246
|
+
if (isNullSink(pngFlag)) {
|
|
1247
|
+
note(`Discarded PNG (${pngFlag})`);
|
|
1248
|
+
} else {
|
|
1249
|
+
writeFileSync8(resolve4(pngFlag), png);
|
|
1250
|
+
note(`Wrote ${pngFlag} (${png.length} bytes, ${width}x${height})`);
|
|
1251
|
+
}
|
|
1252
|
+
return done((cb) => cb());
|
|
894
1253
|
}
|
|
895
1254
|
note("Rendering…");
|
|
896
|
-
const result = await renderer.renderToJson(js, { width, height });
|
|
1255
|
+
const result = await renderer.renderToJson(js, { width, height, ...timing });
|
|
897
1256
|
for (const w of [...warnings, ...result.warnings])
|
|
898
1257
|
warnErr(w);
|
|
899
1258
|
const out = flagStr(args, "o", "out");
|
|
1259
|
+
if (out && isNullSink(out)) {
|
|
1260
|
+
note(`Discarded output (${out})`);
|
|
1261
|
+
return done((cb) => cb());
|
|
1262
|
+
}
|
|
900
1263
|
if (out) {
|
|
901
|
-
|
|
1264
|
+
writeFileSync8(resolve4(out), result.text);
|
|
902
1265
|
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
|
-
`);
|
|
1266
|
+
return done((cb) => cb());
|
|
909
1267
|
}
|
|
1268
|
+
const text = result.text.endsWith(`
|
|
1269
|
+
`) ? result.text : result.text + `
|
|
1270
|
+
`;
|
|
1271
|
+
return done((cb) => process.stdout.write(text, cb));
|
|
910
1272
|
};
|
|
911
1273
|
|
|
912
1274
|
// src/commands/pull.ts
|
|
913
|
-
import { existsSync as
|
|
914
|
-
import { join as
|
|
1275
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync7, rmSync, writeFileSync as writeFileSync9 } from "node:fs";
|
|
1276
|
+
import { dirname as dirname6, join as join10 } from "node:path";
|
|
915
1277
|
var pull = async (args) => {
|
|
916
1278
|
const root = findProjectRoot(process.cwd());
|
|
917
1279
|
const manifest = readManifest(root);
|
|
918
1280
|
const config = loadConfig();
|
|
919
1281
|
const apiUrl = requireApiUrl(config);
|
|
920
1282
|
const token = requireToken(config);
|
|
921
|
-
if (
|
|
1283
|
+
if (args._.length > 0) {
|
|
1284
|
+
const project2 = await getProject(apiUrl, token, manifest.uuid);
|
|
1285
|
+
await pullPaths(root, manifest, project2, args._.map(toKey), apiUrl, token);
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
if (!flagBool(args, "force") && hasChanges(diffLocal(manifest, scanProject(root, manifest)))) {
|
|
922
1289
|
throw new CliError("You have local changes. Push them first, or run `lecodes pull --force` to discard them.");
|
|
923
1290
|
}
|
|
924
1291
|
info(c.dim("Fetching project…"));
|
|
@@ -926,8 +1293,8 @@ var pull = async (args) => {
|
|
|
926
1293
|
const remotePaths = new Set([...buildPaths(project.assets).entries()].filter(([id]) => project.assets.find((a) => a.id === id)?.type !== "folder").map(([, path]) => path));
|
|
927
1294
|
for (const path of Object.keys(manifest.files)) {
|
|
928
1295
|
if (!remotePaths.has(path)) {
|
|
929
|
-
const abs =
|
|
930
|
-
if (
|
|
1296
|
+
const abs = join10(root, ...path.replace(/^\//, "").split("/"));
|
|
1297
|
+
if (existsSync7(abs))
|
|
931
1298
|
rmSync(abs);
|
|
932
1299
|
}
|
|
933
1300
|
}
|
|
@@ -941,42 +1308,110 @@ var pull = async (args) => {
|
|
|
941
1308
|
writeManifest(root, { ...manifest, name: project.name, baseHeadId: commits.headId, files });
|
|
942
1309
|
success(`Pulled latest into ${c.bold(manifest.name)}.`);
|
|
943
1310
|
};
|
|
1311
|
+
var kindOf2 = (type) => type === "file" ? "resource" : type;
|
|
1312
|
+
var pullPaths = async (root, manifest, project, paths, apiUrl, token) => {
|
|
1313
|
+
const pathById = buildPaths(project.assets);
|
|
1314
|
+
const byPath = new Map;
|
|
1315
|
+
for (const asset of project.assets)
|
|
1316
|
+
byPath.set(pathById.get(asset.id), asset);
|
|
1317
|
+
const targets = [];
|
|
1318
|
+
for (const path of paths) {
|
|
1319
|
+
const asset = byPath.get(path);
|
|
1320
|
+
if (!asset)
|
|
1321
|
+
throw new CliError(`Not found on the server: ${path}`);
|
|
1322
|
+
if (asset.type === "folder") {
|
|
1323
|
+
const prefix = path + "/";
|
|
1324
|
+
const under = project.assets.filter((a) => a.type !== "folder" && pathById.get(a.id).startsWith(prefix));
|
|
1325
|
+
if (under.length === 0)
|
|
1326
|
+
throw new CliError(`Folder is empty on the server: ${path}`);
|
|
1327
|
+
targets.push(...under);
|
|
1328
|
+
} else {
|
|
1329
|
+
targets.push(asset);
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
const files = { ...manifest.files };
|
|
1333
|
+
for (const asset of targets) {
|
|
1334
|
+
const key = pathById.get(asset.id);
|
|
1335
|
+
const abs = join10(root, ...key.replace(/^\//, "").split("/"));
|
|
1336
|
+
mkdirSync7(dirname6(abs), { recursive: true });
|
|
1337
|
+
let sha;
|
|
1338
|
+
if (asset.type === "file") {
|
|
1339
|
+
if (!asset.file?.src)
|
|
1340
|
+
continue;
|
|
1341
|
+
const bytes = await downloadBlob(apiUrl, asset.file.src);
|
|
1342
|
+
writeFileSync9(abs, bytes);
|
|
1343
|
+
sha = sha256(bytes);
|
|
1344
|
+
} else {
|
|
1345
|
+
const text = asset.text ?? "";
|
|
1346
|
+
writeFileSync9(abs, text);
|
|
1347
|
+
sha = sha256(text);
|
|
1348
|
+
}
|
|
1349
|
+
files[key] = { id: asset.id, type: kindOf2(asset.type), sha };
|
|
1350
|
+
info(c.green(` pulled ${key}`));
|
|
1351
|
+
}
|
|
1352
|
+
writeManifest(root, { ...manifest, files });
|
|
1353
|
+
success(`Pulled ${targets.length} file${targets.length === 1 ? "" : "s"} from the server.`);
|
|
1354
|
+
info(c.dim("Local edits to other files are untouched. Run `lecodes diff` to review, then `lecodes push --force`."));
|
|
1355
|
+
};
|
|
944
1356
|
|
|
945
1357
|
// src/commands/push.ts
|
|
946
|
-
import { basename as
|
|
947
|
-
import { readFileSync as
|
|
1358
|
+
import { basename as basename3 } from "node:path";
|
|
1359
|
+
import { readFileSync as readFileSync7 } from "node:fs";
|
|
948
1360
|
var push = async (args) => {
|
|
949
1361
|
const root = findProjectRoot(process.cwd());
|
|
950
1362
|
const manifest = readManifest(root);
|
|
951
1363
|
const config = loadConfig();
|
|
952
1364
|
const apiUrl = requireApiUrl(config);
|
|
953
1365
|
const token = requireToken(config);
|
|
954
|
-
const
|
|
955
|
-
const
|
|
956
|
-
|
|
957
|
-
|
|
1366
|
+
const publish = flagBool(args, "publish");
|
|
1367
|
+
const scan = scanProject(root, manifest);
|
|
1368
|
+
const diff2 = diffLocal(manifest, scan);
|
|
1369
|
+
if (!hasChanges(diff2)) {
|
|
1370
|
+
if (publish) {
|
|
1371
|
+
await publishProject(apiUrl, token, manifest.uuid);
|
|
1372
|
+
success("No file changes — published the current project state.");
|
|
1373
|
+
} else {
|
|
1374
|
+
info(c.dim("Working tree clean — nothing to push."));
|
|
1375
|
+
}
|
|
958
1376
|
return;
|
|
959
1377
|
}
|
|
960
|
-
|
|
1378
|
+
const force = flagBool(args, "force");
|
|
1379
|
+
if (!force && manifest.baseHeadId !== null) {
|
|
961
1380
|
const commits = await getCommits(apiUrl, token, manifest.uuid);
|
|
962
1381
|
if (commits.headId !== manifest.baseHeadId) {
|
|
963
|
-
throw new CliError("The project changed on the server since your last sync. Run `lecodes pull`
|
|
1382
|
+
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.");
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
if (force) {
|
|
1386
|
+
const project = await getProject(apiUrl, token, manifest.uuid);
|
|
1387
|
+
const serverDiff = computeServerDiff(project, scan);
|
|
1388
|
+
if (serverDiff.length) {
|
|
1389
|
+
info(c.yellow(`--force will overwrite the server. ${serverDiff.length} file${serverDiff.length === 1 ? "" : "s"} change:`));
|
|
1390
|
+
const label = { added: c.green(" create "), removed: c.red(" delete "), modified: c.yellow(" update ") };
|
|
1391
|
+
for (const e of serverDiff)
|
|
1392
|
+
log(label[e.status] + e.path + (e.binary ? c.dim(" (binary)") : ""));
|
|
1393
|
+
if (process.stdin.isTTY && !flagBool(args, "y", "yes")) {
|
|
1394
|
+
if (!await confirm("Overwrite the server with your local copy?", false)) {
|
|
1395
|
+
info(c.dim("Aborted."));
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
964
1399
|
}
|
|
965
1400
|
}
|
|
966
1401
|
const message = (flagStr(args, "m", "message") ?? await prompt("Checkpoint message")) || "Update from lecodes CLI";
|
|
967
|
-
const sourceFor = (path) => manifest.files[path] ?? (
|
|
1402
|
+
const sourceFor = (path) => manifest.files[path] ?? (diff2.moves.has(path) ? manifest.files[diff2.moves.get(path)] : undefined);
|
|
968
1403
|
const files = [];
|
|
969
1404
|
let uploaded = 0;
|
|
970
1405
|
for (const file of scan.files) {
|
|
971
1406
|
const source = sourceFor(file.path);
|
|
972
|
-
const kind = source?.type ?? classifyKind(
|
|
1407
|
+
const kind = source?.type ?? classifyKind(basename3(file.path));
|
|
973
1408
|
const entry = { path: file.path, type: kind };
|
|
974
1409
|
if (source)
|
|
975
1410
|
entry.assetId = source.id;
|
|
976
1411
|
if (isTextKind(kind)) {
|
|
977
|
-
entry.text =
|
|
1412
|
+
entry.text = readFileSync7(file.absPath, "utf8");
|
|
978
1413
|
} else if (!source || source.sha !== file.sha) {
|
|
979
|
-
const blob = await uploadBlob(apiUrl, token, manifest.uuid,
|
|
1414
|
+
const blob = await uploadBlob(apiUrl, token, manifest.uuid, basename3(file.path), readFileSync7(file.absPath));
|
|
980
1415
|
entry.blobFileId = blob.id;
|
|
981
1416
|
uploaded++;
|
|
982
1417
|
}
|
|
@@ -985,9 +1420,11 @@ var push = async (args) => {
|
|
|
985
1420
|
const folders = scan.dirs.map((path) => ({ path }));
|
|
986
1421
|
if (uploaded > 0)
|
|
987
1422
|
info(c.dim(`Uploaded ${uploaded} binary file${uploaded === 1 ? "" : "s"}.`));
|
|
988
|
-
const res = await sync(apiUrl, token, manifest.uuid, { message, files, folders });
|
|
1423
|
+
const res = await sync(apiUrl, token, manifest.uuid, { message, files, folders, publish });
|
|
989
1424
|
if (!res.created) {
|
|
990
1425
|
info(c.dim("Server reported no changes."));
|
|
1426
|
+
if (publish)
|
|
1427
|
+
reportPublish(res);
|
|
991
1428
|
return;
|
|
992
1429
|
}
|
|
993
1430
|
const shaByPath = new Map(scan.files.map((f) => [f.path, f.sha]));
|
|
@@ -997,37 +1434,49 @@ var push = async (args) => {
|
|
|
997
1434
|
}
|
|
998
1435
|
writeManifest(root, { ...manifest, baseHeadId: res.headId, files: newFiles });
|
|
999
1436
|
const counts = [
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1437
|
+
diff2.added.length - diff2.moves.size && `${diff2.added.length - diff2.moves.size} added`,
|
|
1438
|
+
diff2.modified.length && `${diff2.modified.length} modified`,
|
|
1439
|
+
diff2.moves.size && `${diff2.moves.size} moved`,
|
|
1440
|
+
diff2.deleted.length - diff2.moves.size && `${diff2.deleted.length - diff2.moves.size} deleted`
|
|
1004
1441
|
].filter(Boolean).join(", ");
|
|
1005
1442
|
success(`Pushed checkpoint${res.seq ? ` #${res.seq}` : ""}${counts ? ` (${counts})` : ""}: ${message}`);
|
|
1443
|
+
if (publish)
|
|
1444
|
+
reportPublish(res);
|
|
1445
|
+
};
|
|
1446
|
+
var reportPublish = (res) => {
|
|
1447
|
+
if (res.published)
|
|
1448
|
+
success("Published — the project is now live.");
|
|
1449
|
+
else
|
|
1450
|
+
warn(`Push landed, but publish failed: ${res.publishError ?? "unknown error"}`);
|
|
1006
1451
|
};
|
|
1007
1452
|
|
|
1008
1453
|
// src/commands/status.ts
|
|
1009
1454
|
var status = async (_args) => {
|
|
1010
1455
|
const root = findProjectRoot(process.cwd());
|
|
1011
1456
|
const manifest = readManifest(root);
|
|
1012
|
-
const
|
|
1457
|
+
const scan = scanProject(root, manifest);
|
|
1458
|
+
const diff2 = diffLocal(manifest, scan);
|
|
1013
1459
|
info(`Project ${c.bold(manifest.name)} (${manifest.uuid})`);
|
|
1014
|
-
|
|
1015
|
-
|
|
1460
|
+
const ignoredNote = scan.ignored.length ? c.dim(` (${scan.ignored.length} ignored)`) : "";
|
|
1461
|
+
if (!hasChanges(diff2)) {
|
|
1462
|
+
info(c.dim("Working tree clean — nothing to push.") + ignoredNote);
|
|
1016
1463
|
return;
|
|
1017
1464
|
}
|
|
1018
|
-
for (const f of
|
|
1019
|
-
if (!
|
|
1465
|
+
for (const f of diff2.added) {
|
|
1466
|
+
if (!diff2.moves.has(f.path))
|
|
1020
1467
|
log(c.green(` added ${f.path}`));
|
|
1021
1468
|
}
|
|
1022
|
-
for (const f of
|
|
1469
|
+
for (const f of diff2.modified)
|
|
1023
1470
|
log(c.yellow(` modified ${f.path}`));
|
|
1024
|
-
for (const [to, from] of
|
|
1471
|
+
for (const [to, from] of diff2.moves)
|
|
1025
1472
|
log(c.cyan(` moved ${from} → ${to}`));
|
|
1026
|
-
const movedFrom = new Set(
|
|
1027
|
-
for (const path of
|
|
1473
|
+
const movedFrom = new Set(diff2.moves.values());
|
|
1474
|
+
for (const path of diff2.deleted) {
|
|
1028
1475
|
if (!movedFrom.has(path))
|
|
1029
1476
|
log(c.red(` deleted ${path}`));
|
|
1030
1477
|
}
|
|
1478
|
+
if (scan.ignored.length)
|
|
1479
|
+
info(c.dim(` ${scan.ignored.length} ignored (.lecodesignore)`));
|
|
1031
1480
|
};
|
|
1032
1481
|
|
|
1033
1482
|
// src/index.ts
|
|
@@ -1040,14 +1489,23 @@ ${c.bold("Commands:")}
|
|
|
1040
1489
|
--token <pat> store a token from the web UI instead
|
|
1041
1490
|
--api <url> API origin (default https://le.codes)
|
|
1042
1491
|
--web <url> app origin for the browser flow
|
|
1492
|
+
create <name> [dir] Create a new project on the server and scaffold it locally
|
|
1493
|
+
(types from the server + tsconfig + a starter main.ts)
|
|
1494
|
+
--team <uuid> create it inside a team
|
|
1043
1495
|
clone <uuid|url> [dir] Download a project's files + types into a new folder
|
|
1044
1496
|
--no-types skip the .d.ts / tsconfig generation
|
|
1045
1497
|
status Show local changes vs the last sync
|
|
1046
|
-
|
|
1498
|
+
diff [path…] Show a unified diff of local vs the current server state
|
|
1499
|
+
--name-only list changed paths + status only
|
|
1500
|
+
pull [path…] Overwrite local files with the project's current state
|
|
1501
|
+
(with path… : pull only those files/folders, keep other edits)
|
|
1047
1502
|
--force discard uncommitted local changes
|
|
1048
1503
|
push Push local changes as one new checkpoint
|
|
1049
1504
|
-m, --message checkpoint message
|
|
1050
|
-
--
|
|
1505
|
+
--publish also deploy the project live (compile) with the push
|
|
1506
|
+
--force push even if the server moved on (shows + confirms
|
|
1507
|
+
which server files get overwritten)
|
|
1508
|
+
-y, --yes skip the --force overwrite confirmation
|
|
1051
1509
|
preview Compile the local project to JS (prints to stdout)
|
|
1052
1510
|
-o, --out <file> write to a file instead of stdout
|
|
1053
1511
|
--entry <path> override the entrypoint
|
|
@@ -1061,9 +1519,20 @@ ${c.bold("Commands:")}
|
|
|
1061
1519
|
--scale <n> PNG device-pixel scale (default 2)
|
|
1062
1520
|
--width/--height viewport size (default 390x844)
|
|
1063
1521
|
--entry <path> override the entrypoint
|
|
1522
|
+
--settle <ms> wait for promises/timers/fetch before snapshot
|
|
1523
|
+
--wait-network let data fetches hit the real network
|
|
1524
|
+
--logs forward the app's console output (to stderr)
|
|
1064
1525
|
|
|
1065
1526
|
Config lives in ~/.lecodes/config.json (override with LECODES_API / LECODES_TOKEN).`;
|
|
1066
|
-
var commands = { login, clone, status, pull, push, preview, render };
|
|
1527
|
+
var commands = { login, create, clone, status, diff, pull, push, preview, render };
|
|
1528
|
+
var readVersion = () => {
|
|
1529
|
+
try {
|
|
1530
|
+
const here = dirname7(fileURLToPath3(import.meta.url));
|
|
1531
|
+
return JSON.parse(readFileSync8(join11(here, "..", "package.json"), "utf8")).version ?? "unknown";
|
|
1532
|
+
} catch {
|
|
1533
|
+
return "unknown";
|
|
1534
|
+
}
|
|
1535
|
+
};
|
|
1067
1536
|
var main = async () => {
|
|
1068
1537
|
const argv = process.argv.slice(2);
|
|
1069
1538
|
const command = argv[0];
|
|
@@ -1071,6 +1540,10 @@ var main = async () => {
|
|
|
1071
1540
|
log(HELP);
|
|
1072
1541
|
return;
|
|
1073
1542
|
}
|
|
1543
|
+
if (command === "--version" || command === "-v" || command === "version") {
|
|
1544
|
+
log(readVersion());
|
|
1545
|
+
return;
|
|
1546
|
+
}
|
|
1074
1547
|
const run = commands[command];
|
|
1075
1548
|
if (!run) {
|
|
1076
1549
|
log(c.red(`Unknown command: ${command}`));
|