real-prototypes-skill 0.1.1 → 0.1.2
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/.claude/skills/real-prototypes-skill/SKILL.md +212 -16
- package/.claude/skills/real-prototypes-skill/cli.js +523 -17
- package/.claude/skills/real-prototypes-skill/scripts/detect-prototype.js +652 -0
- package/.claude/skills/real-prototypes-skill/scripts/extract-components.js +731 -0
- package/.claude/skills/real-prototypes-skill/scripts/extract-css.js +557 -0
- package/.claude/skills/real-prototypes-skill/scripts/generate-plan.js +744 -0
- package/.claude/skills/real-prototypes-skill/scripts/html-to-react.js +645 -0
- package/.claude/skills/real-prototypes-skill/scripts/inject-component.js +604 -0
- package/.claude/skills/real-prototypes-skill/scripts/project-structure.js +457 -0
- package/.claude/skills/real-prototypes-skill/scripts/visual-diff.js +474 -0
- package/.claude/skills/real-prototypes-skill/validation/color-validator.js +496 -0
- package/bin/cli.js +1 -1
- package/package.json +4 -1
|
@@ -0,0 +1,652 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Prototype Detection Module
|
|
5
|
+
*
|
|
6
|
+
* Detects existing prototypes in a project directory to prevent creating
|
|
7
|
+
* new projects when one already exists. Also maps captured pages to
|
|
8
|
+
* existing prototype files.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* node detect-prototype.js --project <name>
|
|
12
|
+
* node detect-prototype.js --path /path/to/project
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
|
|
18
|
+
// Framework detection patterns
|
|
19
|
+
const FRAMEWORK_PATTERNS = {
|
|
20
|
+
'next.js-app-router': {
|
|
21
|
+
markers: ['next.config.js', 'next.config.mjs', 'next.config.ts'],
|
|
22
|
+
structure: ['app/', 'src/app/'],
|
|
23
|
+
pagePattern: /page\.(tsx?|jsx?)$/,
|
|
24
|
+
layoutPattern: /layout\.(tsx?|jsx?)$/
|
|
25
|
+
},
|
|
26
|
+
'next.js-pages-router': {
|
|
27
|
+
markers: ['next.config.js', 'next.config.mjs', 'next.config.ts'],
|
|
28
|
+
structure: ['pages/', 'src/pages/'],
|
|
29
|
+
pagePattern: /\.(tsx?|jsx?)$/
|
|
30
|
+
},
|
|
31
|
+
'react-vite': {
|
|
32
|
+
markers: ['vite.config.js', 'vite.config.ts'],
|
|
33
|
+
structure: ['src/'],
|
|
34
|
+
pagePattern: /\.(tsx?|jsx?)$/
|
|
35
|
+
},
|
|
36
|
+
'react-cra': {
|
|
37
|
+
markers: ['react-scripts'],
|
|
38
|
+
structure: ['src/'],
|
|
39
|
+
pagePattern: /\.(tsx?|jsx?)$/
|
|
40
|
+
},
|
|
41
|
+
'vue': {
|
|
42
|
+
markers: ['vue.config.js', 'vite.config.js'],
|
|
43
|
+
structure: ['src/'],
|
|
44
|
+
pagePattern: /\.vue$/
|
|
45
|
+
},
|
|
46
|
+
'angular': {
|
|
47
|
+
markers: ['angular.json'],
|
|
48
|
+
structure: ['src/app/'],
|
|
49
|
+
pagePattern: /\.component\.ts$/
|
|
50
|
+
},
|
|
51
|
+
'svelte': {
|
|
52
|
+
markers: ['svelte.config.js'],
|
|
53
|
+
structure: ['src/'],
|
|
54
|
+
pagePattern: /\.svelte$/
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// Styling approach detection
|
|
59
|
+
const STYLING_PATTERNS = {
|
|
60
|
+
'tailwind': {
|
|
61
|
+
markers: ['tailwind.config.js', 'tailwind.config.ts', 'tailwind.config.mjs'],
|
|
62
|
+
imports: ['tailwindcss', 'tailwind']
|
|
63
|
+
},
|
|
64
|
+
'css-modules': {
|
|
65
|
+
patterns: [/\.module\.css$/, /\.module\.scss$/]
|
|
66
|
+
},
|
|
67
|
+
'styled-components': {
|
|
68
|
+
imports: ['styled-components']
|
|
69
|
+
},
|
|
70
|
+
'emotion': {
|
|
71
|
+
imports: ['@emotion/react', '@emotion/styled']
|
|
72
|
+
},
|
|
73
|
+
'sass': {
|
|
74
|
+
markers: [],
|
|
75
|
+
patterns: [/\.scss$/, /\.sass$/]
|
|
76
|
+
},
|
|
77
|
+
'inline-styles': {
|
|
78
|
+
patterns: [/style\s*=\s*\{\{/]
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
class PrototypeDetector {
|
|
83
|
+
constructor(projectPath) {
|
|
84
|
+
this.projectPath = path.resolve(projectPath);
|
|
85
|
+
this.result = {
|
|
86
|
+
exists: false,
|
|
87
|
+
framework: null,
|
|
88
|
+
frameworkVersion: null,
|
|
89
|
+
styling: [],
|
|
90
|
+
projectRoot: null,
|
|
91
|
+
srcRoot: null,
|
|
92
|
+
pages: [],
|
|
93
|
+
components: [],
|
|
94
|
+
mappedPages: {},
|
|
95
|
+
packageJson: null
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Run full detection
|
|
101
|
+
*/
|
|
102
|
+
detect() {
|
|
103
|
+
// Step 1: Find package.json
|
|
104
|
+
this.findPackageJson();
|
|
105
|
+
|
|
106
|
+
if (!this.result.exists) {
|
|
107
|
+
return this.result;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Step 2: Detect framework
|
|
111
|
+
this.detectFramework();
|
|
112
|
+
|
|
113
|
+
// Step 3: Find source root
|
|
114
|
+
this.findSourceRoot();
|
|
115
|
+
|
|
116
|
+
// Step 4: Detect styling approach
|
|
117
|
+
this.detectStyling();
|
|
118
|
+
|
|
119
|
+
// Step 5: Find existing pages
|
|
120
|
+
this.findPages();
|
|
121
|
+
|
|
122
|
+
// Step 6: Find existing components
|
|
123
|
+
this.findComponents();
|
|
124
|
+
|
|
125
|
+
return this.result;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Find package.json in project directory
|
|
130
|
+
*/
|
|
131
|
+
findPackageJson() {
|
|
132
|
+
const possiblePaths = [
|
|
133
|
+
path.join(this.projectPath, 'package.json'),
|
|
134
|
+
path.join(this.projectPath, 'prototype', 'package.json'),
|
|
135
|
+
path.join(this.projectPath, 'src', 'package.json')
|
|
136
|
+
];
|
|
137
|
+
|
|
138
|
+
for (const pkgPath of possiblePaths) {
|
|
139
|
+
if (fs.existsSync(pkgPath)) {
|
|
140
|
+
try {
|
|
141
|
+
this.result.packageJson = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
142
|
+
this.result.projectRoot = path.dirname(pkgPath);
|
|
143
|
+
this.result.exists = true;
|
|
144
|
+
return;
|
|
145
|
+
} catch (e) {
|
|
146
|
+
// Continue to next path
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Detect framework from markers and dependencies
|
|
154
|
+
*/
|
|
155
|
+
detectFramework() {
|
|
156
|
+
if (!this.result.packageJson) return;
|
|
157
|
+
|
|
158
|
+
const deps = {
|
|
159
|
+
...this.result.packageJson.dependencies,
|
|
160
|
+
...this.result.packageJson.devDependencies
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
// Check for Next.js
|
|
164
|
+
if (deps['next']) {
|
|
165
|
+
const version = deps['next'].replace(/[\^~]/, '');
|
|
166
|
+
this.result.frameworkVersion = version;
|
|
167
|
+
|
|
168
|
+
// Check if using App Router or Pages Router
|
|
169
|
+
const appDir = this.checkDir('app') || this.checkDir('src/app');
|
|
170
|
+
const pagesDir = this.checkDir('pages') || this.checkDir('src/pages');
|
|
171
|
+
|
|
172
|
+
if (appDir) {
|
|
173
|
+
this.result.framework = 'next.js-app-router';
|
|
174
|
+
} else if (pagesDir) {
|
|
175
|
+
this.result.framework = 'next.js-pages-router';
|
|
176
|
+
} else {
|
|
177
|
+
// Default to app router for Next.js 13+
|
|
178
|
+
const majorVersion = parseInt(version.split('.')[0], 10);
|
|
179
|
+
this.result.framework = majorVersion >= 13 ? 'next.js-app-router' : 'next.js-pages-router';
|
|
180
|
+
}
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Check for Vite + React
|
|
185
|
+
if (deps['vite'] && deps['react']) {
|
|
186
|
+
this.result.framework = 'react-vite';
|
|
187
|
+
this.result.frameworkVersion = deps['vite'].replace(/[\^~]/, '');
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Check for CRA
|
|
192
|
+
if (deps['react-scripts']) {
|
|
193
|
+
this.result.framework = 'react-cra';
|
|
194
|
+
this.result.frameworkVersion = deps['react-scripts'].replace(/[\^~]/, '');
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Check for Vue
|
|
199
|
+
if (deps['vue']) {
|
|
200
|
+
this.result.framework = 'vue';
|
|
201
|
+
this.result.frameworkVersion = deps['vue'].replace(/[\^~]/, '');
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Check for Angular
|
|
206
|
+
if (deps['@angular/core']) {
|
|
207
|
+
this.result.framework = 'angular';
|
|
208
|
+
this.result.frameworkVersion = deps['@angular/core'].replace(/[\^~]/, '');
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Check for Svelte
|
|
213
|
+
if (deps['svelte']) {
|
|
214
|
+
this.result.framework = 'svelte';
|
|
215
|
+
this.result.frameworkVersion = deps['svelte'].replace(/[\^~]/, '');
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Default to React if react is present
|
|
220
|
+
if (deps['react']) {
|
|
221
|
+
this.result.framework = 'react';
|
|
222
|
+
this.result.frameworkVersion = deps['react'].replace(/[\^~]/, '');
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Check if directory exists relative to project root
|
|
228
|
+
*/
|
|
229
|
+
checkDir(relativePath) {
|
|
230
|
+
const fullPath = path.join(this.result.projectRoot, relativePath);
|
|
231
|
+
return fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory() ? fullPath : null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Find the source root directory
|
|
236
|
+
*/
|
|
237
|
+
findSourceRoot() {
|
|
238
|
+
const candidates = ['src', 'app', 'pages', 'lib'];
|
|
239
|
+
|
|
240
|
+
for (const candidate of candidates) {
|
|
241
|
+
const candidatePath = path.join(this.result.projectRoot, candidate);
|
|
242
|
+
if (fs.existsSync(candidatePath) && fs.statSync(candidatePath).isDirectory()) {
|
|
243
|
+
this.result.srcRoot = candidatePath;
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Default to project root
|
|
249
|
+
this.result.srcRoot = this.result.projectRoot;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Detect styling approach used in the project
|
|
254
|
+
*/
|
|
255
|
+
detectStyling() {
|
|
256
|
+
if (!this.result.packageJson) return;
|
|
257
|
+
|
|
258
|
+
const deps = {
|
|
259
|
+
...this.result.packageJson.dependencies,
|
|
260
|
+
...this.result.packageJson.devDependencies
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
// Check for Tailwind
|
|
264
|
+
if (deps['tailwindcss'] || this.fileExists('tailwind.config.js') ||
|
|
265
|
+
this.fileExists('tailwind.config.ts') || this.fileExists('tailwind.config.mjs')) {
|
|
266
|
+
this.result.styling.push('tailwind');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Check for styled-components
|
|
270
|
+
if (deps['styled-components']) {
|
|
271
|
+
this.result.styling.push('styled-components');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Check for Emotion
|
|
275
|
+
if (deps['@emotion/react'] || deps['@emotion/styled']) {
|
|
276
|
+
this.result.styling.push('emotion');
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Check for Sass
|
|
280
|
+
if (deps['sass'] || deps['node-sass']) {
|
|
281
|
+
this.result.styling.push('sass');
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Scan for CSS modules
|
|
285
|
+
if (this.result.srcRoot) {
|
|
286
|
+
const hasModules = this.scanForPattern(this.result.srcRoot, /\.module\.(css|scss)$/);
|
|
287
|
+
if (hasModules) {
|
|
288
|
+
this.result.styling.push('css-modules');
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Default to inline styles if nothing detected
|
|
293
|
+
if (this.result.styling.length === 0) {
|
|
294
|
+
this.result.styling.push('inline-styles');
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Check if file exists relative to project root
|
|
300
|
+
*/
|
|
301
|
+
fileExists(relativePath) {
|
|
302
|
+
return fs.existsSync(path.join(this.result.projectRoot, relativePath));
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Scan directory for files matching pattern
|
|
307
|
+
*/
|
|
308
|
+
scanForPattern(dir, pattern, maxDepth = 3, currentDepth = 0) {
|
|
309
|
+
if (currentDepth > maxDepth) return false;
|
|
310
|
+
|
|
311
|
+
try {
|
|
312
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
313
|
+
|
|
314
|
+
for (const entry of entries) {
|
|
315
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
|
|
316
|
+
|
|
317
|
+
const fullPath = path.join(dir, entry.name);
|
|
318
|
+
|
|
319
|
+
if (entry.isFile() && pattern.test(entry.name)) {
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (entry.isDirectory()) {
|
|
324
|
+
if (this.scanForPattern(fullPath, pattern, maxDepth, currentDepth + 1)) {
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
} catch (e) {
|
|
330
|
+
// Ignore errors
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Find existing page files
|
|
338
|
+
*/
|
|
339
|
+
findPages() {
|
|
340
|
+
const framework = this.result.framework;
|
|
341
|
+
|
|
342
|
+
if (!framework || !this.result.projectRoot) return;
|
|
343
|
+
|
|
344
|
+
let pageRoots = [];
|
|
345
|
+
let pagePattern = /\.(tsx?|jsx?)$/;
|
|
346
|
+
|
|
347
|
+
if (framework === 'next.js-app-router') {
|
|
348
|
+
pageRoots = [
|
|
349
|
+
path.join(this.result.projectRoot, 'app'),
|
|
350
|
+
path.join(this.result.projectRoot, 'src', 'app')
|
|
351
|
+
];
|
|
352
|
+
pagePattern = /page\.(tsx?|jsx?)$/;
|
|
353
|
+
} else if (framework === 'next.js-pages-router') {
|
|
354
|
+
pageRoots = [
|
|
355
|
+
path.join(this.result.projectRoot, 'pages'),
|
|
356
|
+
path.join(this.result.projectRoot, 'src', 'pages')
|
|
357
|
+
];
|
|
358
|
+
} else if (framework === 'vue') {
|
|
359
|
+
pageRoots = [
|
|
360
|
+
path.join(this.result.projectRoot, 'src', 'views'),
|
|
361
|
+
path.join(this.result.projectRoot, 'src', 'pages')
|
|
362
|
+
];
|
|
363
|
+
pagePattern = /\.vue$/;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
for (const pageRoot of pageRoots) {
|
|
367
|
+
if (fs.existsSync(pageRoot)) {
|
|
368
|
+
this.scanPages(pageRoot, pageRoot, pagePattern);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Recursively scan for page files
|
|
375
|
+
*/
|
|
376
|
+
scanPages(dir, rootDir, pattern, currentPath = '') {
|
|
377
|
+
try {
|
|
378
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
379
|
+
|
|
380
|
+
for (const entry of entries) {
|
|
381
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
|
|
382
|
+
|
|
383
|
+
const fullPath = path.join(dir, entry.name);
|
|
384
|
+
const relativePath = path.join(currentPath, entry.name);
|
|
385
|
+
|
|
386
|
+
if (entry.isFile() && pattern.test(entry.name)) {
|
|
387
|
+
// Convert file path to route
|
|
388
|
+
const route = this.filePathToRoute(relativePath, pattern);
|
|
389
|
+
|
|
390
|
+
this.result.pages.push({
|
|
391
|
+
file: fullPath,
|
|
392
|
+
relativePath: relativePath,
|
|
393
|
+
route: route,
|
|
394
|
+
name: this.routeToName(route)
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (entry.isDirectory()) {
|
|
399
|
+
this.scanPages(fullPath, rootDir, pattern, relativePath);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
} catch (e) {
|
|
403
|
+
// Ignore errors
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Convert file path to route
|
|
409
|
+
*/
|
|
410
|
+
filePathToRoute(filePath, pattern) {
|
|
411
|
+
let route = filePath
|
|
412
|
+
.replace(pattern, '')
|
|
413
|
+
.replace(/\\/g, '/')
|
|
414
|
+
.replace(/\/index$/, '')
|
|
415
|
+
.replace(/\/page$/, '');
|
|
416
|
+
|
|
417
|
+
// Handle dynamic routes [param]
|
|
418
|
+
route = route.replace(/\[([^\]]+)\]/g, ':$1');
|
|
419
|
+
|
|
420
|
+
return '/' + route || '/';
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Convert route to human-readable name
|
|
425
|
+
*/
|
|
426
|
+
routeToName(route) {
|
|
427
|
+
return route
|
|
428
|
+
.split('/')
|
|
429
|
+
.filter(Boolean)
|
|
430
|
+
.map(part => part.replace(/^:/, ''))
|
|
431
|
+
.join('-') || 'home';
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Find existing component files
|
|
436
|
+
*/
|
|
437
|
+
findComponents() {
|
|
438
|
+
const componentDirs = [
|
|
439
|
+
path.join(this.result.projectRoot, 'components'),
|
|
440
|
+
path.join(this.result.projectRoot, 'src', 'components'),
|
|
441
|
+
path.join(this.result.srcRoot || this.result.projectRoot, 'components')
|
|
442
|
+
];
|
|
443
|
+
|
|
444
|
+
for (const dir of componentDirs) {
|
|
445
|
+
if (fs.existsSync(dir)) {
|
|
446
|
+
this.scanComponents(dir, dir);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Recursively scan for component files
|
|
453
|
+
*/
|
|
454
|
+
scanComponents(dir, rootDir) {
|
|
455
|
+
try {
|
|
456
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
457
|
+
|
|
458
|
+
for (const entry of entries) {
|
|
459
|
+
if (entry.name.startsWith('.')) continue;
|
|
460
|
+
|
|
461
|
+
const fullPath = path.join(dir, entry.name);
|
|
462
|
+
|
|
463
|
+
if (entry.isFile() && /\.(tsx?|jsx?|vue|svelte)$/.test(entry.name)) {
|
|
464
|
+
const relativePath = path.relative(rootDir, fullPath);
|
|
465
|
+
const name = entry.name.replace(/\.(tsx?|jsx?|vue|svelte)$/, '');
|
|
466
|
+
|
|
467
|
+
this.result.components.push({
|
|
468
|
+
file: fullPath,
|
|
469
|
+
relativePath: relativePath,
|
|
470
|
+
name: name
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (entry.isDirectory()) {
|
|
475
|
+
this.scanComponents(fullPath, rootDir);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
} catch (e) {
|
|
479
|
+
// Ignore errors
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Map captured pages from manifest to existing prototype files
|
|
485
|
+
*/
|
|
486
|
+
mapCapturedPages(manifest) {
|
|
487
|
+
if (!manifest || !manifest.pages || !this.result.pages.length) {
|
|
488
|
+
return this.result.mappedPages;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
for (const capturedPage of manifest.pages) {
|
|
492
|
+
const capturedName = capturedPage.name.toLowerCase()
|
|
493
|
+
.replace(/[_-]/g, '')
|
|
494
|
+
.replace(/\s+/g, '');
|
|
495
|
+
|
|
496
|
+
// Try to find matching prototype page
|
|
497
|
+
for (const prototypePage of this.result.pages) {
|
|
498
|
+
const protoName = prototypePage.name.toLowerCase()
|
|
499
|
+
.replace(/[_-]/g, '')
|
|
500
|
+
.replace(/\s+/g, '');
|
|
501
|
+
|
|
502
|
+
if (capturedName.includes(protoName) || protoName.includes(capturedName)) {
|
|
503
|
+
this.result.mappedPages[capturedPage.name] = {
|
|
504
|
+
captured: capturedPage,
|
|
505
|
+
prototype: prototypePage,
|
|
506
|
+
similarity: this.calculateSimilarity(capturedName, protoName)
|
|
507
|
+
};
|
|
508
|
+
break;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return this.result.mappedPages;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Calculate string similarity (simple Jaccard)
|
|
518
|
+
*/
|
|
519
|
+
calculateSimilarity(str1, str2) {
|
|
520
|
+
const set1 = new Set(str1.split(''));
|
|
521
|
+
const set2 = new Set(str2.split(''));
|
|
522
|
+
const intersection = new Set([...set1].filter(x => set2.has(x)));
|
|
523
|
+
const union = new Set([...set1, ...set2]);
|
|
524
|
+
return intersection.size / union.size;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Detect existing prototype in project directory
|
|
530
|
+
*/
|
|
531
|
+
function detectPrototype(projectPath) {
|
|
532
|
+
const detector = new PrototypeDetector(projectPath);
|
|
533
|
+
return detector.detect();
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Map captured pages to prototype files
|
|
538
|
+
*/
|
|
539
|
+
function mapPages(projectPath, manifest) {
|
|
540
|
+
const detector = new PrototypeDetector(projectPath);
|
|
541
|
+
detector.detect();
|
|
542
|
+
return detector.mapCapturedPages(manifest);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Format detection result for CLI output
|
|
547
|
+
*/
|
|
548
|
+
function formatResult(result) {
|
|
549
|
+
const lines = [];
|
|
550
|
+
|
|
551
|
+
if (!result.exists) {
|
|
552
|
+
lines.push('\x1b[33m⚠ No existing prototype found\x1b[0m');
|
|
553
|
+
return lines.join('\n');
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
lines.push('\x1b[32m✓ Existing prototype found\x1b[0m');
|
|
557
|
+
lines.push(` Framework: ${result.framework || 'Unknown'}${result.frameworkVersion ? ` v${result.frameworkVersion}` : ''}`);
|
|
558
|
+
lines.push(` Project root: ${result.projectRoot}`);
|
|
559
|
+
lines.push(` Styling: ${result.styling.join(', ') || 'Unknown'}`);
|
|
560
|
+
|
|
561
|
+
if (result.pages.length > 0) {
|
|
562
|
+
lines.push(`\n \x1b[1mExisting Pages (${result.pages.length}):\x1b[0m`);
|
|
563
|
+
for (const page of result.pages.slice(0, 10)) {
|
|
564
|
+
lines.push(` ${page.route} → ${path.basename(page.file)}`);
|
|
565
|
+
}
|
|
566
|
+
if (result.pages.length > 10) {
|
|
567
|
+
lines.push(` ... and ${result.pages.length - 10} more`);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
if (result.components.length > 0) {
|
|
572
|
+
lines.push(`\n \x1b[1mExisting Components (${result.components.length}):\x1b[0m`);
|
|
573
|
+
for (const comp of result.components.slice(0, 10)) {
|
|
574
|
+
lines.push(` ${comp.name}`);
|
|
575
|
+
}
|
|
576
|
+
if (result.components.length > 10) {
|
|
577
|
+
lines.push(` ... and ${result.components.length - 10} more`);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (Object.keys(result.mappedPages).length > 0) {
|
|
582
|
+
lines.push(`\n \x1b[1mMapped Pages:\x1b[0m`);
|
|
583
|
+
for (const [captured, mapping] of Object.entries(result.mappedPages)) {
|
|
584
|
+
lines.push(` ${captured} → ${mapping.prototype.file}`);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
return lines.join('\n');
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// CLI execution
|
|
592
|
+
if (require.main === module) {
|
|
593
|
+
const args = process.argv.slice(2);
|
|
594
|
+
let projectPath = '.';
|
|
595
|
+
let manifestPath = null;
|
|
596
|
+
|
|
597
|
+
for (let i = 0; i < args.length; i++) {
|
|
598
|
+
if (args[i] === '--path' || args[i] === '-p') {
|
|
599
|
+
projectPath = args[++i];
|
|
600
|
+
} else if (args[i] === '--project') {
|
|
601
|
+
const projectName = args[++i];
|
|
602
|
+
const SKILL_DIR = __dirname;
|
|
603
|
+
const PROJECTS_DIR = path.resolve(SKILL_DIR, '../../../../projects');
|
|
604
|
+
projectPath = path.join(PROJECTS_DIR, projectName, 'prototype');
|
|
605
|
+
} else if (args[i] === '--manifest' || args[i] === '-m') {
|
|
606
|
+
manifestPath = args[++i];
|
|
607
|
+
} else if (args[i] === '--help' || args[i] === '-h') {
|
|
608
|
+
console.log(`
|
|
609
|
+
Usage: node detect-prototype.js [options]
|
|
610
|
+
|
|
611
|
+
Options:
|
|
612
|
+
--path, -p <path> Path to project directory
|
|
613
|
+
--project <name> Project name (looks in projects/<name>/prototype)
|
|
614
|
+
--manifest, -m <path> Path to manifest.json for page mapping
|
|
615
|
+
--help, -h Show this help
|
|
616
|
+
|
|
617
|
+
Examples:
|
|
618
|
+
node detect-prototype.js --project my-app
|
|
619
|
+
node detect-prototype.js --path ./projects/my-app/prototype
|
|
620
|
+
node detect-prototype.js --path ./prototype --manifest ./references/manifest.json
|
|
621
|
+
`);
|
|
622
|
+
process.exit(0);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const result = detectPrototype(projectPath);
|
|
627
|
+
|
|
628
|
+
// Load manifest if provided
|
|
629
|
+
if (manifestPath && fs.existsSync(manifestPath)) {
|
|
630
|
+
try {
|
|
631
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
632
|
+
const detector = new PrototypeDetector(projectPath);
|
|
633
|
+
detector.detect();
|
|
634
|
+
detector.mapCapturedPages(manifest);
|
|
635
|
+
result.mappedPages = detector.result.mappedPages;
|
|
636
|
+
} catch (e) {
|
|
637
|
+
console.error(`Failed to load manifest: ${e.message}`);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
console.log(formatResult(result));
|
|
642
|
+
|
|
643
|
+
// Exit with code 0 if prototype exists, 1 if not
|
|
644
|
+
process.exit(result.exists ? 0 : 1);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
module.exports = {
|
|
648
|
+
PrototypeDetector,
|
|
649
|
+
detectPrototype,
|
|
650
|
+
mapPages,
|
|
651
|
+
formatResult
|
|
652
|
+
};
|