scss-variable-extractor 1.6.4 → 1.6.5

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/bin/cli.js CHANGED
@@ -1,652 +1,807 @@
1
- #!/usr/bin/env node
2
-
3
- const { Command } = require('commander');
4
- const chalk = require('chalk');
5
- const path = require('path');
6
- const { loadConfig } = require('../src/config');
7
- const { scanScssFiles, scanTemplateFiles } = require('../src/scanner');
8
- const { parseScss } = require('../src/parser');
9
- const { analyzeValues } = require('../src/analyzer');
10
- const { generateVariablesFile, generateReport } = require('../src/generator');
11
- const { refactorScssFiles } = require('../src/refactorer');
12
- const { analyzeAngularPatterns, refactorAngularPatterns, generateAngularPatternReport } = require('../src/ng-refactorer');
13
- const { detectBootstrap, migrateBootstrapToMaterial, generateBootstrapReport } = require('../src/bootstrap-migrator');
14
- const { analyzeStyleOrganization, generateOrganizationReport } = require('../src/style-organizer');
15
-
16
- const program = new Command();
17
-
18
- program
19
- .name('scss-extract')
20
- .description('Analyzes Angular SCSS files and extracts repeated hardcoded values into reusable variables')
21
- .version('1.0.0');
22
-
23
- // Analyze command
24
- program
25
- .command('analyze')
26
- .description('Dry-run analysis - identifies repeated values without modifying files')
27
- .argument('[src]', 'Source directory to scan (optional if using angular.json)')
28
- .option('--threshold <number>', 'Minimum repeat count threshold', parseInt)
29
- .option('--format <format>', 'Report format (table, json, markdown)', 'table')
30
- .option('--config <path>', 'Path to config file')
31
- .option('--angular-json <path>', 'Path to angular.json file')
32
- .option('--project <name>', 'Angular project name (uses default if not specified)')
33
- .option('--no-angular', 'Disable angular.json integration')
34
- .action(async (src, options) => {
35
- try {
36
- console.log(chalk.cyan.bold('\nšŸ” SCSS Variable Extraction Analysis\n'));
37
-
38
- const config = loadConfig(options.config, {
39
- useAngularJson: options.angular !== false,
40
- angularJsonPath: options.angularJson,
41
- projectName: options.project
42
- });
43
-
44
- // Override config with command-line options
45
- if (src) config.src = src;
46
- if (options.threshold) config.threshold = options.threshold;
47
- if (options.format) config.reportFormat = options.format;
48
-
49
- // Show Angular project info if available
50
- if (config.angular) {
51
- console.log(chalk.cyan(`Angular Project: ${config.angular.project}`));
52
- console.log(chalk.gray(`Prefix: ${config.angular.prefix}`));
53
- console.log(chalk.gray(`Style: ${config.angular.stylePreprocessor}\n`));
54
- }
55
-
56
- console.log(chalk.gray(`Scanning: ${config.src}`));
57
- console.log(chalk.gray(`Threshold: ${config.threshold} occurrences\n`));
58
-
59
- // Scan files
60
- const files = await scanScssFiles(config.src, config.ignore);
61
- console.log(chalk.green(`āœ“ Found ${files.length} SCSS files\n`));
62
-
63
- // Parse all files
64
- const allExtracted = {
65
- colors: [],
66
- spacing: [],
67
- fontSizes: [],
68
- fontWeights: [],
69
- fontFamilies: [],
70
- borderRadius: [],
71
- shadows: [],
72
- zIndex: [],
73
- sizing: [],
74
- lineHeight: [],
75
- opacity: [],
76
- transitions: []
77
- };
78
-
79
- files.forEach(file => {
80
- const content = require('fs').readFileSync(file, 'utf8');
81
- const extracted = parseScss(content, file);
82
-
83
- Object.keys(extracted).forEach(category => {
84
- allExtracted[category].push(...extracted[category]);
85
- });
86
- });
87
-
88
- // Analyze
89
- const analysis = analyzeValues(allExtracted, config);
90
-
91
- // Generate report
92
- const report = generateReport(analysis, config.reportFormat, config);
93
- console.log(report);
94
-
95
- } catch (error) {
96
- console.error(chalk.red('āœ— Error:'), error.message);
97
- process.exit(1);
98
- }
99
- });
100
-
101
- // Generate command
102
- program
103
- .command('generate')
104
- .description('Generate variables file only (does not modify existing SCSS files)')
105
- .argument('[src]', 'Source directory to scan (optional if using angular.json)')
106
- .option('--output <path>', 'Output path for variables file')
107
- .option('--threshold <number>', 'Minimum repeat count threshold', parseInt)
108
- .option('--config <path>', 'Path to config file')
109
- .option('--angular-json <path>', 'Path to angular.json file')
110
- .option('--project <name>', 'Angular project name (uses default if not specified)')
111
- .option('--no-angular', 'Disable angular.json integration')
112
- .action(async (src, options) => {
113
- try {
114
- console.log(chalk.cyan.bold('\nšŸ“ Generating SCSS Variables File\n'));
115
-
116
- const config = loadConfig(options.config, {
117
- useAngularJson: options.angular !== false,
118
- angularJsonPath: options.angularJson,
119
- projectName: options.project
120
- });
121
-
122
- // Override config with command-line options
123
- if (src) config.src = src;
124
- if (options.output) config.output = options.output;
125
- if (options.threshold) config.threshold = options.threshold;
126
-
127
- // Show Angular project info if available
128
- if (config.angular) {
129
- console.log(chalk.cyan(`Angular Project: ${config.angular.project}`));
130
- console.log(chalk.gray(`Prefix: ${config.angular.prefix}`));
131
- console.log(chalk.gray(`Style: ${config.angular.stylePreprocessor}\n`));
132
- }
133
-
134
- console.log(chalk.gray(`Scanning: ${config.src}`));
135
- console.log(chalk.gray(`Output: ${config.output}\n`));
136
-
137
- // Scan files
138
- const files = await scanScssFiles(config.src, config.ignore);
139
- console.log(chalk.green(`āœ“ Found ${files.length} SCSS files`));
140
-
141
- // Parse all files
142
- const allExtracted = {
143
- colors: [],
144
- spacing: [],
145
- fontSizes: [],
146
- fontWeights: [],
147
- fontFamilies: [],
148
- borderRadius: [],
149
- shadows: [],
150
- zIndex: [],
151
- sizing: [],
152
- lineHeight: [],
153
- opacity: [],
154
- transitions: []
155
- };
156
-
157
- files.forEach(file => {
158
- const content = require('fs').readFileSync(file, 'utf8');
159
- const extracted = parseScss(content, file);
160
-
161
- Object.keys(extracted).forEach(category => {
162
- allExtracted[category].push(...extracted[category]);
163
- });
164
- });
165
-
166
- // Analyze
167
- const analysis = analyzeValues(allExtracted, config);
168
-
169
- // Generate variables file
170
- generateVariablesFile(analysis, config.output, config);
171
-
172
- console.log(chalk.green(`\nāœ“ Generated variables file: ${config.output}`));
173
-
174
- // Show summary
175
- const totalVars = Object.values(analysis).reduce((sum, arr) => sum + arr.length, 0);
176
- console.log(chalk.bold(`\nTotal variables extracted: ${totalVars}\n`));
177
-
178
- } catch (error) {
179
- console.error(chalk.red('āœ— Error:'), error.message);
180
- process.exit(1);
181
- }
182
- });
183
-
184
- // Refactor command
185
- program
186
- .command('refactor')
187
- .description('Full extraction + replacement (generates variables file and refactors SCSS files)')
188
- .argument('[src]', 'Source directory to scan (optional if using angular.json)')
189
- .option('--output <path>', 'Output path for variables file')
190
- .option('--threshold <number>', 'Minimum repeat count threshold', parseInt)
191
- .option('--config <path>', 'Path to config file')
192
- .option('--angular-json <path>', 'Path to angular.json file')
193
- .option('--project <name>', 'Angular project name (uses default if not specified)')
194
- .option('--no-angular', 'Disable angular.json integration')
195
- .action(async (src, options) => {
196
- try {
197
- console.log(chalk.cyan.bold('\nšŸ”§ SCSS Refactoring - Full Extraction\n'));
198
-
199
- const config = loadConfig(options.config, {
200
- useAngularJson: options.angular !== false,
201
- angularJsonPath: options.angularJson,
202
- projectName: options.project
203
- });
204
-
205
- // Override config with command-line options
206
- if (src) config.src = src;
207
- if (options.output) config.output = options.output;
208
- if (options.threshold) config.threshold = options.threshold;
209
-
210
- // Show Angular project info if available
211
- if (config.angular) {
212
- console.log(chalk.cyan(`Angular Project: ${config.angular.project}`));
213
- console.log(chalk.gray(`Prefix: ${config.angular.prefix}`));
214
- console.log(chalk.gray(`Style: ${config.angular.stylePreprocessor}\n`));
215
- }
216
-
217
- console.log(chalk.gray(`Scanning: ${config.src}`));
218
- console.log(chalk.gray(`Output: ${config.output}\n`));
219
-
220
- // Scan files
221
- const files = await scanScssFiles(config.src, config.ignore);
222
- console.log(chalk.green(`āœ“ Found ${files.length} SCSS files`));
223
-
224
- // Parse all files
225
- const allExtracted = {
226
- colors: [],
227
- spacing: [],
228
- fontSizes: [],
229
- fontWeights: [],
230
- fontFamilies: [],
231
- borderRadius: [],
232
- shadows: [],
233
- zIndex: [],
234
- sizing: [],
235
- lineHeight: [],
236
- opacity: [],
237
- transitions: []
238
- };
239
-
240
- files.forEach(file => {
241
- const content = require('fs').readFileSync(file, 'utf8');
242
- const extracted = parseScss(content, file);
243
-
244
- Object.keys(extracted).forEach(category => {
245
- allExtracted[category].push(...extracted[category]);
246
- });
247
- });
248
-
249
- // Analyze
250
- const analysis = analyzeValues(allExtracted, config);
251
-
252
- // Generate variables file
253
- generateVariablesFile(analysis, config.output, config);
254
- console.log(chalk.green(`āœ“ Generated variables file: ${config.output}`));
255
-
256
- // Refactor files
257
- const refactoredFiles = refactorScssFiles(files, analysis, config.output, config);
258
- console.log(chalk.green(`āœ“ Refactored ${refactoredFiles.length} SCSS files`));
259
-
260
- // Show summary
261
- const totalVars = Object.values(analysis).reduce((sum, arr) => sum + arr.length, 0);
262
- console.log(chalk.bold(`\nTotal variables extracted: ${totalVars}`));
263
- console.log(chalk.bold(`Files modified: ${refactoredFiles.length}\n`));
264
-
265
- } catch (error) {
266
- console.error(chalk.red('āœ— Error:'), error.message);
267
- process.exit(1);
268
- }
269
- });
270
-
271
- // Analyze Angular patterns command (ng-deep, !important)
272
- program
273
- .command('analyze-patterns')
274
- .description('Analyze SCSS files for Angular Material v15+ anti-patterns (::ng-deep, !important)')
275
- .argument('[src]', 'Source directory to scan (optional if using angular.json)')
276
- .option('--format <format>', 'Report format (table, json, markdown)', 'table')
277
- .option('--config <path>', 'Path to config file')
278
- .option('--angular-json <path>', 'Path to angular.json file')
279
- .option('--project <name>', 'Angular project name (uses default if not specified)')
280
- .option('--no-angular', 'Disable angular.json integration')
281
- .action(async (src, options) => {
282
- try {
283
- console.log(chalk.cyan.bold('\nšŸ” Angular Anti-Pattern Analysis\n'));
284
-
285
- const config = loadConfig(options.config, {
286
- useAngularJson: options.angular !== false,
287
- angularJsonPath: options.angularJson,
288
- projectName: options.project
289
- });
290
- if (src) config.src = src;
291
-
292
- // Show Angular project info if available
293
- if (config.angular) {
294
- console.log(chalk.cyan(`Angular Project: ${config.angular.project}`));
295
- console.log(chalk.gray(`Prefix: ${config.angular.prefix}`));
296
- console.log(chalk.gray(`Style: ${config.angular.stylePreprocessor}\n`));
297
- }
298
-
299
- console.log(chalk.gray(`Scanning: ${config.src}\n`));
300
-
301
- // Scan files
302
- const files = await scanScssFiles(config.src, config.ignore);
303
- console.log(chalk.green(`āœ“ Found ${files.length} SCSS files\n`));
304
-
305
- // Analyze patterns
306
- const analysis = analyzeAngularPatterns(files);
307
-
308
- // Generate report
309
- const report = generateAngularPatternReport(analysis, options.format);
310
- console.log(report);
311
-
312
- // Exit with error code if issues found
313
- if (analysis.summary.ngDeepCount > 0 || analysis.summary.importantCount > 0) {
314
- console.log(chalk.yellow('⚠ Anti-patterns detected. Run "modernize" command to fix them.\n'));
315
- } else {
316
- console.log(chalk.green('āœ“ No anti-patterns detected!\n'));
317
- }
318
-
319
- } catch (error) {
320
- console.error(chalk.red('āœ— Error:'), error.message);
321
- process.exit(1);
322
- }
323
- });
324
-
325
- // Modernize command (refactor ng-deep and !important)
326
- program
327
- .command('modernize')
328
- .description('Refactor SCSS files following Angular Material v15+ best practices')
329
- .argument('[src]', 'Source directory to scan (optional if using angular.json)')
330
- .option('--global-styles <path>', 'Path to global styles file', './src/styles.scss')
331
- .option('--no-ng-deep', 'Skip ::ng-deep refactoring')
332
- .option('--no-important', 'Skip !important refactoring')
333
- .option('--dry-run', 'Preview changes without modifying files')
334
- .option('--config <path>', 'Path to config file')
335
- .option('--angular-json <path>', 'Path to angular.json file')
336
- .option('--project <name>', 'Angular project name (uses default if not specified)')
337
- .option('--no-angular', 'Disable angular.json integration')
338
- .action(async (src, options) => {
339
- try {
340
- console.log(chalk.cyan.bold('\nšŸ”§ Modernizing SCSS Files (Angular Material v15+ Best Practices)\n'));
341
-
342
- const config = loadConfig(options.config, {
343
- useAngularJson: options.angular !== false,
344
- angularJsonPath: options.angularJson,
345
- projectName: options.project
346
- });
347
- if (src) config.src = src;
348
-
349
- if (options.dryRun) {
350
- console.log(chalk.yellow('DRY RUN MODE - No files will be modified\n'));
351
- }
352
-
353
- // Show Angular project info if available
354
- if (config.angular) {
355
- console.log(chalk.cyan(`Angular Project: ${config.angular.project}`));
356
- console.log(chalk.gray(`Prefix: ${config.angular.prefix}`));
357
- console.log(chalk.gray(`Style: ${config.angular.stylePreprocessor}\n`));
358
- }
359
-
360
- console.log(chalk.gray(`Scanning: ${config.src}`));
361
- console.log(chalk.gray(`Global styles: ${options.globalStyles}\n`));
362
-
363
- // Scan files
364
- const files = await scanScssFiles(config.src, config.ignore);
365
- console.log(chalk.green(`āœ“ Found ${files.length} SCSS files\n`));
366
-
367
- // Refactor patterns
368
- const results = refactorAngularPatterns(files, {
369
- removeNgDeep: options.ngDeep !== false,
370
- removeImportant: options.important !== false,
371
- createGlobalStyles: true,
372
- globalStylesPath: options.globalStyles,
373
- dryRun: options.dryRun
374
- });
375
-
376
- // Show results
377
- if (results.modified.length > 0) {
378
- console.log(chalk.green(`āœ“ Modified ${results.modified.length} file(s)\n`));
379
-
380
- results.modified.forEach(file => {
381
- console.log(chalk.white(` ${path.basename(file.file)}:`));
382
- file.changes.forEach(change => {
383
- if (change.type === 'ng-deep-removed') {
384
- console.log(chalk.yellow(` - Removed ${change.count} ::ng-deep occurrence(s)`));
385
- } else if (change.type === 'important-removed') {
386
- console.log(chalk.yellow(` - Removed ${change.count} !important declaration(s)`));
387
- }
388
- });
389
- });
390
- console.log();
391
- }
392
-
393
- if (results.globalStyles.length > 0) {
394
- console.log(chalk.cyan(`āœ“ Extracted ${results.globalStyles.length} style(s) to ${options.globalStyles}\n`));
395
- }
396
-
397
- if (results.warnings.length > 0) {
398
- console.log(chalk.yellow.bold('⚠ Warnings:\n'));
399
- results.warnings.forEach(warning => {
400
- console.log(chalk.yellow(` ${warning.message}`));
401
- });
402
- console.log();
403
- }
404
-
405
- if (results.modified.length === 0) {
406
- console.log(chalk.green('āœ“ No changes needed - files already follow best practices!\n'));
407
- } else if (!options.dryRun) {
408
- console.log(chalk.bold.green('āœ“ Modernization complete!\n'));
409
- console.log(chalk.gray('Next steps:'));
410
- console.log(chalk.gray(' 1. Review the modified files'));
411
- console.log(chalk.gray(' 2. Test your application'));
412
- console.log(chalk.gray(' 3. Consider using Angular Material theme mixins for component theming'));
413
- console.log(chalk.gray(' 4. Review extracted global styles and organize as needed\n'));
414
- }
415
-
416
- } catch (error) {
417
- console.error(chalk.red('āœ— Error:'), error.message);
418
- process.exit(1);
419
- }
420
- });
421
-
422
- // Detect Bootstrap command
423
- program
424
- .command('detect-bootstrap')
425
- .description('Detect Bootstrap classes and analyze migration to Angular Material')
426
- .argument('[src]', 'Source directory to scan (optional if using angular.json)')
427
- .option('--format <format>', 'Report format (table, json, markdown)', 'table')
428
- .option('--config <path>', 'Path to config file')
429
- .option('--angular-json <path>', 'Path to angular.json file')
430
- .option('--project <name>', 'Angular project name (uses default if not specified)')
431
- .option('--no-angular', 'Disable angular.json integration')
432
- .action(async (src, options) => {
433
- try {
434
- console.log(chalk.cyan.bold('\nšŸ“¦ Bootstrap to Material Migration Analysis\n'));
435
-
436
- const config = loadConfig(options.config, {
437
- useAngularJson: options.angular !== false,
438
- angularJsonPath: options.angularJson,
439
- projectName: options.project
440
- });
441
- if (src) config.src = src;
442
-
443
- // Show Angular project info if available
444
- if (config.angular) {
445
- console.log(chalk.cyan(`Angular Project: ${config.angular.project}`));
446
- console.log(chalk.gray(`Prefix: ${config.angular.prefix}`));
447
- console.log(chalk.gray(`Style: ${config.angular.stylePreprocessor}\n`));
448
- }
449
-
450
- console.log(chalk.gray(`Scanning: ${config.src}\n`));
451
-
452
- // Scan files - look for SCSS, HTML, and TS files
453
- const allFiles = await scanTemplateFiles(config.src, config.ignore);
454
- console.log(chalk.green(`āœ“ Found ${allFiles.length} files (SCSS, HTML, TS)\n`));
455
-
456
- // Detect Bootstrap
457
- const detection = detectBootstrap(allFiles);
458
-
459
- // Generate report
460
- const report = generateBootstrapReport(detection, options.format);
461
- console.log(report);
462
-
463
- // Provide migration guidance
464
- if (detection.summary.totalBootstrapClasses > 0) {
465
- console.log(chalk.yellow('⚠ Bootstrap classes detected. Use "migrate-bootstrap" command to convert.\n'));
466
- console.log(chalk.gray('Migration options:'));
467
- console.log(chalk.gray(' • Components (btn, card, etc.) → Angular Material components'));
468
- console.log(chalk.gray(' • Utilities (d-flex, spacing, etc.) → Custom utility classes'));
469
- console.log(chalk.gray(' • Grid system → CSS Grid or Flexbox\n'));
470
- } else {
471
- console.log(chalk.green('āœ“ No Bootstrap classes detected!\n'));
472
- }
473
-
474
- } catch (error) {
475
- console.error(chalk.red('āœ— Error:'), error.message);
476
- process.exit(1);
477
- }
478
- });
479
-
480
- // Migrate Bootstrap command
481
- program
482
- .command('migrate-bootstrap')
483
- .description('Migrate Bootstrap classes to Angular Material MDC-based styles')
484
- .argument('[src]', 'Source directory to scan (optional if using angular.json)')
485
- .option('--custom-utilities <path>', 'Path to custom utilities file', './src/styles/utilities.scss')
486
- .option('--no-custom-utilities', 'Skip creating custom utility classes')
487
- .option('--no-remove-imports', 'Keep Bootstrap imports')
488
- .option('--dry-run', 'Preview changes without modifying files')
489
- .option('--config <path>', 'Path to config file')
490
- .option('--angular-json <path>', 'Path to angular.json file')
491
- .option('--project <name>', 'Angular project name (uses default if not specified)')
492
- .option('--no-angular', 'Disable angular.json integration')
493
- .action(async (src, options) => {
494
- try {
495
- console.log(chalk.cyan.bold('\nšŸ”„ Migrating Bootstrap to Angular Material\n'));
496
-
497
- const config = loadConfig(options.config, {
498
- useAngularJson: options.angular !== false,
499
- angularJsonPath: options.angularJson,
500
- projectName: options.project
501
- });
502
- if (src) config.src = src;
503
-
504
- if (options.dryRun) {
505
- console.log(chalk.yellow('DRY RUN MODE - No files will be modified\n'));
506
- }
507
-
508
- // Show Angular project info if available
509
- if (config.angular) {
510
- console.log(chalk.cyan(`Angular Project: ${config.angular.project}`));
511
- console.log(chalk.gray(`Prefix: ${config.angular.prefix}`));
512
- console.log(chalk.gray(`Style: ${config.angular.stylePreprocessor}\n`));
513
- }
514
-
515
- console.log(chalk.gray(`Scanning: ${config.src}`));
516
- console.log(chalk.gray(`Utilities output: ${options.customUtilities || './src/styles/utilities.scss'}\n`));
517
-
518
- // Scan files - look for SCSS, HTML, and TS files
519
- const allFiles = await scanTemplateFiles(config.src, config.ignore);
520
- console.log(chalk.green(`āœ“ Found ${allFiles.length} files (SCSS, HTML, TS)\n`));
521
-
522
- // Migrate Bootstrap
523
- const results = migrateBootstrapToMaterial(allFiles, {
524
- createCustomUtilities: options.customUtilities !== false,
525
- customUtilitiesPath: options.customUtilities || './src/styles/utilities.scss',
526
- removeBootstrapImports: options.removeImports !== false,
527
- dryRun: options.dryRun
528
- });
529
-
530
- // Show results
531
- if (results.modified.length > 0) {
532
- console.log(chalk.green(`āœ“ Modified ${results.modified.length} file(s)\n`));
533
-
534
- results.modified.slice(0, 10).forEach(file => {
535
- console.log(chalk.white(` ${path.basename(file.file)}:`));
536
- file.changes.forEach(change => {
537
- if (change.type === 'import-removed') {
538
- console.log(chalk.yellow(` - ${change.description}`));
539
- }
540
- });
541
- });
542
- if (results.modified.length > 10) {
543
- console.log(chalk.gray(` ... and ${results.modified.length - 10} more`));
544
- }
545
- console.log();
546
- }
547
-
548
- if (results.customUtilities.length > 0) {
549
- console.log(chalk.cyan(`āœ“ Generated ${results.customUtilities.length} custom utility class(es) in ${options.customUtilities || './src/styles/utilities.scss'}\n`));
550
- }
551
-
552
- if (results.componentMigrations.length > 0) {
553
- console.log(chalk.yellow(`⚠ ${results.componentMigrations.length} component(s) require manual migration\n`));
554
- }
555
-
556
- if (results.warnings.length > 0) {
557
- console.log(chalk.yellow.bold('⚠ Warnings:\n'));
558
- results.warnings.slice(0, 5).forEach(warning => {
559
- console.log(chalk.yellow(` ${warning.message}`));
560
- if (warning.variables) {
561
- console.log(chalk.gray(` Variables: ${warning.variables.join(', ')}`));
562
- }
563
- });
564
- if (results.warnings.length > 5) {
565
- console.log(chalk.gray(` ... and ${results.warnings.length - 5} more warnings`));
566
- }
567
- console.log();
568
- }
569
-
570
- if (results.modified.length === 0 && results.warnings.length === 0) {
571
- console.log(chalk.green('āœ“ No Bootstrap code detected!\n'));
572
- } else if (!options.dryRun) {
573
- console.log(chalk.bold.green('āœ“ Migration complete!\n'));
574
- console.log(chalk.gray('Next steps:'));
575
- console.log(chalk.gray(' 1. Review the modified SCSS files'));
576
- console.log(chalk.gray(' 2. Update HTML templates to use Angular Material components'));
577
- console.log(chalk.gray(' 3. Import custom utilities in your styles.scss'));
578
- console.log(chalk.gray(' 4. Install @angular/material if not already installed'));
579
- console.log(chalk.gray(' 5. Test your application thoroughly\n'));
580
- console.log(chalk.cyan('Component Migration Guide:'));
581
- console.log(chalk.gray(' • .btn → <button mat-button>'));
582
- console.log(chalk.gray(' • .card → <mat-card>'));
583
- console.log(chalk.gray(' • .form-control → <mat-form-field><input matInput></mat-form-field>'));
584
- console.log(chalk.gray(' • See: https://material.angular.io/components\n'));
585
- }
586
-
587
- } catch (error) {
588
- console.error(chalk.red('āœ— Error:'), error.message);
589
- process.exit(1);
590
- }
591
- });
592
-
593
- // Analyze style organization command
594
- program
595
- .command('analyze-organization')
596
- .description('Analyze style organization and get recommendations for best practices')
597
- .argument('[src]', 'Source directory to scan (optional if using angular.json)')
598
- .option('--format <format>', 'Report format (table, json, markdown)', 'table')
599
- .option('--config <path>', 'Path to config file')
600
- .option('--angular-json <path>', 'Path to angular.json file')
601
- .option('--project <name>', 'Angular project name (uses default if not specified)')
602
- .option('--no-angular', 'Disable angular.json integration')
603
- .action(async (src, options) => {
604
- try {
605
- console.log(chalk.cyan.bold('\nšŸ“‹ Style Organization Analysis\n'));
606
-
607
- const config = loadConfig(options.config, {
608
- useAngularJson: options.angular !== false,
609
- angularJsonPath: options.angularJson,
610
- projectName: options.project
611
- });
612
- if (src) config.src = src;
613
-
614
- // Show Angular project info if available
615
- if (config.angular) {
616
- console.log(chalk.cyan(`Angular Project: ${config.angular.project}`));
617
- console.log(chalk.gray(`Prefix: ${config.angular.prefix}`));
618
- console.log(chalk.gray(`Style: ${config.angular.stylePreprocessor}\n`));
619
- }
620
-
621
- console.log(chalk.gray(`Scanning: ${config.src}\n`));
622
-
623
- // Scan files
624
- const files = await scanScssFiles(config.src, config.ignore);
625
- console.log(chalk.green(`āœ“ Found ${files.length} SCSS files\n`));
626
-
627
- // Analyze organization
628
- const analysis = analyzeStyleOrganization(files, config);
629
-
630
- // Generate report
631
- const report = generateOrganizationReport(analysis, options.format);
632
- console.log(report);
633
-
634
- // Provide guidance
635
- if (analysis.recommendations.length > 0) {
636
- console.log(chalk.yellow.bold('šŸ’” Next Steps:\n'));
637
- console.log(chalk.gray('1. Review recommendations above'));
638
- console.log(chalk.gray('2. Run "refactor" command to extract variables'));
639
- console.log(chalk.gray('3. Run "modernize" to remove anti-patterns'));
640
- console.log(chalk.gray('4. Consider restructuring based on suggested organization'));
641
- console.log(chalk.gray('5. Use @use instead of @import for better modularity\n'));
642
- } else {
643
- console.log(chalk.green('āœ“ Your styles are well organized!\n'));
644
- }
645
-
646
- } catch (error) {
647
- console.error(chalk.red('āœ— Error:'), error.message);
648
- process.exit(1);
649
- }
650
- });
651
-
652
- program.parse();
1
+ #!/usr/bin/env node
2
+
3
+ const { Command } = require('commander');
4
+ const chalk = require('chalk');
5
+ const path = require('path');
6
+ const { loadConfig } = require('../src/config');
7
+ const { scanScssFiles, scanTemplateFiles } = require('../src/scanner');
8
+ const { parseScss } = require('../src/parser');
9
+ const { analyzeValues } = require('../src/analyzer');
10
+ const { generateVariablesFile, generateReport } = require('../src/generator');
11
+ const { refactorScssFiles } = require('../src/refactorer');
12
+ const {
13
+ analyzeAngularPatterns,
14
+ refactorAngularPatterns,
15
+ generateAngularPatternReport,
16
+ } = require('../src/ng-refactorer');
17
+ const {
18
+ detectBootstrap,
19
+ migrateBootstrapToMaterial,
20
+ generateBootstrapReport,
21
+ } = require('../src/bootstrap-migrator');
22
+ const { analyzeStyleOrganization, generateOrganizationReport } = require('../src/style-organizer');
23
+ const { generateThemeStructure, analyzeThemeReadiness } = require('../src/theme-utils');
24
+
25
+ const program = new Command();
26
+
27
+ program
28
+ .name('scss-extract')
29
+ .description(
30
+ 'Analyzes Angular SCSS files and extracts repeated hardcoded values into reusable variables'
31
+ )
32
+ .version('1.0.0');
33
+
34
+ // Analyze command
35
+ program
36
+ .command('analyze')
37
+ .description('Dry-run analysis - identifies repeated values without modifying files')
38
+ .argument('[src]', 'Source directory to scan (optional if using angular.json)')
39
+ .option('--threshold <number>', 'Minimum repeat count threshold', parseInt)
40
+ .option('--format <format>', 'Report format (table, json, markdown)', 'table')
41
+ .option('--config <path>', 'Path to config file')
42
+ .option('--angular-json <path>', 'Path to angular.json file')
43
+ .option('--project <name>', 'Angular project name (uses default if not specified)')
44
+ .option('--no-angular', 'Disable angular.json integration')
45
+ .action(async (src, options) => {
46
+ try {
47
+ console.log(chalk.cyan.bold('\nšŸ” SCSS Variable Extraction Analysis\n'));
48
+
49
+ const config = loadConfig(options.config, {
50
+ useAngularJson: options.angular !== false,
51
+ angularJsonPath: options.angularJson,
52
+ projectName: options.project,
53
+ });
54
+
55
+ // Override config with command-line options
56
+ if (src) config.src = src;
57
+ if (options.threshold) config.threshold = options.threshold;
58
+ if (options.format) config.reportFormat = options.format;
59
+
60
+ // Show Angular project info if available
61
+ if (config.angular) {
62
+ console.log(chalk.cyan(`Angular Project: ${config.angular.project}`));
63
+ console.log(chalk.gray(`Prefix: ${config.angular.prefix}`));
64
+ console.log(chalk.gray(`Style: ${config.angular.stylePreprocessor}\n`));
65
+ }
66
+
67
+ console.log(chalk.gray(`Scanning: ${config.src}`));
68
+ console.log(chalk.gray(`Threshold: ${config.threshold} occurrences\n`));
69
+
70
+ // Scan files
71
+ const files = await scanScssFiles(config.src, config.ignore);
72
+ console.log(chalk.green(`āœ“ Found ${files.length} SCSS files\n`));
73
+
74
+ // Parse all files
75
+ const allExtracted = {
76
+ colors: [],
77
+ spacing: [],
78
+ fontSizes: [],
79
+ fontWeights: [],
80
+ fontFamilies: [],
81
+ borderRadius: [],
82
+ shadows: [],
83
+ zIndex: [],
84
+ sizing: [],
85
+ lineHeight: [],
86
+ opacity: [],
87
+ transitions: [],
88
+ };
89
+
90
+ files.forEach(file => {
91
+ const content = require('fs').readFileSync(file, 'utf8');
92
+ const extracted = parseScss(content, file);
93
+
94
+ Object.keys(extracted).forEach(category => {
95
+ allExtracted[category].push(...extracted[category]);
96
+ });
97
+ });
98
+
99
+ // Analyze
100
+ const analysis = analyzeValues(allExtracted, config);
101
+
102
+ // Generate report
103
+ const report = generateReport(analysis, config.reportFormat, config);
104
+ console.log(report);
105
+ } catch (error) {
106
+ console.error(chalk.red('āœ— Error:'), error.message);
107
+ process.exit(1);
108
+ }
109
+ });
110
+
111
+ // Generate command
112
+ program
113
+ .command('generate')
114
+ .description('Generate variables file only (does not modify existing SCSS files)')
115
+ .argument('[src]', 'Source directory to scan (optional if using angular.json)')
116
+ .option('--output <path>', 'Output path for variables file')
117
+ .option('--threshold <number>', 'Minimum repeat count threshold', parseInt)
118
+ .option('--config <path>', 'Path to config file')
119
+ .option('--angular-json <path>', 'Path to angular.json file')
120
+ .option('--project <name>', 'Angular project name (uses default if not specified)')
121
+ .option('--no-angular', 'Disable angular.json integration')
122
+ .action(async (src, options) => {
123
+ try {
124
+ console.log(chalk.cyan.bold('\nšŸ“ Generating SCSS Variables File\n'));
125
+
126
+ const config = loadConfig(options.config, {
127
+ useAngularJson: options.angular !== false,
128
+ angularJsonPath: options.angularJson,
129
+ projectName: options.project,
130
+ });
131
+
132
+ // Override config with command-line options
133
+ if (src) config.src = src;
134
+ if (options.output) config.output = options.output;
135
+ if (options.threshold) config.threshold = options.threshold;
136
+
137
+ // Show Angular project info if available
138
+ if (config.angular) {
139
+ console.log(chalk.cyan(`Angular Project: ${config.angular.project}`));
140
+ console.log(chalk.gray(`Prefix: ${config.angular.prefix}`));
141
+ console.log(chalk.gray(`Style: ${config.angular.stylePreprocessor}\n`));
142
+ }
143
+
144
+ console.log(chalk.gray(`Scanning: ${config.src}`));
145
+ console.log(chalk.gray(`Output: ${config.output}\n`));
146
+
147
+ // Scan files
148
+ const files = await scanScssFiles(config.src, config.ignore);
149
+ console.log(chalk.green(`āœ“ Found ${files.length} SCSS files`));
150
+
151
+ // Parse all files
152
+ const allExtracted = {
153
+ colors: [],
154
+ spacing: [],
155
+ fontSizes: [],
156
+ fontWeights: [],
157
+ fontFamilies: [],
158
+ borderRadius: [],
159
+ shadows: [],
160
+ zIndex: [],
161
+ sizing: [],
162
+ lineHeight: [],
163
+ opacity: [],
164
+ transitions: [],
165
+ };
166
+
167
+ files.forEach(file => {
168
+ const content = require('fs').readFileSync(file, 'utf8');
169
+ const extracted = parseScss(content, file);
170
+
171
+ Object.keys(extracted).forEach(category => {
172
+ allExtracted[category].push(...extracted[category]);
173
+ });
174
+ });
175
+
176
+ // Analyze
177
+ const analysis = analyzeValues(allExtracted, config);
178
+
179
+ // Generate variables file
180
+ generateVariablesFile(analysis, config.output, config);
181
+
182
+ console.log(chalk.green(`\nāœ“ Generated variables file: ${config.output}`));
183
+
184
+ // Show summary
185
+ const totalVars = Object.values(analysis).reduce((sum, arr) => sum + arr.length, 0);
186
+ console.log(chalk.bold(`\nTotal variables extracted: ${totalVars}\n`));
187
+ } catch (error) {
188
+ console.error(chalk.red('āœ— Error:'), error.message);
189
+ process.exit(1);
190
+ }
191
+ });
192
+
193
+ // Refactor command
194
+ program
195
+ .command('refactor')
196
+ .description('Full extraction + replacement (generates variables file and refactors SCSS files)')
197
+ .argument('[src]', 'Source directory to scan (optional if using angular.json)')
198
+ .option('--output <path>', 'Output path for variables file')
199
+ .option('--threshold <number>', 'Minimum repeat count threshold', parseInt)
200
+ .option('--config <path>', 'Path to config file')
201
+ .option('--angular-json <path>', 'Path to angular.json file')
202
+ .option('--project <name>', 'Angular project name (uses default if not specified)')
203
+ .option('--no-angular', 'Disable angular.json integration')
204
+ .action(async (src, options) => {
205
+ try {
206
+ console.log(chalk.cyan.bold('\nšŸ”§ SCSS Refactoring - Full Extraction\n'));
207
+
208
+ const config = loadConfig(options.config, {
209
+ useAngularJson: options.angular !== false,
210
+ angularJsonPath: options.angularJson,
211
+ projectName: options.project,
212
+ });
213
+
214
+ // Override config with command-line options
215
+ if (src) config.src = src;
216
+ if (options.output) config.output = options.output;
217
+ if (options.threshold) config.threshold = options.threshold;
218
+
219
+ // Show Angular project info if available
220
+ if (config.angular) {
221
+ console.log(chalk.cyan(`Angular Project: ${config.angular.project}`));
222
+ console.log(chalk.gray(`Prefix: ${config.angular.prefix}`));
223
+ console.log(chalk.gray(`Style: ${config.angular.stylePreprocessor}\n`));
224
+ }
225
+
226
+ console.log(chalk.gray(`Scanning: ${config.src}`));
227
+ console.log(chalk.gray(`Output: ${config.output}\n`));
228
+
229
+ // Scan files
230
+ const files = await scanScssFiles(config.src, config.ignore);
231
+ console.log(chalk.green(`āœ“ Found ${files.length} SCSS files`));
232
+
233
+ // Parse all files
234
+ const allExtracted = {
235
+ colors: [],
236
+ spacing: [],
237
+ fontSizes: [],
238
+ fontWeights: [],
239
+ fontFamilies: [],
240
+ borderRadius: [],
241
+ shadows: [],
242
+ zIndex: [],
243
+ sizing: [],
244
+ lineHeight: [],
245
+ opacity: [],
246
+ transitions: [],
247
+ };
248
+
249
+ files.forEach(file => {
250
+ const content = require('fs').readFileSync(file, 'utf8');
251
+ const extracted = parseScss(content, file);
252
+
253
+ Object.keys(extracted).forEach(category => {
254
+ allExtracted[category].push(...extracted[category]);
255
+ });
256
+ });
257
+
258
+ // Analyze
259
+ const analysis = analyzeValues(allExtracted, config);
260
+
261
+ // Generate variables file
262
+ generateVariablesFile(analysis, config.output, config);
263
+ console.log(chalk.green(`āœ“ Generated variables file: ${config.output}`));
264
+
265
+ // Refactor files
266
+ const refactoredFiles = refactorScssFiles(files, analysis, config.output, config);
267
+ console.log(chalk.green(`āœ“ Refactored ${refactoredFiles.length} SCSS files`));
268
+
269
+ // Show summary
270
+ const totalVars = Object.values(analysis).reduce((sum, arr) => sum + arr.length, 0);
271
+ console.log(chalk.bold(`\nTotal variables extracted: ${totalVars}`));
272
+ console.log(chalk.bold(`Files modified: ${refactoredFiles.length}\n`));
273
+ } catch (error) {
274
+ console.error(chalk.red('āœ— Error:'), error.message);
275
+ process.exit(1);
276
+ }
277
+ });
278
+
279
+ // Analyze Angular patterns command (ng-deep, !important)
280
+ program
281
+ .command('analyze-patterns')
282
+ .description('Analyze SCSS files for Angular Material v15+ anti-patterns (::ng-deep, !important)')
283
+ .argument('[src]', 'Source directory to scan (optional if using angular.json)')
284
+ .option('--format <format>', 'Report format (table, json, markdown)', 'table')
285
+ .option('--config <path>', 'Path to config file')
286
+ .option('--angular-json <path>', 'Path to angular.json file')
287
+ .option('--project <name>', 'Angular project name (uses default if not specified)')
288
+ .option('--no-angular', 'Disable angular.json integration')
289
+ .action(async (src, options) => {
290
+ try {
291
+ console.log(chalk.cyan.bold('\nšŸ” Angular Anti-Pattern Analysis\n'));
292
+
293
+ const config = loadConfig(options.config, {
294
+ useAngularJson: options.angular !== false,
295
+ angularJsonPath: options.angularJson,
296
+ projectName: options.project,
297
+ });
298
+ if (src) config.src = src;
299
+
300
+ // Show Angular project info if available
301
+ if (config.angular) {
302
+ console.log(chalk.cyan(`Angular Project: ${config.angular.project}`));
303
+ console.log(chalk.gray(`Prefix: ${config.angular.prefix}`));
304
+ console.log(chalk.gray(`Style: ${config.angular.stylePreprocessor}\n`));
305
+ }
306
+
307
+ console.log(chalk.gray(`Scanning: ${config.src}\n`));
308
+
309
+ // Scan files
310
+ const files = await scanScssFiles(config.src, config.ignore);
311
+ console.log(chalk.green(`āœ“ Found ${files.length} SCSS files\n`));
312
+
313
+ // Analyze patterns
314
+ const analysis = analyzeAngularPatterns(files);
315
+
316
+ // Generate report
317
+ const report = generateAngularPatternReport(analysis, options.format);
318
+ console.log(report);
319
+
320
+ // Exit with error code if issues found
321
+ if (analysis.summary.ngDeepCount > 0 || analysis.summary.importantCount > 0) {
322
+ console.log(
323
+ chalk.yellow('⚠ Anti-patterns detected. Run "modernize" command to fix them.\n')
324
+ );
325
+ } else {
326
+ console.log(chalk.green('āœ“ No anti-patterns detected!\n'));
327
+ }
328
+ } catch (error) {
329
+ console.error(chalk.red('āœ— Error:'), error.message);
330
+ process.exit(1);
331
+ }
332
+ });
333
+
334
+ // Modernize command (refactor ng-deep and !important)
335
+ program
336
+ .command('modernize')
337
+ .description('Refactor SCSS files following Angular Material v15+ best practices')
338
+ .argument('[src]', 'Source directory to scan (optional if using angular.json)')
339
+ .option('--global-styles <path>', 'Path to global styles file', './src/styles.scss')
340
+ .option('--no-ng-deep', 'Skip ::ng-deep refactoring')
341
+ .option('--no-important', 'Skip !important refactoring')
342
+ .option('--dry-run', 'Preview changes without modifying files')
343
+ .option('--config <path>', 'Path to config file')
344
+ .option('--angular-json <path>', 'Path to angular.json file')
345
+ .option('--project <name>', 'Angular project name (uses default if not specified)')
346
+ .option('--no-angular', 'Disable angular.json integration')
347
+ .action(async (src, options) => {
348
+ try {
349
+ console.log(
350
+ chalk.cyan.bold('\nšŸ”§ Modernizing SCSS Files (Angular Material v15+ Best Practices)\n')
351
+ );
352
+
353
+ const config = loadConfig(options.config, {
354
+ useAngularJson: options.angular !== false,
355
+ angularJsonPath: options.angularJson,
356
+ projectName: options.project,
357
+ });
358
+ if (src) config.src = src;
359
+
360
+ if (options.dryRun) {
361
+ console.log(chalk.yellow('DRY RUN MODE - No files will be modified\n'));
362
+ }
363
+
364
+ // Show Angular project info if available
365
+ if (config.angular) {
366
+ console.log(chalk.cyan(`Angular Project: ${config.angular.project}`));
367
+ console.log(chalk.gray(`Prefix: ${config.angular.prefix}`));
368
+ console.log(chalk.gray(`Style: ${config.angular.stylePreprocessor}\n`));
369
+ }
370
+
371
+ console.log(chalk.gray(`Scanning: ${config.src}`));
372
+ console.log(chalk.gray(`Global styles: ${options.globalStyles}\n`));
373
+
374
+ // Scan files
375
+ const files = await scanScssFiles(config.src, config.ignore);
376
+ console.log(chalk.green(`āœ“ Found ${files.length} SCSS files\n`));
377
+
378
+ // Refactor patterns
379
+ const results = refactorAngularPatterns(files, {
380
+ removeNgDeep: options.ngDeep !== false,
381
+ removeImportant: options.important !== false,
382
+ createGlobalStyles: true,
383
+ globalStylesPath: options.globalStyles,
384
+ dryRun: options.dryRun,
385
+ });
386
+
387
+ // Show results
388
+ if (results.modified.length > 0) {
389
+ console.log(chalk.green(`āœ“ Modified ${results.modified.length} file(s)\n`));
390
+
391
+ results.modified.forEach(file => {
392
+ console.log(chalk.white(` ${path.basename(file.file)}:`));
393
+ file.changes.forEach(change => {
394
+ if (change.type === 'ng-deep-removed') {
395
+ console.log(chalk.yellow(` - Removed ${change.count} ::ng-deep occurrence(s)`));
396
+ } else if (change.type === 'important-removed') {
397
+ console.log(chalk.yellow(` - Removed ${change.count} !important declaration(s)`));
398
+ }
399
+ });
400
+ });
401
+ console.log();
402
+ }
403
+
404
+ if (results.globalStyles.length > 0) {
405
+ console.log(
406
+ chalk.cyan(
407
+ `āœ“ Extracted ${results.globalStyles.length} style(s) to ${options.globalStyles}\n`
408
+ )
409
+ );
410
+ }
411
+
412
+ if (results.warnings.length > 0) {
413
+ console.log(chalk.yellow.bold('⚠ Warnings:\n'));
414
+ results.warnings.forEach(warning => {
415
+ console.log(chalk.yellow(` ${warning.message}`));
416
+ });
417
+ console.log();
418
+ }
419
+
420
+ if (results.modified.length === 0) {
421
+ console.log(chalk.green('āœ“ No changes needed - files already follow best practices!\n'));
422
+ } else if (!options.dryRun) {
423
+ console.log(chalk.bold.green('āœ“ Modernization complete!\n'));
424
+ console.log(chalk.gray('Next steps:'));
425
+ console.log(chalk.gray(' 1. Review the modified files'));
426
+ console.log(chalk.gray(' 2. Test your application'));
427
+ console.log(
428
+ chalk.gray(' 3. Consider using Angular Material theme mixins for component theming')
429
+ );
430
+ console.log(chalk.gray(' 4. Review extracted global styles and organize as needed\n'));
431
+ }
432
+ } catch (error) {
433
+ console.error(chalk.red('āœ— Error:'), error.message);
434
+ process.exit(1);
435
+ }
436
+ });
437
+
438
+ // Detect Bootstrap command
439
+ program
440
+ .command('detect-bootstrap')
441
+ .description('Detect Bootstrap classes and analyze migration to Angular Material')
442
+ .argument('[src]', 'Source directory to scan (optional if using angular.json)')
443
+ .option('--format <format>', 'Report format (table, json, markdown)', 'table')
444
+ .option('--config <path>', 'Path to config file')
445
+ .option('--angular-json <path>', 'Path to angular.json file')
446
+ .option('--project <name>', 'Angular project name (uses default if not specified)')
447
+ .option('--no-angular', 'Disable angular.json integration')
448
+ .action(async (src, options) => {
449
+ try {
450
+ console.log(chalk.cyan.bold('\nšŸ“¦ Bootstrap to Material Migration Analysis\n'));
451
+
452
+ const config = loadConfig(options.config, {
453
+ useAngularJson: options.angular !== false,
454
+ angularJsonPath: options.angularJson,
455
+ projectName: options.project,
456
+ });
457
+ if (src) config.src = src;
458
+
459
+ // Show Angular project info if available
460
+ if (config.angular) {
461
+ console.log(chalk.cyan(`Angular Project: ${config.angular.project}`));
462
+ console.log(chalk.gray(`Prefix: ${config.angular.prefix}`));
463
+ console.log(chalk.gray(`Style: ${config.angular.stylePreprocessor}\n`));
464
+ }
465
+
466
+ console.log(chalk.gray(`Scanning: ${config.src}\n`));
467
+
468
+ // Scan files - look for SCSS, HTML, and TS files
469
+ const allFiles = await scanTemplateFiles(config.src, config.ignore);
470
+ console.log(chalk.green(`āœ“ Found ${allFiles.length} files (SCSS, HTML, TS)\n`));
471
+
472
+ // Detect Bootstrap
473
+ const detection = detectBootstrap(allFiles);
474
+
475
+ // Generate report
476
+ const report = generateBootstrapReport(detection, options.format);
477
+ console.log(report);
478
+
479
+ // Provide migration guidance
480
+ if (detection.summary.totalBootstrapClasses > 0) {
481
+ console.log(
482
+ chalk.yellow(
483
+ '⚠ Bootstrap classes detected. Use "migrate-bootstrap" command to convert.\n'
484
+ )
485
+ );
486
+ console.log(chalk.gray('Migration options:'));
487
+ console.log(chalk.gray(' • Components (btn, card, etc.) → Angular Material components'));
488
+ console.log(chalk.gray(' • Utilities (d-flex, spacing, etc.) → Custom utility classes'));
489
+ console.log(chalk.gray(' • Grid system → CSS Grid or Flexbox\n'));
490
+ } else {
491
+ console.log(chalk.green('āœ“ No Bootstrap classes detected!\n'));
492
+ }
493
+ } catch (error) {
494
+ console.error(chalk.red('āœ— Error:'), error.message);
495
+ process.exit(1);
496
+ }
497
+ });
498
+
499
+ // Migrate Bootstrap command
500
+ program
501
+ .command('migrate-bootstrap')
502
+ .description('Migrate Bootstrap classes to Angular Material MDC-based styles')
503
+ .argument('[src]', 'Source directory to scan (optional if using angular.json)')
504
+ .option(
505
+ '--custom-utilities <path>',
506
+ 'Path to custom utilities file',
507
+ './src/styles/utilities.scss'
508
+ )
509
+ .option('--no-custom-utilities', 'Skip creating custom utility classes')
510
+ .option('--no-remove-imports', 'Keep Bootstrap imports')
511
+ .option('--dry-run', 'Preview changes without modifying files')
512
+ .option('--config <path>', 'Path to config file')
513
+ .option('--angular-json <path>', 'Path to angular.json file')
514
+ .option('--project <name>', 'Angular project name (uses default if not specified)')
515
+ .option('--no-angular', 'Disable angular.json integration')
516
+ .action(async (src, options) => {
517
+ try {
518
+ console.log(chalk.cyan.bold('\nšŸ”„ Migrating Bootstrap to Angular Material\n'));
519
+
520
+ const config = loadConfig(options.config, {
521
+ useAngularJson: options.angular !== false,
522
+ angularJsonPath: options.angularJson,
523
+ projectName: options.project,
524
+ });
525
+ if (src) config.src = src;
526
+
527
+ if (options.dryRun) {
528
+ console.log(chalk.yellow('DRY RUN MODE - No files will be modified\n'));
529
+ }
530
+
531
+ // Show Angular project info if available
532
+ if (config.angular) {
533
+ console.log(chalk.cyan(`Angular Project: ${config.angular.project}`));
534
+ console.log(chalk.gray(`Prefix: ${config.angular.prefix}`));
535
+ console.log(chalk.gray(`Style: ${config.angular.stylePreprocessor}\n`));
536
+ }
537
+
538
+ console.log(chalk.gray(`Scanning: ${config.src}`));
539
+ console.log(
540
+ chalk.gray(
541
+ `Utilities output: ${options.customUtilities || './src/styles/utilities.scss'}\n`
542
+ )
543
+ );
544
+
545
+ // Scan files - look for SCSS, HTML, and TS files
546
+ const allFiles = await scanTemplateFiles(config.src, config.ignore);
547
+ console.log(chalk.green(`āœ“ Found ${allFiles.length} files (SCSS, HTML, TS)\n`));
548
+
549
+ // Migrate Bootstrap
550
+ const results = migrateBootstrapToMaterial(allFiles, {
551
+ createCustomUtilities: options.customUtilities !== false,
552
+ customUtilitiesPath: options.customUtilities || './src/styles/utilities.scss',
553
+ removeBootstrapImports: options.removeImports !== false,
554
+ dryRun: options.dryRun,
555
+ });
556
+
557
+ // Show results
558
+ if (results.modified.length > 0) {
559
+ console.log(chalk.green(`āœ“ Modified ${results.modified.length} file(s)\n`));
560
+
561
+ results.modified.slice(0, 10).forEach(file => {
562
+ console.log(chalk.white(` ${path.basename(file.file)}:`));
563
+ file.changes.forEach(change => {
564
+ if (change.type === 'import-removed') {
565
+ console.log(chalk.yellow(` - ${change.description}`));
566
+ }
567
+ });
568
+ });
569
+ if (results.modified.length > 10) {
570
+ console.log(chalk.gray(` ... and ${results.modified.length - 10} more`));
571
+ }
572
+ console.log();
573
+ }
574
+
575
+ if (results.customUtilities.length > 0) {
576
+ console.log(
577
+ chalk.cyan(
578
+ `āœ“ Generated ${results.customUtilities.length} custom utility class(es) in ${options.customUtilities || './src/styles/utilities.scss'}\n`
579
+ )
580
+ );
581
+ }
582
+
583
+ if (results.componentMigrations.length > 0) {
584
+ console.log(
585
+ chalk.yellow(
586
+ `⚠ ${results.componentMigrations.length} component(s) require manual migration\n`
587
+ )
588
+ );
589
+ }
590
+
591
+ if (results.warnings.length > 0) {
592
+ console.log(chalk.yellow.bold('⚠ Warnings:\n'));
593
+ results.warnings.slice(0, 5).forEach(warning => {
594
+ console.log(chalk.yellow(` ${warning.message}`));
595
+ if (warning.variables) {
596
+ console.log(chalk.gray(` Variables: ${warning.variables.join(', ')}`));
597
+ }
598
+ });
599
+ if (results.warnings.length > 5) {
600
+ console.log(chalk.gray(` ... and ${results.warnings.length - 5} more warnings`));
601
+ }
602
+ console.log();
603
+ }
604
+
605
+ if (results.modified.length === 0 && results.warnings.length === 0) {
606
+ console.log(chalk.green('āœ“ No Bootstrap code detected!\n'));
607
+ } else if (!options.dryRun) {
608
+ console.log(chalk.bold.green('āœ“ Migration complete!\n'));
609
+ console.log(chalk.gray('Next steps:'));
610
+ console.log(chalk.gray(' 1. Review the modified SCSS files'));
611
+ console.log(chalk.gray(' 2. Update HTML templates to use Angular Material components'));
612
+ console.log(chalk.gray(' 3. Import custom utilities in your styles.scss'));
613
+ console.log(chalk.gray(' 4. Install @angular/material if not already installed'));
614
+ console.log(chalk.gray(' 5. Test your application thoroughly\n'));
615
+ console.log(chalk.cyan('Component Migration Guide:'));
616
+ console.log(chalk.gray(' • .btn → <button mat-button>'));
617
+ console.log(chalk.gray(' • .card → <mat-card>'));
618
+ console.log(
619
+ chalk.gray(' • .form-control → <mat-form-field><input matInput></mat-form-field>')
620
+ );
621
+ console.log(chalk.gray(' • See: https://material.angular.io/components\n'));
622
+ }
623
+ } catch (error) {
624
+ console.error(chalk.red('āœ— Error:'), error.message);
625
+ process.exit(1);
626
+ }
627
+ });
628
+
629
+ // Analyze style organization command
630
+ program
631
+ .command('analyze-organization')
632
+ .description('Analyze style organization and get recommendations for best practices')
633
+ .argument('[src]', 'Source directory to scan (optional if using angular.json)')
634
+ .option('--format <format>', 'Report format (table, json, markdown)', 'table')
635
+ .option('--config <path>', 'Path to config file')
636
+ .option('--angular-json <path>', 'Path to angular.json file')
637
+ .option('--project <name>', 'Angular project name (uses default if not specified)')
638
+ .option('--no-angular', 'Disable angular.json integration')
639
+ .action(async (src, options) => {
640
+ try {
641
+ console.log(chalk.cyan.bold('\nšŸ“‹ Style Organization Analysis\n'));
642
+
643
+ const config = loadConfig(options.config, {
644
+ useAngularJson: options.angular !== false,
645
+ angularJsonPath: options.angularJson,
646
+ projectName: options.project,
647
+ });
648
+ if (src) config.src = src;
649
+
650
+ // Show Angular project info if available
651
+ if (config.angular) {
652
+ console.log(chalk.cyan(`Angular Project: ${config.angular.project}`));
653
+ console.log(chalk.gray(`Prefix: ${config.angular.prefix}`));
654
+ console.log(chalk.gray(`Style: ${config.angular.stylePreprocessor}\n`));
655
+ }
656
+
657
+ console.log(chalk.gray(`Scanning: ${config.src}\n`));
658
+
659
+ // Scan files
660
+ const files = await scanScssFiles(config.src, config.ignore);
661
+ console.log(chalk.green(`āœ“ Found ${files.length} SCSS files\n`));
662
+
663
+ // Analyze organization
664
+ const analysis = analyzeStyleOrganization(files, config);
665
+
666
+ // Generate report
667
+ const report = generateOrganizationReport(analysis, options.format);
668
+ console.log(report);
669
+
670
+ // Provide guidance
671
+ if (analysis.recommendations.length > 0) {
672
+ console.log(chalk.yellow.bold('šŸ’” Next Steps:\n'));
673
+ console.log(chalk.gray('1. Review recommendations above'));
674
+ console.log(chalk.gray('2. Run "refactor" command to extract variables'));
675
+ console.log(chalk.gray('3. Run "modernize" to remove anti-patterns'));
676
+ console.log(chalk.gray('4. Consider restructuring based on suggested organization'));
677
+ console.log(chalk.gray('5. Use @use instead of @import for better modularity\n'));
678
+ } else {
679
+ console.log(chalk.green('āœ“ Your styles are well organized!\n'));
680
+ }
681
+ } catch (error) {
682
+ console.error(chalk.red('āœ— Error:'), error.message);
683
+ process.exit(1);
684
+ }
685
+ });
686
+
687
+ // Generate theme structure command
688
+ program
689
+ .command('generate-themes')
690
+ .description('Generate theme structure for dark/light mode support with Angular Material')
691
+ .argument('[src]', 'Source directory to scan for analysis (optional)')
692
+ .option('--output <dir>', 'Output directory for theme files', './src/styles')
693
+ .option('--analyze', 'Analyze existing styles for theme readiness')
694
+ .option('--format <format>', 'Report format (table, json, markdown)', 'table')
695
+ .action(async (src, options) => {
696
+ try {
697
+ console.log(chalk.cyan.bold('\nšŸŽØ Theme Structure Generator\n'));
698
+
699
+ const fs = require('fs');
700
+ const outputDir = path.resolve(options.output);
701
+
702
+ // Analyze existing styles if requested
703
+ if (options.analyze && src) {
704
+ console.log(chalk.gray(`Analyzing: ${src}\n`));
705
+ const config = loadConfig();
706
+ const files = await scanScssFiles(src, config.ignore);
707
+ const analysis = analyzeThemeReadiness(files);
708
+
709
+ console.log(chalk.blue.bold('Theme Readiness Analysis:\n'));
710
+ console.log(chalk.gray(`Total hardcoded colors: ${analysis.hardcodedColors.length}`));
711
+ console.log(chalk.gray(`Unique colors: ${analysis.colorUsage.size}`));
712
+ console.log(
713
+ chalk.gray(`Components needing theme mixins: ${analysis.themeableComponents.length}\n`)
714
+ );
715
+
716
+ if (analysis.recommendations.length > 0) {
717
+ console.log(chalk.yellow.bold('šŸ“‹ Recommendations:\n'));
718
+ analysis.recommendations.forEach(rec => {
719
+ const icon = rec.priority === 'high' ? 'šŸ”“' : '🟔';
720
+ console.log(chalk.gray(`${icon} ${rec.message}`));
721
+ });
722
+ console.log();
723
+ }
724
+ }
725
+
726
+ // Generate theme files
727
+ console.log(chalk.gray(`Generating theme files in: ${outputDir}\n`));
728
+
729
+ const themeStructure = generateThemeStructure({
730
+ outputDir,
731
+ includeComponents: true,
732
+ });
733
+
734
+ // Create output directory if it doesn't exist
735
+ if (!fs.existsSync(outputDir)) {
736
+ fs.mkdirSync(outputDir, { recursive: true });
737
+ }
738
+
739
+ // Write theme files
740
+ const filesToWrite = {
741
+ '_theme-base.scss': themeStructure.files.base,
742
+ '_theme-light.scss': themeStructure.files.lightTheme,
743
+ '_theme-dark.scss': themeStructure.files.darkTheme,
744
+ 'themes.scss': themeStructure.files.themeLoader,
745
+ '_css-variables.scss': themeStructure.files.cssVariables,
746
+ };
747
+
748
+ if (themeStructure.files.componentThemes) {
749
+ filesToWrite['_component-theme-mixin.scss'] = themeStructure.files.componentThemes;
750
+ }
751
+
752
+ Object.entries(filesToWrite).forEach(([filename, content]) => {
753
+ const filepath = path.join(outputDir, filename);
754
+ fs.writeFileSync(filepath, content, 'utf8');
755
+ console.log(chalk.green(`āœ“ Created ${filename}`));
756
+ });
757
+
758
+ console.log();
759
+ console.log(chalk.green.bold('āœ“ Theme structure generated!\n'));
760
+
761
+ // Show recommendations
762
+ console.log(chalk.blue.bold('šŸ“– Usage Guide:\n'));
763
+ console.log(chalk.gray('1. Import themes.scss in your styles.scss:'));
764
+ console.log(chalk.cyan(" @use './styles/themes';"));
765
+ console.log();
766
+ console.log(chalk.gray('2. Add theme class to your app component:'));
767
+ console.log(chalk.cyan(' <div [class.dark-theme]="isDarkMode">'));
768
+ console.log(chalk.cyan(' <router-outlet></router-outlet>'));
769
+ console.log(chalk.cyan(' </div>'));
770
+ console.log();
771
+ console.log(chalk.gray('3. Toggle theme in your component:'));
772
+ console.log(chalk.cyan(' isDarkMode = false;'));
773
+ console.log(chalk.cyan(' toggleTheme() {'));
774
+ console.log(chalk.cyan(' this.isDarkMode = !this.isDarkMode;'));
775
+ console.log(chalk.cyan(' }'));
776
+ console.log();
777
+ console.log(chalk.gray('4. Use theme variables in components:'));
778
+ console.log(chalk.cyan(" @use '../../styles/theme-base' as base;"));
779
+ console.log(chalk.cyan(' .my-element {'));
780
+ console.log(chalk.cyan(' padding: base.$spacing-md;'));
781
+ console.log(chalk.cyan(' }'));
782
+ console.log();
783
+
784
+ // Show recommendations
785
+ console.log(chalk.yellow.bold('šŸ’” Best Practices:\n'));
786
+ themeStructure.recommendations.forEach(rec => {
787
+ const icon = rec.priority === 'high' ? 'šŸ”“' : rec.priority === 'medium' ? '🟔' : '🟢';
788
+ console.log(chalk.gray(`${icon} [${rec.category}] ${rec.recommendation}`));
789
+ });
790
+ console.log();
791
+
792
+ // Show structure
793
+ console.log(chalk.blue.bold('šŸ“ Recommended Directory Structure:\n'));
794
+ Object.entries(themeStructure.structure).forEach(([dir, files]) => {
795
+ console.log(chalk.cyan(dir));
796
+ Object.entries(files).forEach(([file, desc]) => {
797
+ console.log(chalk.gray(` ā”œā”€ ${file} - ${desc}`));
798
+ });
799
+ });
800
+ console.log();
801
+ } catch (error) {
802
+ console.error(chalk.red('āœ— Error:'), error.message);
803
+ process.exit(1);
804
+ }
805
+ });
806
+
807
+ program.parse();