@loworbitstudio/visor 0.1.0 → 0.5.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,21 +1,569 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
+ import { Command as Command2 } from "commander";
5
+
6
+ // src/commands/check.ts
4
7
  import { Command } from "commander";
5
8
 
9
+ // src/registry/resolve.ts
10
+ import { readFileSync } from "fs";
11
+ import { join, dirname } from "path";
12
+ import { fileURLToPath } from "url";
13
+ var __dirname = dirname(fileURLToPath(import.meta.url));
14
+ var cachedRegistry = null;
15
+ var cachedManifest = null;
16
+ function loadRegistry() {
17
+ if (cachedRegistry) return cachedRegistry;
18
+ const registryPath = join(__dirname, "registry.json");
19
+ const raw = readFileSync(registryPath, "utf-8");
20
+ cachedRegistry = JSON.parse(raw);
21
+ return cachedRegistry;
22
+ }
23
+ function loadManifest() {
24
+ if (cachedManifest) return cachedManifest;
25
+ const manifestPath = join(__dirname, "visor-manifest.json");
26
+ const raw = readFileSync(manifestPath, "utf-8");
27
+ cachedManifest = JSON.parse(raw);
28
+ return cachedManifest;
29
+ }
30
+ function findItem(registry, name) {
31
+ return registry.items.find((item) => item.name === name);
32
+ }
33
+ function resolveTransitiveDeps(registry, names, onWarning) {
34
+ const resolved = /* @__PURE__ */ new Map();
35
+ const queue = names.map((n) => ({
36
+ name: n,
37
+ ancestors: /* @__PURE__ */ new Set()
38
+ }));
39
+ while (queue.length > 0) {
40
+ const { name, ancestors } = queue.shift();
41
+ if (resolved.has(name)) continue;
42
+ const item = findItem(registry, name);
43
+ if (!item) {
44
+ throw new Error(`Registry item "${name}" not found.`);
45
+ }
46
+ resolved.set(name, item);
47
+ if (item.registryDependencies) {
48
+ const childAncestors = new Set(ancestors);
49
+ childAncestors.add(name);
50
+ for (const dep of item.registryDependencies) {
51
+ if (childAncestors.has(dep)) {
52
+ onWarning?.(`Circular registry dependency: ${name} \u2192 ${dep}`);
53
+ } else if (!resolved.has(dep)) {
54
+ queue.push({ name: dep, ancestors: childAncestors });
55
+ }
56
+ }
57
+ }
58
+ }
59
+ return Array.from(resolved.values());
60
+ }
61
+ function collectDependencies(items) {
62
+ const deps = /* @__PURE__ */ new Set();
63
+ const devDeps = /* @__PURE__ */ new Set();
64
+ const pubDeps = /* @__PURE__ */ new Map();
65
+ for (const item of items) {
66
+ if (item.dependencies) {
67
+ for (const dep of item.dependencies) {
68
+ deps.add(dep);
69
+ }
70
+ }
71
+ if (item.devDependencies) {
72
+ for (const dep of item.devDependencies) {
73
+ devDeps.add(dep);
74
+ }
75
+ }
76
+ if (item.pubDependencies) {
77
+ for (const dep of item.pubDependencies) {
78
+ pubDeps.set(dep.pub, dep);
79
+ }
80
+ }
81
+ }
82
+ return {
83
+ dependencies: Array.from(deps).sort(),
84
+ devDependencies: Array.from(devDeps).sort(),
85
+ pubDependencies: Array.from(pubDeps.values()).sort(
86
+ (a, b) => a.pub.localeCompare(b.pub)
87
+ )
88
+ };
89
+ }
90
+ function filterItemsByTarget(items, target) {
91
+ return items.filter(
92
+ (item) => item.target === target || item.target === void 0
93
+ );
94
+ }
95
+ function slug(name) {
96
+ return name.toLowerCase().replace(/[^a-z0-9]/g, "");
97
+ }
98
+ function findItemForTarget(registry, name, target) {
99
+ const needle = slug(name);
100
+ return registry.items.find(
101
+ (item) => slug(item.name) === needle && item.target === target
102
+ ) ?? registry.items.find(
103
+ (item) => slug(item.name) === needle && item.target === void 0
104
+ );
105
+ }
106
+
107
+ // src/check/catalog.ts
108
+ var STOP_WORDS = /* @__PURE__ */ new Set([
109
+ "a",
110
+ "an",
111
+ "the",
112
+ "with",
113
+ "for",
114
+ "and",
115
+ "or",
116
+ "to",
117
+ "in",
118
+ "of",
119
+ "is",
120
+ "that",
121
+ "this",
122
+ "it",
123
+ "as",
124
+ "at",
125
+ "by",
126
+ "on",
127
+ "be",
128
+ "are",
129
+ "was",
130
+ "were"
131
+ ]);
132
+ function toKebab(s) {
133
+ return s.replace(/([A-Z])/g, (m) => `-${m.toLowerCase()}`).replace(/^-/, "").toLowerCase();
134
+ }
135
+ function tokenize(text) {
136
+ return text.toLowerCase().split(/[\s\-_,]+/).filter((t) => t.length > 1 && !STOP_WORDS.has(t));
137
+ }
138
+ function installCmd(name, type) {
139
+ if (type === "block") return `npx visor add ${name} --block`;
140
+ if (type === "pattern") return null;
141
+ return `npx visor add ${name}`;
142
+ }
143
+ function getAllCatalogItems(manifest) {
144
+ const items = [];
145
+ for (const [name, c] of Object.entries(manifest.components)) {
146
+ items.push({ type: "component", name, category: c.category, description: c.description });
147
+ for (const sub of c.sub_components ?? []) {
148
+ items.push({ type: "component", name: toKebab(sub.name), category: c.category, description: sub.description });
149
+ }
150
+ }
151
+ for (const [name, b] of Object.entries(manifest.blocks)) {
152
+ items.push({ type: "block", name, category: b.category, description: b.description });
153
+ }
154
+ for (const [name, h] of Object.entries(manifest.hooks)) {
155
+ items.push({ type: "hook", name, description: h.description });
156
+ }
157
+ for (const [name, p] of Object.entries(manifest.patterns)) {
158
+ items.push({ type: "pattern", name, description: p.description });
159
+ }
160
+ return items;
161
+ }
162
+ function findByName(manifest, pattern2) {
163
+ const normalized = toKebab(pattern2);
164
+ if (normalized in manifest.components) {
165
+ const c = manifest.components[normalized];
166
+ return { found: true, name: normalized, type: "component", category: c.category, description: c.description, installCmd: `npx visor add ${normalized}` };
167
+ }
168
+ if (normalized in manifest.blocks) {
169
+ const b = manifest.blocks[normalized];
170
+ return { found: true, name: normalized, type: "block", category: b.category, description: b.description, installCmd: `npx visor add ${normalized} --block` };
171
+ }
172
+ if (normalized in manifest.hooks) {
173
+ const h = manifest.hooks[normalized];
174
+ return { found: true, name: normalized, type: "hook", description: h.description, installCmd: `npx visor add ${normalized}` };
175
+ }
176
+ if (normalized in manifest.patterns) {
177
+ const p = manifest.patterns[normalized];
178
+ return { found: true, name: normalized, type: "pattern", description: p.description, installCmd: null };
179
+ }
180
+ for (const [parentName, c] of Object.entries(manifest.components)) {
181
+ for (const sub of c.sub_components ?? []) {
182
+ if (toKebab(sub.name) === normalized) {
183
+ return { found: true, name: toKebab(sub.name), type: "component", category: c.category, description: sub.description, installCmd: `npx visor add ${parentName}` };
184
+ }
185
+ }
186
+ }
187
+ return { found: false };
188
+ }
189
+ function fuzzyFind(manifest, query, limit = 5) {
190
+ const queryTokens = tokenize(query);
191
+ if (queryTokens.length === 0) return [];
192
+ const results = [];
193
+ for (const item of getAllCatalogItems(manifest)) {
194
+ const searchText = [item.name, item.description].join(" ").toLowerCase();
195
+ const matched = queryTokens.filter((t) => searchText.includes(t));
196
+ if (matched.length > 0) {
197
+ results.push({
198
+ name: item.name,
199
+ type: item.type,
200
+ category: item.category,
201
+ description: item.description,
202
+ score: matched.length,
203
+ matchReason: `Matched: ${matched.join(", ")}`,
204
+ installCmd: installCmd(item.name, item.type)
205
+ });
206
+ }
207
+ }
208
+ return results.sort((a, b) => b.score - a.score).slice(0, limit);
209
+ }
210
+
211
+ // src/check/jsx-scan.ts
212
+ import { readFileSync as readFileSync2, readdirSync, statSync } from "fs";
213
+ import { resolve, extname, join as join2 } from "path";
214
+
215
+ // src/check/native-map.ts
216
+ var NATIVE_TO_VISOR = {
217
+ button: { visorName: "button", displayName: "Button" },
218
+ textarea: { visorName: "textarea", displayName: "Textarea" },
219
+ form: { visorName: "form", displayName: "Form" },
220
+ label: { visorName: "label", displayName: "Label" },
221
+ fieldset: { visorName: "fieldset", displayName: "Fieldset" },
222
+ select: { visorName: "select", displayName: "Select" },
223
+ table: { visorName: "table", displayName: "Table", notes: "use DataTable for full interactive features" },
224
+ img: { visorName: "image", displayName: "Image" },
225
+ dialog: { visorName: "dialog", displayName: "Dialog" },
226
+ details: { visorName: "accordion", displayName: "Accordion", notes: "or Collapsible for a single section" },
227
+ summary: { visorName: "accordion", displayName: "Accordion", notes: "wrap as Accordion trigger" }
228
+ };
229
+ var INPUT_TYPE_MAP = {
230
+ number: { visorName: "number-input", displayName: "NumberInput" },
231
+ password: { visorName: "password-input", displayName: "PasswordInput" },
232
+ search: { visorName: "search-input", displayName: "SearchInput" },
233
+ tel: { visorName: "phone-input", displayName: "PhoneInput" },
234
+ phone: { visorName: "phone-input", displayName: "PhoneInput" },
235
+ // All other input types (text, email, url, date, etc.) map to the base Input.
236
+ _default: { visorName: "input", displayName: "Input" }
237
+ };
238
+
239
+ // src/check/jsx-scan.ts
240
+ async function getBabelParser() {
241
+ const { parse } = await import("@babel/parser");
242
+ return parse;
243
+ }
244
+ function walk(node, visit) {
245
+ if (!node || typeof node !== "object") return;
246
+ const obj = node;
247
+ visit(obj);
248
+ for (const key of Object.keys(obj)) {
249
+ if (key === "parent" || key === "tokens" || key === "errors") continue;
250
+ const val = obj[key];
251
+ if (Array.isArray(val)) {
252
+ for (const child of val) walk(child, visit);
253
+ } else if (val && typeof val === "object" && "type" in val) {
254
+ walk(val, visit);
255
+ }
256
+ }
257
+ }
258
+ function getInputType(attribs) {
259
+ for (const attr of attribs) {
260
+ const a = attr;
261
+ if (a.type !== "JSXAttribute") continue;
262
+ const nameNode = a.name;
263
+ if (nameNode?.name !== "type") continue;
264
+ const valueNode = a.value;
265
+ if (!valueNode) continue;
266
+ if (valueNode.type === "StringLiteral") {
267
+ return String(valueNode.value ?? "text");
268
+ }
269
+ if (valueNode.type === "JSXExpressionContainer") {
270
+ const expr = valueNode.expression;
271
+ if (expr?.type === "StringLiteral") return String(expr.value ?? "text");
272
+ }
273
+ }
274
+ return "_default";
275
+ }
276
+ function collectJsxFindings(source, filePath, parse) {
277
+ let ast;
278
+ try {
279
+ ast = parse(source, {
280
+ sourceType: "module",
281
+ plugins: ["jsx", "typescript"],
282
+ errorRecovery: true
283
+ });
284
+ } catch {
285
+ return [];
286
+ }
287
+ const findings = [];
288
+ walk(ast, (node) => {
289
+ if (node.type !== "JSXOpeningElement") return;
290
+ const nameNode = node.name;
291
+ if (!nameNode) return;
292
+ const tagName = nameNode.type === "JSXIdentifier" ? String(nameNode.name ?? "") : "";
293
+ if (!tagName || tagName[0] !== tagName[0].toLowerCase()) return;
294
+ const loc = node.loc;
295
+ const line = loc?.start?.line ?? 0;
296
+ const column = loc?.start?.column ?? 0;
297
+ if (tagName === "input") {
298
+ const attribs = node.attributes ?? [];
299
+ const typeVal = getInputType(attribs);
300
+ const mapping2 = INPUT_TYPE_MAP[typeVal] ?? INPUT_TYPE_MAP["_default"];
301
+ findings.push({
302
+ file: filePath,
303
+ line,
304
+ column,
305
+ nativeTag: typeVal !== "_default" ? `input[type=${typeVal}]` : "input",
306
+ suggestedPrimitive: mapping2.displayName,
307
+ installCmd: `npx visor add ${mapping2.visorName}`
308
+ });
309
+ return;
310
+ }
311
+ const mapping = NATIVE_TO_VISOR[tagName];
312
+ if (!mapping) return;
313
+ const finding = {
314
+ file: filePath,
315
+ line,
316
+ column,
317
+ nativeTag: tagName,
318
+ suggestedPrimitive: mapping.displayName,
319
+ installCmd: `npx visor add ${mapping.visorName}`
320
+ };
321
+ if (mapping.notes) finding.rationale = mapping.notes;
322
+ findings.push(finding);
323
+ });
324
+ return findings;
325
+ }
326
+ function collectFiles(pathArg) {
327
+ const JSX_EXTS = /* @__PURE__ */ new Set([".jsx", ".tsx", ".js", ".ts"]);
328
+ try {
329
+ const s = statSync(pathArg);
330
+ if (s.isDirectory()) {
331
+ let recurse2 = function(dir) {
332
+ for (const entry of readdirSync(dir)) {
333
+ if (entry.startsWith(".") || entry === "node_modules") continue;
334
+ const full = join2(dir, entry);
335
+ const es = statSync(full);
336
+ if (es.isDirectory()) recurse2(full);
337
+ else if (JSX_EXTS.has(extname(full))) files.push(full);
338
+ }
339
+ };
340
+ var recurse = recurse2;
341
+ const files = [];
342
+ recurse2(pathArg);
343
+ return files;
344
+ }
345
+ if (JSX_EXTS.has(extname(pathArg))) return [pathArg];
346
+ } catch {
347
+ }
348
+ return [];
349
+ }
350
+ async function scanJsx(pathArg) {
351
+ const parse = await getBabelParser();
352
+ let files;
353
+ let stdinMode = false;
354
+ if (pathArg === "-") {
355
+ stdinMode = true;
356
+ files = ["<stdin>"];
357
+ } else {
358
+ files = collectFiles(resolve(pathArg));
359
+ }
360
+ const allFindings = [];
361
+ for (const file of files) {
362
+ let source;
363
+ if (stdinMode) {
364
+ source = readFileSync2(0, "utf-8");
365
+ } else {
366
+ try {
367
+ source = readFileSync2(file, "utf-8");
368
+ } catch {
369
+ continue;
370
+ }
371
+ }
372
+ allFindings.push(...collectJsxFindings(source, stdinMode ? "<stdin>" : file, parse));
373
+ }
374
+ return {
375
+ findings: allFindings,
376
+ summary: { scanned: files.length, hits: allFindings.length }
377
+ };
378
+ }
379
+
380
+ // src/utils/logger.ts
381
+ import pc from "picocolors";
382
+ var logger = {
383
+ info(message) {
384
+ console.log(message);
385
+ },
386
+ success(message) {
387
+ console.log(pc.green(`\u2713 ${message}`));
388
+ },
389
+ warn(message) {
390
+ console.log(pc.yellow(`\u26A0 ${message}`));
391
+ },
392
+ error(message) {
393
+ console.error(pc.red(`\u2717 ${message}`));
394
+ },
395
+ item(message) {
396
+ console.log(pc.dim(` ${message}`));
397
+ },
398
+ heading(message) {
399
+ console.log(pc.bold(message));
400
+ },
401
+ blank() {
402
+ console.log();
403
+ }
404
+ };
405
+
406
+ // src/commands/check.ts
407
+ var TYPE_FILTER = {
408
+ ui: "component",
409
+ blocks: "block",
410
+ hooks: "hook",
411
+ patterns: "pattern"
412
+ };
413
+ function checkListCommand(options) {
414
+ const manifest = loadManifest();
415
+ let items = getAllCatalogItems(manifest);
416
+ if (options.type && options.type !== "all") {
417
+ const filterType = TYPE_FILTER[options.type];
418
+ items = items.filter((i) => i.type === filterType);
419
+ }
420
+ const byType = {};
421
+ for (const item of items) {
422
+ byType[item.type] = (byType[item.type] ?? 0) + 1;
423
+ }
424
+ if (options.json) {
425
+ console.log(
426
+ JSON.stringify(
427
+ {
428
+ success: true,
429
+ items: items.map((i) => ({ type: i.type, name: i.name, category: i.category ?? null, description: i.description })),
430
+ summary: { total: items.length, byType }
431
+ },
432
+ null,
433
+ 2
434
+ )
435
+ );
436
+ process.exit(0);
437
+ return;
438
+ }
439
+ const groups = /* @__PURE__ */ new Map();
440
+ for (const item of items) {
441
+ const key = item.type;
442
+ if (!groups.has(key)) groups.set(key, []);
443
+ groups.get(key).push(item);
444
+ }
445
+ for (const [type, group] of groups) {
446
+ logger.heading(`${type}s (${group.length})`);
447
+ logger.blank();
448
+ for (const item of group) {
449
+ logger.info(` ${item.name.padEnd(28)} ${item.description}`);
450
+ }
451
+ logger.blank();
452
+ }
453
+ }
454
+ function checkHasCommand(pattern2, options) {
455
+ const manifest = loadManifest();
456
+ if (options.fuzzy) {
457
+ const results = fuzzyFind(manifest, pattern2, 5);
458
+ if (results.length === 0) {
459
+ if (options.json) {
460
+ console.log(JSON.stringify({ success: false, found: false, query: pattern2, results: [] }, null, 2));
461
+ } else {
462
+ logger.warn(`No fuzzy matches for "${pattern2}"`);
463
+ }
464
+ process.exit(1);
465
+ return;
466
+ }
467
+ if (options.json) {
468
+ console.log(JSON.stringify({ success: true, found: true, query: pattern2, results }, null, 2));
469
+ process.exit(0);
470
+ return;
471
+ }
472
+ logger.heading(`Fuzzy matches for "${pattern2}":`);
473
+ logger.blank();
474
+ for (const r of results) {
475
+ const cmd2 = r.installCmd ? ` \u2014 ${r.installCmd}` : "";
476
+ logger.info(` ${r.name} [${r.type}]${cmd2}`);
477
+ logger.info(` ${r.description.slice(0, 80)}`);
478
+ }
479
+ return;
480
+ }
481
+ const result = findByName(manifest, pattern2);
482
+ if (!result.found) {
483
+ if (options.json) {
484
+ console.log(JSON.stringify({ success: false, found: false, query: pattern2 }, null, 2));
485
+ } else {
486
+ logger.warn(`"${pattern2}" not found in Visor catalog. Try --fuzzy for partial matches.`);
487
+ }
488
+ process.exit(1);
489
+ return;
490
+ }
491
+ if (options.json) {
492
+ console.log(
493
+ JSON.stringify(
494
+ {
495
+ success: true,
496
+ found: true,
497
+ name: result.name,
498
+ type: result.type,
499
+ category: result.category ?? null,
500
+ description: result.description,
501
+ installCmd: result.installCmd
502
+ },
503
+ null,
504
+ 2
505
+ )
506
+ );
507
+ process.exit(0);
508
+ return;
509
+ }
510
+ const cmd = result.installCmd ? ` \u2014 ${result.installCmd}` : "";
511
+ logger.success(`${result.name} [${result.type}]${cmd}`);
512
+ logger.info(` ${result.description}`);
513
+ }
514
+ async function checkDiffCommand(pathArg, options) {
515
+ const result = await scanJsx(pathArg);
516
+ if (options.json) {
517
+ console.log(JSON.stringify({ success: true, ...result }, null, 2));
518
+ if (options.failOnHits && result.summary.hits > 0) process.exit(1);
519
+ process.exit(0);
520
+ return;
521
+ }
522
+ if (result.summary.hits === 0) {
523
+ logger.success(`No native HTML primitives found \u2014 ${result.summary.scanned} file(s) scanned.`);
524
+ return;
525
+ }
526
+ logger.heading(`Found ${result.summary.hits} native HTML usage(s) in ${result.summary.scanned} file(s):
527
+ `);
528
+ for (const f of result.findings) {
529
+ const loc = `${f.file}:${f.line}:${f.column}`;
530
+ const note = f.rationale ? ` (${f.rationale})` : "";
531
+ logger.warn(` <${f.nativeTag}> \u2192 use <${f.suggestedPrimitive}>${note}`);
532
+ logger.item(` ${loc} ${f.installCmd}`);
533
+ }
534
+ logger.blank();
535
+ if (options.failOnHits) process.exit(1);
536
+ }
537
+ function checkCommand() {
538
+ const check = new Command("check").description("Check Visor catalog \u2014 list items, test existence, scan JSX for native HTML");
539
+ 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) => {
540
+ checkListCommand(options);
541
+ });
542
+ check.command("has").description("Check whether a component, block, hook, or pattern exists in the Visor catalog").argument("<pattern>", "component name (kebab-case or PascalCase)").option("--fuzzy", "run fuzzy match and return top 5 results").option("--json", "output structured JSON (for AI agents)").action((pattern2, options) => {
543
+ checkHasCommand(pattern2, options);
544
+ });
545
+ check.command("diff").description("Scan JSX/TSX for native HTML elements that have Visor equivalents").argument("<path>", "file path, directory, or - for stdin").option("--fail-on-hits", "exit 1 if any native HTML usages are found (for CI use)").option("--json", "output structured JSON (for AI agents)").action(async (pathArg, options) => {
546
+ await checkDiffCommand(pathArg, options);
547
+ });
548
+ return check;
549
+ }
550
+
6
551
  // src/commands/init.ts
7
- import { existsSync as existsSync3, writeFileSync as writeFileSync2, mkdirSync } from "fs";
8
- import { join as join3, dirname } from "path";
552
+ import { existsSync as existsSync3, writeFileSync as writeFileSync2, mkdirSync, readFileSync as readFileSync5 } from "fs";
553
+ import { join as join5, dirname as dirname2 } from "path";
554
+ import { fileURLToPath as fileURLToPath2 } from "url";
555
+ import * as childProcess from "child_process";
9
556
 
10
557
  // src/config/config.ts
11
- import { readFileSync, writeFileSync, existsSync } from "fs";
12
- import { join } from "path";
558
+ import { readFileSync as readFileSync3, writeFileSync, existsSync } from "fs";
559
+ import { join as join3 } from "path";
13
560
 
14
561
  // src/config/defaults.ts
15
562
  var DEFAULT_CONFIG = {
16
563
  paths: {
17
564
  components: "components/ui",
18
565
  deckComponents: "components/deck",
566
+ flutterComponents: "lib/visor/components",
19
567
  blocks: "blocks",
20
568
  hooks: "hooks",
21
569
  lib: "lib"
@@ -25,7 +573,7 @@ var CONFIG_FILE = "visor.json";
25
573
 
26
574
  // src/config/config.ts
27
575
  function getConfigPath(cwd) {
28
- return join(cwd, CONFIG_FILE);
576
+ return join3(cwd, CONFIG_FILE);
29
577
  }
30
578
  function configExists(cwd) {
31
579
  return existsSync(getConfigPath(cwd));
@@ -37,8 +585,29 @@ function loadConfig(cwd) {
37
585
  `No ${CONFIG_FILE} found. Run "visor init" first.`
38
586
  );
39
587
  }
40
- const raw = readFileSync(configPath, "utf-8");
588
+ const raw = readFileSync3(configPath, "utf-8");
41
589
  const parsed = JSON.parse(raw);
590
+ const knownKeys = /* @__PURE__ */ new Set(["paths"]);
591
+ for (const key of Object.keys(parsed)) {
592
+ if (!knownKeys.has(key)) {
593
+ console.warn(`Warning: unknown key "${key}" in visor.json`);
594
+ }
595
+ }
596
+ if (parsed.paths !== void 0) {
597
+ if (typeof parsed.paths !== "object" || parsed.paths === null || Array.isArray(parsed.paths)) {
598
+ throw new Error(
599
+ `Invalid visor.json: paths must be an object, got ${Array.isArray(parsed.paths) ? "array" : typeof parsed.paths}`
600
+ );
601
+ }
602
+ const paths = parsed.paths;
603
+ for (const [key, value] of Object.entries(paths)) {
604
+ if (typeof value !== "string") {
605
+ throw new Error(
606
+ `Invalid visor.json: paths.${key} must be a string, got ${typeof value}`
607
+ );
608
+ }
609
+ }
610
+ }
42
611
  return {
43
612
  paths: {
44
613
  ...DEFAULT_CONFIG.paths,
@@ -53,12 +622,12 @@ function writeConfig(cwd, config) {
53
622
 
54
623
  // src/utils/packages.ts
55
624
  import { execFileSync } from "child_process";
56
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
57
- import { join as join2 } from "path";
625
+ import { existsSync as existsSync2, readFileSync as readFileSync4 } from "fs";
626
+ import { join as join4 } from "path";
58
627
  function readPackageJson(cwd) {
59
- const pkgPath = join2(cwd, "package.json");
628
+ const pkgPath = join4(cwd, "package.json");
60
629
  if (!existsSync2(pkgPath)) return null;
61
- return JSON.parse(readFileSync2(pkgPath, "utf-8"));
630
+ return JSON.parse(readFileSync4(pkgPath, "utf-8"));
62
631
  }
63
632
  function isPackageInstalled(packageName, cwd) {
64
633
  const pkg = readPackageJson(cwd);
@@ -82,38 +651,46 @@ function installPackages(packages, cwd, dev = false) {
82
651
  }
83
652
  }
84
653
 
85
- // src/utils/logger.ts
86
- import pc from "picocolors";
87
- var logger = {
88
- info(message) {
89
- console.log(message);
90
- },
91
- success(message) {
92
- console.log(pc.green(`\u2713 ${message}`));
93
- },
94
- warn(message) {
95
- console.log(pc.yellow(`\u26A0 ${message}`));
96
- },
97
- error(message) {
98
- console.error(pc.red(`\u2717 ${message}`));
99
- },
100
- item(message) {
101
- console.log(pc.dim(` ${message}`));
102
- },
103
- heading(message) {
104
- console.log(pc.bold(message));
105
- },
106
- blank() {
107
- console.log();
108
- }
109
- };
110
-
111
654
  // src/commands/templates/nextjs.ts
655
+ var NEXTJS_PINNED_VERSION = "15.1.6";
656
+ var CREATE_NEXT_APP_FLAGS = [
657
+ "--ts",
658
+ "--app",
659
+ "--no-tailwind",
660
+ "--no-eslint",
661
+ "--no-src-dir",
662
+ "--import-alias",
663
+ "@/*",
664
+ "--use-npm"
665
+ ];
112
666
  var NEXTJS_STARTER_YAML = `name: my-app
113
667
  version: 1
114
668
  colors:
115
669
  primary: "#2563EB"
116
670
  `;
671
+ function generateNextjsLayout() {
672
+ return `import "./globals.css";
673
+ import { FOWT_SCRIPT } from "@loworbitstudio/visor-theme-engine/fowt";
674
+ import type { Metadata } from "next";
675
+ import type { ReactNode } from "react";
676
+
677
+ export const metadata: Metadata = {
678
+ title: "My Visor App",
679
+ description: "Built with Visor \u2014 Low Orbit Studio's design system.",
680
+ };
681
+
682
+ export default function RootLayout({ children }: { children: ReactNode }) {
683
+ return (
684
+ <html lang="en">
685
+ <head>
686
+ <script>{FOWT_SCRIPT}</script>
687
+ </head>
688
+ <body>{children}</body>
689
+ </html>
690
+ );
691
+ }
692
+ `;
693
+ }
117
694
 
118
695
  // src/commands/init.ts
119
696
  import { generateThemeData } from "@loworbitstudio/visor-theme-engine";
@@ -124,18 +701,14 @@ function initCommand(cwd, options) {
124
701
  const filesSkipped = [];
125
702
  const warnings = [];
126
703
  if (options?.template && options.template !== "nextjs") {
127
- if (json) {
128
- console.log(
129
- JSON.stringify(
130
- { success: false, error: `Unknown template: ${options.template}. Available templates: nextjs` },
131
- null,
132
- 2
133
- )
134
- );
135
- } else {
136
- logger.error(`Unknown template: ${options.template}`);
137
- logger.info("Available templates: nextjs");
138
- }
704
+ emitError(json, `Unknown template: ${options.template}. Available templates: nextjs`);
705
+ process.exit(1);
706
+ }
707
+ if (options?.template === "nextjs" && existsSync3(join5(cwd, "package.json"))) {
708
+ emitError(
709
+ json,
710
+ "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"
711
+ );
139
712
  process.exit(1);
140
713
  }
141
714
  if (configExists(cwd)) {
@@ -158,52 +731,63 @@ function initCommand(cwd, options) {
158
731
  }
159
732
  }
160
733
  if (options?.template === "nextjs") {
161
- scaffoldNextjs(cwd, json, filesCreated, filesSkipped);
734
+ scaffoldNextjs(cwd, json, filesCreated, filesSkipped, warnings);
162
735
  }
163
- const missingTokens = !hasVisorTokens(cwd);
164
- if (missingTokens) {
165
- const warning = "@loworbitstudio/visor-core is not installed. Components require it for styling.";
166
- warnings.push(warning);
167
- if (!json) {
168
- logger.blank();
169
- logger.warn(warning);
170
- logger.info(" For Next.js: re-run with --template nextjs to generate tokens inline.");
736
+ if (options?.template !== "nextjs") {
737
+ const missingTokens = !hasVisorTokens(cwd);
738
+ if (missingTokens) {
739
+ const warning = "@loworbitstudio/visor-core is not installed. Components require it for styling.";
740
+ warnings.push(warning);
741
+ if (!json) {
742
+ logger.blank();
743
+ logger.warn(warning);
744
+ logger.info(" For a complete one-command setup: run `npx @loworbitstudio/visor init --template nextjs` in an empty directory.");
745
+ }
171
746
  }
172
747
  }
173
748
  if (json) {
174
- const nextSteps = [];
175
- if (options?.template === "nextjs") {
176
- nextSteps.push("Customize colors in .visor.yaml");
177
- nextSteps.push("Add FOWT prevention script to your layout.tsx <head>");
178
- nextSteps.push("Run: npx visor add button \u2014 to add your first component");
179
- } else {
180
- nextSteps.push("Run: npx visor add button \u2014 to add your first component");
181
- }
182
- if (missingTokens) {
183
- nextSteps.push("Re-run with --template nextjs to generate tokens inline (no npm package needed)");
184
- }
185
- console.log(
186
- JSON.stringify(
187
- {
188
- success: true,
189
- config: DEFAULT_CONFIG,
190
- files: { created: filesCreated, skipped: filesSkipped },
191
- warnings,
192
- nextSteps
193
- },
194
- null,
195
- 2
196
- )
197
- );
749
+ const nextSteps = buildNextSteps(options, warnings);
750
+ const result = {
751
+ success: true,
752
+ config: DEFAULT_CONFIG,
753
+ files: { created: filesCreated, skipped: filesSkipped },
754
+ warnings,
755
+ nextSteps
756
+ };
757
+ console.log(JSON.stringify(result, null, 2));
198
758
  process.exit(0);
199
759
  }
200
760
  }
201
- function scaffoldNextjs(cwd, json, filesCreated, filesSkipped) {
761
+ function buildNextSteps(options, warnings) {
762
+ const steps = [];
763
+ if (options?.template === "nextjs") {
764
+ steps.push("Run: npm run dev \u2014 start the development server");
765
+ steps.push("Customize colors in .visor.yaml, then re-run `npx visor theme apply .visor.yaml --adapter nextjs`");
766
+ steps.push("Run: npx visor add button \u2014 add your first component");
767
+ } else {
768
+ steps.push("Run: npx visor add button \u2014 add your first component");
769
+ }
770
+ if (warnings.some((w) => w.includes("visor-core"))) {
771
+ steps.push("For a complete one-command setup: re-run with --template nextjs in an empty directory");
772
+ }
773
+ return steps;
774
+ }
775
+ function emitError(json, message) {
776
+ if (json) {
777
+ const result = { success: false, error: message };
778
+ console.log(JSON.stringify(result, null, 2));
779
+ } else {
780
+ logger.error(message);
781
+ }
782
+ }
783
+ function scaffoldNextjs(cwd, json, filesCreated, filesSkipped, warnings) {
202
784
  if (!json) {
203
785
  logger.blank();
204
- logger.info("Scaffolding NextJS theme...");
786
+ logger.info("Scaffolding a Borealis-native Next.js app...");
205
787
  }
206
- const yamlPath = join3(cwd, ".visor.yaml");
788
+ runCreateNextApp(cwd, json);
789
+ runInstallVisorDeps(cwd, json);
790
+ const yamlPath = join5(cwd, ".visor.yaml");
207
791
  if (existsSync3(yamlPath)) {
208
792
  filesSkipped.push(".visor.yaml");
209
793
  if (!json) {
@@ -222,119 +806,143 @@ function scaffoldNextjs(cwd, json, filesCreated, filesSkipped) {
222
806
  tokens: data.tokens,
223
807
  config: data.config
224
808
  });
225
- const globalsPath = join3(cwd, "app", "globals.css");
226
- const globalsDir = dirname(globalsPath);
809
+ const globalsPath = join5(cwd, "app", "globals.css");
810
+ const globalsDir = dirname2(globalsPath);
811
+ mkdirSync(globalsDir, { recursive: true });
227
812
  if (existsSync3(globalsPath)) {
228
- filesSkipped.push("app/globals.css");
229
- if (!json) {
230
- logger.warn("app/globals.css already exists. Skipping.");
231
- }
813
+ writeFileSync2(globalsPath, css, "utf-8");
814
+ filesCreated.push("app/globals.css");
232
815
  } else {
233
- mkdirSync(globalsDir, { recursive: true });
234
816
  writeFileSync2(globalsPath, css, "utf-8");
235
817
  filesCreated.push("app/globals.css");
818
+ }
819
+ if (!json) {
820
+ logger.success("Created app/globals.css with theme tokens");
821
+ }
822
+ const layoutPath = join5(cwd, "app", "layout.tsx");
823
+ writeFileSync2(layoutPath, generateNextjsLayout(), "utf-8");
824
+ filesCreated.push("app/layout.tsx");
825
+ if (!json) {
826
+ logger.success("Wired app/layout.tsx with FOWT prevention and theme tokens");
827
+ }
828
+ const stampDir = join5(cwd, ".lo");
829
+ const stampPath = join5(stampDir, "borealis.json");
830
+ if (existsSync3(stampPath)) {
831
+ filesSkipped.push(".lo/borealis.json");
832
+ if (!json) {
833
+ logger.warn(".lo/borealis.json already exists. Skipping.");
834
+ }
835
+ } else {
836
+ mkdirSync(stampDir, { recursive: true });
837
+ const stamp = {
838
+ visorVersion: readVisorCliVersion(),
839
+ initializedAt: (/* @__PURE__ */ new Date()).toISOString()
840
+ };
841
+ writeFileSync2(stampPath, JSON.stringify(stamp, null, 2) + "\n", "utf-8");
842
+ filesCreated.push(".lo/borealis.json");
236
843
  if (!json) {
237
- logger.success("Created app/globals.css with theme tokens");
844
+ logger.success("Stamped .lo/borealis.json");
238
845
  }
239
846
  }
240
847
  if (!json) {
848
+ logger.blank();
849
+ logger.success("Your Borealis-native Next.js app is ready.");
241
850
  logger.blank();
242
851
  logger.info("Next steps:");
243
- logger.item("Customize colors in .visor.yaml");
244
- logger.item("Add FOWT prevention script to your layout.tsx <head>");
245
- logger.item("Run: npx visor add button \u2014 to add your first component");
852
+ logger.item("npm run dev # start the dev server");
853
+ logger.item("Edit .visor.yaml to customize tokens, then re-run theme apply");
854
+ logger.item("npx visor add button # add your first component");
246
855
  }
856
+ void warnings;
247
857
  }
248
-
249
- // src/registry/resolve.ts
250
- import { readFileSync as readFileSync3 } from "fs";
251
- import { join as join4, dirname as dirname2 } from "path";
252
- import { fileURLToPath } from "url";
253
- var __dirname = dirname2(fileURLToPath(import.meta.url));
254
- var cachedRegistry = null;
255
- function loadRegistry() {
256
- if (cachedRegistry) return cachedRegistry;
257
- const registryPath = join4(__dirname, "registry.json");
258
- const raw = readFileSync3(registryPath, "utf-8");
259
- cachedRegistry = JSON.parse(raw);
260
- return cachedRegistry;
858
+ function runCreateNextApp(cwd, json) {
859
+ if (!json) {
860
+ logger.info(`Running create-next-app@${NEXTJS_PINNED_VERSION}...`);
861
+ }
862
+ const result = childProcess.spawnSync(
863
+ "npx",
864
+ [`create-next-app@${NEXTJS_PINNED_VERSION}`, ".", ...CREATE_NEXT_APP_FLAGS],
865
+ { cwd, stdio: json ? "ignore" : "inherit" }
866
+ );
867
+ assertSpawnSuccess(result, "create-next-app");
261
868
  }
262
- function findItem(registry, name) {
263
- return registry.items.find((item) => item.name === name);
869
+ function runInstallVisorDeps(cwd, json) {
870
+ if (!json) {
871
+ logger.info("Installing @loworbitstudio/visor-core and visor-theme-engine...");
872
+ }
873
+ const result = childProcess.spawnSync(
874
+ "npm",
875
+ [
876
+ "install",
877
+ "@loworbitstudio/visor-core",
878
+ "@loworbitstudio/visor-theme-engine"
879
+ ],
880
+ { cwd, stdio: json ? "ignore" : "inherit" }
881
+ );
882
+ assertSpawnSuccess(result, "npm install");
264
883
  }
265
- function resolveTransitiveDeps(registry, names) {
266
- const resolved = /* @__PURE__ */ new Map();
267
- const queue = [...names];
268
- while (queue.length > 0) {
269
- const name = queue.shift();
270
- if (resolved.has(name)) continue;
271
- const item = findItem(registry, name);
272
- if (!item) {
273
- throw new Error(`Registry item "${name}" not found.`);
274
- }
275
- resolved.set(name, item);
276
- if (item.registryDependencies) {
277
- for (const dep of item.registryDependencies) {
278
- if (!resolved.has(dep)) {
279
- queue.push(dep);
280
- }
281
- }
282
- }
884
+ function assertSpawnSuccess(result, label) {
885
+ if (result.error) {
886
+ throw new Error(`${label} failed to start: ${result.error.message}`);
887
+ }
888
+ if (typeof result.status === "number" && result.status !== 0) {
889
+ throw new Error(`${label} exited with code ${result.status}`);
283
890
  }
284
- return Array.from(resolved.values());
285
891
  }
286
- function collectDependencies(items) {
287
- const deps = /* @__PURE__ */ new Set();
288
- const devDeps = /* @__PURE__ */ new Set();
289
- for (const item of items) {
290
- if (item.dependencies) {
291
- for (const dep of item.dependencies) {
292
- deps.add(dep);
293
- }
294
- }
295
- if (item.devDependencies) {
296
- for (const dep of item.devDependencies) {
297
- devDeps.add(dep);
892
+ function readVisorCliVersion() {
893
+ try {
894
+ const here = dirname2(fileURLToPath2(import.meta.url));
895
+ for (let i = 0; i < 5; i++) {
896
+ const segments = new Array(i).fill("..");
897
+ const candidate = join5(here, ...segments, "package.json");
898
+ try {
899
+ const pkg = JSON.parse(readFileSync5(candidate, "utf-8"));
900
+ if (pkg.name === "@loworbitstudio/visor" && pkg.version) {
901
+ return pkg.version;
902
+ }
903
+ } catch {
298
904
  }
299
905
  }
906
+ } catch {
300
907
  }
301
- return {
302
- dependencies: Array.from(deps).sort(),
303
- devDependencies: Array.from(devDeps).sort()
304
- };
908
+ return "0.0.0-dev";
305
909
  }
306
910
 
307
911
  // src/utils/fs.ts
308
912
  import {
309
913
  writeFileSync as writeFileSync3,
310
- readFileSync as readFileSync4,
914
+ readFileSync as readFileSync6,
311
915
  existsSync as existsSync4,
312
916
  mkdirSync as mkdirSync2
313
917
  } from "fs";
314
- import { dirname as dirname3, join as join5 } from "path";
918
+ import { dirname as dirname3, join as join6 } from "path";
315
919
  function resolveOutputPath(registryPath, type, config, cwd) {
316
920
  let relativePath;
317
921
  if (type === "registry:block") {
318
922
  relativePath = registryPath.replace(/^blocks\//, "");
319
- return join5(cwd, config.paths.blocks, relativePath);
923
+ return join6(cwd, config.paths.blocks, relativePath);
320
924
  }
321
925
  if (type === "registry:ui") {
322
926
  if (registryPath.startsWith("components/deck/")) {
323
927
  relativePath = registryPath.replace(/^components\/deck\//, "");
324
- return join5(cwd, config.paths.deckComponents, relativePath);
928
+ return join6(cwd, config.paths.deckComponents, relativePath);
929
+ }
930
+ if (registryPath.startsWith("components/flutter/")) {
931
+ relativePath = registryPath.replace(/^components\/flutter\//, "");
932
+ return join6(cwd, config.paths.flutterComponents, relativePath);
325
933
  }
326
934
  relativePath = registryPath.replace(/^components\/ui\//, "");
327
- return join5(cwd, config.paths.components, relativePath);
935
+ return join6(cwd, config.paths.components, relativePath);
328
936
  }
329
937
  if (type === "registry:hook") {
330
938
  relativePath = registryPath.replace(/^hooks\//, "");
331
- return join5(cwd, config.paths.hooks, relativePath);
939
+ return join6(cwd, config.paths.hooks, relativePath);
332
940
  }
333
941
  if (type === "registry:lib") {
334
942
  relativePath = registryPath.replace(/^lib\//, "");
335
- return join5(cwd, config.paths.lib, relativePath);
943
+ return join6(cwd, config.paths.lib, relativePath);
336
944
  }
337
- return join5(cwd, registryPath);
945
+ return join6(cwd, registryPath);
338
946
  }
339
947
  function writeFile(filePath, content) {
340
948
  const dir = dirname3(filePath);
@@ -345,7 +953,7 @@ function writeFile(filePath, content) {
345
953
  }
346
954
  function readFile(filePath) {
347
955
  if (!existsSync4(filePath)) return null;
348
- return readFileSync4(filePath, "utf-8");
956
+ return readFileSync6(filePath, "utf-8");
349
957
  }
350
958
  function fileExists(filePath) {
351
959
  return existsSync4(filePath);
@@ -488,9 +1096,141 @@ function listCommand(cwd, options = {}) {
488
1096
  }
489
1097
  }
490
1098
 
1099
+ // src/utils/pubspec.ts
1100
+ import { existsSync as existsSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
1101
+ import { join as join7 } from "path";
1102
+ import { parseDocument, YAMLMap } from "yaml";
1103
+ function mergePubspec(pubspecText, deps) {
1104
+ const doc = parseDocument(pubspecText);
1105
+ const added = [];
1106
+ const skipped = [];
1107
+ let depsNode = doc.get("dependencies");
1108
+ if (depsNode == null || !(depsNode instanceof YAMLMap)) {
1109
+ depsNode = new YAMLMap();
1110
+ doc.set("dependencies", depsNode);
1111
+ }
1112
+ for (const dep of deps) {
1113
+ if (depsNode.has(dep.pub)) {
1114
+ skipped.push(dep.pub);
1115
+ continue;
1116
+ }
1117
+ depsNode.set(dep.pub, dep.version);
1118
+ added.push(dep.pub);
1119
+ }
1120
+ return { text: doc.toString(), added, skipped };
1121
+ }
1122
+ function pubspecPath(cwd) {
1123
+ return join7(cwd, "pubspec.yaml");
1124
+ }
1125
+ function pubspecExists(cwd) {
1126
+ return existsSync5(pubspecPath(cwd));
1127
+ }
1128
+ function isPubPackageInstalled(packageName, cwd) {
1129
+ if (!pubspecExists(cwd)) return false;
1130
+ const text = readFileSync7(pubspecPath(cwd), "utf-8");
1131
+ const doc = parseDocument(text);
1132
+ const depsNode = doc.get("dependencies");
1133
+ if (!(depsNode instanceof YAMLMap)) return false;
1134
+ return depsNode.has(packageName);
1135
+ }
1136
+ function getUninstalledPubDeps(deps, cwd) {
1137
+ return deps.filter((d) => !isPubPackageInstalled(d.pub, cwd));
1138
+ }
1139
+ function addPubDependencies(deps, cwd) {
1140
+ const path2 = pubspecPath(cwd);
1141
+ if (!existsSync5(path2)) {
1142
+ throw new Error(
1143
+ `No pubspec.yaml found at ${path2}. Run this command from a Flutter project root.`
1144
+ );
1145
+ }
1146
+ const text = readFileSync7(path2, "utf-8");
1147
+ const result = mergePubspec(text, deps);
1148
+ if (result.added.length > 0) {
1149
+ writeFileSync4(path2, result.text, "utf-8");
1150
+ }
1151
+ return result;
1152
+ }
1153
+
1154
+ // src/utils/flutter.ts
1155
+ import { execFileSync as execFileSync2 } from "child_process";
1156
+ import { existsSync as existsSync6, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
1157
+ import { homedir } from "os";
1158
+ import { join as join8 } from "path";
1159
+ function isExecutable(path2) {
1160
+ try {
1161
+ const s = statSync2(path2);
1162
+ return s.isFile();
1163
+ } catch {
1164
+ return false;
1165
+ }
1166
+ }
1167
+ function fromPath(env) {
1168
+ const pathVar = env.PATH ?? "";
1169
+ const sep = process.platform === "win32" ? ";" : ":";
1170
+ const bin = process.platform === "win32" ? "flutter.bat" : "flutter";
1171
+ for (const dir of pathVar.split(sep)) {
1172
+ if (!dir) continue;
1173
+ const candidate = join8(dir, bin);
1174
+ if (isExecutable(candidate)) return candidate;
1175
+ }
1176
+ return null;
1177
+ }
1178
+ function fromFvm(home) {
1179
+ const fvmDefault = join8(home, "fvm", "default", "bin", "flutter");
1180
+ if (isExecutable(fvmDefault)) return fvmDefault;
1181
+ const versionsDir = join8(home, "fvm", "versions");
1182
+ if (!existsSync6(versionsDir)) return null;
1183
+ let best = null;
1184
+ try {
1185
+ for (const name of readdirSync2(versionsDir)) {
1186
+ const candidate = join8(versionsDir, name, "bin", "flutter");
1187
+ if (!isExecutable(candidate)) continue;
1188
+ if (!best || compareSemver(name, best.version) > 0) {
1189
+ best = { version: name, path: candidate };
1190
+ }
1191
+ }
1192
+ } catch {
1193
+ return null;
1194
+ }
1195
+ return best?.path ?? null;
1196
+ }
1197
+ function compareSemver(a, b) {
1198
+ const pa = a.split(".").map((x) => parseInt(x, 10) || 0);
1199
+ const pb = b.split(".").map((x) => parseInt(x, 10) || 0);
1200
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
1201
+ const av = pa[i] ?? 0;
1202
+ const bv = pb[i] ?? 0;
1203
+ if (av !== bv) return av - bv;
1204
+ }
1205
+ return 0;
1206
+ }
1207
+ function findFlutterBin(options = {}) {
1208
+ const env = options.env ?? process.env;
1209
+ const home = options.home ?? homedir();
1210
+ const envRoot = env.FLUTTER_ROOT;
1211
+ if (envRoot) {
1212
+ const bin = join8(envRoot, "bin", "flutter");
1213
+ if (isExecutable(bin)) return bin;
1214
+ }
1215
+ const fromPathBin = fromPath(env);
1216
+ if (fromPathBin) return fromPathBin;
1217
+ return fromFvm(home);
1218
+ }
1219
+ function runFlutterPubGet(cwd, bin) {
1220
+ try {
1221
+ execFileSync2(bin, ["pub", "get"], { cwd, stdio: "inherit" });
1222
+ return true;
1223
+ } catch {
1224
+ return false;
1225
+ }
1226
+ }
1227
+
491
1228
  // src/commands/add.ts
492
1229
  function addCommand(components, cwd, options = {}) {
493
1230
  const json = options.json ?? false;
1231
+ const dryRun = options.dryRun ?? false;
1232
+ const target = options.target ?? "react";
1233
+ const prefix = dryRun ? "[dry-run] " : "";
494
1234
  let autoInitialized = false;
495
1235
  if (!configExists(cwd)) {
496
1236
  writeConfig(cwd, DEFAULT_CONFIG);
@@ -514,9 +1254,12 @@ function addCommand(components, cwd, options = {}) {
514
1254
  }
515
1255
  process.exit(1);
516
1256
  }
1257
+ const targetRegistry = {
1258
+ items: filterItemsByTarget(registry.items, target)
1259
+ };
517
1260
  if (options.block && components.length > 0) {
518
1261
  for (const name of components) {
519
- const item = registry.items.find((i) => i.name === name);
1262
+ const item = targetRegistry.items.find((i) => i.name === name);
520
1263
  if (item && item.type !== "registry:block") {
521
1264
  if (json) {
522
1265
  console.log(
@@ -553,7 +1296,7 @@ function addCommand(components, cwd, options = {}) {
553
1296
  }
554
1297
  process.exit(1);
555
1298
  }
556
- const categoryItems = registry.items.filter(
1299
+ const categoryItems = targetRegistry.items.filter(
557
1300
  (item) => item.category === options.category
558
1301
  );
559
1302
  if (categoryItems.length === 0) {
@@ -579,7 +1322,7 @@ function addCommand(components, cwd, options = {}) {
579
1322
  }
580
1323
  if (itemNames.length === 0) {
581
1324
  if (options.block) {
582
- const blockItems = registry.items.filter(
1325
+ const blockItems = targetRegistry.items.filter(
583
1326
  (item) => item.type === "registry:block"
584
1327
  );
585
1328
  if (json) {
@@ -618,9 +1361,26 @@ function addCommand(components, cwd, options = {}) {
618
1361
  }
619
1362
  process.exit(1);
620
1363
  }
1364
+ const canonicalNames = [];
1365
+ for (const name of itemNames) {
1366
+ const resolved = findItemForTarget(targetRegistry, name, target);
1367
+ if (!resolved) {
1368
+ const message = `Registry item "${name}" not found for target "${target}".`;
1369
+ if (json) {
1370
+ console.log(JSON.stringify({ success: false, error: message }, null, 2));
1371
+ } else {
1372
+ logger.error(message);
1373
+ }
1374
+ process.exit(1);
1375
+ }
1376
+ canonicalNames.push(resolved.name);
1377
+ }
1378
+ const circularWarnings = [];
621
1379
  let items;
622
1380
  try {
623
- items = resolveTransitiveDeps(registry, itemNames);
1381
+ items = resolveTransitiveDeps(targetRegistry, canonicalNames, (msg) => {
1382
+ circularWarnings.push(msg);
1383
+ });
624
1384
  } catch (error) {
625
1385
  if (json) {
626
1386
  const message = error instanceof Error ? error.message : String(error);
@@ -629,6 +1389,11 @@ function addCommand(components, cwd, options = {}) {
629
1389
  }
630
1390
  throw error;
631
1391
  }
1392
+ if (circularWarnings.length > 0 && !json) {
1393
+ for (const warning of circularWarnings) {
1394
+ logger.warn(warning);
1395
+ }
1396
+ }
632
1397
  if (!json) {
633
1398
  logger.info(
634
1399
  `Resolving ${itemNames.length} item(s) \u2192 ${items.length} total (with dependencies)`
@@ -645,16 +1410,18 @@ function addCommand(components, cwd, options = {}) {
645
1410
  config,
646
1411
  cwd
647
1412
  );
648
- if (fileExists(outputPath) && !options.overwrite) {
1413
+ if (!dryRun && fileExists(outputPath) && !options.overwrite) {
649
1414
  if (!json) {
650
- logger.item(`skip ${file.path} (already exists)`);
1415
+ logger.item(`${prefix}skip ${file.path} (already exists)`);
651
1416
  }
652
1417
  skippedFiles.push(file.path);
653
1418
  continue;
654
1419
  }
655
- writeFile(outputPath, file.content);
1420
+ if (!dryRun) {
1421
+ writeFile(outputPath, file.content);
1422
+ }
656
1423
  if (!json) {
657
- logger.success(file.path);
1424
+ logger.success(`${prefix}${file.path}`);
658
1425
  }
659
1426
  writtenFiles.push(file.path);
660
1427
  }
@@ -662,60 +1429,135 @@ function addCommand(components, cwd, options = {}) {
662
1429
  if (!json) {
663
1430
  logger.blank();
664
1431
  logger.info(
665
- `Files: ${writtenFiles.length} written, ${skippedFiles.length} skipped`
1432
+ `${prefix}Files: ${writtenFiles.length} written, ${skippedFiles.length} skipped`
666
1433
  );
667
1434
  }
668
- const { dependencies, devDependencies } = collectDependencies(items);
669
- const uninstalledDeps = getUninstalledDeps(dependencies, cwd);
670
- const uninstalledDevDeps = getUninstalledDeps(devDependencies, cwd);
1435
+ const { dependencies, devDependencies, pubDependencies } = collectDependencies(items);
671
1436
  const installedDeps = [];
672
1437
  const failedDeps = [];
673
- if (uninstalledDeps.length > 0) {
674
- if (!json) {
675
- logger.blank();
676
- logger.info("Installing dependencies...");
677
- }
678
- if (installPackages(uninstalledDeps, cwd)) {
679
- installedDeps.push(...uninstalledDeps);
680
- } else {
681
- failedDeps.push(...uninstalledDeps);
682
- if (!json) {
683
- logger.warn("Some dependencies failed to install. Install them manually:");
684
- logger.info(` npm install ${uninstalledDeps.join(" ")}`);
1438
+ const warnings = [];
1439
+ if (target === "flutter") {
1440
+ const uninstalledPubDeps = dryRun ? pubDependencies : pubspecExists(cwd) ? getUninstalledPubDeps(pubDependencies, cwd) : pubDependencies;
1441
+ if (uninstalledPubDeps.length > 0) {
1442
+ if (dryRun) {
1443
+ if (!json) {
1444
+ logger.blank();
1445
+ logger.info(
1446
+ `${prefix}Would add pub dependencies: ${uninstalledPubDeps.map((d) => `${d.pub}@${d.version}`).join(", ")}`
1447
+ );
1448
+ }
1449
+ installedDeps.push(...uninstalledPubDeps.map((d) => d.pub));
1450
+ } else if (!pubspecExists(cwd)) {
1451
+ const message = "No pubspec.yaml found. Run this from a Flutter project root, or add " + uninstalledPubDeps.map((d) => `${d.pub}: ${d.version}`).join(", ") + " to pubspec.yaml manually.";
1452
+ warnings.push(message);
1453
+ if (!json) {
1454
+ logger.blank();
1455
+ logger.warn(message);
1456
+ }
1457
+ } else {
1458
+ if (!json) {
1459
+ logger.blank();
1460
+ logger.info("Updating pubspec.yaml...");
1461
+ }
1462
+ const result = addPubDependencies(uninstalledPubDeps, cwd);
1463
+ installedDeps.push(...result.added);
1464
+ const flutterBin = findFlutterBin();
1465
+ if (flutterBin) {
1466
+ if (!json) {
1467
+ logger.info("Running flutter pub get...");
1468
+ }
1469
+ if (!runFlutterPubGet(cwd, flutterBin)) {
1470
+ const warning = "flutter pub get failed. Run it manually to refresh dependencies.";
1471
+ warnings.push(warning);
1472
+ if (!json) logger.warn(warning);
1473
+ }
1474
+ } else {
1475
+ const warning = "flutter CLI not found. Run `flutter pub get` manually after setting up Flutter (or FVM).";
1476
+ warnings.push(warning);
1477
+ if (!json) logger.warn(warning);
1478
+ }
685
1479
  }
686
1480
  }
687
- }
688
- if (uninstalledDevDeps.length > 0) {
689
- if (!json) {
690
- logger.blank();
691
- logger.info("Installing dev dependencies...");
1481
+ } else {
1482
+ const uninstalledDeps = dryRun ? dependencies : getUninstalledDeps(dependencies, cwd);
1483
+ const uninstalledDevDeps = dryRun ? devDependencies : getUninstalledDeps(devDependencies, cwd);
1484
+ if (uninstalledDeps.length > 0) {
1485
+ if (dryRun) {
1486
+ if (!json) {
1487
+ logger.blank();
1488
+ logger.info(
1489
+ `${prefix}Would install dependencies: ${uninstalledDeps.join(", ")}`
1490
+ );
1491
+ }
1492
+ installedDeps.push(...uninstalledDeps);
1493
+ } else {
1494
+ if (!json) {
1495
+ logger.blank();
1496
+ logger.info("Installing dependencies...");
1497
+ }
1498
+ if (installPackages(uninstalledDeps, cwd)) {
1499
+ installedDeps.push(...uninstalledDeps);
1500
+ } else {
1501
+ failedDeps.push(...uninstalledDeps);
1502
+ if (!json) {
1503
+ logger.warn(
1504
+ "Some dependencies failed to install. Install them manually:"
1505
+ );
1506
+ logger.info(` npm install ${uninstalledDeps.join(" ")}`);
1507
+ }
1508
+ }
1509
+ }
692
1510
  }
693
- if (installPackages(uninstalledDevDeps, cwd, true)) {
694
- installedDeps.push(...uninstalledDevDeps);
695
- } else {
696
- failedDeps.push(...uninstalledDevDeps);
697
- if (!json) {
698
- logger.warn("Some dev dependencies failed to install. Install them manually:");
699
- logger.info(` npm install --save-dev ${uninstalledDevDeps.join(" ")}`);
1511
+ if (uninstalledDevDeps.length > 0) {
1512
+ if (dryRun) {
1513
+ if (!json) {
1514
+ logger.blank();
1515
+ logger.info(
1516
+ `${prefix}Would install dev dependencies: ${uninstalledDevDeps.join(", ")}`
1517
+ );
1518
+ }
1519
+ installedDeps.push(...uninstalledDevDeps);
1520
+ } else {
1521
+ if (!json) {
1522
+ logger.blank();
1523
+ logger.info("Installing dev dependencies...");
1524
+ }
1525
+ if (installPackages(uninstalledDevDeps, cwd, true)) {
1526
+ installedDeps.push(...uninstalledDevDeps);
1527
+ } else {
1528
+ failedDeps.push(...uninstalledDevDeps);
1529
+ if (!json) {
1530
+ logger.warn(
1531
+ "Some dev dependencies failed to install. Install them manually:"
1532
+ );
1533
+ logger.info(
1534
+ ` npm install --save-dev ${uninstalledDevDeps.join(" ")}`
1535
+ );
1536
+ }
1537
+ }
700
1538
  }
701
1539
  }
702
- }
703
- const warnings = [];
704
- if (!hasVisorTokens(cwd)) {
705
- const warning = "@loworbitstudio/visor-core is not installed. Components require it for styling.";
706
- warnings.push(warning);
707
- if (!json) {
708
- logger.blank();
709
- logger.warn(warning);
710
- logger.info(" For Next.js: npx @loworbitstudio/visor init --template nextjs");
711
- logger.info(" This generates all tokens inline \u2014 no npm package needed.");
1540
+ if (!hasVisorTokens(cwd)) {
1541
+ const warning = "@loworbitstudio/visor-core is not installed. Components require it for styling.";
1542
+ warnings.push(warning);
1543
+ if (!json) {
1544
+ logger.blank();
1545
+ logger.warn(warning);
1546
+ logger.info(
1547
+ " For Next.js: npx @loworbitstudio/visor init --template nextjs"
1548
+ );
1549
+ logger.info(
1550
+ " This generates all tokens inline \u2014 no npm package needed."
1551
+ );
1552
+ }
712
1553
  }
713
1554
  }
714
1555
  if (json) {
715
1556
  console.log(
716
1557
  JSON.stringify(
717
1558
  {
718
- success: true,
1559
+ success: failedDeps.length === 0,
1560
+ ...dryRun ? { dryRun: true } : {},
719
1561
  autoInitialized,
720
1562
  requested: itemNames,
721
1563
  resolved: items.map((i) => i.name),
@@ -727,7 +1569,10 @@ function addCommand(components, cwd, options = {}) {
727
1569
  2
728
1570
  )
729
1571
  );
730
- process.exit(0);
1572
+ process.exit(failedDeps.length > 0 ? 1 : 0);
1573
+ }
1574
+ if (failedDeps.length > 0) {
1575
+ process.exit(1);
731
1576
  }
732
1577
  }
733
1578
 
@@ -750,10 +1595,15 @@ function hasDifferences(localContent, registryContent) {
750
1595
  // src/commands/diff.ts
751
1596
  function diffCommand(componentName, cwd, options = {}) {
752
1597
  const json = options.json ?? false;
1598
+ const all = options.all ?? false;
753
1599
  let config;
754
1600
  let registry;
755
1601
  try {
756
- config = loadConfig(cwd);
1602
+ if (all && !configExists(cwd)) {
1603
+ config = DEFAULT_CONFIG;
1604
+ } else {
1605
+ config = loadConfig(cwd);
1606
+ }
757
1607
  registry = loadRegistry();
758
1608
  } catch (error) {
759
1609
  if (json) {
@@ -763,6 +1613,79 @@ function diffCommand(componentName, cwd, options = {}) {
763
1613
  }
764
1614
  throw error;
765
1615
  }
1616
+ if (all) {
1617
+ const results = [];
1618
+ for (const item of registry.items) {
1619
+ const changedFiles = [];
1620
+ let hasModified = false;
1621
+ let hasAdded = false;
1622
+ for (const file of item.files) {
1623
+ const outputPath = resolveOutputPath(
1624
+ file.path,
1625
+ file.type,
1626
+ config,
1627
+ cwd
1628
+ );
1629
+ const localContent = readFile(outputPath);
1630
+ if (localContent === null) {
1631
+ hasAdded = true;
1632
+ changedFiles.push(file.path);
1633
+ continue;
1634
+ }
1635
+ if (hasDifferences(localContent, file.content)) {
1636
+ hasModified = true;
1637
+ changedFiles.push(file.path);
1638
+ }
1639
+ }
1640
+ let changeType;
1641
+ if (hasModified) {
1642
+ changeType = "modified";
1643
+ } else if (hasAdded && changedFiles.length === item.files.length) {
1644
+ changeType = "added";
1645
+ } else if (hasAdded) {
1646
+ changeType = "modified";
1647
+ } else {
1648
+ changeType = "unchanged";
1649
+ }
1650
+ results.push({
1651
+ component: item.name,
1652
+ changeType,
1653
+ files: changedFiles,
1654
+ breakingChange: false,
1655
+ migrationNote: null
1656
+ });
1657
+ }
1658
+ const total = results.length;
1659
+ const changed = results.filter((r) => r.changeType !== "unchanged").length;
1660
+ const unchanged = total - changed;
1661
+ if (json) {
1662
+ console.log(
1663
+ JSON.stringify(
1664
+ {
1665
+ success: true,
1666
+ results,
1667
+ summary: { total, changed, unchanged }
1668
+ },
1669
+ null,
1670
+ 2
1671
+ )
1672
+ );
1673
+ process.exit(0);
1674
+ return;
1675
+ }
1676
+ const changedItems = results.filter((r) => r.changeType !== "unchanged");
1677
+ if (changedItems.length === 0) {
1678
+ logger.success(`All ${total} components match the registry.`);
1679
+ } else {
1680
+ logger.heading(`${changed} component(s) with upstream changes`);
1681
+ for (const r of changedItems) {
1682
+ logger.info(` ${r.component} [${r.changeType}]: ${r.files.join(", ")}`);
1683
+ }
1684
+ logger.blank();
1685
+ logger.info(`Total: ${total} | Changed: ${changed} | Unchanged: ${unchanged}`);
1686
+ }
1687
+ return;
1688
+ }
766
1689
  const itemsToDiff = componentName ? (() => {
767
1690
  const item = findItem(registry, componentName);
768
1691
  if (!item) {
@@ -850,15 +1773,94 @@ function diffCommand(componentName, cwd, options = {}) {
850
1773
  }
851
1774
  }
852
1775
 
1776
+ // src/commands/info.ts
1777
+ function infoCommand(name, cwd, options = {}) {
1778
+ const json = options.json ?? false;
1779
+ let manifest;
1780
+ try {
1781
+ manifest = loadManifest();
1782
+ } catch (error) {
1783
+ if (json) {
1784
+ const message = error instanceof Error ? error.message : String(error);
1785
+ console.error(JSON.stringify({ success: false, error: message }, null, 2));
1786
+ process.exit(1);
1787
+ }
1788
+ throw error;
1789
+ }
1790
+ let kind = null;
1791
+ let data = null;
1792
+ if (name in manifest.components) {
1793
+ kind = "component";
1794
+ data = manifest.components[name];
1795
+ } else if (name in manifest.hooks) {
1796
+ kind = "hook";
1797
+ data = manifest.hooks[name];
1798
+ } else if (name in manifest.blocks) {
1799
+ kind = "block";
1800
+ data = manifest.blocks[name];
1801
+ } else if (name in manifest.patterns) {
1802
+ kind = "pattern";
1803
+ data = manifest.patterns[name];
1804
+ }
1805
+ if (kind === null || data === null) {
1806
+ const errorPayload = {
1807
+ success: false,
1808
+ error: `Component ${name} not found. Run visor list --json to see available names.`
1809
+ };
1810
+ if (json) {
1811
+ console.error(JSON.stringify(errorPayload, null, 2));
1812
+ } else {
1813
+ logger.error(errorPayload.error);
1814
+ }
1815
+ process.exit(1);
1816
+ return;
1817
+ }
1818
+ if (json) {
1819
+ console.log(
1820
+ JSON.stringify(
1821
+ {
1822
+ success: true,
1823
+ name,
1824
+ kind,
1825
+ data
1826
+ },
1827
+ null,
1828
+ 2
1829
+ )
1830
+ );
1831
+ process.exit(0);
1832
+ return;
1833
+ }
1834
+ const entry = data;
1835
+ logger.heading(`${name} (${kind})`);
1836
+ logger.blank();
1837
+ if (entry.description) {
1838
+ logger.info(String(entry.description));
1839
+ logger.blank();
1840
+ }
1841
+ if (Array.isArray(entry.when_to_use) && entry.when_to_use.length > 0) {
1842
+ logger.info("When to use:");
1843
+ for (const item of entry.when_to_use) {
1844
+ logger.info(` \u2022 ${item}`);
1845
+ }
1846
+ logger.blank();
1847
+ }
1848
+ if (entry.example) {
1849
+ logger.info("Example:");
1850
+ logger.info(String(entry.example));
1851
+ }
1852
+ }
1853
+
853
1854
  // src/commands/theme-apply.ts
854
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, mkdirSync as mkdirSync3 } from "fs";
855
- import { resolve, dirname as dirname4 } from "path";
1855
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync5, mkdirSync as mkdirSync3 } from "fs";
1856
+ import { resolve as resolve2, dirname as dirname4, join as join9 } from "path";
856
1857
  import { generateTheme, generateThemeData as generateThemeData2 } from "@loworbitstudio/visor-theme-engine";
857
1858
  import {
858
1859
  nextjsAdapter as nextjsAdapter2,
859
1860
  fumadocsAdapter,
860
1861
  deckAdapter,
861
- docsAdapter
1862
+ docsAdapter,
1863
+ flutterAdapter
862
1864
  } from "@loworbitstudio/visor-theme-engine/adapters";
863
1865
  function defaultOutputPath(adapter, themeName) {
864
1866
  switch (adapter) {
@@ -867,22 +1869,24 @@ function defaultOutputPath(adapter, themeName) {
867
1869
  case "fumadocs":
868
1870
  return "visor-fumadocs-bridge.css";
869
1871
  case "deck": {
870
- const slug = (themeName ?? "theme").toLowerCase().replace(/\s+/g, "-");
871
- return `visor-deck-${slug}.css`;
1872
+ const slug2 = (themeName ?? "theme").toLowerCase().replace(/\s+/g, "-");
1873
+ return `visor-deck-${slug2}.css`;
872
1874
  }
873
1875
  case "docs": {
874
- const slug = (themeName ?? "theme").toLowerCase().replace(/\s+/g, "-");
875
- return `${slug}-theme.css`;
1876
+ const slug2 = (themeName ?? "theme").toLowerCase().replace(/\s+/g, "-");
1877
+ return `${slug2}-theme.css`;
876
1878
  }
1879
+ case "flutter":
1880
+ return "packages/ui";
877
1881
  default:
878
1882
  return "visor-theme.css";
879
1883
  }
880
1884
  }
881
1885
  function themeApplyCommand(file, cwd, options) {
882
- const filePath = resolve(cwd, file);
1886
+ const filePath = resolve2(cwd, file);
883
1887
  let yamlContent;
884
1888
  try {
885
- yamlContent = readFileSync5(filePath, "utf-8");
1889
+ yamlContent = readFileSync8(filePath, "utf-8");
886
1890
  } catch {
887
1891
  if (options.json) {
888
1892
  console.log(
@@ -897,7 +1901,8 @@ function themeApplyCommand(file, cwd, options) {
897
1901
  }
898
1902
  process.exit(2);
899
1903
  }
900
- let css;
1904
+ let css = null;
1905
+ let fileMap = null;
901
1906
  let themeName;
902
1907
  let sections;
903
1908
  try {
@@ -922,6 +1927,17 @@ function themeApplyCommand(file, cwd, options) {
922
1927
  case "docs":
923
1928
  css = docsAdapter(adapterInput);
924
1929
  break;
1930
+ case "flutter": {
1931
+ const flutterOptions = {
1932
+ packageName: options.packageName,
1933
+ tokensOnly: options.tokensOnly,
1934
+ lightOnly: options.lightOnly,
1935
+ darkOnly: options.darkOnly,
1936
+ themeClassName: options.themeClassName
1937
+ };
1938
+ fileMap = flutterAdapter(adapterInput, flutterOptions);
1939
+ break;
1940
+ }
925
1941
  default:
926
1942
  throw new Error(`Unknown adapter: ${options.adapter}`);
927
1943
  }
@@ -946,12 +1962,56 @@ function themeApplyCommand(file, cwd, options) {
946
1962
  }
947
1963
  process.exit(1);
948
1964
  }
949
- const outputFile = options.output ?? defaultOutputPath(options.adapter, themeName);
950
- const outputPath = resolve(cwd, outputFile);
1965
+ const outputTarget = options.output ?? defaultOutputPath(options.adapter, themeName);
1966
+ const outputPath = resolve2(cwd, outputTarget);
1967
+ if (fileMap) {
1968
+ try {
1969
+ mkdirSync3(outputPath, { recursive: true });
1970
+ let totalBytes = 0;
1971
+ for (const [relPath, content] of Object.entries(fileMap.files)) {
1972
+ const filePath2 = join9(outputPath, relPath);
1973
+ mkdirSync3(dirname4(filePath2), { recursive: true });
1974
+ writeFileSync5(filePath2, content, "utf-8");
1975
+ totalBytes += content.length;
1976
+ }
1977
+ if (options.json) {
1978
+ console.log(
1979
+ JSON.stringify({
1980
+ success: true,
1981
+ directory: outputPath,
1982
+ adapter: options.adapter,
1983
+ files: Object.keys(fileMap.files),
1984
+ size: totalBytes
1985
+ })
1986
+ );
1987
+ } else {
1988
+ logger.success(`Flutter theme package generated: ${outputPath}`);
1989
+ logger.info(`Adapter: ${options.adapter}`);
1990
+ logger.item(`Files: ${Object.keys(fileMap.files).length}`);
1991
+ logger.item(`Size: ${formatSize(totalBytes)}`);
1992
+ }
1993
+ } catch {
1994
+ if (options.json) {
1995
+ console.log(
1996
+ JSON.stringify({
1997
+ success: false,
1998
+ error: `Could not write package to: ${outputPath}`
1999
+ })
2000
+ );
2001
+ } else {
2002
+ logger.error(`Could not write package to: ${outputPath}`);
2003
+ }
2004
+ process.exit(2);
2005
+ }
2006
+ return;
2007
+ }
2008
+ if (css === null) {
2009
+ process.exit(1);
2010
+ }
951
2011
  const outputDir = dirname4(outputPath);
952
2012
  try {
953
2013
  mkdirSync3(outputDir, { recursive: true });
954
- writeFileSync4(outputPath, css, "utf-8");
2014
+ writeFileSync5(outputPath, css, "utf-8");
955
2015
  } catch {
956
2016
  if (options.json) {
957
2017
  console.log(
@@ -992,8 +2052,8 @@ function formatSize(bytes) {
992
2052
  }
993
2053
 
994
2054
  // src/commands/theme-export.ts
995
- import { readFileSync as readFileSync6 } from "fs";
996
- import { resolve as resolve2 } from "path";
2055
+ import { readFileSync as readFileSync9 } from "fs";
2056
+ import { resolve as resolve3 } from "path";
997
2057
  import {
998
2058
  parseConfig,
999
2059
  resolveConfig,
@@ -1001,10 +2061,10 @@ import {
1001
2061
  exportTheme
1002
2062
  } from "@loworbitstudio/visor-theme-engine";
1003
2063
  function themeExportCommand(file, cwd, options) {
1004
- const filePath = resolve2(cwd, file ?? ".visor.yaml");
2064
+ const filePath = resolve3(cwd, file ?? ".visor.yaml");
1005
2065
  let yamlContent;
1006
2066
  try {
1007
- yamlContent = readFileSync6(filePath, "utf-8");
2067
+ yamlContent = readFileSync9(filePath, "utf-8");
1008
2068
  } catch {
1009
2069
  if (options.json) {
1010
2070
  console.log(
@@ -1056,16 +2116,16 @@ function themeExportCommand(file, cwd, options) {
1056
2116
  }
1057
2117
 
1058
2118
  // src/commands/theme-validate.ts
1059
- import { readFileSync as readFileSync7 } from "fs";
1060
- import { resolve as resolve3 } from "path";
2119
+ import { readFileSync as readFileSync10 } from "fs";
2120
+ import { resolve as resolve4 } from "path";
1061
2121
  import { parse as parseYaml } from "yaml";
1062
2122
  import { validate } from "@loworbitstudio/visor-theme-engine";
1063
2123
  import pc2 from "picocolors";
1064
2124
  function themeValidateCommand(file, cwd, options) {
1065
- const filePath = resolve3(cwd, file);
2125
+ const filePath = resolve4(cwd, file);
1066
2126
  let fileContent;
1067
2127
  try {
1068
- fileContent = readFileSync7(filePath, "utf-8");
2128
+ fileContent = readFileSync10(filePath, "utf-8");
1069
2129
  } catch {
1070
2130
  if (options.json) {
1071
2131
  console.log(
@@ -1147,13 +2207,13 @@ function themeValidateCommand(file, cwd, options) {
1147
2207
  function printIssue(issue) {
1148
2208
  const prefix = issue.severity === "error" ? pc2.red(" ERROR") : pc2.yellow(" WARN ");
1149
2209
  const code = pc2.dim(`[${issue.code}]`);
1150
- const path = issue.path ? pc2.dim(` (${issue.path})`) : "";
1151
- console.log(`${prefix} ${code} ${issue.message}${path}`);
2210
+ const path2 = issue.path ? pc2.dim(` (${issue.path})`) : "";
2211
+ console.log(`${prefix} ${code} ${issue.message}${path2}`);
1152
2212
  }
1153
2213
 
1154
2214
  // src/commands/theme-extract.ts
1155
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync5, existsSync as existsSync5, readdirSync, statSync } from "fs";
1156
- import { resolve as resolve4, join as join6, basename, extname, relative } from "path";
2215
+ import { readFileSync as readFileSync11, writeFileSync as writeFileSync6, existsSync as existsSync7, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
2216
+ import { resolve as resolve5, join as join10, basename, extname as extname2, relative } from "path";
1157
2217
  import { stringify as stringifyYaml } from "yaml";
1158
2218
  import {
1159
2219
  extractFromCSS,
@@ -1182,8 +2242,8 @@ var CSS_DIRS = [
1182
2242
  "packages/design-tokens"
1183
2243
  ];
1184
2244
  function themeExtractCommand(cwd, options) {
1185
- const targetDir = resolve4(cwd, options.from ?? ".");
1186
- if (!existsSync5(targetDir)) {
2245
+ const targetDir = resolve5(cwd, options.from ?? ".");
2246
+ if (!existsSync7(targetDir)) {
1187
2247
  if (options.json) {
1188
2248
  console.log(JSON.stringify({ success: false, error: `Directory not found: ${targetDir}` }));
1189
2249
  } else {
@@ -1236,17 +2296,17 @@ function themeExtractCommand(cwd, options) {
1236
2296
  function collectCSSFiles(targetDir) {
1237
2297
  const files = [];
1238
2298
  const seen = /* @__PURE__ */ new Set();
1239
- for (const pattern of CSS_FILE_PATTERNS) {
1240
- const rootPath = join6(targetDir, pattern);
2299
+ for (const pattern2 of CSS_FILE_PATTERNS) {
2300
+ const rootPath = join10(targetDir, pattern2);
1241
2301
  addFileIfExists(rootPath, files, seen);
1242
2302
  for (const dir of CSS_DIRS) {
1243
- const dirPath = join6(targetDir, dir, pattern);
2303
+ const dirPath = join10(targetDir, dir, pattern2);
1244
2304
  addFileIfExists(dirPath, files, seen);
1245
2305
  }
1246
2306
  }
1247
2307
  for (const dir of CSS_DIRS) {
1248
- const dirPath = join6(targetDir, dir);
1249
- if (existsSync5(dirPath) && statSync(dirPath).isDirectory()) {
2308
+ const dirPath = join10(targetDir, dir);
2309
+ if (existsSync7(dirPath) && statSync3(dirPath).isDirectory()) {
1250
2310
  scanDirForCSS(dirPath, files, seen, 2);
1251
2311
  }
1252
2312
  }
@@ -1254,11 +2314,11 @@ function collectCSSFiles(targetDir) {
1254
2314
  return files;
1255
2315
  }
1256
2316
  function addFileIfExists(filePath, files, seen) {
1257
- const resolved = resolve4(filePath);
2317
+ const resolved = resolve5(filePath);
1258
2318
  if (seen.has(resolved)) return;
1259
- if (!existsSync5(resolved)) return;
2319
+ if (!existsSync7(resolved)) return;
1260
2320
  try {
1261
- const content = readFileSync8(resolved, "utf-8");
2321
+ const content = readFileSync11(resolved, "utf-8");
1262
2322
  if (content.includes("--")) {
1263
2323
  files.push({ path: resolved, content });
1264
2324
  seen.add(resolved);
@@ -1267,7 +2327,7 @@ function addFileIfExists(filePath, files, seen) {
1267
2327
  }
1268
2328
  }
1269
2329
  function scanDirForCSS(dir, files, seen, maxDepth) {
1270
- if (!existsSync5(dir)) return;
2330
+ if (!existsSync7(dir)) return;
1271
2331
  const SKIP_DIRS = /* @__PURE__ */ new Set([
1272
2332
  "node_modules",
1273
2333
  ".next",
@@ -1281,15 +2341,15 @@ function scanDirForCSS(dir, files, seen, maxDepth) {
1281
2341
  ".vercel"
1282
2342
  ]);
1283
2343
  try {
1284
- const entries = readdirSync(dir, { withFileTypes: true });
2344
+ const entries = readdirSync3(dir, { withFileTypes: true });
1285
2345
  for (const entry of entries) {
1286
2346
  if (entry.isDirectory()) {
1287
2347
  if (SKIP_DIRS.has(entry.name)) continue;
1288
2348
  if (maxDepth > 0) {
1289
- scanDirForCSS(join6(dir, entry.name), files, seen, maxDepth - 1);
2349
+ scanDirForCSS(join10(dir, entry.name), files, seen, maxDepth - 1);
1290
2350
  }
1291
- } else if (entry.isFile() && extname(entry.name) === ".css") {
1292
- addFileIfExists(join6(dir, entry.name), files, seen);
2351
+ } else if (entry.isFile() && extname2(entry.name) === ".css") {
2352
+ addFileIfExists(join10(dir, entry.name), files, seen);
1293
2353
  }
1294
2354
  }
1295
2355
  } catch {
@@ -1371,10 +2431,10 @@ function extractVarName(varExpr) {
1371
2431
  function parseNextFontFromLayouts(targetDir) {
1372
2432
  const fontMap = /* @__PURE__ */ new Map();
1373
2433
  for (const relPath of LAYOUT_FILE_PATHS) {
1374
- const fullPath = join6(targetDir, relPath);
1375
- if (!existsSync5(fullPath)) continue;
2434
+ const fullPath = join10(targetDir, relPath);
2435
+ if (!existsSync7(fullPath)) continue;
1376
2436
  try {
1377
- const content = readFileSync8(fullPath, "utf-8");
2437
+ const content = readFileSync11(fullPath, "utf-8");
1378
2438
  parseNextFontDeclarations(content, fontMap);
1379
2439
  } catch {
1380
2440
  }
@@ -1421,7 +2481,7 @@ function parseNextFontDeclarations(content, fontMap) {
1421
2481
  const srcMatch = block.match(/src\s*:\s*["']([^"']+)["']/);
1422
2482
  if (srcMatch) {
1423
2483
  const srcPath = srcMatch[1];
1424
- const fileName = basename(srcPath, extname(srcPath));
2484
+ const fileName = basename(srcPath, extname2(srcPath));
1425
2485
  const fontBaseName = fileName.replace(/[-_](Variable|Regular|Bold|Light|Medium|SemiBold|ExtraBold|Thin|Black|Italic).*$/i, "").replace(/[-_]/g, " ").trim();
1426
2486
  if (fontBaseName) {
1427
2487
  fontMap.set(varName, fontBaseName);
@@ -1459,10 +2519,10 @@ var MONO_FONT_NAMES = /* @__PURE__ */ new Set([
1459
2519
  "IBM Plex Mono"
1460
2520
  ]);
1461
2521
  function extractFontHints(targetDir) {
1462
- const pkgPath = join6(targetDir, "package.json");
1463
- if (!existsSync5(pkgPath)) return void 0;
2522
+ const pkgPath = join10(targetDir, "package.json");
2523
+ if (!existsSync7(pkgPath)) return void 0;
1464
2524
  try {
1465
- const pkg = JSON.parse(readFileSync8(pkgPath, "utf-8"));
2525
+ const pkg = JSON.parse(readFileSync11(pkgPath, "utf-8"));
1466
2526
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
1467
2527
  const fonts2 = [];
1468
2528
  for (const [dep, _] of Object.entries(allDeps)) {
@@ -1498,10 +2558,10 @@ function extractFontHints(targetDir) {
1498
2558
  }
1499
2559
  }
1500
2560
  function inferThemeName(targetDir) {
1501
- const pkgPath = join6(targetDir, "package.json");
1502
- if (existsSync5(pkgPath)) {
2561
+ const pkgPath = join10(targetDir, "package.json");
2562
+ if (existsSync7(pkgPath)) {
1503
2563
  try {
1504
- const pkg = JSON.parse(readFileSync8(pkgPath, "utf-8"));
2564
+ const pkg = JSON.parse(readFileSync11(pkgPath, "utf-8"));
1505
2565
  if (pkg.name) {
1506
2566
  const name = pkg.name.replace(/^@[\w-]+\//, "");
1507
2567
  return `${name}-theme`;
@@ -1538,7 +2598,7 @@ function outputJSON(result, validationResult) {
1538
2598
  }
1539
2599
  function outputYAML(result, outputPath, cwd, validationResult) {
1540
2600
  const yamlStr = buildAnnotatedYAML(result);
1541
- const outFile = resolve4(cwd, outputPath ?? ".visor.yaml");
2601
+ const outFile = resolve5(cwd, outputPath ?? ".visor.yaml");
1542
2602
  const high = result.tokens.filter((t) => t.confidence === "high").length;
1543
2603
  const med = result.tokens.filter((t) => t.confidence === "medium").length;
1544
2604
  const low = result.tokens.filter((t) => t.confidence === "low").length;
@@ -1566,7 +2626,7 @@ function outputYAML(result, outputPath, cwd, validationResult) {
1566
2626
  }
1567
2627
  logger.blank();
1568
2628
  }
1569
- writeFileSync5(outFile, yamlStr, "utf-8");
2629
+ writeFileSync6(outFile, yamlStr, "utf-8");
1570
2630
  logger.success(`Theme written to ${relative(cwd, outFile)}`);
1571
2631
  if (validationResult) {
1572
2632
  logger.blank();
@@ -1624,14 +2684,14 @@ function buildAnnotatedYAML(result) {
1624
2684
  }
1625
2685
 
1626
2686
  // src/commands/theme-register.ts
1627
- import { readFileSync as readFileSync9, writeFileSync as writeFileSync6, mkdirSync as mkdirSync4, existsSync as existsSync7 } from "fs";
1628
- import { resolve as resolve6, join as join8 } from "path";
2687
+ import { readFileSync as readFileSync12, writeFileSync as writeFileSync7, mkdirSync as mkdirSync4, existsSync as existsSync9 } from "fs";
2688
+ import { resolve as resolve7, join as join12 } from "path";
1629
2689
  import { generateThemeData as generateThemeData3 } from "@loworbitstudio/visor-theme-engine";
1630
2690
  import { docsAdapter as docsAdapter2 } from "@loworbitstudio/visor-theme-engine/adapters";
1631
2691
 
1632
2692
  // src/utils/theme-helpers.ts
1633
- import { existsSync as existsSync6 } from "fs";
1634
- import { resolve as resolve5, dirname as dirname5, join as join7 } from "path";
2693
+ import { existsSync as existsSync8 } from "fs";
2694
+ import { resolve as resolve6, dirname as dirname5, join as join11 } from "path";
1635
2695
  function toSlug(name) {
1636
2696
  return name.toLowerCase().replace(/\s+/g, "-");
1637
2697
  }
@@ -1639,9 +2699,9 @@ function toLabel(name) {
1639
2699
  return name.split(/[\s-]+/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
1640
2700
  }
1641
2701
  function findRepoRoot(startDir) {
1642
- let current = resolve5(startDir);
2702
+ let current = resolve6(startDir);
1643
2703
  while (true) {
1644
- if (existsSync6(join7(current, "packages", "docs"))) {
2704
+ if (existsSync8(join11(current, "packages", "docs"))) {
1645
2705
  return current;
1646
2706
  }
1647
2707
  const parent = dirname5(current);
@@ -1652,8 +2712,8 @@ function findRepoRoot(startDir) {
1652
2712
  }
1653
2713
 
1654
2714
  // src/commands/theme-register.ts
1655
- function insertGlobalsImport(content, slug) {
1656
- const importLine = `@import './${slug}-theme.css';`;
2715
+ function insertGlobalsImport(content, slug2) {
2716
+ const importLine = `@import './${slug2}-theme.css';`;
1657
2717
  if (content.includes(importLine)) {
1658
2718
  return { updated: content, changed: false };
1659
2719
  }
@@ -1684,8 +2744,8 @@ function insertGlobalsImport(content, slug) {
1684
2744
  lines.splice(insertAt, 0, importLine);
1685
2745
  return { updated: lines.join("\n"), changed: true };
1686
2746
  }
1687
- function insertThemeConfig(content, slug, label, group) {
1688
- if (content.includes(`value: "${slug}"`)) {
2747
+ function insertThemeConfig(content, slug2, label, group) {
2748
+ if (content.includes(`value: "${slug2}"`)) {
1689
2749
  return { updated: content, changed: false };
1690
2750
  }
1691
2751
  const escapedGroup = group.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -1718,7 +2778,7 @@ function insertThemeConfig(content, slug, label, group) {
1718
2778
  }
1719
2779
  let insertPos = closingBracket;
1720
2780
  for (const e of entries) {
1721
- if (slug < e.value) {
2781
+ if (slug2 < e.value) {
1722
2782
  insertPos = e.start;
1723
2783
  break;
1724
2784
  }
@@ -1727,7 +2787,7 @@ function insertThemeConfig(content, slug, label, group) {
1727
2787
  const lineContent = content.slice(prevNewline + 1, insertPos);
1728
2788
  const indentMatch = /^(\s*)/.exec(lineContent);
1729
2789
  const indent = indentMatch ? indentMatch[1] : " ";
1730
- const newEntry = `{ value: "${slug}", label: "${label}" }`;
2790
+ const newEntry = `{ value: "${slug2}", label: "${label}" }`;
1731
2791
  const insertion = entries.length === 0 ? `
1732
2792
  ${indent}${newEntry},
1733
2793
  ` : `${indent}${newEntry},
@@ -1736,10 +2796,10 @@ ${indent}${newEntry},
1736
2796
  return { updated, changed: true };
1737
2797
  }
1738
2798
  function themeRegisterCommand(file, cwd, options) {
1739
- const filePath = resolve6(cwd, file);
2799
+ const filePath = resolve7(cwd, file);
1740
2800
  let yamlContent;
1741
2801
  try {
1742
- yamlContent = readFileSync9(filePath, "utf-8");
2802
+ yamlContent = readFileSync12(filePath, "utf-8");
1743
2803
  } catch {
1744
2804
  if (options.json) {
1745
2805
  console.log(JSON.stringify({ success: false, error: `Could not read file: ${filePath}` }));
@@ -1763,7 +2823,7 @@ function themeRegisterCommand(file, cwd, options) {
1763
2823
  process.exit(1);
1764
2824
  return;
1765
2825
  }
1766
- const slug = toSlug(data.config.name);
2826
+ const slug2 = toSlug(data.config.name);
1767
2827
  const label = toLabel(data.config.name);
1768
2828
  const adapterInput = {
1769
2829
  primitives: data.primitives,
@@ -1782,11 +2842,11 @@ function themeRegisterCommand(file, cwd, options) {
1782
2842
  process.exit(1);
1783
2843
  return;
1784
2844
  }
1785
- const docsAppDir = join8(repoRoot, "packages", "docs", "app");
1786
- const cssFilePath = join8(docsAppDir, `${slug}-theme.css`);
1787
- const globalsPath = join8(docsAppDir, "globals.css");
1788
- const themeConfigPath = join8(repoRoot, "packages", "docs", "lib", "theme-config.ts");
1789
- if (!existsSync7(docsAppDir)) {
2845
+ const docsAppDir = join12(repoRoot, "packages", "docs", "app");
2846
+ const cssFilePath = join12(docsAppDir, `${slug2}-theme.css`);
2847
+ const globalsPath = join12(docsAppDir, "globals.css");
2848
+ const themeConfigPath = join12(repoRoot, "packages", "docs", "lib", "theme-config.ts");
2849
+ if (!existsSync9(docsAppDir)) {
1790
2850
  const msg = `Docs app directory not found: ${docsAppDir}`;
1791
2851
  if (options.json) {
1792
2852
  console.log(JSON.stringify({ success: false, error: msg }));
@@ -1799,8 +2859,8 @@ function themeRegisterCommand(file, cwd, options) {
1799
2859
  let globalsContent = "";
1800
2860
  let themeConfigContent = "";
1801
2861
  try {
1802
- globalsContent = readFileSync9(globalsPath, "utf-8");
1803
- themeConfigContent = readFileSync9(themeConfigPath, "utf-8");
2862
+ globalsContent = readFileSync12(globalsPath, "utf-8");
2863
+ themeConfigContent = readFileSync12(themeConfigPath, "utf-8");
1804
2864
  } catch (err) {
1805
2865
  const msg = err instanceof Error ? err.message : "Could not read docs files";
1806
2866
  if (options.json) {
@@ -1811,12 +2871,12 @@ function themeRegisterCommand(file, cwd, options) {
1811
2871
  process.exit(1);
1812
2872
  return;
1813
2873
  }
1814
- const cssExists = existsSync7(cssFilePath);
1815
- const cssChanged = !cssExists || readFileSync9(cssFilePath, "utf-8") !== css;
1816
- const { updated: newGlobals, changed: globalsChanged } = insertGlobalsImport(globalsContent, slug);
2874
+ const cssExists = existsSync9(cssFilePath);
2875
+ const cssChanged = !cssExists || readFileSync12(cssFilePath, "utf-8") !== css;
2876
+ const { updated: newGlobals, changed: globalsChanged } = insertGlobalsImport(globalsContent, slug2);
1817
2877
  const { updated: newThemeConfig, changed: themeConfigChanged, error: configError } = insertThemeConfig(
1818
2878
  themeConfigContent,
1819
- slug,
2879
+ slug2,
1820
2880
  label,
1821
2881
  options.group
1822
2882
  );
@@ -1834,7 +2894,7 @@ function themeRegisterCommand(file, cwd, options) {
1834
2894
  console.log(JSON.stringify({
1835
2895
  success: true,
1836
2896
  dryRun: true,
1837
- slug,
2897
+ slug: slug2,
1838
2898
  label,
1839
2899
  group: options.group,
1840
2900
  changes: {
@@ -1845,7 +2905,7 @@ function themeRegisterCommand(file, cwd, options) {
1845
2905
  }));
1846
2906
  } else {
1847
2907
  logger.info("Dry run \u2014 no files written");
1848
- logger.item(`Theme: ${label} (${slug})`);
2908
+ logger.item(`Theme: ${label} (${slug2})`);
1849
2909
  logger.item(`Group: ${options.group}`);
1850
2910
  logger.item(`CSS file: ${cssFilePath} \u2014 ${cssChanged ? cssExists ? "update" : "create" : "no change"}`);
1851
2911
  logger.item(`globals.css: ${globalsChanged ? "add import" : "already registered"}`);
@@ -1856,13 +2916,13 @@ function themeRegisterCommand(file, cwd, options) {
1856
2916
  try {
1857
2917
  if (cssChanged) {
1858
2918
  mkdirSync4(docsAppDir, { recursive: true });
1859
- writeFileSync6(cssFilePath, css, "utf-8");
2919
+ writeFileSync7(cssFilePath, css, "utf-8");
1860
2920
  }
1861
2921
  if (globalsChanged) {
1862
- writeFileSync6(globalsPath, newGlobals, "utf-8");
2922
+ writeFileSync7(globalsPath, newGlobals, "utf-8");
1863
2923
  }
1864
2924
  if (themeConfigChanged) {
1865
- writeFileSync6(themeConfigPath, newThemeConfig, "utf-8");
2925
+ writeFileSync7(themeConfigPath, newThemeConfig, "utf-8");
1866
2926
  }
1867
2927
  } catch (err) {
1868
2928
  const msg = err instanceof Error ? err.message : "Write failed";
@@ -1877,14 +2937,14 @@ function themeRegisterCommand(file, cwd, options) {
1877
2937
  if (options.json) {
1878
2938
  console.log(JSON.stringify({
1879
2939
  success: true,
1880
- slug,
2940
+ slug: slug2,
1881
2941
  label,
1882
2942
  group: options.group,
1883
2943
  files: { css: cssFilePath, globals: globalsPath, themeConfig: themeConfigPath },
1884
2944
  changes: { cssFile: cssChanged, globalsCSS: globalsChanged, themeConfig: themeConfigChanged }
1885
2945
  }));
1886
2946
  } else {
1887
- logger.success(`Theme registered: ${label} (${slug})`);
2947
+ logger.success(`Theme registered: ${label} (${slug2})`);
1888
2948
  logger.item(`Group: ${options.group}`);
1889
2949
  if (cssChanged) logger.item(`CSS: ${cssFilePath}`);
1890
2950
  if (globalsChanged) logger.item(`globals.css updated`);
@@ -1896,28 +2956,28 @@ function themeRegisterCommand(file, cwd, options) {
1896
2956
  }
1897
2957
 
1898
2958
  // src/commands/theme-unregister.ts
1899
- import { readFileSync as readFileSync10, writeFileSync as writeFileSync7, existsSync as existsSync8, unlinkSync } from "fs";
1900
- import { join as join9 } from "path";
1901
- function removeGlobalsImport(content, slug) {
1902
- const importLine = `@import './${slug}-theme.css';`;
2959
+ import { readFileSync as readFileSync13, writeFileSync as writeFileSync8, existsSync as existsSync10, unlinkSync } from "fs";
2960
+ import { join as join13 } from "path";
2961
+ function removeGlobalsImport(content, slug2) {
2962
+ const importLine = `@import './${slug2}-theme.css';`;
1903
2963
  if (!content.includes(importLine)) {
1904
2964
  return { updated: content, changed: false };
1905
2965
  }
1906
2966
  const updated = content.split("\n").filter((line) => line !== importLine).join("\n");
1907
2967
  return { updated, changed: true };
1908
2968
  }
1909
- function removeThemeConfigEntry(content, slug) {
1910
- if (!content.includes(`value: "${slug}"`)) {
2969
+ function removeThemeConfigEntry(content, slug2) {
2970
+ if (!content.includes(`value: "${slug2}"`)) {
1911
2971
  return { updated: content, changed: false };
1912
2972
  }
1913
2973
  const entryPattern = new RegExp(
1914
- `\\s*\\{\\s*value:\\s*"${slug.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}"[^}]*\\},?`,
2974
+ `\\s*\\{\\s*value:\\s*"${slug2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}"[^}]*\\},?`,
1915
2975
  "g"
1916
2976
  );
1917
2977
  const updated = content.replace(entryPattern, "");
1918
2978
  return { updated, changed: true };
1919
2979
  }
1920
- function themeUnregisterCommand(slug, cwd, options) {
2980
+ function themeUnregisterCommand(slug2, cwd, options) {
1921
2981
  const repoRoot = findRepoRoot(cwd);
1922
2982
  if (!repoRoot) {
1923
2983
  const msg = "Could not locate repo root (packages/docs/ not found). Run from within the visor repo.";
@@ -1929,11 +2989,11 @@ function themeUnregisterCommand(slug, cwd, options) {
1929
2989
  process.exit(1);
1930
2990
  return;
1931
2991
  }
1932
- const docsAppDir = join9(repoRoot, "packages", "docs", "app");
1933
- const cssFilePath = join9(docsAppDir, `${slug}-theme.css`);
1934
- const globalsPath = join9(docsAppDir, "globals.css");
1935
- const themeConfigPath = join9(repoRoot, "packages", "docs", "lib", "theme-config.ts");
1936
- if (!existsSync8(docsAppDir)) {
2992
+ const docsAppDir = join13(repoRoot, "packages", "docs", "app");
2993
+ const cssFilePath = join13(docsAppDir, `${slug2}-theme.css`);
2994
+ const globalsPath = join13(docsAppDir, "globals.css");
2995
+ const themeConfigPath = join13(repoRoot, "packages", "docs", "lib", "theme-config.ts");
2996
+ if (!existsSync10(docsAppDir)) {
1937
2997
  const msg = `Docs app directory not found: ${docsAppDir}`;
1938
2998
  if (options.json) {
1939
2999
  console.log(JSON.stringify({ success: false, error: msg }));
@@ -1946,8 +3006,8 @@ function themeUnregisterCommand(slug, cwd, options) {
1946
3006
  let globalsContent = "";
1947
3007
  let themeConfigContent = "";
1948
3008
  try {
1949
- globalsContent = readFileSync10(globalsPath, "utf-8");
1950
- themeConfigContent = readFileSync10(themeConfigPath, "utf-8");
3009
+ globalsContent = readFileSync13(globalsPath, "utf-8");
3010
+ themeConfigContent = readFileSync13(themeConfigPath, "utf-8");
1951
3011
  } catch (err) {
1952
3012
  const msg = err instanceof Error ? err.message : "Could not read docs files";
1953
3013
  if (options.json) {
@@ -1958,21 +3018,21 @@ function themeUnregisterCommand(slug, cwd, options) {
1958
3018
  process.exit(1);
1959
3019
  return;
1960
3020
  }
1961
- const cssExists = existsSync8(cssFilePath);
1962
- const { updated: newGlobals, changed: globalsChanged } = removeGlobalsImport(globalsContent, slug);
1963
- const { updated: newThemeConfig, changed: themeConfigChanged } = removeThemeConfigEntry(themeConfigContent, slug);
3021
+ const cssExists = existsSync10(cssFilePath);
3022
+ const { updated: newGlobals, changed: globalsChanged } = removeGlobalsImport(globalsContent, slug2);
3023
+ const { updated: newThemeConfig, changed: themeConfigChanged } = removeThemeConfigEntry(themeConfigContent, slug2);
1964
3024
  if (!cssExists && !globalsChanged && !themeConfigChanged) {
1965
3025
  if (options.json) {
1966
- console.log(JSON.stringify({ success: true, slug, changes: { cssFile: false, globalsCSS: false, themeConfig: false } }));
3026
+ console.log(JSON.stringify({ success: true, slug: slug2, changes: { cssFile: false, globalsCSS: false, themeConfig: false } }));
1967
3027
  } else {
1968
- logger.info(`Theme "${slug}" is not registered \u2014 nothing to remove.`);
3028
+ logger.info(`Theme "${slug2}" is not registered \u2014 nothing to remove.`);
1969
3029
  }
1970
3030
  return;
1971
3031
  }
1972
3032
  try {
1973
3033
  if (cssExists) unlinkSync(cssFilePath);
1974
- if (globalsChanged) writeFileSync7(globalsPath, newGlobals, "utf-8");
1975
- if (themeConfigChanged) writeFileSync7(themeConfigPath, newThemeConfig, "utf-8");
3034
+ if (globalsChanged) writeFileSync8(globalsPath, newGlobals, "utf-8");
3035
+ if (themeConfigChanged) writeFileSync8(themeConfigPath, newThemeConfig, "utf-8");
1976
3036
  } catch (err) {
1977
3037
  const msg = err instanceof Error ? err.message : "Write failed";
1978
3038
  if (options.json) {
@@ -1986,11 +3046,11 @@ function themeUnregisterCommand(slug, cwd, options) {
1986
3046
  if (options.json) {
1987
3047
  console.log(JSON.stringify({
1988
3048
  success: true,
1989
- slug,
3049
+ slug: slug2,
1990
3050
  changes: { cssFile: cssExists, globalsCSS: globalsChanged, themeConfig: themeConfigChanged }
1991
3051
  }));
1992
3052
  } else {
1993
- logger.success(`Theme unregistered: ${slug}`);
3053
+ logger.success(`Theme unregistered: ${slug2}`);
1994
3054
  if (cssExists) logger.item(`CSS file removed: ${cssFilePath}`);
1995
3055
  if (globalsChanged) logger.item(`globals.css updated`);
1996
3056
  if (themeConfigChanged) logger.item(`theme-config.ts updated`);
@@ -1999,32 +3059,47 @@ function themeUnregisterCommand(slug, cwd, options) {
1999
3059
 
2000
3060
  // src/commands/theme-sync.ts
2001
3061
  import {
2002
- readFileSync as readFileSync11,
2003
- writeFileSync as writeFileSync8,
3062
+ readFileSync as readFileSync14,
3063
+ writeFileSync as writeFileSync9,
2004
3064
  mkdirSync as mkdirSync5,
2005
- existsSync as existsSync9,
2006
- readdirSync as readdirSync2,
3065
+ existsSync as existsSync11,
3066
+ readdirSync as readdirSync4,
2007
3067
  unlinkSync as unlinkSync2,
2008
3068
  copyFileSync
2009
3069
  } from "fs";
2010
- import { join as join10, basename as basename2 } from "path";
3070
+ import { join as join14, basename as basename2 } from "path";
2011
3071
  import { parse as parseYaml2 } from "yaml";
2012
3072
  import { generateThemeData as generateThemeData4 } from "@loworbitstudio/visor-theme-engine";
2013
3073
  import { docsAdapter as docsAdapter3 } from "@loworbitstudio/visor-theme-engine/adapters";
2014
- var GLOBALS_BEGIN_MARKER = "/* BEGIN visor-theme-imports \u2014 generated by `visor theme sync` */";
3074
+ var GLOBALS_BEGIN_MARKER = "/* BEGIN visor-theme-imports \u2014 managed by `visor theme sync` */";
2015
3075
  var GLOBALS_END_MARKER = "/* END visor-theme-imports */";
3076
+ var STOCK_GROUPS_BEGIN_MARKER = "/* BEGIN visor-stock-themes \u2014 managed by `visor theme sync` */";
3077
+ var STOCK_GROUPS_END_MARKER = "/* END visor-stock-themes */";
2016
3078
  var GITIGNORE_BEGIN_MARKER = "# BEGIN visor-custom-theme-css (managed by `visor theme sync` \u2014 do not edit manually)";
2017
3079
  var GITIGNORE_END_MARKER = "# END visor-custom-theme-css";
2018
- var THEME_CONFIG_HEADER = "// This file is auto-generated by `visor theme sync`. Do not edit manually.\n";
3080
+ var CUSTOM_OVERLAY_CSS_PATH = "packages/docs/app/custom-themes.generated.css";
3081
+ var CUSTOM_OVERLAY_TS_PATH = "packages/docs/lib/theme-config.custom.generated.ts";
3082
+ var CUSTOM_OVERLAY_IMPORT_LINE = "@import './custom-themes.generated.css';";
2019
3083
  function scanThemeDir(dir) {
2020
- if (!existsSync9(dir)) return [];
2021
- return readdirSync2(dir).filter((f) => f.endsWith(".visor.yaml")).map((f) => join10(dir, f));
3084
+ if (!existsSync11(dir)) return [];
3085
+ return readdirSync4(dir).filter((f) => f.endsWith(".visor.yaml")).map((f) => join14(dir, f));
2022
3086
  }
2023
3087
  function extractGroup(yamlContent) {
2024
3088
  const parsed = parseYaml2(yamlContent);
2025
3089
  if (typeof parsed?.group === "string") return parsed.group;
2026
3090
  return void 0;
2027
3091
  }
3092
+ function extractLabel(yamlContent) {
3093
+ const parsed = parseYaml2(yamlContent);
3094
+ if (typeof parsed?.label === "string") return parsed.label;
3095
+ return void 0;
3096
+ }
3097
+ function extractDefaultMode(yamlContent) {
3098
+ const parsed = parseYaml2(yamlContent);
3099
+ const v = parsed?.["default-mode"];
3100
+ if (v === "dark" || v === "light") return v;
3101
+ return void 0;
3102
+ }
2028
3103
  function sortGroups(groups) {
2029
3104
  return [...groups].sort((a, b) => {
2030
3105
  if (a === "Visor") return -1;
@@ -2032,9 +3107,60 @@ function sortGroups(groups) {
2032
3107
  return a.localeCompare(b);
2033
3108
  });
2034
3109
  }
2035
- function generateThemeConfig(entries) {
3110
+ function updateGlobalsImports(content, stockSlugs) {
3111
+ const importLines = [...stockSlugs].sort().map((slug2) => `@import './${slug2}-theme.css';`).join("\n");
3112
+ const newBlock = `${GLOBALS_BEGIN_MARKER}
3113
+ ${importLines}
3114
+ ${GLOBALS_END_MARKER}`;
3115
+ let updated;
3116
+ const beginIdx = content.indexOf(GLOBALS_BEGIN_MARKER);
3117
+ const endIdx = content.indexOf(GLOBALS_END_MARKER);
3118
+ if (beginIdx !== -1 && endIdx !== -1) {
3119
+ updated = content.slice(0, beginIdx) + newBlock + content.slice(endIdx + GLOBALS_END_MARKER.length);
3120
+ } else {
3121
+ const themeImportPattern = /^@import '\.\/[\w-]+-theme\.css';\n?/gm;
3122
+ const lines = content.split("\n");
3123
+ let firstThemeIdx = -1;
3124
+ let lastThemeIdx = -1;
3125
+ for (let i = 0; i < lines.length; i++) {
3126
+ if (/^@import '\.\/[\w-]+-theme\.css';/.test(lines[i])) {
3127
+ if (firstThemeIdx === -1) firstThemeIdx = i;
3128
+ lastThemeIdx = i;
3129
+ }
3130
+ }
3131
+ if (firstThemeIdx !== -1) {
3132
+ const before = lines.slice(0, firstThemeIdx);
3133
+ const after = lines.slice(lastThemeIdx + 1);
3134
+ updated = [...before, newBlock, ...after].join("\n");
3135
+ } else {
3136
+ void themeImportPattern;
3137
+ const lastImportIdx = lines.reduce(
3138
+ (last, line, i) => line.startsWith("@import") ? i : last,
3139
+ -1
3140
+ );
3141
+ const insertAt = lastImportIdx + 1;
3142
+ lines.splice(insertAt, 0, newBlock);
3143
+ updated = lines.join("\n");
3144
+ }
3145
+ }
3146
+ updated = ensureCustomOverlayImport(updated);
3147
+ return updated;
3148
+ }
3149
+ function ensureCustomOverlayImport(content) {
3150
+ const endMarkerIdx = content.indexOf(GLOBALS_END_MARKER);
3151
+ if (endMarkerIdx === -1) return content;
3152
+ const afterMarker = content.slice(endMarkerIdx + GLOBALS_END_MARKER.length);
3153
+ if (afterMarker.trimStart().startsWith(CUSTOM_OVERLAY_IMPORT_LINE)) return content;
3154
+ const withoutStale = content.replace(
3155
+ new RegExp(`\\n?${CUSTOM_OVERLAY_IMPORT_LINE.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`, "g"),
3156
+ ""
3157
+ );
3158
+ const markerEnd = withoutStale.indexOf(GLOBALS_END_MARKER) + GLOBALS_END_MARKER.length;
3159
+ return withoutStale.slice(0, markerEnd) + "\n" + CUSTOM_OVERLAY_IMPORT_LINE + withoutStale.slice(markerEnd);
3160
+ }
3161
+ function updateStockThemeConfigBlock(content, stockEntries) {
2036
3162
  const groupMap = /* @__PURE__ */ new Map();
2037
- for (const entry of entries) {
3163
+ for (const entry of stockEntries) {
2038
3164
  if (!groupMap.has(entry.group)) groupMap.set(entry.group, []);
2039
3165
  groupMap.get(entry.group).push(entry);
2040
3166
  }
@@ -2044,7 +3170,10 @@ function generateThemeConfig(entries) {
2044
3170
  }
2045
3171
  const groupsTs = sortedGroupNames.map((groupName) => {
2046
3172
  const groupEntries = groupMap.get(groupName);
2047
- const themesTs = groupEntries.map((e) => ` { value: "${e.slug}", label: "${e.label}", yamlFile: "${e.yamlFilename}" },`).join("\n");
3173
+ const themesTs = groupEntries.map((e) => {
3174
+ const modePart = e.defaultMode ? `, defaultMode: "${e.defaultMode}"` : "";
3175
+ return ` { value: "${e.slug}", label: "${e.label}", yamlFile: "${e.yamlFilename}"${modePart} },`;
3176
+ }).join("\n");
2048
3177
  return ` {
2049
3178
  label: "${groupName}",
2050
3179
  themes: [
@@ -2052,62 +3181,68 @@ ${themesTs}
2052
3181
  ],
2053
3182
  },`;
2054
3183
  }).join("\n");
2055
- return `${THEME_CONFIG_HEADER}
2056
- export interface ThemeEntry {
2057
- value: string;
2058
- label: string;
2059
- /** Filename (without .visor.yaml extension) if a YAML config exists in /public/themes/ */
2060
- yamlFile?: string;
2061
- }
2062
-
2063
- export interface ThemeGroup {
2064
- label: string;
2065
- themes: ThemeEntry[];
2066
- }
2067
-
2068
- export const THEME_GROUPS: ThemeGroup[] = [
3184
+ const newBlock = `${STOCK_GROUPS_BEGIN_MARKER}
3185
+ const STOCK_GROUPS: ThemeGroup[] = [
2069
3186
  ${groupsTs}
2070
3187
  ];
2071
-
2072
- export const ALL_THEMES = THEME_GROUPS.flatMap((g) => g.themes.map((t) => t.value));
2073
- `;
3188
+ ${STOCK_GROUPS_END_MARKER}`;
3189
+ const beginIdx = content.indexOf(STOCK_GROUPS_BEGIN_MARKER);
3190
+ const endIdx = content.indexOf(STOCK_GROUPS_END_MARKER);
3191
+ if (beginIdx !== -1 && endIdx !== -1) {
3192
+ return content.slice(0, beginIdx) + newBlock + content.slice(endIdx + STOCK_GROUPS_END_MARKER.length);
3193
+ }
3194
+ const themeGroupsExportIdx = content.indexOf("export const THEME_GROUPS");
3195
+ if (themeGroupsExportIdx !== -1) {
3196
+ return content.slice(0, themeGroupsExportIdx) + newBlock + "\n\n" + content.slice(themeGroupsExportIdx);
3197
+ }
3198
+ return content + "\n\n" + newBlock;
2074
3199
  }
2075
- function updateGlobalsImports(content, slugs) {
2076
- const importLines = [...slugs].sort().map((slug) => `@import './${slug}-theme.css';`).join("\n");
2077
- const newBlock = `${GLOBALS_BEGIN_MARKER}
3200
+ function generateCustomOverlayCss(customEntries) {
3201
+ if (customEntries.length === 0) {
3202
+ return "/* generated by `visor theme sync` \u2014 empty when no custom themes are present */\n";
3203
+ }
3204
+ const importLines = [...customEntries].sort((a, b) => a.slug.localeCompare(b.slug)).map((e) => `@import './${e.slug}-theme.css';`).join("\n");
3205
+ return `/* generated by \`visor theme sync\` \u2014 do not edit manually */
2078
3206
  ${importLines}
2079
- ${GLOBALS_END_MARKER}`;
2080
- const beginIdx = content.indexOf(GLOBALS_BEGIN_MARKER);
2081
- const endIdx = content.indexOf(GLOBALS_END_MARKER);
2082
- if (beginIdx !== -1 && endIdx !== -1) {
2083
- return content.slice(0, beginIdx) + newBlock + content.slice(endIdx + GLOBALS_END_MARKER.length);
3207
+ `;
3208
+ }
3209
+ function generateCustomOverlayTs(customEntries) {
3210
+ if (customEntries.length === 0) {
3211
+ return `import type { ThemeGroup } from "./theme-config";
3212
+ export const customThemeGroups: ThemeGroup[] = [];
3213
+ `;
2084
3214
  }
2085
- const themeImportPattern = /^@import '\.\/[\w-]+-theme\.css';\n?/gm;
2086
- const lines = content.split("\n");
2087
- let firstThemeIdx = -1;
2088
- let lastThemeIdx = -1;
2089
- for (let i = 0; i < lines.length; i++) {
2090
- if (/^@import '\.\/[\w-]+-theme\.css';/.test(lines[i])) {
2091
- if (firstThemeIdx === -1) firstThemeIdx = i;
2092
- lastThemeIdx = i;
2093
- }
3215
+ const groupMap = /* @__PURE__ */ new Map();
3216
+ for (const entry of customEntries) {
3217
+ if (!groupMap.has(entry.group)) groupMap.set(entry.group, []);
3218
+ groupMap.get(entry.group).push(entry);
2094
3219
  }
2095
- if (firstThemeIdx !== -1) {
2096
- const before = lines.slice(0, firstThemeIdx);
2097
- const after = lines.slice(lastThemeIdx + 1);
2098
- return [...before, newBlock, ...after].join("\n");
3220
+ const sortedGroupNames = sortGroups([...groupMap.keys()]);
3221
+ for (const [, groupEntries] of groupMap) {
3222
+ groupEntries.sort((a, b) => a.slug.localeCompare(b.slug));
2099
3223
  }
2100
- void themeImportPattern;
2101
- const lastImportIdx = lines.reduce(
2102
- (last, line, i) => line.startsWith("@import") ? i : last,
2103
- -1
2104
- );
2105
- const insertAt = lastImportIdx + 1;
2106
- lines.splice(insertAt, 0, newBlock);
2107
- return lines.join("\n");
3224
+ const groupsTs = sortedGroupNames.map((groupName) => {
3225
+ const groupEntries = groupMap.get(groupName);
3226
+ const themesTs = groupEntries.map((e) => {
3227
+ const modePart = e.defaultMode ? `, defaultMode: "${e.defaultMode}"` : "";
3228
+ return ` { value: "${e.slug}", label: "${e.label}", yamlFile: "${e.yamlFilename}"${modePart} },`;
3229
+ }).join("\n");
3230
+ return ` {
3231
+ label: "${groupName}",
3232
+ themes: [
3233
+ ${themesTs}
3234
+ ],
3235
+ },`;
3236
+ }).join("\n");
3237
+ return `import type { ThemeGroup } from "./theme-config";
3238
+ // generated by \`visor theme sync\` \u2014 do not edit manually
3239
+ export const customThemeGroups: ThemeGroup[] = [
3240
+ ${groupsTs}
3241
+ ];
3242
+ `;
2108
3243
  }
2109
3244
  function updateGitignoreBlock(content, customSlugs) {
2110
- const cssLines = customSlugs.sort().map((slug) => `packages/docs/app/${slug}-theme.css`).join("\n");
3245
+ const cssLines = customSlugs.sort().map((slug2) => `packages/docs/app/${slug2}-theme.css`).join("\n");
2111
3246
  const newBlock = `${GITIGNORE_BEGIN_MARKER}
2112
3247
  ${cssLines}
2113
3248
  ${GITIGNORE_END_MARKER}`;
@@ -2130,13 +3265,16 @@ function themeSyncCommand(cwd, options) {
2130
3265
  process.exit(1);
2131
3266
  return;
2132
3267
  }
2133
- const themesDir = join10(repoRoot, "themes");
2134
- const customThemesDir = join10(repoRoot, "custom-themes");
2135
- const docsAppDir = join10(repoRoot, "packages", "docs", "app");
2136
- const docsPublicThemesDir = join10(repoRoot, "packages", "docs", "public", "themes");
2137
- const themeConfigPath = join10(repoRoot, "packages", "docs", "lib", "theme-config.ts");
2138
- const globalsPath = join10(docsAppDir, "globals.css");
2139
- const gitignorePath = join10(repoRoot, ".gitignore");
3268
+ const themesDir = join14(repoRoot, "themes");
3269
+ const customThemesDir = join14(repoRoot, "custom-themes");
3270
+ const docsAppDir = join14(repoRoot, "packages", "docs", "app");
3271
+ const docsLibDir = join14(repoRoot, "packages", "docs", "lib");
3272
+ const docsPublicThemesDir = join14(repoRoot, "packages", "docs", "public", "themes");
3273
+ const themeConfigPath = join14(repoRoot, "packages", "docs", "lib", "theme-config.ts");
3274
+ const globalsPath = join14(docsAppDir, "globals.css");
3275
+ const gitignorePath = join14(repoRoot, ".gitignore");
3276
+ const customOverlayCssPath = join14(repoRoot, CUSTOM_OVERLAY_CSS_PATH);
3277
+ const customOverlayTsPath = join14(repoRoot, CUSTOM_OVERLAY_TS_PATH);
2140
3278
  const stockFiles = scanThemeDir(themesDir);
2141
3279
  const customFiles = scanThemeDir(customThemesDir);
2142
3280
  if (stockFiles.length === 0 && customFiles.length === 0) {
@@ -2153,7 +3291,7 @@ function themeSyncCommand(cwd, options) {
2153
3291
  const processFile = (filePath, isCustom) => {
2154
3292
  let yamlContent;
2155
3293
  try {
2156
- yamlContent = readFileSync11(filePath, "utf-8");
3294
+ yamlContent = readFileSync14(filePath, "utf-8");
2157
3295
  } catch {
2158
3296
  errors.push(`Could not read: ${filePath}`);
2159
3297
  return;
@@ -2165,12 +3303,13 @@ function themeSyncCommand(cwd, options) {
2165
3303
  errors.push(`Failed to parse ${basename2(filePath)}: ${err instanceof Error ? err.message : "Unknown error"}`);
2166
3304
  return;
2167
3305
  }
2168
- const slug = toSlug(data.config.name);
2169
- const label = toLabel(data.config.name);
3306
+ const slug2 = toSlug(data.config.name);
3307
+ const label = extractLabel(yamlContent) ?? toLabel(data.config.name);
2170
3308
  const group = extractGroup(yamlContent) ?? (isCustom ? "Custom" : "Visor");
3309
+ const defaultMode = extractDefaultMode(yamlContent);
2171
3310
  const css = docsAdapter3({ primitives: data.primitives, tokens: data.tokens, config: data.config });
2172
3311
  const yamlFilename = basename2(filePath).replace(/\.visor\.yaml$/, "");
2173
- manifest.push({ slug, label, group, css, yamlFilename, isCustom });
3312
+ manifest.push({ slug: slug2, label, group, defaultMode, css, yamlFilename, isCustom });
2174
3313
  };
2175
3314
  for (const f of stockFiles) processFile(f, false);
2176
3315
  for (const f of customFiles) processFile(f, true);
@@ -2183,14 +3322,18 @@ function themeSyncCommand(cwd, options) {
2183
3322
  process.exit(1);
2184
3323
  return;
2185
3324
  }
2186
- const newThemeConfig = generateThemeConfig(manifest);
3325
+ const stockManifest = manifest.filter((e) => !e.isCustom);
3326
+ const customManifest = manifest.filter((e) => e.isCustom);
3327
+ const stockSlugs = stockManifest.map((e) => e.slug);
3328
+ const customSlugs = customManifest.map((e) => e.slug);
2187
3329
  const allSlugs = manifest.map((e) => e.slug);
2188
- const customSlugs = manifest.filter((e) => e.isCustom).map((e) => e.slug);
2189
3330
  let globalsContent;
3331
+ let themeConfigContent;
2190
3332
  let gitignoreContent;
2191
3333
  try {
2192
- globalsContent = readFileSync11(globalsPath, "utf-8");
2193
- gitignoreContent = existsSync9(gitignorePath) ? readFileSync11(gitignorePath, "utf-8") : "";
3334
+ globalsContent = readFileSync14(globalsPath, "utf-8");
3335
+ themeConfigContent = readFileSync14(themeConfigPath, "utf-8");
3336
+ gitignoreContent = existsSync11(gitignorePath) ? readFileSync14(gitignorePath, "utf-8") : "";
2194
3337
  } catch (err) {
2195
3338
  const msg = err instanceof Error ? err.message : "Could not read docs files";
2196
3339
  if (options.json) {
@@ -2201,12 +3344,17 @@ function themeSyncCommand(cwd, options) {
2201
3344
  process.exit(1);
2202
3345
  return;
2203
3346
  }
2204
- const newGlobals = updateGlobalsImports(globalsContent, allSlugs);
3347
+ const newGlobals = updateGlobalsImports(globalsContent, stockSlugs);
3348
+ const newThemeConfig = updateStockThemeConfigBlock(themeConfigContent, stockManifest);
2205
3349
  const newGitignore = customSlugs.length > 0 ? updateGitignoreBlock(gitignoreContent, customSlugs) : gitignoreContent;
2206
- const existingCssFiles = existsSync9(docsAppDir) ? readdirSync2(docsAppDir).filter((f) => f.endsWith("-theme.css")) : [];
3350
+ const newCustomOverlayCss = generateCustomOverlayCss(customManifest);
3351
+ const newCustomOverlayTs = generateCustomOverlayTs(customManifest);
3352
+ const existingCssFiles = existsSync11(docsAppDir) ? readdirSync4(docsAppDir).filter(
3353
+ (f) => f.endsWith("-theme.css") && f !== "custom-themes.generated.css"
3354
+ ) : [];
2207
3355
  const newCssSet = new Set(allSlugs.map((s) => `${s}-theme.css`));
2208
3356
  const staleCssFiles = existingCssFiles.filter((f) => !newCssSet.has(f));
2209
- const existingPublicYamls = existsSync9(docsPublicThemesDir) ? readdirSync2(docsPublicThemesDir).filter((f) => f.endsWith(".visor.yaml")) : [];
3357
+ const existingPublicYamls = existsSync11(docsPublicThemesDir) ? readdirSync4(docsPublicThemesDir).filter((f) => f.endsWith(".visor.yaml")) : [];
2210
3358
  const newPublicYamlSet = new Set(manifest.map((e) => `${e.yamlFilename}.visor.yaml`));
2211
3359
  const stalePublicYamls = existingPublicYamls.filter((f) => !newPublicYamlSet.has(f));
2212
3360
  if (options.dryRun) {
@@ -2216,6 +3364,8 @@ function themeSyncCommand(cwd, options) {
2216
3364
  cssFilesDeleted: staleCssFiles.map((f) => `packages/docs/app/${f}`),
2217
3365
  themeConfig: themeConfigPath,
2218
3366
  globalsCSS: globalsPath,
3367
+ customOverlayCss: CUSTOM_OVERLAY_CSS_PATH,
3368
+ customOverlayTs: CUSTOM_OVERLAY_TS_PATH,
2219
3369
  gitignore: gitignorePath,
2220
3370
  publicYamlsCopied: manifest.map((e) => `packages/docs/public/themes/${e.yamlFilename}.visor.yaml`),
2221
3371
  publicYamlsDeleted: stalePublicYamls.map((f) => `packages/docs/public/themes/${f}`)
@@ -2224,7 +3374,7 @@ function themeSyncCommand(cwd, options) {
2224
3374
  console.log(JSON.stringify({ success: true, dryRun: true, changes }));
2225
3375
  } else {
2226
3376
  logger.info("Dry run \u2014 no files written");
2227
- logger.item(`Themes discovered: ${manifest.length} (${manifest.filter((e) => !e.isCustom).length} stock, ${manifest.filter((e) => e.isCustom).length} custom)`);
3377
+ logger.item(`Themes discovered: ${manifest.length} (${stockManifest.length} stock, ${customManifest.length} custom)`);
2228
3378
  manifest.forEach((e) => logger.item(` ${e.slug} \u2014 group: ${e.group}`));
2229
3379
  if (staleCssFiles.length > 0) logger.item(`CSS files to delete: ${staleCssFiles.join(", ")}`);
2230
3380
  if (stalePublicYamls.length > 0) logger.item(`Public YAMLs to delete: ${stalePublicYamls.join(", ")}`);
@@ -2233,25 +3383,28 @@ function themeSyncCommand(cwd, options) {
2233
3383
  }
2234
3384
  try {
2235
3385
  mkdirSync5(docsAppDir, { recursive: true });
3386
+ mkdirSync5(docsLibDir, { recursive: true });
2236
3387
  mkdirSync5(docsPublicThemesDir, { recursive: true });
2237
3388
  for (const entry of manifest) {
2238
- writeFileSync8(join10(docsAppDir, `${entry.slug}-theme.css`), entry.css, "utf-8");
3389
+ writeFileSync9(join14(docsAppDir, `${entry.slug}-theme.css`), entry.css, "utf-8");
2239
3390
  }
2240
3391
  for (const stale of staleCssFiles) {
2241
- unlinkSync2(join10(docsAppDir, stale));
3392
+ unlinkSync2(join14(docsAppDir, stale));
2242
3393
  }
2243
- writeFileSync8(themeConfigPath, newThemeConfig, "utf-8");
2244
- writeFileSync8(globalsPath, newGlobals, "utf-8");
2245
- if (existsSync9(gitignorePath)) {
2246
- writeFileSync8(gitignorePath, newGitignore, "utf-8");
3394
+ writeFileSync9(customOverlayCssPath, newCustomOverlayCss, "utf-8");
3395
+ writeFileSync9(customOverlayTsPath, newCustomOverlayTs, "utf-8");
3396
+ writeFileSync9(themeConfigPath, newThemeConfig, "utf-8");
3397
+ writeFileSync9(globalsPath, newGlobals, "utf-8");
3398
+ if (existsSync11(gitignorePath)) {
3399
+ writeFileSync9(gitignorePath, newGitignore, "utf-8");
2247
3400
  }
2248
3401
  const allSourceFiles = [...stockFiles, ...customFiles];
2249
3402
  for (const srcFile of allSourceFiles) {
2250
3403
  const filename = basename2(srcFile);
2251
- copyFileSync(srcFile, join10(docsPublicThemesDir, filename));
3404
+ copyFileSync(srcFile, join14(docsPublicThemesDir, filename));
2252
3405
  }
2253
3406
  for (const stale of stalePublicYamls) {
2254
- unlinkSync2(join10(docsPublicThemesDir, stale));
3407
+ unlinkSync2(join14(docsPublicThemesDir, stale));
2255
3408
  }
2256
3409
  } catch (err) {
2257
3410
  const msg = err instanceof Error ? err.message : "Write failed";
@@ -2260,37 +3413,329 @@ function themeSyncCommand(cwd, options) {
2260
3413
  } else {
2261
3414
  logger.error(msg);
2262
3415
  }
2263
- process.exit(2);
3416
+ process.exit(2);
3417
+ return;
3418
+ }
3419
+ if (options.json) {
3420
+ console.log(JSON.stringify({
3421
+ success: true,
3422
+ themes: manifest.length,
3423
+ stock: stockManifest.length,
3424
+ custom: customManifest.length,
3425
+ staleCssDeleted: staleCssFiles.length,
3426
+ staleYamlsDeleted: stalePublicYamls.length,
3427
+ slugs: allSlugs
3428
+ }));
3429
+ } else {
3430
+ logger.success(`Theme sync complete \u2014 ${manifest.length} themes registered`);
3431
+ logger.item(`Stock: ${stockManifest.map((e) => e.slug).join(", ")}`);
3432
+ if (customManifest.length > 0) {
3433
+ logger.item(`Custom: ${customManifest.map((e) => e.slug).join(", ")}`);
3434
+ }
3435
+ if (staleCssFiles.length > 0) {
3436
+ logger.item(`Removed stale CSS: ${staleCssFiles.join(", ")}`);
3437
+ }
3438
+ }
3439
+ }
3440
+
3441
+ // src/commands/theme-batch-apply-flutter.ts
3442
+ import {
3443
+ readFileSync as readFileSync15,
3444
+ writeFileSync as writeFileSync10,
3445
+ mkdirSync as mkdirSync6,
3446
+ existsSync as existsSync12,
3447
+ readdirSync as readdirSync5,
3448
+ rmSync
3449
+ } from "fs";
3450
+ import { join as join15, basename as basename3, dirname as dirname6 } from "path";
3451
+ import { generateThemeData as generateThemeData5 } from "@loworbitstudio/visor-theme-engine";
3452
+ import { flutterAdapter as flutterAdapter2 } from "@loworbitstudio/visor-theme-engine/adapters";
3453
+ function scanThemeDir2(dir) {
3454
+ if (!existsSync12(dir)) return [];
3455
+ return readdirSync5(dir).filter((f) => f.endsWith(".visor.yaml")).map((f) => join15(dir, f)).sort();
3456
+ }
3457
+ function slugToCamel(slug2) {
3458
+ return slug2.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
3459
+ }
3460
+ function mapAdapterPath(relPath) {
3461
+ if (relPath === "pubspec.yaml") return null;
3462
+ if (relPath === "lib/ui.dart") return null;
3463
+ if (relPath.startsWith("lib/src/")) {
3464
+ return relPath.slice("lib/src/".length);
3465
+ }
3466
+ return null;
3467
+ }
3468
+ function emitVisorThemesPubspec() {
3469
+ return [
3470
+ `name: visor_themes`,
3471
+ `description: All Visor-generated Flutter ThemeData packages \u2014 4 stock + 7 custom themes. GENERATED \u2014 regenerate with \`npm run themes:apply-flutter\`.`,
3472
+ `version: 0.1.0+1`,
3473
+ `publish_to: none`,
3474
+ ``,
3475
+ `environment:`,
3476
+ ` sdk: ^3.5.0`,
3477
+ ` flutter: ^3.24.0`,
3478
+ ``,
3479
+ `dependencies:`,
3480
+ ` flutter:`,
3481
+ ` sdk: flutter`,
3482
+ ` visor_core: ^0.1.0`,
3483
+ ``,
3484
+ `dev_dependencies:`,
3485
+ ` flutter_test:`,
3486
+ ` sdk: flutter`,
3487
+ ` flutter_lints: ^5.0.0`,
3488
+ ``,
3489
+ `flutter:`,
3490
+ ` uses-material-design: true`,
3491
+ ``
3492
+ ].join("\n");
3493
+ }
3494
+ function emitVisorThemesPubspecOverrides() {
3495
+ return [
3496
+ `# Path overrides for in-monorepo development.`,
3497
+ `# visor_core is not yet published to pub.dev; this forces the local copy.`,
3498
+ `dependency_overrides:`,
3499
+ ` visor_core:`,
3500
+ ` path: ../visor-flutter`,
3501
+ ``
3502
+ ].join("\n");
3503
+ }
3504
+ function emitAnalysisOptions() {
3505
+ return [
3506
+ `include: package:flutter_lints/flutter.yaml`,
3507
+ ``,
3508
+ `linter:`,
3509
+ ` rules:`,
3510
+ ` - avoid_print`,
3511
+ ``
3512
+ ].join("\n");
3513
+ }
3514
+ function slugToDartPrefix(slug2) {
3515
+ return slug2.replace(/-/g, "_") + "_t";
3516
+ }
3517
+ function emitMetaBarrel(slugs) {
3518
+ const lines = [
3519
+ `// GENERATED BY visor \u2014 DO NOT EDIT.`,
3520
+ `// Regenerate with \`npm run themes:apply-flutter\`.`,
3521
+ `//`,
3522
+ `// Aggregates Dart ThemeData for all Visor themes (4 stock + 7 custom).`,
3523
+ `// Access themes via [VisorThemes], e.g. VisorThemes.blackout.light`,
3524
+ ``,
3525
+ `import 'package:flutter/material.dart';`,
3526
+ ``,
3527
+ `// Re-export visor_core so consumers access VisorColorsData etc. with`,
3528
+ `// a single import of this package.`,
3529
+ `export 'package:visor_core/visor_core.dart';`,
3530
+ ``
3531
+ ];
3532
+ for (const slug2 of slugs) {
3533
+ const prefix = slugToDartPrefix(slug2);
3534
+ lines.push(`import 'src/${slug2}/theme/visor_theme.dart' as ${prefix};`);
3535
+ }
3536
+ lines.push(``);
3537
+ lines.push(`/// A light/dark [ThemeData] pair for a single Visor theme.`);
3538
+ lines.push(`class VisorThemePair {`);
3539
+ lines.push(` final ThemeData light;`);
3540
+ lines.push(` final ThemeData dark;`);
3541
+ lines.push(` const VisorThemePair({required this.light, required this.dark});`);
3542
+ lines.push(`}`);
3543
+ lines.push(``);
3544
+ lines.push(`/// Static access to all Visor-generated Flutter themes.`);
3545
+ lines.push(`///`);
3546
+ lines.push(`/// Usage:`);
3547
+ lines.push(`/// \`\`\`dart`);
3548
+ lines.push(`/// MaterialApp(`);
3549
+ lines.push(`/// theme: VisorThemes.blackout.light,`);
3550
+ lines.push(`/// darkTheme: VisorThemes.blackout.dark,`);
3551
+ lines.push(`/// );`);
3552
+ lines.push(`/// \`\`\``);
3553
+ lines.push(`sealed class VisorThemes {`);
3554
+ for (const slug2 of slugs) {
3555
+ const camel = slugToCamel(slug2);
3556
+ const prefix = slugToDartPrefix(slug2);
3557
+ lines.push(` static VisorThemePair get ${camel} => VisorThemePair(`);
3558
+ lines.push(` light: ${prefix}.VisorAppTheme.light,`);
3559
+ lines.push(` dark: ${prefix}.VisorAppTheme.dark,`);
3560
+ lines.push(` );`);
3561
+ }
3562
+ lines.push(`}`);
3563
+ lines.push(``);
3564
+ return lines.join("\n");
3565
+ }
3566
+ function emitGitignore() {
3567
+ return [
3568
+ `.dart_tool/`,
3569
+ `.packages`,
3570
+ `build/`,
3571
+ `pubspec.lock`,
3572
+ `*.g.dart`,
3573
+ ``
3574
+ ].join("\n");
3575
+ }
3576
+ function themeBatchApplyFlutterCommand(cwd, options) {
3577
+ const repoRoot = findRepoRoot(cwd);
3578
+ if (!repoRoot) {
3579
+ const msg = "Could not locate repo root (packages/docs/ not found). Run from within the visor repo.";
3580
+ if (options.json) {
3581
+ console.log(JSON.stringify({ success: false, error: msg }));
3582
+ } else {
3583
+ logger.error(msg);
3584
+ }
3585
+ process.exit(1);
3586
+ return;
3587
+ }
3588
+ const themesDir = join15(repoRoot, "themes");
3589
+ const customThemesDir = join15(repoRoot, "custom-themes");
3590
+ const outputDir = join15(repoRoot, "packages", "visor_themes");
3591
+ const stockFiles = scanThemeDir2(themesDir);
3592
+ const customFiles = scanThemeDir2(customThemesDir);
3593
+ const allFiles = [...stockFiles, ...customFiles];
3594
+ if (allFiles.length === 0) {
3595
+ const msg = `No .visor.yaml files found in themes/ or custom-themes/. Nothing to generate.`;
3596
+ if (options.json) {
3597
+ console.log(JSON.stringify({ success: false, error: msg }));
3598
+ } else {
3599
+ logger.warn(msg);
3600
+ }
3601
+ return;
3602
+ }
3603
+ if (!options.json) {
3604
+ logger.info(`Found ${allFiles.length} theme YAML files (${stockFiles.length} stock, ${customFiles.length} custom)`);
3605
+ }
3606
+ const processed = [];
3607
+ const errors = [];
3608
+ for (const filePath of allFiles) {
3609
+ let yamlContent;
3610
+ try {
3611
+ yamlContent = readFileSync15(filePath, "utf-8");
3612
+ } catch {
3613
+ errors.push(`Could not read: ${filePath}`);
3614
+ continue;
3615
+ }
3616
+ let data;
3617
+ try {
3618
+ data = generateThemeData5(yamlContent);
3619
+ } catch (err) {
3620
+ errors.push(
3621
+ `Failed to parse ${basename3(filePath)}: ${err instanceof Error ? err.message : "Unknown error"}`
3622
+ );
3623
+ continue;
3624
+ }
3625
+ const slug2 = data.config.name.toLowerCase().replace(/\s+/g, "-");
3626
+ const camel = slugToCamel(slug2);
3627
+ const flutterOptions = {
3628
+ packageName: `visor_themes_${slug2.replace(/-/g, "_")}`,
3629
+ themeClassName: "VisorAppTheme"
3630
+ };
3631
+ let fileMap;
3632
+ try {
3633
+ fileMap = flutterAdapter2(
3634
+ {
3635
+ primitives: data.primitives,
3636
+ tokens: data.tokens,
3637
+ config: data.config
3638
+ },
3639
+ flutterOptions
3640
+ );
3641
+ } catch (err) {
3642
+ errors.push(
3643
+ `Failed flutter adapter for ${slug2}: ${err instanceof Error ? err.message : "Unknown error"}`
3644
+ );
3645
+ continue;
3646
+ }
3647
+ const primsAsUnknown = data.primitives;
3648
+ const primitivesMap = primsAsUnknown;
3649
+ const primaryHex = typeof primitivesMap?.primary500 === "string" ? primitivesMap.primary500 : "#000000";
3650
+ const tokenFiles = {};
3651
+ for (const [relPath, content] of Object.entries(fileMap.files)) {
3652
+ const mapped = mapAdapterPath(relPath);
3653
+ if (mapped !== null) {
3654
+ tokenFiles[mapped] = content;
3655
+ }
3656
+ }
3657
+ if (tokenFiles["theme/visor_theme.dart"]) {
3658
+ tokenFiles["theme/visor_theme.dart"] = tokenFiles["theme/visor_theme.dart"].replace(/import '\.\.\/colors\/visor_colors\.dart';/g, "import '../colors/visor_colors.dart';").replace(/import '\.\.\/typography\/visor_text_styles\.dart';/g, "import '../typography/visor_text_styles.dart';").replace(/import '\.\.\/spacing\/visor_spacing\.dart';/g, "import '../spacing/visor_spacing.dart';").replace(/import '\.\.\/radius\/visor_radius\.dart';/g, "import '../radius/visor_radius.dart';").replace(/import '\.\.\/shadows\/visor_shadows\.dart';/g, "import '../shadows/visor_shadows.dart';").replace(/import '\.\.\/strokes\/visor_stroke_widths\.dart';/g, "import '../strokes/visor_stroke_widths.dart';").replace(/import '\.\.\/opacity\/visor_opacity\.dart';/g, "import '../opacity/visor_opacity.dart';").replace(/import '\.\.\/motion\/visor_motion\.dart';/g, "import '../motion/visor_motion.dart';");
3659
+ }
3660
+ processed.push({ slug: slug2, camel, primaryHex, tokenFiles });
3661
+ }
3662
+ if (errors.length > 0) {
3663
+ if (options.json) {
3664
+ console.log(JSON.stringify({ success: false, errors }));
3665
+ } else {
3666
+ errors.forEach((e) => logger.error(e));
3667
+ }
3668
+ process.exit(1);
3669
+ return;
3670
+ }
3671
+ if (options.dryRun) {
3672
+ if (!options.json) {
3673
+ logger.info(`[dry-run] Would generate ${processed.length} theme packages in ${outputDir}`);
3674
+ for (const { slug: slug2 } of processed) {
3675
+ logger.item(` packages/visor_themes/lib/src/${slug2}/`);
3676
+ }
3677
+ } else {
3678
+ console.log(
3679
+ JSON.stringify({
3680
+ success: true,
3681
+ dryRun: true,
3682
+ themes: processed.map((p) => p.slug),
3683
+ outputDir
3684
+ })
3685
+ );
3686
+ }
2264
3687
  return;
2265
3688
  }
3689
+ const slugs = processed.map((p) => p.slug);
3690
+ const libSrcDir = join15(outputDir, "lib", "src");
3691
+ if (existsSync12(libSrcDir)) {
3692
+ rmSync(libSrcDir, { recursive: true, force: true });
3693
+ }
3694
+ const packageFiles = {
3695
+ "pubspec.yaml": emitVisorThemesPubspec(),
3696
+ "pubspec_overrides.yaml": emitVisorThemesPubspecOverrides(),
3697
+ "analysis_options.yaml": emitAnalysisOptions(),
3698
+ ".gitignore": emitGitignore(),
3699
+ "lib/visor_themes.dart": emitMetaBarrel(slugs)
3700
+ };
3701
+ let totalFiles = 0;
3702
+ for (const [relPath, content] of Object.entries(packageFiles)) {
3703
+ const absPath = join15(outputDir, relPath);
3704
+ mkdirSync6(dirname6(absPath), { recursive: true });
3705
+ writeFileSync10(absPath, content, "utf-8");
3706
+ totalFiles++;
3707
+ }
3708
+ for (const { slug: slug2, tokenFiles } of processed) {
3709
+ const themeBaseDir = join15(outputDir, "lib", "src", slug2);
3710
+ for (const [relPath, content] of Object.entries(tokenFiles)) {
3711
+ const absPath = join15(themeBaseDir, relPath);
3712
+ mkdirSync6(dirname6(absPath), { recursive: true });
3713
+ writeFileSync10(absPath, content, "utf-8");
3714
+ totalFiles++;
3715
+ }
3716
+ }
2266
3717
  if (options.json) {
2267
- console.log(JSON.stringify({
2268
- success: true,
2269
- themes: manifest.length,
2270
- stock: manifest.filter((e) => !e.isCustom).length,
2271
- custom: manifest.filter((e) => e.isCustom).length,
2272
- staleCssDeleted: staleCssFiles.length,
2273
- staleYamlsDeleted: stalePublicYamls.length,
2274
- slugs: allSlugs
2275
- }));
3718
+ console.log(
3719
+ JSON.stringify({
3720
+ success: true,
3721
+ outputDir,
3722
+ themes: slugs,
3723
+ totalFiles
3724
+ })
3725
+ );
2276
3726
  } else {
2277
- logger.success(`Theme sync complete \u2014 ${manifest.length} themes registered`);
2278
- logger.item(`Stock: ${manifest.filter((e) => !e.isCustom).map((e) => e.slug).join(", ")}`);
2279
- if (manifest.filter((e) => e.isCustom).length > 0) {
2280
- logger.item(`Custom: ${manifest.filter((e) => e.isCustom).map((e) => e.slug).join(", ")}`);
2281
- }
2282
- if (staleCssFiles.length > 0) {
2283
- logger.item(`Removed stale CSS: ${staleCssFiles.join(", ")}`);
2284
- }
3727
+ logger.success(`Flutter theme package generated: ${outputDir}`);
3728
+ logger.item(`Themes: ${slugs.join(", ")}`);
3729
+ logger.item(`Total files written: ${totalFiles}`);
2285
3730
  }
2286
3731
  }
2287
3732
 
2288
3733
  // src/commands/fonts-add.ts
2289
- import { existsSync as existsSync10, statSync as statSync2, readdirSync as readdirSync3, readFileSync as readFileSync12 } from "fs";
2290
- import { resolve as resolve7, basename as basename3, extname as extname2 } from "path";
3734
+ import { existsSync as existsSync13, statSync as statSync4, readdirSync as readdirSync6, readFileSync as readFileSync16 } from "fs";
3735
+ import { resolve as resolve8, basename as basename4, extname as extname3 } from "path";
2291
3736
  import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
2292
3737
  function deriveFamilySlug(filename) {
2293
- const name = basename3(filename, extname2(filename));
3738
+ const name = basename4(filename, extname3(filename));
2294
3739
  const WEIGHT_STYLE_SUFFIXES = /* @__PURE__ */ new Set([
2295
3740
  "thin",
2296
3741
  "hairline",
@@ -2331,28 +3776,28 @@ function deriveFamilySlug(filename) {
2331
3776
  return parts.join("-").toLowerCase();
2332
3777
  }
2333
3778
  function collectWoff2Files(inputPath) {
2334
- const resolved = resolve7(inputPath);
2335
- if (!existsSync10(resolved)) {
3779
+ const resolved = resolve8(inputPath);
3780
+ if (!existsSync13(resolved)) {
2336
3781
  throw new Error(`Path not found: ${resolved}`);
2337
3782
  }
2338
- const stat = statSync2(resolved);
3783
+ const stat = statSync4(resolved);
2339
3784
  if (stat.isFile()) {
2340
- if (extname2(resolved).toLowerCase() !== ".woff2") {
3785
+ if (extname3(resolved).toLowerCase() !== ".woff2") {
2341
3786
  throw new Error(
2342
- `Invalid file format: ${basename3(resolved)}. Only .woff2 files are accepted.`
3787
+ `Invalid file format: ${basename4(resolved)}. Only .woff2 files are accepted.`
2343
3788
  );
2344
3789
  }
2345
3790
  return [resolved];
2346
3791
  }
2347
3792
  if (stat.isDirectory()) {
2348
- const files = readdirSync3(resolved).filter((f) => extname2(f).toLowerCase() === ".woff2").map((f) => resolve7(resolved, f));
3793
+ const files = readdirSync6(resolved).filter((f) => extname3(f).toLowerCase() === ".woff2").map((f) => resolve8(resolved, f));
2349
3794
  if (files.length === 0) {
2350
3795
  throw new Error(
2351
3796
  `No .woff2 files found in directory: ${resolved}`
2352
3797
  );
2353
3798
  }
2354
- const nonWoff2Fonts = readdirSync3(resolved).filter((f) => {
2355
- const ext = extname2(f).toLowerCase();
3799
+ const nonWoff2Fonts = readdirSync6(resolved).filter((f) => {
3800
+ const ext = extname3(f).toLowerCase();
2356
3801
  return [".ttf", ".otf", ".woff", ".eot"].includes(ext);
2357
3802
  });
2358
3803
  return files.sort();
@@ -2360,12 +3805,12 @@ function collectWoff2Files(inputPath) {
2360
3805
  throw new Error(`Path is neither a file nor a directory: ${resolved}`);
2361
3806
  }
2362
3807
  function getNonWoff2Fonts(inputPath) {
2363
- const resolved = resolve7(inputPath);
2364
- if (!existsSync10(resolved) || !statSync2(resolved).isDirectory()) {
3808
+ const resolved = resolve8(inputPath);
3809
+ if (!existsSync13(resolved) || !statSync4(resolved).isDirectory()) {
2365
3810
  return [];
2366
3811
  }
2367
- return readdirSync3(resolved).filter((f) => {
2368
- const ext = extname2(f).toLowerCase();
3812
+ return readdirSync6(resolved).filter((f) => {
3813
+ const ext = extname3(f).toLowerCase();
2369
3814
  return [".ttf", ".otf", ".woff", ".eot"].includes(ext);
2370
3815
  });
2371
3816
  }
@@ -2398,7 +3843,7 @@ function createR2Client(config) {
2398
3843
  });
2399
3844
  }
2400
3845
  async function uploadFile(client, bucket, key, filePath) {
2401
- const body = readFileSync12(filePath);
3846
+ const body = readFileSync16(filePath);
2402
3847
  await client.send(
2403
3848
  new PutObjectCommand({
2404
3849
  Bucket: bucket,
@@ -2413,9 +3858,9 @@ async function fontsAddCommand(inputPath, options) {
2413
3858
  try {
2414
3859
  const r2Config = getR2Config();
2415
3860
  const files = collectWoff2Files(inputPath);
2416
- const familySlug = options.family ?? deriveFamilySlug(basename3(files[0]));
2417
- const resolved = resolve7(inputPath);
2418
- const nonWoff2 = statSync2(resolved).isDirectory() ? getNonWoff2Fonts(resolved) : [];
3861
+ const familySlug = options.family ?? deriveFamilySlug(basename4(files[0]));
3862
+ const resolved = resolve8(inputPath);
3863
+ const nonWoff2 = statSync4(resolved).isDirectory() ? getNonWoff2Fonts(resolved) : [];
2419
3864
  if (!json) {
2420
3865
  logger.heading("Visor Font Upload");
2421
3866
  logger.info(`Organization: ${org}`);
@@ -2433,13 +3878,13 @@ async function fontsAddCommand(inputPath, options) {
2433
3878
  const bucket = "visor-fonts";
2434
3879
  const results = [];
2435
3880
  for (const filePath of files) {
2436
- const filename = basename3(filePath);
3881
+ const filename = basename4(filePath);
2437
3882
  const key = buildS3Key(org, familySlug, filename);
2438
3883
  if (!json) {
2439
3884
  logger.info(`Uploading ${filename}...`);
2440
3885
  }
2441
3886
  await uploadFile(client, bucket, key, filePath);
2442
- const size = statSync2(filePath).size;
3887
+ const size = statSync4(filePath).size;
2443
3888
  results.push({ file: filename, key, size });
2444
3889
  if (!json) {
2445
3890
  logger.success(`Uploaded: ${key} (${formatBytes(size)})`);
@@ -2486,27 +3931,670 @@ function formatBytes(bytes) {
2486
3931
  return `${mb.toFixed(1)} MB`;
2487
3932
  }
2488
3933
 
3934
+ // src/commands/doctor.ts
3935
+ import * as fs from "fs";
3936
+ import * as path from "path";
3937
+ import { execFileSync as execFileSync3 } from "child_process";
3938
+ async function doctorCommand(cwd, options, cliVersion) {
3939
+ const checks = [];
3940
+ const visorJsonPath = path.join(cwd, "visor.json");
3941
+ try {
3942
+ const content = fs.readFileSync(visorJsonPath, "utf-8");
3943
+ JSON.parse(content);
3944
+ checks.push({ name: "visor.json", pass: true, severity: "error", message: "visor.json exists and is valid JSON" });
3945
+ } catch {
3946
+ checks.push({
3947
+ name: "visor.json",
3948
+ pass: false,
3949
+ severity: "error",
3950
+ message: "visor.json missing or invalid",
3951
+ fix: "Run `npx visor init` to initialize Visor in this project"
3952
+ });
3953
+ }
3954
+ const visorCorePath = path.join(cwd, "node_modules", "@loworbitstudio", "visor-core");
3955
+ if (fs.existsSync(visorCorePath)) {
3956
+ checks.push({ name: "visor-core", pass: true, severity: "error", message: "@loworbitstudio/visor-core is installed" });
3957
+ } else {
3958
+ checks.push({
3959
+ name: "visor-core",
3960
+ pass: false,
3961
+ severity: "error",
3962
+ message: "@loworbitstudio/visor-core not found in node_modules",
3963
+ fix: "Run `npm install @loworbitstudio/visor-core`"
3964
+ });
3965
+ }
3966
+ const cssFiles = findCssFiles(cwd);
3967
+ const hasVisorImport = cssFiles.some((f) => {
3968
+ try {
3969
+ const content = fs.readFileSync(f, "utf-8");
3970
+ return content.includes("visor-core") || content.includes("@loworbitstudio/visor-core");
3971
+ } catch {
3972
+ return false;
3973
+ }
3974
+ });
3975
+ if (hasVisorImport) {
3976
+ checks.push({ name: "css-import", pass: true, severity: "warning", message: "visor-core CSS import found" });
3977
+ } else {
3978
+ checks.push({
3979
+ name: "css-import",
3980
+ pass: false,
3981
+ severity: "warning",
3982
+ message: "No visor-core CSS import found in CSS files",
3983
+ fix: 'Add `@import "@loworbitstudio/visor-core/tokens.css"` to your global CSS file'
3984
+ });
3985
+ }
3986
+ const pkgJsonPath = path.join(cwd, "package.json");
3987
+ try {
3988
+ const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
3989
+ const reactVersion = pkg.dependencies?.react ?? pkg.devDependencies?.react ?? "";
3990
+ const versionNum = parseFloat(reactVersion.replace(/[^0-9.]/g, ""));
3991
+ if (!reactVersion) {
3992
+ checks.push({
3993
+ name: "react-version",
3994
+ pass: false,
3995
+ severity: "error",
3996
+ message: "React not found in dependencies",
3997
+ fix: "Install React: `npm install react@latest react-dom@latest`"
3998
+ });
3999
+ } else if (versionNum >= 17 || reactVersion.includes("18") || reactVersion.includes("19")) {
4000
+ checks.push({ name: "react-version", pass: true, severity: "error", message: `React ${reactVersion} satisfies peer dep requirement (>=17)` });
4001
+ } else {
4002
+ checks.push({
4003
+ name: "react-version",
4004
+ pass: false,
4005
+ severity: "error",
4006
+ message: `React version ${reactVersion} may not satisfy peer dep requirement (>=17)`,
4007
+ fix: "Upgrade React to v17 or higher: `npm install react@latest react-dom@latest`"
4008
+ });
4009
+ }
4010
+ } catch {
4011
+ checks.push({
4012
+ name: "react-version",
4013
+ pass: false,
4014
+ severity: "warning",
4015
+ message: "Could not read package.json to check React version",
4016
+ fix: "Ensure package.json exists in the project root"
4017
+ });
4018
+ }
4019
+ const componentsDir = path.join(cwd, "components", "ui");
4020
+ if (fs.existsSync(componentsDir) && fs.readdirSync(componentsDir).length > 0) {
4021
+ const count = fs.readdirSync(componentsDir).length;
4022
+ checks.push({ name: "components", pass: true, severity: "info", message: `${count} component(s) found under components/ui/` });
4023
+ } else {
4024
+ checks.push({
4025
+ name: "components",
4026
+ pass: false,
4027
+ severity: "info",
4028
+ message: "No components found under components/ui/",
4029
+ fix: "Add components with `npx visor add <component-name>` (e.g. `npx visor add button`)"
4030
+ });
4031
+ }
4032
+ const manifestPaths = [
4033
+ path.join(cwd, "public", "r", "index.json"),
4034
+ path.join(cwd, "registry", "index.json")
4035
+ ];
4036
+ const foundManifest = manifestPaths.find((p) => fs.existsSync(p));
4037
+ if (foundManifest) {
4038
+ try {
4039
+ const manifestContent = JSON.parse(fs.readFileSync(foundManifest, "utf-8"));
4040
+ const isEmpty = manifestContent === null || Array.isArray(manifestContent) && manifestContent.length === 0 || typeof manifestContent === "object" && Object.keys(manifestContent).length === 0;
4041
+ if (!isEmpty) {
4042
+ checks.push({ name: "registry-manifest", pass: true, severity: "info", message: `Registry manifest found at ${path.relative(cwd, foundManifest)}` });
4043
+ } else {
4044
+ checks.push({
4045
+ name: "registry-manifest",
4046
+ pass: false,
4047
+ severity: "info",
4048
+ message: "Registry manifest is empty",
4049
+ fix: "Run `npx visor build` to regenerate the registry manifest"
4050
+ });
4051
+ }
4052
+ } catch {
4053
+ checks.push({
4054
+ name: "registry-manifest",
4055
+ pass: false,
4056
+ severity: "info",
4057
+ message: "Registry manifest found but could not be parsed",
4058
+ fix: "Run `npx visor build` to regenerate the registry manifest"
4059
+ });
4060
+ }
4061
+ } else {
4062
+ checks.push({
4063
+ name: "registry-manifest",
4064
+ pass: false,
4065
+ severity: "info",
4066
+ message: "No registry manifest found (this is normal for consumer projects)",
4067
+ fix: "If building a design system, run `npx visor build` to generate the registry manifest"
4068
+ });
4069
+ }
4070
+ if (process.platform !== "win32") {
4071
+ try {
4072
+ const globalPath = execFileSync3("which", ["visor"], { encoding: "utf-8" }).trim();
4073
+ if (globalPath) {
4074
+ const globalVersionRaw = execFileSync3(globalPath, ["--version"], { encoding: "utf-8" }).trim();
4075
+ const globalVersion = globalVersionRaw.split(/\s+/).pop() ?? "";
4076
+ if (isOlder(globalVersion, cliVersion)) {
4077
+ checks.push({
4078
+ name: "stale-global-cli",
4079
+ pass: false,
4080
+ severity: "warning",
4081
+ message: `Global visor ${globalVersion} is older than running CLI ${cliVersion}`,
4082
+ fix: "Run npm uninstall -g @loworbitstudio/visor to remove the stale global"
4083
+ });
4084
+ } else {
4085
+ checks.push({
4086
+ name: "stale-global-cli",
4087
+ pass: true,
4088
+ severity: "warning",
4089
+ message: `Global visor ${globalVersion} matches running CLI`
4090
+ });
4091
+ }
4092
+ }
4093
+ } catch {
4094
+ }
4095
+ }
4096
+ const hasErrors = checks.some((c) => !c.pass && c.severity === "error");
4097
+ const hasWarnings = checks.some((c) => !c.pass && c.severity === "warning");
4098
+ const result = {
4099
+ status: hasErrors ? "error" : hasWarnings ? "warning" : "ok",
4100
+ checks
4101
+ };
4102
+ if (options.json) {
4103
+ console.log(JSON.stringify(result, null, 2));
4104
+ process.exit(hasErrors ? 1 : 0);
4105
+ return;
4106
+ }
4107
+ console.log("\nVisor Doctor\n============");
4108
+ for (const check of checks) {
4109
+ const icon = check.pass ? "\u2713" : check.severity === "error" ? "\u2717" : "\u26A0";
4110
+ console.log(`${icon} ${check.name}: ${check.message}`);
4111
+ if (!check.pass && check.fix) {
4112
+ console.log(` Fix: ${check.fix}`);
4113
+ }
4114
+ }
4115
+ console.log(`
4116
+ Status: ${result.status.toUpperCase()}`);
4117
+ process.exit(hasErrors ? 1 : 0);
4118
+ }
4119
+ function isOlder(a, b) {
4120
+ const pa = a.split(".").map((n) => parseInt(n, 10) || 0);
4121
+ const pb = b.split(".").map((n) => parseInt(n, 10) || 0);
4122
+ const len = Math.max(pa.length, pb.length);
4123
+ for (let i = 0; i < len; i++) {
4124
+ const va = pa[i] ?? 0;
4125
+ const vb = pb[i] ?? 0;
4126
+ if (va < vb) return true;
4127
+ if (va > vb) return false;
4128
+ }
4129
+ return false;
4130
+ }
4131
+ function findCssFiles(dir, maxDepth = 3) {
4132
+ const files = [];
4133
+ try {
4134
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
4135
+ for (const entry of entries) {
4136
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
4137
+ const fullPath = path.join(dir, entry.name);
4138
+ if (entry.isFile() && (entry.name.endsWith(".css") || entry.name.endsWith(".scss"))) {
4139
+ files.push(fullPath);
4140
+ } else if (entry.isDirectory() && maxDepth > 0) {
4141
+ files.push(...findCssFiles(fullPath, maxDepth - 1));
4142
+ }
4143
+ }
4144
+ } catch {
4145
+ }
4146
+ return files;
4147
+ }
4148
+
4149
+ // src/utils/patterns.ts
4150
+ import { existsSync as existsSync15, readdirSync as readdirSync8, readFileSync as readFileSync18 } from "fs";
4151
+ import { join as join17 } from "path";
4152
+ import { parse as parseYAML } from "yaml";
4153
+ function loadPatternsFromYaml(repoRoot) {
4154
+ const patternsDir = join17(repoRoot, "patterns");
4155
+ if (!existsSync15(patternsDir)) return [];
4156
+ const files = readdirSync8(patternsDir).filter(
4157
+ (f) => f.endsWith(".visor-pattern.yaml")
4158
+ );
4159
+ return files.map((file) => {
4160
+ const content = readFileSync18(join17(patternsDir, file), "utf-8");
4161
+ return parseYAML(content);
4162
+ }).filter(Boolean);
4163
+ }
4164
+ function findRepoRoot2(startDir) {
4165
+ let current = startDir;
4166
+ while (true) {
4167
+ if (existsSync15(join17(current, "patterns"))) {
4168
+ return current;
4169
+ }
4170
+ const parent = join17(current, "..");
4171
+ if (parent === current) return null;
4172
+ current = parent;
4173
+ }
4174
+ }
4175
+
4176
+ // src/commands/pattern.ts
4177
+ function patternListCommand(cwd, options = {}) {
4178
+ const json = options.json ?? false;
4179
+ const repoRoot = findRepoRoot2(cwd);
4180
+ if (!repoRoot) {
4181
+ if (json) {
4182
+ console.log(
4183
+ JSON.stringify({ success: false, error: "Could not find patterns/ directory." }, null, 2)
4184
+ );
4185
+ process.exit(1);
4186
+ return;
4187
+ }
4188
+ logger.error("Could not find patterns/ directory.");
4189
+ process.exit(1);
4190
+ return;
4191
+ }
4192
+ const patterns = loadPatternsFromYaml(repoRoot);
4193
+ if (json) {
4194
+ const output = patterns.map((p) => ({
4195
+ name: p.name,
4196
+ description: p.description,
4197
+ components_used: p.components_used,
4198
+ when_to_use: p.when_to_use
4199
+ }));
4200
+ console.log(
4201
+ JSON.stringify(
4202
+ {
4203
+ success: true,
4204
+ patterns: output,
4205
+ summary: { total: output.length }
4206
+ },
4207
+ null,
4208
+ 2
4209
+ )
4210
+ );
4211
+ process.exit(0);
4212
+ return;
4213
+ }
4214
+ logger.heading(`Composition Patterns (${patterns.length})`);
4215
+ logger.blank();
4216
+ for (const p of patterns) {
4217
+ logger.info(` ${p.name.padEnd(32)} ${p.description}`);
4218
+ }
4219
+ logger.blank();
4220
+ }
4221
+ function patternInfoCommand(name, cwd, options = {}) {
4222
+ const json = options.json ?? false;
4223
+ const repoRoot = findRepoRoot2(cwd);
4224
+ if (!repoRoot) {
4225
+ if (json) {
4226
+ console.log(
4227
+ JSON.stringify({ success: false, error: "Could not find patterns/ directory." }, null, 2)
4228
+ );
4229
+ process.exit(1);
4230
+ return;
4231
+ }
4232
+ logger.error("Could not find patterns/ directory.");
4233
+ process.exit(1);
4234
+ return;
4235
+ }
4236
+ const patterns = loadPatternsFromYaml(repoRoot);
4237
+ const pattern2 = patterns.find(
4238
+ (p) => p.name.toLowerCase() === name.toLowerCase() || p.name.toLowerCase().replace(/\s+/g, "-") === name.toLowerCase()
4239
+ );
4240
+ if (!pattern2) {
4241
+ if (json) {
4242
+ console.log(
4243
+ JSON.stringify({ success: false, error: `Pattern "${name}" not found.` }, null, 2)
4244
+ );
4245
+ process.exit(1);
4246
+ return;
4247
+ }
4248
+ logger.error(`Pattern "${name}" not found.`);
4249
+ process.exit(1);
4250
+ return;
4251
+ }
4252
+ if (json) {
4253
+ console.log(
4254
+ JSON.stringify(
4255
+ {
4256
+ success: true,
4257
+ pattern: {
4258
+ name: pattern2.name,
4259
+ description: pattern2.description,
4260
+ components_used: pattern2.components_used,
4261
+ ...pattern2.related_blocks ? { related_blocks: pattern2.related_blocks } : {},
4262
+ when_to_use: pattern2.when_to_use,
4263
+ structure: pattern2.structure,
4264
+ notes: pattern2.notes
4265
+ }
4266
+ },
4267
+ null,
4268
+ 2
4269
+ )
4270
+ );
4271
+ process.exit(0);
4272
+ return;
4273
+ }
4274
+ logger.heading(pattern2.name);
4275
+ logger.blank();
4276
+ logger.info(`Description: ${pattern2.description}`);
4277
+ logger.blank();
4278
+ logger.info(`Components used: ${pattern2.components_used.join(", ")}`);
4279
+ logger.blank();
4280
+ logger.info("When to use:");
4281
+ for (const item of pattern2.when_to_use) {
4282
+ logger.info(` - ${item}`);
4283
+ }
4284
+ if (pattern2.related_blocks && pattern2.related_blocks.length > 0) {
4285
+ logger.blank();
4286
+ logger.info(`Related blocks: ${pattern2.related_blocks.join(", ")}`);
4287
+ }
4288
+ logger.blank();
4289
+ logger.info("Structure:");
4290
+ logger.blank();
4291
+ console.log(pattern2.structure);
4292
+ logger.blank();
4293
+ logger.info("Notes:");
4294
+ logger.blank();
4295
+ console.log(pattern2.notes);
4296
+ logger.blank();
4297
+ }
4298
+
4299
+ // src/commands/suggest.ts
4300
+ var STOP_WORDS2 = /* @__PURE__ */ new Set([
4301
+ "a",
4302
+ "an",
4303
+ "the",
4304
+ "with",
4305
+ "for",
4306
+ "and",
4307
+ "or",
4308
+ "to",
4309
+ "in",
4310
+ "of",
4311
+ "is",
4312
+ "that",
4313
+ "this",
4314
+ "it",
4315
+ "as",
4316
+ "at",
4317
+ "by",
4318
+ "on",
4319
+ "be",
4320
+ "are",
4321
+ "was",
4322
+ "were"
4323
+ ]);
4324
+ function tokenize2(text) {
4325
+ return text.toLowerCase().split(/[\s\-_,]+/).filter((t) => t.length > 1 && !STOP_WORDS2.has(t));
4326
+ }
4327
+ function scoreEntry(queryTokens, name, description, whenToUse) {
4328
+ const searchText = [name, description, ...whenToUse].join(" ").toLowerCase();
4329
+ const matchedTokens = queryTokens.filter((t) => searchText.includes(t));
4330
+ return {
4331
+ score: matchedTokens.length,
4332
+ matchReason: matchedTokens.length > 0 ? `Matched: ${matchedTokens.join(", ")}` : ""
4333
+ };
4334
+ }
4335
+ async function suggestCommand(_cwd, options) {
4336
+ const query = options.for;
4337
+ const queryTokens = tokenize2(query);
4338
+ if (queryTokens.length === 0) {
4339
+ const err = {
4340
+ success: false,
4341
+ error: "Query is too short or contains only stop words. Try more specific terms."
4342
+ };
4343
+ if (options.json) {
4344
+ console.error(JSON.stringify(err));
4345
+ process.exit(1);
4346
+ }
4347
+ console.error(err.error);
4348
+ process.exit(1);
4349
+ }
4350
+ const manifest = loadManifest();
4351
+ const results = [];
4352
+ for (const [name, entry] of Object.entries(manifest.components)) {
4353
+ const { score, matchReason } = scoreEntry(
4354
+ queryTokens,
4355
+ name,
4356
+ entry.description,
4357
+ entry.when_to_use || []
4358
+ );
4359
+ if (score >= 1) {
4360
+ results.push({
4361
+ name,
4362
+ type: "component",
4363
+ category: entry.category,
4364
+ score,
4365
+ description: entry.description,
4366
+ match_reason: matchReason,
4367
+ install_command: `npx visor add ${name}`
4368
+ });
4369
+ }
4370
+ }
4371
+ for (const [name, entry] of Object.entries(manifest.blocks)) {
4372
+ const { score, matchReason } = scoreEntry(
4373
+ queryTokens,
4374
+ name,
4375
+ entry.description,
4376
+ entry.when_to_use || []
4377
+ );
4378
+ if (score >= 1) {
4379
+ results.push({
4380
+ name,
4381
+ type: "block",
4382
+ category: entry.category,
4383
+ score,
4384
+ description: entry.description,
4385
+ match_reason: matchReason,
4386
+ install_command: `npx visor add ${name} --block`
4387
+ });
4388
+ }
4389
+ }
4390
+ for (const [name, entry] of Object.entries(manifest.patterns)) {
4391
+ const { score, matchReason } = scoreEntry(
4392
+ queryTokens,
4393
+ name,
4394
+ entry.description,
4395
+ entry.when_to_use || []
4396
+ );
4397
+ if (score >= 1) {
4398
+ results.push({
4399
+ name,
4400
+ type: "pattern",
4401
+ score,
4402
+ description: entry.description,
4403
+ match_reason: matchReason,
4404
+ install_command: null
4405
+ });
4406
+ }
4407
+ }
4408
+ for (const [name, entry] of Object.entries(manifest.hooks)) {
4409
+ const { score, matchReason } = scoreEntry(
4410
+ queryTokens,
4411
+ name,
4412
+ entry.description,
4413
+ []
4414
+ );
4415
+ if (score >= 1) {
4416
+ results.push({
4417
+ name,
4418
+ type: "hook",
4419
+ score,
4420
+ description: entry.description,
4421
+ match_reason: matchReason,
4422
+ install_command: `npx visor add ${name}`
4423
+ });
4424
+ }
4425
+ }
4426
+ results.sort((a, b) => b.score - a.score);
4427
+ const topResults = results.slice(0, 10);
4428
+ if (topResults.length === 0) {
4429
+ const err = {
4430
+ success: false,
4431
+ error: `No matches found for "${query}". Try broader terms.`
4432
+ };
4433
+ if (options.json) {
4434
+ console.error(JSON.stringify(err, null, 2));
4435
+ process.exit(1);
4436
+ }
4437
+ console.error(err.error);
4438
+ process.exit(1);
4439
+ }
4440
+ const totalSearched = Object.keys(manifest.components).length + Object.keys(manifest.blocks).length + Object.keys(manifest.patterns).length + Object.keys(manifest.hooks).length;
4441
+ const output = {
4442
+ success: true,
4443
+ query,
4444
+ results: topResults,
4445
+ summary: {
4446
+ total_searched: totalSearched,
4447
+ total_matched: topResults.length
4448
+ }
4449
+ };
4450
+ if (options.json) {
4451
+ console.log(JSON.stringify(output, null, 2));
4452
+ process.exit(0);
4453
+ }
4454
+ console.log(`
4455
+ Suggestions for "${query}":
4456
+ `);
4457
+ for (const r of topResults) {
4458
+ const cmd = r.install_command ? ` (${r.install_command})` : "";
4459
+ console.log(` ${r.name} [${r.type}]${cmd}`);
4460
+ console.log(` ${r.description.slice(0, 80)}`);
4461
+ }
4462
+ console.log(
4463
+ `
4464
+ ${topResults.length} result${topResults.length !== 1 ? "s" : ""} from ${totalSearched} entries
4465
+ `
4466
+ );
4467
+ }
4468
+
4469
+ // src/commands/tokens.ts
4470
+ async function tokensListCommand(_cwd, options) {
4471
+ const manifest = loadManifest();
4472
+ if (!manifest.tokens) {
4473
+ const err = {
4474
+ success: false,
4475
+ error: "Tokens section not found in manifest. Run npm run build:manifest to rebuild."
4476
+ };
4477
+ if (options.json) {
4478
+ console.error(JSON.stringify(err));
4479
+ } else {
4480
+ console.error(err.error);
4481
+ }
4482
+ process.exit(1);
4483
+ }
4484
+ const { primitives, semantic, adaptive, summary } = manifest.tokens;
4485
+ let tokens2 = [...primitives, ...semantic, ...adaptive];
4486
+ let categoryLabel = "all";
4487
+ if (options.category) {
4488
+ const cat = options.category.toLowerCase();
4489
+ if (cat === "primitives") {
4490
+ tokens2 = primitives;
4491
+ categoryLabel = "primitives";
4492
+ } else if (cat === "semantic") {
4493
+ tokens2 = semantic;
4494
+ categoryLabel = "semantic";
4495
+ } else if (cat === "adaptive") {
4496
+ tokens2 = adaptive;
4497
+ categoryLabel = "adaptive";
4498
+ } else {
4499
+ const err = {
4500
+ success: false,
4501
+ error: `Unknown category "${options.category}". Use: primitives, semantic, adaptive`
4502
+ };
4503
+ if (options.json) {
4504
+ console.error(JSON.stringify(err));
4505
+ } else {
4506
+ console.error(err.error);
4507
+ }
4508
+ process.exit(1);
4509
+ }
4510
+ }
4511
+ if (options.json) {
4512
+ console.log(
4513
+ JSON.stringify(
4514
+ {
4515
+ success: true,
4516
+ tokens: tokens2,
4517
+ summary: {
4518
+ total: tokens2.length,
4519
+ category: categoryLabel,
4520
+ allTotal: summary.total
4521
+ }
4522
+ },
4523
+ null,
4524
+ 2
4525
+ )
4526
+ );
4527
+ return;
4528
+ }
4529
+ console.log(
4530
+ `
4531
+ Visor Tokens (${categoryLabel}) \u2014 ${tokens2.length} tokens
4532
+ `
4533
+ );
4534
+ for (const t of tokens2) {
4535
+ console.log(` ${t.name} [${t.tier}]`);
4536
+ if (t.description) {
4537
+ console.log(` ${t.description}`);
4538
+ }
4539
+ console.log(` Light: ${t.defaultLight}`);
4540
+ console.log(` Dark: ${t.defaultDark}`);
4541
+ }
4542
+ console.log(
4543
+ `
4544
+ Total: ${tokens2.length} tokens shown${categoryLabel !== "all" ? ` (${summary.total} total across all tiers)` : ""}`
4545
+ );
4546
+ }
4547
+
2489
4548
  // src/index.ts
2490
- var program = new Command();
2491
- program.name("visor").description("CLI for the Visor design system").version("0.1.0");
2492
- program.command("init").description("Initialize Visor in the current project").option("--template <name>", "scaffold a themed project (nextjs)").option("--json", "output structured JSON (for AI agents)").action((options) => {
4549
+ var program = new Command2();
4550
+ program.name("visor").description("CLI for the Visor design system").version("0.3.0");
4551
+ program.addCommand(checkCommand());
4552
+ 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) => {
2493
4553
  initCommand(process.cwd(), options);
2494
4554
  });
2495
4555
  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) => {
2496
4556
  listCommand(process.cwd(), options);
2497
4557
  });
2498
- program.command("add").description("Add components, hooks, blocks, or utilities to your project").argument("[items...]", "names of registry items to add").option("--overwrite", "overwrite existing files", false).option("--category <name>", "install all items from a category").option("--block", "install blocks instead of components").option("--json", "output structured JSON (for AI agents)").action((items, options) => {
2499
- addCommand(items, process.cwd(), { overwrite: options.overwrite, category: options.category, block: options.block, json: options.json });
4558
+ program.command("add").description("Add components, hooks, blocks, or utilities to your project").argument("[items...]", "names of registry items to add").option("--overwrite", "overwrite existing files", false).option("--category <name>", "install all items from a category").option("--block", "install blocks instead of components").option("--target <platform>", "target platform: react (default) or flutter", "react").option("--dry-run", "preview what would be added without writing files").option("--json", "output structured JSON (for AI agents)").action((items, options) => {
4559
+ const target = options.target === "flutter" ? "flutter" : "react";
4560
+ addCommand(items, process.cwd(), { overwrite: options.overwrite, category: options.category, block: options.block, target, dryRun: options.dryRun, json: options.json });
2500
4561
  });
2501
4562
  program.command("diff").description(
2502
4563
  "Show differences between local files and the registry"
2503
- ).argument("[component]", "component name to diff (all if omitted)").option("--json", "output structured JSON (for AI agents)").action((component, options) => {
4564
+ ).argument("[component]", "component name to diff (all installed if omitted)").option("--all", "check all registry components for upstream changes").option("--json", "output structured JSON (for AI agents)").action((component, options) => {
2504
4565
  diffCommand(component, process.cwd(), options);
2505
4566
  });
4567
+ program.command("info").description("Show detailed metadata for a component, hook, block, or pattern").argument("<component>", "Name of the component to look up").option("--json", "Output as JSON").action(async (component, options) => {
4568
+ await infoCommand(component, process.cwd(), options);
4569
+ });
4570
+ program.command("doctor").description("Run diagnostics on a Visor installation").option("--json", "Output as JSON (for AI agents)").action(async (options) => {
4571
+ await doctorCommand(process.cwd(), options, program.version() ?? "0.0.0");
4572
+ });
2506
4573
  var theme = program.command("theme").description("Theme management commands");
2507
4574
  theme.command("apply").description(
2508
- "Read a .visor.yaml file and generate full CSS token overrides"
2509
- ).argument("<file>", "path to .visor.yaml file").option("-o, --output <path>", "output CSS file path").option("--json", "output structured JSON (for AI agents)").option("--adapter <name>", "target adapter: nextjs, fumadocs, deck").action(
4575
+ "Read a .visor.yaml file and generate token overrides (CSS or Flutter)"
4576
+ ).argument("<file>", "path to .visor.yaml file").option(
4577
+ "-o, --output <path>",
4578
+ "output CSS file path (or directory for --adapter flutter)"
4579
+ ).option("--json", "output structured JSON (for AI agents)").option(
4580
+ "--adapter <name>",
4581
+ "target adapter: nextjs, fumadocs, deck, docs, flutter"
4582
+ ).option(
4583
+ "--package-name <name>",
4584
+ "(flutter) Dart package name for generated pubspec.yaml (default: ui)"
4585
+ ).option(
4586
+ "--tokens-only",
4587
+ "(flutter) emit only token files \u2014 skip pubspec.yaml and theme scaffolding"
4588
+ ).option(
4589
+ "--light-only",
4590
+ "(flutter) emit only the light-brightness theme getter"
4591
+ ).option(
4592
+ "--dark-only",
4593
+ "(flutter) emit only the dark-brightness theme getter"
4594
+ ).option(
4595
+ "--theme-class-name <name>",
4596
+ "(flutter) class name for generated theme (default: VisorAppTheme)"
4597
+ ).action(
2510
4598
  (file, options) => {
2511
4599
  themeApplyCommand(file, process.cwd(), {
2512
4600
  ...options,
@@ -2550,8 +4638,8 @@ theme.command("register").description(
2550
4638
  theme.command("unregister").description(
2551
4639
  "Remove a theme from the docs site \u2014 deletes CSS file, removes globals.css import and theme-config.ts entry"
2552
4640
  ).argument("<slug>", "theme slug to unregister (e.g. entr, kaiah)").option("--json", "output structured JSON (for AI agents)").action(
2553
- (slug, options) => {
2554
- themeUnregisterCommand(slug, process.cwd(), options);
4641
+ (slug2, options) => {
4642
+ themeUnregisterCommand(slug2, process.cwd(), options);
2555
4643
  }
2556
4644
  );
2557
4645
  theme.command("sync").description(
@@ -2561,10 +4649,31 @@ theme.command("sync").description(
2561
4649
  themeSyncCommand(process.cwd(), options);
2562
4650
  }
2563
4651
  );
4652
+ theme.command("batch-apply-flutter").description(
4653
+ "Batch-generate Dart ThemeData for all .visor.yaml themes (themes/ + custom-themes/) into packages/visor_themes/"
4654
+ ).option("--dry-run", "show what would be generated without writing files").option("--json", "output structured JSON (for AI agents)").action(
4655
+ (options) => {
4656
+ themeBatchApplyFlutterCommand(process.cwd(), options);
4657
+ }
4658
+ );
2564
4659
  var fonts = program.command("fonts").description("Font library management commands");
2565
4660
  fonts.command("add").description("Upload woff2 font files to the Visor Font Library on R2").argument("<path>", "path to a .woff2 file or directory containing .woff2 files").requiredOption("--org <org>", "organization namespace (e.g. low-orbit)").option("--family <name>", "font family slug (auto-inferred from filename if omitted)").option("--json", "output structured JSON (for AI agents)").action(
2566
- (path, options) => {
2567
- fontsAddCommand(path, options);
4661
+ (path2, options) => {
4662
+ fontsAddCommand(path2, options);
2568
4663
  }
2569
4664
  );
4665
+ var pattern = program.command("pattern").description("Work with composition patterns");
4666
+ pattern.command("list").description("List all composition patterns").option("--json", "Output as JSON").action((options) => {
4667
+ patternListCommand(process.cwd(), options);
4668
+ });
4669
+ pattern.command("info").argument("<name>", "Pattern name").description("Show full details for a composition pattern").option("--json", "Output as JSON").action((name, options) => {
4670
+ patternInfoCommand(name, process.cwd(), options);
4671
+ });
4672
+ program.command("suggest").description("Suggest components, blocks, and patterns for a use case").requiredOption("--for <useCase>", 'Use case description (e.g. "dropdown with search")').option("--json", "Output as JSON").action(async (options) => {
4673
+ await suggestCommand(process.cwd(), options);
4674
+ });
4675
+ var tokens = program.command("tokens").description("Explore design tokens");
4676
+ tokens.command("list").description("List all design tokens").option("--json", "output as JSON (for AI agents)").option("--category <category>", "filter by tier: primitives, semantic, adaptive").action(async (options) => {
4677
+ await tokensListCommand(process.cwd(), options);
4678
+ });
2570
4679
  program.parse();