lecodes-cli 0.5.2 → 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
@@ -170,6 +170,8 @@ var uploadBlob = (apiUrl, token, uuid, name, bytes) => {
170
170
  };
171
171
  var sync = (apiUrl, token, uuid, payload) => apiRequest(apiUrl, `/projects/${uuid}/sync`, { token, body: payload });
172
172
  var publishProject = (apiUrl, token, uuid) => apiRequest(apiUrl, `/projects/${uuid}/publish`, { token, body: {} });
173
+ var enableDesignShare = (apiUrl, token, uuid) => apiRequest(apiUrl, `/projects/${uuid}/design/share`, { token, body: {} });
174
+ var disableDesignShare = (apiUrl, token, uuid) => apiRequest(apiUrl, `/projects/${uuid}/design/share`, { token, method: "DELETE" });
173
175
  var downloadBlob = async (apiUrl, src) => {
174
176
  const resp = await fetch(`${apiUrl}${src}`);
175
177
  if (!resp.ok)
@@ -284,7 +286,8 @@ var materializeAssets = async (root, project, apiUrl, token, onProgress) => {
284
286
 
285
287
  // src/types.ts
286
288
  import { dirname as dirname3, join as join4 } from "node:path";
287
- 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";
288
291
  var TSCONFIG = {
289
292
  compilerOptions: {
290
293
  target: "ES2020",
@@ -301,8 +304,7 @@ var TSCONFIG = {
301
304
  include: [`${LECODES_DIR}/types/**/*.d.ts`, "**/*.ts", "**/*.js"],
302
305
  exclude: ["node_modules"]
303
306
  };
304
- var materializeTypes = async (root, apiUrl) => {
305
- const bundle = await getTypes(apiUrl);
307
+ var writeTypeBundle = (root, bundle) => {
306
308
  const typesDir = join4(root, LECODES_DIR, "types");
307
309
  for (const [path, text] of Object.entries(bundle)) {
308
310
  const dest = join4(typesDir, path);
@@ -310,12 +312,37 @@ var materializeTypes = async (root, apiUrl) => {
310
312
  writeFileSync4(dest, text);
311
313
  }
312
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
+ };
313
340
  var writeTsconfig = (root) => {
314
341
  writeFileSync4(join4(root, "tsconfig.json"), JSON.stringify(TSCONFIG, null, 2));
315
342
  };
316
343
 
317
344
  // src/ignore.ts
318
- 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";
319
346
  import { join as join5 } from "node:path";
320
347
  var BUILTIN_PATTERNS = ["node_modules/", ".git/", "_*", "*.log", ".DS_Store"];
321
348
  var DEFAULT_IGNORE_FILE = `# Files matched here are NOT pushed to LeCodes.
@@ -393,9 +420,9 @@ var buildMatcher = (rules) => (key, isDir) => {
393
420
  var loadIgnore = (root) => {
394
421
  const lines = [...BUILTIN_PATTERNS];
395
422
  const file = join5(root, ".lecodesignore");
396
- if (existsSync2(file)) {
423
+ if (existsSync3(file)) {
397
424
  try {
398
- lines.push(...readFileSync3(file, "utf8").split(/\r?\n/));
425
+ lines.push(...readFileSync4(file, "utf8").split(/\r?\n/));
399
426
  } catch {}
400
427
  }
401
428
  const rules = lines.map(compile).filter((r) => r !== null);
@@ -403,7 +430,7 @@ var loadIgnore = (root) => {
403
430
  };
404
431
  var writeDefaultIgnore = (root) => {
405
432
  const file = join5(root, ".lecodesignore");
406
- if (!existsSync2(file))
433
+ if (!existsSync3(file))
407
434
  writeFileSync5(file, DEFAULT_IGNORE_FILE);
408
435
  };
409
436
 
@@ -426,7 +453,7 @@ var clone = async (args) => {
426
453
  const project = await getProject(apiUrl, token, uuid);
427
454
  const dir = args._[1] ?? slug(project.name);
428
455
  const root = resolve(dir);
429
- if (existsSync3(root) && readdirSync(root).length > 0) {
456
+ if (existsSync4(root) && readdirSync(root).length > 0) {
430
457
  throw new CliError(`Directory "${dir}" already exists and is not empty.`);
431
458
  }
432
459
  mkdirSync5(root, { recursive: true });
@@ -446,7 +473,7 @@ var clone = async (args) => {
446
473
  };
447
474
 
448
475
  // src/commands/create.ts
449
- 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";
450
477
  import { join as join6, resolve as resolve2 } from "node:path";
451
478
  var slug2 = (name) => name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "project";
452
479
  var STARTER = `// Welcome to le.codes — no imports needed, the whole SDK is available as globals.
@@ -466,7 +493,7 @@ var create = async (args) => {
466
493
  throw new CliError("Usage: lecodes create <name> [directory]");
467
494
  const dir = args._[1] ?? slug2(name);
468
495
  const root = resolve2(dir);
469
- if (existsSync4(root) && readdirSync2(root).length > 0) {
496
+ if (existsSync5(root) && readdirSync2(root).length > 0) {
470
497
  throw new CliError(`Directory "${dir}" already exists and is not empty.`);
471
498
  }
472
499
  mkdirSync6(root, { recursive: true });
@@ -484,15 +511,15 @@ var create = async (args) => {
484
511
  };
485
512
 
486
513
  // src/commands/design.ts
487
- import { existsSync as existsSync7, mkdirSync as mkdirSync7, readFileSync as readFileSync7, readdirSync as readdirSync5, writeFileSync as writeFileSync7, appendFileSync } from "node:fs";
488
- 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";
489
516
 
490
517
  // src/compile/designCompile.ts
491
- import { readFileSync as readFileSync6 } from "node:fs";
518
+ import { readFileSync as readFileSync8 } from "node:fs";
492
519
  import { basename } from "node:path";
493
520
 
494
521
  // src/localFiles.ts
495
- import { readdirSync as readdirSync3, readFileSync as readFileSync4 } from "node:fs";
522
+ import { readdirSync as readdirSync3, readFileSync as readFileSync5 } from "node:fs";
496
523
  import { join as join7, relative } from "node:path";
497
524
  var SKIP_DIRS = new Set([".lecodes", ".git", "node_modules"]);
498
525
  var SKIP_FILES = new Set(["tsconfig.json", "jsconfig.json", ".gitignore", ".lecodesignore", ".DS_Store"]);
@@ -567,7 +594,7 @@ var scanLocal = (root, opts = {}) => {
567
594
  ignored.push(key);
568
595
  continue;
569
596
  }
570
- files.push({ path: key, absPath: abs, sha: sha256(readFileSync4(abs)) });
597
+ files.push({ path: key, absPath: abs, sha: sha256(readFileSync5(abs)) });
571
598
  }
572
599
  }
573
600
  };
@@ -601,16 +628,16 @@ var diffLocal = (manifest, scan) => {
601
628
  var hasChanges = (diff) => diff.added.length + diff.modified.length + diff.deleted.length > 0;
602
629
 
603
630
  // src/compile/projectCompile.ts
604
- import { existsSync as existsSync6 } from "node:fs";
631
+ import { existsSync as existsSync7 } from "node:fs";
605
632
  import { createRequire as createRequire3 } from "node:module";
606
633
  import { dirname as dirname5, join as join9 } from "node:path";
607
- import { fileURLToPath as fileURLToPath2 } from "node:url";
634
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
608
635
  // ../sdk/src/compile/bundler.ts
609
- 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";
610
637
  import { spawnSync } from "node:child_process";
611
638
  import { createRequire as createRequire2 } from "node:module";
612
639
  import { dirname as dirname4, join as join8, relative as relative2 } from "node:path";
613
- import { fileURLToPath } from "node:url";
640
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
614
641
 
615
642
  // ../sdk/src/compile/assetMacro.ts
616
643
  var ASSET_RE = /\basset\(\s*(["'])((?:[^"'\\]|\\.)*)\1\s*\)/g;
@@ -670,11 +697,11 @@ var classBodyDeclaresAspect = (src, from) => {
670
697
  // ../sdk/src/compile/bundler.ts
671
698
  var resolveInject = () => {
672
699
  const override = process.env.LECODES_SDK_INJECT;
673
- if (override && existsSync5(override))
700
+ if (override && existsSync6(override))
674
701
  return override;
675
- const here = dirname4(fileURLToPath(import.meta.url));
702
+ const here = dirname4(fileURLToPath2(import.meta.url));
676
703
  const src = join8(here, "..", "inject.ts");
677
- if (existsSync5(src))
704
+ if (existsSync6(src))
678
705
  return src;
679
706
  throw new Error("sdk: could not locate the inject source (LECODES_SDK_INJECT / src/inject.ts)");
680
707
  };
@@ -712,7 +739,7 @@ var collectSdkFiles = (sdkSrc) => {
712
739
  if (statSync(p).isDirectory())
713
740
  walk(p);
714
741
  else if (isSdkSource(p))
715
- out["/__sdk/" + relative2(sdkSrc, p).split("\\").join("/")] = readFileSync5(p, "utf8");
742
+ out["/__sdk/" + relative2(sdkSrc, p).split("\\").join("/")] = readFileSync6(p, "utf8");
716
743
  }
717
744
  };
718
745
  walk(sdkSrc);
@@ -722,13 +749,13 @@ var collectSdkFiles = (sdkSrc) => {
722
749
  var resolveChiselBin = () => {
723
750
  const override = process.env.LECODES_CHISEL_BIN;
724
751
  if (override) {
725
- if (existsSync5(override))
752
+ if (existsSync6(override))
726
753
  return override;
727
754
  throw new Error(`LECODES_CHISEL_BIN points at a missing file: ${override}`);
728
755
  }
729
756
  try {
730
757
  const bin = createRequire2(import.meta.url)("@letary/chisel").binaryPath();
731
- if (bin && existsSync5(bin))
758
+ if (bin && existsSync6(bin))
732
759
  return bin;
733
760
  } catch {}
734
761
  throw new Error("chisel binary not found. Install @letary/chisel, or set LECODES_CHISEL_BIN.");
@@ -888,9 +915,9 @@ ${inlineSourceMapComment(map)}` : code;
888
915
  ${inlineSourceMapComment(offsetSourceMap(map, headerLines))}`;
889
916
  };
890
917
  // src/compile/projectCompile.ts
891
- var here = dirname5(fileURLToPath2(import.meta.url));
918
+ var here = dirname5(fileURLToPath3(import.meta.url));
892
919
  var vendoredInject = join9(here, "..", "runtime", "sdk", "inject.ts");
893
- if (existsSync6(vendoredInject))
920
+ if (existsSync7(vendoredInject))
894
921
  process.env.LECODES_SDK_INJECT = vendoredInject;
895
922
  var resolveChiselBin2 = () => {
896
923
  if (process.env.LECODES_CHISEL_BIN)
@@ -914,10 +941,379 @@ var compileProject2 = (opts) => compileProject({
914
941
  localAssets: opts.localAssets
915
942
  });
916
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
+
917
1313
  // src/compile/designCompile.ts
918
1314
  var LOCAL_ASSET_SCHEME = "lecodesfile://";
919
1315
  var compileDesignScreen = async (opts) => {
920
- const { root, manifest, designKey, screenId } = opts;
1316
+ const { root, manifest, designDir, designKey, screenId, state } = opts;
921
1317
  const entries = [];
922
1318
  const scan = scanLocal(root, { ignore: loadIgnore(root), tracked: new Set(Object.keys(manifest?.files ?? {})) });
923
1319
  for (const file of scan.files) {
@@ -932,17 +1328,20 @@ var compileDesignScreen = async (opts) => {
932
1328
  } else if (type === "shader") {
933
1329
  entries.push({ path: file.path, type, shaders: [] });
934
1330
  } else {
935
- entries.push({ path: file.path, type, text: readFileSync6(file.absPath, "utf8") });
1331
+ entries.push({ path: file.path, type, text: readFileSync8(file.absPath, "utf8") });
936
1332
  }
937
1333
  }
1334
+ const invoke = state !== undefined ? `screen(${JSON.stringify(state)})` : "screen()";
938
1335
  const entryPath = `${designKey}/.screen-entry.ts`;
939
1336
  entries.push({
940
1337
  path: entryPath,
941
1338
  type: "text",
942
1339
  text: `import screen from "./screens/${screenId}"
943
- screen.open()
1340
+ ` + `const __screen = typeof screen === "function" ? ${invoke} : screen
1341
+ ` + `__screen.open()
944
1342
  `
945
1343
  });
1344
+ await applyAssetIcons(entries, designDir);
946
1345
  return compileProject2({
947
1346
  entries,
948
1347
  name: opts.name,
@@ -1034,11 +1433,45 @@ var browserLogin = async (webBase, timeoutMs = 5 * 60 * 1000, open = openBrowser
1034
1433
  // src/commands/designTemplates.ts
1035
1434
  var META_JSON = `{
1036
1435
  "screens": {
1037
- "home": { "pos": [0, 0], "desc": "Example screen — replace me" }
1436
+ "home": { "pos": [0, 0], "desc": "Example screen — replace me", "role": "list" }
1038
1437
  },
1039
1438
  "edges": []
1040
1439
  }
1041
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
+ `;
1042
1475
  var TOKENS_TS = `// Design tokens shared by every screen. Colors are hex strings (the engine parses hex only).
1043
1476
  export const colors = {
1044
1477
  bg: "#101114",
@@ -1108,24 +1541,60 @@ This is an intent-capture stage — screens run on inline mock data, before any
1108
1541
 
1109
1542
  \`meta.json\` is the app map: every screen (position, description) and the edges between them
1110
1543
  (navigation intent). **Read it before opening any screen file.** It is the human's file — the
1111
- canvas and the developer own positions and descriptions. You MAY add entries for new screens you
1112
- create (pick a free position: screens are 390×844, leave ~160px gaps) and add edges when the flow
1113
- 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.
1114
1550
 
1115
1551
  \`\`\`json
1116
1552
  {
1117
- "screens": { "login": { "pos": [0, 0], "desc": "Phone login" } },
1118
- "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
+ ]
1119
1562
  }
1120
1563
  \`\`\`
1121
1564
 
1122
1565
  - An edge's optional \`activator\` names the element (its \`name\` style option) that triggers the
1123
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.
1124
1592
 
1125
1593
  ## Screen rules
1126
1594
 
1127
- - **One file = one screen.** \`screens/<id>.ts\` default-exports an already-computed screen:
1128
- \`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.
1129
1598
  - **Styles go through the chained \`.style({ … })\` method** — e.g.
1130
1599
  \`UIText("Today").style({ fontSize: 28, fontWeight: 700 })\`. Passing a style object as the
1131
1600
  first argument is a legacy form.
@@ -1137,28 +1606,111 @@ is clear, but never reposition or rewrite existing human-authored descriptions.
1137
1606
  every screen. Promoting a repeated pattern into \`shared/\` is an explicit, deliberate step.
1138
1607
  - The LeCodes UI API (\`UIScreen\`, \`UIColumn\`, \`UIRow\`, \`UIBox\`, \`UIText\`, \`UIImage\`,
1139
1608
  \`UIButton\`, \`UIInput\`, \`UISpacer\`, \`UIScrollable\`, …) is **global** — no imports needed.
1140
- 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.
1141
1663
 
1142
1664
  ## See what you built
1143
1665
 
1144
1666
  The developer watches the canvas live (screens hot-reload on save). To see a screen yourself:
1145
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
+
1146
1681
  \`\`\`sh
1147
- lecodes design snapshot <id> # semantic JSON of the laid-out screen → stdout
1148
- lecodes design snapshot <id> --png # + PNG screenshot → design/.snapshots/<id>.png
1149
- 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)
1150
1688
  \`\`\`
1151
1689
 
1152
- Snapshot after every screen you create or change, and fix what looks wrong (overflow, overlap,
1153
- 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.
1154
1692
 
1155
1693
  ## Workflow
1156
1694
 
1157
- 1. Read \`meta.json\`, then the \`shared/\` signatures, then only the screens you need.
1158
- 2. Create/edit screen files; register new screens (and clear edges) in \`meta.json\`.
1159
- 3. Snapshot → verify → fix.
1160
- 4. When the developer edits a description or the flow on the canvas, treat meta.json as the
1161
- 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 }
1162
1714
  `;
1163
1715
  var README_HINT = `# design/
1164
1716
 
@@ -1185,48 +1737,72 @@ var getContext = (args) => {
1185
1737
  manifest,
1186
1738
  name: manifest?.name ?? basename2(root),
1187
1739
  dirName,
1188
- designDir: join10(root, dirName),
1740
+ designDir: join11(root, dirName),
1189
1741
  designKey: "/" + dirName.replace(/\\/g, "/")
1190
1742
  };
1191
1743
  };
1192
1744
  var listScreens = (designDir) => {
1193
- const dir = join10(designDir, "screens");
1194
- if (!existsSync7(dir))
1745
+ const dir = join11(designDir, "screens");
1746
+ if (!existsSync9(dir))
1195
1747
  return [];
1196
1748
  return readdirSync5(dir).filter((f) => f.endsWith(".ts") && !f.startsWith(".")).map((f) => f.slice(0, -3)).sort();
1197
1749
  };
1198
1750
  var readMetaSizes = (designDir) => {
1199
1751
  try {
1200
- const meta = JSON.parse(readFileSync7(join10(designDir, "meta.json"), "utf8"));
1752
+ const meta = JSON.parse(readFileSync9(join11(designDir, "meta.json"), "utf8"));
1201
1753
  return meta?.screens ?? {};
1202
1754
  } catch {
1203
1755
  return {};
1204
1756
  }
1205
1757
  };
1206
- 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) => {
1207
1782
  const files = [
1208
1783
  ["meta.json", META_JSON],
1784
+ ["spec.md", SPEC_MD],
1209
1785
  ["README.md", README_HINT],
1210
1786
  ["CLAUDE.md", CLAUDE_MD],
1211
- [join10("shared", "tokens.ts"), TOKENS_TS],
1212
- [join10("screens", "home.ts"), HOME_SCREEN_TS]
1787
+ [join11("shared", "tokens.ts"), TOKENS_TS],
1788
+ [join11("screens", "home.ts"), HOME_SCREEN_TS]
1213
1789
  ];
1214
- mkdirSync7(join10(ctx.designDir, "screens"), { recursive: true });
1215
- mkdirSync7(join10(ctx.designDir, "shared"), { recursive: true });
1790
+ mkdirSync8(join11(ctx.designDir, "screens"), { recursive: true });
1791
+ mkdirSync8(join11(ctx.designDir, "shared"), { recursive: true });
1216
1792
  let created = 0;
1217
1793
  for (const [rel, content] of files) {
1218
- const abs = join10(ctx.designDir, rel);
1219
- if (existsSync7(abs)) {
1794
+ const abs = join11(ctx.designDir, rel);
1795
+ if (existsSync9(abs)) {
1220
1796
  note(`kept ${ctx.dirName}/${rel.replace(/\\/g, "/")}`);
1221
1797
  continue;
1222
1798
  }
1223
- writeFileSync7(abs, content);
1799
+ writeFileSync8(abs, content);
1224
1800
  log(`${c.green("created")} ${ctx.dirName}/${rel.replace(/\\/g, "/")}`);
1225
1801
  created++;
1226
1802
  }
1227
- const ignorePath = join10(ctx.root, ".lecodesignore");
1803
+ const ignorePath = join11(ctx.root, ".lecodesignore");
1228
1804
  const ignoreLine = `${ctx.dirName}/.snapshots/`;
1229
- const current = existsSync7(ignorePath) ? readFileSync7(ignorePath, "utf8") : "";
1805
+ const current = existsSync9(ignorePath) ? readFileSync9(ignorePath, "utf8") : "";
1230
1806
  if (!current.split(/\r?\n/).includes(ignoreLine)) {
1231
1807
  appendFileSync(ignorePath, (current && !current.endsWith(`
1232
1808
  `) ? `
@@ -1234,6 +1810,14 @@ var init = async (ctx) => {
1234
1810
  `);
1235
1811
  log(`${c.green("ignored")} ${ignoreLine} (in .lecodesignore)`);
1236
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.");
1237
1821
  log("");
1238
1822
  log(created ? "Design canvas ready." : "Design canvas already initialized.");
1239
1823
  log(` ${c.bold("lecodes design")} start the canvas (browser)`);
@@ -1250,12 +1834,28 @@ var snapshot = async (ctx, args) => {
1250
1834
  throw new CliError(`No screen "${id}" (have: ${all.join(", ")}).`);
1251
1835
  }
1252
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.");
1253
1841
  let renderer;
1254
1842
  try {
1255
1843
  renderer = await import("lecodes-renderer/headless");
1256
1844
  } catch {
1257
1845
  throw new CliError("The renderer isn't installed. Run: npm i -D lecodes-renderer");
1258
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
+ }
1259
1859
  const killer = setTimeout(() => {
1260
1860
  warnErr("Snapshot timed out after 120s.");
1261
1861
  process.exit(1);
@@ -1263,45 +1863,68 @@ var snapshot = async (ctx, args) => {
1263
1863
  killer.unref?.();
1264
1864
  const png = flagBool(args, "png") || flagStr(args, "png") !== undefined;
1265
1865
  const sizes = readMetaSizes(ctx.designDir);
1266
- const outDir = flagStr(args, "out-dir") ?? join10(ctx.designDir, ".snapshots");
1267
- 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");
1268
1868
  const onConsole = flagBool(args, "logs") ? (level, text) => logErr(`${c.dim(`[${level}]`)} ${text}`) : undefined;
1269
1869
  if (!single)
1270
- mkdirSync7(outDir, { recursive: true });
1870
+ mkdirSync8(outDir, { recursive: true });
1271
1871
  for (const id of targets) {
1272
- note(`Rendering ${id}…`);
1273
- const js = await compileDesignScreen({
1274
- root: ctx.root,
1275
- manifest: ctx.manifest,
1276
- name: ctx.name,
1277
- designKey: ctx.designKey,
1278
- screenId: id,
1279
- assets: "disk"
1280
- });
1281
1872
  const [width, height] = sizes[id]?.size ?? [390, 844];
1282
- const result = await renderer.renderToJson(js, { width, height, localAssets: true, onConsole });
1283
- for (const w of result.warnings)
1284
- warnErr(`${id}: ${w}`);
1285
- if (single) {
1286
- 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(`
1287
1906
  `) ? result.text : result.text + `
1288
1907
  `;
1289
- process.stdout.write(text);
1290
- return;
1291
- }
1292
- writeFileSync7(join10(outDir, `${id}.json`), result.text);
1293
- log(`${c.green("✓")} ${id} → ${join10(outDir, `${id}.json`)}`);
1294
- if (png) {
1295
- const pngResult = await renderer.renderToPng(js, { width, height, localAssets: true, scale: 2, onConsole });
1296
- for (const w of pngResult.warnings)
1297
- warnErr(`${id}: ${w}`);
1298
- writeFileSync7(join10(outDir, `${id}.png`), pngResult.png);
1299
- 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
+ }
1300
1923
  }
1301
1924
  }
1302
1925
  };
1303
1926
  var serve = async (ctx, args) => {
1304
- if (!existsSync7(ctx.designDir)) {
1927
+ if (!existsSync9(ctx.designDir)) {
1305
1928
  throw new CliError(`No ${ctx.dirName}/ folder here. Run "lecodes design init" to scaffold it.`);
1306
1929
  }
1307
1930
  let designServer;
@@ -1311,44 +1934,171 @@ var serve = async (ctx, args) => {
1311
1934
  throw new CliError("The design canvas isn't installed. Run: npm i -D lecodes-design");
1312
1935
  }
1313
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;
1314
1971
  const server = await designServer.startDesignServer({
1315
1972
  root: ctx.root,
1316
1973
  designDir: ctx.designDir,
1317
1974
  name: ctx.name,
1318
1975
  port,
1319
1976
  log: note,
1320
- compileScreen: async (id) => ({
1977
+ compileScreen: async (id, state) => ({
1321
1978
  js: await compileDesignScreen({
1322
1979
  root: ctx.root,
1323
1980
  manifest: ctx.manifest,
1324
1981
  name: ctx.name,
1982
+ designDir: ctx.designDir,
1325
1983
  designKey: ctx.designKey,
1326
1984
  screenId: id,
1985
+ state,
1327
1986
  assets: "server"
1328
1987
  }),
1329
1988
  warnings: []
1330
- })
1989
+ }),
1990
+ renderScreen
1331
1991
  });
1332
1992
  log(`${c.bold("LeCodes Design")} — ${ctx.name}`);
1333
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)")}`);
1334
1995
  log(`Watching: ${ctx.dirName}/ (screens hot-reload on save; Ctrl+C to stop)`);
1335
1996
  if (!flagBool(args, "no-open"))
1336
1997
  openBrowser(server.url);
1337
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
+ };
2017
+ var share = async (ctx, args) => {
2018
+ if (!ctx.manifest) {
2019
+ throw new CliError("`lecodes design share` needs a cloned project. Run it inside one, and push the design first (lecodes push).");
2020
+ }
2021
+ const config = loadConfig();
2022
+ const apiUrl = requireApiUrl(config);
2023
+ const token = requireToken(config);
2024
+ const uuid = ctx.manifest.uuid;
2025
+ if (flagBool(args, "off")) {
2026
+ await disableDesignShare(apiUrl, token, uuid);
2027
+ log(`${c.green("✓")} Design sharing is off.`);
2028
+ return;
2029
+ }
2030
+ const res = await enableDesignShare(apiUrl, token, uuid);
2031
+ log(`${c.bold("Design shared")} — anyone with this link can view the mockups (your app code stays private):`);
2032
+ log(` ${c.bold(res.url ?? "")}`);
2033
+ note("Turn it off with: lecodes design share --off");
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
+ };
1338
2082
  var design = async (args) => {
1339
2083
  const sub = args._[0] ?? "serve";
1340
2084
  const ctx = getContext(args);
1341
2085
  if (sub === "serve")
1342
2086
  return serve(ctx, args);
1343
2087
  if (sub === "init")
1344
- return init(ctx);
2088
+ return init(ctx, args);
1345
2089
  if (sub === "snapshot")
1346
2090
  return snapshot(ctx, args);
1347
- throw new CliError(`Unknown design subcommand "${sub}" (serve | init | snapshot).`);
2091
+ if (sub === "arrange")
2092
+ return arrange(ctx, args);
2093
+ if (sub === "check")
2094
+ return check(ctx, args);
2095
+ if (sub === "share")
2096
+ return share(ctx, args);
2097
+ throw new CliError(`Unknown design subcommand "${sub}" (serve | init | snapshot | arrange | check | share).`);
1348
2098
  };
1349
2099
 
1350
2100
  // src/serverDiff.ts
1351
- import { readFileSync as readFileSync8 } from "node:fs";
2101
+ import { readFileSync as readFileSync10 } from "node:fs";
1352
2102
  import { basename as basename3 } from "node:path";
1353
2103
  var isTextAsset = (t) => t === "text" || t === "shader" || t === "scene";
1354
2104
  var computeServerDiff = (project, scan) => {
@@ -1367,7 +2117,7 @@ var computeServerDiff = (project, scan) => {
1367
2117
  const local = localByPath.get(path);
1368
2118
  if (local && !server) {
1369
2119
  const text = isTextKind(classifyKind(basename3(path)));
1370
- 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 });
1371
2121
  continue;
1372
2122
  }
1373
2123
  if (server && !local) {
@@ -1378,12 +2128,12 @@ var computeServerDiff = (project, scan) => {
1378
2128
  if (server && local) {
1379
2129
  if (isTextAsset(server.type)) {
1380
2130
  const serverText = server.text ?? "";
1381
- const localText = readFileSync8(local.absPath, "utf8");
2131
+ const localText = readFileSync10(local.absPath, "utf8");
1382
2132
  if (serverText !== localText)
1383
2133
  entries.push({ path, status: "modified", binary: false, serverText, localText });
1384
2134
  } else {
1385
2135
  const serverSize = server.file?.size;
1386
- const localSize = Buffer.byteLength(readFileSync8(local.absPath));
2136
+ const localSize = Buffer.byteLength(readFileSync10(local.absPath));
1387
2137
  if (serverSize !== undefined && serverSize !== localSize) {
1388
2138
  entries.push({ path, status: "modified", binary: true });
1389
2139
  }
@@ -1547,11 +2297,11 @@ var login = async (args) => {
1547
2297
  };
1548
2298
 
1549
2299
  // src/commands/preview.ts
1550
- import { writeFileSync as writeFileSync8 } from "node:fs";
2300
+ import { writeFileSync as writeFileSync9 } from "node:fs";
1551
2301
  import { resolve as resolve3 } from "node:path";
1552
2302
 
1553
2303
  // src/compile/collect.ts
1554
- import { readFileSync as readFileSync9 } from "node:fs";
2304
+ import { readFileSync as readFileSync11 } from "node:fs";
1555
2305
  import { basename as basename4 } from "node:path";
1556
2306
  var collectEntries = (root, manifest, project) => {
1557
2307
  const pathById = buildPaths(project.assets);
@@ -1576,7 +2326,7 @@ var collectEntries = (root, manifest, project) => {
1576
2326
  warnings.push(`No compiled shader for ${file.path} — push it to compile the shader.`);
1577
2327
  entries.push({ path: file.path, type, shaders });
1578
2328
  } else {
1579
- entries.push({ path: file.path, type, text: readFileSync9(file.absPath, "utf8") });
2329
+ entries.push({ path: file.path, type, text: readFileSync11(file.absPath, "utf8") });
1580
2330
  }
1581
2331
  }
1582
2332
  return { entries, warnings };
@@ -1606,7 +2356,7 @@ var preview = async (args) => {
1606
2356
  if (out && isNullSink(out)) {
1607
2357
  note(`Discarded output (${out})`);
1608
2358
  } else if (out) {
1609
- writeFileSync8(resolve3(out), js);
2359
+ writeFileSync9(resolve3(out), js);
1610
2360
  note(`Wrote ${out} (${js.length} bytes)`);
1611
2361
  } else {
1612
2362
  process.stdout.write(js);
@@ -1618,7 +2368,7 @@ var preview = async (args) => {
1618
2368
  };
1619
2369
 
1620
2370
  // src/commands/render.ts
1621
- import { writeFileSync as writeFileSync9 } from "node:fs";
2371
+ import { writeFileSync as writeFileSync10 } from "node:fs";
1622
2372
  import { resolve as resolve4 } from "node:path";
1623
2373
  var LOCAL_ASSET_SCHEME2 = "lecodesfile://";
1624
2374
  var DEVICE_SAFE_AREA = {
@@ -1715,7 +2465,7 @@ var render = async (args) => {
1715
2465
  if (isNullSink(pngFlag)) {
1716
2466
  note(`Discarded PNG (${pngFlag})`);
1717
2467
  } else {
1718
- writeFileSync9(resolve4(pngFlag), png);
2468
+ writeFileSync10(resolve4(pngFlag), png);
1719
2469
  note(`Wrote ${pngFlag} (${png.length} bytes, ${width}x${height})`);
1720
2470
  }
1721
2471
  return done((cb) => cb());
@@ -1730,7 +2480,7 @@ var render = async (args) => {
1730
2480
  return done((cb) => cb());
1731
2481
  }
1732
2482
  if (out) {
1733
- writeFileSync9(resolve4(out), result.text);
2483
+ writeFileSync10(resolve4(out), result.text);
1734
2484
  note(`Wrote ${out} (${result.text.length} bytes)`);
1735
2485
  return done((cb) => cb());
1736
2486
  }
@@ -1741,8 +2491,8 @@ var render = async (args) => {
1741
2491
  };
1742
2492
 
1743
2493
  // src/commands/pull.ts
1744
- import { existsSync as existsSync8, mkdirSync as mkdirSync8, rmSync, writeFileSync as writeFileSync10 } from "node:fs";
1745
- 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";
1746
2496
  var pull = async (args) => {
1747
2497
  const root = findProjectRoot(process.cwd());
1748
2498
  const manifest = readManifest(root);
@@ -1762,8 +2512,8 @@ var pull = async (args) => {
1762
2512
  const remotePaths = new Set([...buildPaths(project.assets).entries()].filter(([id]) => project.assets.find((a) => a.id === id)?.type !== "folder").map(([, path]) => path));
1763
2513
  for (const path of Object.keys(manifest.files)) {
1764
2514
  if (!remotePaths.has(path)) {
1765
- const abs = join11(root, ...path.replace(/^\//, "").split("/"));
1766
- if (existsSync8(abs))
2515
+ const abs = join12(root, ...path.replace(/^\//, "").split("/"));
2516
+ if (existsSync10(abs))
1767
2517
  rmSync(abs);
1768
2518
  }
1769
2519
  }
@@ -1801,18 +2551,18 @@ var pullPaths = async (root, manifest, project, paths, apiUrl, token) => {
1801
2551
  const files = { ...manifest.files };
1802
2552
  for (const asset of targets) {
1803
2553
  const key = pathById.get(asset.id);
1804
- const abs = join11(root, ...key.replace(/^\//, "").split("/"));
1805
- mkdirSync8(dirname6(abs), { recursive: true });
2554
+ const abs = join12(root, ...key.replace(/^\//, "").split("/"));
2555
+ mkdirSync9(dirname7(abs), { recursive: true });
1806
2556
  let sha;
1807
2557
  if (asset.type === "file") {
1808
2558
  if (!asset.file?.src)
1809
2559
  continue;
1810
2560
  const bytes = await downloadBlob(apiUrl, asset.file.src);
1811
- writeFileSync10(abs, bytes);
2561
+ writeFileSync11(abs, bytes);
1812
2562
  sha = sha256(bytes);
1813
2563
  } else {
1814
2564
  const text = asset.text ?? "";
1815
- writeFileSync10(abs, text);
2565
+ writeFileSync11(abs, text);
1816
2566
  sha = sha256(text);
1817
2567
  }
1818
2568
  files[key] = { id: asset.id, type: kindOf2(asset.type), sha };
@@ -1825,7 +2575,7 @@ var pullPaths = async (root, manifest, project, paths, apiUrl, token) => {
1825
2575
 
1826
2576
  // src/commands/push.ts
1827
2577
  import { basename as basename5 } from "node:path";
1828
- import { readFileSync as readFileSync10 } from "node:fs";
2578
+ import { readFileSync as readFileSync12 } from "node:fs";
1829
2579
  var push = async (args) => {
1830
2580
  const root = findProjectRoot(process.cwd());
1831
2581
  const manifest = readManifest(root);
@@ -1884,9 +2634,9 @@ var push = async (args) => {
1884
2634
  if (source)
1885
2635
  entry.assetId = source.id;
1886
2636
  if (isTextKind(kind)) {
1887
- entry.text = readFileSync10(file.absPath, "utf8");
2637
+ entry.text = readFileSync12(file.absPath, "utf8");
1888
2638
  } else if (!source || source.sha !== file.sha) {
1889
- 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));
1890
2640
  entry.blobFileId = blob.id;
1891
2641
  uploaded++;
1892
2642
  }
@@ -2027,8 +2777,8 @@ Config lives in ~/.lecodes/config.json (override with LECODES_API / LECODES_TOKE
2027
2777
  var commands = { login, create, clone, status, diff, pull, push, preview, render, design };
2028
2778
  var readVersion = () => {
2029
2779
  try {
2030
- const here2 = dirname7(fileURLToPath3(import.meta.url));
2031
- 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";
2032
2782
  } catch {
2033
2783
  return "unknown";
2034
2784
  }