real-prototypes-skill 0.1.0 → 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.
@@ -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
+ };