lecodes-cli 0.5.3 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -3,12 +3,12 @@ import { createRequire } from "node:module";
3
3
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
4
4
 
5
5
  // src/index.ts
6
- import { readFileSync as readFileSync11 } from "node:fs";
7
- import { dirname as dirname7, join as join12 } from "node:path";
8
- import { fileURLToPath as fileURLToPath3 } from "node:url";
6
+ import { readFileSync as readFileSync13 } from "node:fs";
7
+ import { dirname as dirname8, join as join13 } from "node:path";
8
+ import { fileURLToPath as fileURLToPath4 } from "node:url";
9
9
 
10
10
  // src/commands/clone.ts
11
- import { existsSync as existsSync3, mkdirSync as mkdirSync5, readdirSync } from "node:fs";
11
+ import { existsSync as existsSync4, mkdirSync as mkdirSync5, readdirSync } from "node:fs";
12
12
  import { resolve } from "node:path";
13
13
 
14
14
  // src/util.ts
@@ -286,7 +286,8 @@ var materializeAssets = async (root, project, apiUrl, token, onProgress) => {
286
286
 
287
287
  // src/types.ts
288
288
  import { dirname as dirname3, join as join4 } from "node:path";
289
- import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "node:fs";
289
+ import { fileURLToPath } from "node:url";
290
+ import { existsSync as existsSync2, mkdirSync as mkdirSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "node:fs";
290
291
  var TSCONFIG = {
291
292
  compilerOptions: {
292
293
  target: "ES2020",
@@ -303,8 +304,7 @@ var TSCONFIG = {
303
304
  include: [`${LECODES_DIR}/types/**/*.d.ts`, "**/*.ts", "**/*.js"],
304
305
  exclude: ["node_modules"]
305
306
  };
306
- var materializeTypes = async (root, apiUrl) => {
307
- const bundle = await getTypes(apiUrl);
307
+ var writeTypeBundle = (root, bundle) => {
308
308
  const typesDir = join4(root, LECODES_DIR, "types");
309
309
  for (const [path, text] of Object.entries(bundle)) {
310
310
  const dest = join4(typesDir, path);
@@ -312,12 +312,37 @@ var materializeTypes = async (root, apiUrl) => {
312
312
  writeFileSync4(dest, text);
313
313
  }
314
314
  };
315
+ var materializeTypes = async (root, apiUrl) => {
316
+ writeTypeBundle(root, await getTypes(apiUrl));
317
+ };
318
+ var readLocalTypeBundle = () => {
319
+ const here = dirname3(fileURLToPath(import.meta.url));
320
+ const candidates = [
321
+ join4(here, "..", "runtime", "sdk-types.json"),
322
+ join4(here, "..", "..", "sdk", "dist", "types.json")
323
+ ];
324
+ for (const path of candidates) {
325
+ if (existsSync2(path)) {
326
+ try {
327
+ return JSON.parse(readFileSync3(path, "utf8"));
328
+ } catch {}
329
+ }
330
+ }
331
+ return null;
332
+ };
333
+ var materializeTypesLocal = (root) => {
334
+ const bundle = readLocalTypeBundle();
335
+ if (!bundle)
336
+ return false;
337
+ writeTypeBundle(root, bundle);
338
+ return true;
339
+ };
315
340
  var writeTsconfig = (root) => {
316
341
  writeFileSync4(join4(root, "tsconfig.json"), JSON.stringify(TSCONFIG, null, 2));
317
342
  };
318
343
 
319
344
  // src/ignore.ts
320
- import { existsSync as existsSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync5 } from "node:fs";
345
+ import { existsSync as existsSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "node:fs";
321
346
  import { join as join5 } from "node:path";
322
347
  var BUILTIN_PATTERNS = ["node_modules/", ".git/", "_*", "*.log", ".DS_Store"];
323
348
  var DEFAULT_IGNORE_FILE = `# Files matched here are NOT pushed to LeCodes.
@@ -395,9 +420,9 @@ var buildMatcher = (rules) => (key, isDir) => {
395
420
  var loadIgnore = (root) => {
396
421
  const lines = [...BUILTIN_PATTERNS];
397
422
  const file = join5(root, ".lecodesignore");
398
- if (existsSync2(file)) {
423
+ if (existsSync3(file)) {
399
424
  try {
400
- lines.push(...readFileSync3(file, "utf8").split(/\r?\n/));
425
+ lines.push(...readFileSync4(file, "utf8").split(/\r?\n/));
401
426
  } catch {}
402
427
  }
403
428
  const rules = lines.map(compile).filter((r) => r !== null);
@@ -405,7 +430,7 @@ var loadIgnore = (root) => {
405
430
  };
406
431
  var writeDefaultIgnore = (root) => {
407
432
  const file = join5(root, ".lecodesignore");
408
- if (!existsSync2(file))
433
+ if (!existsSync3(file))
409
434
  writeFileSync5(file, DEFAULT_IGNORE_FILE);
410
435
  };
411
436
 
@@ -428,7 +453,7 @@ var clone = async (args) => {
428
453
  const project = await getProject(apiUrl, token, uuid);
429
454
  const dir = args._[1] ?? slug(project.name);
430
455
  const root = resolve(dir);
431
- if (existsSync3(root) && readdirSync(root).length > 0) {
456
+ if (existsSync4(root) && readdirSync(root).length > 0) {
432
457
  throw new CliError(`Directory "${dir}" already exists and is not empty.`);
433
458
  }
434
459
  mkdirSync5(root, { recursive: true });
@@ -448,7 +473,7 @@ var clone = async (args) => {
448
473
  };
449
474
 
450
475
  // src/commands/create.ts
451
- import { existsSync as existsSync4, mkdirSync as mkdirSync6, readdirSync as readdirSync2, writeFileSync as writeFileSync6 } from "node:fs";
476
+ import { existsSync as existsSync5, mkdirSync as mkdirSync6, readdirSync as readdirSync2, writeFileSync as writeFileSync6 } from "node:fs";
452
477
  import { join as join6, resolve as resolve2 } from "node:path";
453
478
  var slug2 = (name) => name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "project";
454
479
  var STARTER = `// Welcome to le.codes — no imports needed, the whole SDK is available as globals.
@@ -468,7 +493,7 @@ var create = async (args) => {
468
493
  throw new CliError("Usage: lecodes create <name> [directory]");
469
494
  const dir = args._[1] ?? slug2(name);
470
495
  const root = resolve2(dir);
471
- if (existsSync4(root) && readdirSync2(root).length > 0) {
496
+ if (existsSync5(root) && readdirSync2(root).length > 0) {
472
497
  throw new CliError(`Directory "${dir}" already exists and is not empty.`);
473
498
  }
474
499
  mkdirSync6(root, { recursive: true });
@@ -486,15 +511,15 @@ var create = async (args) => {
486
511
  };
487
512
 
488
513
  // src/commands/design.ts
489
- import { existsSync as existsSync7, mkdirSync as mkdirSync7, readFileSync as readFileSync7, readdirSync as readdirSync5, writeFileSync as writeFileSync7, appendFileSync } from "node:fs";
490
- import { basename as basename2, join as join10 } from "node:path";
514
+ import { existsSync as existsSync9, mkdirSync as mkdirSync8, readFileSync as readFileSync9, readdirSync as readdirSync5, writeFileSync as writeFileSync8, appendFileSync } from "node:fs";
515
+ import { basename as basename2, join as join11 } from "node:path";
491
516
 
492
517
  // src/compile/designCompile.ts
493
- import { readFileSync as readFileSync6 } from "node:fs";
518
+ import { readFileSync as readFileSync8 } from "node:fs";
494
519
  import { basename } from "node:path";
495
520
 
496
521
  // src/localFiles.ts
497
- import { readdirSync as readdirSync3, readFileSync as readFileSync4 } from "node:fs";
522
+ import { readdirSync as readdirSync3, readFileSync as readFileSync5 } from "node:fs";
498
523
  import { join as join7, relative } from "node:path";
499
524
  var SKIP_DIRS = new Set([".lecodes", ".git", "node_modules"]);
500
525
  var SKIP_FILES = new Set(["tsconfig.json", "jsconfig.json", ".gitignore", ".lecodesignore", ".DS_Store"]);
@@ -569,7 +594,7 @@ var scanLocal = (root, opts = {}) => {
569
594
  ignored.push(key);
570
595
  continue;
571
596
  }
572
- files.push({ path: key, absPath: abs, sha: sha256(readFileSync4(abs)) });
597
+ files.push({ path: key, absPath: abs, sha: sha256(readFileSync5(abs)) });
573
598
  }
574
599
  }
575
600
  };
@@ -603,16 +628,16 @@ var diffLocal = (manifest, scan) => {
603
628
  var hasChanges = (diff) => diff.added.length + diff.modified.length + diff.deleted.length > 0;
604
629
 
605
630
  // src/compile/projectCompile.ts
606
- import { existsSync as existsSync6 } from "node:fs";
631
+ import { existsSync as existsSync7 } from "node:fs";
607
632
  import { createRequire as createRequire3 } from "node:module";
608
633
  import { dirname as dirname5, join as join9 } from "node:path";
609
- import { fileURLToPath as fileURLToPath2 } from "node:url";
634
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
610
635
  // ../sdk/src/compile/bundler.ts
611
- import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as readFileSync5, statSync } from "node:fs";
636
+ import { existsSync as existsSync6, readdirSync as readdirSync4, readFileSync as readFileSync6, statSync } from "node:fs";
612
637
  import { spawnSync } from "node:child_process";
613
638
  import { createRequire as createRequire2 } from "node:module";
614
639
  import { dirname as dirname4, join as join8, relative as relative2 } from "node:path";
615
- import { fileURLToPath } from "node:url";
640
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
616
641
 
617
642
  // ../sdk/src/compile/assetMacro.ts
618
643
  var ASSET_RE = /\basset\(\s*(["'])((?:[^"'\\]|\\.)*)\1\s*\)/g;
@@ -672,11 +697,11 @@ var classBodyDeclaresAspect = (src, from) => {
672
697
  // ../sdk/src/compile/bundler.ts
673
698
  var resolveInject = () => {
674
699
  const override = process.env.LECODES_SDK_INJECT;
675
- if (override && existsSync5(override))
700
+ if (override && existsSync6(override))
676
701
  return override;
677
- const here = dirname4(fileURLToPath(import.meta.url));
702
+ const here = dirname4(fileURLToPath2(import.meta.url));
678
703
  const src = join8(here, "..", "inject.ts");
679
- if (existsSync5(src))
704
+ if (existsSync6(src))
680
705
  return src;
681
706
  throw new Error("sdk: could not locate the inject source (LECODES_SDK_INJECT / src/inject.ts)");
682
707
  };
@@ -714,7 +739,7 @@ var collectSdkFiles = (sdkSrc) => {
714
739
  if (statSync(p).isDirectory())
715
740
  walk(p);
716
741
  else if (isSdkSource(p))
717
- out["/__sdk/" + relative2(sdkSrc, p).split("\\").join("/")] = readFileSync5(p, "utf8");
742
+ out["/__sdk/" + relative2(sdkSrc, p).split("\\").join("/")] = readFileSync6(p, "utf8");
718
743
  }
719
744
  };
720
745
  walk(sdkSrc);
@@ -724,13 +749,13 @@ var collectSdkFiles = (sdkSrc) => {
724
749
  var resolveChiselBin = () => {
725
750
  const override = process.env.LECODES_CHISEL_BIN;
726
751
  if (override) {
727
- if (existsSync5(override))
752
+ if (existsSync6(override))
728
753
  return override;
729
754
  throw new Error(`LECODES_CHISEL_BIN points at a missing file: ${override}`);
730
755
  }
731
756
  try {
732
757
  const bin = createRequire2(import.meta.url)("@letary/chisel").binaryPath();
733
- if (bin && existsSync5(bin))
758
+ if (bin && existsSync6(bin))
734
759
  return bin;
735
760
  } catch {}
736
761
  throw new Error("chisel binary not found. Install @letary/chisel, or set LECODES_CHISEL_BIN.");
@@ -890,9 +915,9 @@ ${inlineSourceMapComment(map)}` : code;
890
915
  ${inlineSourceMapComment(offsetSourceMap(map, headerLines))}`;
891
916
  };
892
917
  // src/compile/projectCompile.ts
893
- var here = dirname5(fileURLToPath2(import.meta.url));
918
+ var here = dirname5(fileURLToPath3(import.meta.url));
894
919
  var vendoredInject = join9(here, "..", "runtime", "sdk", "inject.ts");
895
- if (existsSync6(vendoredInject))
920
+ if (existsSync7(vendoredInject))
896
921
  process.env.LECODES_SDK_INJECT = vendoredInject;
897
922
  var resolveChiselBin2 = () => {
898
923
  if (process.env.LECODES_CHISEL_BIN)
@@ -916,10 +941,379 @@ var compileProject2 = (opts) => compileProject({
916
941
  localAssets: opts.localAssets
917
942
  });
918
943
 
944
+ // src/compile/assetIcons.ts
945
+ import { createHash as createHash2 } from "node:crypto";
946
+ import { existsSync as existsSync8, mkdirSync as mkdirSync7, readFileSync as readFileSync7, writeFileSync as writeFileSync7 } from "node:fs";
947
+ import { dirname as dirname6, join as join10 } from "node:path";
948
+ var DEFAULT_REGISTRY = process.env.LECODES_ICON_REGISTRY?.trim() || "https://icon-registry.jt3.ru/v1";
949
+ var ICON_ID = /^([a-z0-9]+(?:-[a-z0-9]+)*):([a-z0-9]+(?:-[a-z0-9]+)*)$/;
950
+ var isIdentStart = (c2) => c2 >= "A" && c2 <= "Z" || c2 >= "a" && c2 <= "z" || c2 === "_" || c2 === "$";
951
+ var isIdentPart = (c2) => isIdentStart(c2) || c2 >= "0" && c2 <= "9";
952
+ var skipString = (src, start) => {
953
+ const quote = src[start];
954
+ const n = src.length;
955
+ let i = start + 1;
956
+ while (i < n) {
957
+ const c2 = src[i];
958
+ if (c2 === "\\") {
959
+ i += 2;
960
+ continue;
961
+ }
962
+ if (quote === "`" && c2 === "$" && src[i + 1] === "{") {
963
+ i = skipBraces(src, i + 1);
964
+ continue;
965
+ }
966
+ if (c2 === quote)
967
+ return i + 1;
968
+ i++;
969
+ }
970
+ return n;
971
+ };
972
+ var skipBraces = (src, open) => {
973
+ const n = src.length;
974
+ let depth = 0;
975
+ let i = open;
976
+ while (i < n) {
977
+ const c2 = src[i];
978
+ if (c2 === '"' || c2 === "'" || c2 === "`") {
979
+ i = skipString(src, i);
980
+ continue;
981
+ }
982
+ if (c2 === "{")
983
+ depth++;
984
+ else if (c2 === "}") {
985
+ depth--;
986
+ if (depth === 0)
987
+ return i + 1;
988
+ }
989
+ i++;
990
+ }
991
+ return n;
992
+ };
993
+ var matchParen = (src, open) => {
994
+ const n = src.length;
995
+ let depth = 0;
996
+ let i = open;
997
+ while (i < n) {
998
+ const c2 = src[i];
999
+ if (c2 === '"' || c2 === "'" || c2 === "`") {
1000
+ i = skipString(src, i);
1001
+ continue;
1002
+ }
1003
+ if (c2 === "/" && src[i + 1] === "/") {
1004
+ i += 2;
1005
+ while (i < n && src[i] !== `
1006
+ `)
1007
+ i++;
1008
+ continue;
1009
+ }
1010
+ if (c2 === "/" && src[i + 1] === "*") {
1011
+ i += 2;
1012
+ while (i < n && !(src[i] === "*" && src[i + 1] === "/"))
1013
+ i++;
1014
+ i += 2;
1015
+ continue;
1016
+ }
1017
+ if (c2 === "(")
1018
+ depth++;
1019
+ else if (c2 === ")") {
1020
+ depth--;
1021
+ if (depth === 0)
1022
+ return i;
1023
+ }
1024
+ i++;
1025
+ }
1026
+ return -1;
1027
+ };
1028
+ var splitTopLevel = (text, delim) => {
1029
+ const parts = [];
1030
+ const n = text.length;
1031
+ let depth = 0;
1032
+ let last = 0;
1033
+ let i = 0;
1034
+ while (i < n) {
1035
+ const c2 = text[i];
1036
+ if (c2 === '"' || c2 === "'" || c2 === "`") {
1037
+ i = skipString(text, i);
1038
+ continue;
1039
+ }
1040
+ if (c2 === "(" || c2 === "[" || c2 === "{")
1041
+ depth++;
1042
+ else if (c2 === ")" || c2 === "]" || c2 === "}")
1043
+ depth = Math.max(0, depth - 1);
1044
+ else if (depth === 0 && c2 === delim) {
1045
+ parts.push(text.slice(last, i));
1046
+ last = i + 1;
1047
+ }
1048
+ i++;
1049
+ }
1050
+ parts.push(text.slice(last));
1051
+ return parts;
1052
+ };
1053
+ var stringLiteralValue = (text) => {
1054
+ const t = text.trim();
1055
+ if (t.length < 2)
1056
+ return null;
1057
+ const q = t[0];
1058
+ if (q !== '"' && q !== "'" && q !== "`" || t[t.length - 1] !== q)
1059
+ return null;
1060
+ const inner = t.slice(1, -1);
1061
+ if (q === "`" && inner.includes("${"))
1062
+ return null;
1063
+ if (inner.includes(q) && !inner.includes("\\" + q))
1064
+ return null;
1065
+ return inner.replace(/\\(.)/g, "$1");
1066
+ };
1067
+ var scanAssetIconCalls = (source) => {
1068
+ const calls = [];
1069
+ const n = source.length;
1070
+ let i = 0;
1071
+ while (i < n) {
1072
+ const c2 = source[i];
1073
+ if (c2 === '"' || c2 === "'" || c2 === "`") {
1074
+ i = skipString(source, i);
1075
+ continue;
1076
+ }
1077
+ if (c2 === "/" && source[i + 1] === "/") {
1078
+ i += 2;
1079
+ while (i < n && source[i] !== `
1080
+ `)
1081
+ i++;
1082
+ continue;
1083
+ }
1084
+ if (c2 === "/" && source[i + 1] === "*") {
1085
+ i += 2;
1086
+ while (i < n && !(source[i] === "*" && source[i + 1] === "/"))
1087
+ i++;
1088
+ i += 2;
1089
+ continue;
1090
+ }
1091
+ if (isIdentStart(c2)) {
1092
+ let j = i + 1;
1093
+ while (j < n && isIdentPart(source[j]))
1094
+ j++;
1095
+ if (source.slice(i, j) === "assetIcon" && prevNonSpace(source, i) !== ".") {
1096
+ const call = parseCall(source, i, j);
1097
+ if (call) {
1098
+ calls.push(call);
1099
+ i = call.end;
1100
+ continue;
1101
+ }
1102
+ }
1103
+ i = j;
1104
+ continue;
1105
+ }
1106
+ i++;
1107
+ }
1108
+ return calls;
1109
+ };
1110
+ var prevNonSpace = (src, i) => {
1111
+ let k = i - 1;
1112
+ while (k >= 0 && /\s/.test(src[k]))
1113
+ k--;
1114
+ return k >= 0 ? src[k] : "";
1115
+ };
1116
+ var parseCall = (src, start, identEnd) => {
1117
+ const n = src.length;
1118
+ let i = identEnd;
1119
+ while (i < n && /\s/.test(src[i]))
1120
+ i++;
1121
+ if (src[i] !== "(")
1122
+ return null;
1123
+ const close = matchParen(src, i);
1124
+ if (close === -1)
1125
+ return null;
1126
+ const call = { start, end: close + 1, id: "", pack: "", name: "" };
1127
+ const args = splitTopLevel(src.slice(i + 1, close), ",");
1128
+ const id = stringLiteralValue(args[0] ?? "");
1129
+ if (id === null) {
1130
+ call.error = `assetIcon(...) needs a string-literal id like "lucide:bell" (got \`${(args[0] ?? "").trim() || "()"}\`).`;
1131
+ return call;
1132
+ }
1133
+ const m = ICON_ID.exec(id);
1134
+ if (!m) {
1135
+ call.error = `assetIcon("${id}"): id must be "pack:name", lowercase [a-z0-9-] (e.g. "lucide:bell").`;
1136
+ return call;
1137
+ }
1138
+ call.id = id;
1139
+ call.pack = m[1];
1140
+ call.name = m[2];
1141
+ const optsText = args[1]?.trim();
1142
+ if (optsText) {
1143
+ const color = extractColor(optsText);
1144
+ if (color.error) {
1145
+ call.error = color.error;
1146
+ return call;
1147
+ }
1148
+ if (color.bake !== undefined)
1149
+ call.bake = color.bake;
1150
+ else if (color.tint !== undefined)
1151
+ call.tint = color.tint;
1152
+ }
1153
+ return call;
1154
+ };
1155
+ var extractColor = (optsText) => {
1156
+ if (!optsText.startsWith("{") || !optsText.endsWith("}")) {
1157
+ return { error: `assetIcon(id, opts): opts must be an object literal like { color: "#5b8cff" }.` };
1158
+ }
1159
+ for (const prop of splitTopLevel(optsText.slice(1, -1), ",")) {
1160
+ const kv = splitTopLevel(prop, ":");
1161
+ if (kv.length < 2)
1162
+ continue;
1163
+ const key = kv[0].trim().replace(/^["'`]|["'`]$/g, "");
1164
+ if (key !== "color")
1165
+ continue;
1166
+ const raw = kv.slice(1).join(":").trim();
1167
+ if (!raw)
1168
+ continue;
1169
+ const literal = stringLiteralValue(raw);
1170
+ return literal !== null ? { bake: literal } : { tint: raw };
1171
+ }
1172
+ return {};
1173
+ };
1174
+ var rewriteAssetIcons = (source, calls, svgById) => {
1175
+ let out = source;
1176
+ for (const call of [...calls].sort((a, b) => b.start - a.start)) {
1177
+ const svg = svgById.get(call.id);
1178
+ if (svg === undefined)
1179
+ continue;
1180
+ out = out.slice(0, call.start) + iconExpr(svg, call) + out.slice(call.end);
1181
+ }
1182
+ return out;
1183
+ };
1184
+ var iconExpr = (svg, call) => {
1185
+ const body = call.bake ? svg.split("currentColor").join(call.bake) : svg;
1186
+ const src = `SvgSource(\`${escapeTemplate(body)}\`)`;
1187
+ return call.tint ? `Object.assign(${src}, { tintColor: (${call.tint}) })` : src;
1188
+ };
1189
+ var escapeTemplate = (s) => s.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
1190
+ var isHttp = (s) => /^https?:/i.test(s);
1191
+ var sha12 = (s) => createHash2("sha256").update(s).digest("hex").slice(0, 12);
1192
+ var registryHome = (registry) => registry.replace(/\/v\d+$/, "");
1193
+ var readText = async (registry, rel) => {
1194
+ if (isHttp(registry)) {
1195
+ const res = await fetch(`${registry}/${rel}`);
1196
+ return res.ok ? await res.text() : null;
1197
+ }
1198
+ try {
1199
+ return readFileSync7(join10(registry, rel), "utf8");
1200
+ } catch {
1201
+ return null;
1202
+ }
1203
+ };
1204
+ var readLock = (path) => {
1205
+ try {
1206
+ const lock = JSON.parse(readFileSync7(path, "utf8"));
1207
+ if (lock && typeof lock === "object" && lock.icons && lock.packs)
1208
+ return lock;
1209
+ } catch {}
1210
+ return null;
1211
+ };
1212
+ var serializeLock = (lock) => {
1213
+ const sortKeys = (rec) => Object.fromEntries(Object.keys(rec).sort().map((k) => [k, rec[k]]));
1214
+ return JSON.stringify({ registry: lock.registry, icons: sortKeys(lock.icons), packs: sortKeys(lock.packs) }, null, 2) + `
1215
+ `;
1216
+ };
1217
+ var renderNotices = (lock) => {
1218
+ const packs = [...new Set(Object.values(lock.icons).map((i) => i.pack))].sort();
1219
+ const lines = [
1220
+ "# Icon attribution & notices",
1221
+ "",
1222
+ "Icons vendored from the icon-registry via `assetIcon(...)`. Keep this file in your repo / product.",
1223
+ ""
1224
+ ];
1225
+ for (const p of packs) {
1226
+ const l = lock.packs[p];
1227
+ if (!l)
1228
+ continue;
1229
+ let note2 = `- **${p}** — ${l.spdx}${l.url ? ` — ${l.url}` : ""}`;
1230
+ if (l.attribution)
1231
+ note2 += `
1232
+ - **Attribution required:** ${l.attribution}`;
1233
+ if (l.modifiedNotice)
1234
+ note2 += `
1235
+ - ${l.spdx}: state any modifications if you alter the glyphs.`;
1236
+ lines.push(note2);
1237
+ }
1238
+ lines.push("");
1239
+ return lines.join(`
1240
+ `);
1241
+ };
1242
+ var writeIfChanged = (path, text) => {
1243
+ const current = existsSync8(path) ? safeRead(path) : null;
1244
+ if (current === text)
1245
+ return false;
1246
+ writeFileSync7(path, text);
1247
+ return true;
1248
+ };
1249
+ var safeRead = (path) => {
1250
+ try {
1251
+ return readFileSync7(path, "utf8");
1252
+ } catch {
1253
+ return null;
1254
+ }
1255
+ };
1256
+ var vendorIcons = async (designDir, ids, opts = {}) => {
1257
+ const registry = (opts.registry ?? DEFAULT_REGISTRY).replace(/\/+$/, "");
1258
+ const lockPath = join10(designDir, "icons.lock.json");
1259
+ const lock = readLock(lockPath) ?? { registry, icons: {}, packs: {} };
1260
+ const svgById = new Map;
1261
+ const missing = [];
1262
+ for (const id of ids) {
1263
+ const li = lock.icons[id];
1264
+ const cached = li && safeRead(join10(designDir, li.path));
1265
+ if (cached)
1266
+ svgById.set(id, cached);
1267
+ else
1268
+ missing.push(id);
1269
+ }
1270
+ if (missing.length) {
1271
+ const licRaw = await readText(registry, "licenses.json");
1272
+ if (!licRaw)
1273
+ throw new Error(`assetIcon: can't reach the icon registry (${registry}). Check your connection, or set LECODES_ICON_REGISTRY.`);
1274
+ const licenses = JSON.parse(licRaw);
1275
+ for (const id of missing) {
1276
+ const m = ICON_ID.exec(id);
1277
+ const [, pack, name] = m;
1278
+ const lic = licenses[pack];
1279
+ if (!lic)
1280
+ throw new Error(`assetIcon: unknown icon pack "${pack}" (in "${id}"). Browse packs at ${registryHome(registry)}.`);
1281
+ const svg = await readText(registry, `${pack}/${name}.svg`);
1282
+ if (svg === null)
1283
+ throw new Error(`assetIcon: icon "${id}" not found in the registry. Check the name at ${registryHome(registry)}.`);
1284
+ const rel = `assets/icons/${pack}/${name}.svg`;
1285
+ const abs = join10(designDir, rel);
1286
+ mkdirSync7(dirname6(abs), { recursive: true });
1287
+ writeFileSync7(abs, svg);
1288
+ lock.icons[id] = { pack, name, hash: sha12(svg), spdx: lic.spdx, path: rel };
1289
+ lock.packs[pack] = { spdx: lic.spdx, title: lic.title, url: lic.url, attribution: lic.attribution, modifiedNotice: lic.modifiedNotice };
1290
+ svgById.set(id, svg);
1291
+ }
1292
+ lock.registry = registry;
1293
+ writeIfChanged(lockPath, serializeLock(lock));
1294
+ }
1295
+ if (Object.keys(lock.icons).length)
1296
+ writeIfChanged(join10(designDir, "ICON-NOTICES.md"), renderNotices(lock));
1297
+ return svgById;
1298
+ };
1299
+ var applyAssetIcons = async (entries, designDir, opts = {}) => {
1300
+ const scanned = entries.filter((e) => e.type === "text" && typeof e.text === "string" && e.text.includes("assetIcon")).map((e) => ({ entry: e, calls: scanAssetIconCalls(e.text) })).filter((s) => s.calls.length > 0);
1301
+ if (scanned.length === 0)
1302
+ return;
1303
+ const errors = scanned.flatMap((s) => s.calls.filter((c2) => c2.error).map((c2) => c2.error));
1304
+ if (errors.length)
1305
+ throw new Error([...new Set(errors)].join(`
1306
+ `));
1307
+ const ids = [...new Set(scanned.flatMap((s) => s.calls.map((c2) => c2.id)))];
1308
+ const svgById = await vendorIcons(designDir, ids, opts);
1309
+ for (const { entry, calls } of scanned)
1310
+ entry.text = rewriteAssetIcons(entry.text, calls, svgById);
1311
+ };
1312
+
919
1313
  // src/compile/designCompile.ts
920
1314
  var LOCAL_ASSET_SCHEME = "lecodesfile://";
921
1315
  var compileDesignScreen = async (opts) => {
922
- const { root, manifest, designKey, screenId } = opts;
1316
+ const { root, manifest, designDir, designKey, screenId, state } = opts;
923
1317
  const entries = [];
924
1318
  const scan = scanLocal(root, { ignore: loadIgnore(root), tracked: new Set(Object.keys(manifest?.files ?? {})) });
925
1319
  for (const file of scan.files) {
@@ -934,17 +1328,20 @@ var compileDesignScreen = async (opts) => {
934
1328
  } else if (type === "shader") {
935
1329
  entries.push({ path: file.path, type, shaders: [] });
936
1330
  } else {
937
- entries.push({ path: file.path, type, text: readFileSync6(file.absPath, "utf8") });
1331
+ entries.push({ path: file.path, type, text: readFileSync8(file.absPath, "utf8") });
938
1332
  }
939
1333
  }
1334
+ const invoke = state !== undefined ? `screen(${JSON.stringify(state)})` : "screen()";
940
1335
  const entryPath = `${designKey}/.screen-entry.ts`;
941
1336
  entries.push({
942
1337
  path: entryPath,
943
1338
  type: "text",
944
1339
  text: `import screen from "./screens/${screenId}"
945
- screen.open()
1340
+ ` + `const __screen = typeof screen === "function" ? ${invoke} : screen
1341
+ ` + `__screen.open()
946
1342
  `
947
1343
  });
1344
+ await applyAssetIcons(entries, designDir);
948
1345
  return compileProject2({
949
1346
  entries,
950
1347
  name: opts.name,
@@ -1036,11 +1433,45 @@ var browserLogin = async (webBase, timeoutMs = 5 * 60 * 1000, open = openBrowser
1036
1433
  // src/commands/designTemplates.ts
1037
1434
  var META_JSON = `{
1038
1435
  "screens": {
1039
- "home": { "pos": [0, 0], "desc": "Example screen — replace me" }
1436
+ "home": { "pos": [0, 0], "desc": "Example screen — replace me", "role": "list" }
1040
1437
  },
1041
1438
  "edges": []
1042
1439
  }
1043
1440
  `;
1441
+ var SPEC_MD = `# Concept
1442
+
1443
+ The narrative half of this design. \`meta.json\` + the canvas hold the *visual/flow* half (screens,
1444
+ positions, edges); this file holds what never renders as a screen — the data model, the actors, and
1445
+ the rules & decisions behind them. Prose, not schema. **Half-filled is fine** — fill it in as the
1446
+ design firms up. This is where the concept you'd otherwise keep in chat lives.
1447
+
1448
+ Link a screen with \`[[screen-id]]\` (its file name). \`lecodes design check\` warns when a link names
1449
+ a screen that doesn't exist.
1450
+
1451
+ ## Data model
1452
+
1453
+ The entities the app is about, and their fields. The mock data at the top of each screen file is the
1454
+ first draft of this — promote the entities that stabilize here (one table each).
1455
+
1456
+ <!--
1457
+ ### Task
1458
+ | field | type | notes |
1459
+ |-------|------|-------|
1460
+ | id | string | |
1461
+ | title | string | |
1462
+ | done | boolean | defaults to false |
1463
+ -->
1464
+
1465
+ ## Actors
1466
+
1467
+ Who uses the app, and what each is allowed to do. (Different from a screen's \`role\` in meta.json,
1468
+ which is the screen's *kind* — list, detail, form…)
1469
+
1470
+ ## Rules & decisions
1471
+
1472
+ Behaviour that isn't visible in a single screen — validation, permissions, notifications, edge
1473
+ cases — and the decisions you've made and why, so they aren't re-litigated later.
1474
+ `;
1044
1475
  var TOKENS_TS = `// Design tokens shared by every screen. Colors are hex strings (the engine parses hex only).
1045
1476
  export const colors = {
1046
1477
  bg: "#101114",
@@ -1110,24 +1541,60 @@ This is an intent-capture stage — screens run on inline mock data, before any
1110
1541
 
1111
1542
  \`meta.json\` is the app map: every screen (position, description) and the edges between them
1112
1543
  (navigation intent). **Read it before opening any screen file.** It is the human's file — the
1113
- canvas and the developer own positions and descriptions. You MAY add entries for new screens you
1114
- create (pick a free position: screens are 390×844, leave ~160px gaps) and add edges when the flow
1115
- is clear, but never reposition or rewrite existing human-authored descriptions.
1544
+ canvas and the developer own positions and descriptions. When you create a screen, add its meta
1545
+ entry **without a \`pos\`** and add edges — the board **auto-places it next to its flow neighbours**,
1546
+ so you never hand-pick coordinates (you can't see the board, and positions are the human's to own).
1547
+ \`lecodes design arrange --new\` optionally writes those auto-placements into meta.json; the developer's
1548
+ ⇥ Arrange button re-tidies the whole board. Never reposition or rewrite existing human-authored
1549
+ positions or descriptions.
1116
1550
 
1117
1551
  \`\`\`json
1118
1552
  {
1119
- "screens": { "login": { "pos": [0, 0], "desc": "Phone login" } },
1120
- "edges": [ { "from": "login", "to": "code", "label": "after phone entry", "activator": "submit" } ]
1553
+ "screens": {
1554
+ "login": { "desc": "Phone login", "group": "Onboarding", "role": "auth" },
1555
+ "code": { "group": "Onboarding", "role": "auth" }
1556
+ },
1557
+ "groups": { "Onboarding": { "desc": "First-run flow" } },
1558
+ "edges": [
1559
+ { "from": "login", "to": "code", "label": "after phone entry", "activator": "submit" },
1560
+ { "from": "code", "to": "home@empty", "label": "first sign-in" }
1561
+ ]
1121
1562
  }
1122
1563
  \`\`\`
1123
1564
 
1124
1565
  - An edge's optional \`activator\` names the element (its \`name\` style option) that triggers the
1125
1566
  transition. Give a \`name\` to interactive elements an edge refers to — nothing else needs names.
1567
+ - **Group each screen** you create by its user-facing area — \`"group": "Onboarding"\` — 2–6 screens
1568
+ per group (Onboarding, Checkout, Settings…). Groups render as labeled regions and \`arrange\` lays
1569
+ each out as its own sub-flow, so grouping keeps big boards legible. Semantic clustering is your
1570
+ job (you're good at it); coordinates are \`arrange\`'s (it can see the board, you can't). Don't
1571
+ regroup existing screens unless asked — groups are part of the human's map.
1572
+ - **Tag a screen's \`role\`** in its meta entry — its *kind* (\`list\`, \`detail\`, \`form\`, \`auth\`,
1573
+ \`modal\`…). Optional, renders on the board, captures concept the screen source can't;
1574
+ \`lecodes design check\` lists screens still untagged (never blocking). *(A screen's **states** are
1575
+ a separate thing — declared in the source, not meta; see "Screen states" below.)*
1576
+
1577
+ ## The concept — spec.md
1578
+
1579
+ \`meta.json\` + the canvas are the *visual/flow* half of the design; \`spec.md\` is the *narrative*
1580
+ half — what never renders as a screen and doesn't fit meta.json: the **data model** (entities +
1581
+ fields), the **actors** (who uses the app + their permissions), and the **rules & decisions**
1582
+ (validation, edge cases, and the choices behind them). Write down here what you'd otherwise only
1583
+ keep in the chat.
1584
+
1585
+ - It's prose (markdown), co-owned by you and the developer — edit it with your normal Read/Edit
1586
+ (there's no board writer for it). Half-filled is fine.
1587
+ - The **mock data at the top of each screen is the first draft of the data model** — promote the
1588
+ entities that stabilize into spec.md's tables.
1589
+ - Link a screen with \`[[screen-id]]\`. \`lecodes design check\` (and the \`check_concept\` MCP tool)
1590
+ warn when a link names a screen that doesn't exist, and list screens with no \`role\` yet —
1591
+ coherence, never a blocker.
1126
1592
 
1127
1593
  ## Screen rules
1128
1594
 
1129
- - **One file = one screen.** \`screens/<id>.ts\` default-exports an already-computed screen:
1130
- \`export default UIScreen([ … ]).style({ … })\`. The filename is the screen's identity.
1595
+ - **One file = one screen.** \`screens/<id>.ts\` default-exports a screen — either a value
1596
+ (\`export default UIScreen([ … ]).style({ … })\`) or a **function of state** for multi-state
1597
+ screens (see "Screen states" below). The filename is the screen's identity.
1131
1598
  - **Styles go through the chained \`.style({ … })\` method** — e.g.
1132
1599
  \`UIText("Today").style({ fontSize: 28, fontWeight: 700 })\`. Passing a style object as the
1133
1600
  first argument is a legacy form.
@@ -1139,28 +1606,111 @@ is clear, but never reposition or rewrite existing human-authored descriptions.
1139
1606
  every screen. Promoting a repeated pattern into \`shared/\` is an explicit, deliberate step.
1140
1607
  - The LeCodes UI API (\`UIScreen\`, \`UIColumn\`, \`UIRow\`, \`UIBox\`, \`UIText\`, \`UIImage\`,
1141
1608
  \`UIButton\`, \`UIInput\`, \`UISpacer\`, \`UIScrollable\`, …) is **global** — no imports needed.
1142
- Colors must be hex strings.
1609
+ Colors must be hex strings. This folder ships a \`tsconfig.json\` + bundled SDK types
1610
+ (\`.lecodes/types/\`), so the editor resolves those globals — and \`UINode\` / \`UINodeChild\` for
1611
+ typing a component's return value or a container's children — with no imports. Trust the types;
1612
+ don't fall back to \`any\`.
1613
+
1614
+ ## Screen states (variants)
1615
+
1616
+ A screen often has more than one look — a list vs. its empty state, a form vs. its error state.
1617
+ Instead of a second file per variant, make the default export a **function of a state string whose
1618
+ type is a string-literal union** — the union IS the state list (no separate schema):
1619
+
1620
+ \`\`\`ts
1621
+ export default (state: "default" | "empty" = "default") => UIScreen([
1622
+ Header(child),
1623
+ state === "empty" ? EmptyState("Nothing here yet") : UIScrollable(items.map(Card)),
1624
+ TabBar(tabs, "home"),
1625
+ ])
1626
+ \`\`\`
1627
+
1628
+ - The **union members are the states**, in written order. It may be written inline (as above) or as
1629
+ a named alias in the same file (\`type State = "default" | "empty"; export default (s: State) => …\`).
1630
+ Give the parameter a **default** (\`= "default"\`) — that's the *canonical* state (what a bare
1631
+ render, snapshot, and the board's initial view show). No default → the first member is canonical.
1632
+ - When the data differs per state, derive it from \`state\` at the top of the function — then it's
1633
+ just layout: \`const items = state === "empty" ? [] : MOCK\`.
1634
+ - State ids are lowercase \`[a-z0-9-]\` (filename-safe, no \`@\`). A value export — or a function whose
1635
+ parameter isn't a literal union — is a single state named \`default\` (fully backward compatible).
1636
+ - **Check a state:** \`render_screen\` takes a \`state\`; the board shows a switcher on multi-state
1637
+ screens; \`lecodes design snapshot <id> --state <name>\` / \`--all-states\` write \`<id>@<state>.png\`.
1638
+ - **Edges can target a state:** an edge's \`to\` (or \`from\`) may be \`"<id>@<state>"\` (e.g.
1639
+ \`"home@empty"\`) — still one node per screen. \`lecodes design check\` errors on an edge pointing at
1640
+ a state the screen doesn't declare.
1641
+
1642
+ ## Icons
1643
+
1644
+ Use real icons instead of emoji. \`assetIcon("pack:name")\` is a **compile-time macro**: it vendors
1645
+ one SVG from the icon registry at build time and returns an image source (like \`SvgSource(...)\`), so
1646
+ screens render offline and deterministically in headless snapshots — no network at render time.
1647
+
1648
+ \`\`\`ts
1649
+ UIImage(assetIcon("lucide:bell", { color: colors.accent })).style({ width: 24, height: 24 })
1650
+ UIImage(assetIcon("lucide:check")).style({ width: 20, height: 20, tintColor: colors.muted })
1651
+ \`\`\`
1652
+
1653
+ - Pass the result straight to \`UIImage(...)\`. Recolor it with the \`{ color }\` option — a hex **literal**
1654
+ is baked into the SVG at compile time, a **token/expression** (\`{ color: colors.accent }\`) is applied
1655
+ as a tint — or with the \`tintColor\` **style** prop (\`.style({ tintColor: … })\`; there is no
1656
+ \`.tintColor()\` method). All of these render colored in headless snapshots. The \`"pack:name"\` id must
1657
+ be a **string literal** (it's resolved at compile time).
1658
+ - Browse packs and names at **https://icon-registry.jt3.ru**. Common packs: \`lucide\`, \`tabler\`,
1659
+ \`heroicons\`, \`feather\`, \`bi\`, \`carbon\`.
1660
+ - Vendored SVGs land in \`assets/icons/\`, tracked by \`icons.lock.json\` + \`ICON-NOTICES.md\`
1661
+ (attribution) — **commit these**; they make rebuilds offline. An unknown \`pack:name\` is a clear
1662
+ compile error.
1143
1663
 
1144
1664
  ## See what you built
1145
1665
 
1146
1666
  The developer watches the canvas live (screens hot-reload on save). To see a screen yourself:
1147
1667
 
1668
+ **When the design server is running (it usually is), prefer the MCP tools** — they use the
1669
+ server's warm compile cache and return images inline, no files on disk:
1670
+
1671
+ - \`render_screen\` (id, format: json | png | both, state?) — what a screen actually looks like
1672
+ (pass \`state\` to render a specific state; default is canonical).
1673
+ - \`check_screens\` (ids?) — fast compile check after an edit batch; no render.
1674
+ - \`check_concept\` — spec.md ↔ screens coherence + which screens still lack a \`role\`.
1675
+ - \`design_state\` — the live screens + edges (fresher than re-reading meta.json).
1676
+ - \`board_activity\` (since?) — what the developer just did on the canvas + which screen is
1677
+ selected; call it at the start of a turn.
1678
+
1679
+ Fall back to the CLI when the tools report the server is down:
1680
+
1148
1681
  \`\`\`sh
1149
- lecodes design snapshot <id> # semantic JSON of the laid-out screen → stdout
1150
- lecodes design snapshot <id> --png # + PNG screenshot → design/.snapshots/<id>.png
1151
- lecodes design snapshot # all screens → design/.snapshots/
1682
+ lecodes design snapshot <id> # semantic JSON of the laid-out screen → stdout
1683
+ lecodes design snapshot <id> --png # + PNG screenshot → design/.snapshots/<id>.png
1684
+ lecodes design snapshot <id> --state empty # one state → design/.snapshots/<id>@empty.png|.json
1685
+ lecodes design snapshot <id> --all-states # every state → design/.snapshots/<id>@<state>.png|.json
1686
+ lecodes design snapshot # all screens → design/.snapshots/
1687
+ lecodes design check # concept + flow coherence (spec.md links, states, untagged)
1152
1688
  \`\`\`
1153
1689
 
1154
- Snapshot after every screen you create or change, and fix what looks wrong (overflow, overlap,
1155
- unreadable contrast, off-screen content) before reporting back.
1690
+ Either way: after every screen you create or change, look at it and fix what's wrong (overflow,
1691
+ overlap, unreadable contrast, off-screen content) before reporting back.
1156
1692
 
1157
1693
  ## Workflow
1158
1694
 
1159
- 1. Read \`meta.json\`, then the \`shared/\` signatures, then only the screens you need.
1160
- 2. Create/edit screen files; register new screens (and clear edges) in \`meta.json\`.
1161
- 3. Snapshot → verify → fix.
1162
- 4. When the developer edits a description or the flow on the canvas, treat meta.json as the
1163
- updated spec.
1695
+ 1. \`board_activity\` (if the server is up), then read \`meta.json\`, the \`shared/\` signatures, and
1696
+ only the screens you need.
1697
+ 2. Create/edit screen files; register new screens in \`meta.json\` **without \`pos\`** and add edges —
1698
+ they auto-place next to their neighbours on the board. (\`lecodes design arrange --new\` commits
1699
+ those positions to meta.json if you want them written.)
1700
+ 3. \`check_screens\` → \`render_screen\` (or \`lecodes design snapshot\`) → verify → fix.
1701
+ 4. When the developer edits a description, text, or the flow on the canvas — visible in
1702
+ \`board_activity\` and in \`meta.json\`/the screen source — treat it as the updated spec.
1703
+ `;
1704
+ var DESIGN_MACROS_DTS = `// Generated by \`lecodes design init\`. Globals the design compiler resolves — not shipped SDK runtime.
1705
+
1706
+ /**
1707
+ * Vendor one icon from the icon registry at build time and return an image source (the same shape
1708
+ * SvgSource(...) returns). \`id\` is "pack:name" (e.g. "lucide:bell"); pass the result to UIImage(...).
1709
+ * Recolor with the { color } option — a hex literal is baked into the SVG, a token
1710
+ * (\`{ color: colors.accent }\`) is applied as a tint — or the \`tintColor\` style prop. The id must be a
1711
+ * string literal (resolved, and the SVG vendored, at compile time).
1712
+ */
1713
+ declare function assetIcon(id: string, opts?: { color?: string }): { readonly svg: string, tintColor: string | null }
1164
1714
  `;
1165
1715
  var README_HINT = `# design/
1166
1716
 
@@ -1187,48 +1737,72 @@ var getContext = (args) => {
1187
1737
  manifest,
1188
1738
  name: manifest?.name ?? basename2(root),
1189
1739
  dirName,
1190
- designDir: join10(root, dirName),
1740
+ designDir: join11(root, dirName),
1191
1741
  designKey: "/" + dirName.replace(/\\/g, "/")
1192
1742
  };
1193
1743
  };
1194
1744
  var listScreens = (designDir) => {
1195
- const dir = join10(designDir, "screens");
1196
- if (!existsSync7(dir))
1745
+ const dir = join11(designDir, "screens");
1746
+ if (!existsSync9(dir))
1197
1747
  return [];
1198
1748
  return readdirSync5(dir).filter((f) => f.endsWith(".ts") && !f.startsWith(".")).map((f) => f.slice(0, -3)).sort();
1199
1749
  };
1200
1750
  var readMetaSizes = (designDir) => {
1201
1751
  try {
1202
- const meta = JSON.parse(readFileSync7(join10(designDir, "meta.json"), "utf8"));
1752
+ const meta = JSON.parse(readFileSync9(join11(designDir, "meta.json"), "utf8"));
1203
1753
  return meta?.screens ?? {};
1204
1754
  } catch {
1205
1755
  return {};
1206
1756
  }
1207
1757
  };
1208
- var init = async (ctx) => {
1758
+ var writeMcpConfig = (ctx, port) => {
1759
+ const path = join11(ctx.root, ".mcp.json");
1760
+ const entry = { type: "http", url: `http://localhost:${port}/mcp` };
1761
+ let config = {};
1762
+ if (existsSync9(path)) {
1763
+ try {
1764
+ config = JSON.parse(readFileSync9(path, "utf8"));
1765
+ } catch {
1766
+ warnErr(".mcp.json exists but isn't valid JSON — left it untouched. Add the lecodes-design server by hand.");
1767
+ return;
1768
+ }
1769
+ }
1770
+ config.mcpServers ??= {};
1771
+ const existing = config.mcpServers["lecodes-design"];
1772
+ if (existing?.url === entry.url) {
1773
+ note("kept .mcp.json (lecodes-design MCP server)");
1774
+ return;
1775
+ }
1776
+ config.mcpServers["lecodes-design"] = entry;
1777
+ writeFileSync8(path, JSON.stringify(config, null, 2) + `
1778
+ `);
1779
+ log(`${c.green(existing ? "updated" : "created")} .mcp.json (lecodes-design MCP → ${entry.url})`);
1780
+ };
1781
+ var init = async (ctx, args) => {
1209
1782
  const files = [
1210
1783
  ["meta.json", META_JSON],
1784
+ ["spec.md", SPEC_MD],
1211
1785
  ["README.md", README_HINT],
1212
1786
  ["CLAUDE.md", CLAUDE_MD],
1213
- [join10("shared", "tokens.ts"), TOKENS_TS],
1214
- [join10("screens", "home.ts"), HOME_SCREEN_TS]
1787
+ [join11("shared", "tokens.ts"), TOKENS_TS],
1788
+ [join11("screens", "home.ts"), HOME_SCREEN_TS]
1215
1789
  ];
1216
- mkdirSync7(join10(ctx.designDir, "screens"), { recursive: true });
1217
- mkdirSync7(join10(ctx.designDir, "shared"), { recursive: true });
1790
+ mkdirSync8(join11(ctx.designDir, "screens"), { recursive: true });
1791
+ mkdirSync8(join11(ctx.designDir, "shared"), { recursive: true });
1218
1792
  let created = 0;
1219
1793
  for (const [rel, content] of files) {
1220
- const abs = join10(ctx.designDir, rel);
1221
- if (existsSync7(abs)) {
1794
+ const abs = join11(ctx.designDir, rel);
1795
+ if (existsSync9(abs)) {
1222
1796
  note(`kept ${ctx.dirName}/${rel.replace(/\\/g, "/")}`);
1223
1797
  continue;
1224
1798
  }
1225
- writeFileSync7(abs, content);
1799
+ writeFileSync8(abs, content);
1226
1800
  log(`${c.green("created")} ${ctx.dirName}/${rel.replace(/\\/g, "/")}`);
1227
1801
  created++;
1228
1802
  }
1229
- const ignorePath = join10(ctx.root, ".lecodesignore");
1803
+ const ignorePath = join11(ctx.root, ".lecodesignore");
1230
1804
  const ignoreLine = `${ctx.dirName}/.snapshots/`;
1231
- const current = existsSync7(ignorePath) ? readFileSync7(ignorePath, "utf8") : "";
1805
+ const current = existsSync9(ignorePath) ? readFileSync9(ignorePath, "utf8") : "";
1232
1806
  if (!current.split(/\r?\n/).includes(ignoreLine)) {
1233
1807
  appendFileSync(ignorePath, (current && !current.endsWith(`
1234
1808
  `) ? `
@@ -1236,6 +1810,14 @@ var init = async (ctx) => {
1236
1810
  `);
1237
1811
  log(`${c.green("ignored")} ${ignoreLine} (in .lecodesignore)`);
1238
1812
  }
1813
+ writeMcpConfig(ctx, Number(flagStr(args, "port")) || DEFAULT_PORT);
1814
+ const wroteTypes = materializeTypesLocal(ctx.designDir);
1815
+ writeTsconfig(ctx.designDir);
1816
+ mkdirSync8(join11(ctx.designDir, ".lecodes", "types"), { recursive: true });
1817
+ writeFileSync8(join11(ctx.designDir, ".lecodes", "types", "design-macros.d.ts"), DESIGN_MACROS_DTS);
1818
+ log(`${c.green("created")} ${ctx.dirName}/tsconfig.json${wroteTypes ? ` + ${ctx.dirName}/.lecodes/types/` : ""} (IDE types)`);
1819
+ if (!wroteTypes)
1820
+ warnErr("Bundled SDK types not found — tsconfig written, but globals may show unresolved until the CLI is rebuilt with vendored types.");
1239
1821
  log("");
1240
1822
  log(created ? "Design canvas ready." : "Design canvas already initialized.");
1241
1823
  log(` ${c.bold("lecodes design")} start the canvas (browser)`);
@@ -1252,12 +1834,28 @@ var snapshot = async (ctx, args) => {
1252
1834
  throw new CliError(`No screen "${id}" (have: ${all.join(", ")}).`);
1253
1835
  }
1254
1836
  const targets = requested.length ? requested : all;
1837
+ const stateFlag = flagStr(args, "state");
1838
+ const allStates = flagBool(args, "all-states");
1839
+ if (stateFlag !== undefined && allStates)
1840
+ throw new CliError("Use either --state <name> or --all-states, not both.");
1255
1841
  let renderer;
1256
1842
  try {
1257
1843
  renderer = await import("lecodes-renderer/headless");
1258
1844
  } catch {
1259
1845
  throw new CliError("The renderer isn't installed. Run: npm i -D lecodes-renderer");
1260
1846
  }
1847
+ let discover = null;
1848
+ if (stateFlag !== undefined || allStates) {
1849
+ try {
1850
+ const ds = await import("lecodes-design/server");
1851
+ discover = (src) => {
1852
+ const d = ds.discoverScreenStates(src);
1853
+ return { states: d.states, canonical: d.canonical };
1854
+ };
1855
+ } catch {
1856
+ throw new CliError("Rendering specific states needs the design canvas. Run: npm i -D lecodes-design");
1857
+ }
1858
+ }
1261
1859
  const killer = setTimeout(() => {
1262
1860
  warnErr("Snapshot timed out after 120s.");
1263
1861
  process.exit(1);
@@ -1265,45 +1863,68 @@ var snapshot = async (ctx, args) => {
1265
1863
  killer.unref?.();
1266
1864
  const png = flagBool(args, "png") || flagStr(args, "png") !== undefined;
1267
1865
  const sizes = readMetaSizes(ctx.designDir);
1268
- const outDir = flagStr(args, "out-dir") ?? join10(ctx.designDir, ".snapshots");
1269
- const single = targets.length === 1 && requested.length === 1 && !png && !flagStr(args, "out-dir");
1866
+ const outDir = flagStr(args, "out-dir") ?? join11(ctx.designDir, ".snapshots");
1867
+ const single = targets.length === 1 && requested.length === 1 && !png && !allStates && !flagStr(args, "out-dir");
1270
1868
  const onConsole = flagBool(args, "logs") ? (level, text) => logErr(`${c.dim(`[${level}]`)} ${text}`) : undefined;
1271
1869
  if (!single)
1272
- mkdirSync7(outDir, { recursive: true });
1870
+ mkdirSync8(outDir, { recursive: true });
1273
1871
  for (const id of targets) {
1274
- note(`Rendering ${id}…`);
1275
- const js = await compileDesignScreen({
1276
- root: ctx.root,
1277
- manifest: ctx.manifest,
1278
- name: ctx.name,
1279
- designKey: ctx.designKey,
1280
- screenId: id,
1281
- assets: "disk"
1282
- });
1283
1872
  const [width, height] = sizes[id]?.size ?? [390, 844];
1284
- const result = await renderer.renderToJson(js, { width, height, localAssets: true, onConsole });
1285
- for (const w of result.warnings)
1286
- warnErr(`${id}: ${w}`);
1287
- if (single) {
1288
- const text = result.text.endsWith(`
1873
+ let plan = [{ state: undefined, bases: [id] }];
1874
+ if (discover) {
1875
+ const { states, canonical } = discover(readFileSync9(join11(ctx.designDir, "screens", `${id}.ts`), "utf8"));
1876
+ if (stateFlag !== undefined) {
1877
+ if (!states.includes(stateFlag)) {
1878
+ if (requested.length === 1)
1879
+ throw new CliError(`No state "${stateFlag}" on ${id} (has: ${states.join(", ")}).`);
1880
+ warnErr(`${id}: no state "${stateFlag}" (has: ${states.join(", ")}) — skipped.`);
1881
+ continue;
1882
+ }
1883
+ plan = [{ state: stateFlag, bases: [`${id}@${stateFlag}`] }];
1884
+ } else {
1885
+ plan = states.map((st) => ({ state: st, bases: st === canonical ? [`${id}@${st}`, id] : [`${id}@${st}`] }));
1886
+ }
1887
+ }
1888
+ for (const { state, bases } of plan) {
1889
+ const tag = state ? `${id}@${state}` : id;
1890
+ note(`Rendering ${tag}…`);
1891
+ const js = await compileDesignScreen({
1892
+ root: ctx.root,
1893
+ manifest: ctx.manifest,
1894
+ name: ctx.name,
1895
+ designDir: ctx.designDir,
1896
+ designKey: ctx.designKey,
1897
+ screenId: id,
1898
+ state,
1899
+ assets: "disk"
1900
+ });
1901
+ const result = await renderer.renderToJson(js, { width, height, localAssets: true, onConsole });
1902
+ for (const w of result.warnings)
1903
+ warnErr(`${tag}: ${w}`);
1904
+ if (single) {
1905
+ const text = result.text.endsWith(`
1289
1906
  `) ? result.text : result.text + `
1290
1907
  `;
1291
- process.stdout.write(text);
1292
- return;
1293
- }
1294
- writeFileSync7(join10(outDir, `${id}.json`), result.text);
1295
- log(`${c.green("✓")} ${id} → ${join10(outDir, `${id}.json`)}`);
1296
- if (png) {
1297
- const pngResult = await renderer.renderToPng(js, { width, height, localAssets: true, scale: 2, onConsole });
1298
- for (const w of pngResult.warnings)
1299
- warnErr(`${id}: ${w}`);
1300
- writeFileSync7(join10(outDir, `${id}.png`), pngResult.png);
1301
- log(`${c.green("✓")} ${id} → ${join10(outDir, `${id}.png`)}`);
1908
+ process.stdout.write(text);
1909
+ return;
1910
+ }
1911
+ const pngResult = png ? await renderer.renderToPng(js, { width, height, localAssets: true, scale: 2, onConsole }) : null;
1912
+ if (pngResult)
1913
+ for (const w of pngResult.warnings)
1914
+ warnErr(`${tag}: ${w}`);
1915
+ for (const base of bases) {
1916
+ writeFileSync8(join11(outDir, `${base}.json`), result.text);
1917
+ log(`${c.green("✓")} ${tag} → ${join11(outDir, `${base}.json`)}`);
1918
+ if (pngResult) {
1919
+ writeFileSync8(join11(outDir, `${base}.png`), pngResult.png);
1920
+ log(`${c.green("✓")} ${tag} → ${join11(outDir, `${base}.png`)}`);
1921
+ }
1922
+ }
1302
1923
  }
1303
1924
  }
1304
1925
  };
1305
1926
  var serve = async (ctx, args) => {
1306
- if (!existsSync7(ctx.designDir)) {
1927
+ if (!existsSync9(ctx.designDir)) {
1307
1928
  throw new CliError(`No ${ctx.dirName}/ folder here. Run "lecodes design init" to scaffold it.`);
1308
1929
  }
1309
1930
  let designServer;
@@ -1313,30 +1934,86 @@ var serve = async (ctx, args) => {
1313
1934
  throw new CliError("The design canvas isn't installed. Run: npm i -D lecodes-design");
1314
1935
  }
1315
1936
  const port = Number(flagStr(args, "port")) || DEFAULT_PORT;
1937
+ let renderer = null;
1938
+ try {
1939
+ renderer = await import("lecodes-renderer/headless");
1940
+ } catch {
1941
+ renderer = null;
1942
+ }
1943
+ const renderScreen = renderer ? async (id, format, state) => {
1944
+ const r = renderer;
1945
+ const js = await compileDesignScreen({
1946
+ root: ctx.root,
1947
+ manifest: ctx.manifest,
1948
+ name: ctx.name,
1949
+ designDir: ctx.designDir,
1950
+ designKey: ctx.designKey,
1951
+ screenId: id,
1952
+ state,
1953
+ assets: "disk"
1954
+ });
1955
+ const [width, height] = readMetaSizes(ctx.designDir)[id]?.size ?? [390, 844];
1956
+ const warnings = [];
1957
+ let json;
1958
+ let png;
1959
+ if (format === "json" || format === "both") {
1960
+ const res = await r.renderToJson(js, { width, height, localAssets: true });
1961
+ json = res.text;
1962
+ warnings.push(...res.warnings);
1963
+ }
1964
+ if (format === "png" || format === "both") {
1965
+ const res = await r.renderToPng(js, { width, height, localAssets: true, scale: 2 });
1966
+ png = res.png;
1967
+ warnings.push(...res.warnings);
1968
+ }
1969
+ return { json, png, warnings };
1970
+ } : undefined;
1316
1971
  const server = await designServer.startDesignServer({
1317
1972
  root: ctx.root,
1318
1973
  designDir: ctx.designDir,
1319
1974
  name: ctx.name,
1320
1975
  port,
1321
1976
  log: note,
1322
- compileScreen: async (id) => ({
1977
+ compileScreen: async (id, state) => ({
1323
1978
  js: await compileDesignScreen({
1324
1979
  root: ctx.root,
1325
1980
  manifest: ctx.manifest,
1326
1981
  name: ctx.name,
1982
+ designDir: ctx.designDir,
1327
1983
  designKey: ctx.designKey,
1328
1984
  screenId: id,
1985
+ state,
1329
1986
  assets: "server"
1330
1987
  }),
1331
1988
  warnings: []
1332
- })
1989
+ }),
1990
+ renderScreen
1333
1991
  });
1334
1992
  log(`${c.bold("LeCodes Design")} — ${ctx.name}`);
1335
1993
  log(`Canvas: ${c.bold(server.url)}`);
1994
+ log(`MCP: ${c.bold(server.url + "/mcp")}${renderer ? "" : c.dim(" (render_screen off — npm i -D lecodes-renderer)")}`);
1336
1995
  log(`Watching: ${ctx.dirName}/ (screens hot-reload on save; Ctrl+C to stop)`);
1337
1996
  if (!flagBool(args, "no-open"))
1338
1997
  openBrowser(server.url);
1339
1998
  };
1999
+ var arrange = async (ctx, args) => {
2000
+ if (!existsSync9(ctx.designDir)) {
2001
+ throw new CliError(`No ${ctx.dirName}/ folder here. Run "lecodes design init" to scaffold it.`);
2002
+ }
2003
+ let designServer;
2004
+ try {
2005
+ designServer = await import("lecodes-design/server");
2006
+ } catch {
2007
+ throw new CliError("The design canvas isn't installed. Run: npm i -D lecodes-design");
2008
+ }
2009
+ const onlyNew = flagBool(args, "new");
2010
+ const summary = designServer.arrangeDesign(ctx.designDir, { onlyNew });
2011
+ if (!summary.changed) {
2012
+ log(onlyNew ? "No unplaced screens — nothing to arrange." : "Already tidy — the current layout wins, nothing moved.");
2013
+ return;
2014
+ }
2015
+ log(`${c.green("✓")} ${onlyNew ? "Placed" : "Arranged"} ${summary.moved} screen${summary.moved === 1 ? "" : "s"} in ${ctx.dirName}/meta.json.`);
2016
+ };
1340
2017
  var share = async (ctx, args) => {
1341
2018
  if (!ctx.manifest) {
1342
2019
  throw new CliError("`lecodes design share` needs a cloned project. Run it inside one, and push the design first (lecodes push).");
@@ -1355,22 +2032,73 @@ var share = async (ctx, args) => {
1355
2032
  log(` ${c.bold(res.url ?? "")}`);
1356
2033
  note("Turn it off with: lecodes design share --off");
1357
2034
  };
2035
+ var check = async (ctx, args) => {
2036
+ if (!existsSync9(ctx.designDir)) {
2037
+ throw new CliError(`No ${ctx.dirName}/ folder here. Run "lecodes design init" to scaffold it.`);
2038
+ }
2039
+ let designServer;
2040
+ try {
2041
+ designServer = await import("lecodes-design/server");
2042
+ } catch {
2043
+ throw new CliError("The design canvas isn't installed. Run: npm i -D lecodes-design");
2044
+ }
2045
+ const state = designServer.readDesignState(ctx.designDir);
2046
+ const concept = designServer.conceptReport(ctx.designDir, state.screens);
2047
+ if (state.warnings.length) {
2048
+ log(c.bold(`Warnings (${state.warnings.length})`));
2049
+ for (const w of state.warnings)
2050
+ log(` ${c.yellow("!")} ${w}`);
2051
+ log("");
2052
+ }
2053
+ log(c.bold("Concept"));
2054
+ log(` spec.md: ${concept.hasSpec ? c.green("present") : c.dim("missing (optional)")}`);
2055
+ if (concept.screenRefs.length) {
2056
+ const resolved = concept.screenRefs.filter((r) => r.exists).length;
2057
+ log(` [[links]]: ${resolved}/${concept.screenRefs.length} resolve`);
2058
+ }
2059
+ if (concept.screensWithoutRole.length)
2060
+ log(` ${c.dim("no role:")} ${concept.screensWithoutRole.join(", ")}`);
2061
+ log("");
2062
+ const stateful = state.screens.filter((s) => s.states.length > 1);
2063
+ const undiscoverable = state.screens.filter((s) => s.statesWarning);
2064
+ if (stateful.length || undiscoverable.length) {
2065
+ log(c.bold("States"));
2066
+ for (const s of stateful) {
2067
+ log(` ${s.id}: ${s.states.map((st) => st === s.canonical ? c.green(st) : st).join(", ")}`);
2068
+ }
2069
+ for (const s of undiscoverable)
2070
+ log(` ${c.yellow("!")} ${s.id}: ${s.statesWarning}`);
2071
+ log("");
2072
+ }
2073
+ const hard = state.warnings.length;
2074
+ if (hard === 0) {
2075
+ const n = state.screens.length;
2076
+ log(`${c.green("✓")} No coherence problems${n ? ` across ${n} screen${n === 1 ? "" : "s"}` : ""}.`);
2077
+ return;
2078
+ }
2079
+ log(`${c.red("✗")} ${hard} coherence warning${hard === 1 ? "" : "s"} — see above.`);
2080
+ process.exitCode = 1;
2081
+ };
1358
2082
  var design = async (args) => {
1359
2083
  const sub = args._[0] ?? "serve";
1360
2084
  const ctx = getContext(args);
1361
2085
  if (sub === "serve")
1362
2086
  return serve(ctx, args);
1363
2087
  if (sub === "init")
1364
- return init(ctx);
2088
+ return init(ctx, args);
1365
2089
  if (sub === "snapshot")
1366
2090
  return snapshot(ctx, args);
2091
+ if (sub === "arrange")
2092
+ return arrange(ctx, args);
2093
+ if (sub === "check")
2094
+ return check(ctx, args);
1367
2095
  if (sub === "share")
1368
2096
  return share(ctx, args);
1369
- throw new CliError(`Unknown design subcommand "${sub}" (serve | init | snapshot | share).`);
2097
+ throw new CliError(`Unknown design subcommand "${sub}" (serve | init | snapshot | arrange | check | share).`);
1370
2098
  };
1371
2099
 
1372
2100
  // src/serverDiff.ts
1373
- import { readFileSync as readFileSync8 } from "node:fs";
2101
+ import { readFileSync as readFileSync10 } from "node:fs";
1374
2102
  import { basename as basename3 } from "node:path";
1375
2103
  var isTextAsset = (t) => t === "text" || t === "shader" || t === "scene";
1376
2104
  var computeServerDiff = (project, scan) => {
@@ -1389,7 +2117,7 @@ var computeServerDiff = (project, scan) => {
1389
2117
  const local = localByPath.get(path);
1390
2118
  if (local && !server) {
1391
2119
  const text = isTextKind(classifyKind(basename3(path)));
1392
- entries.push({ path, status: "added", binary: !text, serverText: "", localText: text ? readFileSync8(local.absPath, "utf8") : undefined });
2120
+ entries.push({ path, status: "added", binary: !text, serverText: "", localText: text ? readFileSync10(local.absPath, "utf8") : undefined });
1393
2121
  continue;
1394
2122
  }
1395
2123
  if (server && !local) {
@@ -1400,12 +2128,12 @@ var computeServerDiff = (project, scan) => {
1400
2128
  if (server && local) {
1401
2129
  if (isTextAsset(server.type)) {
1402
2130
  const serverText = server.text ?? "";
1403
- const localText = readFileSync8(local.absPath, "utf8");
2131
+ const localText = readFileSync10(local.absPath, "utf8");
1404
2132
  if (serverText !== localText)
1405
2133
  entries.push({ path, status: "modified", binary: false, serverText, localText });
1406
2134
  } else {
1407
2135
  const serverSize = server.file?.size;
1408
- const localSize = Buffer.byteLength(readFileSync8(local.absPath));
2136
+ const localSize = Buffer.byteLength(readFileSync10(local.absPath));
1409
2137
  if (serverSize !== undefined && serverSize !== localSize) {
1410
2138
  entries.push({ path, status: "modified", binary: true });
1411
2139
  }
@@ -1569,11 +2297,11 @@ var login = async (args) => {
1569
2297
  };
1570
2298
 
1571
2299
  // src/commands/preview.ts
1572
- import { writeFileSync as writeFileSync8 } from "node:fs";
2300
+ import { writeFileSync as writeFileSync9 } from "node:fs";
1573
2301
  import { resolve as resolve3 } from "node:path";
1574
2302
 
1575
2303
  // src/compile/collect.ts
1576
- import { readFileSync as readFileSync9 } from "node:fs";
2304
+ import { readFileSync as readFileSync11 } from "node:fs";
1577
2305
  import { basename as basename4 } from "node:path";
1578
2306
  var collectEntries = (root, manifest, project) => {
1579
2307
  const pathById = buildPaths(project.assets);
@@ -1598,7 +2326,7 @@ var collectEntries = (root, manifest, project) => {
1598
2326
  warnings.push(`No compiled shader for ${file.path} — push it to compile the shader.`);
1599
2327
  entries.push({ path: file.path, type, shaders });
1600
2328
  } else {
1601
- entries.push({ path: file.path, type, text: readFileSync9(file.absPath, "utf8") });
2329
+ entries.push({ path: file.path, type, text: readFileSync11(file.absPath, "utf8") });
1602
2330
  }
1603
2331
  }
1604
2332
  return { entries, warnings };
@@ -1628,7 +2356,7 @@ var preview = async (args) => {
1628
2356
  if (out && isNullSink(out)) {
1629
2357
  note(`Discarded output (${out})`);
1630
2358
  } else if (out) {
1631
- writeFileSync8(resolve3(out), js);
2359
+ writeFileSync9(resolve3(out), js);
1632
2360
  note(`Wrote ${out} (${js.length} bytes)`);
1633
2361
  } else {
1634
2362
  process.stdout.write(js);
@@ -1640,7 +2368,7 @@ var preview = async (args) => {
1640
2368
  };
1641
2369
 
1642
2370
  // src/commands/render.ts
1643
- import { writeFileSync as writeFileSync9 } from "node:fs";
2371
+ import { writeFileSync as writeFileSync10 } from "node:fs";
1644
2372
  import { resolve as resolve4 } from "node:path";
1645
2373
  var LOCAL_ASSET_SCHEME2 = "lecodesfile://";
1646
2374
  var DEVICE_SAFE_AREA = {
@@ -1737,7 +2465,7 @@ var render = async (args) => {
1737
2465
  if (isNullSink(pngFlag)) {
1738
2466
  note(`Discarded PNG (${pngFlag})`);
1739
2467
  } else {
1740
- writeFileSync9(resolve4(pngFlag), png);
2468
+ writeFileSync10(resolve4(pngFlag), png);
1741
2469
  note(`Wrote ${pngFlag} (${png.length} bytes, ${width}x${height})`);
1742
2470
  }
1743
2471
  return done((cb) => cb());
@@ -1752,7 +2480,7 @@ var render = async (args) => {
1752
2480
  return done((cb) => cb());
1753
2481
  }
1754
2482
  if (out) {
1755
- writeFileSync9(resolve4(out), result.text);
2483
+ writeFileSync10(resolve4(out), result.text);
1756
2484
  note(`Wrote ${out} (${result.text.length} bytes)`);
1757
2485
  return done((cb) => cb());
1758
2486
  }
@@ -1763,8 +2491,8 @@ var render = async (args) => {
1763
2491
  };
1764
2492
 
1765
2493
  // src/commands/pull.ts
1766
- import { existsSync as existsSync8, mkdirSync as mkdirSync8, rmSync, writeFileSync as writeFileSync10 } from "node:fs";
1767
- import { dirname as dirname6, join as join11 } from "node:path";
2494
+ import { existsSync as existsSync10, mkdirSync as mkdirSync9, rmSync, writeFileSync as writeFileSync11 } from "node:fs";
2495
+ import { dirname as dirname7, join as join12 } from "node:path";
1768
2496
  var pull = async (args) => {
1769
2497
  const root = findProjectRoot(process.cwd());
1770
2498
  const manifest = readManifest(root);
@@ -1784,8 +2512,8 @@ var pull = async (args) => {
1784
2512
  const remotePaths = new Set([...buildPaths(project.assets).entries()].filter(([id]) => project.assets.find((a) => a.id === id)?.type !== "folder").map(([, path]) => path));
1785
2513
  for (const path of Object.keys(manifest.files)) {
1786
2514
  if (!remotePaths.has(path)) {
1787
- const abs = join11(root, ...path.replace(/^\//, "").split("/"));
1788
- if (existsSync8(abs))
2515
+ const abs = join12(root, ...path.replace(/^\//, "").split("/"));
2516
+ if (existsSync10(abs))
1789
2517
  rmSync(abs);
1790
2518
  }
1791
2519
  }
@@ -1823,18 +2551,18 @@ var pullPaths = async (root, manifest, project, paths, apiUrl, token) => {
1823
2551
  const files = { ...manifest.files };
1824
2552
  for (const asset of targets) {
1825
2553
  const key = pathById.get(asset.id);
1826
- const abs = join11(root, ...key.replace(/^\//, "").split("/"));
1827
- mkdirSync8(dirname6(abs), { recursive: true });
2554
+ const abs = join12(root, ...key.replace(/^\//, "").split("/"));
2555
+ mkdirSync9(dirname7(abs), { recursive: true });
1828
2556
  let sha;
1829
2557
  if (asset.type === "file") {
1830
2558
  if (!asset.file?.src)
1831
2559
  continue;
1832
2560
  const bytes = await downloadBlob(apiUrl, asset.file.src);
1833
- writeFileSync10(abs, bytes);
2561
+ writeFileSync11(abs, bytes);
1834
2562
  sha = sha256(bytes);
1835
2563
  } else {
1836
2564
  const text = asset.text ?? "";
1837
- writeFileSync10(abs, text);
2565
+ writeFileSync11(abs, text);
1838
2566
  sha = sha256(text);
1839
2567
  }
1840
2568
  files[key] = { id: asset.id, type: kindOf2(asset.type), sha };
@@ -1847,7 +2575,7 @@ var pullPaths = async (root, manifest, project, paths, apiUrl, token) => {
1847
2575
 
1848
2576
  // src/commands/push.ts
1849
2577
  import { basename as basename5 } from "node:path";
1850
- import { readFileSync as readFileSync10 } from "node:fs";
2578
+ import { readFileSync as readFileSync12 } from "node:fs";
1851
2579
  var push = async (args) => {
1852
2580
  const root = findProjectRoot(process.cwd());
1853
2581
  const manifest = readManifest(root);
@@ -1906,9 +2634,9 @@ var push = async (args) => {
1906
2634
  if (source)
1907
2635
  entry.assetId = source.id;
1908
2636
  if (isTextKind(kind)) {
1909
- entry.text = readFileSync10(file.absPath, "utf8");
2637
+ entry.text = readFileSync12(file.absPath, "utf8");
1910
2638
  } else if (!source || source.sha !== file.sha) {
1911
- const blob = await uploadBlob(apiUrl, token, manifest.uuid, basename5(file.path), readFileSync10(file.absPath));
2639
+ const blob = await uploadBlob(apiUrl, token, manifest.uuid, basename5(file.path), readFileSync12(file.absPath));
1912
2640
  entry.blobFileId = blob.id;
1913
2641
  uploaded++;
1914
2642
  }
@@ -2049,8 +2777,8 @@ Config lives in ~/.lecodes/config.json (override with LECODES_API / LECODES_TOKE
2049
2777
  var commands = { login, create, clone, status, diff, pull, push, preview, render, design };
2050
2778
  var readVersion = () => {
2051
2779
  try {
2052
- const here2 = dirname7(fileURLToPath3(import.meta.url));
2053
- return JSON.parse(readFileSync11(join12(here2, "..", "package.json"), "utf8")).version ?? "unknown";
2780
+ const here2 = dirname8(fileURLToPath4(import.meta.url));
2781
+ return JSON.parse(readFileSync13(join13(here2, "..", "package.json"), "utf8")).version ?? "unknown";
2054
2782
  } catch {
2055
2783
  return "unknown";
2056
2784
  }