create-qa-architect 5.0.1 → 5.0.7
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/RELEASE_CHECKLIST.md +2 -4
- package/.github/workflows/daily-deploy-check.yml +136 -0
- package/.github/workflows/dependabot-auto-merge.yml +32 -0
- package/.github/workflows/nightly-gitleaks-verification.yml +1 -1
- package/.github/workflows/release.yml +12 -10
- package/.github/workflows/weekly-audit.yml +173 -0
- package/LICENSE +3 -3
- package/README.md +11 -17
- package/config/defaults.js +22 -1
- package/config/quality-config.schema.json +1 -1
- package/create-saas-monetization.js +65 -27
- package/docs/ARCHITECTURE.md +16 -13
- package/docs/DEPLOYMENT.md +1 -2
- package/docs/PREFLIGHT_REPORT.md +100 -0
- package/docs/TESTING.md +4 -6
- package/lib/billing-dashboard.html +6 -12
- package/lib/config-validator.js +8 -2
- package/lib/dependency-monitoring-premium.js +21 -19
- package/lib/github-api.js +249 -0
- package/lib/interactive/questions.js +4 -0
- package/lib/license-validator.js +1 -1
- package/lib/licensing.js +16 -18
- package/lib/package-utils.js +9 -8
- package/lib/project-maturity.js +1 -1
- package/lib/template-loader.js +2 -0
- package/lib/ui-helpers.js +2 -1
- package/lib/validation/base-validator.js +5 -1
- package/lib/validation/cache-manager.js +1 -0
- package/lib/validation/config-security.js +9 -4
- package/lib/validation/validation-factory.js +1 -1
- package/lib/validation/workflow-validation.js +27 -22
- package/lib/yaml-utils.js +15 -10
- package/package.json +17 -14
- package/scripts/check-docs.sh +63 -0
- package/scripts/smart-test-strategy.sh +98 -0
- package/scripts/test-e2e-package.sh +283 -0
- package/scripts/validate-command-patterns.js +112 -0
- package/setup.js +38 -9
- package/templates/QUALITY_TROUBLESHOOTING.md +32 -33
- package/templates/scripts/smart-test-strategy.sh +1 -1
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub API Integration for QA Architect
|
|
3
|
+
* Enables Dependabot alerts and security features via GitHub API
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const https = require('https')
|
|
7
|
+
const { execSync } = require('child_process')
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Get GitHub token from environment or gh CLI
|
|
11
|
+
*/
|
|
12
|
+
function getGitHubToken() {
|
|
13
|
+
// Check environment variable first
|
|
14
|
+
if (process.env.GITHUB_TOKEN) {
|
|
15
|
+
return process.env.GITHUB_TOKEN
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Try to get from gh CLI
|
|
19
|
+
try {
|
|
20
|
+
const token = execSync('gh auth token', { encoding: 'utf8' }).trim()
|
|
21
|
+
if (token) return token
|
|
22
|
+
} catch {
|
|
23
|
+
// gh CLI not available or not authenticated
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get repository info from git remote
|
|
31
|
+
*/
|
|
32
|
+
function getRepoInfo(projectPath = '.') {
|
|
33
|
+
try {
|
|
34
|
+
const remoteUrl = execSync('git remote get-url origin', {
|
|
35
|
+
cwd: projectPath,
|
|
36
|
+
encoding: 'utf8',
|
|
37
|
+
}).trim()
|
|
38
|
+
|
|
39
|
+
// Parse GitHub URL (https or ssh format)
|
|
40
|
+
const httpsMatch = remoteUrl.match(
|
|
41
|
+
/github\.com[/:]([^/]+)\/([^/.]+)(\.git)?$/
|
|
42
|
+
)
|
|
43
|
+
if (httpsMatch) {
|
|
44
|
+
return { owner: httpsMatch[1], repo: httpsMatch[2] }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return null
|
|
48
|
+
} catch {
|
|
49
|
+
return null
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Make GitHub API request
|
|
55
|
+
*/
|
|
56
|
+
function githubRequest(method, path, token, data = null) {
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
const options = {
|
|
59
|
+
hostname: 'api.github.com',
|
|
60
|
+
port: 443,
|
|
61
|
+
path: path,
|
|
62
|
+
method: method,
|
|
63
|
+
headers: {
|
|
64
|
+
Authorization: `Bearer ${token}`,
|
|
65
|
+
Accept: 'application/vnd.github+json',
|
|
66
|
+
'User-Agent': 'qa-architect',
|
|
67
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (data) {
|
|
72
|
+
options.headers['Content-Type'] = 'application/json'
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const req = https.request(options, res => {
|
|
76
|
+
let body = ''
|
|
77
|
+
res.on('data', chunk => (body += chunk))
|
|
78
|
+
res.on('end', () => {
|
|
79
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
80
|
+
resolve({
|
|
81
|
+
status: res.statusCode,
|
|
82
|
+
data: body ? JSON.parse(body) : null,
|
|
83
|
+
})
|
|
84
|
+
} else if (res.statusCode === 204) {
|
|
85
|
+
resolve({ status: 204, data: null })
|
|
86
|
+
} else {
|
|
87
|
+
reject(
|
|
88
|
+
new Error(
|
|
89
|
+
`GitHub API error: ${res.statusCode} - ${body || res.statusMessage}`
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
req.on('error', reject)
|
|
97
|
+
|
|
98
|
+
if (data) {
|
|
99
|
+
req.write(JSON.stringify(data))
|
|
100
|
+
}
|
|
101
|
+
req.end()
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Check if Dependabot alerts are enabled
|
|
107
|
+
*/
|
|
108
|
+
async function checkDependabotStatus(owner, repo, token) {
|
|
109
|
+
try {
|
|
110
|
+
await githubRequest(
|
|
111
|
+
'GET',
|
|
112
|
+
`/repos/${owner}/${repo}/vulnerability-alerts`,
|
|
113
|
+
token
|
|
114
|
+
)
|
|
115
|
+
return true // 204 means enabled
|
|
116
|
+
} catch (error) {
|
|
117
|
+
if (error.message.includes('404')) {
|
|
118
|
+
return false // Not enabled
|
|
119
|
+
}
|
|
120
|
+
throw error
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Enable Dependabot alerts for a repository
|
|
126
|
+
*/
|
|
127
|
+
async function enableDependabotAlerts(owner, repo, token) {
|
|
128
|
+
try {
|
|
129
|
+
await githubRequest(
|
|
130
|
+
'PUT',
|
|
131
|
+
`/repos/${owner}/${repo}/vulnerability-alerts`,
|
|
132
|
+
token
|
|
133
|
+
)
|
|
134
|
+
return { success: true, message: 'Dependabot alerts enabled' }
|
|
135
|
+
} catch (error) {
|
|
136
|
+
return { success: false, message: error.message }
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Enable Dependabot security updates
|
|
142
|
+
*/
|
|
143
|
+
async function enableDependabotSecurityUpdates(owner, repo, token) {
|
|
144
|
+
try {
|
|
145
|
+
await githubRequest(
|
|
146
|
+
'PUT',
|
|
147
|
+
`/repos/${owner}/${repo}/automated-security-fixes`,
|
|
148
|
+
token
|
|
149
|
+
)
|
|
150
|
+
return { success: true, message: 'Dependabot security updates enabled' }
|
|
151
|
+
} catch (error) {
|
|
152
|
+
return { success: false, message: error.message }
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Full setup: Enable all Dependabot features
|
|
158
|
+
*/
|
|
159
|
+
async function setupDependabot(projectPath = '.', options = {}) {
|
|
160
|
+
const { verbose = false } = options
|
|
161
|
+
const results = {
|
|
162
|
+
success: false,
|
|
163
|
+
repoInfo: null,
|
|
164
|
+
alerts: null,
|
|
165
|
+
securityUpdates: null,
|
|
166
|
+
errors: [],
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Get token
|
|
170
|
+
const token = getGitHubToken()
|
|
171
|
+
if (!token) {
|
|
172
|
+
results.errors.push(
|
|
173
|
+
'No GitHub token found. Set GITHUB_TOKEN env var or run `gh auth login`'
|
|
174
|
+
)
|
|
175
|
+
return results
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Get repo info
|
|
179
|
+
const repoInfo = getRepoInfo(projectPath)
|
|
180
|
+
if (!repoInfo) {
|
|
181
|
+
results.errors.push('Could not determine GitHub repository from git remote')
|
|
182
|
+
return results
|
|
183
|
+
}
|
|
184
|
+
results.repoInfo = repoInfo
|
|
185
|
+
|
|
186
|
+
if (verbose) {
|
|
187
|
+
console.log(`📦 Repository: ${repoInfo.owner}/${repoInfo.repo}`)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Check current status
|
|
191
|
+
try {
|
|
192
|
+
const isEnabled = await checkDependabotStatus(
|
|
193
|
+
repoInfo.owner,
|
|
194
|
+
repoInfo.repo,
|
|
195
|
+
token
|
|
196
|
+
)
|
|
197
|
+
if (isEnabled) {
|
|
198
|
+
if (verbose) console.log('✅ Dependabot alerts already enabled')
|
|
199
|
+
results.alerts = { success: true, message: 'Already enabled' }
|
|
200
|
+
} else {
|
|
201
|
+
// Enable alerts
|
|
202
|
+
results.alerts = await enableDependabotAlerts(
|
|
203
|
+
repoInfo.owner,
|
|
204
|
+
repoInfo.repo,
|
|
205
|
+
token
|
|
206
|
+
)
|
|
207
|
+
if (verbose) {
|
|
208
|
+
console.log(
|
|
209
|
+
results.alerts.success
|
|
210
|
+
? '✅ Dependabot alerts enabled'
|
|
211
|
+
: `❌ Failed to enable alerts: ${results.alerts.message}`
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
} catch (error) {
|
|
216
|
+
results.errors.push(`Alerts check failed: ${error.message}`)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Enable security updates
|
|
220
|
+
try {
|
|
221
|
+
results.securityUpdates = await enableDependabotSecurityUpdates(
|
|
222
|
+
repoInfo.owner,
|
|
223
|
+
repoInfo.repo,
|
|
224
|
+
token
|
|
225
|
+
)
|
|
226
|
+
if (verbose) {
|
|
227
|
+
console.log(
|
|
228
|
+
results.securityUpdates.success
|
|
229
|
+
? '✅ Dependabot security updates enabled'
|
|
230
|
+
: `⚠️ Security updates: ${results.securityUpdates.message}`
|
|
231
|
+
)
|
|
232
|
+
}
|
|
233
|
+
} catch (error) {
|
|
234
|
+
results.errors.push(`Security updates failed: ${error.message}`)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
results.success = results.alerts?.success && results.errors.length === 0
|
|
238
|
+
|
|
239
|
+
return results
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
module.exports = {
|
|
243
|
+
getGitHubToken,
|
|
244
|
+
getRepoInfo,
|
|
245
|
+
checkDependabotStatus,
|
|
246
|
+
enableDependabotAlerts,
|
|
247
|
+
enableDependabotSecurityUpdates,
|
|
248
|
+
setupDependabot,
|
|
249
|
+
}
|
package/lib/license-validator.js
CHANGED
|
@@ -26,7 +26,7 @@ class LicenseValidator {
|
|
|
26
26
|
// Allow enterprises to host their own registry
|
|
27
27
|
this.licenseDbUrl =
|
|
28
28
|
process.env.QAA_LICENSE_DB_URL ||
|
|
29
|
-
'https://
|
|
29
|
+
'https://vibebuildlab.com/api/licenses/qa-architect.json'
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
ensureLicenseDir() {
|
package/lib/licensing.js
CHANGED
|
@@ -36,11 +36,11 @@ Object.defineProperty(exports, 'LICENSE_FILE', {
|
|
|
36
36
|
* Standardized to use SCREAMING_SNAKE_CASE for both keys and values
|
|
37
37
|
* for consistency with ErrorCategory and other enums in the codebase.
|
|
38
38
|
*
|
|
39
|
-
* Pricing
|
|
39
|
+
* Pricing:
|
|
40
40
|
* - FREE: $0 (Hobby/OSS - capped)
|
|
41
|
-
* - PRO: $
|
|
42
|
-
* - TEAM:
|
|
43
|
-
* - ENTERPRISE:
|
|
41
|
+
* - PRO: $19/mo or $190/yr (Solo Devs/Small Teams)
|
|
42
|
+
* - TEAM: Contact us (Organizations) - coming soon
|
|
43
|
+
* - ENTERPRISE: Contact us (Large Orgs) - coming soon
|
|
44
44
|
*/
|
|
45
45
|
const LICENSE_TIERS = {
|
|
46
46
|
FREE: 'FREE',
|
|
@@ -216,7 +216,7 @@ function isDeveloperMode() {
|
|
|
216
216
|
if (fs.existsSync(developerMarkerFile)) {
|
|
217
217
|
return true
|
|
218
218
|
}
|
|
219
|
-
} catch
|
|
219
|
+
} catch {
|
|
220
220
|
// Ignore errors checking for marker file
|
|
221
221
|
}
|
|
222
222
|
|
|
@@ -338,7 +338,7 @@ function verifyLicenseSignature(payload, signature) {
|
|
|
338
338
|
Buffer.from(signature),
|
|
339
339
|
Buffer.from(expectedSignature)
|
|
340
340
|
)
|
|
341
|
-
} catch
|
|
341
|
+
} catch {
|
|
342
342
|
// If signature verification fails, treat as invalid
|
|
343
343
|
return false
|
|
344
344
|
}
|
|
@@ -384,7 +384,7 @@ function showUpgradeMessage(feature) {
|
|
|
384
384
|
if (license.tier === LICENSE_TIERS.FREE) {
|
|
385
385
|
console.log('\n🚀 Upgrade to PRO')
|
|
386
386
|
console.log('')
|
|
387
|
-
console.log(' 💰 $
|
|
387
|
+
console.log(' 💰 $19/month or $190/year (save $38)')
|
|
388
388
|
console.log('')
|
|
389
389
|
console.log(' ✅ Unlimited repos, LOC, and runs')
|
|
390
390
|
console.log(' ✅ Smart Test Strategy (70% faster pre-push)')
|
|
@@ -396,16 +396,14 @@ function showUpgradeMessage(feature) {
|
|
|
396
396
|
console.log('')
|
|
397
397
|
console.log(' 🎁 Start 14-day free trial - no credit card required')
|
|
398
398
|
console.log('')
|
|
399
|
-
console.log('🚀 Upgrade: https://vibebuildlab.com/
|
|
399
|
+
console.log('🚀 Upgrade: https://vibebuildlab.com/qa-architect')
|
|
400
400
|
console.log(
|
|
401
401
|
'🔑 Activate: npx create-qa-architect@latest --activate-license'
|
|
402
402
|
)
|
|
403
403
|
} else if (license.tier === LICENSE_TIERS.PRO) {
|
|
404
404
|
console.log('\n👥 Upgrade to TEAM')
|
|
405
405
|
console.log('')
|
|
406
|
-
console.log(
|
|
407
|
-
' 💰 $15/user/month (5-seat min) or $150/user/year (save $30/user)'
|
|
408
|
-
)
|
|
406
|
+
console.log(' 💰 Contact us for Team pricing')
|
|
409
407
|
console.log('')
|
|
410
408
|
console.log(' ✅ All PRO features included')
|
|
411
409
|
console.log(' ✅ Per-seat licensing for your org')
|
|
@@ -414,9 +412,9 @@ function showUpgradeMessage(feature) {
|
|
|
414
412
|
console.log(' ✅ Slack/email alerts for failures')
|
|
415
413
|
console.log(' ✅ Priority support (business hours)')
|
|
416
414
|
console.log('')
|
|
417
|
-
console.log('👥 Upgrade: https://vibebuildlab.com/
|
|
415
|
+
console.log('👥 Upgrade: https://vibebuildlab.com/qa-architect')
|
|
418
416
|
} else if (license.tier === LICENSE_TIERS.TEAM) {
|
|
419
|
-
console.log('\n🏢 Upgrade to ENTERPRISE -
|
|
417
|
+
console.log('\n🏢 Upgrade to ENTERPRISE - Contact us for pricing')
|
|
420
418
|
console.log('')
|
|
421
419
|
console.log(' ✅ All TEAM features included')
|
|
422
420
|
console.log(' ✅ SSO/SAML integration')
|
|
@@ -553,7 +551,7 @@ async function addLegitimateKey(
|
|
|
553
551
|
if (fs.existsSync(legitimateDBFile)) {
|
|
554
552
|
try {
|
|
555
553
|
database = JSON.parse(fs.readFileSync(legitimateDBFile, 'utf8'))
|
|
556
|
-
} catch
|
|
554
|
+
} catch {
|
|
557
555
|
console.error(
|
|
558
556
|
'Warning: Could not parse existing database, creating new one'
|
|
559
557
|
)
|
|
@@ -654,7 +652,7 @@ async function promptLicenseActivation() {
|
|
|
654
652
|
console.log(
|
|
655
653
|
' If you purchased this license, please contact support at:'
|
|
656
654
|
)
|
|
657
|
-
console.log(' Email: support@
|
|
655
|
+
console.log(' Email: support@vibebuildlab.com')
|
|
658
656
|
console.log(
|
|
659
657
|
' Include your license key and purchase email for verification.'
|
|
660
658
|
)
|
|
@@ -745,7 +743,7 @@ function loadUsage() {
|
|
|
745
743
|
|
|
746
744
|
return data
|
|
747
745
|
}
|
|
748
|
-
} catch
|
|
746
|
+
} catch {
|
|
749
747
|
// Ignore errors reading usage file
|
|
750
748
|
}
|
|
751
749
|
|
|
@@ -770,7 +768,7 @@ function saveUsage(usage) {
|
|
|
770
768
|
}
|
|
771
769
|
fs.writeFileSync(getUsageFile(), JSON.stringify(usage, null, 2))
|
|
772
770
|
return true
|
|
773
|
-
} catch
|
|
771
|
+
} catch {
|
|
774
772
|
return false
|
|
775
773
|
}
|
|
776
774
|
}
|
|
@@ -958,7 +956,7 @@ function showLicenseStatus() {
|
|
|
958
956
|
// Show upgrade path
|
|
959
957
|
if (license.tier === LICENSE_TIERS.FREE) {
|
|
960
958
|
console.log('\n💡 Upgrade to PRO for unlimited access + security scanning')
|
|
961
|
-
console.log(' → https://vibebuildlab.com/
|
|
959
|
+
console.log(' → https://vibebuildlab.com/qa-architect')
|
|
962
960
|
}
|
|
963
961
|
}
|
|
964
962
|
|
package/lib/package-utils.js
CHANGED
|
@@ -50,17 +50,17 @@ function mergeDevDependencies(initialDevDeps = {}, defaultDevDeps) {
|
|
|
50
50
|
|
|
51
51
|
/**
|
|
52
52
|
* Merge lint-staged configuration, preserving existing patterns
|
|
53
|
-
* @param {
|
|
54
|
-
* @param {
|
|
55
|
-
* @param {
|
|
56
|
-
* @param {
|
|
57
|
-
* @returns {
|
|
53
|
+
* @param {Record<string, string|string[]>} [existing] - Existing lint-staged config
|
|
54
|
+
* @param {Record<string, string|string[]>} defaults - Default lint-staged config
|
|
55
|
+
* @param {{stylelintTargets?: string[]}} [options] - Merge options
|
|
56
|
+
* @param {(pattern: string) => boolean} [patternChecker] - Function to check if a pattern matches certain criteria
|
|
57
|
+
* @returns {Record<string, string|string[]>} Merged lint-staged config
|
|
58
58
|
*/
|
|
59
59
|
function mergeLintStaged(
|
|
60
|
+
defaults = {},
|
|
60
61
|
existing = {},
|
|
61
|
-
defaults,
|
|
62
62
|
options = {},
|
|
63
|
-
patternChecker =
|
|
63
|
+
patternChecker = _pattern => false
|
|
64
64
|
) {
|
|
65
65
|
const merged = { ...existing }
|
|
66
66
|
const stylelintTargets = options.stylelintTargets || []
|
|
@@ -88,7 +88,8 @@ function mergeLintStaged(
|
|
|
88
88
|
: [merged[pattern]]
|
|
89
89
|
|
|
90
90
|
const newCommands = [...existingCommands]
|
|
91
|
-
|
|
91
|
+
const commandList = Array.isArray(commands) ? commands : [commands]
|
|
92
|
+
commandList.forEach(command => {
|
|
92
93
|
if (!newCommands.includes(command)) {
|
|
93
94
|
newCommands.push(command)
|
|
94
95
|
}
|
package/lib/project-maturity.js
CHANGED
|
@@ -435,7 +435,7 @@ class ProjectMaturityDetector {
|
|
|
435
435
|
|
|
436
436
|
/**
|
|
437
437
|
* Generate GitHub Actions outputs for maturity detection
|
|
438
|
-
* @returns {string} GitHub Actions output format
|
|
438
|
+
* @returns {{maturity: string, sourceCount: number, testCount: number, hasDeps: boolean, hasDocs: boolean, hasCss: boolean, requiredChecks: string, optionalChecks: string, disabledChecks: string}} GitHub Actions output format
|
|
439
439
|
*/
|
|
440
440
|
generateGitHubActionsOutput() {
|
|
441
441
|
const maturity = this.detect()
|
package/lib/template-loader.js
CHANGED
|
@@ -126,6 +126,7 @@ class TemplateLoader {
|
|
|
126
126
|
* // Only loads from whitelisted dirs: config/, .github/, lib/
|
|
127
127
|
*/
|
|
128
128
|
async loadTemplates(dir, baseDir = dir, isPackageDir = false) {
|
|
129
|
+
/** @type {Record<string, string>} */
|
|
129
130
|
const templates = {}
|
|
130
131
|
|
|
131
132
|
try {
|
|
@@ -177,6 +178,7 @@ class TemplateLoader {
|
|
|
177
178
|
* @returns {Promise<Object>} Merged template map
|
|
178
179
|
*/
|
|
179
180
|
async mergeTemplates(customDir, defaultsDir) {
|
|
181
|
+
/** @type {Record<string, string>} */
|
|
180
182
|
const merged = {}
|
|
181
183
|
|
|
182
184
|
// Load defaults first (from package directory - restrict to known template dirs)
|
package/lib/ui-helpers.js
CHANGED
|
@@ -55,7 +55,8 @@ function showProgress(message) {
|
|
|
55
55
|
|
|
56
56
|
// Try to use ora for interactive terminals
|
|
57
57
|
try {
|
|
58
|
-
const
|
|
58
|
+
const oraImport = require('ora')
|
|
59
|
+
const ora = /** @type {any} */ (oraImport.default || oraImport)
|
|
59
60
|
return ora(message).start()
|
|
60
61
|
} catch {
|
|
61
62
|
// Fallback if ora not installed (graceful degradation)
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {Error & {code?: string}} ErrWithCode
|
|
5
|
+
*/
|
|
6
|
+
|
|
3
7
|
/**
|
|
4
8
|
* Base Validator Class
|
|
5
9
|
* Provides common error handling, state management, and validation patterns
|
|
@@ -81,7 +85,7 @@ class BaseValidator {
|
|
|
81
85
|
|
|
82
86
|
/**
|
|
83
87
|
* Format error message for user
|
|
84
|
-
* @param {
|
|
88
|
+
* @param {ErrWithCode} error - The error object
|
|
85
89
|
* @param {string} context - The context
|
|
86
90
|
* @returns {string} Formatted error message
|
|
87
91
|
*/
|
|
@@ -14,6 +14,7 @@ class CacheManager {
|
|
|
14
14
|
options.cacheDir || path.join(process.cwd(), '.create-qa-architect-cache')
|
|
15
15
|
this.ttl = options.ttl || 24 * 60 * 60 * 1000 // Default: 24 hours in milliseconds
|
|
16
16
|
this.enabled = options.enabled !== false // Enable by default
|
|
17
|
+
this.verbose = Boolean(options.verbose)
|
|
17
18
|
|
|
18
19
|
// Ensure cache directory exists
|
|
19
20
|
if (this.enabled) {
|
|
@@ -10,16 +10,19 @@ const { showProgress } = require('../ui-helpers')
|
|
|
10
10
|
|
|
11
11
|
// Pinned gitleaks version for reproducible security scanning
|
|
12
12
|
const GITLEAKS_VERSION = '8.28.0'
|
|
13
|
-
//
|
|
13
|
+
// SHA256 checksums of EXTRACTED BINARIES from gitleaks v8.28.0 release
|
|
14
|
+
// Note: These are checksums of the binary itself, not the tarball/zip archives
|
|
14
15
|
const GITLEAKS_CHECKSUMS = {
|
|
15
16
|
'linux-x64':
|
|
16
|
-
'
|
|
17
|
+
'5fd1b3b0073269484d40078662e921d07427340ab9e6ed526ccd215a565b3298',
|
|
18
|
+
'linux-arm64':
|
|
19
|
+
'3770c7ebeb625e3e96c183525ca18285a01aedef2d75a2c41ceb3e141af2e8b7',
|
|
17
20
|
'darwin-x64':
|
|
18
|
-
'
|
|
21
|
+
'cf09ad7a85683d90221db8324f036f23c8c29107145e1fc4a0dffbfa9e89c09a',
|
|
19
22
|
'darwin-arm64':
|
|
20
23
|
'5588b5d942dffa048720f7e6e1d274283219fb5722a2c7564d22e83ba39087d7',
|
|
21
24
|
'win32-x64':
|
|
22
|
-
'
|
|
25
|
+
'54230c22688d19939f316cd3e2e040cd067ece40a3a8c5b684e5110c62ecbf52',
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
/**
|
|
@@ -199,6 +202,7 @@ class ConfigSecurityScanner {
|
|
|
199
202
|
'darwin-x64': 'darwin_x64',
|
|
200
203
|
'darwin-arm64': 'darwin_arm64',
|
|
201
204
|
'linux-x64': 'linux_x64',
|
|
205
|
+
'linux-arm64': 'linux_arm64',
|
|
202
206
|
'win32-x64': 'windows_x64',
|
|
203
207
|
}
|
|
204
208
|
|
|
@@ -349,6 +353,7 @@ class ConfigSecurityScanner {
|
|
|
349
353
|
timeout: 60000, // 60 second timeout for audit operations
|
|
350
354
|
encoding: 'utf8',
|
|
351
355
|
})
|
|
356
|
+
spinner.succeed('npm audit completed - no high/critical vulnerabilities')
|
|
352
357
|
} catch (error) {
|
|
353
358
|
if (error.signal === 'SIGTERM') {
|
|
354
359
|
// Timeout occurred
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs')
|
|
4
4
|
const path = require('path')
|
|
5
|
-
const { execSync } = require('child_process')
|
|
6
5
|
const { showProgress } = require('../ui-helpers')
|
|
7
6
|
|
|
8
7
|
/**
|
|
@@ -85,32 +84,38 @@ class WorkflowValidator {
|
|
|
85
84
|
const spinner = showProgress('Running actionlint on workflow files...')
|
|
86
85
|
|
|
87
86
|
try {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
87
|
+
const { createLinter } = require('actionlint')
|
|
88
|
+
const workflowFiles = fs
|
|
89
|
+
.readdirSync(workflowDir)
|
|
90
|
+
.filter(file => file.endsWith('.yml') || file.endsWith('.yaml'))
|
|
91
|
+
|
|
92
|
+
const linter = await createLinter()
|
|
93
|
+
let issueCount = 0
|
|
94
|
+
|
|
95
|
+
for (const file of workflowFiles) {
|
|
96
|
+
const filePath = path.join(workflowDir, file)
|
|
97
|
+
const content = fs.readFileSync(filePath, 'utf8')
|
|
98
|
+
const results = linter(content, filePath) || []
|
|
99
|
+
|
|
100
|
+
if (Array.isArray(results) && results.length > 0) {
|
|
101
|
+
issueCount += results.length
|
|
102
|
+
results.forEach(result => {
|
|
103
|
+
this.issues.push(
|
|
104
|
+
`actionlint: ${result.file}:${result.line}:${result.column} ${result.kind} - ${result.message}`
|
|
105
|
+
)
|
|
107
106
|
})
|
|
108
|
-
} else {
|
|
109
|
-
spinner.succeed('actionlint validation passed')
|
|
110
107
|
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (issueCount > 0) {
|
|
111
|
+
spinner.fail(`actionlint found ${issueCount} issue(s)`)
|
|
111
112
|
} else {
|
|
112
113
|
spinner.succeed('actionlint validation passed')
|
|
113
114
|
}
|
|
115
|
+
} catch (error) {
|
|
116
|
+
spinner.fail('actionlint failed to run')
|
|
117
|
+
const reason = error?.message || 'Unknown error'
|
|
118
|
+
this.issues.push(`actionlint: Failed to run - ${reason}`)
|
|
114
119
|
}
|
|
115
120
|
}
|
|
116
121
|
|
package/lib/yaml-utils.js
CHANGED
|
@@ -82,17 +82,22 @@ function convertToYaml(obj, indent = 0) {
|
|
|
82
82
|
if (Array.isArray(obj)) {
|
|
83
83
|
obj.forEach(item => {
|
|
84
84
|
if (typeof item === 'object' && item !== null) {
|
|
85
|
-
// For objects in arrays,
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
yaml +=
|
|
85
|
+
// For objects in arrays, prefix first line with "- " and indent rest
|
|
86
|
+
const entries = Object.entries(item)
|
|
87
|
+
entries.forEach(([key, value], idx) => {
|
|
88
|
+
const safeKey = formatYamlValue(key)
|
|
89
|
+
const prefix = idx === 0 ? `${spaces}- ` : `${spaces} `
|
|
90
|
+
|
|
91
|
+
if (Array.isArray(value)) {
|
|
92
|
+
yaml += `${prefix}${safeKey}:\n`
|
|
93
|
+
yaml += convertToYaml(value, indent + 4)
|
|
94
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
95
|
+
yaml += `${prefix}${safeKey}:\n`
|
|
96
|
+
yaml += convertToYaml(value, indent + 4)
|
|
97
|
+
} else {
|
|
98
|
+
yaml += `${prefix}${safeKey}: ${formatYamlValue(value)}\n`
|
|
94
99
|
}
|
|
95
|
-
}
|
|
100
|
+
})
|
|
96
101
|
} else {
|
|
97
102
|
yaml += `${spaces}- ${formatYamlValue(item)}\n`
|
|
98
103
|
}
|