create-qa-architect 5.13.6 → 5.14.0
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/.semgrep/defensive-patterns.yaml +356 -0
- package/.semgrep/vibe-audit-rules.yaml +332 -0
- package/LICENSE +192 -39
- package/README.md +98 -46
- package/config/requirements-dev.txt +2 -2
- package/docs/POLAR-DEPLOYMENT.md +157 -0
- package/docs/plans/PLAN-vibe-code-auditor.md +130 -0
- package/docs/plans/POLAR-MIGRATION.md +111 -0
- package/docs/plans/pro-features-2026-05.md +159 -0
- package/lib/commands/analyze-ci.js +20 -11
- package/lib/commands/audit.js +668 -0
- package/lib/commands/ci-doctor.js +341 -0
- package/lib/commands/history-scan.js +342 -0
- package/lib/commands/index.js +8 -0
- package/lib/commands/pr-check.js +484 -0
- package/lib/commands/prelaunch-setup.js +4 -0
- package/lib/commands/ship-check.js +570 -0
- package/lib/license-validator.js +200 -6
- package/lib/licensing.js +99 -11
- package/package.json +7 -6
- package/scripts/deploy-consumers.sh +10 -5
- package/scripts/risk-policy-gate.js +410 -0
- package/setup.js +132 -4
- /package/docs/{STRIPE-LIVE-MODE-DEPLOYMENT.md → _archive/STRIPE-LIVE-MODE-DEPLOYMENT.md} +0 -0
- /package/lib/{billing-dashboard.html → _archive/billing-dashboard.html} +0 -0
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Risk Policy Gate - Carson's Code Factory Pattern
|
|
5
|
+
*
|
|
6
|
+
* Validates PR changes against risk-aware merge policy before expensive CI.
|
|
7
|
+
* Implements Carson's "gate preflight before expensive CI" pattern.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs')
|
|
11
|
+
const path = require('path')
|
|
12
|
+
const { execFileSync } = require('child_process')
|
|
13
|
+
const picomatch = require('picomatch')
|
|
14
|
+
|
|
15
|
+
// Load harness configuration
|
|
16
|
+
const CONFIG_PATH = path.join(__dirname, '..', 'harness-config.json')
|
|
17
|
+
|
|
18
|
+
function loadConfig() {
|
|
19
|
+
if (!fs.existsSync(CONFIG_PATH)) {
|
|
20
|
+
console.error('❌ harness-config.json not found')
|
|
21
|
+
process.exit(1)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'))
|
|
26
|
+
} catch (error) {
|
|
27
|
+
console.error('❌ Invalid harness-config.json:', error.message)
|
|
28
|
+
process.exit(1)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Default git runner — uses execFileSync (no shell, no concat). Returns trimmed
|
|
34
|
+
* stdout, throws with `.failed=true` on non-zero exit. Tests inject their own.
|
|
35
|
+
*/
|
|
36
|
+
function defaultGitRunner(args) {
|
|
37
|
+
try {
|
|
38
|
+
return execFileSync('git', args, {
|
|
39
|
+
encoding: 'utf8',
|
|
40
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
41
|
+
}).trim()
|
|
42
|
+
} catch (error) {
|
|
43
|
+
const err = new Error(`git ${args.join(' ')} failed: ${error.message}`)
|
|
44
|
+
err.failed = true
|
|
45
|
+
throw err
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Resolve the base ref for diffing against HEAD.
|
|
51
|
+
*
|
|
52
|
+
* Algorithm (pure — only the injected runner touches git):
|
|
53
|
+
* 1. CI path: GITHUB_BASE_REF + GITHUB_HEAD_REF set → `origin/<base>`
|
|
54
|
+
* 2. CLI: --base <ref> → use it; fail closed if not resolvable
|
|
55
|
+
* 3. HEAD detached → fail closed
|
|
56
|
+
* 4. Try in order: origin/main, origin/master, main, master
|
|
57
|
+
* 5. None resolvable → fail closed
|
|
58
|
+
*
|
|
59
|
+
* Returns { mode: 'ci'|'local', base: '<ref>' }
|
|
60
|
+
* Throws Error with `.reason` for any fail-closed condition.
|
|
61
|
+
*/
|
|
62
|
+
function resolveBase({
|
|
63
|
+
env = process.env,
|
|
64
|
+
baseArg = null,
|
|
65
|
+
gitRunner = defaultGitRunner,
|
|
66
|
+
} = {}) {
|
|
67
|
+
// Step 1: CI path
|
|
68
|
+
if (env.GITHUB_BASE_REF && env.GITHUB_HEAD_REF) {
|
|
69
|
+
return { mode: 'ci', base: `origin/${env.GITHUB_BASE_REF}` }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Step 2: explicit --base
|
|
73
|
+
if (baseArg) {
|
|
74
|
+
if (!refExists(baseArg, gitRunner)) {
|
|
75
|
+
const err = new Error(`--base ${baseArg} is not resolvable in this repo`)
|
|
76
|
+
err.reason = 'base-not-resolvable'
|
|
77
|
+
throw err
|
|
78
|
+
}
|
|
79
|
+
return { mode: 'local', base: baseArg }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Step 3: detached HEAD
|
|
83
|
+
if (isHeadDetached(gitRunner)) {
|
|
84
|
+
const err = new Error('HEAD is detached; pass --base <ref> explicitly')
|
|
85
|
+
err.reason = 'detached-head'
|
|
86
|
+
throw err
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Step 4: candidate base order
|
|
90
|
+
const candidates = ['origin/main', 'origin/master', 'main', 'master']
|
|
91
|
+
for (const ref of candidates) {
|
|
92
|
+
if (refExists(ref, gitRunner)) {
|
|
93
|
+
return { mode: 'local', base: ref }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Step 5: nothing resolved
|
|
98
|
+
const err = new Error(
|
|
99
|
+
'No base ref found (tried origin/main, origin/master, main, master); pass --base explicitly'
|
|
100
|
+
)
|
|
101
|
+
err.reason = 'no-base'
|
|
102
|
+
throw err
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function refExists(ref, gitRunner) {
|
|
106
|
+
try {
|
|
107
|
+
gitRunner(['rev-parse', '--verify', '--quiet', `${ref}^{commit}`])
|
|
108
|
+
return true
|
|
109
|
+
} catch {
|
|
110
|
+
return false
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function isHeadDetached(gitRunner) {
|
|
115
|
+
try {
|
|
116
|
+
gitRunner(['symbolic-ref', '--quiet', 'HEAD'])
|
|
117
|
+
return false
|
|
118
|
+
} catch {
|
|
119
|
+
return true
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Step 6: merge-base + Step 7: union of branch diff + staged + unstaged.
|
|
125
|
+
* Throws with `.reason='no-merge-base'` when merge-base cannot be computed
|
|
126
|
+
* (unrelated history or shallow clone too short).
|
|
127
|
+
*/
|
|
128
|
+
function getChangedFilesForBase(base, gitRunner = defaultGitRunner) {
|
|
129
|
+
let mergeBase
|
|
130
|
+
try {
|
|
131
|
+
mergeBase = gitRunner(['merge-base', 'HEAD', base])
|
|
132
|
+
} catch {
|
|
133
|
+
const err = new Error(
|
|
134
|
+
`Could not compute merge-base between HEAD and ${base} ` +
|
|
135
|
+
'(unrelated history or shallow clone too short); deepen clone or pass --base explicitly'
|
|
136
|
+
)
|
|
137
|
+
err.reason = 'no-merge-base'
|
|
138
|
+
throw err
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (!mergeBase) {
|
|
142
|
+
const err = new Error(`merge-base HEAD ${base} returned empty`)
|
|
143
|
+
err.reason = 'no-merge-base'
|
|
144
|
+
throw err
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const branch = gitRunner(['diff', '--name-only', `${mergeBase}...HEAD`])
|
|
148
|
+
const staged = gitRunner(['diff', '--cached', '--name-only'])
|
|
149
|
+
const unstaged = gitRunner(['diff', '--name-only'])
|
|
150
|
+
|
|
151
|
+
return [
|
|
152
|
+
...new Set(
|
|
153
|
+
[branch, staged, unstaged]
|
|
154
|
+
.flatMap(out => out.split('\n'))
|
|
155
|
+
.filter(f => f.length > 0)
|
|
156
|
+
),
|
|
157
|
+
]
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Top-level wrapper used by main(). Resolves base + diffs in one call.
|
|
162
|
+
* Any failure throws an Error with `.reason` — caller decides exit message.
|
|
163
|
+
*/
|
|
164
|
+
function getChangedFiles({
|
|
165
|
+
env = process.env,
|
|
166
|
+
baseArg = null,
|
|
167
|
+
gitRunner = defaultGitRunner,
|
|
168
|
+
} = {}) {
|
|
169
|
+
const resolved = resolveBase({ env, baseArg, gitRunner })
|
|
170
|
+
const files = getChangedFilesForBase(resolved.base, gitRunner)
|
|
171
|
+
return { files, resolved }
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function parseCliArgs(argv) {
|
|
175
|
+
const result = { baseArg: null }
|
|
176
|
+
for (let i = 0; i < argv.length; i++) {
|
|
177
|
+
if (argv[i] === '--base' && i + 1 < argv.length) {
|
|
178
|
+
result.baseArg = argv[i + 1]
|
|
179
|
+
i++
|
|
180
|
+
} else if (argv[i].startsWith('--base=')) {
|
|
181
|
+
result.baseArg = argv[i].slice('--base='.length)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return result
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const matcherCache = new Map()
|
|
188
|
+
function getMatcher(pattern) {
|
|
189
|
+
let matcher = matcherCache.get(pattern)
|
|
190
|
+
if (!matcher) {
|
|
191
|
+
try {
|
|
192
|
+
matcher = picomatch(pattern, { dot: true })
|
|
193
|
+
} catch {
|
|
194
|
+
console.warn(`Invalid pattern: ${pattern}`)
|
|
195
|
+
matcher = () => false
|
|
196
|
+
}
|
|
197
|
+
matcherCache.set(pattern, matcher)
|
|
198
|
+
}
|
|
199
|
+
return matcher
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function matchesPattern(filepath, patterns) {
|
|
203
|
+
return patterns.some(pattern => getMatcher(pattern)(filepath))
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function calculateRiskTier(filepath, config) {
|
|
207
|
+
const { riskTierRules } = config
|
|
208
|
+
|
|
209
|
+
if (!riskTierRules || typeof riskTierRules !== 'object') {
|
|
210
|
+
return 'low'
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Check in order of decreasing risk - use allowlist of known tiers
|
|
214
|
+
const validTiers = ['critical', 'high', 'medium', 'low']
|
|
215
|
+
for (const tier of validTiers) {
|
|
216
|
+
if (riskTierRules[tier] && Array.isArray(riskTierRules[tier])) {
|
|
217
|
+
if (matchesPattern(filepath, riskTierRules[tier])) {
|
|
218
|
+
return tier
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return 'low' // default
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function validateRequiredChecks(riskTier, config) {
|
|
227
|
+
if (!config.mergePolicy) {
|
|
228
|
+
return {
|
|
229
|
+
valid: false,
|
|
230
|
+
error: 'No mergePolicy defined in config',
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const policy = config.mergePolicy[riskTier]
|
|
235
|
+
if (!policy) {
|
|
236
|
+
return {
|
|
237
|
+
valid: false,
|
|
238
|
+
error: `No merge policy defined for risk tier: ${riskTier}`,
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const { requiredChecks } = policy
|
|
243
|
+
const missingChecks = []
|
|
244
|
+
|
|
245
|
+
for (const check of requiredChecks) {
|
|
246
|
+
if (!config.checkDefinitions[check]) {
|
|
247
|
+
missingChecks.push(check)
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (missingChecks.length > 0) {
|
|
252
|
+
return {
|
|
253
|
+
valid: false,
|
|
254
|
+
error: `Missing check definitions: ${missingChecks.join(', ')}`,
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return { valid: true }
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function analyzeRisks(changedFiles, config) {
|
|
262
|
+
const riskOrder = ['low', 'medium', 'high', 'critical']
|
|
263
|
+
const riskAnalysis = {}
|
|
264
|
+
let highestRisk = 'low'
|
|
265
|
+
|
|
266
|
+
for (const file of changedFiles) {
|
|
267
|
+
const risk = calculateRiskTier(file, config)
|
|
268
|
+
if (!riskAnalysis[risk]) riskAnalysis[risk] = []
|
|
269
|
+
riskAnalysis[risk].push(file)
|
|
270
|
+
|
|
271
|
+
if (riskOrder.indexOf(risk) > riskOrder.indexOf(highestRisk)) {
|
|
272
|
+
highestRisk = risk
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return { riskAnalysis, highestRisk }
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function printRiskAnalysis(riskAnalysis) {
|
|
280
|
+
const TIER_EMOJI = { critical: '🔴', high: '🟠', medium: '🟡', low: '🟢' }
|
|
281
|
+
console.log('📊 Risk Analysis:')
|
|
282
|
+
for (const tier of ['critical', 'high', 'medium', 'low']) {
|
|
283
|
+
const files = riskAnalysis[tier]
|
|
284
|
+
if (!files || files.length === 0) continue
|
|
285
|
+
console.log(
|
|
286
|
+
` ${TIER_EMOJI[tier]} ${tier.toUpperCase()}: ${files.length} files`
|
|
287
|
+
)
|
|
288
|
+
const preview = files.length <= 3 ? files : files.slice(0, 2)
|
|
289
|
+
preview.forEach(f => console.log(` - ${f}`))
|
|
290
|
+
if (files.length > 3) console.log(` ... and ${files.length - 2} more`)
|
|
291
|
+
}
|
|
292
|
+
console.log('')
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function printPolicyRequirements(highestRisk, policy, config) {
|
|
296
|
+
console.log(`🎯 Merge Policy: ${highestRisk.toUpperCase()} tier requirements`)
|
|
297
|
+
console.log(' Required checks:')
|
|
298
|
+
policy.requiredChecks.forEach(check => {
|
|
299
|
+
const def = config.checkDefinitions[check]
|
|
300
|
+
console.log(` ✓ ${check} (${def.description})`)
|
|
301
|
+
})
|
|
302
|
+
console.log(` Review requirement: ${policy.reviewRequirement}`)
|
|
303
|
+
console.log(` Evidence requirement: ${policy.evidenceRequirement}`)
|
|
304
|
+
console.log('')
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function checkDocsDrift(changedFiles, config) {
|
|
308
|
+
if (!config.docsDriftRules?.enabled) return
|
|
309
|
+
const affected = changedFiles.filter(file =>
|
|
310
|
+
config.docsDriftRules.watchPaths.some(pattern =>
|
|
311
|
+
matchesPattern(file, [pattern])
|
|
312
|
+
)
|
|
313
|
+
)
|
|
314
|
+
if (affected.length === 0) return
|
|
315
|
+
console.log('📝 Docs drift check:')
|
|
316
|
+
console.log(' Changed files that may require doc updates:')
|
|
317
|
+
affected.forEach(file => console.log(` - ${file}`))
|
|
318
|
+
console.log(' Required updates:')
|
|
319
|
+
config.docsDriftRules.requiredUpdates.forEach(path =>
|
|
320
|
+
console.log(` - ${path}`)
|
|
321
|
+
)
|
|
322
|
+
console.log('')
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function writeGithubOutput(summary) {
|
|
326
|
+
const outputPath = process.env.GITHUB_OUTPUT
|
|
327
|
+
if (!outputPath || typeof outputPath !== 'string' || outputPath.length === 0)
|
|
328
|
+
return
|
|
329
|
+
try {
|
|
330
|
+
Object.entries(summary).forEach(([key, value]) => {
|
|
331
|
+
fs.appendFileSync(outputPath, `${key}=${value}\n`, { encoding: 'utf8' })
|
|
332
|
+
})
|
|
333
|
+
} catch (error) {
|
|
334
|
+
console.warn('Failed to write GitHub Actions output:', error.message)
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function main() {
|
|
339
|
+
console.log('🔍 Risk Policy Gate - Validating PR changes...\n')
|
|
340
|
+
|
|
341
|
+
const config = loadConfig()
|
|
342
|
+
const { baseArg } = parseCliArgs(process.argv.slice(2))
|
|
343
|
+
|
|
344
|
+
let changedFiles
|
|
345
|
+
let resolved
|
|
346
|
+
try {
|
|
347
|
+
;({ files: changedFiles, resolved } = getChangedFiles({ baseArg }))
|
|
348
|
+
} catch (error) {
|
|
349
|
+
// Fail-closed for any base-resolution / merge-base failure.
|
|
350
|
+
console.error(`❌ Failed to determine changed files: ${error.message}`)
|
|
351
|
+
if (error.reason) {
|
|
352
|
+
console.error(` Reason: ${error.reason}`)
|
|
353
|
+
}
|
|
354
|
+
process.exit(1)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
console.log(`🔎 Base resolution: mode=${resolved.mode} base=${resolved.base}`)
|
|
358
|
+
|
|
359
|
+
if (changedFiles.length === 0) {
|
|
360
|
+
console.log('✅ No changed files detected - policy gate passed')
|
|
361
|
+
return
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
console.log(`📁 Changed files (${changedFiles.length}):`)
|
|
365
|
+
changedFiles.forEach(file => console.log(` ${file}`))
|
|
366
|
+
console.log('')
|
|
367
|
+
|
|
368
|
+
const { riskAnalysis, highestRisk } = analyzeRisks(changedFiles, config)
|
|
369
|
+
printRiskAnalysis(riskAnalysis)
|
|
370
|
+
|
|
371
|
+
const validation = validateRequiredChecks(highestRisk, config)
|
|
372
|
+
if (!validation.valid) {
|
|
373
|
+
console.error(`❌ Policy validation failed: ${validation.error}`)
|
|
374
|
+
process.exit(1)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const policy = config.mergePolicy[highestRisk]
|
|
378
|
+
printPolicyRequirements(highestRisk, policy, config)
|
|
379
|
+
checkDocsDrift(changedFiles, config)
|
|
380
|
+
|
|
381
|
+
if (process.env.GITHUB_OUTPUT) {
|
|
382
|
+
writeGithubOutput({
|
|
383
|
+
highestRisk,
|
|
384
|
+
requiredChecks: policy.requiredChecks.join(','),
|
|
385
|
+
reviewRequired: policy.reviewRequirement !== 'none',
|
|
386
|
+
changedFileCount: changedFiles.length,
|
|
387
|
+
resolvedBase: resolved.base,
|
|
388
|
+
resolutionMode: resolved.mode,
|
|
389
|
+
})
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
console.log('✅ Risk policy gate passed')
|
|
393
|
+
console.log(
|
|
394
|
+
`📈 Proceeding with ${highestRisk.toUpperCase()} tier requirements`
|
|
395
|
+
)
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (require.main === module) {
|
|
399
|
+
main()
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
module.exports = {
|
|
403
|
+
calculateRiskTier,
|
|
404
|
+
validateRequiredChecks,
|
|
405
|
+
matchesPattern,
|
|
406
|
+
resolveBase,
|
|
407
|
+
getChangedFilesForBase,
|
|
408
|
+
getChangedFiles,
|
|
409
|
+
parseCliArgs,
|
|
410
|
+
}
|
package/setup.js
CHANGED
|
@@ -151,6 +151,7 @@ const {
|
|
|
151
151
|
showUpgradeMessage,
|
|
152
152
|
checkUsageCaps,
|
|
153
153
|
incrementUsage,
|
|
154
|
+
ensureLicenseFresh,
|
|
154
155
|
} = require('./lib/licensing')
|
|
155
156
|
|
|
156
157
|
// Smart Test Strategy Generator (Pro/Team/Enterprise feature)
|
|
@@ -455,6 +456,77 @@ const validateAndSanitizeInput = input => {
|
|
|
455
456
|
return sanitized
|
|
456
457
|
}
|
|
457
458
|
|
|
459
|
+
/**
|
|
460
|
+
* Detect which (if any) Pro release-confidence command was invoked.
|
|
461
|
+
* Returns the command name, or null if none.
|
|
462
|
+
*/
|
|
463
|
+
function detectProCommand(sanitizedArgs) {
|
|
464
|
+
if (sanitizedArgs.includes('--ship-check')) return 'ship-check'
|
|
465
|
+
if (sanitizedArgs.includes('--pr-check')) return 'pr-check'
|
|
466
|
+
if (sanitizedArgs.includes('--history-scan')) return 'history-scan'
|
|
467
|
+
if (sanitizedArgs.includes('--audit')) return 'audit'
|
|
468
|
+
return null
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Dispatch to the matching Pro command handler. Always exits the process
|
|
473
|
+
* (the handlers manage their own exit codes).
|
|
474
|
+
*/
|
|
475
|
+
async function runProCommand(command, sanitizedArgs, rawArgs) {
|
|
476
|
+
try {
|
|
477
|
+
const cmdOptions = parseProCommandOptions(sanitizedArgs, rawArgs)
|
|
478
|
+
if (command === 'ship-check') {
|
|
479
|
+
const { handleShipCheck } = require('./lib/commands/ship-check')
|
|
480
|
+
await handleShipCheck(cmdOptions)
|
|
481
|
+
} else if (command === 'pr-check') {
|
|
482
|
+
const { handlePrCheck } = require('./lib/commands/pr-check')
|
|
483
|
+
await handlePrCheck(cmdOptions)
|
|
484
|
+
} else if (command === 'audit') {
|
|
485
|
+
const { handleAudit } = require('./lib/commands/audit')
|
|
486
|
+
await handleAudit(cmdOptions)
|
|
487
|
+
} else {
|
|
488
|
+
const { handleHistoryScan } = require('./lib/commands/history-scan')
|
|
489
|
+
await handleHistoryScan(cmdOptions)
|
|
490
|
+
}
|
|
491
|
+
process.exit(0)
|
|
492
|
+
} catch (error) {
|
|
493
|
+
console.error(`Command error: ${error.message}`)
|
|
494
|
+
process.exit(1)
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Extract options for Pro release-confidence commands (ship-check, pr-check,
|
|
500
|
+
* history-scan). Kept separate from parseArguments() to avoid bloating the
|
|
501
|
+
* main parser's cyclomatic complexity.
|
|
502
|
+
*
|
|
503
|
+
* @param {string[]} sanitizedArgs - Args after validateAndSanitizeInput
|
|
504
|
+
* @param {string[]} rawArgs - Original args (for path values that may include `..`)
|
|
505
|
+
* @returns {{json:boolean, skipTests:boolean, noFail:boolean, base:string|null, depth:string|null, outPath:string|null}}
|
|
506
|
+
*/
|
|
507
|
+
function parseProCommandOptions(sanitizedArgs, rawArgs) {
|
|
508
|
+
const pickValue = (flag, source) => {
|
|
509
|
+
const idx = source.findIndex(arg => arg === flag)
|
|
510
|
+
if (idx === -1) return null
|
|
511
|
+
return source[idx + 1] || null
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const baseValue = pickValue('--base', sanitizedArgs)
|
|
515
|
+
const depthValue = pickValue('--depth', sanitizedArgs)
|
|
516
|
+
const outValue = pickValue('--out', rawArgs)
|
|
517
|
+
|
|
518
|
+
return {
|
|
519
|
+
json: sanitizedArgs.includes('--json'),
|
|
520
|
+
skipTests: sanitizedArgs.includes('--skip-tests'),
|
|
521
|
+
noFail: sanitizedArgs.includes('--no-fail'),
|
|
522
|
+
fix: sanitizedArgs.includes('--fix'),
|
|
523
|
+
base: baseValue,
|
|
524
|
+
depth: depthValue,
|
|
525
|
+
outPath: outValue ? path.resolve(outValue) : null,
|
|
526
|
+
projectPath: process.cwd(),
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
458
530
|
/**
|
|
459
531
|
* Parse CLI arguments and return configuration object
|
|
460
532
|
* @param {string[]} rawArgs - Raw command line arguments
|
|
@@ -711,6 +783,35 @@ WORKFLOW TIERS (GitHub Actions optimization):
|
|
|
711
783
|
--matrix Enable Node.js version matrix testing (20 + 22)
|
|
712
784
|
Use for npm libraries/CLI tools that support multiple Node versions
|
|
713
785
|
--analyze-ci Analyze GitHub Actions usage and get optimization tips (Pro)
|
|
786
|
+
--analyze-ci --doctor Add CI Doctor: flaky tests, duplicated jobs, waste detection (Pro)
|
|
787
|
+
|
|
788
|
+
VIBE-CODE SECURITY AUDIT (Free):
|
|
789
|
+
--audit Scan codebase for security vulnerabilities in AI-generated code
|
|
790
|
+
Runs semgrep SAST (injection, auth, XSS, misconfigs) + npm CVE audit
|
|
791
|
+
Output: Critical/High/Medium/Low findings with file:line + fix guidance
|
|
792
|
+
--audit --json Emit JSON output (for CI integration)
|
|
793
|
+
--audit --out <path> Write markdown report to file (PR-comment-ready)
|
|
794
|
+
--audit --no-fail Always exit 0 (report-only, don't block CI)
|
|
795
|
+
|
|
796
|
+
AUDIT PRO (Pro):
|
|
797
|
+
--audit --fix Generate Claude Code prompts for each Critical/High finding
|
|
798
|
+
(paste directly into Claude Code to fix issues one by one)
|
|
799
|
+
|
|
800
|
+
Requires: semgrep (pip install semgrep / brew install semgrep)
|
|
801
|
+
Pro also adds: hallucinated package detection (npm registry check)
|
|
802
|
+
|
|
803
|
+
RELEASE CONFIDENCE (Pro):
|
|
804
|
+
--ship-check Unified release-readiness report (lint, tests, security,
|
|
805
|
+
coverage, bundle, env, CI cost, docs) with SHIP/REVIEW/BLOCK verdict
|
|
806
|
+
--pr-check Diff-aware risk classifier — flags high-risk file changes and
|
|
807
|
+
missing tests, emits PR-comment-ready markdown
|
|
808
|
+
--history-scan Full git-history secrets audit (gitleaks --log-opts=--all)
|
|
809
|
+
--base <branch> Base branch for --pr-check (default: main, falls back to master)
|
|
810
|
+
--depth <N> Limit --history-scan to last N commits (default: full history)
|
|
811
|
+
--skip-tests Skip running tests in --ship-check (e.g. when slow)
|
|
812
|
+
--json Emit JSON output for --ship-check/--pr-check/--history-scan
|
|
813
|
+
--out <path> Write markdown report to <path> (PR-comment-ready)
|
|
814
|
+
--no-fail Always exit 0 from --pr-check (report-only mode)
|
|
714
815
|
|
|
715
816
|
VALIDATION OPTIONS:
|
|
716
817
|
--validate Run comprehensive validation (same as --comprehensive)
|
|
@@ -723,7 +824,7 @@ VALIDATION OPTIONS:
|
|
|
723
824
|
|
|
724
825
|
LICENSE, TELEMETRY & ERROR REPORTING:
|
|
725
826
|
--license-status Show current license tier and available features
|
|
726
|
-
--activate-license Activate Pro license key from
|
|
827
|
+
--activate-license Activate Pro license key from purchase
|
|
727
828
|
--telemetry-status Show telemetry status and opt-in instructions
|
|
728
829
|
--error-reporting-status Show error reporting status and privacy information
|
|
729
830
|
|
|
@@ -750,7 +851,7 @@ EXAMPLES:
|
|
|
750
851
|
→ Show current license tier and upgrade options
|
|
751
852
|
|
|
752
853
|
npx create-qa-architect@latest --activate-license
|
|
753
|
-
→ Activate Pro license after
|
|
854
|
+
→ Activate Pro license after purchase
|
|
754
855
|
|
|
755
856
|
npx create-qa-architect@latest --telemetry-status
|
|
756
857
|
→ Show telemetry status and privacy information
|
|
@@ -788,6 +889,19 @@ EXAMPLES:
|
|
|
788
889
|
npx create-qa-architect@latest --update --workflow-minimal
|
|
789
890
|
→ Convert existing comprehensive workflow to minimal (reduce CI costs)
|
|
790
891
|
|
|
892
|
+
npx create-qa-architect@latest --audit
|
|
893
|
+
→ Scan for security vulnerabilities in AI-generated code (free)
|
|
894
|
+
→ Requires: semgrep (pip install semgrep)
|
|
895
|
+
|
|
896
|
+
npx create-qa-architect@latest --audit --out audit-report.md
|
|
897
|
+
→ Run audit and write markdown report to file (for PR comments or docs)
|
|
898
|
+
|
|
899
|
+
npx create-qa-architect@latest --audit --json
|
|
900
|
+
→ Emit JSON output for CI integration or tooling
|
|
901
|
+
|
|
902
|
+
npx create-qa-architect@latest --audit --fix
|
|
903
|
+
→ Run audit + generate Claude Code prompts for each finding (Pro)
|
|
904
|
+
|
|
791
905
|
npx create-qa-architect@latest --analyze-ci
|
|
792
906
|
→ Analyze your GitHub Actions usage and get cost optimization recommendations (Pro)
|
|
793
907
|
|
|
@@ -805,6 +919,13 @@ HELP:
|
|
|
805
919
|
process.exit(0)
|
|
806
920
|
}
|
|
807
921
|
|
|
922
|
+
// Pro release-confidence commands must run BEFORE handleDryRun() so they
|
|
923
|
+
// don't get a "🚀 Setting up Quality Automation..." banner in their output.
|
|
924
|
+
const proCommand = detectProCommand(sanitizedArgs)
|
|
925
|
+
if (proCommand) {
|
|
926
|
+
return runProCommand(proCommand, sanitizedArgs, args)
|
|
927
|
+
}
|
|
928
|
+
|
|
808
929
|
// Handle dry-run mode and show mode banner
|
|
809
930
|
handleDryRun({ isDryRun, isUpdateMode, isDependencyMonitoringMode })
|
|
810
931
|
|
|
@@ -826,12 +947,13 @@ HELP:
|
|
|
826
947
|
handleMaturityCheck()
|
|
827
948
|
}
|
|
828
949
|
|
|
829
|
-
// Handle CI cost analysis command
|
|
950
|
+
// Handle CI cost analysis command (with optional --doctor expansion)
|
|
830
951
|
if (isAnalyzeCiMode) {
|
|
831
952
|
return (async () => {
|
|
832
953
|
try {
|
|
833
954
|
const { handleAnalyzeCi } = require('./lib/commands/analyze-ci')
|
|
834
|
-
|
|
955
|
+
const doctor = sanitizedArgs.includes('--doctor')
|
|
956
|
+
await handleAnalyzeCi({ doctor })
|
|
835
957
|
process.exit(0)
|
|
836
958
|
} catch (error) {
|
|
837
959
|
console.error('CI cost analysis error:', error.message)
|
|
@@ -1134,6 +1256,12 @@ HELP:
|
|
|
1134
1256
|
process.exit(1)
|
|
1135
1257
|
}
|
|
1136
1258
|
|
|
1259
|
+
// Re-check the signed registry before any Pro feature lookup (quality
|
|
1260
|
+
// tooling, Smart Test Strategy, etc.) so a revoked/cancelled subscription
|
|
1261
|
+
// stops unlocking Pro here too — not only on the standalone Pro commands.
|
|
1262
|
+
// Fails open offline; only a fresh, signature-verified fetch downgrades.
|
|
1263
|
+
await ensureLicenseFresh()
|
|
1264
|
+
|
|
1137
1265
|
// Enforce FREE tier repo limit (1 private repo)
|
|
1138
1266
|
// Must happen before any file modifications
|
|
1139
1267
|
const license = getLicenseInfo()
|
|
File without changes
|
|
File without changes
|