@loworbitstudio/visor 1.14.0 → 1.15.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,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { readFileSync as readFileSync28 } from "fs";
5
- import { dirname as dirname10, join as join25 } from "path";
4
+ import { readFileSync as readFileSync32 } from "fs";
5
+ import { dirname as dirname11, join as join30 } from "path";
6
6
  import { fileURLToPath as fileURLToPath4 } from "url";
7
7
  import { Command as Command2 } from "commander";
8
8
 
@@ -1191,10 +1191,10 @@ function checkCommand() {
1191
1191
  }
1192
1192
 
1193
1193
  // src/commands/init.ts
1194
- import { existsSync as existsSync4, writeFileSync as writeFileSync2, mkdirSync, readFileSync as readFileSync7 } from "fs";
1195
- import { join as join6, dirname as dirname3 } from "path";
1194
+ import { existsSync as existsSync6, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync9 } from "fs";
1195
+ import { join as join8, dirname as dirname4, basename as basename2 } from "path";
1196
1196
  import { fileURLToPath as fileURLToPath2 } from "url";
1197
- import * as childProcess from "child_process";
1197
+ import * as childProcess2 from "child_process";
1198
1198
 
1199
1199
  // src/config/config.ts
1200
1200
  import { readFileSync as readFileSync5, writeFileSync, existsSync as existsSync2 } from "fs";
@@ -1378,6 +1378,156 @@ export default function RootLayout({ children }: { children: ReactNode }) {
1378
1378
  `;
1379
1379
  }
1380
1380
 
1381
+ // src/commands/init-plays/registry.ts
1382
+ import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync7, writeFileSync as writeFileSync2 } from "fs";
1383
+ import { dirname as dirname3, join as join6 } from "path";
1384
+
1385
+ // src/commands/init-plays/pattern-build.ts
1386
+ var patternBuildPlay = {
1387
+ id: "pattern-build",
1388
+ loSubdir: "pattern-builds",
1389
+ label: "Pattern build",
1390
+ description: "Design + converge a reusable Borealis pattern (admin-ui, forms, ...)."
1391
+ };
1392
+
1393
+ // src/commands/init-plays/new-web-app.ts
1394
+ var newWebAppPlay = {
1395
+ id: "new-web-app",
1396
+ loSubdir: "new-web-apps",
1397
+ label: "New web app",
1398
+ description: "A fresh NextJS app entering the Playbook lifecycle."
1399
+ };
1400
+
1401
+ // src/commands/init-plays/feature-addition.ts
1402
+ var featureAdditionPlay = {
1403
+ id: "feature-addition",
1404
+ loSubdir: "feature-additions",
1405
+ label: "Feature addition",
1406
+ description: "An existing project onboarding into the Playbook mid-stream (safe, idempotent)."
1407
+ };
1408
+
1409
+ // src/commands/init-plays/registry.ts
1410
+ var KNOWN_PLAYS = [
1411
+ patternBuildPlay,
1412
+ newWebAppPlay,
1413
+ featureAdditionPlay
1414
+ ];
1415
+ function getPlay(id) {
1416
+ return KNOWN_PLAYS.find((p) => p.id === id);
1417
+ }
1418
+ function knownPlayIds() {
1419
+ return KNOWN_PLAYS.map((p) => p.id);
1420
+ }
1421
+ function playStatePaths(cwd, def, name) {
1422
+ const relDir = join6(".lo", def.loSubdir, name);
1423
+ const dir = join6(cwd, relDir);
1424
+ return {
1425
+ dir,
1426
+ statePath: join6(dir, "state.json"),
1427
+ relStatePath: join6(relDir, "state.json")
1428
+ };
1429
+ }
1430
+ function readPlayState(statePath) {
1431
+ if (!existsSync4(statePath)) return null;
1432
+ try {
1433
+ return JSON.parse(readFileSync7(statePath, "utf-8"));
1434
+ } catch {
1435
+ return null;
1436
+ }
1437
+ }
1438
+ function writePlayState(statePath, state) {
1439
+ mkdirSync(dirname3(statePath), { recursive: true });
1440
+ writeFileSync2(statePath, JSON.stringify(state, null, 2) + "\n", "utf-8");
1441
+ }
1442
+
1443
+ // src/lib/lo-ports-bridge.ts
1444
+ import * as childProcess from "child_process";
1445
+ var LO_PORTS_COMMAND = "lo-ports";
1446
+ var LO_PORTS_ARGS = ["next", "--json"];
1447
+ var FALLBACK_RANGE_START = 4200;
1448
+ var FALLBACK_RANGE_SIZE = 100;
1449
+ function allocatePort(name, options = {}) {
1450
+ const runCommand = options.runCommand ?? defaultRunCommand;
1451
+ const stdout = safeRun(runCommand);
1452
+ const fromRegistry = stdout != null ? parsePort(stdout) : null;
1453
+ if (fromRegistry != null) {
1454
+ return { port: fromRegistry, source: "lo-ports" };
1455
+ }
1456
+ const port = heuristicPort(name);
1457
+ return {
1458
+ port,
1459
+ source: "fallback",
1460
+ warning: `Could not allocate a dev port via /lo-ports (no \`${LO_PORTS_COMMAND}\` command found). Used heuristic port ${port} instead \u2014 register it with \`/lo-ports\` when convenient.`
1461
+ };
1462
+ }
1463
+ function safeRun(runCommand) {
1464
+ try {
1465
+ return runCommand();
1466
+ } catch {
1467
+ return null;
1468
+ }
1469
+ }
1470
+ function defaultRunCommand() {
1471
+ const result = childProcess.spawnSync(LO_PORTS_COMMAND, LO_PORTS_ARGS, {
1472
+ encoding: "utf-8"
1473
+ });
1474
+ if (result.error) return null;
1475
+ if (typeof result.status === "number" && result.status !== 0) return null;
1476
+ return result.stdout ?? null;
1477
+ }
1478
+ function parsePort(stdout) {
1479
+ const trimmed = stdout.trim();
1480
+ if (trimmed.length === 0) return null;
1481
+ try {
1482
+ const parsed = JSON.parse(trimmed);
1483
+ if (typeof parsed === "number" && isValidPort(parsed)) return parsed;
1484
+ if (parsed != null && typeof parsed === "object" && "port" in parsed && isValidPort(parsed.port)) {
1485
+ return parsed.port;
1486
+ }
1487
+ } catch {
1488
+ }
1489
+ const bare = Number(trimmed);
1490
+ return isValidPort(bare) ? bare : null;
1491
+ }
1492
+ function isValidPort(value) {
1493
+ return typeof value === "number" && Number.isInteger(value) && value > 0 && value < 65536;
1494
+ }
1495
+ function heuristicPort(name) {
1496
+ let hash = 0;
1497
+ for (let i = 0; i < name.length; i++) {
1498
+ hash = hash * 31 + name.charCodeAt(i) >>> 0;
1499
+ }
1500
+ return FALLBACK_RANGE_START + hash % FALLBACK_RANGE_SIZE;
1501
+ }
1502
+
1503
+ // src/lib/lo-play-checklist.ts
1504
+ import { existsSync as existsSync5, readFileSync as readFileSync8 } from "fs";
1505
+ import { homedir } from "os";
1506
+ import { join as join7 } from "path";
1507
+ function defaultChecklistRoot() {
1508
+ return join7(homedir(), ".claude", "skills", "lo-play");
1509
+ }
1510
+ function readEntryChecklist(playType, options = {}) {
1511
+ const root = options.root ?? defaultChecklistRoot();
1512
+ const path2 = join7(root, playType, "entry-checklist.md");
1513
+ if (!existsSync5(path2)) {
1514
+ return {
1515
+ found: false,
1516
+ path: path2,
1517
+ fallbackMessage: `Entry checklist not found at ${path2}. Run the play manually from \`/lo-play ${playType}\`.`
1518
+ };
1519
+ }
1520
+ try {
1521
+ return { found: true, path: path2, content: readFileSync8(path2, "utf-8") };
1522
+ } catch {
1523
+ return {
1524
+ found: false,
1525
+ path: path2,
1526
+ fallbackMessage: `Entry checklist at ${path2} could not be read. Run the play manually from \`/lo-play ${playType}\`.`
1527
+ };
1528
+ }
1529
+ }
1530
+
1381
1531
  // src/commands/init.ts
1382
1532
  import { generateThemeData as generateThemeData2 } from "@loworbitstudio/visor-theme-engine";
1383
1533
  import { nextjsAdapter } from "@loworbitstudio/visor-theme-engine/adapters";
@@ -1390,7 +1540,27 @@ function initCommand(cwd, options) {
1390
1540
  emitError(json, `Unknown template: ${options.template}. Available templates: nextjs`);
1391
1541
  process.exit(1);
1392
1542
  }
1393
- if (options?.template === "nextjs" && existsSync4(join6(cwd, "package.json"))) {
1543
+ let playDef;
1544
+ let playName = "";
1545
+ if (options?.for) {
1546
+ playDef = getPlay(options.for);
1547
+ if (!playDef) {
1548
+ emitError(
1549
+ json,
1550
+ `Unknown play: ${options.for}. Known plays: ${knownPlayIds().join(", ")}`
1551
+ );
1552
+ process.exit(1);
1553
+ }
1554
+ playName = options.playName ?? basename2(cwd);
1555
+ if (!/^[a-z0-9][a-z0-9-_]*$/i.test(playName)) {
1556
+ emitError(
1557
+ json,
1558
+ `Invalid play name '${playName}'. Use letters, digits, '-' or '_' (e.g. --play-name organization-management).`
1559
+ );
1560
+ process.exit(1);
1561
+ }
1562
+ }
1563
+ if (options?.template === "nextjs" && existsSync6(join8(cwd, "package.json"))) {
1394
1564
  emitError(
1395
1565
  json,
1396
1566
  "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"
@@ -1431,6 +1601,10 @@ function initCommand(cwd, options) {
1431
1601
  }
1432
1602
  }
1433
1603
  }
1604
+ let playResult;
1605
+ if (playDef) {
1606
+ playResult = runPlayInit(cwd, playDef, playName, options ?? {}, json, warnings);
1607
+ }
1434
1608
  if (json) {
1435
1609
  const nextSteps = buildNextSteps(options, warnings);
1436
1610
  const result = {
@@ -1438,12 +1612,91 @@ function initCommand(cwd, options) {
1438
1612
  config: DEFAULT_CONFIG,
1439
1613
  files: { created: filesCreated, skipped: filesSkipped },
1440
1614
  warnings,
1441
- nextSteps
1615
+ nextSteps,
1616
+ ...playResult ? { play: playResult } : {}
1442
1617
  };
1443
1618
  console.log(JSON.stringify(result, null, 2));
1444
1619
  process.exit(0);
1445
1620
  }
1446
1621
  }
1622
+ function runPlayInit(cwd, def, name, options, json, warnings) {
1623
+ const { statePath, relStatePath } = playStatePaths(cwd, def, name);
1624
+ const existing = readPlayState(statePath);
1625
+ const checklist = readEntryChecklist(def.id);
1626
+ if (existing) {
1627
+ if (!json) {
1628
+ logger.blank();
1629
+ logger.warn(
1630
+ `Play '${def.id}' / '${name}' already initialized at ${relStatePath} (phase ${existing.phase}${existing.devPort ? `, port ${existing.devPort}` : ""}). Nothing to do.`
1631
+ );
1632
+ printChecklist(def.id, checklist);
1633
+ }
1634
+ return {
1635
+ play: def.id,
1636
+ name,
1637
+ statePath: relStatePath,
1638
+ phase: existing.phase,
1639
+ devPort: existing.devPort,
1640
+ portSource: existing.portSource,
1641
+ theme: existing.theme,
1642
+ from: existing.from,
1643
+ alreadyInitialized: true,
1644
+ checklist: { found: checklist.found, path: checklist.path }
1645
+ };
1646
+ }
1647
+ const port = allocatePort(name);
1648
+ if (port.warning) warnings.push(port.warning);
1649
+ const state = {
1650
+ play: def.id,
1651
+ name,
1652
+ phase: 0,
1653
+ ...options.theme ? { theme: options.theme } : {},
1654
+ ...options.from ? { from: options.from } : {},
1655
+ devPort: port.port,
1656
+ portSource: port.source,
1657
+ createdWith: `@loworbitstudio/visor@${readVisorCliVersion()}`,
1658
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1659
+ };
1660
+ writePlayState(statePath, state);
1661
+ if (!json) {
1662
+ logger.blank();
1663
+ if (options.template === "nextjs") {
1664
+ logger.success("Visor scaffold: NextJS + tokens + FOWT layout");
1665
+ }
1666
+ logger.success(`Playbook state: ${relStatePath} (phase 0)`);
1667
+ if (port.source === "lo-ports") {
1668
+ logger.success(`Dev port allocated: ${port.port} (via /lo-ports)`);
1669
+ } else {
1670
+ logger.warn(port.warning ?? `Dev port ${port.port} (heuristic fallback)`);
1671
+ logger.success(`Dev port: ${port.port} (heuristic fallback)`);
1672
+ }
1673
+ if (options.theme) logger.success(`Theme recorded: ${options.theme}`);
1674
+ printChecklist(def.id, checklist);
1675
+ }
1676
+ return {
1677
+ play: def.id,
1678
+ name,
1679
+ statePath: relStatePath,
1680
+ phase: 0,
1681
+ devPort: port.port,
1682
+ portSource: port.source,
1683
+ theme: options.theme,
1684
+ from: options.from,
1685
+ alreadyInitialized: false,
1686
+ checklist: { found: checklist.found, path: checklist.path }
1687
+ };
1688
+ }
1689
+ function printChecklist(playId, checklist) {
1690
+ logger.blank();
1691
+ if (checklist.found) {
1692
+ logger.info(`Next steps (from ${checklist.path}):`);
1693
+ for (const line of checklist.content.split("\n")) {
1694
+ logger.item(line);
1695
+ }
1696
+ } else {
1697
+ logger.warn(checklist.fallbackMessage);
1698
+ }
1699
+ }
1447
1700
  function buildNextSteps(options, warnings) {
1448
1701
  const steps = [];
1449
1702
  if (options?.template === "nextjs") {
@@ -1473,14 +1726,14 @@ function scaffoldNextjs(cwd, json, filesCreated, filesSkipped, warnings) {
1473
1726
  }
1474
1727
  runCreateNextApp(cwd, json);
1475
1728
  runInstallVisorDeps(cwd, json);
1476
- const yamlPath = join6(cwd, ".visor.yaml");
1477
- if (existsSync4(yamlPath)) {
1729
+ const yamlPath = join8(cwd, ".visor.yaml");
1730
+ if (existsSync6(yamlPath)) {
1478
1731
  filesSkipped.push(".visor.yaml");
1479
1732
  if (!json) {
1480
1733
  logger.warn(".visor.yaml already exists. Skipping.");
1481
1734
  }
1482
1735
  } else {
1483
- writeFileSync2(yamlPath, NEXTJS_STARTER_YAML, "utf-8");
1736
+ writeFileSync3(yamlPath, NEXTJS_STARTER_YAML, "utf-8");
1484
1737
  filesCreated.push(".visor.yaml");
1485
1738
  if (!json) {
1486
1739
  logger.success("Created .visor.yaml");
@@ -1492,40 +1745,40 @@ function scaffoldNextjs(cwd, json, filesCreated, filesSkipped, warnings) {
1492
1745
  tokens: data.tokens,
1493
1746
  config: data.config
1494
1747
  });
1495
- const globalsPath = join6(cwd, "app", "globals.css");
1496
- const globalsDir = dirname3(globalsPath);
1497
- mkdirSync(globalsDir, { recursive: true });
1498
- if (existsSync4(globalsPath)) {
1499
- writeFileSync2(globalsPath, css, "utf-8");
1748
+ const globalsPath = join8(cwd, "app", "globals.css");
1749
+ const globalsDir = dirname4(globalsPath);
1750
+ mkdirSync2(globalsDir, { recursive: true });
1751
+ if (existsSync6(globalsPath)) {
1752
+ writeFileSync3(globalsPath, css, "utf-8");
1500
1753
  filesCreated.push("app/globals.css");
1501
1754
  } else {
1502
- writeFileSync2(globalsPath, css, "utf-8");
1755
+ writeFileSync3(globalsPath, css, "utf-8");
1503
1756
  filesCreated.push("app/globals.css");
1504
1757
  }
1505
1758
  if (!json) {
1506
1759
  logger.success("Created app/globals.css with theme tokens");
1507
1760
  }
1508
- const layoutPath = join6(cwd, "app", "layout.tsx");
1761
+ const layoutPath = join8(cwd, "app", "layout.tsx");
1509
1762
  const colorScheme = extractColorScheme(NEXTJS_STARTER_YAML);
1510
- writeFileSync2(layoutPath, generateNextjsLayout(colorScheme), "utf-8");
1763
+ writeFileSync3(layoutPath, generateNextjsLayout(colorScheme), "utf-8");
1511
1764
  filesCreated.push("app/layout.tsx");
1512
1765
  if (!json) {
1513
1766
  logger.success("Wired app/layout.tsx with FOWT prevention and theme tokens");
1514
1767
  }
1515
- const stampDir = join6(cwd, ".lo");
1516
- const stampPath = join6(stampDir, "borealis.json");
1517
- if (existsSync4(stampPath)) {
1768
+ const stampDir = join8(cwd, ".lo");
1769
+ const stampPath = join8(stampDir, "borealis.json");
1770
+ if (existsSync6(stampPath)) {
1518
1771
  filesSkipped.push(".lo/borealis.json");
1519
1772
  if (!json) {
1520
1773
  logger.warn(".lo/borealis.json already exists. Skipping.");
1521
1774
  }
1522
1775
  } else {
1523
- mkdirSync(stampDir, { recursive: true });
1776
+ mkdirSync2(stampDir, { recursive: true });
1524
1777
  const stamp = {
1525
1778
  visorVersion: readVisorCliVersion(),
1526
1779
  initializedAt: (/* @__PURE__ */ new Date()).toISOString()
1527
1780
  };
1528
- writeFileSync2(stampPath, JSON.stringify(stamp, null, 2) + "\n", "utf-8");
1781
+ writeFileSync3(stampPath, JSON.stringify(stamp, null, 2) + "\n", "utf-8");
1529
1782
  filesCreated.push(".lo/borealis.json");
1530
1783
  if (!json) {
1531
1784
  logger.success("Stamped .lo/borealis.json");
@@ -1546,7 +1799,7 @@ function runCreateNextApp(cwd, json) {
1546
1799
  if (!json) {
1547
1800
  logger.info(`Running create-next-app@${NEXTJS_PINNED_VERSION}...`);
1548
1801
  }
1549
- const result = childProcess.spawnSync(
1802
+ const result = childProcess2.spawnSync(
1550
1803
  "npx",
1551
1804
  [`create-next-app@${NEXTJS_PINNED_VERSION}`, ".", ...CREATE_NEXT_APP_FLAGS],
1552
1805
  { cwd, stdio: json ? "ignore" : "inherit" }
@@ -1557,7 +1810,7 @@ function runInstallVisorDeps(cwd, json) {
1557
1810
  if (!json) {
1558
1811
  logger.info("Installing @loworbitstudio/visor-core and visor-theme-engine...");
1559
1812
  }
1560
- const result = childProcess.spawnSync(
1813
+ const result = childProcess2.spawnSync(
1561
1814
  "npm",
1562
1815
  [
1563
1816
  "install",
@@ -1578,12 +1831,12 @@ function assertSpawnSuccess(result, label) {
1578
1831
  }
1579
1832
  function readVisorCliVersion() {
1580
1833
  try {
1581
- const here = dirname3(fileURLToPath2(import.meta.url));
1834
+ const here = dirname4(fileURLToPath2(import.meta.url));
1582
1835
  for (let i = 0; i < 5; i++) {
1583
1836
  const segments = new Array(i).fill("..");
1584
- const candidate = join6(here, ...segments, "package.json");
1837
+ const candidate = join8(here, ...segments, "package.json");
1585
1838
  try {
1586
- const pkg2 = JSON.parse(readFileSync7(candidate, "utf-8"));
1839
+ const pkg2 = JSON.parse(readFileSync9(candidate, "utf-8"));
1587
1840
  if (pkg2.name === "@loworbitstudio/visor" && pkg2.version) {
1588
1841
  return pkg2.version;
1589
1842
  }
@@ -1597,53 +1850,53 @@ function readVisorCliVersion() {
1597
1850
 
1598
1851
  // src/utils/fs.ts
1599
1852
  import {
1600
- writeFileSync as writeFileSync3,
1601
- readFileSync as readFileSync8,
1602
- existsSync as existsSync5,
1603
- mkdirSync as mkdirSync2
1853
+ writeFileSync as writeFileSync4,
1854
+ readFileSync as readFileSync10,
1855
+ existsSync as existsSync7,
1856
+ mkdirSync as mkdirSync3
1604
1857
  } from "fs";
1605
- import { dirname as dirname4, join as join7 } from "path";
1858
+ import { dirname as dirname5, join as join9 } from "path";
1606
1859
  function resolveOutputPath(registryPath, type, config, cwd) {
1607
1860
  let relativePath;
1608
1861
  if (type === "registry:block") {
1609
1862
  relativePath = registryPath.replace(/^blocks\//, "");
1610
- return join7(cwd, config.paths.blocks, relativePath);
1863
+ return join9(cwd, config.paths.blocks, relativePath);
1611
1864
  }
1612
1865
  if (type === "registry:ui") {
1613
1866
  if (registryPath.startsWith("components/deck/")) {
1614
1867
  relativePath = registryPath.replace(/^components\/deck\//, "");
1615
- return join7(cwd, config.paths.deckComponents, relativePath);
1868
+ return join9(cwd, config.paths.deckComponents, relativePath);
1616
1869
  }
1617
1870
  if (registryPath.startsWith("components/flutter/")) {
1618
1871
  relativePath = registryPath.replace(/^components\/flutter\//, "");
1619
- return join7(cwd, config.paths.flutterComponents, relativePath);
1872
+ return join9(cwd, config.paths.flutterComponents, relativePath);
1620
1873
  }
1621
1874
  relativePath = registryPath.replace(/^components\/ui\//, "");
1622
- return join7(cwd, config.paths.components, relativePath);
1875
+ return join9(cwd, config.paths.components, relativePath);
1623
1876
  }
1624
1877
  if (type === "registry:hook") {
1625
1878
  relativePath = registryPath.replace(/^hooks\//, "");
1626
- return join7(cwd, config.paths.hooks, relativePath);
1879
+ return join9(cwd, config.paths.hooks, relativePath);
1627
1880
  }
1628
1881
  if (type === "registry:lib") {
1629
1882
  relativePath = registryPath.replace(/^lib\//, "");
1630
- return join7(cwd, config.paths.lib, relativePath);
1883
+ return join9(cwd, config.paths.lib, relativePath);
1631
1884
  }
1632
- return join7(cwd, registryPath);
1885
+ return join9(cwd, registryPath);
1633
1886
  }
1634
1887
  function writeFile(filePath, content) {
1635
- const dir = dirname4(filePath);
1636
- if (!existsSync5(dir)) {
1637
- mkdirSync2(dir, { recursive: true });
1888
+ const dir = dirname5(filePath);
1889
+ if (!existsSync7(dir)) {
1890
+ mkdirSync3(dir, { recursive: true });
1638
1891
  }
1639
- writeFileSync3(filePath, content, "utf-8");
1892
+ writeFileSync4(filePath, content, "utf-8");
1640
1893
  }
1641
1894
  function readFile(filePath) {
1642
- if (!existsSync5(filePath)) return null;
1643
- return readFileSync8(filePath, "utf-8");
1895
+ if (!existsSync7(filePath)) return null;
1896
+ return readFileSync10(filePath, "utf-8");
1644
1897
  }
1645
1898
  function fileExists(filePath) {
1646
- return existsSync5(filePath);
1899
+ return existsSync7(filePath);
1647
1900
  }
1648
1901
 
1649
1902
  // src/commands/list.ts
@@ -1784,8 +2037,8 @@ function listCommand(cwd, options = {}) {
1784
2037
  }
1785
2038
 
1786
2039
  // src/utils/pubspec.ts
1787
- import { existsSync as existsSync6, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "fs";
1788
- import { join as join8 } from "path";
2040
+ import { existsSync as existsSync8, readFileSync as readFileSync11, writeFileSync as writeFileSync5 } from "fs";
2041
+ import { join as join10 } from "path";
1789
2042
  import { parseDocument, YAMLMap } from "yaml";
1790
2043
  function mergePubspec(pubspecText, deps) {
1791
2044
  const doc = parseDocument(pubspecText);
@@ -1807,14 +2060,14 @@ function mergePubspec(pubspecText, deps) {
1807
2060
  return { text: doc.toString(), added, skipped };
1808
2061
  }
1809
2062
  function pubspecPath(cwd) {
1810
- return join8(cwd, "pubspec.yaml");
2063
+ return join10(cwd, "pubspec.yaml");
1811
2064
  }
1812
2065
  function pubspecExists(cwd) {
1813
- return existsSync6(pubspecPath(cwd));
2066
+ return existsSync8(pubspecPath(cwd));
1814
2067
  }
1815
2068
  function isPubPackageInstalled(packageName, cwd) {
1816
2069
  if (!pubspecExists(cwd)) return false;
1817
- const text = readFileSync9(pubspecPath(cwd), "utf-8");
2070
+ const text = readFileSync11(pubspecPath(cwd), "utf-8");
1818
2071
  const doc = parseDocument(text);
1819
2072
  const depsNode = doc.get("dependencies");
1820
2073
  if (!(depsNode instanceof YAMLMap)) return false;
@@ -1825,24 +2078,24 @@ function getUninstalledPubDeps(deps, cwd) {
1825
2078
  }
1826
2079
  function addPubDependencies(deps, cwd) {
1827
2080
  const path2 = pubspecPath(cwd);
1828
- if (!existsSync6(path2)) {
2081
+ if (!existsSync8(path2)) {
1829
2082
  throw new Error(
1830
2083
  `No pubspec.yaml found at ${path2}. Run this command from a Flutter project root.`
1831
2084
  );
1832
2085
  }
1833
- const text = readFileSync9(path2, "utf-8");
2086
+ const text = readFileSync11(path2, "utf-8");
1834
2087
  const result = mergePubspec(text, deps);
1835
2088
  if (result.added.length > 0) {
1836
- writeFileSync4(path2, result.text, "utf-8");
2089
+ writeFileSync5(path2, result.text, "utf-8");
1837
2090
  }
1838
2091
  return result;
1839
2092
  }
1840
2093
 
1841
2094
  // src/utils/flutter.ts
1842
2095
  import { execFileSync as execFileSync2 } from "child_process";
1843
- import { existsSync as existsSync7, readdirSync as readdirSync3, statSync as statSync4 } from "fs";
1844
- import { homedir } from "os";
1845
- import { join as join9 } from "path";
2096
+ import { existsSync as existsSync9, readdirSync as readdirSync3, statSync as statSync4 } from "fs";
2097
+ import { homedir as homedir2 } from "os";
2098
+ import { join as join11 } from "path";
1846
2099
  function isExecutable(path2) {
1847
2100
  try {
1848
2101
  const s = statSync4(path2);
@@ -1857,20 +2110,20 @@ function fromPath(env) {
1857
2110
  const bin = process.platform === "win32" ? "flutter.bat" : "flutter";
1858
2111
  for (const dir of pathVar.split(sep2)) {
1859
2112
  if (!dir) continue;
1860
- const candidate = join9(dir, bin);
2113
+ const candidate = join11(dir, bin);
1861
2114
  if (isExecutable(candidate)) return candidate;
1862
2115
  }
1863
2116
  return null;
1864
2117
  }
1865
2118
  function fromFvm(home) {
1866
- const fvmDefault = join9(home, "fvm", "default", "bin", "flutter");
2119
+ const fvmDefault = join11(home, "fvm", "default", "bin", "flutter");
1867
2120
  if (isExecutable(fvmDefault)) return fvmDefault;
1868
- const versionsDir = join9(home, "fvm", "versions");
1869
- if (!existsSync7(versionsDir)) return null;
2121
+ const versionsDir = join11(home, "fvm", "versions");
2122
+ if (!existsSync9(versionsDir)) return null;
1870
2123
  let best = null;
1871
2124
  try {
1872
2125
  for (const name of readdirSync3(versionsDir)) {
1873
- const candidate = join9(versionsDir, name, "bin", "flutter");
2126
+ const candidate = join11(versionsDir, name, "bin", "flutter");
1874
2127
  if (!isExecutable(candidate)) continue;
1875
2128
  if (!best || compareSemver(name, best.version) > 0) {
1876
2129
  best = { version: name, path: candidate };
@@ -1893,10 +2146,10 @@ function compareSemver(a, b) {
1893
2146
  }
1894
2147
  function findFlutterBin(options = {}) {
1895
2148
  const env = options.env ?? process.env;
1896
- const home = options.home ?? homedir();
2149
+ const home = options.home ?? homedir2();
1897
2150
  const envRoot = env.FLUTTER_ROOT;
1898
2151
  if (envRoot) {
1899
- const bin = join9(envRoot, "bin", "flutter");
2152
+ const bin = join11(envRoot, "bin", "flutter");
1900
2153
  if (isExecutable(bin)) return bin;
1901
2154
  }
1902
2155
  const fromPathBin = fromPath(env);
@@ -2539,8 +2792,8 @@ function infoCommand(name, cwd, options = {}) {
2539
2792
  }
2540
2793
 
2541
2794
  // src/commands/theme-apply.ts
2542
- import { readFileSync as readFileSync10, writeFileSync as writeFileSync5, mkdirSync as mkdirSync3 } from "fs";
2543
- import { resolve as resolve5, dirname as dirname5, join as join10 } from "path";
2795
+ import { readFileSync as readFileSync12, writeFileSync as writeFileSync6, mkdirSync as mkdirSync4 } from "fs";
2796
+ import { resolve as resolve5, dirname as dirname6, join as join12 } from "path";
2544
2797
  import { generateTheme, generateThemeData as generateThemeData3 } from "@loworbitstudio/visor-theme-engine";
2545
2798
  import {
2546
2799
  nextjsAdapter as nextjsAdapter2,
@@ -2573,7 +2826,7 @@ function themeApplyCommand(file, cwd, options) {
2573
2826
  const filePath = resolve5(cwd, file);
2574
2827
  let yamlContent;
2575
2828
  try {
2576
- yamlContent = readFileSync10(filePath, "utf-8");
2829
+ yamlContent = readFileSync12(filePath, "utf-8");
2577
2830
  } catch {
2578
2831
  if (options.json) {
2579
2832
  console.log(
@@ -2655,12 +2908,12 @@ function themeApplyCommand(file, cwd, options) {
2655
2908
  const outputPath = resolve5(cwd, outputTarget);
2656
2909
  if (fileMap) {
2657
2910
  try {
2658
- mkdirSync3(outputPath, { recursive: true });
2911
+ mkdirSync4(outputPath, { recursive: true });
2659
2912
  let totalBytes = 0;
2660
2913
  for (const [relPath, content] of Object.entries(fileMap.files)) {
2661
- const filePath2 = join10(outputPath, relPath);
2662
- mkdirSync3(dirname5(filePath2), { recursive: true });
2663
- writeFileSync5(filePath2, content, "utf-8");
2914
+ const filePath2 = join12(outputPath, relPath);
2915
+ mkdirSync4(dirname6(filePath2), { recursive: true });
2916
+ writeFileSync6(filePath2, content, "utf-8");
2664
2917
  totalBytes += content.length;
2665
2918
  }
2666
2919
  if (options.json) {
@@ -2697,10 +2950,10 @@ function themeApplyCommand(file, cwd, options) {
2697
2950
  if (css === null) {
2698
2951
  process.exit(1);
2699
2952
  }
2700
- const outputDir = dirname5(outputPath);
2953
+ const outputDir = dirname6(outputPath);
2701
2954
  try {
2702
- mkdirSync3(outputDir, { recursive: true });
2703
- writeFileSync5(outputPath, css, "utf-8");
2955
+ mkdirSync4(outputDir, { recursive: true });
2956
+ writeFileSync6(outputPath, css, "utf-8");
2704
2957
  } catch {
2705
2958
  if (options.json) {
2706
2959
  console.log(
@@ -2741,7 +2994,7 @@ function formatSize(bytes) {
2741
2994
  }
2742
2995
 
2743
2996
  // src/commands/theme-export.ts
2744
- import { readFileSync as readFileSync11 } from "fs";
2997
+ import { readFileSync as readFileSync13 } from "fs";
2745
2998
  import { resolve as resolve6 } from "path";
2746
2999
  import {
2747
3000
  parseConfig,
@@ -2753,7 +3006,7 @@ function themeExportCommand(file, cwd, options) {
2753
3006
  const filePath = resolve6(cwd, file ?? ".visor.yaml");
2754
3007
  let yamlContent;
2755
3008
  try {
2756
- yamlContent = readFileSync11(filePath, "utf-8");
3009
+ yamlContent = readFileSync13(filePath, "utf-8");
2757
3010
  } catch {
2758
3011
  if (options.json) {
2759
3012
  console.log(
@@ -2805,7 +3058,7 @@ function themeExportCommand(file, cwd, options) {
2805
3058
  }
2806
3059
 
2807
3060
  // src/commands/theme-validate.ts
2808
- import { readFileSync as readFileSync12 } from "fs";
3061
+ import { readFileSync as readFileSync14 } from "fs";
2809
3062
  import { resolve as resolve7 } from "path";
2810
3063
  import { parse as parseYaml2 } from "yaml";
2811
3064
  import { validate } from "@loworbitstudio/visor-theme-engine";
@@ -2814,7 +3067,7 @@ function themeValidateCommand(file, cwd, options) {
2814
3067
  const filePath = resolve7(cwd, file);
2815
3068
  let fileContent;
2816
3069
  try {
2817
- fileContent = readFileSync12(filePath, "utf-8");
3070
+ fileContent = readFileSync14(filePath, "utf-8");
2818
3071
  } catch {
2819
3072
  if (options.json) {
2820
3073
  console.log(
@@ -2903,7 +3156,7 @@ function printIssue(issue) {
2903
3156
 
2904
3157
  // src/commands/theme-verify.ts
2905
3158
  import { spawnSync as _spawnSync } from "child_process";
2906
- import { existsSync as existsSync8 } from "fs";
3159
+ import { existsSync as existsSync10 } from "fs";
2907
3160
  import { resolve as resolve8 } from "path";
2908
3161
  function themeVerifyCommand(dir, cwd, options, _spawnFn = _spawnSync) {
2909
3162
  const target = options.target ?? "flutter";
@@ -2927,7 +3180,7 @@ function themeVerifyCommand(dir, cwd, options, _spawnFn = _spawnSync) {
2927
3180
  process.exit(1);
2928
3181
  }
2929
3182
  const dirPath = resolve8(cwd, dir);
2930
- if (!existsSync8(dirPath)) {
3183
+ if (!existsSync10(dirPath)) {
2931
3184
  if (options.json) {
2932
3185
  console.log(
2933
3186
  JSON.stringify({
@@ -3014,8 +3267,8 @@ function themeVerifyCommand(dir, cwd, options, _spawnFn = _spawnSync) {
3014
3267
  }
3015
3268
 
3016
3269
  // src/commands/theme-extract.ts
3017
- import { readFileSync as readFileSync13, writeFileSync as writeFileSync6, existsSync as existsSync9, readdirSync as readdirSync4, statSync as statSync5 } from "fs";
3018
- import { resolve as resolve9, join as join11, basename as basename2, extname as extname3, relative } from "path";
3270
+ import { readFileSync as readFileSync15, writeFileSync as writeFileSync7, existsSync as existsSync11, readdirSync as readdirSync4, statSync as statSync5 } from "fs";
3271
+ import { resolve as resolve9, join as join13, basename as basename3, extname as extname3, relative } from "path";
3019
3272
  import { stringify as stringifyYaml } from "yaml";
3020
3273
  import {
3021
3274
  extractFromCSS,
@@ -3045,7 +3298,7 @@ var CSS_DIRS = [
3045
3298
  ];
3046
3299
  function themeExtractCommand(cwd, options) {
3047
3300
  const targetDir = resolve9(cwd, options.from ?? ".");
3048
- if (!existsSync9(targetDir)) {
3301
+ if (!existsSync11(targetDir)) {
3049
3302
  if (options.json) {
3050
3303
  console.log(JSON.stringify({ success: false, error: `Directory not found: ${targetDir}` }));
3051
3304
  } else {
@@ -3099,16 +3352,16 @@ function collectCSSFiles(targetDir) {
3099
3352
  const files = [];
3100
3353
  const seen = /* @__PURE__ */ new Set();
3101
3354
  for (const pattern2 of CSS_FILE_PATTERNS) {
3102
- const rootPath = join11(targetDir, pattern2);
3355
+ const rootPath = join13(targetDir, pattern2);
3103
3356
  addFileIfExists(rootPath, files, seen);
3104
3357
  for (const dir of CSS_DIRS) {
3105
- const dirPath = join11(targetDir, dir, pattern2);
3358
+ const dirPath = join13(targetDir, dir, pattern2);
3106
3359
  addFileIfExists(dirPath, files, seen);
3107
3360
  }
3108
3361
  }
3109
3362
  for (const dir of CSS_DIRS) {
3110
- const dirPath = join11(targetDir, dir);
3111
- if (existsSync9(dirPath) && statSync5(dirPath).isDirectory()) {
3363
+ const dirPath = join13(targetDir, dir);
3364
+ if (existsSync11(dirPath) && statSync5(dirPath).isDirectory()) {
3112
3365
  scanDirForCSS(dirPath, files, seen, 2);
3113
3366
  }
3114
3367
  }
@@ -3118,9 +3371,9 @@ function collectCSSFiles(targetDir) {
3118
3371
  function addFileIfExists(filePath, files, seen) {
3119
3372
  const resolved = resolve9(filePath);
3120
3373
  if (seen.has(resolved)) return;
3121
- if (!existsSync9(resolved)) return;
3374
+ if (!existsSync11(resolved)) return;
3122
3375
  try {
3123
- const content = readFileSync13(resolved, "utf-8");
3376
+ const content = readFileSync15(resolved, "utf-8");
3124
3377
  if (content.includes("--")) {
3125
3378
  files.push({ path: resolved, content });
3126
3379
  seen.add(resolved);
@@ -3129,7 +3382,7 @@ function addFileIfExists(filePath, files, seen) {
3129
3382
  }
3130
3383
  }
3131
3384
  function scanDirForCSS(dir, files, seen, maxDepth) {
3132
- if (!existsSync9(dir)) return;
3385
+ if (!existsSync11(dir)) return;
3133
3386
  const SKIP_DIRS = /* @__PURE__ */ new Set([
3134
3387
  "node_modules",
3135
3388
  ".next",
@@ -3148,10 +3401,10 @@ function scanDirForCSS(dir, files, seen, maxDepth) {
3148
3401
  if (entry.isDirectory()) {
3149
3402
  if (SKIP_DIRS.has(entry.name)) continue;
3150
3403
  if (maxDepth > 0) {
3151
- scanDirForCSS(join11(dir, entry.name), files, seen, maxDepth - 1);
3404
+ scanDirForCSS(join13(dir, entry.name), files, seen, maxDepth - 1);
3152
3405
  }
3153
3406
  } else if (entry.isFile() && extname3(entry.name) === ".css") {
3154
- addFileIfExists(join11(dir, entry.name), files, seen);
3407
+ addFileIfExists(join13(dir, entry.name), files, seen);
3155
3408
  }
3156
3409
  }
3157
3410
  } catch {
@@ -3233,10 +3486,10 @@ function extractVarName(varExpr) {
3233
3486
  function parseNextFontFromLayouts(targetDir) {
3234
3487
  const fontMap = /* @__PURE__ */ new Map();
3235
3488
  for (const relPath of LAYOUT_FILE_PATHS) {
3236
- const fullPath = join11(targetDir, relPath);
3237
- if (!existsSync9(fullPath)) continue;
3489
+ const fullPath = join13(targetDir, relPath);
3490
+ if (!existsSync11(fullPath)) continue;
3238
3491
  try {
3239
- const content = readFileSync13(fullPath, "utf-8");
3492
+ const content = readFileSync15(fullPath, "utf-8");
3240
3493
  parseNextFontDeclarations(content, fontMap);
3241
3494
  } catch {
3242
3495
  }
@@ -3283,7 +3536,7 @@ function parseNextFontDeclarations(content, fontMap) {
3283
3536
  const srcMatch = block.match(/src\s*:\s*["']([^"']+)["']/);
3284
3537
  if (srcMatch) {
3285
3538
  const srcPath = srcMatch[1];
3286
- const fileName = basename2(srcPath, extname3(srcPath));
3539
+ const fileName = basename3(srcPath, extname3(srcPath));
3287
3540
  const fontBaseName = fileName.replace(/[-_](Variable|Regular|Bold|Light|Medium|SemiBold|ExtraBold|Thin|Black|Italic).*$/i, "").replace(/[-_]/g, " ").trim();
3288
3541
  if (fontBaseName) {
3289
3542
  fontMap.set(varName, fontBaseName);
@@ -3321,10 +3574,10 @@ var MONO_FONT_NAMES = /* @__PURE__ */ new Set([
3321
3574
  "IBM Plex Mono"
3322
3575
  ]);
3323
3576
  function extractFontHints(targetDir) {
3324
- const pkgPath = join11(targetDir, "package.json");
3325
- if (!existsSync9(pkgPath)) return void 0;
3577
+ const pkgPath = join13(targetDir, "package.json");
3578
+ if (!existsSync11(pkgPath)) return void 0;
3326
3579
  try {
3327
- const pkg2 = JSON.parse(readFileSync13(pkgPath, "utf-8"));
3580
+ const pkg2 = JSON.parse(readFileSync15(pkgPath, "utf-8"));
3328
3581
  const allDeps = { ...pkg2.dependencies, ...pkg2.devDependencies };
3329
3582
  const fonts2 = [];
3330
3583
  for (const [dep, _] of Object.entries(allDeps)) {
@@ -3360,10 +3613,10 @@ function extractFontHints(targetDir) {
3360
3613
  }
3361
3614
  }
3362
3615
  function inferThemeName(targetDir) {
3363
- const pkgPath = join11(targetDir, "package.json");
3364
- if (existsSync9(pkgPath)) {
3616
+ const pkgPath = join13(targetDir, "package.json");
3617
+ if (existsSync11(pkgPath)) {
3365
3618
  try {
3366
- const pkg2 = JSON.parse(readFileSync13(pkgPath, "utf-8"));
3619
+ const pkg2 = JSON.parse(readFileSync15(pkgPath, "utf-8"));
3367
3620
  if (pkg2.name) {
3368
3621
  const name = pkg2.name.replace(/^@[\w-]+\//, "");
3369
3622
  return `${name}-theme`;
@@ -3371,7 +3624,7 @@ function inferThemeName(targetDir) {
3371
3624
  } catch {
3372
3625
  }
3373
3626
  }
3374
- return `${basename2(targetDir)}-theme`;
3627
+ return `${basename3(targetDir)}-theme`;
3375
3628
  }
3376
3629
  function confidenceComment(confidence) {
3377
3630
  return `# confidence: ${confidence}`;
@@ -3428,7 +3681,7 @@ function outputYAML(result, outputPath, cwd, validationResult) {
3428
3681
  }
3429
3682
  logger.blank();
3430
3683
  }
3431
- writeFileSync6(outFile, yamlStr, "utf-8");
3684
+ writeFileSync7(outFile, yamlStr, "utf-8");
3432
3685
  logger.success(`Theme written to ${relative(cwd, outFile)}`);
3433
3686
  if (validationResult) {
3434
3687
  logger.blank();
@@ -3486,14 +3739,14 @@ function buildAnnotatedYAML(result) {
3486
3739
  }
3487
3740
 
3488
3741
  // src/commands/theme-register.ts
3489
- import { readFileSync as readFileSync15, writeFileSync as writeFileSync7, mkdirSync as mkdirSync4, existsSync as existsSync11 } from "fs";
3490
- import { resolve as resolve11, join as join13 } from "path";
3742
+ import { readFileSync as readFileSync17, writeFileSync as writeFileSync8, mkdirSync as mkdirSync5, existsSync as existsSync13 } from "fs";
3743
+ import { resolve as resolve11, join as join15 } from "path";
3491
3744
  import { generateThemeData as generateThemeData4 } from "@loworbitstudio/visor-theme-engine";
3492
3745
  import { docsAdapter as docsAdapter2 } from "@loworbitstudio/visor-theme-engine/adapters";
3493
3746
 
3494
3747
  // src/utils/theme-helpers.ts
3495
- import { existsSync as existsSync10, lstatSync, readdirSync as readdirSync5, readFileSync as readFileSync14, readlinkSync, realpathSync, statSync as statSync6 } from "fs";
3496
- import { resolve as resolve10, dirname as dirname6, join as join12 } from "path";
3748
+ import { existsSync as existsSync12, lstatSync, readdirSync as readdirSync5, readFileSync as readFileSync16, readlinkSync, realpathSync, statSync as statSync6 } from "fs";
3749
+ import { resolve as resolve10, dirname as dirname7, join as join14 } from "path";
3497
3750
  import { execFileSync as execFileSync3 } from "child_process";
3498
3751
  function toSlug(name) {
3499
3752
  return name.toLowerCase().replace(/\s+/g, "-");
@@ -3504,10 +3757,10 @@ function toLabel(name) {
3504
3757
  function findRepoRoot(startDir) {
3505
3758
  let current = resolve10(startDir);
3506
3759
  while (true) {
3507
- if (existsSync10(join12(current, "packages", "docs"))) {
3760
+ if (existsSync12(join14(current, "packages", "docs"))) {
3508
3761
  return current;
3509
3762
  }
3510
- const parent = dirname6(current);
3763
+ const parent = dirname7(current);
3511
3764
  if (parent === current) break;
3512
3765
  current = parent;
3513
3766
  }
@@ -3522,8 +3775,8 @@ function findMainRepoRoot(startDir) {
3522
3775
  }).trim();
3523
3776
  if (commonDir) {
3524
3777
  const absoluteCommonDir = resolve10(startDir, commonDir);
3525
- const candidate = dirname6(absoluteCommonDir);
3526
- if (existsSync10(join12(candidate, "packages", "docs"))) {
3778
+ const candidate = dirname7(absoluteCommonDir);
3779
+ if (existsSync12(join14(candidate, "packages", "docs"))) {
3527
3780
  return candidate;
3528
3781
  }
3529
3782
  }
@@ -3542,10 +3795,10 @@ var BrokenSymlinkError = class extends Error {
3542
3795
  code = "BROKEN_SYMLINK";
3543
3796
  };
3544
3797
  function assertNoBrokenSymlinks(dir) {
3545
- if (!existsSync10(dir)) return;
3798
+ if (!existsSync12(dir)) return;
3546
3799
  const entries = readdirSync5(dir, { withFileTypes: true });
3547
3800
  for (const entry of entries) {
3548
- const entryPath = join12(dir, entry.name);
3801
+ const entryPath = join14(dir, entry.name);
3549
3802
  let lst;
3550
3803
  try {
3551
3804
  lst = lstatSync(entryPath);
@@ -3563,27 +3816,27 @@ function assertNoBrokenSymlinks(dir) {
3563
3816
  }
3564
3817
  }
3565
3818
  function scanParentForPrivateThemes(parentDir) {
3566
- if (!existsSync10(parentDir)) return [];
3819
+ if (!existsSync12(parentDir)) return [];
3567
3820
  const entries = readdirSync5(parentDir, { withFileTypes: true });
3568
3821
  const matches = [];
3569
3822
  for (const entry of entries) {
3570
3823
  if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
3571
- const candidate = join12(parentDir, entry.name, "visor-themes-private", "themes");
3572
- if (existsSync10(candidate)) {
3824
+ const candidate = join14(parentDir, entry.name, "visor-themes-private", "themes");
3825
+ if (existsSync12(candidate)) {
3573
3826
  matches.push(candidate);
3574
3827
  }
3575
3828
  }
3576
3829
  return matches.sort();
3577
3830
  }
3578
3831
  function scanNestedThemeDir(dir) {
3579
- if (!existsSync10(dir)) return [];
3832
+ if (!existsSync12(dir)) return [];
3580
3833
  assertNoBrokenSymlinks(dir);
3581
3834
  const entries = readdirSync5(dir, { withFileTypes: true });
3582
3835
  const out = [];
3583
3836
  for (const entry of entries) {
3584
3837
  if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
3585
- const themeFile = join12(dir, entry.name, "theme.visor.yaml");
3586
- if (existsSync10(themeFile)) {
3838
+ const themeFile = join14(dir, entry.name, "theme.visor.yaml");
3839
+ if (existsSync12(themeFile)) {
3587
3840
  out.push({ filePath: themeFile, slug: entry.name });
3588
3841
  }
3589
3842
  }
@@ -3592,10 +3845,10 @@ function scanNestedThemeDir(dir) {
3592
3845
  function detectVisorWorkspace(cwd) {
3593
3846
  let current = resolve10(cwd);
3594
3847
  while (true) {
3595
- const pkgPath = join12(current, "package.json");
3596
- if (existsSync10(pkgPath)) {
3848
+ const pkgPath = join14(current, "package.json");
3849
+ if (existsSync12(pkgPath)) {
3597
3850
  try {
3598
- const pkg2 = JSON.parse(readFileSync14(pkgPath, "utf-8"));
3851
+ const pkg2 = JSON.parse(readFileSync16(pkgPath, "utf-8"));
3599
3852
  const workspaces = pkg2.workspaces ?? [];
3600
3853
  const hasCli = workspaces.some((w) => w.includes("packages/cli"));
3601
3854
  const hasEngine = workspaces.some((w) => w.includes("packages/theme-engine"));
@@ -3605,7 +3858,7 @@ function detectVisorWorkspace(cwd) {
3605
3858
  } catch {
3606
3859
  }
3607
3860
  }
3608
- const parent = dirname6(current);
3861
+ const parent = dirname7(current);
3609
3862
  if (parent === current) break;
3610
3863
  current = parent;
3611
3864
  }
@@ -3613,7 +3866,7 @@ function detectVisorWorkspace(cwd) {
3613
3866
  }
3614
3867
  function isLocalVisorBinary(workspaceRoot, scriptPath) {
3615
3868
  if (!scriptPath) return false;
3616
- const expectedPrefix = join12(workspaceRoot, "packages", "cli", "dist");
3869
+ const expectedPrefix = join14(workspaceRoot, "packages", "cli", "dist");
3617
3870
  let resolvedScript;
3618
3871
  let resolvedPrefix;
3619
3872
  try {
@@ -3717,7 +3970,7 @@ function themeRegisterCommand(file, cwd, options) {
3717
3970
  const filePath = resolve11(cwd, file);
3718
3971
  let yamlContent;
3719
3972
  try {
3720
- yamlContent = readFileSync15(filePath, "utf-8");
3973
+ yamlContent = readFileSync17(filePath, "utf-8");
3721
3974
  } catch {
3722
3975
  if (options.json) {
3723
3976
  console.log(JSON.stringify({ success: false, error: `Could not read file: ${filePath}` }));
@@ -3760,11 +4013,11 @@ function themeRegisterCommand(file, cwd, options) {
3760
4013
  process.exit(1);
3761
4014
  return;
3762
4015
  }
3763
- const docsAppDir = join13(repoRoot, "packages", "docs", "app");
3764
- const cssFilePath = join13(docsAppDir, `${slug2}-theme.css`);
3765
- const globalsPath = join13(docsAppDir, "globals.css");
3766
- const themeConfigPath = join13(repoRoot, "packages", "docs", "lib", "theme-config.ts");
3767
- if (!existsSync11(docsAppDir)) {
4016
+ const docsAppDir = join15(repoRoot, "packages", "docs", "app");
4017
+ const cssFilePath = join15(docsAppDir, `${slug2}-theme.css`);
4018
+ const globalsPath = join15(docsAppDir, "globals.css");
4019
+ const themeConfigPath = join15(repoRoot, "packages", "docs", "lib", "theme-config.ts");
4020
+ if (!existsSync13(docsAppDir)) {
3768
4021
  const msg = `Docs app directory not found: ${docsAppDir}`;
3769
4022
  if (options.json) {
3770
4023
  console.log(JSON.stringify({ success: false, error: msg }));
@@ -3777,8 +4030,8 @@ function themeRegisterCommand(file, cwd, options) {
3777
4030
  let globalsContent = "";
3778
4031
  let themeConfigContent = "";
3779
4032
  try {
3780
- globalsContent = readFileSync15(globalsPath, "utf-8");
3781
- themeConfigContent = readFileSync15(themeConfigPath, "utf-8");
4033
+ globalsContent = readFileSync17(globalsPath, "utf-8");
4034
+ themeConfigContent = readFileSync17(themeConfigPath, "utf-8");
3782
4035
  } catch (err) {
3783
4036
  const msg = err instanceof Error ? err.message : "Could not read docs files";
3784
4037
  if (options.json) {
@@ -3789,8 +4042,8 @@ function themeRegisterCommand(file, cwd, options) {
3789
4042
  process.exit(1);
3790
4043
  return;
3791
4044
  }
3792
- const cssExists = existsSync11(cssFilePath);
3793
- const cssChanged = !cssExists || readFileSync15(cssFilePath, "utf-8") !== css;
4045
+ const cssExists = existsSync13(cssFilePath);
4046
+ const cssChanged = !cssExists || readFileSync17(cssFilePath, "utf-8") !== css;
3794
4047
  const { updated: newGlobals, changed: globalsChanged } = insertGlobalsImport(globalsContent, slug2);
3795
4048
  const { updated: newThemeConfig, changed: themeConfigChanged, error: configError } = insertThemeConfig(
3796
4049
  themeConfigContent,
@@ -3833,14 +4086,14 @@ function themeRegisterCommand(file, cwd, options) {
3833
4086
  }
3834
4087
  try {
3835
4088
  if (cssChanged) {
3836
- mkdirSync4(docsAppDir, { recursive: true });
3837
- writeFileSync7(cssFilePath, css, "utf-8");
4089
+ mkdirSync5(docsAppDir, { recursive: true });
4090
+ writeFileSync8(cssFilePath, css, "utf-8");
3838
4091
  }
3839
4092
  if (globalsChanged) {
3840
- writeFileSync7(globalsPath, newGlobals, "utf-8");
4093
+ writeFileSync8(globalsPath, newGlobals, "utf-8");
3841
4094
  }
3842
4095
  if (themeConfigChanged) {
3843
- writeFileSync7(themeConfigPath, newThemeConfig, "utf-8");
4096
+ writeFileSync8(themeConfigPath, newThemeConfig, "utf-8");
3844
4097
  }
3845
4098
  } catch (err) {
3846
4099
  const msg = err instanceof Error ? err.message : "Write failed";
@@ -3874,8 +4127,8 @@ function themeRegisterCommand(file, cwd, options) {
3874
4127
  }
3875
4128
 
3876
4129
  // src/commands/theme-unregister.ts
3877
- import { readFileSync as readFileSync16, writeFileSync as writeFileSync8, existsSync as existsSync12, unlinkSync } from "fs";
3878
- import { join as join14 } from "path";
4130
+ import { readFileSync as readFileSync18, writeFileSync as writeFileSync9, existsSync as existsSync14, unlinkSync } from "fs";
4131
+ import { join as join16 } from "path";
3879
4132
  function removeGlobalsImport(content, slug2) {
3880
4133
  const importLine = `@import './${slug2}-theme.css';`;
3881
4134
  if (!content.includes(importLine)) {
@@ -3907,11 +4160,11 @@ function themeUnregisterCommand(slug2, cwd, options) {
3907
4160
  process.exit(1);
3908
4161
  return;
3909
4162
  }
3910
- const docsAppDir = join14(repoRoot, "packages", "docs", "app");
3911
- const cssFilePath = join14(docsAppDir, `${slug2}-theme.css`);
3912
- const globalsPath = join14(docsAppDir, "globals.css");
3913
- const themeConfigPath = join14(repoRoot, "packages", "docs", "lib", "theme-config.ts");
3914
- if (!existsSync12(docsAppDir)) {
4163
+ const docsAppDir = join16(repoRoot, "packages", "docs", "app");
4164
+ const cssFilePath = join16(docsAppDir, `${slug2}-theme.css`);
4165
+ const globalsPath = join16(docsAppDir, "globals.css");
4166
+ const themeConfigPath = join16(repoRoot, "packages", "docs", "lib", "theme-config.ts");
4167
+ if (!existsSync14(docsAppDir)) {
3915
4168
  const msg = `Docs app directory not found: ${docsAppDir}`;
3916
4169
  if (options.json) {
3917
4170
  console.log(JSON.stringify({ success: false, error: msg }));
@@ -3924,8 +4177,8 @@ function themeUnregisterCommand(slug2, cwd, options) {
3924
4177
  let globalsContent = "";
3925
4178
  let themeConfigContent = "";
3926
4179
  try {
3927
- globalsContent = readFileSync16(globalsPath, "utf-8");
3928
- themeConfigContent = readFileSync16(themeConfigPath, "utf-8");
4180
+ globalsContent = readFileSync18(globalsPath, "utf-8");
4181
+ themeConfigContent = readFileSync18(themeConfigPath, "utf-8");
3929
4182
  } catch (err) {
3930
4183
  const msg = err instanceof Error ? err.message : "Could not read docs files";
3931
4184
  if (options.json) {
@@ -3936,7 +4189,7 @@ function themeUnregisterCommand(slug2, cwd, options) {
3936
4189
  process.exit(1);
3937
4190
  return;
3938
4191
  }
3939
- const cssExists = existsSync12(cssFilePath);
4192
+ const cssExists = existsSync14(cssFilePath);
3940
4193
  const { updated: newGlobals, changed: globalsChanged } = removeGlobalsImport(globalsContent, slug2);
3941
4194
  const { updated: newThemeConfig, changed: themeConfigChanged } = removeThemeConfigEntry(themeConfigContent, slug2);
3942
4195
  if (!cssExists && !globalsChanged && !themeConfigChanged) {
@@ -3949,8 +4202,8 @@ function themeUnregisterCommand(slug2, cwd, options) {
3949
4202
  }
3950
4203
  try {
3951
4204
  if (cssExists) unlinkSync(cssFilePath);
3952
- if (globalsChanged) writeFileSync8(globalsPath, newGlobals, "utf-8");
3953
- if (themeConfigChanged) writeFileSync8(themeConfigPath, newThemeConfig, "utf-8");
4205
+ if (globalsChanged) writeFileSync9(globalsPath, newGlobals, "utf-8");
4206
+ if (themeConfigChanged) writeFileSync9(themeConfigPath, newThemeConfig, "utf-8");
3954
4207
  } catch (err) {
3955
4208
  const msg = err instanceof Error ? err.message : "Write failed";
3956
4209
  if (options.json) {
@@ -3977,15 +4230,15 @@ function themeUnregisterCommand(slug2, cwd, options) {
3977
4230
 
3978
4231
  // src/commands/theme-sync.ts
3979
4232
  import {
3980
- readFileSync as readFileSync17,
3981
- writeFileSync as writeFileSync9,
3982
- mkdirSync as mkdirSync5,
3983
- existsSync as existsSync13,
4233
+ readFileSync as readFileSync19,
4234
+ writeFileSync as writeFileSync10,
4235
+ mkdirSync as mkdirSync6,
4236
+ existsSync as existsSync15,
3984
4237
  readdirSync as readdirSync6,
3985
4238
  unlinkSync as unlinkSync2,
3986
4239
  copyFileSync
3987
4240
  } from "fs";
3988
- import { join as join15, basename as basename3, resolve as resolve12, sep, relative as relative2 } from "path";
4241
+ import { join as join17, basename as basename4, resolve as resolve12, sep, relative as relative2 } from "path";
3989
4242
  import { parse as parseYaml3 } from "yaml";
3990
4243
  import { generateThemeData as generateThemeData5, validateFontCoverage, formatFontCoverageError } from "@loworbitstudio/visor-theme-engine";
3991
4244
  import { docsAdapter as docsAdapter3 } from "@loworbitstudio/visor-theme-engine/adapters";
@@ -3999,9 +4252,9 @@ var CUSTOM_OVERLAY_CSS_PATH = "packages/docs/app/custom-themes.generated.css";
3999
4252
  var CUSTOM_OVERLAY_TS_PATH = "packages/docs/lib/theme-config.custom.generated.ts";
4000
4253
  var CUSTOM_OVERLAY_IMPORT_LINE = "@import './custom-themes.generated.css';";
4001
4254
  function scanThemeDir(dir) {
4002
- if (!existsSync13(dir)) return [];
4255
+ if (!existsSync15(dir)) return [];
4003
4256
  assertNoBrokenSymlinks(dir);
4004
- return readdirSync6(dir).filter((f) => f.endsWith(".visor.yaml")).map((f) => join15(dir, f));
4257
+ return readdirSync6(dir).filter((f) => f.endsWith(".visor.yaml")).map((f) => join17(dir, f));
4005
4258
  }
4006
4259
  function resolveCustomSources(repoRoot, mainRepoRoot, warn) {
4007
4260
  const merged = /* @__PURE__ */ new Map();
@@ -4026,19 +4279,19 @@ function resolveCustomSources(repoRoot, mainRepoRoot, warn) {
4026
4279
  const envPath = process.env[PRIVATE_THEMES_ENV_VAR];
4027
4280
  if (envPath && envPath.trim() !== "") {
4028
4281
  const resolved = resolve12(envPath);
4029
- if (!existsSync13(resolved)) {
4282
+ if (!existsSync15(resolved)) {
4030
4283
  throw new Error(
4031
4284
  `${PRIVATE_THEMES_ENV_VAR} is set to "${envPath}" but the path does not exist. Expected a directory containing {slug}/theme.visor.yaml entries.`
4032
4285
  );
4033
4286
  }
4034
4287
  addNested(resolved, "env");
4035
4288
  }
4036
- const siblingPath = join15(mainRepoRoot, "..", "visor-themes-private", "themes");
4037
- const siblingExists = existsSync13(siblingPath);
4289
+ const siblingPath = join17(mainRepoRoot, "..", "visor-themes-private", "themes");
4290
+ const siblingExists = existsSync15(siblingPath);
4038
4291
  if (siblingExists) {
4039
4292
  addNested(siblingPath, "sibling");
4040
4293
  }
4041
- const parentDir = join15(mainRepoRoot, "..");
4294
+ const parentDir = join17(mainRepoRoot, "..");
4042
4295
  const parentGlobMatches = scanParentForPrivateThemes(parentDir).filter(
4043
4296
  (path2) => !path2.startsWith(mainRepoRoot + sep)
4044
4297
  );
@@ -4059,10 +4312,10 @@ function resolveCustomSources(repoRoot, mainRepoRoot, warn) {
4059
4312
  }
4060
4313
  }
4061
4314
  }
4062
- const legacyDir = join15(repoRoot, "custom-themes");
4315
+ const legacyDir = join17(repoRoot, "custom-themes");
4063
4316
  const legacyFiles = scanThemeDir(legacyDir);
4064
4317
  for (const legacyFile of legacyFiles) {
4065
- const slug2 = basename3(legacyFile).replace(/\.visor\.yaml$/, "");
4318
+ const slug2 = basename4(legacyFile).replace(/\.visor\.yaml$/, "");
4066
4319
  const existing = merged.get(slug2);
4067
4320
  if (existing) {
4068
4321
  warn(
@@ -4116,14 +4369,14 @@ function reportBrokenSymlink(err, options) {
4116
4369
  }
4117
4370
  }
4118
4371
  function buildEmptySourcesMessage(mainRepoRoot) {
4119
- const expectedSibling = join15(mainRepoRoot, "..", "visor-themes-private");
4372
+ const expectedSibling = join17(mainRepoRoot, "..", "visor-themes-private");
4120
4373
  return [
4121
4374
  "No theme sources discovered. Cannot proceed \u2014 refusing to wipe generated CSS.",
4122
4375
  "",
4123
4376
  "Resolution order checked:",
4124
4377
  ` 1. Env var ${PRIVATE_THEMES_ENV_VAR} (unset or empty)`,
4125
4378
  ` 2. Sibling checkout at ${expectedSibling}/themes/ (not found)`,
4126
- ` 3. One-level-deeper at ${join15(mainRepoRoot, "..")}/<parent>/visor-themes-private/themes/ (not found)`,
4379
+ ` 3. One-level-deeper at ${join17(mainRepoRoot, "..")}/<parent>/visor-themes-private/themes/ (not found)`,
4127
4380
  " 4. Legacy custom-themes/ (no .visor.yaml files)",
4128
4381
  "",
4129
4382
  "To fix, clone the private themes repo as a sibling:",
@@ -4328,14 +4581,14 @@ function themeSyncCommand(cwd, options) {
4328
4581
  return;
4329
4582
  }
4330
4583
  const mainRepoRoot = findMainRepoRoot(cwd) ?? repoRoot;
4331
- const themesDir = join15(repoRoot, "themes");
4332
- const docsAppDir = join15(repoRoot, "packages", "docs", "app");
4333
- const docsLibDir = join15(repoRoot, "packages", "docs", "lib");
4334
- const docsPublicThemesDir = join15(repoRoot, "packages", "docs", "public", "themes");
4335
- const themeConfigPath = join15(repoRoot, "packages", "docs", "lib", "theme-config.ts");
4336
- const globalsPath = join15(docsAppDir, "globals.css");
4337
- const customOverlayCssPath = join15(repoRoot, CUSTOM_OVERLAY_CSS_PATH);
4338
- const customOverlayTsPath = join15(repoRoot, CUSTOM_OVERLAY_TS_PATH);
4584
+ const themesDir = join17(repoRoot, "themes");
4585
+ const docsAppDir = join17(repoRoot, "packages", "docs", "app");
4586
+ const docsLibDir = join17(repoRoot, "packages", "docs", "lib");
4587
+ const docsPublicThemesDir = join17(repoRoot, "packages", "docs", "public", "themes");
4588
+ const themeConfigPath = join17(repoRoot, "packages", "docs", "lib", "theme-config.ts");
4589
+ const globalsPath = join17(docsAppDir, "globals.css");
4590
+ const customOverlayCssPath = join17(repoRoot, CUSTOM_OVERLAY_CSS_PATH);
4591
+ const customOverlayTsPath = join17(repoRoot, CUSTOM_OVERLAY_TS_PATH);
4339
4592
  let stockFiles;
4340
4593
  try {
4341
4594
  stockFiles = scanThemeDir(themesDir);
@@ -4392,7 +4645,7 @@ function themeSyncCommand(cwd, options) {
4392
4645
  const processFile = (filePath, isCustom, slugOverride) => {
4393
4646
  let yamlContent;
4394
4647
  try {
4395
- yamlContent = readFileSync17(filePath, "utf-8");
4648
+ yamlContent = readFileSync19(filePath, "utf-8");
4396
4649
  } catch {
4397
4650
  failures.push({ filePath, error: `Could not read: ${filePath}` });
4398
4651
  return;
@@ -4403,7 +4656,7 @@ function themeSyncCommand(cwd, options) {
4403
4656
  } catch (err) {
4404
4657
  failures.push({
4405
4658
  filePath,
4406
- error: `Failed to parse ${basename3(filePath)}: ${err instanceof Error ? err.message : "Unknown error"}`
4659
+ error: `Failed to parse ${basename4(filePath)}: ${err instanceof Error ? err.message : "Unknown error"}`
4407
4660
  });
4408
4661
  return;
4409
4662
  }
@@ -4417,12 +4670,12 @@ function themeSyncCommand(cwd, options) {
4417
4670
  for (const e of coverage.errors) {
4418
4671
  failures.push({
4419
4672
  filePath,
4420
- error: formatFontCoverageError(basename3(filePath), e.declaredAt, e.family)
4673
+ error: formatFontCoverageError(basename4(filePath), e.declaredAt, e.family)
4421
4674
  });
4422
4675
  }
4423
4676
  return;
4424
4677
  }
4425
- const yamlFilename = slugOverride ?? basename3(filePath).replace(/\.visor\.yaml$/, "");
4678
+ const yamlFilename = slugOverride ?? basename4(filePath).replace(/\.visor\.yaml$/, "");
4426
4679
  manifest.push({ slug: slug2, label, group, defaultMode, css, yamlFilename, isCustom });
4427
4680
  };
4428
4681
  for (const f of stockFiles) processFile(f, false);
@@ -4442,8 +4695,8 @@ function themeSyncCommand(cwd, options) {
4442
4695
  let globalsContent;
4443
4696
  let themeConfigContent;
4444
4697
  try {
4445
- globalsContent = readFileSync17(globalsPath, "utf-8");
4446
- themeConfigContent = readFileSync17(themeConfigPath, "utf-8");
4698
+ globalsContent = readFileSync19(globalsPath, "utf-8");
4699
+ themeConfigContent = readFileSync19(themeConfigPath, "utf-8");
4447
4700
  } catch (err) {
4448
4701
  const msg = err instanceof Error ? err.message : "Could not read docs files";
4449
4702
  if (options.json) {
@@ -4458,12 +4711,12 @@ function themeSyncCommand(cwd, options) {
4458
4711
  const newThemeConfig = updateStockThemeConfigBlock(themeConfigContent, stockManifest);
4459
4712
  const newCustomOverlayCss = generateCustomOverlayCss(customManifest);
4460
4713
  const newCustomOverlayTs = generateCustomOverlayTs(customManifest);
4461
- const existingCssFiles = existsSync13(docsAppDir) ? readdirSync6(docsAppDir).filter(
4714
+ const existingCssFiles = existsSync15(docsAppDir) ? readdirSync6(docsAppDir).filter(
4462
4715
  (f) => f.endsWith("-theme.css") && f !== "custom-themes.generated.css"
4463
4716
  ) : [];
4464
4717
  const newCssSet = new Set(allSlugs.map((s) => `${s}-theme.css`));
4465
4718
  const staleCssFiles = existingCssFiles.filter((f) => !newCssSet.has(f));
4466
- const existingPublicYamls = existsSync13(docsPublicThemesDir) ? readdirSync6(docsPublicThemesDir).filter((f) => f.endsWith(".visor.yaml")) : [];
4719
+ const existingPublicYamls = existsSync15(docsPublicThemesDir) ? readdirSync6(docsPublicThemesDir).filter((f) => f.endsWith(".visor.yaml")) : [];
4467
4720
  const newPublicYamlSet = new Set(manifest.map((e) => `${e.yamlFilename}.visor.yaml`));
4468
4721
  const stalePublicYamls = existingPublicYamls.filter((f) => !newPublicYamlSet.has(f));
4469
4722
  if (options.dryRun) {
@@ -4503,28 +4756,28 @@ function themeSyncCommand(cwd, options) {
4503
4756
  return;
4504
4757
  }
4505
4758
  try {
4506
- mkdirSync5(docsAppDir, { recursive: true });
4507
- mkdirSync5(docsLibDir, { recursive: true });
4508
- mkdirSync5(docsPublicThemesDir, { recursive: true });
4759
+ mkdirSync6(docsAppDir, { recursive: true });
4760
+ mkdirSync6(docsLibDir, { recursive: true });
4761
+ mkdirSync6(docsPublicThemesDir, { recursive: true });
4509
4762
  for (const entry of manifest) {
4510
- writeFileSync9(join15(docsAppDir, `${entry.slug}-theme.css`), entry.css, "utf-8");
4763
+ writeFileSync10(join17(docsAppDir, `${entry.slug}-theme.css`), entry.css, "utf-8");
4511
4764
  }
4512
4765
  for (const stale of staleCssFiles) {
4513
- unlinkSync2(join15(docsAppDir, stale));
4766
+ unlinkSync2(join17(docsAppDir, stale));
4514
4767
  }
4515
- writeFileSync9(customOverlayCssPath, newCustomOverlayCss, "utf-8");
4516
- writeFileSync9(customOverlayTsPath, newCustomOverlayTs, "utf-8");
4517
- writeFileSync9(themeConfigPath, newThemeConfig, "utf-8");
4518
- writeFileSync9(globalsPath, newGlobals, "utf-8");
4768
+ writeFileSync10(customOverlayCssPath, newCustomOverlayCss, "utf-8");
4769
+ writeFileSync10(customOverlayTsPath, newCustomOverlayTs, "utf-8");
4770
+ writeFileSync10(themeConfigPath, newThemeConfig, "utf-8");
4771
+ writeFileSync10(globalsPath, newGlobals, "utf-8");
4519
4772
  for (const srcFile of stockFiles) {
4520
- copyFileSync(srcFile, join15(docsPublicThemesDir, basename3(srcFile)));
4773
+ copyFileSync(srcFile, join17(docsPublicThemesDir, basename4(srcFile)));
4521
4774
  }
4522
4775
  for (const c of customSources) {
4523
- const targetName = c.origin === "legacy" ? basename3(c.filePath) : `${c.slug}.visor.yaml`;
4524
- copyFileSync(c.filePath, join15(docsPublicThemesDir, targetName));
4776
+ const targetName = c.origin === "legacy" ? basename4(c.filePath) : `${c.slug}.visor.yaml`;
4777
+ copyFileSync(c.filePath, join17(docsPublicThemesDir, targetName));
4525
4778
  }
4526
4779
  for (const stale of stalePublicYamls) {
4527
- unlinkSync2(join15(docsPublicThemesDir, stale));
4780
+ unlinkSync2(join17(docsPublicThemesDir, stale));
4528
4781
  }
4529
4782
  } catch (err) {
4530
4783
  const msg = err instanceof Error ? err.message : "Write failed";
@@ -4577,19 +4830,19 @@ function themeSyncCommand(cwd, options) {
4577
4830
 
4578
4831
  // src/commands/theme-batch-apply-flutter.ts
4579
4832
  import {
4580
- readFileSync as readFileSync18,
4581
- writeFileSync as writeFileSync10,
4582
- mkdirSync as mkdirSync6,
4583
- existsSync as existsSync14,
4833
+ readFileSync as readFileSync20,
4834
+ writeFileSync as writeFileSync11,
4835
+ mkdirSync as mkdirSync7,
4836
+ existsSync as existsSync16,
4584
4837
  readdirSync as readdirSync7,
4585
4838
  rmSync
4586
4839
  } from "fs";
4587
- import { join as join16, basename as basename4, dirname as dirname7 } from "path";
4840
+ import { join as join18, basename as basename5, dirname as dirname8 } from "path";
4588
4841
  import { generateThemeData as generateThemeData6 } from "@loworbitstudio/visor-theme-engine";
4589
4842
  import { flutterAdapter as flutterAdapter2 } from "@loworbitstudio/visor-theme-engine/adapters";
4590
4843
  function scanThemeDir2(dir) {
4591
- if (!existsSync14(dir)) return [];
4592
- return readdirSync7(dir).filter((f) => f.endsWith(".visor.yaml")).map((f) => join16(dir, f)).sort();
4844
+ if (!existsSync16(dir)) return [];
4845
+ return readdirSync7(dir).filter((f) => f.endsWith(".visor.yaml")).map((f) => join18(dir, f)).sort();
4593
4846
  }
4594
4847
  function slugToCamel(slug2) {
4595
4848
  return slug2.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
@@ -4722,9 +4975,9 @@ function themeBatchApplyFlutterCommand(cwd, options) {
4722
4975
  process.exit(1);
4723
4976
  return;
4724
4977
  }
4725
- const themesDir = join16(repoRoot, "themes");
4726
- const customThemesDir = join16(repoRoot, "custom-themes");
4727
- const outputDir = join16(repoRoot, "packages", "visor_themes");
4978
+ const themesDir = join18(repoRoot, "themes");
4979
+ const customThemesDir = join18(repoRoot, "custom-themes");
4980
+ const outputDir = join18(repoRoot, "packages", "visor_themes");
4728
4981
  const stockFiles = scanThemeDir2(themesDir);
4729
4982
  const customFiles = scanThemeDir2(customThemesDir);
4730
4983
  const allFiles = [...stockFiles, ...customFiles];
@@ -4745,7 +4998,7 @@ function themeBatchApplyFlutterCommand(cwd, options) {
4745
4998
  for (const filePath of allFiles) {
4746
4999
  let yamlContent;
4747
5000
  try {
4748
- yamlContent = readFileSync18(filePath, "utf-8");
5001
+ yamlContent = readFileSync20(filePath, "utf-8");
4749
5002
  } catch {
4750
5003
  errors.push(`Could not read: ${filePath}`);
4751
5004
  continue;
@@ -4755,7 +5008,7 @@ function themeBatchApplyFlutterCommand(cwd, options) {
4755
5008
  data = generateThemeData6(yamlContent);
4756
5009
  } catch (err) {
4757
5010
  errors.push(
4758
- `Failed to parse ${basename4(filePath)}: ${err instanceof Error ? err.message : "Unknown error"}`
5011
+ `Failed to parse ${basename5(filePath)}: ${err instanceof Error ? err.message : "Unknown error"}`
4759
5012
  );
4760
5013
  continue;
4761
5014
  }
@@ -4824,8 +5077,8 @@ function themeBatchApplyFlutterCommand(cwd, options) {
4824
5077
  return;
4825
5078
  }
4826
5079
  const slugs = processed.map((p) => p.slug);
4827
- const libSrcDir = join16(outputDir, "lib", "src");
4828
- if (existsSync14(libSrcDir)) {
5080
+ const libSrcDir = join18(outputDir, "lib", "src");
5081
+ if (existsSync16(libSrcDir)) {
4829
5082
  rmSync(libSrcDir, { recursive: true, force: true });
4830
5083
  }
4831
5084
  const packageFiles = {
@@ -4837,17 +5090,17 @@ function themeBatchApplyFlutterCommand(cwd, options) {
4837
5090
  };
4838
5091
  let totalFiles = 0;
4839
5092
  for (const [relPath, content] of Object.entries(packageFiles)) {
4840
- const absPath = join16(outputDir, relPath);
4841
- mkdirSync6(dirname7(absPath), { recursive: true });
4842
- writeFileSync10(absPath, content, "utf-8");
5093
+ const absPath = join18(outputDir, relPath);
5094
+ mkdirSync7(dirname8(absPath), { recursive: true });
5095
+ writeFileSync11(absPath, content, "utf-8");
4843
5096
  totalFiles++;
4844
5097
  }
4845
5098
  for (const { slug: slug2, tokenFiles } of processed) {
4846
- const themeBaseDir = join16(outputDir, "lib", "src", slug2);
5099
+ const themeBaseDir = join18(outputDir, "lib", "src", slug2);
4847
5100
  for (const [relPath, content] of Object.entries(tokenFiles)) {
4848
- const absPath = join16(themeBaseDir, relPath);
4849
- mkdirSync6(dirname7(absPath), { recursive: true });
4850
- writeFileSync10(absPath, content, "utf-8");
5101
+ const absPath = join18(themeBaseDir, relPath);
5102
+ mkdirSync7(dirname8(absPath), { recursive: true });
5103
+ writeFileSync11(absPath, content, "utf-8");
4851
5104
  totalFiles++;
4852
5105
  }
4853
5106
  }
@@ -4868,11 +5121,11 @@ function themeBatchApplyFlutterCommand(cwd, options) {
4868
5121
  }
4869
5122
 
4870
5123
  // src/commands/fonts-add.ts
4871
- import { existsSync as existsSync15, statSync as statSync7, readdirSync as readdirSync8, readFileSync as readFileSync19 } from "fs";
4872
- import { resolve as resolve13, basename as basename5, extname as extname4 } from "path";
5124
+ import { existsSync as existsSync17, statSync as statSync7, readdirSync as readdirSync8, readFileSync as readFileSync21 } from "fs";
5125
+ import { resolve as resolve13, basename as basename6, extname as extname4 } from "path";
4873
5126
  import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
4874
5127
  function deriveFamilySlug(filename) {
4875
- const name = basename5(filename, extname4(filename));
5128
+ const name = basename6(filename, extname4(filename));
4876
5129
  const WEIGHT_STYLE_SUFFIXES = /* @__PURE__ */ new Set([
4877
5130
  "thin",
4878
5131
  "hairline",
@@ -4914,14 +5167,14 @@ function deriveFamilySlug(filename) {
4914
5167
  }
4915
5168
  function collectWoff2Files(inputPath) {
4916
5169
  const resolved = resolve13(inputPath);
4917
- if (!existsSync15(resolved)) {
5170
+ if (!existsSync17(resolved)) {
4918
5171
  throw new Error(`Path not found: ${resolved}`);
4919
5172
  }
4920
5173
  const stat = statSync7(resolved);
4921
5174
  if (stat.isFile()) {
4922
5175
  if (extname4(resolved).toLowerCase() !== ".woff2") {
4923
5176
  throw new Error(
4924
- `Invalid file format: ${basename5(resolved)}. Only .woff2 files are accepted.`
5177
+ `Invalid file format: ${basename6(resolved)}. Only .woff2 files are accepted.`
4925
5178
  );
4926
5179
  }
4927
5180
  return [resolved];
@@ -4943,7 +5196,7 @@ function collectWoff2Files(inputPath) {
4943
5196
  }
4944
5197
  function getNonWoff2Fonts(inputPath) {
4945
5198
  const resolved = resolve13(inputPath);
4946
- if (!existsSync15(resolved) || !statSync7(resolved).isDirectory()) {
5199
+ if (!existsSync17(resolved) || !statSync7(resolved).isDirectory()) {
4947
5200
  return [];
4948
5201
  }
4949
5202
  return readdirSync8(resolved).filter((f) => {
@@ -4980,7 +5233,7 @@ function createR2Client(config) {
4980
5233
  });
4981
5234
  }
4982
5235
  async function uploadFile(client, bucket, key, filePath) {
4983
- const body = readFileSync19(filePath);
5236
+ const body = readFileSync21(filePath);
4984
5237
  await client.send(
4985
5238
  new PutObjectCommand({
4986
5239
  Bucket: bucket,
@@ -4995,7 +5248,7 @@ async function fontsAddCommand(inputPath, options) {
4995
5248
  try {
4996
5249
  const r2Config = getR2Config();
4997
5250
  const files = collectWoff2Files(inputPath);
4998
- const familySlug = options.family ?? deriveFamilySlug(basename5(files[0]));
5251
+ const familySlug = options.family ?? deriveFamilySlug(basename6(files[0]));
4999
5252
  const resolved = resolve13(inputPath);
5000
5253
  const nonWoff2 = statSync7(resolved).isDirectory() ? getNonWoff2Fonts(resolved) : [];
5001
5254
  if (!json) {
@@ -5015,7 +5268,7 @@ async function fontsAddCommand(inputPath, options) {
5015
5268
  const bucket = "visor-fonts";
5016
5269
  const results = [];
5017
5270
  for (const filePath of files) {
5018
- const filename = basename5(filePath);
5271
+ const filename = basename6(filePath);
5019
5272
  const key = buildS3Key(org, familySlug, filename);
5020
5273
  if (!json) {
5021
5274
  logger.info(`Uploading ${filename}...`);
@@ -5284,27 +5537,27 @@ function findCssFiles(dir, maxDepth = 3) {
5284
5537
  }
5285
5538
 
5286
5539
  // src/utils/patterns.ts
5287
- import { existsSync as existsSync17, readdirSync as readdirSync10, readFileSync as readFileSync21 } from "fs";
5288
- import { join as join18 } from "path";
5540
+ import { existsSync as existsSync19, readdirSync as readdirSync10, readFileSync as readFileSync23 } from "fs";
5541
+ import { join as join20 } from "path";
5289
5542
  import { parse as parseYAML } from "yaml";
5290
5543
  function loadPatternsFromYaml(repoRoot) {
5291
- const patternsDir = join18(repoRoot, "patterns");
5292
- if (!existsSync17(patternsDir)) return [];
5544
+ const patternsDir = join20(repoRoot, "patterns");
5545
+ if (!existsSync19(patternsDir)) return [];
5293
5546
  const files = readdirSync10(patternsDir).filter(
5294
5547
  (f) => f.endsWith(".visor-pattern.yaml")
5295
5548
  );
5296
5549
  return files.map((file) => {
5297
- const content = readFileSync21(join18(patternsDir, file), "utf-8");
5550
+ const content = readFileSync23(join20(patternsDir, file), "utf-8");
5298
5551
  return parseYAML(content);
5299
5552
  }).filter(Boolean);
5300
5553
  }
5301
5554
  function findRepoRoot2(startDir) {
5302
5555
  let current = startDir;
5303
5556
  while (true) {
5304
- if (existsSync17(join18(current, "patterns"))) {
5557
+ if (existsSync19(join20(current, "patterns"))) {
5305
5558
  return current;
5306
5559
  }
5307
- const parent = join18(current, "..");
5560
+ const parent = join20(current, "..");
5308
5561
  if (parent === current) return null;
5309
5562
  current = parent;
5310
5563
  }
@@ -5683,8 +5936,8 @@ Visor Tokens (${categoryLabel}) \u2014 ${tokens2.length} tokens
5683
5936
  }
5684
5937
 
5685
5938
  // src/commands/migrate-token-substitution.ts
5686
- import { readFileSync as readFileSync22, writeFileSync as writeFileSync11, readdirSync as readdirSync11, statSync as statSync8, existsSync as existsSync18 } from "fs";
5687
- import { resolve as resolve14, join as join19, relative as relative4 } from "path";
5939
+ import { readFileSync as readFileSync24, writeFileSync as writeFileSync12, readdirSync as readdirSync11, statSync as statSync8, existsSync as existsSync20 } from "fs";
5940
+ import { resolve as resolve14, join as join21, relative as relative4 } from "path";
5688
5941
  import { parse as parseYaml4 } from "yaml";
5689
5942
  import pc4 from "picocolors";
5690
5943
  var V7_ENTR_SUBSTITUTION_MAP = {
@@ -5707,14 +5960,14 @@ var BUILT_IN_SUBSTITUTION_MAPS = {
5707
5960
  var DEFAULT_THEME_ID = "entr";
5708
5961
  function readMapFromThemeFile(themeId, cwd) {
5709
5962
  const candidates = [
5710
- join19(cwd, "themes", `${themeId}.visor.yaml`),
5711
- join19(cwd, "custom-themes", `${themeId}.visor.yaml`),
5712
- join19(cwd, "packages", "docs", "public", "themes", `${themeId}.visor.yaml`)
5963
+ join21(cwd, "themes", `${themeId}.visor.yaml`),
5964
+ join21(cwd, "custom-themes", `${themeId}.visor.yaml`),
5965
+ join21(cwd, "packages", "docs", "public", "themes", `${themeId}.visor.yaml`)
5713
5966
  ];
5714
5967
  for (const candidate of candidates) {
5715
- if (existsSync18(candidate)) {
5968
+ if (existsSync20(candidate)) {
5716
5969
  try {
5717
- const raw = readFileSync22(candidate, "utf-8");
5970
+ const raw = readFileSync24(candidate, "utf-8");
5718
5971
  const parsed = parseYaml4(raw);
5719
5972
  if (parsed?.migrate?.["token-substitution"]) {
5720
5973
  return parsed.migrate["token-substitution"];
@@ -5770,7 +6023,7 @@ function collectCssFiles(dirPath) {
5770
6023
  function walk2(current) {
5771
6024
  const entries = readdirSync11(current, { withFileTypes: true });
5772
6025
  for (const entry of entries) {
5773
- const fullPath = join19(current, entry.name);
6026
+ const fullPath = join21(current, entry.name);
5774
6027
  if (entry.isDirectory()) {
5775
6028
  if (["node_modules", ".git", ".next", "dist", "build", ".cache"].includes(entry.name)) {
5776
6029
  continue;
@@ -5798,7 +6051,7 @@ function runSubstitutionPass(targetPath, map, themeId) {
5798
6051
  const fileResults = [];
5799
6052
  let totalSubstitutions = 0;
5800
6053
  for (const file of files) {
5801
- const content = readFileSync22(file, "utf-8");
6054
+ const content = readFileSync24(file, "utf-8");
5802
6055
  const { newContent, substitutions } = applySubstitutionsToContent(content, map);
5803
6056
  if (substitutions.length > 0) {
5804
6057
  fileResults.push({ file, substitutions, originalContent: content, newContent });
@@ -5860,7 +6113,7 @@ function migrateTokenSubstitutionCommand(targetArg, cwd, options) {
5860
6113
  if (options.json) {
5861
6114
  if (apply) {
5862
6115
  for (const f of result.files) {
5863
- writeFileSync11(f.file, f.newContent, "utf-8");
6116
+ writeFileSync12(f.file, f.newContent, "utf-8");
5864
6117
  }
5865
6118
  console.log(JSON.stringify({
5866
6119
  success: true,
@@ -5900,7 +6153,7 @@ function migrateTokenSubstitutionCommand(targetArg, cwd, options) {
5900
6153
  logger.warn(`Dry run \u2014 no files written. Re-run with ${pc4.bold("--apply")} to commit changes.`);
5901
6154
  } else {
5902
6155
  for (const f of result.files) {
5903
- writeFileSync11(f.file, f.newContent, "utf-8");
6156
+ writeFileSync12(f.file, f.newContent, "utf-8");
5904
6157
  }
5905
6158
  logger.heading(`visor migrate token-substitution \u2014 applied`);
5906
6159
  logger.blank();
@@ -5921,23 +6174,23 @@ function migrateTokenSubstitutionCommand(targetArg, cwd, options) {
5921
6174
  }
5922
6175
 
5923
6176
  // src/commands/sandbox/init.ts
5924
- import { existsSync as existsSync21, mkdirSync as mkdirSync9, readFileSync as readFileSync25, rmSync as rmSync2 } from "fs";
5925
- import { isAbsolute as isAbsolute2, join as join22, resolve as resolve16, dirname as dirname9 } from "path";
6177
+ import { existsSync as existsSync23, mkdirSync as mkdirSync10, readFileSync as readFileSync27, rmSync as rmSync2 } from "fs";
6178
+ import { isAbsolute as isAbsolute2, join as join24, resolve as resolve16, dirname as dirname10 } from "path";
5926
6179
  import { fileURLToPath as fileURLToPath3 } from "url";
5927
- import * as childProcess2 from "child_process";
6180
+ import * as childProcess3 from "child_process";
5928
6181
 
5929
6182
  // src/commands/sandbox/parse-handoff.ts
5930
- import { readFileSync as readFileSync23, existsSync as existsSync19 } from "fs";
5931
- import { dirname as dirname8, resolve as resolve15, isAbsolute } from "path";
6183
+ import { readFileSync as readFileSync25, existsSync as existsSync21 } from "fs";
6184
+ import { dirname as dirname9, resolve as resolve15, isAbsolute } from "path";
5932
6185
  function parseHandoff(handoffPath) {
5933
- const text = readFileSync23(handoffPath, "utf-8");
6186
+ const text = readFileSync25(handoffPath, "utf-8");
5934
6187
  const lines2 = text.split("\n");
5935
6188
  const warnings = [];
5936
6189
  const pattern2 = extractPatternSlug(lines2, warnings);
5937
6190
  const theme2 = extractMetaField(lines2, "Theme");
5938
6191
  const recipePath = extractRecipePath(text, handoffPath, warnings);
5939
6192
  const primitives = extractPrimitives(text, warnings);
5940
- const { screens, mockShapes } = recipePath && existsSync19(recipePath) ? extractRecipeArtifacts(recipePath, warnings) : { screens: [], mockShapes: [] };
6193
+ const { screens, mockShapes } = recipePath && existsSync21(recipePath) ? extractRecipeArtifacts(recipePath, warnings) : { screens: [], mockShapes: [] };
5941
6194
  return { pattern: pattern2, theme: theme2, recipePath, primitives, screens, mockShapes, warnings };
5942
6195
  }
5943
6196
  function extractPatternSlug(lines2, warnings) {
@@ -5969,7 +6222,7 @@ function extractRecipePath(text, handoffPath, warnings) {
5969
6222
  }
5970
6223
  const href = m[2].trim();
5971
6224
  if (isAbsolute(href)) return href;
5972
- return resolve15(dirname8(handoffPath), href);
6225
+ return resolve15(dirname9(handoffPath), href);
5973
6226
  }
5974
6227
  var STATUS_PATTERNS = [
5975
6228
  { re: /NEW\s+gap/i, status: "gap-new" },
@@ -6016,7 +6269,7 @@ function extractPrimitives(text, warnings) {
6016
6269
  return primitives;
6017
6270
  }
6018
6271
  function extractRecipeArtifacts(recipePath, warnings) {
6019
- const text = readFileSync23(recipePath, "utf-8");
6272
+ const text = readFileSync25(recipePath, "utf-8");
6020
6273
  const screens = extractScreens(text);
6021
6274
  const mockShapes = extractMockFields(text, warnings);
6022
6275
  return { screens, mockShapes };
@@ -6090,8 +6343,8 @@ function tryPort(port) {
6090
6343
  }
6091
6344
 
6092
6345
  // src/commands/sandbox/scaffold.ts
6093
- import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync12, existsSync as existsSync20, readdirSync as readdirSync12 } from "fs";
6094
- import { join as join20 } from "path";
6346
+ import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync13, existsSync as existsSync22, readdirSync as readdirSync12 } from "fs";
6347
+ import { join as join22 } from "path";
6095
6348
 
6096
6349
  // src/commands/sandbox/templates.ts
6097
6350
  var SANDBOX_PACKAGE_VERSION = "16.2.6";
@@ -6591,12 +6844,12 @@ function toPascalCase(s) {
6591
6844
 
6592
6845
  // src/commands/sandbox/scaffold.ts
6593
6846
  function writeScaffold(sandboxDir, manifest, port, options = {}) {
6594
- mkdirSync7(sandboxDir, { recursive: true });
6847
+ mkdirSync8(sandboxDir, { recursive: true });
6595
6848
  const created = [];
6596
6849
  const writeIfNew = (rel, contents) => {
6597
- const full = join20(sandboxDir, rel);
6598
- mkdirSync7(dirOf(full), { recursive: true });
6599
- writeFileSync12(full, contents, "utf-8");
6850
+ const full = join22(sandboxDir, rel);
6851
+ mkdirSync8(dirOf(full), { recursive: true });
6852
+ writeFileSync13(full, contents, "utf-8");
6600
6853
  created.push(rel);
6601
6854
  };
6602
6855
  writeIfNew("package.json", packageJsonTemplate(manifest.pattern));
@@ -6622,7 +6875,7 @@ function writeScaffold(sandboxDir, manifest, port, options = {}) {
6622
6875
  }
6623
6876
  }
6624
6877
  writeIfNew("playwright.capture.mjs", captureScriptTemplate());
6625
- mkdirSync7(join20(sandboxDir, "captures", "approved"), { recursive: true });
6878
+ mkdirSync8(join22(sandboxDir, "captures", "approved"), { recursive: true });
6626
6879
  return { created };
6627
6880
  }
6628
6881
  function dirOf(p) {
@@ -6656,10 +6909,10 @@ function writeSandboxConfig(sandboxDir, manifest, port, options) {
6656
6909
  stripChromeSelectors: options.prototypeImport.stripChromeSelectors
6657
6910
  } : null
6658
6911
  };
6659
- writeFileSync12(join20(sandboxDir, "sandbox.json"), JSON.stringify(config, null, 2) + "\n", "utf-8");
6912
+ writeFileSync13(join22(sandboxDir, "sandbox.json"), JSON.stringify(config, null, 2) + "\n", "utf-8");
6660
6913
  }
6661
6914
  function sandboxIsEmpty(sandboxDir) {
6662
- if (!existsSync20(sandboxDir)) return true;
6915
+ if (!existsSync22(sandboxDir)) return true;
6663
6916
  try {
6664
6917
  return readdirSync12(sandboxDir).filter((f) => !f.startsWith(".")).length === 0;
6665
6918
  } catch {
@@ -6668,8 +6921,8 @@ function sandboxIsEmpty(sandboxDir) {
6668
6921
  }
6669
6922
 
6670
6923
  // src/commands/sandbox/html-prototype.ts
6671
- import { mkdirSync as mkdirSync8, readdirSync as readdirSync13, readFileSync as readFileSync24, statSync as statSync9, writeFileSync as writeFileSync13 } from "fs";
6672
- import { join as join21 } from "path";
6924
+ import { mkdirSync as mkdirSync9, readdirSync as readdirSync13, readFileSync as readFileSync26, statSync as statSync9, writeFileSync as writeFileSync14 } from "fs";
6925
+ import { join as join23 } from "path";
6673
6926
 
6674
6927
  // src/commands/sandbox/strip-chrome.ts
6675
6928
  var DEFAULT_STRIP_SELECTORS = [
@@ -6853,8 +7106,8 @@ function stripDocumentaryChrome(html, selectors) {
6853
7106
  var SCREEN_FILE_PATTERN = /^screen-(\d+)-([^/.]+)\.html$/i;
6854
7107
  function copyHtmlPrototype(sourceDir, sandboxDir, manifest, options = {}) {
6855
7108
  const warnings = [];
6856
- const destAbs = join21(sandboxDir, "public", "prototype");
6857
- mkdirSync8(destAbs, { recursive: true });
7109
+ const destAbs = join23(sandboxDir, "public", "prototype");
7110
+ mkdirSync9(destAbs, { recursive: true });
6858
7111
  const stripChromeSelectors = options.stripChromeSelectors ?? [];
6859
7112
  const copiedFiles = copyTreeRelative(sourceDir, destAbs, "", stripChromeSelectors);
6860
7113
  const screenFiles = listOrderedScreenFiles(sourceDir);
@@ -6907,24 +7160,24 @@ function deriveStateCoverageScreen(file, existingNames) {
6907
7160
  }
6908
7161
  function copyTreeRelative(srcDir, destDir, relDir = "", stripChromeSelectors = []) {
6909
7162
  const out = [];
6910
- const entries = readdirSync13(join21(srcDir, relDir));
7163
+ const entries = readdirSync13(join23(srcDir, relDir));
6911
7164
  for (const name of entries) {
6912
7165
  if (name.startsWith(".")) continue;
6913
- const srcPath = join21(srcDir, relDir, name);
6914
- const destPath = join21(destDir, relDir, name);
7166
+ const srcPath = join23(srcDir, relDir, name);
7167
+ const destPath = join23(destDir, relDir, name);
6915
7168
  const stat = statSync9(srcPath);
6916
7169
  if (stat.isDirectory()) {
6917
- mkdirSync8(destPath, { recursive: true });
6918
- out.push(...copyTreeRelative(srcDir, destDir, join21(relDir, name), stripChromeSelectors));
7170
+ mkdirSync9(destPath, { recursive: true });
7171
+ out.push(...copyTreeRelative(srcDir, destDir, join23(relDir, name), stripChromeSelectors));
6919
7172
  } else if (stat.isFile()) {
6920
7173
  if (stripChromeSelectors.length > 0 && name.toLowerCase().endsWith(".html")) {
6921
- const html = readFileSync24(srcPath, "utf-8");
7174
+ const html = readFileSync26(srcPath, "utf-8");
6922
7175
  const stripped = stripDocumentaryChrome(html, stripChromeSelectors);
6923
- writeFileSync13(destPath, stripped);
7176
+ writeFileSync14(destPath, stripped);
6924
7177
  } else {
6925
- writeFileSync13(destPath, readFileSync24(srcPath));
7178
+ writeFileSync14(destPath, readFileSync26(srcPath));
6926
7179
  }
6927
- out.push(join21("public", "prototype", relDir, name).replace(/\\/g, "/"));
7180
+ out.push(join23("public", "prototype", relDir, name).replace(/\\/g, "/"));
6928
7181
  }
6929
7182
  }
6930
7183
  return out;
@@ -6977,7 +7230,7 @@ async function runInit(name, cwd, options) {
6977
7230
  throw new Error(`Invalid sandbox name '${name}'. Use letters, digits, '-' or '_'.`);
6978
7231
  }
6979
7232
  const handoffPath = isAbsolute2(options.handoff) ? options.handoff : resolve16(cwd, options.handoff);
6980
- if (!existsSync21(handoffPath)) {
7233
+ if (!existsSync23(handoffPath)) {
6981
7234
  throw new Error(`Handoff manifest not found: ${handoffPath}`);
6982
7235
  }
6983
7236
  const manifest = parseHandoff(handoffPath);
@@ -6986,7 +7239,7 @@ async function runInit(name, cwd, options) {
6986
7239
  `Handoff has no primitives \u2014 refusing to scaffold an empty sandbox. Check the manifest at ${handoffPath}`
6987
7240
  );
6988
7241
  }
6989
- const sandboxDir = join22(cwd, ".lo", "sandbox", name);
7242
+ const sandboxDir = join24(cwd, ".lo", "sandbox", name);
6990
7243
  if (!sandboxIsEmpty(sandboxDir)) {
6991
7244
  if (!options.overwrite) {
6992
7245
  throw new Error(
@@ -6995,12 +7248,12 @@ async function runInit(name, cwd, options) {
6995
7248
  }
6996
7249
  rmSync2(sandboxDir, { recursive: true, force: true });
6997
7250
  }
6998
- mkdirSync9(sandboxDir, { recursive: true });
7251
+ mkdirSync10(sandboxDir, { recursive: true });
6999
7252
  const port = await findOpenPort();
7000
7253
  let prototypeImport;
7001
7254
  if (options.fromHtmlPrototype) {
7002
7255
  const prototypeDir = isAbsolute2(options.fromHtmlPrototype) ? options.fromHtmlPrototype : resolve16(cwd, options.fromHtmlPrototype);
7003
- if (!existsSync21(prototypeDir)) {
7256
+ if (!existsSync23(prototypeDir)) {
7004
7257
  throw new Error(`HTML prototype directory not found: ${prototypeDir}`);
7005
7258
  }
7006
7259
  const stripChromeSelectors = resolveStripSelectors(
@@ -7064,17 +7317,17 @@ function applyThemeIfPossible(sandboxDir, manifest, theme2, cwd, json, themeFile
7064
7317
  candidates.push(isAbsolute2(theme2) ? theme2 : resolve16(cwd, theme2));
7065
7318
  const privateRoot = process.env.VISOR_THEMES_PRIVATE_PATH;
7066
7319
  if (privateRoot && privateRoot.length > 0) {
7067
- candidates.push(join22(privateRoot, "themes", theme2, "theme.visor.yaml"));
7320
+ candidates.push(join24(privateRoot, "themes", theme2, "theme.visor.yaml"));
7068
7321
  }
7069
7322
  candidates.push(
7070
- join22(cwd, "themes", `${theme2}.visor.yaml`),
7071
- join22(cwd, "custom-themes", `${theme2}.visor.yaml`)
7323
+ join24(cwd, "themes", `${theme2}.visor.yaml`),
7324
+ join24(cwd, "custom-themes", `${theme2}.visor.yaml`)
7072
7325
  );
7073
- const yamlPath = candidates.find((p) => existsSync21(p));
7326
+ const yamlPath = candidates.find((p) => existsSync23(p));
7074
7327
  if (!yamlPath) {
7075
7328
  const searched = candidates.join(", ");
7076
7329
  manifest.warnings.push(
7077
- `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 ${join22(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.`
7330
+ `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 ${join24(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.`
7078
7331
  );
7079
7332
  if (!json) {
7080
7333
  logger.warn(`Theme '${theme2}' not found \u2014 leaving placeholder globals.css.`);
@@ -7082,12 +7335,12 @@ function applyThemeIfPossible(sandboxDir, manifest, theme2, cwd, json, themeFile
7082
7335
  `Re-run with --theme-file <path>, or set VISOR_THEMES_PRIVATE_PATH=<dir-containing-themes/${theme2}/theme.visor.yaml>.`
7083
7336
  );
7084
7337
  logger.item(
7085
- `Or apply manually: npx visor theme apply <path> --adapter nextjs -o ${join22(sandboxDir, "app", "globals.css")}`
7338
+ `Or apply manually: npx visor theme apply <path> --adapter nextjs -o ${join24(sandboxDir, "app", "globals.css")}`
7086
7339
  );
7087
7340
  }
7088
7341
  return;
7089
7342
  }
7090
- const globalsOut = join22(sandboxDir, "app", "globals.css");
7343
+ const globalsOut = join24(sandboxDir, "app", "globals.css");
7091
7344
  try {
7092
7345
  themeApplyCommand(yamlPath, sandboxDir, {
7093
7346
  output: globalsOut,
@@ -7118,7 +7371,7 @@ function tryAddPrimitive(primitive, sandboxDir, json) {
7118
7371
  }
7119
7372
  function runNpmInstall(sandboxDir, json) {
7120
7373
  if (!json) logger.info("Installing sandbox dependencies...");
7121
- const result = childProcess2.spawnSync("npm", ["install", "--no-audit", "--no-fund"], {
7374
+ const result = childProcess3.spawnSync("npm", ["install", "--no-audit", "--no-fund"], {
7122
7375
  cwd: sandboxDir,
7123
7376
  stdio: json ? "ignore" : "inherit"
7124
7377
  });
@@ -7140,12 +7393,12 @@ function loadKnownPrimitives() {
7140
7393
  }
7141
7394
  function readCliVersion() {
7142
7395
  try {
7143
- const here = dirname9(fileURLToPath3(import.meta.url));
7396
+ const here = dirname10(fileURLToPath3(import.meta.url));
7144
7397
  for (let i = 0; i < 6; i++) {
7145
7398
  const segments = new Array(i).fill("..");
7146
- const candidate = join22(here, ...segments, "package.json");
7399
+ const candidate = join24(here, ...segments, "package.json");
7147
7400
  try {
7148
- const pkg2 = JSON.parse(readFileSync25(candidate, "utf-8"));
7401
+ const pkg2 = JSON.parse(readFileSync27(candidate, "utf-8"));
7149
7402
  if (pkg2.name === "@loworbitstudio/visor" && pkg2.version) return pkg2.version;
7150
7403
  } catch {
7151
7404
  }
@@ -7156,14 +7409,14 @@ function readCliVersion() {
7156
7409
  }
7157
7410
 
7158
7411
  // src/commands/sandbox/dev.ts
7159
- import { existsSync as existsSync22, readFileSync as readFileSync26 } from "fs";
7160
- import { join as join23 } from "path";
7161
- import * as childProcess3 from "child_process";
7412
+ import { existsSync as existsSync24, readFileSync as readFileSync28 } from "fs";
7413
+ import { join as join25 } from "path";
7414
+ import * as childProcess4 from "child_process";
7162
7415
  function sandboxDevCommand(cwd, options) {
7163
7416
  const json = options.json ?? false;
7164
- const sandboxDir = join23(cwd, ".lo", "sandbox", options.name);
7165
- const configPath = join23(sandboxDir, "sandbox.json");
7166
- if (!existsSync22(configPath)) {
7417
+ const sandboxDir = join25(cwd, ".lo", "sandbox", options.name);
7418
+ const configPath = join25(sandboxDir, "sandbox.json");
7419
+ if (!existsSync24(configPath)) {
7167
7420
  fail(
7168
7421
  json,
7169
7422
  `Sandbox '${options.name}' not found at ${sandboxDir}. Run 'visor sandbox init ${options.name} --handoff ... --theme ...' first.`
@@ -7172,7 +7425,7 @@ function sandboxDevCommand(cwd, options) {
7172
7425
  }
7173
7426
  let config;
7174
7427
  try {
7175
- config = JSON.parse(readFileSync26(configPath, "utf-8"));
7428
+ config = JSON.parse(readFileSync28(configPath, "utf-8"));
7176
7429
  } catch (err) {
7177
7430
  fail(json, `Invalid sandbox.json at ${configPath}: ${err.message}`);
7178
7431
  return;
@@ -7199,7 +7452,7 @@ function sandboxDevCommand(cwd, options) {
7199
7452
  spawnNextDev(sandboxDir, config.port, json);
7200
7453
  }
7201
7454
  function spawnNextDev(sandboxDir, port, json) {
7202
- const child = childProcess3.spawn(
7455
+ const child = childProcess4.spawn(
7203
7456
  "npx",
7204
7457
  ["--no-install", "next", "dev", "--port", String(port)],
7205
7458
  {
@@ -7230,20 +7483,20 @@ function fail(json, message) {
7230
7483
  // src/commands/sandbox/approve.ts
7231
7484
  import {
7232
7485
  copyFileSync as copyFileSync2,
7233
- existsSync as existsSync23,
7234
- mkdirSync as mkdirSync10,
7235
- readFileSync as readFileSync27,
7486
+ existsSync as existsSync25,
7487
+ mkdirSync as mkdirSync11,
7488
+ readFileSync as readFileSync29,
7236
7489
  readdirSync as readdirSync14,
7237
7490
  rmSync as rmSync3,
7238
- writeFileSync as writeFileSync14
7491
+ writeFileSync as writeFileSync15
7239
7492
  } from "fs";
7240
- import { basename as basename6, join as join24 } from "path";
7241
- import * as childProcess4 from "child_process";
7493
+ import { basename as basename7, join as join26 } from "path";
7494
+ import * as childProcess5 from "child_process";
7242
7495
  function sandboxApproveCommand(cwd, options) {
7243
7496
  const json = options.json ?? false;
7244
- const sandboxDir = join24(cwd, ".lo", "sandbox", options.name);
7245
- const configPath = join24(sandboxDir, "sandbox.json");
7246
- if (!existsSync23(configPath)) {
7497
+ const sandboxDir = join26(cwd, ".lo", "sandbox", options.name);
7498
+ const configPath = join26(sandboxDir, "sandbox.json");
7499
+ if (!existsSync25(configPath)) {
7247
7500
  fail2(
7248
7501
  json,
7249
7502
  `Sandbox '${options.name}' not found at ${sandboxDir}. Run 'visor sandbox init' first.`
@@ -7257,11 +7510,11 @@ function sandboxApproveCommand(cwd, options) {
7257
7510
  runCapture(sandboxDir, configPath, options.name, json);
7258
7511
  }
7259
7512
  function runCapture(sandboxDir, configPath, sandboxName, json) {
7260
- const config = JSON.parse(readFileSync27(configPath, "utf-8"));
7261
- const captureScriptPath = join24(sandboxDir, "playwright.capture.mjs");
7262
- writeFileSync14(captureScriptPath, captureScriptTemplate(), "utf-8");
7513
+ const config = JSON.parse(readFileSync29(configPath, "utf-8"));
7514
+ const captureScriptPath = join26(sandboxDir, "playwright.capture.mjs");
7515
+ writeFileSync15(captureScriptPath, captureScriptTemplate(), "utf-8");
7263
7516
  ensurePlaywrightInstalled(sandboxDir, json);
7264
- const result = childProcess4.spawnSync(
7517
+ const result = childProcess5.spawnSync(
7265
7518
  "npx",
7266
7519
  ["--no-install", "node", "playwright.capture.mjs"],
7267
7520
  {
@@ -7280,9 +7533,9 @@ function runCapture(sandboxDir, configPath, sandboxName, json) {
7280
7533
  ${result.stderr}`);
7281
7534
  return;
7282
7535
  }
7283
- const pendingDir = join24(sandboxDir, "captures", "pending");
7284
- const diffsDir = join24(sandboxDir, "captures", "diffs");
7285
- const approvedDir = join24(sandboxDir, "captures", "approved");
7536
+ const pendingDir = join26(sandboxDir, "captures", "pending");
7537
+ const diffsDir = join26(sandboxDir, "captures", "diffs");
7538
+ const approvedDir = join26(sandboxDir, "captures", "approved");
7286
7539
  const pendingFiles = safeListPngs(pendingDir);
7287
7540
  const diffFiles = safeListPngs(diffsDir);
7288
7541
  const hasBaseline = safeListPngs(approvedDir).length > 0;
@@ -7319,9 +7572,9 @@ ${result.stderr}`);
7319
7572
  }
7320
7573
  }
7321
7574
  function runPromotion(sandboxDir, sandboxName, json) {
7322
- const pendingDir = join24(sandboxDir, "captures", "pending");
7323
- const approvedDir = join24(sandboxDir, "captures", "approved");
7324
- const diffsDir = join24(sandboxDir, "captures", "diffs");
7575
+ const pendingDir = join26(sandboxDir, "captures", "pending");
7576
+ const approvedDir = join26(sandboxDir, "captures", "approved");
7577
+ const diffsDir = join26(sandboxDir, "captures", "diffs");
7325
7578
  const pendingFiles = safeListPngs(pendingDir);
7326
7579
  if (pendingFiles.length === 0) {
7327
7580
  fail2(
@@ -7330,10 +7583,10 @@ function runPromotion(sandboxDir, sandboxName, json) {
7330
7583
  );
7331
7584
  return;
7332
7585
  }
7333
- mkdirSync10(approvedDir, { recursive: true });
7586
+ mkdirSync11(approvedDir, { recursive: true });
7334
7587
  const promoted = [];
7335
7588
  for (const src of pendingFiles) {
7336
- const dest = join24(approvedDir, basename6(src));
7589
+ const dest = join26(approvedDir, basename7(src));
7337
7590
  copyFileSync2(src, dest);
7338
7591
  promoted.push(dest);
7339
7592
  }
@@ -7353,10 +7606,10 @@ function runPromotion(sandboxDir, sandboxName, json) {
7353
7606
  }
7354
7607
  }
7355
7608
  function ensurePlaywrightInstalled(sandboxDir, json) {
7356
- const markerPath = join24(sandboxDir, ".playwright-installed");
7357
- if (existsSync23(markerPath)) return;
7609
+ const markerPath = join26(sandboxDir, ".playwright-installed");
7610
+ if (existsSync25(markerPath)) return;
7358
7611
  if (!json) logger.info("Installing Playwright Chromium (one-time)...");
7359
- const result = childProcess4.spawnSync(
7612
+ const result = childProcess5.spawnSync(
7360
7613
  "npx",
7361
7614
  ["--no-install", "playwright", "install", "chromium"],
7362
7615
  {
@@ -7370,11 +7623,11 @@ function ensurePlaywrightInstalled(sandboxDir, json) {
7370
7623
  if (typeof result.status === "number" && result.status !== 0) {
7371
7624
  throw new Error(`playwright install exited with code ${result.status}`);
7372
7625
  }
7373
- writeFileSync14(markerPath, (/* @__PURE__ */ new Date()).toISOString(), "utf-8");
7626
+ writeFileSync15(markerPath, (/* @__PURE__ */ new Date()).toISOString(), "utf-8");
7374
7627
  }
7375
7628
  function safeListPngs(dir) {
7376
- if (!existsSync23(dir)) return [];
7377
- return readdirSync14(dir).filter((f) => f.endsWith(".png")).map((f) => join24(dir, f));
7629
+ if (!existsSync25(dir)) return [];
7630
+ return readdirSync14(dir).filter((f) => f.endsWith(".png")).map((f) => join26(dir, f));
7378
7631
  }
7379
7632
  function tryParseJson(s) {
7380
7633
  try {
@@ -7392,15 +7645,372 @@ function fail2(json, message) {
7392
7645
  process.exit(1);
7393
7646
  }
7394
7647
 
7648
+ // src/commands/spawn.ts
7649
+ import {
7650
+ cpSync,
7651
+ existsSync as existsSync28,
7652
+ mkdirSync as mkdirSync12,
7653
+ readFileSync as readFileSync31,
7654
+ readdirSync as readdirSync16,
7655
+ rmSync as rmSync4,
7656
+ writeFileSync as writeFileSync16
7657
+ } from "fs";
7658
+ import { basename as basename8, isAbsolute as isAbsolute3, join as join29, resolve as resolve17 } from "path";
7659
+ import { homedir as homedir3 } from "os";
7660
+ import * as childProcess6 from "child_process";
7661
+ import { parse as parseYaml5 } from "yaml";
7662
+ import { generateThemeData as generateThemeData7 } from "@loworbitstudio/visor-theme-engine";
7663
+ import { nextjsAdapter as nextjsAdapter3 } from "@loworbitstudio/visor-theme-engine/adapters";
7664
+ import { validate as validate3 } from "@loworbitstudio/visor-theme-engine";
7665
+
7666
+ // src/lib/blessed-discovery.ts
7667
+ import { existsSync as existsSync27, readdirSync as readdirSync15 } from "fs";
7668
+ import { join as join28 } from "path";
7669
+
7670
+ // src/lib/blessed-manifest.ts
7671
+ import { existsSync as existsSync26, readFileSync as readFileSync30 } from "fs";
7672
+ import { join as join27 } from "path";
7673
+ import { z } from "zod";
7674
+ var BLESSED_MANIFEST_FILENAME = "blessed-manifest.json";
7675
+ var blessedManifestSchema = z.object({
7676
+ /** Blessed-build shape, e.g. `admin-ui`. Matched against the `{shape}` in `blessed:{shape}:{pattern}`. */
7677
+ shape: z.string().min(1),
7678
+ /** Pattern name within the shape, e.g. `organization-management`. */
7679
+ pattern: z.string().min(1),
7680
+ /** The theme the reference build was authored + captured against. */
7681
+ base_theme: z.string().min(1),
7682
+ /** Minimum Visor version this build is known to work with (semver range). */
7683
+ requires_visor: z.string().min(1),
7684
+ /** Path (relative to the build root) to the approved capture baseline. */
7685
+ captures_baseline: z.string().min(1),
7686
+ /** Three-gates disposition at the time the build was blessed. */
7687
+ three_gates_status: z.string().min(1)
7688
+ }).strict();
7689
+ function parseBlessedManifest(buildDir) {
7690
+ const manifestPath = join27(buildDir, BLESSED_MANIFEST_FILENAME);
7691
+ if (!existsSync26(manifestPath)) {
7692
+ return {
7693
+ ok: false,
7694
+ error: `this directory is not a blessed build (missing ${BLESSED_MANIFEST_FILENAME}); see docs/blessed-builds.md`
7695
+ };
7696
+ }
7697
+ let raw;
7698
+ try {
7699
+ raw = readFileSync30(manifestPath, "utf-8");
7700
+ } catch {
7701
+ return { ok: false, error: `could not read ${manifestPath}` };
7702
+ }
7703
+ let json;
7704
+ try {
7705
+ json = JSON.parse(raw);
7706
+ } catch (err) {
7707
+ const message = err instanceof Error ? err.message : "invalid JSON";
7708
+ return { ok: false, error: `invalid JSON in ${manifestPath}: ${message}` };
7709
+ }
7710
+ const result = blessedManifestSchema.safeParse(json);
7711
+ if (!result.success) {
7712
+ const issues = result.error.issues.map((issue) => {
7713
+ const path2 = issue.path.join(".") || "(root)";
7714
+ return `${path2}: ${issue.message}`;
7715
+ }).join("; ");
7716
+ return {
7717
+ ok: false,
7718
+ error: `invalid ${BLESSED_MANIFEST_FILENAME} at ${manifestPath}: ${issues}`
7719
+ };
7720
+ }
7721
+ return { ok: true, manifest: result.data };
7722
+ }
7723
+
7724
+ // src/lib/blessed-discovery.ts
7725
+ var WALK_EXCLUDE_DIRS = /* @__PURE__ */ new Set([
7726
+ "node_modules",
7727
+ ".next",
7728
+ ".git",
7729
+ "dist",
7730
+ ".turbo",
7731
+ "coverage",
7732
+ ".cache"
7733
+ ]);
7734
+ function discoverBlessedBuilds(blessedDir, maxDepth = 8) {
7735
+ const builds = [];
7736
+ const errors = [];
7737
+ if (!existsSync27(blessedDir)) {
7738
+ return { builds, errors };
7739
+ }
7740
+ walk2(blessedDir, 0);
7741
+ builds.sort((a, b) => {
7742
+ if (a.manifest.shape !== b.manifest.shape) {
7743
+ return a.manifest.shape.localeCompare(b.manifest.shape);
7744
+ }
7745
+ return a.manifest.pattern.localeCompare(b.manifest.pattern);
7746
+ });
7747
+ return { builds, errors };
7748
+ function walk2(dir, depth) {
7749
+ if (depth > maxDepth) return;
7750
+ let entries;
7751
+ try {
7752
+ entries = readdirSync15(dir, { withFileTypes: true });
7753
+ } catch {
7754
+ return;
7755
+ }
7756
+ const hasManifest = entries.some(
7757
+ (entry) => entry.isFile() && entry.name === BLESSED_MANIFEST_FILENAME
7758
+ );
7759
+ if (hasManifest) {
7760
+ const result = parseBlessedManifest(dir);
7761
+ if (result.ok) {
7762
+ builds.push({ dir, manifest: result.manifest });
7763
+ } else {
7764
+ errors.push({ dir, error: result.error });
7765
+ }
7766
+ return;
7767
+ }
7768
+ for (const entry of entries) {
7769
+ if (!entry.isDirectory()) continue;
7770
+ if (WALK_EXCLUDE_DIRS.has(entry.name)) continue;
7771
+ walk2(join28(dir, entry.name), depth + 1);
7772
+ }
7773
+ }
7774
+ }
7775
+ function resolveBlessedBuild(blessedDir, shape, pattern2) {
7776
+ const { builds } = discoverBlessedBuilds(blessedDir);
7777
+ const build = builds.find(
7778
+ (candidate) => candidate.manifest.shape === shape && candidate.manifest.pattern === pattern2
7779
+ );
7780
+ return { build, available: builds };
7781
+ }
7782
+
7783
+ // src/commands/spawn.ts
7784
+ var DEFAULT_BLESSED_DIR = join29(
7785
+ homedir3(),
7786
+ "Code",
7787
+ "low-orbit",
7788
+ "low-orbit-playbook",
7789
+ "design-prototypes"
7790
+ );
7791
+ var FORK_EXCLUDE = /* @__PURE__ */ new Set([
7792
+ "node_modules",
7793
+ ".next",
7794
+ ".git",
7795
+ "dist",
7796
+ ".turbo",
7797
+ ".cache",
7798
+ "coverage",
7799
+ ".DS_Store",
7800
+ "tsconfig.tsbuildinfo"
7801
+ ]);
7802
+ function parseBlessedIdentifier(from) {
7803
+ const PREFIX = "blessed:";
7804
+ if (!from.startsWith(PREFIX)) {
7805
+ throw new Error(
7806
+ `Invalid --from '${from}'. Expected the form blessed:{shape}:{pattern} (e.g. blessed:admin-ui:organization-management).`
7807
+ );
7808
+ }
7809
+ const rest = from.slice(PREFIX.length);
7810
+ const parts = rest.split(":");
7811
+ if (parts.length !== 2 || parts[0].length === 0 || parts[1].length === 0) {
7812
+ throw new Error(
7813
+ `Invalid --from '${from}'. Expected the form blessed:{shape}:{pattern} (e.g. blessed:admin-ui:organization-management).`
7814
+ );
7815
+ }
7816
+ return { shape: parts[0], pattern: parts[1] };
7817
+ }
7818
+ function resolveBlessedDir(cwd, options) {
7819
+ 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;
7820
+ return isAbsolute3(raw) ? raw : resolve17(cwd, raw);
7821
+ }
7822
+ function resolveThemeFile(theme2, cwd, themeFile) {
7823
+ const candidates = [];
7824
+ if (themeFile) {
7825
+ candidates.push(isAbsolute3(themeFile) ? themeFile : resolve17(cwd, themeFile));
7826
+ }
7827
+ candidates.push(isAbsolute3(theme2) ? theme2 : resolve17(cwd, theme2));
7828
+ const privateRoot = process.env.VISOR_THEMES_PRIVATE_PATH;
7829
+ if (privateRoot && privateRoot.length > 0) {
7830
+ candidates.push(join29(privateRoot, "themes", theme2, "theme.visor.yaml"));
7831
+ }
7832
+ candidates.push(
7833
+ join29(cwd, "themes", `${theme2}.visor.yaml`),
7834
+ join29(cwd, "custom-themes", `${theme2}.visor.yaml`)
7835
+ );
7836
+ const found = candidates.find((candidate) => existsSync28(candidate));
7837
+ if (!found) {
7838
+ throw new Error(
7839
+ `Theme '${theme2}' not found (searched: ${candidates.join(", ")}). Pass --theme-file <path>, use a direct path, or set VISOR_THEMES_PRIVATE_PATH.`
7840
+ );
7841
+ }
7842
+ return { path: found, searched: candidates };
7843
+ }
7844
+ function resolveGlobalsCssPath(outputDir) {
7845
+ const candidates = [
7846
+ join29(outputDir, "app", "globals.css"),
7847
+ join29(outputDir, "src", "app", "globals.css")
7848
+ ];
7849
+ const existing = candidates.find((candidate) => existsSync28(candidate));
7850
+ return existing ?? candidates[0];
7851
+ }
7852
+ function applyThemeToFork(themeFile, globalsOut) {
7853
+ const yaml = readFileSync31(themeFile, "utf-8");
7854
+ const data = generateThemeData7(yaml);
7855
+ const css = nextjsAdapter3(
7856
+ { primitives: data.primitives, tokens: data.tokens, config: data.config },
7857
+ {}
7858
+ );
7859
+ mkdirSync12(join29(globalsOut, ".."), { recursive: true });
7860
+ writeFileSync16(globalsOut, css, "utf-8");
7861
+ }
7862
+ function runNpmInstall2(outputDir, json) {
7863
+ const result = childProcess6.spawnSync("npm", ["install", "--no-audit", "--no-fund"], {
7864
+ cwd: outputDir,
7865
+ stdio: json ? "ignore" : "inherit"
7866
+ });
7867
+ if (result.error) {
7868
+ throw new Error(`npm install failed to start: ${result.error.message}`);
7869
+ }
7870
+ if (typeof result.status === "number" && result.status !== 0) {
7871
+ throw new Error(`npm install exited with code ${result.status}`);
7872
+ }
7873
+ }
7874
+ function runSpawn(cwd, options) {
7875
+ if (!options.from) throw new Error("Missing required --from blessed:{shape}:{pattern}.");
7876
+ if (!options.theme) throw new Error("Missing required --theme <id>.");
7877
+ if (!options.output) throw new Error("Missing required --output <path>.");
7878
+ const { shape, pattern: pattern2 } = parseBlessedIdentifier(options.from);
7879
+ const blessedDir = resolveBlessedDir(cwd, options);
7880
+ const { build, available } = resolveBlessedBuild(blessedDir, shape, pattern2);
7881
+ if (!build) {
7882
+ const list = available.length > 0 ? available.map((b) => ` - blessed:${b.manifest.shape}:${b.manifest.pattern}`).join("\n") : " (none found)";
7883
+ throw new Error(
7884
+ `No blessed build found for blessed:${shape}:${pattern2} under ${blessedDir}.
7885
+ Available builds:
7886
+ ${list}`
7887
+ );
7888
+ }
7889
+ const outputDir = isAbsolute3(options.output) ? options.output : resolve17(cwd, options.output);
7890
+ if (existsSync28(outputDir) && readdirSync16(outputDir).length > 0) {
7891
+ throw new Error(`Output directory already exists and is not empty: ${outputDir}`);
7892
+ }
7893
+ const { path: themeFile } = resolveThemeFile(options.theme, cwd, options.themeFile);
7894
+ cpSync(build.dir, outputDir, {
7895
+ recursive: true,
7896
+ filter: (src) => !FORK_EXCLUDE.has(basename8(src))
7897
+ });
7898
+ let validated = false;
7899
+ let installed = false;
7900
+ try {
7901
+ const globalsOut = resolveGlobalsCssPath(outputDir);
7902
+ applyThemeToFork(themeFile, globalsOut);
7903
+ if (options.validate) {
7904
+ const parsed = parseYaml5(readFileSync31(themeFile, "utf-8"));
7905
+ const result = validate3(parsed);
7906
+ if (!result.valid) {
7907
+ const messages = result.errors.map((e) => e.message).join("; ");
7908
+ throw new Error(`Theme validation failed: ${messages}`);
7909
+ }
7910
+ validated = true;
7911
+ }
7912
+ if (options.install) {
7913
+ runNpmInstall2(outputDir, options.json ?? false);
7914
+ installed = true;
7915
+ }
7916
+ } catch (err) {
7917
+ rmSync4(outputDir, { recursive: true, force: true });
7918
+ throw err;
7919
+ }
7920
+ return {
7921
+ success: true,
7922
+ build: {
7923
+ shape: build.manifest.shape,
7924
+ pattern: build.manifest.pattern,
7925
+ requiresVisor: build.manifest.requires_visor,
7926
+ source: build.dir
7927
+ },
7928
+ output: outputDir,
7929
+ themeApplied: options.theme,
7930
+ themeFile,
7931
+ installed,
7932
+ validated
7933
+ };
7934
+ }
7935
+ function listBlessed(cwd, options) {
7936
+ const blessedDir = resolveBlessedDir(cwd, options);
7937
+ const { builds, errors } = discoverBlessedBuilds(blessedDir);
7938
+ if (options.json) {
7939
+ console.log(
7940
+ JSON.stringify({
7941
+ success: true,
7942
+ blessedDir,
7943
+ builds: builds.map((b) => ({
7944
+ from: `blessed:${b.manifest.shape}:${b.manifest.pattern}`,
7945
+ shape: b.manifest.shape,
7946
+ pattern: b.manifest.pattern,
7947
+ base_theme: b.manifest.base_theme,
7948
+ requires_visor: b.manifest.requires_visor,
7949
+ three_gates_status: b.manifest.three_gates_status,
7950
+ dir: b.dir
7951
+ })),
7952
+ errors
7953
+ })
7954
+ );
7955
+ return;
7956
+ }
7957
+ logger.heading(`Blessed builds under ${blessedDir}`);
7958
+ if (builds.length === 0) {
7959
+ logger.info(" (none found)");
7960
+ }
7961
+ for (const b of builds) {
7962
+ logger.success(`blessed:${b.manifest.shape}:${b.manifest.pattern}`);
7963
+ logger.item(`base theme: ${b.manifest.base_theme} \xB7 requires visor ${b.manifest.requires_visor} \xB7 gates: ${b.manifest.three_gates_status}`);
7964
+ }
7965
+ for (const e of errors) {
7966
+ logger.warn(`Skipped ${e.dir}: ${e.error}`);
7967
+ }
7968
+ }
7969
+ function spawnCommand(cwd, options) {
7970
+ const json = options.json ?? false;
7971
+ if (options.listBlessed) {
7972
+ try {
7973
+ listBlessed(cwd, options);
7974
+ } catch (err) {
7975
+ const message = err instanceof Error ? err.message : String(err);
7976
+ if (json) console.log(JSON.stringify({ success: false, error: message }));
7977
+ else logger.error(message);
7978
+ process.exit(1);
7979
+ }
7980
+ return;
7981
+ }
7982
+ try {
7983
+ const result = runSpawn(cwd, options);
7984
+ if (json) {
7985
+ console.log(JSON.stringify(result));
7986
+ return;
7987
+ }
7988
+ logger.success(
7989
+ `Discovered blessed build: ${result.build.shape}/${result.build.pattern} (requires visor ${result.build.requiresVisor})`
7990
+ );
7991
+ logger.success(`Forked to ${result.output} (excluded node_modules, .next, .git)`);
7992
+ logger.success(`Theme applied: ${result.themeApplied} \u2192 ${result.output}`);
7993
+ if (result.validated) logger.success("Theme validated");
7994
+ if (result.installed) logger.success("Installed dependencies");
7995
+ logger.blank();
7996
+ logger.info(`Next: cd ${result.output} && npm run dev`);
7997
+ } catch (err) {
7998
+ const message = err instanceof Error ? err.message : String(err);
7999
+ if (json) console.log(JSON.stringify({ success: false, error: message }));
8000
+ else logger.error(message);
8001
+ process.exit(1);
8002
+ }
8003
+ }
8004
+
7395
8005
  // src/index.ts
7396
- var __dirname2 = dirname10(fileURLToPath4(import.meta.url));
8006
+ var __dirname2 = dirname11(fileURLToPath4(import.meta.url));
7397
8007
  var pkg = JSON.parse(
7398
- readFileSync28(join25(__dirname2, "..", "package.json"), "utf-8")
8008
+ readFileSync32(join30(__dirname2, "..", "package.json"), "utf-8")
7399
8009
  );
7400
8010
  var program = new Command2();
7401
8011
  program.name("visor").description("CLI for the Visor design system").version(pkg.version);
7402
8012
  program.addCommand(checkCommand());
7403
- program.command("init").description("Initialize Visor \u2014 with --template nextjs, scaffolds a complete runnable Borealis-native Next.js app in one command").option("--template <name>", "scaffold a complete runnable app (templates: nextjs)").option("--json", "output structured JSON (for AI agents)").action((options) => {
8013
+ program.command("init").description("Initialize Visor \u2014 with --template nextjs, scaffolds a complete runnable Borealis-native Next.js app in one command").option("--template <name>", "scaffold a complete runnable app (templates: nextjs)").option("--for <play>", "bootstrap a Playbook play (pattern-build, new-web-app, feature-addition)").option("--play-name <name>", "name for the play instance (default: current directory name)").option("--theme <id>", "theme id to record in the play's state metadata").option("--from <ref>", "brief source for the play (e.g. a PL-N Linear ticket)").option("--json", "output structured JSON (for AI agents)").action((options) => {
7404
8014
  initCommand(process.cwd(), options);
7405
8015
  });
7406
8016
  program.command("list").description("List all available registry items").option("--json", "output structured JSON (for AI agents)").option("--category <name>", "filter items by category").action((options) => {
@@ -7585,4 +8195,9 @@ sandbox.command("approve").description(
7585
8195
  sandboxApproveCommand(process.cwd(), options);
7586
8196
  }
7587
8197
  );
8198
+ program.command("spawn").description(
8199
+ "Fork a Playbook blessed reference build with atomic theme re-skinning \u2014 one command replaces cp -R + npm install + theme apply"
8200
+ ).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) => {
8201
+ spawnCommand(process.cwd(), options);
8202
+ });
7588
8203
  program.parse();