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,604 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Component Injection Module
|
|
5
|
+
*
|
|
6
|
+
* Injects new React components into existing files using AST manipulation.
|
|
7
|
+
* Preserves existing code structure and formatting while adding new imports
|
|
8
|
+
* and component usage at specified locations.
|
|
9
|
+
*
|
|
10
|
+
* Features:
|
|
11
|
+
* - AST-based injection using Babel
|
|
12
|
+
* - Multiple injection position options (before, after, replace, wrap)
|
|
13
|
+
* - Import statement injection (preserves existing imports)
|
|
14
|
+
* - Backup/rollback capability
|
|
15
|
+
* - Selector-based injection points
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* node inject-component.js --target ./page.tsx --component HealthScore --position "after:.header"
|
|
19
|
+
* node inject-component.js --project my-app --target src/app/accounts/page.tsx --component Card --position "after:Header"
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const path = require('path');
|
|
24
|
+
const parser = require('@babel/parser');
|
|
25
|
+
const traverse = require('@babel/traverse').default;
|
|
26
|
+
const generate = require('@babel/generator').default;
|
|
27
|
+
const t = require('@babel/types');
|
|
28
|
+
|
|
29
|
+
// Injection position types
|
|
30
|
+
const POSITION_TYPES = {
|
|
31
|
+
BEFORE: 'before',
|
|
32
|
+
AFTER: 'after',
|
|
33
|
+
REPLACE: 'replace',
|
|
34
|
+
WRAP: 'wrap',
|
|
35
|
+
FIRST_CHILD: 'first-child',
|
|
36
|
+
LAST_CHILD: 'last-child'
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
class ComponentInjector {
|
|
40
|
+
constructor(options = {}) {
|
|
41
|
+
this.options = {
|
|
42
|
+
createBackup: true,
|
|
43
|
+
preserveFormatting: true,
|
|
44
|
+
...options
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
this.targetFile = null;
|
|
48
|
+
this.originalCode = null;
|
|
49
|
+
this.ast = null;
|
|
50
|
+
this.injectionResult = null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Load target file
|
|
55
|
+
*/
|
|
56
|
+
loadFile(filePath) {
|
|
57
|
+
this.targetFile = path.resolve(filePath);
|
|
58
|
+
|
|
59
|
+
if (!fs.existsSync(this.targetFile)) {
|
|
60
|
+
throw new Error(`Target file not found: ${this.targetFile}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
this.originalCode = fs.readFileSync(this.targetFile, 'utf-8');
|
|
64
|
+
this.parseAST();
|
|
65
|
+
|
|
66
|
+
return this;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Parse code to AST
|
|
71
|
+
*/
|
|
72
|
+
parseAST() {
|
|
73
|
+
const isTypeScript = this.targetFile.endsWith('.tsx') || this.targetFile.endsWith('.ts');
|
|
74
|
+
|
|
75
|
+
this.ast = parser.parse(this.originalCode, {
|
|
76
|
+
sourceType: 'module',
|
|
77
|
+
plugins: [
|
|
78
|
+
'jsx',
|
|
79
|
+
isTypeScript ? 'typescript' : 'flow',
|
|
80
|
+
'classProperties',
|
|
81
|
+
'decorators-legacy',
|
|
82
|
+
'exportDefaultFrom',
|
|
83
|
+
'exportNamespaceFrom',
|
|
84
|
+
'dynamicImport',
|
|
85
|
+
'optionalChaining',
|
|
86
|
+
'nullishCoalescingOperator'
|
|
87
|
+
]
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return this;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Create backup of original file
|
|
95
|
+
*/
|
|
96
|
+
createBackup() {
|
|
97
|
+
if (!this.options.createBackup) return null;
|
|
98
|
+
|
|
99
|
+
const backupPath = this.targetFile + '.backup';
|
|
100
|
+
fs.writeFileSync(backupPath, this.originalCode);
|
|
101
|
+
return backupPath;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Restore from backup
|
|
106
|
+
*/
|
|
107
|
+
restoreBackup() {
|
|
108
|
+
const backupPath = this.targetFile + '.backup';
|
|
109
|
+
if (fs.existsSync(backupPath)) {
|
|
110
|
+
fs.copyFileSync(backupPath, this.targetFile);
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Add import statement for component
|
|
118
|
+
*/
|
|
119
|
+
addImport(componentName, importPath, isDefault = false) {
|
|
120
|
+
const importDeclaration = isDefault
|
|
121
|
+
? t.importDeclaration(
|
|
122
|
+
[t.importDefaultSpecifier(t.identifier(componentName))],
|
|
123
|
+
t.stringLiteral(importPath)
|
|
124
|
+
)
|
|
125
|
+
: t.importDeclaration(
|
|
126
|
+
[t.importSpecifier(t.identifier(componentName), t.identifier(componentName))],
|
|
127
|
+
t.stringLiteral(importPath)
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
// Find the last import statement
|
|
131
|
+
let lastImportIndex = -1;
|
|
132
|
+
for (let i = 0; i < this.ast.program.body.length; i++) {
|
|
133
|
+
if (t.isImportDeclaration(this.ast.program.body[i])) {
|
|
134
|
+
lastImportIndex = i;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Check if import already exists
|
|
139
|
+
const importExists = this.ast.program.body.some(node => {
|
|
140
|
+
if (!t.isImportDeclaration(node)) return false;
|
|
141
|
+
if (node.source.value !== importPath) return false;
|
|
142
|
+
return node.specifiers.some(spec => {
|
|
143
|
+
if (t.isImportSpecifier(spec)) {
|
|
144
|
+
return spec.imported.name === componentName;
|
|
145
|
+
}
|
|
146
|
+
if (t.isImportDefaultSpecifier(spec)) {
|
|
147
|
+
return spec.local.name === componentName;
|
|
148
|
+
}
|
|
149
|
+
return false;
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
if (!importExists) {
|
|
154
|
+
// Insert after last import or at the beginning
|
|
155
|
+
const insertIndex = lastImportIndex >= 0 ? lastImportIndex + 1 : 0;
|
|
156
|
+
this.ast.program.body.splice(insertIndex, 0, importDeclaration);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return this;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Find JSX element by selector
|
|
164
|
+
* Supports: component name, className, id
|
|
165
|
+
*/
|
|
166
|
+
findJSXElement(selector) {
|
|
167
|
+
let found = null;
|
|
168
|
+
|
|
169
|
+
const selectorType = this.parseSelector(selector);
|
|
170
|
+
|
|
171
|
+
traverse(this.ast, {
|
|
172
|
+
JSXElement(path) {
|
|
173
|
+
if (found) return;
|
|
174
|
+
|
|
175
|
+
const openingElement = path.node.openingElement;
|
|
176
|
+
const elementName = openingElement.name.name ||
|
|
177
|
+
(openingElement.name.object && openingElement.name.property ?
|
|
178
|
+
`${openingElement.name.object.name}.${openingElement.name.property.name}` : null);
|
|
179
|
+
|
|
180
|
+
// Match by component name
|
|
181
|
+
if (selectorType.type === 'component' && elementName === selectorType.value) {
|
|
182
|
+
found = path;
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Match by className
|
|
187
|
+
if (selectorType.type === 'class') {
|
|
188
|
+
const classAttr = openingElement.attributes.find(attr =>
|
|
189
|
+
t.isJSXAttribute(attr) && attr.name.name === 'className'
|
|
190
|
+
);
|
|
191
|
+
if (classAttr && t.isStringLiteral(classAttr.value)) {
|
|
192
|
+
const classes = classAttr.value.value.split(' ');
|
|
193
|
+
if (classes.includes(selectorType.value)) {
|
|
194
|
+
found = path;
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Match by id
|
|
201
|
+
if (selectorType.type === 'id') {
|
|
202
|
+
const idAttr = openingElement.attributes.find(attr =>
|
|
203
|
+
t.isJSXAttribute(attr) && attr.name.name === 'id'
|
|
204
|
+
);
|
|
205
|
+
if (idAttr && t.isStringLiteral(idAttr.value) && idAttr.value.value === selectorType.value) {
|
|
206
|
+
found = path;
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
return found;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Parse selector string
|
|
218
|
+
*/
|
|
219
|
+
parseSelector(selector) {
|
|
220
|
+
if (selector.startsWith('.')) {
|
|
221
|
+
return { type: 'class', value: selector.substring(1) };
|
|
222
|
+
}
|
|
223
|
+
if (selector.startsWith('#')) {
|
|
224
|
+
return { type: 'id', value: selector.substring(1) };
|
|
225
|
+
}
|
|
226
|
+
return { type: 'component', value: selector };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Create JSX element for component
|
|
231
|
+
*/
|
|
232
|
+
createJSXElement(componentName, props = {}) {
|
|
233
|
+
const attributes = Object.entries(props).map(([key, value]) => {
|
|
234
|
+
if (typeof value === 'boolean') {
|
|
235
|
+
return value ? t.jsxAttribute(t.jsxIdentifier(key)) : null;
|
|
236
|
+
}
|
|
237
|
+
if (typeof value === 'string') {
|
|
238
|
+
return t.jsxAttribute(t.jsxIdentifier(key), t.stringLiteral(value));
|
|
239
|
+
}
|
|
240
|
+
return t.jsxAttribute(
|
|
241
|
+
t.jsxIdentifier(key),
|
|
242
|
+
t.jsxExpressionContainer(t.identifier(String(value)))
|
|
243
|
+
);
|
|
244
|
+
}).filter(Boolean);
|
|
245
|
+
|
|
246
|
+
return t.jsxElement(
|
|
247
|
+
t.jsxOpeningElement(
|
|
248
|
+
t.jsxIdentifier(componentName),
|
|
249
|
+
attributes,
|
|
250
|
+
true // self-closing
|
|
251
|
+
),
|
|
252
|
+
null,
|
|
253
|
+
[],
|
|
254
|
+
true
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Inject component at position relative to selector
|
|
260
|
+
*/
|
|
261
|
+
injectComponent(componentName, selector, position = POSITION_TYPES.AFTER, props = {}) {
|
|
262
|
+
const targetPath = this.findJSXElement(selector);
|
|
263
|
+
|
|
264
|
+
if (!targetPath) {
|
|
265
|
+
throw new Error(`Could not find element matching selector: ${selector}`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const newElement = this.createJSXElement(componentName, props);
|
|
269
|
+
|
|
270
|
+
switch (position) {
|
|
271
|
+
case POSITION_TYPES.BEFORE:
|
|
272
|
+
this.insertBefore(targetPath, newElement);
|
|
273
|
+
break;
|
|
274
|
+
case POSITION_TYPES.AFTER:
|
|
275
|
+
this.insertAfter(targetPath, newElement);
|
|
276
|
+
break;
|
|
277
|
+
case POSITION_TYPES.REPLACE:
|
|
278
|
+
targetPath.replaceWith(newElement);
|
|
279
|
+
break;
|
|
280
|
+
case POSITION_TYPES.WRAP:
|
|
281
|
+
this.wrapElement(targetPath, componentName, props);
|
|
282
|
+
break;
|
|
283
|
+
case POSITION_TYPES.FIRST_CHILD:
|
|
284
|
+
this.insertFirstChild(targetPath, newElement);
|
|
285
|
+
break;
|
|
286
|
+
case POSITION_TYPES.LAST_CHILD:
|
|
287
|
+
this.insertLastChild(targetPath, newElement);
|
|
288
|
+
break;
|
|
289
|
+
default:
|
|
290
|
+
throw new Error(`Unknown position type: ${position}`);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
this.injectionResult = {
|
|
294
|
+
componentName,
|
|
295
|
+
selector,
|
|
296
|
+
position,
|
|
297
|
+
success: true
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
return this;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Insert element before target
|
|
305
|
+
*/
|
|
306
|
+
insertBefore(targetPath, newElement) {
|
|
307
|
+
const parent = targetPath.parentPath;
|
|
308
|
+
if (t.isJSXElement(parent.node) || t.isJSXFragment(parent.node)) {
|
|
309
|
+
const children = parent.node.children;
|
|
310
|
+
const index = children.indexOf(targetPath.node);
|
|
311
|
+
if (index !== -1) {
|
|
312
|
+
children.splice(index, 0, newElement, t.jsxText('\n'));
|
|
313
|
+
}
|
|
314
|
+
} else if (targetPath.inList) {
|
|
315
|
+
targetPath.insertBefore(newElement);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Insert element after target
|
|
321
|
+
*/
|
|
322
|
+
insertAfter(targetPath, newElement) {
|
|
323
|
+
const parent = targetPath.parentPath;
|
|
324
|
+
if (t.isJSXElement(parent.node) || t.isJSXFragment(parent.node)) {
|
|
325
|
+
const children = parent.node.children;
|
|
326
|
+
const index = children.indexOf(targetPath.node);
|
|
327
|
+
if (index !== -1) {
|
|
328
|
+
children.splice(index + 1, 0, t.jsxText('\n'), newElement);
|
|
329
|
+
}
|
|
330
|
+
} else if (targetPath.inList) {
|
|
331
|
+
targetPath.insertAfter(newElement);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Wrap element with new component
|
|
337
|
+
*/
|
|
338
|
+
wrapElement(targetPath, componentName, props = {}) {
|
|
339
|
+
const originalElement = t.cloneNode(targetPath.node, true);
|
|
340
|
+
|
|
341
|
+
const attributes = Object.entries(props).map(([key, value]) => {
|
|
342
|
+
if (typeof value === 'string') {
|
|
343
|
+
return t.jsxAttribute(t.jsxIdentifier(key), t.stringLiteral(value));
|
|
344
|
+
}
|
|
345
|
+
return t.jsxAttribute(
|
|
346
|
+
t.jsxIdentifier(key),
|
|
347
|
+
t.jsxExpressionContainer(t.identifier(String(value)))
|
|
348
|
+
);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
const wrapperElement = t.jsxElement(
|
|
352
|
+
t.jsxOpeningElement(t.jsxIdentifier(componentName), attributes, false),
|
|
353
|
+
t.jsxClosingElement(t.jsxIdentifier(componentName)),
|
|
354
|
+
[t.jsxText('\n'), originalElement, t.jsxText('\n')],
|
|
355
|
+
false
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
targetPath.replaceWith(wrapperElement);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Insert as first child
|
|
363
|
+
*/
|
|
364
|
+
insertFirstChild(targetPath, newElement) {
|
|
365
|
+
if (t.isJSXElement(targetPath.node)) {
|
|
366
|
+
targetPath.node.children.unshift(t.jsxText('\n'), newElement);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Insert as last child
|
|
372
|
+
*/
|
|
373
|
+
insertLastChild(targetPath, newElement) {
|
|
374
|
+
if (t.isJSXElement(targetPath.node)) {
|
|
375
|
+
targetPath.node.children.push(newElement, t.jsxText('\n'));
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Generate code from AST
|
|
381
|
+
*/
|
|
382
|
+
generateCode() {
|
|
383
|
+
const output = generate(this.ast, {
|
|
384
|
+
retainLines: this.options.preserveFormatting,
|
|
385
|
+
compact: false,
|
|
386
|
+
jsescapeOption: { minimal: true }
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
return output.code;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Write modified code to file
|
|
394
|
+
*/
|
|
395
|
+
writeFile() {
|
|
396
|
+
if (this.options.createBackup) {
|
|
397
|
+
this.createBackup();
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const newCode = this.generateCode();
|
|
401
|
+
fs.writeFileSync(this.targetFile, newCode);
|
|
402
|
+
|
|
403
|
+
return this.targetFile;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Get diff between original and modified code
|
|
408
|
+
*/
|
|
409
|
+
getDiff() {
|
|
410
|
+
const newCode = this.generateCode();
|
|
411
|
+
const originalLines = this.originalCode.split('\n');
|
|
412
|
+
const newLines = newCode.split('\n');
|
|
413
|
+
|
|
414
|
+
const diff = [];
|
|
415
|
+
const maxLines = Math.max(originalLines.length, newLines.length);
|
|
416
|
+
|
|
417
|
+
for (let i = 0; i < maxLines; i++) {
|
|
418
|
+
const originalLine = originalLines[i] || '';
|
|
419
|
+
const newLine = newLines[i] || '';
|
|
420
|
+
|
|
421
|
+
if (originalLine !== newLine) {
|
|
422
|
+
if (originalLine && !newLine) {
|
|
423
|
+
diff.push({ line: i + 1, type: 'removed', content: originalLine });
|
|
424
|
+
} else if (!originalLine && newLine) {
|
|
425
|
+
diff.push({ line: i + 1, type: 'added', content: newLine });
|
|
426
|
+
} else {
|
|
427
|
+
diff.push({ line: i + 1, type: 'modified', original: originalLine, new: newLine });
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return diff;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Inject component into file
|
|
438
|
+
*/
|
|
439
|
+
function injectComponent(targetFile, componentName, importPath, selector, position, props = {}) {
|
|
440
|
+
const injector = new ComponentInjector();
|
|
441
|
+
|
|
442
|
+
injector.loadFile(targetFile);
|
|
443
|
+
injector.addImport(componentName, importPath);
|
|
444
|
+
injector.injectComponent(componentName, selector, position, props);
|
|
445
|
+
|
|
446
|
+
return {
|
|
447
|
+
injector,
|
|
448
|
+
code: injector.generateCode(),
|
|
449
|
+
diff: injector.getDiff(),
|
|
450
|
+
result: injector.injectionResult
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// CLI execution
|
|
455
|
+
if (require.main === module) {
|
|
456
|
+
const args = process.argv.slice(2);
|
|
457
|
+
let targetFile = null;
|
|
458
|
+
let componentName = null;
|
|
459
|
+
let importPath = null;
|
|
460
|
+
let selector = null;
|
|
461
|
+
let position = POSITION_TYPES.AFTER;
|
|
462
|
+
let projectName = null;
|
|
463
|
+
let dryRun = false;
|
|
464
|
+
|
|
465
|
+
for (let i = 0; i < args.length; i++) {
|
|
466
|
+
switch (args[i]) {
|
|
467
|
+
case '--target':
|
|
468
|
+
case '-t':
|
|
469
|
+
targetFile = args[++i];
|
|
470
|
+
break;
|
|
471
|
+
case '--component':
|
|
472
|
+
case '-c':
|
|
473
|
+
componentName = args[++i];
|
|
474
|
+
break;
|
|
475
|
+
case '--import':
|
|
476
|
+
case '-i':
|
|
477
|
+
importPath = args[++i];
|
|
478
|
+
break;
|
|
479
|
+
case '--position':
|
|
480
|
+
case '-p':
|
|
481
|
+
const posStr = args[++i];
|
|
482
|
+
// Parse position string like "after:.header" or "before:Header"
|
|
483
|
+
const [pos, sel] = posStr.includes(':') ? posStr.split(':') : ['after', posStr];
|
|
484
|
+
position = pos;
|
|
485
|
+
selector = sel;
|
|
486
|
+
break;
|
|
487
|
+
case '--project':
|
|
488
|
+
projectName = args[++i];
|
|
489
|
+
break;
|
|
490
|
+
case '--dry-run':
|
|
491
|
+
dryRun = true;
|
|
492
|
+
break;
|
|
493
|
+
case '--help':
|
|
494
|
+
case '-h':
|
|
495
|
+
console.log(`
|
|
496
|
+
Usage: node inject-component.js [options]
|
|
497
|
+
|
|
498
|
+
Options:
|
|
499
|
+
--target, -t <path> Target file to modify
|
|
500
|
+
--component, -c <name> Component name to inject
|
|
501
|
+
--import, -i <path> Import path for component
|
|
502
|
+
--position, -p <pos> Position string (e.g., "after:.header", "before:Header")
|
|
503
|
+
--project <name> Project name
|
|
504
|
+
--dry-run Show changes without writing
|
|
505
|
+
--help, -h Show this help
|
|
506
|
+
|
|
507
|
+
Position formats:
|
|
508
|
+
after:<selector> Insert after matching element
|
|
509
|
+
before:<selector> Insert before matching element
|
|
510
|
+
replace:<selector> Replace matching element
|
|
511
|
+
wrap:<selector> Wrap matching element
|
|
512
|
+
first-child:<selector> Insert as first child
|
|
513
|
+
last-child:<selector> Insert as last child
|
|
514
|
+
|
|
515
|
+
Selectors:
|
|
516
|
+
ComponentName Match by React component name
|
|
517
|
+
.className Match by CSS class
|
|
518
|
+
#id Match by element id
|
|
519
|
+
|
|
520
|
+
Examples:
|
|
521
|
+
node inject-component.js \\
|
|
522
|
+
--target src/app/page.tsx \\
|
|
523
|
+
--component HealthScore \\
|
|
524
|
+
--import "@/components/HealthScore" \\
|
|
525
|
+
--position "after:Header"
|
|
526
|
+
|
|
527
|
+
node inject-component.js \\
|
|
528
|
+
--target src/app/accounts/page.tsx \\
|
|
529
|
+
--component DataCard \\
|
|
530
|
+
--import "./DataCard" \\
|
|
531
|
+
--position "last-child:.main-content" \\
|
|
532
|
+
--dry-run
|
|
533
|
+
`);
|
|
534
|
+
process.exit(0);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Handle project-based paths
|
|
539
|
+
if (projectName && targetFile) {
|
|
540
|
+
const SKILL_DIR = path.dirname(__dirname);
|
|
541
|
+
const PROJECTS_DIR = path.resolve(SKILL_DIR, '../../../projects');
|
|
542
|
+
targetFile = path.join(PROJECTS_DIR, projectName, 'prototype', targetFile);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Set default import path if not specified
|
|
546
|
+
if (!importPath && componentName) {
|
|
547
|
+
importPath = `@/components/${componentName}`;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Validation
|
|
551
|
+
if (!targetFile || !componentName || !selector) {
|
|
552
|
+
console.error('\x1b[31mError:\x1b[0m --target, --component, and --position (with selector) are required');
|
|
553
|
+
process.exit(1);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
try {
|
|
557
|
+
console.log(`\n\x1b[1mComponent Injection\x1b[0m`);
|
|
558
|
+
console.log(`Target: ${targetFile}`);
|
|
559
|
+
console.log(`Component: ${componentName}`);
|
|
560
|
+
console.log(`Import: ${importPath}`);
|
|
561
|
+
console.log(`Position: ${position}`);
|
|
562
|
+
console.log(`Selector: ${selector}`);
|
|
563
|
+
console.log('');
|
|
564
|
+
|
|
565
|
+
const result = injectComponent(targetFile, componentName, importPath, selector, position);
|
|
566
|
+
|
|
567
|
+
// Show diff
|
|
568
|
+
if (result.diff.length > 0) {
|
|
569
|
+
console.log('\x1b[1mChanges:\x1b[0m');
|
|
570
|
+
for (const change of result.diff.slice(0, 20)) {
|
|
571
|
+
if (change.type === 'added') {
|
|
572
|
+
console.log(`\x1b[32m+ Line ${change.line}: ${change.content}\x1b[0m`);
|
|
573
|
+
} else if (change.type === 'removed') {
|
|
574
|
+
console.log(`\x1b[31m- Line ${change.line}: ${change.content}\x1b[0m`);
|
|
575
|
+
} else if (change.type === 'modified') {
|
|
576
|
+
console.log(`\x1b[33m~ Line ${change.line}:\x1b[0m`);
|
|
577
|
+
console.log(` \x1b[31m- ${change.original}\x1b[0m`);
|
|
578
|
+
console.log(` \x1b[32m+ ${change.new}\x1b[0m`);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
if (result.diff.length > 20) {
|
|
582
|
+
console.log(`... and ${result.diff.length - 20} more changes`);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
if (dryRun) {
|
|
587
|
+
console.log('\n\x1b[33mDry run - no changes written\x1b[0m');
|
|
588
|
+
} else {
|
|
589
|
+
result.injector.writeFile();
|
|
590
|
+
console.log('\n\x1b[32m✓ Changes written successfully\x1b[0m');
|
|
591
|
+
console.log(`Backup: ${targetFile}.backup`);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
} catch (error) {
|
|
595
|
+
console.error(`\x1b[31mError:\x1b[0m ${error.message}`);
|
|
596
|
+
process.exit(1);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
module.exports = {
|
|
601
|
+
ComponentInjector,
|
|
602
|
+
injectComponent,
|
|
603
|
+
POSITION_TYPES
|
|
604
|
+
};
|