qaa-agent 1.9.2 → 1.9.5
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/CHANGELOG.md +206 -179
- package/CLAUDE.md +718 -557
- package/README.md +384 -357
- package/VERSION +1 -1
- package/agents/qa-pipeline-orchestrator.md +1739 -1425
- package/agents/qaa-analyzer.md +0 -1
- package/agents/qaa-bug-detective.md +790 -655
- package/agents/qaa-codebase-mapper.md +50 -1
- package/agents/qaa-discovery.md +421 -384
- package/agents/qaa-e2e-runner.md +715 -577
- package/agents/qaa-executor.md +947 -830
- package/agents/qaa-planner.md +14 -1
- package/agents/qaa-project-researcher.md +533 -400
- package/agents/qaa-scanner.md +77 -1
- package/agents/qaa-testid-injector.md +0 -1
- package/agents/qaa-validator.md +86 -1
- package/bin/install.cjs +375 -253
- package/bin/lib/context7-cache.cjs +299 -0
- package/bin/lib/intent-detector.cjs +488 -0
- package/commands/qa-audit.md +255 -126
- package/commands/qa-create-test.md +666 -365
- package/commands/qa-fix.md +684 -513
- package/commands/qa-map.md +283 -139
- package/commands/qa-pr.md +63 -0
- package/commands/qa-research.md +181 -157
- package/commands/qa-start.md +62 -6
- package/commands/qa-test-report.md +219 -219
- package/frameworks/cypress.json +54 -0
- package/frameworks/jest.json +59 -0
- package/frameworks/playwright.json +58 -0
- package/frameworks/pytest.json +59 -0
- package/frameworks/robot-framework.json +54 -0
- package/frameworks/selenium.json +65 -0
- package/frameworks/vitest.json +57 -0
- package/package.json +5 -2
- package/workflows/qa-analyze.md +100 -4
- package/workflows/qa-from-ticket.md +50 -2
- package/workflows/qa-gap.md +50 -2
- package/workflows/qa-start.md +2048 -1405
- package/workflows/qa-testid.md +50 -2
- package/workflows/qa-validate.md +50 -2
package/bin/install.cjs
CHANGED
|
@@ -1,253 +1,375 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* QAA Agent Installer
|
|
5
|
-
*
|
|
6
|
-
* Installs QAA (QA Automation Agent) into the user's Claude Code environment.
|
|
7
|
-
*
|
|
8
|
-
* What it does:
|
|
9
|
-
* 1. Copies agents, commands, skills, templates, workflows, docs, bin, and config files
|
|
10
|
-
* to the chosen install directory (global ~/.claude/qaa or local ./.claude/qaa)
|
|
11
|
-
* 2. Registers Playwright MCP and Context7 MCP as global MCP servers
|
|
12
|
-
* 3. Merges required permissions into Claude Code settings.json
|
|
13
|
-
*
|
|
14
|
-
* Usage:
|
|
15
|
-
* npx qaa-agent
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
const fs = require('fs');
|
|
19
|
-
const path = require('path');
|
|
20
|
-
const readline = require('readline');
|
|
21
|
-
const { execSync } = require('child_process');
|
|
22
|
-
|
|
23
|
-
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
24
|
-
|
|
25
|
-
function log(msg) { console.log(` ${msg}`); }
|
|
26
|
-
function success(msg) { console.log(` ✓ ${msg}`); }
|
|
27
|
-
function warn(msg) { console.log(` ⚠ ${msg}`); }
|
|
28
|
-
function fail(msg) { console.error(` ✗ ${msg}`); }
|
|
29
|
-
|
|
30
|
-
function ask(question) {
|
|
31
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
32
|
-
return new Promise(resolve => {
|
|
33
|
-
rl.question(` ${question} `, answer => {
|
|
34
|
-
rl.close();
|
|
35
|
-
resolve(answer.trim());
|
|
36
|
-
});
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function copyDirRecursive(src, dest) {
|
|
41
|
-
if (!fs.existsSync(src)) return 0;
|
|
42
|
-
fs.mkdirSync(dest, { recursive: true });
|
|
43
|
-
let count = 0;
|
|
44
|
-
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
45
|
-
for (const entry of entries) {
|
|
46
|
-
const srcPath = path.join(src, entry.name);
|
|
47
|
-
const destPath = path.join(dest, entry.name);
|
|
48
|
-
if (entry.isDirectory()) {
|
|
49
|
-
count += copyDirRecursive(srcPath, destPath);
|
|
50
|
-
} else {
|
|
51
|
-
fs.copyFileSync(srcPath, destPath);
|
|
52
|
-
count++;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
return count;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function deepMerge(target, source) {
|
|
59
|
-
for (const key of Object.keys(source)) {
|
|
60
|
-
if (
|
|
61
|
-
source[key] && typeof source[key] === 'object' && !Array.isArray(source[key]) &&
|
|
62
|
-
target[key] && typeof target[key] === 'object' && !Array.isArray(target[key])
|
|
63
|
-
) {
|
|
64
|
-
deepMerge(target[key], source[key]);
|
|
65
|
-
} else if (Array.isArray(source[key]) && Array.isArray(target[key])) {
|
|
66
|
-
// Merge arrays without duplicates
|
|
67
|
-
const merged = [...new Set([...target[key], ...source[key]])];
|
|
68
|
-
target[key] = merged;
|
|
69
|
-
} else {
|
|
70
|
-
target[key] = source[key];
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
return target;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// ── MCP Registration ─────────────────────────────────────────────────────────
|
|
77
|
-
|
|
78
|
-
function registerMcpServers(claudeJsonPath) {
|
|
79
|
-
const mcpServers = {
|
|
80
|
-
playwright: {
|
|
81
|
-
command: 'npx',
|
|
82
|
-
args: ['@playwright/mcp@latest']
|
|
83
|
-
},
|
|
84
|
-
context7: {
|
|
85
|
-
command: 'npx',
|
|
86
|
-
args: ['-y', '@upstash/context7-mcp@latest']
|
|
87
|
-
}
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
let config = {};
|
|
91
|
-
if (fs.existsSync(claudeJsonPath)) {
|
|
92
|
-
try {
|
|
93
|
-
config = JSON.parse(fs.readFileSync(claudeJsonPath, 'utf-8'));
|
|
94
|
-
} catch {
|
|
95
|
-
config = {};
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (!config.mcpServers) config.mcpServers = {};
|
|
100
|
-
|
|
101
|
-
let added = [];
|
|
102
|
-
for (const [name, serverConfig] of Object.entries(mcpServers)) {
|
|
103
|
-
if (!config.mcpServers[name]) {
|
|
104
|
-
config.mcpServers[name] = serverConfig;
|
|
105
|
-
added.push(name);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
fs.writeFileSync(claudeJsonPath, JSON.stringify(config, null, 2) + '\n');
|
|
110
|
-
return added;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// ── Settings Merge ───────────────────────────────────────────────────────────
|
|
114
|
-
|
|
115
|
-
function mergeSettings(installDir, packageDir) {
|
|
116
|
-
const srcSettings = path.join(packageDir, 'settings.json');
|
|
117
|
-
if (!fs.existsSync(srcSettings)) return false;
|
|
118
|
-
|
|
119
|
-
const claudeDir = path.dirname(installDir);
|
|
120
|
-
const destSettings = path.join(claudeDir, 'settings.json');
|
|
121
|
-
|
|
122
|
-
const source = JSON.parse(fs.readFileSync(srcSettings, 'utf-8'));
|
|
123
|
-
|
|
124
|
-
let target = {};
|
|
125
|
-
if (fs.existsSync(destSettings)) {
|
|
126
|
-
try {
|
|
127
|
-
target = JSON.parse(fs.readFileSync(destSettings, 'utf-8'));
|
|
128
|
-
} catch {
|
|
129
|
-
target = {};
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
deepMerge(target, source);
|
|
134
|
-
fs.writeFileSync(destSettings, JSON.stringify(target, null, 2) + '\n');
|
|
135
|
-
return true;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// ──
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
console.log('');
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
console.log('');
|
|
240
|
-
log(
|
|
241
|
-
log('
|
|
242
|
-
log('');
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* QAA Agent Installer
|
|
5
|
+
*
|
|
6
|
+
* Installs QAA (QA Automation Agent) into the user's Claude Code environment.
|
|
7
|
+
*
|
|
8
|
+
* What it does:
|
|
9
|
+
* 1. Copies agents, commands, skills, templates, workflows, docs, bin, and config files
|
|
10
|
+
* to the chosen install directory (global ~/.claude/qaa or local ./.claude/qaa)
|
|
11
|
+
* 2. Registers Playwright MCP and Context7 MCP as global MCP servers
|
|
12
|
+
* 3. Merges required permissions into Claude Code settings.json
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* npx qaa-agent
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const readline = require('readline');
|
|
21
|
+
const { execSync } = require('child_process');
|
|
22
|
+
|
|
23
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
function log(msg) { console.log(` ${msg}`); }
|
|
26
|
+
function success(msg) { console.log(` ✓ ${msg}`); }
|
|
27
|
+
function warn(msg) { console.log(` ⚠ ${msg}`); }
|
|
28
|
+
function fail(msg) { console.error(` ✗ ${msg}`); }
|
|
29
|
+
|
|
30
|
+
function ask(question) {
|
|
31
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
32
|
+
return new Promise(resolve => {
|
|
33
|
+
rl.question(` ${question} `, answer => {
|
|
34
|
+
rl.close();
|
|
35
|
+
resolve(answer.trim());
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function copyDirRecursive(src, dest, dryRun) {
|
|
41
|
+
if (!fs.existsSync(src)) return 0;
|
|
42
|
+
if (!dryRun) fs.mkdirSync(dest, { recursive: true });
|
|
43
|
+
let count = 0;
|
|
44
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
45
|
+
for (const entry of entries) {
|
|
46
|
+
const srcPath = path.join(src, entry.name);
|
|
47
|
+
const destPath = path.join(dest, entry.name);
|
|
48
|
+
if (entry.isDirectory()) {
|
|
49
|
+
count += copyDirRecursive(srcPath, destPath, dryRun);
|
|
50
|
+
} else {
|
|
51
|
+
if (!dryRun) fs.copyFileSync(srcPath, destPath);
|
|
52
|
+
count++;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return count;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function deepMerge(target, source) {
|
|
59
|
+
for (const key of Object.keys(source)) {
|
|
60
|
+
if (
|
|
61
|
+
source[key] && typeof source[key] === 'object' && !Array.isArray(source[key]) &&
|
|
62
|
+
target[key] && typeof target[key] === 'object' && !Array.isArray(target[key])
|
|
63
|
+
) {
|
|
64
|
+
deepMerge(target[key], source[key]);
|
|
65
|
+
} else if (Array.isArray(source[key]) && Array.isArray(target[key])) {
|
|
66
|
+
// Merge arrays without duplicates
|
|
67
|
+
const merged = [...new Set([...target[key], ...source[key]])];
|
|
68
|
+
target[key] = merged;
|
|
69
|
+
} else {
|
|
70
|
+
target[key] = source[key];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return target;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ── MCP Registration ─────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
function registerMcpServers(claudeJsonPath, dryRun) {
|
|
79
|
+
const mcpServers = {
|
|
80
|
+
playwright: {
|
|
81
|
+
command: 'npx',
|
|
82
|
+
args: ['@playwright/mcp@latest']
|
|
83
|
+
},
|
|
84
|
+
context7: {
|
|
85
|
+
command: 'npx',
|
|
86
|
+
args: ['-y', '@upstash/context7-mcp@latest']
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
let config = {};
|
|
91
|
+
if (fs.existsSync(claudeJsonPath)) {
|
|
92
|
+
try {
|
|
93
|
+
config = JSON.parse(fs.readFileSync(claudeJsonPath, 'utf-8'));
|
|
94
|
+
} catch {
|
|
95
|
+
config = {};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!config.mcpServers) config.mcpServers = {};
|
|
100
|
+
|
|
101
|
+
let added = [];
|
|
102
|
+
for (const [name, serverConfig] of Object.entries(mcpServers)) {
|
|
103
|
+
if (!config.mcpServers[name]) {
|
|
104
|
+
config.mcpServers[name] = serverConfig;
|
|
105
|
+
added.push(name);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!dryRun) fs.writeFileSync(claudeJsonPath, JSON.stringify(config, null, 2) + '\n');
|
|
110
|
+
return added;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ── Settings Merge ───────────────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
function mergeSettings(installDir, packageDir, dryRun) {
|
|
116
|
+
const srcSettings = path.join(packageDir, 'settings.json');
|
|
117
|
+
if (!fs.existsSync(srcSettings)) return false;
|
|
118
|
+
|
|
119
|
+
const claudeDir = path.dirname(installDir);
|
|
120
|
+
const destSettings = path.join(claudeDir, 'settings.json');
|
|
121
|
+
|
|
122
|
+
const source = JSON.parse(fs.readFileSync(srcSettings, 'utf-8'));
|
|
123
|
+
|
|
124
|
+
let target = {};
|
|
125
|
+
if (fs.existsSync(destSettings)) {
|
|
126
|
+
try {
|
|
127
|
+
target = JSON.parse(fs.readFileSync(destSettings, 'utf-8'));
|
|
128
|
+
} catch {
|
|
129
|
+
target = {};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
deepMerge(target, source);
|
|
134
|
+
if (!dryRun) fs.writeFileSync(destSettings, JSON.stringify(target, null, 2) + '\n');
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ── Live Location Sync (finding #52) ─────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
// Mirror QAA-owned files into a Claude Code live dir. Only files matching the
|
|
141
|
+
// QAA pattern are copied; QAA orphans (removed in this version) are pruned.
|
|
142
|
+
// Foreign files (gsd-*, sc:*, etc.) are never copied or deleted.
|
|
143
|
+
function syncLive(srcDir, destDir, matches, dryRun) {
|
|
144
|
+
const copied = [];
|
|
145
|
+
const pruned = [];
|
|
146
|
+
if (!fs.existsSync(srcDir)) return { copied, pruned, skipped: true };
|
|
147
|
+
|
|
148
|
+
const srcFiles = fs.readdirSync(srcDir).filter(matches);
|
|
149
|
+
const srcSet = new Set(srcFiles);
|
|
150
|
+
|
|
151
|
+
// Prune QAA orphans that no longer ship in this version
|
|
152
|
+
if (fs.existsSync(destDir)) {
|
|
153
|
+
for (const f of fs.readdirSync(destDir).filter(matches)) {
|
|
154
|
+
if (!srcSet.has(f)) {
|
|
155
|
+
pruned.push(f);
|
|
156
|
+
if (!dryRun) fs.rmSync(path.join(destDir, f), { force: true });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Copy / overwrite QAA files
|
|
162
|
+
if (srcFiles.length > 0 && !dryRun) fs.mkdirSync(destDir, { recursive: true });
|
|
163
|
+
for (const f of srcFiles) {
|
|
164
|
+
copied.push(f);
|
|
165
|
+
if (!dryRun) fs.copyFileSync(path.join(srcDir, f), path.join(destDir, f));
|
|
166
|
+
}
|
|
167
|
+
return { copied, pruned, skipped: false };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Sync CLAUDE.md into the live location using QAA markers so user content is
|
|
171
|
+
// preserved. Returns { action, backupPath? }.
|
|
172
|
+
function syncClaudeMd(installDir, claudeDir, dryRun, version) {
|
|
173
|
+
const srcPath = path.join(installDir, 'CLAUDE.md');
|
|
174
|
+
const destPath = path.join(claudeDir, 'CLAUDE.md');
|
|
175
|
+
if (!fs.existsSync(srcPath)) return { action: 'skip' };
|
|
176
|
+
|
|
177
|
+
const BEGIN = `<!-- QAA-BEGIN v${version} -->`;
|
|
178
|
+
const END = '<!-- QAA-END -->';
|
|
179
|
+
const markerRe = /<!-- QAA-BEGIN[^>]*-->[\s\S]*?<!-- QAA-END -->/;
|
|
180
|
+
const srcContent = fs.readFileSync(srcPath, 'utf-8').trim();
|
|
181
|
+
const wrapped = `${BEGIN}\n${srcContent}\n${END}`;
|
|
182
|
+
|
|
183
|
+
// Case 1: no live CLAUDE.md → create it wrapped
|
|
184
|
+
if (!fs.existsSync(destPath)) {
|
|
185
|
+
if (!dryRun) {
|
|
186
|
+
fs.mkdirSync(claudeDir, { recursive: true });
|
|
187
|
+
fs.writeFileSync(destPath, wrapped + '\n');
|
|
188
|
+
}
|
|
189
|
+
return { action: 'created' };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const existing = fs.readFileSync(destPath, 'utf-8');
|
|
193
|
+
|
|
194
|
+
// Case 2: markers present → replace only the QAA section
|
|
195
|
+
if (markerRe.test(existing)) {
|
|
196
|
+
if (!dryRun) fs.writeFileSync(destPath, existing.replace(markerRe, wrapped));
|
|
197
|
+
return { action: 'updated-section' };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Case 3: exists without markers → backup, then replace wrapped
|
|
201
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
202
|
+
const backupPath = path.join(claudeDir, `CLAUDE.md.bak-${ts}`);
|
|
203
|
+
if (!dryRun) {
|
|
204
|
+
fs.copyFileSync(destPath, backupPath);
|
|
205
|
+
fs.writeFileSync(destPath, wrapped + '\n');
|
|
206
|
+
}
|
|
207
|
+
return { action: 'backup-replaced', backupPath };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ── Main ─────────────────────────────────────────────────────────────────────
|
|
211
|
+
|
|
212
|
+
async function main() {
|
|
213
|
+
console.log('');
|
|
214
|
+
console.log(' ╔═══════════════════════════════════════╗');
|
|
215
|
+
console.log(' ║ QAA — QA Automation Agent Installer ║');
|
|
216
|
+
console.log(' ╚═══════════════════════════════════════╝');
|
|
217
|
+
console.log('');
|
|
218
|
+
|
|
219
|
+
// Determine package root (where the npm package files are)
|
|
220
|
+
const packageDir = path.resolve(__dirname, '..');
|
|
221
|
+
|
|
222
|
+
const dryRun = process.argv.includes('--dry-run');
|
|
223
|
+
if (dryRun) warn('--dry-run mode — no files will be written.');
|
|
224
|
+
|
|
225
|
+
let pkgVersion = 'unknown';
|
|
226
|
+
try {
|
|
227
|
+
pkgVersion = JSON.parse(fs.readFileSync(path.join(packageDir, 'package.json'), 'utf-8')).version || 'unknown';
|
|
228
|
+
} catch { /* keep 'unknown' */ }
|
|
229
|
+
|
|
230
|
+
// Check that package files exist
|
|
231
|
+
const requiredDirs = ['agents', 'commands', 'skills'];
|
|
232
|
+
const missing = requiredDirs.filter(d => !fs.existsSync(path.join(packageDir, d)));
|
|
233
|
+
if (missing.length > 0) {
|
|
234
|
+
fail(`Package incomplete — missing: ${missing.join(', ')}`);
|
|
235
|
+
process.exit(1);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Ask install scope
|
|
239
|
+
console.log(' Install scope:');
|
|
240
|
+
console.log(' 1) Global — ~/.claude/qaa (available in all projects)');
|
|
241
|
+
console.log(' 2) Local — ./.claude/qaa (this project only)');
|
|
242
|
+
console.log('');
|
|
243
|
+
const scopeChoice = await ask('Choose [1/2] (default: 1):');
|
|
244
|
+
const isGlobal = scopeChoice !== '2';
|
|
245
|
+
|
|
246
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE;
|
|
247
|
+
if (!homeDir) {
|
|
248
|
+
fail('Cannot determine home directory (HOME / USERPROFILE not set).');
|
|
249
|
+
process.exit(1);
|
|
250
|
+
}
|
|
251
|
+
const claudeDir = isGlobal
|
|
252
|
+
? path.join(homeDir, '.claude')
|
|
253
|
+
: path.join(process.cwd(), '.claude');
|
|
254
|
+
const installDir = path.join(claudeDir, 'qaa');
|
|
255
|
+
|
|
256
|
+
// Check for existing installation
|
|
257
|
+
if (fs.existsSync(installDir)) {
|
|
258
|
+
const overwrite = await ask('QAA already installed at this location. Overwrite? [y/N]:');
|
|
259
|
+
if (overwrite.toLowerCase() !== 'y') {
|
|
260
|
+
log('Installation cancelled.');
|
|
261
|
+
process.exit(0);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
console.log('');
|
|
266
|
+
log(`Installing to: ${installDir}`);
|
|
267
|
+
console.log('');
|
|
268
|
+
|
|
269
|
+
// ── Step 1: Copy files ──────────────────────────────────────────────────
|
|
270
|
+
|
|
271
|
+
const dirsToCopy = ['agents', 'commands', 'skills', 'templates', 'workflows', 'docs', 'bin'];
|
|
272
|
+
const filesToCopy = ['CLAUDE.md', 'CHANGELOG.md', '.mcp.json', 'package.json'];
|
|
273
|
+
|
|
274
|
+
let totalFiles = 0;
|
|
275
|
+
|
|
276
|
+
for (const dir of dirsToCopy) {
|
|
277
|
+
const src = path.join(packageDir, dir);
|
|
278
|
+
const dest = path.join(installDir, dir);
|
|
279
|
+
if (fs.existsSync(src)) {
|
|
280
|
+
const count = copyDirRecursive(src, dest, dryRun);
|
|
281
|
+
success(`${dir}/ — ${count} files`);
|
|
282
|
+
totalFiles += count;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
for (const file of filesToCopy) {
|
|
287
|
+
const src = path.join(packageDir, file);
|
|
288
|
+
const dest = path.join(installDir, file);
|
|
289
|
+
if (fs.existsSync(src)) {
|
|
290
|
+
if (!dryRun) {
|
|
291
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
292
|
+
fs.copyFileSync(src, dest);
|
|
293
|
+
}
|
|
294
|
+
success(file);
|
|
295
|
+
totalFiles++;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
console.log('');
|
|
300
|
+
|
|
301
|
+
// ── Step 2: Register MCP servers ────────────────────────────────────────
|
|
302
|
+
|
|
303
|
+
const claudeJsonPath = path.join(homeDir, '.claude.json');
|
|
304
|
+
const addedMcps = registerMcpServers(claudeJsonPath, dryRun);
|
|
305
|
+
|
|
306
|
+
if (addedMcps.length > 0) {
|
|
307
|
+
success(`MCP servers registered: ${addedMcps.join(', ')} → ${claudeJsonPath}`);
|
|
308
|
+
} else {
|
|
309
|
+
success('MCP servers already configured (playwright, context7)');
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ── Step 3: Merge settings ──────────────────────────────────────────────
|
|
313
|
+
|
|
314
|
+
const settingsMerged = mergeSettings(installDir, packageDir, dryRun);
|
|
315
|
+
if (settingsMerged) {
|
|
316
|
+
success('Permissions merged into settings.json');
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ── Step 4: Sync to live locations (finding #52) ────────────────────────
|
|
320
|
+
//
|
|
321
|
+
// Claude Code loads slash commands from <claudeDir>/commands, sub-agents from
|
|
322
|
+
// <claudeDir>/agents, and global standards from <claudeDir>/CLAUDE.md — NOT
|
|
323
|
+
// from the qaa/ install dir. Mirror QAA files there so new versions activate.
|
|
324
|
+
// Only QAA-owned files (qa*.md / qaa-*.md) are touched.
|
|
325
|
+
|
|
326
|
+
console.log('');
|
|
327
|
+
|
|
328
|
+
const qaMatch = (f) => /^qa.*\.md$/i.test(f);
|
|
329
|
+
const tag = dryRun ? '[dry-run] ' : '';
|
|
330
|
+
|
|
331
|
+
const cmdSync = syncLive(
|
|
332
|
+
path.join(installDir, 'commands'),
|
|
333
|
+
path.join(claudeDir, 'commands'),
|
|
334
|
+
qaMatch, dryRun
|
|
335
|
+
);
|
|
336
|
+
success(`${tag}Live commands → ${path.join(claudeDir, 'commands')} (${cmdSync.copied.length} synced${cmdSync.pruned.length ? `, ${cmdSync.pruned.length} pruned` : ''})`);
|
|
337
|
+
if (cmdSync.pruned.length) log(`${tag}Pruned: ${cmdSync.pruned.join(', ')}`);
|
|
338
|
+
|
|
339
|
+
const agentSync = syncLive(
|
|
340
|
+
path.join(installDir, 'agents'),
|
|
341
|
+
path.join(claudeDir, 'agents'),
|
|
342
|
+
qaMatch, dryRun
|
|
343
|
+
);
|
|
344
|
+
success(`${tag}Live agents → ${path.join(claudeDir, 'agents')} (${agentSync.copied.length} synced${agentSync.pruned.length ? `, ${agentSync.pruned.length} pruned` : ''})`);
|
|
345
|
+
if (agentSync.pruned.length) log(`${tag}Pruned: ${agentSync.pruned.join(', ')}`);
|
|
346
|
+
|
|
347
|
+
const cmd = syncClaudeMd(installDir, claudeDir, dryRun, pkgVersion);
|
|
348
|
+
if (cmd.action === 'created') success(`${tag}CLAUDE.md created (QAA markers) → ${path.join(claudeDir, 'CLAUDE.md')}`);
|
|
349
|
+
else if (cmd.action === 'updated-section') success(`${tag}CLAUDE.md QAA section updated → ${path.join(claudeDir, 'CLAUDE.md')}`);
|
|
350
|
+
else if (cmd.action === 'backup-replaced') warn(`${tag}CLAUDE.md had no QAA markers — backed up to ${cmd.backupPath}, replaced with marked version`);
|
|
351
|
+
else if (cmd.action === 'skip') warn(`${tag}No CLAUDE.md in package to sync`);
|
|
352
|
+
|
|
353
|
+
// ── Done ────────────────────────────────────────────────────────────────
|
|
354
|
+
|
|
355
|
+
console.log('');
|
|
356
|
+
console.log(' ╔═══════════════════════════════════════╗');
|
|
357
|
+
console.log(' ║ Installation complete! ║');
|
|
358
|
+
console.log(' ╚═══════════════════════════════════════╝');
|
|
359
|
+
console.log('');
|
|
360
|
+
if (dryRun) log('Dry-run complete — nothing was written.');
|
|
361
|
+
log(`${totalFiles} files installed to ${installDir}`);
|
|
362
|
+
log(`Live commands & agents synced to ${claudeDir}`);
|
|
363
|
+
log('MCP servers: playwright, context7');
|
|
364
|
+
log('');
|
|
365
|
+
log('Restart Claude Code, then run any QAA command:');
|
|
366
|
+
log(' /qa-start --dev-repo ./your-project');
|
|
367
|
+
log(' /qa-create-test login');
|
|
368
|
+
log(' /qa-map');
|
|
369
|
+
console.log('');
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
main().catch(err => {
|
|
373
|
+
fail(err.message);
|
|
374
|
+
process.exit(1);
|
|
375
|
+
});
|