@loworbitstudio/visor 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +2570 -0
- package/dist/registry.json +3526 -0
- package/dist/visor-manifest.json +6575 -0
- package/package.json +54 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2570 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/init.ts
|
|
7
|
+
import { existsSync as existsSync3, writeFileSync as writeFileSync2, mkdirSync } from "fs";
|
|
8
|
+
import { join as join3, dirname } from "path";
|
|
9
|
+
|
|
10
|
+
// src/config/config.ts
|
|
11
|
+
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
12
|
+
import { join } from "path";
|
|
13
|
+
|
|
14
|
+
// src/config/defaults.ts
|
|
15
|
+
var DEFAULT_CONFIG = {
|
|
16
|
+
paths: {
|
|
17
|
+
components: "components/ui",
|
|
18
|
+
deckComponents: "components/deck",
|
|
19
|
+
blocks: "blocks",
|
|
20
|
+
hooks: "hooks",
|
|
21
|
+
lib: "lib"
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
var CONFIG_FILE = "visor.json";
|
|
25
|
+
|
|
26
|
+
// src/config/config.ts
|
|
27
|
+
function getConfigPath(cwd) {
|
|
28
|
+
return join(cwd, CONFIG_FILE);
|
|
29
|
+
}
|
|
30
|
+
function configExists(cwd) {
|
|
31
|
+
return existsSync(getConfigPath(cwd));
|
|
32
|
+
}
|
|
33
|
+
function loadConfig(cwd) {
|
|
34
|
+
const configPath = getConfigPath(cwd);
|
|
35
|
+
if (!existsSync(configPath)) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`No ${CONFIG_FILE} found. Run "visor init" first.`
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
41
|
+
const parsed = JSON.parse(raw);
|
|
42
|
+
return {
|
|
43
|
+
paths: {
|
|
44
|
+
...DEFAULT_CONFIG.paths,
|
|
45
|
+
...parsed.paths
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function writeConfig(cwd, config) {
|
|
50
|
+
const configPath = getConfigPath(cwd);
|
|
51
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// src/utils/packages.ts
|
|
55
|
+
import { execFileSync } from "child_process";
|
|
56
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
57
|
+
import { join as join2 } from "path";
|
|
58
|
+
function readPackageJson(cwd) {
|
|
59
|
+
const pkgPath = join2(cwd, "package.json");
|
|
60
|
+
if (!existsSync2(pkgPath)) return null;
|
|
61
|
+
return JSON.parse(readFileSync2(pkgPath, "utf-8"));
|
|
62
|
+
}
|
|
63
|
+
function isPackageInstalled(packageName, cwd) {
|
|
64
|
+
const pkg = readPackageJson(cwd);
|
|
65
|
+
if (!pkg) return false;
|
|
66
|
+
return !!(pkg.dependencies?.[packageName] || pkg.devDependencies?.[packageName]);
|
|
67
|
+
}
|
|
68
|
+
function hasVisorTokens(cwd) {
|
|
69
|
+
return isPackageInstalled("@loworbitstudio/visor-core", cwd);
|
|
70
|
+
}
|
|
71
|
+
function getUninstalledDeps(dependencies, cwd) {
|
|
72
|
+
return dependencies.filter((dep) => !isPackageInstalled(dep, cwd));
|
|
73
|
+
}
|
|
74
|
+
function installPackages(packages, cwd, dev = false) {
|
|
75
|
+
if (packages.length === 0) return true;
|
|
76
|
+
const args = ["install", dev ? "--save-dev" : "--save", ...packages];
|
|
77
|
+
try {
|
|
78
|
+
execFileSync("npm", args, { cwd, stdio: "inherit" });
|
|
79
|
+
return true;
|
|
80
|
+
} catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
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
|
+
// src/commands/templates/nextjs.ts
|
|
112
|
+
var NEXTJS_STARTER_YAML = `name: my-app
|
|
113
|
+
version: 1
|
|
114
|
+
colors:
|
|
115
|
+
primary: "#2563EB"
|
|
116
|
+
`;
|
|
117
|
+
|
|
118
|
+
// src/commands/init.ts
|
|
119
|
+
import { generateThemeData } from "@loworbitstudio/visor-theme-engine";
|
|
120
|
+
import { nextjsAdapter } from "@loworbitstudio/visor-theme-engine/adapters";
|
|
121
|
+
function initCommand(cwd, options) {
|
|
122
|
+
const json = options?.json ?? false;
|
|
123
|
+
const filesCreated = [];
|
|
124
|
+
const filesSkipped = [];
|
|
125
|
+
const warnings = [];
|
|
126
|
+
if (options?.template && options.template !== "nextjs") {
|
|
127
|
+
if (json) {
|
|
128
|
+
console.log(
|
|
129
|
+
JSON.stringify(
|
|
130
|
+
{ success: false, error: `Unknown template: ${options.template}. Available templates: nextjs` },
|
|
131
|
+
null,
|
|
132
|
+
2
|
|
133
|
+
)
|
|
134
|
+
);
|
|
135
|
+
} else {
|
|
136
|
+
logger.error(`Unknown template: ${options.template}`);
|
|
137
|
+
logger.info("Available templates: nextjs");
|
|
138
|
+
}
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
if (configExists(cwd)) {
|
|
142
|
+
filesSkipped.push("visor.json");
|
|
143
|
+
if (!json) {
|
|
144
|
+
logger.warn("visor.json already exists. Skipping config creation.");
|
|
145
|
+
}
|
|
146
|
+
} else {
|
|
147
|
+
writeConfig(cwd, DEFAULT_CONFIG);
|
|
148
|
+
filesCreated.push("visor.json");
|
|
149
|
+
if (!json) {
|
|
150
|
+
logger.success("Created visor.json");
|
|
151
|
+
logger.blank();
|
|
152
|
+
logger.info("Default paths:");
|
|
153
|
+
logger.item(`components \u2192 ${DEFAULT_CONFIG.paths.components}`);
|
|
154
|
+
logger.item(`deck components \u2192 ${DEFAULT_CONFIG.paths.deckComponents}`);
|
|
155
|
+
logger.item(`blocks \u2192 ${DEFAULT_CONFIG.paths.blocks}`);
|
|
156
|
+
logger.item(`hooks \u2192 ${DEFAULT_CONFIG.paths.hooks}`);
|
|
157
|
+
logger.item(`lib \u2192 ${DEFAULT_CONFIG.paths.lib}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (options?.template === "nextjs") {
|
|
161
|
+
scaffoldNextjs(cwd, json, filesCreated, filesSkipped);
|
|
162
|
+
}
|
|
163
|
+
const missingTokens = !hasVisorTokens(cwd);
|
|
164
|
+
if (missingTokens) {
|
|
165
|
+
const warning = "@loworbitstudio/visor-core is not installed. Components require it for styling.";
|
|
166
|
+
warnings.push(warning);
|
|
167
|
+
if (!json) {
|
|
168
|
+
logger.blank();
|
|
169
|
+
logger.warn(warning);
|
|
170
|
+
logger.info(" For Next.js: re-run with --template nextjs to generate tokens inline.");
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (json) {
|
|
174
|
+
const nextSteps = [];
|
|
175
|
+
if (options?.template === "nextjs") {
|
|
176
|
+
nextSteps.push("Customize colors in .visor.yaml");
|
|
177
|
+
nextSteps.push("Add FOWT prevention script to your layout.tsx <head>");
|
|
178
|
+
nextSteps.push("Run: npx visor add button \u2014 to add your first component");
|
|
179
|
+
} else {
|
|
180
|
+
nextSteps.push("Run: npx visor add button \u2014 to add your first component");
|
|
181
|
+
}
|
|
182
|
+
if (missingTokens) {
|
|
183
|
+
nextSteps.push("Re-run with --template nextjs to generate tokens inline (no npm package needed)");
|
|
184
|
+
}
|
|
185
|
+
console.log(
|
|
186
|
+
JSON.stringify(
|
|
187
|
+
{
|
|
188
|
+
success: true,
|
|
189
|
+
config: DEFAULT_CONFIG,
|
|
190
|
+
files: { created: filesCreated, skipped: filesSkipped },
|
|
191
|
+
warnings,
|
|
192
|
+
nextSteps
|
|
193
|
+
},
|
|
194
|
+
null,
|
|
195
|
+
2
|
|
196
|
+
)
|
|
197
|
+
);
|
|
198
|
+
process.exit(0);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
function scaffoldNextjs(cwd, json, filesCreated, filesSkipped) {
|
|
202
|
+
if (!json) {
|
|
203
|
+
logger.blank();
|
|
204
|
+
logger.info("Scaffolding NextJS theme...");
|
|
205
|
+
}
|
|
206
|
+
const yamlPath = join3(cwd, ".visor.yaml");
|
|
207
|
+
if (existsSync3(yamlPath)) {
|
|
208
|
+
filesSkipped.push(".visor.yaml");
|
|
209
|
+
if (!json) {
|
|
210
|
+
logger.warn(".visor.yaml already exists. Skipping.");
|
|
211
|
+
}
|
|
212
|
+
} else {
|
|
213
|
+
writeFileSync2(yamlPath, NEXTJS_STARTER_YAML, "utf-8");
|
|
214
|
+
filesCreated.push(".visor.yaml");
|
|
215
|
+
if (!json) {
|
|
216
|
+
logger.success("Created .visor.yaml");
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const data = generateThemeData(NEXTJS_STARTER_YAML);
|
|
220
|
+
const css = nextjsAdapter({
|
|
221
|
+
primitives: data.primitives,
|
|
222
|
+
tokens: data.tokens,
|
|
223
|
+
config: data.config
|
|
224
|
+
});
|
|
225
|
+
const globalsPath = join3(cwd, "app", "globals.css");
|
|
226
|
+
const globalsDir = dirname(globalsPath);
|
|
227
|
+
if (existsSync3(globalsPath)) {
|
|
228
|
+
filesSkipped.push("app/globals.css");
|
|
229
|
+
if (!json) {
|
|
230
|
+
logger.warn("app/globals.css already exists. Skipping.");
|
|
231
|
+
}
|
|
232
|
+
} else {
|
|
233
|
+
mkdirSync(globalsDir, { recursive: true });
|
|
234
|
+
writeFileSync2(globalsPath, css, "utf-8");
|
|
235
|
+
filesCreated.push("app/globals.css");
|
|
236
|
+
if (!json) {
|
|
237
|
+
logger.success("Created app/globals.css with theme tokens");
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (!json) {
|
|
241
|
+
logger.blank();
|
|
242
|
+
logger.info("Next steps:");
|
|
243
|
+
logger.item("Customize colors in .visor.yaml");
|
|
244
|
+
logger.item("Add FOWT prevention script to your layout.tsx <head>");
|
|
245
|
+
logger.item("Run: npx visor add button \u2014 to add your first component");
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// src/registry/resolve.ts
|
|
250
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
251
|
+
import { join as join4, dirname as dirname2 } from "path";
|
|
252
|
+
import { fileURLToPath } from "url";
|
|
253
|
+
var __dirname = dirname2(fileURLToPath(import.meta.url));
|
|
254
|
+
var cachedRegistry = null;
|
|
255
|
+
function loadRegistry() {
|
|
256
|
+
if (cachedRegistry) return cachedRegistry;
|
|
257
|
+
const registryPath = join4(__dirname, "registry.json");
|
|
258
|
+
const raw = readFileSync3(registryPath, "utf-8");
|
|
259
|
+
cachedRegistry = JSON.parse(raw);
|
|
260
|
+
return cachedRegistry;
|
|
261
|
+
}
|
|
262
|
+
function findItem(registry, name) {
|
|
263
|
+
return registry.items.find((item) => item.name === name);
|
|
264
|
+
}
|
|
265
|
+
function resolveTransitiveDeps(registry, names) {
|
|
266
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
267
|
+
const queue = [...names];
|
|
268
|
+
while (queue.length > 0) {
|
|
269
|
+
const name = queue.shift();
|
|
270
|
+
if (resolved.has(name)) continue;
|
|
271
|
+
const item = findItem(registry, name);
|
|
272
|
+
if (!item) {
|
|
273
|
+
throw new Error(`Registry item "${name}" not found.`);
|
|
274
|
+
}
|
|
275
|
+
resolved.set(name, item);
|
|
276
|
+
if (item.registryDependencies) {
|
|
277
|
+
for (const dep of item.registryDependencies) {
|
|
278
|
+
if (!resolved.has(dep)) {
|
|
279
|
+
queue.push(dep);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return Array.from(resolved.values());
|
|
285
|
+
}
|
|
286
|
+
function collectDependencies(items) {
|
|
287
|
+
const deps = /* @__PURE__ */ new Set();
|
|
288
|
+
const devDeps = /* @__PURE__ */ new Set();
|
|
289
|
+
for (const item of items) {
|
|
290
|
+
if (item.dependencies) {
|
|
291
|
+
for (const dep of item.dependencies) {
|
|
292
|
+
deps.add(dep);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (item.devDependencies) {
|
|
296
|
+
for (const dep of item.devDependencies) {
|
|
297
|
+
devDeps.add(dep);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return {
|
|
302
|
+
dependencies: Array.from(deps).sort(),
|
|
303
|
+
devDependencies: Array.from(devDeps).sort()
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// src/utils/fs.ts
|
|
308
|
+
import {
|
|
309
|
+
writeFileSync as writeFileSync3,
|
|
310
|
+
readFileSync as readFileSync4,
|
|
311
|
+
existsSync as existsSync4,
|
|
312
|
+
mkdirSync as mkdirSync2
|
|
313
|
+
} from "fs";
|
|
314
|
+
import { dirname as dirname3, join as join5 } from "path";
|
|
315
|
+
function resolveOutputPath(registryPath, type, config, cwd) {
|
|
316
|
+
let relativePath;
|
|
317
|
+
if (type === "registry:block") {
|
|
318
|
+
relativePath = registryPath.replace(/^blocks\//, "");
|
|
319
|
+
return join5(cwd, config.paths.blocks, relativePath);
|
|
320
|
+
}
|
|
321
|
+
if (type === "registry:ui") {
|
|
322
|
+
if (registryPath.startsWith("components/deck/")) {
|
|
323
|
+
relativePath = registryPath.replace(/^components\/deck\//, "");
|
|
324
|
+
return join5(cwd, config.paths.deckComponents, relativePath);
|
|
325
|
+
}
|
|
326
|
+
relativePath = registryPath.replace(/^components\/ui\//, "");
|
|
327
|
+
return join5(cwd, config.paths.components, relativePath);
|
|
328
|
+
}
|
|
329
|
+
if (type === "registry:hook") {
|
|
330
|
+
relativePath = registryPath.replace(/^hooks\//, "");
|
|
331
|
+
return join5(cwd, config.paths.hooks, relativePath);
|
|
332
|
+
}
|
|
333
|
+
if (type === "registry:lib") {
|
|
334
|
+
relativePath = registryPath.replace(/^lib\//, "");
|
|
335
|
+
return join5(cwd, config.paths.lib, relativePath);
|
|
336
|
+
}
|
|
337
|
+
return join5(cwd, registryPath);
|
|
338
|
+
}
|
|
339
|
+
function writeFile(filePath, content) {
|
|
340
|
+
const dir = dirname3(filePath);
|
|
341
|
+
if (!existsSync4(dir)) {
|
|
342
|
+
mkdirSync2(dir, { recursive: true });
|
|
343
|
+
}
|
|
344
|
+
writeFileSync3(filePath, content, "utf-8");
|
|
345
|
+
}
|
|
346
|
+
function readFile(filePath) {
|
|
347
|
+
if (!existsSync4(filePath)) return null;
|
|
348
|
+
return readFileSync4(filePath, "utf-8");
|
|
349
|
+
}
|
|
350
|
+
function fileExists(filePath) {
|
|
351
|
+
return existsSync4(filePath);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// src/commands/list.ts
|
|
355
|
+
var TYPE_LABELS = {
|
|
356
|
+
"registry:ui": "Components",
|
|
357
|
+
"registry:hook": "Hooks",
|
|
358
|
+
"registry:lib": "Utilities",
|
|
359
|
+
"registry:block": "Blocks",
|
|
360
|
+
"registry:page": "Pages",
|
|
361
|
+
"registry:theme": "Themes",
|
|
362
|
+
"registry:style": "Styles"
|
|
363
|
+
};
|
|
364
|
+
var CATEGORY_LABELS = {
|
|
365
|
+
deck: "Deck Components",
|
|
366
|
+
authentication: "Authentication Blocks"
|
|
367
|
+
};
|
|
368
|
+
function listCommand(cwd, options = {}) {
|
|
369
|
+
const json = options.json ?? false;
|
|
370
|
+
let registry;
|
|
371
|
+
try {
|
|
372
|
+
registry = loadRegistry();
|
|
373
|
+
} catch (error) {
|
|
374
|
+
if (json) {
|
|
375
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
376
|
+
console.log(JSON.stringify({ success: false, error: message }, null, 2));
|
|
377
|
+
process.exit(1);
|
|
378
|
+
}
|
|
379
|
+
throw error;
|
|
380
|
+
}
|
|
381
|
+
let registryItems = registry.items;
|
|
382
|
+
if (options.category) {
|
|
383
|
+
registryItems = registry.items.filter(
|
|
384
|
+
(item) => item.category === options.category
|
|
385
|
+
);
|
|
386
|
+
if (registryItems.length === 0) {
|
|
387
|
+
if (json) {
|
|
388
|
+
console.log(
|
|
389
|
+
JSON.stringify(
|
|
390
|
+
{ success: false, error: `No items found in category "${options.category}".` },
|
|
391
|
+
null,
|
|
392
|
+
2
|
|
393
|
+
)
|
|
394
|
+
);
|
|
395
|
+
process.exit(1);
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
logger.error(`No items found in category "${options.category}".`);
|
|
399
|
+
process.exit(1);
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
const hasConfig = configExists(cwd);
|
|
404
|
+
const groupMap = /* @__PURE__ */ new Map();
|
|
405
|
+
for (const item of registryItems) {
|
|
406
|
+
const groupKey = item.category ? `${item.type}||${item.category}` : item.type;
|
|
407
|
+
const existing = groupMap.get(groupKey);
|
|
408
|
+
if (existing) {
|
|
409
|
+
existing.items.push(item);
|
|
410
|
+
} else {
|
|
411
|
+
groupMap.set(groupKey, {
|
|
412
|
+
type: item.type,
|
|
413
|
+
category: item.category,
|
|
414
|
+
items: [item]
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
const config = hasConfig ? loadConfig(cwd) : null;
|
|
419
|
+
if (json) {
|
|
420
|
+
const items = registryItems.map((item) => {
|
|
421
|
+
let installed = false;
|
|
422
|
+
if (config) {
|
|
423
|
+
const firstFile = item.files[0];
|
|
424
|
+
if (firstFile) {
|
|
425
|
+
const outputPath = resolveOutputPath(
|
|
426
|
+
firstFile.path,
|
|
427
|
+
firstFile.type,
|
|
428
|
+
config,
|
|
429
|
+
cwd
|
|
430
|
+
);
|
|
431
|
+
installed = fileExists(outputPath);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return {
|
|
435
|
+
type: item.type,
|
|
436
|
+
category: item.category ?? null,
|
|
437
|
+
name: item.name,
|
|
438
|
+
description: item.description ?? null,
|
|
439
|
+
installed
|
|
440
|
+
};
|
|
441
|
+
});
|
|
442
|
+
const byType = {};
|
|
443
|
+
for (const item of items) {
|
|
444
|
+
byType[item.type] = (byType[item.type] ?? 0) + 1;
|
|
445
|
+
}
|
|
446
|
+
console.log(
|
|
447
|
+
JSON.stringify(
|
|
448
|
+
{
|
|
449
|
+
success: true,
|
|
450
|
+
items,
|
|
451
|
+
summary: {
|
|
452
|
+
total: items.length,
|
|
453
|
+
installed: items.filter((i) => i.installed).length,
|
|
454
|
+
byType
|
|
455
|
+
}
|
|
456
|
+
},
|
|
457
|
+
null,
|
|
458
|
+
2
|
|
459
|
+
)
|
|
460
|
+
);
|
|
461
|
+
process.exit(0);
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
for (const group of groupMap.values()) {
|
|
465
|
+
const label = group.category ? CATEGORY_LABELS[group.category] ?? `${TYPE_LABELS[group.type] ?? group.type} (${group.category})` : TYPE_LABELS[group.type] ?? group.type;
|
|
466
|
+
logger.heading(`${label} (${group.items.length})`);
|
|
467
|
+
logger.blank();
|
|
468
|
+
for (const item of group.items) {
|
|
469
|
+
let installed = false;
|
|
470
|
+
if (config) {
|
|
471
|
+
const firstFile = item.files[0];
|
|
472
|
+
if (firstFile) {
|
|
473
|
+
const outputPath = resolveOutputPath(
|
|
474
|
+
firstFile.path,
|
|
475
|
+
firstFile.type,
|
|
476
|
+
config,
|
|
477
|
+
cwd
|
|
478
|
+
);
|
|
479
|
+
installed = fileExists(outputPath);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
const status = installed ? " (installed)" : "";
|
|
483
|
+
const name = item.name.padEnd(24);
|
|
484
|
+
const desc = item.description ?? "";
|
|
485
|
+
logger.info(` ${name} ${desc}${status}`);
|
|
486
|
+
}
|
|
487
|
+
logger.blank();
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// src/commands/add.ts
|
|
492
|
+
function addCommand(components, cwd, options = {}) {
|
|
493
|
+
const json = options.json ?? false;
|
|
494
|
+
let autoInitialized = false;
|
|
495
|
+
if (!configExists(cwd)) {
|
|
496
|
+
writeConfig(cwd, DEFAULT_CONFIG);
|
|
497
|
+
autoInitialized = true;
|
|
498
|
+
if (!json) {
|
|
499
|
+
logger.info("No visor.json found \u2014 created one with default paths.");
|
|
500
|
+
logger.blank();
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
let config;
|
|
504
|
+
let registry;
|
|
505
|
+
try {
|
|
506
|
+
config = loadConfig(cwd);
|
|
507
|
+
registry = loadRegistry();
|
|
508
|
+
} catch (error) {
|
|
509
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
510
|
+
if (json) {
|
|
511
|
+
console.log(JSON.stringify({ success: false, error: message }, null, 2));
|
|
512
|
+
} else {
|
|
513
|
+
logger.error(message);
|
|
514
|
+
}
|
|
515
|
+
process.exit(1);
|
|
516
|
+
}
|
|
517
|
+
if (options.block && components.length > 0) {
|
|
518
|
+
for (const name of components) {
|
|
519
|
+
const item = registry.items.find((i) => i.name === name);
|
|
520
|
+
if (item && item.type !== "registry:block") {
|
|
521
|
+
if (json) {
|
|
522
|
+
console.log(
|
|
523
|
+
JSON.stringify(
|
|
524
|
+
{ success: false, error: `"${name}" is not a block. Remove the --block flag to install it as a component.` },
|
|
525
|
+
null,
|
|
526
|
+
2
|
|
527
|
+
)
|
|
528
|
+
);
|
|
529
|
+
} else {
|
|
530
|
+
logger.error(
|
|
531
|
+
`"${name}" is not a block. Remove the --block flag to install it as a component.`
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
process.exit(1);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
let itemNames = components;
|
|
539
|
+
if (options.category) {
|
|
540
|
+
if (components.length > 0) {
|
|
541
|
+
if (json) {
|
|
542
|
+
console.log(
|
|
543
|
+
JSON.stringify(
|
|
544
|
+
{ success: false, error: "Cannot use --category with individual component names. Use one or the other." },
|
|
545
|
+
null,
|
|
546
|
+
2
|
|
547
|
+
)
|
|
548
|
+
);
|
|
549
|
+
} else {
|
|
550
|
+
logger.error(
|
|
551
|
+
"Cannot use --category with individual component names. Use one or the other."
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
process.exit(1);
|
|
555
|
+
}
|
|
556
|
+
const categoryItems = registry.items.filter(
|
|
557
|
+
(item) => item.category === options.category
|
|
558
|
+
);
|
|
559
|
+
if (categoryItems.length === 0) {
|
|
560
|
+
if (json) {
|
|
561
|
+
console.log(
|
|
562
|
+
JSON.stringify(
|
|
563
|
+
{ success: false, error: `No items found in category "${options.category}".` },
|
|
564
|
+
null,
|
|
565
|
+
2
|
|
566
|
+
)
|
|
567
|
+
);
|
|
568
|
+
} else {
|
|
569
|
+
logger.error(`No items found in category "${options.category}".`);
|
|
570
|
+
}
|
|
571
|
+
process.exit(1);
|
|
572
|
+
}
|
|
573
|
+
itemNames = categoryItems.map((item) => item.name);
|
|
574
|
+
if (!json) {
|
|
575
|
+
logger.info(
|
|
576
|
+
`Category "${options.category}": ${itemNames.length} item(s) found`
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
if (itemNames.length === 0) {
|
|
581
|
+
if (options.block) {
|
|
582
|
+
const blockItems = registry.items.filter(
|
|
583
|
+
(item) => item.type === "registry:block"
|
|
584
|
+
);
|
|
585
|
+
if (json) {
|
|
586
|
+
console.log(
|
|
587
|
+
JSON.stringify(
|
|
588
|
+
{
|
|
589
|
+
success: false,
|
|
590
|
+
error: blockItems.length === 0 ? "No blocks available in the registry." : `No block name specified. Available blocks: ${blockItems.map((i) => i.name).join(", ")}`
|
|
591
|
+
},
|
|
592
|
+
null,
|
|
593
|
+
2
|
|
594
|
+
)
|
|
595
|
+
);
|
|
596
|
+
} else {
|
|
597
|
+
if (blockItems.length === 0) {
|
|
598
|
+
logger.error("No blocks available in the registry.");
|
|
599
|
+
} else {
|
|
600
|
+
logger.error("No block name specified. Available blocks:");
|
|
601
|
+
for (const item of blockItems) {
|
|
602
|
+
logger.info(` ${item.name}`);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
process.exit(1);
|
|
607
|
+
}
|
|
608
|
+
if (json) {
|
|
609
|
+
console.log(
|
|
610
|
+
JSON.stringify(
|
|
611
|
+
{ success: false, error: "No items specified. Provide item names or use --category." },
|
|
612
|
+
null,
|
|
613
|
+
2
|
|
614
|
+
)
|
|
615
|
+
);
|
|
616
|
+
} else {
|
|
617
|
+
logger.error("No items specified. Provide item names or use --category.");
|
|
618
|
+
}
|
|
619
|
+
process.exit(1);
|
|
620
|
+
}
|
|
621
|
+
let items;
|
|
622
|
+
try {
|
|
623
|
+
items = resolveTransitiveDeps(registry, itemNames);
|
|
624
|
+
} catch (error) {
|
|
625
|
+
if (json) {
|
|
626
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
627
|
+
console.log(JSON.stringify({ success: false, error: message }, null, 2));
|
|
628
|
+
process.exit(1);
|
|
629
|
+
}
|
|
630
|
+
throw error;
|
|
631
|
+
}
|
|
632
|
+
if (!json) {
|
|
633
|
+
logger.info(
|
|
634
|
+
`Resolving ${itemNames.length} item(s) \u2192 ${items.length} total (with dependencies)`
|
|
635
|
+
);
|
|
636
|
+
logger.blank();
|
|
637
|
+
}
|
|
638
|
+
const writtenFiles = [];
|
|
639
|
+
const skippedFiles = [];
|
|
640
|
+
for (const item of items) {
|
|
641
|
+
for (const file of item.files) {
|
|
642
|
+
const outputPath = resolveOutputPath(
|
|
643
|
+
file.path,
|
|
644
|
+
file.type,
|
|
645
|
+
config,
|
|
646
|
+
cwd
|
|
647
|
+
);
|
|
648
|
+
if (fileExists(outputPath) && !options.overwrite) {
|
|
649
|
+
if (!json) {
|
|
650
|
+
logger.item(`skip ${file.path} (already exists)`);
|
|
651
|
+
}
|
|
652
|
+
skippedFiles.push(file.path);
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
writeFile(outputPath, file.content);
|
|
656
|
+
if (!json) {
|
|
657
|
+
logger.success(file.path);
|
|
658
|
+
}
|
|
659
|
+
writtenFiles.push(file.path);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
if (!json) {
|
|
663
|
+
logger.blank();
|
|
664
|
+
logger.info(
|
|
665
|
+
`Files: ${writtenFiles.length} written, ${skippedFiles.length} skipped`
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
const { dependencies, devDependencies } = collectDependencies(items);
|
|
669
|
+
const uninstalledDeps = getUninstalledDeps(dependencies, cwd);
|
|
670
|
+
const uninstalledDevDeps = getUninstalledDeps(devDependencies, cwd);
|
|
671
|
+
const installedDeps = [];
|
|
672
|
+
const failedDeps = [];
|
|
673
|
+
if (uninstalledDeps.length > 0) {
|
|
674
|
+
if (!json) {
|
|
675
|
+
logger.blank();
|
|
676
|
+
logger.info("Installing dependencies...");
|
|
677
|
+
}
|
|
678
|
+
if (installPackages(uninstalledDeps, cwd)) {
|
|
679
|
+
installedDeps.push(...uninstalledDeps);
|
|
680
|
+
} else {
|
|
681
|
+
failedDeps.push(...uninstalledDeps);
|
|
682
|
+
if (!json) {
|
|
683
|
+
logger.warn("Some dependencies failed to install. Install them manually:");
|
|
684
|
+
logger.info(` npm install ${uninstalledDeps.join(" ")}`);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
if (uninstalledDevDeps.length > 0) {
|
|
689
|
+
if (!json) {
|
|
690
|
+
logger.blank();
|
|
691
|
+
logger.info("Installing dev dependencies...");
|
|
692
|
+
}
|
|
693
|
+
if (installPackages(uninstalledDevDeps, cwd, true)) {
|
|
694
|
+
installedDeps.push(...uninstalledDevDeps);
|
|
695
|
+
} else {
|
|
696
|
+
failedDeps.push(...uninstalledDevDeps);
|
|
697
|
+
if (!json) {
|
|
698
|
+
logger.warn("Some dev dependencies failed to install. Install them manually:");
|
|
699
|
+
logger.info(` npm install --save-dev ${uninstalledDevDeps.join(" ")}`);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
const warnings = [];
|
|
704
|
+
if (!hasVisorTokens(cwd)) {
|
|
705
|
+
const warning = "@loworbitstudio/visor-core is not installed. Components require it for styling.";
|
|
706
|
+
warnings.push(warning);
|
|
707
|
+
if (!json) {
|
|
708
|
+
logger.blank();
|
|
709
|
+
logger.warn(warning);
|
|
710
|
+
logger.info(" For Next.js: npx @loworbitstudio/visor init --template nextjs");
|
|
711
|
+
logger.info(" This generates all tokens inline \u2014 no npm package needed.");
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
if (json) {
|
|
715
|
+
console.log(
|
|
716
|
+
JSON.stringify(
|
|
717
|
+
{
|
|
718
|
+
success: true,
|
|
719
|
+
autoInitialized,
|
|
720
|
+
requested: itemNames,
|
|
721
|
+
resolved: items.map((i) => i.name),
|
|
722
|
+
files: { written: writtenFiles, skipped: skippedFiles },
|
|
723
|
+
dependencies: { installed: installedDeps, failed: failedDeps },
|
|
724
|
+
warnings
|
|
725
|
+
},
|
|
726
|
+
null,
|
|
727
|
+
2
|
|
728
|
+
)
|
|
729
|
+
);
|
|
730
|
+
process.exit(0);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// src/utils/diff.ts
|
|
735
|
+
import { createTwoFilesPatch } from "diff";
|
|
736
|
+
function computeDiff(filePath, localContent, registryContent) {
|
|
737
|
+
return createTwoFilesPatch(
|
|
738
|
+
`a/${filePath}`,
|
|
739
|
+
`b/${filePath}`,
|
|
740
|
+
localContent,
|
|
741
|
+
registryContent,
|
|
742
|
+
"local",
|
|
743
|
+
"registry"
|
|
744
|
+
);
|
|
745
|
+
}
|
|
746
|
+
function hasDifferences(localContent, registryContent) {
|
|
747
|
+
return localContent !== registryContent;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// src/commands/diff.ts
|
|
751
|
+
function diffCommand(componentName, cwd, options = {}) {
|
|
752
|
+
const json = options.json ?? false;
|
|
753
|
+
let config;
|
|
754
|
+
let registry;
|
|
755
|
+
try {
|
|
756
|
+
config = loadConfig(cwd);
|
|
757
|
+
registry = loadRegistry();
|
|
758
|
+
} catch (error) {
|
|
759
|
+
if (json) {
|
|
760
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
761
|
+
console.log(JSON.stringify({ success: false, error: message }, null, 2));
|
|
762
|
+
process.exit(1);
|
|
763
|
+
}
|
|
764
|
+
throw error;
|
|
765
|
+
}
|
|
766
|
+
const itemsToDiff = componentName ? (() => {
|
|
767
|
+
const item = findItem(registry, componentName);
|
|
768
|
+
if (!item) {
|
|
769
|
+
if (json) {
|
|
770
|
+
console.log(
|
|
771
|
+
JSON.stringify(
|
|
772
|
+
{ success: false, error: `Component "${componentName}" not found in registry.` },
|
|
773
|
+
null,
|
|
774
|
+
2
|
|
775
|
+
)
|
|
776
|
+
);
|
|
777
|
+
} else {
|
|
778
|
+
logger.error(`Component "${componentName}" not found in registry.`);
|
|
779
|
+
}
|
|
780
|
+
process.exit(1);
|
|
781
|
+
}
|
|
782
|
+
return [item];
|
|
783
|
+
})() : registry.items;
|
|
784
|
+
let totalDiffs = 0;
|
|
785
|
+
let totalFiles = 0;
|
|
786
|
+
const diffs = [];
|
|
787
|
+
for (const item of itemsToDiff) {
|
|
788
|
+
let itemHasDiff = false;
|
|
789
|
+
for (const file of item.files) {
|
|
790
|
+
const outputPath = resolveOutputPath(
|
|
791
|
+
file.path,
|
|
792
|
+
file.type,
|
|
793
|
+
config,
|
|
794
|
+
cwd
|
|
795
|
+
);
|
|
796
|
+
const localContent = readFile(outputPath);
|
|
797
|
+
if (localContent === null) continue;
|
|
798
|
+
totalFiles++;
|
|
799
|
+
const fileHasDiff = hasDifferences(localContent, file.content);
|
|
800
|
+
if (json) {
|
|
801
|
+
const diffText = fileHasDiff ? computeDiff(file.path, localContent, file.content) : "";
|
|
802
|
+
diffs.push({
|
|
803
|
+
file: file.path,
|
|
804
|
+
component: item.name,
|
|
805
|
+
hasDifferences: fileHasDiff,
|
|
806
|
+
diff: diffText
|
|
807
|
+
});
|
|
808
|
+
if (fileHasDiff) {
|
|
809
|
+
totalDiffs++;
|
|
810
|
+
}
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
813
|
+
if (!fileHasDiff) continue;
|
|
814
|
+
if (!itemHasDiff) {
|
|
815
|
+
logger.heading(item.name);
|
|
816
|
+
itemHasDiff = true;
|
|
817
|
+
}
|
|
818
|
+
const diff = computeDiff(file.path, localContent, file.content);
|
|
819
|
+
console.log(diff);
|
|
820
|
+
totalDiffs++;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
if (json) {
|
|
824
|
+
console.log(
|
|
825
|
+
JSON.stringify(
|
|
826
|
+
{
|
|
827
|
+
success: true,
|
|
828
|
+
diffs,
|
|
829
|
+
summary: {
|
|
830
|
+
totalFiles,
|
|
831
|
+
filesWithDiffs: totalDiffs
|
|
832
|
+
}
|
|
833
|
+
},
|
|
834
|
+
null,
|
|
835
|
+
2
|
|
836
|
+
)
|
|
837
|
+
);
|
|
838
|
+
process.exit(0);
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
if (totalDiffs === 0) {
|
|
842
|
+
if (totalFiles === 0) {
|
|
843
|
+
logger.info("No installed components found.");
|
|
844
|
+
} else {
|
|
845
|
+
logger.success("All files match the registry. No differences found.");
|
|
846
|
+
}
|
|
847
|
+
} else {
|
|
848
|
+
logger.blank();
|
|
849
|
+
logger.info(`${totalDiffs} file(s) with differences`);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// src/commands/theme-apply.ts
|
|
854
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, mkdirSync as mkdirSync3 } from "fs";
|
|
855
|
+
import { resolve, dirname as dirname4 } from "path";
|
|
856
|
+
import { generateTheme, generateThemeData as generateThemeData2 } from "@loworbitstudio/visor-theme-engine";
|
|
857
|
+
import {
|
|
858
|
+
nextjsAdapter as nextjsAdapter2,
|
|
859
|
+
fumadocsAdapter,
|
|
860
|
+
deckAdapter,
|
|
861
|
+
docsAdapter
|
|
862
|
+
} from "@loworbitstudio/visor-theme-engine/adapters";
|
|
863
|
+
function defaultOutputPath(adapter, themeName) {
|
|
864
|
+
switch (adapter) {
|
|
865
|
+
case "nextjs":
|
|
866
|
+
return "globals.css";
|
|
867
|
+
case "fumadocs":
|
|
868
|
+
return "visor-fumadocs-bridge.css";
|
|
869
|
+
case "deck": {
|
|
870
|
+
const slug = (themeName ?? "theme").toLowerCase().replace(/\s+/g, "-");
|
|
871
|
+
return `visor-deck-${slug}.css`;
|
|
872
|
+
}
|
|
873
|
+
case "docs": {
|
|
874
|
+
const slug = (themeName ?? "theme").toLowerCase().replace(/\s+/g, "-");
|
|
875
|
+
return `${slug}-theme.css`;
|
|
876
|
+
}
|
|
877
|
+
default:
|
|
878
|
+
return "visor-theme.css";
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
function themeApplyCommand(file, cwd, options) {
|
|
882
|
+
const filePath = resolve(cwd, file);
|
|
883
|
+
let yamlContent;
|
|
884
|
+
try {
|
|
885
|
+
yamlContent = readFileSync5(filePath, "utf-8");
|
|
886
|
+
} catch {
|
|
887
|
+
if (options.json) {
|
|
888
|
+
console.log(
|
|
889
|
+
JSON.stringify({
|
|
890
|
+
success: false,
|
|
891
|
+
error: `Could not read file: ${filePath}`
|
|
892
|
+
})
|
|
893
|
+
);
|
|
894
|
+
} else {
|
|
895
|
+
logger.error(`Could not read file: ${filePath}`);
|
|
896
|
+
logger.info("Make sure the file exists and is readable.");
|
|
897
|
+
}
|
|
898
|
+
process.exit(2);
|
|
899
|
+
}
|
|
900
|
+
let css;
|
|
901
|
+
let themeName;
|
|
902
|
+
let sections;
|
|
903
|
+
try {
|
|
904
|
+
if (options.adapter) {
|
|
905
|
+
const data = generateThemeData2(yamlContent);
|
|
906
|
+
themeName = data.config.name;
|
|
907
|
+
const adapterInput = {
|
|
908
|
+
primitives: data.primitives,
|
|
909
|
+
tokens: data.tokens,
|
|
910
|
+
config: data.config
|
|
911
|
+
};
|
|
912
|
+
switch (options.adapter) {
|
|
913
|
+
case "nextjs":
|
|
914
|
+
css = nextjsAdapter2(adapterInput);
|
|
915
|
+
break;
|
|
916
|
+
case "fumadocs":
|
|
917
|
+
css = fumadocsAdapter(adapterInput);
|
|
918
|
+
break;
|
|
919
|
+
case "deck":
|
|
920
|
+
css = deckAdapter(adapterInput);
|
|
921
|
+
break;
|
|
922
|
+
case "docs":
|
|
923
|
+
css = docsAdapter(adapterInput);
|
|
924
|
+
break;
|
|
925
|
+
default:
|
|
926
|
+
throw new Error(`Unknown adapter: ${options.adapter}`);
|
|
927
|
+
}
|
|
928
|
+
} else {
|
|
929
|
+
const output = generateTheme(yamlContent);
|
|
930
|
+
css = output.fullBundleCss;
|
|
931
|
+
sections = {
|
|
932
|
+
primitives: output.primitivesCss.length,
|
|
933
|
+
semantic: output.semanticCss.length,
|
|
934
|
+
light: output.lightCss.length,
|
|
935
|
+
dark: output.darkCss.length,
|
|
936
|
+
fullBundle: output.fullBundleCss.length
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
} catch (err) {
|
|
940
|
+
const message = err instanceof Error ? err.message : "Unknown error generating theme";
|
|
941
|
+
if (options.json) {
|
|
942
|
+
console.log(JSON.stringify({ success: false, error: message }));
|
|
943
|
+
} else {
|
|
944
|
+
logger.error("Failed to generate theme.");
|
|
945
|
+
logger.info(message);
|
|
946
|
+
}
|
|
947
|
+
process.exit(1);
|
|
948
|
+
}
|
|
949
|
+
const outputFile = options.output ?? defaultOutputPath(options.adapter, themeName);
|
|
950
|
+
const outputPath = resolve(cwd, outputFile);
|
|
951
|
+
const outputDir = dirname4(outputPath);
|
|
952
|
+
try {
|
|
953
|
+
mkdirSync3(outputDir, { recursive: true });
|
|
954
|
+
writeFileSync4(outputPath, css, "utf-8");
|
|
955
|
+
} catch {
|
|
956
|
+
if (options.json) {
|
|
957
|
+
console.log(
|
|
958
|
+
JSON.stringify({
|
|
959
|
+
success: false,
|
|
960
|
+
error: `Could not write to: ${outputPath}`
|
|
961
|
+
})
|
|
962
|
+
);
|
|
963
|
+
} else {
|
|
964
|
+
logger.error(`Could not write to: ${outputPath}`);
|
|
965
|
+
}
|
|
966
|
+
process.exit(2);
|
|
967
|
+
}
|
|
968
|
+
if (options.json) {
|
|
969
|
+
const jsonResult = {
|
|
970
|
+
success: true,
|
|
971
|
+
file: outputPath
|
|
972
|
+
};
|
|
973
|
+
if (options.adapter) {
|
|
974
|
+
jsonResult.adapter = options.adapter;
|
|
975
|
+
jsonResult.size = css.length;
|
|
976
|
+
}
|
|
977
|
+
if (sections) {
|
|
978
|
+
jsonResult.sections = sections;
|
|
979
|
+
}
|
|
980
|
+
console.log(JSON.stringify(jsonResult));
|
|
981
|
+
} else {
|
|
982
|
+
logger.success(`Theme CSS generated: ${outputPath}`);
|
|
983
|
+
if (options.adapter) {
|
|
984
|
+
logger.info(`Adapter: ${options.adapter}`);
|
|
985
|
+
}
|
|
986
|
+
logger.item(`Size: ${formatSize(css.length)}`);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
function formatSize(bytes) {
|
|
990
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
991
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// src/commands/theme-export.ts
|
|
995
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
996
|
+
import { resolve as resolve2 } from "path";
|
|
997
|
+
import {
|
|
998
|
+
parseConfig,
|
|
999
|
+
resolveConfig,
|
|
1000
|
+
generatePrimitives,
|
|
1001
|
+
exportTheme
|
|
1002
|
+
} from "@loworbitstudio/visor-theme-engine";
|
|
1003
|
+
function themeExportCommand(file, cwd, options) {
|
|
1004
|
+
const filePath = resolve2(cwd, file ?? ".visor.yaml");
|
|
1005
|
+
let yamlContent;
|
|
1006
|
+
try {
|
|
1007
|
+
yamlContent = readFileSync6(filePath, "utf-8");
|
|
1008
|
+
} catch {
|
|
1009
|
+
if (options.json) {
|
|
1010
|
+
console.log(
|
|
1011
|
+
JSON.stringify({
|
|
1012
|
+
success: false,
|
|
1013
|
+
error: `Could not read file: ${filePath}`
|
|
1014
|
+
})
|
|
1015
|
+
);
|
|
1016
|
+
} else {
|
|
1017
|
+
logger.error(`Could not read file: ${filePath}`);
|
|
1018
|
+
logger.info(
|
|
1019
|
+
"Make sure a .visor.yaml file exists in the current directory, or specify a path."
|
|
1020
|
+
);
|
|
1021
|
+
}
|
|
1022
|
+
process.exit(2);
|
|
1023
|
+
}
|
|
1024
|
+
let config;
|
|
1025
|
+
try {
|
|
1026
|
+
config = parseConfig(yamlContent);
|
|
1027
|
+
} catch (err) {
|
|
1028
|
+
const message = err instanceof Error ? err.message : "Unknown error parsing config";
|
|
1029
|
+
if (options.json) {
|
|
1030
|
+
console.log(JSON.stringify({ success: false, error: message }));
|
|
1031
|
+
} else {
|
|
1032
|
+
logger.error("Failed to parse theme config.");
|
|
1033
|
+
logger.info(message);
|
|
1034
|
+
}
|
|
1035
|
+
process.exit(1);
|
|
1036
|
+
}
|
|
1037
|
+
const resolved = resolveConfig(config);
|
|
1038
|
+
const primitives = generatePrimitives(resolved);
|
|
1039
|
+
const exportedYaml = exportTheme(primitives, resolved);
|
|
1040
|
+
const format = options.format ?? "yaml";
|
|
1041
|
+
if (options.json) {
|
|
1042
|
+
if (format === "json") {
|
|
1043
|
+
const parsed = parseConfig(exportedYaml);
|
|
1044
|
+
console.log(JSON.stringify({ success: true, theme: parsed }));
|
|
1045
|
+
} else {
|
|
1046
|
+
console.log(JSON.stringify({ success: true, yaml: exportedYaml }));
|
|
1047
|
+
}
|
|
1048
|
+
} else {
|
|
1049
|
+
if (format === "json") {
|
|
1050
|
+
const parsed = parseConfig(exportedYaml);
|
|
1051
|
+
console.log(JSON.stringify(parsed, null, 2));
|
|
1052
|
+
} else {
|
|
1053
|
+
console.log(exportedYaml);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// src/commands/theme-validate.ts
|
|
1059
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
1060
|
+
import { resolve as resolve3 } from "path";
|
|
1061
|
+
import { parse as parseYaml } from "yaml";
|
|
1062
|
+
import { validate } from "@loworbitstudio/visor-theme-engine";
|
|
1063
|
+
import pc2 from "picocolors";
|
|
1064
|
+
function themeValidateCommand(file, cwd, options) {
|
|
1065
|
+
const filePath = resolve3(cwd, file);
|
|
1066
|
+
let fileContent;
|
|
1067
|
+
try {
|
|
1068
|
+
fileContent = readFileSync7(filePath, "utf-8");
|
|
1069
|
+
} catch {
|
|
1070
|
+
if (options.json) {
|
|
1071
|
+
console.log(
|
|
1072
|
+
JSON.stringify({
|
|
1073
|
+
valid: false,
|
|
1074
|
+
errors: [
|
|
1075
|
+
{
|
|
1076
|
+
severity: "error",
|
|
1077
|
+
code: "FILE_NOT_FOUND",
|
|
1078
|
+
message: `Could not read file: ${filePath}`
|
|
1079
|
+
}
|
|
1080
|
+
],
|
|
1081
|
+
warnings: []
|
|
1082
|
+
})
|
|
1083
|
+
);
|
|
1084
|
+
} else {
|
|
1085
|
+
logger.error(`Could not read file: ${filePath}`);
|
|
1086
|
+
logger.info("Make sure the file exists and is readable.");
|
|
1087
|
+
}
|
|
1088
|
+
process.exit(2);
|
|
1089
|
+
}
|
|
1090
|
+
let parsed;
|
|
1091
|
+
try {
|
|
1092
|
+
parsed = parseYaml(fileContent);
|
|
1093
|
+
} catch (err) {
|
|
1094
|
+
const message = err instanceof Error ? err.message : "Invalid YAML syntax";
|
|
1095
|
+
if (options.json) {
|
|
1096
|
+
console.log(
|
|
1097
|
+
JSON.stringify({
|
|
1098
|
+
valid: false,
|
|
1099
|
+
errors: [
|
|
1100
|
+
{
|
|
1101
|
+
severity: "error",
|
|
1102
|
+
code: "YAML_PARSE_ERROR",
|
|
1103
|
+
message
|
|
1104
|
+
}
|
|
1105
|
+
],
|
|
1106
|
+
warnings: []
|
|
1107
|
+
})
|
|
1108
|
+
);
|
|
1109
|
+
} else {
|
|
1110
|
+
logger.error("Invalid YAML syntax.");
|
|
1111
|
+
logger.info(message);
|
|
1112
|
+
}
|
|
1113
|
+
process.exit(1);
|
|
1114
|
+
}
|
|
1115
|
+
const result = validate(parsed);
|
|
1116
|
+
if (options.json) {
|
|
1117
|
+
console.log(JSON.stringify(result));
|
|
1118
|
+
process.exit(result.valid ? 0 : 1);
|
|
1119
|
+
}
|
|
1120
|
+
if (result.valid && result.warnings.length === 0) {
|
|
1121
|
+
logger.success("Theme is valid. No issues found.");
|
|
1122
|
+
process.exit(0);
|
|
1123
|
+
}
|
|
1124
|
+
if (result.valid) {
|
|
1125
|
+
logger.success("Theme is valid.");
|
|
1126
|
+
logger.blank();
|
|
1127
|
+
}
|
|
1128
|
+
if (result.errors.length > 0) {
|
|
1129
|
+
logger.heading(`${result.errors.length} error(s):`);
|
|
1130
|
+
for (const err of result.errors) {
|
|
1131
|
+
printIssue(err);
|
|
1132
|
+
}
|
|
1133
|
+
logger.blank();
|
|
1134
|
+
}
|
|
1135
|
+
if (result.warnings.length > 0) {
|
|
1136
|
+
logger.heading(`${result.warnings.length} warning(s):`);
|
|
1137
|
+
for (const warn of result.warnings) {
|
|
1138
|
+
printIssue(warn);
|
|
1139
|
+
}
|
|
1140
|
+
logger.blank();
|
|
1141
|
+
}
|
|
1142
|
+
if (!result.valid) {
|
|
1143
|
+
logger.error("Validation failed. Fix the errors above before applying this theme.");
|
|
1144
|
+
process.exit(1);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
function printIssue(issue) {
|
|
1148
|
+
const prefix = issue.severity === "error" ? pc2.red(" ERROR") : pc2.yellow(" WARN ");
|
|
1149
|
+
const code = pc2.dim(`[${issue.code}]`);
|
|
1150
|
+
const path = issue.path ? pc2.dim(` (${issue.path})`) : "";
|
|
1151
|
+
console.log(`${prefix} ${code} ${issue.message}${path}`);
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
// src/commands/theme-extract.ts
|
|
1155
|
+
import { readFileSync as readFileSync8, writeFileSync as writeFileSync5, existsSync as existsSync5, readdirSync, statSync } from "fs";
|
|
1156
|
+
import { resolve as resolve4, join as join6, basename, extname, relative } from "path";
|
|
1157
|
+
import { stringify as stringifyYaml } from "yaml";
|
|
1158
|
+
import {
|
|
1159
|
+
extractFromCSS,
|
|
1160
|
+
validate as validate2
|
|
1161
|
+
} from "@loworbitstudio/visor-theme-engine";
|
|
1162
|
+
var CSS_FILE_PATTERNS = [
|
|
1163
|
+
"globals.css",
|
|
1164
|
+
"global.css",
|
|
1165
|
+
"tokens.css",
|
|
1166
|
+
"variables.css",
|
|
1167
|
+
"theme.css",
|
|
1168
|
+
"design-tokens.css",
|
|
1169
|
+
"primitives.css",
|
|
1170
|
+
"semantic.css",
|
|
1171
|
+
"adaptive.css"
|
|
1172
|
+
];
|
|
1173
|
+
var CSS_DIRS = [
|
|
1174
|
+
"src",
|
|
1175
|
+
"app",
|
|
1176
|
+
"styles",
|
|
1177
|
+
"css",
|
|
1178
|
+
"src/styles",
|
|
1179
|
+
"src/app",
|
|
1180
|
+
"src/css",
|
|
1181
|
+
"packages/tokens",
|
|
1182
|
+
"packages/design-tokens"
|
|
1183
|
+
];
|
|
1184
|
+
function themeExtractCommand(cwd, options) {
|
|
1185
|
+
const targetDir = resolve4(cwd, options.from ?? ".");
|
|
1186
|
+
if (!existsSync5(targetDir)) {
|
|
1187
|
+
if (options.json) {
|
|
1188
|
+
console.log(JSON.stringify({ success: false, error: `Directory not found: ${targetDir}` }));
|
|
1189
|
+
} else {
|
|
1190
|
+
logger.error(`Directory not found: ${targetDir}`);
|
|
1191
|
+
}
|
|
1192
|
+
process.exit(2);
|
|
1193
|
+
}
|
|
1194
|
+
if (!options.json) {
|
|
1195
|
+
logger.heading("Visor Theme Extractor");
|
|
1196
|
+
logger.info(`Scanning: ${targetDir}`);
|
|
1197
|
+
logger.blank();
|
|
1198
|
+
}
|
|
1199
|
+
const cssFiles = collectCSSFiles(targetDir);
|
|
1200
|
+
if (cssFiles.length === 0) {
|
|
1201
|
+
if (options.json) {
|
|
1202
|
+
console.log(JSON.stringify({
|
|
1203
|
+
success: false,
|
|
1204
|
+
error: "No CSS files found to extract from."
|
|
1205
|
+
}));
|
|
1206
|
+
} else {
|
|
1207
|
+
logger.error("No CSS files found to extract from.");
|
|
1208
|
+
logger.info("Make sure the target directory contains .css files with custom properties.");
|
|
1209
|
+
}
|
|
1210
|
+
process.exit(2);
|
|
1211
|
+
}
|
|
1212
|
+
if (!options.json) {
|
|
1213
|
+
logger.info(`Found ${cssFiles.length} CSS file(s):`);
|
|
1214
|
+
for (const f of cssFiles) {
|
|
1215
|
+
logger.item(relative(targetDir, f.path));
|
|
1216
|
+
}
|
|
1217
|
+
logger.blank();
|
|
1218
|
+
}
|
|
1219
|
+
const themeName = inferThemeName(targetDir);
|
|
1220
|
+
const result = extractFromCSS(cssFiles, themeName);
|
|
1221
|
+
resolveVarFontReferences(result, targetDir);
|
|
1222
|
+
const fontHints = extractFontHints(targetDir);
|
|
1223
|
+
if (fontHints && !result.config.typography) {
|
|
1224
|
+
result.config.typography = fontHints;
|
|
1225
|
+
}
|
|
1226
|
+
let validationResult;
|
|
1227
|
+
if (options.runValidation) {
|
|
1228
|
+
validationResult = validate2(result.config);
|
|
1229
|
+
}
|
|
1230
|
+
if (options.json) {
|
|
1231
|
+
outputJSON(result, validationResult);
|
|
1232
|
+
} else {
|
|
1233
|
+
outputYAML(result, options.output, cwd, validationResult);
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
function collectCSSFiles(targetDir) {
|
|
1237
|
+
const files = [];
|
|
1238
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1239
|
+
for (const pattern of CSS_FILE_PATTERNS) {
|
|
1240
|
+
const rootPath = join6(targetDir, pattern);
|
|
1241
|
+
addFileIfExists(rootPath, files, seen);
|
|
1242
|
+
for (const dir of CSS_DIRS) {
|
|
1243
|
+
const dirPath = join6(targetDir, dir, pattern);
|
|
1244
|
+
addFileIfExists(dirPath, files, seen);
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
for (const dir of CSS_DIRS) {
|
|
1248
|
+
const dirPath = join6(targetDir, dir);
|
|
1249
|
+
if (existsSync5(dirPath) && statSync(dirPath).isDirectory()) {
|
|
1250
|
+
scanDirForCSS(dirPath, files, seen, 2);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
scanDirForCSS(targetDir, files, seen, 0);
|
|
1254
|
+
return files;
|
|
1255
|
+
}
|
|
1256
|
+
function addFileIfExists(filePath, files, seen) {
|
|
1257
|
+
const resolved = resolve4(filePath);
|
|
1258
|
+
if (seen.has(resolved)) return;
|
|
1259
|
+
if (!existsSync5(resolved)) return;
|
|
1260
|
+
try {
|
|
1261
|
+
const content = readFileSync8(resolved, "utf-8");
|
|
1262
|
+
if (content.includes("--")) {
|
|
1263
|
+
files.push({ path: resolved, content });
|
|
1264
|
+
seen.add(resolved);
|
|
1265
|
+
}
|
|
1266
|
+
} catch {
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
function scanDirForCSS(dir, files, seen, maxDepth) {
|
|
1270
|
+
if (!existsSync5(dir)) return;
|
|
1271
|
+
const SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
1272
|
+
"node_modules",
|
|
1273
|
+
".next",
|
|
1274
|
+
".nuxt",
|
|
1275
|
+
"dist",
|
|
1276
|
+
"build",
|
|
1277
|
+
".git",
|
|
1278
|
+
".cache",
|
|
1279
|
+
"coverage",
|
|
1280
|
+
".turbo",
|
|
1281
|
+
".vercel"
|
|
1282
|
+
]);
|
|
1283
|
+
try {
|
|
1284
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
1285
|
+
for (const entry of entries) {
|
|
1286
|
+
if (entry.isDirectory()) {
|
|
1287
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
1288
|
+
if (maxDepth > 0) {
|
|
1289
|
+
scanDirForCSS(join6(dir, entry.name), files, seen, maxDepth - 1);
|
|
1290
|
+
}
|
|
1291
|
+
} else if (entry.isFile() && extname(entry.name) === ".css") {
|
|
1292
|
+
addFileIfExists(join6(dir, entry.name), files, seen);
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
} catch {
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
var LAYOUT_FILE_PATHS = [
|
|
1299
|
+
"src/app/layout.tsx",
|
|
1300
|
+
"src/app/layout.ts",
|
|
1301
|
+
"src/app/layout.jsx",
|
|
1302
|
+
"src/app/layout.js",
|
|
1303
|
+
"app/layout.tsx",
|
|
1304
|
+
"app/layout.ts",
|
|
1305
|
+
"app/layout.jsx",
|
|
1306
|
+
"app/layout.js",
|
|
1307
|
+
"src/pages/_app.tsx",
|
|
1308
|
+
"src/pages/_app.ts",
|
|
1309
|
+
"src/pages/_app.jsx",
|
|
1310
|
+
"src/pages/_app.js",
|
|
1311
|
+
"pages/_app.tsx",
|
|
1312
|
+
"pages/_app.ts",
|
|
1313
|
+
"pages/_app.jsx",
|
|
1314
|
+
"pages/_app.js"
|
|
1315
|
+
];
|
|
1316
|
+
var NEXT_FONT_MAP = {
|
|
1317
|
+
Inter: "Inter",
|
|
1318
|
+
Roboto: "Roboto",
|
|
1319
|
+
Open_Sans: "Open Sans",
|
|
1320
|
+
Lato: "Lato",
|
|
1321
|
+
Poppins: "Poppins",
|
|
1322
|
+
Montserrat: "Montserrat",
|
|
1323
|
+
Raleway: "Raleway",
|
|
1324
|
+
Nunito: "Nunito",
|
|
1325
|
+
Playfair_Display: "Playfair Display",
|
|
1326
|
+
Source_Code_Pro: "Source Code Pro",
|
|
1327
|
+
Fira_Code: "Fira Code",
|
|
1328
|
+
JetBrains_Mono: "JetBrains Mono",
|
|
1329
|
+
Roboto_Mono: "Roboto Mono",
|
|
1330
|
+
IBM_Plex_Mono: "IBM Plex Mono",
|
|
1331
|
+
IBM_Plex_Sans: "IBM Plex Sans",
|
|
1332
|
+
DM_Sans: "DM Sans",
|
|
1333
|
+
Space_Grotesk: "Space Grotesk",
|
|
1334
|
+
Geist: "Geist",
|
|
1335
|
+
Geist_Mono: "Geist Mono",
|
|
1336
|
+
Manrope: "Manrope",
|
|
1337
|
+
Outfit: "Outfit",
|
|
1338
|
+
Plus_Jakarta_Sans: "Plus Jakarta Sans",
|
|
1339
|
+
Work_Sans: "Work Sans",
|
|
1340
|
+
Rubik: "Rubik",
|
|
1341
|
+
Sora: "Sora",
|
|
1342
|
+
Lexend: "Lexend"
|
|
1343
|
+
};
|
|
1344
|
+
function resolveVarFontReferences(result, targetDir) {
|
|
1345
|
+
const typo = result.config.typography;
|
|
1346
|
+
if (!typo) return;
|
|
1347
|
+
const hasVarRef = typo.heading?.family?.startsWith("var(") || typo.body?.family?.startsWith("var(") || typo.mono?.family?.startsWith("var(");
|
|
1348
|
+
if (!hasVarRef) return;
|
|
1349
|
+
const fontMap = parseNextFontFromLayouts(targetDir);
|
|
1350
|
+
if (fontMap.size === 0) return;
|
|
1351
|
+
if (typo.heading?.family?.startsWith("var(")) {
|
|
1352
|
+
const varName = extractVarName(typo.heading.family);
|
|
1353
|
+
const resolved = fontMap.get(varName);
|
|
1354
|
+
if (resolved) typo.heading.family = resolved;
|
|
1355
|
+
}
|
|
1356
|
+
if (typo.body?.family?.startsWith("var(")) {
|
|
1357
|
+
const varName = extractVarName(typo.body.family);
|
|
1358
|
+
const resolved = fontMap.get(varName);
|
|
1359
|
+
if (resolved) typo.body.family = resolved;
|
|
1360
|
+
}
|
|
1361
|
+
if (typo.mono?.family?.startsWith("var(")) {
|
|
1362
|
+
const varName = extractVarName(typo.mono.family);
|
|
1363
|
+
const resolved = fontMap.get(varName);
|
|
1364
|
+
if (resolved) typo.mono.family = resolved;
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
function extractVarName(varExpr) {
|
|
1368
|
+
const match = varExpr.match(/var\(\s*(--[\w-]+)/);
|
|
1369
|
+
return match ? match[1] : varExpr;
|
|
1370
|
+
}
|
|
1371
|
+
function parseNextFontFromLayouts(targetDir) {
|
|
1372
|
+
const fontMap = /* @__PURE__ */ new Map();
|
|
1373
|
+
for (const relPath of LAYOUT_FILE_PATHS) {
|
|
1374
|
+
const fullPath = join6(targetDir, relPath);
|
|
1375
|
+
if (!existsSync5(fullPath)) continue;
|
|
1376
|
+
try {
|
|
1377
|
+
const content = readFileSync8(fullPath, "utf-8");
|
|
1378
|
+
parseNextFontDeclarations(content, fontMap);
|
|
1379
|
+
} catch {
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
return fontMap;
|
|
1383
|
+
}
|
|
1384
|
+
function parseNextFontDeclarations(content, fontMap) {
|
|
1385
|
+
const googleImportRe = /import\s*\{([^}]+)\}\s*from\s*["']next\/font\/google["']/g;
|
|
1386
|
+
let importMatch;
|
|
1387
|
+
const importedFonts = [];
|
|
1388
|
+
const googleImportMatches = content.matchAll(googleImportRe);
|
|
1389
|
+
for (const m of googleImportMatches) {
|
|
1390
|
+
const names = m[1].split(",").map((n) => n.trim()).filter(Boolean);
|
|
1391
|
+
importedFonts.push(...names);
|
|
1392
|
+
}
|
|
1393
|
+
for (const fontName of importedFonts) {
|
|
1394
|
+
const family = NEXT_FONT_MAP[fontName] ?? fontName.replace(/_/g, " ");
|
|
1395
|
+
const callRe = new RegExp(
|
|
1396
|
+
`(?:const|let|var)\\s+\\w+\\s*=\\s*${fontName}\\s*\\(\\s*\\{([\\s\\S]*?)\\}\\s*\\)`,
|
|
1397
|
+
"m"
|
|
1398
|
+
);
|
|
1399
|
+
const callMatch = content.match(callRe);
|
|
1400
|
+
if (callMatch) {
|
|
1401
|
+
const varMatch = callMatch[1].match(/variable\s*:\s*["'](--[\w-]+)["']/);
|
|
1402
|
+
if (varMatch) {
|
|
1403
|
+
fontMap.set(varMatch[1], family);
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
const localImportRe = /import\s+(\w+)\s+from\s*["']next\/font\/local["']/;
|
|
1408
|
+
const localImportMatch = content.match(localImportRe);
|
|
1409
|
+
if (localImportMatch) {
|
|
1410
|
+
const localFnName = localImportMatch[1];
|
|
1411
|
+
const localCallRe = new RegExp(
|
|
1412
|
+
`(?:const|let|var)\\s+\\w+\\s*=\\s*${localFnName}\\s*\\(\\s*\\{([\\s\\S]*?)\\}\\s*\\)`,
|
|
1413
|
+
"gm"
|
|
1414
|
+
);
|
|
1415
|
+
const localCallMatches = content.matchAll(localCallRe);
|
|
1416
|
+
for (const localCallMatch of localCallMatches) {
|
|
1417
|
+
const block = localCallMatch[1];
|
|
1418
|
+
const varMatch = block.match(/variable\s*:\s*["'](--[\w-]+)["']/);
|
|
1419
|
+
if (!varMatch) continue;
|
|
1420
|
+
const varName = varMatch[1];
|
|
1421
|
+
const srcMatch = block.match(/src\s*:\s*["']([^"']+)["']/);
|
|
1422
|
+
if (srcMatch) {
|
|
1423
|
+
const srcPath = srcMatch[1];
|
|
1424
|
+
const fileName = basename(srcPath, extname(srcPath));
|
|
1425
|
+
const fontBaseName = fileName.replace(/[-_](Variable|Regular|Bold|Light|Medium|SemiBold|ExtraBold|Thin|Black|Italic).*$/i, "").replace(/[-_]/g, " ").trim();
|
|
1426
|
+
if (fontBaseName) {
|
|
1427
|
+
fontMap.set(varName, fontBaseName);
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
var FONT_PACKAGE_MAP = {
|
|
1434
|
+
"@fontsource/inter": "Inter",
|
|
1435
|
+
"@fontsource/roboto": "Roboto",
|
|
1436
|
+
"@fontsource/open-sans": "Open Sans",
|
|
1437
|
+
"@fontsource/lato": "Lato",
|
|
1438
|
+
"@fontsource/poppins": "Poppins",
|
|
1439
|
+
"@fontsource/montserrat": "Montserrat",
|
|
1440
|
+
"@fontsource/raleway": "Raleway",
|
|
1441
|
+
"@fontsource/nunito": "Nunito",
|
|
1442
|
+
"@fontsource/playfair-display": "Playfair Display",
|
|
1443
|
+
"@fontsource/source-code-pro": "Source Code Pro",
|
|
1444
|
+
"@fontsource/fira-code": "Fira Code",
|
|
1445
|
+
"@fontsource/jetbrains-mono": "JetBrains Mono",
|
|
1446
|
+
"@fontsource-variable/inter": "Inter",
|
|
1447
|
+
"@fontsource-variable/roboto": "Roboto",
|
|
1448
|
+
"@fontsource-variable/open-sans": "Open Sans",
|
|
1449
|
+
"next/font": ""
|
|
1450
|
+
// handled separately
|
|
1451
|
+
};
|
|
1452
|
+
var MONO_FONT_NAMES = /* @__PURE__ */ new Set([
|
|
1453
|
+
"Source Code Pro",
|
|
1454
|
+
"Fira Code",
|
|
1455
|
+
"JetBrains Mono",
|
|
1456
|
+
"Roboto Mono",
|
|
1457
|
+
"SF Mono",
|
|
1458
|
+
"Cascadia Code",
|
|
1459
|
+
"IBM Plex Mono"
|
|
1460
|
+
]);
|
|
1461
|
+
function extractFontHints(targetDir) {
|
|
1462
|
+
const pkgPath = join6(targetDir, "package.json");
|
|
1463
|
+
if (!existsSync5(pkgPath)) return void 0;
|
|
1464
|
+
try {
|
|
1465
|
+
const pkg = JSON.parse(readFileSync8(pkgPath, "utf-8"));
|
|
1466
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
1467
|
+
const fonts2 = [];
|
|
1468
|
+
for (const [dep, _] of Object.entries(allDeps)) {
|
|
1469
|
+
const family = FONT_PACKAGE_MAP[dep];
|
|
1470
|
+
if (family) fonts2.push(family);
|
|
1471
|
+
if (dep.startsWith("@fontsource/") && !FONT_PACKAGE_MAP[dep]) {
|
|
1472
|
+
const name = dep.replace("@fontsource/", "").split("-").map(
|
|
1473
|
+
(w) => w.charAt(0).toUpperCase() + w.slice(1)
|
|
1474
|
+
).join(" ");
|
|
1475
|
+
fonts2.push(name);
|
|
1476
|
+
}
|
|
1477
|
+
if (dep.startsWith("@fontsource-variable/") && !FONT_PACKAGE_MAP[dep]) {
|
|
1478
|
+
const name = dep.replace("@fontsource-variable/", "").split("-").map(
|
|
1479
|
+
(w) => w.charAt(0).toUpperCase() + w.slice(1)
|
|
1480
|
+
).join(" ");
|
|
1481
|
+
fonts2.push(name);
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
if (fonts2.length === 0) return void 0;
|
|
1485
|
+
const result = {};
|
|
1486
|
+
const monoFonts = fonts2.filter((f) => MONO_FONT_NAMES.has(f));
|
|
1487
|
+
const bodyFonts = fonts2.filter((f) => !MONO_FONT_NAMES.has(f));
|
|
1488
|
+
if (bodyFonts.length > 0) {
|
|
1489
|
+
result.heading = { family: bodyFonts[0] };
|
|
1490
|
+
result.body = { family: bodyFonts[0] };
|
|
1491
|
+
}
|
|
1492
|
+
if (monoFonts.length > 0) {
|
|
1493
|
+
result.mono = { family: monoFonts[0] };
|
|
1494
|
+
}
|
|
1495
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
1496
|
+
} catch {
|
|
1497
|
+
return void 0;
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
function inferThemeName(targetDir) {
|
|
1501
|
+
const pkgPath = join6(targetDir, "package.json");
|
|
1502
|
+
if (existsSync5(pkgPath)) {
|
|
1503
|
+
try {
|
|
1504
|
+
const pkg = JSON.parse(readFileSync8(pkgPath, "utf-8"));
|
|
1505
|
+
if (pkg.name) {
|
|
1506
|
+
const name = pkg.name.replace(/^@[\w-]+\//, "");
|
|
1507
|
+
return `${name}-theme`;
|
|
1508
|
+
}
|
|
1509
|
+
} catch {
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
return `${basename(targetDir)}-theme`;
|
|
1513
|
+
}
|
|
1514
|
+
function confidenceComment(confidence) {
|
|
1515
|
+
return `# confidence: ${confidence}`;
|
|
1516
|
+
}
|
|
1517
|
+
function outputJSON(result, validationResult) {
|
|
1518
|
+
const output = {
|
|
1519
|
+
success: true,
|
|
1520
|
+
config: result.config,
|
|
1521
|
+
extraction: {
|
|
1522
|
+
tokens: result.tokens,
|
|
1523
|
+
unmapped: result.unmapped,
|
|
1524
|
+
warnings: result.warnings,
|
|
1525
|
+
summary: {
|
|
1526
|
+
totalTokens: result.tokens.length,
|
|
1527
|
+
highConfidence: result.tokens.filter((t) => t.confidence === "high").length,
|
|
1528
|
+
mediumConfidence: result.tokens.filter((t) => t.confidence === "medium").length,
|
|
1529
|
+
lowConfidence: result.tokens.filter((t) => t.confidence === "low").length,
|
|
1530
|
+
unmappedCount: result.unmapped.length
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
};
|
|
1534
|
+
if (validationResult) {
|
|
1535
|
+
output.validation = validationResult;
|
|
1536
|
+
}
|
|
1537
|
+
console.log(JSON.stringify(output, null, 2));
|
|
1538
|
+
}
|
|
1539
|
+
function outputYAML(result, outputPath, cwd, validationResult) {
|
|
1540
|
+
const yamlStr = buildAnnotatedYAML(result);
|
|
1541
|
+
const outFile = resolve4(cwd, outputPath ?? ".visor.yaml");
|
|
1542
|
+
const high = result.tokens.filter((t) => t.confidence === "high").length;
|
|
1543
|
+
const med = result.tokens.filter((t) => t.confidence === "medium").length;
|
|
1544
|
+
const low = result.tokens.filter((t) => t.confidence === "low").length;
|
|
1545
|
+
logger.info("Extraction summary:");
|
|
1546
|
+
logger.item(`${result.tokens.length} tokens extracted`);
|
|
1547
|
+
logger.item(` High confidence: ${high}`);
|
|
1548
|
+
logger.item(` Medium confidence: ${med}`);
|
|
1549
|
+
logger.item(` Low confidence: ${low}`);
|
|
1550
|
+
logger.item(`${result.unmapped.length} unmapped tokens`);
|
|
1551
|
+
logger.blank();
|
|
1552
|
+
if (result.warnings.length > 0) {
|
|
1553
|
+
logger.warn("Warnings:");
|
|
1554
|
+
for (const w of result.warnings) {
|
|
1555
|
+
logger.item(w);
|
|
1556
|
+
}
|
|
1557
|
+
logger.blank();
|
|
1558
|
+
}
|
|
1559
|
+
if (result.unmapped.length > 0) {
|
|
1560
|
+
logger.info("Unmapped tokens (review manually):");
|
|
1561
|
+
for (const u of result.unmapped.slice(0, 10)) {
|
|
1562
|
+
logger.item(`${u.name}: ${u.value} (${u.context})`);
|
|
1563
|
+
}
|
|
1564
|
+
if (result.unmapped.length > 10) {
|
|
1565
|
+
logger.item(`... and ${result.unmapped.length - 10} more`);
|
|
1566
|
+
}
|
|
1567
|
+
logger.blank();
|
|
1568
|
+
}
|
|
1569
|
+
writeFileSync5(outFile, yamlStr, "utf-8");
|
|
1570
|
+
logger.success(`Theme written to ${relative(cwd, outFile)}`);
|
|
1571
|
+
if (validationResult) {
|
|
1572
|
+
logger.blank();
|
|
1573
|
+
if (validationResult.valid) {
|
|
1574
|
+
logger.success("Validation passed");
|
|
1575
|
+
} else {
|
|
1576
|
+
logger.warn("Validation issues:");
|
|
1577
|
+
for (const err of validationResult.errors) {
|
|
1578
|
+
logger.error(` ${err.code}: ${err.message}`);
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
if (validationResult.warnings.length > 0) {
|
|
1582
|
+
for (const w of validationResult.warnings) {
|
|
1583
|
+
logger.warn(` ${w.code}: ${w.message}`);
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
function buildAnnotatedYAML(result) {
|
|
1589
|
+
const baseYaml = stringifyYaml(result.config, { lineWidth: 0 });
|
|
1590
|
+
const confidenceMap = /* @__PURE__ */ new Map();
|
|
1591
|
+
for (const token of result.tokens) {
|
|
1592
|
+
confidenceMap.set(token.name, token.confidence);
|
|
1593
|
+
}
|
|
1594
|
+
const lines = baseYaml.split("\n");
|
|
1595
|
+
const annotated = [];
|
|
1596
|
+
let inColors = false;
|
|
1597
|
+
let inColorsDark = false;
|
|
1598
|
+
for (const line of lines) {
|
|
1599
|
+
if (/^colors:/.test(line)) {
|
|
1600
|
+
inColors = true;
|
|
1601
|
+
inColorsDark = false;
|
|
1602
|
+
} else if (/^colors-dark:/.test(line)) {
|
|
1603
|
+
inColorsDark = true;
|
|
1604
|
+
inColors = false;
|
|
1605
|
+
} else if (/^\S/.test(line) && !line.startsWith(" ")) {
|
|
1606
|
+
inColors = false;
|
|
1607
|
+
inColorsDark = false;
|
|
1608
|
+
}
|
|
1609
|
+
if (inColors || inColorsDark) {
|
|
1610
|
+
const match = line.match(/^\s+([\w-]+):\s/);
|
|
1611
|
+
if (match) {
|
|
1612
|
+
const role = match[1];
|
|
1613
|
+
const key = `colors.${role}`;
|
|
1614
|
+
const confidence = confidenceMap.get(key);
|
|
1615
|
+
if (confidence) {
|
|
1616
|
+
annotated.push(`${line} ${confidenceComment(confidence)}`);
|
|
1617
|
+
continue;
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
annotated.push(line);
|
|
1622
|
+
}
|
|
1623
|
+
return annotated.join("\n");
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
// src/commands/theme-register.ts
|
|
1627
|
+
import { readFileSync as readFileSync9, writeFileSync as writeFileSync6, mkdirSync as mkdirSync4, existsSync as existsSync7 } from "fs";
|
|
1628
|
+
import { resolve as resolve6, join as join8 } from "path";
|
|
1629
|
+
import { generateThemeData as generateThemeData3 } from "@loworbitstudio/visor-theme-engine";
|
|
1630
|
+
import { docsAdapter as docsAdapter2 } from "@loworbitstudio/visor-theme-engine/adapters";
|
|
1631
|
+
|
|
1632
|
+
// src/utils/theme-helpers.ts
|
|
1633
|
+
import { existsSync as existsSync6 } from "fs";
|
|
1634
|
+
import { resolve as resolve5, dirname as dirname5, join as join7 } from "path";
|
|
1635
|
+
function toSlug(name) {
|
|
1636
|
+
return name.toLowerCase().replace(/\s+/g, "-");
|
|
1637
|
+
}
|
|
1638
|
+
function toLabel(name) {
|
|
1639
|
+
return name.split(/[\s-]+/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
1640
|
+
}
|
|
1641
|
+
function findRepoRoot(startDir) {
|
|
1642
|
+
let current = resolve5(startDir);
|
|
1643
|
+
while (true) {
|
|
1644
|
+
if (existsSync6(join7(current, "packages", "docs"))) {
|
|
1645
|
+
return current;
|
|
1646
|
+
}
|
|
1647
|
+
const parent = dirname5(current);
|
|
1648
|
+
if (parent === current) break;
|
|
1649
|
+
current = parent;
|
|
1650
|
+
}
|
|
1651
|
+
return null;
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
// src/commands/theme-register.ts
|
|
1655
|
+
function insertGlobalsImport(content, slug) {
|
|
1656
|
+
const importLine = `@import './${slug}-theme.css';`;
|
|
1657
|
+
if (content.includes(importLine)) {
|
|
1658
|
+
return { updated: content, changed: false };
|
|
1659
|
+
}
|
|
1660
|
+
const lines = content.split("\n");
|
|
1661
|
+
const themeImportPattern = /^@import '\.\/[\w-]+-theme\.css';/;
|
|
1662
|
+
const themeImportIndices = [];
|
|
1663
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1664
|
+
if (themeImportPattern.test(lines[i])) {
|
|
1665
|
+
themeImportIndices.push(i);
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
if (themeImportIndices.length === 0) {
|
|
1669
|
+
const lastImportIdx = lines.reduce(
|
|
1670
|
+
(last, line, i) => line.startsWith("@import") ? i : last,
|
|
1671
|
+
-1
|
|
1672
|
+
);
|
|
1673
|
+
const insertAt2 = lastImportIdx + 1;
|
|
1674
|
+
lines.splice(insertAt2, 0, importLine);
|
|
1675
|
+
return { updated: lines.join("\n"), changed: true };
|
|
1676
|
+
}
|
|
1677
|
+
let insertAt = themeImportIndices[themeImportIndices.length - 1] + 1;
|
|
1678
|
+
for (const idx of themeImportIndices) {
|
|
1679
|
+
if (importLine < lines[idx]) {
|
|
1680
|
+
insertAt = idx;
|
|
1681
|
+
break;
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
lines.splice(insertAt, 0, importLine);
|
|
1685
|
+
return { updated: lines.join("\n"), changed: true };
|
|
1686
|
+
}
|
|
1687
|
+
function insertThemeConfig(content, slug, label, group) {
|
|
1688
|
+
if (content.includes(`value: "${slug}"`)) {
|
|
1689
|
+
return { updated: content, changed: false };
|
|
1690
|
+
}
|
|
1691
|
+
const escapedGroup = group.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1692
|
+
const groupPattern = new RegExp(`label:\\s*"${escapedGroup}"`);
|
|
1693
|
+
const groupMatch = groupPattern.exec(content);
|
|
1694
|
+
if (!groupMatch) {
|
|
1695
|
+
return {
|
|
1696
|
+
updated: content,
|
|
1697
|
+
changed: false,
|
|
1698
|
+
error: `Group "${group}" not found in theme-config.ts. Available groups: Visor, Client, Low Orbit.`
|
|
1699
|
+
};
|
|
1700
|
+
}
|
|
1701
|
+
const afterGroup = content.slice(groupMatch.index);
|
|
1702
|
+
const themesMatch = /themes:\s*\[/.exec(afterGroup);
|
|
1703
|
+
if (!themesMatch) {
|
|
1704
|
+
return { updated: content, changed: false, error: `Could not find themes array for group "${group}".` };
|
|
1705
|
+
}
|
|
1706
|
+
const themesStart = groupMatch.index + themesMatch.index + themesMatch[0].length;
|
|
1707
|
+
const closingBracket = content.indexOf("]", themesStart);
|
|
1708
|
+
const themesContent = content.slice(themesStart, closingBracket);
|
|
1709
|
+
const entryPattern = /\{\s*value:\s*"([\w-]+)"[^}]*\}/g;
|
|
1710
|
+
const entries = [];
|
|
1711
|
+
let m;
|
|
1712
|
+
while ((m = entryPattern.exec(themesContent)) !== null) {
|
|
1713
|
+
entries.push({
|
|
1714
|
+
value: m[1],
|
|
1715
|
+
start: themesStart + m.index,
|
|
1716
|
+
end: themesStart + m.index + m[0].length
|
|
1717
|
+
});
|
|
1718
|
+
}
|
|
1719
|
+
let insertPos = closingBracket;
|
|
1720
|
+
for (const e of entries) {
|
|
1721
|
+
if (slug < e.value) {
|
|
1722
|
+
insertPos = e.start;
|
|
1723
|
+
break;
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
const prevNewline = content.lastIndexOf("\n", insertPos);
|
|
1727
|
+
const lineContent = content.slice(prevNewline + 1, insertPos);
|
|
1728
|
+
const indentMatch = /^(\s*)/.exec(lineContent);
|
|
1729
|
+
const indent = indentMatch ? indentMatch[1] : " ";
|
|
1730
|
+
const newEntry = `{ value: "${slug}", label: "${label}" }`;
|
|
1731
|
+
const insertion = entries.length === 0 ? `
|
|
1732
|
+
${indent}${newEntry},
|
|
1733
|
+
` : `${indent}${newEntry},
|
|
1734
|
+
`;
|
|
1735
|
+
const updated = content.slice(0, insertPos) + insertion + content.slice(insertPos);
|
|
1736
|
+
return { updated, changed: true };
|
|
1737
|
+
}
|
|
1738
|
+
function themeRegisterCommand(file, cwd, options) {
|
|
1739
|
+
const filePath = resolve6(cwd, file);
|
|
1740
|
+
let yamlContent;
|
|
1741
|
+
try {
|
|
1742
|
+
yamlContent = readFileSync9(filePath, "utf-8");
|
|
1743
|
+
} catch {
|
|
1744
|
+
if (options.json) {
|
|
1745
|
+
console.log(JSON.stringify({ success: false, error: `Could not read file: ${filePath}` }));
|
|
1746
|
+
} else {
|
|
1747
|
+
logger.error(`Could not read file: ${filePath}`);
|
|
1748
|
+
}
|
|
1749
|
+
process.exit(2);
|
|
1750
|
+
return;
|
|
1751
|
+
}
|
|
1752
|
+
let data;
|
|
1753
|
+
try {
|
|
1754
|
+
data = generateThemeData3(yamlContent);
|
|
1755
|
+
} catch (err) {
|
|
1756
|
+
const message = err instanceof Error ? err.message : "Unknown error parsing theme";
|
|
1757
|
+
if (options.json) {
|
|
1758
|
+
console.log(JSON.stringify({ success: false, error: message }));
|
|
1759
|
+
} else {
|
|
1760
|
+
logger.error("Failed to parse theme.");
|
|
1761
|
+
logger.info(message);
|
|
1762
|
+
}
|
|
1763
|
+
process.exit(1);
|
|
1764
|
+
return;
|
|
1765
|
+
}
|
|
1766
|
+
const slug = toSlug(data.config.name);
|
|
1767
|
+
const label = toLabel(data.config.name);
|
|
1768
|
+
const adapterInput = {
|
|
1769
|
+
primitives: data.primitives,
|
|
1770
|
+
tokens: data.tokens,
|
|
1771
|
+
config: data.config
|
|
1772
|
+
};
|
|
1773
|
+
const css = docsAdapter2(adapterInput);
|
|
1774
|
+
const repoRoot = findRepoRoot(cwd);
|
|
1775
|
+
if (!repoRoot) {
|
|
1776
|
+
const msg = "Could not locate repo root (packages/docs/ not found). Run from within the visor repo.";
|
|
1777
|
+
if (options.json) {
|
|
1778
|
+
console.log(JSON.stringify({ success: false, error: msg }));
|
|
1779
|
+
} else {
|
|
1780
|
+
logger.error(msg);
|
|
1781
|
+
}
|
|
1782
|
+
process.exit(1);
|
|
1783
|
+
return;
|
|
1784
|
+
}
|
|
1785
|
+
const docsAppDir = join8(repoRoot, "packages", "docs", "app");
|
|
1786
|
+
const cssFilePath = join8(docsAppDir, `${slug}-theme.css`);
|
|
1787
|
+
const globalsPath = join8(docsAppDir, "globals.css");
|
|
1788
|
+
const themeConfigPath = join8(repoRoot, "packages", "docs", "lib", "theme-config.ts");
|
|
1789
|
+
if (!existsSync7(docsAppDir)) {
|
|
1790
|
+
const msg = `Docs app directory not found: ${docsAppDir}`;
|
|
1791
|
+
if (options.json) {
|
|
1792
|
+
console.log(JSON.stringify({ success: false, error: msg }));
|
|
1793
|
+
} else {
|
|
1794
|
+
logger.error(msg);
|
|
1795
|
+
}
|
|
1796
|
+
process.exit(1);
|
|
1797
|
+
return;
|
|
1798
|
+
}
|
|
1799
|
+
let globalsContent = "";
|
|
1800
|
+
let themeConfigContent = "";
|
|
1801
|
+
try {
|
|
1802
|
+
globalsContent = readFileSync9(globalsPath, "utf-8");
|
|
1803
|
+
themeConfigContent = readFileSync9(themeConfigPath, "utf-8");
|
|
1804
|
+
} catch (err) {
|
|
1805
|
+
const msg = err instanceof Error ? err.message : "Could not read docs files";
|
|
1806
|
+
if (options.json) {
|
|
1807
|
+
console.log(JSON.stringify({ success: false, error: msg }));
|
|
1808
|
+
} else {
|
|
1809
|
+
logger.error(msg);
|
|
1810
|
+
}
|
|
1811
|
+
process.exit(1);
|
|
1812
|
+
return;
|
|
1813
|
+
}
|
|
1814
|
+
const cssExists = existsSync7(cssFilePath);
|
|
1815
|
+
const cssChanged = !cssExists || readFileSync9(cssFilePath, "utf-8") !== css;
|
|
1816
|
+
const { updated: newGlobals, changed: globalsChanged } = insertGlobalsImport(globalsContent, slug);
|
|
1817
|
+
const { updated: newThemeConfig, changed: themeConfigChanged, error: configError } = insertThemeConfig(
|
|
1818
|
+
themeConfigContent,
|
|
1819
|
+
slug,
|
|
1820
|
+
label,
|
|
1821
|
+
options.group
|
|
1822
|
+
);
|
|
1823
|
+
if (configError) {
|
|
1824
|
+
if (options.json) {
|
|
1825
|
+
console.log(JSON.stringify({ success: false, error: configError }));
|
|
1826
|
+
} else {
|
|
1827
|
+
logger.error(configError);
|
|
1828
|
+
}
|
|
1829
|
+
process.exit(1);
|
|
1830
|
+
return;
|
|
1831
|
+
}
|
|
1832
|
+
if (options.dryRun) {
|
|
1833
|
+
if (options.json) {
|
|
1834
|
+
console.log(JSON.stringify({
|
|
1835
|
+
success: true,
|
|
1836
|
+
dryRun: true,
|
|
1837
|
+
slug,
|
|
1838
|
+
label,
|
|
1839
|
+
group: options.group,
|
|
1840
|
+
changes: {
|
|
1841
|
+
cssFile: { path: cssFilePath, changed: cssChanged },
|
|
1842
|
+
globalsCSS: { path: globalsPath, changed: globalsChanged },
|
|
1843
|
+
themeConfig: { path: themeConfigPath, changed: themeConfigChanged }
|
|
1844
|
+
}
|
|
1845
|
+
}));
|
|
1846
|
+
} else {
|
|
1847
|
+
logger.info("Dry run \u2014 no files written");
|
|
1848
|
+
logger.item(`Theme: ${label} (${slug})`);
|
|
1849
|
+
logger.item(`Group: ${options.group}`);
|
|
1850
|
+
logger.item(`CSS file: ${cssFilePath} \u2014 ${cssChanged ? cssExists ? "update" : "create" : "no change"}`);
|
|
1851
|
+
logger.item(`globals.css: ${globalsChanged ? "add import" : "already registered"}`);
|
|
1852
|
+
logger.item(`theme-config.ts: ${themeConfigChanged ? "add entry" : "already registered"}`);
|
|
1853
|
+
}
|
|
1854
|
+
return;
|
|
1855
|
+
}
|
|
1856
|
+
try {
|
|
1857
|
+
if (cssChanged) {
|
|
1858
|
+
mkdirSync4(docsAppDir, { recursive: true });
|
|
1859
|
+
writeFileSync6(cssFilePath, css, "utf-8");
|
|
1860
|
+
}
|
|
1861
|
+
if (globalsChanged) {
|
|
1862
|
+
writeFileSync6(globalsPath, newGlobals, "utf-8");
|
|
1863
|
+
}
|
|
1864
|
+
if (themeConfigChanged) {
|
|
1865
|
+
writeFileSync6(themeConfigPath, newThemeConfig, "utf-8");
|
|
1866
|
+
}
|
|
1867
|
+
} catch (err) {
|
|
1868
|
+
const msg = err instanceof Error ? err.message : "Write failed";
|
|
1869
|
+
if (options.json) {
|
|
1870
|
+
console.log(JSON.stringify({ success: false, error: msg }));
|
|
1871
|
+
} else {
|
|
1872
|
+
logger.error(msg);
|
|
1873
|
+
}
|
|
1874
|
+
process.exit(2);
|
|
1875
|
+
return;
|
|
1876
|
+
}
|
|
1877
|
+
if (options.json) {
|
|
1878
|
+
console.log(JSON.stringify({
|
|
1879
|
+
success: true,
|
|
1880
|
+
slug,
|
|
1881
|
+
label,
|
|
1882
|
+
group: options.group,
|
|
1883
|
+
files: { css: cssFilePath, globals: globalsPath, themeConfig: themeConfigPath },
|
|
1884
|
+
changes: { cssFile: cssChanged, globalsCSS: globalsChanged, themeConfig: themeConfigChanged }
|
|
1885
|
+
}));
|
|
1886
|
+
} else {
|
|
1887
|
+
logger.success(`Theme registered: ${label} (${slug})`);
|
|
1888
|
+
logger.item(`Group: ${options.group}`);
|
|
1889
|
+
if (cssChanged) logger.item(`CSS: ${cssFilePath}`);
|
|
1890
|
+
if (globalsChanged) logger.item(`globals.css updated`);
|
|
1891
|
+
if (themeConfigChanged) logger.item(`theme-config.ts updated`);
|
|
1892
|
+
if (!cssChanged && !globalsChanged && !themeConfigChanged) {
|
|
1893
|
+
logger.info("Already registered \u2014 no changes needed.");
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
// src/commands/theme-unregister.ts
|
|
1899
|
+
import { readFileSync as readFileSync10, writeFileSync as writeFileSync7, existsSync as existsSync8, unlinkSync } from "fs";
|
|
1900
|
+
import { join as join9 } from "path";
|
|
1901
|
+
function removeGlobalsImport(content, slug) {
|
|
1902
|
+
const importLine = `@import './${slug}-theme.css';`;
|
|
1903
|
+
if (!content.includes(importLine)) {
|
|
1904
|
+
return { updated: content, changed: false };
|
|
1905
|
+
}
|
|
1906
|
+
const updated = content.split("\n").filter((line) => line !== importLine).join("\n");
|
|
1907
|
+
return { updated, changed: true };
|
|
1908
|
+
}
|
|
1909
|
+
function removeThemeConfigEntry(content, slug) {
|
|
1910
|
+
if (!content.includes(`value: "${slug}"`)) {
|
|
1911
|
+
return { updated: content, changed: false };
|
|
1912
|
+
}
|
|
1913
|
+
const entryPattern = new RegExp(
|
|
1914
|
+
`\\s*\\{\\s*value:\\s*"${slug.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}"[^}]*\\},?`,
|
|
1915
|
+
"g"
|
|
1916
|
+
);
|
|
1917
|
+
const updated = content.replace(entryPattern, "");
|
|
1918
|
+
return { updated, changed: true };
|
|
1919
|
+
}
|
|
1920
|
+
function themeUnregisterCommand(slug, cwd, options) {
|
|
1921
|
+
const repoRoot = findRepoRoot(cwd);
|
|
1922
|
+
if (!repoRoot) {
|
|
1923
|
+
const msg = "Could not locate repo root (packages/docs/ not found). Run from within the visor repo.";
|
|
1924
|
+
if (options.json) {
|
|
1925
|
+
console.log(JSON.stringify({ success: false, error: msg }));
|
|
1926
|
+
} else {
|
|
1927
|
+
logger.error(msg);
|
|
1928
|
+
}
|
|
1929
|
+
process.exit(1);
|
|
1930
|
+
return;
|
|
1931
|
+
}
|
|
1932
|
+
const docsAppDir = join9(repoRoot, "packages", "docs", "app");
|
|
1933
|
+
const cssFilePath = join9(docsAppDir, `${slug}-theme.css`);
|
|
1934
|
+
const globalsPath = join9(docsAppDir, "globals.css");
|
|
1935
|
+
const themeConfigPath = join9(repoRoot, "packages", "docs", "lib", "theme-config.ts");
|
|
1936
|
+
if (!existsSync8(docsAppDir)) {
|
|
1937
|
+
const msg = `Docs app directory not found: ${docsAppDir}`;
|
|
1938
|
+
if (options.json) {
|
|
1939
|
+
console.log(JSON.stringify({ success: false, error: msg }));
|
|
1940
|
+
} else {
|
|
1941
|
+
logger.error(msg);
|
|
1942
|
+
}
|
|
1943
|
+
process.exit(1);
|
|
1944
|
+
return;
|
|
1945
|
+
}
|
|
1946
|
+
let globalsContent = "";
|
|
1947
|
+
let themeConfigContent = "";
|
|
1948
|
+
try {
|
|
1949
|
+
globalsContent = readFileSync10(globalsPath, "utf-8");
|
|
1950
|
+
themeConfigContent = readFileSync10(themeConfigPath, "utf-8");
|
|
1951
|
+
} catch (err) {
|
|
1952
|
+
const msg = err instanceof Error ? err.message : "Could not read docs files";
|
|
1953
|
+
if (options.json) {
|
|
1954
|
+
console.log(JSON.stringify({ success: false, error: msg }));
|
|
1955
|
+
} else {
|
|
1956
|
+
logger.error(msg);
|
|
1957
|
+
}
|
|
1958
|
+
process.exit(1);
|
|
1959
|
+
return;
|
|
1960
|
+
}
|
|
1961
|
+
const cssExists = existsSync8(cssFilePath);
|
|
1962
|
+
const { updated: newGlobals, changed: globalsChanged } = removeGlobalsImport(globalsContent, slug);
|
|
1963
|
+
const { updated: newThemeConfig, changed: themeConfigChanged } = removeThemeConfigEntry(themeConfigContent, slug);
|
|
1964
|
+
if (!cssExists && !globalsChanged && !themeConfigChanged) {
|
|
1965
|
+
if (options.json) {
|
|
1966
|
+
console.log(JSON.stringify({ success: true, slug, changes: { cssFile: false, globalsCSS: false, themeConfig: false } }));
|
|
1967
|
+
} else {
|
|
1968
|
+
logger.info(`Theme "${slug}" is not registered \u2014 nothing to remove.`);
|
|
1969
|
+
}
|
|
1970
|
+
return;
|
|
1971
|
+
}
|
|
1972
|
+
try {
|
|
1973
|
+
if (cssExists) unlinkSync(cssFilePath);
|
|
1974
|
+
if (globalsChanged) writeFileSync7(globalsPath, newGlobals, "utf-8");
|
|
1975
|
+
if (themeConfigChanged) writeFileSync7(themeConfigPath, newThemeConfig, "utf-8");
|
|
1976
|
+
} catch (err) {
|
|
1977
|
+
const msg = err instanceof Error ? err.message : "Write failed";
|
|
1978
|
+
if (options.json) {
|
|
1979
|
+
console.log(JSON.stringify({ success: false, error: msg }));
|
|
1980
|
+
} else {
|
|
1981
|
+
logger.error(msg);
|
|
1982
|
+
}
|
|
1983
|
+
process.exit(2);
|
|
1984
|
+
return;
|
|
1985
|
+
}
|
|
1986
|
+
if (options.json) {
|
|
1987
|
+
console.log(JSON.stringify({
|
|
1988
|
+
success: true,
|
|
1989
|
+
slug,
|
|
1990
|
+
changes: { cssFile: cssExists, globalsCSS: globalsChanged, themeConfig: themeConfigChanged }
|
|
1991
|
+
}));
|
|
1992
|
+
} else {
|
|
1993
|
+
logger.success(`Theme unregistered: ${slug}`);
|
|
1994
|
+
if (cssExists) logger.item(`CSS file removed: ${cssFilePath}`);
|
|
1995
|
+
if (globalsChanged) logger.item(`globals.css updated`);
|
|
1996
|
+
if (themeConfigChanged) logger.item(`theme-config.ts updated`);
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
// src/commands/theme-sync.ts
|
|
2001
|
+
import {
|
|
2002
|
+
readFileSync as readFileSync11,
|
|
2003
|
+
writeFileSync as writeFileSync8,
|
|
2004
|
+
mkdirSync as mkdirSync5,
|
|
2005
|
+
existsSync as existsSync9,
|
|
2006
|
+
readdirSync as readdirSync2,
|
|
2007
|
+
unlinkSync as unlinkSync2,
|
|
2008
|
+
copyFileSync
|
|
2009
|
+
} from "fs";
|
|
2010
|
+
import { join as join10, basename as basename2 } from "path";
|
|
2011
|
+
import { parse as parseYaml2 } from "yaml";
|
|
2012
|
+
import { generateThemeData as generateThemeData4 } from "@loworbitstudio/visor-theme-engine";
|
|
2013
|
+
import { docsAdapter as docsAdapter3 } from "@loworbitstudio/visor-theme-engine/adapters";
|
|
2014
|
+
var GLOBALS_BEGIN_MARKER = "/* BEGIN visor-theme-imports \u2014 generated by `visor theme sync` */";
|
|
2015
|
+
var GLOBALS_END_MARKER = "/* END visor-theme-imports */";
|
|
2016
|
+
var GITIGNORE_BEGIN_MARKER = "# BEGIN visor-custom-theme-css (managed by `visor theme sync` \u2014 do not edit manually)";
|
|
2017
|
+
var GITIGNORE_END_MARKER = "# END visor-custom-theme-css";
|
|
2018
|
+
var THEME_CONFIG_HEADER = "// This file is auto-generated by `visor theme sync`. Do not edit manually.\n";
|
|
2019
|
+
function scanThemeDir(dir) {
|
|
2020
|
+
if (!existsSync9(dir)) return [];
|
|
2021
|
+
return readdirSync2(dir).filter((f) => f.endsWith(".visor.yaml")).map((f) => join10(dir, f));
|
|
2022
|
+
}
|
|
2023
|
+
function extractGroup(yamlContent) {
|
|
2024
|
+
const parsed = parseYaml2(yamlContent);
|
|
2025
|
+
if (typeof parsed?.group === "string") return parsed.group;
|
|
2026
|
+
return void 0;
|
|
2027
|
+
}
|
|
2028
|
+
function sortGroups(groups) {
|
|
2029
|
+
return [...groups].sort((a, b) => {
|
|
2030
|
+
if (a === "Visor") return -1;
|
|
2031
|
+
if (b === "Visor") return 1;
|
|
2032
|
+
return a.localeCompare(b);
|
|
2033
|
+
});
|
|
2034
|
+
}
|
|
2035
|
+
function generateThemeConfig(entries) {
|
|
2036
|
+
const groupMap = /* @__PURE__ */ new Map();
|
|
2037
|
+
for (const entry of entries) {
|
|
2038
|
+
if (!groupMap.has(entry.group)) groupMap.set(entry.group, []);
|
|
2039
|
+
groupMap.get(entry.group).push(entry);
|
|
2040
|
+
}
|
|
2041
|
+
const sortedGroupNames = sortGroups([...groupMap.keys()]);
|
|
2042
|
+
for (const [, groupEntries] of groupMap) {
|
|
2043
|
+
groupEntries.sort((a, b) => a.slug.localeCompare(b.slug));
|
|
2044
|
+
}
|
|
2045
|
+
const groupsTs = sortedGroupNames.map((groupName) => {
|
|
2046
|
+
const groupEntries = groupMap.get(groupName);
|
|
2047
|
+
const themesTs = groupEntries.map((e) => ` { value: "${e.slug}", label: "${e.label}", yamlFile: "${e.yamlFilename}" },`).join("\n");
|
|
2048
|
+
return ` {
|
|
2049
|
+
label: "${groupName}",
|
|
2050
|
+
themes: [
|
|
2051
|
+
${themesTs}
|
|
2052
|
+
],
|
|
2053
|
+
},`;
|
|
2054
|
+
}).join("\n");
|
|
2055
|
+
return `${THEME_CONFIG_HEADER}
|
|
2056
|
+
export interface ThemeEntry {
|
|
2057
|
+
value: string;
|
|
2058
|
+
label: string;
|
|
2059
|
+
/** Filename (without .visor.yaml extension) if a YAML config exists in /public/themes/ */
|
|
2060
|
+
yamlFile?: string;
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
export interface ThemeGroup {
|
|
2064
|
+
label: string;
|
|
2065
|
+
themes: ThemeEntry[];
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
export const THEME_GROUPS: ThemeGroup[] = [
|
|
2069
|
+
${groupsTs}
|
|
2070
|
+
];
|
|
2071
|
+
|
|
2072
|
+
export const ALL_THEMES = THEME_GROUPS.flatMap((g) => g.themes.map((t) => t.value));
|
|
2073
|
+
`;
|
|
2074
|
+
}
|
|
2075
|
+
function updateGlobalsImports(content, slugs) {
|
|
2076
|
+
const importLines = [...slugs].sort().map((slug) => `@import './${slug}-theme.css';`).join("\n");
|
|
2077
|
+
const newBlock = `${GLOBALS_BEGIN_MARKER}
|
|
2078
|
+
${importLines}
|
|
2079
|
+
${GLOBALS_END_MARKER}`;
|
|
2080
|
+
const beginIdx = content.indexOf(GLOBALS_BEGIN_MARKER);
|
|
2081
|
+
const endIdx = content.indexOf(GLOBALS_END_MARKER);
|
|
2082
|
+
if (beginIdx !== -1 && endIdx !== -1) {
|
|
2083
|
+
return content.slice(0, beginIdx) + newBlock + content.slice(endIdx + GLOBALS_END_MARKER.length);
|
|
2084
|
+
}
|
|
2085
|
+
const themeImportPattern = /^@import '\.\/[\w-]+-theme\.css';\n?/gm;
|
|
2086
|
+
const lines = content.split("\n");
|
|
2087
|
+
let firstThemeIdx = -1;
|
|
2088
|
+
let lastThemeIdx = -1;
|
|
2089
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2090
|
+
if (/^@import '\.\/[\w-]+-theme\.css';/.test(lines[i])) {
|
|
2091
|
+
if (firstThemeIdx === -1) firstThemeIdx = i;
|
|
2092
|
+
lastThemeIdx = i;
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
if (firstThemeIdx !== -1) {
|
|
2096
|
+
const before = lines.slice(0, firstThemeIdx);
|
|
2097
|
+
const after = lines.slice(lastThemeIdx + 1);
|
|
2098
|
+
return [...before, newBlock, ...after].join("\n");
|
|
2099
|
+
}
|
|
2100
|
+
void themeImportPattern;
|
|
2101
|
+
const lastImportIdx = lines.reduce(
|
|
2102
|
+
(last, line, i) => line.startsWith("@import") ? i : last,
|
|
2103
|
+
-1
|
|
2104
|
+
);
|
|
2105
|
+
const insertAt = lastImportIdx + 1;
|
|
2106
|
+
lines.splice(insertAt, 0, newBlock);
|
|
2107
|
+
return lines.join("\n");
|
|
2108
|
+
}
|
|
2109
|
+
function updateGitignoreBlock(content, customSlugs) {
|
|
2110
|
+
const cssLines = customSlugs.sort().map((slug) => `packages/docs/app/${slug}-theme.css`).join("\n");
|
|
2111
|
+
const newBlock = `${GITIGNORE_BEGIN_MARKER}
|
|
2112
|
+
${cssLines}
|
|
2113
|
+
${GITIGNORE_END_MARKER}`;
|
|
2114
|
+
const beginIdx = content.indexOf(GITIGNORE_BEGIN_MARKER);
|
|
2115
|
+
const endIdx = content.indexOf(GITIGNORE_END_MARKER);
|
|
2116
|
+
if (beginIdx !== -1 && endIdx !== -1) {
|
|
2117
|
+
return content.slice(0, beginIdx) + newBlock + content.slice(endIdx + GITIGNORE_END_MARKER.length);
|
|
2118
|
+
}
|
|
2119
|
+
return content.trimEnd() + "\n\n" + newBlock + "\n";
|
|
2120
|
+
}
|
|
2121
|
+
function themeSyncCommand(cwd, options) {
|
|
2122
|
+
const repoRoot = findRepoRoot(cwd);
|
|
2123
|
+
if (!repoRoot) {
|
|
2124
|
+
const msg = "Could not locate repo root (packages/docs/ not found). Run from within the visor repo.";
|
|
2125
|
+
if (options.json) {
|
|
2126
|
+
console.log(JSON.stringify({ success: false, error: msg }));
|
|
2127
|
+
} else {
|
|
2128
|
+
logger.error(msg);
|
|
2129
|
+
}
|
|
2130
|
+
process.exit(1);
|
|
2131
|
+
return;
|
|
2132
|
+
}
|
|
2133
|
+
const themesDir = join10(repoRoot, "themes");
|
|
2134
|
+
const customThemesDir = join10(repoRoot, "custom-themes");
|
|
2135
|
+
const docsAppDir = join10(repoRoot, "packages", "docs", "app");
|
|
2136
|
+
const docsPublicThemesDir = join10(repoRoot, "packages", "docs", "public", "themes");
|
|
2137
|
+
const themeConfigPath = join10(repoRoot, "packages", "docs", "lib", "theme-config.ts");
|
|
2138
|
+
const globalsPath = join10(docsAppDir, "globals.css");
|
|
2139
|
+
const gitignorePath = join10(repoRoot, ".gitignore");
|
|
2140
|
+
const stockFiles = scanThemeDir(themesDir);
|
|
2141
|
+
const customFiles = scanThemeDir(customThemesDir);
|
|
2142
|
+
if (stockFiles.length === 0 && customFiles.length === 0) {
|
|
2143
|
+
const msg = `No .visor.yaml files found in themes/ or custom-themes/. Nothing to sync.`;
|
|
2144
|
+
if (options.json) {
|
|
2145
|
+
console.log(JSON.stringify({ success: false, error: msg }));
|
|
2146
|
+
} else {
|
|
2147
|
+
logger.warn(msg);
|
|
2148
|
+
}
|
|
2149
|
+
return;
|
|
2150
|
+
}
|
|
2151
|
+
const manifest = [];
|
|
2152
|
+
const errors = [];
|
|
2153
|
+
const processFile = (filePath, isCustom) => {
|
|
2154
|
+
let yamlContent;
|
|
2155
|
+
try {
|
|
2156
|
+
yamlContent = readFileSync11(filePath, "utf-8");
|
|
2157
|
+
} catch {
|
|
2158
|
+
errors.push(`Could not read: ${filePath}`);
|
|
2159
|
+
return;
|
|
2160
|
+
}
|
|
2161
|
+
let data;
|
|
2162
|
+
try {
|
|
2163
|
+
data = generateThemeData4(yamlContent);
|
|
2164
|
+
} catch (err) {
|
|
2165
|
+
errors.push(`Failed to parse ${basename2(filePath)}: ${err instanceof Error ? err.message : "Unknown error"}`);
|
|
2166
|
+
return;
|
|
2167
|
+
}
|
|
2168
|
+
const slug = toSlug(data.config.name);
|
|
2169
|
+
const label = toLabel(data.config.name);
|
|
2170
|
+
const group = extractGroup(yamlContent) ?? (isCustom ? "Custom" : "Visor");
|
|
2171
|
+
const css = docsAdapter3({ primitives: data.primitives, tokens: data.tokens, config: data.config });
|
|
2172
|
+
const yamlFilename = basename2(filePath).replace(/\.visor\.yaml$/, "");
|
|
2173
|
+
manifest.push({ slug, label, group, css, yamlFilename, isCustom });
|
|
2174
|
+
};
|
|
2175
|
+
for (const f of stockFiles) processFile(f, false);
|
|
2176
|
+
for (const f of customFiles) processFile(f, true);
|
|
2177
|
+
if (errors.length > 0) {
|
|
2178
|
+
if (options.json) {
|
|
2179
|
+
console.log(JSON.stringify({ success: false, errors }));
|
|
2180
|
+
} else {
|
|
2181
|
+
errors.forEach((e) => logger.error(e));
|
|
2182
|
+
}
|
|
2183
|
+
process.exit(1);
|
|
2184
|
+
return;
|
|
2185
|
+
}
|
|
2186
|
+
const newThemeConfig = generateThemeConfig(manifest);
|
|
2187
|
+
const allSlugs = manifest.map((e) => e.slug);
|
|
2188
|
+
const customSlugs = manifest.filter((e) => e.isCustom).map((e) => e.slug);
|
|
2189
|
+
let globalsContent;
|
|
2190
|
+
let gitignoreContent;
|
|
2191
|
+
try {
|
|
2192
|
+
globalsContent = readFileSync11(globalsPath, "utf-8");
|
|
2193
|
+
gitignoreContent = existsSync9(gitignorePath) ? readFileSync11(gitignorePath, "utf-8") : "";
|
|
2194
|
+
} catch (err) {
|
|
2195
|
+
const msg = err instanceof Error ? err.message : "Could not read docs files";
|
|
2196
|
+
if (options.json) {
|
|
2197
|
+
console.log(JSON.stringify({ success: false, error: msg }));
|
|
2198
|
+
} else {
|
|
2199
|
+
logger.error(msg);
|
|
2200
|
+
}
|
|
2201
|
+
process.exit(1);
|
|
2202
|
+
return;
|
|
2203
|
+
}
|
|
2204
|
+
const newGlobals = updateGlobalsImports(globalsContent, allSlugs);
|
|
2205
|
+
const newGitignore = customSlugs.length > 0 ? updateGitignoreBlock(gitignoreContent, customSlugs) : gitignoreContent;
|
|
2206
|
+
const existingCssFiles = existsSync9(docsAppDir) ? readdirSync2(docsAppDir).filter((f) => f.endsWith("-theme.css")) : [];
|
|
2207
|
+
const newCssSet = new Set(allSlugs.map((s) => `${s}-theme.css`));
|
|
2208
|
+
const staleCssFiles = existingCssFiles.filter((f) => !newCssSet.has(f));
|
|
2209
|
+
const existingPublicYamls = existsSync9(docsPublicThemesDir) ? readdirSync2(docsPublicThemesDir).filter((f) => f.endsWith(".visor.yaml")) : [];
|
|
2210
|
+
const newPublicYamlSet = new Set(manifest.map((e) => `${e.yamlFilename}.visor.yaml`));
|
|
2211
|
+
const stalePublicYamls = existingPublicYamls.filter((f) => !newPublicYamlSet.has(f));
|
|
2212
|
+
if (options.dryRun) {
|
|
2213
|
+
const changes = {
|
|
2214
|
+
themesDiscovered: manifest.map((e) => ({ slug: e.slug, group: e.group, isCustom: e.isCustom })),
|
|
2215
|
+
cssFilesGenerated: allSlugs.map((s) => `packages/docs/app/${s}-theme.css`),
|
|
2216
|
+
cssFilesDeleted: staleCssFiles.map((f) => `packages/docs/app/${f}`),
|
|
2217
|
+
themeConfig: themeConfigPath,
|
|
2218
|
+
globalsCSS: globalsPath,
|
|
2219
|
+
gitignore: gitignorePath,
|
|
2220
|
+
publicYamlsCopied: manifest.map((e) => `packages/docs/public/themes/${e.yamlFilename}.visor.yaml`),
|
|
2221
|
+
publicYamlsDeleted: stalePublicYamls.map((f) => `packages/docs/public/themes/${f}`)
|
|
2222
|
+
};
|
|
2223
|
+
if (options.json) {
|
|
2224
|
+
console.log(JSON.stringify({ success: true, dryRun: true, changes }));
|
|
2225
|
+
} else {
|
|
2226
|
+
logger.info("Dry run \u2014 no files written");
|
|
2227
|
+
logger.item(`Themes discovered: ${manifest.length} (${manifest.filter((e) => !e.isCustom).length} stock, ${manifest.filter((e) => e.isCustom).length} custom)`);
|
|
2228
|
+
manifest.forEach((e) => logger.item(` ${e.slug} \u2014 group: ${e.group}`));
|
|
2229
|
+
if (staleCssFiles.length > 0) logger.item(`CSS files to delete: ${staleCssFiles.join(", ")}`);
|
|
2230
|
+
if (stalePublicYamls.length > 0) logger.item(`Public YAMLs to delete: ${stalePublicYamls.join(", ")}`);
|
|
2231
|
+
}
|
|
2232
|
+
return;
|
|
2233
|
+
}
|
|
2234
|
+
try {
|
|
2235
|
+
mkdirSync5(docsAppDir, { recursive: true });
|
|
2236
|
+
mkdirSync5(docsPublicThemesDir, { recursive: true });
|
|
2237
|
+
for (const entry of manifest) {
|
|
2238
|
+
writeFileSync8(join10(docsAppDir, `${entry.slug}-theme.css`), entry.css, "utf-8");
|
|
2239
|
+
}
|
|
2240
|
+
for (const stale of staleCssFiles) {
|
|
2241
|
+
unlinkSync2(join10(docsAppDir, stale));
|
|
2242
|
+
}
|
|
2243
|
+
writeFileSync8(themeConfigPath, newThemeConfig, "utf-8");
|
|
2244
|
+
writeFileSync8(globalsPath, newGlobals, "utf-8");
|
|
2245
|
+
if (existsSync9(gitignorePath)) {
|
|
2246
|
+
writeFileSync8(gitignorePath, newGitignore, "utf-8");
|
|
2247
|
+
}
|
|
2248
|
+
const allSourceFiles = [...stockFiles, ...customFiles];
|
|
2249
|
+
for (const srcFile of allSourceFiles) {
|
|
2250
|
+
const filename = basename2(srcFile);
|
|
2251
|
+
copyFileSync(srcFile, join10(docsPublicThemesDir, filename));
|
|
2252
|
+
}
|
|
2253
|
+
for (const stale of stalePublicYamls) {
|
|
2254
|
+
unlinkSync2(join10(docsPublicThemesDir, stale));
|
|
2255
|
+
}
|
|
2256
|
+
} catch (err) {
|
|
2257
|
+
const msg = err instanceof Error ? err.message : "Write failed";
|
|
2258
|
+
if (options.json) {
|
|
2259
|
+
console.log(JSON.stringify({ success: false, error: msg }));
|
|
2260
|
+
} else {
|
|
2261
|
+
logger.error(msg);
|
|
2262
|
+
}
|
|
2263
|
+
process.exit(2);
|
|
2264
|
+
return;
|
|
2265
|
+
}
|
|
2266
|
+
if (options.json) {
|
|
2267
|
+
console.log(JSON.stringify({
|
|
2268
|
+
success: true,
|
|
2269
|
+
themes: manifest.length,
|
|
2270
|
+
stock: manifest.filter((e) => !e.isCustom).length,
|
|
2271
|
+
custom: manifest.filter((e) => e.isCustom).length,
|
|
2272
|
+
staleCssDeleted: staleCssFiles.length,
|
|
2273
|
+
staleYamlsDeleted: stalePublicYamls.length,
|
|
2274
|
+
slugs: allSlugs
|
|
2275
|
+
}));
|
|
2276
|
+
} else {
|
|
2277
|
+
logger.success(`Theme sync complete \u2014 ${manifest.length} themes registered`);
|
|
2278
|
+
logger.item(`Stock: ${manifest.filter((e) => !e.isCustom).map((e) => e.slug).join(", ")}`);
|
|
2279
|
+
if (manifest.filter((e) => e.isCustom).length > 0) {
|
|
2280
|
+
logger.item(`Custom: ${manifest.filter((e) => e.isCustom).map((e) => e.slug).join(", ")}`);
|
|
2281
|
+
}
|
|
2282
|
+
if (staleCssFiles.length > 0) {
|
|
2283
|
+
logger.item(`Removed stale CSS: ${staleCssFiles.join(", ")}`);
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
// src/commands/fonts-add.ts
|
|
2289
|
+
import { existsSync as existsSync10, statSync as statSync2, readdirSync as readdirSync3, readFileSync as readFileSync12 } from "fs";
|
|
2290
|
+
import { resolve as resolve7, basename as basename3, extname as extname2 } from "path";
|
|
2291
|
+
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
|
|
2292
|
+
function deriveFamilySlug(filename) {
|
|
2293
|
+
const name = basename3(filename, extname2(filename));
|
|
2294
|
+
const WEIGHT_STYLE_SUFFIXES = /* @__PURE__ */ new Set([
|
|
2295
|
+
"thin",
|
|
2296
|
+
"hairline",
|
|
2297
|
+
"extralight",
|
|
2298
|
+
"ultralight",
|
|
2299
|
+
"light",
|
|
2300
|
+
"regular",
|
|
2301
|
+
"normal",
|
|
2302
|
+
"medium",
|
|
2303
|
+
"semibold",
|
|
2304
|
+
"demibold",
|
|
2305
|
+
"bold",
|
|
2306
|
+
"extrabold",
|
|
2307
|
+
"ultrabold",
|
|
2308
|
+
"black",
|
|
2309
|
+
"heavy",
|
|
2310
|
+
"italic",
|
|
2311
|
+
"oblique",
|
|
2312
|
+
"bolditalic",
|
|
2313
|
+
"semibolditalic",
|
|
2314
|
+
"lightitalic",
|
|
2315
|
+
"mediumitalic",
|
|
2316
|
+
"thinitalic",
|
|
2317
|
+
"extrabolditanic",
|
|
2318
|
+
"extrabolditalic",
|
|
2319
|
+
"blackitalic",
|
|
2320
|
+
"heavyitalic",
|
|
2321
|
+
"extralightitalic",
|
|
2322
|
+
"ultralightitalic",
|
|
2323
|
+
"ultrabolditalic",
|
|
2324
|
+
"demibolditalic",
|
|
2325
|
+
"regularitalic"
|
|
2326
|
+
]);
|
|
2327
|
+
const parts = name.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2").split("-");
|
|
2328
|
+
while (parts.length > 1 && WEIGHT_STYLE_SUFFIXES.has(parts[parts.length - 1].toLowerCase())) {
|
|
2329
|
+
parts.pop();
|
|
2330
|
+
}
|
|
2331
|
+
return parts.join("-").toLowerCase();
|
|
2332
|
+
}
|
|
2333
|
+
function collectWoff2Files(inputPath) {
|
|
2334
|
+
const resolved = resolve7(inputPath);
|
|
2335
|
+
if (!existsSync10(resolved)) {
|
|
2336
|
+
throw new Error(`Path not found: ${resolved}`);
|
|
2337
|
+
}
|
|
2338
|
+
const stat = statSync2(resolved);
|
|
2339
|
+
if (stat.isFile()) {
|
|
2340
|
+
if (extname2(resolved).toLowerCase() !== ".woff2") {
|
|
2341
|
+
throw new Error(
|
|
2342
|
+
`Invalid file format: ${basename3(resolved)}. Only .woff2 files are accepted.`
|
|
2343
|
+
);
|
|
2344
|
+
}
|
|
2345
|
+
return [resolved];
|
|
2346
|
+
}
|
|
2347
|
+
if (stat.isDirectory()) {
|
|
2348
|
+
const files = readdirSync3(resolved).filter((f) => extname2(f).toLowerCase() === ".woff2").map((f) => resolve7(resolved, f));
|
|
2349
|
+
if (files.length === 0) {
|
|
2350
|
+
throw new Error(
|
|
2351
|
+
`No .woff2 files found in directory: ${resolved}`
|
|
2352
|
+
);
|
|
2353
|
+
}
|
|
2354
|
+
const nonWoff2Fonts = readdirSync3(resolved).filter((f) => {
|
|
2355
|
+
const ext = extname2(f).toLowerCase();
|
|
2356
|
+
return [".ttf", ".otf", ".woff", ".eot"].includes(ext);
|
|
2357
|
+
});
|
|
2358
|
+
return files.sort();
|
|
2359
|
+
}
|
|
2360
|
+
throw new Error(`Path is neither a file nor a directory: ${resolved}`);
|
|
2361
|
+
}
|
|
2362
|
+
function getNonWoff2Fonts(inputPath) {
|
|
2363
|
+
const resolved = resolve7(inputPath);
|
|
2364
|
+
if (!existsSync10(resolved) || !statSync2(resolved).isDirectory()) {
|
|
2365
|
+
return [];
|
|
2366
|
+
}
|
|
2367
|
+
return readdirSync3(resolved).filter((f) => {
|
|
2368
|
+
const ext = extname2(f).toLowerCase();
|
|
2369
|
+
return [".ttf", ".otf", ".woff", ".eot"].includes(ext);
|
|
2370
|
+
});
|
|
2371
|
+
}
|
|
2372
|
+
function buildS3Key(org, familySlug, filename) {
|
|
2373
|
+
return `${org}/${familySlug}/${filename}`;
|
|
2374
|
+
}
|
|
2375
|
+
function getR2Config() {
|
|
2376
|
+
const accessKeyId = process.env.VISOR_R2_ACCESS_KEY_ID;
|
|
2377
|
+
const secretAccessKey = process.env.VISOR_R2_SECRET_ACCESS_KEY;
|
|
2378
|
+
const endpoint = process.env.VISOR_R2_ENDPOINT;
|
|
2379
|
+
if (!accessKeyId || !secretAccessKey || !endpoint) {
|
|
2380
|
+
const missing = [];
|
|
2381
|
+
if (!accessKeyId) missing.push("VISOR_R2_ACCESS_KEY_ID");
|
|
2382
|
+
if (!secretAccessKey) missing.push("VISOR_R2_SECRET_ACCESS_KEY");
|
|
2383
|
+
if (!endpoint) missing.push("VISOR_R2_ENDPOINT");
|
|
2384
|
+
throw new Error(
|
|
2385
|
+
`Missing required environment variables: ${missing.join(", ")}`
|
|
2386
|
+
);
|
|
2387
|
+
}
|
|
2388
|
+
return { accessKeyId, secretAccessKey, endpoint };
|
|
2389
|
+
}
|
|
2390
|
+
function createR2Client(config) {
|
|
2391
|
+
return new S3Client({
|
|
2392
|
+
region: "auto",
|
|
2393
|
+
endpoint: config.endpoint,
|
|
2394
|
+
credentials: {
|
|
2395
|
+
accessKeyId: config.accessKeyId,
|
|
2396
|
+
secretAccessKey: config.secretAccessKey
|
|
2397
|
+
}
|
|
2398
|
+
});
|
|
2399
|
+
}
|
|
2400
|
+
async function uploadFile(client, bucket, key, filePath) {
|
|
2401
|
+
const body = readFileSync12(filePath);
|
|
2402
|
+
await client.send(
|
|
2403
|
+
new PutObjectCommand({
|
|
2404
|
+
Bucket: bucket,
|
|
2405
|
+
Key: key,
|
|
2406
|
+
Body: body,
|
|
2407
|
+
ContentType: "font/woff2"
|
|
2408
|
+
})
|
|
2409
|
+
);
|
|
2410
|
+
}
|
|
2411
|
+
async function fontsAddCommand(inputPath, options) {
|
|
2412
|
+
const { org, json } = options;
|
|
2413
|
+
try {
|
|
2414
|
+
const r2Config = getR2Config();
|
|
2415
|
+
const files = collectWoff2Files(inputPath);
|
|
2416
|
+
const familySlug = options.family ?? deriveFamilySlug(basename3(files[0]));
|
|
2417
|
+
const resolved = resolve7(inputPath);
|
|
2418
|
+
const nonWoff2 = statSync2(resolved).isDirectory() ? getNonWoff2Fonts(resolved) : [];
|
|
2419
|
+
if (!json) {
|
|
2420
|
+
logger.heading("Visor Font Upload");
|
|
2421
|
+
logger.info(`Organization: ${org}`);
|
|
2422
|
+
logger.info(`Font family: ${familySlug}`);
|
|
2423
|
+
logger.info(`Files to upload: ${files.length}`);
|
|
2424
|
+
logger.blank();
|
|
2425
|
+
if (nonWoff2.length > 0) {
|
|
2426
|
+
logger.warn(
|
|
2427
|
+
`Skipping ${nonWoff2.length} non-woff2 file(s): ${nonWoff2.join(", ")}`
|
|
2428
|
+
);
|
|
2429
|
+
logger.blank();
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
const client = createR2Client(r2Config);
|
|
2433
|
+
const bucket = "visor-fonts";
|
|
2434
|
+
const results = [];
|
|
2435
|
+
for (const filePath of files) {
|
|
2436
|
+
const filename = basename3(filePath);
|
|
2437
|
+
const key = buildS3Key(org, familySlug, filename);
|
|
2438
|
+
if (!json) {
|
|
2439
|
+
logger.info(`Uploading ${filename}...`);
|
|
2440
|
+
}
|
|
2441
|
+
await uploadFile(client, bucket, key, filePath);
|
|
2442
|
+
const size = statSync2(filePath).size;
|
|
2443
|
+
results.push({ file: filename, key, size });
|
|
2444
|
+
if (!json) {
|
|
2445
|
+
logger.success(`Uploaded: ${key} (${formatBytes(size)})`);
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
if (json) {
|
|
2449
|
+
console.log(
|
|
2450
|
+
JSON.stringify(
|
|
2451
|
+
{
|
|
2452
|
+
success: true,
|
|
2453
|
+
org,
|
|
2454
|
+
family: familySlug,
|
|
2455
|
+
uploaded: results,
|
|
2456
|
+
totalFiles: results.length,
|
|
2457
|
+
totalBytes: results.reduce((sum, r) => sum + r.size, 0)
|
|
2458
|
+
},
|
|
2459
|
+
null,
|
|
2460
|
+
2
|
|
2461
|
+
)
|
|
2462
|
+
);
|
|
2463
|
+
} else {
|
|
2464
|
+
logger.blank();
|
|
2465
|
+
logger.success(
|
|
2466
|
+
`${results.length} file(s) uploaded to ${org}/${familySlug}/`
|
|
2467
|
+
);
|
|
2468
|
+
}
|
|
2469
|
+
} catch (error) {
|
|
2470
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2471
|
+
if (json) {
|
|
2472
|
+
console.log(
|
|
2473
|
+
JSON.stringify({ success: false, error: message })
|
|
2474
|
+
);
|
|
2475
|
+
} else {
|
|
2476
|
+
logger.error(message);
|
|
2477
|
+
}
|
|
2478
|
+
process.exit(2);
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
function formatBytes(bytes) {
|
|
2482
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
2483
|
+
const kb = bytes / 1024;
|
|
2484
|
+
if (kb < 1024) return `${kb.toFixed(1)} KB`;
|
|
2485
|
+
const mb = kb / 1024;
|
|
2486
|
+
return `${mb.toFixed(1)} MB`;
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2489
|
+
// src/index.ts
|
|
2490
|
+
var program = new Command();
|
|
2491
|
+
program.name("visor").description("CLI for the Visor design system").version("0.1.0");
|
|
2492
|
+
program.command("init").description("Initialize Visor in the current project").option("--template <name>", "scaffold a themed project (nextjs)").option("--json", "output structured JSON (for AI agents)").action((options) => {
|
|
2493
|
+
initCommand(process.cwd(), options);
|
|
2494
|
+
});
|
|
2495
|
+
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
|
+
listCommand(process.cwd(), options);
|
|
2497
|
+
});
|
|
2498
|
+
program.command("add").description("Add components, hooks, blocks, or utilities to your project").argument("[items...]", "names of registry items to add").option("--overwrite", "overwrite existing files", false).option("--category <name>", "install all items from a category").option("--block", "install blocks instead of components").option("--json", "output structured JSON (for AI agents)").action((items, options) => {
|
|
2499
|
+
addCommand(items, process.cwd(), { overwrite: options.overwrite, category: options.category, block: options.block, json: options.json });
|
|
2500
|
+
});
|
|
2501
|
+
program.command("diff").description(
|
|
2502
|
+
"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) => {
|
|
2504
|
+
diffCommand(component, process.cwd(), options);
|
|
2505
|
+
});
|
|
2506
|
+
var theme = program.command("theme").description("Theme management commands");
|
|
2507
|
+
theme.command("apply").description(
|
|
2508
|
+
"Read a .visor.yaml file and generate full CSS token overrides"
|
|
2509
|
+
).argument("<file>", "path to .visor.yaml file").option("-o, --output <path>", "output CSS file path").option("--json", "output structured JSON (for AI agents)").option("--adapter <name>", "target adapter: nextjs, fumadocs, deck").action(
|
|
2510
|
+
(file, options) => {
|
|
2511
|
+
themeApplyCommand(file, process.cwd(), {
|
|
2512
|
+
...options,
|
|
2513
|
+
adapter: options.adapter
|
|
2514
|
+
});
|
|
2515
|
+
}
|
|
2516
|
+
);
|
|
2517
|
+
theme.command("export").description(
|
|
2518
|
+
"Read current theme tokens and produce a .visor.yaml (or other format)"
|
|
2519
|
+
).argument("[file]", "path to source .visor.yaml file").option(
|
|
2520
|
+
"--format <format>",
|
|
2521
|
+
"output format: yaml or json",
|
|
2522
|
+
"yaml"
|
|
2523
|
+
).option("--json", "output structured JSON (for AI agents)").action(
|
|
2524
|
+
(file, options) => {
|
|
2525
|
+
themeExportCommand(file, process.cwd(), options);
|
|
2526
|
+
}
|
|
2527
|
+
);
|
|
2528
|
+
theme.command("validate").description("Run full validation ruleset on a .visor.yaml file").argument("<file>", "path to .visor.yaml file").option("--json", "output structured JSON (for AI agents)").action(
|
|
2529
|
+
(file, options) => {
|
|
2530
|
+
themeValidateCommand(file, process.cwd(), options);
|
|
2531
|
+
}
|
|
2532
|
+
);
|
|
2533
|
+
theme.command("extract").description(
|
|
2534
|
+
"Scan an existing project's CSS and produce a best-effort .visor.yaml theme file"
|
|
2535
|
+
).option("--from <path>", "path to project directory to scan").option("--json", "output structured JSON (for AI agents)").option("-o, --output <path>", "output file path (default: .visor.yaml)").option("--validate", "run validator on the extracted theme").action(
|
|
2536
|
+
(options) => {
|
|
2537
|
+
themeExtractCommand(process.cwd(), {
|
|
2538
|
+
...options,
|
|
2539
|
+
runValidation: options.validate
|
|
2540
|
+
});
|
|
2541
|
+
}
|
|
2542
|
+
);
|
|
2543
|
+
theme.command("register").description(
|
|
2544
|
+
"Register a theme in the docs site \u2014 creates CSS, updates globals.css and theme-config.ts"
|
|
2545
|
+
).argument("<file>", "path to .visor.yaml file").requiredOption("--group <name>", "theme group to register in (e.g. Visor, Client, Low Orbit)").option("--dry-run", "show what would change without writing files").option("--json", "output structured JSON (for AI agents)").action(
|
|
2546
|
+
(file, options) => {
|
|
2547
|
+
themeRegisterCommand(file, process.cwd(), options);
|
|
2548
|
+
}
|
|
2549
|
+
);
|
|
2550
|
+
theme.command("unregister").description(
|
|
2551
|
+
"Remove a theme from the docs site \u2014 deletes CSS file, removes globals.css import and theme-config.ts entry"
|
|
2552
|
+
).argument("<slug>", "theme slug to unregister (e.g. entr, kaiah)").option("--json", "output structured JSON (for AI agents)").action(
|
|
2553
|
+
(slug, options) => {
|
|
2554
|
+
themeUnregisterCommand(slug, process.cwd(), options);
|
|
2555
|
+
}
|
|
2556
|
+
);
|
|
2557
|
+
theme.command("sync").description(
|
|
2558
|
+
"Scan themes/ and custom-themes/ directories, regenerate all theme CSS, globals.css imports, and theme-config.ts"
|
|
2559
|
+
).option("--dry-run", "show what would change without writing files").option("--json", "output structured JSON (for AI agents)").action(
|
|
2560
|
+
(options) => {
|
|
2561
|
+
themeSyncCommand(process.cwd(), options);
|
|
2562
|
+
}
|
|
2563
|
+
);
|
|
2564
|
+
var fonts = program.command("fonts").description("Font library management commands");
|
|
2565
|
+
fonts.command("add").description("Upload woff2 font files to the Visor Font Library on R2").argument("<path>", "path to a .woff2 file or directory containing .woff2 files").requiredOption("--org <org>", "organization namespace (e.g. low-orbit)").option("--family <name>", "font family slug (auto-inferred from filename if omitted)").option("--json", "output structured JSON (for AI agents)").action(
|
|
2566
|
+
(path, options) => {
|
|
2567
|
+
fontsAddCommand(path, options);
|
|
2568
|
+
}
|
|
2569
|
+
);
|
|
2570
|
+
program.parse();
|