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,457 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Project Structure Handler
|
|
5
|
+
*
|
|
6
|
+
* Handles project root detection, monorepo support, and path resolution
|
|
7
|
+
* to prevent working directory confusion and file path issues.
|
|
8
|
+
*
|
|
9
|
+
* Features:
|
|
10
|
+
* - Project root detection (finds package.json)
|
|
11
|
+
* - Monorepo support (lerna, nx, turborepo, pnpm workspaces)
|
|
12
|
+
* - Absolute path resolution
|
|
13
|
+
* - File existence verification
|
|
14
|
+
* - Path utilities for consistent file operations
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* const { ProjectStructure } = require('./project-structure');
|
|
18
|
+
* const ps = new ProjectStructure('/path/to/project');
|
|
19
|
+
* ps.resolve('src/components/Button.tsx');
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const path = require('path');
|
|
24
|
+
|
|
25
|
+
// Monorepo configuration files
|
|
26
|
+
const MONOREPO_MARKERS = {
|
|
27
|
+
lerna: 'lerna.json',
|
|
28
|
+
nx: 'nx.json',
|
|
29
|
+
turborepo: 'turbo.json',
|
|
30
|
+
pnpm: 'pnpm-workspace.yaml',
|
|
31
|
+
yarn: 'yarn.lock', // Check for workspaces in package.json
|
|
32
|
+
rush: 'rush.json'
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Common project structure patterns
|
|
36
|
+
const PROJECT_PATTERNS = {
|
|
37
|
+
nextjs: {
|
|
38
|
+
markers: ['next.config.js', 'next.config.mjs', 'next.config.ts'],
|
|
39
|
+
srcDirs: ['src', 'app', 'pages'],
|
|
40
|
+
configFiles: ['next.config.js', 'next.config.mjs', 'next.config.ts']
|
|
41
|
+
},
|
|
42
|
+
react: {
|
|
43
|
+
markers: ['package.json'],
|
|
44
|
+
srcDirs: ['src'],
|
|
45
|
+
configFiles: ['vite.config.js', 'vite.config.ts', 'webpack.config.js']
|
|
46
|
+
},
|
|
47
|
+
vue: {
|
|
48
|
+
markers: ['vue.config.js', 'vite.config.js'],
|
|
49
|
+
srcDirs: ['src'],
|
|
50
|
+
configFiles: ['vue.config.js', 'vite.config.js']
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
class ProjectStructure {
|
|
55
|
+
constructor(startPath = process.cwd()) {
|
|
56
|
+
this.startPath = path.resolve(startPath);
|
|
57
|
+
this.projectRoot = null;
|
|
58
|
+
this.srcRoot = null;
|
|
59
|
+
this.packageJson = null;
|
|
60
|
+
this.isMonorepo = false;
|
|
61
|
+
this.monorepoType = null;
|
|
62
|
+
this.workspaces = [];
|
|
63
|
+
|
|
64
|
+
this.detect();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Detect project structure
|
|
69
|
+
*/
|
|
70
|
+
detect() {
|
|
71
|
+
this.findProjectRoot();
|
|
72
|
+
this.detectMonorepo();
|
|
73
|
+
this.findSrcRoot();
|
|
74
|
+
return this;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Find project root by looking for package.json
|
|
79
|
+
*/
|
|
80
|
+
findProjectRoot() {
|
|
81
|
+
let currentDir = this.startPath;
|
|
82
|
+
const root = path.parse(currentDir).root;
|
|
83
|
+
|
|
84
|
+
while (currentDir !== root) {
|
|
85
|
+
const pkgPath = path.join(currentDir, 'package.json');
|
|
86
|
+
|
|
87
|
+
if (fs.existsSync(pkgPath)) {
|
|
88
|
+
try {
|
|
89
|
+
this.packageJson = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
90
|
+
this.projectRoot = currentDir;
|
|
91
|
+
return currentDir;
|
|
92
|
+
} catch (e) {
|
|
93
|
+
// Continue searching
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
currentDir = path.dirname(currentDir);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// If no package.json found, use start path
|
|
101
|
+
this.projectRoot = this.startPath;
|
|
102
|
+
return this.startPath;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Detect if project is part of a monorepo
|
|
107
|
+
*/
|
|
108
|
+
detectMonorepo() {
|
|
109
|
+
if (!this.projectRoot) return;
|
|
110
|
+
|
|
111
|
+
// Check for monorepo markers going up from project root
|
|
112
|
+
let currentDir = this.projectRoot;
|
|
113
|
+
const root = path.parse(currentDir).root;
|
|
114
|
+
let levelsUp = 0;
|
|
115
|
+
const maxLevels = 5;
|
|
116
|
+
|
|
117
|
+
while (currentDir !== root && levelsUp < maxLevels) {
|
|
118
|
+
for (const [type, marker] of Object.entries(MONOREPO_MARKERS)) {
|
|
119
|
+
const markerPath = path.join(currentDir, marker);
|
|
120
|
+
|
|
121
|
+
if (fs.existsSync(markerPath)) {
|
|
122
|
+
this.isMonorepo = true;
|
|
123
|
+
this.monorepoType = type;
|
|
124
|
+
this.monorepoRoot = currentDir;
|
|
125
|
+
|
|
126
|
+
// Try to find workspaces
|
|
127
|
+
this.detectWorkspaces(currentDir, type);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Also check for yarn/npm workspaces in package.json
|
|
133
|
+
const pkgPath = path.join(currentDir, 'package.json');
|
|
134
|
+
if (fs.existsSync(pkgPath)) {
|
|
135
|
+
try {
|
|
136
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
137
|
+
if (pkg.workspaces) {
|
|
138
|
+
this.isMonorepo = true;
|
|
139
|
+
this.monorepoType = 'npm/yarn-workspaces';
|
|
140
|
+
this.monorepoRoot = currentDir;
|
|
141
|
+
this.workspaces = Array.isArray(pkg.workspaces)
|
|
142
|
+
? pkg.workspaces
|
|
143
|
+
: pkg.workspaces.packages || [];
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
} catch (e) {
|
|
147
|
+
// Continue
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
currentDir = path.dirname(currentDir);
|
|
152
|
+
levelsUp++;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Detect workspace packages
|
|
158
|
+
*/
|
|
159
|
+
detectWorkspaces(monorepoRoot, type) {
|
|
160
|
+
switch (type) {
|
|
161
|
+
case 'lerna':
|
|
162
|
+
this.detectLernaWorkspaces(monorepoRoot);
|
|
163
|
+
break;
|
|
164
|
+
case 'pnpm':
|
|
165
|
+
this.detectPnpmWorkspaces(monorepoRoot);
|
|
166
|
+
break;
|
|
167
|
+
case 'nx':
|
|
168
|
+
this.detectNxWorkspaces(monorepoRoot);
|
|
169
|
+
break;
|
|
170
|
+
case 'turborepo':
|
|
171
|
+
this.detectTurboWorkspaces(monorepoRoot);
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Detect Lerna workspaces
|
|
178
|
+
*/
|
|
179
|
+
detectLernaWorkspaces(monorepoRoot) {
|
|
180
|
+
const lernaPath = path.join(monorepoRoot, 'lerna.json');
|
|
181
|
+
try {
|
|
182
|
+
const lerna = JSON.parse(fs.readFileSync(lernaPath, 'utf-8'));
|
|
183
|
+
this.workspaces = lerna.packages || ['packages/*'];
|
|
184
|
+
} catch (e) {
|
|
185
|
+
this.workspaces = ['packages/*'];
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Detect pnpm workspaces
|
|
191
|
+
*/
|
|
192
|
+
detectPnpmWorkspaces(monorepoRoot) {
|
|
193
|
+
const pnpmPath = path.join(monorepoRoot, 'pnpm-workspace.yaml');
|
|
194
|
+
try {
|
|
195
|
+
const content = fs.readFileSync(pnpmPath, 'utf-8');
|
|
196
|
+
// Simple YAML parsing for packages array
|
|
197
|
+
const match = content.match(/packages:\s*\n((?:\s+-\s+.+\n?)+)/);
|
|
198
|
+
if (match) {
|
|
199
|
+
this.workspaces = match[1]
|
|
200
|
+
.split('\n')
|
|
201
|
+
.filter(line => line.trim().startsWith('-'))
|
|
202
|
+
.map(line => line.replace(/^\s*-\s*['"]?/, '').replace(/['"]?\s*$/, ''));
|
|
203
|
+
}
|
|
204
|
+
} catch (e) {
|
|
205
|
+
this.workspaces = ['packages/*'];
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Detect Nx workspaces
|
|
211
|
+
*/
|
|
212
|
+
detectNxWorkspaces(monorepoRoot) {
|
|
213
|
+
// Nx uses apps/ and libs/ by default
|
|
214
|
+
this.workspaces = ['apps/*', 'libs/*', 'packages/*'];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Detect Turborepo workspaces
|
|
219
|
+
*/
|
|
220
|
+
detectTurboWorkspaces(monorepoRoot) {
|
|
221
|
+
// Turborepo reads from package.json workspaces
|
|
222
|
+
const pkgPath = path.join(monorepoRoot, 'package.json');
|
|
223
|
+
try {
|
|
224
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
225
|
+
this.workspaces = pkg.workspaces || ['packages/*', 'apps/*'];
|
|
226
|
+
} catch (e) {
|
|
227
|
+
this.workspaces = ['packages/*', 'apps/*'];
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Find source root directory
|
|
233
|
+
*/
|
|
234
|
+
findSrcRoot() {
|
|
235
|
+
if (!this.projectRoot) return;
|
|
236
|
+
|
|
237
|
+
const candidates = ['src', 'app', 'lib', 'source'];
|
|
238
|
+
|
|
239
|
+
for (const candidate of candidates) {
|
|
240
|
+
const candidatePath = path.join(this.projectRoot, candidate);
|
|
241
|
+
if (fs.existsSync(candidatePath) && fs.statSync(candidatePath).isDirectory()) {
|
|
242
|
+
this.srcRoot = candidatePath;
|
|
243
|
+
return candidatePath;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Default to project root
|
|
248
|
+
this.srcRoot = this.projectRoot;
|
|
249
|
+
return this.projectRoot;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Resolve a path relative to project root (always returns absolute path)
|
|
254
|
+
*/
|
|
255
|
+
resolve(...pathSegments) {
|
|
256
|
+
const relativePath = path.join(...pathSegments);
|
|
257
|
+
|
|
258
|
+
// If already absolute, return as-is
|
|
259
|
+
if (path.isAbsolute(relativePath)) {
|
|
260
|
+
return relativePath;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return path.join(this.projectRoot, relativePath);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Resolve a path relative to src root
|
|
268
|
+
*/
|
|
269
|
+
resolveSrc(...pathSegments) {
|
|
270
|
+
const relativePath = path.join(...pathSegments);
|
|
271
|
+
|
|
272
|
+
if (path.isAbsolute(relativePath)) {
|
|
273
|
+
return relativePath;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return path.join(this.srcRoot, relativePath);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Check if a file exists
|
|
281
|
+
*/
|
|
282
|
+
exists(filePath) {
|
|
283
|
+
const absolutePath = this.resolve(filePath);
|
|
284
|
+
return fs.existsSync(absolutePath);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Verify a file exists after creation
|
|
289
|
+
*/
|
|
290
|
+
verify(filePath) {
|
|
291
|
+
const absolutePath = this.resolve(filePath);
|
|
292
|
+
|
|
293
|
+
if (!fs.existsSync(absolutePath)) {
|
|
294
|
+
throw new Error(`File verification failed: ${absolutePath} does not exist`);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const stats = fs.statSync(absolutePath);
|
|
298
|
+
return {
|
|
299
|
+
exists: true,
|
|
300
|
+
path: absolutePath,
|
|
301
|
+
size: stats.size,
|
|
302
|
+
created: stats.birthtime,
|
|
303
|
+
modified: stats.mtime
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Create directory if it doesn't exist
|
|
309
|
+
*/
|
|
310
|
+
ensureDir(dirPath) {
|
|
311
|
+
const absolutePath = this.resolve(dirPath);
|
|
312
|
+
|
|
313
|
+
if (!fs.existsSync(absolutePath)) {
|
|
314
|
+
fs.mkdirSync(absolutePath, { recursive: true });
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return absolutePath;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Write file with verification
|
|
322
|
+
*/
|
|
323
|
+
writeFile(filePath, content) {
|
|
324
|
+
const absolutePath = this.resolve(filePath);
|
|
325
|
+
const dirPath = path.dirname(absolutePath);
|
|
326
|
+
|
|
327
|
+
// Ensure directory exists
|
|
328
|
+
if (!fs.existsSync(dirPath)) {
|
|
329
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Write file
|
|
333
|
+
fs.writeFileSync(absolutePath, content, 'utf-8');
|
|
334
|
+
|
|
335
|
+
// Verify
|
|
336
|
+
return this.verify(filePath);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Read file with absolute path handling
|
|
341
|
+
*/
|
|
342
|
+
readFile(filePath) {
|
|
343
|
+
const absolutePath = this.resolve(filePath);
|
|
344
|
+
|
|
345
|
+
if (!fs.existsSync(absolutePath)) {
|
|
346
|
+
throw new Error(`File not found: ${absolutePath}`);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return fs.readFileSync(absolutePath, 'utf-8');
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Get relative path from project root
|
|
354
|
+
*/
|
|
355
|
+
relative(absolutePath) {
|
|
356
|
+
return path.relative(this.projectRoot, absolutePath);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Get project info summary
|
|
361
|
+
*/
|
|
362
|
+
getInfo() {
|
|
363
|
+
return {
|
|
364
|
+
projectRoot: this.projectRoot,
|
|
365
|
+
srcRoot: this.srcRoot,
|
|
366
|
+
isMonorepo: this.isMonorepo,
|
|
367
|
+
monorepoType: this.monorepoType,
|
|
368
|
+
monorepoRoot: this.monorepoRoot,
|
|
369
|
+
workspaces: this.workspaces,
|
|
370
|
+
packageName: this.packageJson?.name,
|
|
371
|
+
packageVersion: this.packageJson?.version
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Format info for CLI output
|
|
377
|
+
*/
|
|
378
|
+
formatInfo() {
|
|
379
|
+
const info = this.getInfo();
|
|
380
|
+
const lines = [];
|
|
381
|
+
|
|
382
|
+
lines.push('\x1b[1mProject Structure\x1b[0m');
|
|
383
|
+
lines.push(` Project Root: ${info.projectRoot}`);
|
|
384
|
+
lines.push(` Source Root: ${info.srcRoot}`);
|
|
385
|
+
|
|
386
|
+
if (info.packageName) {
|
|
387
|
+
lines.push(` Package: ${info.packageName}@${info.packageVersion || 'unknown'}`);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (info.isMonorepo) {
|
|
391
|
+
lines.push(`\n\x1b[1mMonorepo Detected\x1b[0m`);
|
|
392
|
+
lines.push(` Type: ${info.monorepoType}`);
|
|
393
|
+
lines.push(` Root: ${info.monorepoRoot}`);
|
|
394
|
+
lines.push(` Workspaces: ${info.workspaces.join(', ')}`);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return lines.join('\n');
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Create project structure instance for a path
|
|
403
|
+
*/
|
|
404
|
+
function createProjectStructure(startPath) {
|
|
405
|
+
return new ProjectStructure(startPath);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Find project root from any starting path
|
|
410
|
+
*/
|
|
411
|
+
function findProjectRoot(startPath) {
|
|
412
|
+
const ps = new ProjectStructure(startPath);
|
|
413
|
+
return ps.projectRoot;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Check if path is within a monorepo
|
|
418
|
+
*/
|
|
419
|
+
function isInMonorepo(startPath) {
|
|
420
|
+
const ps = new ProjectStructure(startPath);
|
|
421
|
+
return ps.isMonorepo;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// CLI execution
|
|
425
|
+
if (require.main === module) {
|
|
426
|
+
const args = process.argv.slice(2);
|
|
427
|
+
let startPath = process.cwd();
|
|
428
|
+
|
|
429
|
+
for (let i = 0; i < args.length; i++) {
|
|
430
|
+
if (args[i] === '--path' || args[i] === '-p') {
|
|
431
|
+
startPath = args[++i];
|
|
432
|
+
} else if (args[i] === '--help' || args[i] === '-h') {
|
|
433
|
+
console.log(`
|
|
434
|
+
Usage: node project-structure.js [options]
|
|
435
|
+
|
|
436
|
+
Options:
|
|
437
|
+
--path, -p <path> Starting path to analyze
|
|
438
|
+
--help, -h Show this help
|
|
439
|
+
|
|
440
|
+
Examples:
|
|
441
|
+
node project-structure.js
|
|
442
|
+
node project-structure.js --path ./projects/my-app/prototype
|
|
443
|
+
`);
|
|
444
|
+
process.exit(0);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const ps = new ProjectStructure(startPath);
|
|
449
|
+
console.log(ps.formatInfo());
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
module.exports = {
|
|
453
|
+
ProjectStructure,
|
|
454
|
+
createProjectStructure,
|
|
455
|
+
findProjectRoot,
|
|
456
|
+
isInMonorepo
|
|
457
|
+
};
|