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.
@@ -0,0 +1,744 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Plan Generator Module
5
+ *
6
+ * Generates implementation plans with exact file paths, injection points,
7
+ * validation checkpoints, and mode specifications (EXTEND vs CREATE).
8
+ *
9
+ * Features:
10
+ * - Analyzes existing prototype structure
11
+ * - Identifies files to modify vs create
12
+ * - Specifies exact injection points with selectors
13
+ * - Includes validation checkpoints
14
+ * - Generates dependency graph for tasks
15
+ *
16
+ * Usage:
17
+ * node generate-plan.js --project <name> --feature <feature-description>
18
+ */
19
+
20
+ const fs = require('fs');
21
+ const path = require('path');
22
+ const { detectPrototype } = require('./detect-prototype');
23
+ const { ProjectStructure } = require('./project-structure');
24
+
25
+ class PlanGenerator {
26
+ constructor(projectDir, options = {}) {
27
+ this.projectDir = path.resolve(projectDir);
28
+ this.options = {
29
+ featureDescription: '',
30
+ targetPage: null,
31
+ ...options
32
+ };
33
+
34
+ this.refsDir = path.join(this.projectDir, 'references');
35
+ this.protoDir = path.join(this.projectDir, 'prototype');
36
+
37
+ this.prototypeInfo = null;
38
+ this.manifest = null;
39
+ this.designTokens = null;
40
+ this.plan = null;
41
+ }
42
+
43
+ /**
44
+ * Validate that required captures exist - MANDATORY before generating plan
45
+ * @throws {Error} if captures are missing
46
+ */
47
+ validateCapturesExist() {
48
+ const errors = [];
49
+
50
+ // Check design tokens
51
+ const tokensPath = path.join(this.refsDir, 'design-tokens.json');
52
+ if (!fs.existsSync(tokensPath)) {
53
+ errors.push('design-tokens.json missing');
54
+ }
55
+
56
+ // Check manifest
57
+ const manifestPath = path.join(this.refsDir, 'manifest.json');
58
+ if (!fs.existsSync(manifestPath)) {
59
+ errors.push('manifest.json missing');
60
+ }
61
+
62
+ // Check screenshots
63
+ const screenshotsDir = path.join(this.refsDir, 'screenshots');
64
+ if (!fs.existsSync(screenshotsDir)) {
65
+ errors.push('screenshots/ directory missing');
66
+ } else {
67
+ const screenshots = fs.readdirSync(screenshotsDir).filter(f => f.endsWith('.png'));
68
+ if (screenshots.length === 0) {
69
+ errors.push('No screenshots found in screenshots/');
70
+ }
71
+ }
72
+
73
+ if (errors.length > 0) {
74
+ const projectName = path.basename(this.projectDir);
75
+ throw new Error(
76
+ `CAPTURES REQUIRED - Cannot generate plan without captures\n\n` +
77
+ `Missing:\n - ${errors.join('\n - ')}\n\n` +
78
+ `You MUST capture the existing platform first:\n` +
79
+ ` node cli.js capture --project ${projectName} --url <PLATFORM_URL>\n\n` +
80
+ `This skill is for adding features to EXISTING platforms.\n` +
81
+ `It does NOT create new designs from scratch.`
82
+ );
83
+ }
84
+
85
+ return true;
86
+ }
87
+
88
+ /**
89
+ * Load project data
90
+ */
91
+ loadProjectData() {
92
+ // Load manifest
93
+ const manifestPath = path.join(this.refsDir, 'manifest.json');
94
+ if (fs.existsSync(manifestPath)) {
95
+ this.manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
96
+ }
97
+
98
+ // Load design tokens
99
+ const tokensPath = path.join(this.refsDir, 'design-tokens.json');
100
+ if (fs.existsSync(tokensPath)) {
101
+ this.designTokens = JSON.parse(fs.readFileSync(tokensPath, 'utf-8'));
102
+ }
103
+
104
+ // Detect prototype
105
+ if (fs.existsSync(this.protoDir)) {
106
+ this.prototypeInfo = detectPrototype(this.protoDir);
107
+ }
108
+
109
+ return this;
110
+ }
111
+
112
+ /**
113
+ * Generate implementation plan
114
+ */
115
+ generate() {
116
+ // MANDATORY: Validate captures exist before generating plan
117
+ this.validateCapturesExist();
118
+
119
+ this.loadProjectData();
120
+
121
+ const mode = this.determineMode();
122
+ const tasks = this.generateTasks();
123
+ const validation = this.generateValidation();
124
+ const dependencies = this.analyzeDependencies(tasks);
125
+
126
+ this.plan = {
127
+ version: '1.0.0',
128
+ generatedAt: new Date().toISOString(),
129
+ feature: this.options.featureDescription,
130
+ mode,
131
+ project: {
132
+ path: this.projectDir,
133
+ referencesPath: this.refsDir,
134
+ prototypePath: this.protoDir
135
+ },
136
+ existingPrototype: this.prototypeInfo?.exists ? {
137
+ framework: this.prototypeInfo.framework,
138
+ frameworkVersion: this.prototypeInfo.frameworkVersion,
139
+ styling: this.prototypeInfo.styling,
140
+ pagesCount: this.prototypeInfo.pages?.length || 0,
141
+ componentsCount: this.prototypeInfo.components?.length || 0
142
+ } : null,
143
+ designSystem: this.designTokens ? {
144
+ primaryColor: this.designTokens.colors?.primary,
145
+ totalColors: this.designTokens.totalColorsFound,
146
+ hasTokens: true
147
+ } : null,
148
+ tasks,
149
+ dependencies,
150
+ validation,
151
+ criticalRules: this.getCriticalRules()
152
+ };
153
+
154
+ return this.plan;
155
+ }
156
+
157
+ /**
158
+ * Determine mode: EXTEND_EXISTING or CREATE_NEW
159
+ */
160
+ determineMode() {
161
+ if (this.prototypeInfo?.exists) {
162
+ return {
163
+ type: 'EXTEND_EXISTING',
164
+ reason: 'Existing prototype detected',
165
+ recommendation: 'Modify existing files instead of creating new ones',
166
+ existingFiles: this.prototypeInfo.pages?.length || 0
167
+ };
168
+ }
169
+
170
+ return {
171
+ type: 'CREATE_NEW',
172
+ reason: 'No existing prototype found',
173
+ recommendation: 'Create new prototype structure'
174
+ };
175
+ }
176
+
177
+ /**
178
+ * Generate task list
179
+ */
180
+ generateTasks() {
181
+ const tasks = [];
182
+ let taskId = 1;
183
+
184
+ // Task 1: Setup/Verification
185
+ tasks.push({
186
+ id: taskId++,
187
+ phase: 'setup',
188
+ action: 'verify',
189
+ description: 'Verify captured assets exist',
190
+ checks: [
191
+ {
192
+ item: 'design-tokens.json',
193
+ path: path.join(this.refsDir, 'design-tokens.json'),
194
+ required: true,
195
+ exists: this.designTokens !== null
196
+ },
197
+ {
198
+ item: 'manifest.json',
199
+ path: path.join(this.refsDir, 'manifest.json'),
200
+ required: true,
201
+ exists: this.manifest !== null
202
+ },
203
+ {
204
+ item: 'screenshots',
205
+ path: path.join(this.refsDir, 'screenshots'),
206
+ required: true,
207
+ exists: fs.existsSync(path.join(this.refsDir, 'screenshots'))
208
+ }
209
+ ]
210
+ });
211
+
212
+ // Mode-specific tasks
213
+ if (this.plan?.mode?.type === 'EXTEND_EXISTING' || this.prototypeInfo?.exists) {
214
+ tasks.push(...this.generateExtendTasks(taskId));
215
+ } else {
216
+ tasks.push(...this.generateCreateTasks(taskId));
217
+ }
218
+
219
+ return tasks;
220
+ }
221
+
222
+ /**
223
+ * Generate tasks for extending existing prototype
224
+ */
225
+ generateExtendTasks(startId) {
226
+ const tasks = [];
227
+ let taskId = startId;
228
+
229
+ // Task: Identify target file
230
+ const targetPage = this.options.targetPage || this.findBestTargetPage();
231
+
232
+ if (targetPage) {
233
+ tasks.push({
234
+ id: taskId++,
235
+ phase: 'analysis',
236
+ action: 'identify_target',
237
+ description: 'Identify target file for modification',
238
+ target: {
239
+ file: targetPage.file,
240
+ route: targetPage.route,
241
+ name: targetPage.name
242
+ }
243
+ });
244
+
245
+ // Task: Read and analyze existing file
246
+ tasks.push({
247
+ id: taskId++,
248
+ phase: 'analysis',
249
+ action: 'analyze_structure',
250
+ description: 'Analyze existing file structure',
251
+ file: targetPage.file,
252
+ outputs: ['component_tree', 'existing_imports', 'injection_points']
253
+ });
254
+
255
+ // Task: Create new component
256
+ tasks.push({
257
+ id: taskId++,
258
+ phase: 'implementation',
259
+ action: 'create_component',
260
+ description: `Create new component for ${this.options.featureDescription}`,
261
+ output: {
262
+ directory: path.join(this.protoDir, 'src', 'components'),
263
+ suggestedName: this.generateComponentName(this.options.featureDescription),
264
+ template: 'Use design tokens from design-tokens.json'
265
+ },
266
+ constraints: [
267
+ 'Use ONLY colors from design-tokens.json',
268
+ 'Match existing styling approach',
269
+ 'Follow existing naming conventions'
270
+ ]
271
+ });
272
+
273
+ // Task: Inject component
274
+ tasks.push({
275
+ id: taskId++,
276
+ phase: 'implementation',
277
+ action: 'inject_component',
278
+ description: 'Inject component into existing page',
279
+ injection: {
280
+ targetFile: targetPage.file,
281
+ method: 'insert-after',
282
+ selector: this.suggestInjectionPoint(targetPage),
283
+ preserveExisting: true,
284
+ addImport: true
285
+ }
286
+ });
287
+ }
288
+
289
+ // Task: Validate colors
290
+ tasks.push({
291
+ id: taskId++,
292
+ phase: 'validation',
293
+ action: 'validate_colors',
294
+ description: 'Validate all colors against design tokens',
295
+ command: `node cli.js validate-colors --project ${path.basename(this.projectDir)}`
296
+ });
297
+
298
+ // Task: Visual comparison
299
+ tasks.push({
300
+ id: taskId++,
301
+ phase: 'validation',
302
+ action: 'visual_diff',
303
+ description: 'Compare generated output with reference',
304
+ command: `node cli.js visual-diff --project ${path.basename(this.projectDir)} --page ${this.options.targetPage || 'homepage'}`,
305
+ threshold: 95
306
+ });
307
+
308
+ return tasks;
309
+ }
310
+
311
+ /**
312
+ * Generate tasks for creating new prototype
313
+ */
314
+ generateCreateTasks(startId) {
315
+ const tasks = [];
316
+ let taskId = startId;
317
+
318
+ // Task: Initialize Next.js project
319
+ tasks.push({
320
+ id: taskId++,
321
+ phase: 'setup',
322
+ action: 'initialize_project',
323
+ description: 'Initialize Next.js project',
324
+ command: 'npx create-next-app@latest --typescript --tailwind --eslint --app --src-dir',
325
+ output: this.protoDir
326
+ });
327
+
328
+ // Task: Configure Tailwind with design tokens
329
+ tasks.push({
330
+ id: taskId++,
331
+ phase: 'setup',
332
+ action: 'configure_styling',
333
+ description: 'Configure Tailwind with design tokens',
334
+ note: 'Use inline styles for colors, not Tailwind color classes'
335
+ });
336
+
337
+ // Task: Extract component library
338
+ tasks.push({
339
+ id: taskId++,
340
+ phase: 'implementation',
341
+ action: 'extract_components',
342
+ description: 'Extract reusable components from captured HTML',
343
+ command: `node cli.js extract-components --project ${path.basename(this.projectDir)}`
344
+ });
345
+
346
+ // Task: Convert HTML to React
347
+ if (this.manifest?.pages) {
348
+ for (const page of this.manifest.pages.slice(0, 5)) {
349
+ tasks.push({
350
+ id: taskId++,
351
+ phase: 'implementation',
352
+ action: 'convert_page',
353
+ description: `Convert ${page.name} to React`,
354
+ input: path.join(this.refsDir, 'html', `${page.name}.html`),
355
+ output: path.join(this.protoDir, 'src', 'app', page.route || page.name, 'page.tsx')
356
+ });
357
+ }
358
+ }
359
+
360
+ // Task: Final validation
361
+ tasks.push({
362
+ id: taskId++,
363
+ phase: 'validation',
364
+ action: 'validate_all',
365
+ description: 'Run all validations',
366
+ command: `node cli.js validate --project ${path.basename(this.projectDir)} --phase post-gen`
367
+ });
368
+
369
+ return tasks;
370
+ }
371
+
372
+ /**
373
+ * Find best target page for modification
374
+ */
375
+ findBestTargetPage() {
376
+ if (!this.prototypeInfo?.pages?.length) return null;
377
+
378
+ // Prefer pages matching feature description
379
+ const featureWords = this.options.featureDescription.toLowerCase().split(/\s+/);
380
+
381
+ for (const page of this.prototypeInfo.pages) {
382
+ const pageName = page.name.toLowerCase();
383
+ for (const word of featureWords) {
384
+ if (pageName.includes(word)) {
385
+ return page;
386
+ }
387
+ }
388
+ }
389
+
390
+ // Default to first page
391
+ return this.prototypeInfo.pages[0];
392
+ }
393
+
394
+ /**
395
+ * Suggest injection point for component
396
+ */
397
+ suggestInjectionPoint(targetPage) {
398
+ // Common injection points
399
+ const suggestions = [
400
+ 'Header',
401
+ 'header',
402
+ '.header',
403
+ '.page-header',
404
+ 'main',
405
+ '.main-content',
406
+ '.content'
407
+ ];
408
+
409
+ return {
410
+ suggestions,
411
+ recommended: suggestions[0],
412
+ note: 'Verify injection point by reading the target file first'
413
+ };
414
+ }
415
+
416
+ /**
417
+ * Generate component name from feature description
418
+ */
419
+ generateComponentName(description) {
420
+ const words = description
421
+ .replace(/[^a-zA-Z0-9\s]/g, '')
422
+ .split(/\s+/)
423
+ .filter(Boolean)
424
+ .slice(0, 3);
425
+
426
+ return words
427
+ .map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
428
+ .join('');
429
+ }
430
+
431
+ /**
432
+ * Analyze task dependencies
433
+ */
434
+ analyzeDependencies(tasks) {
435
+ const dependencies = {};
436
+
437
+ for (const task of tasks) {
438
+ dependencies[task.id] = {
439
+ blockedBy: [],
440
+ blocks: []
441
+ };
442
+
443
+ // Setup tasks block all others
444
+ if (task.phase === 'setup') {
445
+ for (const otherTask of tasks) {
446
+ if (otherTask.phase !== 'setup' && otherTask.id !== task.id) {
447
+ dependencies[task.id].blocks.push(otherTask.id);
448
+ }
449
+ }
450
+ }
451
+
452
+ // Analysis blocks implementation
453
+ if (task.phase === 'analysis') {
454
+ for (const otherTask of tasks) {
455
+ if (otherTask.phase === 'implementation') {
456
+ dependencies[task.id].blocks.push(otherTask.id);
457
+ dependencies[otherTask.id] = dependencies[otherTask.id] || { blockedBy: [], blocks: [] };
458
+ dependencies[otherTask.id].blockedBy.push(task.id);
459
+ }
460
+ }
461
+ }
462
+
463
+ // Implementation blocks validation
464
+ if (task.phase === 'implementation') {
465
+ for (const otherTask of tasks) {
466
+ if (otherTask.phase === 'validation') {
467
+ dependencies[task.id].blocks.push(otherTask.id);
468
+ dependencies[otherTask.id] = dependencies[otherTask.id] || { blockedBy: [], blocks: [] };
469
+ dependencies[otherTask.id].blockedBy.push(task.id);
470
+ }
471
+ }
472
+ }
473
+ }
474
+
475
+ return dependencies;
476
+ }
477
+
478
+ /**
479
+ * Generate validation rules
480
+ */
481
+ generateValidation() {
482
+ return {
483
+ preGeneration: [
484
+ {
485
+ check: 'design_tokens_exist',
486
+ path: path.join(this.refsDir, 'design-tokens.json'),
487
+ required: true
488
+ },
489
+ {
490
+ check: 'manifest_exists',
491
+ path: path.join(this.refsDir, 'manifest.json'),
492
+ required: true
493
+ },
494
+ {
495
+ check: 'screenshots_exist',
496
+ path: path.join(this.refsDir, 'screenshots'),
497
+ required: true
498
+ }
499
+ ],
500
+ postGeneration: [
501
+ {
502
+ check: 'color_validation',
503
+ command: 'validate-colors',
504
+ description: 'All colors must be from design-tokens.json',
505
+ blocking: true
506
+ },
507
+ {
508
+ check: 'visual_diff',
509
+ minSimilarity: 95,
510
+ description: 'Visual output must match reference >95%',
511
+ blocking: false
512
+ },
513
+ {
514
+ check: 'no_tailwind_defaults',
515
+ description: 'No Tailwind default colors (bg-blue-500, etc.)',
516
+ blocking: true
517
+ }
518
+ ]
519
+ };
520
+ }
521
+
522
+ /**
523
+ * Get critical rules
524
+ */
525
+ getCriticalRules() {
526
+ return {
527
+ never: [
528
+ 'Create new design systems or color schemes',
529
+ 'Deviate from captured design tokens',
530
+ 'Use colors not in design-tokens.json',
531
+ 'Create new prototype if one exists',
532
+ 'Replace existing pages - always extend',
533
+ 'Introduce new styling paradigms (styled-components if using CSS modules, etc.)'
534
+ ],
535
+ always: [
536
+ 'Search for existing prototype first',
537
+ 'Parse captured HTML for exact structure',
538
+ 'Validate colors against design-tokens.json',
539
+ 'Use screenshot for visual reference',
540
+ 'Preserve 100% of existing functionality',
541
+ 'Match framework and styling of existing code',
542
+ 'Insert at exact location specified in plan',
543
+ 'Verify visual output matches reference >95%'
544
+ ]
545
+ };
546
+ }
547
+
548
+ /**
549
+ * Write plan to file
550
+ */
551
+ writePlan(outputPath) {
552
+ const plan = this.generate();
553
+ const content = JSON.stringify(plan, null, 2);
554
+
555
+ const dir = path.dirname(outputPath);
556
+ if (!fs.existsSync(dir)) {
557
+ fs.mkdirSync(dir, { recursive: true });
558
+ }
559
+
560
+ fs.writeFileSync(outputPath, content);
561
+ return outputPath;
562
+ }
563
+
564
+ /**
565
+ * Format plan for CLI output
566
+ */
567
+ formatPlan() {
568
+ const plan = this.plan || this.generate();
569
+ const lines = [];
570
+
571
+ // Header
572
+ lines.push('\x1b[1m═══════════════════════════════════════════════════════════\x1b[0m');
573
+ lines.push('\x1b[1m IMPLEMENTATION PLAN \x1b[0m');
574
+ lines.push('\x1b[1m═══════════════════════════════════════════════════════════\x1b[0m');
575
+ lines.push('');
576
+
577
+ // Project Paths - CRITICAL for Claude to know where to write files
578
+ lines.push('\x1b[1mProject Paths:\x1b[0m');
579
+ lines.push(` \x1b[33mPrototype Directory:\x1b[0m ${plan.project.prototypePath}`);
580
+ lines.push(` References: ${plan.project.referencesPath}`);
581
+ lines.push('');
582
+
583
+ // Mode
584
+ const modeColor = plan.mode.type === 'EXTEND_EXISTING' ? '\x1b[33m' : '\x1b[32m';
585
+ lines.push(`\x1b[1mMode:\x1b[0m ${modeColor}${plan.mode.type}\x1b[0m`);
586
+ lines.push(` ${plan.mode.recommendation}`);
587
+ lines.push('');
588
+
589
+ // Feature
590
+ if (plan.feature) {
591
+ lines.push(`\x1b[1mFeature:\x1b[0m ${plan.feature}`);
592
+ lines.push('');
593
+ }
594
+
595
+ // Existing Prototype
596
+ if (plan.existingPrototype) {
597
+ lines.push('\x1b[1mExisting Prototype:\x1b[0m');
598
+ lines.push(` Framework: ${plan.existingPrototype.framework}`);
599
+ lines.push(` Styling: ${plan.existingPrototype.styling.join(', ')}`);
600
+ lines.push(` Pages: ${plan.existingPrototype.pagesCount}`);
601
+ lines.push(` Components: ${plan.existingPrototype.componentsCount}`);
602
+ lines.push('');
603
+ }
604
+
605
+ // Tasks
606
+ lines.push('\x1b[1mTasks:\x1b[0m');
607
+ for (const task of plan.tasks) {
608
+ const phaseColors = {
609
+ setup: '\x1b[36m',
610
+ analysis: '\x1b[35m',
611
+ implementation: '\x1b[33m',
612
+ validation: '\x1b[32m'
613
+ };
614
+ const color = phaseColors[task.phase] || '';
615
+ lines.push(` ${color}[${task.phase.toUpperCase()}]\x1b[0m ${task.id}. ${task.description}`);
616
+
617
+ if (task.target) {
618
+ lines.push(` Target: ${task.target.file}`);
619
+ }
620
+ if (task.injection) {
621
+ lines.push(` Inject: ${task.injection.method} ${task.injection.selector?.recommended || ''}`);
622
+ }
623
+ if (task.command) {
624
+ lines.push(` Command: ${task.command}`);
625
+ }
626
+ }
627
+ lines.push('');
628
+
629
+ // Validation
630
+ lines.push('\x1b[1mValidation Checkpoints:\x1b[0m');
631
+ for (const check of plan.validation.postGeneration) {
632
+ const status = check.blocking ? '\x1b[31m[BLOCKING]\x1b[0m' : '\x1b[33m[WARNING]\x1b[0m';
633
+ lines.push(` ${status} ${check.description}`);
634
+ }
635
+ lines.push('');
636
+
637
+ // Critical Rules
638
+ lines.push('\x1b[1mCritical Rules:\x1b[0m');
639
+ lines.push(' \x1b[31mNEVER:\x1b[0m');
640
+ for (const rule of plan.criticalRules.never.slice(0, 3)) {
641
+ lines.push(` ✗ ${rule}`);
642
+ }
643
+ lines.push(' \x1b[32mALWAYS:\x1b[0m');
644
+ for (const rule of plan.criticalRules.always.slice(0, 3)) {
645
+ lines.push(` ✓ ${rule}`);
646
+ }
647
+
648
+ return lines.join('\n');
649
+ }
650
+ }
651
+
652
+ /**
653
+ * Generate plan for project
654
+ */
655
+ function generatePlan(projectDir, options = {}) {
656
+ const generator = new PlanGenerator(projectDir, options);
657
+ return generator.generate();
658
+ }
659
+
660
+ // CLI execution
661
+ if (require.main === module) {
662
+ const args = process.argv.slice(2);
663
+ let projectName = null;
664
+ let projectDir = null;
665
+ let feature = '';
666
+ let targetPage = null;
667
+ let outputPath = null;
668
+
669
+ for (let i = 0; i < args.length; i++) {
670
+ switch (args[i]) {
671
+ case '--project':
672
+ projectName = args[++i];
673
+ break;
674
+ case '--path':
675
+ projectDir = args[++i];
676
+ break;
677
+ case '--feature':
678
+ feature = args[++i];
679
+ break;
680
+ case '--target':
681
+ targetPage = args[++i];
682
+ break;
683
+ case '--output':
684
+ case '-o':
685
+ outputPath = args[++i];
686
+ break;
687
+ case '--help':
688
+ case '-h':
689
+ console.log(`
690
+ Usage: node generate-plan.js [options]
691
+
692
+ Options:
693
+ --project <name> Project name
694
+ --path <path> Project directory path
695
+ --feature <desc> Feature description
696
+ --target <page> Target page for modification
697
+ --output, -o <path> Output path for plan JSON
698
+ --help, -h Show this help
699
+
700
+ Examples:
701
+ node generate-plan.js --project my-app --feature "Add health score widget"
702
+ node generate-plan.js --path ./projects/my-app --feature "User profile section" --target accounts
703
+ `);
704
+ process.exit(0);
705
+ }
706
+ }
707
+
708
+ // Handle project-based path
709
+ if (projectName && !projectDir) {
710
+ const SKILL_DIR = path.dirname(__dirname);
711
+ const PROJECTS_DIR = path.resolve(SKILL_DIR, '../../../projects');
712
+ projectDir = path.join(PROJECTS_DIR, projectName);
713
+ }
714
+
715
+ if (!projectDir) {
716
+ console.error('\x1b[31mError:\x1b[0m --project or --path is required');
717
+ process.exit(1);
718
+ }
719
+
720
+ try {
721
+ const generator = new PlanGenerator(projectDir, {
722
+ featureDescription: feature,
723
+ targetPage
724
+ });
725
+
726
+ const plan = generator.generate();
727
+
728
+ console.log(generator.formatPlan());
729
+
730
+ if (outputPath) {
731
+ generator.writePlan(outputPath);
732
+ console.log(`\n\x1b[32m✓ Plan written to: ${outputPath}\x1b[0m`);
733
+ }
734
+
735
+ } catch (error) {
736
+ console.error(`\x1b[31mError:\x1b[0m ${error.message}`);
737
+ process.exit(1);
738
+ }
739
+ }
740
+
741
+ module.exports = {
742
+ PlanGenerator,
743
+ generatePlan
744
+ };