@ai-qa/workflow 2.0.8 → 2.0.9

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 (2) hide show
  1. package/install.js +316 -0
  2. package/package.json +2 -1
package/install.js ADDED
@@ -0,0 +1,316 @@
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
+ 'agents',
26
+
27
+
28
+
29
+
30
+ ]);
31
+
32
+ // Template files to copy/update
33
+ const QA_ITEMS = [
34
+ { src: 'ai-qa-workflow.js', dest: 'ai-qa-workflow.js' },
35
+ { src: 'scripts', dest: 'scripts', dir: true },
36
+ { src: 'opencode.json', dest: 'opencode.json' },
37
+ { src: '.qa-workflow.json', dest: '.qa-workflow.json' },
38
+ { src: 'prompts', dest: 'prompts', dir: true },
39
+ { src: '.github/agents', dest: '.github/agents', dir: true },
40
+ { src: '.opencode', dest: '.opencode', dir: true },
41
+ { src: 'README.md', dest: 'README.md' },
42
+ { src: 'PROJECT_GUIDE.md', dest: 'PROJECT_GUIDE.md' },
43
+ { src: '.cursorrules', dest: '.cursorrules' },
44
+ { src: '.geminirules', dest: '.geminirules' },
45
+ { src: '.github/copilot-instructions.md', dest: '.github/copilot-instructions.md' },
46
+ { src: 'agents/router.md', dest: 'agents/router.md' },
47
+
48
+ ];
49
+
50
+ // Files to copy even during update (excludes user config files)
51
+ const UPDATE_ITEMS = QA_ITEMS.filter(item => {
52
+ if (item.dir) return !USER_DIRS.has(item.dest);
53
+ return !USER_FILES.has(item.dest);
54
+ });
55
+
56
+ const DIRS_TO_CREATE = ['user-story', 'specs', 'tests', 'test-results'];
57
+
58
+ const NPM_SCRIPTS = {
59
+ 'qa': 'node ai-qa-workflow.js',
60
+ 'qa:init': 'node ai-qa-workflow.js init',
61
+ 'qa:plan': 'node ai-qa-workflow.js plan',
62
+ 'qa:generate': 'node ai-qa-workflow.js generate',
63
+ 'qa:execute': 'node ai-qa-workflow.js execute',
64
+ 'qa:heal': 'node ai-qa-workflow.js heal',
65
+ 'qa:report': 'node ai-qa-workflow.js report',
66
+ 'qa:report:allure': 'node ai-qa-workflow.js report:allure',
67
+ 'qa:run': 'node ai-qa-workflow.js run',
68
+ 'qa:status': 'node ai-qa-workflow.js status',
69
+ 'qa:list': 'node ai-qa-workflow.js list',
70
+ 'dashboard': 'cd qa-dashboard && npm start',
71
+ 'dashboard:dev': 'cd qa-dashboard && npx nodemon app.js',
72
+ 'dashboard:stop': 'npx kill-port 4000',
73
+ };
74
+
75
+ const BANNER = `
76
+ ╔══════════════════════════════════════════╗
77
+ ║ AI QA Pipeline Installer v2.0 ║
78
+ ╚══════════════════════════════════════════╝
79
+ `;
80
+
81
+ function copyRecursive(src, dest, skipDirs) {
82
+ skipDirs = skipDirs || new Set();
83
+ const stat = fs.statSync(src);
84
+ if (stat.isDirectory()) {
85
+ const base = path.basename(src);
86
+ if (skipDirs.has(base)) return;
87
+ if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
88
+ fs.readdirSync(src).forEach(item => copyRecursive(path.join(src, item), path.join(dest, item), skipDirs));
89
+ } else {
90
+ if (!fs.existsSync(dest) || fs.readFileSync(src).length !== fs.readFileSync(dest).length) {
91
+ fs.copyFileSync(src, dest);
92
+ return true;
93
+ }
94
+ }
95
+ return false;
96
+ }
97
+
98
+ function countFiles(dir) {
99
+ let count = 0;
100
+ if (!fs.existsSync(dir)) return 0;
101
+ fs.readdirSync(dir).forEach(item => {
102
+ try {
103
+ const full = path.join(dir, item);
104
+ if (fs.statSync(full).isDirectory()) count += countFiles(full);
105
+ else count++;
106
+ } catch (e) {}
107
+ });
108
+ return count;
109
+ }
110
+
111
+ function addNpmScripts(pkgPath, overwrite) {
112
+ if (!fs.existsSync(pkgPath)) return;
113
+ let pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
114
+ if (!pkg.scripts) pkg.scripts = {};
115
+ let changed = 0;
116
+ for (const [key, val] of Object.entries(NPM_SCRIPTS)) {
117
+ if (!pkg.scripts[key] || overwrite) {
118
+ pkg.scripts[key] = val;
119
+ changed++;
120
+ }
121
+ }
122
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
123
+ return changed;
124
+ }
125
+
126
+ function ask(query) {
127
+ const rl = require('readline').createInterface({ input: process.stdin, output: process.stdout });
128
+ return new Promise(resolve => rl.question(query, a => { rl.close(); resolve(a.toLowerCase()); }));
129
+ }
130
+
131
+ async function install(targetPath, mode) {
132
+ const isUpdate = mode === 'update';
133
+
134
+ console.log(BANNER);
135
+ console.log(` Mode: ${isUpdate ? 'UPDATE (preserves config)' : 'FRESH INSTALL'}`);
136
+ console.log(` Target: ${targetPath}\n`);
137
+
138
+ // 1. Copy template files
139
+ const items = isUpdate ? UPDATE_ITEMS : QA_ITEMS;
140
+ console.log(` ── Step 1: QA Pipeline Files ──`);
141
+ for (const item of items) {
142
+ const srcPath = path.join(TEMPLATE_DIR, item.src);
143
+ const destPath = path.join(targetPath, item.dest);
144
+ if (!fs.existsSync(srcPath)) { console.log(` ⚠ ${item.src} not found, skipping`); continue; }
145
+ if (item.dir) {
146
+ if (!fs.existsSync(destPath)) fs.mkdirSync(destPath, { recursive: true });
147
+ const skipDirs = isUpdate ? new Set(['node_modules', 'data']) : new Set();
148
+ copyRecursive(srcPath, destPath, skipDirs);
149
+ const count = countFiles(srcPath);
150
+ console.log(` ✓ ${item.dest}/ (${count} files)`);
151
+ } else {
152
+ fs.copyFileSync(srcPath, destPath);
153
+ console.log(` ✓ ${item.dest}`);
154
+ }
155
+ }
156
+
157
+ // 2. Create directories (fresh install only)
158
+ if (!isUpdate) {
159
+ console.log(`\n ── Step 2: Project Directories ──`);
160
+ for (const dir of DIRS_TO_CREATE) {
161
+ const dirPath = path.join(targetPath, dir);
162
+ if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }); console.log(` ✓ ${dir}/`); }
163
+ else console.log(` • ${dir}/ (exists)`);
164
+ }
165
+ }
166
+
167
+ // 3. Add/update npm scripts
168
+ console.log(`\n ── Step 3: NPM Scripts ──`);
169
+ const pkgPath = path.join(targetPath, 'package.json');
170
+ const changed = addNpmScripts(pkgPath, isUpdate);
171
+ if (changed > 0) console.log(` ✓ ${isUpdate ? 'Updated' : 'Added'} ${changed} npm scripts (qa:*, dashboard)`);
172
+ else console.log(` • Scripts already configured`);
173
+
174
+ // 4. Dashboard
175
+ const dashboardSrc = path.join(TEMPLATE_DIR, 'qa-dashboard');
176
+ const dashboardDest = path.join(targetPath, 'qa-dashboard');
177
+ if (fs.existsSync(dashboardSrc)) {
178
+ console.log(`\n ── Step 4: QA Dashboard ──`);
179
+ if (!fs.existsSync(dashboardDest)) {
180
+ fs.mkdirSync(dashboardDest, { recursive: true });
181
+ copyRecursive(dashboardSrc, dashboardDest);
182
+ console.log(` ✓ Dashboard copied to qa-dashboard/`);
183
+ console.log(` → Installing dashboard dependencies...`);
184
+ try { execSync('npm install', { cwd: dashboardDest, stdio: 'pipe', timeout: 120000 }); console.log(` ✓ Dashboard deps installed`); } catch (e) { console.log(` ⚠ cd qa-dashboard && npm install`); }
185
+ } else {
186
+ const skipDirs = new Set(['node_modules', 'data']);
187
+ copyRecursive(dashboardSrc, dashboardDest, skipDirs);
188
+ console.log(` ✓ Dashboard updated (preserved data/, node_modules/)`);
189
+ if (!isUpdate) {
190
+ console.log(` → Installing dashboard dependencies...`);
191
+ try { execSync('npm install', { cwd: dashboardDest, stdio: 'pipe', timeout: 120000 }); console.log(` ✓ Dashboard deps installed`); } catch (e) { console.log(` ⚠ cd qa-dashboard && npm install`); }
192
+ }
193
+ }
194
+ }
195
+
196
+ // 5. Register project in dashboard (fresh install only)
197
+ if (!isUpdate && fs.existsSync(dashboardDest)) {
198
+ console.log(`\n ── Step 5: Link Dashboard → Project ──`);
199
+ const dashDataDir = path.join(dashboardDest, 'data');
200
+ const dashProjectsFile = path.join(dashDataDir, 'projects.json');
201
+ if (fs.existsSync(dashDataDir)) {
202
+ let projects = [];
203
+ if (fs.existsSync(dashProjectsFile)) {
204
+ try { projects = JSON.parse(fs.readFileSync(dashProjectsFile, 'utf-8')); } catch(e) {}
205
+ }
206
+ const projectName = path.basename(targetPath);
207
+ const projectId = projectName.toLowerCase().replace(/[^a-z0-9]+/g, '-');
208
+ if (!projects.find(p => p.id === projectId)) {
209
+ projects.push({ id: projectId, name: projectName, path: targetPath, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() });
210
+ fs.writeFileSync(dashProjectsFile, JSON.stringify(projects, null, 2));
211
+ console.log(` ✓ Project "${projectName}" registered in dashboard`);
212
+ }
213
+ }
214
+ }
215
+
216
+ // 6. Install Playwright (fresh only)
217
+ if (!isUpdate) {
218
+ console.log(`\n ── Step 6: Dependencies ──`);
219
+ if (!fs.existsSync(path.join(targetPath, 'node_modules', '@playwright'))) {
220
+ console.log(` → Installing @playwright/test...`);
221
+ 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`); }
222
+ } else console.log(` • Playwright already installed`);
223
+ }
224
+
225
+ // Summary
226
+ const totalFiles = countFiles(path.join(targetPath, 'scripts'));
227
+ const dashboardCount = countFiles(path.join(targetPath, 'qa-dashboard'));
228
+
229
+ console.log(`\n ╔══════════════════════════════════════════╗`);
230
+ console.log(` ║ ✓ ${isUpdate ? 'UPDATE COMPLETE' : 'INSTALLATION COMPLETE'}${' '.repeat(isUpdate ? 13 : 7)}║`);
231
+ console.log(` ╚══════════════════════════════════════════╝\n`);
232
+
233
+ if (isUpdate) {
234
+ console.log(` Pipeline files updated. Your config and data are preserved.`);
235
+ console.log(` Restart the dashboard if it was running.\n`);
236
+ } else {
237
+ console.log(` Files: ~${totalFiles} scripts + ${dashboardCount} dashboard files`);
238
+ console.log(` Next:\n`);
239
+ console.log(` npm run qa:init Initialize pipeline (auto-detect config)`);
240
+ console.log(` npm run qa:run Run a user story through full pipeline`);
241
+ console.log(` npm run qa:status Check pipeline state`);
242
+ console.log(` npm run dashboard Start dashboard (port 4000)\n`);
243
+ }
244
+ }
245
+
246
+ // ---- CLI entry ----
247
+ const args = process.argv.slice(2);
248
+ const isHelp = !args.length || args.includes('--help') || args.includes('-h');
249
+
250
+ if (isHelp) {
251
+ console.log(`
252
+ AI QA Pipeline Installer
253
+
254
+ USAGE:
255
+
256
+ npx ai-qa-workflow init [--yes] Install into current directory
257
+ npx ai-qa-workflow update [--yes] Update pipeline files (preserves config)
258
+
259
+ Or from the template directory:
260
+
261
+ node install.js <target> [--yes] Fresh install into target project
262
+ node install.js <target> --update Update existing installation
263
+ node install.js . --yes Install into current directory
264
+
265
+ FLAGS:
266
+ --yes, -y Skip all prompts
267
+ --update,-u Update mode (preserves config, stories, results)
268
+
269
+ EXAMPLES:
270
+ npx ai-qa-workflow init --yes
271
+ node install.js ../my-project --yes
272
+ node install.js ../my-project --update
273
+ `);
274
+ process.exit(0);
275
+ }
276
+
277
+ async function main() {
278
+ let targetPath;
279
+ let mode = 'install';
280
+
281
+ // Parse flags and target
282
+ const nonFlagArgs = args.filter(a => !a.startsWith('-'));
283
+
284
+ if (IS_SELF || args.includes('init')) {
285
+ targetPath = process.cwd();
286
+ mode = 'install';
287
+ } else if (args.includes('update')) {
288
+ targetPath = process.cwd();
289
+ mode = 'update';
290
+ } else if (IS_UPDATE) {
291
+ targetPath = path.resolve(nonFlagArgs[0] || process.cwd());
292
+ mode = 'update';
293
+ } else if (nonFlagArgs.length > 0) {
294
+ targetPath = path.resolve(nonFlagArgs[0]);
295
+ mode = 'install';
296
+ } else {
297
+ targetPath = process.cwd();
298
+ mode = 'install';
299
+ }
300
+
301
+ if (!fs.existsSync(targetPath) || !fs.statSync(targetPath).isDirectory()) {
302
+ console.error(` ✗ Target not found: ${targetPath}`);
303
+ process.exit(1);
304
+ }
305
+
306
+ // Confirm unless --yes
307
+ if (!YES) {
308
+ const modeLabel = mode === 'update' ? 'UPDATE pipeline files' : 'INSTALL pipeline';
309
+ const answer = await ask(` ${modeLabel} in "${path.basename(targetPath)}"? (Y/n) `);
310
+ if (answer.startsWith('n')) { console.log(' Aborted.'); process.exit(0); }
311
+ }
312
+
313
+ await install(targetPath, mode);
314
+ }
315
+
316
+ main().catch(e => { console.error(e); process.exit(1); });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-qa/workflow",
3
- "version": "2.0.8",
3
+ "version": "2.0.9",
4
4
  "description": "One-command AI QA Pipeline — User Story to Test Report. Auto-detects project config, generates Playwright tests, self-heals failures, dashboard UI.",
5
5
  "keywords": [
6
6
  "qa",
@@ -25,6 +25,7 @@
25
25
  "templates/",
26
26
  "ai-qa-workflow.js",
27
27
  "cli.js",
28
+ "install.js",
28
29
  "README.md",
29
30
  "PROJECT_GUIDE.md",
30
31
  ".cursorrules",