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/.github/workflows/quality.yml +11 -11
- package/.github/workflows/shell-ci.yml.example +82 -0
- package/.github/workflows/shell-quality.yml.example +148 -0
- package/README.md +120 -12
- package/config/shell-ci.yml +82 -0
- package/config/shell-quality.yml +148 -0
- package/docs/CI-COST-ANALYSIS.md +323 -0
- package/eslint.config.cjs +2 -0
- package/lib/commands/analyze-ci.js +616 -0
- package/lib/commands/deps.js +70 -22
- package/lib/commands/index.js +4 -0
- package/lib/config-validator.js +28 -45
- package/lib/error-reporter.js +1 -1
- package/lib/github-api.js +34 -4
- package/lib/license-signing.js +15 -0
- package/lib/licensing.js +116 -22
- package/lib/package-utils.js +9 -9
- package/lib/project-maturity.js +58 -6
- package/lib/smart-strategy-generator.js +20 -3
- package/lib/telemetry.js +1 -1
- package/lib/ui-helpers.js +1 -1
- package/lib/validation/config-security.js +22 -18
- package/lib/validation/index.js +68 -97
- package/package.json +3 -3
- package/scripts/validate-claude-md.js +80 -0
- package/setup.js +607 -51
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
|
-
//
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
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
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
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
|
-
|
|
958
|
-
|
|
959
|
-
|
|
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
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
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
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
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.
|
|
1039
|
-
console.
|
|
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
|
-
|
|
1076
|
-
|
|
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
|
-
|
|
1095
|
-
|
|
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.
|
|
1439
|
-
'
|
|
1831
|
+
console.error(
|
|
1832
|
+
'\n❌ CRITICAL: Generated .qualityrc.json failed validation'
|
|
1440
1833
|
)
|
|
1441
|
-
|
|
1442
|
-
|
|
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(
|
|
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,
|