create-qa-architect 5.0.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/.editorconfig +12 -0
- package/.github/CLAUDE_MD_AUTOMATION.md +248 -0
- package/.github/PROGRESSIVE_QUALITY_IMPLEMENTATION.md +408 -0
- package/.github/PROGRESSIVE_QUALITY_PROPOSAL.md +443 -0
- package/.github/RELEASE_CHECKLIST.md +100 -0
- package/.github/dependabot.yml +50 -0
- package/.github/git-sync.sh +48 -0
- package/.github/workflows/claude-md-validation.yml +82 -0
- package/.github/workflows/nightly-gitleaks-verification.yml +176 -0
- package/.github/workflows/pnpm-ci.yml.example +53 -0
- package/.github/workflows/python-ci.yml.example +69 -0
- package/.github/workflows/quality-legacy.yml.backup +165 -0
- package/.github/workflows/quality-progressive.yml.example +291 -0
- package/.github/workflows/quality.yml +436 -0
- package/.github/workflows/release.yml +53 -0
- package/.nvmrc +1 -0
- package/.prettierignore +14 -0
- package/.prettierrc +9 -0
- package/.stylelintrc.json +5 -0
- package/README.md +212 -0
- package/config/.lighthouserc.js +45 -0
- package/config/.pre-commit-config.yaml +66 -0
- package/config/constants.js +128 -0
- package/config/defaults.js +124 -0
- package/config/pyproject.toml +124 -0
- package/config/quality-config.schema.json +97 -0
- package/config/quality-python.yml +89 -0
- package/config/requirements-dev.txt +15 -0
- package/create-saas-monetization.js +1465 -0
- package/eslint.config.cjs +117 -0
- package/eslint.config.ts.cjs +99 -0
- package/legal/README.md +106 -0
- package/legal/copyright.md +76 -0
- package/legal/disclaimer.md +146 -0
- package/legal/privacy-policy.html +324 -0
- package/legal/privacy-policy.md +196 -0
- package/legal/terms-of-service.md +224 -0
- package/lib/billing-dashboard.html +645 -0
- package/lib/config-validator.js +163 -0
- package/lib/dependency-monitoring-basic.js +185 -0
- package/lib/dependency-monitoring-premium.js +1490 -0
- package/lib/error-reporter.js +444 -0
- package/lib/interactive/prompt.js +128 -0
- package/lib/interactive/questions.js +146 -0
- package/lib/license-validator.js +403 -0
- package/lib/licensing.js +989 -0
- package/lib/package-utils.js +187 -0
- package/lib/project-maturity.js +516 -0
- package/lib/security-enhancements.js +340 -0
- package/lib/setup-enhancements.js +317 -0
- package/lib/smart-strategy-generator.js +344 -0
- package/lib/telemetry.js +323 -0
- package/lib/template-loader.js +252 -0
- package/lib/typescript-config-generator.js +210 -0
- package/lib/ui-helpers.js +74 -0
- package/lib/validation/base-validator.js +174 -0
- package/lib/validation/cache-manager.js +158 -0
- package/lib/validation/config-security.js +741 -0
- package/lib/validation/documentation.js +326 -0
- package/lib/validation/index.js +186 -0
- package/lib/validation/validation-factory.js +153 -0
- package/lib/validation/workflow-validation.js +172 -0
- package/lib/yaml-utils.js +120 -0
- package/marketing/beta-user-email-campaign.md +372 -0
- package/marketing/landing-page.html +721 -0
- package/package.json +165 -0
- package/setup.js +2076 -0
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Error reporting module for crash analytics (opt-in only)
|
|
5
|
+
*
|
|
6
|
+
* Privacy principles:
|
|
7
|
+
* - Completely opt-in (prompt on error or ENV var)
|
|
8
|
+
* - No personal information collected (paths/usernames sanitized)
|
|
9
|
+
* - Local storage primary (optional remote sync)
|
|
10
|
+
* - Easy to inspect and delete
|
|
11
|
+
* - User can add context/comments
|
|
12
|
+
*
|
|
13
|
+
* Data collected:
|
|
14
|
+
* - Error category (dependency, permission, config, validation, network)
|
|
15
|
+
* - Error type and sanitized message
|
|
16
|
+
* - Sanitized stack trace (paths removed)
|
|
17
|
+
* - Node version and OS platform
|
|
18
|
+
* - Operation attempted (setup, validate, deps)
|
|
19
|
+
* - Optional user comment/context
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const fs = require('fs')
|
|
23
|
+
const path = require('path')
|
|
24
|
+
const os = require('os')
|
|
25
|
+
const crypto = require('crypto')
|
|
26
|
+
const { REPORTING_LIMITS } = require('../config/constants')
|
|
27
|
+
|
|
28
|
+
const ERROR_REPORTS_DIR =
|
|
29
|
+
process.env.QAA_ERROR_DIR || path.join(os.homedir(), '.create-qa-architect')
|
|
30
|
+
const ERROR_REPORTS_FILE = path.join(ERROR_REPORTS_DIR, 'error-reports.json')
|
|
31
|
+
const MAX_REPORTS = REPORTING_LIMITS.MAX_ERROR_REPORTS
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Error categories for classification
|
|
35
|
+
*/
|
|
36
|
+
const ErrorCategory = {
|
|
37
|
+
DEPENDENCY_ERROR: 'DEPENDENCY_ERROR', // npm install, missing packages
|
|
38
|
+
PERMISSION_ERROR: 'PERMISSION_ERROR', // EACCES, EPERM
|
|
39
|
+
CONFIGURATION_ERROR: 'CONFIGURATION_ERROR', // Invalid configs
|
|
40
|
+
VALIDATION_ERROR: 'VALIDATION_ERROR', // ESLint/Prettier failures
|
|
41
|
+
NETWORK_ERROR: 'NETWORK_ERROR', // npm registry, git
|
|
42
|
+
UNKNOWN_ERROR: 'UNKNOWN_ERROR', // Uncategorized
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Check if error reporting is enabled
|
|
47
|
+
* Can be enabled via ENV var or interactive prompt
|
|
48
|
+
*/
|
|
49
|
+
function isErrorReportingEnabled() {
|
|
50
|
+
const envEnabled =
|
|
51
|
+
process.env.QAA_ERROR_REPORTING === 'true' ||
|
|
52
|
+
process.env.QAA_ERROR_REPORTING === '1'
|
|
53
|
+
|
|
54
|
+
return envEnabled
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Categorize error based on error message and code
|
|
59
|
+
*/
|
|
60
|
+
function categorizeError(error) {
|
|
61
|
+
const message = error?.message?.toLowerCase() || ''
|
|
62
|
+
const code = error?.code || ''
|
|
63
|
+
|
|
64
|
+
// Permission errors
|
|
65
|
+
if (code === 'EACCES' || code === 'EPERM' || message.includes('permission')) {
|
|
66
|
+
return ErrorCategory.PERMISSION_ERROR
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Dependency errors
|
|
70
|
+
if (
|
|
71
|
+
message.includes('npm install') ||
|
|
72
|
+
message.includes('cannot find module') ||
|
|
73
|
+
message.includes('module not found') ||
|
|
74
|
+
code === 'MODULE_NOT_FOUND'
|
|
75
|
+
) {
|
|
76
|
+
return ErrorCategory.DEPENDENCY_ERROR
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Network errors
|
|
80
|
+
if (
|
|
81
|
+
code === 'ENOTFOUND' ||
|
|
82
|
+
code === 'ETIMEDOUT' ||
|
|
83
|
+
code === 'ECONNREFUSED' ||
|
|
84
|
+
message.includes('network') ||
|
|
85
|
+
message.includes('registry')
|
|
86
|
+
) {
|
|
87
|
+
return ErrorCategory.NETWORK_ERROR
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Configuration errors
|
|
91
|
+
if (
|
|
92
|
+
message.includes('package.json') ||
|
|
93
|
+
message.includes('invalid config') ||
|
|
94
|
+
message.includes('parse error') ||
|
|
95
|
+
message.includes('syntax error')
|
|
96
|
+
) {
|
|
97
|
+
return ErrorCategory.CONFIGURATION_ERROR
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Validation errors
|
|
101
|
+
if (
|
|
102
|
+
message.includes('eslint') ||
|
|
103
|
+
message.includes('prettier') ||
|
|
104
|
+
message.includes('stylelint') ||
|
|
105
|
+
message.includes('validation failed')
|
|
106
|
+
) {
|
|
107
|
+
return ErrorCategory.VALIDATION_ERROR
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return ErrorCategory.UNKNOWN_ERROR
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Sanitize file path to remove personal information
|
|
115
|
+
*/
|
|
116
|
+
function sanitizePath(filePath) {
|
|
117
|
+
if (!filePath || typeof filePath !== 'string') return filePath
|
|
118
|
+
|
|
119
|
+
// Remove username from common paths
|
|
120
|
+
const homeDir = os.homedir()
|
|
121
|
+
const sanitized = filePath.replace(homeDir, '/Users/<redacted>')
|
|
122
|
+
|
|
123
|
+
// Remove common user-specific directories
|
|
124
|
+
return sanitized
|
|
125
|
+
.replace(/\/Users\/[^/]+/g, '/Users/<redacted>')
|
|
126
|
+
.replace(/\/home\/[^/]+/g, '/home/<redacted>')
|
|
127
|
+
.replace(/C:\\Users\\[^\\]+/g, 'C:\\Users\\<redacted>')
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Sanitize error message to remove personal information
|
|
132
|
+
*/
|
|
133
|
+
function sanitizeMessage(message) {
|
|
134
|
+
if (!message || typeof message !== 'string') return message
|
|
135
|
+
|
|
136
|
+
let sanitized = message
|
|
137
|
+
|
|
138
|
+
// Remove file paths
|
|
139
|
+
sanitized = sanitizePath(sanitized)
|
|
140
|
+
|
|
141
|
+
// Remove git URLs with tokens
|
|
142
|
+
sanitized = sanitized.replace(
|
|
143
|
+
/https:\/\/[^@]+@github\.com/g,
|
|
144
|
+
'https://<token>@github.com'
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
// Remove email addresses
|
|
148
|
+
sanitized = sanitized.replace(
|
|
149
|
+
/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g,
|
|
150
|
+
'<email>'
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
return sanitized
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Sanitize stack trace to remove personal information
|
|
158
|
+
*/
|
|
159
|
+
function sanitizeStackTrace(stack) {
|
|
160
|
+
if (!stack || typeof stack !== 'string') return stack
|
|
161
|
+
|
|
162
|
+
return stack
|
|
163
|
+
.split('\n')
|
|
164
|
+
.map(line => sanitizePath(line))
|
|
165
|
+
.join('\n')
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Generate unique error report ID
|
|
170
|
+
*/
|
|
171
|
+
function generateReportId() {
|
|
172
|
+
return crypto.randomBytes(8).toString('hex')
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Ensure error reports directory exists
|
|
177
|
+
*/
|
|
178
|
+
function ensureErrorReportsDir() {
|
|
179
|
+
if (!fs.existsSync(ERROR_REPORTS_DIR)) {
|
|
180
|
+
fs.mkdirSync(ERROR_REPORTS_DIR, { recursive: true, mode: 0o700 })
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Load existing error reports
|
|
186
|
+
*/
|
|
187
|
+
function loadErrorReports() {
|
|
188
|
+
try {
|
|
189
|
+
if (fs.existsSync(ERROR_REPORTS_FILE)) {
|
|
190
|
+
const data = fs.readFileSync(ERROR_REPORTS_FILE, 'utf8')
|
|
191
|
+
return JSON.parse(data)
|
|
192
|
+
}
|
|
193
|
+
} catch {
|
|
194
|
+
// If corrupted, start fresh
|
|
195
|
+
console.warn('⚠️ Error reports data corrupted, starting fresh')
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
version: 1,
|
|
200
|
+
reports: [],
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Save error reports (with rotation)
|
|
206
|
+
*/
|
|
207
|
+
function saveErrorReports(data) {
|
|
208
|
+
try {
|
|
209
|
+
ensureErrorReportsDir()
|
|
210
|
+
|
|
211
|
+
// Rotate: keep only last MAX_REPORTS
|
|
212
|
+
if (data.reports.length > MAX_REPORTS) {
|
|
213
|
+
data.reports = data.reports.slice(-MAX_REPORTS)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
fs.writeFileSync(
|
|
217
|
+
ERROR_REPORTS_FILE,
|
|
218
|
+
JSON.stringify(data, null, 2),
|
|
219
|
+
{ mode: 0o600 } // Owner read/write only
|
|
220
|
+
)
|
|
221
|
+
} catch (error) {
|
|
222
|
+
// Silently fail - error reporting should never break the tool
|
|
223
|
+
if (process.env.DEBUG) {
|
|
224
|
+
console.error('Error reporting save failed:', error.message)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Error reporter for capturing and analyzing crashes
|
|
231
|
+
*/
|
|
232
|
+
class ErrorReporter {
|
|
233
|
+
constructor(operation = 'unknown') {
|
|
234
|
+
this.operation = operation
|
|
235
|
+
this.enabled = isErrorReportingEnabled()
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Capture and report an error
|
|
240
|
+
*
|
|
241
|
+
* @param {Error} error - The error to report
|
|
242
|
+
* @param {object} context - Additional context
|
|
243
|
+
* @param {string} userComment - Optional user comment
|
|
244
|
+
*/
|
|
245
|
+
captureError(error, context = {}, userComment = null) {
|
|
246
|
+
if (!this.enabled && !context.forceCapture) {
|
|
247
|
+
return null
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
const data = loadErrorReports()
|
|
252
|
+
|
|
253
|
+
const category = categorizeError(error)
|
|
254
|
+
const reportId = generateReportId()
|
|
255
|
+
|
|
256
|
+
const report = {
|
|
257
|
+
id: reportId,
|
|
258
|
+
timestamp: new Date().toISOString(),
|
|
259
|
+
category,
|
|
260
|
+
errorType: error?.constructor?.name || 'Error',
|
|
261
|
+
message: sanitizeMessage(error?.message || 'Unknown error'),
|
|
262
|
+
sanitizedStack: sanitizeStackTrace(error?.stack || ''),
|
|
263
|
+
operation: this.operation,
|
|
264
|
+
context: {
|
|
265
|
+
nodeVersion: process.version,
|
|
266
|
+
platform: os.platform(),
|
|
267
|
+
arch: os.arch(),
|
|
268
|
+
...context,
|
|
269
|
+
},
|
|
270
|
+
userComment: userComment || null,
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
data.reports.push(report)
|
|
274
|
+
saveErrorReports(data)
|
|
275
|
+
|
|
276
|
+
return reportId
|
|
277
|
+
} catch (captureError) {
|
|
278
|
+
// Silently fail - error reporting should never break the tool
|
|
279
|
+
if (process.env.DEBUG) {
|
|
280
|
+
console.error('Error capture failed:', captureError.message)
|
|
281
|
+
}
|
|
282
|
+
return null
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Get friendly error message for user
|
|
288
|
+
*/
|
|
289
|
+
getFriendlyMessage(error) {
|
|
290
|
+
const category = categorizeError(error)
|
|
291
|
+
|
|
292
|
+
const messages = {
|
|
293
|
+
[ErrorCategory.DEPENDENCY_ERROR]: {
|
|
294
|
+
title: '📦 Dependency Issue',
|
|
295
|
+
suggestion: 'Try running: npm install\nOr check your package.json file',
|
|
296
|
+
},
|
|
297
|
+
[ErrorCategory.PERMISSION_ERROR]: {
|
|
298
|
+
title: '🔒 Permission Denied',
|
|
299
|
+
suggestion:
|
|
300
|
+
'Try running with appropriate permissions or check file ownership',
|
|
301
|
+
},
|
|
302
|
+
[ErrorCategory.CONFIGURATION_ERROR]: {
|
|
303
|
+
title: '⚙️ Configuration Error',
|
|
304
|
+
suggestion: 'Check your configuration files for syntax errors',
|
|
305
|
+
},
|
|
306
|
+
[ErrorCategory.VALIDATION_ERROR]: {
|
|
307
|
+
title: '✅ Validation Failed',
|
|
308
|
+
suggestion:
|
|
309
|
+
'Review the validation errors above and fix them before continuing',
|
|
310
|
+
},
|
|
311
|
+
[ErrorCategory.NETWORK_ERROR]: {
|
|
312
|
+
title: '🌐 Network Issue',
|
|
313
|
+
suggestion: 'Check your internet connection and try again',
|
|
314
|
+
},
|
|
315
|
+
[ErrorCategory.UNKNOWN_ERROR]: {
|
|
316
|
+
title: '❌ Unexpected Error',
|
|
317
|
+
suggestion: 'Please report this issue with the error details below',
|
|
318
|
+
},
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return messages[category] || messages[ErrorCategory.UNKNOWN_ERROR]
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Show error report prompt to user
|
|
326
|
+
*/
|
|
327
|
+
async promptErrorReport(error) {
|
|
328
|
+
const friendly = this.getFriendlyMessage(error)
|
|
329
|
+
|
|
330
|
+
console.error('\n' + '━'.repeat(60))
|
|
331
|
+
console.error(`${friendly.title}`)
|
|
332
|
+
console.error('━'.repeat(60))
|
|
333
|
+
console.error(`Error: ${error?.message || 'Unknown error'}`)
|
|
334
|
+
console.error(`\n💡 Suggestion: ${friendly.suggestion}`)
|
|
335
|
+
console.error('━'.repeat(60))
|
|
336
|
+
|
|
337
|
+
if (!this.enabled) {
|
|
338
|
+
console.log('\n📊 Help improve this tool by reporting errors')
|
|
339
|
+
console.log('Enable error reporting: export QAA_ERROR_REPORTING=true')
|
|
340
|
+
console.log(`Report will be saved locally at: ${ERROR_REPORTS_FILE}`)
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Get error report statistics
|
|
347
|
+
*/
|
|
348
|
+
function getErrorReportStats() {
|
|
349
|
+
const data = loadErrorReports()
|
|
350
|
+
|
|
351
|
+
const stats = {
|
|
352
|
+
totalReports: data.reports.length,
|
|
353
|
+
byCategory: {},
|
|
354
|
+
byPlatform: {},
|
|
355
|
+
byNodeVersion: {},
|
|
356
|
+
recentReports: data.reports.slice(-10),
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
data.reports.forEach(report => {
|
|
360
|
+
// Count by category
|
|
361
|
+
stats.byCategory[report.category] =
|
|
362
|
+
(stats.byCategory[report.category] || 0) + 1
|
|
363
|
+
|
|
364
|
+
// Count by platform
|
|
365
|
+
const platform = report.context?.platform || 'unknown'
|
|
366
|
+
stats.byPlatform[platform] = (stats.byPlatform[platform] || 0) + 1
|
|
367
|
+
|
|
368
|
+
// Count by Node version
|
|
369
|
+
const nodeVersion = report.context?.nodeVersion || 'unknown'
|
|
370
|
+
stats.byNodeVersion[nodeVersion] =
|
|
371
|
+
(stats.byNodeVersion[nodeVersion] || 0) + 1
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
return stats
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Clear all error reports
|
|
379
|
+
*/
|
|
380
|
+
function clearErrorReports() {
|
|
381
|
+
try {
|
|
382
|
+
if (fs.existsSync(ERROR_REPORTS_FILE)) {
|
|
383
|
+
fs.unlinkSync(ERROR_REPORTS_FILE)
|
|
384
|
+
return true
|
|
385
|
+
}
|
|
386
|
+
return false
|
|
387
|
+
} catch (error) {
|
|
388
|
+
console.error('Failed to clear error reports:', error.message)
|
|
389
|
+
return false
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Show error reporting status
|
|
395
|
+
*/
|
|
396
|
+
function showErrorReportingStatus() {
|
|
397
|
+
const enabled = isErrorReportingEnabled()
|
|
398
|
+
|
|
399
|
+
console.log('\n📊 Error Reporting Status')
|
|
400
|
+
console.log('─'.repeat(50))
|
|
401
|
+
console.log(
|
|
402
|
+
`Status: ${enabled ? '✅ Enabled' : '❌ Disabled (opt-in required)'}`
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
if (enabled) {
|
|
406
|
+
const stats = getErrorReportStats()
|
|
407
|
+
console.log(`Total error reports: ${stats.totalReports}`)
|
|
408
|
+
console.log(`Storage: ${ERROR_REPORTS_FILE}`)
|
|
409
|
+
|
|
410
|
+
if (stats.totalReports > 0) {
|
|
411
|
+
console.log('\nBy Category:')
|
|
412
|
+
Object.entries(stats.byCategory).forEach(([category, count]) => {
|
|
413
|
+
console.log(` ${category}: ${count}`)
|
|
414
|
+
})
|
|
415
|
+
}
|
|
416
|
+
} else {
|
|
417
|
+
console.log('\nTo enable error reporting (opt-in):')
|
|
418
|
+
console.log(' export QAA_ERROR_REPORTING=true')
|
|
419
|
+
console.log(' # or add to ~/.bashrc or ~/.zshrc')
|
|
420
|
+
console.log('\nWhy enable error reporting?')
|
|
421
|
+
console.log(' - Helps identify common issues and failure patterns')
|
|
422
|
+
console.log(' - All data stays local (no network calls)')
|
|
423
|
+
console.log(' - No personal information collected (paths sanitized)')
|
|
424
|
+
console.log(
|
|
425
|
+
' - Easy to inspect: cat ~/.create-qa-architect/error-reports.json'
|
|
426
|
+
)
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
console.log('─'.repeat(50))
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
module.exports = {
|
|
433
|
+
ErrorReporter,
|
|
434
|
+
ErrorCategory,
|
|
435
|
+
isErrorReportingEnabled,
|
|
436
|
+
categorizeError,
|
|
437
|
+
sanitizePath,
|
|
438
|
+
sanitizeMessage,
|
|
439
|
+
sanitizeStackTrace,
|
|
440
|
+
getErrorReportStats,
|
|
441
|
+
clearErrorReports,
|
|
442
|
+
showErrorReportingStatus,
|
|
443
|
+
ERROR_REPORTS_FILE,
|
|
444
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const readline = require('readline')
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Interactive prompt utility using Node.js readline
|
|
7
|
+
* Handles user input with TTY detection for CI safety
|
|
8
|
+
*/
|
|
9
|
+
class InteractivePrompt {
|
|
10
|
+
constructor(options = {}) {
|
|
11
|
+
this.input = options.input || process.stdin
|
|
12
|
+
this.output = options.output || process.stdout
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Check if running in a TTY environment
|
|
17
|
+
* @returns {boolean} True if TTY (interactive terminal)
|
|
18
|
+
*/
|
|
19
|
+
isTTY() {
|
|
20
|
+
return Boolean(this.input.isTTY && this.output.isTTY)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Ask a single question and get user input
|
|
25
|
+
* @param {string} question - The question to ask
|
|
26
|
+
* @returns {Promise<string>} User's answer
|
|
27
|
+
*/
|
|
28
|
+
async ask(question) {
|
|
29
|
+
if (!this.isTTY()) {
|
|
30
|
+
throw new Error('Interactive mode requires a TTY environment')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
const rl = readline.createInterface({
|
|
35
|
+
input: this.input,
|
|
36
|
+
output: this.output,
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
rl.question(question, answer => {
|
|
40
|
+
rl.close()
|
|
41
|
+
resolve(answer.trim())
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
rl.on('SIGINT', () => {
|
|
45
|
+
rl.close()
|
|
46
|
+
reject(new Error('User cancelled interactive mode'))
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Ask a yes/no question
|
|
53
|
+
* @param {string} question - The question to ask
|
|
54
|
+
* @param {boolean} defaultValue - Default value if user just presses Enter
|
|
55
|
+
* @returns {Promise<boolean>} True for yes, false for no
|
|
56
|
+
*/
|
|
57
|
+
async confirm(question, defaultValue = false) {
|
|
58
|
+
const defaultHint = defaultValue ? ' [Y/n]' : ' [y/N]'
|
|
59
|
+
const answer = await this.ask(question + defaultHint + ' ')
|
|
60
|
+
|
|
61
|
+
if (answer === '') {
|
|
62
|
+
return defaultValue
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const normalized = answer.toLowerCase()
|
|
66
|
+
return normalized === 'y' || normalized === 'yes'
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Present a multiple choice question
|
|
71
|
+
* @param {string} question - The question to ask
|
|
72
|
+
* @param {Array<{value: string, label: string}>} choices - Available choices
|
|
73
|
+
* @returns {Promise<string>} Selected choice value
|
|
74
|
+
*/
|
|
75
|
+
async select(question, choices) {
|
|
76
|
+
const choiceList = choices
|
|
77
|
+
.map((choice, index) => ` ${index + 1}) ${choice.label}`)
|
|
78
|
+
.join('\n')
|
|
79
|
+
|
|
80
|
+
const fullQuestion = `${question}\n${choiceList}\n\nEnter your choice (1-${choices.length}): `
|
|
81
|
+
const answer = await this.ask(fullQuestion)
|
|
82
|
+
|
|
83
|
+
const choiceIndex = parseInt(answer, 10) - 1
|
|
84
|
+
if (
|
|
85
|
+
isNaN(choiceIndex) ||
|
|
86
|
+
choiceIndex < 0 ||
|
|
87
|
+
choiceIndex >= choices.length
|
|
88
|
+
) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`Invalid choice: ${answer}. Please enter a number between 1 and ${choices.length}`
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return choices[choiceIndex].value
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Present a multi-select question (checkboxes)
|
|
99
|
+
* @param {string} question - The question to ask
|
|
100
|
+
* @param {Array<{value: string, label: string}>} choices - Available choices
|
|
101
|
+
* @returns {Promise<Array<string>>} Array of selected choice values
|
|
102
|
+
*/
|
|
103
|
+
async multiSelect(question, choices) {
|
|
104
|
+
const choiceList = choices
|
|
105
|
+
.map((choice, index) => ` ${index + 1}) ${choice.label}`)
|
|
106
|
+
.join('\n')
|
|
107
|
+
|
|
108
|
+
const fullQuestion =
|
|
109
|
+
`${question}\n${choiceList}\n\n` +
|
|
110
|
+
`Enter your choices (comma-separated, e.g., "1,3,5" or press Enter for none): `
|
|
111
|
+
|
|
112
|
+
const answer = await this.ask(fullQuestion)
|
|
113
|
+
|
|
114
|
+
if (answer === '') {
|
|
115
|
+
return []
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const selections = answer
|
|
119
|
+
.split(',')
|
|
120
|
+
.map(s => s.trim())
|
|
121
|
+
.map(s => parseInt(s, 10) - 1)
|
|
122
|
+
.filter(index => !isNaN(index) && index >= 0 && index < choices.length)
|
|
123
|
+
|
|
124
|
+
return selections.map(index => choices[index].value)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
module.exports = { InteractivePrompt }
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Question definitions and answer parsing for interactive mode
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Generate question definitions for interactive flow
|
|
9
|
+
* @returns {Array<Object>} Array of question objects
|
|
10
|
+
*/
|
|
11
|
+
function generateQuestions() {
|
|
12
|
+
return [
|
|
13
|
+
{
|
|
14
|
+
name: 'operationMode',
|
|
15
|
+
type: 'select',
|
|
16
|
+
message: 'What would you like to do?',
|
|
17
|
+
choices: [
|
|
18
|
+
{
|
|
19
|
+
value: 'setup',
|
|
20
|
+
label: 'Full quality automation setup (recommended)',
|
|
21
|
+
},
|
|
22
|
+
{ value: 'update', label: 'Update existing configuration' },
|
|
23
|
+
{ value: 'validate', label: 'Run validation checks only' },
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: 'dependencyMonitoring',
|
|
28
|
+
type: 'confirm',
|
|
29
|
+
message: 'Include basic dependency monitoring (Free Tier)?',
|
|
30
|
+
default: true,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: 'dryRun',
|
|
34
|
+
type: 'confirm',
|
|
35
|
+
message: 'Preview changes without modifying files (dry-run mode)?',
|
|
36
|
+
default: false,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: 'toolExclusions',
|
|
40
|
+
type: 'multiSelect',
|
|
41
|
+
message: 'Disable any specific validation tools?',
|
|
42
|
+
choices: [
|
|
43
|
+
{
|
|
44
|
+
value: 'npm-audit',
|
|
45
|
+
label: 'npm audit (dependency vulnerability checks)',
|
|
46
|
+
},
|
|
47
|
+
{ value: 'gitleaks', label: 'gitleaks (secret scanning)' },
|
|
48
|
+
{
|
|
49
|
+
value: 'actionlint',
|
|
50
|
+
label: 'actionlint (GitHub Actions workflow validation)',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
value: 'markdownlint',
|
|
54
|
+
label: 'markdownlint (markdown formatting checks)',
|
|
55
|
+
},
|
|
56
|
+
{ value: 'eslint-security', label: 'ESLint security rules' },
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Parse user answers into CLI flags
|
|
64
|
+
* @param {Object} answers - User's answers to questions
|
|
65
|
+
* @returns {Array<string>} Array of CLI flags
|
|
66
|
+
*/
|
|
67
|
+
function parseAnswers(answers) {
|
|
68
|
+
const flags = []
|
|
69
|
+
|
|
70
|
+
// Operation mode
|
|
71
|
+
if (answers.operationMode === 'update') {
|
|
72
|
+
flags.push('--update')
|
|
73
|
+
} else if (answers.operationMode === 'validate') {
|
|
74
|
+
flags.push('--comprehensive')
|
|
75
|
+
}
|
|
76
|
+
// 'setup' mode is default (no flag needed)
|
|
77
|
+
|
|
78
|
+
// Dependency monitoring
|
|
79
|
+
if (answers.dependencyMonitoring) {
|
|
80
|
+
flags.push('--deps')
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Dry-run mode
|
|
84
|
+
if (answers.dryRun) {
|
|
85
|
+
flags.push('--dry-run')
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Tool exclusions
|
|
89
|
+
if (Array.isArray(answers.toolExclusions)) {
|
|
90
|
+
answers.toolExclusions.forEach(tool => {
|
|
91
|
+
flags.push(`--no-${tool}`)
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return flags
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Run interactive prompt flow
|
|
100
|
+
* @param {InteractivePrompt} prompt - Prompt instance
|
|
101
|
+
* @returns {Promise<Array<string>>} Array of CLI flags based on answers
|
|
102
|
+
*/
|
|
103
|
+
async function runInteractiveFlow(prompt) {
|
|
104
|
+
const questions = generateQuestions()
|
|
105
|
+
const answers = {}
|
|
106
|
+
|
|
107
|
+
console.log('\n🚀 Interactive Quality Automation Setup\n')
|
|
108
|
+
|
|
109
|
+
for (const question of questions) {
|
|
110
|
+
try {
|
|
111
|
+
if (question.type === 'confirm') {
|
|
112
|
+
answers[question.name] = await prompt.confirm(
|
|
113
|
+
question.message,
|
|
114
|
+
question.default
|
|
115
|
+
)
|
|
116
|
+
} else if (question.type === 'select') {
|
|
117
|
+
answers[question.name] = await prompt.select(
|
|
118
|
+
question.message,
|
|
119
|
+
question.choices
|
|
120
|
+
)
|
|
121
|
+
} else if (question.type === 'multiSelect') {
|
|
122
|
+
answers[question.name] = await prompt.multiSelect(
|
|
123
|
+
question.message,
|
|
124
|
+
question.choices
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
} catch (error) {
|
|
128
|
+
// Handle user cancellation or other errors
|
|
129
|
+
if (error.message.includes('cancelled')) {
|
|
130
|
+
console.log('\n\n❌ Interactive mode cancelled by user\n')
|
|
131
|
+
process.exit(0)
|
|
132
|
+
}
|
|
133
|
+
throw error
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
console.log('\n✅ Configuration complete!\n')
|
|
138
|
+
|
|
139
|
+
return parseAnswers(answers)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
module.exports = {
|
|
143
|
+
generateQuestions,
|
|
144
|
+
parseAnswers,
|
|
145
|
+
runInteractiveFlow,
|
|
146
|
+
}
|