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,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
+ };