@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/README.md +26 -0
- package/dist/CHANGELOG.json +594 -0
- package/dist/index.js +1674 -301
- package/dist/registry.json +17 -11
- package/dist/visor-manifest.json +3867 -530
- package/package.json +10 -3
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
|
|
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
|
|
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 =
|
|
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
|
|
57
|
-
import { join as
|
|
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 =
|
|
600
|
+
const pkgPath = join4(cwd, "package.json");
|
|
60
601
|
if (!existsSync2(pkgPath)) return null;
|
|
61
|
-
return JSON.parse(
|
|
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 =
|
|
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 =
|
|
226
|
-
const globalsDir =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
781
|
+
return join6(cwd, config.paths.deckComponents, relativePath);
|
|
325
782
|
}
|
|
326
783
|
relativePath = registryPath.replace(/^components\/ui\//, "");
|
|
327
|
-
return
|
|
784
|
+
return join6(cwd, config.paths.components, relativePath);
|
|
328
785
|
}
|
|
329
786
|
if (type === "registry:hook") {
|
|
330
787
|
relativePath = registryPath.replace(/^hooks\//, "");
|
|
331
|
-
return
|
|
788
|
+
return join6(cwd, config.paths.hooks, relativePath);
|
|
332
789
|
}
|
|
333
790
|
if (type === "registry:lib") {
|
|
334
791
|
relativePath = registryPath.replace(/^lib\//, "");
|
|
335
|
-
return
|
|
792
|
+
return join6(cwd, config.paths.lib, relativePath);
|
|
336
793
|
}
|
|
337
|
-
return
|
|
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
|
|
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(
|
|
1117
|
+
logger.item(`${prefix}skip ${file.path} (already exists)`);
|
|
651
1118
|
}
|
|
652
1119
|
skippedFiles.push(file.path);
|
|
653
1120
|
continue;
|
|
654
1121
|
}
|
|
655
|
-
|
|
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
|
-
|
|
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 (
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
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.
|
|
684
|
-
logger.info(
|
|
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 (
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
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.
|
|
699
|
-
logger.info(
|
|
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:
|
|
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
|
-
|
|
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
|
|
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 =
|
|
1527
|
+
const filePath = resolve2(cwd, file);
|
|
883
1528
|
let yamlContent;
|
|
884
1529
|
try {
|
|
885
|
-
yamlContent =
|
|
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 =
|
|
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
|
|
996
|
-
import { resolve as
|
|
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 =
|
|
1649
|
+
const filePath = resolve3(cwd, file ?? ".visor.yaml");
|
|
1005
1650
|
let yamlContent;
|
|
1006
1651
|
try {
|
|
1007
|
-
yamlContent =
|
|
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
|
|
1060
|
-
import { resolve as
|
|
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 =
|
|
1710
|
+
const filePath = resolve4(cwd, file);
|
|
1066
1711
|
let fileContent;
|
|
1067
1712
|
try {
|
|
1068
|
-
fileContent =
|
|
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
|
|
1151
|
-
console.log(`${prefix} ${code} ${issue.message}${
|
|
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
|
|
1156
|
-
import { resolve as
|
|
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 =
|
|
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
|
|
1240
|
-
const rootPath =
|
|
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 =
|
|
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 =
|
|
1249
|
-
if (existsSync5(dirPath) &&
|
|
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 =
|
|
1902
|
+
const resolved = resolve5(filePath);
|
|
1258
1903
|
if (seen.has(resolved)) return;
|
|
1259
1904
|
if (!existsSync5(resolved)) return;
|
|
1260
1905
|
try {
|
|
1261
|
-
const content =
|
|
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 =
|
|
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(
|
|
1934
|
+
scanDirForCSS(join7(dir, entry.name), files, seen, maxDepth - 1);
|
|
1290
1935
|
}
|
|
1291
|
-
} else if (entry.isFile() &&
|
|
1292
|
-
addFileIfExists(
|
|
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 =
|
|
2019
|
+
const fullPath = join7(targetDir, relPath);
|
|
1375
2020
|
if (!existsSync5(fullPath)) continue;
|
|
1376
2021
|
try {
|
|
1377
|
-
const content =
|
|
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,
|
|
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 =
|
|
2107
|
+
const pkgPath = join7(targetDir, "package.json");
|
|
1463
2108
|
if (!existsSync5(pkgPath)) return void 0;
|
|
1464
2109
|
try {
|
|
1465
|
-
const pkg = JSON.parse(
|
|
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 =
|
|
2146
|
+
const pkgPath = join7(targetDir, "package.json");
|
|
1502
2147
|
if (existsSync5(pkgPath)) {
|
|
1503
2148
|
try {
|
|
1504
|
-
const pkg = JSON.parse(
|
|
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 =
|
|
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
|
|
1628
|
-
import { resolve as
|
|
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
|
|
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 =
|
|
2287
|
+
let current = resolve6(startDir);
|
|
1643
2288
|
while (true) {
|
|
1644
|
-
if (existsSync6(
|
|
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 =
|
|
2384
|
+
const filePath = resolve7(cwd, file);
|
|
1740
2385
|
let yamlContent;
|
|
1741
2386
|
try {
|
|
1742
|
-
yamlContent =
|
|
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 =
|
|
1786
|
-
const cssFilePath =
|
|
1787
|
-
const globalsPath =
|
|
1788
|
-
const themeConfigPath =
|
|
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 =
|
|
1803
|
-
themeConfigContent =
|
|
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 ||
|
|
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
|
|
1900
|
-
import { join as
|
|
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 =
|
|
1933
|
-
const cssFilePath =
|
|
1934
|
-
const globalsPath =
|
|
1935
|
-
const themeConfigPath =
|
|
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 =
|
|
1950
|
-
themeConfigContent =
|
|
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
|
|
2647
|
+
readFileSync as readFileSync12,
|
|
2003
2648
|
writeFileSync as writeFileSync8,
|
|
2004
2649
|
mkdirSync as mkdirSync5,
|
|
2005
2650
|
existsSync as existsSync9,
|
|
2006
|
-
readdirSync as
|
|
2651
|
+
readdirSync as readdirSync3,
|
|
2007
2652
|
unlinkSync as unlinkSync2,
|
|
2008
2653
|
copyFileSync
|
|
2009
2654
|
} from "fs";
|
|
2010
|
-
import { join as
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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) =>
|
|
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
|
|
2056
|
-
|
|
2057
|
-
|
|
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 =
|
|
2134
|
-
const customThemesDir =
|
|
2135
|
-
const docsAppDir =
|
|
2136
|
-
const
|
|
2137
|
-
const
|
|
2138
|
-
const
|
|
2139
|
-
const
|
|
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 =
|
|
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
|
|
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 =
|
|
2193
|
-
|
|
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,
|
|
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
|
|
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) ?
|
|
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} (${
|
|
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(
|
|
2974
|
+
writeFileSync8(join11(docsAppDir, `${entry.slug}-theme.css`), entry.css, "utf-8");
|
|
2239
2975
|
}
|
|
2240
2976
|
for (const stale of staleCssFiles) {
|
|
2241
|
-
unlinkSync2(
|
|
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,
|
|
2989
|
+
copyFileSync(srcFile, join11(docsPublicThemesDir, filename));
|
|
2252
2990
|
}
|
|
2253
2991
|
for (const stale of stalePublicYamls) {
|
|
2254
|
-
unlinkSync2(
|
|
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:
|
|
2271
|
-
custom:
|
|
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: ${
|
|
2279
|
-
if (
|
|
2280
|
-
logger.item(`Custom: ${
|
|
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
|
|
2290
|
-
import { resolve as
|
|
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,
|
|
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 =
|
|
3072
|
+
const resolved = resolve8(inputPath);
|
|
2335
3073
|
if (!existsSync10(resolved)) {
|
|
2336
3074
|
throw new Error(`Path not found: ${resolved}`);
|
|
2337
3075
|
}
|
|
2338
|
-
const stat =
|
|
3076
|
+
const stat = statSync3(resolved);
|
|
2339
3077
|
if (stat.isFile()) {
|
|
2340
|
-
if (
|
|
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 =
|
|
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 =
|
|
2355
|
-
const ext =
|
|
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 =
|
|
2364
|
-
if (!existsSync10(resolved) || !
|
|
3101
|
+
const resolved = resolve8(inputPath);
|
|
3102
|
+
if (!existsSync10(resolved) || !statSync3(resolved).isDirectory()) {
|
|
2365
3103
|
return [];
|
|
2366
3104
|
}
|
|
2367
|
-
return
|
|
2368
|
-
const ext =
|
|
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 =
|
|
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 =
|
|
2418
|
-
const nonWoff2 =
|
|
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 =
|
|
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
|
|
2491
|
-
program.name("visor").description("CLI for the Visor design system").version("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
|
-
(
|
|
2567
|
-
fontsAddCommand(
|
|
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();
|