create-qa-architect 5.0.7 → 5.4.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.
Files changed (45) hide show
  1. package/.github/workflows/auto-release.yml +49 -0
  2. package/.github/workflows/quality.yml +11 -11
  3. package/.github/workflows/shell-ci.yml.example +82 -0
  4. package/.github/workflows/shell-quality.yml.example +148 -0
  5. package/README.md +165 -12
  6. package/config/shell-ci.yml +82 -0
  7. package/config/shell-quality.yml +148 -0
  8. package/docs/ADOPTION-SUMMARY.md +41 -0
  9. package/docs/ARCHITECTURE-REVIEW.md +67 -0
  10. package/docs/ARCHITECTURE.md +29 -45
  11. package/docs/CI-COST-ANALYSIS.md +323 -0
  12. package/docs/CODE-REVIEW.md +100 -0
  13. package/docs/REQUIREMENTS.md +148 -0
  14. package/docs/SECURITY-AUDIT.md +68 -0
  15. package/docs/test-trace-matrix.md +28 -0
  16. package/eslint.config.cjs +2 -0
  17. package/lib/commands/analyze-ci.js +616 -0
  18. package/lib/commands/deps.js +293 -0
  19. package/lib/commands/index.js +29 -0
  20. package/lib/commands/validate.js +85 -0
  21. package/lib/config-validator.js +28 -45
  22. package/lib/error-reporter.js +14 -2
  23. package/lib/github-api.js +138 -13
  24. package/lib/license-signing.js +125 -0
  25. package/lib/license-validator.js +359 -71
  26. package/lib/licensing.js +434 -106
  27. package/lib/package-utils.js +9 -9
  28. package/lib/prelaunch-validator.js +828 -0
  29. package/lib/project-maturity.js +58 -6
  30. package/lib/quality-tools-generator.js +495 -0
  31. package/lib/result-types.js +112 -0
  32. package/lib/security-enhancements.js +1 -1
  33. package/lib/smart-strategy-generator.js +46 -10
  34. package/lib/telemetry.js +1 -1
  35. package/lib/template-loader.js +52 -19
  36. package/lib/ui-helpers.js +1 -1
  37. package/lib/validation/cache-manager.js +36 -6
  38. package/lib/validation/config-security.js +100 -33
  39. package/lib/validation/index.js +68 -97
  40. package/lib/validation/workflow-validation.js +28 -7
  41. package/package.json +4 -6
  42. package/scripts/check-test-coverage.sh +46 -0
  43. package/scripts/validate-claude-md.js +80 -0
  44. package/setup.js +923 -301
  45. package/create-saas-monetization.js +0 -1513
@@ -0,0 +1,616 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * GitHub Actions Cost Analyzer
5
+ *
6
+ * Analyzes GitHub Actions usage patterns and provides cost optimization recommendations.
7
+ * Pro feature that helps developers avoid unexpected CI/CD bills.
8
+ */
9
+
10
+ const fs = require('fs')
11
+ const path = require('path')
12
+ const { execSync } = require('child_process')
13
+ const yaml = require('js-yaml')
14
+ const { showProgress } = require('../ui-helpers')
15
+
16
+ /**
17
+ * Discover all GitHub Actions workflow files in the project
18
+ * @param {string} projectPath - Root path of the project
19
+ * @returns {{name: string, path: string}[]} Array of workflow files
20
+ */
21
+ function discoverWorkflows(projectPath) {
22
+ const workflowDir = path.join(projectPath, '.github', 'workflows')
23
+
24
+ if (!fs.existsSync(workflowDir)) {
25
+ return []
26
+ }
27
+
28
+ const files = fs.readdirSync(workflowDir)
29
+ return files
30
+ .filter(file => file.endsWith('.yml') || file.endsWith('.yaml'))
31
+ .map(file => ({
32
+ name: file,
33
+ path: path.join(workflowDir, file),
34
+ }))
35
+ }
36
+
37
+ /**
38
+ * Estimate workflow duration based on job steps
39
+ * @param {object} workflow - Parsed YAML workflow object
40
+ * @returns {number} Estimated duration in minutes
41
+ */
42
+ function estimateWorkflowDuration(workflow) {
43
+ if (!workflow.jobs) {
44
+ return 0
45
+ }
46
+
47
+ let totalMinutes = 0
48
+
49
+ for (const [_jobName, job] of Object.entries(workflow.jobs)) {
50
+ // Default job duration estimate: 5 minutes
51
+ let jobMinutes = 5
52
+
53
+ if (job.steps && Array.isArray(job.steps)) {
54
+ // Estimate based on known operations
55
+ for (const step of job.steps) {
56
+ // Check for expensive operations
57
+ if (step.name) {
58
+ const stepName = step.name.toLowerCase()
59
+
60
+ // Known expensive operations
61
+ if (stepName.includes('test') || stepName.includes('e2e')) {
62
+ jobMinutes += 10 // Tests typically take longer
63
+ } else if (
64
+ stepName.includes('build') ||
65
+ stepName.includes('compile')
66
+ ) {
67
+ jobMinutes += 5
68
+ } else if (
69
+ stepName.includes('deploy') ||
70
+ stepName.includes('publish')
71
+ ) {
72
+ jobMinutes += 3
73
+ } else if (
74
+ stepName.includes('install') ||
75
+ stepName.includes('setup')
76
+ ) {
77
+ jobMinutes += 2
78
+ } else {
79
+ jobMinutes += 1 // Generic step
80
+ }
81
+ } else {
82
+ jobMinutes += 1 // Generic step without name
83
+ }
84
+ }
85
+
86
+ // Cap individual job at reasonable limits
87
+ jobMinutes = Math.min(jobMinutes, 60) // Max 60 min per job
88
+ }
89
+
90
+ // Check for matrix strategy (multiplies job runs)
91
+ if (job.strategy && job.strategy.matrix) {
92
+ const matrixSize = calculateMatrixSize(job.strategy.matrix)
93
+ jobMinutes *= matrixSize
94
+ }
95
+
96
+ totalMinutes += jobMinutes
97
+ }
98
+
99
+ return Math.ceil(totalMinutes)
100
+ }
101
+
102
+ /**
103
+ * Calculate the size of a GitHub Actions matrix strategy
104
+ * @param {object} matrix - Matrix configuration
105
+ * @returns {number} Number of matrix combinations
106
+ */
107
+ function calculateMatrixSize(matrix) {
108
+ let size = 1
109
+
110
+ for (const [_key, values] of Object.entries(matrix)) {
111
+ if (Array.isArray(values)) {
112
+ size *= values.length
113
+ }
114
+ }
115
+
116
+ return size
117
+ }
118
+
119
+ /**
120
+ * Get commit frequency from git log
121
+ * @param {string} projectPath - Root path of the project
122
+ * @param {number} days - Number of days to analyze (default: 30)
123
+ * @returns {{commitsPerDay: number, totalCommits: number}} Commit frequency stats
124
+ */
125
+ function getCommitFrequency(projectPath, days = 30) {
126
+ try {
127
+ // Safe: No user input, hardcoded git command
128
+ const gitLog = execSync('git log --oneline --since="30 days ago" --all', {
129
+ cwd: projectPath,
130
+ encoding: 'utf8',
131
+ stdio: ['pipe', 'pipe', 'ignore'], // Suppress stderr
132
+ }).trim()
133
+
134
+ if (!gitLog) {
135
+ return { commitsPerDay: 0, totalCommits: 0 }
136
+ }
137
+
138
+ const totalCommits = gitLog.split('\n').length
139
+ const commitsPerDay = totalCommits / days
140
+
141
+ return {
142
+ commitsPerDay: Math.max(commitsPerDay, 0.5), // Min 0.5 commits/day
143
+ totalCommits,
144
+ }
145
+ } catch (_error) {
146
+ // Not a git repo or no commits
147
+ return { commitsPerDay: 1, totalCommits: 0 } // Assume 1 commit/day
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Calculate monthly CI costs based on workflow usage
153
+ * @param {Array} workflows - Array of workflow analysis results
154
+ * @param {number} commitsPerDay - Average commits per day
155
+ * @returns {object} Cost breakdown and recommendations
156
+ */
157
+ function calculateMonthlyCosts(workflows, commitsPerDay) {
158
+ const minutesPerWorkflow = workflows.reduce(
159
+ (total, wf) => total + wf.estimatedDuration,
160
+ 0
161
+ )
162
+ const workflowRunsPerDay = commitsPerDay * workflows.length
163
+ const minutesPerDay = minutesPerWorkflow * commitsPerDay
164
+ const minutesPerMonth = Math.ceil(minutesPerDay * 30)
165
+
166
+ // GitHub Actions pricing (as of 2024)
167
+ const FREE_TIER_MINUTES = 2000 // Free tier monthly limit
168
+ const TEAM_TIER_MINUTES = 3000 // Team tier monthly limit
169
+ const COST_PER_MINUTE = 0.008 // $0.008/min for private repos
170
+
171
+ const freeOverage = Math.max(0, minutesPerMonth - FREE_TIER_MINUTES)
172
+ const teamOverage = Math.max(0, minutesPerMonth - TEAM_TIER_MINUTES)
173
+
174
+ const freeOverageCost = freeOverage * COST_PER_MINUTE
175
+ const teamOverageCost = teamOverage * COST_PER_MINUTE
176
+
177
+ return {
178
+ minutesPerMonth,
179
+ minutesPerDay,
180
+ workflowRunsPerDay,
181
+ breakdown: workflows.map(wf => ({
182
+ name: wf.name,
183
+ minutesPerRun: wf.estimatedDuration,
184
+ runsPerMonth: Math.ceil(commitsPerDay * 30),
185
+ minutesPerMonth: Math.ceil(wf.estimatedDuration * commitsPerDay * 30),
186
+ })),
187
+ tiers: {
188
+ free: {
189
+ limit: FREE_TIER_MINUTES,
190
+ overage: freeOverage,
191
+ cost: freeOverageCost,
192
+ withinLimit: minutesPerMonth <= FREE_TIER_MINUTES,
193
+ },
194
+ team: {
195
+ limit: TEAM_TIER_MINUTES,
196
+ overage: teamOverage,
197
+ cost: teamOverageCost,
198
+ withinLimit: minutesPerMonth <= TEAM_TIER_MINUTES,
199
+ monthlyCost: 4, // $4/user/month
200
+ },
201
+ },
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Analyze workflows for optimization opportunities
207
+ * @param {Array} workflows - Array of parsed workflow objects
208
+ * @param {number} commitsPerDay - Average commits per day
209
+ * @returns {Array} Array of optimization recommendations
210
+ */
211
+ function analyzeOptimizations(workflows, commitsPerDay) {
212
+ const recommendations = []
213
+
214
+ for (const wf of workflows) {
215
+ const workflow = wf.parsed
216
+ const workflowName = wf.name
217
+
218
+ if (!workflow.jobs) continue
219
+
220
+ // Check each job for optimization opportunities
221
+ for (const [jobName, job] of Object.entries(workflow.jobs)) {
222
+ // 1. Detect missing caching
223
+ const hasSteps = job.steps && Array.isArray(job.steps)
224
+ if (hasSteps) {
225
+ const hasCaching = job.steps.some(
226
+ step =>
227
+ step.uses &&
228
+ (step.uses.includes('actions/cache') ||
229
+ step.uses.includes('actions/setup-node'))
230
+ )
231
+ const hasInstall = job.steps.some(
232
+ step =>
233
+ step.run &&
234
+ (step.run.includes('npm install') ||
235
+ step.run.includes('yarn install') ||
236
+ step.run.includes('pnpm install') ||
237
+ step.run.includes('pip install'))
238
+ )
239
+
240
+ if (hasInstall && !hasCaching) {
241
+ // Estimate 2-5 min savings per run
242
+ const savingsPerRun = 3
243
+ const savingsPerMonth = Math.ceil(savingsPerRun * commitsPerDay * 30)
244
+
245
+ recommendations.push({
246
+ type: 'caching',
247
+ workflow: workflowName,
248
+ job: jobName,
249
+ title: 'Add dependency caching',
250
+ description: `Job "${jobName}" installs dependencies but doesn't cache them`,
251
+ action: 'Add actions/cache before install step',
252
+ potentialSavings: savingsPerMonth,
253
+ savingsPerRun,
254
+ priority: 'high',
255
+ })
256
+ }
257
+ }
258
+
259
+ // 2. Detect oversized matrix strategies
260
+ if (job.strategy && job.strategy.matrix) {
261
+ const matrixSize = calculateMatrixSize(job.strategy.matrix)
262
+ if (matrixSize >= 6) {
263
+ // Suggest reducing by 50%
264
+ const currentMinutes = wf.estimatedDuration
265
+ const reductionFactor = 0.5
266
+ const savingsPerMonth = Math.ceil(
267
+ currentMinutes * reductionFactor * commitsPerDay * 30
268
+ )
269
+
270
+ recommendations.push({
271
+ type: 'matrix',
272
+ workflow: workflowName,
273
+ job: jobName,
274
+ title: 'Reduce matrix size',
275
+ description: `Job "${jobName}" runs ${matrixSize} matrix combinations`,
276
+ action: `Consider testing only LTS + latest versions (reduce to ${Math.ceil(matrixSize / 2)} combinations)`,
277
+ potentialSavings: savingsPerMonth,
278
+ savingsPerRun: Math.ceil(currentMinutes * reductionFactor),
279
+ priority: matrixSize >= 9 ? 'high' : 'medium',
280
+ })
281
+ }
282
+ }
283
+ }
284
+
285
+ // 3. Detect high-frequency scheduled workflows
286
+ if (workflow.on) {
287
+ const triggers = Array.isArray(workflow.on) ? workflow.on : [workflow.on]
288
+ const hasSchedule =
289
+ triggers.includes('schedule') ||
290
+ (typeof workflow.on === 'object' && workflow.on.schedule)
291
+
292
+ if (hasSchedule && workflowName.includes('nightly')) {
293
+ const currentRuns = 30 // Daily = 30 runs/month
294
+ const proposedRuns = 4 // Weekly = 4 runs/month
295
+ const savingsPerMonth = Math.ceil(
296
+ wf.estimatedDuration * (currentRuns - proposedRuns)
297
+ )
298
+
299
+ recommendations.push({
300
+ type: 'frequency',
301
+ workflow: workflowName,
302
+ title: 'Reduce schedule frequency',
303
+ description: `"${workflowName}" runs nightly (30x/month)`,
304
+ action: 'Change to weekly schedule (4x/month)',
305
+ potentialSavings: savingsPerMonth,
306
+ savingsPerRun: 0,
307
+ priority: savingsPerMonth > 500 ? 'high' : 'medium',
308
+ })
309
+ }
310
+
311
+ if (hasSchedule && workflowName.includes('weekly')) {
312
+ const currentRuns = 4 // Weekly = 4 runs/month
313
+ const proposedRuns = 1 // Monthly = 1 run/month
314
+ const savingsPerMonth = Math.ceil(
315
+ wf.estimatedDuration * (currentRuns - proposedRuns)
316
+ )
317
+
318
+ if (savingsPerMonth > 50) {
319
+ recommendations.push({
320
+ type: 'frequency',
321
+ workflow: workflowName,
322
+ title: 'Reduce schedule frequency',
323
+ description: `"${workflowName}" runs weekly (4x/month)`,
324
+ action: 'Change to monthly schedule (1x/month)',
325
+ potentialSavings: savingsPerMonth,
326
+ savingsPerRun: 0,
327
+ priority: 'low',
328
+ })
329
+ }
330
+ }
331
+ }
332
+
333
+ // 4. Detect missing path filters
334
+ if (workflow.on && typeof workflow.on === 'object') {
335
+ const hasPush = workflow.on.push || workflow.on.pull_request
336
+ const hasPathFilter =
337
+ (workflow.on.push && workflow.on.push.paths) ||
338
+ (workflow.on.pull_request && workflow.on.pull_request.paths)
339
+
340
+ if (hasPush && !hasPathFilter && !workflowName.includes('release')) {
341
+ // Estimate 20% of commits are docs-only
342
+ const wastedRuns = commitsPerDay * 0.2 * 30
343
+ const savingsPerMonth = Math.ceil(wf.estimatedDuration * wastedRuns)
344
+
345
+ if (savingsPerMonth > 50) {
346
+ recommendations.push({
347
+ type: 'conditional',
348
+ workflow: workflowName,
349
+ title: 'Add path filters',
350
+ description: `"${workflowName}" runs on all commits`,
351
+ action:
352
+ 'Skip CI for docs-only changes (paths-ignore: ["**/*.md", "docs/**"])',
353
+ potentialSavings: savingsPerMonth,
354
+ savingsPerRun: 0,
355
+ priority: 'medium',
356
+ })
357
+ }
358
+ }
359
+ }
360
+ }
361
+
362
+ // Sort by potential savings (highest first)
363
+ recommendations.sort((a, b) => b.potentialSavings - a.potentialSavings)
364
+
365
+ return recommendations
366
+ }
367
+
368
+ /**
369
+ * Generate cost analysis report for terminal output
370
+ * @param {object} analysis - Complete cost analysis results
371
+ */
372
+ function generateReport(analysis) {
373
+ const { workflows, costs, commitStats, optimizations } = analysis
374
+
375
+ console.log('\nšŸ“Š GitHub Actions Usage Analysis')
376
+ console.log('━'.repeat(50))
377
+
378
+ // Repository info
379
+ try {
380
+ // Safe: No user input, hardcoded git command
381
+ const remoteUrl = execSync('git remote get-url origin', {
382
+ encoding: 'utf8',
383
+ stdio: ['pipe', 'pipe', 'ignore'],
384
+ }).trim()
385
+ const repoName = remoteUrl.split('/').pop().replace('.git', '')
386
+ console.log(`Repository: ${repoName}`)
387
+ } catch (_error) {
388
+ console.log('Repository: (local)')
389
+ }
390
+
391
+ console.log('')
392
+
393
+ // Usage summary
394
+ console.log(
395
+ `Estimated usage: ${costs.minutesPerMonth.toLocaleString()} min/month`
396
+ )
397
+ console.log(
398
+ ` Commit frequency: ~${commitStats.commitsPerDay.toFixed(1)} commits/day`
399
+ )
400
+ console.log(` Workflows detected: ${workflows.length}`)
401
+ console.log('')
402
+
403
+ // Workflow breakdown
404
+ if (costs.breakdown.length > 0) {
405
+ console.log('Workflow breakdown:')
406
+ for (const wf of costs.breakdown) {
407
+ console.log(` ā”œā”€ ${wf.name}:`)
408
+ console.log(` • ~${wf.minutesPerRun} min/run`)
409
+ console.log(
410
+ ` • ~${wf.runsPerMonth} runs/month = ${wf.minutesPerMonth} min/month`
411
+ )
412
+ }
413
+ console.log('')
414
+ }
415
+
416
+ // Cost analysis
417
+ console.log('šŸ’° Cost Analysis')
418
+
419
+ // Free tier
420
+ if (costs.tiers.free.withinLimit) {
421
+ console.log(
422
+ `Free tier (${costs.tiers.free.limit.toLocaleString()} min): āœ… WITHIN LIMIT`
423
+ )
424
+ const remaining = costs.tiers.free.limit - costs.minutesPerMonth
425
+ console.log(` Remaining: ${remaining.toLocaleString()} min/month`)
426
+ } else {
427
+ console.log(
428
+ `Free tier (${costs.tiers.free.limit.toLocaleString()} min): āš ļø EXCEEDED by ${costs.tiers.free.overage.toLocaleString()} min`
429
+ )
430
+ console.log(`Overage cost: $${costs.tiers.free.cost.toFixed(2)}/month`)
431
+ console.log('')
432
+ console.log('Alternative options:')
433
+
434
+ // Team tier comparison
435
+ if (costs.tiers.team.withinLimit) {
436
+ console.log(
437
+ ` Team plan ($${costs.tiers.team.monthlyCost}/user/month): āœ… Stays within ${costs.tiers.team.limit.toLocaleString()} min limit`
438
+ )
439
+ const savings = costs.tiers.free.cost - costs.tiers.team.monthlyCost
440
+ if (savings > 0) {
441
+ console.log(` Saves $${savings.toFixed(2)}/month per user`)
442
+ }
443
+ } else {
444
+ console.log(
445
+ ` Team plan ($${costs.tiers.team.monthlyCost}/user/month): Still exceeds (${costs.tiers.team.overage.toLocaleString()} min overage)`
446
+ )
447
+ console.log(
448
+ ` Total cost: $${(costs.tiers.team.monthlyCost + costs.tiers.team.cost).toFixed(2)}/month`
449
+ )
450
+ }
451
+
452
+ // Self-hosted option
453
+ console.log(' Self-hosted runners: $0/min (but VPS costs ~$5-20/month)')
454
+ }
455
+
456
+ console.log('')
457
+
458
+ // Optimization recommendations
459
+ if (optimizations && optimizations.length > 0) {
460
+ console.log('šŸ’” Optimization Recommendations')
461
+ console.log('')
462
+
463
+ const totalPotentialSavings = optimizations.reduce(
464
+ (sum, rec) => sum + rec.potentialSavings,
465
+ 0
466
+ )
467
+ const totalSavingsCost = totalPotentialSavings * 0.008
468
+
469
+ console.log(
470
+ `Found ${optimizations.length} optimization opportunities (potential savings: ${totalPotentialSavings.toLocaleString()} min/month = $${totalSavingsCost.toFixed(2)}/month)`
471
+ )
472
+ console.log('')
473
+
474
+ // Group by priority
475
+ const highPriority = optimizations.filter(r => r.priority === 'high')
476
+ const mediumPriority = optimizations.filter(r => r.priority === 'medium')
477
+ const lowPriority = optimizations.filter(r => r.priority === 'low')
478
+
479
+ if (highPriority.length > 0) {
480
+ console.log('šŸ”“ High Priority:')
481
+ for (const rec of highPriority) {
482
+ console.log(` ā”œā”€ ${rec.title} (${rec.workflow})`)
483
+ console.log(` • ${rec.description}`)
484
+ console.log(` • Action: ${rec.action}`)
485
+ console.log(
486
+ ` • Savings: ${rec.potentialSavings.toLocaleString()} min/month ($${(rec.potentialSavings * 0.008).toFixed(2)}/month)`
487
+ )
488
+ }
489
+ console.log('')
490
+ }
491
+
492
+ if (mediumPriority.length > 0) {
493
+ console.log('🟔 Medium Priority:')
494
+ for (const rec of mediumPriority) {
495
+ console.log(` ā”œā”€ ${rec.title} (${rec.workflow})`)
496
+ console.log(` • ${rec.description}`)
497
+ console.log(` • Action: ${rec.action}`)
498
+ console.log(
499
+ ` • Savings: ${rec.potentialSavings.toLocaleString()} min/month ($${(rec.potentialSavings * 0.008).toFixed(2)}/month)`
500
+ )
501
+ }
502
+ console.log('')
503
+ }
504
+
505
+ if (lowPriority.length > 0) {
506
+ console.log('🟢 Low Priority:')
507
+ for (const rec of lowPriority) {
508
+ console.log(` ā”œā”€ ${rec.title} (${rec.workflow})`)
509
+ console.log(` • ${rec.description}`)
510
+ console.log(` • Action: ${rec.action}`)
511
+ console.log(
512
+ ` • Savings: ${rec.potentialSavings.toLocaleString()} min/month ($${(rec.potentialSavings * 0.008).toFixed(2)}/month)`
513
+ )
514
+ }
515
+ console.log('')
516
+ }
517
+ } else {
518
+ console.log(
519
+ 'āœ… No optimization opportunities detected - workflows look good!'
520
+ )
521
+ console.log('')
522
+ }
523
+
524
+ console.log('━'.repeat(50))
525
+ console.log('')
526
+ }
527
+
528
+ /**
529
+ * Main handler for --analyze-ci command
530
+ */
531
+ async function handleAnalyzeCi() {
532
+ const projectPath = process.cwd()
533
+
534
+ // Check if Pro feature (FREE tier for now during development)
535
+ // TODO: Enable Pro gating after testing
536
+ // const license = getLicenseInfo()
537
+ // if (!hasFeature('ciCostAnalysis')) {
538
+ // showUpgradeMessage('GitHub Actions cost analysis')
539
+ // process.exit(1)
540
+ // }
541
+
542
+ const spinner = showProgress('Analyzing GitHub Actions workflows...')
543
+
544
+ try {
545
+ // Step 1: Discover workflows
546
+ const workflowFiles = discoverWorkflows(projectPath)
547
+
548
+ if (workflowFiles.length === 0) {
549
+ spinner.fail('No GitHub Actions workflows found')
550
+ console.log('\nāŒ No .github/workflows directory or workflow files found')
551
+ console.log(
552
+ ' Run this command in a repository with GitHub Actions configured'
553
+ )
554
+ process.exit(1)
555
+ }
556
+
557
+ // Step 2: Parse and analyze workflows
558
+ const workflows = []
559
+ for (const wf of workflowFiles) {
560
+ try {
561
+ const content = fs.readFileSync(wf.path, 'utf8')
562
+ const parsed = yaml.load(content)
563
+
564
+ const estimatedDuration = estimateWorkflowDuration(parsed)
565
+ workflows.push({
566
+ name: wf.name,
567
+ path: wf.path,
568
+ estimatedDuration,
569
+ parsed,
570
+ })
571
+ } catch (error) {
572
+ console.warn(`āš ļø Could not parse ${wf.name}: ${error.message}`)
573
+ }
574
+ }
575
+
576
+ // Step 3: Get commit frequency
577
+ const commitStats = getCommitFrequency(projectPath)
578
+
579
+ // Step 4: Calculate costs
580
+ const costs = calculateMonthlyCosts(workflows, commitStats.commitsPerDay)
581
+
582
+ // Step 5: Analyze optimization opportunities
583
+ const optimizations = analyzeOptimizations(
584
+ workflows,
585
+ commitStats.commitsPerDay
586
+ )
587
+
588
+ spinner.succeed('Analysis complete')
589
+
590
+ // Step 6: Generate report
591
+ generateReport({
592
+ workflows,
593
+ costs,
594
+ commitStats,
595
+ optimizations,
596
+ })
597
+
598
+ process.exit(0)
599
+ } catch (error) {
600
+ spinner.fail('Analysis failed')
601
+ console.error(`\nāŒ Error: ${error.message}`)
602
+ if (process.env.DEBUG) {
603
+ console.error(error.stack)
604
+ }
605
+ process.exit(1)
606
+ }
607
+ }
608
+
609
+ module.exports = {
610
+ handleAnalyzeCi,
611
+ discoverWorkflows,
612
+ estimateWorkflowDuration,
613
+ getCommitFrequency,
614
+ calculateMonthlyCosts,
615
+ analyzeOptimizations,
616
+ }