create-qa-architect 5.0.0 → 5.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/.github/RELEASE_CHECKLIST.md +2 -4
  2. package/.github/workflows/daily-deploy-check.yml +136 -0
  3. package/.github/workflows/nightly-gitleaks-verification.yml +1 -1
  4. package/.github/workflows/quality.yml +3 -0
  5. package/.github/workflows/release.yml +12 -10
  6. package/.github/workflows/weekly-audit.yml +173 -0
  7. package/LICENSE +66 -0
  8. package/README.md +44 -30
  9. package/config/defaults.js +22 -1
  10. package/config/quality-config.schema.json +1 -1
  11. package/create-saas-monetization.js +75 -27
  12. package/docs/ARCHITECTURE.md +53 -0
  13. package/docs/DEPLOYMENT.md +62 -0
  14. package/docs/PREFLIGHT_REPORT.md +108 -0
  15. package/docs/SLA_GATES.md +28 -0
  16. package/docs/TESTING.md +61 -0
  17. package/docs/security/SOC2_STARTER.md +29 -0
  18. package/lib/config-validator.js +8 -2
  19. package/lib/dependency-monitoring-basic.js +73 -26
  20. package/lib/dependency-monitoring-premium.js +21 -19
  21. package/lib/github-api.js +249 -0
  22. package/lib/interactive/questions.js +4 -0
  23. package/lib/license-validator.js +1 -1
  24. package/lib/licensing.js +11 -10
  25. package/lib/package-utils.js +224 -8
  26. package/lib/project-maturity.js +1 -1
  27. package/lib/setup-enhancements.js +33 -0
  28. package/lib/template-loader.js +2 -0
  29. package/lib/ui-helpers.js +2 -1
  30. package/lib/validation/base-validator.js +5 -1
  31. package/lib/validation/cache-manager.js +1 -0
  32. package/lib/validation/config-security.js +5 -4
  33. package/lib/validation/validation-factory.js +1 -1
  34. package/lib/yaml-utils.js +15 -10
  35. package/package.json +18 -13
  36. package/scripts/check-docs.sh +63 -0
  37. package/scripts/smart-test-strategy.sh +98 -0
  38. package/scripts/test-e2e-package.sh +283 -0
  39. package/scripts/validate-command-patterns.js +112 -0
  40. package/setup.js +161 -44
  41. package/templates/QUALITY_TROUBLESHOOTING.md +403 -0
  42. package/templates/ci/circleci-config.yml +35 -0
  43. package/templates/ci/gitlab-ci.yml +47 -0
  44. package/templates/integration-tests/api-service.test.js +244 -0
  45. package/templates/integration-tests/frontend-app.test.js +267 -0
  46. package/templates/scripts/smart-test-strategy.sh +109 -0
  47. package/templates/test-stubs/e2e.smoke.test.js +12 -0
  48. package/templates/test-stubs/unit.test.js +7 -0
  49. package/legal/README.md +0 -106
  50. package/legal/copyright.md +0 -76
  51. package/legal/disclaimer.md +0 -146
  52. package/legal/privacy-policy.html +0 -324
  53. package/legal/privacy-policy.md +0 -196
  54. package/legal/terms-of-service.md +0 -224
  55. package/marketing/beta-user-email-campaign.md +0 -372
  56. package/marketing/landing-page.html +0 -721
@@ -289,7 +289,7 @@ function matchesPattern(depName, pattern) {
289
289
  if (!regex) {
290
290
  // Cache miss - compile and store
291
291
  const regexPattern = pattern.replace(/\*/g, '.*')
292
- // eslint-disable-next-line security/detect-non-literal-regexp
292
+ // eslint-disable-next-line security/detect-non-literal-regexp -- Safe: pattern from internal config, only allows * wildcards replaced with .*, anchored with ^$
293
293
  regex = new RegExp(`^${regexPattern}$`)
294
294
 
295
295
  // Implement size-limited cache with FIFO eviction
@@ -348,7 +348,7 @@ function generateReactGroups(_frameworkInfo) {
348
348
  /**
349
349
  * Generate dependency groups for Vue ecosystem
350
350
  *
351
- * @param {Object} frameworkInfo - Vue detection results
351
+ * @param {Object} _frameworkInfo - Vue detection results
352
352
  * @returns {Object} Dependabot groups configuration
353
353
  */
354
354
  function generateVueGroups(_frameworkInfo) {
@@ -376,7 +376,7 @@ function generateVueGroups(_frameworkInfo) {
376
376
  /**
377
377
  * Generate dependency groups for Angular ecosystem
378
378
  *
379
- * @param {Object} frameworkInfo - Angular detection results
379
+ * @param {Object} _frameworkInfo - Angular detection results
380
380
  * @returns {Object} Dependabot groups configuration
381
381
  */
382
382
  function generateAngularGroups(_frameworkInfo) {
@@ -404,7 +404,7 @@ function generateAngularGroups(_frameworkInfo) {
404
404
  /**
405
405
  * Generate dependency groups for testing frameworks
406
406
  *
407
- * @param {Object} frameworkInfo - Testing framework detection results
407
+ * @param {Object} _frameworkInfo - Testing framework detection results
408
408
  * @returns {Object} Dependabot groups configuration
409
409
  */
410
410
  function generateTestingGroups(_frameworkInfo) {
@@ -428,7 +428,7 @@ function generateTestingGroups(_frameworkInfo) {
428
428
  /**
429
429
  * Generate dependency groups for build tools
430
430
  *
431
- * @param {Object} frameworkInfo - Build tool detection results
431
+ * @param {Object} _frameworkInfo - Build tool detection results
432
432
  * @returns {Object} Dependabot groups configuration
433
433
  */
434
434
  function generateBuildToolGroups(_frameworkInfo) {
@@ -446,7 +446,7 @@ function generateBuildToolGroups(_frameworkInfo) {
446
446
  /**
447
447
  * Generate Storybook dependency groups
448
448
  *
449
- * @param {Object} frameworkInfo - Storybook detection results
449
+ * @param {Object} _frameworkInfo - Storybook detection results
450
450
  * @returns {Object} Dependabot groups configuration
451
451
  */
452
452
  function generateStorybookGroups(_frameworkInfo) {
@@ -490,7 +490,7 @@ function hasPythonProject(projectPath) {
490
490
  * attacks with maliciously large requirements files.
491
491
  *
492
492
  * @param {string} requirementsPath - Path to requirements.txt file
493
- * @returns {Object<string, string>} Map of package names to version specifiers
493
+ * @returns {Record<string, string>} Map of package names to version specifiers
494
494
  * @throws {Error} If file exceeds MAX_REQUIREMENTS_FILE_SIZE
495
495
  *
496
496
  * @example
@@ -512,6 +512,7 @@ function parsePipRequirements(requirementsPath) {
512
512
  }
513
513
 
514
514
  const content = fs.readFileSync(requirementsPath, 'utf8')
515
+ /** @type {Record<string, string>} */
515
516
  const dependencies = {}
516
517
 
517
518
  content.split('\n').forEach(line => {
@@ -529,7 +530,7 @@ function parsePipRequirements(requirementsPath) {
529
530
  // Support dotted names (zope.interface), hyphens (pytest-cov), underscores (google_cloud)
530
531
  // Also handle extras like fastapi[all] by capturing everything before the bracket
531
532
  // Fixed: Replaced (.*) with ([^\s]*) to prevent catastrophic backtracking
532
- // eslint-disable-next-line security/detect-unsafe-regex
533
+ // eslint-disable-next-line security/detect-unsafe-regex -- Safe: bounded character classes [\w.-], [\w,\s-], [^\s], anchored ^$, no nested quantifiers
533
534
  const match = line.match(/^([\w.-]+)(\[[\w,\s-]+\])?([><=!~]+)?([^\s]*)$/)
534
535
  if (match) {
535
536
  const [, name, _extras, operator, version] = match
@@ -552,12 +553,13 @@ function parsePipRequirements(requirementsPath) {
552
553
  */
553
554
  function parsePyprojectToml(pyprojectPath) {
554
555
  const content = fs.readFileSync(pyprojectPath, 'utf8')
556
+ /** @type {Record<string, string>} */
555
557
  const dependencies = {}
556
558
 
557
559
  // Parse PEP 621 list-style dependencies: dependencies = ["package>=1.0.0", ...]
558
560
  // Match main dependencies array: dependencies = [...]
559
561
  // Allow optional whitespace/comments after ] to handle: ] # end of deps
560
- // eslint-disable-next-line security/detect-unsafe-regex
562
+ // eslint-disable-next-line security/detect-unsafe-regex -- Safe: lazy quantifier *? prevents backtracking, anchored ^$, bounded alternation
561
563
  const mainDepPattern = /^dependencies\s*=\s*\[([\s\S]*?)\]\s*(?:#.*)?$/m
562
564
  const mainMatch = mainDepPattern.exec(content)
563
565
 
@@ -574,7 +576,7 @@ function parsePyprojectToml(pyprojectPath) {
574
576
  // Support dotted names, hyphens, underscores, and extras
575
577
  // Fixed: Replaced ($|.*) with ([^\s]*) to prevent catastrophic backtracking
576
578
  const match = depString.match(
577
- /^([\w.-]+)(\[[\w,\s-]+\])?([><=!~]+)?([^\s]*)$/ // eslint-disable-line security/detect-unsafe-regex
579
+ /^([\w.-]+)(\[[\w,\s-]+\])?([><=!~]+)?([^\s]*)$/ // eslint-disable-line security/detect-unsafe-regex -- Safe: bounded character classes, anchored ^$, no nested quantifiers
578
580
  )
579
581
  if (match) {
580
582
  const [, name, _extras, operator, version] = match
@@ -606,7 +608,7 @@ function parsePyprojectToml(pyprojectPath) {
606
608
  const depString = pkgMatch[1].trim()
607
609
 
608
610
  const match = depString.match(
609
- /^([\w.-]+)(\[[\w,\s-]+\])?([><=!~]+)?($|.*)$/ // eslint-disable-line security/detect-unsafe-regex
611
+ /^([\w.-]+)(\[[\w,\s-]+\])?([><=!~]+)?([^\s]*)$/ // eslint-disable-line security/detect-unsafe-regex -- Safe: bounded character classes, anchored ^$, no nested quantifiers
610
612
  )
611
613
  if (match) {
612
614
  const [, name, _extras, operator, version] = match
@@ -829,7 +831,7 @@ function parseCargoToml(cargoPath) {
829
831
  if (!trimmed || trimmed.startsWith('#')) continue
830
832
 
831
833
  // Match simple pattern: name = "version"
832
- // eslint-disable-next-line security/detect-unsafe-regex
834
+ // eslint-disable-next-line security/detect-unsafe-regex -- Safe: bounded groups \w+, [^"']+, anchored ^, no nested quantifiers
833
835
  const simpleMatch = trimmed.match(/^(\w+(?:-\w+)*)\s*=\s*["']([^"']+)["']/)
834
836
  if (simpleMatch) {
835
837
  const [, name, version] = simpleMatch
@@ -840,7 +842,7 @@ function parseCargoToml(cargoPath) {
840
842
 
841
843
  // Match complex pattern: name = { version = "...", ... }
842
844
  const complexMatch = trimmed.match(
843
- // eslint-disable-next-line security/detect-unsafe-regex
845
+ // eslint-disable-next-line security/detect-unsafe-regex -- Safe: bounded negated class [^}]*, anchored ^, no nested quantifiers
844
846
  /^(\w+(?:-\w+)*)\s*=\s*\{[^}]*version\s*=\s*["']([^"']+)["']/
845
847
  )
846
848
  if (complexMatch) {
@@ -970,7 +972,7 @@ function parseGemfile(gemfilePath) {
970
972
 
971
973
  // Match: gem 'rails', '~> 7.0' or gem 'rails'
972
974
  const gemMatch = trimmed.match(
973
- // eslint-disable-next-line security/detect-unsafe-regex
975
+ // eslint-disable-next-line security/detect-unsafe-regex -- Safe: bounded negated class [^'"]+, no nested quantifiers, processed line-by-line
974
976
  /gem\s+['"]([^'"]+)['"]\s*(?:,\s*['"]([^'"]+)['"])?/
975
977
  )
976
978
  if (gemMatch) {
@@ -1270,11 +1272,11 @@ function generateBundlerGroups(bundlerFrameworks) {
1270
1272
  /**
1271
1273
  * Generate premium Dependabot configuration with multi-language framework-aware grouping
1272
1274
  *
1273
- * @param {Object} options - Configuration options
1274
- * @param {string} options.projectPath - Path to project directory
1275
- * @param {string} options.schedule - Update schedule (daily, weekly, monthly)
1276
- * @param {string} options.day - Day of week for updates
1277
- * @param {string} options.time - Time for updates
1275
+ * @param {Object} [options] - Configuration options
1276
+ * @param {string} [options.projectPath='.'] - Path to project directory
1277
+ * @param {string} [options.schedule='weekly'] - Update schedule (daily, weekly, monthly)
1278
+ * @param {string} [options.day='monday'] - Day of week for updates
1279
+ * @param {string} [options.time='09:00'] - Time for updates
1278
1280
  * @returns {Object|null} Dependabot configuration object or null if not licensed
1279
1281
  */
1280
1282
  function generatePremiumDependabotConfig(options = {}) {
@@ -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
+ }
@@ -1,5 +1,9 @@
1
1
  'use strict'
2
2
 
3
+ /**
4
+ * @typedef {import('./prompt').InteractivePrompt} InteractivePrompt
5
+ */
6
+
3
7
  /**
4
8
  * Question definitions and answer parsing for interactive mode
5
9
  */
@@ -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://license.aibuilderlab.com/qaa/legitimate-licenses.json'
29
+ 'https://vibebuildlab.com/api/licenses/qa-architect.json'
30
30
  }
31
31
 
32
32
  ensureLicenseDir() {
package/lib/licensing.js CHANGED
@@ -216,7 +216,7 @@ function isDeveloperMode() {
216
216
  if (fs.existsSync(developerMarkerFile)) {
217
217
  return true
218
218
  }
219
- } catch (_error) {
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 (_error) {
341
+ } catch {
342
342
  // If signature verification fails, treat as invalid
343
343
  return false
344
344
  }
@@ -396,7 +396,7 @@ 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/cqa')
399
+ console.log('🚀 Upgrade: https://vibebuildlab.com/tools/qa-architect')
400
400
  console.log(
401
401
  '🔑 Activate: npx create-qa-architect@latest --activate-license'
402
402
  )
@@ -414,7 +414,7 @@ function showUpgradeMessage(feature) {
414
414
  console.log(' ✅ Slack/email alerts for failures')
415
415
  console.log(' ✅ Priority support (business hours)')
416
416
  console.log('')
417
- console.log('👥 Upgrade: https://vibebuildlab.com/cqa/team')
417
+ console.log('👥 Upgrade: https://vibebuildlab.com/tools/qa-architect')
418
418
  } else if (license.tier === LICENSE_TIERS.TEAM) {
419
419
  console.log('\n🏢 Upgrade to ENTERPRISE - $249/month (annual) + onboarding')
420
420
  console.log('')
@@ -553,7 +553,7 @@ async function addLegitimateKey(
553
553
  if (fs.existsSync(legitimateDBFile)) {
554
554
  try {
555
555
  database = JSON.parse(fs.readFileSync(legitimateDBFile, 'utf8'))
556
- } catch (_error) {
556
+ } catch {
557
557
  console.error(
558
558
  'Warning: Could not parse existing database, creating new one'
559
559
  )
@@ -654,7 +654,7 @@ async function promptLicenseActivation() {
654
654
  console.log(
655
655
  ' If you purchased this license, please contact support at:'
656
656
  )
657
- console.log(' Email: support@aibuilderlab.com')
657
+ console.log(' Email: support@vibebuildlab.com')
658
658
  console.log(
659
659
  ' Include your license key and purchase email for verification.'
660
660
  )
@@ -745,7 +745,7 @@ function loadUsage() {
745
745
 
746
746
  return data
747
747
  }
748
- } catch (_error) {
748
+ } catch {
749
749
  // Ignore errors reading usage file
750
750
  }
751
751
 
@@ -770,7 +770,7 @@ function saveUsage(usage) {
770
770
  }
771
771
  fs.writeFileSync(getUsageFile(), JSON.stringify(usage, null, 2))
772
772
  return true
773
- } catch (_error) {
773
+ } catch {
774
774
  return false
775
775
  }
776
776
  }
@@ -795,7 +795,8 @@ function checkUsageCaps(operation = 'general') {
795
795
  usage: {
796
796
  prePushRuns: usage.prePushRuns,
797
797
  dependencyPRs: usage.dependencyPRs,
798
- repos: usage.repos.length,
798
+ repos: usage.repos || [],
799
+ repoCount: (usage.repos || []).length,
799
800
  },
800
801
  caps: {
801
802
  maxPrePushRunsPerMonth: caps.maxPrePushRunsPerMonth,
@@ -957,7 +958,7 @@ function showLicenseStatus() {
957
958
  // Show upgrade path
958
959
  if (license.tier === LICENSE_TIERS.FREE) {
959
960
  console.log('\n💡 Upgrade to PRO for unlimited access + security scanning')
960
- console.log(' → https://vibebuildlab.com/cqa')
961
+ console.log(' → https://vibebuildlab.com/tools/qa-architect')
961
962
  }
962
963
  }
963
964