@loworbitstudio/visor 1.13.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,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { readFileSync as readFileSync27 } 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
 
9
9
  // src/commands/check.ts
10
10
  import { Command } from "commander";
11
11
  import { statSync as statSync3 } from "fs";
12
- import { resolve as resolve3, dirname as dirname2 } from "path";
12
+ import { resolve as resolve4, dirname as dirname2 } from "path";
13
13
 
14
14
  // src/registry/resolve.ts
15
15
  import { readFileSync } from "fs";
@@ -867,6 +867,62 @@ function scanDesign(pathArg, options = {}) {
867
867
  };
868
868
  }
869
869
 
870
+ // src/check/theme-mode.ts
871
+ import { readFileSync as readFileSync4 } from "fs";
872
+ import { resolve as resolve3 } from "path";
873
+ import {
874
+ generateThemeData,
875
+ parseColor,
876
+ getLuminance
877
+ } from "@loworbitstudio/visor-theme-engine";
878
+ var LUMINANCE_THRESHOLD = 0.2;
879
+ function checkThemeModeSource(yamlSource, threshold = LUMINANCE_THRESHOLD) {
880
+ const data = generateThemeData(yamlSource);
881
+ const mode = data.config["color-scheme"];
882
+ const theme2 = data.config.name;
883
+ if (mode === "adaptive") {
884
+ return {
885
+ pass: true,
886
+ skipped: true,
887
+ mode,
888
+ theme: theme2,
889
+ computed_bg: null,
890
+ luminance: null,
891
+ threshold,
892
+ reason: "adaptive theme \u2014 supports both modes, no single background to assert"
893
+ };
894
+ }
895
+ const page = data.tokens.surface.page;
896
+ const computed_bg = mode === "dark-only" ? page.dark : page.light;
897
+ const parsed = parseColor(computed_bg);
898
+ if (!parsed) {
899
+ throw new Error(
900
+ `Could not parse app-root background color "${computed_bg}" for theme "${theme2}".`
901
+ );
902
+ }
903
+ const luminance = getLuminance(parsed.rgb[0], parsed.rgb[1], parsed.rgb[2]);
904
+ const isDark = luminance < threshold;
905
+ const pass = mode === "dark-only" ? isDark : !isDark;
906
+ const declaredWord = mode === "dark-only" ? "dark" : "light";
907
+ const actualWord = isDark ? "dark" : "light";
908
+ const reason = pass ? `theme declares ${mode}; app-root background renders ${actualWord} (luminance ${luminance.toFixed(4)}) \u2014 matches` : `theme declares ${mode} but app-root background "${computed_bg}" renders ${actualWord} (luminance ${luminance.toFixed(4)}, threshold ${threshold}) \u2014 expected ${declaredWord}`;
909
+ return {
910
+ pass,
911
+ skipped: false,
912
+ mode,
913
+ theme: theme2,
914
+ computed_bg,
915
+ luminance,
916
+ threshold,
917
+ reason
918
+ };
919
+ }
920
+ function checkThemeModeFile(filePath, threshold = LUMINANCE_THRESHOLD) {
921
+ const abs = resolve3(filePath);
922
+ const source = readFileSync4(abs, "utf-8");
923
+ return checkThemeModeSource(source, threshold);
924
+ }
925
+
870
926
  // src/utils/logger.ts
871
927
  import pc from "picocolors";
872
928
  var logger = {
@@ -1026,7 +1082,7 @@ async function checkDiffCommand(pathArg, options) {
1026
1082
  if (options.failOnHits) process.exit(1);
1027
1083
  }
1028
1084
  function checkDesignCommand(pathArg, options) {
1029
- const absPath = resolve3(pathArg);
1085
+ const absPath = resolve4(pathArg);
1030
1086
  const rcDir = statSync3(absPath).isDirectory() ? absPath : dirname2(absPath);
1031
1087
  const rc = loadVisorRc(rcDir);
1032
1088
  const result = scanDesign(absPath, {
@@ -1077,6 +1133,43 @@ function checkDesignCommand(pathArg, options) {
1077
1133
  logger.blank();
1078
1134
  if (!options.noFail && summary.errorCount > 0) process.exit(1);
1079
1135
  }
1136
+ function checkThemeModeCommand(pathArg, options) {
1137
+ let result;
1138
+ try {
1139
+ result = checkThemeModeFile(pathArg);
1140
+ } catch (err) {
1141
+ const message = err instanceof Error ? err.message : String(err);
1142
+ const useJson2 = options.json || options.format === "json";
1143
+ if (useJson2) {
1144
+ console.log(JSON.stringify({ success: false, error: message }, null, 2));
1145
+ } else {
1146
+ logger.error(message);
1147
+ }
1148
+ process.exit(1);
1149
+ return;
1150
+ }
1151
+ const useJson = options.json || options.format === "json";
1152
+ const shouldFail = options.fail !== false;
1153
+ if (useJson) {
1154
+ console.log(JSON.stringify({ success: true, ...result }, null, 2));
1155
+ if (shouldFail && !result.pass) process.exit(1);
1156
+ process.exit(0);
1157
+ return;
1158
+ }
1159
+ if (result.skipped) {
1160
+ logger.info(`\u20DD skipped \u2014 ${result.theme}: ${result.reason}`);
1161
+ process.exit(0);
1162
+ return;
1163
+ }
1164
+ if (result.pass) {
1165
+ logger.success(`${result.theme} \u2014 ${result.reason}`);
1166
+ process.exit(0);
1167
+ return;
1168
+ }
1169
+ logger.error(`${result.theme} \u2014 ${result.reason}`);
1170
+ logger.item(` offending background: ${pc2.cyan(result.computed_bg ?? "unknown")} (luminance ${result.luminance?.toFixed(4)})`);
1171
+ if (shouldFail) process.exit(1);
1172
+ }
1080
1173
  function checkCommand() {
1081
1174
  const check = new Command("check").description("Check Visor catalog \u2014 list items, test existence, scan JSX for native HTML");
1082
1175
  check.command("list").description("List all catalog items (components, blocks, hooks, patterns)").option("--type <type>", "filter by type: ui, blocks, hooks, patterns, all (default: all)").option("--json", "output structured JSON (for AI agents)").action((options) => {
@@ -1091,17 +1184,20 @@ function checkCommand() {
1091
1184
  check.command("design").description("Scan frontend code for Borealis design anti-patterns (deterministic, no LLM)").argument("<path>", "file path or directory to scan").option("--format <format>", "output format: json or human (default: human when TTY, json otherwise)").option("--errors-only", "report only error-severity rules (skip warnings)").option("--no-fail", "do not exit 1 on errors (advisory mode)").option("--json", "shorthand for --format json").action((pathArg, options) => {
1092
1185
  checkDesignCommand(pathArg, options);
1093
1186
  });
1187
+ check.command("theme-mode").description("Assert a theme's rendered app-root background matches its declared color-scheme (dark-only/light-only/adaptive)").argument("<path>", "path to a .visor.yaml theme file").option("--format <format>", "output format: json or human (default: human)").option("--no-fail", "do not exit 1 on a mode mismatch (advisory mode)").option("--json", "shorthand for --format json").action((pathArg, options) => {
1188
+ checkThemeModeCommand(pathArg, options);
1189
+ });
1094
1190
  return check;
1095
1191
  }
1096
1192
 
1097
1193
  // src/commands/init.ts
1098
- import { existsSync as existsSync4, writeFileSync as writeFileSync2, mkdirSync, readFileSync as readFileSync6 } from "fs";
1099
- 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";
1100
1196
  import { fileURLToPath as fileURLToPath2 } from "url";
1101
- import * as childProcess from "child_process";
1197
+ import * as childProcess2 from "child_process";
1102
1198
 
1103
1199
  // src/config/config.ts
1104
- import { readFileSync as readFileSync4, writeFileSync, existsSync as existsSync2 } from "fs";
1200
+ import { readFileSync as readFileSync5, writeFileSync, existsSync as existsSync2 } from "fs";
1105
1201
  import { join as join4 } from "path";
1106
1202
 
1107
1203
  // src/config/defaults.ts
@@ -1131,7 +1227,7 @@ function loadConfig(cwd) {
1131
1227
  `No ${CONFIG_FILE} found. Run "visor init" first.`
1132
1228
  );
1133
1229
  }
1134
- const raw = readFileSync4(configPath, "utf-8");
1230
+ const raw = readFileSync5(configPath, "utf-8");
1135
1231
  const parsed = JSON.parse(raw);
1136
1232
  const knownKeys = /* @__PURE__ */ new Set(["paths"]);
1137
1233
  for (const key of Object.keys(parsed)) {
@@ -1168,12 +1264,12 @@ function writeConfig(cwd, config) {
1168
1264
 
1169
1265
  // src/utils/packages.ts
1170
1266
  import { execFileSync } from "child_process";
1171
- import { existsSync as existsSync3, readFileSync as readFileSync5 } from "fs";
1267
+ import { existsSync as existsSync3, readFileSync as readFileSync6 } from "fs";
1172
1268
  import { join as join5 } from "path";
1173
1269
  function readPackageJson(cwd) {
1174
1270
  const pkgPath = join5(cwd, "package.json");
1175
1271
  if (!existsSync3(pkgPath)) return null;
1176
- return JSON.parse(readFileSync5(pkgPath, "utf-8"));
1272
+ return JSON.parse(readFileSync6(pkgPath, "utf-8"));
1177
1273
  }
1178
1274
  function isPackageInstalled(packageName, cwd) {
1179
1275
  const pkg2 = readPackageJson(cwd);
@@ -1198,6 +1294,7 @@ function installPackages(packages, cwd, dev = false) {
1198
1294
  }
1199
1295
 
1200
1296
  // src/commands/templates/nextjs.ts
1297
+ import { parse as parseYaml } from "yaml";
1201
1298
  var NEXTJS_PINNED_VERSION = "15.1.6";
1202
1299
  var CREATE_NEXT_APP_FLAGS = [
1203
1300
  "--ts",
@@ -1211,11 +1308,24 @@ var CREATE_NEXT_APP_FLAGS = [
1211
1308
  ];
1212
1309
  var NEXTJS_STARTER_YAML = `name: my-app
1213
1310
  version: 1
1311
+ # color-scheme controls the mode the scaffold applies at the root <html>:
1312
+ # adaptive \u2014 follow the OS via prefers-color-scheme (default)
1313
+ # dark-only \u2014 always dark (adds className="dark" + color-scheme: dark)
1314
+ # light-only \u2014 always light
1315
+ color-scheme: adaptive
1214
1316
  colors:
1215
1317
  primary: "#2563EB"
1216
1318
  `;
1217
- function generateNextjsLayout() {
1218
- return `import "./globals.css";
1319
+ function extractColorScheme(yamlContent) {
1320
+ const parsed = parseYaml(yamlContent);
1321
+ const v = parsed?.["color-scheme"];
1322
+ if (v === "dark-only" || v === "light-only" || v === "adaptive") return v;
1323
+ return void 0;
1324
+ }
1325
+ function generateNextjsLayout(colorScheme) {
1326
+ const forcedMode = colorScheme === "dark-only" ? "dark" : colorScheme === "light-only" ? "light" : void 0;
1327
+ if (forcedMode === void 0) {
1328
+ return `import "./globals.css";
1219
1329
  import { FOWT_SCRIPT } from "@loworbitstudio/visor-theme-engine/fowt";
1220
1330
  import type { Metadata } from "next";
1221
1331
  import type { ReactNode } from "react";
@@ -1235,11 +1345,191 @@ export default function RootLayout({ children }: { children: ReactNode }) {
1235
1345
  </html>
1236
1346
  );
1237
1347
  }
1348
+ `;
1349
+ }
1350
+ return `import "./globals.css";
1351
+ import { generateFowtScript } from "@loworbitstudio/visor-theme-engine/fowt";
1352
+ import type { Metadata } from "next";
1353
+ import type { ReactNode } from "react";
1354
+
1355
+ export const metadata: Metadata = {
1356
+ title: "My Visor App",
1357
+ description: "Built with Visor \u2014 Low Orbit Studio's design system.",
1358
+ };
1359
+
1360
+ // Theme declares \`color-scheme: ${colorScheme}\` \u2014 force ${forcedMode} at the root.
1361
+ const FOWT_SCRIPT = generateFowtScript({ defaultTheme: "${forcedMode}" });
1362
+
1363
+ export default function RootLayout({ children }: { children: ReactNode }) {
1364
+ return (
1365
+ <html
1366
+ lang="en"
1367
+ className="${forcedMode}"
1368
+ style={{ colorScheme: "${forcedMode}" }}
1369
+ suppressHydrationWarning
1370
+ >
1371
+ <head>
1372
+ <script>{FOWT_SCRIPT}</script>
1373
+ </head>
1374
+ <body>{children}</body>
1375
+ </html>
1376
+ );
1377
+ }
1238
1378
  `;
1239
1379
  }
1240
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
+
1241
1531
  // src/commands/init.ts
1242
- import { generateThemeData } from "@loworbitstudio/visor-theme-engine";
1532
+ import { generateThemeData as generateThemeData2 } from "@loworbitstudio/visor-theme-engine";
1243
1533
  import { nextjsAdapter } from "@loworbitstudio/visor-theme-engine/adapters";
1244
1534
  function initCommand(cwd, options) {
1245
1535
  const json = options?.json ?? false;
@@ -1250,7 +1540,27 @@ function initCommand(cwd, options) {
1250
1540
  emitError(json, `Unknown template: ${options.template}. Available templates: nextjs`);
1251
1541
  process.exit(1);
1252
1542
  }
1253
- 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"))) {
1254
1564
  emitError(
1255
1565
  json,
1256
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"
@@ -1291,6 +1601,10 @@ function initCommand(cwd, options) {
1291
1601
  }
1292
1602
  }
1293
1603
  }
1604
+ let playResult;
1605
+ if (playDef) {
1606
+ playResult = runPlayInit(cwd, playDef, playName, options ?? {}, json, warnings);
1607
+ }
1294
1608
  if (json) {
1295
1609
  const nextSteps = buildNextSteps(options, warnings);
1296
1610
  const result = {
@@ -1298,12 +1612,91 @@ function initCommand(cwd, options) {
1298
1612
  config: DEFAULT_CONFIG,
1299
1613
  files: { created: filesCreated, skipped: filesSkipped },
1300
1614
  warnings,
1301
- nextSteps
1615
+ nextSteps,
1616
+ ...playResult ? { play: playResult } : {}
1302
1617
  };
1303
1618
  console.log(JSON.stringify(result, null, 2));
1304
1619
  process.exit(0);
1305
1620
  }
1306
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
+ }
1307
1700
  function buildNextSteps(options, warnings) {
1308
1701
  const steps = [];
1309
1702
  if (options?.template === "nextjs") {
@@ -1333,58 +1726,59 @@ function scaffoldNextjs(cwd, json, filesCreated, filesSkipped, warnings) {
1333
1726
  }
1334
1727
  runCreateNextApp(cwd, json);
1335
1728
  runInstallVisorDeps(cwd, json);
1336
- const yamlPath = join6(cwd, ".visor.yaml");
1337
- if (existsSync4(yamlPath)) {
1729
+ const yamlPath = join8(cwd, ".visor.yaml");
1730
+ if (existsSync6(yamlPath)) {
1338
1731
  filesSkipped.push(".visor.yaml");
1339
1732
  if (!json) {
1340
1733
  logger.warn(".visor.yaml already exists. Skipping.");
1341
1734
  }
1342
1735
  } else {
1343
- writeFileSync2(yamlPath, NEXTJS_STARTER_YAML, "utf-8");
1736
+ writeFileSync3(yamlPath, NEXTJS_STARTER_YAML, "utf-8");
1344
1737
  filesCreated.push(".visor.yaml");
1345
1738
  if (!json) {
1346
1739
  logger.success("Created .visor.yaml");
1347
1740
  }
1348
1741
  }
1349
- const data = generateThemeData(NEXTJS_STARTER_YAML);
1742
+ const data = generateThemeData2(NEXTJS_STARTER_YAML);
1350
1743
  const css = nextjsAdapter({
1351
1744
  primitives: data.primitives,
1352
1745
  tokens: data.tokens,
1353
1746
  config: data.config
1354
1747
  });
1355
- const globalsPath = join6(cwd, "app", "globals.css");
1356
- const globalsDir = dirname3(globalsPath);
1357
- mkdirSync(globalsDir, { recursive: true });
1358
- if (existsSync4(globalsPath)) {
1359
- 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");
1360
1753
  filesCreated.push("app/globals.css");
1361
1754
  } else {
1362
- writeFileSync2(globalsPath, css, "utf-8");
1755
+ writeFileSync3(globalsPath, css, "utf-8");
1363
1756
  filesCreated.push("app/globals.css");
1364
1757
  }
1365
1758
  if (!json) {
1366
1759
  logger.success("Created app/globals.css with theme tokens");
1367
1760
  }
1368
- const layoutPath = join6(cwd, "app", "layout.tsx");
1369
- writeFileSync2(layoutPath, generateNextjsLayout(), "utf-8");
1761
+ const layoutPath = join8(cwd, "app", "layout.tsx");
1762
+ const colorScheme = extractColorScheme(NEXTJS_STARTER_YAML);
1763
+ writeFileSync3(layoutPath, generateNextjsLayout(colorScheme), "utf-8");
1370
1764
  filesCreated.push("app/layout.tsx");
1371
1765
  if (!json) {
1372
1766
  logger.success("Wired app/layout.tsx with FOWT prevention and theme tokens");
1373
1767
  }
1374
- const stampDir = join6(cwd, ".lo");
1375
- const stampPath = join6(stampDir, "borealis.json");
1376
- if (existsSync4(stampPath)) {
1768
+ const stampDir = join8(cwd, ".lo");
1769
+ const stampPath = join8(stampDir, "borealis.json");
1770
+ if (existsSync6(stampPath)) {
1377
1771
  filesSkipped.push(".lo/borealis.json");
1378
1772
  if (!json) {
1379
1773
  logger.warn(".lo/borealis.json already exists. Skipping.");
1380
1774
  }
1381
1775
  } else {
1382
- mkdirSync(stampDir, { recursive: true });
1776
+ mkdirSync2(stampDir, { recursive: true });
1383
1777
  const stamp = {
1384
1778
  visorVersion: readVisorCliVersion(),
1385
1779
  initializedAt: (/* @__PURE__ */ new Date()).toISOString()
1386
1780
  };
1387
- writeFileSync2(stampPath, JSON.stringify(stamp, null, 2) + "\n", "utf-8");
1781
+ writeFileSync3(stampPath, JSON.stringify(stamp, null, 2) + "\n", "utf-8");
1388
1782
  filesCreated.push(".lo/borealis.json");
1389
1783
  if (!json) {
1390
1784
  logger.success("Stamped .lo/borealis.json");
@@ -1405,7 +1799,7 @@ function runCreateNextApp(cwd, json) {
1405
1799
  if (!json) {
1406
1800
  logger.info(`Running create-next-app@${NEXTJS_PINNED_VERSION}...`);
1407
1801
  }
1408
- const result = childProcess.spawnSync(
1802
+ const result = childProcess2.spawnSync(
1409
1803
  "npx",
1410
1804
  [`create-next-app@${NEXTJS_PINNED_VERSION}`, ".", ...CREATE_NEXT_APP_FLAGS],
1411
1805
  { cwd, stdio: json ? "ignore" : "inherit" }
@@ -1416,7 +1810,7 @@ function runInstallVisorDeps(cwd, json) {
1416
1810
  if (!json) {
1417
1811
  logger.info("Installing @loworbitstudio/visor-core and visor-theme-engine...");
1418
1812
  }
1419
- const result = childProcess.spawnSync(
1813
+ const result = childProcess2.spawnSync(
1420
1814
  "npm",
1421
1815
  [
1422
1816
  "install",
@@ -1437,12 +1831,12 @@ function assertSpawnSuccess(result, label) {
1437
1831
  }
1438
1832
  function readVisorCliVersion() {
1439
1833
  try {
1440
- const here = dirname3(fileURLToPath2(import.meta.url));
1834
+ const here = dirname4(fileURLToPath2(import.meta.url));
1441
1835
  for (let i = 0; i < 5; i++) {
1442
1836
  const segments = new Array(i).fill("..");
1443
- const candidate = join6(here, ...segments, "package.json");
1837
+ const candidate = join8(here, ...segments, "package.json");
1444
1838
  try {
1445
- const pkg2 = JSON.parse(readFileSync6(candidate, "utf-8"));
1839
+ const pkg2 = JSON.parse(readFileSync9(candidate, "utf-8"));
1446
1840
  if (pkg2.name === "@loworbitstudio/visor" && pkg2.version) {
1447
1841
  return pkg2.version;
1448
1842
  }
@@ -1456,53 +1850,53 @@ function readVisorCliVersion() {
1456
1850
 
1457
1851
  // src/utils/fs.ts
1458
1852
  import {
1459
- writeFileSync as writeFileSync3,
1460
- readFileSync as readFileSync7,
1461
- existsSync as existsSync5,
1462
- mkdirSync as mkdirSync2
1853
+ writeFileSync as writeFileSync4,
1854
+ readFileSync as readFileSync10,
1855
+ existsSync as existsSync7,
1856
+ mkdirSync as mkdirSync3
1463
1857
  } from "fs";
1464
- import { dirname as dirname4, join as join7 } from "path";
1858
+ import { dirname as dirname5, join as join9 } from "path";
1465
1859
  function resolveOutputPath(registryPath, type, config, cwd) {
1466
1860
  let relativePath;
1467
1861
  if (type === "registry:block") {
1468
1862
  relativePath = registryPath.replace(/^blocks\//, "");
1469
- return join7(cwd, config.paths.blocks, relativePath);
1863
+ return join9(cwd, config.paths.blocks, relativePath);
1470
1864
  }
1471
1865
  if (type === "registry:ui") {
1472
1866
  if (registryPath.startsWith("components/deck/")) {
1473
1867
  relativePath = registryPath.replace(/^components\/deck\//, "");
1474
- return join7(cwd, config.paths.deckComponents, relativePath);
1868
+ return join9(cwd, config.paths.deckComponents, relativePath);
1475
1869
  }
1476
1870
  if (registryPath.startsWith("components/flutter/")) {
1477
1871
  relativePath = registryPath.replace(/^components\/flutter\//, "");
1478
- return join7(cwd, config.paths.flutterComponents, relativePath);
1872
+ return join9(cwd, config.paths.flutterComponents, relativePath);
1479
1873
  }
1480
1874
  relativePath = registryPath.replace(/^components\/ui\//, "");
1481
- return join7(cwd, config.paths.components, relativePath);
1875
+ return join9(cwd, config.paths.components, relativePath);
1482
1876
  }
1483
1877
  if (type === "registry:hook") {
1484
1878
  relativePath = registryPath.replace(/^hooks\//, "");
1485
- return join7(cwd, config.paths.hooks, relativePath);
1879
+ return join9(cwd, config.paths.hooks, relativePath);
1486
1880
  }
1487
1881
  if (type === "registry:lib") {
1488
1882
  relativePath = registryPath.replace(/^lib\//, "");
1489
- return join7(cwd, config.paths.lib, relativePath);
1883
+ return join9(cwd, config.paths.lib, relativePath);
1490
1884
  }
1491
- return join7(cwd, registryPath);
1885
+ return join9(cwd, registryPath);
1492
1886
  }
1493
1887
  function writeFile(filePath, content) {
1494
- const dir = dirname4(filePath);
1495
- if (!existsSync5(dir)) {
1496
- mkdirSync2(dir, { recursive: true });
1888
+ const dir = dirname5(filePath);
1889
+ if (!existsSync7(dir)) {
1890
+ mkdirSync3(dir, { recursive: true });
1497
1891
  }
1498
- writeFileSync3(filePath, content, "utf-8");
1892
+ writeFileSync4(filePath, content, "utf-8");
1499
1893
  }
1500
1894
  function readFile(filePath) {
1501
- if (!existsSync5(filePath)) return null;
1502
- return readFileSync7(filePath, "utf-8");
1895
+ if (!existsSync7(filePath)) return null;
1896
+ return readFileSync10(filePath, "utf-8");
1503
1897
  }
1504
1898
  function fileExists(filePath) {
1505
- return existsSync5(filePath);
1899
+ return existsSync7(filePath);
1506
1900
  }
1507
1901
 
1508
1902
  // src/commands/list.ts
@@ -1643,8 +2037,8 @@ function listCommand(cwd, options = {}) {
1643
2037
  }
1644
2038
 
1645
2039
  // src/utils/pubspec.ts
1646
- import { existsSync as existsSync6, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "fs";
1647
- 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";
1648
2042
  import { parseDocument, YAMLMap } from "yaml";
1649
2043
  function mergePubspec(pubspecText, deps) {
1650
2044
  const doc = parseDocument(pubspecText);
@@ -1666,14 +2060,14 @@ function mergePubspec(pubspecText, deps) {
1666
2060
  return { text: doc.toString(), added, skipped };
1667
2061
  }
1668
2062
  function pubspecPath(cwd) {
1669
- return join8(cwd, "pubspec.yaml");
2063
+ return join10(cwd, "pubspec.yaml");
1670
2064
  }
1671
2065
  function pubspecExists(cwd) {
1672
- return existsSync6(pubspecPath(cwd));
2066
+ return existsSync8(pubspecPath(cwd));
1673
2067
  }
1674
2068
  function isPubPackageInstalled(packageName, cwd) {
1675
2069
  if (!pubspecExists(cwd)) return false;
1676
- const text = readFileSync8(pubspecPath(cwd), "utf-8");
2070
+ const text = readFileSync11(pubspecPath(cwd), "utf-8");
1677
2071
  const doc = parseDocument(text);
1678
2072
  const depsNode = doc.get("dependencies");
1679
2073
  if (!(depsNode instanceof YAMLMap)) return false;
@@ -1684,24 +2078,24 @@ function getUninstalledPubDeps(deps, cwd) {
1684
2078
  }
1685
2079
  function addPubDependencies(deps, cwd) {
1686
2080
  const path2 = pubspecPath(cwd);
1687
- if (!existsSync6(path2)) {
2081
+ if (!existsSync8(path2)) {
1688
2082
  throw new Error(
1689
2083
  `No pubspec.yaml found at ${path2}. Run this command from a Flutter project root.`
1690
2084
  );
1691
2085
  }
1692
- const text = readFileSync8(path2, "utf-8");
2086
+ const text = readFileSync11(path2, "utf-8");
1693
2087
  const result = mergePubspec(text, deps);
1694
2088
  if (result.added.length > 0) {
1695
- writeFileSync4(path2, result.text, "utf-8");
2089
+ writeFileSync5(path2, result.text, "utf-8");
1696
2090
  }
1697
2091
  return result;
1698
2092
  }
1699
2093
 
1700
2094
  // src/utils/flutter.ts
1701
2095
  import { execFileSync as execFileSync2 } from "child_process";
1702
- import { existsSync as existsSync7, readdirSync as readdirSync3, statSync as statSync4 } from "fs";
1703
- import { homedir } from "os";
1704
- 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";
1705
2099
  function isExecutable(path2) {
1706
2100
  try {
1707
2101
  const s = statSync4(path2);
@@ -1716,20 +2110,20 @@ function fromPath(env) {
1716
2110
  const bin = process.platform === "win32" ? "flutter.bat" : "flutter";
1717
2111
  for (const dir of pathVar.split(sep2)) {
1718
2112
  if (!dir) continue;
1719
- const candidate = join9(dir, bin);
2113
+ const candidate = join11(dir, bin);
1720
2114
  if (isExecutable(candidate)) return candidate;
1721
2115
  }
1722
2116
  return null;
1723
2117
  }
1724
2118
  function fromFvm(home) {
1725
- const fvmDefault = join9(home, "fvm", "default", "bin", "flutter");
2119
+ const fvmDefault = join11(home, "fvm", "default", "bin", "flutter");
1726
2120
  if (isExecutable(fvmDefault)) return fvmDefault;
1727
- const versionsDir = join9(home, "fvm", "versions");
1728
- if (!existsSync7(versionsDir)) return null;
2121
+ const versionsDir = join11(home, "fvm", "versions");
2122
+ if (!existsSync9(versionsDir)) return null;
1729
2123
  let best = null;
1730
2124
  try {
1731
2125
  for (const name of readdirSync3(versionsDir)) {
1732
- const candidate = join9(versionsDir, name, "bin", "flutter");
2126
+ const candidate = join11(versionsDir, name, "bin", "flutter");
1733
2127
  if (!isExecutable(candidate)) continue;
1734
2128
  if (!best || compareSemver(name, best.version) > 0) {
1735
2129
  best = { version: name, path: candidate };
@@ -1752,10 +2146,10 @@ function compareSemver(a, b) {
1752
2146
  }
1753
2147
  function findFlutterBin(options = {}) {
1754
2148
  const env = options.env ?? process.env;
1755
- const home = options.home ?? homedir();
2149
+ const home = options.home ?? homedir2();
1756
2150
  const envRoot = env.FLUTTER_ROOT;
1757
2151
  if (envRoot) {
1758
- const bin = join9(envRoot, "bin", "flutter");
2152
+ const bin = join11(envRoot, "bin", "flutter");
1759
2153
  if (isExecutable(bin)) return bin;
1760
2154
  }
1761
2155
  const fromPathBin = fromPath(env);
@@ -2398,9 +2792,9 @@ function infoCommand(name, cwd, options = {}) {
2398
2792
  }
2399
2793
 
2400
2794
  // src/commands/theme-apply.ts
2401
- import { readFileSync as readFileSync9, writeFileSync as writeFileSync5, mkdirSync as mkdirSync3 } from "fs";
2402
- import { resolve as resolve4, dirname as dirname5, join as join10 } from "path";
2403
- import { generateTheme, generateThemeData as generateThemeData2 } from "@loworbitstudio/visor-theme-engine";
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";
2797
+ import { generateTheme, generateThemeData as generateThemeData3 } from "@loworbitstudio/visor-theme-engine";
2404
2798
  import {
2405
2799
  nextjsAdapter as nextjsAdapter2,
2406
2800
  fumadocsAdapter,
@@ -2429,10 +2823,10 @@ function defaultOutputPath(adapter, themeName) {
2429
2823
  }
2430
2824
  }
2431
2825
  function themeApplyCommand(file, cwd, options) {
2432
- const filePath = resolve4(cwd, file);
2826
+ const filePath = resolve5(cwd, file);
2433
2827
  let yamlContent;
2434
2828
  try {
2435
- yamlContent = readFileSync9(filePath, "utf-8");
2829
+ yamlContent = readFileSync12(filePath, "utf-8");
2436
2830
  } catch {
2437
2831
  if (options.json) {
2438
2832
  console.log(
@@ -2453,7 +2847,7 @@ function themeApplyCommand(file, cwd, options) {
2453
2847
  let sections;
2454
2848
  try {
2455
2849
  if (options.adapter) {
2456
- const data = generateThemeData2(yamlContent);
2850
+ const data = generateThemeData3(yamlContent);
2457
2851
  themeName = data.config.name;
2458
2852
  const adapterInput = {
2459
2853
  primitives: data.primitives,
@@ -2511,15 +2905,15 @@ function themeApplyCommand(file, cwd, options) {
2511
2905
  process.exit(1);
2512
2906
  }
2513
2907
  const outputTarget = options.output ?? defaultOutputPath(options.adapter, themeName);
2514
- const outputPath = resolve4(cwd, outputTarget);
2908
+ const outputPath = resolve5(cwd, outputTarget);
2515
2909
  if (fileMap) {
2516
2910
  try {
2517
- mkdirSync3(outputPath, { recursive: true });
2911
+ mkdirSync4(outputPath, { recursive: true });
2518
2912
  let totalBytes = 0;
2519
2913
  for (const [relPath, content] of Object.entries(fileMap.files)) {
2520
- const filePath2 = join10(outputPath, relPath);
2521
- mkdirSync3(dirname5(filePath2), { recursive: true });
2522
- writeFileSync5(filePath2, content, "utf-8");
2914
+ const filePath2 = join12(outputPath, relPath);
2915
+ mkdirSync4(dirname6(filePath2), { recursive: true });
2916
+ writeFileSync6(filePath2, content, "utf-8");
2523
2917
  totalBytes += content.length;
2524
2918
  }
2525
2919
  if (options.json) {
@@ -2556,10 +2950,10 @@ function themeApplyCommand(file, cwd, options) {
2556
2950
  if (css === null) {
2557
2951
  process.exit(1);
2558
2952
  }
2559
- const outputDir = dirname5(outputPath);
2953
+ const outputDir = dirname6(outputPath);
2560
2954
  try {
2561
- mkdirSync3(outputDir, { recursive: true });
2562
- writeFileSync5(outputPath, css, "utf-8");
2955
+ mkdirSync4(outputDir, { recursive: true });
2956
+ writeFileSync6(outputPath, css, "utf-8");
2563
2957
  } catch {
2564
2958
  if (options.json) {
2565
2959
  console.log(
@@ -2600,8 +2994,8 @@ function formatSize(bytes) {
2600
2994
  }
2601
2995
 
2602
2996
  // src/commands/theme-export.ts
2603
- import { readFileSync as readFileSync10 } from "fs";
2604
- import { resolve as resolve5 } from "path";
2997
+ import { readFileSync as readFileSync13 } from "fs";
2998
+ import { resolve as resolve6 } from "path";
2605
2999
  import {
2606
3000
  parseConfig,
2607
3001
  resolveConfig,
@@ -2609,10 +3003,10 @@ import {
2609
3003
  exportTheme
2610
3004
  } from "@loworbitstudio/visor-theme-engine";
2611
3005
  function themeExportCommand(file, cwd, options) {
2612
- const filePath = resolve5(cwd, file ?? ".visor.yaml");
3006
+ const filePath = resolve6(cwd, file ?? ".visor.yaml");
2613
3007
  let yamlContent;
2614
3008
  try {
2615
- yamlContent = readFileSync10(filePath, "utf-8");
3009
+ yamlContent = readFileSync13(filePath, "utf-8");
2616
3010
  } catch {
2617
3011
  if (options.json) {
2618
3012
  console.log(
@@ -2664,16 +3058,16 @@ function themeExportCommand(file, cwd, options) {
2664
3058
  }
2665
3059
 
2666
3060
  // src/commands/theme-validate.ts
2667
- import { readFileSync as readFileSync11 } from "fs";
2668
- import { resolve as resolve6 } from "path";
2669
- import { parse as parseYaml } from "yaml";
3061
+ import { readFileSync as readFileSync14 } from "fs";
3062
+ import { resolve as resolve7 } from "path";
3063
+ import { parse as parseYaml2 } from "yaml";
2670
3064
  import { validate } from "@loworbitstudio/visor-theme-engine";
2671
3065
  import pc3 from "picocolors";
2672
3066
  function themeValidateCommand(file, cwd, options) {
2673
- const filePath = resolve6(cwd, file);
3067
+ const filePath = resolve7(cwd, file);
2674
3068
  let fileContent;
2675
3069
  try {
2676
- fileContent = readFileSync11(filePath, "utf-8");
3070
+ fileContent = readFileSync14(filePath, "utf-8");
2677
3071
  } catch {
2678
3072
  if (options.json) {
2679
3073
  console.log(
@@ -2697,7 +3091,7 @@ function themeValidateCommand(file, cwd, options) {
2697
3091
  }
2698
3092
  let parsed;
2699
3093
  try {
2700
- parsed = parseYaml(fileContent);
3094
+ parsed = parseYaml2(fileContent);
2701
3095
  } catch (err) {
2702
3096
  const message = err instanceof Error ? err.message : "Invalid YAML syntax";
2703
3097
  if (options.json) {
@@ -2762,8 +3156,8 @@ function printIssue(issue) {
2762
3156
 
2763
3157
  // src/commands/theme-verify.ts
2764
3158
  import { spawnSync as _spawnSync } from "child_process";
2765
- import { existsSync as existsSync8 } from "fs";
2766
- import { resolve as resolve7 } from "path";
3159
+ import { existsSync as existsSync10 } from "fs";
3160
+ import { resolve as resolve8 } from "path";
2767
3161
  function themeVerifyCommand(dir, cwd, options, _spawnFn = _spawnSync) {
2768
3162
  const target = options.target ?? "flutter";
2769
3163
  if (target !== "flutter") {
@@ -2785,8 +3179,8 @@ function themeVerifyCommand(dir, cwd, options, _spawnFn = _spawnSync) {
2785
3179
  }
2786
3180
  process.exit(1);
2787
3181
  }
2788
- const dirPath = resolve7(cwd, dir);
2789
- if (!existsSync8(dirPath)) {
3182
+ const dirPath = resolve8(cwd, dir);
3183
+ if (!existsSync10(dirPath)) {
2790
3184
  if (options.json) {
2791
3185
  console.log(
2792
3186
  JSON.stringify({
@@ -2873,8 +3267,8 @@ function themeVerifyCommand(dir, cwd, options, _spawnFn = _spawnSync) {
2873
3267
  }
2874
3268
 
2875
3269
  // src/commands/theme-extract.ts
2876
- import { readFileSync as readFileSync12, writeFileSync as writeFileSync6, existsSync as existsSync9, readdirSync as readdirSync4, statSync as statSync5 } from "fs";
2877
- import { resolve as resolve8, 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";
2878
3272
  import { stringify as stringifyYaml } from "yaml";
2879
3273
  import {
2880
3274
  extractFromCSS,
@@ -2903,8 +3297,8 @@ var CSS_DIRS = [
2903
3297
  "packages/design-tokens"
2904
3298
  ];
2905
3299
  function themeExtractCommand(cwd, options) {
2906
- const targetDir = resolve8(cwd, options.from ?? ".");
2907
- if (!existsSync9(targetDir)) {
3300
+ const targetDir = resolve9(cwd, options.from ?? ".");
3301
+ if (!existsSync11(targetDir)) {
2908
3302
  if (options.json) {
2909
3303
  console.log(JSON.stringify({ success: false, error: `Directory not found: ${targetDir}` }));
2910
3304
  } else {
@@ -2958,16 +3352,16 @@ function collectCSSFiles(targetDir) {
2958
3352
  const files = [];
2959
3353
  const seen = /* @__PURE__ */ new Set();
2960
3354
  for (const pattern2 of CSS_FILE_PATTERNS) {
2961
- const rootPath = join11(targetDir, pattern2);
3355
+ const rootPath = join13(targetDir, pattern2);
2962
3356
  addFileIfExists(rootPath, files, seen);
2963
3357
  for (const dir of CSS_DIRS) {
2964
- const dirPath = join11(targetDir, dir, pattern2);
3358
+ const dirPath = join13(targetDir, dir, pattern2);
2965
3359
  addFileIfExists(dirPath, files, seen);
2966
3360
  }
2967
3361
  }
2968
3362
  for (const dir of CSS_DIRS) {
2969
- const dirPath = join11(targetDir, dir);
2970
- if (existsSync9(dirPath) && statSync5(dirPath).isDirectory()) {
3363
+ const dirPath = join13(targetDir, dir);
3364
+ if (existsSync11(dirPath) && statSync5(dirPath).isDirectory()) {
2971
3365
  scanDirForCSS(dirPath, files, seen, 2);
2972
3366
  }
2973
3367
  }
@@ -2975,11 +3369,11 @@ function collectCSSFiles(targetDir) {
2975
3369
  return files;
2976
3370
  }
2977
3371
  function addFileIfExists(filePath, files, seen) {
2978
- const resolved = resolve8(filePath);
3372
+ const resolved = resolve9(filePath);
2979
3373
  if (seen.has(resolved)) return;
2980
- if (!existsSync9(resolved)) return;
3374
+ if (!existsSync11(resolved)) return;
2981
3375
  try {
2982
- const content = readFileSync12(resolved, "utf-8");
3376
+ const content = readFileSync15(resolved, "utf-8");
2983
3377
  if (content.includes("--")) {
2984
3378
  files.push({ path: resolved, content });
2985
3379
  seen.add(resolved);
@@ -2988,7 +3382,7 @@ function addFileIfExists(filePath, files, seen) {
2988
3382
  }
2989
3383
  }
2990
3384
  function scanDirForCSS(dir, files, seen, maxDepth) {
2991
- if (!existsSync9(dir)) return;
3385
+ if (!existsSync11(dir)) return;
2992
3386
  const SKIP_DIRS = /* @__PURE__ */ new Set([
2993
3387
  "node_modules",
2994
3388
  ".next",
@@ -3007,10 +3401,10 @@ function scanDirForCSS(dir, files, seen, maxDepth) {
3007
3401
  if (entry.isDirectory()) {
3008
3402
  if (SKIP_DIRS.has(entry.name)) continue;
3009
3403
  if (maxDepth > 0) {
3010
- scanDirForCSS(join11(dir, entry.name), files, seen, maxDepth - 1);
3404
+ scanDirForCSS(join13(dir, entry.name), files, seen, maxDepth - 1);
3011
3405
  }
3012
3406
  } else if (entry.isFile() && extname3(entry.name) === ".css") {
3013
- addFileIfExists(join11(dir, entry.name), files, seen);
3407
+ addFileIfExists(join13(dir, entry.name), files, seen);
3014
3408
  }
3015
3409
  }
3016
3410
  } catch {
@@ -3092,10 +3486,10 @@ function extractVarName(varExpr) {
3092
3486
  function parseNextFontFromLayouts(targetDir) {
3093
3487
  const fontMap = /* @__PURE__ */ new Map();
3094
3488
  for (const relPath of LAYOUT_FILE_PATHS) {
3095
- const fullPath = join11(targetDir, relPath);
3096
- if (!existsSync9(fullPath)) continue;
3489
+ const fullPath = join13(targetDir, relPath);
3490
+ if (!existsSync11(fullPath)) continue;
3097
3491
  try {
3098
- const content = readFileSync12(fullPath, "utf-8");
3492
+ const content = readFileSync15(fullPath, "utf-8");
3099
3493
  parseNextFontDeclarations(content, fontMap);
3100
3494
  } catch {
3101
3495
  }
@@ -3142,7 +3536,7 @@ function parseNextFontDeclarations(content, fontMap) {
3142
3536
  const srcMatch = block.match(/src\s*:\s*["']([^"']+)["']/);
3143
3537
  if (srcMatch) {
3144
3538
  const srcPath = srcMatch[1];
3145
- const fileName = basename2(srcPath, extname3(srcPath));
3539
+ const fileName = basename3(srcPath, extname3(srcPath));
3146
3540
  const fontBaseName = fileName.replace(/[-_](Variable|Regular|Bold|Light|Medium|SemiBold|ExtraBold|Thin|Black|Italic).*$/i, "").replace(/[-_]/g, " ").trim();
3147
3541
  if (fontBaseName) {
3148
3542
  fontMap.set(varName, fontBaseName);
@@ -3180,10 +3574,10 @@ var MONO_FONT_NAMES = /* @__PURE__ */ new Set([
3180
3574
  "IBM Plex Mono"
3181
3575
  ]);
3182
3576
  function extractFontHints(targetDir) {
3183
- const pkgPath = join11(targetDir, "package.json");
3184
- if (!existsSync9(pkgPath)) return void 0;
3577
+ const pkgPath = join13(targetDir, "package.json");
3578
+ if (!existsSync11(pkgPath)) return void 0;
3185
3579
  try {
3186
- const pkg2 = JSON.parse(readFileSync12(pkgPath, "utf-8"));
3580
+ const pkg2 = JSON.parse(readFileSync15(pkgPath, "utf-8"));
3187
3581
  const allDeps = { ...pkg2.dependencies, ...pkg2.devDependencies };
3188
3582
  const fonts2 = [];
3189
3583
  for (const [dep, _] of Object.entries(allDeps)) {
@@ -3219,10 +3613,10 @@ function extractFontHints(targetDir) {
3219
3613
  }
3220
3614
  }
3221
3615
  function inferThemeName(targetDir) {
3222
- const pkgPath = join11(targetDir, "package.json");
3223
- if (existsSync9(pkgPath)) {
3616
+ const pkgPath = join13(targetDir, "package.json");
3617
+ if (existsSync11(pkgPath)) {
3224
3618
  try {
3225
- const pkg2 = JSON.parse(readFileSync12(pkgPath, "utf-8"));
3619
+ const pkg2 = JSON.parse(readFileSync15(pkgPath, "utf-8"));
3226
3620
  if (pkg2.name) {
3227
3621
  const name = pkg2.name.replace(/^@[\w-]+\//, "");
3228
3622
  return `${name}-theme`;
@@ -3230,7 +3624,7 @@ function inferThemeName(targetDir) {
3230
3624
  } catch {
3231
3625
  }
3232
3626
  }
3233
- return `${basename2(targetDir)}-theme`;
3627
+ return `${basename3(targetDir)}-theme`;
3234
3628
  }
3235
3629
  function confidenceComment(confidence) {
3236
3630
  return `# confidence: ${confidence}`;
@@ -3259,7 +3653,7 @@ function outputJSON(result, validationResult) {
3259
3653
  }
3260
3654
  function outputYAML(result, outputPath, cwd, validationResult) {
3261
3655
  const yamlStr = buildAnnotatedYAML(result);
3262
- const outFile = resolve8(cwd, outputPath ?? ".visor.yaml");
3656
+ const outFile = resolve9(cwd, outputPath ?? ".visor.yaml");
3263
3657
  const high = result.tokens.filter((t) => t.confidence === "high").length;
3264
3658
  const med = result.tokens.filter((t) => t.confidence === "medium").length;
3265
3659
  const low = result.tokens.filter((t) => t.confidence === "low").length;
@@ -3287,7 +3681,7 @@ function outputYAML(result, outputPath, cwd, validationResult) {
3287
3681
  }
3288
3682
  logger.blank();
3289
3683
  }
3290
- writeFileSync6(outFile, yamlStr, "utf-8");
3684
+ writeFileSync7(outFile, yamlStr, "utf-8");
3291
3685
  logger.success(`Theme written to ${relative(cwd, outFile)}`);
3292
3686
  if (validationResult) {
3293
3687
  logger.blank();
@@ -3345,14 +3739,14 @@ function buildAnnotatedYAML(result) {
3345
3739
  }
3346
3740
 
3347
3741
  // src/commands/theme-register.ts
3348
- import { readFileSync as readFileSync14, writeFileSync as writeFileSync7, mkdirSync as mkdirSync4, existsSync as existsSync11 } from "fs";
3349
- import { resolve as resolve10, join as join13 } from "path";
3350
- import { generateThemeData as generateThemeData3 } from "@loworbitstudio/visor-theme-engine";
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";
3744
+ import { generateThemeData as generateThemeData4 } from "@loworbitstudio/visor-theme-engine";
3351
3745
  import { docsAdapter as docsAdapter2 } from "@loworbitstudio/visor-theme-engine/adapters";
3352
3746
 
3353
3747
  // src/utils/theme-helpers.ts
3354
- import { existsSync as existsSync10, lstatSync, readdirSync as readdirSync5, readFileSync as readFileSync13, readlinkSync, realpathSync, statSync as statSync6 } from "fs";
3355
- import { resolve as resolve9, 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";
3356
3750
  import { execFileSync as execFileSync3 } from "child_process";
3357
3751
  function toSlug(name) {
3358
3752
  return name.toLowerCase().replace(/\s+/g, "-");
@@ -3361,12 +3755,12 @@ function toLabel(name) {
3361
3755
  return name.split(/[\s-]+/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
3362
3756
  }
3363
3757
  function findRepoRoot(startDir) {
3364
- let current = resolve9(startDir);
3758
+ let current = resolve10(startDir);
3365
3759
  while (true) {
3366
- if (existsSync10(join12(current, "packages", "docs"))) {
3760
+ if (existsSync12(join14(current, "packages", "docs"))) {
3367
3761
  return current;
3368
3762
  }
3369
- const parent = dirname6(current);
3763
+ const parent = dirname7(current);
3370
3764
  if (parent === current) break;
3371
3765
  current = parent;
3372
3766
  }
@@ -3380,9 +3774,9 @@ function findMainRepoRoot(startDir) {
3380
3774
  stdio: ["ignore", "pipe", "ignore"]
3381
3775
  }).trim();
3382
3776
  if (commonDir) {
3383
- const absoluteCommonDir = resolve9(startDir, commonDir);
3384
- const candidate = dirname6(absoluteCommonDir);
3385
- if (existsSync10(join12(candidate, "packages", "docs"))) {
3777
+ const absoluteCommonDir = resolve10(startDir, commonDir);
3778
+ const candidate = dirname7(absoluteCommonDir);
3779
+ if (existsSync12(join14(candidate, "packages", "docs"))) {
3386
3780
  return candidate;
3387
3781
  }
3388
3782
  }
@@ -3401,10 +3795,10 @@ var BrokenSymlinkError = class extends Error {
3401
3795
  code = "BROKEN_SYMLINK";
3402
3796
  };
3403
3797
  function assertNoBrokenSymlinks(dir) {
3404
- if (!existsSync10(dir)) return;
3798
+ if (!existsSync12(dir)) return;
3405
3799
  const entries = readdirSync5(dir, { withFileTypes: true });
3406
3800
  for (const entry of entries) {
3407
- const entryPath = join12(dir, entry.name);
3801
+ const entryPath = join14(dir, entry.name);
3408
3802
  let lst;
3409
3803
  try {
3410
3804
  lst = lstatSync(entryPath);
@@ -3422,39 +3816,39 @@ function assertNoBrokenSymlinks(dir) {
3422
3816
  }
3423
3817
  }
3424
3818
  function scanParentForPrivateThemes(parentDir) {
3425
- if (!existsSync10(parentDir)) return [];
3819
+ if (!existsSync12(parentDir)) return [];
3426
3820
  const entries = readdirSync5(parentDir, { withFileTypes: true });
3427
3821
  const matches = [];
3428
3822
  for (const entry of entries) {
3429
3823
  if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
3430
- const candidate = join12(parentDir, entry.name, "visor-themes-private", "themes");
3431
- if (existsSync10(candidate)) {
3824
+ const candidate = join14(parentDir, entry.name, "visor-themes-private", "themes");
3825
+ if (existsSync12(candidate)) {
3432
3826
  matches.push(candidate);
3433
3827
  }
3434
3828
  }
3435
3829
  return matches.sort();
3436
3830
  }
3437
3831
  function scanNestedThemeDir(dir) {
3438
- if (!existsSync10(dir)) return [];
3832
+ if (!existsSync12(dir)) return [];
3439
3833
  assertNoBrokenSymlinks(dir);
3440
3834
  const entries = readdirSync5(dir, { withFileTypes: true });
3441
3835
  const out = [];
3442
3836
  for (const entry of entries) {
3443
3837
  if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
3444
- const themeFile = join12(dir, entry.name, "theme.visor.yaml");
3445
- if (existsSync10(themeFile)) {
3838
+ const themeFile = join14(dir, entry.name, "theme.visor.yaml");
3839
+ if (existsSync12(themeFile)) {
3446
3840
  out.push({ filePath: themeFile, slug: entry.name });
3447
3841
  }
3448
3842
  }
3449
3843
  return out;
3450
3844
  }
3451
3845
  function detectVisorWorkspace(cwd) {
3452
- let current = resolve9(cwd);
3846
+ let current = resolve10(cwd);
3453
3847
  while (true) {
3454
- const pkgPath = join12(current, "package.json");
3455
- if (existsSync10(pkgPath)) {
3848
+ const pkgPath = join14(current, "package.json");
3849
+ if (existsSync12(pkgPath)) {
3456
3850
  try {
3457
- const pkg2 = JSON.parse(readFileSync13(pkgPath, "utf-8"));
3851
+ const pkg2 = JSON.parse(readFileSync16(pkgPath, "utf-8"));
3458
3852
  const workspaces = pkg2.workspaces ?? [];
3459
3853
  const hasCli = workspaces.some((w) => w.includes("packages/cli"));
3460
3854
  const hasEngine = workspaces.some((w) => w.includes("packages/theme-engine"));
@@ -3464,7 +3858,7 @@ function detectVisorWorkspace(cwd) {
3464
3858
  } catch {
3465
3859
  }
3466
3860
  }
3467
- const parent = dirname6(current);
3861
+ const parent = dirname7(current);
3468
3862
  if (parent === current) break;
3469
3863
  current = parent;
3470
3864
  }
@@ -3472,18 +3866,18 @@ function detectVisorWorkspace(cwd) {
3472
3866
  }
3473
3867
  function isLocalVisorBinary(workspaceRoot, scriptPath) {
3474
3868
  if (!scriptPath) return false;
3475
- const expectedPrefix = join12(workspaceRoot, "packages", "cli", "dist");
3869
+ const expectedPrefix = join14(workspaceRoot, "packages", "cli", "dist");
3476
3870
  let resolvedScript;
3477
3871
  let resolvedPrefix;
3478
3872
  try {
3479
3873
  resolvedScript = realpathSync(scriptPath);
3480
3874
  } catch {
3481
- resolvedScript = resolve9(scriptPath);
3875
+ resolvedScript = resolve10(scriptPath);
3482
3876
  }
3483
3877
  try {
3484
3878
  resolvedPrefix = realpathSync(expectedPrefix);
3485
3879
  } catch {
3486
- resolvedPrefix = resolve9(expectedPrefix);
3880
+ resolvedPrefix = resolve10(expectedPrefix);
3487
3881
  }
3488
3882
  return resolvedScript.startsWith(resolvedPrefix + "/") || resolvedScript === resolvedPrefix;
3489
3883
  }
@@ -3573,10 +3967,10 @@ ${indent}${newEntry},
3573
3967
  return { updated, changed: true };
3574
3968
  }
3575
3969
  function themeRegisterCommand(file, cwd, options) {
3576
- const filePath = resolve10(cwd, file);
3970
+ const filePath = resolve11(cwd, file);
3577
3971
  let yamlContent;
3578
3972
  try {
3579
- yamlContent = readFileSync14(filePath, "utf-8");
3973
+ yamlContent = readFileSync17(filePath, "utf-8");
3580
3974
  } catch {
3581
3975
  if (options.json) {
3582
3976
  console.log(JSON.stringify({ success: false, error: `Could not read file: ${filePath}` }));
@@ -3588,7 +3982,7 @@ function themeRegisterCommand(file, cwd, options) {
3588
3982
  }
3589
3983
  let data;
3590
3984
  try {
3591
- data = generateThemeData3(yamlContent);
3985
+ data = generateThemeData4(yamlContent);
3592
3986
  } catch (err) {
3593
3987
  const message = err instanceof Error ? err.message : "Unknown error parsing theme";
3594
3988
  if (options.json) {
@@ -3619,11 +4013,11 @@ function themeRegisterCommand(file, cwd, options) {
3619
4013
  process.exit(1);
3620
4014
  return;
3621
4015
  }
3622
- const docsAppDir = join13(repoRoot, "packages", "docs", "app");
3623
- const cssFilePath = join13(docsAppDir, `${slug2}-theme.css`);
3624
- const globalsPath = join13(docsAppDir, "globals.css");
3625
- const themeConfigPath = join13(repoRoot, "packages", "docs", "lib", "theme-config.ts");
3626
- 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)) {
3627
4021
  const msg = `Docs app directory not found: ${docsAppDir}`;
3628
4022
  if (options.json) {
3629
4023
  console.log(JSON.stringify({ success: false, error: msg }));
@@ -3636,8 +4030,8 @@ function themeRegisterCommand(file, cwd, options) {
3636
4030
  let globalsContent = "";
3637
4031
  let themeConfigContent = "";
3638
4032
  try {
3639
- globalsContent = readFileSync14(globalsPath, "utf-8");
3640
- themeConfigContent = readFileSync14(themeConfigPath, "utf-8");
4033
+ globalsContent = readFileSync17(globalsPath, "utf-8");
4034
+ themeConfigContent = readFileSync17(themeConfigPath, "utf-8");
3641
4035
  } catch (err) {
3642
4036
  const msg = err instanceof Error ? err.message : "Could not read docs files";
3643
4037
  if (options.json) {
@@ -3648,8 +4042,8 @@ function themeRegisterCommand(file, cwd, options) {
3648
4042
  process.exit(1);
3649
4043
  return;
3650
4044
  }
3651
- const cssExists = existsSync11(cssFilePath);
3652
- const cssChanged = !cssExists || readFileSync14(cssFilePath, "utf-8") !== css;
4045
+ const cssExists = existsSync13(cssFilePath);
4046
+ const cssChanged = !cssExists || readFileSync17(cssFilePath, "utf-8") !== css;
3653
4047
  const { updated: newGlobals, changed: globalsChanged } = insertGlobalsImport(globalsContent, slug2);
3654
4048
  const { updated: newThemeConfig, changed: themeConfigChanged, error: configError } = insertThemeConfig(
3655
4049
  themeConfigContent,
@@ -3692,14 +4086,14 @@ function themeRegisterCommand(file, cwd, options) {
3692
4086
  }
3693
4087
  try {
3694
4088
  if (cssChanged) {
3695
- mkdirSync4(docsAppDir, { recursive: true });
3696
- writeFileSync7(cssFilePath, css, "utf-8");
4089
+ mkdirSync5(docsAppDir, { recursive: true });
4090
+ writeFileSync8(cssFilePath, css, "utf-8");
3697
4091
  }
3698
4092
  if (globalsChanged) {
3699
- writeFileSync7(globalsPath, newGlobals, "utf-8");
4093
+ writeFileSync8(globalsPath, newGlobals, "utf-8");
3700
4094
  }
3701
4095
  if (themeConfigChanged) {
3702
- writeFileSync7(themeConfigPath, newThemeConfig, "utf-8");
4096
+ writeFileSync8(themeConfigPath, newThemeConfig, "utf-8");
3703
4097
  }
3704
4098
  } catch (err) {
3705
4099
  const msg = err instanceof Error ? err.message : "Write failed";
@@ -3733,8 +4127,8 @@ function themeRegisterCommand(file, cwd, options) {
3733
4127
  }
3734
4128
 
3735
4129
  // src/commands/theme-unregister.ts
3736
- import { readFileSync as readFileSync15, writeFileSync as writeFileSync8, existsSync as existsSync12, unlinkSync } from "fs";
3737
- 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";
3738
4132
  function removeGlobalsImport(content, slug2) {
3739
4133
  const importLine = `@import './${slug2}-theme.css';`;
3740
4134
  if (!content.includes(importLine)) {
@@ -3766,11 +4160,11 @@ function themeUnregisterCommand(slug2, cwd, options) {
3766
4160
  process.exit(1);
3767
4161
  return;
3768
4162
  }
3769
- const docsAppDir = join14(repoRoot, "packages", "docs", "app");
3770
- const cssFilePath = join14(docsAppDir, `${slug2}-theme.css`);
3771
- const globalsPath = join14(docsAppDir, "globals.css");
3772
- const themeConfigPath = join14(repoRoot, "packages", "docs", "lib", "theme-config.ts");
3773
- 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)) {
3774
4168
  const msg = `Docs app directory not found: ${docsAppDir}`;
3775
4169
  if (options.json) {
3776
4170
  console.log(JSON.stringify({ success: false, error: msg }));
@@ -3783,8 +4177,8 @@ function themeUnregisterCommand(slug2, cwd, options) {
3783
4177
  let globalsContent = "";
3784
4178
  let themeConfigContent = "";
3785
4179
  try {
3786
- globalsContent = readFileSync15(globalsPath, "utf-8");
3787
- themeConfigContent = readFileSync15(themeConfigPath, "utf-8");
4180
+ globalsContent = readFileSync18(globalsPath, "utf-8");
4181
+ themeConfigContent = readFileSync18(themeConfigPath, "utf-8");
3788
4182
  } catch (err) {
3789
4183
  const msg = err instanceof Error ? err.message : "Could not read docs files";
3790
4184
  if (options.json) {
@@ -3795,7 +4189,7 @@ function themeUnregisterCommand(slug2, cwd, options) {
3795
4189
  process.exit(1);
3796
4190
  return;
3797
4191
  }
3798
- const cssExists = existsSync12(cssFilePath);
4192
+ const cssExists = existsSync14(cssFilePath);
3799
4193
  const { updated: newGlobals, changed: globalsChanged } = removeGlobalsImport(globalsContent, slug2);
3800
4194
  const { updated: newThemeConfig, changed: themeConfigChanged } = removeThemeConfigEntry(themeConfigContent, slug2);
3801
4195
  if (!cssExists && !globalsChanged && !themeConfigChanged) {
@@ -3808,8 +4202,8 @@ function themeUnregisterCommand(slug2, cwd, options) {
3808
4202
  }
3809
4203
  try {
3810
4204
  if (cssExists) unlinkSync(cssFilePath);
3811
- if (globalsChanged) writeFileSync8(globalsPath, newGlobals, "utf-8");
3812
- if (themeConfigChanged) writeFileSync8(themeConfigPath, newThemeConfig, "utf-8");
4205
+ if (globalsChanged) writeFileSync9(globalsPath, newGlobals, "utf-8");
4206
+ if (themeConfigChanged) writeFileSync9(themeConfigPath, newThemeConfig, "utf-8");
3813
4207
  } catch (err) {
3814
4208
  const msg = err instanceof Error ? err.message : "Write failed";
3815
4209
  if (options.json) {
@@ -3836,17 +4230,17 @@ function themeUnregisterCommand(slug2, cwd, options) {
3836
4230
 
3837
4231
  // src/commands/theme-sync.ts
3838
4232
  import {
3839
- readFileSync as readFileSync16,
3840
- writeFileSync as writeFileSync9,
3841
- mkdirSync as mkdirSync5,
3842
- existsSync as existsSync13,
4233
+ readFileSync as readFileSync19,
4234
+ writeFileSync as writeFileSync10,
4235
+ mkdirSync as mkdirSync6,
4236
+ existsSync as existsSync15,
3843
4237
  readdirSync as readdirSync6,
3844
4238
  unlinkSync as unlinkSync2,
3845
4239
  copyFileSync
3846
4240
  } from "fs";
3847
- import { join as join15, basename as basename3, resolve as resolve11, sep, relative as relative2 } from "path";
3848
- import { parse as parseYaml2 } from "yaml";
3849
- import { generateThemeData as generateThemeData4, validateFontCoverage, formatFontCoverageError } from "@loworbitstudio/visor-theme-engine";
4241
+ import { join as join17, basename as basename4, resolve as resolve12, sep, relative as relative2 } from "path";
4242
+ import { parse as parseYaml3 } from "yaml";
4243
+ import { generateThemeData as generateThemeData5, validateFontCoverage, formatFontCoverageError } from "@loworbitstudio/visor-theme-engine";
3850
4244
  import { docsAdapter as docsAdapter3 } from "@loworbitstudio/visor-theme-engine/adapters";
3851
4245
  var PRIVATE_THEMES_REPO_URL = "git@github.com:low-orbit-studio/visor-themes-private.git";
3852
4246
  var PRIVATE_THEMES_ENV_VAR = "VISOR_THEMES_PRIVATE_PATH";
@@ -3858,9 +4252,9 @@ var CUSTOM_OVERLAY_CSS_PATH = "packages/docs/app/custom-themes.generated.css";
3858
4252
  var CUSTOM_OVERLAY_TS_PATH = "packages/docs/lib/theme-config.custom.generated.ts";
3859
4253
  var CUSTOM_OVERLAY_IMPORT_LINE = "@import './custom-themes.generated.css';";
3860
4254
  function scanThemeDir(dir) {
3861
- if (!existsSync13(dir)) return [];
4255
+ if (!existsSync15(dir)) return [];
3862
4256
  assertNoBrokenSymlinks(dir);
3863
- 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));
3864
4258
  }
3865
4259
  function resolveCustomSources(repoRoot, mainRepoRoot, warn) {
3866
4260
  const merged = /* @__PURE__ */ new Map();
@@ -3884,20 +4278,20 @@ function resolveCustomSources(repoRoot, mainRepoRoot, warn) {
3884
4278
  };
3885
4279
  const envPath = process.env[PRIVATE_THEMES_ENV_VAR];
3886
4280
  if (envPath && envPath.trim() !== "") {
3887
- const resolved = resolve11(envPath);
3888
- if (!existsSync13(resolved)) {
4281
+ const resolved = resolve12(envPath);
4282
+ if (!existsSync15(resolved)) {
3889
4283
  throw new Error(
3890
4284
  `${PRIVATE_THEMES_ENV_VAR} is set to "${envPath}" but the path does not exist. Expected a directory containing {slug}/theme.visor.yaml entries.`
3891
4285
  );
3892
4286
  }
3893
4287
  addNested(resolved, "env");
3894
4288
  }
3895
- const siblingPath = join15(mainRepoRoot, "..", "visor-themes-private", "themes");
3896
- const siblingExists = existsSync13(siblingPath);
4289
+ const siblingPath = join17(mainRepoRoot, "..", "visor-themes-private", "themes");
4290
+ const siblingExists = existsSync15(siblingPath);
3897
4291
  if (siblingExists) {
3898
4292
  addNested(siblingPath, "sibling");
3899
4293
  }
3900
- const parentDir = join15(mainRepoRoot, "..");
4294
+ const parentDir = join17(mainRepoRoot, "..");
3901
4295
  const parentGlobMatches = scanParentForPrivateThemes(parentDir).filter(
3902
4296
  (path2) => !path2.startsWith(mainRepoRoot + sep)
3903
4297
  );
@@ -3918,10 +4312,10 @@ function resolveCustomSources(repoRoot, mainRepoRoot, warn) {
3918
4312
  }
3919
4313
  }
3920
4314
  }
3921
- const legacyDir = join15(repoRoot, "custom-themes");
4315
+ const legacyDir = join17(repoRoot, "custom-themes");
3922
4316
  const legacyFiles = scanThemeDir(legacyDir);
3923
4317
  for (const legacyFile of legacyFiles) {
3924
- const slug2 = basename3(legacyFile).replace(/\.visor\.yaml$/, "");
4318
+ const slug2 = basename4(legacyFile).replace(/\.visor\.yaml$/, "");
3925
4319
  const existing = merged.get(slug2);
3926
4320
  if (existing) {
3927
4321
  warn(
@@ -3975,14 +4369,14 @@ function reportBrokenSymlink(err, options) {
3975
4369
  }
3976
4370
  }
3977
4371
  function buildEmptySourcesMessage(mainRepoRoot) {
3978
- const expectedSibling = join15(mainRepoRoot, "..", "visor-themes-private");
4372
+ const expectedSibling = join17(mainRepoRoot, "..", "visor-themes-private");
3979
4373
  return [
3980
4374
  "No theme sources discovered. Cannot proceed \u2014 refusing to wipe generated CSS.",
3981
4375
  "",
3982
4376
  "Resolution order checked:",
3983
4377
  ` 1. Env var ${PRIVATE_THEMES_ENV_VAR} (unset or empty)`,
3984
4378
  ` 2. Sibling checkout at ${expectedSibling}/themes/ (not found)`,
3985
- ` 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)`,
3986
4380
  " 4. Legacy custom-themes/ (no .visor.yaml files)",
3987
4381
  "",
3988
4382
  "To fix, clone the private themes repo as a sibling:",
@@ -4008,17 +4402,17 @@ function enforceWorkspaceGuard(cwd) {
4008
4402
  ].join("\n");
4009
4403
  }
4010
4404
  function extractGroup(yamlContent) {
4011
- const parsed = parseYaml2(yamlContent);
4405
+ const parsed = parseYaml3(yamlContent);
4012
4406
  if (typeof parsed?.group === "string") return parsed.group;
4013
4407
  return void 0;
4014
4408
  }
4015
4409
  function extractLabel(yamlContent) {
4016
- const parsed = parseYaml2(yamlContent);
4410
+ const parsed = parseYaml3(yamlContent);
4017
4411
  if (typeof parsed?.label === "string") return parsed.label;
4018
4412
  return void 0;
4019
4413
  }
4020
4414
  function extractDefaultMode(yamlContent) {
4021
- const parsed = parseYaml2(yamlContent);
4415
+ const parsed = parseYaml3(yamlContent);
4022
4416
  const v = parsed?.["default-mode"];
4023
4417
  if (v === "dark" || v === "light") return v;
4024
4418
  return void 0;
@@ -4187,14 +4581,14 @@ function themeSyncCommand(cwd, options) {
4187
4581
  return;
4188
4582
  }
4189
4583
  const mainRepoRoot = findMainRepoRoot(cwd) ?? repoRoot;
4190
- const themesDir = join15(repoRoot, "themes");
4191
- const docsAppDir = join15(repoRoot, "packages", "docs", "app");
4192
- const docsLibDir = join15(repoRoot, "packages", "docs", "lib");
4193
- const docsPublicThemesDir = join15(repoRoot, "packages", "docs", "public", "themes");
4194
- const themeConfigPath = join15(repoRoot, "packages", "docs", "lib", "theme-config.ts");
4195
- const globalsPath = join15(docsAppDir, "globals.css");
4196
- const customOverlayCssPath = join15(repoRoot, CUSTOM_OVERLAY_CSS_PATH);
4197
- 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);
4198
4592
  let stockFiles;
4199
4593
  try {
4200
4594
  stockFiles = scanThemeDir(themesDir);
@@ -4251,18 +4645,18 @@ function themeSyncCommand(cwd, options) {
4251
4645
  const processFile = (filePath, isCustom, slugOverride) => {
4252
4646
  let yamlContent;
4253
4647
  try {
4254
- yamlContent = readFileSync16(filePath, "utf-8");
4648
+ yamlContent = readFileSync19(filePath, "utf-8");
4255
4649
  } catch {
4256
4650
  failures.push({ filePath, error: `Could not read: ${filePath}` });
4257
4651
  return;
4258
4652
  }
4259
4653
  let data;
4260
4654
  try {
4261
- data = generateThemeData4(yamlContent);
4655
+ data = generateThemeData5(yamlContent);
4262
4656
  } catch (err) {
4263
4657
  failures.push({
4264
4658
  filePath,
4265
- 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"}`
4266
4660
  });
4267
4661
  return;
4268
4662
  }
@@ -4276,12 +4670,12 @@ function themeSyncCommand(cwd, options) {
4276
4670
  for (const e of coverage.errors) {
4277
4671
  failures.push({
4278
4672
  filePath,
4279
- error: formatFontCoverageError(basename3(filePath), e.declaredAt, e.family)
4673
+ error: formatFontCoverageError(basename4(filePath), e.declaredAt, e.family)
4280
4674
  });
4281
4675
  }
4282
4676
  return;
4283
4677
  }
4284
- const yamlFilename = slugOverride ?? basename3(filePath).replace(/\.visor\.yaml$/, "");
4678
+ const yamlFilename = slugOverride ?? basename4(filePath).replace(/\.visor\.yaml$/, "");
4285
4679
  manifest.push({ slug: slug2, label, group, defaultMode, css, yamlFilename, isCustom });
4286
4680
  };
4287
4681
  for (const f of stockFiles) processFile(f, false);
@@ -4301,8 +4695,8 @@ function themeSyncCommand(cwd, options) {
4301
4695
  let globalsContent;
4302
4696
  let themeConfigContent;
4303
4697
  try {
4304
- globalsContent = readFileSync16(globalsPath, "utf-8");
4305
- themeConfigContent = readFileSync16(themeConfigPath, "utf-8");
4698
+ globalsContent = readFileSync19(globalsPath, "utf-8");
4699
+ themeConfigContent = readFileSync19(themeConfigPath, "utf-8");
4306
4700
  } catch (err) {
4307
4701
  const msg = err instanceof Error ? err.message : "Could not read docs files";
4308
4702
  if (options.json) {
@@ -4317,12 +4711,12 @@ function themeSyncCommand(cwd, options) {
4317
4711
  const newThemeConfig = updateStockThemeConfigBlock(themeConfigContent, stockManifest);
4318
4712
  const newCustomOverlayCss = generateCustomOverlayCss(customManifest);
4319
4713
  const newCustomOverlayTs = generateCustomOverlayTs(customManifest);
4320
- const existingCssFiles = existsSync13(docsAppDir) ? readdirSync6(docsAppDir).filter(
4714
+ const existingCssFiles = existsSync15(docsAppDir) ? readdirSync6(docsAppDir).filter(
4321
4715
  (f) => f.endsWith("-theme.css") && f !== "custom-themes.generated.css"
4322
4716
  ) : [];
4323
4717
  const newCssSet = new Set(allSlugs.map((s) => `${s}-theme.css`));
4324
4718
  const staleCssFiles = existingCssFiles.filter((f) => !newCssSet.has(f));
4325
- 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")) : [];
4326
4720
  const newPublicYamlSet = new Set(manifest.map((e) => `${e.yamlFilename}.visor.yaml`));
4327
4721
  const stalePublicYamls = existingPublicYamls.filter((f) => !newPublicYamlSet.has(f));
4328
4722
  if (options.dryRun) {
@@ -4362,28 +4756,28 @@ function themeSyncCommand(cwd, options) {
4362
4756
  return;
4363
4757
  }
4364
4758
  try {
4365
- mkdirSync5(docsAppDir, { recursive: true });
4366
- mkdirSync5(docsLibDir, { recursive: true });
4367
- mkdirSync5(docsPublicThemesDir, { recursive: true });
4759
+ mkdirSync6(docsAppDir, { recursive: true });
4760
+ mkdirSync6(docsLibDir, { recursive: true });
4761
+ mkdirSync6(docsPublicThemesDir, { recursive: true });
4368
4762
  for (const entry of manifest) {
4369
- writeFileSync9(join15(docsAppDir, `${entry.slug}-theme.css`), entry.css, "utf-8");
4763
+ writeFileSync10(join17(docsAppDir, `${entry.slug}-theme.css`), entry.css, "utf-8");
4370
4764
  }
4371
4765
  for (const stale of staleCssFiles) {
4372
- unlinkSync2(join15(docsAppDir, stale));
4766
+ unlinkSync2(join17(docsAppDir, stale));
4373
4767
  }
4374
- writeFileSync9(customOverlayCssPath, newCustomOverlayCss, "utf-8");
4375
- writeFileSync9(customOverlayTsPath, newCustomOverlayTs, "utf-8");
4376
- writeFileSync9(themeConfigPath, newThemeConfig, "utf-8");
4377
- 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");
4378
4772
  for (const srcFile of stockFiles) {
4379
- copyFileSync(srcFile, join15(docsPublicThemesDir, basename3(srcFile)));
4773
+ copyFileSync(srcFile, join17(docsPublicThemesDir, basename4(srcFile)));
4380
4774
  }
4381
4775
  for (const c of customSources) {
4382
- const targetName = c.origin === "legacy" ? basename3(c.filePath) : `${c.slug}.visor.yaml`;
4383
- 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));
4384
4778
  }
4385
4779
  for (const stale of stalePublicYamls) {
4386
- unlinkSync2(join15(docsPublicThemesDir, stale));
4780
+ unlinkSync2(join17(docsPublicThemesDir, stale));
4387
4781
  }
4388
4782
  } catch (err) {
4389
4783
  const msg = err instanceof Error ? err.message : "Write failed";
@@ -4436,19 +4830,19 @@ function themeSyncCommand(cwd, options) {
4436
4830
 
4437
4831
  // src/commands/theme-batch-apply-flutter.ts
4438
4832
  import {
4439
- readFileSync as readFileSync17,
4440
- writeFileSync as writeFileSync10,
4441
- mkdirSync as mkdirSync6,
4442
- existsSync as existsSync14,
4833
+ readFileSync as readFileSync20,
4834
+ writeFileSync as writeFileSync11,
4835
+ mkdirSync as mkdirSync7,
4836
+ existsSync as existsSync16,
4443
4837
  readdirSync as readdirSync7,
4444
4838
  rmSync
4445
4839
  } from "fs";
4446
- import { join as join16, basename as basename4, dirname as dirname7 } from "path";
4447
- import { generateThemeData as generateThemeData5 } from "@loworbitstudio/visor-theme-engine";
4840
+ import { join as join18, basename as basename5, dirname as dirname8 } from "path";
4841
+ import { generateThemeData as generateThemeData6 } from "@loworbitstudio/visor-theme-engine";
4448
4842
  import { flutterAdapter as flutterAdapter2 } from "@loworbitstudio/visor-theme-engine/adapters";
4449
4843
  function scanThemeDir2(dir) {
4450
- if (!existsSync14(dir)) return [];
4451
- 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();
4452
4846
  }
4453
4847
  function slugToCamel(slug2) {
4454
4848
  return slug2.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
@@ -4581,9 +4975,9 @@ function themeBatchApplyFlutterCommand(cwd, options) {
4581
4975
  process.exit(1);
4582
4976
  return;
4583
4977
  }
4584
- const themesDir = join16(repoRoot, "themes");
4585
- const customThemesDir = join16(repoRoot, "custom-themes");
4586
- 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");
4587
4981
  const stockFiles = scanThemeDir2(themesDir);
4588
4982
  const customFiles = scanThemeDir2(customThemesDir);
4589
4983
  const allFiles = [...stockFiles, ...customFiles];
@@ -4604,17 +4998,17 @@ function themeBatchApplyFlutterCommand(cwd, options) {
4604
4998
  for (const filePath of allFiles) {
4605
4999
  let yamlContent;
4606
5000
  try {
4607
- yamlContent = readFileSync17(filePath, "utf-8");
5001
+ yamlContent = readFileSync20(filePath, "utf-8");
4608
5002
  } catch {
4609
5003
  errors.push(`Could not read: ${filePath}`);
4610
5004
  continue;
4611
5005
  }
4612
5006
  let data;
4613
5007
  try {
4614
- data = generateThemeData5(yamlContent);
5008
+ data = generateThemeData6(yamlContent);
4615
5009
  } catch (err) {
4616
5010
  errors.push(
4617
- `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"}`
4618
5012
  );
4619
5013
  continue;
4620
5014
  }
@@ -4683,8 +5077,8 @@ function themeBatchApplyFlutterCommand(cwd, options) {
4683
5077
  return;
4684
5078
  }
4685
5079
  const slugs = processed.map((p) => p.slug);
4686
- const libSrcDir = join16(outputDir, "lib", "src");
4687
- if (existsSync14(libSrcDir)) {
5080
+ const libSrcDir = join18(outputDir, "lib", "src");
5081
+ if (existsSync16(libSrcDir)) {
4688
5082
  rmSync(libSrcDir, { recursive: true, force: true });
4689
5083
  }
4690
5084
  const packageFiles = {
@@ -4696,17 +5090,17 @@ function themeBatchApplyFlutterCommand(cwd, options) {
4696
5090
  };
4697
5091
  let totalFiles = 0;
4698
5092
  for (const [relPath, content] of Object.entries(packageFiles)) {
4699
- const absPath = join16(outputDir, relPath);
4700
- mkdirSync6(dirname7(absPath), { recursive: true });
4701
- writeFileSync10(absPath, content, "utf-8");
5093
+ const absPath = join18(outputDir, relPath);
5094
+ mkdirSync7(dirname8(absPath), { recursive: true });
5095
+ writeFileSync11(absPath, content, "utf-8");
4702
5096
  totalFiles++;
4703
5097
  }
4704
5098
  for (const { slug: slug2, tokenFiles } of processed) {
4705
- const themeBaseDir = join16(outputDir, "lib", "src", slug2);
5099
+ const themeBaseDir = join18(outputDir, "lib", "src", slug2);
4706
5100
  for (const [relPath, content] of Object.entries(tokenFiles)) {
4707
- const absPath = join16(themeBaseDir, relPath);
4708
- mkdirSync6(dirname7(absPath), { recursive: true });
4709
- writeFileSync10(absPath, content, "utf-8");
5101
+ const absPath = join18(themeBaseDir, relPath);
5102
+ mkdirSync7(dirname8(absPath), { recursive: true });
5103
+ writeFileSync11(absPath, content, "utf-8");
4710
5104
  totalFiles++;
4711
5105
  }
4712
5106
  }
@@ -4727,11 +5121,11 @@ function themeBatchApplyFlutterCommand(cwd, options) {
4727
5121
  }
4728
5122
 
4729
5123
  // src/commands/fonts-add.ts
4730
- import { existsSync as existsSync15, statSync as statSync7, readdirSync as readdirSync8, readFileSync as readFileSync18 } from "fs";
4731
- import { resolve as resolve12, 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";
4732
5126
  import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
4733
5127
  function deriveFamilySlug(filename) {
4734
- const name = basename5(filename, extname4(filename));
5128
+ const name = basename6(filename, extname4(filename));
4735
5129
  const WEIGHT_STYLE_SUFFIXES = /* @__PURE__ */ new Set([
4736
5130
  "thin",
4737
5131
  "hairline",
@@ -4772,21 +5166,21 @@ function deriveFamilySlug(filename) {
4772
5166
  return parts.join("-").toLowerCase();
4773
5167
  }
4774
5168
  function collectWoff2Files(inputPath) {
4775
- const resolved = resolve12(inputPath);
4776
- if (!existsSync15(resolved)) {
5169
+ const resolved = resolve13(inputPath);
5170
+ if (!existsSync17(resolved)) {
4777
5171
  throw new Error(`Path not found: ${resolved}`);
4778
5172
  }
4779
5173
  const stat = statSync7(resolved);
4780
5174
  if (stat.isFile()) {
4781
5175
  if (extname4(resolved).toLowerCase() !== ".woff2") {
4782
5176
  throw new Error(
4783
- `Invalid file format: ${basename5(resolved)}. Only .woff2 files are accepted.`
5177
+ `Invalid file format: ${basename6(resolved)}. Only .woff2 files are accepted.`
4784
5178
  );
4785
5179
  }
4786
5180
  return [resolved];
4787
5181
  }
4788
5182
  if (stat.isDirectory()) {
4789
- const files = readdirSync8(resolved).filter((f) => extname4(f).toLowerCase() === ".woff2").map((f) => resolve12(resolved, f));
5183
+ const files = readdirSync8(resolved).filter((f) => extname4(f).toLowerCase() === ".woff2").map((f) => resolve13(resolved, f));
4790
5184
  if (files.length === 0) {
4791
5185
  throw new Error(
4792
5186
  `No .woff2 files found in directory: ${resolved}`
@@ -4801,8 +5195,8 @@ function collectWoff2Files(inputPath) {
4801
5195
  throw new Error(`Path is neither a file nor a directory: ${resolved}`);
4802
5196
  }
4803
5197
  function getNonWoff2Fonts(inputPath) {
4804
- const resolved = resolve12(inputPath);
4805
- if (!existsSync15(resolved) || !statSync7(resolved).isDirectory()) {
5198
+ const resolved = resolve13(inputPath);
5199
+ if (!existsSync17(resolved) || !statSync7(resolved).isDirectory()) {
4806
5200
  return [];
4807
5201
  }
4808
5202
  return readdirSync8(resolved).filter((f) => {
@@ -4839,7 +5233,7 @@ function createR2Client(config) {
4839
5233
  });
4840
5234
  }
4841
5235
  async function uploadFile(client, bucket, key, filePath) {
4842
- const body = readFileSync18(filePath);
5236
+ const body = readFileSync21(filePath);
4843
5237
  await client.send(
4844
5238
  new PutObjectCommand({
4845
5239
  Bucket: bucket,
@@ -4854,8 +5248,8 @@ async function fontsAddCommand(inputPath, options) {
4854
5248
  try {
4855
5249
  const r2Config = getR2Config();
4856
5250
  const files = collectWoff2Files(inputPath);
4857
- const familySlug = options.family ?? deriveFamilySlug(basename5(files[0]));
4858
- const resolved = resolve12(inputPath);
5251
+ const familySlug = options.family ?? deriveFamilySlug(basename6(files[0]));
5252
+ const resolved = resolve13(inputPath);
4859
5253
  const nonWoff2 = statSync7(resolved).isDirectory() ? getNonWoff2Fonts(resolved) : [];
4860
5254
  if (!json) {
4861
5255
  logger.heading("Visor Font Upload");
@@ -4874,7 +5268,7 @@ async function fontsAddCommand(inputPath, options) {
4874
5268
  const bucket = "visor-fonts";
4875
5269
  const results = [];
4876
5270
  for (const filePath of files) {
4877
- const filename = basename5(filePath);
5271
+ const filename = basename6(filePath);
4878
5272
  const key = buildS3Key(org, familySlug, filename);
4879
5273
  if (!json) {
4880
5274
  logger.info(`Uploading ${filename}...`);
@@ -5143,27 +5537,27 @@ function findCssFiles(dir, maxDepth = 3) {
5143
5537
  }
5144
5538
 
5145
5539
  // src/utils/patterns.ts
5146
- import { existsSync as existsSync17, readdirSync as readdirSync10, readFileSync as readFileSync20 } from "fs";
5147
- 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";
5148
5542
  import { parse as parseYAML } from "yaml";
5149
5543
  function loadPatternsFromYaml(repoRoot) {
5150
- const patternsDir = join18(repoRoot, "patterns");
5151
- if (!existsSync17(patternsDir)) return [];
5544
+ const patternsDir = join20(repoRoot, "patterns");
5545
+ if (!existsSync19(patternsDir)) return [];
5152
5546
  const files = readdirSync10(patternsDir).filter(
5153
5547
  (f) => f.endsWith(".visor-pattern.yaml")
5154
5548
  );
5155
5549
  return files.map((file) => {
5156
- const content = readFileSync20(join18(patternsDir, file), "utf-8");
5550
+ const content = readFileSync23(join20(patternsDir, file), "utf-8");
5157
5551
  return parseYAML(content);
5158
5552
  }).filter(Boolean);
5159
5553
  }
5160
5554
  function findRepoRoot2(startDir) {
5161
5555
  let current = startDir;
5162
5556
  while (true) {
5163
- if (existsSync17(join18(current, "patterns"))) {
5557
+ if (existsSync19(join20(current, "patterns"))) {
5164
5558
  return current;
5165
5559
  }
5166
- const parent = join18(current, "..");
5560
+ const parent = join20(current, "..");
5167
5561
  if (parent === current) return null;
5168
5562
  current = parent;
5169
5563
  }
@@ -5542,9 +5936,9 @@ Visor Tokens (${categoryLabel}) \u2014 ${tokens2.length} tokens
5542
5936
  }
5543
5937
 
5544
5938
  // src/commands/migrate-token-substitution.ts
5545
- import { readFileSync as readFileSync21, writeFileSync as writeFileSync11, readdirSync as readdirSync11, statSync as statSync8, existsSync as existsSync18 } from "fs";
5546
- import { resolve as resolve13, join as join19, relative as relative4 } from "path";
5547
- import { parse as parseYaml3 } from "yaml";
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";
5941
+ import { parse as parseYaml4 } from "yaml";
5548
5942
  import pc4 from "picocolors";
5549
5943
  var V7_ENTR_SUBSTITUTION_MAP = {
5550
5944
  "--panel": "--surface-card",
@@ -5566,15 +5960,15 @@ var BUILT_IN_SUBSTITUTION_MAPS = {
5566
5960
  var DEFAULT_THEME_ID = "entr";
5567
5961
  function readMapFromThemeFile(themeId, cwd) {
5568
5962
  const candidates = [
5569
- join19(cwd, "themes", `${themeId}.visor.yaml`),
5570
- join19(cwd, "custom-themes", `${themeId}.visor.yaml`),
5571
- 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`)
5572
5966
  ];
5573
5967
  for (const candidate of candidates) {
5574
- if (existsSync18(candidate)) {
5968
+ if (existsSync20(candidate)) {
5575
5969
  try {
5576
- const raw = readFileSync21(candidate, "utf-8");
5577
- const parsed = parseYaml3(raw);
5970
+ const raw = readFileSync24(candidate, "utf-8");
5971
+ const parsed = parseYaml4(raw);
5578
5972
  if (parsed?.migrate?.["token-substitution"]) {
5579
5973
  return parsed.migrate["token-substitution"];
5580
5974
  }
@@ -5629,7 +6023,7 @@ function collectCssFiles(dirPath) {
5629
6023
  function walk2(current) {
5630
6024
  const entries = readdirSync11(current, { withFileTypes: true });
5631
6025
  for (const entry of entries) {
5632
- const fullPath = join19(current, entry.name);
6026
+ const fullPath = join21(current, entry.name);
5633
6027
  if (entry.isDirectory()) {
5634
6028
  if (["node_modules", ".git", ".next", "dist", "build", ".cache"].includes(entry.name)) {
5635
6029
  continue;
@@ -5657,7 +6051,7 @@ function runSubstitutionPass(targetPath, map, themeId) {
5657
6051
  const fileResults = [];
5658
6052
  let totalSubstitutions = 0;
5659
6053
  for (const file of files) {
5660
- const content = readFileSync21(file, "utf-8");
6054
+ const content = readFileSync24(file, "utf-8");
5661
6055
  const { newContent, substitutions } = applySubstitutionsToContent(content, map);
5662
6056
  if (substitutions.length > 0) {
5663
6057
  fileResults.push({ file, substitutions, originalContent: content, newContent });
@@ -5691,7 +6085,7 @@ function migrateTokenSubstitutionCommand(targetArg, cwd, options) {
5691
6085
  process.exit(1);
5692
6086
  return;
5693
6087
  }
5694
- const targetPath = resolve13(cwd, targetArg ?? ".");
6088
+ const targetPath = resolve14(cwd, targetArg ?? ".");
5695
6089
  try {
5696
6090
  statSync8(targetPath);
5697
6091
  } catch {
@@ -5719,7 +6113,7 @@ function migrateTokenSubstitutionCommand(targetArg, cwd, options) {
5719
6113
  if (options.json) {
5720
6114
  if (apply) {
5721
6115
  for (const f of result.files) {
5722
- writeFileSync11(f.file, f.newContent, "utf-8");
6116
+ writeFileSync12(f.file, f.newContent, "utf-8");
5723
6117
  }
5724
6118
  console.log(JSON.stringify({
5725
6119
  success: true,
@@ -5759,7 +6153,7 @@ function migrateTokenSubstitutionCommand(targetArg, cwd, options) {
5759
6153
  logger.warn(`Dry run \u2014 no files written. Re-run with ${pc4.bold("--apply")} to commit changes.`);
5760
6154
  } else {
5761
6155
  for (const f of result.files) {
5762
- writeFileSync11(f.file, f.newContent, "utf-8");
6156
+ writeFileSync12(f.file, f.newContent, "utf-8");
5763
6157
  }
5764
6158
  logger.heading(`visor migrate token-substitution \u2014 applied`);
5765
6159
  logger.blank();
@@ -5780,23 +6174,23 @@ function migrateTokenSubstitutionCommand(targetArg, cwd, options) {
5780
6174
  }
5781
6175
 
5782
6176
  // src/commands/sandbox/init.ts
5783
- import { existsSync as existsSync21, mkdirSync as mkdirSync9, readFileSync as readFileSync24, rmSync as rmSync2 } from "fs";
5784
- import { isAbsolute as isAbsolute2, join as join22, resolve as resolve15, 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";
5785
6179
  import { fileURLToPath as fileURLToPath3 } from "url";
5786
- import * as childProcess2 from "child_process";
6180
+ import * as childProcess3 from "child_process";
5787
6181
 
5788
6182
  // src/commands/sandbox/parse-handoff.ts
5789
- import { readFileSync as readFileSync22, existsSync as existsSync19 } from "fs";
5790
- import { dirname as dirname8, resolve as resolve14, isAbsolute } from "path";
6183
+ import { readFileSync as readFileSync25, existsSync as existsSync21 } from "fs";
6184
+ import { dirname as dirname9, resolve as resolve15, isAbsolute } from "path";
5791
6185
  function parseHandoff(handoffPath) {
5792
- const text = readFileSync22(handoffPath, "utf-8");
6186
+ const text = readFileSync25(handoffPath, "utf-8");
5793
6187
  const lines2 = text.split("\n");
5794
6188
  const warnings = [];
5795
6189
  const pattern2 = extractPatternSlug(lines2, warnings);
5796
6190
  const theme2 = extractMetaField(lines2, "Theme");
5797
6191
  const recipePath = extractRecipePath(text, handoffPath, warnings);
5798
6192
  const primitives = extractPrimitives(text, warnings);
5799
- const { screens, mockShapes } = recipePath && existsSync19(recipePath) ? extractRecipeArtifacts(recipePath, warnings) : { screens: [], mockShapes: [] };
6193
+ const { screens, mockShapes } = recipePath && existsSync21(recipePath) ? extractRecipeArtifacts(recipePath, warnings) : { screens: [], mockShapes: [] };
5800
6194
  return { pattern: pattern2, theme: theme2, recipePath, primitives, screens, mockShapes, warnings };
5801
6195
  }
5802
6196
  function extractPatternSlug(lines2, warnings) {
@@ -5828,7 +6222,7 @@ function extractRecipePath(text, handoffPath, warnings) {
5828
6222
  }
5829
6223
  const href = m[2].trim();
5830
6224
  if (isAbsolute(href)) return href;
5831
- return resolve14(dirname8(handoffPath), href);
6225
+ return resolve15(dirname9(handoffPath), href);
5832
6226
  }
5833
6227
  var STATUS_PATTERNS = [
5834
6228
  { re: /NEW\s+gap/i, status: "gap-new" },
@@ -5875,7 +6269,7 @@ function extractPrimitives(text, warnings) {
5875
6269
  return primitives;
5876
6270
  }
5877
6271
  function extractRecipeArtifacts(recipePath, warnings) {
5878
- const text = readFileSync22(recipePath, "utf-8");
6272
+ const text = readFileSync25(recipePath, "utf-8");
5879
6273
  const screens = extractScreens(text);
5880
6274
  const mockShapes = extractMockFields(text, warnings);
5881
6275
  return { screens, mockShapes };
@@ -5949,8 +6343,8 @@ function tryPort(port) {
5949
6343
  }
5950
6344
 
5951
6345
  // src/commands/sandbox/scaffold.ts
5952
- import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync12, existsSync as existsSync20, readdirSync as readdirSync12 } from "fs";
5953
- 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";
5954
6348
 
5955
6349
  // src/commands/sandbox/templates.ts
5956
6350
  var SANDBOX_PACKAGE_VERSION = "16.2.6";
@@ -6450,12 +6844,12 @@ function toPascalCase(s) {
6450
6844
 
6451
6845
  // src/commands/sandbox/scaffold.ts
6452
6846
  function writeScaffold(sandboxDir, manifest, port, options = {}) {
6453
- mkdirSync7(sandboxDir, { recursive: true });
6847
+ mkdirSync8(sandboxDir, { recursive: true });
6454
6848
  const created = [];
6455
6849
  const writeIfNew = (rel, contents) => {
6456
- const full = join20(sandboxDir, rel);
6457
- mkdirSync7(dirOf(full), { recursive: true });
6458
- writeFileSync12(full, contents, "utf-8");
6850
+ const full = join22(sandboxDir, rel);
6851
+ mkdirSync8(dirOf(full), { recursive: true });
6852
+ writeFileSync13(full, contents, "utf-8");
6459
6853
  created.push(rel);
6460
6854
  };
6461
6855
  writeIfNew("package.json", packageJsonTemplate(manifest.pattern));
@@ -6481,7 +6875,7 @@ function writeScaffold(sandboxDir, manifest, port, options = {}) {
6481
6875
  }
6482
6876
  }
6483
6877
  writeIfNew("playwright.capture.mjs", captureScriptTemplate());
6484
- mkdirSync7(join20(sandboxDir, "captures", "approved"), { recursive: true });
6878
+ mkdirSync8(join22(sandboxDir, "captures", "approved"), { recursive: true });
6485
6879
  return { created };
6486
6880
  }
6487
6881
  function dirOf(p) {
@@ -6515,10 +6909,10 @@ function writeSandboxConfig(sandboxDir, manifest, port, options) {
6515
6909
  stripChromeSelectors: options.prototypeImport.stripChromeSelectors
6516
6910
  } : null
6517
6911
  };
6518
- 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");
6519
6913
  }
6520
6914
  function sandboxIsEmpty(sandboxDir) {
6521
- if (!existsSync20(sandboxDir)) return true;
6915
+ if (!existsSync22(sandboxDir)) return true;
6522
6916
  try {
6523
6917
  return readdirSync12(sandboxDir).filter((f) => !f.startsWith(".")).length === 0;
6524
6918
  } catch {
@@ -6527,8 +6921,8 @@ function sandboxIsEmpty(sandboxDir) {
6527
6921
  }
6528
6922
 
6529
6923
  // src/commands/sandbox/html-prototype.ts
6530
- import { mkdirSync as mkdirSync8, readdirSync as readdirSync13, readFileSync as readFileSync23, statSync as statSync9, writeFileSync as writeFileSync13 } from "fs";
6531
- 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";
6532
6926
 
6533
6927
  // src/commands/sandbox/strip-chrome.ts
6534
6928
  var DEFAULT_STRIP_SELECTORS = [
@@ -6712,8 +7106,8 @@ function stripDocumentaryChrome(html, selectors) {
6712
7106
  var SCREEN_FILE_PATTERN = /^screen-(\d+)-([^/.]+)\.html$/i;
6713
7107
  function copyHtmlPrototype(sourceDir, sandboxDir, manifest, options = {}) {
6714
7108
  const warnings = [];
6715
- const destAbs = join21(sandboxDir, "public", "prototype");
6716
- mkdirSync8(destAbs, { recursive: true });
7109
+ const destAbs = join23(sandboxDir, "public", "prototype");
7110
+ mkdirSync9(destAbs, { recursive: true });
6717
7111
  const stripChromeSelectors = options.stripChromeSelectors ?? [];
6718
7112
  const copiedFiles = copyTreeRelative(sourceDir, destAbs, "", stripChromeSelectors);
6719
7113
  const screenFiles = listOrderedScreenFiles(sourceDir);
@@ -6766,24 +7160,24 @@ function deriveStateCoverageScreen(file, existingNames) {
6766
7160
  }
6767
7161
  function copyTreeRelative(srcDir, destDir, relDir = "", stripChromeSelectors = []) {
6768
7162
  const out = [];
6769
- const entries = readdirSync13(join21(srcDir, relDir));
7163
+ const entries = readdirSync13(join23(srcDir, relDir));
6770
7164
  for (const name of entries) {
6771
7165
  if (name.startsWith(".")) continue;
6772
- const srcPath = join21(srcDir, relDir, name);
6773
- const destPath = join21(destDir, relDir, name);
7166
+ const srcPath = join23(srcDir, relDir, name);
7167
+ const destPath = join23(destDir, relDir, name);
6774
7168
  const stat = statSync9(srcPath);
6775
7169
  if (stat.isDirectory()) {
6776
- mkdirSync8(destPath, { recursive: true });
6777
- out.push(...copyTreeRelative(srcDir, destDir, join21(relDir, name), stripChromeSelectors));
7170
+ mkdirSync9(destPath, { recursive: true });
7171
+ out.push(...copyTreeRelative(srcDir, destDir, join23(relDir, name), stripChromeSelectors));
6778
7172
  } else if (stat.isFile()) {
6779
7173
  if (stripChromeSelectors.length > 0 && name.toLowerCase().endsWith(".html")) {
6780
- const html = readFileSync23(srcPath, "utf-8");
7174
+ const html = readFileSync26(srcPath, "utf-8");
6781
7175
  const stripped = stripDocumentaryChrome(html, stripChromeSelectors);
6782
- writeFileSync13(destPath, stripped);
7176
+ writeFileSync14(destPath, stripped);
6783
7177
  } else {
6784
- writeFileSync13(destPath, readFileSync23(srcPath));
7178
+ writeFileSync14(destPath, readFileSync26(srcPath));
6785
7179
  }
6786
- out.push(join21("public", "prototype", relDir, name).replace(/\\/g, "/"));
7180
+ out.push(join23("public", "prototype", relDir, name).replace(/\\/g, "/"));
6787
7181
  }
6788
7182
  }
6789
7183
  return out;
@@ -6835,8 +7229,8 @@ async function runInit(name, cwd, options) {
6835
7229
  if (!name || !/^[a-z0-9][a-z0-9-_]*$/i.test(name)) {
6836
7230
  throw new Error(`Invalid sandbox name '${name}'. Use letters, digits, '-' or '_'.`);
6837
7231
  }
6838
- const handoffPath = isAbsolute2(options.handoff) ? options.handoff : resolve15(cwd, options.handoff);
6839
- if (!existsSync21(handoffPath)) {
7232
+ const handoffPath = isAbsolute2(options.handoff) ? options.handoff : resolve16(cwd, options.handoff);
7233
+ if (!existsSync23(handoffPath)) {
6840
7234
  throw new Error(`Handoff manifest not found: ${handoffPath}`);
6841
7235
  }
6842
7236
  const manifest = parseHandoff(handoffPath);
@@ -6845,7 +7239,7 @@ async function runInit(name, cwd, options) {
6845
7239
  `Handoff has no primitives \u2014 refusing to scaffold an empty sandbox. Check the manifest at ${handoffPath}`
6846
7240
  );
6847
7241
  }
6848
- const sandboxDir = join22(cwd, ".lo", "sandbox", name);
7242
+ const sandboxDir = join24(cwd, ".lo", "sandbox", name);
6849
7243
  if (!sandboxIsEmpty(sandboxDir)) {
6850
7244
  if (!options.overwrite) {
6851
7245
  throw new Error(
@@ -6854,12 +7248,12 @@ async function runInit(name, cwd, options) {
6854
7248
  }
6855
7249
  rmSync2(sandboxDir, { recursive: true, force: true });
6856
7250
  }
6857
- mkdirSync9(sandboxDir, { recursive: true });
7251
+ mkdirSync10(sandboxDir, { recursive: true });
6858
7252
  const port = await findOpenPort();
6859
7253
  let prototypeImport;
6860
7254
  if (options.fromHtmlPrototype) {
6861
- const prototypeDir = isAbsolute2(options.fromHtmlPrototype) ? options.fromHtmlPrototype : resolve15(cwd, options.fromHtmlPrototype);
6862
- if (!existsSync21(prototypeDir)) {
7255
+ const prototypeDir = isAbsolute2(options.fromHtmlPrototype) ? options.fromHtmlPrototype : resolve16(cwd, options.fromHtmlPrototype);
7256
+ if (!existsSync23(prototypeDir)) {
6863
7257
  throw new Error(`HTML prototype directory not found: ${prototypeDir}`);
6864
7258
  }
6865
7259
  const stripChromeSelectors = resolveStripSelectors(
@@ -6918,22 +7312,22 @@ async function runInit(name, cwd, options) {
6918
7312
  function applyThemeIfPossible(sandboxDir, manifest, theme2, cwd, json, themeFile) {
6919
7313
  const candidates = [];
6920
7314
  if (themeFile) {
6921
- candidates.push(isAbsolute2(themeFile) ? themeFile : resolve15(cwd, themeFile));
7315
+ candidates.push(isAbsolute2(themeFile) ? themeFile : resolve16(cwd, themeFile));
6922
7316
  }
6923
- candidates.push(isAbsolute2(theme2) ? theme2 : resolve15(cwd, theme2));
7317
+ candidates.push(isAbsolute2(theme2) ? theme2 : resolve16(cwd, theme2));
6924
7318
  const privateRoot = process.env.VISOR_THEMES_PRIVATE_PATH;
6925
7319
  if (privateRoot && privateRoot.length > 0) {
6926
- candidates.push(join22(privateRoot, "themes", theme2, "theme.visor.yaml"));
7320
+ candidates.push(join24(privateRoot, "themes", theme2, "theme.visor.yaml"));
6927
7321
  }
6928
7322
  candidates.push(
6929
- join22(cwd, "themes", `${theme2}.visor.yaml`),
6930
- join22(cwd, "custom-themes", `${theme2}.visor.yaml`)
7323
+ join24(cwd, "themes", `${theme2}.visor.yaml`),
7324
+ join24(cwd, "custom-themes", `${theme2}.visor.yaml`)
6931
7325
  );
6932
- const yamlPath = candidates.find((p) => existsSync21(p));
7326
+ const yamlPath = candidates.find((p) => existsSync23(p));
6933
7327
  if (!yamlPath) {
6934
7328
  const searched = candidates.join(", ");
6935
7329
  manifest.warnings.push(
6936
- `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.`
6937
7331
  );
6938
7332
  if (!json) {
6939
7333
  logger.warn(`Theme '${theme2}' not found \u2014 leaving placeholder globals.css.`);
@@ -6941,12 +7335,12 @@ function applyThemeIfPossible(sandboxDir, manifest, theme2, cwd, json, themeFile
6941
7335
  `Re-run with --theme-file <path>, or set VISOR_THEMES_PRIVATE_PATH=<dir-containing-themes/${theme2}/theme.visor.yaml>.`
6942
7336
  );
6943
7337
  logger.item(
6944
- `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")}`
6945
7339
  );
6946
7340
  }
6947
7341
  return;
6948
7342
  }
6949
- const globalsOut = join22(sandboxDir, "app", "globals.css");
7343
+ const globalsOut = join24(sandboxDir, "app", "globals.css");
6950
7344
  try {
6951
7345
  themeApplyCommand(yamlPath, sandboxDir, {
6952
7346
  output: globalsOut,
@@ -6977,7 +7371,7 @@ function tryAddPrimitive(primitive, sandboxDir, json) {
6977
7371
  }
6978
7372
  function runNpmInstall(sandboxDir, json) {
6979
7373
  if (!json) logger.info("Installing sandbox dependencies...");
6980
- const result = childProcess2.spawnSync("npm", ["install", "--no-audit", "--no-fund"], {
7374
+ const result = childProcess3.spawnSync("npm", ["install", "--no-audit", "--no-fund"], {
6981
7375
  cwd: sandboxDir,
6982
7376
  stdio: json ? "ignore" : "inherit"
6983
7377
  });
@@ -6999,12 +7393,12 @@ function loadKnownPrimitives() {
6999
7393
  }
7000
7394
  function readCliVersion() {
7001
7395
  try {
7002
- const here = dirname9(fileURLToPath3(import.meta.url));
7396
+ const here = dirname10(fileURLToPath3(import.meta.url));
7003
7397
  for (let i = 0; i < 6; i++) {
7004
7398
  const segments = new Array(i).fill("..");
7005
- const candidate = join22(here, ...segments, "package.json");
7399
+ const candidate = join24(here, ...segments, "package.json");
7006
7400
  try {
7007
- const pkg2 = JSON.parse(readFileSync24(candidate, "utf-8"));
7401
+ const pkg2 = JSON.parse(readFileSync27(candidate, "utf-8"));
7008
7402
  if (pkg2.name === "@loworbitstudio/visor" && pkg2.version) return pkg2.version;
7009
7403
  } catch {
7010
7404
  }
@@ -7015,14 +7409,14 @@ function readCliVersion() {
7015
7409
  }
7016
7410
 
7017
7411
  // src/commands/sandbox/dev.ts
7018
- import { existsSync as existsSync22, readFileSync as readFileSync25 } from "fs";
7019
- import { join as join23 } from "path";
7020
- 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";
7021
7415
  function sandboxDevCommand(cwd, options) {
7022
7416
  const json = options.json ?? false;
7023
- const sandboxDir = join23(cwd, ".lo", "sandbox", options.name);
7024
- const configPath = join23(sandboxDir, "sandbox.json");
7025
- if (!existsSync22(configPath)) {
7417
+ const sandboxDir = join25(cwd, ".lo", "sandbox", options.name);
7418
+ const configPath = join25(sandboxDir, "sandbox.json");
7419
+ if (!existsSync24(configPath)) {
7026
7420
  fail(
7027
7421
  json,
7028
7422
  `Sandbox '${options.name}' not found at ${sandboxDir}. Run 'visor sandbox init ${options.name} --handoff ... --theme ...' first.`
@@ -7031,7 +7425,7 @@ function sandboxDevCommand(cwd, options) {
7031
7425
  }
7032
7426
  let config;
7033
7427
  try {
7034
- config = JSON.parse(readFileSync25(configPath, "utf-8"));
7428
+ config = JSON.parse(readFileSync28(configPath, "utf-8"));
7035
7429
  } catch (err) {
7036
7430
  fail(json, `Invalid sandbox.json at ${configPath}: ${err.message}`);
7037
7431
  return;
@@ -7058,7 +7452,7 @@ function sandboxDevCommand(cwd, options) {
7058
7452
  spawnNextDev(sandboxDir, config.port, json);
7059
7453
  }
7060
7454
  function spawnNextDev(sandboxDir, port, json) {
7061
- const child = childProcess3.spawn(
7455
+ const child = childProcess4.spawn(
7062
7456
  "npx",
7063
7457
  ["--no-install", "next", "dev", "--port", String(port)],
7064
7458
  {
@@ -7089,20 +7483,20 @@ function fail(json, message) {
7089
7483
  // src/commands/sandbox/approve.ts
7090
7484
  import {
7091
7485
  copyFileSync as copyFileSync2,
7092
- existsSync as existsSync23,
7093
- mkdirSync as mkdirSync10,
7094
- readFileSync as readFileSync26,
7486
+ existsSync as existsSync25,
7487
+ mkdirSync as mkdirSync11,
7488
+ readFileSync as readFileSync29,
7095
7489
  readdirSync as readdirSync14,
7096
7490
  rmSync as rmSync3,
7097
- writeFileSync as writeFileSync14
7491
+ writeFileSync as writeFileSync15
7098
7492
  } from "fs";
7099
- import { basename as basename6, join as join24 } from "path";
7100
- import * as childProcess4 from "child_process";
7493
+ import { basename as basename7, join as join26 } from "path";
7494
+ import * as childProcess5 from "child_process";
7101
7495
  function sandboxApproveCommand(cwd, options) {
7102
7496
  const json = options.json ?? false;
7103
- const sandboxDir = join24(cwd, ".lo", "sandbox", options.name);
7104
- const configPath = join24(sandboxDir, "sandbox.json");
7105
- if (!existsSync23(configPath)) {
7497
+ const sandboxDir = join26(cwd, ".lo", "sandbox", options.name);
7498
+ const configPath = join26(sandboxDir, "sandbox.json");
7499
+ if (!existsSync25(configPath)) {
7106
7500
  fail2(
7107
7501
  json,
7108
7502
  `Sandbox '${options.name}' not found at ${sandboxDir}. Run 'visor sandbox init' first.`
@@ -7116,11 +7510,11 @@ function sandboxApproveCommand(cwd, options) {
7116
7510
  runCapture(sandboxDir, configPath, options.name, json);
7117
7511
  }
7118
7512
  function runCapture(sandboxDir, configPath, sandboxName, json) {
7119
- const config = JSON.parse(readFileSync26(configPath, "utf-8"));
7120
- const captureScriptPath = join24(sandboxDir, "playwright.capture.mjs");
7121
- 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");
7122
7516
  ensurePlaywrightInstalled(sandboxDir, json);
7123
- const result = childProcess4.spawnSync(
7517
+ const result = childProcess5.spawnSync(
7124
7518
  "npx",
7125
7519
  ["--no-install", "node", "playwright.capture.mjs"],
7126
7520
  {
@@ -7139,9 +7533,9 @@ function runCapture(sandboxDir, configPath, sandboxName, json) {
7139
7533
  ${result.stderr}`);
7140
7534
  return;
7141
7535
  }
7142
- const pendingDir = join24(sandboxDir, "captures", "pending");
7143
- const diffsDir = join24(sandboxDir, "captures", "diffs");
7144
- 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");
7145
7539
  const pendingFiles = safeListPngs(pendingDir);
7146
7540
  const diffFiles = safeListPngs(diffsDir);
7147
7541
  const hasBaseline = safeListPngs(approvedDir).length > 0;
@@ -7178,9 +7572,9 @@ ${result.stderr}`);
7178
7572
  }
7179
7573
  }
7180
7574
  function runPromotion(sandboxDir, sandboxName, json) {
7181
- const pendingDir = join24(sandboxDir, "captures", "pending");
7182
- const approvedDir = join24(sandboxDir, "captures", "approved");
7183
- 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");
7184
7578
  const pendingFiles = safeListPngs(pendingDir);
7185
7579
  if (pendingFiles.length === 0) {
7186
7580
  fail2(
@@ -7189,10 +7583,10 @@ function runPromotion(sandboxDir, sandboxName, json) {
7189
7583
  );
7190
7584
  return;
7191
7585
  }
7192
- mkdirSync10(approvedDir, { recursive: true });
7586
+ mkdirSync11(approvedDir, { recursive: true });
7193
7587
  const promoted = [];
7194
7588
  for (const src of pendingFiles) {
7195
- const dest = join24(approvedDir, basename6(src));
7589
+ const dest = join26(approvedDir, basename7(src));
7196
7590
  copyFileSync2(src, dest);
7197
7591
  promoted.push(dest);
7198
7592
  }
@@ -7212,10 +7606,10 @@ function runPromotion(sandboxDir, sandboxName, json) {
7212
7606
  }
7213
7607
  }
7214
7608
  function ensurePlaywrightInstalled(sandboxDir, json) {
7215
- const markerPath = join24(sandboxDir, ".playwright-installed");
7216
- if (existsSync23(markerPath)) return;
7609
+ const markerPath = join26(sandboxDir, ".playwright-installed");
7610
+ if (existsSync25(markerPath)) return;
7217
7611
  if (!json) logger.info("Installing Playwright Chromium (one-time)...");
7218
- const result = childProcess4.spawnSync(
7612
+ const result = childProcess5.spawnSync(
7219
7613
  "npx",
7220
7614
  ["--no-install", "playwright", "install", "chromium"],
7221
7615
  {
@@ -7229,11 +7623,11 @@ function ensurePlaywrightInstalled(sandboxDir, json) {
7229
7623
  if (typeof result.status === "number" && result.status !== 0) {
7230
7624
  throw new Error(`playwright install exited with code ${result.status}`);
7231
7625
  }
7232
- writeFileSync14(markerPath, (/* @__PURE__ */ new Date()).toISOString(), "utf-8");
7626
+ writeFileSync15(markerPath, (/* @__PURE__ */ new Date()).toISOString(), "utf-8");
7233
7627
  }
7234
7628
  function safeListPngs(dir) {
7235
- if (!existsSync23(dir)) return [];
7236
- 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));
7237
7631
  }
7238
7632
  function tryParseJson(s) {
7239
7633
  try {
@@ -7251,15 +7645,372 @@ function fail2(json, message) {
7251
7645
  process.exit(1);
7252
7646
  }
7253
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
+
7254
8005
  // src/index.ts
7255
- var __dirname2 = dirname10(fileURLToPath4(import.meta.url));
8006
+ var __dirname2 = dirname11(fileURLToPath4(import.meta.url));
7256
8007
  var pkg = JSON.parse(
7257
- readFileSync27(join25(__dirname2, "..", "package.json"), "utf-8")
8008
+ readFileSync32(join30(__dirname2, "..", "package.json"), "utf-8")
7258
8009
  );
7259
8010
  var program = new Command2();
7260
8011
  program.name("visor").description("CLI for the Visor design system").version(pkg.version);
7261
8012
  program.addCommand(checkCommand());
7262
- 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) => {
7263
8014
  initCommand(process.cwd(), options);
7264
8015
  });
7265
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) => {
@@ -7444,4 +8195,9 @@ sandbox.command("approve").description(
7444
8195
  sandboxApproveCommand(process.cwd(), options);
7445
8196
  }
7446
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
+ });
7447
8203
  program.parse();