@ai-qa/workflow 2.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.
Files changed (48) hide show
  1. package/.github/agents/playwright-test-generator.agent.md +33 -0
  2. package/.github/agents/playwright-test-healer.agent.md +36 -0
  3. package/.github/agents/playwright-test-planner.agent.md +44 -0
  4. package/.opencode/agents/qa-generator.md +19 -0
  5. package/.opencode/agents/qa-healer.md +25 -0
  6. package/.opencode/agents/qa-planner.md +20 -0
  7. package/.qa-workflow.json +22 -0
  8. package/README.md +365 -0
  9. package/ai-qa-workflow.js +330 -0
  10. package/cli.js +7 -0
  11. package/docs/application-context.md +20 -0
  12. package/install.js +303 -0
  13. package/opencode.json +31 -0
  14. package/package.json +30 -0
  15. package/prompts/QAe2eprompt.md +513 -0
  16. package/prompts/general_prompt.md +13 -0
  17. package/qa-dashboard/.env +3 -0
  18. package/qa-dashboard/app.js +46 -0
  19. package/qa-dashboard/package.json +18 -0
  20. package/qa-dashboard/public/css/style.css +266 -0
  21. package/qa-dashboard/public/js/main.js +6 -0
  22. package/qa-dashboard/routes/analytics.js +52 -0
  23. package/qa-dashboard/routes/export.js +153 -0
  24. package/qa-dashboard/routes/index.js +10 -0
  25. package/qa-dashboard/routes/projects.js +92 -0
  26. package/qa-dashboard/routes/runs.js +66 -0
  27. package/qa-dashboard/routes/stories.js +101 -0
  28. package/qa-dashboard/routes/test-data.js +82 -0
  29. package/qa-dashboard/services/cli-bridge.js +143 -0
  30. package/qa-dashboard/services/project-manager.js +61 -0
  31. package/qa-dashboard/views/analytics.ejs +188 -0
  32. package/qa-dashboard/views/error.ejs +8 -0
  33. package/qa-dashboard/views/index.ejs +83 -0
  34. package/qa-dashboard/views/layouts/main.ejs +28 -0
  35. package/qa-dashboard/views/project-add.ejs +23 -0
  36. package/qa-dashboard/views/project.ejs +97 -0
  37. package/qa-dashboard/views/projects.ejs +29 -0
  38. package/qa-dashboard/views/run.ejs +84 -0
  39. package/qa-dashboard/views/runs.ejs +64 -0
  40. package/qa-dashboard/views/stories.ejs +53 -0
  41. package/qa-dashboard/views/story.ejs +63 -0
  42. package/qa-dashboard/views/test-data.ejs +117 -0
  43. package/scripts/executor.js +142 -0
  44. package/scripts/generator.js +130 -0
  45. package/scripts/healer.js +207 -0
  46. package/scripts/planner.js +142 -0
  47. package/scripts/reporter.js +190 -0
  48. package/scripts/utils.js +244 -0
@@ -0,0 +1,330 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { DIRS, ROOT, CONFIG, ensureDir, listUserStories, listTestPlans, listTestSpecs, timestamp, log, reloadConfig, autoDetectConfig } = require('./scripts/utils');
4
+ const { generateTestPlan } = require('./scripts/planner');
5
+ const { generateTestSpec } = require('./scripts/generator');
6
+ const { executeTests } = require('./scripts/executor');
7
+ const { selfHeal } = require('./scripts/healer');
8
+ const { generateReport } = require('./scripts/reporter');
9
+ const path = require('path');
10
+ const fs = require('fs');
11
+
12
+ const PROJECT_NAME = CONFIG.project.name;
13
+
14
+ const COMMANDS = {
15
+ init: { desc: 'Create required directories and config', fn: cmdInit },
16
+ plan: { desc: '<user-story.md> Generate test plan from user story', fn: cmdPlan },
17
+ generate:{ desc: '<test-plan.md> Generate test spec from test plan', fn: cmdGenerate },
18
+ execute: { desc: '[test-name] Run tests with Playwright', fn: cmdExecute },
19
+ heal: { desc: '[run-id] Self-heal failed tests', fn: cmdHeal },
20
+ report: { desc: '[run-id] Generate execution report', fn: cmdReport },
21
+ 'report:allure': { desc: '[run-id] Generate Allure report from test results', fn: cmdAllure },
22
+ run: { desc: '<story-name.md> Full pipeline: plan → generate → execute → heal → report', fn: cmdRun },
23
+ status: { desc: 'Show workflow state', fn: cmdStatus },
24
+ list: { desc: 'List user stories, test plans, and test specs', fn: cmdList },
25
+ };
26
+
27
+ function cmdInit() {
28
+ log('INIT', 'Creating required directories...');
29
+ Object.values(DIRS).forEach(dir => ensureDir(dir));
30
+
31
+ const configPath = path.join(ROOT, '.qa-workflow.json');
32
+ if (!fs.existsSync(configPath)) {
33
+ log('INIT', 'Scanning project to auto-detect configuration...');
34
+ const detected = autoDetectConfig();
35
+ fs.writeFileSync(configPath, JSON.stringify(detected, null, 2) + '\n', 'utf-8');
36
+ log('INIT', '.qa-workflow.json created with auto-detected settings');
37
+ log('INIT', `Project: ${detected.project.name} → ${detected.project.url}`);
38
+ reloadConfig();
39
+ }
40
+
41
+ const envContent = `# Application Context
42
+ # Auto-generated from project scan. Edit to provide more context for AI agents.
43
+
44
+ ## Project
45
+ - Name: ${CONFIG.project.name}
46
+ - Description: ${CONFIG.project.description || '(auto-detect from README)'}
47
+ - URL: ${CONFIG.project.url}
48
+ - Environment: ${CONFIG.project.environment || 'Web Application'}
49
+
50
+ ## Tech Stack
51
+ - (auto-detected from project files)
52
+
53
+ ## Stable Selectors
54
+ - (populate during exploration)
55
+
56
+ ## Known Flaky Areas
57
+ - (populate during execution)
58
+
59
+ ## Authentication
60
+ - User: ${CONFIG.auth.user || '(set in .qa-workflow.json or auto-detect from .env)'}
61
+ `;
62
+ const envPath = path.join(DIRS.docs, 'application-context.md');
63
+ if (!fs.existsSync(envPath)) {
64
+ fs.writeFileSync(envPath, envContent, 'utf-8');
65
+ log('INIT', 'docs/application-context.md created — AI agents will use this for context');
66
+ }
67
+
68
+ log('INIT', 'Ready. Directories: user-story/, specs/, tests/, test-results/, docs/');
69
+ log('INIT', 'Edit .qa-workflow.json to override auto-detected values');
70
+ }
71
+
72
+ function cmdPlan() {
73
+ const storyName = process.argv[3];
74
+ if (!storyName) {
75
+ console.log(' Usage: node ai-qa-workflow.js plan <user-story-file.md>');
76
+ console.log(` Available: ${listUserStories().join(', ')}`);
77
+ process.exit(1);
78
+ }
79
+ ensureDir(DIRS.specs);
80
+ const result = generateTestPlan(storyName);
81
+ console.log(`\n ✓ Test plan generated: specs/${path.basename(result.planPath)}`);
82
+ console.log(`\n Next step: Review and edit the test plan, then run:`);
83
+ console.log(` node ai-qa-workflow.js generate ${path.basename(result.planPath)}`);
84
+ }
85
+
86
+ function cmdGenerate() {
87
+ const planName = process.argv[3];
88
+ if (!planName) {
89
+ console.log(' Usage: node ai-qa-workflow.js generate <test-plan-file.md>');
90
+ console.log(` Available: ${listTestPlans().join(', ')}`);
91
+ process.exit(1);
92
+ }
93
+ ensureDir(DIRS.tests);
94
+ const result = generateTestSpec(planName);
95
+ console.log(`\n ✓ Test spec generated: tests/${result.specName}.spec.ts`);
96
+ console.log(`\n Next step: Implement test logic in the spec file, then run:`);
97
+ console.log(` node ai-qa-workflow.js execute ${result.specName}`);
98
+ }
99
+
100
+ function cmdExecute() {
101
+ const testName = process.argv[3];
102
+ const headed = process.argv.includes('--headed');
103
+ ensureDir(DIRS.testResults);
104
+
105
+ console.log('');
106
+ const result = executeTests(testName, { headed });
107
+
108
+ console.log(`\n Result: ${result.success ? '✓ PASSED' : '✗ FAILED'}`);
109
+ console.log(` Duration: ${(result.duration / 1000).toFixed(1)}s`);
110
+ if (result.failedTests && result.failedTests.length > 0) {
111
+ console.log(` Failures: ${result.failedTests.length}`);
112
+ result.failedTests.forEach(ft => console.log(` - ${ft.test || ft.file}`));
113
+ }
114
+
115
+ if (!result.success) {
116
+ console.log(`\n Next step: Auto-heal failures:`);
117
+ console.log(` node ai-qa-workflow.js heal ${result.runId}`);
118
+ } else {
119
+ console.log(`\n Next step: Generate report:`);
120
+ console.log(` node ai-qa-workflow.js report ${result.runId}`);
121
+ }
122
+ }
123
+
124
+ function cmdHeal() {
125
+ const runId = process.argv[3];
126
+ const result = selfHeal(runId, PROJECT_NAME);
127
+
128
+ if (result.healed.length > 0) {
129
+ console.log(`\n ✓ Healed: ${result.healed.length} test(s)`);
130
+ result.healed.forEach(h => console.log(` - ${h.test || h.file} (${h.strategy})`));
131
+ }
132
+ if (result.remaining.length > 0) {
133
+ console.log(`\n ✗ Still failing: ${result.remaining.length} test(s)`);
134
+ result.remaining.forEach(ft => console.log(` - ${ft.test || ft.file}`));
135
+ }
136
+ if (result.healed.length === 0 && result.remaining.length === 0) {
137
+ console.log('\n No failures to heal.');
138
+ }
139
+
140
+ console.log(`\n Next step: Generate report:`);
141
+ console.log(` node ai-qa-workflow.js report ${result.reportPath ? path.basename(path.dirname(result.reportPath)) : runId || '(latest)'}`);
142
+ }
143
+
144
+ function cmdReport() {
145
+ const runId = process.argv[3];
146
+ const result = generateReport(runId);
147
+
148
+ if (!result) {
149
+ console.log('\n No test results found. Run tests first: node ai-qa-workflow.js execute');
150
+ process.exit(1);
151
+ }
152
+
153
+ console.log(`\n ✓ Report generated: ${result.reportPath}`);
154
+ console.log(` Passed: ${result.stats.passed} | Failed: ${result.stats.failed} | Healed: ${result.stats.healed}`);
155
+ }
156
+
157
+ function cmdAllure() {
158
+ const { execSync } = require('child_process');
159
+ const allureResults = path.join(ROOT, 'allure-results');
160
+ const allureReport = path.join(ROOT, 'allure-report');
161
+
162
+ if (!fs.existsSync(allureResults)) {
163
+ console.log('\n No allure-results/ directory found.');
164
+ console.log(' Ensure your Playwright config outputs Allure results.');
165
+ console.log(' Add to playwright.config: reporter: [["list"], ["allure-playwright"]]');
166
+ console.log(' Install: npm install -D allure-playwright');
167
+ process.exit(1);
168
+ }
169
+
170
+ log('ALLURE', `Generating report from: allure-results/`);
171
+
172
+ try {
173
+ execSync(`npx allure generate "${allureResults}" --clean -o "${allureReport}"`, {
174
+ cwd: ROOT,
175
+ encoding: 'utf-8',
176
+ timeout: 60000,
177
+ stdio: 'pipe',
178
+ });
179
+ log('ALLURE', `Report generated: allure-report/index.html`);
180
+ console.log(`\n ✓ Allure report: allure-report/index.html`);
181
+ console.log(` Open in dashboard: /allure-report`);
182
+ } catch (err) {
183
+ console.error('\n ✗ Failed to generate Allure report.');
184
+ console.error(' Ensure Allure CLI is available: npm install -D allure-commandline');
185
+ if (err.stderr) console.error(` ${err.stderr.substring(0, 500)}`);
186
+ process.exit(1);
187
+ }
188
+ }
189
+
190
+ function cmdRun() {
191
+ const storyName = process.argv[3];
192
+ if (!storyName) {
193
+ console.log(' Usage: node ai-qa-workflow.js run <user-story-file.md>');
194
+ console.log(` Available: ${listUserStories().join(', ')}`);
195
+ process.exit(1);
196
+ }
197
+
198
+ console.log('\n╔════════════════════════════════════════╗');
199
+ console.log('║ AI QA WORKFLOW - FULL PIPELINE ║');
200
+ console.log('╚════════════════════════════════════════╝\n');
201
+
202
+ // Step 1: Plan
203
+ log('RUN', 'Step 1/5: Generating test plan...');
204
+ ensureDir(DIRS.specs);
205
+ const plan = generateTestPlan(storyName);
206
+ console.log(` → specs/${path.basename(plan.planPath)}\n`);
207
+
208
+ // Step 2: Generate
209
+ log('RUN', 'Step 2/5: Generating test spec...');
210
+ ensureDir(DIRS.tests);
211
+ const spec = generateTestSpec(path.basename(plan.planPath));
212
+ console.log(` → tests/${spec.specName}.spec.ts\n`);
213
+
214
+ // Step 3: Execute
215
+ log('RUN', 'Step 3/5: Executing tests...');
216
+ ensureDir(DIRS.testResults);
217
+ const execResult = executeTests(spec.specName);
218
+ console.log(` → ${execResult.success ? '✓ PASSED' : '✗ FAILED'} (${(execResult.duration / 1000).toFixed(1)}s)\n`);
219
+
220
+ // Step 4: Heal (if needed)
221
+ if (!execResult.success) {
222
+ log('RUN', 'Step 4/5: Self-healing...');
223
+ const healResult = selfHeal(execResult.runId, PROJECT_NAME);
224
+ console.log(` → Healed: ${healResult.healed.length} | Remaining: ${healResult.remaining.length}\n`);
225
+ } else {
226
+ log('RUN', 'Step 4/5: Skipped (all tests passed)');
227
+ console.log('');
228
+ }
229
+
230
+ // Step 5: Report
231
+ log('RUN', 'Step 5/5: Generating report...');
232
+ const report = generateReport(execResult.runId);
233
+ console.log(` → ${report ? report.reportPath : 'N/A'}\n`);
234
+
235
+ console.log('╔════════════════════════════════════════╗');
236
+ console.log('║ WORKFLOW COMPLETE ║');
237
+ console.log('╚════════════════════════════════════════╝');
238
+ if (report) {
239
+ console.log(`\n Report: test-results/${execResult.runId}/final-test-report.md`);
240
+ console.log(` Passed: ${report.stats.passed} | Failed: ${report.stats.failed} | Healed: ${report.stats.healed}`);
241
+ }
242
+ }
243
+
244
+ function cmdStatus() {
245
+ console.log('\n📊 AI QA Workflow Status\n');
246
+
247
+ const stories = listUserStories();
248
+ console.log(`📝 User Stories: ${stories.length}`);
249
+ stories.forEach(s => {
250
+ const planFile = s.replace(/\.md$/, '-test-plan.md');
251
+ const specFile = s.replace(/\.md$/, '.spec.ts');
252
+
253
+ const hasPlan = listTestPlans().includes(planFile);
254
+ const hasSpec = listTestSpecs().includes(specFile);
255
+
256
+ const status = hasSpec ? '✅ Test written' : hasPlan ? '📋 Test plan ready' : '📄 Story only';
257
+ console.log(` ${status} ${s}`);
258
+ });
259
+
260
+ const plans = listTestPlans();
261
+ console.log(`\n📋 Test Plans: ${plans.length}`);
262
+ plans.forEach(p => {
263
+ const hasSpec = listTestSpecs().includes(p.replace('-test-plan.md', '.spec.ts'));
264
+ console.log(` ${hasSpec ? '✅ Test written' : '📄 Plan only'} ${p}`);
265
+ });
266
+
267
+ const specs = listTestSpecs();
268
+ console.log(`\n🧪 Test Specs: ${specs.length}`);
269
+ specs.forEach(s => console.log(` ${s}`));
270
+
271
+ if (fs.existsSync(DIRS.testResults)) {
272
+ const runDirs = fs.readdirSync(DIRS.testResults)
273
+ .filter(d => fs.statSync(path.join(DIRS.testResults, d)).isDirectory());
274
+ console.log(`\n📊 Execution Runs: ${runDirs.length}`);
275
+ runDirs.forEach(d => {
276
+ const resultFile = path.join(DIRS.testResults, d, 'execution-result.json');
277
+ if (fs.existsSync(resultFile)) {
278
+ const result = JSON.parse(fs.readFileSync(resultFile, 'utf-8'));
279
+ console.log(` ${result.success ? '✅' : '❌'} ${d} (${(result.duration / 1000).toFixed(1)}s)${result.failedTests ? ` - ${result.failedTests.length} failure(s)` : ''}`);
280
+ } else {
281
+ console.log(` ⏳ ${d} (incomplete)`);
282
+ }
283
+ });
284
+ }
285
+
286
+ console.log('');
287
+ }
288
+
289
+ function cmdList() {
290
+ console.log('\n📝 User Stories:');
291
+ listUserStories().forEach(s => console.log(` ${s}`));
292
+
293
+ console.log('\n📋 Test Plans:');
294
+ listTestPlans().forEach(p => console.log(` ${p}`));
295
+
296
+ console.log('\n🧪 Test Specs:');
297
+ listTestSpecs().forEach(s => console.log(` ${s}`));
298
+ console.log('');
299
+ }
300
+
301
+ const cmd = process.argv[2];
302
+
303
+ if (!cmd || cmd === '--help' || cmd === '-h') {
304
+ console.log('\nAI QA Workflow - Test Automation Pipeline\n');
305
+ console.log('Usage: node ai-qa-workflow.js <command> [options]\n');
306
+ console.log('Commands:');
307
+ for (const [name, { desc }] of Object.entries(COMMANDS)) {
308
+ console.log(` ${name.padEnd(12)} ${desc}`);
309
+ }
310
+ console.log('\nExamples:');
311
+ console.log(' node ai-qa-workflow.js init');
312
+ console.log(' node ai-qa-workflow.js plan US-EXPLORE-01.md');
313
+ console.log(' node ai-qa-workflow.js generate us-explore-01-test-plan.md');
314
+ console.log(' node ai-qa-workflow.js execute us-explore-01');
315
+ console.log(' node ai-qa-workflow.js run US-EXPLORE-01.md');
316
+ console.log(' node ai-qa-workflow.js report:allure');
317
+ console.log(' node ai-qa-workflow.js status\n');
318
+ process.exit(0);
319
+ }
320
+
321
+ if (!COMMANDS[cmd]) {
322
+ console.error(`Unknown command: ${cmd}`);
323
+ console.log(`Available: ${Object.keys(COMMANDS).join(', ')}`);
324
+ process.exit(1);
325
+ }
326
+
327
+ ensureDir(DIRS.docs);
328
+ ensureDir(DIRS.testResults);
329
+
330
+ COMMANDS[cmd].fn();
package/cli.js ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+
3
+ const path = require('path');
4
+
5
+ // Forward to install.js with --self flag so it installs into cwd
6
+ process.argv.push('--self');
7
+ require(path.join(__dirname, 'install.js'));
@@ -0,0 +1,20 @@
1
+ # Application Context
2
+ # Auto-generated from project scan. Edit to provide more context for AI agents.
3
+
4
+ ## Project
5
+ - Name: ai-qa-workflow-template
6
+ - Description: Reusable AI QA Workflow - User Story to Test Report pipeline
7
+ - URL: http://localhost:4000
8
+ - Environment: Web Application
9
+
10
+ ## Tech Stack
11
+ - (auto-detected from project files)
12
+
13
+ ## Stable Selectors
14
+ - (populate during exploration)
15
+
16
+ ## Known Flaky Areas
17
+ - (populate during execution)
18
+
19
+ ## Authentication
20
+ - User: (set in .qa-workflow.json or auto-detect from .env)
package/install.js ADDED
@@ -0,0 +1,303 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { execSync } = require('child_process');
6
+
7
+ const TEMPLATE_DIR = __dirname;
8
+ const YES = process.argv.includes('--yes') || process.argv.includes('-y');
9
+ const IS_UPDATE = process.argv.includes('--update') || process.argv.includes('-u');
10
+ const IS_SELF = process.argv.includes('--self') || process.argv.includes('-s');
11
+
12
+ // Files that belong to the user — NEVER overwritten
13
+ const USER_FILES = new Set([
14
+ '.qa-workflow.json',
15
+ 'opencode.json',
16
+ 'docs/application-context.md',
17
+ ]);
18
+
19
+ // Directories that are pure user data — NEVER touched
20
+ const USER_DIRS = new Set([
21
+ 'user-story',
22
+ 'specs',
23
+ 'tests',
24
+ 'test-results',
25
+ ]);
26
+
27
+ // Template files to copy/update
28
+ const QA_ITEMS = [
29
+ { src: 'ai-qa-workflow.js', dest: 'ai-qa-workflow.js' },
30
+ { src: 'scripts', dest: 'scripts', dir: true },
31
+ { src: 'opencode.json', dest: 'opencode.json' },
32
+ { src: '.qa-workflow.json', dest: '.qa-workflow.json' },
33
+ { src: 'prompts', dest: 'prompts', dir: true },
34
+ { src: '.github/agents', dest: '.github/agents', dir: true },
35
+ { src: '.opencode', dest: '.opencode', dir: true },
36
+ ];
37
+
38
+ // Files to copy even during update (excludes user config files)
39
+ const UPDATE_ITEMS = QA_ITEMS.filter(item => {
40
+ if (item.dir) return !USER_DIRS.has(item.dest);
41
+ return !USER_FILES.has(item.dest);
42
+ });
43
+
44
+ const DIRS_TO_CREATE = ['user-story', 'specs', 'tests', 'test-results'];
45
+
46
+ const NPM_SCRIPTS = {
47
+ 'qa': 'node ai-qa-workflow.js',
48
+ 'qa:init': 'node ai-qa-workflow.js init',
49
+ 'qa:plan': 'node ai-qa-workflow.js plan',
50
+ 'qa:generate': 'node ai-qa-workflow.js generate',
51
+ 'qa:execute': 'node ai-qa-workflow.js execute',
52
+ 'qa:heal': 'node ai-qa-workflow.js heal',
53
+ 'qa:report': 'node ai-qa-workflow.js report',
54
+ 'qa:report:allure': 'node ai-qa-workflow.js report:allure',
55
+ 'qa:run': 'node ai-qa-workflow.js run',
56
+ 'qa:status': 'node ai-qa-workflow.js status',
57
+ 'qa:list': 'node ai-qa-workflow.js list',
58
+ 'dashboard': 'cd qa-dashboard && npm start',
59
+ 'dashboard:dev': 'cd qa-dashboard && npx nodemon app.js',
60
+ };
61
+
62
+ const BANNER = `
63
+ ╔══════════════════════════════════════════╗
64
+ ║ AI QA Pipeline Installer v2.0 ║
65
+ ╚══════════════════════════════════════════╝
66
+ `;
67
+
68
+ function copyRecursive(src, dest, skipDirs) {
69
+ skipDirs = skipDirs || new Set();
70
+ const stat = fs.statSync(src);
71
+ if (stat.isDirectory()) {
72
+ const base = path.basename(src);
73
+ if (skipDirs.has(base)) return;
74
+ if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
75
+ fs.readdirSync(src).forEach(item => copyRecursive(path.join(src, item), path.join(dest, item), skipDirs));
76
+ } else {
77
+ if (!fs.existsSync(dest) || fs.readFileSync(src).length !== fs.readFileSync(dest).length) {
78
+ fs.copyFileSync(src, dest);
79
+ return true;
80
+ }
81
+ }
82
+ return false;
83
+ }
84
+
85
+ function countFiles(dir) {
86
+ let count = 0;
87
+ if (!fs.existsSync(dir)) return 0;
88
+ fs.readdirSync(dir).forEach(item => {
89
+ try {
90
+ const full = path.join(dir, item);
91
+ if (fs.statSync(full).isDirectory()) count += countFiles(full);
92
+ else count++;
93
+ } catch (e) {}
94
+ });
95
+ return count;
96
+ }
97
+
98
+ function addNpmScripts(pkgPath, overwrite) {
99
+ if (!fs.existsSync(pkgPath)) return;
100
+ let pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
101
+ if (!pkg.scripts) pkg.scripts = {};
102
+ let changed = 0;
103
+ for (const [key, val] of Object.entries(NPM_SCRIPTS)) {
104
+ if (!pkg.scripts[key] || overwrite) {
105
+ pkg.scripts[key] = val;
106
+ changed++;
107
+ }
108
+ }
109
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
110
+ return changed;
111
+ }
112
+
113
+ function ask(query) {
114
+ const rl = require('readline').createInterface({ input: process.stdin, output: process.stdout });
115
+ return new Promise(resolve => rl.question(query, a => { rl.close(); resolve(a.toLowerCase()); }));
116
+ }
117
+
118
+ async function install(targetPath, mode) {
119
+ const isUpdate = mode === 'update';
120
+
121
+ console.log(BANNER);
122
+ console.log(` Mode: ${isUpdate ? 'UPDATE (preserves config)' : 'FRESH INSTALL'}`);
123
+ console.log(` Target: ${targetPath}\n`);
124
+
125
+ // 1. Copy template files
126
+ const items = isUpdate ? UPDATE_ITEMS : QA_ITEMS;
127
+ console.log(` ── Step 1: QA Pipeline Files ──`);
128
+ for (const item of items) {
129
+ const srcPath = path.join(TEMPLATE_DIR, item.src);
130
+ const destPath = path.join(targetPath, item.dest);
131
+ if (!fs.existsSync(srcPath)) { console.log(` ⚠ ${item.src} not found, skipping`); continue; }
132
+ if (item.dir) {
133
+ if (!fs.existsSync(destPath)) fs.mkdirSync(destPath, { recursive: true });
134
+ const skipDirs = isUpdate ? new Set(['node_modules', 'data']) : new Set();
135
+ copyRecursive(srcPath, destPath, skipDirs);
136
+ const count = countFiles(srcPath);
137
+ console.log(` ✓ ${item.dest}/ (${count} files)`);
138
+ } else {
139
+ fs.copyFileSync(srcPath, destPath);
140
+ console.log(` ✓ ${item.dest}`);
141
+ }
142
+ }
143
+
144
+ // 2. Create directories (fresh install only)
145
+ if (!isUpdate) {
146
+ console.log(`\n ── Step 2: Project Directories ──`);
147
+ for (const dir of DIRS_TO_CREATE) {
148
+ const dirPath = path.join(targetPath, dir);
149
+ if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }); console.log(` ✓ ${dir}/`); }
150
+ else console.log(` • ${dir}/ (exists)`);
151
+ }
152
+ }
153
+
154
+ // 3. Add/update npm scripts
155
+ console.log(`\n ── Step 3: NPM Scripts ──`);
156
+ const pkgPath = path.join(targetPath, 'package.json');
157
+ const changed = addNpmScripts(pkgPath, isUpdate);
158
+ if (changed > 0) console.log(` ✓ ${isUpdate ? 'Updated' : 'Added'} ${changed} npm scripts (qa:*, dashboard)`);
159
+ else console.log(` • Scripts already configured`);
160
+
161
+ // 4. Dashboard
162
+ const dashboardSrc = path.join(TEMPLATE_DIR, 'qa-dashboard');
163
+ const dashboardDest = path.join(targetPath, 'qa-dashboard');
164
+ if (fs.existsSync(dashboardSrc)) {
165
+ console.log(`\n ── Step 4: QA Dashboard ──`);
166
+ if (!fs.existsSync(dashboardDest)) {
167
+ fs.mkdirSync(dashboardDest, { recursive: true });
168
+ copyRecursive(dashboardSrc, dashboardDest);
169
+ console.log(` ✓ Dashboard copied to qa-dashboard/`);
170
+ console.log(` → Installing dashboard dependencies...`);
171
+ try { execSync('npm install', { cwd: dashboardDest, stdio: 'pipe', timeout: 120000 }); console.log(` ✓ Dashboard deps installed`); } catch (e) { console.log(` ⚠ cd qa-dashboard && npm install`); }
172
+ } else {
173
+ const skipDirs = new Set(['node_modules', 'data']);
174
+ copyRecursive(dashboardSrc, dashboardDest, skipDirs);
175
+ console.log(` ✓ Dashboard updated (preserved data/, node_modules/)`);
176
+ if (!isUpdate) {
177
+ console.log(` → Installing dashboard dependencies...`);
178
+ try { execSync('npm install', { cwd: dashboardDest, stdio: 'pipe', timeout: 120000 }); console.log(` ✓ Dashboard deps installed`); } catch (e) { console.log(` ⚠ cd qa-dashboard && npm install`); }
179
+ }
180
+ }
181
+ }
182
+
183
+ // 5. Register project in dashboard (fresh install only)
184
+ if (!isUpdate && fs.existsSync(dashboardDest)) {
185
+ console.log(`\n ── Step 5: Link Dashboard → Project ──`);
186
+ const dashDataDir = path.join(dashboardDest, 'data');
187
+ const dashProjectsFile = path.join(dashDataDir, 'projects.json');
188
+ if (fs.existsSync(dashDataDir)) {
189
+ let projects = [];
190
+ if (fs.existsSync(dashProjectsFile)) {
191
+ try { projects = JSON.parse(fs.readFileSync(dashProjectsFile, 'utf-8')); } catch(e) {}
192
+ }
193
+ const projectName = path.basename(targetPath);
194
+ const projectId = projectName.toLowerCase().replace(/[^a-z0-9]+/g, '-');
195
+ if (!projects.find(p => p.id === projectId)) {
196
+ projects.push({ id: projectId, name: projectName, path: targetPath, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() });
197
+ fs.writeFileSync(dashProjectsFile, JSON.stringify(projects, null, 2));
198
+ console.log(` ✓ Project "${projectName}" registered in dashboard`);
199
+ }
200
+ }
201
+ }
202
+
203
+ // 6. Install Playwright (fresh only)
204
+ if (!isUpdate) {
205
+ console.log(`\n ── Step 6: Dependencies ──`);
206
+ if (!fs.existsSync(path.join(targetPath, 'node_modules', '@playwright'))) {
207
+ console.log(` → Installing @playwright/test...`);
208
+ try { execSync('npm install @playwright/test', { cwd: targetPath, stdio: 'pipe', timeout: 120000 }); console.log(` ✓ Playwright installed`); } catch (e) { console.log(` ⚠ npm install failed: npm install @playwright/test`); }
209
+ } else console.log(` • Playwright already installed`);
210
+ }
211
+
212
+ // Summary
213
+ const totalFiles = countFiles(path.join(targetPath, 'scripts'));
214
+ const dashboardCount = countFiles(path.join(targetPath, 'qa-dashboard'));
215
+
216
+ console.log(`\n ╔══════════════════════════════════════════╗`);
217
+ console.log(` ║ ✓ ${isUpdate ? 'UPDATE COMPLETE' : 'INSTALLATION COMPLETE'}${' '.repeat(isUpdate ? 13 : 7)}║`);
218
+ console.log(` ╚══════════════════════════════════════════╝\n`);
219
+
220
+ if (isUpdate) {
221
+ console.log(` Pipeline files updated. Your config and data are preserved.`);
222
+ console.log(` Restart the dashboard if it was running.\n`);
223
+ } else {
224
+ console.log(` Files: ~${totalFiles} scripts + ${dashboardCount} dashboard files`);
225
+ console.log(` Next:\n`);
226
+ console.log(` npm run qa:init Initialize pipeline (auto-detect config)`);
227
+ console.log(` npm run qa:run Run a user story through full pipeline`);
228
+ console.log(` npm run qa:status Check pipeline state`);
229
+ console.log(` npm run dashboard Start dashboard (port 4000)\n`);
230
+ }
231
+ }
232
+
233
+ // ---- CLI entry ----
234
+ const args = process.argv.slice(2);
235
+ const isHelp = !args.length || args.includes('--help') || args.includes('-h');
236
+
237
+ if (isHelp) {
238
+ console.log(`
239
+ AI QA Pipeline Installer
240
+
241
+ USAGE:
242
+
243
+ npx ai-qa-workflow init [--yes] Install into current directory
244
+ npx ai-qa-workflow update [--yes] Update pipeline files (preserves config)
245
+
246
+ Or from the template directory:
247
+
248
+ node install.js <target> [--yes] Fresh install into target project
249
+ node install.js <target> --update Update existing installation
250
+ node install.js . --yes Install into current directory
251
+
252
+ FLAGS:
253
+ --yes, -y Skip all prompts
254
+ --update,-u Update mode (preserves config, stories, results)
255
+
256
+ EXAMPLES:
257
+ npx ai-qa-workflow init --yes
258
+ node install.js ../my-project --yes
259
+ node install.js ../my-project --update
260
+ `);
261
+ process.exit(0);
262
+ }
263
+
264
+ async function main() {
265
+ let targetPath;
266
+ let mode = 'install';
267
+
268
+ // Parse flags and target
269
+ const nonFlagArgs = args.filter(a => !a.startsWith('-'));
270
+
271
+ if (IS_SELF || args.includes('init')) {
272
+ targetPath = process.cwd();
273
+ mode = 'install';
274
+ } else if (args.includes('update')) {
275
+ targetPath = process.cwd();
276
+ mode = 'update';
277
+ } else if (IS_UPDATE) {
278
+ targetPath = path.resolve(nonFlagArgs[0] || process.cwd());
279
+ mode = 'update';
280
+ } else if (nonFlagArgs.length > 0) {
281
+ targetPath = path.resolve(nonFlagArgs[0]);
282
+ mode = 'install';
283
+ } else {
284
+ targetPath = process.cwd();
285
+ mode = 'install';
286
+ }
287
+
288
+ if (!fs.existsSync(targetPath) || !fs.statSync(targetPath).isDirectory()) {
289
+ console.error(` ✗ Target not found: ${targetPath}`);
290
+ process.exit(1);
291
+ }
292
+
293
+ // Confirm unless --yes
294
+ if (!YES) {
295
+ const modeLabel = mode === 'update' ? 'UPDATE pipeline files' : 'INSTALL pipeline';
296
+ const answer = await ask(` ${modeLabel} in "${path.basename(targetPath)}"? (Y/n) `);
297
+ if (answer.startsWith('n')) { console.log(' Aborted.'); process.exit(0); }
298
+ }
299
+
300
+ await install(targetPath, mode);
301
+ }
302
+
303
+ main().catch(e => { console.error(e); process.exit(1); });