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