create-qa-architect 5.0.7 → 5.3.1

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.
@@ -34,7 +34,11 @@ class TemplateLoader {
34
34
  try {
35
35
  const stats = fs.statSync(templatePath)
36
36
  return stats.isDirectory()
37
- } catch {
37
+ } catch (error) {
38
+ // Silent failure fix: Log unexpected errors in debug mode
39
+ if (process.env.DEBUG && error.code !== 'ENOENT') {
40
+ console.warn(`⚠️ Template path check failed: ${error.message}`)
41
+ }
38
42
  return false
39
43
  }
40
44
  }
@@ -51,7 +55,13 @@ class TemplateLoader {
51
55
 
52
56
  const stats = fs.statSync(templateDir)
53
57
  return stats.isDirectory()
54
- } catch {
58
+ } catch (error) {
59
+ // Silent failure fix: Log unexpected errors in debug mode
60
+ if (process.env.DEBUG && error.code !== 'ENOENT') {
61
+ console.warn(
62
+ `⚠️ Template structure validation failed: ${error.message}`
63
+ )
64
+ }
55
65
  return false
56
66
  }
57
67
  }
@@ -128,6 +138,7 @@ class TemplateLoader {
128
138
  async loadTemplates(dir, baseDir = dir, isPackageDir = false) {
129
139
  /** @type {Record<string, string>} */
130
140
  const templates = {}
141
+ const errors = []
131
142
 
132
143
  try {
133
144
  const entries = fs.readdirSync(dir, { withFileTypes: true })
@@ -144,29 +155,51 @@ class TemplateLoader {
144
155
  const fullPath = path.join(dir, entry.name)
145
156
  const relativePath = path.relative(baseDir, fullPath)
146
157
 
147
- if (entry.isDirectory()) {
148
- // Recursively load from subdirectories
149
- // After first level, we're no longer in package root
150
- const subTemplates = await this.loadTemplates(
151
- fullPath,
152
- baseDir,
153
- false
154
- )
155
- Object.assign(templates, subTemplates)
156
- } else if (entry.isFile()) {
157
- // Load file content asynchronously for better performance
158
- const content = await fs.promises.readFile(fullPath, 'utf8')
159
- templates[relativePath] = content
158
+ try {
159
+ if (entry.isDirectory()) {
160
+ // Recursively load from subdirectories
161
+ const subTemplates = await this.loadTemplates(
162
+ fullPath,
163
+ baseDir,
164
+ false
165
+ )
166
+ Object.assign(templates, subTemplates)
167
+ } else if (entry.isFile()) {
168
+ // DR6 fix: Handle individual file errors separately
169
+ const content = await fs.promises.readFile(fullPath, 'utf8')
170
+ templates[relativePath] = content
171
+ }
172
+ } catch (fileError) {
173
+ // Track individual file errors
174
+ errors.push({ file: relativePath, error: fileError.message })
175
+
176
+ if (this.strict) {
177
+ throw new Error(
178
+ `Failed to load template ${relativePath}: ${fileError.message}`
179
+ )
180
+ } else if (this.verbose) {
181
+ console.warn(
182
+ `⚠️ Skipping template ${relativePath}: ${fileError.message}`
183
+ )
184
+ }
160
185
  }
161
186
  }
162
187
  } catch (error) {
163
- if (this.verbose) {
164
- console.warn(
165
- `⚠️ Warning: Could not load templates from ${dir}: ${error.message}`
166
- )
188
+ const message = `Could not load templates from ${dir}: ${error.message}`
189
+
190
+ if (this.strict) {
191
+ throw new Error(message)
192
+ } else if (this.verbose) {
193
+ console.warn(`⚠️ ${message}`)
167
194
  }
168
195
  }
169
196
 
197
+ // DR6 fix: Alert if critical templates are missing
198
+ if (errors.length > 0 && this.verbose) {
199
+ console.warn(`⚠️ ${errors.length} template file(s) failed to load`)
200
+ console.warn(' This may result in incomplete configuration')
201
+ }
202
+
170
203
  return templates
171
204
  }
172
205
 
@@ -40,8 +40,14 @@ class CacheManager {
40
40
  try {
41
41
  const content = fs.readFileSync(filePath, 'utf8')
42
42
  return crypto.createHash('sha256').update(content).digest('hex')
43
- } catch {
43
+ } catch (error) {
44
44
  // If file doesn't exist or can't be read, return timestamp-based key
45
+ // Silent failure fix: Log unexpected errors in debug mode
46
+ if (process.env.DEBUG && error.code !== 'ENOENT') {
47
+ console.warn(
48
+ `⚠️ Cache key generation failed for ${filePath}: ${error.message}`
49
+ )
50
+ }
45
51
  return crypto
46
52
  .createHash('sha256')
47
53
  .update(`${filePath}-${Date.now()}`)
@@ -89,8 +95,12 @@ class CacheManager {
89
95
  }
90
96
 
91
97
  return cached.result
92
- } catch {
98
+ } catch (error) {
93
99
  // If cache file is corrupted or unreadable, treat as cache miss
100
+ // Silent failure fix: Log unexpected errors in debug mode
101
+ if (process.env.DEBUG && error.code !== 'ENOENT') {
102
+ console.warn(`⚠️ Cache read failed: ${error.message}`)
103
+ }
94
104
  return null
95
105
  }
96
106
  }
@@ -127,24 +137,44 @@ class CacheManager {
127
137
  */
128
138
  clear() {
129
139
  if (!this.enabled) {
130
- return
140
+ return { success: true, cleared: 0 }
131
141
  }
132
142
 
143
+ const results = { success: true, cleared: 0, errors: [] }
144
+
133
145
  try {
134
146
  if (fs.existsSync(this.cacheDir)) {
135
147
  const files = fs.readdirSync(this.cacheDir)
136
148
  files.forEach(file => {
137
149
  if (file.endsWith('.json')) {
138
- fs.unlinkSync(path.join(this.cacheDir, file))
150
+ try {
151
+ fs.unlinkSync(path.join(this.cacheDir, file))
152
+ results.cleared++
153
+ } catch (error) {
154
+ // DR11 fix: Track individual file errors
155
+ results.errors.push({ file, error: error.message })
156
+ results.success = false
157
+ }
139
158
  }
140
159
  })
141
160
  }
142
161
  } catch (error) {
143
- // Silently fail if cache clear fails
162
+ // DR11 fix: Report directory-level failures
163
+ results.success = false
164
+ results.errors.push({ directory: this.cacheDir, error: error.message })
165
+
144
166
  if (this.verbose) {
145
- console.warn(`Cache clear failed: ${error.message}`)
167
+ console.warn(`❌ Cache clear failed: ${error.message}`)
146
168
  }
147
169
  }
170
+
171
+ // DR11 fix: Warn if any failures occurred
172
+ if (!results.success && this.verbose) {
173
+ console.warn(`⚠️ Failed to clear ${results.errors.length} cache file(s)`)
174
+ console.warn(' Some cached validation results may be stale')
175
+ }
176
+
177
+ return results
148
178
  }
149
179
 
150
180
  /**
@@ -5,7 +5,7 @@ const path = require('path')
5
5
  const os = require('os')
6
6
  const crypto = require('crypto')
7
7
  const https = require('https')
8
- const { execSync } = require('child_process')
8
+ const { execSync, spawnSync } = require('child_process')
9
9
  const { showProgress } = require('../ui-helpers')
10
10
 
11
11
  // Pinned gitleaks version for reproducible security scanning
@@ -213,20 +213,37 @@ class ConfigSecurityScanner {
213
213
  /**
214
214
  * Download file from URL to target path
215
215
  */
216
- downloadFile(url, targetPath) {
217
- return new Promise((resolve, reject) => {
218
- const file = fs.createWriteStream(targetPath)
216
+ downloadFile(url, targetPath, redirectCount = 0) {
217
+ const MAX_REDIRECTS = 5
219
218
 
219
+ return new Promise((resolve, reject) => {
220
220
  https
221
221
  .get(url, response => {
222
- if (response.statusCode === 302 || response.statusCode === 301) {
223
- // Follow redirect
224
- return this.downloadFile(response.headers.location, targetPath)
222
+ if (
223
+ response.statusCode === 302 ||
224
+ response.statusCode === 301 ||
225
+ response.statusCode === 303 ||
226
+ response.statusCode === 307 ||
227
+ response.statusCode === 308
228
+ ) {
229
+ if (redirectCount >= MAX_REDIRECTS) {
230
+ reject(new Error('Too many redirects while downloading gitleaks'))
231
+ return
232
+ }
233
+ const nextUrl = response.headers.location
234
+ if (!nextUrl) {
235
+ reject(new Error('Redirect without location header'))
236
+ return
237
+ }
238
+ response.resume()
239
+ this.downloadFile(nextUrl, targetPath, redirectCount + 1)
225
240
  .then(resolve)
226
241
  .catch(reject)
242
+ return
227
243
  }
228
244
 
229
245
  if (response.statusCode !== 200) {
246
+ response.resume()
230
247
  reject(
231
248
  new Error(
232
249
  `HTTP ${response.statusCode}: ${response.statusMessage}`
@@ -235,6 +252,7 @@ class ConfigSecurityScanner {
235
252
  return
236
253
  }
237
254
 
255
+ const file = fs.createWriteStream(targetPath)
238
256
  response.pipe(file)
239
257
 
240
258
  file.on('finish', () => {
@@ -351,6 +369,7 @@ class ConfigSecurityScanner {
351
369
  execSync(auditCmd, {
352
370
  stdio: 'pipe',
353
371
  timeout: 60000, // 60 second timeout for audit operations
372
+ maxBuffer: 10 * 1024 * 1024,
354
373
  encoding: 'utf8',
355
374
  })
356
375
  spinner.succeed('npm audit completed - no high/critical vulnerabilities')
@@ -368,11 +387,13 @@ class ConfigSecurityScanner {
368
387
  const auditResult = JSON.parse(error.stdout.toString())
369
388
  if (auditResult.metadata && auditResult.metadata.vulnerabilities) {
370
389
  const vulns = auditResult.metadata.vulnerabilities
371
- const total = vulns.high + vulns.critical + vulns.moderate
390
+ const total = vulns.high + vulns.critical
372
391
  if (total > 0) {
373
- spinner.fail(`npm audit found ${total} vulnerabilities`)
392
+ spinner.fail(
393
+ `npm audit found ${total} high/critical vulnerabilities`
394
+ )
374
395
  this.issues.push(
375
- `${packageManager} audit: ${total} vulnerabilities found (${vulns.high} high, ${vulns.critical} critical). Run '${packageManager} audit fix' to resolve.`
396
+ `${packageManager} audit: ${total} high/critical vulnerabilities found (${vulns.high} high, ${vulns.critical} critical). Run '${packageManager} audit fix' to resolve.`
376
397
  )
377
398
  } else {
378
399
  spinner.succeed(
@@ -405,16 +426,34 @@ class ConfigSecurityScanner {
405
426
 
406
427
  // Build command - handle npx vs direct binary execution
407
428
  const isNpxCommand = gitleaksBinary.startsWith('npx ')
408
- const command = isNpxCommand
409
- ? `${gitleaksBinary} detect --source . --redact`
410
- : `"${gitleaksBinary}" detect --source . --redact`
429
+ const args = ['detect', '--source', '.', '--redact']
430
+ const command = isNpxCommand ? 'npx' : gitleaksBinary
431
+ const commandArgs = isNpxCommand ? ['gitleaks', ...args] : args
411
432
 
412
433
  // Run gitleaks with --redact to prevent secret exposure and timeout for safety
413
- execSync(command, {
434
+ const result = spawnSync(command, commandArgs, {
414
435
  stdio: 'pipe',
415
436
  timeout: 30000, // 30 second timeout to prevent hangs
416
437
  encoding: 'utf8',
417
438
  })
439
+
440
+ if (result.error) {
441
+ const error = result.error
442
+ error.stdout = result.stdout
443
+ error.stderr = result.stderr
444
+ error.status = result.status
445
+ error.signal = result.signal
446
+ throw error
447
+ }
448
+
449
+ if (result.status !== 0) {
450
+ const error = new Error('gitleaks exited with non-zero status')
451
+ error.status = result.status
452
+ error.stdout = result.stdout
453
+ error.stderr = result.stderr
454
+ error.signal = result.signal
455
+ throw error
456
+ }
418
457
  spinner.succeed('gitleaks scan completed - no secrets detected')
419
458
  } catch (error) {
420
459
  if (error.signal === 'SIGTERM') {
@@ -676,14 +715,38 @@ class ConfigSecurityScanner {
676
715
  const secretPattern =
677
716
  /(?:SECRET|PASSWORD|KEY|TOKEN)\s*=\s*["']?[^"\s']+/gi
678
717
  if (secretPattern.test(envStatement)) {
718
+ const redacted = this.redactDockerEnv(envStatement)
679
719
  this.issues.push(
680
- `Dockerfile: Hardcoded secret in ENV statement: ${envStatement.trim()}`
720
+ `Dockerfile: Hardcoded secret in ENV statement: ${redacted}`
681
721
  )
682
722
  }
683
723
  }
684
724
  }
685
725
  }
686
726
 
727
+ redactDockerEnv(statement) {
728
+ const trimmed = statement.trim()
729
+ const match = trimmed.match(/^ENV\s+(.+)$/i)
730
+ if (!match) return trimmed
731
+
732
+ const body = match[1].trim()
733
+ if (body.includes('=')) {
734
+ const redactedBody = body.replace(
735
+ /=\s*(?:'[^']*'|"[^"]*"|[^ \t\r\n]+)/g,
736
+ '=[REDACTED]'
737
+ )
738
+ return `ENV ${redactedBody}`
739
+ }
740
+
741
+ const parts = body.split(/\s+/)
742
+ if (parts.length >= 2) {
743
+ parts[1] = '[REDACTED]'
744
+ return `ENV ${parts.join(' ')}`
745
+ }
746
+
747
+ return `ENV ${body}`
748
+ }
749
+
687
750
  /**
688
751
  * Check .gitignore for security-sensitive files
689
752
  */
@@ -95,15 +95,36 @@ class WorkflowValidator {
95
95
  for (const file of workflowFiles) {
96
96
  const filePath = path.join(workflowDir, file)
97
97
  const content = fs.readFileSync(filePath, 'utf8')
98
- const results = linter(content, filePath) || []
99
98
 
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}`
99
+ try {
100
+ const results = linter(content, filePath) || []
101
+
102
+ if (Array.isArray(results) && results.length > 0) {
103
+ // Filter out false positives for 'vars' context (valid GitHub feature, not recognized by actionlint WASM)
104
+ const filteredResults = results.filter(
105
+ result =>
106
+ !(
107
+ result.kind === 'expression' &&
108
+ result.message?.includes('undefined variable "vars"')
109
+ )
105
110
  )
106
- })
111
+ issueCount += filteredResults.length
112
+ filteredResults.forEach(result => {
113
+ this.issues.push(
114
+ `actionlint: ${result.file}:${result.line}:${result.column} ${result.kind} - ${result.message}`
115
+ )
116
+ })
117
+ }
118
+ } catch (lintError) {
119
+ // WASM actionlint has limits on file complexity - skip silently for large files
120
+ if (
121
+ lintError.message?.includes('unreachable') &&
122
+ content.length > 10000
123
+ ) {
124
+ // Large file exceeded WASM limits - not a validation failure
125
+ continue
126
+ }
127
+ throw lintError
107
128
  }
108
129
  }
109
130
 
package/package.json CHANGED
@@ -1,11 +1,10 @@
1
1
  {
2
2
  "name": "create-qa-architect",
3
- "version": "5.0.7",
3
+ "version": "5.3.1",
4
4
  "description": "QA Architect - Bootstrap quality automation for JavaScript/TypeScript and Python projects with GitHub Actions, pre-commit hooks, linting, formatting, and smart test strategy",
5
5
  "main": "setup.js",
6
6
  "bin": {
7
- "create-qa-architect": "./setup.js",
8
- "create-saas-monetization": "./create-saas-monetization.js"
7
+ "create-qa-architect": "./setup.js"
9
8
  },
10
9
  "scripts": {
11
10
  "prepare": "husky",
@@ -66,7 +65,6 @@
66
65
  "license": "SEE LICENSE IN LICENSE",
67
66
  "files": [
68
67
  "setup.js",
69
- "create-saas-monetization.js",
70
68
  "config/",
71
69
  "docs/",
72
70
  "lib/",
@@ -0,0 +1,46 @@
1
+ #!/bin/bash
2
+ # Check test coverage for new features
3
+ # Run manually or via pre-commit hook (warning only)
4
+
5
+ set -e
6
+
7
+ YELLOW='\033[1;33m'
8
+ NC='\033[0m'
9
+
10
+ warnings=0
11
+
12
+ echo "🔍 Checking test coverage..."
13
+
14
+ # Find source files without corresponding test files
15
+ for file in $(find src -name "*.ts" -o -name "*.tsx" 2>/dev/null | grep -v "\.test\." | grep -v "\.d\.ts" || true); do
16
+ # Skip layout, page, and config files (Next.js conventions)
17
+ if [[ "$file" == *"/layout."* ]] || [[ "$file" == *"/page."* ]] || [[ "$file" == *".config."* ]]; then
18
+ continue
19
+ fi
20
+
21
+ # Skip API routes (tested via integration tests)
22
+ if [[ "$file" == *"/api/"* ]]; then
23
+ continue
24
+ fi
25
+
26
+ # Check for corresponding test file
27
+ test_file="${file%.*}.test.${file##*.}"
28
+
29
+ if [ ! -f "$test_file" ] && [ ! -f "tests/unit/$(basename ${file%.*}).test.ts" ]; then
30
+ # Only warn for lib/ and components/ (core business logic)
31
+ if [[ "$file" == *"/lib/"* ]] || [[ "$file" == *"/components/"* ]]; then
32
+ echo -e "${YELLOW}⚠️ May need test: $file${NC}"
33
+ ((warnings++)) || true
34
+ fi
35
+ fi
36
+ done
37
+
38
+ if [ $warnings -gt 0 ]; then
39
+ echo ""
40
+ echo -e "${YELLOW}⚠️ $warnings file(s) in lib/ or components/ may need tests${NC}"
41
+ echo " Run: pnpm test:coverage to check actual coverage"
42
+ else
43
+ echo "✅ All core files appear to have tests"
44
+ fi
45
+
46
+ exit 0