@loworbitstudio/visor 1.21.0 → 1.23.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
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { readFileSync as readFileSync33 } from "fs";
5
- import { dirname as dirname14, join as join33 } from "path";
4
+ import { readFileSync as readFileSync34 } from "fs";
5
+ import { dirname as dirname16, join as join34 } from "path";
6
6
  import { fileURLToPath as fileURLToPath4 } from "url";
7
7
  import { Command as Command2 } from "commander";
8
8
 
9
9
  // src/commands/check.ts
10
10
  import { Command } from "commander";
11
- import { statSync as statSync3 } from "fs";
12
- import { resolve as resolve4, dirname as dirname3 } from "path";
11
+ import { statSync as statSync4 } from "fs";
12
+ import { resolve as resolve5, dirname as dirname4 } from "path";
13
13
 
14
14
  // src/registry/resolve.ts
15
15
  import { readFileSync } from "fs";
@@ -395,13 +395,160 @@ async function scanJsx(pathArg) {
395
395
  }
396
396
 
397
397
  // src/check/design.ts
398
- import { readFileSync as readFileSync3, readdirSync as readdirSync2, statSync as statSync2, existsSync } from "fs";
399
- import { resolve as resolve2, extname as extname2, join as join3, basename, dirname as dirname2 } from "path";
398
+ import { readFileSync as readFileSync4, readdirSync as readdirSync2, statSync as statSync3, existsSync as existsSync2 } from "fs";
399
+ import { resolve as resolve3, extname as extname2, join as join4, basename, dirname as dirname3 } from "path";
400
+
401
+ // src/check/jsx-ast.ts
402
+ import { parse as babelParse } from "@babel/parser";
403
+ function parseJsx(source) {
404
+ try {
405
+ return babelParse(source, {
406
+ sourceType: "module",
407
+ plugins: ["jsx", "typescript"],
408
+ errorRecovery: true
409
+ });
410
+ } catch {
411
+ return null;
412
+ }
413
+ }
414
+
415
+ // src/check/taxonomy.ts
416
+ import { readFileSync as readFileSync3, existsSync, statSync as statSync2 } from "fs";
417
+ import { resolve as resolve2, join as join3, dirname as dirname2, isAbsolute } from "path";
418
+ var DISCOVERY_CANDIDATES = [
419
+ "taxonomy.json",
420
+ join3(".visor", "taxonomy.json"),
421
+ join3("design-prototypes", "admin-ui", "taxonomy.json")
422
+ ];
423
+ function slugToIdentifier(slug2) {
424
+ const bare = slug2.includes("/") ? slug2.slice(slug2.lastIndexOf("/") + 1) : slug2;
425
+ return bare.split(/[-_]/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
426
+ }
427
+ function refToSlug(ref) {
428
+ const bare = ref.includes("/") ? ref.slice(ref.lastIndexOf("/") + 1) : ref;
429
+ if (!/^[a-z][a-z0-9-]*$/.test(bare)) return null;
430
+ return bare;
431
+ }
432
+ function parseTaxonomy(data, path2) {
433
+ const root = data;
434
+ const tiers = Array.isArray(root?.tiers) ? root.tiers : null;
435
+ if (!tiers) {
436
+ throw new Error(`taxonomy.json at ${path2} has no "tiers" array \u2014 not a kit taxonomy.`);
437
+ }
438
+ const slugs = /* @__PURE__ */ new Set();
439
+ for (const tier of tiers) {
440
+ const families = tier.families;
441
+ if (!Array.isArray(families)) continue;
442
+ for (const family of families) {
443
+ const rows = family.rows;
444
+ if (!Array.isArray(rows)) continue;
445
+ for (const row of rows) {
446
+ const entries = row?.visor?.entries;
447
+ if (!Array.isArray(entries)) continue;
448
+ for (const entry of entries) {
449
+ if (entry?.kind !== "component" && entry?.kind !== "block") continue;
450
+ if (typeof entry.ref !== "string") continue;
451
+ const slug2 = refToSlug(entry.ref);
452
+ if (slug2) slugs.add(slug2);
453
+ }
454
+ }
455
+ }
456
+ }
457
+ if (slugs.size === 0) {
458
+ throw new Error(`taxonomy.json at ${path2} declares no component or block elements.`);
459
+ }
460
+ const identifiers = /* @__PURE__ */ new Set();
461
+ for (const slug2 of slugs) identifiers.add(slugToIdentifier(slug2));
462
+ return { path: path2, slugs, identifiers };
463
+ }
464
+ function loadFrom(path2) {
465
+ const raw = readFileSync3(path2, "utf-8");
466
+ let data;
467
+ try {
468
+ data = JSON.parse(raw);
469
+ } catch {
470
+ throw new Error(`taxonomy.json at ${path2} is not valid JSON.`);
471
+ }
472
+ return parseTaxonomy(data, path2);
473
+ }
474
+ function discover(startDir) {
475
+ let dir = startDir;
476
+ for (; ; ) {
477
+ for (const candidate of DISCOVERY_CANDIDATES) {
478
+ const full = join3(dir, candidate);
479
+ if (existsSync(full)) return full;
480
+ }
481
+ if (existsSync(join3(dir, ".git"))) return null;
482
+ const parent = dirname2(dir);
483
+ if (parent === dir) return null;
484
+ dir = parent;
485
+ }
486
+ }
487
+ function resolveTaxonomy(options) {
488
+ const { explicitPath, visorrcPath, scanPath, env = process.env } = options;
489
+ const scanAbs = resolve2(scanPath);
490
+ let scanDir = scanAbs;
491
+ try {
492
+ if (!statSync2(scanAbs).isDirectory()) scanDir = dirname2(scanAbs);
493
+ } catch {
494
+ scanDir = dirname2(scanAbs);
495
+ }
496
+ const envPath = env.VISOR_TAXONOMY;
497
+ const configured = [];
498
+ if (explicitPath) configured.push({ path: resolve2(explicitPath), source: "flag" });
499
+ else if (envPath) configured.push({ path: resolve2(envPath), source: "env" });
500
+ else if (visorrcPath) {
501
+ configured.push({
502
+ path: isAbsolute(visorrcPath) ? visorrcPath : resolve2(scanDir, visorrcPath),
503
+ source: "visorrc"
504
+ });
505
+ }
506
+ if (configured.length > 0) {
507
+ const { path: path2, source } = configured[0];
508
+ if (!existsSync(path2)) {
509
+ return {
510
+ taxonomy: null,
511
+ path: path2,
512
+ source,
513
+ error: `Kit taxonomy not found at ${path2}.`,
514
+ requested: true
515
+ };
516
+ }
517
+ try {
518
+ return { taxonomy: loadFrom(path2), path: path2, source, error: null, requested: true };
519
+ } catch (err) {
520
+ return {
521
+ taxonomy: null,
522
+ path: path2,
523
+ source,
524
+ error: err instanceof Error ? err.message : String(err),
525
+ requested: true
526
+ };
527
+ }
528
+ }
529
+ const found = discover(scanDir);
530
+ if (!found) {
531
+ return { taxonomy: null, path: null, source: null, error: null, requested: false };
532
+ }
533
+ try {
534
+ return { taxonomy: loadFrom(found), path: found, source: "discovered", error: null, requested: false };
535
+ } catch (err) {
536
+ return {
537
+ taxonomy: null,
538
+ path: found,
539
+ source: "discovered",
540
+ error: err instanceof Error ? err.message : String(err),
541
+ requested: false
542
+ };
543
+ }
544
+ }
545
+
546
+ // src/check/design.ts
400
547
  function loadVisorRc(dir) {
401
- const rcPath = join3(dir, ".visorrc.json");
402
- if (!existsSync(rcPath)) return {};
548
+ const rcPath = join4(dir, ".visorrc.json");
549
+ if (!existsSync2(rcPath)) return {};
403
550
  try {
404
- const raw = readFileSync3(rcPath, "utf-8");
551
+ const raw = readFileSync4(rcPath, "utf-8");
405
552
  return JSON.parse(raw);
406
553
  } catch {
407
554
  return {};
@@ -411,15 +558,15 @@ var CODE_EXTS = /* @__PURE__ */ new Set([".tsx", ".jsx", ".ts", ".js"]);
411
558
  var STYLE_EXTS = /* @__PURE__ */ new Set([".css", ".module.css"]);
412
559
  var ALL_EXTS = /* @__PURE__ */ new Set([...CODE_EXTS, ...STYLE_EXTS]);
413
560
  function collectFiles2(pathArg) {
414
- const abs = resolve2(pathArg);
561
+ const abs = resolve3(pathArg);
415
562
  try {
416
- const s = statSync2(abs);
563
+ const s = statSync3(abs);
417
564
  if (s.isDirectory()) {
418
565
  let recurse2 = function(dir) {
419
566
  for (const entry of readdirSync2(dir)) {
420
567
  if (entry.startsWith(".") || entry === "node_modules" || entry === "dist") continue;
421
- const full = join3(dir, entry);
422
- const es = statSync2(full);
568
+ const full = join4(dir, entry);
569
+ const es = statSync3(full);
423
570
  if (es.isDirectory()) recurse2(full);
424
571
  else if (ALL_EXTS.has(extname2(full))) files.push(full);
425
572
  }
@@ -493,6 +640,93 @@ function ruleHardcodedHex(source, filePath) {
493
640
  return found;
494
641
  }
495
642
  var PX_WHITELIST = /* @__PURE__ */ new Set(["0px", "1px", "2px", "3px"]);
643
+ var LEGACY_PX_CONTEXT_RE = /margin|padding|width|height|gap|top:|left:|right:|bottom:|font-size|line-height|min-width|max-width|min-height|max-height/;
644
+ var SIZING_PROPS = /* @__PURE__ */ new Set([
645
+ // box model
646
+ "margin",
647
+ "margintop",
648
+ "marginright",
649
+ "marginbottom",
650
+ "marginleft",
651
+ "marginblock",
652
+ "marginblockstart",
653
+ "marginblockend",
654
+ "margininline",
655
+ "margininlinestart",
656
+ "margininlineend",
657
+ "padding",
658
+ "paddingtop",
659
+ "paddingright",
660
+ "paddingbottom",
661
+ "paddingleft",
662
+ "paddingblock",
663
+ "paddingblockstart",
664
+ "paddingblockend",
665
+ "paddinginline",
666
+ "paddinginlinestart",
667
+ "paddinginlineend",
668
+ // intrinsic size
669
+ "width",
670
+ "minwidth",
671
+ "maxwidth",
672
+ "height",
673
+ "minheight",
674
+ "maxheight",
675
+ "size",
676
+ "blocksize",
677
+ "minblocksize",
678
+ "maxblocksize",
679
+ "inlinesize",
680
+ "mininlinesize",
681
+ "maxinlinesize",
682
+ // position
683
+ "top",
684
+ "right",
685
+ "bottom",
686
+ "left",
687
+ "inset",
688
+ "insetblock",
689
+ "insetinline",
690
+ // layout gaps
691
+ "gap",
692
+ "rowgap",
693
+ "columngap",
694
+ "gridgap",
695
+ "gridrowgap",
696
+ "gridcolumngap",
697
+ // typographic metrics
698
+ "fontsize",
699
+ "lineheight",
700
+ "letterspacing",
701
+ "wordspacing",
702
+ "textindent",
703
+ // corner radius
704
+ "borderradius",
705
+ "bordertopleftradius",
706
+ "bordertoprightradius",
707
+ "borderbottomleftradius",
708
+ "borderbottomrightradius",
709
+ "borderstartstartradius",
710
+ "borderstartendradius",
711
+ "borderendstartradius",
712
+ "borderendendradius",
713
+ // flex + scroll boxes
714
+ "flexbasis",
715
+ "scrollmargin",
716
+ "scrollpadding"
717
+ ]);
718
+ var PROP_NAME_RE = /(^|[^\w$])(-{0,2}[A-Za-z][\w-]*)\s*:/g;
719
+ function normalizeProp(name) {
720
+ return name.replace(/[^A-Za-z0-9]/g, "").toLowerCase();
721
+ }
722
+ function hasSizingProp(line) {
723
+ PROP_NAME_RE.lastIndex = 0;
724
+ let m;
725
+ while ((m = PROP_NAME_RE.exec(line)) !== null) {
726
+ if (SIZING_PROPS.has(normalizeProp(m[2]))) return true;
727
+ }
728
+ return false;
729
+ }
496
730
  function ruleHardcodedPx(source, filePath) {
497
731
  const found = [];
498
732
  const PX_RE = /\b(\d+(?:\.\d+)?)px\b/g;
@@ -500,24 +734,134 @@ function ruleHardcodedPx(source, filePath) {
500
734
  for (let i = 0; i < src.length; i++) {
501
735
  const l = src[i];
502
736
  if (l.trim().startsWith("//") || l.trim().startsWith("*") || l.trim().startsWith("/*")) continue;
503
- if (!/margin|padding|width|height|gap|top:|left:|right:|bottom:|font-size|line-height|min-width|max-width|min-height|max-height/.test(l)) continue;
737
+ const legacy = LEGACY_PX_CONTEXT_RE.test(l);
738
+ if (!legacy && !hasSizingProp(l)) continue;
504
739
  let m;
505
740
  PX_RE.lastIndex = 0;
506
741
  while ((m = PX_RE.exec(l)) !== null) {
507
742
  const full = m[0];
508
743
  if (PX_WHITELIST.has(full)) continue;
509
- found.push(finding(
744
+ found.push(legacy ? finding(
510
745
  filePath,
511
746
  i + 1,
512
747
  "hardcoded-px",
513
748
  "error",
514
749
  `Hardcoded pixel value "${full}" in spacing/sizing bypasses the Borealis spacing token system.`,
515
750
  "Replace with a semantic spacing token: var(--space-1), var(--space-2), var(--space-4), etc."
751
+ ) : finding(
752
+ filePath,
753
+ i + 1,
754
+ "hardcoded-px",
755
+ "warn",
756
+ `Hardcoded pixel value "${full}" in spacing/sizing bypasses the Borealis spacing token system. Reported as a warning because this property spelling was previously skipped (VI-631) \u2014 it will graduate to an error in a future major.`,
757
+ "Replace with a semantic spacing token: var(--space-1), var(--space-2), var(--space-4), etc."
516
758
  ));
517
759
  }
518
760
  }
519
761
  return found;
520
762
  }
763
+ var activeKit = null;
764
+ function ruleInlineStyleObject(source, filePath) {
765
+ if (!CODE_EXTS.has(extname2(filePath))) return [];
766
+ const ast = parseJsx(source);
767
+ if (!ast) return [];
768
+ const found = [];
769
+ walk(ast, (node) => {
770
+ if (node.type !== "JSXAttribute") return;
771
+ const nameNode = node.name;
772
+ if (nameNode?.type !== "JSXIdentifier" || nameNode.name !== "style") return;
773
+ const valueNode = node.value;
774
+ if (!valueNode || valueNode.type !== "JSXExpressionContainer") return;
775
+ const expr = valueNode.expression;
776
+ if (expr?.type !== "ObjectExpression") return;
777
+ const declared = [];
778
+ for (const raw of expr.properties ?? []) {
779
+ const prop = raw;
780
+ if (prop.type !== "ObjectProperty") continue;
781
+ if (prop.computed === true) continue;
782
+ const key = prop.key;
783
+ let keyName = null;
784
+ if (key?.type === "Identifier") keyName = String(key.name ?? "");
785
+ else if (key?.type === "StringLiteral") keyName = String(key.value ?? "");
786
+ if (!keyName) continue;
787
+ if (keyName.startsWith("--")) continue;
788
+ declared.push(keyName);
789
+ }
790
+ if (declared.length === 0) return;
791
+ const loc = node.loc;
792
+ found.push(finding(
793
+ filePath,
794
+ loc?.start?.line ?? 1,
795
+ "inline-style-object",
796
+ "warn",
797
+ `Inline style object declares ${declared.map((d) => `"${d}"`).join(", ")}. Inline styles introduce styling outside the kit \u2014 untokenized, unthemeable, and invisible to the design system.`,
798
+ `Move these declarations into the component's CSS Module and reference tokens, or pass a CSS-variable bridge: style={{ "--my-token": value }}.`
799
+ ));
800
+ });
801
+ return found;
802
+ }
803
+ function ruleKitElementRedeclared(source, filePath) {
804
+ const kit = activeKit;
805
+ if (!kit) return [];
806
+ if (!CODE_EXTS.has(extname2(filePath))) return [];
807
+ if (isOwnedKitSource(filePath, kit)) return [];
808
+ const ast = parseJsx(source);
809
+ if (!ast) return [];
810
+ const program2 = ast.program;
811
+ const body = program2?.body ?? [];
812
+ const found = [];
813
+ for (const raw of body) {
814
+ for (const decl of moduleScopeDeclarations(raw)) {
815
+ if (!kit.identifiers.has(decl.name)) continue;
816
+ found.push(finding(
817
+ filePath,
818
+ decl.line,
819
+ "kit-element-redeclared",
820
+ "warn",
821
+ `Local re-declaration of the kit element "${decl.name}". This surface builds its own copy of a kit element instead of composing the kit's \u2014 the styling it introduces is outside the kit by construction.`,
822
+ `Compose the kit element instead (\`npx visor add ${identifierToSlug(decl.name)}\`), or rename this local component so it does not shadow a kit element.`
823
+ ));
824
+ }
825
+ }
826
+ return found;
827
+ }
828
+ function moduleScopeDeclarations(raw) {
829
+ let node = raw;
830
+ if (!node) return [];
831
+ if (node.type === "ExportNamedDeclaration" || node.type === "ExportDefaultDeclaration") {
832
+ node = node.declaration;
833
+ if (!node) return [];
834
+ }
835
+ const line = node.loc?.start?.line ?? 1;
836
+ if (node.type === "FunctionDeclaration" || node.type === "ClassDeclaration") {
837
+ const id = node.id;
838
+ const name = id?.name;
839
+ return typeof name === "string" ? [{ name, line }] : [];
840
+ }
841
+ if (node.type === "VariableDeclaration") {
842
+ const out = [];
843
+ for (const d of node.declarations ?? []) {
844
+ const declarator = d;
845
+ const id = declarator.id;
846
+ if (id?.type !== "Identifier" || typeof id.name !== "string") continue;
847
+ const init = declarator.init;
848
+ const initType = init?.type;
849
+ if (initType !== "ArrowFunctionExpression" && initType !== "FunctionExpression" && initType !== "CallExpression") continue;
850
+ const dLine = declarator.loc?.start?.line ?? line;
851
+ out.push({ name: id.name, line: dLine });
852
+ }
853
+ return out;
854
+ }
855
+ return [];
856
+ }
857
+ function identifierToSlug(identifier) {
858
+ return identifier.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
859
+ }
860
+ function isOwnedKitSource(filePath, kit) {
861
+ const base = basename(filePath).replace(/\.(tsx|ts|jsx|js)$/, "").replace(/\.module$/, "");
862
+ if (kit.slugs.has(base)) return true;
863
+ return kit.slugs.has(basename(dirname3(filePath)));
864
+ }
521
865
  function ruleMissingDarkModeBlock(source, filePath) {
522
866
  const ext = extname2(filePath);
523
867
  if (!STYLE_EXTS.has(ext)) return [];
@@ -836,10 +1180,10 @@ function resetBaseLayerCache() {
836
1180
  baseLayerReported.clear();
837
1181
  }
838
1182
  function findProjectRoot(filePath) {
839
- let dir = dirname2(resolve2(filePath));
1183
+ let dir = dirname3(resolve3(filePath));
840
1184
  for (; ; ) {
841
- if (existsSync(join3(dir, "package.json"))) return dir;
842
- const parent = dirname2(dir);
1185
+ if (existsSync2(join4(dir, "package.json"))) return dir;
1186
+ const parent = dirname3(dir);
843
1187
  if (parent === dir) return null;
844
1188
  dir = parent;
845
1189
  }
@@ -856,10 +1200,10 @@ function collectStylesheets(root) {
856
1200
  }
857
1201
  for (const entry of entries) {
858
1202
  if (entry === "node_modules" || entry === "dist" || entry.startsWith(".")) continue;
859
- const full = join3(dir, entry);
1203
+ const full = join4(dir, entry);
860
1204
  let s;
861
1205
  try {
862
- s = statSync2(full);
1206
+ s = statSync3(full);
863
1207
  } catch {
864
1208
  continue;
865
1209
  }
@@ -878,7 +1222,7 @@ function baseLayerState(root) {
878
1222
  for (const sheet of collectStylesheets(root)) {
879
1223
  let css;
880
1224
  try {
881
- css = readFileSync3(sheet, "utf-8");
1225
+ css = readFileSync4(sheet, "utf-8");
882
1226
  } catch {
883
1227
  continue;
884
1228
  }
@@ -886,12 +1230,12 @@ function baseLayerState(root) {
886
1230
  if (PREFLIGHT_RE.test(css)) hasPreflight = true;
887
1231
  if (hasReset && hasPreflight) break;
888
1232
  }
889
- const corePath = join3(root, "node_modules", "@loworbitstudio", "visor-core");
1233
+ const corePath = join4(root, "node_modules", "@loworbitstudio", "visor-core");
890
1234
  const state = {
891
1235
  hasReset,
892
1236
  hasPreflight,
893
- visorCoreInstalled: existsSync(corePath),
894
- visorCoreExportsReset: existsSync(join3(corePath, "dist", "reset.css"))
1237
+ visorCoreInstalled: existsSync2(corePath),
1238
+ visorCoreExportsReset: existsSync2(join4(corePath, "dist", "reset.css"))
895
1239
  };
896
1240
  baseLayerStateCache.set(root, state);
897
1241
  return state;
@@ -954,12 +1298,35 @@ var RULES = [
954
1298
  { name: "line-length-over-75ch", severity: "warn", fn: ruleLineLengthOver75ch },
955
1299
  { name: "gradient-text", severity: "warn", fn: ruleGradientText },
956
1300
  { name: "excessive-card-nesting", severity: "warn", fn: ruleExcessiveCardNesting },
957
- { name: "missing-visor-base-layer", severity: "warn", fn: ruleMissingVisorBaseLayer }
1301
+ { name: "missing-visor-base-layer", severity: "warn", fn: ruleMissingVisorBaseLayer },
1302
+ // Composition (VI-631) — warning-only by default (D3) so the camelCase fix and
1303
+ // the two new assertions can be adopted without escalating an existing
1304
+ // project's exit code on first run.
1305
+ { name: "inline-style-object", severity: "warn", fn: ruleInlineStyleObject },
1306
+ { name: "kit-element-redeclared", severity: "warn", fn: ruleKitElementRedeclared }
958
1307
  ];
1308
+ var COMPOSITION_SCOPE = {
1309
+ scope: "composition-only",
1310
+ asserts: "this surface introduced no styling outside the kit",
1311
+ doesNotAssert: [
1312
+ "arrangement (right elements, wrong order)",
1313
+ "content (wrong icon, dropped hint)",
1314
+ "data / reachability"
1315
+ ],
1316
+ limit: "Green means this surface introduced no styling outside the kit. It does NOT mean the surface is on-design. Arrangement (right elements, wrong order), content (wrong icon, dropped hint), and data/reachability are not checked here and still need a human."
1317
+ };
959
1318
  function scanDesign(pathArg, options = {}) {
960
1319
  const files = collectFiles2(pathArg);
961
1320
  const { disabledRules = [], errorsOnly = false } = options;
962
1321
  resetBaseLayerCache();
1322
+ const resolution = resolveTaxonomy({
1323
+ explicitPath: options.taxonomyPath,
1324
+ visorrcPath: options.visorrcTaxonomyPath,
1325
+ scanPath: pathArg,
1326
+ env: options.env
1327
+ });
1328
+ const engaged = options.composition === true || resolution.requested || resolution.path !== null;
1329
+ activeKit = resolution.taxonomy;
963
1330
  const activeRules = RULES.filter((r) => {
964
1331
  if (disabledRules.includes(r.name)) return false;
965
1332
  if (errorsOnly && r.severity !== "error") return false;
@@ -967,20 +1334,35 @@ function scanDesign(pathArg, options = {}) {
967
1334
  });
968
1335
  const errors = [];
969
1336
  const warnings = [];
970
- for (const file of files) {
971
- let source;
972
- try {
973
- source = readFileSync3(file, "utf-8");
974
- } catch {
975
- continue;
976
- }
977
- for (const rule of activeRules) {
978
- const ruleFindings = rule.fn(source, file);
979
- for (const f of ruleFindings) {
980
- if (f.severity === "error") errors.push(f);
981
- else warnings.push(f);
1337
+ try {
1338
+ for (const file of files) {
1339
+ let source;
1340
+ try {
1341
+ source = readFileSync4(file, "utf-8");
1342
+ } catch {
1343
+ continue;
1344
+ }
1345
+ for (const rule of activeRules) {
1346
+ const ruleFindings = rule.fn(source, file);
1347
+ for (const f of ruleFindings) {
1348
+ if (errorsOnly && f.severity !== "error") continue;
1349
+ if (f.severity === "error") errors.push(f);
1350
+ else warnings.push(f);
1351
+ }
982
1352
  }
983
1353
  }
1354
+ } finally {
1355
+ activeKit = null;
1356
+ }
1357
+ if (engaged && !resolution.taxonomy && !disabledRules.includes("kit-taxonomy-missing")) {
1358
+ errors.push(finding(
1359
+ resolve3(pathArg),
1360
+ 1,
1361
+ "kit-taxonomy-missing",
1362
+ "error",
1363
+ resolution.error ?? "Kit membership could not be asserted: no taxonomy.json resolved. The composition lint fails closed rather than reporting a green it did not earn.",
1364
+ 'Point the lint at the kit definition: --taxonomy <path/to/taxonomy.json>, the VISOR_TAXONOMY environment variable, or a "taxonomy" key in .visorrc.json.'
1365
+ ));
984
1366
  }
985
1367
  return {
986
1368
  errors,
@@ -989,13 +1371,34 @@ function scanDesign(pathArg, options = {}) {
989
1371
  errorCount: errors.length,
990
1372
  warningCount: warnings.length,
991
1373
  filesScanned: files.length
1374
+ },
1375
+ composition: {
1376
+ scope: COMPOSITION_SCOPE.scope,
1377
+ asserts: COMPOSITION_SCOPE.asserts,
1378
+ doesNotAssert: [...COMPOSITION_SCOPE.doesNotAssert],
1379
+ limit: COMPOSITION_SCOPE.limit,
1380
+ kitMembership: {
1381
+ asserted: resolution.taxonomy !== null,
1382
+ taxonomyPath: resolution.path,
1383
+ source: resolution.source,
1384
+ elementCount: resolution.taxonomy?.slugs.size ?? 0,
1385
+ reason: resolution.taxonomy ? `resolved from ${resolution.source} (${resolution.taxonomy.slugs.size} kit elements)` : resolution.error ?? "no taxonomy.json resolved; pass --taxonomy <path> to assert kit membership"
1386
+ }
992
1387
  }
993
1388
  };
994
1389
  }
1390
+ function compositionScopeNotice() {
1391
+ return [
1392
+ "Composition scope \u2014 this check asserts one thing:",
1393
+ ` ${COMPOSITION_SCOPE.asserts}.`,
1394
+ "It does NOT assert the surface is on-design. Uncovered residue:",
1395
+ ...COMPOSITION_SCOPE.doesNotAssert.map((r) => ` \xB7 ${r}`)
1396
+ ];
1397
+ }
995
1398
 
996
1399
  // src/check/theme-mode.ts
997
- import { readFileSync as readFileSync4 } from "fs";
998
- import { resolve as resolve3 } from "path";
1400
+ import { readFileSync as readFileSync5 } from "fs";
1401
+ import { resolve as resolve4 } from "path";
999
1402
  import {
1000
1403
  generateThemeData,
1001
1404
  parseColor,
@@ -1044,8 +1447,8 @@ function checkThemeModeSource(yamlSource, threshold = LUMINANCE_THRESHOLD) {
1044
1447
  };
1045
1448
  }
1046
1449
  function checkThemeModeFile(filePath, threshold = LUMINANCE_THRESHOLD) {
1047
- const abs = resolve3(filePath);
1048
- const source = readFileSync4(abs, "utf-8");
1450
+ const abs = resolve4(filePath);
1451
+ const source = readFileSync5(abs, "utf-8");
1049
1452
  return checkThemeModeSource(source, threshold);
1050
1453
  }
1051
1454
 
@@ -1184,16 +1587,21 @@ function checkHasCommand(pattern2, options) {
1184
1587
  logger.success(`${result.name} [${result.type}]${cmd}`);
1185
1588
  logger.info(` ${result.description}`);
1186
1589
  }
1590
+ function printCompositionScope() {
1591
+ logger.blank();
1592
+ for (const line of compositionScopeNotice()) logger.item(line);
1593
+ logger.blank();
1594
+ }
1187
1595
  async function checkDiffCommand(pathArg, options) {
1188
1596
  const result = await scanJsx(pathArg);
1189
1597
  if (options.json) {
1190
- console.log(JSON.stringify({ success: true, ...result }, null, 2));
1191
- if (options.failOnHits && result.summary.hits > 0) process.exit(1);
1192
- process.exit(0);
1598
+ console.log(JSON.stringify({ success: true, ...result, composition: COMPOSITION_SCOPE }, null, 2));
1599
+ if (options.failOnHits && result.summary.hits > 0) process.exitCode = 1;
1193
1600
  return;
1194
1601
  }
1195
1602
  if (result.summary.hits === 0) {
1196
1603
  logger.success(`No native HTML primitives found \u2014 ${result.summary.scanned} file(s) scanned.`);
1604
+ printCompositionScope();
1197
1605
  return;
1198
1606
  }
1199
1607
  logger.heading(`Found ${result.summary.hits} native HTML usage(s) in ${result.summary.scanned} file(s):
@@ -1204,27 +1612,40 @@ async function checkDiffCommand(pathArg, options) {
1204
1612
  logger.warn(` <${f.nativeTag}> \u2192 use <${f.suggestedPrimitive}>${note}`);
1205
1613
  logger.item(` ${loc} ${f.installCmd}`);
1206
1614
  }
1207
- logger.blank();
1615
+ printCompositionScope();
1208
1616
  if (options.failOnHits) process.exit(1);
1209
1617
  }
1210
1618
  function checkDesignCommand(pathArg, options) {
1211
- const absPath = resolve4(pathArg);
1212
- const rcDir = statSync3(absPath).isDirectory() ? absPath : dirname3(absPath);
1619
+ const absPath = resolve5(pathArg);
1620
+ const rcDir = statSync4(absPath).isDirectory() ? absPath : dirname4(absPath);
1213
1621
  const rc = loadVisorRc(rcDir);
1214
1622
  const result = scanDesign(absPath, {
1215
1623
  disabledRules: rc.disabledRules ?? [],
1216
- errorsOnly: options.errorsOnly ?? false
1624
+ errorsOnly: options.errorsOnly ?? false,
1625
+ taxonomyPath: options.taxonomy,
1626
+ visorrcTaxonomyPath: rc.taxonomy,
1627
+ composition: options.composition ?? false
1217
1628
  });
1218
1629
  const useJson = options.json || options.format === "json";
1219
1630
  if (useJson) {
1220
1631
  console.log(JSON.stringify({ success: true, ...result }, null, 2));
1221
- if (!options.noFail && result.summary.errorCount > 0) process.exit(1);
1222
- process.exit(0);
1632
+ if (!options.noFail && result.summary.errorCount > 0) process.exitCode = 1;
1223
1633
  return;
1224
1634
  }
1225
- const { errors, warnings, summary } = result;
1635
+ const { errors, warnings, summary, composition } = result;
1636
+ const printKitMembership = () => {
1637
+ const km = composition.kitMembership;
1638
+ if (km.asserted) {
1639
+ logger.item(`Kit membership: asserted against ${km.taxonomyPath} (${km.elementCount} element(s), via ${km.source}).`);
1640
+ } else {
1641
+ logger.item(`Kit membership: NOT asserted \u2014 ${km.reason}`);
1642
+ }
1643
+ };
1226
1644
  if (summary.errorCount === 0 && summary.warningCount === 0) {
1227
1645
  logger.success(`No design anti-patterns found \u2014 ${summary.filesScanned} file(s) scanned.`);
1646
+ printCompositionScope();
1647
+ printKitMembership();
1648
+ logger.blank();
1228
1649
  process.exit(0);
1229
1650
  return;
1230
1651
  }
@@ -1256,6 +1677,8 @@ function checkDesignCommand(pathArg, options) {
1256
1677
  } else {
1257
1678
  logger.warn(`${summary.warningCount} warning(s) (0 errors)`);
1258
1679
  }
1680
+ printCompositionScope();
1681
+ printKitMembership();
1259
1682
  logger.blank();
1260
1683
  if (!options.noFail && summary.errorCount > 0) process.exit(1);
1261
1684
  }
@@ -1307,7 +1730,7 @@ function checkCommand() {
1307
1730
  check.command("diff").description("Scan JSX/TSX for native HTML elements that have Visor equivalents").argument("<path>", "file path, directory, or - for stdin").option("--fail-on-hits", "exit 1 if any native HTML usages are found (for CI use)").option("--json", "output structured JSON (for AI agents)").action(async (pathArg, options) => {
1308
1731
  await checkDiffCommand(pathArg, options);
1309
1732
  });
1310
- check.command("design").description("Scan frontend code for Borealis design anti-patterns (deterministic, no LLM)").argument("<path>", "file path or directory to scan").option("--format <format>", "output format: json or human (default: human when TTY, json otherwise)").option("--errors-only", "report only error-severity rules (skip warnings)").option("--no-fail", "do not exit 1 on errors (advisory mode)").option("--json", "shorthand for --format json").action((pathArg, options) => {
1733
+ check.command("design").description("Scan frontend code for Borealis design anti-patterns (deterministic, no LLM). Asserts composition \u2014 that the surface introduced no styling outside the kit \u2014 never arrangement.").argument("<path>", "file path or directory to scan").option("--format <format>", "output format: json or human (default: human when TTY, json otherwise)").option("--errors-only", "report only error-severity rules (skip warnings)").option("--no-fail", "do not exit 1 on errors (advisory mode)").option("--json", "shorthand for --format json").option("--taxonomy <path>", "path to the kit's taxonomy.json \u2014 the definition kit membership is asserted against").option("--composition", "require the kit-membership assertion: fail closed when no taxonomy.json resolves").action((pathArg, options) => {
1311
1734
  checkDesignCommand(pathArg, options);
1312
1735
  });
1313
1736
  check.command("theme-mode").description("Assert a theme's rendered app-root background matches its declared color-scheme (dark-only/light-only/adaptive)").argument("<path>", "path to a .visor.yaml theme file").option("--format <format>", "output format: json or human (default: human)").option("--no-fail", "do not exit 1 on a mode mismatch (advisory mode)").option("--json", "shorthand for --format json").action((pathArg, options) => {
@@ -1317,14 +1740,14 @@ function checkCommand() {
1317
1740
  }
1318
1741
 
1319
1742
  // src/commands/init.ts
1320
- import { existsSync as existsSync6, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync9 } from "fs";
1321
- import { join as join8, dirname as dirname5, basename as basename2 } from "path";
1743
+ import { existsSync as existsSync7, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync10 } from "fs";
1744
+ import { join as join9, dirname as dirname6, basename as basename2 } from "path";
1322
1745
  import { fileURLToPath as fileURLToPath2 } from "url";
1323
1746
  import * as childProcess2 from "child_process";
1324
1747
 
1325
1748
  // src/config/config.ts
1326
- import { readFileSync as readFileSync5, writeFileSync, existsSync as existsSync2 } from "fs";
1327
- import { join as join4 } from "path";
1749
+ import { readFileSync as readFileSync6, writeFileSync, existsSync as existsSync3 } from "fs";
1750
+ import { join as join5 } from "path";
1328
1751
 
1329
1752
  // src/config/defaults.ts
1330
1753
  var DEFAULT_CONFIG = {
@@ -1341,19 +1764,19 @@ var CONFIG_FILE = "visor.json";
1341
1764
 
1342
1765
  // src/config/config.ts
1343
1766
  function getConfigPath(cwd) {
1344
- return join4(cwd, CONFIG_FILE);
1767
+ return join5(cwd, CONFIG_FILE);
1345
1768
  }
1346
1769
  function configExists(cwd) {
1347
- return existsSync2(getConfigPath(cwd));
1770
+ return existsSync3(getConfigPath(cwd));
1348
1771
  }
1349
1772
  function loadConfig(cwd) {
1350
1773
  const configPath = getConfigPath(cwd);
1351
- if (!existsSync2(configPath)) {
1774
+ if (!existsSync3(configPath)) {
1352
1775
  throw new Error(
1353
1776
  `No ${CONFIG_FILE} found. Run "visor init" first.`
1354
1777
  );
1355
1778
  }
1356
- const raw = readFileSync5(configPath, "utf-8");
1779
+ const raw = readFileSync6(configPath, "utf-8");
1357
1780
  const parsed = JSON.parse(raw);
1358
1781
  const knownKeys = /* @__PURE__ */ new Set(["paths"]);
1359
1782
  for (const key of Object.keys(parsed)) {
@@ -1390,12 +1813,12 @@ function writeConfig(cwd, config) {
1390
1813
 
1391
1814
  // src/utils/packages.ts
1392
1815
  import { execFileSync } from "child_process";
1393
- import { existsSync as existsSync3, readFileSync as readFileSync6 } from "fs";
1394
- import { join as join5 } from "path";
1816
+ import { existsSync as existsSync4, readFileSync as readFileSync7 } from "fs";
1817
+ import { join as join6 } from "path";
1395
1818
  function readPackageJson(cwd) {
1396
- const pkgPath = join5(cwd, "package.json");
1397
- if (!existsSync3(pkgPath)) return null;
1398
- return JSON.parse(readFileSync6(pkgPath, "utf-8"));
1819
+ const pkgPath = join6(cwd, "package.json");
1820
+ if (!existsSync4(pkgPath)) return null;
1821
+ return JSON.parse(readFileSync7(pkgPath, "utf-8"));
1399
1822
  }
1400
1823
  function isPackageInstalled(packageName, cwd) {
1401
1824
  const pkg2 = readPackageJson(cwd);
@@ -1505,8 +1928,8 @@ export default function RootLayout({ children }: { children: ReactNode }) {
1505
1928
  }
1506
1929
 
1507
1930
  // src/commands/init-plays/registry.ts
1508
- import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync7, writeFileSync as writeFileSync2 } from "fs";
1509
- import { dirname as dirname4, join as join6 } from "path";
1931
+ import { existsSync as existsSync5, mkdirSync, readFileSync as readFileSync8, writeFileSync as writeFileSync2 } from "fs";
1932
+ import { dirname as dirname5, join as join7 } from "path";
1510
1933
 
1511
1934
  // src/commands/init-plays/pattern-build.ts
1512
1935
  var patternBuildPlay = {
@@ -1545,24 +1968,24 @@ function knownPlayIds() {
1545
1968
  return KNOWN_PLAYS.map((p) => p.id);
1546
1969
  }
1547
1970
  function playStatePaths(cwd, def, name) {
1548
- const relDir = join6(".lo", def.loSubdir, name);
1549
- const dir = join6(cwd, relDir);
1971
+ const relDir = join7(".lo", def.loSubdir, name);
1972
+ const dir = join7(cwd, relDir);
1550
1973
  return {
1551
1974
  dir,
1552
- statePath: join6(dir, "state.json"),
1553
- relStatePath: join6(relDir, "state.json")
1975
+ statePath: join7(dir, "state.json"),
1976
+ relStatePath: join7(relDir, "state.json")
1554
1977
  };
1555
1978
  }
1556
1979
  function readPlayState(statePath) {
1557
- if (!existsSync4(statePath)) return null;
1980
+ if (!existsSync5(statePath)) return null;
1558
1981
  try {
1559
- return JSON.parse(readFileSync7(statePath, "utf-8"));
1982
+ return JSON.parse(readFileSync8(statePath, "utf-8"));
1560
1983
  } catch {
1561
1984
  return null;
1562
1985
  }
1563
1986
  }
1564
1987
  function writePlayState(statePath, state) {
1565
- mkdirSync(dirname4(statePath), { recursive: true });
1988
+ mkdirSync(dirname5(statePath), { recursive: true });
1566
1989
  writeFileSync2(statePath, JSON.stringify(state, null, 2) + "\n", "utf-8");
1567
1990
  }
1568
1991
 
@@ -1627,16 +2050,16 @@ function heuristicPort(name) {
1627
2050
  }
1628
2051
 
1629
2052
  // src/lib/lo-play-checklist.ts
1630
- import { existsSync as existsSync5, readFileSync as readFileSync8 } from "fs";
2053
+ import { existsSync as existsSync6, readFileSync as readFileSync9 } from "fs";
1631
2054
  import { homedir } from "os";
1632
- import { join as join7 } from "path";
2055
+ import { join as join8 } from "path";
1633
2056
  function defaultChecklistRoot() {
1634
- return join7(homedir(), ".claude", "skills", "lo-play");
2057
+ return join8(homedir(), ".claude", "skills", "lo-play");
1635
2058
  }
1636
2059
  function readEntryChecklist(playType, options = {}) {
1637
2060
  const root = options.root ?? defaultChecklistRoot();
1638
- const path2 = join7(root, playType, "entry-checklist.md");
1639
- if (!existsSync5(path2)) {
2061
+ const path2 = join8(root, playType, "entry-checklist.md");
2062
+ if (!existsSync6(path2)) {
1640
2063
  return {
1641
2064
  found: false,
1642
2065
  path: path2,
@@ -1644,7 +2067,7 @@ function readEntryChecklist(playType, options = {}) {
1644
2067
  };
1645
2068
  }
1646
2069
  try {
1647
- return { found: true, path: path2, content: readFileSync8(path2, "utf-8") };
2070
+ return { found: true, path: path2, content: readFileSync9(path2, "utf-8") };
1648
2071
  } catch {
1649
2072
  return {
1650
2073
  found: false,
@@ -1686,34 +2109,17 @@ function initCommand(cwd, options) {
1686
2109
  process.exit(1);
1687
2110
  }
1688
2111
  }
1689
- if (options?.template === "nextjs" && existsSync6(join8(cwd, "package.json"))) {
2112
+ if (options?.template === "nextjs" && existsSync7(join9(cwd, "package.json"))) {
1690
2113
  emitError(
1691
2114
  json,
1692
2115
  "package.json already exists in this directory. visor init --template nextjs only scaffolds into empty directories. For an existing app, see the retrofit flow: https://visor.loworbit.studio/docs/guides/migration"
1693
2116
  );
1694
2117
  process.exit(1);
1695
2118
  }
1696
- if (configExists(cwd)) {
1697
- filesSkipped.push("visor.json");
1698
- if (!json) {
1699
- logger.warn("visor.json already exists. Skipping config creation.");
1700
- }
1701
- } else {
1702
- writeConfig(cwd, DEFAULT_CONFIG);
1703
- filesCreated.push("visor.json");
1704
- if (!json) {
1705
- logger.success("Created visor.json");
1706
- logger.blank();
1707
- logger.info("Default paths:");
1708
- logger.item(`components \u2192 ${DEFAULT_CONFIG.paths.components}`);
1709
- logger.item(`deck components \u2192 ${DEFAULT_CONFIG.paths.deckComponents}`);
1710
- logger.item(`blocks \u2192 ${DEFAULT_CONFIG.paths.blocks}`);
1711
- logger.item(`hooks \u2192 ${DEFAULT_CONFIG.paths.hooks}`);
1712
- logger.item(`lib \u2192 ${DEFAULT_CONFIG.paths.lib}`);
1713
- }
1714
- }
1715
2119
  if (options?.template === "nextjs") {
1716
2120
  scaffoldNextjs(cwd, json, filesCreated, filesSkipped, warnings);
2121
+ } else {
2122
+ writeVisorConfig(cwd, json, filesCreated, filesSkipped);
1717
2123
  }
1718
2124
  if (options?.template !== "nextjs") {
1719
2125
  const missingTokens = !hasVisorTokens(cwd);
@@ -1845,15 +2251,37 @@ function emitError(json, message) {
1845
2251
  logger.error(message);
1846
2252
  }
1847
2253
  }
2254
+ function writeVisorConfig(cwd, json, filesCreated, filesSkipped) {
2255
+ if (configExists(cwd)) {
2256
+ filesSkipped.push("visor.json");
2257
+ if (!json) {
2258
+ logger.warn("visor.json already exists. Skipping config creation.");
2259
+ }
2260
+ return;
2261
+ }
2262
+ writeConfig(cwd, DEFAULT_CONFIG);
2263
+ filesCreated.push("visor.json");
2264
+ if (!json) {
2265
+ logger.success("Created visor.json");
2266
+ logger.blank();
2267
+ logger.info("Default paths:");
2268
+ logger.item(`components \u2192 ${DEFAULT_CONFIG.paths.components}`);
2269
+ logger.item(`deck components \u2192 ${DEFAULT_CONFIG.paths.deckComponents}`);
2270
+ logger.item(`blocks \u2192 ${DEFAULT_CONFIG.paths.blocks}`);
2271
+ logger.item(`hooks \u2192 ${DEFAULT_CONFIG.paths.hooks}`);
2272
+ logger.item(`lib \u2192 ${DEFAULT_CONFIG.paths.lib}`);
2273
+ }
2274
+ }
1848
2275
  function scaffoldNextjs(cwd, json, filesCreated, filesSkipped, warnings) {
1849
2276
  if (!json) {
1850
2277
  logger.blank();
1851
2278
  logger.info("Scaffolding a Borealis-native Next.js app...");
1852
2279
  }
1853
2280
  runCreateNextApp(cwd, json);
2281
+ writeVisorConfig(cwd, json, filesCreated, filesSkipped);
1854
2282
  runInstallVisorDeps(cwd, json);
1855
- const yamlPath = join8(cwd, ".visor.yaml");
1856
- if (existsSync6(yamlPath)) {
2283
+ const yamlPath = join9(cwd, ".visor.yaml");
2284
+ if (existsSync7(yamlPath)) {
1857
2285
  filesSkipped.push(".visor.yaml");
1858
2286
  if (!json) {
1859
2287
  logger.warn(".visor.yaml already exists. Skipping.");
@@ -1871,10 +2299,10 @@ function scaffoldNextjs(cwd, json, filesCreated, filesSkipped, warnings) {
1871
2299
  tokens: data.tokens,
1872
2300
  config: data.config
1873
2301
  });
1874
- const globalsPath = join8(cwd, "app", "globals.css");
1875
- const globalsDir = dirname5(globalsPath);
2302
+ const globalsPath = join9(cwd, "app", "globals.css");
2303
+ const globalsDir = dirname6(globalsPath);
1876
2304
  mkdirSync2(globalsDir, { recursive: true });
1877
- if (existsSync6(globalsPath)) {
2305
+ if (existsSync7(globalsPath)) {
1878
2306
  writeFileSync3(globalsPath, css, "utf-8");
1879
2307
  filesCreated.push("app/globals.css");
1880
2308
  } else {
@@ -1884,16 +2312,16 @@ function scaffoldNextjs(cwd, json, filesCreated, filesSkipped, warnings) {
1884
2312
  if (!json) {
1885
2313
  logger.success("Created app/globals.css with theme tokens");
1886
2314
  }
1887
- const layoutPath = join8(cwd, "app", "layout.tsx");
2315
+ const layoutPath = join9(cwd, "app", "layout.tsx");
1888
2316
  const colorScheme = extractColorScheme(NEXTJS_STARTER_YAML);
1889
2317
  writeFileSync3(layoutPath, generateNextjsLayout(colorScheme), "utf-8");
1890
2318
  filesCreated.push("app/layout.tsx");
1891
2319
  if (!json) {
1892
2320
  logger.success("Wired app/layout.tsx with FOWT prevention and theme tokens");
1893
2321
  }
1894
- const stampDir = join8(cwd, ".lo");
1895
- const stampPath = join8(stampDir, "borealis.json");
1896
- if (existsSync6(stampPath)) {
2322
+ const stampDir = join9(cwd, ".lo");
2323
+ const stampPath = join9(stampDir, "borealis.json");
2324
+ if (existsSync7(stampPath)) {
1897
2325
  filesSkipped.push(".lo/borealis.json");
1898
2326
  if (!json) {
1899
2327
  logger.warn(".lo/borealis.json already exists. Skipping.");
@@ -1957,12 +2385,12 @@ function assertSpawnSuccess(result, label) {
1957
2385
  }
1958
2386
  function readVisorCliVersion() {
1959
2387
  try {
1960
- const here = dirname5(fileURLToPath2(import.meta.url));
2388
+ const here = dirname6(fileURLToPath2(import.meta.url));
1961
2389
  for (let i = 0; i < 5; i++) {
1962
2390
  const segments = new Array(i).fill("..");
1963
- const candidate = join8(here, ...segments, "package.json");
2391
+ const candidate = join9(here, ...segments, "package.json");
1964
2392
  try {
1965
- const pkg2 = JSON.parse(readFileSync9(candidate, "utf-8"));
2393
+ const pkg2 = JSON.parse(readFileSync10(candidate, "utf-8"));
1966
2394
  if (pkg2.name === "@loworbitstudio/visor" && pkg2.version) {
1967
2395
  return pkg2.version;
1968
2396
  }
@@ -1977,52 +2405,52 @@ function readVisorCliVersion() {
1977
2405
  // src/utils/fs.ts
1978
2406
  import {
1979
2407
  writeFileSync as writeFileSync4,
1980
- readFileSync as readFileSync10,
1981
- existsSync as existsSync7,
2408
+ readFileSync as readFileSync11,
2409
+ existsSync as existsSync8,
1982
2410
  mkdirSync as mkdirSync3
1983
2411
  } from "fs";
1984
- import { dirname as dirname6, join as join9 } from "path";
2412
+ import { dirname as dirname7, join as join10 } from "path";
1985
2413
  function resolveOutputPath(registryPath, type, config, cwd) {
1986
2414
  let relativePath;
1987
2415
  if (type === "registry:block") {
1988
2416
  relativePath = registryPath.replace(/^blocks\//, "");
1989
- return join9(cwd, config.paths.blocks, relativePath);
2417
+ return join10(cwd, config.paths.blocks, relativePath);
1990
2418
  }
1991
2419
  if (type === "registry:ui") {
1992
2420
  if (registryPath.startsWith("components/deck/")) {
1993
2421
  relativePath = registryPath.replace(/^components\/deck\//, "");
1994
- return join9(cwd, config.paths.deckComponents, relativePath);
2422
+ return join10(cwd, config.paths.deckComponents, relativePath);
1995
2423
  }
1996
2424
  if (registryPath.startsWith("components/flutter/")) {
1997
2425
  relativePath = registryPath.replace(/^components\/flutter\//, "");
1998
- return join9(cwd, config.paths.flutterComponents, relativePath);
2426
+ return join10(cwd, config.paths.flutterComponents, relativePath);
1999
2427
  }
2000
2428
  relativePath = registryPath.replace(/^components\/ui\//, "");
2001
- return join9(cwd, config.paths.components, relativePath);
2429
+ return join10(cwd, config.paths.components, relativePath);
2002
2430
  }
2003
2431
  if (type === "registry:hook") {
2004
2432
  relativePath = registryPath.replace(/^hooks\//, "");
2005
- return join9(cwd, config.paths.hooks, relativePath);
2433
+ return join10(cwd, config.paths.hooks, relativePath);
2006
2434
  }
2007
2435
  if (type === "registry:lib") {
2008
2436
  relativePath = registryPath.replace(/^lib\//, "");
2009
- return join9(cwd, config.paths.lib, relativePath);
2437
+ return join10(cwd, config.paths.lib, relativePath);
2010
2438
  }
2011
- return join9(cwd, registryPath);
2439
+ return join10(cwd, registryPath);
2012
2440
  }
2013
2441
  function writeFile(filePath, content) {
2014
- const dir = dirname6(filePath);
2015
- if (!existsSync7(dir)) {
2442
+ const dir = dirname7(filePath);
2443
+ if (!existsSync8(dir)) {
2016
2444
  mkdirSync3(dir, { recursive: true });
2017
2445
  }
2018
2446
  writeFileSync4(filePath, content, "utf-8");
2019
2447
  }
2020
2448
  function readFile(filePath) {
2021
- if (!existsSync7(filePath)) return null;
2022
- return readFileSync10(filePath, "utf-8");
2449
+ if (!existsSync8(filePath)) return null;
2450
+ return readFileSync11(filePath, "utf-8");
2023
2451
  }
2024
2452
  function fileExists(filePath) {
2025
- return existsSync7(filePath);
2453
+ return existsSync8(filePath);
2026
2454
  }
2027
2455
 
2028
2456
  // src/commands/list.ts
@@ -2163,8 +2591,8 @@ function listCommand(cwd, options = {}) {
2163
2591
  }
2164
2592
 
2165
2593
  // src/utils/pubspec.ts
2166
- import { existsSync as existsSync8, readFileSync as readFileSync11, writeFileSync as writeFileSync5 } from "fs";
2167
- import { join as join10 } from "path";
2594
+ import { existsSync as existsSync9, readFileSync as readFileSync12, writeFileSync as writeFileSync5 } from "fs";
2595
+ import { join as join11 } from "path";
2168
2596
  import { parseDocument, YAMLMap } from "yaml";
2169
2597
  function mergePubspec(pubspecText, deps) {
2170
2598
  const doc = parseDocument(pubspecText);
@@ -2186,14 +2614,14 @@ function mergePubspec(pubspecText, deps) {
2186
2614
  return { text: doc.toString(), added, skipped };
2187
2615
  }
2188
2616
  function pubspecPath(cwd) {
2189
- return join10(cwd, "pubspec.yaml");
2617
+ return join11(cwd, "pubspec.yaml");
2190
2618
  }
2191
2619
  function pubspecExists(cwd) {
2192
- return existsSync8(pubspecPath(cwd));
2620
+ return existsSync9(pubspecPath(cwd));
2193
2621
  }
2194
2622
  function isPubPackageInstalled(packageName, cwd) {
2195
2623
  if (!pubspecExists(cwd)) return false;
2196
- const text = readFileSync11(pubspecPath(cwd), "utf-8");
2624
+ const text = readFileSync12(pubspecPath(cwd), "utf-8");
2197
2625
  const doc = parseDocument(text);
2198
2626
  const depsNode = doc.get("dependencies");
2199
2627
  if (!(depsNode instanceof YAMLMap)) return false;
@@ -2204,12 +2632,12 @@ function getUninstalledPubDeps(deps, cwd) {
2204
2632
  }
2205
2633
  function addPubDependencies(deps, cwd) {
2206
2634
  const path2 = pubspecPath(cwd);
2207
- if (!existsSync8(path2)) {
2635
+ if (!existsSync9(path2)) {
2208
2636
  throw new Error(
2209
2637
  `No pubspec.yaml found at ${path2}. Run this command from a Flutter project root.`
2210
2638
  );
2211
2639
  }
2212
- const text = readFileSync11(path2, "utf-8");
2640
+ const text = readFileSync12(path2, "utf-8");
2213
2641
  const result = mergePubspec(text, deps);
2214
2642
  if (result.added.length > 0) {
2215
2643
  writeFileSync5(path2, result.text, "utf-8");
@@ -2219,12 +2647,12 @@ function addPubDependencies(deps, cwd) {
2219
2647
 
2220
2648
  // src/utils/flutter.ts
2221
2649
  import { execFileSync as execFileSync2 } from "child_process";
2222
- import { existsSync as existsSync9, readdirSync as readdirSync3, statSync as statSync4 } from "fs";
2650
+ import { existsSync as existsSync10, readdirSync as readdirSync3, statSync as statSync5 } from "fs";
2223
2651
  import { homedir as homedir2 } from "os";
2224
- import { join as join11 } from "path";
2652
+ import { join as join12 } from "path";
2225
2653
  function isExecutable(path2) {
2226
2654
  try {
2227
- const s = statSync4(path2);
2655
+ const s = statSync5(path2);
2228
2656
  return s.isFile();
2229
2657
  } catch {
2230
2658
  return false;
@@ -2236,20 +2664,20 @@ function fromPath(env) {
2236
2664
  const bin = process.platform === "win32" ? "flutter.bat" : "flutter";
2237
2665
  for (const dir of pathVar.split(sep2)) {
2238
2666
  if (!dir) continue;
2239
- const candidate = join11(dir, bin);
2667
+ const candidate = join12(dir, bin);
2240
2668
  if (isExecutable(candidate)) return candidate;
2241
2669
  }
2242
2670
  return null;
2243
2671
  }
2244
2672
  function fromFvm(home) {
2245
- const fvmDefault = join11(home, "fvm", "default", "bin", "flutter");
2673
+ const fvmDefault = join12(home, "fvm", "default", "bin", "flutter");
2246
2674
  if (isExecutable(fvmDefault)) return fvmDefault;
2247
- const versionsDir = join11(home, "fvm", "versions");
2248
- if (!existsSync9(versionsDir)) return null;
2675
+ const versionsDir = join12(home, "fvm", "versions");
2676
+ if (!existsSync10(versionsDir)) return null;
2249
2677
  let best = null;
2250
2678
  try {
2251
2679
  for (const name of readdirSync3(versionsDir)) {
2252
- const candidate = join11(versionsDir, name, "bin", "flutter");
2680
+ const candidate = join12(versionsDir, name, "bin", "flutter");
2253
2681
  if (!isExecutable(candidate)) continue;
2254
2682
  if (!best || compareSemver(name, best.version) > 0) {
2255
2683
  best = { version: name, path: candidate };
@@ -2275,7 +2703,7 @@ function findFlutterBin(options = {}) {
2275
2703
  const home = options.home ?? homedir2();
2276
2704
  const envRoot = env.FLUTTER_ROOT;
2277
2705
  if (envRoot) {
2278
- const bin = join11(envRoot, "bin", "flutter");
2706
+ const bin = join12(envRoot, "bin", "flutter");
2279
2707
  if (isExecutable(bin)) return bin;
2280
2708
  }
2281
2709
  const fromPathBin = fromPath(env);
@@ -2938,8 +3366,8 @@ function infoCommand(name, cwd, options = {}) {
2938
3366
  }
2939
3367
 
2940
3368
  // src/commands/theme-apply.ts
2941
- import { readFileSync as readFileSync13, writeFileSync as writeFileSync8, mkdirSync as mkdirSync6 } from "fs";
2942
- import { resolve as resolve7, dirname as dirname8, join as join15 } from "path";
3369
+ import { readFileSync as readFileSync14, writeFileSync as writeFileSync8, mkdirSync as mkdirSync6 } from "fs";
3370
+ import { resolve as resolve8, dirname as dirname9, join as join16 } from "path";
2943
3371
  import { generateTheme, generateThemeData as generateThemeData3 } from "@loworbitstudio/visor-theme-engine";
2944
3372
  import {
2945
3373
  nextjsAdapter as nextjsAdapter2,
@@ -2950,8 +3378,8 @@ import {
2950
3378
  } from "@loworbitstudio/visor-theme-engine/adapters";
2951
3379
 
2952
3380
  // src/lib/blessed-manifest.ts
2953
- import { existsSync as existsSync10, readFileSync as readFileSync12 } from "fs";
2954
- import { join as join12 } from "path";
3381
+ import { existsSync as existsSync11, readFileSync as readFileSync13 } from "fs";
3382
+ import { join as join13 } from "path";
2955
3383
  import { z } from "zod";
2956
3384
  var BLESSED_MANIFEST_FILENAME = "blessed-manifest.json";
2957
3385
  var themeApplyTargetSchema = z.discriminatedUnion("kind", [
@@ -2983,8 +3411,8 @@ var blessedManifestSchema = z.object({
2983
3411
  theme_apply_target: themeApplyTargetSchema.optional()
2984
3412
  }).strict();
2985
3413
  function parseBlessedManifest(buildDir) {
2986
- const manifestPath = join12(buildDir, BLESSED_MANIFEST_FILENAME);
2987
- if (!existsSync10(manifestPath)) {
3414
+ const manifestPath = join13(buildDir, BLESSED_MANIFEST_FILENAME);
3415
+ if (!existsSync11(manifestPath)) {
2988
3416
  return {
2989
3417
  ok: false,
2990
3418
  error: `this directory is not a blessed build (missing ${BLESSED_MANIFEST_FILENAME}); see docs/blessed-builds.md`
@@ -2992,7 +3420,7 @@ function parseBlessedManifest(buildDir) {
2992
3420
  }
2993
3421
  let raw;
2994
3422
  try {
2995
- raw = readFileSync12(manifestPath, "utf-8");
3423
+ raw = readFileSync13(manifestPath, "utf-8");
2996
3424
  } catch {
2997
3425
  return { ok: false, error: `could not read ${manifestPath}` };
2998
3426
  }
@@ -3018,31 +3446,31 @@ function parseBlessedManifest(buildDir) {
3018
3446
  }
3019
3447
 
3020
3448
  // src/lib/theme-apply-targets/globals-css.ts
3021
- import { existsSync as existsSync11, mkdirSync as mkdirSync4, writeFileSync as writeFileSync6 } from "fs";
3022
- import { dirname as dirname7, isAbsolute, join as join13, resolve as resolve5 } from "path";
3449
+ import { existsSync as existsSync12, mkdirSync as mkdirSync4, writeFileSync as writeFileSync6 } from "fs";
3450
+ import { dirname as dirname8, isAbsolute as isAbsolute2, join as join14, resolve as resolve6 } from "path";
3023
3451
  function applyGlobalsCss(input) {
3024
3452
  const outFile = input.path !== void 0 ? resolveInsideBuild(input.buildDir, input.path) : resolveDefaultGlobalsCss(input.buildDir);
3025
- mkdirSync4(dirname7(outFile), { recursive: true });
3453
+ mkdirSync4(dirname8(outFile), { recursive: true });
3026
3454
  writeFileSync6(outFile, input.adapterCss, "utf-8");
3027
3455
  return outFile;
3028
3456
  }
3029
3457
  function resolveDefaultGlobalsCss(buildDir) {
3030
3458
  const candidates = [
3031
- join13(buildDir, "app", "globals.css"),
3032
- join13(buildDir, "src", "app", "globals.css")
3459
+ join14(buildDir, "app", "globals.css"),
3460
+ join14(buildDir, "src", "app", "globals.css")
3033
3461
  ];
3034
- return candidates.find((candidate) => existsSync11(candidate)) ?? candidates[0];
3462
+ return candidates.find((candidate) => existsSync12(candidate)) ?? candidates[0];
3035
3463
  }
3036
3464
  function resolveInsideBuild(buildDir, relativeOrAbsolute) {
3037
- return isAbsolute(relativeOrAbsolute) ? relativeOrAbsolute : resolve5(buildDir, relativeOrAbsolute);
3465
+ return isAbsolute2(relativeOrAbsolute) ? relativeOrAbsolute : resolve6(buildDir, relativeOrAbsolute);
3038
3466
  }
3039
3467
 
3040
3468
  // src/lib/theme-apply-targets/themes-css-dir.ts
3041
3469
  import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync7 } from "fs";
3042
- import { isAbsolute as isAbsolute2, join as join14, resolve as resolve6 } from "path";
3470
+ import { isAbsolute as isAbsolute3, join as join15, resolve as resolve7 } from "path";
3043
3471
  function applyThemesCssDir(input) {
3044
- const dir = isAbsolute2(input.path) ? input.path : resolve6(input.buildDir, input.path);
3045
- const outFile = join14(dir, `${input.themeId}.css`);
3472
+ const dir = isAbsolute3(input.path) ? input.path : resolve7(input.buildDir, input.path);
3473
+ const outFile = join15(dir, `${input.themeId}.css`);
3046
3474
  mkdirSync5(dir, { recursive: true });
3047
3475
  writeFileSync7(outFile, input.adapterCss, "utf-8");
3048
3476
  return outFile;
@@ -3106,10 +3534,10 @@ function defaultOutputPath(adapter, themeName) {
3106
3534
  }
3107
3535
  }
3108
3536
  function themeApplyCommand(file, cwd, options) {
3109
- const filePath = resolve7(cwd, file);
3537
+ const filePath = resolve8(cwd, file);
3110
3538
  let yamlContent;
3111
3539
  try {
3112
- yamlContent = readFileSync13(filePath, "utf-8");
3540
+ yamlContent = readFileSync14(filePath, "utf-8");
3113
3541
  } catch {
3114
3542
  if (options.json) {
3115
3543
  console.log(
@@ -3203,7 +3631,7 @@ function themeApplyCommand(file, cwd, options) {
3203
3631
  else logger.error(message);
3204
3632
  process.exit(1);
3205
3633
  }
3206
- const buildDir = resolve7(cwd, options.targetPath);
3634
+ const buildDir = resolve8(cwd, options.targetPath);
3207
3635
  const manifestResult = parseBlessedManifest(buildDir);
3208
3636
  if (!manifestResult.ok) {
3209
3637
  if (options.json) {
@@ -3246,14 +3674,14 @@ function themeApplyCommand(file, cwd, options) {
3246
3674
  return;
3247
3675
  }
3248
3676
  const outputTarget = options.output ?? defaultOutputPath(options.adapter, themeName);
3249
- const outputPath = resolve7(cwd, outputTarget);
3677
+ const outputPath = resolve8(cwd, outputTarget);
3250
3678
  if (fileMap) {
3251
3679
  try {
3252
3680
  mkdirSync6(outputPath, { recursive: true });
3253
3681
  let totalBytes = 0;
3254
3682
  for (const [relPath, content] of Object.entries(fileMap.files)) {
3255
- const filePath2 = join15(outputPath, relPath);
3256
- mkdirSync6(dirname8(filePath2), { recursive: true });
3683
+ const filePath2 = join16(outputPath, relPath);
3684
+ mkdirSync6(dirname9(filePath2), { recursive: true });
3257
3685
  writeFileSync8(filePath2, content, "utf-8");
3258
3686
  totalBytes += content.length;
3259
3687
  }
@@ -3291,7 +3719,7 @@ function themeApplyCommand(file, cwd, options) {
3291
3719
  if (css === null) {
3292
3720
  process.exit(1);
3293
3721
  }
3294
- const outputDir = dirname8(outputPath);
3722
+ const outputDir = dirname9(outputPath);
3295
3723
  try {
3296
3724
  mkdirSync6(outputDir, { recursive: true });
3297
3725
  writeFileSync8(outputPath, css, "utf-8");
@@ -3335,8 +3763,8 @@ function formatSize(bytes) {
3335
3763
  }
3336
3764
 
3337
3765
  // src/commands/theme-export.ts
3338
- import { readFileSync as readFileSync14 } from "fs";
3339
- import { resolve as resolve8 } from "path";
3766
+ import { readFileSync as readFileSync15 } from "fs";
3767
+ import { resolve as resolve9 } from "path";
3340
3768
  import {
3341
3769
  parseConfig,
3342
3770
  resolveConfig,
@@ -3344,10 +3772,10 @@ import {
3344
3772
  exportTheme
3345
3773
  } from "@loworbitstudio/visor-theme-engine";
3346
3774
  function themeExportCommand(file, cwd, options) {
3347
- const filePath = resolve8(cwd, file ?? ".visor.yaml");
3775
+ const filePath = resolve9(cwd, file ?? ".visor.yaml");
3348
3776
  let yamlContent;
3349
3777
  try {
3350
- yamlContent = readFileSync14(filePath, "utf-8");
3778
+ yamlContent = readFileSync15(filePath, "utf-8");
3351
3779
  } catch {
3352
3780
  if (options.json) {
3353
3781
  console.log(
@@ -3399,16 +3827,16 @@ function themeExportCommand(file, cwd, options) {
3399
3827
  }
3400
3828
 
3401
3829
  // src/commands/theme-validate.ts
3402
- import { readFileSync as readFileSync15 } from "fs";
3403
- import { resolve as resolve9 } from "path";
3830
+ import { readFileSync as readFileSync16 } from "fs";
3831
+ import { resolve as resolve10 } from "path";
3404
3832
  import { parse as parseYaml2 } from "yaml";
3405
3833
  import { validate } from "@loworbitstudio/visor-theme-engine";
3406
3834
  import pc3 from "picocolors";
3407
3835
  function themeValidateCommand(file, cwd, options) {
3408
- const filePath = resolve9(cwd, file);
3836
+ const filePath = resolve10(cwd, file);
3409
3837
  let fileContent;
3410
3838
  try {
3411
- fileContent = readFileSync15(filePath, "utf-8");
3839
+ fileContent = readFileSync16(filePath, "utf-8");
3412
3840
  } catch {
3413
3841
  if (options.json) {
3414
3842
  console.log(
@@ -3497,8 +3925,8 @@ function printIssue(issue) {
3497
3925
 
3498
3926
  // src/commands/theme-verify.ts
3499
3927
  import { spawnSync as _spawnSync } from "child_process";
3500
- import { existsSync as existsSync12 } from "fs";
3501
- import { resolve as resolve10 } from "path";
3928
+ import { existsSync as existsSync13 } from "fs";
3929
+ import { resolve as resolve11 } from "path";
3502
3930
  function themeVerifyCommand(dir, cwd, options, _spawnFn = _spawnSync) {
3503
3931
  const target = options.target ?? "flutter";
3504
3932
  if (target !== "flutter") {
@@ -3520,8 +3948,8 @@ function themeVerifyCommand(dir, cwd, options, _spawnFn = _spawnSync) {
3520
3948
  }
3521
3949
  process.exit(1);
3522
3950
  }
3523
- const dirPath = resolve10(cwd, dir);
3524
- if (!existsSync12(dirPath)) {
3951
+ const dirPath = resolve11(cwd, dir);
3952
+ if (!existsSync13(dirPath)) {
3525
3953
  if (options.json) {
3526
3954
  console.log(
3527
3955
  JSON.stringify({
@@ -3608,8 +4036,8 @@ function themeVerifyCommand(dir, cwd, options, _spawnFn = _spawnSync) {
3608
4036
  }
3609
4037
 
3610
4038
  // src/commands/theme-extract.ts
3611
- import { readFileSync as readFileSync16, writeFileSync as writeFileSync9, existsSync as existsSync13, readdirSync as readdirSync4, statSync as statSync5 } from "fs";
3612
- import { resolve as resolve11, join as join16, basename as basename3, extname as extname3, relative } from "path";
4039
+ import { readFileSync as readFileSync17, writeFileSync as writeFileSync9, existsSync as existsSync14, readdirSync as readdirSync4, statSync as statSync6 } from "fs";
4040
+ import { resolve as resolve12, join as join17, basename as basename3, extname as extname3, relative } from "path";
3613
4041
  import { stringify as stringifyYaml } from "yaml";
3614
4042
  import {
3615
4043
  extractFromCSS,
@@ -3638,8 +4066,8 @@ var CSS_DIRS = [
3638
4066
  "packages/design-tokens"
3639
4067
  ];
3640
4068
  function themeExtractCommand(cwd, options) {
3641
- const targetDir = resolve11(cwd, options.from ?? ".");
3642
- if (!existsSync13(targetDir)) {
4069
+ const targetDir = resolve12(cwd, options.from ?? ".");
4070
+ if (!existsSync14(targetDir)) {
3643
4071
  if (options.json) {
3644
4072
  console.log(JSON.stringify({ success: false, error: `Directory not found: ${targetDir}` }));
3645
4073
  } else {
@@ -3693,16 +4121,16 @@ function collectCSSFiles(targetDir) {
3693
4121
  const files = [];
3694
4122
  const seen = /* @__PURE__ */ new Set();
3695
4123
  for (const pattern2 of CSS_FILE_PATTERNS) {
3696
- const rootPath = join16(targetDir, pattern2);
4124
+ const rootPath = join17(targetDir, pattern2);
3697
4125
  addFileIfExists(rootPath, files, seen);
3698
4126
  for (const dir of CSS_DIRS) {
3699
- const dirPath = join16(targetDir, dir, pattern2);
4127
+ const dirPath = join17(targetDir, dir, pattern2);
3700
4128
  addFileIfExists(dirPath, files, seen);
3701
4129
  }
3702
4130
  }
3703
4131
  for (const dir of CSS_DIRS) {
3704
- const dirPath = join16(targetDir, dir);
3705
- if (existsSync13(dirPath) && statSync5(dirPath).isDirectory()) {
4132
+ const dirPath = join17(targetDir, dir);
4133
+ if (existsSync14(dirPath) && statSync6(dirPath).isDirectory()) {
3706
4134
  scanDirForCSS(dirPath, files, seen, 2);
3707
4135
  }
3708
4136
  }
@@ -3710,11 +4138,11 @@ function collectCSSFiles(targetDir) {
3710
4138
  return files;
3711
4139
  }
3712
4140
  function addFileIfExists(filePath, files, seen) {
3713
- const resolved = resolve11(filePath);
4141
+ const resolved = resolve12(filePath);
3714
4142
  if (seen.has(resolved)) return;
3715
- if (!existsSync13(resolved)) return;
4143
+ if (!existsSync14(resolved)) return;
3716
4144
  try {
3717
- const content = readFileSync16(resolved, "utf-8");
4145
+ const content = readFileSync17(resolved, "utf-8");
3718
4146
  if (content.includes("--")) {
3719
4147
  files.push({ path: resolved, content });
3720
4148
  seen.add(resolved);
@@ -3723,7 +4151,7 @@ function addFileIfExists(filePath, files, seen) {
3723
4151
  }
3724
4152
  }
3725
4153
  function scanDirForCSS(dir, files, seen, maxDepth) {
3726
- if (!existsSync13(dir)) return;
4154
+ if (!existsSync14(dir)) return;
3727
4155
  const SKIP_DIRS = /* @__PURE__ */ new Set([
3728
4156
  "node_modules",
3729
4157
  ".next",
@@ -3742,10 +4170,10 @@ function scanDirForCSS(dir, files, seen, maxDepth) {
3742
4170
  if (entry.isDirectory()) {
3743
4171
  if (SKIP_DIRS.has(entry.name)) continue;
3744
4172
  if (maxDepth > 0) {
3745
- scanDirForCSS(join16(dir, entry.name), files, seen, maxDepth - 1);
4173
+ scanDirForCSS(join17(dir, entry.name), files, seen, maxDepth - 1);
3746
4174
  }
3747
4175
  } else if (entry.isFile() && extname3(entry.name) === ".css") {
3748
- addFileIfExists(join16(dir, entry.name), files, seen);
4176
+ addFileIfExists(join17(dir, entry.name), files, seen);
3749
4177
  }
3750
4178
  }
3751
4179
  } catch {
@@ -3827,10 +4255,10 @@ function extractVarName(varExpr) {
3827
4255
  function parseNextFontFromLayouts(targetDir) {
3828
4256
  const fontMap = /* @__PURE__ */ new Map();
3829
4257
  for (const relPath of LAYOUT_FILE_PATHS) {
3830
- const fullPath = join16(targetDir, relPath);
3831
- if (!existsSync13(fullPath)) continue;
4258
+ const fullPath = join17(targetDir, relPath);
4259
+ if (!existsSync14(fullPath)) continue;
3832
4260
  try {
3833
- const content = readFileSync16(fullPath, "utf-8");
4261
+ const content = readFileSync17(fullPath, "utf-8");
3834
4262
  parseNextFontDeclarations(content, fontMap);
3835
4263
  } catch {
3836
4264
  }
@@ -3915,10 +4343,10 @@ var MONO_FONT_NAMES = /* @__PURE__ */ new Set([
3915
4343
  "IBM Plex Mono"
3916
4344
  ]);
3917
4345
  function extractFontHints(targetDir) {
3918
- const pkgPath = join16(targetDir, "package.json");
3919
- if (!existsSync13(pkgPath)) return void 0;
4346
+ const pkgPath = join17(targetDir, "package.json");
4347
+ if (!existsSync14(pkgPath)) return void 0;
3920
4348
  try {
3921
- const pkg2 = JSON.parse(readFileSync16(pkgPath, "utf-8"));
4349
+ const pkg2 = JSON.parse(readFileSync17(pkgPath, "utf-8"));
3922
4350
  const allDeps = { ...pkg2.dependencies, ...pkg2.devDependencies };
3923
4351
  const fonts2 = [];
3924
4352
  for (const [dep, _] of Object.entries(allDeps)) {
@@ -3954,10 +4382,10 @@ function extractFontHints(targetDir) {
3954
4382
  }
3955
4383
  }
3956
4384
  function inferThemeName(targetDir) {
3957
- const pkgPath = join16(targetDir, "package.json");
3958
- if (existsSync13(pkgPath)) {
4385
+ const pkgPath = join17(targetDir, "package.json");
4386
+ if (existsSync14(pkgPath)) {
3959
4387
  try {
3960
- const pkg2 = JSON.parse(readFileSync16(pkgPath, "utf-8"));
4388
+ const pkg2 = JSON.parse(readFileSync17(pkgPath, "utf-8"));
3961
4389
  if (pkg2.name) {
3962
4390
  const name = pkg2.name.replace(/^@[\w-]+\//, "");
3963
4391
  return `${name}-theme`;
@@ -3994,7 +4422,7 @@ function outputJSON(result, validationResult) {
3994
4422
  }
3995
4423
  function outputYAML(result, outputPath, cwd, validationResult) {
3996
4424
  const yamlStr = buildAnnotatedYAML(result);
3997
- const outFile = resolve11(cwd, outputPath ?? ".visor.yaml");
4425
+ const outFile = resolve12(cwd, outputPath ?? ".visor.yaml");
3998
4426
  const high = result.tokens.filter((t) => t.confidence === "high").length;
3999
4427
  const med = result.tokens.filter((t) => t.confidence === "medium").length;
4000
4428
  const low = result.tokens.filter((t) => t.confidence === "low").length;
@@ -4080,14 +4508,14 @@ function buildAnnotatedYAML(result) {
4080
4508
  }
4081
4509
 
4082
4510
  // src/commands/theme-register.ts
4083
- import { readFileSync as readFileSync18, writeFileSync as writeFileSync10, mkdirSync as mkdirSync7, existsSync as existsSync15 } from "fs";
4084
- import { resolve as resolve13, join as join18 } from "path";
4511
+ import { readFileSync as readFileSync19, writeFileSync as writeFileSync10, mkdirSync as mkdirSync7, existsSync as existsSync16 } from "fs";
4512
+ import { resolve as resolve14, join as join19 } from "path";
4085
4513
  import { generateThemeData as generateThemeData4 } from "@loworbitstudio/visor-theme-engine";
4086
4514
  import { docsAdapter as docsAdapter2 } from "@loworbitstudio/visor-theme-engine/adapters";
4087
4515
 
4088
4516
  // src/utils/theme-helpers.ts
4089
- import { existsSync as existsSync14, lstatSync, readdirSync as readdirSync5, readFileSync as readFileSync17, readlinkSync, realpathSync, statSync as statSync6 } from "fs";
4090
- import { resolve as resolve12, dirname as dirname9, join as join17 } from "path";
4517
+ import { existsSync as existsSync15, lstatSync, readdirSync as readdirSync5, readFileSync as readFileSync18, readlinkSync, realpathSync, statSync as statSync7 } from "fs";
4518
+ import { resolve as resolve13, dirname as dirname10, join as join18 } from "path";
4091
4519
  import { execFileSync as execFileSync3 } from "child_process";
4092
4520
  function toSlug(name) {
4093
4521
  return name.toLowerCase().replace(/\s+/g, "-");
@@ -4096,12 +4524,12 @@ function toLabel(name) {
4096
4524
  return name.split(/[\s-]+/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
4097
4525
  }
4098
4526
  function findRepoRoot(startDir) {
4099
- let current = resolve12(startDir);
4527
+ let current = resolve13(startDir);
4100
4528
  while (true) {
4101
- if (existsSync14(join17(current, "packages", "docs"))) {
4529
+ if (existsSync15(join18(current, "packages", "docs"))) {
4102
4530
  return current;
4103
4531
  }
4104
- const parent = dirname9(current);
4532
+ const parent = dirname10(current);
4105
4533
  if (parent === current) break;
4106
4534
  current = parent;
4107
4535
  }
@@ -4115,9 +4543,9 @@ function findMainRepoRoot(startDir) {
4115
4543
  stdio: ["ignore", "pipe", "ignore"]
4116
4544
  }).trim();
4117
4545
  if (commonDir) {
4118
- const absoluteCommonDir = resolve12(startDir, commonDir);
4119
- const candidate = dirname9(absoluteCommonDir);
4120
- if (existsSync14(join17(candidate, "packages", "docs"))) {
4546
+ const absoluteCommonDir = resolve13(startDir, commonDir);
4547
+ const candidate = dirname10(absoluteCommonDir);
4548
+ if (existsSync15(join18(candidate, "packages", "docs"))) {
4121
4549
  return candidate;
4122
4550
  }
4123
4551
  }
@@ -4136,10 +4564,10 @@ var BrokenSymlinkError = class extends Error {
4136
4564
  code = "BROKEN_SYMLINK";
4137
4565
  };
4138
4566
  function assertNoBrokenSymlinks(dir) {
4139
- if (!existsSync14(dir)) return;
4567
+ if (!existsSync15(dir)) return;
4140
4568
  const entries = readdirSync5(dir, { withFileTypes: true });
4141
4569
  for (const entry of entries) {
4142
- const entryPath = join17(dir, entry.name);
4570
+ const entryPath = join18(dir, entry.name);
4143
4571
  let lst;
4144
4572
  try {
4145
4573
  lst = lstatSync(entryPath);
@@ -4149,7 +4577,7 @@ function assertNoBrokenSymlinks(dir) {
4149
4577
  if (lst.isSymbolicLink()) {
4150
4578
  const target = readlinkSync(entryPath);
4151
4579
  try {
4152
- statSync6(entryPath);
4580
+ statSync7(entryPath);
4153
4581
  } catch {
4154
4582
  throw new BrokenSymlinkError(entryPath, target);
4155
4583
  }
@@ -4157,39 +4585,39 @@ function assertNoBrokenSymlinks(dir) {
4157
4585
  }
4158
4586
  }
4159
4587
  function scanParentForPrivateThemes(parentDir) {
4160
- if (!existsSync14(parentDir)) return [];
4588
+ if (!existsSync15(parentDir)) return [];
4161
4589
  const entries = readdirSync5(parentDir, { withFileTypes: true });
4162
4590
  const matches = [];
4163
4591
  for (const entry of entries) {
4164
4592
  if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
4165
- const candidate = join17(parentDir, entry.name, "visor-themes-private", "themes");
4166
- if (existsSync14(candidate)) {
4593
+ const candidate = join18(parentDir, entry.name, "visor-themes-private", "themes");
4594
+ if (existsSync15(candidate)) {
4167
4595
  matches.push(candidate);
4168
4596
  }
4169
4597
  }
4170
4598
  return matches.sort();
4171
4599
  }
4172
4600
  function scanNestedThemeDir(dir) {
4173
- if (!existsSync14(dir)) return [];
4601
+ if (!existsSync15(dir)) return [];
4174
4602
  assertNoBrokenSymlinks(dir);
4175
4603
  const entries = readdirSync5(dir, { withFileTypes: true });
4176
4604
  const out = [];
4177
4605
  for (const entry of entries) {
4178
4606
  if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
4179
- const themeFile = join17(dir, entry.name, "theme.visor.yaml");
4180
- if (existsSync14(themeFile)) {
4607
+ const themeFile = join18(dir, entry.name, "theme.visor.yaml");
4608
+ if (existsSync15(themeFile)) {
4181
4609
  out.push({ filePath: themeFile, slug: entry.name });
4182
4610
  }
4183
4611
  }
4184
4612
  return out;
4185
4613
  }
4186
4614
  function detectVisorWorkspace(cwd) {
4187
- let current = resolve12(cwd);
4615
+ let current = resolve13(cwd);
4188
4616
  while (true) {
4189
- const pkgPath = join17(current, "package.json");
4190
- if (existsSync14(pkgPath)) {
4617
+ const pkgPath = join18(current, "package.json");
4618
+ if (existsSync15(pkgPath)) {
4191
4619
  try {
4192
- const pkg2 = JSON.parse(readFileSync17(pkgPath, "utf-8"));
4620
+ const pkg2 = JSON.parse(readFileSync18(pkgPath, "utf-8"));
4193
4621
  const workspaces = pkg2.workspaces ?? [];
4194
4622
  const hasCli = workspaces.some((w) => w.includes("packages/cli"));
4195
4623
  const hasEngine = workspaces.some((w) => w.includes("packages/theme-engine"));
@@ -4199,7 +4627,7 @@ function detectVisorWorkspace(cwd) {
4199
4627
  } catch {
4200
4628
  }
4201
4629
  }
4202
- const parent = dirname9(current);
4630
+ const parent = dirname10(current);
4203
4631
  if (parent === current) break;
4204
4632
  current = parent;
4205
4633
  }
@@ -4207,18 +4635,18 @@ function detectVisorWorkspace(cwd) {
4207
4635
  }
4208
4636
  function isLocalVisorBinary(workspaceRoot, scriptPath) {
4209
4637
  if (!scriptPath) return false;
4210
- const expectedPrefix = join17(workspaceRoot, "packages", "cli", "dist");
4638
+ const expectedPrefix = join18(workspaceRoot, "packages", "cli", "dist");
4211
4639
  let resolvedScript;
4212
4640
  let resolvedPrefix;
4213
4641
  try {
4214
4642
  resolvedScript = realpathSync(scriptPath);
4215
4643
  } catch {
4216
- resolvedScript = resolve12(scriptPath);
4644
+ resolvedScript = resolve13(scriptPath);
4217
4645
  }
4218
4646
  try {
4219
4647
  resolvedPrefix = realpathSync(expectedPrefix);
4220
4648
  } catch {
4221
- resolvedPrefix = resolve12(expectedPrefix);
4649
+ resolvedPrefix = resolve13(expectedPrefix);
4222
4650
  }
4223
4651
  return resolvedScript.startsWith(resolvedPrefix + "/") || resolvedScript === resolvedPrefix;
4224
4652
  }
@@ -4308,10 +4736,10 @@ ${indent}${newEntry},
4308
4736
  return { updated, changed: true };
4309
4737
  }
4310
4738
  function themeRegisterCommand(file, cwd, options) {
4311
- const filePath = resolve13(cwd, file);
4739
+ const filePath = resolve14(cwd, file);
4312
4740
  let yamlContent;
4313
4741
  try {
4314
- yamlContent = readFileSync18(filePath, "utf-8");
4742
+ yamlContent = readFileSync19(filePath, "utf-8");
4315
4743
  } catch {
4316
4744
  if (options.json) {
4317
4745
  console.log(JSON.stringify({ success: false, error: `Could not read file: ${filePath}` }));
@@ -4354,11 +4782,11 @@ function themeRegisterCommand(file, cwd, options) {
4354
4782
  process.exit(1);
4355
4783
  return;
4356
4784
  }
4357
- const docsAppDir = join18(repoRoot, "packages", "docs", "app");
4358
- const cssFilePath = join18(docsAppDir, `${slug2}-theme.css`);
4359
- const globalsPath = join18(docsAppDir, "globals.css");
4360
- const themeConfigPath = join18(repoRoot, "packages", "docs", "lib", "theme-config.ts");
4361
- if (!existsSync15(docsAppDir)) {
4785
+ const docsAppDir = join19(repoRoot, "packages", "docs", "app");
4786
+ const cssFilePath = join19(docsAppDir, `${slug2}-theme.css`);
4787
+ const globalsPath = join19(docsAppDir, "globals.css");
4788
+ const themeConfigPath = join19(repoRoot, "packages", "docs", "lib", "theme-config.ts");
4789
+ if (!existsSync16(docsAppDir)) {
4362
4790
  const msg = `Docs app directory not found: ${docsAppDir}`;
4363
4791
  if (options.json) {
4364
4792
  console.log(JSON.stringify({ success: false, error: msg }));
@@ -4371,8 +4799,8 @@ function themeRegisterCommand(file, cwd, options) {
4371
4799
  let globalsContent = "";
4372
4800
  let themeConfigContent = "";
4373
4801
  try {
4374
- globalsContent = readFileSync18(globalsPath, "utf-8");
4375
- themeConfigContent = readFileSync18(themeConfigPath, "utf-8");
4802
+ globalsContent = readFileSync19(globalsPath, "utf-8");
4803
+ themeConfigContent = readFileSync19(themeConfigPath, "utf-8");
4376
4804
  } catch (err) {
4377
4805
  const msg = err instanceof Error ? err.message : "Could not read docs files";
4378
4806
  if (options.json) {
@@ -4383,8 +4811,8 @@ function themeRegisterCommand(file, cwd, options) {
4383
4811
  process.exit(1);
4384
4812
  return;
4385
4813
  }
4386
- const cssExists = existsSync15(cssFilePath);
4387
- const cssChanged = !cssExists || readFileSync18(cssFilePath, "utf-8") !== css;
4814
+ const cssExists = existsSync16(cssFilePath);
4815
+ const cssChanged = !cssExists || readFileSync19(cssFilePath, "utf-8") !== css;
4388
4816
  const { updated: newGlobals, changed: globalsChanged } = insertGlobalsImport(globalsContent, slug2);
4389
4817
  const { updated: newThemeConfig, changed: themeConfigChanged, error: configError } = insertThemeConfig(
4390
4818
  themeConfigContent,
@@ -4468,8 +4896,8 @@ function themeRegisterCommand(file, cwd, options) {
4468
4896
  }
4469
4897
 
4470
4898
  // src/commands/theme-unregister.ts
4471
- import { readFileSync as readFileSync19, writeFileSync as writeFileSync11, existsSync as existsSync16, unlinkSync } from "fs";
4472
- import { join as join19 } from "path";
4899
+ import { readFileSync as readFileSync20, writeFileSync as writeFileSync11, existsSync as existsSync17, unlinkSync } from "fs";
4900
+ import { join as join20 } from "path";
4473
4901
  function removeGlobalsImport(content, slug2) {
4474
4902
  const importLine = `@import './${slug2}-theme.css';`;
4475
4903
  if (!content.includes(importLine)) {
@@ -4501,11 +4929,11 @@ function themeUnregisterCommand(slug2, cwd, options) {
4501
4929
  process.exit(1);
4502
4930
  return;
4503
4931
  }
4504
- const docsAppDir = join19(repoRoot, "packages", "docs", "app");
4505
- const cssFilePath = join19(docsAppDir, `${slug2}-theme.css`);
4506
- const globalsPath = join19(docsAppDir, "globals.css");
4507
- const themeConfigPath = join19(repoRoot, "packages", "docs", "lib", "theme-config.ts");
4508
- if (!existsSync16(docsAppDir)) {
4932
+ const docsAppDir = join20(repoRoot, "packages", "docs", "app");
4933
+ const cssFilePath = join20(docsAppDir, `${slug2}-theme.css`);
4934
+ const globalsPath = join20(docsAppDir, "globals.css");
4935
+ const themeConfigPath = join20(repoRoot, "packages", "docs", "lib", "theme-config.ts");
4936
+ if (!existsSync17(docsAppDir)) {
4509
4937
  const msg = `Docs app directory not found: ${docsAppDir}`;
4510
4938
  if (options.json) {
4511
4939
  console.log(JSON.stringify({ success: false, error: msg }));
@@ -4518,8 +4946,8 @@ function themeUnregisterCommand(slug2, cwd, options) {
4518
4946
  let globalsContent = "";
4519
4947
  let themeConfigContent = "";
4520
4948
  try {
4521
- globalsContent = readFileSync19(globalsPath, "utf-8");
4522
- themeConfigContent = readFileSync19(themeConfigPath, "utf-8");
4949
+ globalsContent = readFileSync20(globalsPath, "utf-8");
4950
+ themeConfigContent = readFileSync20(themeConfigPath, "utf-8");
4523
4951
  } catch (err) {
4524
4952
  const msg = err instanceof Error ? err.message : "Could not read docs files";
4525
4953
  if (options.json) {
@@ -4530,7 +4958,7 @@ function themeUnregisterCommand(slug2, cwd, options) {
4530
4958
  process.exit(1);
4531
4959
  return;
4532
4960
  }
4533
- const cssExists = existsSync16(cssFilePath);
4961
+ const cssExists = existsSync17(cssFilePath);
4534
4962
  const { updated: newGlobals, changed: globalsChanged } = removeGlobalsImport(globalsContent, slug2);
4535
4963
  const { updated: newThemeConfig, changed: themeConfigChanged } = removeThemeConfigEntry(themeConfigContent, slug2);
4536
4964
  if (!cssExists && !globalsChanged && !themeConfigChanged) {
@@ -4571,15 +4999,15 @@ function themeUnregisterCommand(slug2, cwd, options) {
4571
4999
 
4572
5000
  // src/commands/theme-sync.ts
4573
5001
  import {
4574
- readFileSync as readFileSync20,
5002
+ readFileSync as readFileSync21,
4575
5003
  writeFileSync as writeFileSync12,
4576
5004
  mkdirSync as mkdirSync8,
4577
- existsSync as existsSync17,
5005
+ existsSync as existsSync18,
4578
5006
  readdirSync as readdirSync6,
4579
5007
  unlinkSync as unlinkSync2,
4580
5008
  copyFileSync
4581
5009
  } from "fs";
4582
- import { join as join20, basename as basename4, resolve as resolve14, sep, relative as relative2 } from "path";
5010
+ import { join as join21, basename as basename4, resolve as resolve15, sep, relative as relative2 } from "path";
4583
5011
  import { parse as parseYaml3 } from "yaml";
4584
5012
  import { generateThemeData as generateThemeData5, validateFontCoverage, formatFontCoverageError } from "@loworbitstudio/visor-theme-engine";
4585
5013
  import { docsAdapter as docsAdapter3 } from "@loworbitstudio/visor-theme-engine/adapters";
@@ -4593,9 +5021,9 @@ var CUSTOM_OVERLAY_CSS_PATH = "packages/docs/app/custom-themes.generated.css";
4593
5021
  var CUSTOM_OVERLAY_TS_PATH = "packages/docs/lib/theme-config.custom.generated.ts";
4594
5022
  var CUSTOM_OVERLAY_IMPORT_LINE = "@import './custom-themes.generated.css';";
4595
5023
  function scanThemeDir(dir) {
4596
- if (!existsSync17(dir)) return [];
5024
+ if (!existsSync18(dir)) return [];
4597
5025
  assertNoBrokenSymlinks(dir);
4598
- return readdirSync6(dir).filter((f) => f.endsWith(".visor.yaml")).map((f) => join20(dir, f));
5026
+ return readdirSync6(dir).filter((f) => f.endsWith(".visor.yaml")).map((f) => join21(dir, f));
4599
5027
  }
4600
5028
  function resolveCustomSources(repoRoot, mainRepoRoot, warn) {
4601
5029
  const merged = /* @__PURE__ */ new Map();
@@ -4619,20 +5047,20 @@ function resolveCustomSources(repoRoot, mainRepoRoot, warn) {
4619
5047
  };
4620
5048
  const envPath = process.env[PRIVATE_THEMES_ENV_VAR];
4621
5049
  if (envPath && envPath.trim() !== "") {
4622
- const resolved = resolve14(envPath);
4623
- if (!existsSync17(resolved)) {
5050
+ const resolved = resolve15(envPath);
5051
+ if (!existsSync18(resolved)) {
4624
5052
  throw new Error(
4625
5053
  `${PRIVATE_THEMES_ENV_VAR} is set to "${envPath}" but the path does not exist. Expected a directory containing {slug}/theme.visor.yaml entries.`
4626
5054
  );
4627
5055
  }
4628
5056
  addNested(resolved, "env");
4629
5057
  }
4630
- const siblingPath = join20(mainRepoRoot, "..", "visor-themes-private", "themes");
4631
- const siblingExists = existsSync17(siblingPath);
5058
+ const siblingPath = join21(mainRepoRoot, "..", "visor-themes-private", "themes");
5059
+ const siblingExists = existsSync18(siblingPath);
4632
5060
  if (siblingExists) {
4633
5061
  addNested(siblingPath, "sibling");
4634
5062
  }
4635
- const parentDir = join20(mainRepoRoot, "..");
5063
+ const parentDir = join21(mainRepoRoot, "..");
4636
5064
  const parentGlobMatches = scanParentForPrivateThemes(parentDir).filter(
4637
5065
  (path2) => !path2.startsWith(mainRepoRoot + sep)
4638
5066
  );
@@ -4653,7 +5081,7 @@ function resolveCustomSources(repoRoot, mainRepoRoot, warn) {
4653
5081
  }
4654
5082
  }
4655
5083
  }
4656
- const legacyDir = join20(repoRoot, "custom-themes");
5084
+ const legacyDir = join21(repoRoot, "custom-themes");
4657
5085
  const legacyFiles = scanThemeDir(legacyDir);
4658
5086
  for (const legacyFile of legacyFiles) {
4659
5087
  const slug2 = basename4(legacyFile).replace(/\.visor\.yaml$/, "");
@@ -4710,14 +5138,14 @@ function reportBrokenSymlink(err, options) {
4710
5138
  }
4711
5139
  }
4712
5140
  function buildEmptySourcesMessage(mainRepoRoot) {
4713
- const expectedSibling = join20(mainRepoRoot, "..", "visor-themes-private");
5141
+ const expectedSibling = join21(mainRepoRoot, "..", "visor-themes-private");
4714
5142
  return [
4715
5143
  "No theme sources discovered. Cannot proceed \u2014 refusing to wipe generated CSS.",
4716
5144
  "",
4717
5145
  "Resolution order checked:",
4718
5146
  ` 1. Env var ${PRIVATE_THEMES_ENV_VAR} (unset or empty)`,
4719
5147
  ` 2. Sibling checkout at ${expectedSibling}/themes/ (not found)`,
4720
- ` 3. One-level-deeper at ${join20(mainRepoRoot, "..")}/<parent>/visor-themes-private/themes/ (not found)`,
5148
+ ` 3. One-level-deeper at ${join21(mainRepoRoot, "..")}/<parent>/visor-themes-private/themes/ (not found)`,
4721
5149
  " 4. Legacy custom-themes/ (no .visor.yaml files)",
4722
5150
  "",
4723
5151
  "To fix, clone the private themes repo as a sibling:",
@@ -4922,14 +5350,14 @@ function themeSyncCommand(cwd, options) {
4922
5350
  return;
4923
5351
  }
4924
5352
  const mainRepoRoot = findMainRepoRoot(cwd) ?? repoRoot;
4925
- const themesDir = join20(repoRoot, "themes");
4926
- const docsAppDir = join20(repoRoot, "packages", "docs", "app");
4927
- const docsLibDir = join20(repoRoot, "packages", "docs", "lib");
4928
- const docsPublicThemesDir = join20(repoRoot, "packages", "docs", "public", "themes");
4929
- const themeConfigPath = join20(repoRoot, "packages", "docs", "lib", "theme-config.ts");
4930
- const globalsPath = join20(docsAppDir, "globals.css");
4931
- const customOverlayCssPath = join20(repoRoot, CUSTOM_OVERLAY_CSS_PATH);
4932
- const customOverlayTsPath = join20(repoRoot, CUSTOM_OVERLAY_TS_PATH);
5353
+ const themesDir = join21(repoRoot, "themes");
5354
+ const docsAppDir = join21(repoRoot, "packages", "docs", "app");
5355
+ const docsLibDir = join21(repoRoot, "packages", "docs", "lib");
5356
+ const docsPublicThemesDir = join21(repoRoot, "packages", "docs", "public", "themes");
5357
+ const themeConfigPath = join21(repoRoot, "packages", "docs", "lib", "theme-config.ts");
5358
+ const globalsPath = join21(docsAppDir, "globals.css");
5359
+ const customOverlayCssPath = join21(repoRoot, CUSTOM_OVERLAY_CSS_PATH);
5360
+ const customOverlayTsPath = join21(repoRoot, CUSTOM_OVERLAY_TS_PATH);
4933
5361
  let stockFiles;
4934
5362
  try {
4935
5363
  stockFiles = scanThemeDir(themesDir);
@@ -4986,7 +5414,7 @@ function themeSyncCommand(cwd, options) {
4986
5414
  const processFile = (filePath, isCustom, slugOverride) => {
4987
5415
  let yamlContent;
4988
5416
  try {
4989
- yamlContent = readFileSync20(filePath, "utf-8");
5417
+ yamlContent = readFileSync21(filePath, "utf-8");
4990
5418
  } catch {
4991
5419
  failures.push({ filePath, error: `Could not read: ${filePath}` });
4992
5420
  return;
@@ -5036,8 +5464,8 @@ function themeSyncCommand(cwd, options) {
5036
5464
  let globalsContent;
5037
5465
  let themeConfigContent;
5038
5466
  try {
5039
- globalsContent = readFileSync20(globalsPath, "utf-8");
5040
- themeConfigContent = readFileSync20(themeConfigPath, "utf-8");
5467
+ globalsContent = readFileSync21(globalsPath, "utf-8");
5468
+ themeConfigContent = readFileSync21(themeConfigPath, "utf-8");
5041
5469
  } catch (err) {
5042
5470
  const msg = err instanceof Error ? err.message : "Could not read docs files";
5043
5471
  if (options.json) {
@@ -5052,12 +5480,12 @@ function themeSyncCommand(cwd, options) {
5052
5480
  const newThemeConfig = updateStockThemeConfigBlock(themeConfigContent, stockManifest);
5053
5481
  const newCustomOverlayCss = generateCustomOverlayCss(customManifest);
5054
5482
  const newCustomOverlayTs = generateCustomOverlayTs(customManifest);
5055
- const existingCssFiles = existsSync17(docsAppDir) ? readdirSync6(docsAppDir).filter(
5483
+ const existingCssFiles = existsSync18(docsAppDir) ? readdirSync6(docsAppDir).filter(
5056
5484
  (f) => f.endsWith("-theme.css") && f !== "custom-themes.generated.css"
5057
5485
  ) : [];
5058
5486
  const newCssSet = new Set(allSlugs.map((s) => `${s}-theme.css`));
5059
5487
  const staleCssFiles = existingCssFiles.filter((f) => !newCssSet.has(f));
5060
- const existingPublicYamls = existsSync17(docsPublicThemesDir) ? readdirSync6(docsPublicThemesDir).filter((f) => f.endsWith(".visor.yaml")) : [];
5488
+ const existingPublicYamls = existsSync18(docsPublicThemesDir) ? readdirSync6(docsPublicThemesDir).filter((f) => f.endsWith(".visor.yaml")) : [];
5061
5489
  const newPublicYamlSet = new Set(manifest.map((e) => `${e.yamlFilename}.visor.yaml`));
5062
5490
  const stalePublicYamls = existingPublicYamls.filter((f) => !newPublicYamlSet.has(f));
5063
5491
  if (options.dryRun) {
@@ -5101,24 +5529,24 @@ function themeSyncCommand(cwd, options) {
5101
5529
  mkdirSync8(docsLibDir, { recursive: true });
5102
5530
  mkdirSync8(docsPublicThemesDir, { recursive: true });
5103
5531
  for (const entry of manifest) {
5104
- writeFileSync12(join20(docsAppDir, `${entry.slug}-theme.css`), entry.css, "utf-8");
5532
+ writeFileSync12(join21(docsAppDir, `${entry.slug}-theme.css`), entry.css, "utf-8");
5105
5533
  }
5106
5534
  for (const stale of staleCssFiles) {
5107
- unlinkSync2(join20(docsAppDir, stale));
5535
+ unlinkSync2(join21(docsAppDir, stale));
5108
5536
  }
5109
5537
  writeFileSync12(customOverlayCssPath, newCustomOverlayCss, "utf-8");
5110
5538
  writeFileSync12(customOverlayTsPath, newCustomOverlayTs, "utf-8");
5111
5539
  writeFileSync12(themeConfigPath, newThemeConfig, "utf-8");
5112
5540
  writeFileSync12(globalsPath, newGlobals, "utf-8");
5113
5541
  for (const srcFile of stockFiles) {
5114
- copyFileSync(srcFile, join20(docsPublicThemesDir, basename4(srcFile)));
5542
+ copyFileSync(srcFile, join21(docsPublicThemesDir, basename4(srcFile)));
5115
5543
  }
5116
5544
  for (const c of customSources) {
5117
5545
  const targetName = c.origin === "legacy" ? basename4(c.filePath) : `${c.slug}.visor.yaml`;
5118
- copyFileSync(c.filePath, join20(docsPublicThemesDir, targetName));
5546
+ copyFileSync(c.filePath, join21(docsPublicThemesDir, targetName));
5119
5547
  }
5120
5548
  for (const stale of stalePublicYamls) {
5121
- unlinkSync2(join20(docsPublicThemesDir, stale));
5549
+ unlinkSync2(join21(docsPublicThemesDir, stale));
5122
5550
  }
5123
5551
  } catch (err) {
5124
5552
  const msg = err instanceof Error ? err.message : "Write failed";
@@ -5171,19 +5599,19 @@ function themeSyncCommand(cwd, options) {
5171
5599
 
5172
5600
  // src/commands/theme-batch-apply-flutter.ts
5173
5601
  import {
5174
- readFileSync as readFileSync21,
5602
+ readFileSync as readFileSync22,
5175
5603
  writeFileSync as writeFileSync13,
5176
5604
  mkdirSync as mkdirSync9,
5177
- existsSync as existsSync18,
5605
+ existsSync as existsSync19,
5178
5606
  readdirSync as readdirSync7,
5179
5607
  rmSync
5180
5608
  } from "fs";
5181
- import { join as join21, basename as basename5, dirname as dirname10 } from "path";
5609
+ import { join as join22, basename as basename5, dirname as dirname11 } from "path";
5182
5610
  import { generateThemeData as generateThemeData6 } from "@loworbitstudio/visor-theme-engine";
5183
5611
  import { flutterAdapter as flutterAdapter2 } from "@loworbitstudio/visor-theme-engine/adapters";
5184
5612
  function scanThemeDir2(dir) {
5185
- if (!existsSync18(dir)) return [];
5186
- return readdirSync7(dir).filter((f) => f.endsWith(".visor.yaml")).map((f) => join21(dir, f)).sort();
5613
+ if (!existsSync19(dir)) return [];
5614
+ return readdirSync7(dir).filter((f) => f.endsWith(".visor.yaml")).map((f) => join22(dir, f)).sort();
5187
5615
  }
5188
5616
  function slugToCamel(slug2) {
5189
5617
  return slug2.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
@@ -5316,9 +5744,9 @@ function themeBatchApplyFlutterCommand(cwd, options) {
5316
5744
  process.exit(1);
5317
5745
  return;
5318
5746
  }
5319
- const themesDir = join21(repoRoot, "themes");
5320
- const customThemesDir = join21(repoRoot, "custom-themes");
5321
- const outputDir = join21(repoRoot, "packages", "visor_themes");
5747
+ const themesDir = join22(repoRoot, "themes");
5748
+ const customThemesDir = join22(repoRoot, "custom-themes");
5749
+ const outputDir = join22(repoRoot, "packages", "visor_themes");
5322
5750
  const stockFiles = scanThemeDir2(themesDir);
5323
5751
  const customFiles = scanThemeDir2(customThemesDir);
5324
5752
  const allFiles = [...stockFiles, ...customFiles];
@@ -5339,7 +5767,7 @@ function themeBatchApplyFlutterCommand(cwd, options) {
5339
5767
  for (const filePath of allFiles) {
5340
5768
  let yamlContent;
5341
5769
  try {
5342
- yamlContent = readFileSync21(filePath, "utf-8");
5770
+ yamlContent = readFileSync22(filePath, "utf-8");
5343
5771
  } catch {
5344
5772
  errors.push(`Could not read: ${filePath}`);
5345
5773
  continue;
@@ -5418,8 +5846,8 @@ function themeBatchApplyFlutterCommand(cwd, options) {
5418
5846
  return;
5419
5847
  }
5420
5848
  const slugs = processed.map((p) => p.slug);
5421
- const libSrcDir = join21(outputDir, "lib", "src");
5422
- if (existsSync18(libSrcDir)) {
5849
+ const libSrcDir = join22(outputDir, "lib", "src");
5850
+ if (existsSync19(libSrcDir)) {
5423
5851
  rmSync(libSrcDir, { recursive: true, force: true });
5424
5852
  }
5425
5853
  const packageFiles = {
@@ -5431,16 +5859,16 @@ function themeBatchApplyFlutterCommand(cwd, options) {
5431
5859
  };
5432
5860
  let totalFiles = 0;
5433
5861
  for (const [relPath, content] of Object.entries(packageFiles)) {
5434
- const absPath = join21(outputDir, relPath);
5435
- mkdirSync9(dirname10(absPath), { recursive: true });
5862
+ const absPath = join22(outputDir, relPath);
5863
+ mkdirSync9(dirname11(absPath), { recursive: true });
5436
5864
  writeFileSync13(absPath, content, "utf-8");
5437
5865
  totalFiles++;
5438
5866
  }
5439
5867
  for (const { slug: slug2, tokenFiles } of processed) {
5440
- const themeBaseDir = join21(outputDir, "lib", "src", slug2);
5868
+ const themeBaseDir = join22(outputDir, "lib", "src", slug2);
5441
5869
  for (const [relPath, content] of Object.entries(tokenFiles)) {
5442
- const absPath = join21(themeBaseDir, relPath);
5443
- mkdirSync9(dirname10(absPath), { recursive: true });
5870
+ const absPath = join22(themeBaseDir, relPath);
5871
+ mkdirSync9(dirname11(absPath), { recursive: true });
5444
5872
  writeFileSync13(absPath, content, "utf-8");
5445
5873
  totalFiles++;
5446
5874
  }
@@ -5462,8 +5890,8 @@ function themeBatchApplyFlutterCommand(cwd, options) {
5462
5890
  }
5463
5891
 
5464
5892
  // src/commands/fonts-add.ts
5465
- import { existsSync as existsSync19, statSync as statSync7, readdirSync as readdirSync8, readFileSync as readFileSync22 } from "fs";
5466
- import { resolve as resolve15, basename as basename6, extname as extname4 } from "path";
5893
+ import { existsSync as existsSync20, statSync as statSync8, readdirSync as readdirSync8, readFileSync as readFileSync23 } from "fs";
5894
+ import { resolve as resolve16, basename as basename6, extname as extname4 } from "path";
5467
5895
  import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
5468
5896
  function deriveFamilySlug(filename) {
5469
5897
  const name = basename6(filename, extname4(filename));
@@ -5507,11 +5935,11 @@ function deriveFamilySlug(filename) {
5507
5935
  return parts.join("-").toLowerCase();
5508
5936
  }
5509
5937
  function collectWoff2Files(inputPath) {
5510
- const resolved = resolve15(inputPath);
5511
- if (!existsSync19(resolved)) {
5938
+ const resolved = resolve16(inputPath);
5939
+ if (!existsSync20(resolved)) {
5512
5940
  throw new Error(`Path not found: ${resolved}`);
5513
5941
  }
5514
- const stat = statSync7(resolved);
5942
+ const stat = statSync8(resolved);
5515
5943
  if (stat.isFile()) {
5516
5944
  if (extname4(resolved).toLowerCase() !== ".woff2") {
5517
5945
  throw new Error(
@@ -5521,7 +5949,7 @@ function collectWoff2Files(inputPath) {
5521
5949
  return [resolved];
5522
5950
  }
5523
5951
  if (stat.isDirectory()) {
5524
- const files = readdirSync8(resolved).filter((f) => extname4(f).toLowerCase() === ".woff2").map((f) => resolve15(resolved, f));
5952
+ const files = readdirSync8(resolved).filter((f) => extname4(f).toLowerCase() === ".woff2").map((f) => resolve16(resolved, f));
5525
5953
  if (files.length === 0) {
5526
5954
  throw new Error(
5527
5955
  `No .woff2 files found in directory: ${resolved}`
@@ -5536,8 +5964,8 @@ function collectWoff2Files(inputPath) {
5536
5964
  throw new Error(`Path is neither a file nor a directory: ${resolved}`);
5537
5965
  }
5538
5966
  function getNonWoff2Fonts(inputPath) {
5539
- const resolved = resolve15(inputPath);
5540
- if (!existsSync19(resolved) || !statSync7(resolved).isDirectory()) {
5967
+ const resolved = resolve16(inputPath);
5968
+ if (!existsSync20(resolved) || !statSync8(resolved).isDirectory()) {
5541
5969
  return [];
5542
5970
  }
5543
5971
  return readdirSync8(resolved).filter((f) => {
@@ -5574,7 +6002,7 @@ function createR2Client(config) {
5574
6002
  });
5575
6003
  }
5576
6004
  async function uploadFile(client, bucket, key, filePath) {
5577
- const body = readFileSync22(filePath);
6005
+ const body = readFileSync23(filePath);
5578
6006
  await client.send(
5579
6007
  new PutObjectCommand({
5580
6008
  Bucket: bucket,
@@ -5590,8 +6018,8 @@ async function fontsAddCommand(inputPath, options) {
5590
6018
  const r2Config = getR2Config();
5591
6019
  const files = collectWoff2Files(inputPath);
5592
6020
  const familySlug = options.family ?? deriveFamilySlug(basename6(files[0]));
5593
- const resolved = resolve15(inputPath);
5594
- const nonWoff2 = statSync7(resolved).isDirectory() ? getNonWoff2Fonts(resolved) : [];
6021
+ const resolved = resolve16(inputPath);
6022
+ const nonWoff2 = statSync8(resolved).isDirectory() ? getNonWoff2Fonts(resolved) : [];
5595
6023
  if (!json) {
5596
6024
  logger.heading("Visor Font Upload");
5597
6025
  logger.info(`Organization: ${org}`);
@@ -5615,7 +6043,7 @@ async function fontsAddCommand(inputPath, options) {
5615
6043
  logger.info(`Uploading ${filename}...`);
5616
6044
  }
5617
6045
  await uploadFile(client, bucket, key, filePath);
5618
- const size = statSync7(filePath).size;
6046
+ const size = statSync8(filePath).size;
5619
6047
  results.push({ file: filename, key, size });
5620
6048
  if (!json) {
5621
6049
  logger.success(`Uploaded: ${key} (${formatBytes(size)})`);
@@ -5878,27 +6306,27 @@ function findCssFiles(dir, maxDepth = 3) {
5878
6306
  }
5879
6307
 
5880
6308
  // src/utils/patterns.ts
5881
- import { existsSync as existsSync21, readdirSync as readdirSync10, readFileSync as readFileSync24 } from "fs";
5882
- import { join as join23 } from "path";
6309
+ import { existsSync as existsSync22, readdirSync as readdirSync10, readFileSync as readFileSync25 } from "fs";
6310
+ import { join as join24 } from "path";
5883
6311
  import { parse as parseYAML } from "yaml";
5884
6312
  function loadPatternsFromYaml(repoRoot) {
5885
- const patternsDir = join23(repoRoot, "patterns");
5886
- if (!existsSync21(patternsDir)) return [];
6313
+ const patternsDir = join24(repoRoot, "patterns");
6314
+ if (!existsSync22(patternsDir)) return [];
5887
6315
  const files = readdirSync10(patternsDir).filter(
5888
6316
  (f) => f.endsWith(".visor-pattern.yaml")
5889
6317
  );
5890
6318
  return files.map((file) => {
5891
- const content = readFileSync24(join23(patternsDir, file), "utf-8");
6319
+ const content = readFileSync25(join24(patternsDir, file), "utf-8");
5892
6320
  return parseYAML(content);
5893
6321
  }).filter(Boolean);
5894
6322
  }
5895
6323
  function findRepoRoot2(startDir) {
5896
6324
  let current = startDir;
5897
6325
  while (true) {
5898
- if (existsSync21(join23(current, "patterns"))) {
6326
+ if (existsSync22(join24(current, "patterns"))) {
5899
6327
  return current;
5900
6328
  }
5901
- const parent = join23(current, "..");
6329
+ const parent = join24(current, "..");
5902
6330
  if (parent === current) return null;
5903
6331
  current = parent;
5904
6332
  }
@@ -6277,8 +6705,8 @@ Visor Tokens (${categoryLabel}) \u2014 ${tokens2.length} tokens
6277
6705
  }
6278
6706
 
6279
6707
  // src/commands/migrate-token-substitution.ts
6280
- import { readFileSync as readFileSync25, writeFileSync as writeFileSync14, readdirSync as readdirSync11, statSync as statSync8, existsSync as existsSync22 } from "fs";
6281
- import { resolve as resolve16, join as join24, relative as relative4 } from "path";
6708
+ import { readFileSync as readFileSync26, writeFileSync as writeFileSync14, readdirSync as readdirSync11, statSync as statSync9, existsSync as existsSync23 } from "fs";
6709
+ import { resolve as resolve17, join as join25, relative as relative4 } from "path";
6282
6710
  import { parse as parseYaml4 } from "yaml";
6283
6711
  import pc4 from "picocolors";
6284
6712
  var V7_ENTR_SUBSTITUTION_MAP = {
@@ -6301,14 +6729,14 @@ var BUILT_IN_SUBSTITUTION_MAPS = {
6301
6729
  var DEFAULT_THEME_ID = "entr";
6302
6730
  function readMapFromThemeFile(themeId, cwd) {
6303
6731
  const candidates = [
6304
- join24(cwd, "themes", `${themeId}.visor.yaml`),
6305
- join24(cwd, "custom-themes", `${themeId}.visor.yaml`),
6306
- join24(cwd, "packages", "docs", "public", "themes", `${themeId}.visor.yaml`)
6732
+ join25(cwd, "themes", `${themeId}.visor.yaml`),
6733
+ join25(cwd, "custom-themes", `${themeId}.visor.yaml`),
6734
+ join25(cwd, "packages", "docs", "public", "themes", `${themeId}.visor.yaml`)
6307
6735
  ];
6308
6736
  for (const candidate of candidates) {
6309
- if (existsSync22(candidate)) {
6737
+ if (existsSync23(candidate)) {
6310
6738
  try {
6311
- const raw = readFileSync25(candidate, "utf-8");
6739
+ const raw = readFileSync26(candidate, "utf-8");
6312
6740
  const parsed = parseYaml4(raw);
6313
6741
  if (parsed?.migrate?.["token-substitution"]) {
6314
6742
  return parsed.migrate["token-substitution"];
@@ -6364,7 +6792,7 @@ function collectCssFiles(dirPath) {
6364
6792
  function walk2(current) {
6365
6793
  const entries = readdirSync11(current, { withFileTypes: true });
6366
6794
  for (const entry of entries) {
6367
- const fullPath = join24(current, entry.name);
6795
+ const fullPath = join25(current, entry.name);
6368
6796
  if (entry.isDirectory()) {
6369
6797
  if (["node_modules", ".git", ".next", "dist", "build", ".cache"].includes(entry.name)) {
6370
6798
  continue;
@@ -6383,7 +6811,7 @@ function collectCssFiles(dirPath) {
6383
6811
  }
6384
6812
  function runSubstitutionPass(targetPath, map, themeId) {
6385
6813
  let files;
6386
- const stat = statSync8(targetPath);
6814
+ const stat = statSync9(targetPath);
6387
6815
  if (stat.isFile()) {
6388
6816
  files = [targetPath];
6389
6817
  } else {
@@ -6392,7 +6820,7 @@ function runSubstitutionPass(targetPath, map, themeId) {
6392
6820
  const fileResults = [];
6393
6821
  let totalSubstitutions = 0;
6394
6822
  for (const file of files) {
6395
- const content = readFileSync25(file, "utf-8");
6823
+ const content = readFileSync26(file, "utf-8");
6396
6824
  const { newContent, substitutions } = applySubstitutionsToContent(content, map);
6397
6825
  if (substitutions.length > 0) {
6398
6826
  fileResults.push({ file, substitutions, originalContent: content, newContent });
@@ -6426,9 +6854,9 @@ function migrateTokenSubstitutionCommand(targetArg, cwd, options) {
6426
6854
  process.exit(1);
6427
6855
  return;
6428
6856
  }
6429
- const targetPath = resolve16(cwd, targetArg ?? ".");
6857
+ const targetPath = resolve17(cwd, targetArg ?? ".");
6430
6858
  try {
6431
- statSync8(targetPath);
6859
+ statSync9(targetPath);
6432
6860
  } catch {
6433
6861
  if (options.json) {
6434
6862
  console.log(JSON.stringify({
@@ -6515,23 +6943,23 @@ function migrateTokenSubstitutionCommand(targetArg, cwd, options) {
6515
6943
  }
6516
6944
 
6517
6945
  // src/commands/sandbox/init.ts
6518
- import { existsSync as existsSync25, mkdirSync as mkdirSync12, readFileSync as readFileSync28, rmSync as rmSync2 } from "fs";
6519
- import { isAbsolute as isAbsolute4, join as join27, resolve as resolve18, dirname as dirname12 } from "path";
6946
+ import { existsSync as existsSync26, mkdirSync as mkdirSync12, readFileSync as readFileSync29, rmSync as rmSync2 } from "fs";
6947
+ import { isAbsolute as isAbsolute5, join as join28, resolve as resolve19, dirname as dirname13 } from "path";
6520
6948
  import { fileURLToPath as fileURLToPath3 } from "url";
6521
6949
  import * as childProcess3 from "child_process";
6522
6950
 
6523
6951
  // src/commands/sandbox/parse-handoff.ts
6524
- import { readFileSync as readFileSync26, existsSync as existsSync23 } from "fs";
6525
- import { dirname as dirname11, resolve as resolve17, isAbsolute as isAbsolute3 } from "path";
6952
+ import { readFileSync as readFileSync27, existsSync as existsSync24 } from "fs";
6953
+ import { dirname as dirname12, resolve as resolve18, isAbsolute as isAbsolute4 } from "path";
6526
6954
  function parseHandoff(handoffPath) {
6527
- const text = readFileSync26(handoffPath, "utf-8");
6955
+ const text = readFileSync27(handoffPath, "utf-8");
6528
6956
  const lines2 = text.split("\n");
6529
6957
  const warnings = [];
6530
6958
  const pattern2 = extractPatternSlug(lines2, warnings);
6531
6959
  const theme2 = extractMetaField(lines2, "Theme");
6532
6960
  const recipePath = extractRecipePath(text, handoffPath, warnings);
6533
6961
  const primitives = extractPrimitives(text, warnings);
6534
- const { screens, mockShapes } = recipePath && existsSync23(recipePath) ? extractRecipeArtifacts(recipePath, warnings) : { screens: [], mockShapes: [] };
6962
+ const { screens, mockShapes } = recipePath && existsSync24(recipePath) ? extractRecipeArtifacts(recipePath, warnings) : { screens: [], mockShapes: [] };
6535
6963
  return { pattern: pattern2, theme: theme2, recipePath, primitives, screens, mockShapes, warnings };
6536
6964
  }
6537
6965
  function extractPatternSlug(lines2, warnings) {
@@ -6562,8 +6990,8 @@ function extractRecipePath(text, handoffPath, warnings) {
6562
6990
  return void 0;
6563
6991
  }
6564
6992
  const href = m[2].trim();
6565
- if (isAbsolute3(href)) return href;
6566
- return resolve17(dirname11(handoffPath), href);
6993
+ if (isAbsolute4(href)) return href;
6994
+ return resolve18(dirname12(handoffPath), href);
6567
6995
  }
6568
6996
  var STATUS_PATTERNS = [
6569
6997
  { re: /NEW\s+gap/i, status: "gap-new" },
@@ -6610,7 +7038,7 @@ function extractPrimitives(text, warnings) {
6610
7038
  return primitives;
6611
7039
  }
6612
7040
  function extractRecipeArtifacts(recipePath, warnings) {
6613
- const text = readFileSync26(recipePath, "utf-8");
7041
+ const text = readFileSync27(recipePath, "utf-8");
6614
7042
  const screens = extractScreens(text);
6615
7043
  const mockShapes = extractMockFields(text, warnings);
6616
7044
  return { screens, mockShapes };
@@ -6684,8 +7112,8 @@ function tryPort(port) {
6684
7112
  }
6685
7113
 
6686
7114
  // src/commands/sandbox/scaffold.ts
6687
- import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync15, existsSync as existsSync24, readdirSync as readdirSync12 } from "fs";
6688
- import { join as join25 } from "path";
7115
+ import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync15, existsSync as existsSync25, readdirSync as readdirSync12 } from "fs";
7116
+ import { join as join26 } from "path";
6689
7117
 
6690
7118
  // src/commands/sandbox/templates.ts
6691
7119
  var SANDBOX_PACKAGE_VERSION = "16.2.6";
@@ -7188,7 +7616,7 @@ function writeScaffold(sandboxDir, manifest, port, options = {}) {
7188
7616
  mkdirSync10(sandboxDir, { recursive: true });
7189
7617
  const created = [];
7190
7618
  const writeIfNew = (rel, contents) => {
7191
- const full = join25(sandboxDir, rel);
7619
+ const full = join26(sandboxDir, rel);
7192
7620
  mkdirSync10(dirOf(full), { recursive: true });
7193
7621
  writeFileSync15(full, contents, "utf-8");
7194
7622
  created.push(rel);
@@ -7216,7 +7644,7 @@ function writeScaffold(sandboxDir, manifest, port, options = {}) {
7216
7644
  }
7217
7645
  }
7218
7646
  writeIfNew("playwright.capture.mjs", captureScriptTemplate());
7219
- mkdirSync10(join25(sandboxDir, "captures", "approved"), { recursive: true });
7647
+ mkdirSync10(join26(sandboxDir, "captures", "approved"), { recursive: true });
7220
7648
  return { created };
7221
7649
  }
7222
7650
  function dirOf(p) {
@@ -7250,10 +7678,10 @@ function writeSandboxConfig(sandboxDir, manifest, port, options) {
7250
7678
  stripChromeSelectors: options.prototypeImport.stripChromeSelectors
7251
7679
  } : null
7252
7680
  };
7253
- writeFileSync15(join25(sandboxDir, "sandbox.json"), JSON.stringify(config, null, 2) + "\n", "utf-8");
7681
+ writeFileSync15(join26(sandboxDir, "sandbox.json"), JSON.stringify(config, null, 2) + "\n", "utf-8");
7254
7682
  }
7255
7683
  function sandboxIsEmpty(sandboxDir) {
7256
- if (!existsSync24(sandboxDir)) return true;
7684
+ if (!existsSync25(sandboxDir)) return true;
7257
7685
  try {
7258
7686
  return readdirSync12(sandboxDir).filter((f) => !f.startsWith(".")).length === 0;
7259
7687
  } catch {
@@ -7262,8 +7690,8 @@ function sandboxIsEmpty(sandboxDir) {
7262
7690
  }
7263
7691
 
7264
7692
  // src/commands/sandbox/html-prototype.ts
7265
- import { mkdirSync as mkdirSync11, readdirSync as readdirSync13, readFileSync as readFileSync27, statSync as statSync9, writeFileSync as writeFileSync16 } from "fs";
7266
- import { join as join26 } from "path";
7693
+ import { mkdirSync as mkdirSync11, readdirSync as readdirSync13, readFileSync as readFileSync28, statSync as statSync10, writeFileSync as writeFileSync16 } from "fs";
7694
+ import { join as join27 } from "path";
7267
7695
 
7268
7696
  // src/commands/sandbox/strip-chrome.ts
7269
7697
  var DEFAULT_STRIP_SELECTORS = [
@@ -7447,7 +7875,7 @@ function stripDocumentaryChrome(html, selectors) {
7447
7875
  var SCREEN_FILE_PATTERN = /^screen-(\d+)-([^/.]+)\.html$/i;
7448
7876
  function copyHtmlPrototype(sourceDir, sandboxDir, manifest, options = {}) {
7449
7877
  const warnings = [];
7450
- const destAbs = join26(sandboxDir, "public", "prototype");
7878
+ const destAbs = join27(sandboxDir, "public", "prototype");
7451
7879
  mkdirSync11(destAbs, { recursive: true });
7452
7880
  const stripChromeSelectors = options.stripChromeSelectors ?? [];
7453
7881
  const copiedFiles = copyTreeRelative(sourceDir, destAbs, "", stripChromeSelectors);
@@ -7501,24 +7929,24 @@ function deriveStateCoverageScreen(file, existingNames) {
7501
7929
  }
7502
7930
  function copyTreeRelative(srcDir, destDir, relDir = "", stripChromeSelectors = []) {
7503
7931
  const out = [];
7504
- const entries = readdirSync13(join26(srcDir, relDir));
7932
+ const entries = readdirSync13(join27(srcDir, relDir));
7505
7933
  for (const name of entries) {
7506
7934
  if (name.startsWith(".")) continue;
7507
- const srcPath = join26(srcDir, relDir, name);
7508
- const destPath = join26(destDir, relDir, name);
7509
- const stat = statSync9(srcPath);
7935
+ const srcPath = join27(srcDir, relDir, name);
7936
+ const destPath = join27(destDir, relDir, name);
7937
+ const stat = statSync10(srcPath);
7510
7938
  if (stat.isDirectory()) {
7511
7939
  mkdirSync11(destPath, { recursive: true });
7512
- out.push(...copyTreeRelative(srcDir, destDir, join26(relDir, name), stripChromeSelectors));
7940
+ out.push(...copyTreeRelative(srcDir, destDir, join27(relDir, name), stripChromeSelectors));
7513
7941
  } else if (stat.isFile()) {
7514
7942
  if (stripChromeSelectors.length > 0 && name.toLowerCase().endsWith(".html")) {
7515
- const html = readFileSync27(srcPath, "utf-8");
7943
+ const html = readFileSync28(srcPath, "utf-8");
7516
7944
  const stripped = stripDocumentaryChrome(html, stripChromeSelectors);
7517
7945
  writeFileSync16(destPath, stripped);
7518
7946
  } else {
7519
- writeFileSync16(destPath, readFileSync27(srcPath));
7947
+ writeFileSync16(destPath, readFileSync28(srcPath));
7520
7948
  }
7521
- out.push(join26("public", "prototype", relDir, name).replace(/\\/g, "/"));
7949
+ out.push(join27("public", "prototype", relDir, name).replace(/\\/g, "/"));
7522
7950
  }
7523
7951
  }
7524
7952
  return out;
@@ -7570,8 +7998,8 @@ async function runInit(name, cwd, options) {
7570
7998
  if (!name || !/^[a-z0-9][a-z0-9-_]*$/i.test(name)) {
7571
7999
  throw new Error(`Invalid sandbox name '${name}'. Use letters, digits, '-' or '_'.`);
7572
8000
  }
7573
- const handoffPath = isAbsolute4(options.handoff) ? options.handoff : resolve18(cwd, options.handoff);
7574
- if (!existsSync25(handoffPath)) {
8001
+ const handoffPath = isAbsolute5(options.handoff) ? options.handoff : resolve19(cwd, options.handoff);
8002
+ if (!existsSync26(handoffPath)) {
7575
8003
  throw new Error(`Handoff manifest not found: ${handoffPath}`);
7576
8004
  }
7577
8005
  const manifest = parseHandoff(handoffPath);
@@ -7580,7 +8008,7 @@ async function runInit(name, cwd, options) {
7580
8008
  `Handoff has no primitives \u2014 refusing to scaffold an empty sandbox. Check the manifest at ${handoffPath}`
7581
8009
  );
7582
8010
  }
7583
- const sandboxDir = join27(cwd, ".lo", "sandbox", name);
8011
+ const sandboxDir = join28(cwd, ".lo", "sandbox", name);
7584
8012
  if (!sandboxIsEmpty(sandboxDir)) {
7585
8013
  if (!options.overwrite) {
7586
8014
  throw new Error(
@@ -7593,8 +8021,8 @@ async function runInit(name, cwd, options) {
7593
8021
  const port = await findOpenPort();
7594
8022
  let prototypeImport;
7595
8023
  if (options.fromHtmlPrototype) {
7596
- const prototypeDir = isAbsolute4(options.fromHtmlPrototype) ? options.fromHtmlPrototype : resolve18(cwd, options.fromHtmlPrototype);
7597
- if (!existsSync25(prototypeDir)) {
8024
+ const prototypeDir = isAbsolute5(options.fromHtmlPrototype) ? options.fromHtmlPrototype : resolve19(cwd, options.fromHtmlPrototype);
8025
+ if (!existsSync26(prototypeDir)) {
7598
8026
  throw new Error(`HTML prototype directory not found: ${prototypeDir}`);
7599
8027
  }
7600
8028
  const stripChromeSelectors = resolveStripSelectors(
@@ -7653,22 +8081,22 @@ async function runInit(name, cwd, options) {
7653
8081
  function applyThemeIfPossible(sandboxDir, manifest, theme2, cwd, json, themeFile) {
7654
8082
  const candidates = [];
7655
8083
  if (themeFile) {
7656
- candidates.push(isAbsolute4(themeFile) ? themeFile : resolve18(cwd, themeFile));
8084
+ candidates.push(isAbsolute5(themeFile) ? themeFile : resolve19(cwd, themeFile));
7657
8085
  }
7658
- candidates.push(isAbsolute4(theme2) ? theme2 : resolve18(cwd, theme2));
8086
+ candidates.push(isAbsolute5(theme2) ? theme2 : resolve19(cwd, theme2));
7659
8087
  const privateRoot = process.env.VISOR_THEMES_PRIVATE_PATH;
7660
8088
  if (privateRoot && privateRoot.length > 0) {
7661
- candidates.push(join27(privateRoot, "themes", theme2, "theme.visor.yaml"));
8089
+ candidates.push(join28(privateRoot, "themes", theme2, "theme.visor.yaml"));
7662
8090
  }
7663
8091
  candidates.push(
7664
- join27(cwd, "themes", `${theme2}.visor.yaml`),
7665
- join27(cwd, "custom-themes", `${theme2}.visor.yaml`)
8092
+ join28(cwd, "themes", `${theme2}.visor.yaml`),
8093
+ join28(cwd, "custom-themes", `${theme2}.visor.yaml`)
7666
8094
  );
7667
- const yamlPath = candidates.find((p) => existsSync25(p));
8095
+ const yamlPath = candidates.find((p) => existsSync26(p));
7668
8096
  if (!yamlPath) {
7669
8097
  const searched = candidates.join(", ");
7670
8098
  manifest.warnings.push(
7671
- `Theme '${theme2}' not found (searched: ${searched}) \u2014 sandbox uses placeholder globals.css. Run 'npx visor theme apply <path-to-theme.visor.yaml> --adapter nextjs -o ${join27(sandboxDir, "app", "globals.css")}' manually, or re-run with --theme-file <path>, or set VISOR_THEMES_PRIVATE_PATH to a directory containing themes/${theme2}/theme.visor.yaml.`
8099
+ `Theme '${theme2}' not found (searched: ${searched}) \u2014 sandbox uses placeholder globals.css. Run 'npx visor theme apply <path-to-theme.visor.yaml> --adapter nextjs -o ${join28(sandboxDir, "app", "globals.css")}' manually, or re-run with --theme-file <path>, or set VISOR_THEMES_PRIVATE_PATH to a directory containing themes/${theme2}/theme.visor.yaml.`
7672
8100
  );
7673
8101
  if (!json) {
7674
8102
  logger.warn(`Theme '${theme2}' not found \u2014 leaving placeholder globals.css.`);
@@ -7676,12 +8104,12 @@ function applyThemeIfPossible(sandboxDir, manifest, theme2, cwd, json, themeFile
7676
8104
  `Re-run with --theme-file <path>, or set VISOR_THEMES_PRIVATE_PATH=<dir-containing-themes/${theme2}/theme.visor.yaml>.`
7677
8105
  );
7678
8106
  logger.item(
7679
- `Or apply manually: npx visor theme apply <path> --adapter nextjs -o ${join27(sandboxDir, "app", "globals.css")}`
8107
+ `Or apply manually: npx visor theme apply <path> --adapter nextjs -o ${join28(sandboxDir, "app", "globals.css")}`
7680
8108
  );
7681
8109
  }
7682
8110
  return;
7683
8111
  }
7684
- const globalsOut = join27(sandboxDir, "app", "globals.css");
8112
+ const globalsOut = join28(sandboxDir, "app", "globals.css");
7685
8113
  try {
7686
8114
  themeApplyCommand(yamlPath, sandboxDir, {
7687
8115
  output: globalsOut,
@@ -7734,12 +8162,12 @@ function loadKnownPrimitives() {
7734
8162
  }
7735
8163
  function readCliVersion() {
7736
8164
  try {
7737
- const here = dirname12(fileURLToPath3(import.meta.url));
8165
+ const here = dirname13(fileURLToPath3(import.meta.url));
7738
8166
  for (let i = 0; i < 6; i++) {
7739
8167
  const segments = new Array(i).fill("..");
7740
- const candidate = join27(here, ...segments, "package.json");
8168
+ const candidate = join28(here, ...segments, "package.json");
7741
8169
  try {
7742
- const pkg2 = JSON.parse(readFileSync28(candidate, "utf-8"));
8170
+ const pkg2 = JSON.parse(readFileSync29(candidate, "utf-8"));
7743
8171
  if (pkg2.name === "@loworbitstudio/visor" && pkg2.version) return pkg2.version;
7744
8172
  } catch {
7745
8173
  }
@@ -7750,14 +8178,14 @@ function readCliVersion() {
7750
8178
  }
7751
8179
 
7752
8180
  // src/commands/sandbox/dev.ts
7753
- import { existsSync as existsSync26, readFileSync as readFileSync29 } from "fs";
7754
- import { join as join28 } from "path";
8181
+ import { existsSync as existsSync27, readFileSync as readFileSync30 } from "fs";
8182
+ import { join as join29 } from "path";
7755
8183
  import * as childProcess4 from "child_process";
7756
8184
  function sandboxDevCommand(cwd, options) {
7757
8185
  const json = options.json ?? false;
7758
- const sandboxDir = join28(cwd, ".lo", "sandbox", options.name);
7759
- const configPath = join28(sandboxDir, "sandbox.json");
7760
- if (!existsSync26(configPath)) {
8186
+ const sandboxDir = join29(cwd, ".lo", "sandbox", options.name);
8187
+ const configPath = join29(sandboxDir, "sandbox.json");
8188
+ if (!existsSync27(configPath)) {
7761
8189
  fail(
7762
8190
  json,
7763
8191
  `Sandbox '${options.name}' not found at ${sandboxDir}. Run 'visor sandbox init ${options.name} --handoff ... --theme ...' first.`
@@ -7766,7 +8194,7 @@ function sandboxDevCommand(cwd, options) {
7766
8194
  }
7767
8195
  let config;
7768
8196
  try {
7769
- config = JSON.parse(readFileSync29(configPath, "utf-8"));
8197
+ config = JSON.parse(readFileSync30(configPath, "utf-8"));
7770
8198
  } catch (err) {
7771
8199
  fail(json, `Invalid sandbox.json at ${configPath}: ${err.message}`);
7772
8200
  return;
@@ -7824,20 +8252,20 @@ function fail(json, message) {
7824
8252
  // src/commands/sandbox/approve.ts
7825
8253
  import {
7826
8254
  copyFileSync as copyFileSync2,
7827
- existsSync as existsSync27,
8255
+ existsSync as existsSync28,
7828
8256
  mkdirSync as mkdirSync13,
7829
- readFileSync as readFileSync30,
8257
+ readFileSync as readFileSync31,
7830
8258
  readdirSync as readdirSync14,
7831
8259
  rmSync as rmSync3,
7832
8260
  writeFileSync as writeFileSync17
7833
8261
  } from "fs";
7834
- import { basename as basename7, join as join29 } from "path";
8262
+ import { basename as basename7, join as join30 } from "path";
7835
8263
  import * as childProcess5 from "child_process";
7836
8264
  function sandboxApproveCommand(cwd, options) {
7837
8265
  const json = options.json ?? false;
7838
- const sandboxDir = join29(cwd, ".lo", "sandbox", options.name);
7839
- const configPath = join29(sandboxDir, "sandbox.json");
7840
- if (!existsSync27(configPath)) {
8266
+ const sandboxDir = join30(cwd, ".lo", "sandbox", options.name);
8267
+ const configPath = join30(sandboxDir, "sandbox.json");
8268
+ if (!existsSync28(configPath)) {
7841
8269
  fail2(
7842
8270
  json,
7843
8271
  `Sandbox '${options.name}' not found at ${sandboxDir}. Run 'visor sandbox init' first.`
@@ -7851,8 +8279,8 @@ function sandboxApproveCommand(cwd, options) {
7851
8279
  runCapture(sandboxDir, configPath, options.name, json);
7852
8280
  }
7853
8281
  function runCapture(sandboxDir, configPath, sandboxName, json) {
7854
- const config = JSON.parse(readFileSync30(configPath, "utf-8"));
7855
- const captureScriptPath = join29(sandboxDir, "playwright.capture.mjs");
8282
+ const config = JSON.parse(readFileSync31(configPath, "utf-8"));
8283
+ const captureScriptPath = join30(sandboxDir, "playwright.capture.mjs");
7856
8284
  writeFileSync17(captureScriptPath, captureScriptTemplate(), "utf-8");
7857
8285
  ensurePlaywrightInstalled(sandboxDir, json);
7858
8286
  const result = childProcess5.spawnSync(
@@ -7874,9 +8302,9 @@ function runCapture(sandboxDir, configPath, sandboxName, json) {
7874
8302
  ${result.stderr}`);
7875
8303
  return;
7876
8304
  }
7877
- const pendingDir = join29(sandboxDir, "captures", "pending");
7878
- const diffsDir = join29(sandboxDir, "captures", "diffs");
7879
- const approvedDir = join29(sandboxDir, "captures", "approved");
8305
+ const pendingDir = join30(sandboxDir, "captures", "pending");
8306
+ const diffsDir = join30(sandboxDir, "captures", "diffs");
8307
+ const approvedDir = join30(sandboxDir, "captures", "approved");
7880
8308
  const pendingFiles = safeListPngs(pendingDir);
7881
8309
  const diffFiles = safeListPngs(diffsDir);
7882
8310
  const hasBaseline = safeListPngs(approvedDir).length > 0;
@@ -7913,9 +8341,9 @@ ${result.stderr}`);
7913
8341
  }
7914
8342
  }
7915
8343
  function runPromotion(sandboxDir, sandboxName, json) {
7916
- const pendingDir = join29(sandboxDir, "captures", "pending");
7917
- const approvedDir = join29(sandboxDir, "captures", "approved");
7918
- const diffsDir = join29(sandboxDir, "captures", "diffs");
8344
+ const pendingDir = join30(sandboxDir, "captures", "pending");
8345
+ const approvedDir = join30(sandboxDir, "captures", "approved");
8346
+ const diffsDir = join30(sandboxDir, "captures", "diffs");
7919
8347
  const pendingFiles = safeListPngs(pendingDir);
7920
8348
  if (pendingFiles.length === 0) {
7921
8349
  fail2(
@@ -7927,7 +8355,7 @@ function runPromotion(sandboxDir, sandboxName, json) {
7927
8355
  mkdirSync13(approvedDir, { recursive: true });
7928
8356
  const promoted = [];
7929
8357
  for (const src of pendingFiles) {
7930
- const dest = join29(approvedDir, basename7(src));
8358
+ const dest = join30(approvedDir, basename7(src));
7931
8359
  copyFileSync2(src, dest);
7932
8360
  promoted.push(dest);
7933
8361
  }
@@ -7947,8 +8375,8 @@ function runPromotion(sandboxDir, sandboxName, json) {
7947
8375
  }
7948
8376
  }
7949
8377
  function ensurePlaywrightInstalled(sandboxDir, json) {
7950
- const markerPath = join29(sandboxDir, ".playwright-installed");
7951
- if (existsSync27(markerPath)) return;
8378
+ const markerPath = join30(sandboxDir, ".playwright-installed");
8379
+ if (existsSync28(markerPath)) return;
7952
8380
  if (!json) logger.info("Installing Playwright Chromium (one-time)...");
7953
8381
  const result = childProcess5.spawnSync(
7954
8382
  "npx",
@@ -7967,8 +8395,8 @@ function ensurePlaywrightInstalled(sandboxDir, json) {
7967
8395
  writeFileSync17(markerPath, (/* @__PURE__ */ new Date()).toISOString(), "utf-8");
7968
8396
  }
7969
8397
  function safeListPngs(dir) {
7970
- if (!existsSync27(dir)) return [];
7971
- return readdirSync14(dir).filter((f) => f.endsWith(".png")).map((f) => join29(dir, f));
8398
+ if (!existsSync28(dir)) return [];
8399
+ return readdirSync14(dir).filter((f) => f.endsWith(".png")).map((f) => join30(dir, f));
7972
8400
  }
7973
8401
  function tryParseJson(s) {
7974
8402
  try {
@@ -7989,13 +8417,13 @@ function fail2(json, message) {
7989
8417
  // src/commands/spawn.ts
7990
8418
  import {
7991
8419
  cpSync,
7992
- existsSync as existsSync29,
7993
- readFileSync as readFileSync31,
8420
+ existsSync as existsSync30,
8421
+ readFileSync as readFileSync32,
7994
8422
  readdirSync as readdirSync16,
7995
- rmSync as rmSync4
8423
+ rmSync as rmSync4,
8424
+ statSync as statSync12
7996
8425
  } from "fs";
7997
- import { basename as basename8, isAbsolute as isAbsolute5, join as join31, resolve as resolve19 } from "path";
7998
- import { homedir as homedir3 } from "os";
8426
+ import { basename as basename8, dirname as dirname14, isAbsolute as isAbsolute6, join as join32, resolve as resolve20 } from "path";
7999
8427
  import * as childProcess6 from "child_process";
8000
8428
  import { parse as parseYaml5 } from "yaml";
8001
8429
  import { generateThemeData as generateThemeData7 } from "@loworbitstudio/visor-theme-engine";
@@ -8003,8 +8431,8 @@ import { nextjsAdapter as nextjsAdapter3 } from "@loworbitstudio/visor-theme-eng
8003
8431
  import { validate as validate3 } from "@loworbitstudio/visor-theme-engine";
8004
8432
 
8005
8433
  // src/lib/blessed-discovery.ts
8006
- import { existsSync as existsSync28, readdirSync as readdirSync15 } from "fs";
8007
- import { join as join30 } from "path";
8434
+ import { existsSync as existsSync29, readdirSync as readdirSync15, statSync as statSync11 } from "fs";
8435
+ import { join as join31 } from "path";
8008
8436
  var WALK_EXCLUDE_DIRS = /* @__PURE__ */ new Set([
8009
8437
  "node_modules",
8010
8438
  ".next",
@@ -8014,11 +8442,15 @@ var WALK_EXCLUDE_DIRS = /* @__PURE__ */ new Set([
8014
8442
  "coverage",
8015
8443
  ".cache"
8016
8444
  ]);
8445
+ var REFERENCE_BUILD_DIRNAME = "reference-build";
8446
+ var APPROVED_CAPTURES_SUBPATH = join31("captures", "approved");
8447
+ var MISSING_MANIFEST_REASON = `missing ${BLESSED_MANIFEST_FILENAME}`;
8017
8448
  function discoverBlessedBuilds(blessedDir, maxDepth = 8) {
8018
8449
  const builds = [];
8450
+ const incomplete = [];
8019
8451
  const errors = [];
8020
- if (!existsSync28(blessedDir)) {
8021
- return { builds, errors };
8452
+ if (!existsSync29(blessedDir)) {
8453
+ return { builds, incomplete, errors };
8022
8454
  }
8023
8455
  walk2(blessedDir, 0);
8024
8456
  builds.sort((a, b) => {
@@ -8027,7 +8459,8 @@ function discoverBlessedBuilds(blessedDir, maxDepth = 8) {
8027
8459
  }
8028
8460
  return a.manifest.pattern.localeCompare(b.manifest.pattern);
8029
8461
  });
8030
- return { builds, errors };
8462
+ incomplete.sort((a, b) => a.dir.localeCompare(b.dir));
8463
+ return { builds, incomplete, errors };
8031
8464
  function walk2(dir, depth) {
8032
8465
  if (depth > maxDepth) return;
8033
8466
  let entries;
@@ -8048,13 +8481,30 @@ function discoverBlessedBuilds(blessedDir, maxDepth = 8) {
8048
8481
  }
8049
8482
  return;
8050
8483
  }
8484
+ if (isNearMissRoot(dir, entries)) {
8485
+ incomplete.push({ dir, reason: MISSING_MANIFEST_REASON });
8486
+ return;
8487
+ }
8051
8488
  for (const entry of entries) {
8052
8489
  if (!entry.isDirectory()) continue;
8053
8490
  if (WALK_EXCLUDE_DIRS.has(entry.name)) continue;
8054
- walk2(join30(dir, entry.name), depth + 1);
8491
+ walk2(join31(dir, entry.name), depth + 1);
8055
8492
  }
8056
8493
  }
8057
8494
  }
8495
+ function isNearMissRoot(dir, entries) {
8496
+ const hasReferenceBuild = entries.some(
8497
+ (entry) => entry.isDirectory() && entry.name === REFERENCE_BUILD_DIRNAME
8498
+ );
8499
+ if (!hasReferenceBuild) return false;
8500
+ const approved = join31(dir, APPROVED_CAPTURES_SUBPATH);
8501
+ try {
8502
+ if (!statSync11(approved).isDirectory()) return false;
8503
+ } catch {
8504
+ return false;
8505
+ }
8506
+ return !existsSync29(join31(dir, REFERENCE_BUILD_DIRNAME, BLESSED_MANIFEST_FILENAME));
8507
+ }
8058
8508
  function resolveBlessedBuild(blessedDir, shape, pattern2) {
8059
8509
  const { builds } = discoverBlessedBuilds(blessedDir);
8060
8510
  const build = builds.find(
@@ -8064,13 +8514,18 @@ function resolveBlessedBuild(blessedDir, shape, pattern2) {
8064
8514
  }
8065
8515
 
8066
8516
  // src/commands/spawn.ts
8067
- var DEFAULT_BLESSED_DIR = join31(
8068
- homedir3(),
8069
- "Code",
8070
- "low-orbit",
8071
- "low-orbit-playbook",
8072
- "design-prototypes"
8073
- );
8517
+ var BLESSED_ROOT_DIRNAME = "design-prototypes";
8518
+ var NO_BLESSED_ROOT_ERROR = `No blessed-build root configured. Pass --blessed-dir <path>, set VISOR_BLESSED_DIR, or run from a checkout that contains a '${BLESSED_ROOT_DIRNAME}/' directory. See docs/blessed-builds.md.`;
8519
+ function discoverBlessedRoot(cwd) {
8520
+ let dir = resolve20(cwd);
8521
+ for (; ; ) {
8522
+ const candidate = join32(dir, BLESSED_ROOT_DIRNAME);
8523
+ if (existsSync30(candidate) && statSync12(candidate).isDirectory()) return candidate;
8524
+ const parent = dirname14(dir);
8525
+ if (parent === dir) return void 0;
8526
+ dir = parent;
8527
+ }
8528
+ }
8074
8529
  var FORK_EXCLUDE = /* @__PURE__ */ new Set([
8075
8530
  "node_modules",
8076
8531
  ".next",
@@ -8099,24 +8554,27 @@ function parseBlessedIdentifier(from) {
8099
8554
  return { shape: parts[0], pattern: parts[1] };
8100
8555
  }
8101
8556
  function resolveBlessedDir(cwd, options) {
8102
- const raw = options.blessedDir ?? (process.env.VISOR_BLESSED_DIR && process.env.VISOR_BLESSED_DIR.length > 0 ? process.env.VISOR_BLESSED_DIR : void 0) ?? DEFAULT_BLESSED_DIR;
8103
- return isAbsolute5(raw) ? raw : resolve19(cwd, raw);
8557
+ const explicit = options.blessedDir ?? (process.env.VISOR_BLESSED_DIR && process.env.VISOR_BLESSED_DIR.length > 0 ? process.env.VISOR_BLESSED_DIR : void 0);
8558
+ if (explicit !== void 0) {
8559
+ return isAbsolute6(explicit) ? explicit : resolve20(cwd, explicit);
8560
+ }
8561
+ return discoverBlessedRoot(cwd);
8104
8562
  }
8105
8563
  function resolveThemeFile(theme2, cwd, themeFile) {
8106
8564
  const candidates = [];
8107
8565
  if (themeFile) {
8108
- candidates.push(isAbsolute5(themeFile) ? themeFile : resolve19(cwd, themeFile));
8566
+ candidates.push(isAbsolute6(themeFile) ? themeFile : resolve20(cwd, themeFile));
8109
8567
  }
8110
- candidates.push(isAbsolute5(theme2) ? theme2 : resolve19(cwd, theme2));
8568
+ candidates.push(isAbsolute6(theme2) ? theme2 : resolve20(cwd, theme2));
8111
8569
  const privateRoot = process.env.VISOR_THEMES_PRIVATE_PATH;
8112
8570
  if (privateRoot && privateRoot.length > 0) {
8113
- candidates.push(join31(privateRoot, "themes", theme2, "theme.visor.yaml"));
8571
+ candidates.push(join32(privateRoot, "themes", theme2, "theme.visor.yaml"));
8114
8572
  }
8115
8573
  candidates.push(
8116
- join31(cwd, "themes", `${theme2}.visor.yaml`),
8117
- join31(cwd, "custom-themes", `${theme2}.visor.yaml`)
8574
+ join32(cwd, "themes", `${theme2}.visor.yaml`),
8575
+ join32(cwd, "custom-themes", `${theme2}.visor.yaml`)
8118
8576
  );
8119
- const found = candidates.find((candidate) => existsSync29(candidate));
8577
+ const found = candidates.find((candidate) => existsSync30(candidate));
8120
8578
  if (!found) {
8121
8579
  throw new Error(
8122
8580
  `Theme '${theme2}' not found (searched: ${candidates.join(", ")}). Pass --theme-file <path>, use a direct path, or set VISOR_THEMES_PRIVATE_PATH.`
@@ -8125,7 +8583,7 @@ function resolveThemeFile(theme2, cwd, themeFile) {
8125
8583
  return { path: found, searched: candidates };
8126
8584
  }
8127
8585
  function applyThemeToFork(themeFile, outputDir, manifest) {
8128
- const yaml = readFileSync31(themeFile, "utf-8");
8586
+ const yaml = readFileSync32(themeFile, "utf-8");
8129
8587
  const data = generateThemeData7(yaml);
8130
8588
  const themeId = data.config.name;
8131
8589
  if (!themeId || themeId.length === 0) {
@@ -8162,6 +8620,7 @@ function runSpawn(cwd, options) {
8162
8620
  if (!options.output) throw new Error("Missing required --output <path>.");
8163
8621
  const { shape, pattern: pattern2 } = parseBlessedIdentifier(options.from);
8164
8622
  const blessedDir = resolveBlessedDir(cwd, options);
8623
+ if (blessedDir === void 0) throw new Error(NO_BLESSED_ROOT_ERROR);
8165
8624
  const { build, available } = resolveBlessedBuild(blessedDir, shape, pattern2);
8166
8625
  if (!build) {
8167
8626
  const list = available.length > 0 ? available.map((b) => ` - blessed:${b.manifest.shape}:${b.manifest.pattern}`).join("\n") : " (none found)";
@@ -8171,8 +8630,8 @@ Available builds:
8171
8630
  ${list}`
8172
8631
  );
8173
8632
  }
8174
- const outputDir = isAbsolute5(options.output) ? options.output : resolve19(cwd, options.output);
8175
- if (existsSync29(outputDir) && readdirSync16(outputDir).length > 0) {
8633
+ const outputDir = isAbsolute6(options.output) ? options.output : resolve20(cwd, options.output);
8634
+ if (existsSync30(outputDir) && readdirSync16(outputDir).length > 0) {
8176
8635
  throw new Error(`Output directory already exists and is not empty: ${outputDir}`);
8177
8636
  }
8178
8637
  const { path: themeFile } = resolveThemeFile(options.theme, cwd, options.themeFile);
@@ -8185,7 +8644,7 @@ ${list}`
8185
8644
  try {
8186
8645
  applyThemeToFork(themeFile, outputDir, build.manifest);
8187
8646
  if (options.validate) {
8188
- const parsed = parseYaml5(readFileSync31(themeFile, "utf-8"));
8647
+ const parsed = parseYaml5(readFileSync32(themeFile, "utf-8"));
8189
8648
  const result = validate3(parsed);
8190
8649
  if (!result.valid) {
8191
8650
  const messages = result.errors.map((e) => e.message).join("; ");
@@ -8218,7 +8677,8 @@ ${list}`
8218
8677
  }
8219
8678
  function listBlessed(cwd, options) {
8220
8679
  const blessedDir = resolveBlessedDir(cwd, options);
8221
- const { builds, errors } = discoverBlessedBuilds(blessedDir);
8680
+ if (blessedDir === void 0) throw new Error(NO_BLESSED_ROOT_ERROR);
8681
+ const { builds, incomplete, errors } = discoverBlessedBuilds(blessedDir);
8222
8682
  if (options.json) {
8223
8683
  console.log(
8224
8684
  JSON.stringify({
@@ -8233,6 +8693,7 @@ function listBlessed(cwd, options) {
8233
8693
  three_gates_status: b.manifest.three_gates_status,
8234
8694
  dir: b.dir
8235
8695
  })),
8696
+ incomplete,
8236
8697
  errors
8237
8698
  })
8238
8699
  );
@@ -8246,6 +8707,14 @@ function listBlessed(cwd, options) {
8246
8707
  logger.success(`blessed:${b.manifest.shape}:${b.manifest.pattern}`);
8247
8708
  logger.item(`base theme: ${b.manifest.base_theme} \xB7 requires visor ${b.manifest.requires_visor} \xB7 gates: ${b.manifest.three_gates_status}`);
8248
8709
  }
8710
+ if (incomplete.length > 0) {
8711
+ logger.blank();
8712
+ logger.heading(`Near-miss builds (not spawnable) \u2014 ${incomplete.length}`);
8713
+ for (const i of incomplete) {
8714
+ logger.warn(i.dir);
8715
+ logger.item(i.reason);
8716
+ }
8717
+ }
8249
8718
  for (const e of errors) {
8250
8719
  logger.warn(`Skipped ${e.dir}: ${e.error}`);
8251
8720
  }
@@ -8287,8 +8756,8 @@ function spawnCommand(cwd, options) {
8287
8756
  }
8288
8757
 
8289
8758
  // src/commands/render.ts
8290
- import { existsSync as existsSync30, mkdirSync as mkdirSync14, readFileSync as readFileSync32 } from "fs";
8291
- import { dirname as dirname13, isAbsolute as isAbsolute6, resolve as resolve20 } from "path";
8759
+ import { existsSync as existsSync31, mkdirSync as mkdirSync14, readFileSync as readFileSync33 } from "fs";
8760
+ import { dirname as dirname15, isAbsolute as isAbsolute7, resolve as resolve21 } from "path";
8292
8761
  var FIXTURES = {
8293
8762
  "stat-card": {
8294
8763
  default: {
@@ -8440,28 +8909,28 @@ function pascalCase(kebab) {
8440
8909
  }
8441
8910
  function firstExisting(candidates) {
8442
8911
  for (const c of candidates) {
8443
- if (existsSync30(c)) return c;
8912
+ if (existsSync31(c)) return c;
8444
8913
  }
8445
8914
  return null;
8446
8915
  }
8447
8916
  function resolveComponentFile(cwd, name) {
8448
8917
  return firstExisting([
8449
- resolve20(cwd, "components", "ui", name, `${name}.tsx`),
8450
- resolve20(cwd, "components", "devtools", name, `${name}.tsx`),
8451
- resolve20(cwd, "components", "ui", name, "index.tsx")
8918
+ resolve21(cwd, "components", "ui", name, `${name}.tsx`),
8919
+ resolve21(cwd, "components", "devtools", name, `${name}.tsx`),
8920
+ resolve21(cwd, "components", "ui", name, "index.tsx")
8452
8921
  ]);
8453
8922
  }
8454
8923
  function resolveThemeCssFile(cwd, slug2) {
8455
8924
  return firstExisting([
8456
- resolve20(cwd, "packages", "docs", "app", `${slug2}-theme.css`),
8457
- resolve20(cwd, "app", `${slug2}-theme.css`),
8458
- resolve20(cwd, `${slug2}-theme.css`)
8925
+ resolve21(cwd, "packages", "docs", "app", `${slug2}-theme.css`),
8926
+ resolve21(cwd, "app", `${slug2}-theme.css`),
8927
+ resolve21(cwd, `${slug2}-theme.css`)
8459
8928
  ]);
8460
8929
  }
8461
8930
  function resolveTokensCssFile(cwd) {
8462
8931
  return firstExisting([
8463
- resolve20(cwd, "packages", "tokens", "dist", "tokens.css"),
8464
- resolve20(cwd, "node_modules", "@loworbitstudio", "visor-core", "dist", "tokens.css")
8932
+ resolve21(cwd, "packages", "tokens", "dist", "tokens.css"),
8933
+ resolve21(cwd, "node_modules", "@loworbitstudio", "visor-core", "dist", "tokens.css")
8465
8934
  ]);
8466
8935
  }
8467
8936
  async function loadOptional(moduleName) {
@@ -8640,8 +9109,8 @@ async function renderCommand(component, cwd, options) {
8640
9109
  }
8641
9110
  const themeClass = `${options.theme}-theme`;
8642
9111
  const html = buildHtml({
8643
- tokensCss: readFileSync32(tokensCssFile, "utf-8"),
8644
- themeCss: readFileSync32(themeCssFile, "utf-8"),
9112
+ tokensCss: readFileSync33(tokensCssFile, "utf-8"),
9113
+ themeCss: readFileSync33(themeCssFile, "utf-8"),
8645
9114
  componentCss,
8646
9115
  bundleJs,
8647
9116
  themeClass,
@@ -8650,7 +9119,7 @@ async function renderCommand(component, cwd, options) {
8650
9119
  const width = Number(options.width ?? 720);
8651
9120
  const height = Number(options.height ?? 640);
8652
9121
  const outPath = resolveOutPath(cwd, options.out, component, options.theme, mode, state);
8653
- mkdirSync14(dirname13(outPath), { recursive: true });
9122
+ mkdirSync14(dirname15(outPath), { recursive: true });
8654
9123
  const browser = await playwright.chromium.launch();
8655
9124
  let probe;
8656
9125
  try {
@@ -8706,7 +9175,7 @@ async function renderCommand(component, cwd, options) {
8706
9175
  } finally {
8707
9176
  await browser.close();
8708
9177
  }
8709
- const fileSize = existsSync30(outPath) ? statBytes(outPath) : 0;
9178
+ const fileSize = existsSync31(outPath) ? statBytes(outPath) : 0;
8710
9179
  if (json) {
8711
9180
  console.log(
8712
9181
  JSON.stringify(
@@ -8742,14 +9211,14 @@ async function renderCommand(component, cwd, options) {
8742
9211
  }
8743
9212
  function resolveOutPath(cwd, out, component, theme2, mode, state) {
8744
9213
  if (out) {
8745
- return isAbsolute6(out) ? out : resolve20(cwd, out);
9214
+ return isAbsolute7(out) ? out : resolve21(cwd, out);
8746
9215
  }
8747
9216
  const suffix = state === "default" ? "" : `__${state}`;
8748
- return resolve20(cwd, ".visor", "renders", `${component}__${theme2}__${mode}${suffix}.png`);
9217
+ return resolve21(cwd, ".visor", "renders", `${component}__${theme2}__${mode}${suffix}.png`);
8749
9218
  }
8750
9219
  function statBytes(path2) {
8751
9220
  try {
8752
- return readFileSync32(path2).length;
9221
+ return readFileSync33(path2).length;
8753
9222
  } catch {
8754
9223
  return 0;
8755
9224
  }
@@ -8762,9 +9231,9 @@ function formatBytes2(bytes) {
8762
9231
  }
8763
9232
 
8764
9233
  // src/index.ts
8765
- var __dirname2 = dirname14(fileURLToPath4(import.meta.url));
9234
+ var __dirname2 = dirname16(fileURLToPath4(import.meta.url));
8766
9235
  var pkg = JSON.parse(
8767
- readFileSync33(join33(__dirname2, "..", "package.json"), "utf-8")
9236
+ readFileSync34(join34(__dirname2, "..", "package.json"), "utf-8")
8768
9237
  );
8769
9238
  var program = new Command2();
8770
9239
  program.name("visor").description("CLI for the Visor design system").version(pkg.version);
@@ -8959,7 +9428,7 @@ sandbox.command("approve").description(
8959
9428
  );
8960
9429
  program.command("spawn").description(
8961
9430
  "Fork a Playbook blessed reference build with atomic theme re-skinning \u2014 one command replaces cp -R + npm install + theme apply"
8962
- ).option("--from <identifier>", "blessed build to spawn: blessed:{shape}:{pattern}").option("--theme <id>", "theme id (or path to a .visor.yaml) to re-skin the fork with").option("--theme-file <path>", "explicit path to a theme.visor.yaml \u2014 bypasses --theme name resolution").option("--output <path>", "destination directory for the forked project").option("--blessed-dir <path>", "override the blessed-build root (default: VISOR_BLESSED_DIR or ~/Code/low-orbit/low-orbit-playbook/design-prototypes)").option("--install", "run npm install in the forked project (default: skip)").option("--validate", "validate the applied theme after forking (default: skip)").option("--list-blessed", "list all discoverable blessed builds and exit").option("--json", "output structured JSON (for AI agents)").action((options) => {
9431
+ ).option("--from <identifier>", "blessed build to spawn: blessed:{shape}:{pattern}").option("--theme <id>", "theme id (or path to a .visor.yaml) to re-skin the fork with").option("--theme-file <path>", "explicit path to a theme.visor.yaml \u2014 bypasses --theme name resolution").option("--output <path>", "destination directory for the forked project").option("--blessed-dir <path>", "override the blessed-build root (default: VISOR_BLESSED_DIR, else the nearest design-prototypes/ directory found by walking up from the cwd)").option("--install", "run npm install in the forked project (default: skip)").option("--validate", "validate the applied theme after forking (default: skip)").option("--list-blessed", "list all discoverable blessed builds and exit").option("--json", "output structured JSON (for AI agents)").action((options) => {
8963
9432
  spawnCommand(process.cwd(), options);
8964
9433
  });
8965
9434
  program.command("render").description(