vibe-design-system 1.0.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/bin/init.js ADDED
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * VDS installer: npx vibe-design-system init
4
+ * Copies vds-core-template to vds-core, adds scripts and /vds route.
5
+ */
6
+ import fs from "fs";
7
+ import path from "path";
8
+ import { fileURLToPath } from "url";
9
+
10
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
+ const INSTALLER_ROOT = path.join(__dirname, "..");
12
+ const TEMPLATE_DIR = path.join(INSTALLER_ROOT, "vds-core-template");
13
+
14
+ function getProjectRoot() {
15
+ const cwd = process.cwd();
16
+ try {
17
+ const pkg = JSON.parse(fs.readFileSync(path.join(cwd, "package.json"), "utf-8"));
18
+ if (pkg.name === "vibe-design-system") return path.join(cwd, "..");
19
+ } catch (_) {}
20
+ return cwd;
21
+ }
22
+
23
+ function copyRecursive(src, dest) {
24
+ const stat = fs.statSync(src);
25
+ if (stat.isDirectory()) {
26
+ if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
27
+ for (const name of fs.readdirSync(src)) {
28
+ copyRecursive(path.join(src, name), path.join(dest, name));
29
+ }
30
+ } else {
31
+ fs.copyFileSync(src, dest);
32
+ }
33
+ }
34
+
35
+ function addScripts(projectRoot) {
36
+ const pkgPath = path.join(projectRoot, "package.json");
37
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
38
+ const scripts = pkg.scripts || {};
39
+ if (!scripts.vds) scripts.vds = "node vds-core/scan.mjs";
40
+ if (!scripts["vds:watch"]) scripts["vds:watch"] = "node vds-core/scan.mjs --watch";
41
+ pkg.scripts = scripts;
42
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2), "utf-8");
43
+ }
44
+
45
+ function hasVdsRoute(content) {
46
+ return /path\s*=\s*["']\/vds["']/.test(content) || /path\s*=\s*\{["']\/vds["']\}/.test(content);
47
+ }
48
+
49
+ function hasVdsImport(content) {
50
+ return /from\s+["']@\/vds-core["']/.test(content) || /from\s+["'].*vds-core["']/.test(content);
51
+ }
52
+
53
+ function addVdsRouteToApp(projectRoot) {
54
+ const appPath = path.join(projectRoot, "src", "App.tsx");
55
+ const mainPath = path.join(projectRoot, "src", "main.tsx");
56
+ let pathToEdit = null;
57
+ if (fs.existsSync(appPath)) pathToEdit = appPath;
58
+ else if (fs.existsSync(mainPath)) pathToEdit = mainPath;
59
+ if (!pathToEdit) return;
60
+
61
+ let content = fs.readFileSync(pathToEdit, "utf-8");
62
+ if (hasVdsRoute(content)) return;
63
+
64
+ if (!hasVdsImport(content)) {
65
+ const lastImportMatch = content.match(/\n(import\s+.+?;\s*)\n(?!\s*import)/s);
66
+ if (lastImportMatch) {
67
+ content = content.replace(lastImportMatch[0], lastImportMatch[0] + '\nimport VdsDashboard from "@/vds-core";');
68
+ } else {
69
+ content = content.replace(/(\n)(export\s|const\s|\()/m, '\nimport VdsDashboard from "@/vds-core";\n$2');
70
+ }
71
+ }
72
+
73
+ const routeInsert = '<Route path="/vds" element={<VdsDashboard />} />';
74
+ if (content.includes('<Route path="*"') || content.includes("<Route path=\"*\"")) {
75
+ content = content.replace(
76
+ /(\s*)(<Route\s+path=["']\*["'])/,
77
+ `$1${routeInsert}\n$1$2`
78
+ );
79
+ } else {
80
+ const routesMatch = content.match(/(<Routes[^>]*>)([\s\S]*?)(<\/Routes>)/);
81
+ if (routesMatch) {
82
+ const [, open, inner, close] = routesMatch;
83
+ content = content.replace(routesMatch[0], open + inner.trimEnd() + "\n " + routeInsert + "\n " + close);
84
+ }
85
+ }
86
+
87
+ fs.writeFileSync(pathToEdit, content, "utf-8");
88
+ }
89
+
90
+ console.log("🎨 VDS kuruluyor...");
91
+
92
+ const projectRoot = getProjectRoot();
93
+ const vdsCoreDest = path.join(projectRoot, "vds-core");
94
+
95
+ if (!fs.existsSync(vdsCoreDest)) {
96
+ if (!fs.existsSync(TEMPLATE_DIR)) {
97
+ console.error("vds-core-template bulunamadı.");
98
+ process.exit(1);
99
+ }
100
+ copyRecursive(TEMPLATE_DIR, vdsCoreDest);
101
+ }
102
+
103
+ addScripts(projectRoot);
104
+ addVdsRouteToApp(projectRoot);
105
+
106
+ console.log("✅ VDS kuruldu! npm run vds:watch çalıştırın, sonra /vds adresini açın.");
package/package.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "vibe-design-system",
3
+ "version": "1.0.0",
4
+ "description": "Auto-generate design systems for vibe coding projects",
5
+ "type": "module",
6
+ "bin": {
7
+ "vibe-design-system": "./bin/init.js"
8
+ },
9
+ "files": ["bin", "vds-core-template"],
10
+ "engines": { "node": ">=18" }
11
+ }
@@ -0,0 +1,28 @@
1
+ # VDS Core — Ghost mode
2
+
3
+ Zero-config, autonomous design system. Drop this folder into any Vite + React project and activate with one route.
4
+
5
+ ## Activation (any project)
6
+
7
+ 1. **Scan** (regenerate manifest):
8
+ ```bash
9
+ npm run vds
10
+ ```
11
+ Add to `package.json` if missing:
12
+ ```json
13
+ "vds": "node vds-core/scan.mjs"
14
+ ```
15
+
16
+ 2. **Mount** the dashboard (e.g. in your router):
17
+ ```tsx
18
+ import VdsDashboard from "@/vds-core";
19
+ // ...
20
+ <Route path="/vds" element={<VdsDashboard />} />
21
+ ```
22
+
23
+ 3. Ensure `public/vds-output.json` exists (created by step 1). Then open `/vds`.
24
+
25
+ ## Layout
26
+
27
+ - `vds-core/scan.mjs` — Node script; scans `src/components`, CSS, Tailwind config. Writes `vds-output.json` and `public/vds-output.json`. Paths are relative to **project root** (parent of `vds-core`).
28
+ - `src/vds-core/` — Dashboard UI; reads `/vds-output.json`, renders Foundations + Component Library. No config; works as soon as the route is mounted.
@@ -0,0 +1,496 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * VDS Core — Ghost scan. Run from project root: node vds-core/scan.mjs
4
+ * Paths are relative to the host project root (parent of vds-core).
5
+ * Set VDS_LOCALE=en|tr for CLI message language (default: en).
6
+ */
7
+ import fs from "fs";
8
+ import path from "path";
9
+ import { fileURLToPath } from "url";
10
+
11
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
+ const PROJECT_ROOT = path.join(__dirname, "..");
13
+
14
+ const CLI_LOCALES = {
15
+ en: {
16
+ componentsNotFound: "src/components not found. VDS scan skipped.",
17
+ scanComplete: "VDS: {n} components → vds-output.json & public/vds-output.json",
18
+ },
19
+ tr: {
20
+ componentsNotFound: "src/components bulunamadı. VDS taraması atlandı.",
21
+ scanComplete: "VDS: {n} bileşen → vds-output.json ve public/vds-output.json",
22
+ },
23
+ };
24
+ const CLI_LOCALE = (process.env.VDS_LOCALE === "tr" ? "tr" : "en");
25
+ const cliT = (key, n) => CLI_LOCALES[CLI_LOCALE][key].replace("{n}", String(n));
26
+ const COMPONENTS_DIR = path.join(PROJECT_ROOT, "src", "components");
27
+ const OUTPUT_FILE = path.join(PROJECT_ROOT, "vds-output.json");
28
+ const PUBLIC_MANIFEST = path.join(PROJECT_ROOT, "public", "vds-output.json");
29
+ const HISTORY_FILE = path.join(PROJECT_ROOT, "vds-history.json");
30
+
31
+ function incPatch(version) {
32
+ const parts = String(version || "1.0.0").split(".");
33
+ const major = parseInt(parts[0], 10) || 1;
34
+ const minor = parseInt(parts[1], 10) || 0;
35
+ const patch = (parseInt(parts[2], 10) || 0) + 1;
36
+ return `${major}.${minor}.${patch}`;
37
+ }
38
+
39
+ function readPreviousOutput() {
40
+ try {
41
+ if (fs.existsSync(OUTPUT_FILE)) {
42
+ const raw = fs.readFileSync(OUTPUT_FILE, "utf-8");
43
+ const data = JSON.parse(raw);
44
+ return data.components || [];
45
+ }
46
+ } catch (_) {}
47
+ return null;
48
+ }
49
+
50
+ function readLastVersion() {
51
+ try {
52
+ if (fs.existsSync(HISTORY_FILE)) {
53
+ const raw = fs.readFileSync(HISTORY_FILE, "utf-8");
54
+ const data = JSON.parse(raw);
55
+ const entries = Array.isArray(data) ? data : data.entries || [];
56
+ const last = entries[entries.length - 1];
57
+ return last ? last.version : "1.0.0";
58
+ }
59
+ } catch (_) {}
60
+ return "1.0.0";
61
+ }
62
+
63
+ function readHistoryEntries() {
64
+ try {
65
+ if (fs.existsSync(HISTORY_FILE)) {
66
+ const raw = fs.readFileSync(HISTORY_FILE, "utf-8");
67
+ const data = JSON.parse(raw);
68
+ return Array.isArray(data) ? data : data.entries || [];
69
+ }
70
+ } catch (_) {}
71
+ return [];
72
+ }
73
+
74
+ function compareComponents(prevList, newResults) {
75
+ const prevByFile = new Map((prevList || []).map((c) => [c.file, c]));
76
+ const newByFile = new Map(newResults.map((c) => [c.file, c]));
77
+ const added = [];
78
+ const removed = [];
79
+ const modified = [];
80
+ for (const [file, c] of newByFile) {
81
+ const p = prevByFile.get(file);
82
+ if (!p) added.push({ type: "added", name: c.name, group: c.group, category: c.category });
83
+ else if (p.name !== c.name || p.group !== c.group || p.category !== c.category) {
84
+ modified.push({ type: "modified", name: c.name, group: c.group, category: c.category });
85
+ }
86
+ }
87
+ for (const [file, c] of prevByFile) {
88
+ if (!newByFile.has(file)) removed.push({ type: "removed", name: c.name, group: c.group, category: c.category });
89
+ }
90
+ return { added, removed, modified };
91
+ }
92
+
93
+ const CLASSIFICATION_RULES = [
94
+ { keywords: ["analysisdashboard", "componentlibrary"], group: "Sections", category: "Dashboard" },
95
+ {
96
+ keywords: [
97
+ "enterprisepushpanel",
98
+ "figmalibrarygenerator",
99
+ "integrationguide",
100
+ "projectdropzone",
101
+ "repoconnect",
102
+ "tokensstudioguide",
103
+ ],
104
+ group: "Sections",
105
+ category: "Features",
106
+ },
107
+ { keywords: ["hover-card"], group: "Feedback", category: "Feedback" },
108
+ {
109
+ keywords: [
110
+ "card",
111
+ "table",
112
+ "list",
113
+ "badge",
114
+ "avatar",
115
+ "chart",
116
+ "accordion",
117
+ "tabs",
118
+ "collapsible",
119
+ "toggle-group",
120
+ "carousel",
121
+ "scroll-area",
122
+ ],
123
+ group: "Data Display",
124
+ category: "Data Display",
125
+ },
126
+ { keywords: ["toggle"], group: "Actions", category: "Toggle" },
127
+ { keywords: ["button", "cta"], group: "Actions", category: "Button" },
128
+ { keywords: ["drawer", "sheet"], group: "Feedback", category: "Overlay" },
129
+ {
130
+ keywords: [
131
+ "modal",
132
+ "dialog",
133
+ "alert",
134
+ "toast",
135
+ "toaster",
136
+ "sonner",
137
+ "tooltip",
138
+ "popover",
139
+ "hover-card",
140
+ "progress",
141
+ "skeleton",
142
+ ],
143
+ group: "Feedback",
144
+ category: "Feedback",
145
+ },
146
+ {
147
+ keywords: [
148
+ "input",
149
+ "form",
150
+ "select",
151
+ "checkbox",
152
+ "textarea",
153
+ "label",
154
+ "switch",
155
+ "radio",
156
+ "slider",
157
+ "calendar",
158
+ "input-otp",
159
+ "command",
160
+ ],
161
+ group: "Forms",
162
+ category: "Forms",
163
+ },
164
+ {
165
+ keywords: [
166
+ "nav",
167
+ "header",
168
+ "sidebar",
169
+ "menu",
170
+ "menubar",
171
+ "dropdown-menu",
172
+ "navigation-menu",
173
+ "context-menu",
174
+ "navlink",
175
+ "breadcrumb",
176
+ "pagination",
177
+ ],
178
+ group: "Navigation",
179
+ category: "Navigation",
180
+ },
181
+ { keywords: ["separator", "aspect-ratio", "resizable"], group: "Layout", category: "Layout" },
182
+ {
183
+ keywords: ["hero", "banner", "section", "pricing", "feature", "featuresgrid", "interactivedemo", "modernhero"],
184
+ group: "Sections",
185
+ category: "Sections",
186
+ },
187
+ ];
188
+
189
+ function getAllComponentFiles(dir, baseDir = dir) {
190
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
191
+ const files = [];
192
+ for (const entry of entries) {
193
+ const fullPath = path.join(dir, entry.name);
194
+ if (entry.isDirectory()) {
195
+ files.push(...getAllComponentFiles(fullPath, baseDir));
196
+ } else if (entry.isFile() && /\.(tsx|jsx)$/i.test(entry.name)) {
197
+ files.push(path.relative(baseDir, fullPath).replace(/\\/g, "/"));
198
+ }
199
+ }
200
+ return files;
201
+ }
202
+
203
+ function extractVdsTags(content) {
204
+ const tags = {};
205
+ const tagNames = ["vds-group", "vds-category", "vds-name", "vds-description"];
206
+ for (const tag of tagNames) {
207
+ const re = new RegExp(`@${tag}\\s+([^\n*]+)`, "g");
208
+ const m = re.exec(content);
209
+ if (m) tags[tag.replace("vds-", "")] = m[1].trim();
210
+ }
211
+ return Object.keys(tags).length ? tags : null;
212
+ }
213
+
214
+ function classifyByFileName(filePath) {
215
+ const baseName = path.basename(filePath, path.extname(filePath)).toLowerCase();
216
+ for (const rule of CLASSIFICATION_RULES) {
217
+ const found = rule.keywords.some((kw) => baseName.includes(kw));
218
+ if (found) return { group: rule.group, category: rule.category };
219
+ }
220
+ return { group: "Uncategorized", category: "Uncategorized" };
221
+ }
222
+
223
+ function humanizeName(filePath) {
224
+ const base = path.basename(filePath, path.extname(filePath));
225
+ return base.replace(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase()).trim();
226
+ }
227
+
228
+ function extractTailwindTokens(content) {
229
+ const tokens = new Set();
230
+ const patterns = [
231
+ /className\s*=\s*["'`]([^"'`]+)["'`]/g,
232
+ /className\s*=\s*\{\s*["'`]([^"'`]+)["'`]/g,
233
+ /cn\s*\(\s*["'`]([^"'`]+)["'`]/g,
234
+ /cva\s*\(\s*["'`]([^"'`]+)["'`]/g,
235
+ /["'`]([a-zA-Z0-9_\-\/\s\[\]&:%.]+(?:hover|focus|active|disabled|sm|md|lg|xl|2xl|dark:)[a-zA-Z0-9_\-\/\s\[\]&:%.]*)["'`]/g,
236
+ ];
237
+ for (const re of patterns) {
238
+ let m;
239
+ while ((m = re.exec(content)) !== null) {
240
+ const part = m[1];
241
+ part
242
+ .replace(/\$\{[^}]*\}/g, " ")
243
+ .split(/\s+/)
244
+ .map((s) => s.trim())
245
+ .filter((s) => s.length > 0 && /^[a-zA-Z0-9_\-\[\]\/:&%.]+$/.test(s))
246
+ .forEach((s) => tokens.add(s));
247
+ }
248
+ }
249
+ return [...tokens].sort();
250
+ }
251
+
252
+ /** Parse HSL string "0 0% 0%" or "hsl(0 0% 0%)" and return hex. */
253
+ function hslToHex(hslStr) {
254
+ const match = hslStr.match(/hsl\s*\(\s*([\d.]+)\s*[, ]\s*([\d.]+)%\s*[, ]\s*([\d.]+)%\s*\)/) ||
255
+ hslStr.match(/^([\d.]+)\s+([\d.]+)%\s+([\d.]+)%$/);
256
+ if (!match) return null;
257
+ const h = Number(match[1]) / 360;
258
+ const s = Number(match[2]) / 100;
259
+ const l = Number(match[3]) / 100;
260
+ let r, g, b;
261
+ if (s === 0) {
262
+ r = g = b = l;
263
+ } else {
264
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
265
+ const p = 2 * l - q;
266
+ r = hue2rgb(p, q, h + 1 / 3);
267
+ g = hue2rgb(p, q, h);
268
+ b = hue2rgb(p, q, h - 1 / 3);
269
+ }
270
+ const toHex = (x) => {
271
+ const n = Math.round(Math.max(0, Math.min(255, x * 255)));
272
+ return n.toString(16).padStart(2, "0");
273
+ };
274
+ return "#" + toHex(r) + toHex(g) + toHex(b);
275
+ }
276
+ function hue2rgb(p, q, t) {
277
+ if (t < 0) t += 1;
278
+ if (t > 1) t -= 1;
279
+ if (t < 1 / 6) return p + (q - p) * 6 * t;
280
+ if (t < 1 / 2) return q;
281
+ if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
282
+ return p;
283
+ }
284
+
285
+ function parseCssVarBlock(block) {
286
+ const out = {};
287
+ const varRe = /--([a-zA-Z0-9-]+)\s*:\s*([^;]+);/g;
288
+ let m;
289
+ while ((m = varRe.exec(block)) !== null) {
290
+ const name = m[1];
291
+ const raw = m[2].trim();
292
+ out[name] = raw;
293
+ }
294
+ return out;
295
+ }
296
+
297
+ function extractFoundations() {
298
+ const colors = {};
299
+ const colorsDark = {};
300
+ const typography = {};
301
+ const cssRadiusVars = {};
302
+ const borderRadiusScale = {};
303
+ const cssPath = path.join(PROJECT_ROOT, "src", "index.css");
304
+ const globalsCss = path.join(PROJECT_ROOT, "src", "globals.css");
305
+ const appGlobals = path.join(PROJECT_ROOT, "app", "globals.css");
306
+ const cssToRead = fs.existsSync(cssPath) ? cssPath : fs.existsSync(globalsCss) ? globalsCss : appGlobals;
307
+
308
+ try {
309
+ if (fs.existsSync(cssToRead)) {
310
+ const css = fs.readFileSync(cssToRead, "utf-8");
311
+
312
+ const rootMatch = css.match(/:root\s*\{([\s\S]*?)\}/);
313
+ if (rootMatch) {
314
+ const rootVars = parseCssVarBlock(rootMatch[1]);
315
+ for (const [name, value] of Object.entries(rootVars)) {
316
+ if (/\d+\s+\d+%/.test(value) || /\d+%/.test(value)) {
317
+ const hsl = `hsl(${value})`;
318
+ colors[name] = { value: hsl, hex: hslToHex(hsl) || hsl };
319
+ } else if (/rem|px/.test(value)) {
320
+ cssRadiusVars[name] = value;
321
+ } else {
322
+ colors[name] = { value, hex: value };
323
+ }
324
+ }
325
+ }
326
+
327
+ const darkMatch = css.match(/\.dark\s*\{([\s\S]*?)\}/) ||
328
+ css.match(/\[data-theme\s*=\s*["']dark["']\]\s*\{([\s\S]*?)\}/);
329
+ if (darkMatch) {
330
+ const darkVars = parseCssVarBlock(darkMatch[1]);
331
+ for (const [name, value] of Object.entries(darkVars)) {
332
+ if (/\d+\s+\d+%/.test(value) || /\d+%/.test(value)) {
333
+ const hsl = `hsl(${value})`;
334
+ colorsDark[name] = { value: hsl, hex: hslToHex(hsl) || hsl };
335
+ }
336
+ }
337
+ }
338
+
339
+ const bodyMatch = css.match(/body\s*\{[^}]*font-family:\s*([^;]+);/);
340
+ if (bodyMatch) typography.body = bodyMatch[1].trim();
341
+ const monoMatch = css.match(/code,\s*pre,\s*\.font-mono\s*\{[^}]*font-family:\s*([^;]+);/);
342
+ if (monoMatch) typography.mono = monoMatch[1].trim();
343
+ }
344
+ } catch (_) {}
345
+
346
+ try {
347
+ const twPath = path.join(PROJECT_ROOT, "tailwind.config.ts");
348
+ const twPathJs = path.join(PROJECT_ROOT, "tailwind.config.js");
349
+ const twFile = fs.existsSync(twPath) ? twPath : twPathJs;
350
+ if (fs.existsSync(twFile)) {
351
+ const tw = fs.readFileSync(twFile, "utf-8");
352
+ const sansMatch = tw.match(/sans:\s*\[([^\]]+)\]/);
353
+ if (sansMatch) {
354
+ typography.tailwindSans = sansMatch[1]
355
+ .split(",")
356
+ .map((s) => s.trim().replace(/^['"`]|['"`]$/g, ""))
357
+ .filter(Boolean);
358
+ }
359
+ const monoMatch2 = tw.match(/mono:\s*\[([^\]]+)\]/);
360
+ if (monoMatch2) {
361
+ typography.tailwindMono = monoMatch2[1]
362
+ .split(",")
363
+ .map((s) => s.trim().replace(/^['"`]|['"`]$/g, ""))
364
+ .filter(Boolean);
365
+ }
366
+ const brMatch = tw.match(/borderRadius:\s*\{([\s\S]*?)\}/);
367
+ if (brMatch) {
368
+ const body = brMatch[1];
369
+ const brRe = /(\w+):\s*"([^"]+)"/g;
370
+ let m2;
371
+ while ((m2 = brRe.exec(body)) !== null) borderRadiusScale[m2[1]] = m2[2];
372
+ }
373
+ }
374
+ } catch (_) {}
375
+
376
+ const radius = {};
377
+ if (cssRadiusVars.radius) radius.base = cssRadiusVars.radius;
378
+ if (Object.keys(borderRadiusScale).length > 0) radius.borderRadius = borderRadiusScale;
379
+ const foundationsColors = { ...colors };
380
+ if (Object.keys(colorsDark).length > 0) foundationsColors._dark = colorsDark;
381
+ return { colors: foundationsColors, typography, radius };
382
+ }
383
+
384
+ function scan() {
385
+ if (!fs.existsSync(COMPONENTS_DIR)) {
386
+ console.error(CLI_LOCALES[CLI_LOCALE].componentsNotFound);
387
+ process.exit(1);
388
+ }
389
+ const relativeFiles = getAllComponentFiles(COMPONENTS_DIR);
390
+ const results = [];
391
+ for (const rel of relativeFiles) {
392
+ const fullPath = path.join(COMPONENTS_DIR, rel);
393
+ const content = fs.readFileSync(fullPath, "utf-8");
394
+ const vdsTags = extractVdsTags(content);
395
+ let group, category, name, description;
396
+ if (vdsTags) {
397
+ group = vdsTags.group ?? "Uncategorized";
398
+ category = vdsTags.category ?? "Uncategorized";
399
+ name = vdsTags.name ?? humanizeName(rel);
400
+ description = vdsTags.description ?? "";
401
+ } else {
402
+ const classified = classifyByFileName(rel);
403
+ group = classified.group;
404
+ category = classified.category;
405
+ name = humanizeName(rel);
406
+ description = "";
407
+ }
408
+ const tokens = extractTailwindTokens(content);
409
+ results.push({ file: rel, name, group, category, description, tokens });
410
+ }
411
+ const foundations = extractFoundations();
412
+ const output = {
413
+ scannedAt: new Date().toISOString(),
414
+ totalComponents: results.length,
415
+ components: results,
416
+ foundations,
417
+ };
418
+
419
+ const prevComponents = readPreviousOutput();
420
+ const { added, removed, modified } = compareComponents(prevComponents, results);
421
+ const hasChanges = added.length > 0 || removed.length > 0 || modified.length > 0;
422
+
423
+ fs.writeFileSync(OUTPUT_FILE, JSON.stringify(output, null, 2), "utf-8");
424
+ const publicDir = path.join(PROJECT_ROOT, "public");
425
+ if (!fs.existsSync(publicDir)) fs.mkdirSync(publicDir, { recursive: true });
426
+ fs.writeFileSync(PUBLIC_MANIFEST, JSON.stringify(output, null, 2), "utf-8");
427
+
428
+ if (hasChanges) {
429
+ const lastVersion = readLastVersion();
430
+ const nextVersion = incPatch(lastVersion);
431
+ const changes = [...added, ...removed, ...modified];
432
+ const historyEntry = {
433
+ version: nextVersion,
434
+ timestamp: new Date().toISOString(),
435
+ changes,
436
+ };
437
+ const entries = readHistoryEntries();
438
+ entries.push(historyEntry);
439
+ fs.writeFileSync(HISTORY_FILE, JSON.stringify(entries, null, 2), "utf-8");
440
+ console.log(
441
+ `[VDS] v${nextVersion}: ${added.length} added, ${removed.length} removed, ${modified.length} modified`
442
+ );
443
+ console.log(cliT("scanComplete", results.length));
444
+ }
445
+ }
446
+
447
+ const isWatch = process.argv.includes("--watch");
448
+
449
+ if (isWatch) {
450
+ if (!fs.existsSync(COMPONENTS_DIR)) {
451
+ console.error(CLI_LOCALES[CLI_LOCALE].componentsNotFound);
452
+ process.exit(1);
453
+ }
454
+ let debounceTimer = null;
455
+ let lastChangedFiles = new Set();
456
+
457
+ function runScan() {
458
+ const files = [...lastChangedFiles];
459
+ lastChangedFiles.clear();
460
+ if (files.length) {
461
+ const rel = path.relative(PROJECT_ROOT, files[0]);
462
+ if (files.length === 1) {
463
+ console.log(`[VDS] ${rel}`);
464
+ } else {
465
+ console.log(`[VDS] ${files.length} files changed`);
466
+ }
467
+ }
468
+ scan();
469
+ console.log("[VDS] Watch: src/components (.tsx, .jsx). Scan complete.\n");
470
+ }
471
+
472
+ function scheduleScan(filename) {
473
+ if (!filename || !/\.(tsx|jsx)$/i.test(filename)) return;
474
+ lastChangedFiles.add(path.join(COMPONENTS_DIR, filename));
475
+ clearTimeout(debounceTimer);
476
+ debounceTimer = setTimeout(runScan, 300);
477
+ }
478
+
479
+ console.log("[VDS] Watching src/components for .tsx / .jsx changes…\n");
480
+ runScan();
481
+
482
+ try {
483
+ const watcher = fs.watch(COMPONENTS_DIR, { recursive: true }, (eventType, filename) => {
484
+ if (filename) scheduleScan(filename);
485
+ });
486
+ process.on("SIGINT", () => {
487
+ watcher.close();
488
+ process.exit(0);
489
+ });
490
+ } catch (err) {
491
+ console.error("[VDS] Watch failed:", err.message);
492
+ process.exit(1);
493
+ }
494
+ } else {
495
+ scan();
496
+ }