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,344 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smart Test Strategy Generator
|
|
3
|
+
* Generates project-specific smart test strategy scripts
|
|
4
|
+
* Premium feature (Pro/Team/Enterprise tiers)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs')
|
|
8
|
+
const path = require('path')
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Project type configurations with risk patterns and test commands
|
|
12
|
+
*/
|
|
13
|
+
const PROJECT_CONFIGS = {
|
|
14
|
+
// CLI tools (like create-qa-architect)
|
|
15
|
+
cli: {
|
|
16
|
+
name: 'CLI Tool',
|
|
17
|
+
highRiskRegex: 'setup\\.js|lib/.*|templates/.*|config/.*|bin/.*',
|
|
18
|
+
testCommands: {
|
|
19
|
+
comprehensive:
|
|
20
|
+
'npm run test:comprehensive 2>/dev/null || npm run test 2>/dev/null || npm test',
|
|
21
|
+
medium:
|
|
22
|
+
'npm run test:medium 2>/dev/null || npm run test:fast 2>/dev/null || npm test',
|
|
23
|
+
fast: 'npm run test:fast 2>/dev/null || npm run test:unit 2>/dev/null || npm test',
|
|
24
|
+
minimal: 'npm run lint && npm run format:check',
|
|
25
|
+
},
|
|
26
|
+
detection: projectPath => {
|
|
27
|
+
const pkg = readPackageJson(projectPath)
|
|
28
|
+
return pkg?.bin || pkg?.scripts?.setup
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
// Web applications (Next.js, React, Vue, etc.)
|
|
33
|
+
webapp: {
|
|
34
|
+
name: 'Web Application',
|
|
35
|
+
highRiskRegex:
|
|
36
|
+
'auth|payment|security|crypto|api/|pages/api/|app/api/|middleware',
|
|
37
|
+
testCommands: {
|
|
38
|
+
comprehensive:
|
|
39
|
+
'npm run test:comprehensive 2>/dev/null || (npm test && npm run test:e2e 2>/dev/null)',
|
|
40
|
+
medium:
|
|
41
|
+
'npm run test:medium 2>/dev/null || npm run test -- --testPathIgnorePatterns=e2e',
|
|
42
|
+
fast: 'npm run test:fast 2>/dev/null || npm run test -- --watch=false --coverage=false',
|
|
43
|
+
minimal: 'npm run lint && npm run format:check',
|
|
44
|
+
},
|
|
45
|
+
detection: projectPath => {
|
|
46
|
+
const pkg = readPackageJson(projectPath)
|
|
47
|
+
const deps = { ...pkg?.dependencies, ...pkg?.devDependencies }
|
|
48
|
+
return (
|
|
49
|
+
deps?.next ||
|
|
50
|
+
deps?.react ||
|
|
51
|
+
deps?.vue ||
|
|
52
|
+
deps?.['@angular/core'] ||
|
|
53
|
+
deps?.svelte
|
|
54
|
+
)
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
// SaaS applications (payment, billing, auth heavy)
|
|
59
|
+
saas: {
|
|
60
|
+
name: 'SaaS Application',
|
|
61
|
+
highRiskRegex:
|
|
62
|
+
'auth|payment|billing|stripe|subscription|prisma/schema|middleware|webhook',
|
|
63
|
+
testCommands: {
|
|
64
|
+
comprehensive:
|
|
65
|
+
'npm run test:comprehensive 2>/dev/null || (npm test && npm run security:audit 2>/dev/null)',
|
|
66
|
+
medium: 'npm run test:medium 2>/dev/null || npm test',
|
|
67
|
+
fast: 'npm run test:fast 2>/dev/null || npm run test:unit 2>/dev/null || npm test',
|
|
68
|
+
minimal: 'npm run lint && npm run format:check',
|
|
69
|
+
},
|
|
70
|
+
detection: projectPath => {
|
|
71
|
+
const pkg = readPackageJson(projectPath)
|
|
72
|
+
const deps = { ...pkg?.dependencies, ...pkg?.devDependencies }
|
|
73
|
+
return deps?.stripe || deps?.['@stripe/stripe-js'] || deps?.prisma
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
// API services (Express, Fastify, etc.)
|
|
78
|
+
api: {
|
|
79
|
+
name: 'API Service',
|
|
80
|
+
highRiskRegex: 'routes/|controllers/|middleware/|auth|security|database',
|
|
81
|
+
testCommands: {
|
|
82
|
+
comprehensive:
|
|
83
|
+
'npm run test:comprehensive 2>/dev/null || npm run test:integration 2>/dev/null || npm test',
|
|
84
|
+
medium:
|
|
85
|
+
'npm run test:medium 2>/dev/null || npm run test -- --testPathIgnorePatterns=integration',
|
|
86
|
+
fast: 'npm run test:fast 2>/dev/null || npm run test:unit 2>/dev/null || npm test',
|
|
87
|
+
minimal: 'npm run lint && npm run format:check',
|
|
88
|
+
},
|
|
89
|
+
detection: projectPath => {
|
|
90
|
+
const pkg = readPackageJson(projectPath)
|
|
91
|
+
const deps = { ...pkg?.dependencies, ...pkg?.devDependencies }
|
|
92
|
+
return (
|
|
93
|
+
deps?.express ||
|
|
94
|
+
deps?.fastify ||
|
|
95
|
+
deps?.koa ||
|
|
96
|
+
deps?.hapi ||
|
|
97
|
+
deps?.restify
|
|
98
|
+
)
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
// Library/Package
|
|
103
|
+
library: {
|
|
104
|
+
name: 'Library/Package',
|
|
105
|
+
highRiskRegex: 'src/|lib/|index\\.(js|ts)|package\\.json',
|
|
106
|
+
testCommands: {
|
|
107
|
+
comprehensive:
|
|
108
|
+
'npm run test:comprehensive 2>/dev/null || (npm test && npm run build 2>/dev/null)',
|
|
109
|
+
medium: 'npm run test:medium 2>/dev/null || npm test',
|
|
110
|
+
fast: 'npm run test:fast 2>/dev/null || npm run test:unit 2>/dev/null || npm test',
|
|
111
|
+
minimal: 'npm run lint && npm run format:check',
|
|
112
|
+
},
|
|
113
|
+
detection: projectPath => {
|
|
114
|
+
const pkg = readPackageJson(projectPath)
|
|
115
|
+
return pkg?.main || pkg?.module || pkg?.exports
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
// Documentation project
|
|
120
|
+
docs: {
|
|
121
|
+
name: 'Documentation',
|
|
122
|
+
highRiskRegex: 'guides/security|guides/deployment|setup-instructions',
|
|
123
|
+
testCommands: {
|
|
124
|
+
comprehensive:
|
|
125
|
+
'npm run test:comprehensive 2>/dev/null || (npm run lint && npm run link:check 2>/dev/null)',
|
|
126
|
+
medium:
|
|
127
|
+
'npm run test:medium 2>/dev/null || npm run lint && npm run spell:check 2>/dev/null',
|
|
128
|
+
fast: 'npm run test:fast 2>/dev/null || npm run lint',
|
|
129
|
+
minimal: 'npm run lint 2>/dev/null || npx markdownlint "**/*.md"',
|
|
130
|
+
},
|
|
131
|
+
detection: projectPath => {
|
|
132
|
+
const hasReadme = fs.existsSync(path.join(projectPath, 'README.md'))
|
|
133
|
+
const hasDocs = fs.existsSync(path.join(projectPath, 'docs'))
|
|
134
|
+
const pkg = readPackageJson(projectPath)
|
|
135
|
+
return hasDocs && !pkg?.dependencies && hasReadme
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
// Default fallback
|
|
140
|
+
default: {
|
|
141
|
+
name: 'General Project',
|
|
142
|
+
highRiskRegex: 'src/|lib/|config/|package\\.json',
|
|
143
|
+
testCommands: {
|
|
144
|
+
comprehensive:
|
|
145
|
+
'npm run test:comprehensive 2>/dev/null || npm test 2>/dev/null || echo "No test script found"',
|
|
146
|
+
medium:
|
|
147
|
+
'npm run test:medium 2>/dev/null || npm test 2>/dev/null || echo "No test script found"',
|
|
148
|
+
fast: 'npm run test:fast 2>/dev/null || npm run test:unit 2>/dev/null || npm test 2>/dev/null || echo "No test script found"',
|
|
149
|
+
minimal: 'npm run lint && npm run format:check',
|
|
150
|
+
},
|
|
151
|
+
detection: () => true, // Fallback always matches
|
|
152
|
+
},
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Read package.json from project path
|
|
157
|
+
*/
|
|
158
|
+
function readPackageJson(projectPath) {
|
|
159
|
+
try {
|
|
160
|
+
const pkgPath = path.join(projectPath, 'package.json')
|
|
161
|
+
if (fs.existsSync(pkgPath)) {
|
|
162
|
+
return JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
|
|
163
|
+
}
|
|
164
|
+
} catch {
|
|
165
|
+
// Ignore errors
|
|
166
|
+
}
|
|
167
|
+
return null
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Detect project type based on dependencies and structure
|
|
172
|
+
*/
|
|
173
|
+
function detectProjectType(projectPath) {
|
|
174
|
+
// Check each project type in priority order
|
|
175
|
+
const typeOrder = ['saas', 'webapp', 'api', 'cli', 'library', 'docs']
|
|
176
|
+
|
|
177
|
+
for (const type of typeOrder) {
|
|
178
|
+
const config = PROJECT_CONFIGS[type]
|
|
179
|
+
if (config.detection(projectPath)) {
|
|
180
|
+
return type
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return 'default'
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Generate smart test strategy script for a project
|
|
189
|
+
*/
|
|
190
|
+
function generateSmartStrategy(options = {}) {
|
|
191
|
+
const {
|
|
192
|
+
projectPath = process.cwd(),
|
|
193
|
+
projectName = path.basename(projectPath),
|
|
194
|
+
projectType = null, // Auto-detect if not provided
|
|
195
|
+
customHighRiskRegex = null,
|
|
196
|
+
customTestCommands = null,
|
|
197
|
+
} = options
|
|
198
|
+
|
|
199
|
+
// Detect or use provided project type
|
|
200
|
+
const detectedType = projectType || detectProjectType(projectPath)
|
|
201
|
+
const config = PROJECT_CONFIGS[detectedType] || PROJECT_CONFIGS.default
|
|
202
|
+
|
|
203
|
+
// Allow custom overrides
|
|
204
|
+
const highRiskRegex = customHighRiskRegex || config.highRiskRegex
|
|
205
|
+
const testCommands = { ...config.testCommands, ...customTestCommands }
|
|
206
|
+
|
|
207
|
+
// Read template
|
|
208
|
+
const templatePath = path.join(
|
|
209
|
+
__dirname,
|
|
210
|
+
'..',
|
|
211
|
+
'templates',
|
|
212
|
+
'scripts',
|
|
213
|
+
'smart-test-strategy.sh'
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
if (!fs.existsSync(templatePath)) {
|
|
217
|
+
throw new Error(`Smart strategy template not found at ${templatePath}`)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
let template = fs.readFileSync(templatePath, 'utf8')
|
|
221
|
+
|
|
222
|
+
// Replace placeholders
|
|
223
|
+
template = template.replace(/\{\{PROJECT_NAME\}\}/g, projectName)
|
|
224
|
+
template = template.replace(/\{\{HIGH_RISK_REGEX\}\}/g, highRiskRegex)
|
|
225
|
+
template = template.replace(
|
|
226
|
+
/\{\{HIGH_RISK_PATTERN\}\}/g,
|
|
227
|
+
`Project type: ${config.name}`
|
|
228
|
+
)
|
|
229
|
+
template = template.replace(
|
|
230
|
+
/\{\{TEST_COMPREHENSIVE\}\}/g,
|
|
231
|
+
testCommands.comprehensive
|
|
232
|
+
)
|
|
233
|
+
template = template.replace(/\{\{TEST_MEDIUM\}\}/g, testCommands.medium)
|
|
234
|
+
template = template.replace(/\{\{TEST_FAST\}\}/g, testCommands.fast)
|
|
235
|
+
template = template.replace(/\{\{TEST_MINIMAL\}\}/g, testCommands.minimal)
|
|
236
|
+
template = template.replace(
|
|
237
|
+
/\{\{COMPREHENSIVE_COMMAND\}\}/g,
|
|
238
|
+
`Runs: ${testCommands.comprehensive}`
|
|
239
|
+
)
|
|
240
|
+
template = template.replace(
|
|
241
|
+
/\{\{MEDIUM_COMMAND\}\}/g,
|
|
242
|
+
`Runs: ${testCommands.medium}`
|
|
243
|
+
)
|
|
244
|
+
template = template.replace(
|
|
245
|
+
/\{\{FAST_COMMAND\}\}/g,
|
|
246
|
+
`Runs: ${testCommands.fast}`
|
|
247
|
+
)
|
|
248
|
+
template = template.replace(
|
|
249
|
+
/\{\{MINIMAL_COMMAND\}\}/g,
|
|
250
|
+
`Runs: ${testCommands.minimal}`
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
script: template,
|
|
255
|
+
projectType: detectedType,
|
|
256
|
+
projectTypeName: config.name,
|
|
257
|
+
highRiskRegex,
|
|
258
|
+
testCommands,
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Write smart strategy script to project
|
|
264
|
+
*/
|
|
265
|
+
function writeSmartStrategy(projectPath, script) {
|
|
266
|
+
const scriptsDir = path.join(projectPath, 'scripts')
|
|
267
|
+
const scriptPath = path.join(scriptsDir, 'smart-test-strategy.sh')
|
|
268
|
+
|
|
269
|
+
// Create scripts directory if needed
|
|
270
|
+
if (!fs.existsSync(scriptsDir)) {
|
|
271
|
+
fs.mkdirSync(scriptsDir, { recursive: true })
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Write script
|
|
275
|
+
fs.writeFileSync(scriptPath, script)
|
|
276
|
+
fs.chmodSync(scriptPath, 0o755)
|
|
277
|
+
|
|
278
|
+
return scriptPath
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Generate pre-push hook that uses smart strategy
|
|
283
|
+
*/
|
|
284
|
+
function generateSmartPrePushHook() {
|
|
285
|
+
return `#!/bin/sh
|
|
286
|
+
. "$(dirname "$0")/_/husky.sh"
|
|
287
|
+
|
|
288
|
+
echo "š Running smart pre-push validation..."
|
|
289
|
+
|
|
290
|
+
# Check if smart test strategy script exists
|
|
291
|
+
if [ -f "scripts/smart-test-strategy.sh" ]; then
|
|
292
|
+
bash scripts/smart-test-strategy.sh
|
|
293
|
+
else
|
|
294
|
+
# Fallback to basic validation
|
|
295
|
+
echo "š Linting..."
|
|
296
|
+
npm run lint || {
|
|
297
|
+
echo "ā Lint failed! Fix errors before pushing."
|
|
298
|
+
exit 1
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
echo "⨠Checking formatting..."
|
|
302
|
+
npm run format:check || {
|
|
303
|
+
echo "ā Format check failed! Run 'npm run format' to fix."
|
|
304
|
+
exit 1
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
# Run tests if they exist
|
|
308
|
+
if node -e "const pkg=require('./package.json');process.exit(pkg.scripts.test?0:1)" 2>/dev/null; then
|
|
309
|
+
echo "š§Ŗ Running tests..."
|
|
310
|
+
npm test || {
|
|
311
|
+
echo "ā Tests failed! Fix failing tests before pushing."
|
|
312
|
+
exit 1
|
|
313
|
+
}
|
|
314
|
+
fi
|
|
315
|
+
|
|
316
|
+
echo "ā
Pre-push validation passed!"
|
|
317
|
+
fi
|
|
318
|
+
`
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Add test tier scripts to package.json
|
|
323
|
+
*/
|
|
324
|
+
function getTestTierScripts(_projectType) {
|
|
325
|
+
// const config = PROJECT_CONFIGS[projectType] || PROJECT_CONFIGS.default
|
|
326
|
+
|
|
327
|
+
return {
|
|
328
|
+
'test:fast': 'vitest run --reporter=basic --coverage=false',
|
|
329
|
+
'test:medium':
|
|
330
|
+
'vitest run --reporter=basic --testPathIgnorePatterns=e2e,integration',
|
|
331
|
+
'test:comprehensive':
|
|
332
|
+
'vitest run && npm run lint && npm run format:check && npm run security:audit 2>/dev/null || true',
|
|
333
|
+
'test:smart': 'bash scripts/smart-test-strategy.sh',
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
module.exports = {
|
|
338
|
+
PROJECT_CONFIGS,
|
|
339
|
+
detectProjectType,
|
|
340
|
+
generateSmartStrategy,
|
|
341
|
+
writeSmartStrategy,
|
|
342
|
+
generateSmartPrePushHook,
|
|
343
|
+
getTestTierScripts,
|
|
344
|
+
}
|
package/lib/telemetry.js
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Telemetry module for usage tracking (opt-in only)
|
|
5
|
+
*
|
|
6
|
+
* Privacy principles:
|
|
7
|
+
* - Completely opt-in (ENV var or explicit flag)
|
|
8
|
+
* - No personal information collected (no paths, usernames, IPs)
|
|
9
|
+
* - Local storage only (no network calls)
|
|
10
|
+
* - Easy to inspect and delete
|
|
11
|
+
* - Anonymous session IDs
|
|
12
|
+
*
|
|
13
|
+
* Data collected:
|
|
14
|
+
* - Event types (setup_started, setup_completed, setup_failed)
|
|
15
|
+
* - Timestamps
|
|
16
|
+
* - Template selection (custom vs default)
|
|
17
|
+
* - Features used (validate, deps, dry-run, interactive)
|
|
18
|
+
* - Node version and OS platform
|
|
19
|
+
* - Error types (if failed)
|
|
20
|
+
* - Setup duration
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const fs = require('fs')
|
|
24
|
+
const path = require('path')
|
|
25
|
+
const os = require('os')
|
|
26
|
+
const crypto = require('crypto')
|
|
27
|
+
const { REPORTING_LIMITS } = require('../config/constants')
|
|
28
|
+
|
|
29
|
+
const getTelemetryDir = () =>
|
|
30
|
+
process.env.QAA_TELEMETRY_DIR ||
|
|
31
|
+
path.join(os.homedir(), '.create-qa-architect')
|
|
32
|
+
const getTelemetryFile = () => path.join(getTelemetryDir(), 'telemetry.json')
|
|
33
|
+
const TELEMETRY_FILE = getTelemetryFile()
|
|
34
|
+
const MAX_EVENTS = REPORTING_LIMITS.MAX_TELEMETRY_EVENTS
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Check if telemetry is enabled
|
|
38
|
+
* Opt-in via environment variable or explicit flag
|
|
39
|
+
*/
|
|
40
|
+
function isTelemetryEnabled() {
|
|
41
|
+
// Check environment variable
|
|
42
|
+
const envEnabled =
|
|
43
|
+
process.env.QAA_TELEMETRY === 'true' || process.env.QAA_TELEMETRY === '1'
|
|
44
|
+
|
|
45
|
+
// Check config file (future: allow persistent opt-in)
|
|
46
|
+
// For now, only ENV var
|
|
47
|
+
|
|
48
|
+
return envEnabled
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Generate anonymous session ID
|
|
53
|
+
*/
|
|
54
|
+
function generateSessionId() {
|
|
55
|
+
return crypto.randomBytes(16).toString('hex')
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Ensure telemetry directory exists
|
|
60
|
+
*/
|
|
61
|
+
function ensureTelemetryDir() {
|
|
62
|
+
const dir = getTelemetryDir()
|
|
63
|
+
if (!fs.existsSync(dir)) {
|
|
64
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 })
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Load existing telemetry data
|
|
70
|
+
*/
|
|
71
|
+
function loadTelemetryData() {
|
|
72
|
+
try {
|
|
73
|
+
const file = getTelemetryFile()
|
|
74
|
+
if (fs.existsSync(file)) {
|
|
75
|
+
const data = fs.readFileSync(file, 'utf8')
|
|
76
|
+
return JSON.parse(data)
|
|
77
|
+
}
|
|
78
|
+
} catch {
|
|
79
|
+
// If corrupted, start fresh
|
|
80
|
+
console.warn('ā ļø Telemetry data corrupted, starting fresh')
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
version: 1,
|
|
85
|
+
events: [],
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Save telemetry data (with rotation)
|
|
91
|
+
*/
|
|
92
|
+
function saveTelemetryData(data) {
|
|
93
|
+
try {
|
|
94
|
+
ensureTelemetryDir()
|
|
95
|
+
const file = getTelemetryFile()
|
|
96
|
+
|
|
97
|
+
// Rotate: keep only last MAX_EVENTS
|
|
98
|
+
if (data.events.length > MAX_EVENTS) {
|
|
99
|
+
data.events = data.events.slice(-MAX_EVENTS)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
fs.writeFileSync(file, JSON.stringify(data, null, 2), {
|
|
103
|
+
mode: 0o600, // Owner read/write only
|
|
104
|
+
})
|
|
105
|
+
} catch (error) {
|
|
106
|
+
// Silently fail - telemetry should never break the tool
|
|
107
|
+
// Only log in debug mode
|
|
108
|
+
if (process.env.DEBUG) {
|
|
109
|
+
console.error('Telemetry save error:', error.message)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Record a telemetry event
|
|
116
|
+
*
|
|
117
|
+
* @param {string} eventType - Event type (setup_started, setup_completed, setup_failed)
|
|
118
|
+
* @param {object} metadata - Additional event metadata
|
|
119
|
+
* @param {string} sessionId - Session ID for grouping related events
|
|
120
|
+
*/
|
|
121
|
+
function recordEvent(eventType, metadata = {}, sessionId = null) {
|
|
122
|
+
if (!isTelemetryEnabled()) {
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const data = loadTelemetryData()
|
|
128
|
+
|
|
129
|
+
const event = {
|
|
130
|
+
eventType,
|
|
131
|
+
timestamp: new Date().toISOString(),
|
|
132
|
+
sessionId: sessionId || generateSessionId(),
|
|
133
|
+
metadata: {
|
|
134
|
+
nodeVersion: process.version,
|
|
135
|
+
platform: os.platform(),
|
|
136
|
+
arch: os.arch(),
|
|
137
|
+
...metadata,
|
|
138
|
+
},
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
data.events.push(event)
|
|
142
|
+
saveTelemetryData(data)
|
|
143
|
+
} catch (error) {
|
|
144
|
+
// Silently fail - telemetry should never break the tool
|
|
145
|
+
if (process.env.DEBUG) {
|
|
146
|
+
console.error('Telemetry record error:', error.message)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Create a telemetry session for tracking related events
|
|
153
|
+
*/
|
|
154
|
+
class TelemetrySession {
|
|
155
|
+
constructor() {
|
|
156
|
+
this.sessionId = generateSessionId()
|
|
157
|
+
this.startTime = Date.now()
|
|
158
|
+
this.enabled = isTelemetryEnabled()
|
|
159
|
+
this.telemetryFile = getTelemetryFile()
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Record setup start event
|
|
164
|
+
*/
|
|
165
|
+
recordStart(metadata = {}) {
|
|
166
|
+
if (!this.enabled) return
|
|
167
|
+
|
|
168
|
+
recordEvent(
|
|
169
|
+
'setup_started',
|
|
170
|
+
{
|
|
171
|
+
...metadata,
|
|
172
|
+
timestamp: new Date(this.startTime).toISOString(),
|
|
173
|
+
},
|
|
174
|
+
this.sessionId
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Record setup completion event
|
|
180
|
+
*/
|
|
181
|
+
recordComplete(metadata = {}) {
|
|
182
|
+
if (!this.enabled) return
|
|
183
|
+
|
|
184
|
+
const duration = Date.now() - this.startTime
|
|
185
|
+
|
|
186
|
+
recordEvent(
|
|
187
|
+
'setup_completed',
|
|
188
|
+
{
|
|
189
|
+
...metadata,
|
|
190
|
+
durationMs: duration,
|
|
191
|
+
durationSec: Math.round(duration / 1000),
|
|
192
|
+
},
|
|
193
|
+
this.sessionId
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Record setup failure event
|
|
199
|
+
*/
|
|
200
|
+
recordFailure(error, metadata = {}) {
|
|
201
|
+
if (!this.enabled) return
|
|
202
|
+
|
|
203
|
+
const duration = Date.now() - this.startTime
|
|
204
|
+
|
|
205
|
+
recordEvent(
|
|
206
|
+
'setup_failed',
|
|
207
|
+
{
|
|
208
|
+
...metadata,
|
|
209
|
+
errorType: error?.constructor?.name || 'Unknown',
|
|
210
|
+
errorMessage: error?.message || 'Unknown error',
|
|
211
|
+
durationMs: duration,
|
|
212
|
+
},
|
|
213
|
+
this.sessionId
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Record validation event
|
|
219
|
+
*/
|
|
220
|
+
recordValidation(validationType, passed, metadata = {}) {
|
|
221
|
+
if (!this.enabled) return
|
|
222
|
+
|
|
223
|
+
recordEvent(
|
|
224
|
+
'validation',
|
|
225
|
+
{
|
|
226
|
+
...metadata,
|
|
227
|
+
validationType,
|
|
228
|
+
passed,
|
|
229
|
+
},
|
|
230
|
+
this.sessionId
|
|
231
|
+
)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Get telemetry statistics (for debugging/testing)
|
|
237
|
+
*/
|
|
238
|
+
function getTelemetryStats() {
|
|
239
|
+
const data = loadTelemetryData()
|
|
240
|
+
|
|
241
|
+
const stats = {
|
|
242
|
+
totalEvents: data.events.length,
|
|
243
|
+
eventTypes: {},
|
|
244
|
+
platforms: {},
|
|
245
|
+
nodeVersions: {},
|
|
246
|
+
recentEvents: data.events.slice(-10),
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
data.events.forEach(event => {
|
|
250
|
+
// Count event types
|
|
251
|
+
stats.eventTypes[event.eventType] =
|
|
252
|
+
(stats.eventTypes[event.eventType] || 0) + 1
|
|
253
|
+
|
|
254
|
+
// Count platforms
|
|
255
|
+
const platform = event.metadata?.platform || 'unknown'
|
|
256
|
+
stats.platforms[platform] = (stats.platforms[platform] || 0) + 1
|
|
257
|
+
|
|
258
|
+
// Count Node versions
|
|
259
|
+
const nodeVersion = event.metadata?.nodeVersion || 'unknown'
|
|
260
|
+
stats.nodeVersions[nodeVersion] = (stats.nodeVersions[nodeVersion] || 0) + 1
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
return stats
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Clear all telemetry data
|
|
268
|
+
*/
|
|
269
|
+
function clearTelemetry() {
|
|
270
|
+
try {
|
|
271
|
+
if (fs.existsSync(TELEMETRY_FILE)) {
|
|
272
|
+
fs.unlinkSync(TELEMETRY_FILE)
|
|
273
|
+
return true
|
|
274
|
+
}
|
|
275
|
+
return false
|
|
276
|
+
} catch (error) {
|
|
277
|
+
console.error('Failed to clear telemetry:', error.message)
|
|
278
|
+
return false
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Show telemetry status and opt-in instructions
|
|
284
|
+
*/
|
|
285
|
+
function showTelemetryStatus() {
|
|
286
|
+
const enabled = isTelemetryEnabled()
|
|
287
|
+
|
|
288
|
+
console.log('\nš Telemetry Status')
|
|
289
|
+
console.log('ā'.repeat(50))
|
|
290
|
+
console.log(
|
|
291
|
+
`Status: ${enabled ? 'ā
Enabled' : 'ā Disabled (opt-in required)'}`
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
if (enabled) {
|
|
295
|
+
const stats = getTelemetryStats()
|
|
296
|
+
console.log(`Events collected: ${stats.totalEvents}`)
|
|
297
|
+
console.log(`Storage: ${TELEMETRY_FILE}`)
|
|
298
|
+
} else {
|
|
299
|
+
console.log('\nTo enable telemetry (opt-in):')
|
|
300
|
+
console.log(' export QAA_TELEMETRY=true')
|
|
301
|
+
console.log(' # or add to ~/.bashrc or ~/.zshrc')
|
|
302
|
+
console.log('\nWhy enable telemetry?')
|
|
303
|
+
console.log(' - Helps improve the tool based on real usage patterns')
|
|
304
|
+
console.log(' - All data stays local (no network calls)')
|
|
305
|
+
console.log(' - No personal information collected')
|
|
306
|
+
console.log(
|
|
307
|
+
' - Easy to inspect: cat ~/.create-qa-architect/telemetry.json'
|
|
308
|
+
)
|
|
309
|
+
console.log(' - Easy to delete: rm ~/.create-qa-architect/telemetry.json')
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
console.log('ā'.repeat(50))
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
module.exports = {
|
|
316
|
+
isTelemetryEnabled,
|
|
317
|
+
recordEvent,
|
|
318
|
+
TelemetrySession,
|
|
319
|
+
getTelemetryStats,
|
|
320
|
+
clearTelemetry,
|
|
321
|
+
showTelemetryStatus,
|
|
322
|
+
TELEMETRY_FILE,
|
|
323
|
+
}
|