real-prototypes-skill 0.1.1 → 0.1.3
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 +66 -15
- package/package.json +4 -1
|
@@ -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
|
+
};
|