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.
@@ -86,25 +86,18 @@ async function handleDependencyMonitoring() {
86
86
  if (hasRuby) console.log('šŸ’Ž Detected: Ruby project')
87
87
  console.log(`šŸ“‹ License tier: ${license.tier.toUpperCase()}`)
88
88
 
89
- // Enforce Free tier caps for dependency monitoring (counted as dependency PRs)
90
- if (license.tier === 'FREE') {
91
- const capCheck = checkUsageCaps('dependency-pr')
92
- if (!capCheck.allowed) {
93
- console.error(`āŒ ${capCheck.reason}`)
94
- console.error(
95
- ' Upgrade to Pro, Team, or Enterprise for unlimited runs: https://vibebuildlab.com/qa-architect'
96
- )
97
- process.exit(1)
98
- }
99
-
100
- const increment = incrementUsage('dependency-pr')
101
- const usage = increment.usage || capCheck.usage
102
- const caps = capCheck.caps
103
- if (usage && caps && caps.maxDependencyPRsPerMonth !== undefined) {
104
- console.log(
105
- `🧮 Usage: ${usage.dependencyPRs}/${caps.maxDependencyPRsPerMonth} dependency monitoring runs used this month`
106
- )
107
- }
89
+ // Use sentinel value instead of null for consistent access patterns
90
+ const capCheck =
91
+ license.tier === 'FREE'
92
+ ? checkUsageCaps('dependency-pr')
93
+ : { allowed: true, usage: {}, caps: {} }
94
+
95
+ if (!capCheck.allowed) {
96
+ console.error(`āŒ ${capCheck.reason}`)
97
+ console.error(
98
+ ' Upgrade to Pro, Team, or Enterprise for unlimited runs: https://vibebuildlab.com/qa-architect'
99
+ )
100
+ process.exit(1)
108
101
  }
109
102
 
110
103
  const dependabotPath = path.join(projectPath, '.github', 'dependabot.yml')
@@ -207,6 +200,17 @@ async function handleDependencyMonitoring() {
207
200
  showUpgradeMessage('Framework-Aware Dependency Grouping')
208
201
  }
209
202
 
203
+ if (license.tier === 'FREE') {
204
+ const increment = incrementUsage('dependency-pr')
205
+ const usage = increment.usage || capCheck.usage
206
+ const caps = capCheck.caps
207
+ if (usage && caps && caps.maxDependencyPRsPerMonth !== undefined) {
208
+ console.log(
209
+ `🧮 Usage: ${usage.dependencyPRs}/${caps.maxDependencyPRsPerMonth} dependency monitoring runs used this month`
210
+ )
211
+ }
212
+ }
213
+
210
214
  // Auto-enable Dependabot on GitHub if token available
211
215
  console.log('\nšŸ”§ Attempting to enable Dependabot on GitHub...')
212
216
  try {
@@ -225,9 +229,53 @@ async function handleDependencyMonitoring() {
225
229
  )
226
230
  }
227
231
  } catch (error) {
228
- console.log('āš ļø Could not auto-enable Dependabot:', error.message)
229
- console.log('\nšŸ’” Manual steps:')
230
- console.log(' • Enable Dependabot in GitHub repo settings')
232
+ const errorId = 'DEPENDABOT_AUTO_ENABLE_FAILED'
233
+
234
+ // Diagnose specific error types
235
+ let diagnosis = 'Unknown error'
236
+ let remedy = 'Check the error message for details'
237
+
238
+ if (
239
+ error.message.includes('401') ||
240
+ error.message.includes('authentication')
241
+ ) {
242
+ diagnosis = 'GitHub token is invalid or missing'
243
+ remedy = 'Set GITHUB_TOKEN environment variable with a valid token'
244
+ } else if (error.message.includes('404')) {
245
+ diagnosis = 'Repository not found or insufficient permissions'
246
+ remedy = 'Ensure token has repo:write access to this repository'
247
+ } else if (error.message.includes('rate limit')) {
248
+ diagnosis = 'GitHub API rate limit exceeded'
249
+ remedy = 'Wait 1 hour or use authenticated token for higher limits'
250
+ } else if (
251
+ error.code === 'ENOTFOUND' ||
252
+ error.message.includes('network')
253
+ ) {
254
+ diagnosis = 'Network connectivity issue'
255
+ remedy = 'Check internet connection and GitHub API status'
256
+ } else if (error.message.includes('timeout')) {
257
+ diagnosis = 'Request timed out'
258
+ remedy = 'Retry the operation or check network connectivity'
259
+ }
260
+
261
+ console.error(`\nāŒ [${errorId}] Could not auto-enable Dependabot`)
262
+ console.error(` Diagnosis: ${diagnosis}`)
263
+ console.error(` Error: ${error.message}`)
264
+ console.error(`\n šŸ”§ Recommended fix: ${remedy}`)
265
+
266
+ if (process.env.DEBUG) {
267
+ console.error(`\n Debug info:`)
268
+ console.error(` • Error code: ${error.code || 'N/A'}`)
269
+ console.error(` • Stack: ${error.stack}`)
270
+ }
271
+
272
+ console.log('\nšŸ’” Alternative: Enable manually:')
273
+ console.log(' 1. Go to GitHub repo → Settings → Code security')
274
+ console.log(' 2. Enable "Dependabot alerts"')
275
+ console.log(' 3. Enable "Dependabot security updates"')
276
+ console.log(
277
+ `\n • Report issue: https://github.com/vibebuildlab/qa-architect/issues/new?title=${errorId}`
278
+ )
231
279
  }
232
280
 
233
281
  console.log('\nšŸ’” Next steps:')
@@ -12,6 +12,7 @@ const {
12
12
  detectRustProject,
13
13
  detectRubyProject,
14
14
  } = require('./deps')
15
+ const { handleAnalyzeCi } = require('./analyze-ci')
15
16
 
16
17
  module.exports = {
17
18
  // Validation commands
@@ -22,4 +23,7 @@ module.exports = {
22
23
  detectPythonProject,
23
24
  detectRustProject,
24
25
  detectRubyProject,
26
+
27
+ // CI/CD optimization commands
28
+ handleAnalyzeCi,
25
29
  }
@@ -11,6 +11,33 @@ const addFormats = /** @type {(ajv: any) => void} */ (
11
11
  addFormatsImport.default || addFormatsImport
12
12
  )
13
13
 
14
+ /**
15
+ * Format a single AJV validation error into a human-readable message
16
+ */
17
+ function formatValidationError(error) {
18
+ const errorPath = error.instancePath || '(root)'
19
+ const message = error.message || 'validation failed'
20
+
21
+ const errorFormatters = {
22
+ required: () =>
23
+ `Missing required field: ${error.params?.missingProperty || 'unknown'}`,
24
+ enum: () =>
25
+ `Invalid value at ${errorPath}: must be one of ${error.params?.allowedValues?.join(', ') || 'unknown values'}`,
26
+ type: () =>
27
+ `Invalid type at ${errorPath}: expected ${error.params?.type || 'unknown'}`,
28
+ pattern: () => `Invalid format at ${errorPath}: ${message}`,
29
+ minimum: () =>
30
+ `Invalid value at ${errorPath}: must be >= ${error.params?.limit ?? 'unknown'}`,
31
+ additionalProperties: () =>
32
+ `Unknown property at ${errorPath}: ${error.params?.additionalProperty || 'unknown'}`,
33
+ }
34
+
35
+ const formatter = errorFormatters[error.keyword]
36
+ return formatter
37
+ ? formatter()
38
+ : `Validation error at ${errorPath}: ${message}`
39
+ }
40
+
14
41
  function validateQualityConfig(configPath) {
15
42
  const result = {
16
43
  valid: false,
@@ -61,51 +88,7 @@ function validateQualityConfig(configPath) {
61
88
 
62
89
  if (!valid) {
63
90
  if (validate.errors) {
64
- validate.errors.forEach(error => {
65
- const errorPath = error.instancePath || '(root)'
66
- const message = error.message || 'validation failed'
67
-
68
- if (error.keyword === 'required') {
69
- result.errors.push(
70
- 'Missing required field: ' +
71
- (error.params?.missingProperty || 'unknown')
72
- )
73
- } else if (error.keyword === 'enum') {
74
- result.errors.push(
75
- 'Invalid value at ' +
76
- errorPath +
77
- ': must be one of ' +
78
- (error.params?.allowedValues?.join(', ') || 'unknown values')
79
- )
80
- } else if (error.keyword === 'type') {
81
- result.errors.push(
82
- 'Invalid type at ' +
83
- errorPath +
84
- ': expected ' +
85
- (error.params?.type || 'unknown')
86
- )
87
- } else if (error.keyword === 'pattern') {
88
- result.errors.push('Invalid format at ' + errorPath + ': ' + message)
89
- } else if (error.keyword === 'minimum') {
90
- result.errors.push(
91
- 'Invalid value at ' +
92
- errorPath +
93
- ': must be >= ' +
94
- (error.params?.limit ?? 'unknown')
95
- )
96
- } else if (error.keyword === 'additionalProperties') {
97
- result.errors.push(
98
- 'Unknown property at ' +
99
- errorPath +
100
- ': ' +
101
- (error.params?.additionalProperty || 'unknown')
102
- )
103
- } else {
104
- result.errors.push(
105
- 'Validation error at ' + errorPath + ': ' + message
106
- )
107
- }
108
- })
91
+ result.errors = validate.errors.map(formatValidationError)
109
92
  }
110
93
  } else {
111
94
  result.valid = true
@@ -190,7 +190,7 @@ function loadErrorReports() {
190
190
  const data = fs.readFileSync(ERROR_REPORTS_FILE, 'utf8')
191
191
  return JSON.parse(data)
192
192
  }
193
- } catch {
193
+ } catch (_error) {
194
194
  // If corrupted, start fresh
195
195
  console.warn('āš ļø Error reports data corrupted, starting fresh')
196
196
  }
package/lib/github-api.js CHANGED
@@ -127,9 +127,30 @@ function getRepoInfo(projectPath = '.') {
127
127
  }
128
128
  }
129
129
 
130
+ /**
131
+ * Sanitize error messages to remove sensitive tokens
132
+ * DR29 fix: Prevent token exposure in error messages
133
+ */
134
+ function sanitizeError(error, token) {
135
+ if (!error || !token) return error
136
+
137
+ const message = error.message || String(error)
138
+ // Use string replace with global flag instead of RegExp to avoid security warning
139
+ const sanitized = message.split(token).join('***REDACTED***')
140
+
141
+ if (error instanceof Error) {
142
+ const sanitizedError = new Error(sanitized)
143
+ sanitizedError.stack = error.stack?.split(token).join('***REDACTED***')
144
+ return sanitizedError
145
+ }
146
+
147
+ return new Error(sanitized)
148
+ }
149
+
130
150
  /**
131
151
  * Make GitHub API request with rate limiting
132
152
  * TD5 fix: Added rate limiting to prevent hitting GitHub's API limits
153
+ * DR29 fix: Sanitize errors to prevent token exposure
133
154
  */
134
155
  async function githubRequest(method, path, token, data = null) {
135
156
  // TD5 fix: Acquire rate limit token before making request
@@ -163,9 +184,13 @@ async function githubRequest(method, path, token, data = null) {
163
184
  const data = body ? JSON.parse(body) : null
164
185
  resolve({ status: res.statusCode, data })
165
186
  } catch (_error) {
187
+ // DR29 fix: Sanitize error before rejecting
166
188
  reject(
167
- new Error(
168
- `GitHub API returned invalid JSON (status ${res.statusCode}): ${body.slice(0, 100)}`
189
+ sanitizeError(
190
+ new Error(
191
+ `GitHub API returned invalid JSON (status ${res.statusCode}): ${body.slice(0, 100)}`
192
+ ),
193
+ token
169
194
  )
170
195
  )
171
196
  }
@@ -181,14 +206,19 @@ async function githubRequest(method, path, token, data = null) {
181
206
  // Use raw body if JSON parse fails
182
207
  }
183
208
 
209
+ // DR29 fix: Sanitize error before rejecting
184
210
  reject(
185
- new Error(`GitHub API error: ${res.statusCode} - ${errorBody}`)
211
+ sanitizeError(
212
+ new Error(`GitHub API error: ${res.statusCode} - ${errorBody}`),
213
+ token
214
+ )
186
215
  )
187
216
  }
188
217
  })
189
218
  })
190
219
 
191
- req.on('error', reject)
220
+ // DR29 fix: Sanitize network errors
221
+ req.on('error', error => reject(sanitizeError(error, token)))
192
222
 
193
223
  if (data) {
194
224
  req.write(JSON.stringify(data))
@@ -30,9 +30,24 @@ function stableStringify(value, seen = new WeakSet()) {
30
30
  return `{${entries.join(',')}}`
31
31
  }
32
32
 
33
+ /**
34
+ * Normalize and validate email format
35
+ * DR21 fix: Added email format validation before hashing
36
+ * @param {string} email - Email address to normalize
37
+ * @returns {string|null} - Normalized email or null if invalid
38
+ */
33
39
  function normalizeEmail(email) {
34
40
  if (!email || typeof email !== 'string') return null
35
41
  const normalized = email.trim().toLowerCase()
42
+
43
+ // Basic email format validation (RFC 5322 simplified)
44
+ // Must have: local@domain.tld format
45
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
46
+
47
+ if (!emailRegex.test(normalized)) {
48
+ return null
49
+ }
50
+
36
51
  return normalized.length > 0 ? normalized : null
37
52
  }
38
53
 
package/lib/licensing.js CHANGED
@@ -122,6 +122,8 @@ const FEATURES = deepFreeze({
122
122
  linkValidation: true, // āœ… Broken link detection
123
123
  docsValidation: true, // āœ… Documentation completeness
124
124
  envValidation: false, // āŒ PRO feature - env vars audit
125
+ // CI/CD optimization
126
+ ciCostAnalysis: false, // āŒ PRO feature - GitHub Actions cost analysis
125
127
  roadmap: [
126
128
  'āœ… ESLint, Prettier, Stylelint configuration',
127
129
  'āœ… Basic Husky pre-commit hooks',
@@ -165,6 +167,8 @@ const FEATURES = deepFreeze({
165
167
  linkValidation: true, // āœ… Broken link detection
166
168
  docsValidation: true, // āœ… Documentation completeness
167
169
  envValidation: true, // āœ… Env vars audit
170
+ // CI/CD optimization
171
+ ciCostAnalysis: true, // āœ… GitHub Actions cost analysis
168
172
  roadmap: [
169
173
  'āœ… Unlimited repos and runs',
170
174
  'āœ… Smart Test Strategy (70% faster pre-push validation)',
@@ -218,6 +222,8 @@ const FEATURES = deepFreeze({
218
222
  linkValidation: true,
219
223
  docsValidation: true,
220
224
  envValidation: true,
225
+ // CI/CD optimization - inherited from PRO
226
+ ciCostAnalysis: true,
221
227
  roadmap: [
222
228
  'āœ… All PRO features included',
223
229
  'āœ… Per-seat licensing (5-seat minimum)',
@@ -267,6 +273,8 @@ const FEATURES = deepFreeze({
267
273
  linkValidation: true,
268
274
  docsValidation: true,
269
275
  envValidation: true,
276
+ // CI/CD optimization - inherited from TEAM
277
+ ciCostAnalysis: true,
270
278
  // Enterprise-specific
271
279
  ssoIntegration: true, // SSO/SAML
272
280
  scimReady: true,
@@ -556,6 +564,17 @@ function saveLicense(tier, key, email, expires = null) {
556
564
  }
557
565
  }
558
566
 
567
+ // DR21 fix: Validate email format before hashing
568
+ if (email) {
569
+ const normalizedEmail = require('./license-signing').normalizeEmail(email)
570
+ if (!normalizedEmail) {
571
+ return {
572
+ success: false,
573
+ error: `Invalid email format: "${email}". Must be valid email address (e.g., user@example.com)`,
574
+ }
575
+ }
576
+ }
577
+
559
578
  const licenseDir = getLicenseDir()
560
579
  const licenseFile = getLicenseFile()
561
580
  const normalizedKey = normalizeLicenseKey(key)
@@ -565,7 +584,7 @@ function saveLicense(tier, key, email, expires = null) {
565
584
  )
566
585
 
567
586
  if (!fs.existsSync(licenseDir)) {
568
- fs.mkdirSync(licenseDir, { recursive: true })
587
+ fs.mkdirSync(licenseDir, { recursive: true, mode: 0o700 })
569
588
  }
570
589
 
571
590
  if (!privateKey) {
@@ -595,7 +614,9 @@ function saveLicense(tier, key, email, expires = null) {
595
614
  signature,
596
615
  }
597
616
 
598
- fs.writeFileSync(licenseFile, JSON.stringify(licenseData, null, 2))
617
+ fs.writeFileSync(licenseFile, JSON.stringify(licenseData, null, 2), {
618
+ mode: 0o600,
619
+ })
599
620
  return { success: true }
600
621
  } catch (error) {
601
622
  return { success: false, error: error.message }
@@ -621,7 +642,7 @@ function saveLicenseWithSignature(tier, key, email, validation) {
621
642
  const normalizedKey = normalizeLicenseKey(key)
622
643
 
623
644
  if (!fs.existsSync(licenseDir)) {
624
- fs.mkdirSync(licenseDir, { recursive: true })
645
+ fs.mkdirSync(licenseDir, { recursive: true, mode: 0o700 })
625
646
  }
626
647
 
627
648
  const licenseData = {
@@ -638,7 +659,9 @@ function saveLicenseWithSignature(tier, key, email, validation) {
638
659
  issued: validation.issued,
639
660
  }
640
661
 
641
- fs.writeFileSync(licenseFile, JSON.stringify(licenseData, null, 2))
662
+ fs.writeFileSync(licenseFile, JSON.stringify(licenseData, null, 2), {
663
+ mode: 0o600,
664
+ })
642
665
  return { success: true }
643
666
  } catch (error) {
644
667
  return { success: false, error: error.message }
@@ -694,6 +717,18 @@ async function addLegitimateKey(
694
717
  purchaseEmail = null
695
718
  ) {
696
719
  try {
720
+ // DR21 fix: Validate email format before hashing
721
+ if (purchaseEmail) {
722
+ const normalizedEmail =
723
+ require('./license-signing').normalizeEmail(purchaseEmail)
724
+ if (!normalizedEmail) {
725
+ return {
726
+ success: false,
727
+ error: `Invalid email format: "${purchaseEmail}". Must be valid email address (e.g., user@example.com)`,
728
+ }
729
+ }
730
+ }
731
+
697
732
  const normalizedKey = normalizeLicenseKey(licenseKey)
698
733
  // Use the same license directory as the CLI
699
734
  const licenseDir =
@@ -716,19 +751,29 @@ async function addLegitimateKey(
716
751
  try {
717
752
  database = JSON.parse(fs.readFileSync(legitimateDBFile, 'utf8'))
718
753
  } catch (parseError) {
719
- // Silent failure fix: Backup corrupt file before overwriting
754
+ // DR8 fix: Return error instead of continuing with corrupted database
720
755
  const backupPath = `${legitimateDBFile}.corrupted.${Date.now()}`
756
+ let backupSucceeded = false
757
+
721
758
  try {
722
759
  fs.copyFileSync(legitimateDBFile, backupPath)
760
+ backupSucceeded = true
723
761
  console.error(
724
- `Warning: Could not parse existing database. Backed up to ${backupPath}`
762
+ `āš ļø Database corruption detected. Backed up to ${backupPath}`
725
763
  )
726
- } catch {
764
+ } catch (backupError) {
727
765
  console.error(
728
- 'Warning: Could not parse or backup existing database, creating new one'
766
+ `āŒ CRITICAL: Could not backup corrupted database: ${backupError.message}`
729
767
  )
730
768
  }
731
- console.error(`Parse error: ${parseError.message}`)
769
+
770
+ // Always return error on corruption - forces investigation
771
+ return {
772
+ success: false,
773
+ error: backupSucceeded
774
+ ? `License database corrupted (backup saved to ${backupPath}). Manual review required before adding keys.`
775
+ : `License database corrupted AND backup failed. Cannot proceed without data loss risk. Parse error: ${parseError.message}`,
776
+ }
732
777
  }
733
778
  }
734
779
 
@@ -940,30 +985,51 @@ function loadUsage() {
940
985
  // DR8 fix: Prevent quota bypass through file corruption
941
986
  if (error instanceof SyntaxError) {
942
987
  const usageFile = getUsageFile()
943
- console.warn('āš ļø Usage tracking file corrupted')
944
- console.warn(' Creating backup and resetting to current month start')
988
+ console.error(`\nāŒ CRITICAL: Usage tracking file is corrupted`)
989
+ console.error(` File: ${usageFile}`)
990
+ console.error(` Parse error: ${error.message}\n`)
945
991
 
946
992
  // Backup corrupted file for forensics
947
993
  const backupPath = `${usageFile}.corrupted.${Date.now()}`
948
994
  try {
949
995
  fs.copyFileSync(usageFile, backupPath)
950
- console.warn(` Backup saved: ${backupPath}`)
996
+ console.log(` āœ… Backup saved: ${backupPath}`)
951
997
  } catch (_backupError) {
952
- // Ignore backup failures
998
+ console.error(` āŒ Could not create backup`)
953
999
  }
954
1000
 
955
- // DR8 fix: For FREE tier, reset to max usage to prevent quota bypass
956
1001
  const license = getLicenseInfo()
1002
+
957
1003
  if (license.tier === LICENSE_TIERS.FREE) {
958
- console.warn(
959
- ' āš ļø FREE tier: Starting with maximum usage for security'
1004
+ console.error(`\nāš ļø FREE TIER CORRUPTION POLICY:`)
1005
+ console.error(
1006
+ ` To prevent quota bypass, your usage has been reset to maximum.`
1007
+ )
1008
+ console.error(` This is a security measure, not a penalty.\n`)
1009
+ console.error(` To restore your usage:`)
1010
+ console.error(` 1. Review the backup file: ${backupPath}`)
1011
+ console.error(` 2. If data looks correct, manually fix JSON syntax`)
1012
+ console.error(` 3. Copy corrected JSON back to: ${usageFile}`)
1013
+ console.error(
1014
+ ` 4. Or delete ${usageFile} to start fresh this month\n`
960
1015
  )
1016
+ console.error(` If this keeps happening, please report the issue.`)
1017
+
1018
+ // Provide clear recovery path
1019
+ console.error(`\nšŸ”§ Quick fix: rm ${usageFile}`)
1020
+ console.error(
1021
+ ` This will reset your usage to 0 for the current month.\n`
1022
+ )
1023
+
961
1024
  const caps = FEATURES[LICENSE_TIERS.FREE]
962
1025
  return {
963
1026
  month: getCurrentMonth(),
964
1027
  prePushRuns: caps.maxPrePushRunsPerMonth,
965
1028
  dependencyPRs: caps.maxDependencyPRsPerMonth,
966
- repos: [],
1029
+ repos: Array.from(
1030
+ { length: caps.maxPrivateRepos },
1031
+ (_item, index) => `corrupted-${index + 1}`
1032
+ ),
967
1033
  }
968
1034
  }
969
1035
  } else if (process.env.DEBUG && error?.code !== 'ENOENT') {
@@ -996,14 +1062,42 @@ function saveUsage(usage) {
996
1062
  try {
997
1063
  const licenseDir = getLicenseDir()
998
1064
  if (!fs.existsSync(licenseDir)) {
999
- fs.mkdirSync(licenseDir, { recursive: true })
1065
+ fs.mkdirSync(licenseDir, { recursive: true, mode: 0o700 })
1000
1066
  }
1001
- fs.writeFileSync(getUsageFile(), JSON.stringify(usage, null, 2))
1067
+ fs.writeFileSync(getUsageFile(), JSON.stringify(usage, null, 2), {
1068
+ mode: 0o600,
1069
+ })
1002
1070
  return true
1003
1071
  } catch (error) {
1004
- // TD12 fix: Log errors saving usage data instead of silently failing
1005
- console.warn(`āš ļø Failed to save usage data: ${error.message}`)
1006
- return false
1072
+ const license = getLicenseInfo()
1073
+ const usageFile = getUsageFile()
1074
+
1075
+ // For FREE tier, this is critical - can't track quota
1076
+ if (license.tier === LICENSE_TIERS.FREE) {
1077
+ console.error(`\nāŒ CRITICAL: Cannot save usage tracking data`)
1078
+ console.error(` File: ${usageFile}`)
1079
+ console.error(` Error: ${error.message} (${error.code})`)
1080
+ console.error(`\n FREE tier quota enforcement requires usage tracking.`)
1081
+ console.error(` Please fix this filesystem issue:\n`)
1082
+
1083
+ if (error.code === 'ENOSPC') {
1084
+ console.error(` • Disk full - free up space`)
1085
+ } else if (error.code === 'EACCES') {
1086
+ console.error(` • Permission denied - check directory permissions`)
1087
+ console.error(` • Try: chmod 700 ${getLicenseDir()}`)
1088
+ } else if (error.code === 'EROFS') {
1089
+ console.error(` • Filesystem is readonly - remount as read-write`)
1090
+ } else {
1091
+ console.error(` • Unexpected error - please report this issue`)
1092
+ }
1093
+
1094
+ throw error // Don't allow FREE tier to continue without tracking
1095
+ } else {
1096
+ // Pro/Team/Enterprise - warn but don't fail
1097
+ console.warn(`āš ļø Failed to save usage data: ${error.message}`)
1098
+ console.warn(` This won't affect Pro/Team/Enterprise functionality`)
1099
+ return false
1100
+ }
1007
1101
  }
1008
1102
  }
1009
1103
 
@@ -136,7 +136,7 @@ function detectPackageManager(projectPath = process.cwd()) {
136
136
  return pmName
137
137
  }
138
138
  }
139
- } catch {
139
+ } catch (_error) {
140
140
  // Ignore parse errors
141
141
  }
142
142
  }
@@ -204,7 +204,7 @@ function detectMonorepoType(projectPath = process.cwd()) {
204
204
  try {
205
205
  const nxJson = JSON.parse(fs.readFileSync(nxJsonPath, 'utf8'))
206
206
  result.config = nxJson
207
- } catch {
207
+ } catch (_error) {
208
208
  // Ignore parse errors
209
209
  }
210
210
  }
@@ -218,7 +218,7 @@ function detectMonorepoType(projectPath = process.cwd()) {
218
218
  try {
219
219
  const turboJson = JSON.parse(fs.readFileSync(turboJsonPath, 'utf8'))
220
220
  result.config = turboJson
221
- } catch {
221
+ } catch (_error) {
222
222
  // Ignore parse errors
223
223
  }
224
224
  }
@@ -240,7 +240,7 @@ function detectMonorepoType(projectPath = process.cwd()) {
240
240
  try {
241
241
  const lernaJson = JSON.parse(fs.readFileSync(lernaJsonPath, 'utf8'))
242
242
  result.packages = lernaJson.packages || ['packages/*']
243
- } catch {
243
+ } catch (_error) {
244
244
  result.packages = ['packages/*']
245
245
  }
246
246
  }
@@ -280,7 +280,7 @@ function detectMonorepoType(projectPath = process.cwd()) {
280
280
  if (packages.length > 0) {
281
281
  result.packages = packages
282
282
  }
283
- } catch {
283
+ } catch (_error) {
284
284
  // Ignore parse errors
285
285
  }
286
286
  }
@@ -303,7 +303,7 @@ function detectMonorepoType(projectPath = process.cwd()) {
303
303
  result.packageManager === 'yarn' ? 'yarn' : result.packageManager
304
304
  }
305
305
  }
306
- } catch {
306
+ } catch (_error) {
307
307
  // Ignore parse errors
308
308
  }
309
309
  }
@@ -351,7 +351,7 @@ function resolveWorkspacePackages(projectPath, patterns) {
351
351
  path: pkgPath,
352
352
  relativePath: path.relative(projectPath, pkgPath),
353
353
  })
354
- } catch {
354
+ } catch (_error) {
355
355
  packages.push({
356
356
  name: entry.name,
357
357
  path: pkgPath,
@@ -361,7 +361,7 @@ function resolveWorkspacePackages(projectPath, patterns) {
361
361
  }
362
362
  }
363
363
  }
364
- } catch {
364
+ } catch (_error) {
365
365
  // Ignore read errors
366
366
  }
367
367
  }
@@ -377,7 +377,7 @@ function resolveWorkspacePackages(projectPath, patterns) {
377
377
  path: pkgPath,
378
378
  relativePath: pattern,
379
379
  })
380
- } catch {
380
+ } catch (_error) {
381
381
  packages.push({
382
382
  name: path.basename(pkgPath),
383
383
  path: pkgPath,