create-qa-architect 5.3.1 → 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.
package/setup.js CHANGED
@@ -65,6 +65,7 @@
65
65
 
66
66
  const fs = require('fs')
67
67
  const path = require('path')
68
+ const crypto = require('crypto')
68
69
  const { execSync } = require('child_process')
69
70
  const {
70
71
  mergeScripts,
@@ -189,6 +190,31 @@ const STYLELINT_EXTENSION_GLOB = `*.{${STYLELINT_EXTENSIONS.join(',')}}`
189
190
  const STYLELINT_SCAN_EXCLUDES = new Set(EXCLUDE_DIRECTORIES.STYLELINT)
190
191
  const MAX_STYLELINT_SCAN_DEPTH = SCAN_LIMITS.STYLELINT_MAX_DEPTH
191
192
 
193
+ function normalizeRepoIdentifier(remoteUrl) {
194
+ if (!remoteUrl || typeof remoteUrl !== 'string') return null
195
+
196
+ const scpMatch = remoteUrl.match(/^[^@]+@([^:]+):(.+?)(\.git)?$/)
197
+ if (scpMatch) {
198
+ const host = scpMatch[1]
199
+ const repoPath = scpMatch[2].replace(/^\/+/, '').replace(/\.git$/, '')
200
+ return `${host}/${repoPath}`
201
+ }
202
+
203
+ try {
204
+ const parsed = new URL(remoteUrl)
205
+ const host = parsed.hostname
206
+ const repoPath = parsed.pathname.replace(/^\/+/, '').replace(/\.git$/, '')
207
+ if (!host || !repoPath) return null
208
+ return `${host}/${repoPath}`
209
+ } catch (_error) {
210
+ return null
211
+ }
212
+ }
213
+
214
+ function hashRepoIdentifier(value) {
215
+ return crypto.createHash('sha256').update(value).digest('hex')
216
+ }
217
+
192
218
  function injectCollaborationSteps(workflowContent, options = {}) {
193
219
  const { enableSlackAlerts = false, enablePrComments = false } = options
194
220
  let updated = workflowContent
@@ -228,11 +254,43 @@ const safeReadDir = dir => {
228
254
  try {
229
255
  return fs.readdirSync(dir, { withFileTypes: true })
230
256
  } catch (error) {
231
- // Silent failure fix: Log non-ENOENT errors in DEBUG mode
232
- // ENOENT is expected (dir doesn't exist), other errors may indicate issues
233
- if (process.env.DEBUG && error?.code !== 'ENOENT') {
234
- console.warn(`⚠️ Could not read directory ${dir}: ${error.message}`)
257
+ // ENOENT is expected (dir doesn't exist) - return empty silently
258
+ if (error?.code === 'ENOENT') {
259
+ if (process.env.DEBUG) {
260
+ console.warn(
261
+ ` Debug: safeReadDir(${dir}) returned empty (directory not found)`
262
+ )
263
+ }
264
+ return []
265
+ }
266
+
267
+ // All other errors are unexpected and should be logged with context
268
+ console.error(`❌ Failed to read directory: ${dir}`)
269
+ console.error(` Error: ${error.message} (${error.code || 'unknown'})`)
270
+ console.error(` This may indicate a permission or filesystem issue`)
271
+
272
+ // Report to error tracking in production
273
+ if (process.env.NODE_ENV === 'production') {
274
+ try {
275
+ const errorReporter = new ErrorReporter()
276
+ errorReporter.captureException(error, {
277
+ context: 'safeReadDir',
278
+ directory: dir,
279
+ errorCode: error.code,
280
+ })
281
+ } catch (_error) {
282
+ // Don't fail if error reporting fails
283
+ }
284
+ }
285
+
286
+ // Re-throw for critical errors instead of silently returning []
287
+ if (['EACCES', 'EIO', 'ELOOP', 'EMFILE'].includes(error.code)) {
288
+ throw new Error(
289
+ `Cannot read directory ${dir}: ${error.message}. ` +
290
+ `This may indicate a serious filesystem or permission issue.`
291
+ )
235
292
  }
293
+
236
294
  return []
237
295
  }
238
296
  }
@@ -413,8 +471,14 @@ function parseArguments(rawArgs) {
413
471
  const isCheckMaturityMode = sanitizedArgs.includes('--check-maturity')
414
472
  const isValidateConfigMode = sanitizedArgs.includes('--validate-config')
415
473
  const isActivateLicenseMode = sanitizedArgs.includes('--activate-license')
474
+ const isAnalyzeCiMode = sanitizedArgs.includes('--analyze-ci')
416
475
  const isPrelaunchMode = sanitizedArgs.includes('--prelaunch')
417
476
  const isDryRun = sanitizedArgs.includes('--dry-run')
477
+ const isWorkflowMinimal = sanitizedArgs.includes('--workflow-minimal')
478
+ const isWorkflowStandard = sanitizedArgs.includes('--workflow-standard')
479
+ const isWorkflowComprehensive = sanitizedArgs.includes(
480
+ '--workflow-comprehensive'
481
+ )
418
482
  const ciProviderIndex = sanitizedArgs.findIndex(arg => arg === '--ci')
419
483
  const ciProvider =
420
484
  ciProviderIndex !== -1 && sanitizedArgs[ciProviderIndex + 1]
@@ -426,11 +490,39 @@ function parseArguments(rawArgs) {
426
490
  // Custom template directory - use raw args to preserve valid path characters (&, <, >, etc.)
427
491
  // Normalize path to prevent traversal attacks and make absolute
428
492
  const templateFlagIndex = sanitizedArgs.findIndex(arg => arg === '--template')
429
- const customTemplatePath =
493
+ let customTemplatePath =
430
494
  templateFlagIndex !== -1 && rawArgs[templateFlagIndex + 1]
431
495
  ? path.resolve(rawArgs[templateFlagIndex + 1])
432
496
  : null
433
497
 
498
+ // Validate custom template path early to prevent path traversal attacks
499
+ if (customTemplatePath) {
500
+ const inputPath = rawArgs[templateFlagIndex + 1]
501
+ // Check for suspicious patterns (path traversal attempts)
502
+ if (inputPath.includes('..') || inputPath.includes('~')) {
503
+ console.error(
504
+ `❌ Invalid template path: "${inputPath}". Path traversal patterns not allowed.`
505
+ )
506
+ console.error(' Use absolute paths only (e.g., /Users/you/templates)')
507
+ process.exit(1)
508
+ }
509
+
510
+ // Verify the resolved path exists and is a directory
511
+ try {
512
+ const stats = fs.statSync(customTemplatePath)
513
+ if (!stats.isDirectory()) {
514
+ console.error(
515
+ `❌ Template path is not a directory: ${customTemplatePath}`
516
+ )
517
+ process.exit(1)
518
+ }
519
+ } catch (error) {
520
+ console.error(`❌ Template path does not exist: ${customTemplatePath}`)
521
+ console.error(` Error: ${error.message}`)
522
+ process.exit(1)
523
+ }
524
+ }
525
+
434
526
  // Granular tool disable options
435
527
  const disableNpmAudit = sanitizedArgs.includes('--no-npm-audit')
436
528
  const disableGitleaks = sanitizedArgs.includes('--no-gitleaks')
@@ -454,6 +546,7 @@ function parseArguments(rawArgs) {
454
546
  isCheckMaturityMode,
455
547
  isValidateConfigMode,
456
548
  isActivateLicenseMode,
549
+ isAnalyzeCiMode,
457
550
  isPrelaunchMode,
458
551
  isDryRun,
459
552
  ciProvider,
@@ -466,6 +559,9 @@ function parseArguments(rawArgs) {
466
559
  disableMarkdownlint,
467
560
  disableEslintSecurity,
468
561
  allowLatestGitleaks,
562
+ isWorkflowMinimal,
563
+ isWorkflowStandard,
564
+ isWorkflowComprehensive,
469
565
  }
470
566
  }
471
567
 
@@ -494,6 +590,7 @@ function parseArguments(rawArgs) {
494
590
  isValidateConfigMode,
495
591
  isActivateLicenseMode,
496
592
  isPrelaunchMode,
593
+ isAnalyzeCiMode,
497
594
  isDryRun,
498
595
  ciProvider,
499
596
  enableSlackAlerts,
@@ -505,6 +602,9 @@ function parseArguments(rawArgs) {
505
602
  disableMarkdownlint,
506
603
  disableEslintSecurity,
507
604
  allowLatestGitleaks,
605
+ isWorkflowMinimal,
606
+ isWorkflowStandard,
607
+ isWorkflowComprehensive,
508
608
  } = parsedConfig
509
609
 
510
610
  // Initialize telemetry session (opt-in only, fails silently)
@@ -567,6 +667,8 @@ function parseArguments(rawArgs) {
567
667
  isCheckMaturityMode,
568
668
  isValidateConfigMode,
569
669
  isActivateLicenseMode,
670
+ isPrelaunchMode,
671
+ isAnalyzeCiMode,
570
672
  isDryRun,
571
673
  ciProvider,
572
674
  enableSlackAlerts,
@@ -578,6 +680,9 @@ function parseArguments(rawArgs) {
578
680
  disableMarkdownlint,
579
681
  disableEslintSecurity,
580
682
  allowLatestGitleaks,
683
+ isWorkflowMinimal,
684
+ isWorkflowStandard,
685
+ isWorkflowComprehensive,
581
686
  } = parsedConfig)
582
687
 
583
688
  console.log('📋 Configuration after interactive selections applied\n')
@@ -612,6 +717,15 @@ SETUP OPTIONS:
612
717
  --template <path> Use custom templates from specified directory
613
718
  --dry-run Preview changes without modifying files
614
719
 
720
+ WORKFLOW TIERS (GitHub Actions optimization):
721
+ --workflow-minimal Minimal CI (default) - Single Node version, weekly security
722
+ ~5-10 min/commit, ~$0-5/mo for typical projects
723
+ --workflow-standard Standard CI - Matrix testing on main, weekly security
724
+ ~15-20 min/commit, ~$5-20/mo for typical projects
725
+ --workflow-comprehensive Comprehensive CI - Matrix on every push, security inline
726
+ ~50-100 min/commit, ~$100-350/mo for typical projects
727
+ --analyze-ci Analyze GitHub Actions usage and get optimization tips (Pro)
728
+
615
729
  VALIDATION OPTIONS:
616
730
  --validate Run comprehensive validation (same as --comprehensive)
617
731
  --comprehensive Run all validation checks
@@ -679,6 +793,18 @@ EXAMPLES:
679
793
  npx create-qa-architect@latest --dry-run
680
794
  → Preview what files and configurations would be created/modified
681
795
 
796
+ npx create-qa-architect@latest --workflow-minimal
797
+ → Set up with minimal CI (default) - fastest, cheapest, ideal for solo devs
798
+
799
+ npx create-qa-architect@latest --workflow-standard
800
+ → Set up with standard CI - balanced quality/cost for small teams
801
+
802
+ npx create-qa-architect@latest --update --workflow-minimal
803
+ → Convert existing comprehensive workflow to minimal (reduce CI costs)
804
+
805
+ npx create-qa-architect@latest --analyze-ci
806
+ → Analyze your GitHub Actions usage and get cost optimization recommendations (Pro)
807
+
682
808
  PRIVACY & TELEMETRY:
683
809
  Telemetry and error reporting are OPT-IN only (disabled by default). To enable:
684
810
  export QAA_TELEMETRY=true # Usage tracking (local only)
@@ -786,6 +912,20 @@ HELP:
786
912
  process.exit(0)
787
913
  }
788
914
 
915
+ // Handle CI cost analysis command
916
+ if (isAnalyzeCiMode) {
917
+ return (async () => {
918
+ try {
919
+ const { handleAnalyzeCi } = require('./lib/commands/analyze-ci')
920
+ await handleAnalyzeCi()
921
+ process.exit(0)
922
+ } catch (error) {
923
+ console.error('CI cost analysis error:', error.message)
924
+ process.exit(1)
925
+ }
926
+ })()
927
+ }
928
+
789
929
  // Handle validate config command
790
930
  if (isValidateConfigMode) {
791
931
  const { validateAndReport } = require('./lib/config-validator')
@@ -939,47 +1079,84 @@ HELP:
939
1079
 
940
1080
  // 1. Lighthouse CI - available to all, thresholds for Pro+
941
1081
  if (hasLighthouse) {
942
- const lighthousePath = path.join(projectPath, 'lighthouserc.js')
943
- if (!fs.existsSync(lighthousePath)) {
944
- writeLighthouseConfig(projectPath, {
945
- hasThresholds: hasLighthouseThresholds,
946
- })
947
- addedTools.push(
948
- hasLighthouseThresholds
949
- ? 'Lighthouse CI (with thresholds)'
950
- : 'Lighthouse CI (basic)'
951
- )
1082
+ try {
1083
+ const lighthousePath = path.join(projectPath, 'lighthouserc.js')
1084
+ if (!fs.existsSync(lighthousePath)) {
1085
+ writeLighthouseConfig(projectPath, {
1086
+ hasThresholds: hasLighthouseThresholds,
1087
+ })
1088
+ addedTools.push(
1089
+ hasLighthouseThresholds
1090
+ ? 'Lighthouse CI (with thresholds)'
1091
+ : 'Lighthouse CI (basic)'
1092
+ )
1093
+ }
1094
+ } catch (error) {
1095
+ console.warn('⚠️ Failed to configure Lighthouse CI:', error.message)
1096
+ if (process.env.DEBUG) {
1097
+ console.error(' Stack:', error.stack)
1098
+ }
952
1099
  }
953
1100
  }
954
1101
 
955
1102
  // 2. Bundle size limits - Pro only
956
1103
  if (hasBundleSizeLimits) {
957
- if (!pkgJson.content['size-limit']) {
958
- writeSizeLimitConfig(projectPath)
959
- addedTools.push('Bundle size limits (size-limit)')
1104
+ try {
1105
+ if (!pkgJson.content['size-limit']) {
1106
+ writeSizeLimitConfig(projectPath)
1107
+ addedTools.push('Bundle size limits (size-limit)')
1108
+ }
1109
+ } catch (error) {
1110
+ console.warn(
1111
+ '⚠️ Failed to configure bundle size limits:',
1112
+ error.message
1113
+ )
1114
+ if (process.env.DEBUG) {
1115
+ console.error(' Stack:', error.stack)
1116
+ }
960
1117
  }
961
1118
  }
962
1119
 
963
1120
  // 3. axe-core accessibility testing - available to all
964
1121
  if (hasAxeAccessibility) {
965
- const axeTestPath = path.join(
966
- projectPath,
967
- 'tests',
968
- 'accessibility.test.js'
969
- )
970
- if (!fs.existsSync(axeTestPath)) {
971
- writeAxeTestSetup(projectPath)
972
- addedTools.push('axe-core accessibility tests')
1122
+ try {
1123
+ const axeTestPath = path.join(
1124
+ projectPath,
1125
+ 'tests',
1126
+ 'accessibility.test.js'
1127
+ )
1128
+ if (!fs.existsSync(axeTestPath)) {
1129
+ writeAxeTestSetup(projectPath)
1130
+ addedTools.push('axe-core accessibility tests')
1131
+ }
1132
+ } catch (error) {
1133
+ console.warn(
1134
+ '⚠️ Failed to configure axe-core tests:',
1135
+ error.message
1136
+ )
1137
+ if (process.env.DEBUG) {
1138
+ console.error(' Stack:', error.stack)
1139
+ }
973
1140
  }
974
1141
  }
975
1142
 
976
1143
  // 4. Conventional commits (commitlint) - available to all
977
1144
  if (hasConventionalCommits) {
978
- const commitlintPath = path.join(projectPath, 'commitlint.config.js')
979
- if (!fs.existsSync(commitlintPath)) {
980
- writeCommitlintConfig(projectPath)
981
- writeCommitMsgHook(projectPath)
982
- addedTools.push('Conventional commits (commitlint)')
1145
+ try {
1146
+ const commitlintPath = path.join(
1147
+ projectPath,
1148
+ 'commitlint.config.js'
1149
+ )
1150
+ if (!fs.existsSync(commitlintPath)) {
1151
+ writeCommitlintConfig(projectPath)
1152
+ writeCommitMsgHook(projectPath)
1153
+ addedTools.push('Conventional commits (commitlint)')
1154
+ }
1155
+ } catch (error) {
1156
+ console.warn('⚠️ Failed to configure commitlint:', error.message)
1157
+ if (process.env.DEBUG) {
1158
+ console.error(' Stack:', error.stack)
1159
+ }
983
1160
  }
984
1161
  }
985
1162
 
@@ -1035,9 +1212,212 @@ HELP:
1035
1212
  qualitySpinner.succeed('Quality tools already configured')
1036
1213
  }
1037
1214
  } catch (error) {
1038
- qualitySpinner.warn('Could not set up some quality tools')
1039
- console.warn('⚠️ Quality tools setup error:', error.message)
1215
+ qualitySpinner.fail('Quality tools setup failed')
1216
+ console.error(
1217
+ `❌ Unexpected error during quality tools setup: ${error.message}`
1218
+ )
1219
+ if (process.env.DEBUG) {
1220
+ console.error(' Stack:', error.stack)
1221
+ }
1222
+ console.error(
1223
+ ' Please report this issue at https://github.com/your-repo/issues'
1224
+ )
1225
+ throw error // Re-throw to prevent silent continuation
1226
+ }
1227
+ }
1228
+
1229
+ /**
1230
+ * Detect existing workflow mode from quality.yml
1231
+ * @param {string} projectPath - Path to the project
1232
+ * @returns {'minimal'|'standard'|'comprehensive'|null} Detected mode
1233
+ */
1234
+ function detectExistingWorkflowMode(projectPath) {
1235
+ const workflowPath = path.join(
1236
+ projectPath,
1237
+ '.github',
1238
+ 'workflows',
1239
+ 'quality.yml'
1240
+ )
1241
+
1242
+ if (!fs.existsSync(workflowPath)) {
1243
+ return null
1244
+ }
1245
+
1246
+ try {
1247
+ const content = fs.readFileSync(workflowPath, 'utf8')
1248
+
1249
+ // Check for version markers (new format)
1250
+ if (content.includes('# WORKFLOW_MODE: minimal')) {
1251
+ return 'minimal'
1252
+ }
1253
+ if (content.includes('# WORKFLOW_MODE: standard')) {
1254
+ return 'standard'
1255
+ }
1256
+ if (content.includes('# WORKFLOW_MODE: comprehensive')) {
1257
+ return 'comprehensive'
1258
+ }
1259
+
1260
+ // Legacy detection (no version marker)
1261
+ // Comprehensive: has security job + matrix testing on every push
1262
+ // Standard: has matrix testing but security is scheduled
1263
+ // Minimal: no matrix, single node version
1264
+ const hasSecurityJob = /jobs:\s*\n\s*security:/m.test(content)
1265
+ const hasMatrixInTests = /tests:[\s\S]*?strategy:[\s\S]*?matrix:/m.test(
1266
+ content
1267
+ )
1268
+ const hasScheduledSecurity =
1269
+ /on:\s*\n\s*schedule:[\s\S]*?- cron:/m.test(content)
1270
+
1271
+ if (hasSecurityJob && hasMatrixInTests && !hasScheduledSecurity) {
1272
+ return 'comprehensive'
1273
+ }
1274
+ if (hasMatrixInTests && hasScheduledSecurity) {
1275
+ return 'standard'
1276
+ }
1277
+ if (!hasMatrixInTests) {
1278
+ return 'minimal'
1279
+ }
1280
+
1281
+ // Default to comprehensive for unknown patterns
1282
+ return 'comprehensive'
1283
+ } catch (error) {
1284
+ console.warn(
1285
+ `⚠️ Could not detect existing workflow mode: ${error.message}`
1286
+ )
1287
+ return null
1288
+ }
1289
+ }
1290
+
1291
+ /**
1292
+ * Inject workflow mode-specific configuration into quality.yml
1293
+ * @param {string} workflowContent - Template content
1294
+ * @param {'minimal'|'standard'|'comprehensive'} mode - Selected mode
1295
+ * @returns {string} Modified workflow content
1296
+ */
1297
+ function injectWorkflowMode(workflowContent, mode) {
1298
+ let updated = workflowContent
1299
+
1300
+ // Add version marker at the top (after name section)
1301
+ const versionMarker = `# WORKFLOW_MODE: ${mode}`
1302
+ if (!updated.includes('# WORKFLOW_MODE:')) {
1303
+ // Insert after the comment block and before jobs
1304
+ updated = updated.replace(/(\n\njobs:)/, `\n${versionMarker}\n$1`)
1305
+ }
1306
+
1307
+ // Replace PATH_FILTERS_PLACEHOLDER
1308
+ if (updated.includes('# PATH_FILTERS_PLACEHOLDER')) {
1309
+ if (mode === 'minimal' || mode === 'standard') {
1310
+ const pathsIgnore = `paths-ignore:
1311
+ - '**.md'
1312
+ - 'docs/**'
1313
+ - 'LICENSE'
1314
+ - '.gitignore'
1315
+ - '.editorconfig'`
1316
+ updated = updated.replace('# PATH_FILTERS_PLACEHOLDER', pathsIgnore)
1317
+ } else {
1318
+ // comprehensive: Remove placeholder
1319
+ updated = updated.replace(/\s*# PATH_FILTERS_PLACEHOLDER\n?/, '')
1320
+ }
1321
+ }
1322
+
1323
+ // Replace SECURITY_SCHEDULE_PLACEHOLDER
1324
+ if (updated.includes('# SECURITY_SCHEDULE_PLACEHOLDER')) {
1325
+ if (mode === 'minimal' || mode === 'standard') {
1326
+ // Add weekly schedule for security scans
1327
+ const scheduleConfig = `schedule:
1328
+ - cron: '0 0 * * 0' # Weekly on Sunday (security scans)
1329
+ workflow_dispatch: # Manual trigger`
1330
+ updated = updated.replace(
1331
+ '# SECURITY_SCHEDULE_PLACEHOLDER',
1332
+ scheduleConfig
1333
+ )
1334
+ } else {
1335
+ // comprehensive: Remove placeholder
1336
+ updated = updated.replace(/\s*# SECURITY_SCHEDULE_PLACEHOLDER\n?/, '')
1337
+ }
1338
+ }
1339
+
1340
+ // Replace SECURITY_CONDITION_PLACEHOLDER
1341
+ if (updated.includes('# SECURITY_CONDITION_PLACEHOLDER')) {
1342
+ if (mode === 'minimal' || mode === 'standard') {
1343
+ // Only run security on schedule events (not on every push)
1344
+ // Replace both the placeholder comment AND the existing if line
1345
+ updated = updated.replace(
1346
+ /# SECURITY_CONDITION_PLACEHOLDER\n\s*if: needs\.detect-maturity\.outputs\.has-deps == 'true'/,
1347
+ `if: (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && needs.detect-maturity.outputs.has-deps == 'true'`
1348
+ )
1349
+ } else {
1350
+ // comprehensive: Remove placeholder, keep the existing if condition
1351
+ updated = updated.replace(
1352
+ /\s*# SECURITY_CONDITION_PLACEHOLDER\n?/,
1353
+ ''
1354
+ )
1355
+ }
1356
+ }
1357
+
1358
+ // Replace MATRIX_PLACEHOLDER
1359
+ if (updated.includes('# MATRIX_PLACEHOLDER')) {
1360
+ if (mode === 'minimal') {
1361
+ // Single Node version, no matrix
1362
+ // Replace both the placeholder comment AND the existing strategy block
1363
+ updated = updated.replace(
1364
+ /# MATRIX_PLACEHOLDER\n\s*strategy:\n\s*fail-fast: false/,
1365
+ `strategy:
1366
+ fail-fast: false
1367
+ matrix:
1368
+ node-version: [22]`
1369
+ )
1370
+ } else if (mode === 'standard') {
1371
+ // Matrix testing only on main branch
1372
+ // 1. Modify the job-level if condition to add branch check
1373
+ updated = updated.replace(
1374
+ /if: fromJSON\(needs\.detect-maturity\.outputs\.test-count\) > 0\n\s*# TESTS_CONDITION_PLACEHOLDER/,
1375
+ `if: github.ref == 'refs/heads/main' && fromJSON(needs.detect-maturity.outputs.test-count) > 0
1376
+ # TESTS_CONDITION_PLACEHOLDER`
1377
+ )
1378
+ // 2. Replace matrix placeholder and existing strategy block
1379
+ updated = updated.replace(
1380
+ /# MATRIX_PLACEHOLDER\n\s*strategy:\n\s*fail-fast: false/,
1381
+ `strategy:
1382
+ fail-fast: false
1383
+ matrix:
1384
+ node-version: [20, 22]`
1385
+ )
1386
+ } else {
1387
+ // comprehensive: Matrix on every push
1388
+ // Replace placeholder, merge with existing strategy
1389
+ updated = updated.replace(
1390
+ /# MATRIX_PLACEHOLDER\n\s*strategy:\n\s*fail-fast: false/,
1391
+ `strategy:
1392
+ fail-fast: false
1393
+ matrix:
1394
+ node-version: [20, 22]`
1395
+ )
1396
+ }
1397
+ }
1398
+
1399
+ // Replace TESTS_CONDITION_PLACEHOLDER
1400
+ if (updated.includes('# TESTS_CONDITION_PLACEHOLDER')) {
1401
+ let testsCondition = ''
1402
+
1403
+ if (mode === 'minimal') {
1404
+ // No matrix, always run
1405
+ testsCondition = '# Runs on every push (single Node version)'
1406
+ } else if (mode === 'standard') {
1407
+ // Matrix only on main
1408
+ testsCondition = '# Matrix testing only on main branch'
1409
+ } else {
1410
+ // comprehensive: Matrix on every push
1411
+ testsCondition = '# Matrix testing on every push'
1412
+ }
1413
+
1414
+ updated = updated.replace(
1415
+ '# TESTS_CONDITION_PLACEHOLDER',
1416
+ testsCondition
1417
+ )
1040
1418
  }
1419
+
1420
+ return updated
1041
1421
  }
1042
1422
 
1043
1423
  // Normal setup flow
@@ -1054,7 +1434,7 @@ HELP:
1054
1434
  try {
1055
1435
  execSync('git status', { stdio: 'ignore' })
1056
1436
  gitSpinner.succeed('Git repository verified')
1057
- } catch {
1437
+ } catch (_error) {
1058
1438
  gitSpinner.fail('Not a git repository')
1059
1439
  console.error('❌ This must be run in a git repository')
1060
1440
  console.log('Run "git init" first, then try again.')
@@ -1064,6 +1444,8 @@ HELP:
1064
1444
  // Enforce FREE tier repo limit (1 private repo)
1065
1445
  // Must happen before any file modifications
1066
1446
  const license = getLicenseInfo()
1447
+ let pendingRepoRegistration = null
1448
+ let pendingRepoUsageSnapshot = null
1067
1449
  if (license.tier === 'FREE') {
1068
1450
  // Generate unique repo ID from git remote or directory name
1069
1451
  let repoId
@@ -1072,10 +1454,11 @@ HELP:
1072
1454
  encoding: 'utf8',
1073
1455
  stdio: ['pipe', 'pipe', 'ignore'],
1074
1456
  }).trim()
1075
- repoId = remoteUrl
1076
- } catch {
1457
+ const normalized = normalizeRepoIdentifier(remoteUrl)
1458
+ repoId = hashRepoIdentifier(normalized || remoteUrl)
1459
+ } catch (_error) {
1077
1460
  // No remote - use absolute path as fallback
1078
- repoId = process.cwd()
1461
+ repoId = hashRepoIdentifier(process.cwd())
1079
1462
  }
1080
1463
 
1081
1464
  const repoCheck = checkUsageCaps('repo')
@@ -1091,11 +1474,8 @@ HELP:
1091
1474
  process.exit(1)
1092
1475
  }
1093
1476
 
1094
- // Register this repo
1095
- incrementUsage('repo', 1, repoId)
1096
- console.log(
1097
- `✅ Registered repo (FREE tier: ${(repoCheck.usage?.repoCount || 0) + 1}/1 repos used)`
1098
- )
1477
+ pendingRepoRegistration = repoId
1478
+ pendingRepoUsageSnapshot = repoCheck.usage
1099
1479
  }
1100
1480
  }
1101
1481
 
@@ -1282,6 +1662,19 @@ HELP:
1282
1662
  )
1283
1663
  }
1284
1664
 
1665
+ // Shell project detection
1666
+ const { ProjectMaturityDetector } = require('./lib/project-maturity')
1667
+ const maturityDetector = new ProjectMaturityDetector({
1668
+ projectPath: process.cwd(),
1669
+ })
1670
+ const projectStats = maturityDetector.analyzeProject()
1671
+ const usesShell = projectStats.isShellProject
1672
+ if (usesShell) {
1673
+ console.log(
1674
+ '🐚 Detected shell script project; enabling shell quality automation'
1675
+ )
1676
+ }
1677
+
1285
1678
  const stylelintTargets = findStylelintTargets(process.cwd())
1286
1679
  const usingDefaultStylelintTarget =
1287
1680
  stylelintTargets.length === 1 &&
@@ -1435,12 +1828,28 @@ HELP:
1435
1828
  // Validate the generated config
1436
1829
  const validationResult = validateQualityConfig(qualityrcPath)
1437
1830
  if (!validationResult.valid) {
1438
- console.warn(
1439
- '⚠️ Warning: Generated config has validation issues (this should not happen):'
1831
+ console.error(
1832
+ '\n❌ CRITICAL: Generated .qualityrc.json failed validation'
1440
1833
  )
1441
- validationResult.errors.forEach(error => {
1442
- console.warn(` - ${error}`)
1834
+ console.error(
1835
+ ' This should never happen. Please report this bug.\n'
1836
+ )
1837
+
1838
+ console.error('Validation errors:')
1839
+ validationResult.errors.forEach((error, index) => {
1840
+ console.error(` ${index + 1}. ${error}`)
1443
1841
  })
1842
+
1843
+ console.error(`\n🐛 Report issue with this info:`)
1844
+ console.error(` • File: ${qualityrcPath}`)
1845
+ console.error(` • Detected maturity: ${detectedMaturity}`)
1846
+ console.error(` • Error count: ${validationResult.errors.length}`)
1847
+ console.error(
1848
+ ` • https://github.com/vibebuildlab/qa-architect/issues/new\n`
1849
+ )
1850
+
1851
+ // Don't continue - this is a bug in the tool itself
1852
+ throw new Error('Invalid quality config generated - cannot continue')
1444
1853
  }
1445
1854
  } else {
1446
1855
  // TD8 fix: Re-enabled validation (was disabled for debugging)
@@ -1534,6 +1943,23 @@ HELP:
1534
1943
  }
1535
1944
 
1536
1945
  const workflowFile = path.join(githubWorkflowDir, 'quality.yml')
1946
+
1947
+ // Determine workflow mode
1948
+ let workflowMode = 'minimal' // Default to minimal
1949
+ if (isWorkflowMinimal) {
1950
+ workflowMode = 'minimal'
1951
+ } else if (isWorkflowStandard) {
1952
+ workflowMode = 'standard'
1953
+ } else if (isWorkflowComprehensive) {
1954
+ workflowMode = 'comprehensive'
1955
+ } else if (fs.existsSync(workflowFile)) {
1956
+ // Detect existing mode when updating
1957
+ const existingMode = detectExistingWorkflowMode(process.cwd())
1958
+ if (existingMode) {
1959
+ workflowMode = existingMode
1960
+ }
1961
+ }
1962
+
1537
1963
  if (!fs.existsSync(workflowFile)) {
1538
1964
  let templateWorkflow =
1539
1965
  templateLoader.getTemplate(
@@ -1545,13 +1971,59 @@ HELP:
1545
1971
  'utf8'
1546
1972
  )
1547
1973
 
1974
+ // Inject workflow mode configuration
1975
+ templateWorkflow = injectWorkflowMode(templateWorkflow, workflowMode)
1976
+
1977
+ // Inject collaboration steps
1548
1978
  templateWorkflow = injectCollaborationSteps(templateWorkflow, {
1549
1979
  enableSlackAlerts,
1550
1980
  enablePrComments,
1551
1981
  })
1552
1982
 
1553
1983
  fs.writeFileSync(workflowFile, templateWorkflow)
1554
- console.log('✅ Added GitHub Actions workflow')
1984
+ console.log(`✅ Added GitHub Actions workflow (${workflowMode} mode)`)
1985
+ } else if (isUpdateMode) {
1986
+ // Update existing workflow with new mode if explicitly specified
1987
+ if (
1988
+ isWorkflowMinimal ||
1989
+ isWorkflowStandard ||
1990
+ isWorkflowComprehensive
1991
+ ) {
1992
+ // Load fresh template and re-inject
1993
+ let templateWorkflow =
1994
+ templateLoader.getTemplate(
1995
+ templates,
1996
+ path.join('.github', 'workflows', 'quality.yml')
1997
+ ) ||
1998
+ fs.readFileSync(
1999
+ path.join(__dirname, '.github/workflows/quality.yml'),
2000
+ 'utf8'
2001
+ )
2002
+
2003
+ // Inject workflow mode configuration
2004
+ templateWorkflow = injectWorkflowMode(
2005
+ templateWorkflow,
2006
+ workflowMode
2007
+ )
2008
+
2009
+ // Inject collaboration steps (preserve from existing if present)
2010
+ const existingWorkflow = fs.readFileSync(workflowFile, 'utf8')
2011
+ const hasSlackAlerts =
2012
+ existingWorkflow.includes('SLACK_WEBHOOK_URL')
2013
+ const hasPrComments = existingWorkflow.includes(
2014
+ 'PR_COMMENT_PLACEHOLDER'
2015
+ )
2016
+
2017
+ templateWorkflow = injectCollaborationSteps(templateWorkflow, {
2018
+ enableSlackAlerts: hasSlackAlerts,
2019
+ enablePrComments: hasPrComments,
2020
+ })
2021
+
2022
+ fs.writeFileSync(workflowFile, templateWorkflow)
2023
+ console.log(
2024
+ `♻️ Updated GitHub Actions workflow to ${workflowMode} mode`
2025
+ )
2026
+ }
1555
2027
  }
1556
2028
  }
1557
2029
 
@@ -1762,7 +2234,7 @@ let tier = 'FREE'
1762
2234
  try {
1763
2235
  const data = JSON.parse(fs.readFileSync(licenseFile, 'utf8'))
1764
2236
  tier = (data && data.tier) || 'FREE'
1765
- } catch {
2237
+ } catch (error) {
1766
2238
  tier = 'FREE'
1767
2239
  }
1768
2240
 
@@ -1775,7 +2247,7 @@ try {
1775
2247
  if (data.month === currentMonth) {
1776
2248
  usage = { ...usage, ...data }
1777
2249
  }
1778
- } catch {
2250
+ } catch (error) {
1779
2251
  // First run or corrupt file – start fresh
1780
2252
  }
1781
2253
 
@@ -1963,6 +2435,86 @@ echo "✅ Pre-push validation passed!"
1963
2435
  }
1964
2436
  }
1965
2437
 
2438
+ pythonSpinner.succeed('Python quality tools configured')
2439
+ }
2440
+
2441
+ // Shell project setup
2442
+ if (usesShell) {
2443
+ // Copy Shell CI workflow (GitHub Actions only)
2444
+ if (ciProvider === 'github') {
2445
+ const shellCiWorkflowFile = path.join(
2446
+ githubWorkflowDir,
2447
+ 'shell-ci.yml'
2448
+ )
2449
+ if (!fs.existsSync(shellCiWorkflowFile)) {
2450
+ const templateShellCiWorkflow =
2451
+ templateLoader.getTemplate(
2452
+ templates,
2453
+ path.join('config', 'shell-ci.yml')
2454
+ ) ||
2455
+ fs.readFileSync(
2456
+ path.join(__dirname, 'config/shell-ci.yml'),
2457
+ 'utf8'
2458
+ )
2459
+ fs.writeFileSync(shellCiWorkflowFile, templateShellCiWorkflow)
2460
+ console.log('✅ Added Shell CI GitHub Actions workflow')
2461
+ }
2462
+
2463
+ // Copy Shell Quality workflow
2464
+ const shellQualityWorkflowFile = path.join(
2465
+ githubWorkflowDir,
2466
+ 'shell-quality.yml'
2467
+ )
2468
+ if (!fs.existsSync(shellQualityWorkflowFile)) {
2469
+ const templateShellQualityWorkflow =
2470
+ templateLoader.getTemplate(
2471
+ templates,
2472
+ path.join('config', 'shell-quality.yml')
2473
+ ) ||
2474
+ fs.readFileSync(
2475
+ path.join(__dirname, 'config/shell-quality.yml'),
2476
+ 'utf8'
2477
+ )
2478
+ fs.writeFileSync(
2479
+ shellQualityWorkflowFile,
2480
+ templateShellQualityWorkflow
2481
+ )
2482
+ console.log('✅ Added Shell Quality GitHub Actions workflow')
2483
+ }
2484
+ }
2485
+
2486
+ // Create a basic README if it doesn't exist
2487
+ const readmePath = path.join(process.cwd(), 'README.md')
2488
+ if (!fs.existsSync(readmePath)) {
2489
+ const projectName = path.basename(process.cwd())
2490
+ const basicReadme = `# ${projectName}
2491
+
2492
+ Shell script collection for ${projectName}.
2493
+
2494
+ ## Usage
2495
+
2496
+ \`\`\`bash
2497
+ # Make scripts executable
2498
+ chmod +x *.sh
2499
+
2500
+ # Run a script
2501
+ ./script-name.sh
2502
+ \`\`\`
2503
+
2504
+ ## Development
2505
+
2506
+ Quality checks are automated via GitHub Actions:
2507
+ - ShellCheck linting
2508
+ - Syntax validation
2509
+ - Permission checks
2510
+ - Best practices analysis
2511
+ `
2512
+ fs.writeFileSync(readmePath, basicReadme)
2513
+ console.log('✅ Created basic README.md')
2514
+ }
2515
+ }
2516
+
2517
+ if (usesPython) {
1966
2518
  // Create tests directory if it doesn't exist
1967
2519
  const testsDir = path.join(process.cwd(), 'tests')
1968
2520
  if (!fs.existsSync(testsDir)) {
@@ -2005,8 +2557,6 @@ echo "✅ Pre-push validation passed!"
2005
2557
  )
2006
2558
  }
2007
2559
  }
2008
-
2009
- pythonSpinner.succeed('Python quality tools configured')
2010
2560
  }
2011
2561
 
2012
2562
  // Smart Test Strategy (Pro/Team/Enterprise feature)
@@ -2172,6 +2722,12 @@ describe('Test framework validation', () => {
2172
2722
 
2173
2723
  console.log('\n🎉 Quality automation setup complete!')
2174
2724
 
2725
+ if (pendingRepoRegistration) {
2726
+ incrementUsage('repo', 1, pendingRepoRegistration)
2727
+ const repoCount = (pendingRepoUsageSnapshot?.repoCount || 0) + 1
2728
+ console.log(`✅ Registered repo (FREE tier: ${repoCount}/1 repos used)`)
2729
+ }
2730
+
2175
2731
  // Record telemetry completion event (opt-in only, fails silently)
2176
2732
  telemetry.recordComplete({
2177
2733
  usesPython,