@loworbitstudio/visor 0.1.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,15 +1,535 @@
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
+ for (const item of items) {
65
+ if (item.dependencies) {
66
+ for (const dep of item.dependencies) {
67
+ deps.add(dep);
68
+ }
69
+ }
70
+ if (item.devDependencies) {
71
+ for (const dep of item.devDependencies) {
72
+ devDeps.add(dep);
73
+ }
74
+ }
75
+ }
76
+ return {
77
+ dependencies: Array.from(deps).sort(),
78
+ devDependencies: Array.from(devDeps).sort()
79
+ };
80
+ }
81
+
82
+ // src/check/catalog.ts
83
+ var STOP_WORDS = /* @__PURE__ */ new Set([
84
+ "a",
85
+ "an",
86
+ "the",
87
+ "with",
88
+ "for",
89
+ "and",
90
+ "or",
91
+ "to",
92
+ "in",
93
+ "of",
94
+ "is",
95
+ "that",
96
+ "this",
97
+ "it",
98
+ "as",
99
+ "at",
100
+ "by",
101
+ "on",
102
+ "be",
103
+ "are",
104
+ "was",
105
+ "were"
106
+ ]);
107
+ function toKebab(s) {
108
+ return s.replace(/([A-Z])/g, (m) => `-${m.toLowerCase()}`).replace(/^-/, "").toLowerCase();
109
+ }
110
+ function tokenize(text) {
111
+ return text.toLowerCase().split(/[\s\-_,]+/).filter((t) => t.length > 1 && !STOP_WORDS.has(t));
112
+ }
113
+ function installCmd(name, type) {
114
+ if (type === "block") return `npx visor add ${name} --block`;
115
+ if (type === "pattern") return null;
116
+ return `npx visor add ${name}`;
117
+ }
118
+ function getAllCatalogItems(manifest) {
119
+ const items = [];
120
+ for (const [name, c] of Object.entries(manifest.components)) {
121
+ items.push({ type: "component", name, category: c.category, description: c.description });
122
+ for (const sub of c.sub_components ?? []) {
123
+ items.push({ type: "component", name: toKebab(sub.name), category: c.category, description: sub.description });
124
+ }
125
+ }
126
+ for (const [name, b] of Object.entries(manifest.blocks)) {
127
+ items.push({ type: "block", name, category: b.category, description: b.description });
128
+ }
129
+ for (const [name, h] of Object.entries(manifest.hooks)) {
130
+ items.push({ type: "hook", name, description: h.description });
131
+ }
132
+ for (const [name, p] of Object.entries(manifest.patterns)) {
133
+ items.push({ type: "pattern", name, description: p.description });
134
+ }
135
+ return items;
136
+ }
137
+ function findByName(manifest, pattern2) {
138
+ const normalized = toKebab(pattern2);
139
+ if (normalized in manifest.components) {
140
+ const c = manifest.components[normalized];
141
+ return { found: true, name: normalized, type: "component", category: c.category, description: c.description, installCmd: `npx visor add ${normalized}` };
142
+ }
143
+ if (normalized in manifest.blocks) {
144
+ const b = manifest.blocks[normalized];
145
+ return { found: true, name: normalized, type: "block", category: b.category, description: b.description, installCmd: `npx visor add ${normalized} --block` };
146
+ }
147
+ if (normalized in manifest.hooks) {
148
+ const h = manifest.hooks[normalized];
149
+ return { found: true, name: normalized, type: "hook", description: h.description, installCmd: `npx visor add ${normalized}` };
150
+ }
151
+ if (normalized in manifest.patterns) {
152
+ const p = manifest.patterns[normalized];
153
+ return { found: true, name: normalized, type: "pattern", description: p.description, installCmd: null };
154
+ }
155
+ for (const [parentName, c] of Object.entries(manifest.components)) {
156
+ for (const sub of c.sub_components ?? []) {
157
+ if (toKebab(sub.name) === normalized) {
158
+ return { found: true, name: toKebab(sub.name), type: "component", category: c.category, description: sub.description, installCmd: `npx visor add ${parentName}` };
159
+ }
160
+ }
161
+ }
162
+ return { found: false };
163
+ }
164
+ function fuzzyFind(manifest, query, limit = 5) {
165
+ const queryTokens = tokenize(query);
166
+ if (queryTokens.length === 0) return [];
167
+ const results = [];
168
+ for (const item of getAllCatalogItems(manifest)) {
169
+ const searchText = [item.name, item.description].join(" ").toLowerCase();
170
+ const matched = queryTokens.filter((t) => searchText.includes(t));
171
+ if (matched.length > 0) {
172
+ results.push({
173
+ name: item.name,
174
+ type: item.type,
175
+ category: item.category,
176
+ description: item.description,
177
+ score: matched.length,
178
+ matchReason: `Matched: ${matched.join(", ")}`,
179
+ installCmd: installCmd(item.name, item.type)
180
+ });
181
+ }
182
+ }
183
+ return results.sort((a, b) => b.score - a.score).slice(0, limit);
184
+ }
185
+
186
+ // src/check/jsx-scan.ts
187
+ import { readFileSync as readFileSync2, readdirSync, statSync } from "fs";
188
+ import { resolve, extname, join as join2 } from "path";
189
+
190
+ // src/check/native-map.ts
191
+ var NATIVE_TO_VISOR = {
192
+ button: { visorName: "button", displayName: "Button" },
193
+ textarea: { visorName: "textarea", displayName: "Textarea" },
194
+ form: { visorName: "form", displayName: "Form" },
195
+ label: { visorName: "label", displayName: "Label" },
196
+ fieldset: { visorName: "fieldset", displayName: "Fieldset" },
197
+ select: { visorName: "select", displayName: "Select" },
198
+ table: { visorName: "table", displayName: "Table", notes: "use DataTable for full interactive features" },
199
+ img: { visorName: "image", displayName: "Image" },
200
+ dialog: { visorName: "dialog", displayName: "Dialog" },
201
+ details: { visorName: "accordion", displayName: "Accordion", notes: "or Collapsible for a single section" },
202
+ summary: { visorName: "accordion", displayName: "Accordion", notes: "wrap as Accordion trigger" }
203
+ };
204
+ var INPUT_TYPE_MAP = {
205
+ number: { visorName: "number-input", displayName: "NumberInput" },
206
+ password: { visorName: "password-input", displayName: "PasswordInput" },
207
+ search: { visorName: "search-input", displayName: "SearchInput" },
208
+ tel: { visorName: "phone-input", displayName: "PhoneInput" },
209
+ phone: { visorName: "phone-input", displayName: "PhoneInput" },
210
+ // All other input types (text, email, url, date, etc.) map to the base Input.
211
+ _default: { visorName: "input", displayName: "Input" }
212
+ };
213
+
214
+ // src/check/jsx-scan.ts
215
+ async function getBabelParser() {
216
+ const { parse } = await import("@babel/parser");
217
+ return parse;
218
+ }
219
+ function walk(node, visit) {
220
+ if (!node || typeof node !== "object") return;
221
+ const obj = node;
222
+ visit(obj);
223
+ for (const key of Object.keys(obj)) {
224
+ if (key === "parent" || key === "tokens" || key === "errors") continue;
225
+ const val = obj[key];
226
+ if (Array.isArray(val)) {
227
+ for (const child of val) walk(child, visit);
228
+ } else if (val && typeof val === "object" && "type" in val) {
229
+ walk(val, visit);
230
+ }
231
+ }
232
+ }
233
+ function getInputType(attribs) {
234
+ for (const attr of attribs) {
235
+ const a = attr;
236
+ if (a.type !== "JSXAttribute") continue;
237
+ const nameNode = a.name;
238
+ if (nameNode?.name !== "type") continue;
239
+ const valueNode = a.value;
240
+ if (!valueNode) continue;
241
+ if (valueNode.type === "StringLiteral") {
242
+ return String(valueNode.value ?? "text");
243
+ }
244
+ if (valueNode.type === "JSXExpressionContainer") {
245
+ const expr = valueNode.expression;
246
+ if (expr?.type === "StringLiteral") return String(expr.value ?? "text");
247
+ }
248
+ }
249
+ return "_default";
250
+ }
251
+ function collectJsxFindings(source, filePath, parse) {
252
+ let ast;
253
+ try {
254
+ ast = parse(source, {
255
+ sourceType: "module",
256
+ plugins: ["jsx", "typescript"],
257
+ errorRecovery: true
258
+ });
259
+ } catch {
260
+ return [];
261
+ }
262
+ const findings = [];
263
+ walk(ast, (node) => {
264
+ if (node.type !== "JSXOpeningElement") return;
265
+ const nameNode = node.name;
266
+ if (!nameNode) return;
267
+ const tagName = nameNode.type === "JSXIdentifier" ? String(nameNode.name ?? "") : "";
268
+ if (!tagName || tagName[0] !== tagName[0].toLowerCase()) return;
269
+ const loc = node.loc;
270
+ const line = loc?.start?.line ?? 0;
271
+ const column = loc?.start?.column ?? 0;
272
+ if (tagName === "input") {
273
+ const attribs = node.attributes ?? [];
274
+ const typeVal = getInputType(attribs);
275
+ const mapping2 = INPUT_TYPE_MAP[typeVal] ?? INPUT_TYPE_MAP["_default"];
276
+ findings.push({
277
+ file: filePath,
278
+ line,
279
+ column,
280
+ nativeTag: typeVal !== "_default" ? `input[type=${typeVal}]` : "input",
281
+ suggestedPrimitive: mapping2.displayName,
282
+ installCmd: `npx visor add ${mapping2.visorName}`
283
+ });
284
+ return;
285
+ }
286
+ const mapping = NATIVE_TO_VISOR[tagName];
287
+ if (!mapping) return;
288
+ const finding = {
289
+ file: filePath,
290
+ line,
291
+ column,
292
+ nativeTag: tagName,
293
+ suggestedPrimitive: mapping.displayName,
294
+ installCmd: `npx visor add ${mapping.visorName}`
295
+ };
296
+ if (mapping.notes) finding.rationale = mapping.notes;
297
+ findings.push(finding);
298
+ });
299
+ return findings;
300
+ }
301
+ function collectFiles(pathArg) {
302
+ const JSX_EXTS = /* @__PURE__ */ new Set([".jsx", ".tsx", ".js", ".ts"]);
303
+ try {
304
+ const s = statSync(pathArg);
305
+ if (s.isDirectory()) {
306
+ let recurse2 = function(dir) {
307
+ for (const entry of readdirSync(dir)) {
308
+ if (entry.startsWith(".") || entry === "node_modules") continue;
309
+ const full = join2(dir, entry);
310
+ const es = statSync(full);
311
+ if (es.isDirectory()) recurse2(full);
312
+ else if (JSX_EXTS.has(extname(full))) files.push(full);
313
+ }
314
+ };
315
+ var recurse = recurse2;
316
+ const files = [];
317
+ recurse2(pathArg);
318
+ return files;
319
+ }
320
+ if (JSX_EXTS.has(extname(pathArg))) return [pathArg];
321
+ } catch {
322
+ }
323
+ return [];
324
+ }
325
+ async function scanJsx(pathArg) {
326
+ const parse = await getBabelParser();
327
+ let files;
328
+ let stdinMode = false;
329
+ if (pathArg === "-") {
330
+ stdinMode = true;
331
+ files = ["<stdin>"];
332
+ } else {
333
+ files = collectFiles(resolve(pathArg));
334
+ }
335
+ const allFindings = [];
336
+ for (const file of files) {
337
+ let source;
338
+ if (stdinMode) {
339
+ source = readFileSync2(0, "utf-8");
340
+ } else {
341
+ try {
342
+ source = readFileSync2(file, "utf-8");
343
+ } catch {
344
+ continue;
345
+ }
346
+ }
347
+ allFindings.push(...collectJsxFindings(source, stdinMode ? "<stdin>" : file, parse));
348
+ }
349
+ return {
350
+ findings: allFindings,
351
+ summary: { scanned: files.length, hits: allFindings.length }
352
+ };
353
+ }
354
+
355
+ // src/utils/logger.ts
356
+ import pc from "picocolors";
357
+ var logger = {
358
+ info(message) {
359
+ console.log(message);
360
+ },
361
+ success(message) {
362
+ console.log(pc.green(`\u2713 ${message}`));
363
+ },
364
+ warn(message) {
365
+ console.log(pc.yellow(`\u26A0 ${message}`));
366
+ },
367
+ error(message) {
368
+ console.error(pc.red(`\u2717 ${message}`));
369
+ },
370
+ item(message) {
371
+ console.log(pc.dim(` ${message}`));
372
+ },
373
+ heading(message) {
374
+ console.log(pc.bold(message));
375
+ },
376
+ blank() {
377
+ console.log();
378
+ }
379
+ };
380
+
381
+ // src/commands/check.ts
382
+ var TYPE_FILTER = {
383
+ ui: "component",
384
+ blocks: "block",
385
+ hooks: "hook",
386
+ patterns: "pattern"
387
+ };
388
+ function checkListCommand(options) {
389
+ const manifest = loadManifest();
390
+ let items = getAllCatalogItems(manifest);
391
+ if (options.type && options.type !== "all") {
392
+ const filterType = TYPE_FILTER[options.type];
393
+ items = items.filter((i) => i.type === filterType);
394
+ }
395
+ const byType = {};
396
+ for (const item of items) {
397
+ byType[item.type] = (byType[item.type] ?? 0) + 1;
398
+ }
399
+ if (options.json) {
400
+ console.log(
401
+ JSON.stringify(
402
+ {
403
+ success: true,
404
+ items: items.map((i) => ({ type: i.type, name: i.name, category: i.category ?? null, description: i.description })),
405
+ summary: { total: items.length, byType }
406
+ },
407
+ null,
408
+ 2
409
+ )
410
+ );
411
+ process.exit(0);
412
+ return;
413
+ }
414
+ const groups = /* @__PURE__ */ new Map();
415
+ for (const item of items) {
416
+ const key = item.type;
417
+ if (!groups.has(key)) groups.set(key, []);
418
+ groups.get(key).push(item);
419
+ }
420
+ for (const [type, group] of groups) {
421
+ logger.heading(`${type}s (${group.length})`);
422
+ logger.blank();
423
+ for (const item of group) {
424
+ logger.info(` ${item.name.padEnd(28)} ${item.description}`);
425
+ }
426
+ logger.blank();
427
+ }
428
+ }
429
+ function checkHasCommand(pattern2, options) {
430
+ const manifest = loadManifest();
431
+ if (options.fuzzy) {
432
+ const results = fuzzyFind(manifest, pattern2, 5);
433
+ if (results.length === 0) {
434
+ if (options.json) {
435
+ console.log(JSON.stringify({ success: false, found: false, query: pattern2, results: [] }, null, 2));
436
+ } else {
437
+ logger.warn(`No fuzzy matches for "${pattern2}"`);
438
+ }
439
+ process.exit(1);
440
+ return;
441
+ }
442
+ if (options.json) {
443
+ console.log(JSON.stringify({ success: true, found: true, query: pattern2, results }, null, 2));
444
+ process.exit(0);
445
+ return;
446
+ }
447
+ logger.heading(`Fuzzy matches for "${pattern2}":`);
448
+ logger.blank();
449
+ for (const r of results) {
450
+ const cmd2 = r.installCmd ? ` \u2014 ${r.installCmd}` : "";
451
+ logger.info(` ${r.name} [${r.type}]${cmd2}`);
452
+ logger.info(` ${r.description.slice(0, 80)}`);
453
+ }
454
+ return;
455
+ }
456
+ const result = findByName(manifest, pattern2);
457
+ if (!result.found) {
458
+ if (options.json) {
459
+ console.log(JSON.stringify({ success: false, found: false, query: pattern2 }, null, 2));
460
+ } else {
461
+ logger.warn(`"${pattern2}" not found in Visor catalog. Try --fuzzy for partial matches.`);
462
+ }
463
+ process.exit(1);
464
+ return;
465
+ }
466
+ if (options.json) {
467
+ console.log(
468
+ JSON.stringify(
469
+ {
470
+ success: true,
471
+ found: true,
472
+ name: result.name,
473
+ type: result.type,
474
+ category: result.category ?? null,
475
+ description: result.description,
476
+ installCmd: result.installCmd
477
+ },
478
+ null,
479
+ 2
480
+ )
481
+ );
482
+ process.exit(0);
483
+ return;
484
+ }
485
+ const cmd = result.installCmd ? ` \u2014 ${result.installCmd}` : "";
486
+ logger.success(`${result.name} [${result.type}]${cmd}`);
487
+ logger.info(` ${result.description}`);
488
+ }
489
+ async function checkDiffCommand(pathArg, options) {
490
+ const result = await scanJsx(pathArg);
491
+ if (options.json) {
492
+ console.log(JSON.stringify({ success: true, ...result }, null, 2));
493
+ if (options.failOnHits && result.summary.hits > 0) process.exit(1);
494
+ process.exit(0);
495
+ return;
496
+ }
497
+ if (result.summary.hits === 0) {
498
+ logger.success(`No native HTML primitives found \u2014 ${result.summary.scanned} file(s) scanned.`);
499
+ return;
500
+ }
501
+ logger.heading(`Found ${result.summary.hits} native HTML usage(s) in ${result.summary.scanned} file(s):
502
+ `);
503
+ for (const f of result.findings) {
504
+ const loc = `${f.file}:${f.line}:${f.column}`;
505
+ const note = f.rationale ? ` (${f.rationale})` : "";
506
+ logger.warn(` <${f.nativeTag}> \u2192 use <${f.suggestedPrimitive}>${note}`);
507
+ logger.item(` ${loc} ${f.installCmd}`);
508
+ }
509
+ logger.blank();
510
+ if (options.failOnHits) process.exit(1);
511
+ }
512
+ function checkCommand() {
513
+ const check = new Command("check").description("Check Visor catalog \u2014 list items, test existence, scan JSX for native HTML");
514
+ 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) => {
515
+ checkListCommand(options);
516
+ });
517
+ 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) => {
518
+ checkHasCommand(pattern2, options);
519
+ });
520
+ 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) => {
521
+ await checkDiffCommand(pathArg, options);
522
+ });
523
+ return check;
524
+ }
525
+
6
526
  // src/commands/init.ts
7
527
  import { existsSync as existsSync3, writeFileSync as writeFileSync2, mkdirSync } from "fs";
8
- import { join as join3, dirname } from "path";
528
+ import { join as join5, dirname as dirname2 } from "path";
9
529
 
10
530
  // src/config/config.ts
11
- import { readFileSync, writeFileSync, existsSync } from "fs";
12
- import { join } from "path";
531
+ import { readFileSync as readFileSync3, writeFileSync, existsSync } from "fs";
532
+ import { join as join3 } from "path";
13
533
 
14
534
  // src/config/defaults.ts
15
535
  var DEFAULT_CONFIG = {
@@ -25,7 +545,7 @@ var CONFIG_FILE = "visor.json";
25
545
 
26
546
  // src/config/config.ts
27
547
  function getConfigPath(cwd) {
28
- return join(cwd, CONFIG_FILE);
548
+ return join3(cwd, CONFIG_FILE);
29
549
  }
30
550
  function configExists(cwd) {
31
551
  return existsSync(getConfigPath(cwd));
@@ -37,8 +557,29 @@ function loadConfig(cwd) {
37
557
  `No ${CONFIG_FILE} found. Run "visor init" first.`
38
558
  );
39
559
  }
40
- const raw = readFileSync(configPath, "utf-8");
560
+ const raw = readFileSync3(configPath, "utf-8");
41
561
  const parsed = JSON.parse(raw);
562
+ const knownKeys = /* @__PURE__ */ new Set(["paths"]);
563
+ for (const key of Object.keys(parsed)) {
564
+ if (!knownKeys.has(key)) {
565
+ console.warn(`Warning: unknown key "${key}" in visor.json`);
566
+ }
567
+ }
568
+ if (parsed.paths !== void 0) {
569
+ if (typeof parsed.paths !== "object" || parsed.paths === null || Array.isArray(parsed.paths)) {
570
+ throw new Error(
571
+ `Invalid visor.json: paths must be an object, got ${Array.isArray(parsed.paths) ? "array" : typeof parsed.paths}`
572
+ );
573
+ }
574
+ const paths = parsed.paths;
575
+ for (const [key, value] of Object.entries(paths)) {
576
+ if (typeof value !== "string") {
577
+ throw new Error(
578
+ `Invalid visor.json: paths.${key} must be a string, got ${typeof value}`
579
+ );
580
+ }
581
+ }
582
+ }
42
583
  return {
43
584
  paths: {
44
585
  ...DEFAULT_CONFIG.paths,
@@ -53,12 +594,12 @@ function writeConfig(cwd, config) {
53
594
 
54
595
  // src/utils/packages.ts
55
596
  import { execFileSync } from "child_process";
56
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
57
- import { join as join2 } from "path";
597
+ import { existsSync as existsSync2, readFileSync as readFileSync4 } from "fs";
598
+ import { join as join4 } from "path";
58
599
  function readPackageJson(cwd) {
59
- const pkgPath = join2(cwd, "package.json");
600
+ const pkgPath = join4(cwd, "package.json");
60
601
  if (!existsSync2(pkgPath)) return null;
61
- return JSON.parse(readFileSync2(pkgPath, "utf-8"));
602
+ return JSON.parse(readFileSync4(pkgPath, "utf-8"));
62
603
  }
63
604
  function isPackageInstalled(packageName, cwd) {
64
605
  const pkg = readPackageJson(cwd);
@@ -82,32 +623,6 @@ function installPackages(packages, cwd, dev = false) {
82
623
  }
83
624
  }
84
625
 
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
626
  // src/commands/templates/nextjs.ts
112
627
  var NEXTJS_STARTER_YAML = `name: my-app
113
628
  version: 1
@@ -203,7 +718,7 @@ function scaffoldNextjs(cwd, json, filesCreated, filesSkipped) {
203
718
  logger.blank();
204
719
  logger.info("Scaffolding NextJS theme...");
205
720
  }
206
- const yamlPath = join3(cwd, ".visor.yaml");
721
+ const yamlPath = join5(cwd, ".visor.yaml");
207
722
  if (existsSync3(yamlPath)) {
208
723
  filesSkipped.push(".visor.yaml");
209
724
  if (!json) {
@@ -222,8 +737,8 @@ function scaffoldNextjs(cwd, json, filesCreated, filesSkipped) {
222
737
  tokens: data.tokens,
223
738
  config: data.config
224
739
  });
225
- const globalsPath = join3(cwd, "app", "globals.css");
226
- const globalsDir = dirname(globalsPath);
740
+ const globalsPath = join5(cwd, "app", "globals.css");
741
+ const globalsDir = dirname2(globalsPath);
227
742
  if (existsSync3(globalsPath)) {
228
743
  filesSkipped.push("app/globals.css");
229
744
  if (!json) {
@@ -246,95 +761,37 @@ function scaffoldNextjs(cwd, json, filesCreated, filesSkipped) {
246
761
  }
247
762
  }
248
763
 
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;
261
- }
262
- function findItem(registry, name) {
263
- return registry.items.find((item) => item.name === name);
264
- }
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
- }
283
- }
284
- return Array.from(resolved.values());
285
- }
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);
298
- }
299
- }
300
- }
301
- return {
302
- dependencies: Array.from(deps).sort(),
303
- devDependencies: Array.from(devDeps).sort()
304
- };
305
- }
306
-
307
764
  // src/utils/fs.ts
308
765
  import {
309
766
  writeFileSync as writeFileSync3,
310
- readFileSync as readFileSync4,
767
+ readFileSync as readFileSync5,
311
768
  existsSync as existsSync4,
312
769
  mkdirSync as mkdirSync2
313
770
  } from "fs";
314
- import { dirname as dirname3, join as join5 } from "path";
771
+ import { dirname as dirname3, join as join6 } from "path";
315
772
  function resolveOutputPath(registryPath, type, config, cwd) {
316
773
  let relativePath;
317
774
  if (type === "registry:block") {
318
775
  relativePath = registryPath.replace(/^blocks\//, "");
319
- return join5(cwd, config.paths.blocks, relativePath);
776
+ return join6(cwd, config.paths.blocks, relativePath);
320
777
  }
321
778
  if (type === "registry:ui") {
322
779
  if (registryPath.startsWith("components/deck/")) {
323
780
  relativePath = registryPath.replace(/^components\/deck\//, "");
324
- return join5(cwd, config.paths.deckComponents, relativePath);
781
+ return join6(cwd, config.paths.deckComponents, relativePath);
325
782
  }
326
783
  relativePath = registryPath.replace(/^components\/ui\//, "");
327
- return join5(cwd, config.paths.components, relativePath);
784
+ return join6(cwd, config.paths.components, relativePath);
328
785
  }
329
786
  if (type === "registry:hook") {
330
787
  relativePath = registryPath.replace(/^hooks\//, "");
331
- return join5(cwd, config.paths.hooks, relativePath);
788
+ return join6(cwd, config.paths.hooks, relativePath);
332
789
  }
333
790
  if (type === "registry:lib") {
334
791
  relativePath = registryPath.replace(/^lib\//, "");
335
- return join5(cwd, config.paths.lib, relativePath);
792
+ return join6(cwd, config.paths.lib, relativePath);
336
793
  }
337
- return join5(cwd, registryPath);
794
+ return join6(cwd, registryPath);
338
795
  }
339
796
  function writeFile(filePath, content) {
340
797
  const dir = dirname3(filePath);
@@ -345,7 +802,7 @@ function writeFile(filePath, content) {
345
802
  }
346
803
  function readFile(filePath) {
347
804
  if (!existsSync4(filePath)) return null;
348
- return readFileSync4(filePath, "utf-8");
805
+ return readFileSync5(filePath, "utf-8");
349
806
  }
350
807
  function fileExists(filePath) {
351
808
  return existsSync4(filePath);
@@ -491,6 +948,8 @@ function listCommand(cwd, options = {}) {
491
948
  // src/commands/add.ts
492
949
  function addCommand(components, cwd, options = {}) {
493
950
  const json = options.json ?? false;
951
+ const dryRun = options.dryRun ?? false;
952
+ const prefix = dryRun ? "[dry-run] " : "";
494
953
  let autoInitialized = false;
495
954
  if (!configExists(cwd)) {
496
955
  writeConfig(cwd, DEFAULT_CONFIG);
@@ -618,9 +1077,12 @@ function addCommand(components, cwd, options = {}) {
618
1077
  }
619
1078
  process.exit(1);
620
1079
  }
1080
+ const circularWarnings = [];
621
1081
  let items;
622
1082
  try {
623
- items = resolveTransitiveDeps(registry, itemNames);
1083
+ items = resolveTransitiveDeps(registry, itemNames, (msg) => {
1084
+ circularWarnings.push(msg);
1085
+ });
624
1086
  } catch (error) {
625
1087
  if (json) {
626
1088
  const message = error instanceof Error ? error.message : String(error);
@@ -629,6 +1091,11 @@ function addCommand(components, cwd, options = {}) {
629
1091
  }
630
1092
  throw error;
631
1093
  }
1094
+ if (circularWarnings.length > 0 && !json) {
1095
+ for (const warning of circularWarnings) {
1096
+ logger.warn(warning);
1097
+ }
1098
+ }
632
1099
  if (!json) {
633
1100
  logger.info(
634
1101
  `Resolving ${itemNames.length} item(s) \u2192 ${items.length} total (with dependencies)`
@@ -645,16 +1112,18 @@ function addCommand(components, cwd, options = {}) {
645
1112
  config,
646
1113
  cwd
647
1114
  );
648
- if (fileExists(outputPath) && !options.overwrite) {
1115
+ if (!dryRun && fileExists(outputPath) && !options.overwrite) {
649
1116
  if (!json) {
650
- logger.item(`skip ${file.path} (already exists)`);
1117
+ logger.item(`${prefix}skip ${file.path} (already exists)`);
651
1118
  }
652
1119
  skippedFiles.push(file.path);
653
1120
  continue;
654
1121
  }
655
- writeFile(outputPath, file.content);
1122
+ if (!dryRun) {
1123
+ writeFile(outputPath, file.content);
1124
+ }
656
1125
  if (!json) {
657
- logger.success(file.path);
1126
+ logger.success(`${prefix}${file.path}`);
658
1127
  }
659
1128
  writtenFiles.push(file.path);
660
1129
  }
@@ -662,41 +1131,57 @@ function addCommand(components, cwd, options = {}) {
662
1131
  if (!json) {
663
1132
  logger.blank();
664
1133
  logger.info(
665
- `Files: ${writtenFiles.length} written, ${skippedFiles.length} skipped`
1134
+ `${prefix}Files: ${writtenFiles.length} written, ${skippedFiles.length} skipped`
666
1135
  );
667
1136
  }
668
1137
  const { dependencies, devDependencies } = collectDependencies(items);
669
- const uninstalledDeps = getUninstalledDeps(dependencies, cwd);
670
- const uninstalledDevDeps = getUninstalledDeps(devDependencies, cwd);
1138
+ const uninstalledDeps = dryRun ? dependencies : getUninstalledDeps(dependencies, cwd);
1139
+ const uninstalledDevDeps = dryRun ? devDependencies : getUninstalledDeps(devDependencies, cwd);
671
1140
  const installedDeps = [];
672
1141
  const failedDeps = [];
673
1142
  if (uninstalledDeps.length > 0) {
674
- if (!json) {
675
- logger.blank();
676
- logger.info("Installing dependencies...");
677
- }
678
- if (installPackages(uninstalledDeps, cwd)) {
1143
+ if (dryRun) {
1144
+ if (!json) {
1145
+ logger.blank();
1146
+ logger.info(`${prefix}Would install dependencies: ${uninstalledDeps.join(", ")}`);
1147
+ }
679
1148
  installedDeps.push(...uninstalledDeps);
680
1149
  } else {
681
- failedDeps.push(...uninstalledDeps);
682
1150
  if (!json) {
683
- logger.warn("Some dependencies failed to install. Install them manually:");
684
- logger.info(` npm install ${uninstalledDeps.join(" ")}`);
1151
+ logger.blank();
1152
+ logger.info("Installing dependencies...");
1153
+ }
1154
+ if (installPackages(uninstalledDeps, cwd)) {
1155
+ installedDeps.push(...uninstalledDeps);
1156
+ } else {
1157
+ failedDeps.push(...uninstalledDeps);
1158
+ if (!json) {
1159
+ logger.warn("Some dependencies failed to install. Install them manually:");
1160
+ logger.info(` npm install ${uninstalledDeps.join(" ")}`);
1161
+ }
685
1162
  }
686
1163
  }
687
1164
  }
688
1165
  if (uninstalledDevDeps.length > 0) {
689
- if (!json) {
690
- logger.blank();
691
- logger.info("Installing dev dependencies...");
692
- }
693
- if (installPackages(uninstalledDevDeps, cwd, true)) {
1166
+ if (dryRun) {
1167
+ if (!json) {
1168
+ logger.blank();
1169
+ logger.info(`${prefix}Would install dev dependencies: ${uninstalledDevDeps.join(", ")}`);
1170
+ }
694
1171
  installedDeps.push(...uninstalledDevDeps);
695
1172
  } else {
696
- failedDeps.push(...uninstalledDevDeps);
697
1173
  if (!json) {
698
- logger.warn("Some dev dependencies failed to install. Install them manually:");
699
- logger.info(` npm install --save-dev ${uninstalledDevDeps.join(" ")}`);
1174
+ logger.blank();
1175
+ logger.info("Installing dev dependencies...");
1176
+ }
1177
+ if (installPackages(uninstalledDevDeps, cwd, true)) {
1178
+ installedDeps.push(...uninstalledDevDeps);
1179
+ } else {
1180
+ failedDeps.push(...uninstalledDevDeps);
1181
+ if (!json) {
1182
+ logger.warn("Some dev dependencies failed to install. Install them manually:");
1183
+ logger.info(` npm install --save-dev ${uninstalledDevDeps.join(" ")}`);
1184
+ }
700
1185
  }
701
1186
  }
702
1187
  }
@@ -715,7 +1200,8 @@ function addCommand(components, cwd, options = {}) {
715
1200
  console.log(
716
1201
  JSON.stringify(
717
1202
  {
718
- success: true,
1203
+ success: failedDeps.length === 0,
1204
+ ...dryRun ? { dryRun: true } : {},
719
1205
  autoInitialized,
720
1206
  requested: itemNames,
721
1207
  resolved: items.map((i) => i.name),
@@ -727,7 +1213,10 @@ function addCommand(components, cwd, options = {}) {
727
1213
  2
728
1214
  )
729
1215
  );
730
- process.exit(0);
1216
+ process.exit(failedDeps.length > 0 ? 1 : 0);
1217
+ }
1218
+ if (failedDeps.length > 0) {
1219
+ process.exit(1);
731
1220
  }
732
1221
  }
733
1222
 
@@ -750,10 +1239,15 @@ function hasDifferences(localContent, registryContent) {
750
1239
  // src/commands/diff.ts
751
1240
  function diffCommand(componentName, cwd, options = {}) {
752
1241
  const json = options.json ?? false;
1242
+ const all = options.all ?? false;
753
1243
  let config;
754
1244
  let registry;
755
1245
  try {
756
- config = loadConfig(cwd);
1246
+ if (all && !configExists(cwd)) {
1247
+ config = DEFAULT_CONFIG;
1248
+ } else {
1249
+ config = loadConfig(cwd);
1250
+ }
757
1251
  registry = loadRegistry();
758
1252
  } catch (error) {
759
1253
  if (json) {
@@ -763,6 +1257,79 @@ function diffCommand(componentName, cwd, options = {}) {
763
1257
  }
764
1258
  throw error;
765
1259
  }
1260
+ if (all) {
1261
+ const results = [];
1262
+ for (const item of registry.items) {
1263
+ const changedFiles = [];
1264
+ let hasModified = false;
1265
+ let hasAdded = false;
1266
+ for (const file of item.files) {
1267
+ const outputPath = resolveOutputPath(
1268
+ file.path,
1269
+ file.type,
1270
+ config,
1271
+ cwd
1272
+ );
1273
+ const localContent = readFile(outputPath);
1274
+ if (localContent === null) {
1275
+ hasAdded = true;
1276
+ changedFiles.push(file.path);
1277
+ continue;
1278
+ }
1279
+ if (hasDifferences(localContent, file.content)) {
1280
+ hasModified = true;
1281
+ changedFiles.push(file.path);
1282
+ }
1283
+ }
1284
+ let changeType;
1285
+ if (hasModified) {
1286
+ changeType = "modified";
1287
+ } else if (hasAdded && changedFiles.length === item.files.length) {
1288
+ changeType = "added";
1289
+ } else if (hasAdded) {
1290
+ changeType = "modified";
1291
+ } else {
1292
+ changeType = "unchanged";
1293
+ }
1294
+ results.push({
1295
+ component: item.name,
1296
+ changeType,
1297
+ files: changedFiles,
1298
+ breakingChange: false,
1299
+ migrationNote: null
1300
+ });
1301
+ }
1302
+ const total = results.length;
1303
+ const changed = results.filter((r) => r.changeType !== "unchanged").length;
1304
+ const unchanged = total - changed;
1305
+ if (json) {
1306
+ console.log(
1307
+ JSON.stringify(
1308
+ {
1309
+ success: true,
1310
+ results,
1311
+ summary: { total, changed, unchanged }
1312
+ },
1313
+ null,
1314
+ 2
1315
+ )
1316
+ );
1317
+ process.exit(0);
1318
+ return;
1319
+ }
1320
+ const changedItems = results.filter((r) => r.changeType !== "unchanged");
1321
+ if (changedItems.length === 0) {
1322
+ logger.success(`All ${total} components match the registry.`);
1323
+ } else {
1324
+ logger.heading(`${changed} component(s) with upstream changes`);
1325
+ for (const r of changedItems) {
1326
+ logger.info(` ${r.component} [${r.changeType}]: ${r.files.join(", ")}`);
1327
+ }
1328
+ logger.blank();
1329
+ logger.info(`Total: ${total} | Changed: ${changed} | Unchanged: ${unchanged}`);
1330
+ }
1331
+ return;
1332
+ }
766
1333
  const itemsToDiff = componentName ? (() => {
767
1334
  const item = findItem(registry, componentName);
768
1335
  if (!item) {
@@ -850,9 +1417,87 @@ function diffCommand(componentName, cwd, options = {}) {
850
1417
  }
851
1418
  }
852
1419
 
1420
+ // src/commands/info.ts
1421
+ function infoCommand(name, cwd, options = {}) {
1422
+ const json = options.json ?? false;
1423
+ let manifest;
1424
+ try {
1425
+ manifest = loadManifest();
1426
+ } catch (error) {
1427
+ if (json) {
1428
+ const message = error instanceof Error ? error.message : String(error);
1429
+ console.error(JSON.stringify({ success: false, error: message }, null, 2));
1430
+ process.exit(1);
1431
+ }
1432
+ throw error;
1433
+ }
1434
+ let kind = null;
1435
+ let data = null;
1436
+ if (name in manifest.components) {
1437
+ kind = "component";
1438
+ data = manifest.components[name];
1439
+ } else if (name in manifest.hooks) {
1440
+ kind = "hook";
1441
+ data = manifest.hooks[name];
1442
+ } else if (name in manifest.blocks) {
1443
+ kind = "block";
1444
+ data = manifest.blocks[name];
1445
+ } else if (name in manifest.patterns) {
1446
+ kind = "pattern";
1447
+ data = manifest.patterns[name];
1448
+ }
1449
+ if (kind === null || data === null) {
1450
+ const errorPayload = {
1451
+ success: false,
1452
+ error: `Component ${name} not found. Run visor list --json to see available names.`
1453
+ };
1454
+ if (json) {
1455
+ console.error(JSON.stringify(errorPayload, null, 2));
1456
+ } else {
1457
+ logger.error(errorPayload.error);
1458
+ }
1459
+ process.exit(1);
1460
+ return;
1461
+ }
1462
+ if (json) {
1463
+ console.log(
1464
+ JSON.stringify(
1465
+ {
1466
+ success: true,
1467
+ name,
1468
+ kind,
1469
+ data
1470
+ },
1471
+ null,
1472
+ 2
1473
+ )
1474
+ );
1475
+ process.exit(0);
1476
+ return;
1477
+ }
1478
+ const entry = data;
1479
+ logger.heading(`${name} (${kind})`);
1480
+ logger.blank();
1481
+ if (entry.description) {
1482
+ logger.info(String(entry.description));
1483
+ logger.blank();
1484
+ }
1485
+ if (Array.isArray(entry.when_to_use) && entry.when_to_use.length > 0) {
1486
+ logger.info("When to use:");
1487
+ for (const item of entry.when_to_use) {
1488
+ logger.info(` \u2022 ${item}`);
1489
+ }
1490
+ logger.blank();
1491
+ }
1492
+ if (entry.example) {
1493
+ logger.info("Example:");
1494
+ logger.info(String(entry.example));
1495
+ }
1496
+ }
1497
+
853
1498
  // 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";
1499
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync4, mkdirSync as mkdirSync3 } from "fs";
1500
+ import { resolve as resolve2, dirname as dirname4 } from "path";
856
1501
  import { generateTheme, generateThemeData as generateThemeData2 } from "@loworbitstudio/visor-theme-engine";
857
1502
  import {
858
1503
  nextjsAdapter as nextjsAdapter2,
@@ -879,10 +1524,10 @@ function defaultOutputPath(adapter, themeName) {
879
1524
  }
880
1525
  }
881
1526
  function themeApplyCommand(file, cwd, options) {
882
- const filePath = resolve(cwd, file);
1527
+ const filePath = resolve2(cwd, file);
883
1528
  let yamlContent;
884
1529
  try {
885
- yamlContent = readFileSync5(filePath, "utf-8");
1530
+ yamlContent = readFileSync6(filePath, "utf-8");
886
1531
  } catch {
887
1532
  if (options.json) {
888
1533
  console.log(
@@ -947,7 +1592,7 @@ function themeApplyCommand(file, cwd, options) {
947
1592
  process.exit(1);
948
1593
  }
949
1594
  const outputFile = options.output ?? defaultOutputPath(options.adapter, themeName);
950
- const outputPath = resolve(cwd, outputFile);
1595
+ const outputPath = resolve2(cwd, outputFile);
951
1596
  const outputDir = dirname4(outputPath);
952
1597
  try {
953
1598
  mkdirSync3(outputDir, { recursive: true });
@@ -992,8 +1637,8 @@ function formatSize(bytes) {
992
1637
  }
993
1638
 
994
1639
  // src/commands/theme-export.ts
995
- import { readFileSync as readFileSync6 } from "fs";
996
- import { resolve as resolve2 } from "path";
1640
+ import { readFileSync as readFileSync7 } from "fs";
1641
+ import { resolve as resolve3 } from "path";
997
1642
  import {
998
1643
  parseConfig,
999
1644
  resolveConfig,
@@ -1001,10 +1646,10 @@ import {
1001
1646
  exportTheme
1002
1647
  } from "@loworbitstudio/visor-theme-engine";
1003
1648
  function themeExportCommand(file, cwd, options) {
1004
- const filePath = resolve2(cwd, file ?? ".visor.yaml");
1649
+ const filePath = resolve3(cwd, file ?? ".visor.yaml");
1005
1650
  let yamlContent;
1006
1651
  try {
1007
- yamlContent = readFileSync6(filePath, "utf-8");
1652
+ yamlContent = readFileSync7(filePath, "utf-8");
1008
1653
  } catch {
1009
1654
  if (options.json) {
1010
1655
  console.log(
@@ -1056,16 +1701,16 @@ function themeExportCommand(file, cwd, options) {
1056
1701
  }
1057
1702
 
1058
1703
  // src/commands/theme-validate.ts
1059
- import { readFileSync as readFileSync7 } from "fs";
1060
- import { resolve as resolve3 } from "path";
1704
+ import { readFileSync as readFileSync8 } from "fs";
1705
+ import { resolve as resolve4 } from "path";
1061
1706
  import { parse as parseYaml } from "yaml";
1062
1707
  import { validate } from "@loworbitstudio/visor-theme-engine";
1063
1708
  import pc2 from "picocolors";
1064
1709
  function themeValidateCommand(file, cwd, options) {
1065
- const filePath = resolve3(cwd, file);
1710
+ const filePath = resolve4(cwd, file);
1066
1711
  let fileContent;
1067
1712
  try {
1068
- fileContent = readFileSync7(filePath, "utf-8");
1713
+ fileContent = readFileSync8(filePath, "utf-8");
1069
1714
  } catch {
1070
1715
  if (options.json) {
1071
1716
  console.log(
@@ -1147,13 +1792,13 @@ function themeValidateCommand(file, cwd, options) {
1147
1792
  function printIssue(issue) {
1148
1793
  const prefix = issue.severity === "error" ? pc2.red(" ERROR") : pc2.yellow(" WARN ");
1149
1794
  const code = pc2.dim(`[${issue.code}]`);
1150
- const path = issue.path ? pc2.dim(` (${issue.path})`) : "";
1151
- console.log(`${prefix} ${code} ${issue.message}${path}`);
1795
+ const path2 = issue.path ? pc2.dim(` (${issue.path})`) : "";
1796
+ console.log(`${prefix} ${code} ${issue.message}${path2}`);
1152
1797
  }
1153
1798
 
1154
1799
  // 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";
1800
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync5, existsSync as existsSync5, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
1801
+ import { resolve as resolve5, join as join7, basename, extname as extname2, relative } from "path";
1157
1802
  import { stringify as stringifyYaml } from "yaml";
1158
1803
  import {
1159
1804
  extractFromCSS,
@@ -1182,7 +1827,7 @@ var CSS_DIRS = [
1182
1827
  "packages/design-tokens"
1183
1828
  ];
1184
1829
  function themeExtractCommand(cwd, options) {
1185
- const targetDir = resolve4(cwd, options.from ?? ".");
1830
+ const targetDir = resolve5(cwd, options.from ?? ".");
1186
1831
  if (!existsSync5(targetDir)) {
1187
1832
  if (options.json) {
1188
1833
  console.log(JSON.stringify({ success: false, error: `Directory not found: ${targetDir}` }));
@@ -1236,17 +1881,17 @@ function themeExtractCommand(cwd, options) {
1236
1881
  function collectCSSFiles(targetDir) {
1237
1882
  const files = [];
1238
1883
  const seen = /* @__PURE__ */ new Set();
1239
- for (const pattern of CSS_FILE_PATTERNS) {
1240
- const rootPath = join6(targetDir, pattern);
1884
+ for (const pattern2 of CSS_FILE_PATTERNS) {
1885
+ const rootPath = join7(targetDir, pattern2);
1241
1886
  addFileIfExists(rootPath, files, seen);
1242
1887
  for (const dir of CSS_DIRS) {
1243
- const dirPath = join6(targetDir, dir, pattern);
1888
+ const dirPath = join7(targetDir, dir, pattern2);
1244
1889
  addFileIfExists(dirPath, files, seen);
1245
1890
  }
1246
1891
  }
1247
1892
  for (const dir of CSS_DIRS) {
1248
- const dirPath = join6(targetDir, dir);
1249
- if (existsSync5(dirPath) && statSync(dirPath).isDirectory()) {
1893
+ const dirPath = join7(targetDir, dir);
1894
+ if (existsSync5(dirPath) && statSync2(dirPath).isDirectory()) {
1250
1895
  scanDirForCSS(dirPath, files, seen, 2);
1251
1896
  }
1252
1897
  }
@@ -1254,11 +1899,11 @@ function collectCSSFiles(targetDir) {
1254
1899
  return files;
1255
1900
  }
1256
1901
  function addFileIfExists(filePath, files, seen) {
1257
- const resolved = resolve4(filePath);
1902
+ const resolved = resolve5(filePath);
1258
1903
  if (seen.has(resolved)) return;
1259
1904
  if (!existsSync5(resolved)) return;
1260
1905
  try {
1261
- const content = readFileSync8(resolved, "utf-8");
1906
+ const content = readFileSync9(resolved, "utf-8");
1262
1907
  if (content.includes("--")) {
1263
1908
  files.push({ path: resolved, content });
1264
1909
  seen.add(resolved);
@@ -1281,15 +1926,15 @@ function scanDirForCSS(dir, files, seen, maxDepth) {
1281
1926
  ".vercel"
1282
1927
  ]);
1283
1928
  try {
1284
- const entries = readdirSync(dir, { withFileTypes: true });
1929
+ const entries = readdirSync2(dir, { withFileTypes: true });
1285
1930
  for (const entry of entries) {
1286
1931
  if (entry.isDirectory()) {
1287
1932
  if (SKIP_DIRS.has(entry.name)) continue;
1288
1933
  if (maxDepth > 0) {
1289
- scanDirForCSS(join6(dir, entry.name), files, seen, maxDepth - 1);
1934
+ scanDirForCSS(join7(dir, entry.name), files, seen, maxDepth - 1);
1290
1935
  }
1291
- } else if (entry.isFile() && extname(entry.name) === ".css") {
1292
- addFileIfExists(join6(dir, entry.name), files, seen);
1936
+ } else if (entry.isFile() && extname2(entry.name) === ".css") {
1937
+ addFileIfExists(join7(dir, entry.name), files, seen);
1293
1938
  }
1294
1939
  }
1295
1940
  } catch {
@@ -1371,10 +2016,10 @@ function extractVarName(varExpr) {
1371
2016
  function parseNextFontFromLayouts(targetDir) {
1372
2017
  const fontMap = /* @__PURE__ */ new Map();
1373
2018
  for (const relPath of LAYOUT_FILE_PATHS) {
1374
- const fullPath = join6(targetDir, relPath);
2019
+ const fullPath = join7(targetDir, relPath);
1375
2020
  if (!existsSync5(fullPath)) continue;
1376
2021
  try {
1377
- const content = readFileSync8(fullPath, "utf-8");
2022
+ const content = readFileSync9(fullPath, "utf-8");
1378
2023
  parseNextFontDeclarations(content, fontMap);
1379
2024
  } catch {
1380
2025
  }
@@ -1421,7 +2066,7 @@ function parseNextFontDeclarations(content, fontMap) {
1421
2066
  const srcMatch = block.match(/src\s*:\s*["']([^"']+)["']/);
1422
2067
  if (srcMatch) {
1423
2068
  const srcPath = srcMatch[1];
1424
- const fileName = basename(srcPath, extname(srcPath));
2069
+ const fileName = basename(srcPath, extname2(srcPath));
1425
2070
  const fontBaseName = fileName.replace(/[-_](Variable|Regular|Bold|Light|Medium|SemiBold|ExtraBold|Thin|Black|Italic).*$/i, "").replace(/[-_]/g, " ").trim();
1426
2071
  if (fontBaseName) {
1427
2072
  fontMap.set(varName, fontBaseName);
@@ -1459,10 +2104,10 @@ var MONO_FONT_NAMES = /* @__PURE__ */ new Set([
1459
2104
  "IBM Plex Mono"
1460
2105
  ]);
1461
2106
  function extractFontHints(targetDir) {
1462
- const pkgPath = join6(targetDir, "package.json");
2107
+ const pkgPath = join7(targetDir, "package.json");
1463
2108
  if (!existsSync5(pkgPath)) return void 0;
1464
2109
  try {
1465
- const pkg = JSON.parse(readFileSync8(pkgPath, "utf-8"));
2110
+ const pkg = JSON.parse(readFileSync9(pkgPath, "utf-8"));
1466
2111
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
1467
2112
  const fonts2 = [];
1468
2113
  for (const [dep, _] of Object.entries(allDeps)) {
@@ -1498,10 +2143,10 @@ function extractFontHints(targetDir) {
1498
2143
  }
1499
2144
  }
1500
2145
  function inferThemeName(targetDir) {
1501
- const pkgPath = join6(targetDir, "package.json");
2146
+ const pkgPath = join7(targetDir, "package.json");
1502
2147
  if (existsSync5(pkgPath)) {
1503
2148
  try {
1504
- const pkg = JSON.parse(readFileSync8(pkgPath, "utf-8"));
2149
+ const pkg = JSON.parse(readFileSync9(pkgPath, "utf-8"));
1505
2150
  if (pkg.name) {
1506
2151
  const name = pkg.name.replace(/^@[\w-]+\//, "");
1507
2152
  return `${name}-theme`;
@@ -1538,7 +2183,7 @@ function outputJSON(result, validationResult) {
1538
2183
  }
1539
2184
  function outputYAML(result, outputPath, cwd, validationResult) {
1540
2185
  const yamlStr = buildAnnotatedYAML(result);
1541
- const outFile = resolve4(cwd, outputPath ?? ".visor.yaml");
2186
+ const outFile = resolve5(cwd, outputPath ?? ".visor.yaml");
1542
2187
  const high = result.tokens.filter((t) => t.confidence === "high").length;
1543
2188
  const med = result.tokens.filter((t) => t.confidence === "medium").length;
1544
2189
  const low = result.tokens.filter((t) => t.confidence === "low").length;
@@ -1624,14 +2269,14 @@ function buildAnnotatedYAML(result) {
1624
2269
  }
1625
2270
 
1626
2271
  // 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";
2272
+ import { readFileSync as readFileSync10, writeFileSync as writeFileSync6, mkdirSync as mkdirSync4, existsSync as existsSync7 } from "fs";
2273
+ import { resolve as resolve7, join as join9 } from "path";
1629
2274
  import { generateThemeData as generateThemeData3 } from "@loworbitstudio/visor-theme-engine";
1630
2275
  import { docsAdapter as docsAdapter2 } from "@loworbitstudio/visor-theme-engine/adapters";
1631
2276
 
1632
2277
  // src/utils/theme-helpers.ts
1633
2278
  import { existsSync as existsSync6 } from "fs";
1634
- import { resolve as resolve5, dirname as dirname5, join as join7 } from "path";
2279
+ import { resolve as resolve6, dirname as dirname5, join as join8 } from "path";
1635
2280
  function toSlug(name) {
1636
2281
  return name.toLowerCase().replace(/\s+/g, "-");
1637
2282
  }
@@ -1639,9 +2284,9 @@ function toLabel(name) {
1639
2284
  return name.split(/[\s-]+/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
1640
2285
  }
1641
2286
  function findRepoRoot(startDir) {
1642
- let current = resolve5(startDir);
2287
+ let current = resolve6(startDir);
1643
2288
  while (true) {
1644
- if (existsSync6(join7(current, "packages", "docs"))) {
2289
+ if (existsSync6(join8(current, "packages", "docs"))) {
1645
2290
  return current;
1646
2291
  }
1647
2292
  const parent = dirname5(current);
@@ -1736,10 +2381,10 @@ ${indent}${newEntry},
1736
2381
  return { updated, changed: true };
1737
2382
  }
1738
2383
  function themeRegisterCommand(file, cwd, options) {
1739
- const filePath = resolve6(cwd, file);
2384
+ const filePath = resolve7(cwd, file);
1740
2385
  let yamlContent;
1741
2386
  try {
1742
- yamlContent = readFileSync9(filePath, "utf-8");
2387
+ yamlContent = readFileSync10(filePath, "utf-8");
1743
2388
  } catch {
1744
2389
  if (options.json) {
1745
2390
  console.log(JSON.stringify({ success: false, error: `Could not read file: ${filePath}` }));
@@ -1782,10 +2427,10 @@ function themeRegisterCommand(file, cwd, options) {
1782
2427
  process.exit(1);
1783
2428
  return;
1784
2429
  }
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");
2430
+ const docsAppDir = join9(repoRoot, "packages", "docs", "app");
2431
+ const cssFilePath = join9(docsAppDir, `${slug}-theme.css`);
2432
+ const globalsPath = join9(docsAppDir, "globals.css");
2433
+ const themeConfigPath = join9(repoRoot, "packages", "docs", "lib", "theme-config.ts");
1789
2434
  if (!existsSync7(docsAppDir)) {
1790
2435
  const msg = `Docs app directory not found: ${docsAppDir}`;
1791
2436
  if (options.json) {
@@ -1799,8 +2444,8 @@ function themeRegisterCommand(file, cwd, options) {
1799
2444
  let globalsContent = "";
1800
2445
  let themeConfigContent = "";
1801
2446
  try {
1802
- globalsContent = readFileSync9(globalsPath, "utf-8");
1803
- themeConfigContent = readFileSync9(themeConfigPath, "utf-8");
2447
+ globalsContent = readFileSync10(globalsPath, "utf-8");
2448
+ themeConfigContent = readFileSync10(themeConfigPath, "utf-8");
1804
2449
  } catch (err) {
1805
2450
  const msg = err instanceof Error ? err.message : "Could not read docs files";
1806
2451
  if (options.json) {
@@ -1812,7 +2457,7 @@ function themeRegisterCommand(file, cwd, options) {
1812
2457
  return;
1813
2458
  }
1814
2459
  const cssExists = existsSync7(cssFilePath);
1815
- const cssChanged = !cssExists || readFileSync9(cssFilePath, "utf-8") !== css;
2460
+ const cssChanged = !cssExists || readFileSync10(cssFilePath, "utf-8") !== css;
1816
2461
  const { updated: newGlobals, changed: globalsChanged } = insertGlobalsImport(globalsContent, slug);
1817
2462
  const { updated: newThemeConfig, changed: themeConfigChanged, error: configError } = insertThemeConfig(
1818
2463
  themeConfigContent,
@@ -1896,8 +2541,8 @@ function themeRegisterCommand(file, cwd, options) {
1896
2541
  }
1897
2542
 
1898
2543
  // 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";
2544
+ import { readFileSync as readFileSync11, writeFileSync as writeFileSync7, existsSync as existsSync8, unlinkSync } from "fs";
2545
+ import { join as join10 } from "path";
1901
2546
  function removeGlobalsImport(content, slug) {
1902
2547
  const importLine = `@import './${slug}-theme.css';`;
1903
2548
  if (!content.includes(importLine)) {
@@ -1929,10 +2574,10 @@ function themeUnregisterCommand(slug, cwd, options) {
1929
2574
  process.exit(1);
1930
2575
  return;
1931
2576
  }
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");
2577
+ const docsAppDir = join10(repoRoot, "packages", "docs", "app");
2578
+ const cssFilePath = join10(docsAppDir, `${slug}-theme.css`);
2579
+ const globalsPath = join10(docsAppDir, "globals.css");
2580
+ const themeConfigPath = join10(repoRoot, "packages", "docs", "lib", "theme-config.ts");
1936
2581
  if (!existsSync8(docsAppDir)) {
1937
2582
  const msg = `Docs app directory not found: ${docsAppDir}`;
1938
2583
  if (options.json) {
@@ -1946,8 +2591,8 @@ function themeUnregisterCommand(slug, cwd, options) {
1946
2591
  let globalsContent = "";
1947
2592
  let themeConfigContent = "";
1948
2593
  try {
1949
- globalsContent = readFileSync10(globalsPath, "utf-8");
1950
- themeConfigContent = readFileSync10(themeConfigPath, "utf-8");
2594
+ globalsContent = readFileSync11(globalsPath, "utf-8");
2595
+ themeConfigContent = readFileSync11(themeConfigPath, "utf-8");
1951
2596
  } catch (err) {
1952
2597
  const msg = err instanceof Error ? err.message : "Could not read docs files";
1953
2598
  if (options.json) {
@@ -1999,32 +2644,47 @@ function themeUnregisterCommand(slug, cwd, options) {
1999
2644
 
2000
2645
  // src/commands/theme-sync.ts
2001
2646
  import {
2002
- readFileSync as readFileSync11,
2647
+ readFileSync as readFileSync12,
2003
2648
  writeFileSync as writeFileSync8,
2004
2649
  mkdirSync as mkdirSync5,
2005
2650
  existsSync as existsSync9,
2006
- readdirSync as readdirSync2,
2651
+ readdirSync as readdirSync3,
2007
2652
  unlinkSync as unlinkSync2,
2008
2653
  copyFileSync
2009
2654
  } from "fs";
2010
- import { join as join10, basename as basename2 } from "path";
2655
+ import { join as join11, basename as basename2 } from "path";
2011
2656
  import { parse as parseYaml2 } from "yaml";
2012
2657
  import { generateThemeData as generateThemeData4 } from "@loworbitstudio/visor-theme-engine";
2013
2658
  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` */";
2659
+ var GLOBALS_BEGIN_MARKER = "/* BEGIN visor-theme-imports \u2014 managed by `visor theme sync` */";
2015
2660
  var GLOBALS_END_MARKER = "/* END visor-theme-imports */";
2661
+ var STOCK_GROUPS_BEGIN_MARKER = "/* BEGIN visor-stock-themes \u2014 managed by `visor theme sync` */";
2662
+ var STOCK_GROUPS_END_MARKER = "/* END visor-stock-themes */";
2016
2663
  var GITIGNORE_BEGIN_MARKER = "# BEGIN visor-custom-theme-css (managed by `visor theme sync` \u2014 do not edit manually)";
2017
2664
  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";
2665
+ var CUSTOM_OVERLAY_CSS_PATH = "packages/docs/app/custom-themes.generated.css";
2666
+ var CUSTOM_OVERLAY_TS_PATH = "packages/docs/lib/theme-config.custom.generated.ts";
2667
+ var CUSTOM_OVERLAY_IMPORT_LINE = "@import './custom-themes.generated.css';";
2019
2668
  function scanThemeDir(dir) {
2020
2669
  if (!existsSync9(dir)) return [];
2021
- return readdirSync2(dir).filter((f) => f.endsWith(".visor.yaml")).map((f) => join10(dir, f));
2670
+ return readdirSync3(dir).filter((f) => f.endsWith(".visor.yaml")).map((f) => join11(dir, f));
2022
2671
  }
2023
2672
  function extractGroup(yamlContent) {
2024
2673
  const parsed = parseYaml2(yamlContent);
2025
2674
  if (typeof parsed?.group === "string") return parsed.group;
2026
2675
  return void 0;
2027
2676
  }
2677
+ function extractLabel(yamlContent) {
2678
+ const parsed = parseYaml2(yamlContent);
2679
+ if (typeof parsed?.label === "string") return parsed.label;
2680
+ return void 0;
2681
+ }
2682
+ function extractDefaultMode(yamlContent) {
2683
+ const parsed = parseYaml2(yamlContent);
2684
+ const v = parsed?.["default-mode"];
2685
+ if (v === "dark" || v === "light") return v;
2686
+ return void 0;
2687
+ }
2028
2688
  function sortGroups(groups) {
2029
2689
  return [...groups].sort((a, b) => {
2030
2690
  if (a === "Visor") return -1;
@@ -2032,9 +2692,113 @@ function sortGroups(groups) {
2032
2692
  return a.localeCompare(b);
2033
2693
  });
2034
2694
  }
2035
- function generateThemeConfig(entries) {
2695
+ function updateGlobalsImports(content, stockSlugs) {
2696
+ const importLines = [...stockSlugs].sort().map((slug) => `@import './${slug}-theme.css';`).join("\n");
2697
+ const newBlock = `${GLOBALS_BEGIN_MARKER}
2698
+ ${importLines}
2699
+ ${GLOBALS_END_MARKER}`;
2700
+ let updated;
2701
+ const beginIdx = content.indexOf(GLOBALS_BEGIN_MARKER);
2702
+ const endIdx = content.indexOf(GLOBALS_END_MARKER);
2703
+ if (beginIdx !== -1 && endIdx !== -1) {
2704
+ updated = content.slice(0, beginIdx) + newBlock + content.slice(endIdx + GLOBALS_END_MARKER.length);
2705
+ } else {
2706
+ const themeImportPattern = /^@import '\.\/[\w-]+-theme\.css';\n?/gm;
2707
+ const lines = content.split("\n");
2708
+ let firstThemeIdx = -1;
2709
+ let lastThemeIdx = -1;
2710
+ for (let i = 0; i < lines.length; i++) {
2711
+ if (/^@import '\.\/[\w-]+-theme\.css';/.test(lines[i])) {
2712
+ if (firstThemeIdx === -1) firstThemeIdx = i;
2713
+ lastThemeIdx = i;
2714
+ }
2715
+ }
2716
+ if (firstThemeIdx !== -1) {
2717
+ const before = lines.slice(0, firstThemeIdx);
2718
+ const after = lines.slice(lastThemeIdx + 1);
2719
+ updated = [...before, newBlock, ...after].join("\n");
2720
+ } else {
2721
+ void themeImportPattern;
2722
+ const lastImportIdx = lines.reduce(
2723
+ (last, line, i) => line.startsWith("@import") ? i : last,
2724
+ -1
2725
+ );
2726
+ const insertAt = lastImportIdx + 1;
2727
+ lines.splice(insertAt, 0, newBlock);
2728
+ updated = lines.join("\n");
2729
+ }
2730
+ }
2731
+ updated = ensureCustomOverlayImport(updated);
2732
+ return updated;
2733
+ }
2734
+ function ensureCustomOverlayImport(content) {
2735
+ const endMarkerIdx = content.indexOf(GLOBALS_END_MARKER);
2736
+ if (endMarkerIdx === -1) return content;
2737
+ const afterMarker = content.slice(endMarkerIdx + GLOBALS_END_MARKER.length);
2738
+ if (afterMarker.trimStart().startsWith(CUSTOM_OVERLAY_IMPORT_LINE)) return content;
2739
+ const withoutStale = content.replace(
2740
+ new RegExp(`\\n?${CUSTOM_OVERLAY_IMPORT_LINE.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`, "g"),
2741
+ ""
2742
+ );
2743
+ const markerEnd = withoutStale.indexOf(GLOBALS_END_MARKER) + GLOBALS_END_MARKER.length;
2744
+ return withoutStale.slice(0, markerEnd) + "\n" + CUSTOM_OVERLAY_IMPORT_LINE + withoutStale.slice(markerEnd);
2745
+ }
2746
+ function updateStockThemeConfigBlock(content, stockEntries) {
2747
+ const groupMap = /* @__PURE__ */ new Map();
2748
+ for (const entry of stockEntries) {
2749
+ if (!groupMap.has(entry.group)) groupMap.set(entry.group, []);
2750
+ groupMap.get(entry.group).push(entry);
2751
+ }
2752
+ const sortedGroupNames = sortGroups([...groupMap.keys()]);
2753
+ for (const [, groupEntries] of groupMap) {
2754
+ groupEntries.sort((a, b) => a.slug.localeCompare(b.slug));
2755
+ }
2756
+ const groupsTs = sortedGroupNames.map((groupName) => {
2757
+ const groupEntries = groupMap.get(groupName);
2758
+ const themesTs = groupEntries.map((e) => {
2759
+ const modePart = e.defaultMode ? `, defaultMode: "${e.defaultMode}"` : "";
2760
+ return ` { value: "${e.slug}", label: "${e.label}", yamlFile: "${e.yamlFilename}"${modePart} },`;
2761
+ }).join("\n");
2762
+ return ` {
2763
+ label: "${groupName}",
2764
+ themes: [
2765
+ ${themesTs}
2766
+ ],
2767
+ },`;
2768
+ }).join("\n");
2769
+ const newBlock = `${STOCK_GROUPS_BEGIN_MARKER}
2770
+ const STOCK_GROUPS: ThemeGroup[] = [
2771
+ ${groupsTs}
2772
+ ];
2773
+ ${STOCK_GROUPS_END_MARKER}`;
2774
+ const beginIdx = content.indexOf(STOCK_GROUPS_BEGIN_MARKER);
2775
+ const endIdx = content.indexOf(STOCK_GROUPS_END_MARKER);
2776
+ if (beginIdx !== -1 && endIdx !== -1) {
2777
+ return content.slice(0, beginIdx) + newBlock + content.slice(endIdx + STOCK_GROUPS_END_MARKER.length);
2778
+ }
2779
+ const themeGroupsExportIdx = content.indexOf("export const THEME_GROUPS");
2780
+ if (themeGroupsExportIdx !== -1) {
2781
+ return content.slice(0, themeGroupsExportIdx) + newBlock + "\n\n" + content.slice(themeGroupsExportIdx);
2782
+ }
2783
+ return content + "\n\n" + newBlock;
2784
+ }
2785
+ function generateCustomOverlayCss(customEntries) {
2786
+ if (customEntries.length === 0) {
2787
+ return "/* generated by `visor theme sync` \u2014 empty when no custom themes are present */\n";
2788
+ }
2789
+ const importLines = [...customEntries].sort((a, b) => a.slug.localeCompare(b.slug)).map((e) => `@import './${e.slug}-theme.css';`).join("\n");
2790
+ return `/* generated by \`visor theme sync\` \u2014 do not edit manually */
2791
+ ${importLines}
2792
+ `;
2793
+ }
2794
+ function generateCustomOverlayTs(customEntries) {
2795
+ if (customEntries.length === 0) {
2796
+ return `import type { ThemeGroup } from "./theme-config";
2797
+ export const customThemeGroups: ThemeGroup[] = [];
2798
+ `;
2799
+ }
2036
2800
  const groupMap = /* @__PURE__ */ new Map();
2037
- for (const entry of entries) {
2801
+ for (const entry of customEntries) {
2038
2802
  if (!groupMap.has(entry.group)) groupMap.set(entry.group, []);
2039
2803
  groupMap.get(entry.group).push(entry);
2040
2804
  }
@@ -2044,7 +2808,10 @@ function generateThemeConfig(entries) {
2044
2808
  }
2045
2809
  const groupsTs = sortedGroupNames.map((groupName) => {
2046
2810
  const groupEntries = groupMap.get(groupName);
2047
- const themesTs = groupEntries.map((e) => ` { value: "${e.slug}", label: "${e.label}", yamlFile: "${e.yamlFilename}" },`).join("\n");
2811
+ const themesTs = groupEntries.map((e) => {
2812
+ const modePart = e.defaultMode ? `, defaultMode: "${e.defaultMode}"` : "";
2813
+ return ` { value: "${e.slug}", label: "${e.label}", yamlFile: "${e.yamlFilename}"${modePart} },`;
2814
+ }).join("\n");
2048
2815
  return ` {
2049
2816
  label: "${groupName}",
2050
2817
  themes: [
@@ -2052,60 +2819,13 @@ ${themesTs}
2052
2819
  ],
2053
2820
  },`;
2054
2821
  }).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[] = [
2822
+ return `import type { ThemeGroup } from "./theme-config";
2823
+ // generated by \`visor theme sync\` \u2014 do not edit manually
2824
+ export const customThemeGroups: ThemeGroup[] = [
2069
2825
  ${groupsTs}
2070
2826
  ];
2071
-
2072
- export const ALL_THEMES = THEME_GROUPS.flatMap((g) => g.themes.map((t) => t.value));
2073
2827
  `;
2074
2828
  }
2075
- function updateGlobalsImports(content, slugs) {
2076
- const importLines = [...slugs].sort().map((slug) => `@import './${slug}-theme.css';`).join("\n");
2077
- const newBlock = `${GLOBALS_BEGIN_MARKER}
2078
- ${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);
2084
- }
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
- }
2094
- }
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");
2099
- }
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");
2108
- }
2109
2829
  function updateGitignoreBlock(content, customSlugs) {
2110
2830
  const cssLines = customSlugs.sort().map((slug) => `packages/docs/app/${slug}-theme.css`).join("\n");
2111
2831
  const newBlock = `${GITIGNORE_BEGIN_MARKER}
@@ -2130,13 +2850,16 @@ function themeSyncCommand(cwd, options) {
2130
2850
  process.exit(1);
2131
2851
  return;
2132
2852
  }
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");
2853
+ const themesDir = join11(repoRoot, "themes");
2854
+ const customThemesDir = join11(repoRoot, "custom-themes");
2855
+ const docsAppDir = join11(repoRoot, "packages", "docs", "app");
2856
+ const docsLibDir = join11(repoRoot, "packages", "docs", "lib");
2857
+ const docsPublicThemesDir = join11(repoRoot, "packages", "docs", "public", "themes");
2858
+ const themeConfigPath = join11(repoRoot, "packages", "docs", "lib", "theme-config.ts");
2859
+ const globalsPath = join11(docsAppDir, "globals.css");
2860
+ const gitignorePath = join11(repoRoot, ".gitignore");
2861
+ const customOverlayCssPath = join11(repoRoot, CUSTOM_OVERLAY_CSS_PATH);
2862
+ const customOverlayTsPath = join11(repoRoot, CUSTOM_OVERLAY_TS_PATH);
2140
2863
  const stockFiles = scanThemeDir(themesDir);
2141
2864
  const customFiles = scanThemeDir(customThemesDir);
2142
2865
  if (stockFiles.length === 0 && customFiles.length === 0) {
@@ -2153,7 +2876,7 @@ function themeSyncCommand(cwd, options) {
2153
2876
  const processFile = (filePath, isCustom) => {
2154
2877
  let yamlContent;
2155
2878
  try {
2156
- yamlContent = readFileSync11(filePath, "utf-8");
2879
+ yamlContent = readFileSync12(filePath, "utf-8");
2157
2880
  } catch {
2158
2881
  errors.push(`Could not read: ${filePath}`);
2159
2882
  return;
@@ -2166,11 +2889,12 @@ function themeSyncCommand(cwd, options) {
2166
2889
  return;
2167
2890
  }
2168
2891
  const slug = toSlug(data.config.name);
2169
- const label = toLabel(data.config.name);
2892
+ const label = extractLabel(yamlContent) ?? toLabel(data.config.name);
2170
2893
  const group = extractGroup(yamlContent) ?? (isCustom ? "Custom" : "Visor");
2894
+ const defaultMode = extractDefaultMode(yamlContent);
2171
2895
  const css = docsAdapter3({ primitives: data.primitives, tokens: data.tokens, config: data.config });
2172
2896
  const yamlFilename = basename2(filePath).replace(/\.visor\.yaml$/, "");
2173
- manifest.push({ slug, label, group, css, yamlFilename, isCustom });
2897
+ manifest.push({ slug, label, group, defaultMode, css, yamlFilename, isCustom });
2174
2898
  };
2175
2899
  for (const f of stockFiles) processFile(f, false);
2176
2900
  for (const f of customFiles) processFile(f, true);
@@ -2183,14 +2907,18 @@ function themeSyncCommand(cwd, options) {
2183
2907
  process.exit(1);
2184
2908
  return;
2185
2909
  }
2186
- const newThemeConfig = generateThemeConfig(manifest);
2910
+ const stockManifest = manifest.filter((e) => !e.isCustom);
2911
+ const customManifest = manifest.filter((e) => e.isCustom);
2912
+ const stockSlugs = stockManifest.map((e) => e.slug);
2913
+ const customSlugs = customManifest.map((e) => e.slug);
2187
2914
  const allSlugs = manifest.map((e) => e.slug);
2188
- const customSlugs = manifest.filter((e) => e.isCustom).map((e) => e.slug);
2189
2915
  let globalsContent;
2916
+ let themeConfigContent;
2190
2917
  let gitignoreContent;
2191
2918
  try {
2192
- globalsContent = readFileSync11(globalsPath, "utf-8");
2193
- gitignoreContent = existsSync9(gitignorePath) ? readFileSync11(gitignorePath, "utf-8") : "";
2919
+ globalsContent = readFileSync12(globalsPath, "utf-8");
2920
+ themeConfigContent = readFileSync12(themeConfigPath, "utf-8");
2921
+ gitignoreContent = existsSync9(gitignorePath) ? readFileSync12(gitignorePath, "utf-8") : "";
2194
2922
  } catch (err) {
2195
2923
  const msg = err instanceof Error ? err.message : "Could not read docs files";
2196
2924
  if (options.json) {
@@ -2201,12 +2929,17 @@ function themeSyncCommand(cwd, options) {
2201
2929
  process.exit(1);
2202
2930
  return;
2203
2931
  }
2204
- const newGlobals = updateGlobalsImports(globalsContent, allSlugs);
2932
+ const newGlobals = updateGlobalsImports(globalsContent, stockSlugs);
2933
+ const newThemeConfig = updateStockThemeConfigBlock(themeConfigContent, stockManifest);
2205
2934
  const newGitignore = customSlugs.length > 0 ? updateGitignoreBlock(gitignoreContent, customSlugs) : gitignoreContent;
2206
- const existingCssFiles = existsSync9(docsAppDir) ? readdirSync2(docsAppDir).filter((f) => f.endsWith("-theme.css")) : [];
2935
+ const newCustomOverlayCss = generateCustomOverlayCss(customManifest);
2936
+ const newCustomOverlayTs = generateCustomOverlayTs(customManifest);
2937
+ const existingCssFiles = existsSync9(docsAppDir) ? readdirSync3(docsAppDir).filter(
2938
+ (f) => f.endsWith("-theme.css") && f !== "custom-themes.generated.css"
2939
+ ) : [];
2207
2940
  const newCssSet = new Set(allSlugs.map((s) => `${s}-theme.css`));
2208
2941
  const staleCssFiles = existingCssFiles.filter((f) => !newCssSet.has(f));
2209
- const existingPublicYamls = existsSync9(docsPublicThemesDir) ? readdirSync2(docsPublicThemesDir).filter((f) => f.endsWith(".visor.yaml")) : [];
2942
+ const existingPublicYamls = existsSync9(docsPublicThemesDir) ? readdirSync3(docsPublicThemesDir).filter((f) => f.endsWith(".visor.yaml")) : [];
2210
2943
  const newPublicYamlSet = new Set(manifest.map((e) => `${e.yamlFilename}.visor.yaml`));
2211
2944
  const stalePublicYamls = existingPublicYamls.filter((f) => !newPublicYamlSet.has(f));
2212
2945
  if (options.dryRun) {
@@ -2216,6 +2949,8 @@ function themeSyncCommand(cwd, options) {
2216
2949
  cssFilesDeleted: staleCssFiles.map((f) => `packages/docs/app/${f}`),
2217
2950
  themeConfig: themeConfigPath,
2218
2951
  globalsCSS: globalsPath,
2952
+ customOverlayCss: CUSTOM_OVERLAY_CSS_PATH,
2953
+ customOverlayTs: CUSTOM_OVERLAY_TS_PATH,
2219
2954
  gitignore: gitignorePath,
2220
2955
  publicYamlsCopied: manifest.map((e) => `packages/docs/public/themes/${e.yamlFilename}.visor.yaml`),
2221
2956
  publicYamlsDeleted: stalePublicYamls.map((f) => `packages/docs/public/themes/${f}`)
@@ -2224,7 +2959,7 @@ function themeSyncCommand(cwd, options) {
2224
2959
  console.log(JSON.stringify({ success: true, dryRun: true, changes }));
2225
2960
  } else {
2226
2961
  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)`);
2962
+ logger.item(`Themes discovered: ${manifest.length} (${stockManifest.length} stock, ${customManifest.length} custom)`);
2228
2963
  manifest.forEach((e) => logger.item(` ${e.slug} \u2014 group: ${e.group}`));
2229
2964
  if (staleCssFiles.length > 0) logger.item(`CSS files to delete: ${staleCssFiles.join(", ")}`);
2230
2965
  if (stalePublicYamls.length > 0) logger.item(`Public YAMLs to delete: ${stalePublicYamls.join(", ")}`);
@@ -2233,13 +2968,16 @@ function themeSyncCommand(cwd, options) {
2233
2968
  }
2234
2969
  try {
2235
2970
  mkdirSync5(docsAppDir, { recursive: true });
2971
+ mkdirSync5(docsLibDir, { recursive: true });
2236
2972
  mkdirSync5(docsPublicThemesDir, { recursive: true });
2237
2973
  for (const entry of manifest) {
2238
- writeFileSync8(join10(docsAppDir, `${entry.slug}-theme.css`), entry.css, "utf-8");
2974
+ writeFileSync8(join11(docsAppDir, `${entry.slug}-theme.css`), entry.css, "utf-8");
2239
2975
  }
2240
2976
  for (const stale of staleCssFiles) {
2241
- unlinkSync2(join10(docsAppDir, stale));
2977
+ unlinkSync2(join11(docsAppDir, stale));
2242
2978
  }
2979
+ writeFileSync8(customOverlayCssPath, newCustomOverlayCss, "utf-8");
2980
+ writeFileSync8(customOverlayTsPath, newCustomOverlayTs, "utf-8");
2243
2981
  writeFileSync8(themeConfigPath, newThemeConfig, "utf-8");
2244
2982
  writeFileSync8(globalsPath, newGlobals, "utf-8");
2245
2983
  if (existsSync9(gitignorePath)) {
@@ -2248,10 +2986,10 @@ function themeSyncCommand(cwd, options) {
2248
2986
  const allSourceFiles = [...stockFiles, ...customFiles];
2249
2987
  for (const srcFile of allSourceFiles) {
2250
2988
  const filename = basename2(srcFile);
2251
- copyFileSync(srcFile, join10(docsPublicThemesDir, filename));
2989
+ copyFileSync(srcFile, join11(docsPublicThemesDir, filename));
2252
2990
  }
2253
2991
  for (const stale of stalePublicYamls) {
2254
- unlinkSync2(join10(docsPublicThemesDir, stale));
2992
+ unlinkSync2(join11(docsPublicThemesDir, stale));
2255
2993
  }
2256
2994
  } catch (err) {
2257
2995
  const msg = err instanceof Error ? err.message : "Write failed";
@@ -2267,17 +3005,17 @@ function themeSyncCommand(cwd, options) {
2267
3005
  console.log(JSON.stringify({
2268
3006
  success: true,
2269
3007
  themes: manifest.length,
2270
- stock: manifest.filter((e) => !e.isCustom).length,
2271
- custom: manifest.filter((e) => e.isCustom).length,
3008
+ stock: stockManifest.length,
3009
+ custom: customManifest.length,
2272
3010
  staleCssDeleted: staleCssFiles.length,
2273
3011
  staleYamlsDeleted: stalePublicYamls.length,
2274
3012
  slugs: allSlugs
2275
3013
  }));
2276
3014
  } else {
2277
3015
  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(", ")}`);
3016
+ logger.item(`Stock: ${stockManifest.map((e) => e.slug).join(", ")}`);
3017
+ if (customManifest.length > 0) {
3018
+ logger.item(`Custom: ${customManifest.map((e) => e.slug).join(", ")}`);
2281
3019
  }
2282
3020
  if (staleCssFiles.length > 0) {
2283
3021
  logger.item(`Removed stale CSS: ${staleCssFiles.join(", ")}`);
@@ -2286,11 +3024,11 @@ function themeSyncCommand(cwd, options) {
2286
3024
  }
2287
3025
 
2288
3026
  // 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";
3027
+ import { existsSync as existsSync10, statSync as statSync3, readdirSync as readdirSync4, readFileSync as readFileSync13 } from "fs";
3028
+ import { resolve as resolve8, basename as basename3, extname as extname3 } from "path";
2291
3029
  import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
2292
3030
  function deriveFamilySlug(filename) {
2293
- const name = basename3(filename, extname2(filename));
3031
+ const name = basename3(filename, extname3(filename));
2294
3032
  const WEIGHT_STYLE_SUFFIXES = /* @__PURE__ */ new Set([
2295
3033
  "thin",
2296
3034
  "hairline",
@@ -2331,13 +3069,13 @@ function deriveFamilySlug(filename) {
2331
3069
  return parts.join("-").toLowerCase();
2332
3070
  }
2333
3071
  function collectWoff2Files(inputPath) {
2334
- const resolved = resolve7(inputPath);
3072
+ const resolved = resolve8(inputPath);
2335
3073
  if (!existsSync10(resolved)) {
2336
3074
  throw new Error(`Path not found: ${resolved}`);
2337
3075
  }
2338
- const stat = statSync2(resolved);
3076
+ const stat = statSync3(resolved);
2339
3077
  if (stat.isFile()) {
2340
- if (extname2(resolved).toLowerCase() !== ".woff2") {
3078
+ if (extname3(resolved).toLowerCase() !== ".woff2") {
2341
3079
  throw new Error(
2342
3080
  `Invalid file format: ${basename3(resolved)}. Only .woff2 files are accepted.`
2343
3081
  );
@@ -2345,14 +3083,14 @@ function collectWoff2Files(inputPath) {
2345
3083
  return [resolved];
2346
3084
  }
2347
3085
  if (stat.isDirectory()) {
2348
- const files = readdirSync3(resolved).filter((f) => extname2(f).toLowerCase() === ".woff2").map((f) => resolve7(resolved, f));
3086
+ const files = readdirSync4(resolved).filter((f) => extname3(f).toLowerCase() === ".woff2").map((f) => resolve8(resolved, f));
2349
3087
  if (files.length === 0) {
2350
3088
  throw new Error(
2351
3089
  `No .woff2 files found in directory: ${resolved}`
2352
3090
  );
2353
3091
  }
2354
- const nonWoff2Fonts = readdirSync3(resolved).filter((f) => {
2355
- const ext = extname2(f).toLowerCase();
3092
+ const nonWoff2Fonts = readdirSync4(resolved).filter((f) => {
3093
+ const ext = extname3(f).toLowerCase();
2356
3094
  return [".ttf", ".otf", ".woff", ".eot"].includes(ext);
2357
3095
  });
2358
3096
  return files.sort();
@@ -2360,12 +3098,12 @@ function collectWoff2Files(inputPath) {
2360
3098
  throw new Error(`Path is neither a file nor a directory: ${resolved}`);
2361
3099
  }
2362
3100
  function getNonWoff2Fonts(inputPath) {
2363
- const resolved = resolve7(inputPath);
2364
- if (!existsSync10(resolved) || !statSync2(resolved).isDirectory()) {
3101
+ const resolved = resolve8(inputPath);
3102
+ if (!existsSync10(resolved) || !statSync3(resolved).isDirectory()) {
2365
3103
  return [];
2366
3104
  }
2367
- return readdirSync3(resolved).filter((f) => {
2368
- const ext = extname2(f).toLowerCase();
3105
+ return readdirSync4(resolved).filter((f) => {
3106
+ const ext = extname3(f).toLowerCase();
2369
3107
  return [".ttf", ".otf", ".woff", ".eot"].includes(ext);
2370
3108
  });
2371
3109
  }
@@ -2398,7 +3136,7 @@ function createR2Client(config) {
2398
3136
  });
2399
3137
  }
2400
3138
  async function uploadFile(client, bucket, key, filePath) {
2401
- const body = readFileSync12(filePath);
3139
+ const body = readFileSync13(filePath);
2402
3140
  await client.send(
2403
3141
  new PutObjectCommand({
2404
3142
  Bucket: bucket,
@@ -2414,8 +3152,8 @@ async function fontsAddCommand(inputPath, options) {
2414
3152
  const r2Config = getR2Config();
2415
3153
  const files = collectWoff2Files(inputPath);
2416
3154
  const familySlug = options.family ?? deriveFamilySlug(basename3(files[0]));
2417
- const resolved = resolve7(inputPath);
2418
- const nonWoff2 = statSync2(resolved).isDirectory() ? getNonWoff2Fonts(resolved) : [];
3155
+ const resolved = resolve8(inputPath);
3156
+ const nonWoff2 = statSync3(resolved).isDirectory() ? getNonWoff2Fonts(resolved) : [];
2419
3157
  if (!json) {
2420
3158
  logger.heading("Visor Font Upload");
2421
3159
  logger.info(`Organization: ${org}`);
@@ -2439,7 +3177,7 @@ async function fontsAddCommand(inputPath, options) {
2439
3177
  logger.info(`Uploading ${filename}...`);
2440
3178
  }
2441
3179
  await uploadFile(client, bucket, key, filePath);
2442
- const size = statSync2(filePath).size;
3180
+ const size = statSync3(filePath).size;
2443
3181
  results.push({ file: filename, key, size });
2444
3182
  if (!json) {
2445
3183
  logger.success(`Uploaded: ${key} (${formatBytes(size)})`);
@@ -2486,23 +3224,644 @@ function formatBytes(bytes) {
2486
3224
  return `${mb.toFixed(1)} MB`;
2487
3225
  }
2488
3226
 
3227
+ // src/commands/doctor.ts
3228
+ import * as fs from "fs";
3229
+ import * as path from "path";
3230
+ import { execFileSync as execFileSync2 } from "child_process";
3231
+ async function doctorCommand(cwd, options, cliVersion) {
3232
+ const checks = [];
3233
+ const visorJsonPath = path.join(cwd, "visor.json");
3234
+ try {
3235
+ const content = fs.readFileSync(visorJsonPath, "utf-8");
3236
+ JSON.parse(content);
3237
+ checks.push({ name: "visor.json", pass: true, severity: "error", message: "visor.json exists and is valid JSON" });
3238
+ } catch {
3239
+ checks.push({
3240
+ name: "visor.json",
3241
+ pass: false,
3242
+ severity: "error",
3243
+ message: "visor.json missing or invalid",
3244
+ fix: "Run `npx visor init` to initialize Visor in this project"
3245
+ });
3246
+ }
3247
+ const visorCorePath = path.join(cwd, "node_modules", "@loworbitstudio", "visor-core");
3248
+ if (fs.existsSync(visorCorePath)) {
3249
+ checks.push({ name: "visor-core", pass: true, severity: "error", message: "@loworbitstudio/visor-core is installed" });
3250
+ } else {
3251
+ checks.push({
3252
+ name: "visor-core",
3253
+ pass: false,
3254
+ severity: "error",
3255
+ message: "@loworbitstudio/visor-core not found in node_modules",
3256
+ fix: "Run `npm install @loworbitstudio/visor-core`"
3257
+ });
3258
+ }
3259
+ const cssFiles = findCssFiles(cwd);
3260
+ const hasVisorImport = cssFiles.some((f) => {
3261
+ try {
3262
+ const content = fs.readFileSync(f, "utf-8");
3263
+ return content.includes("visor-core") || content.includes("@loworbitstudio/visor-core");
3264
+ } catch {
3265
+ return false;
3266
+ }
3267
+ });
3268
+ if (hasVisorImport) {
3269
+ checks.push({ name: "css-import", pass: true, severity: "warning", message: "visor-core CSS import found" });
3270
+ } else {
3271
+ checks.push({
3272
+ name: "css-import",
3273
+ pass: false,
3274
+ severity: "warning",
3275
+ message: "No visor-core CSS import found in CSS files",
3276
+ fix: 'Add `@import "@loworbitstudio/visor-core/tokens.css"` to your global CSS file'
3277
+ });
3278
+ }
3279
+ const pkgJsonPath = path.join(cwd, "package.json");
3280
+ try {
3281
+ const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
3282
+ const reactVersion = pkg.dependencies?.react ?? pkg.devDependencies?.react ?? "";
3283
+ const versionNum = parseFloat(reactVersion.replace(/[^0-9.]/g, ""));
3284
+ if (!reactVersion) {
3285
+ checks.push({
3286
+ name: "react-version",
3287
+ pass: false,
3288
+ severity: "error",
3289
+ message: "React not found in dependencies",
3290
+ fix: "Install React: `npm install react@latest react-dom@latest`"
3291
+ });
3292
+ } else if (versionNum >= 17 || reactVersion.includes("18") || reactVersion.includes("19")) {
3293
+ checks.push({ name: "react-version", pass: true, severity: "error", message: `React ${reactVersion} satisfies peer dep requirement (>=17)` });
3294
+ } else {
3295
+ checks.push({
3296
+ name: "react-version",
3297
+ pass: false,
3298
+ severity: "error",
3299
+ message: `React version ${reactVersion} may not satisfy peer dep requirement (>=17)`,
3300
+ fix: "Upgrade React to v17 or higher: `npm install react@latest react-dom@latest`"
3301
+ });
3302
+ }
3303
+ } catch {
3304
+ checks.push({
3305
+ name: "react-version",
3306
+ pass: false,
3307
+ severity: "warning",
3308
+ message: "Could not read package.json to check React version",
3309
+ fix: "Ensure package.json exists in the project root"
3310
+ });
3311
+ }
3312
+ const componentsDir = path.join(cwd, "components", "ui");
3313
+ if (fs.existsSync(componentsDir) && fs.readdirSync(componentsDir).length > 0) {
3314
+ const count = fs.readdirSync(componentsDir).length;
3315
+ checks.push({ name: "components", pass: true, severity: "info", message: `${count} component(s) found under components/ui/` });
3316
+ } else {
3317
+ checks.push({
3318
+ name: "components",
3319
+ pass: false,
3320
+ severity: "info",
3321
+ message: "No components found under components/ui/",
3322
+ fix: "Add components with `npx visor add <component-name>` (e.g. `npx visor add button`)"
3323
+ });
3324
+ }
3325
+ const manifestPaths = [
3326
+ path.join(cwd, "public", "r", "index.json"),
3327
+ path.join(cwd, "registry", "index.json")
3328
+ ];
3329
+ const foundManifest = manifestPaths.find((p) => fs.existsSync(p));
3330
+ if (foundManifest) {
3331
+ try {
3332
+ const manifestContent = JSON.parse(fs.readFileSync(foundManifest, "utf-8"));
3333
+ const isEmpty = manifestContent === null || Array.isArray(manifestContent) && manifestContent.length === 0 || typeof manifestContent === "object" && Object.keys(manifestContent).length === 0;
3334
+ if (!isEmpty) {
3335
+ checks.push({ name: "registry-manifest", pass: true, severity: "info", message: `Registry manifest found at ${path.relative(cwd, foundManifest)}` });
3336
+ } else {
3337
+ checks.push({
3338
+ name: "registry-manifest",
3339
+ pass: false,
3340
+ severity: "info",
3341
+ message: "Registry manifest is empty",
3342
+ fix: "Run `npx visor build` to regenerate the registry manifest"
3343
+ });
3344
+ }
3345
+ } catch {
3346
+ checks.push({
3347
+ name: "registry-manifest",
3348
+ pass: false,
3349
+ severity: "info",
3350
+ message: "Registry manifest found but could not be parsed",
3351
+ fix: "Run `npx visor build` to regenerate the registry manifest"
3352
+ });
3353
+ }
3354
+ } else {
3355
+ checks.push({
3356
+ name: "registry-manifest",
3357
+ pass: false,
3358
+ severity: "info",
3359
+ message: "No registry manifest found (this is normal for consumer projects)",
3360
+ fix: "If building a design system, run `npx visor build` to generate the registry manifest"
3361
+ });
3362
+ }
3363
+ if (process.platform !== "win32") {
3364
+ try {
3365
+ const globalPath = execFileSync2("which", ["visor"], { encoding: "utf-8" }).trim();
3366
+ if (globalPath) {
3367
+ const globalVersionRaw = execFileSync2(globalPath, ["--version"], { encoding: "utf-8" }).trim();
3368
+ const globalVersion = globalVersionRaw.split(/\s+/).pop() ?? "";
3369
+ if (isOlder(globalVersion, cliVersion)) {
3370
+ checks.push({
3371
+ name: "stale-global-cli",
3372
+ pass: false,
3373
+ severity: "warning",
3374
+ message: `Global visor ${globalVersion} is older than running CLI ${cliVersion}`,
3375
+ fix: "Run npm uninstall -g @loworbitstudio/visor to remove the stale global"
3376
+ });
3377
+ } else {
3378
+ checks.push({
3379
+ name: "stale-global-cli",
3380
+ pass: true,
3381
+ severity: "warning",
3382
+ message: `Global visor ${globalVersion} matches running CLI`
3383
+ });
3384
+ }
3385
+ }
3386
+ } catch {
3387
+ }
3388
+ }
3389
+ const hasErrors = checks.some((c) => !c.pass && c.severity === "error");
3390
+ const hasWarnings = checks.some((c) => !c.pass && c.severity === "warning");
3391
+ const result = {
3392
+ status: hasErrors ? "error" : hasWarnings ? "warning" : "ok",
3393
+ checks
3394
+ };
3395
+ if (options.json) {
3396
+ console.log(JSON.stringify(result, null, 2));
3397
+ process.exit(hasErrors ? 1 : 0);
3398
+ return;
3399
+ }
3400
+ console.log("\nVisor Doctor\n============");
3401
+ for (const check of checks) {
3402
+ const icon = check.pass ? "\u2713" : check.severity === "error" ? "\u2717" : "\u26A0";
3403
+ console.log(`${icon} ${check.name}: ${check.message}`);
3404
+ if (!check.pass && check.fix) {
3405
+ console.log(` Fix: ${check.fix}`);
3406
+ }
3407
+ }
3408
+ console.log(`
3409
+ Status: ${result.status.toUpperCase()}`);
3410
+ process.exit(hasErrors ? 1 : 0);
3411
+ }
3412
+ function isOlder(a, b) {
3413
+ const pa = a.split(".").map((n) => parseInt(n, 10) || 0);
3414
+ const pb = b.split(".").map((n) => parseInt(n, 10) || 0);
3415
+ const len = Math.max(pa.length, pb.length);
3416
+ for (let i = 0; i < len; i++) {
3417
+ const va = pa[i] ?? 0;
3418
+ const vb = pb[i] ?? 0;
3419
+ if (va < vb) return true;
3420
+ if (va > vb) return false;
3421
+ }
3422
+ return false;
3423
+ }
3424
+ function findCssFiles(dir, maxDepth = 3) {
3425
+ const files = [];
3426
+ try {
3427
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
3428
+ for (const entry of entries) {
3429
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
3430
+ const fullPath = path.join(dir, entry.name);
3431
+ if (entry.isFile() && (entry.name.endsWith(".css") || entry.name.endsWith(".scss"))) {
3432
+ files.push(fullPath);
3433
+ } else if (entry.isDirectory() && maxDepth > 0) {
3434
+ files.push(...findCssFiles(fullPath, maxDepth - 1));
3435
+ }
3436
+ }
3437
+ } catch {
3438
+ }
3439
+ return files;
3440
+ }
3441
+
3442
+ // src/utils/patterns.ts
3443
+ import { existsSync as existsSync12, readdirSync as readdirSync6, readFileSync as readFileSync15 } from "fs";
3444
+ import { join as join13 } from "path";
3445
+ import { parse as parseYAML } from "yaml";
3446
+ function loadPatternsFromYaml(repoRoot) {
3447
+ const patternsDir = join13(repoRoot, "patterns");
3448
+ if (!existsSync12(patternsDir)) return [];
3449
+ const files = readdirSync6(patternsDir).filter(
3450
+ (f) => f.endsWith(".visor-pattern.yaml")
3451
+ );
3452
+ return files.map((file) => {
3453
+ const content = readFileSync15(join13(patternsDir, file), "utf-8");
3454
+ return parseYAML(content);
3455
+ }).filter(Boolean);
3456
+ }
3457
+ function findRepoRoot2(startDir) {
3458
+ let current = startDir;
3459
+ while (true) {
3460
+ if (existsSync12(join13(current, "patterns"))) {
3461
+ return current;
3462
+ }
3463
+ const parent = join13(current, "..");
3464
+ if (parent === current) return null;
3465
+ current = parent;
3466
+ }
3467
+ }
3468
+
3469
+ // src/commands/pattern.ts
3470
+ function patternListCommand(cwd, options = {}) {
3471
+ const json = options.json ?? false;
3472
+ const repoRoot = findRepoRoot2(cwd);
3473
+ if (!repoRoot) {
3474
+ if (json) {
3475
+ console.log(
3476
+ JSON.stringify({ success: false, error: "Could not find patterns/ directory." }, null, 2)
3477
+ );
3478
+ process.exit(1);
3479
+ return;
3480
+ }
3481
+ logger.error("Could not find patterns/ directory.");
3482
+ process.exit(1);
3483
+ return;
3484
+ }
3485
+ const patterns = loadPatternsFromYaml(repoRoot);
3486
+ if (json) {
3487
+ const output = patterns.map((p) => ({
3488
+ name: p.name,
3489
+ description: p.description,
3490
+ components_used: p.components_used,
3491
+ when_to_use: p.when_to_use
3492
+ }));
3493
+ console.log(
3494
+ JSON.stringify(
3495
+ {
3496
+ success: true,
3497
+ patterns: output,
3498
+ summary: { total: output.length }
3499
+ },
3500
+ null,
3501
+ 2
3502
+ )
3503
+ );
3504
+ process.exit(0);
3505
+ return;
3506
+ }
3507
+ logger.heading(`Composition Patterns (${patterns.length})`);
3508
+ logger.blank();
3509
+ for (const p of patterns) {
3510
+ logger.info(` ${p.name.padEnd(32)} ${p.description}`);
3511
+ }
3512
+ logger.blank();
3513
+ }
3514
+ function patternInfoCommand(name, cwd, options = {}) {
3515
+ const json = options.json ?? false;
3516
+ const repoRoot = findRepoRoot2(cwd);
3517
+ if (!repoRoot) {
3518
+ if (json) {
3519
+ console.log(
3520
+ JSON.stringify({ success: false, error: "Could not find patterns/ directory." }, null, 2)
3521
+ );
3522
+ process.exit(1);
3523
+ return;
3524
+ }
3525
+ logger.error("Could not find patterns/ directory.");
3526
+ process.exit(1);
3527
+ return;
3528
+ }
3529
+ const patterns = loadPatternsFromYaml(repoRoot);
3530
+ const pattern2 = patterns.find(
3531
+ (p) => p.name.toLowerCase() === name.toLowerCase() || p.name.toLowerCase().replace(/\s+/g, "-") === name.toLowerCase()
3532
+ );
3533
+ if (!pattern2) {
3534
+ if (json) {
3535
+ console.log(
3536
+ JSON.stringify({ success: false, error: `Pattern "${name}" not found.` }, null, 2)
3537
+ );
3538
+ process.exit(1);
3539
+ return;
3540
+ }
3541
+ logger.error(`Pattern "${name}" not found.`);
3542
+ process.exit(1);
3543
+ return;
3544
+ }
3545
+ if (json) {
3546
+ console.log(
3547
+ JSON.stringify(
3548
+ {
3549
+ success: true,
3550
+ pattern: {
3551
+ name: pattern2.name,
3552
+ description: pattern2.description,
3553
+ components_used: pattern2.components_used,
3554
+ ...pattern2.related_blocks ? { related_blocks: pattern2.related_blocks } : {},
3555
+ when_to_use: pattern2.when_to_use,
3556
+ structure: pattern2.structure,
3557
+ notes: pattern2.notes
3558
+ }
3559
+ },
3560
+ null,
3561
+ 2
3562
+ )
3563
+ );
3564
+ process.exit(0);
3565
+ return;
3566
+ }
3567
+ logger.heading(pattern2.name);
3568
+ logger.blank();
3569
+ logger.info(`Description: ${pattern2.description}`);
3570
+ logger.blank();
3571
+ logger.info(`Components used: ${pattern2.components_used.join(", ")}`);
3572
+ logger.blank();
3573
+ logger.info("When to use:");
3574
+ for (const item of pattern2.when_to_use) {
3575
+ logger.info(` - ${item}`);
3576
+ }
3577
+ if (pattern2.related_blocks && pattern2.related_blocks.length > 0) {
3578
+ logger.blank();
3579
+ logger.info(`Related blocks: ${pattern2.related_blocks.join(", ")}`);
3580
+ }
3581
+ logger.blank();
3582
+ logger.info("Structure:");
3583
+ logger.blank();
3584
+ console.log(pattern2.structure);
3585
+ logger.blank();
3586
+ logger.info("Notes:");
3587
+ logger.blank();
3588
+ console.log(pattern2.notes);
3589
+ logger.blank();
3590
+ }
3591
+
3592
+ // src/commands/suggest.ts
3593
+ var STOP_WORDS2 = /* @__PURE__ */ new Set([
3594
+ "a",
3595
+ "an",
3596
+ "the",
3597
+ "with",
3598
+ "for",
3599
+ "and",
3600
+ "or",
3601
+ "to",
3602
+ "in",
3603
+ "of",
3604
+ "is",
3605
+ "that",
3606
+ "this",
3607
+ "it",
3608
+ "as",
3609
+ "at",
3610
+ "by",
3611
+ "on",
3612
+ "be",
3613
+ "are",
3614
+ "was",
3615
+ "were"
3616
+ ]);
3617
+ function tokenize2(text) {
3618
+ return text.toLowerCase().split(/[\s\-_,]+/).filter((t) => t.length > 1 && !STOP_WORDS2.has(t));
3619
+ }
3620
+ function scoreEntry(queryTokens, name, description, whenToUse) {
3621
+ const searchText = [name, description, ...whenToUse].join(" ").toLowerCase();
3622
+ const matchedTokens = queryTokens.filter((t) => searchText.includes(t));
3623
+ return {
3624
+ score: matchedTokens.length,
3625
+ matchReason: matchedTokens.length > 0 ? `Matched: ${matchedTokens.join(", ")}` : ""
3626
+ };
3627
+ }
3628
+ async function suggestCommand(_cwd, options) {
3629
+ const query = options.for;
3630
+ const queryTokens = tokenize2(query);
3631
+ if (queryTokens.length === 0) {
3632
+ const err = {
3633
+ success: false,
3634
+ error: "Query is too short or contains only stop words. Try more specific terms."
3635
+ };
3636
+ if (options.json) {
3637
+ console.error(JSON.stringify(err));
3638
+ process.exit(1);
3639
+ }
3640
+ console.error(err.error);
3641
+ process.exit(1);
3642
+ }
3643
+ const manifest = loadManifest();
3644
+ const results = [];
3645
+ for (const [name, entry] of Object.entries(manifest.components)) {
3646
+ const { score, matchReason } = scoreEntry(
3647
+ queryTokens,
3648
+ name,
3649
+ entry.description,
3650
+ entry.when_to_use || []
3651
+ );
3652
+ if (score >= 1) {
3653
+ results.push({
3654
+ name,
3655
+ type: "component",
3656
+ category: entry.category,
3657
+ score,
3658
+ description: entry.description,
3659
+ match_reason: matchReason,
3660
+ install_command: `npx visor add ${name}`
3661
+ });
3662
+ }
3663
+ }
3664
+ for (const [name, entry] of Object.entries(manifest.blocks)) {
3665
+ const { score, matchReason } = scoreEntry(
3666
+ queryTokens,
3667
+ name,
3668
+ entry.description,
3669
+ entry.when_to_use || []
3670
+ );
3671
+ if (score >= 1) {
3672
+ results.push({
3673
+ name,
3674
+ type: "block",
3675
+ category: entry.category,
3676
+ score,
3677
+ description: entry.description,
3678
+ match_reason: matchReason,
3679
+ install_command: `npx visor add ${name} --block`
3680
+ });
3681
+ }
3682
+ }
3683
+ for (const [name, entry] of Object.entries(manifest.patterns)) {
3684
+ const { score, matchReason } = scoreEntry(
3685
+ queryTokens,
3686
+ name,
3687
+ entry.description,
3688
+ entry.when_to_use || []
3689
+ );
3690
+ if (score >= 1) {
3691
+ results.push({
3692
+ name,
3693
+ type: "pattern",
3694
+ score,
3695
+ description: entry.description,
3696
+ match_reason: matchReason,
3697
+ install_command: null
3698
+ });
3699
+ }
3700
+ }
3701
+ for (const [name, entry] of Object.entries(manifest.hooks)) {
3702
+ const { score, matchReason } = scoreEntry(
3703
+ queryTokens,
3704
+ name,
3705
+ entry.description,
3706
+ []
3707
+ );
3708
+ if (score >= 1) {
3709
+ results.push({
3710
+ name,
3711
+ type: "hook",
3712
+ score,
3713
+ description: entry.description,
3714
+ match_reason: matchReason,
3715
+ install_command: `npx visor add ${name}`
3716
+ });
3717
+ }
3718
+ }
3719
+ results.sort((a, b) => b.score - a.score);
3720
+ const topResults = results.slice(0, 10);
3721
+ if (topResults.length === 0) {
3722
+ const err = {
3723
+ success: false,
3724
+ error: `No matches found for "${query}". Try broader terms.`
3725
+ };
3726
+ if (options.json) {
3727
+ console.error(JSON.stringify(err, null, 2));
3728
+ process.exit(1);
3729
+ }
3730
+ console.error(err.error);
3731
+ process.exit(1);
3732
+ }
3733
+ const totalSearched = Object.keys(manifest.components).length + Object.keys(manifest.blocks).length + Object.keys(manifest.patterns).length + Object.keys(manifest.hooks).length;
3734
+ const output = {
3735
+ success: true,
3736
+ query,
3737
+ results: topResults,
3738
+ summary: {
3739
+ total_searched: totalSearched,
3740
+ total_matched: topResults.length
3741
+ }
3742
+ };
3743
+ if (options.json) {
3744
+ console.log(JSON.stringify(output, null, 2));
3745
+ process.exit(0);
3746
+ }
3747
+ console.log(`
3748
+ Suggestions for "${query}":
3749
+ `);
3750
+ for (const r of topResults) {
3751
+ const cmd = r.install_command ? ` (${r.install_command})` : "";
3752
+ console.log(` ${r.name} [${r.type}]${cmd}`);
3753
+ console.log(` ${r.description.slice(0, 80)}`);
3754
+ }
3755
+ console.log(
3756
+ `
3757
+ ${topResults.length} result${topResults.length !== 1 ? "s" : ""} from ${totalSearched} entries
3758
+ `
3759
+ );
3760
+ }
3761
+
3762
+ // src/commands/tokens.ts
3763
+ async function tokensListCommand(_cwd, options) {
3764
+ const manifest = loadManifest();
3765
+ if (!manifest.tokens) {
3766
+ const err = {
3767
+ success: false,
3768
+ error: "Tokens section not found in manifest. Run npm run build:manifest to rebuild."
3769
+ };
3770
+ if (options.json) {
3771
+ console.error(JSON.stringify(err));
3772
+ } else {
3773
+ console.error(err.error);
3774
+ }
3775
+ process.exit(1);
3776
+ }
3777
+ const { primitives, semantic, adaptive, summary } = manifest.tokens;
3778
+ let tokens2 = [...primitives, ...semantic, ...adaptive];
3779
+ let categoryLabel = "all";
3780
+ if (options.category) {
3781
+ const cat = options.category.toLowerCase();
3782
+ if (cat === "primitives") {
3783
+ tokens2 = primitives;
3784
+ categoryLabel = "primitives";
3785
+ } else if (cat === "semantic") {
3786
+ tokens2 = semantic;
3787
+ categoryLabel = "semantic";
3788
+ } else if (cat === "adaptive") {
3789
+ tokens2 = adaptive;
3790
+ categoryLabel = "adaptive";
3791
+ } else {
3792
+ const err = {
3793
+ success: false,
3794
+ error: `Unknown category "${options.category}". Use: primitives, semantic, adaptive`
3795
+ };
3796
+ if (options.json) {
3797
+ console.error(JSON.stringify(err));
3798
+ } else {
3799
+ console.error(err.error);
3800
+ }
3801
+ process.exit(1);
3802
+ }
3803
+ }
3804
+ if (options.json) {
3805
+ console.log(
3806
+ JSON.stringify(
3807
+ {
3808
+ success: true,
3809
+ tokens: tokens2,
3810
+ summary: {
3811
+ total: tokens2.length,
3812
+ category: categoryLabel,
3813
+ allTotal: summary.total
3814
+ }
3815
+ },
3816
+ null,
3817
+ 2
3818
+ )
3819
+ );
3820
+ return;
3821
+ }
3822
+ console.log(
3823
+ `
3824
+ Visor Tokens (${categoryLabel}) \u2014 ${tokens2.length} tokens
3825
+ `
3826
+ );
3827
+ for (const t of tokens2) {
3828
+ console.log(` ${t.name} [${t.tier}]`);
3829
+ if (t.description) {
3830
+ console.log(` ${t.description}`);
3831
+ }
3832
+ console.log(` Light: ${t.defaultLight}`);
3833
+ console.log(` Dark: ${t.defaultDark}`);
3834
+ }
3835
+ console.log(
3836
+ `
3837
+ Total: ${tokens2.length} tokens shown${categoryLabel !== "all" ? ` (${summary.total} total across all tiers)` : ""}`
3838
+ );
3839
+ }
3840
+
2489
3841
  // src/index.ts
2490
- var program = new Command();
2491
- program.name("visor").description("CLI for the Visor design system").version("0.1.0");
3842
+ var program = new Command2();
3843
+ program.name("visor").description("CLI for the Visor design system").version("0.3.0");
3844
+ program.addCommand(checkCommand());
2492
3845
  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) => {
2493
3846
  initCommand(process.cwd(), options);
2494
3847
  });
2495
3848
  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
3849
  listCommand(process.cwd(), options);
2497
3850
  });
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 });
3851
+ 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("--dry-run", "preview what would be added without writing files").option("--json", "output structured JSON (for AI agents)").action((items, options) => {
3852
+ addCommand(items, process.cwd(), { overwrite: options.overwrite, category: options.category, block: options.block, dryRun: options.dryRun, json: options.json });
2500
3853
  });
2501
3854
  program.command("diff").description(
2502
3855
  "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) => {
3856
+ ).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
3857
  diffCommand(component, process.cwd(), options);
2505
3858
  });
3859
+ 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) => {
3860
+ await infoCommand(component, process.cwd(), options);
3861
+ });
3862
+ program.command("doctor").description("Run diagnostics on a Visor installation").option("--json", "Output as JSON (for AI agents)").action(async (options) => {
3863
+ await doctorCommand(process.cwd(), options, program.version() ?? "0.0.0");
3864
+ });
2506
3865
  var theme = program.command("theme").description("Theme management commands");
2507
3866
  theme.command("apply").description(
2508
3867
  "Read a .visor.yaml file and generate full CSS token overrides"
@@ -2563,8 +3922,22 @@ theme.command("sync").description(
2563
3922
  );
2564
3923
  var fonts = program.command("fonts").description("Font library management commands");
2565
3924
  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);
3925
+ (path2, options) => {
3926
+ fontsAddCommand(path2, options);
2568
3927
  }
2569
3928
  );
3929
+ var pattern = program.command("pattern").description("Work with composition patterns");
3930
+ pattern.command("list").description("List all composition patterns").option("--json", "Output as JSON").action((options) => {
3931
+ patternListCommand(process.cwd(), options);
3932
+ });
3933
+ pattern.command("info").argument("<name>", "Pattern name").description("Show full details for a composition pattern").option("--json", "Output as JSON").action((name, options) => {
3934
+ patternInfoCommand(name, process.cwd(), options);
3935
+ });
3936
+ 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) => {
3937
+ await suggestCommand(process.cwd(), options);
3938
+ });
3939
+ var tokens = program.command("tokens").description("Explore design tokens");
3940
+ 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) => {
3941
+ await tokensListCommand(process.cwd(), options);
3942
+ });
2570
3943
  program.parse();