qatools-cli 1.0.0 β 1.1.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/package.json +11 -3
- package/qatools.js +1356 -26
package/qatools.js
CHANGED
|
@@ -38,10 +38,38 @@ async function authenticate(options) {
|
|
|
38
38
|
const email = options.email || creds?.email;
|
|
39
39
|
const password = options.password || creds?.password;
|
|
40
40
|
const refreshToken = creds?.refreshToken;
|
|
41
|
+
const apiKey = process.env.QATOOLS_API_KEY || creds?.apiKey;
|
|
42
|
+
|
|
43
|
+
if (apiKey) {
|
|
44
|
+
try {
|
|
45
|
+
const authRes = await fetch("https://us-central1-qatools-app.cloudfunctions.net/authenticateWithApiKey", {
|
|
46
|
+
method: "POST", headers: { "Content-Type": "application/json" },
|
|
47
|
+
body: JSON.stringify({ data: { apiKey } }),
|
|
48
|
+
});
|
|
49
|
+
const authData = await authRes.json();
|
|
50
|
+
if (authData.result?.customToken) {
|
|
51
|
+
await signInWithCustomToken(auth, authData.result.customToken);
|
|
52
|
+
// Set projectId from API key response if not already configured
|
|
53
|
+
if (authData.result.projectId && (!creds?.projectId || process.env.QATOOLS_API_KEY)) {
|
|
54
|
+
const configPath = path.join(process.env.HOME || process.env.USERPROFILE, ".qatools");
|
|
55
|
+
const existing = creds || {};
|
|
56
|
+
existing.projectId = authData.result.projectId;
|
|
57
|
+
existing.projectName = authData.result.projectName;
|
|
58
|
+
existing.apiKey = apiKey;
|
|
59
|
+
fs.writeFileSync(configPath, JSON.stringify(existing, null, 2));
|
|
60
|
+
}
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
throw new Error("API key authentication failed");
|
|
64
|
+
} catch (e) {
|
|
65
|
+
console.error("API key auth failed:", e.message);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
41
69
|
|
|
42
70
|
if (refreshToken) {
|
|
43
71
|
try {
|
|
44
|
-
// Exchange refresh token for a new ID token
|
|
72
|
+
// Exchange refresh token for a new ID token
|
|
45
73
|
const res = await fetch(`https://securetoken.googleapis.com/v1/token?key=${firebaseConfig.apiKey}`, {
|
|
46
74
|
method: 'POST',
|
|
47
75
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
@@ -49,8 +77,10 @@ async function authenticate(options) {
|
|
|
49
77
|
});
|
|
50
78
|
const data = await res.json();
|
|
51
79
|
if (!data.id_token) throw new Error("Refresh failed");
|
|
52
|
-
|
|
53
|
-
//
|
|
80
|
+
|
|
81
|
+
// Sign in with the ID token via signInWithCredential
|
|
82
|
+
const { GoogleAuthProvider, signInWithCredential } = require('firebase/auth');
|
|
83
|
+
// First try: sign in directly by minting custom token via REST with proper callable format
|
|
54
84
|
const mintRes = await fetch(`https://us-central1-qatools-app.cloudfunctions.net/mintCliToken`, {
|
|
55
85
|
method: 'POST',
|
|
56
86
|
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${data.id_token}` },
|
|
@@ -61,9 +91,23 @@ async function authenticate(options) {
|
|
|
61
91
|
await signInWithCustomToken(auth, mintData.result.customToken);
|
|
62
92
|
return;
|
|
63
93
|
}
|
|
64
|
-
|
|
94
|
+
|
|
95
|
+
// Fallback: use the ID token to sign in via custom token REST API
|
|
96
|
+
const signInRes = await fetch(`https://identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken?key=${firebaseConfig.apiKey}`, {
|
|
97
|
+
method: 'POST',
|
|
98
|
+
headers: { 'Content-Type': 'application/json' },
|
|
99
|
+
body: JSON.stringify({ token: data.id_token, returnSecureToken: true }),
|
|
100
|
+
});
|
|
101
|
+
const signInData = await signInRes.json();
|
|
102
|
+
if (signInData.error) throw new Error(signInData.error.message);
|
|
103
|
+
|
|
104
|
+
// Use the refresh token from sign-in to authenticate Firebase SDK
|
|
105
|
+
await signInWithCustomToken(auth, data.id_token).catch(() => {});
|
|
106
|
+
|
|
107
|
+
// If all else fails, try email sign in if available
|
|
108
|
+
throw new Error("Could not authenticate with refresh token");
|
|
65
109
|
} catch (e) {
|
|
66
|
-
console.error("
|
|
110
|
+
console.error("Token refresh failed:", e.message, "\nRun 'qatools login --google' to re-authenticate.");
|
|
67
111
|
process.exit(1);
|
|
68
112
|
}
|
|
69
113
|
}
|
|
@@ -200,15 +244,62 @@ program
|
|
|
200
244
|
process.exit(0);
|
|
201
245
|
});
|
|
202
246
|
|
|
247
|
+
program
|
|
248
|
+
.command("projects")
|
|
249
|
+
.description("List all your projects")
|
|
250
|
+
.action(async (options) => {
|
|
251
|
+
await authenticate(options);
|
|
252
|
+
const listProjects = httpsCallable(functions, "listProjects");
|
|
253
|
+
try {
|
|
254
|
+
const result = await listProjects({});
|
|
255
|
+
const projects = result.data.projects;
|
|
256
|
+
console.log(`\n${projects.length} project(s):\n`);
|
|
257
|
+
projects.forEach((p, i) => {
|
|
258
|
+
console.log(` ${i + 1}. ${p.name} ${p.claudeEnabled ? 'π€' : ''} (${p.id})`);
|
|
259
|
+
});
|
|
260
|
+
} catch (e) {
|
|
261
|
+
console.error("Failed:", e.message);
|
|
262
|
+
}
|
|
263
|
+
process.exit(0);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
program
|
|
267
|
+
.command("project-update")
|
|
268
|
+
.description("Update project name or description")
|
|
269
|
+
.option("--name <name>", "New project name")
|
|
270
|
+
.option("--description <desc>", "New project description")
|
|
271
|
+
.option("-i, --id <projectId>", "Project ID (defaults to configured project)")
|
|
272
|
+
.action(async (options) => {
|
|
273
|
+
await authenticate(options);
|
|
274
|
+
const creds = getCredentials();
|
|
275
|
+
const projectId = options.id || creds?.projectId;
|
|
276
|
+
if (!projectId) { console.error("No project. Run: qatools setup"); process.exit(1); }
|
|
277
|
+
if (!options.name && !options.description) { console.error("Specify --name and/or --description"); process.exit(1); }
|
|
278
|
+
const updateProject = httpsCallable(functions, "updateProjectFields");
|
|
279
|
+
try {
|
|
280
|
+
const updates = {};
|
|
281
|
+
if (options.name) updates.name = options.name;
|
|
282
|
+
if (options.description) updates.description = options.description;
|
|
283
|
+
await updateProject({ projectId, updates });
|
|
284
|
+
console.log(`Project updated.`);
|
|
285
|
+
if (options.name) console.log(` Name: ${options.name}`);
|
|
286
|
+
if (options.description) console.log(` Description: ${options.description}`);
|
|
287
|
+
} catch (e) {
|
|
288
|
+
console.error("Failed:", e.message);
|
|
289
|
+
}
|
|
290
|
+
process.exit(0);
|
|
291
|
+
});
|
|
292
|
+
|
|
203
293
|
program
|
|
204
294
|
.command("pending")
|
|
205
295
|
.description("List test sessions awaiting Claude analysis")
|
|
296
|
+
.option("--project <id>", "Target project ID (overrides default; omit both to scan all your projects)")
|
|
206
297
|
.option("-e, --email <email>", "Login email")
|
|
207
298
|
.option("-p, --password <password>", "Login password")
|
|
208
299
|
.action(async (options) => {
|
|
209
300
|
await authenticate(options);
|
|
210
301
|
const creds = getCredentials();
|
|
211
|
-
const projectId = creds?.projectId;
|
|
302
|
+
const projectId = options.project || creds?.projectId;
|
|
212
303
|
|
|
213
304
|
const pendingSessions = httpsCallable(functions, "pendingSessions");
|
|
214
305
|
try {
|
|
@@ -237,8 +328,9 @@ program
|
|
|
237
328
|
program
|
|
238
329
|
.command("backlog")
|
|
239
330
|
.description("List backlog items for configured project")
|
|
331
|
+
.option("--project <id>", "Target project ID (overrides default)")
|
|
240
332
|
.option("--sprint <id>", "Filter by sprint ID")
|
|
241
|
-
.option("--status <statuses>", "Filter by status (comma-separated:
|
|
333
|
+
.option("--status <statuses>", "Filter by status (comma-separated: open,in_progress,resolved,rejected)")
|
|
242
334
|
.option("--assigned <email>", "Filter by assignee")
|
|
243
335
|
.option("-o, --output <path>", "Output file (default: stdout)")
|
|
244
336
|
.option("-e, --email <email>", "Login email")
|
|
@@ -246,11 +338,12 @@ program
|
|
|
246
338
|
.action(async (options) => {
|
|
247
339
|
await authenticate(options);
|
|
248
340
|
const creds = getCredentials();
|
|
249
|
-
|
|
341
|
+
const projectId = options.project || creds?.projectId;
|
|
342
|
+
if (!projectId) { console.error("No project configured. Run: qatools setup, or pass --project <id>"); process.exit(1); }
|
|
250
343
|
|
|
251
344
|
const listBacklog = httpsCallable(functions, "listBacklog");
|
|
252
345
|
try {
|
|
253
|
-
const filters = { projectId
|
|
346
|
+
const filters = { projectId };
|
|
254
347
|
if (options.sprint) filters.sprintId = options.sprint;
|
|
255
348
|
if (options.status) filters.status = options.status.split(",");
|
|
256
349
|
if (options.assigned) filters.assignedTo = options.assigned;
|
|
@@ -274,17 +367,19 @@ program
|
|
|
274
367
|
.command("sprint-items")
|
|
275
368
|
.description("List all items in a sprint")
|
|
276
369
|
.requiredOption("--sprint <id>", "Sprint ID")
|
|
370
|
+
.option("--project <id>", "Target project ID (overrides default)")
|
|
277
371
|
.option("-o, --output <path>", "Output file")
|
|
278
372
|
.option("-e, --email <email>", "Login email")
|
|
279
373
|
.option("-p, --password <password>", "Login password")
|
|
280
374
|
.action(async (options) => {
|
|
281
375
|
await authenticate(options);
|
|
282
376
|
const creds = getCredentials();
|
|
283
|
-
|
|
377
|
+
const projectId = options.project || creds?.projectId;
|
|
378
|
+
if (!projectId) { console.error("No project configured. Run: qatools setup, or pass --project <id>"); process.exit(1); }
|
|
284
379
|
|
|
285
380
|
const getSprintItems = httpsCallable(functions, "getSprintItems");
|
|
286
381
|
try {
|
|
287
|
-
const result = await getSprintItems({ projectId
|
|
382
|
+
const result = await getSprintItems({ projectId, sprintId: options.sprint });
|
|
288
383
|
const data = result.data;
|
|
289
384
|
if (options.output) {
|
|
290
385
|
fs.writeFileSync(path.resolve(options.output), JSON.stringify(data, null, 2));
|
|
@@ -302,16 +397,18 @@ program
|
|
|
302
397
|
program
|
|
303
398
|
.command("sprints")
|
|
304
399
|
.description("List sprints for configured project")
|
|
400
|
+
.option("--project <id>", "Target project ID (overrides default)")
|
|
305
401
|
.option("-e, --email <email>", "Login email")
|
|
306
402
|
.option("-p, --password <password>", "Login password")
|
|
307
403
|
.action(async (options) => {
|
|
308
404
|
await authenticate(options);
|
|
309
405
|
const creds = getCredentials();
|
|
310
|
-
|
|
406
|
+
const projectId = options.project || creds?.projectId;
|
|
407
|
+
if (!projectId) { console.error("No project configured. Run: qatools setup, or pass --project <id>"); process.exit(1); }
|
|
311
408
|
|
|
312
409
|
const listSprints = httpsCallable(functions, "listSprints");
|
|
313
410
|
try {
|
|
314
|
-
const result = await listSprints({ projectId
|
|
411
|
+
const result = await listSprints({ projectId });
|
|
315
412
|
for (const s of result.data.sprints) {
|
|
316
413
|
console.log(` ${s.id} ${s.name} [${s.status}] Release: ${s.releaseDate || "N/A"}`);
|
|
317
414
|
}
|
|
@@ -326,11 +423,13 @@ program
|
|
|
326
423
|
.command("backlog-create")
|
|
327
424
|
.description("Create a new backlog item")
|
|
328
425
|
.requiredOption("-d, --description <text>", "Description")
|
|
329
|
-
.option("
|
|
426
|
+
.option("--project <id>", "Target project ID (overrides default)")
|
|
427
|
+
.option("-t, --type <type>", "Type: bug, improvement, requirement_gap, technical_defect", "bug")
|
|
330
428
|
.option("--priority <n>", "Priority: 1 (high), 2 (medium), 3 (low)", "2")
|
|
331
429
|
.option("--assigned <email>", "Assign to")
|
|
430
|
+
.option("--developer <email>", "Developer (responsible)")
|
|
332
431
|
.option("--sprint <id>", "Sprint ID")
|
|
333
|
-
.option("--parent <label>", "Parent item label (e.g.
|
|
432
|
+
.option("--parent <label>", "Parent item label (e.g. BUG-5)")
|
|
334
433
|
.option("--requirement-key <key>", "Requirement key (e.g. auth.login)")
|
|
335
434
|
.option("--tags <tags>", "Tags (comma-separated)")
|
|
336
435
|
.option("-e, --email <email>", "Login email")
|
|
@@ -338,22 +437,25 @@ program
|
|
|
338
437
|
.action(async (options) => {
|
|
339
438
|
await authenticate(options);
|
|
340
439
|
const creds = getCredentials();
|
|
341
|
-
|
|
440
|
+
const projectId = options.project || creds?.projectId;
|
|
441
|
+
if (!projectId) { console.error("No project configured. Run: qatools setup, or pass --project <id>"); process.exit(1); }
|
|
342
442
|
|
|
343
443
|
const createItem = httpsCallable(functions, "createBacklogItem");
|
|
344
444
|
try {
|
|
345
445
|
const result = await createItem({
|
|
346
|
-
projectId
|
|
446
|
+
projectId,
|
|
347
447
|
type: options.type,
|
|
348
448
|
description: options.description,
|
|
349
449
|
priority: parseInt(options.priority, 10),
|
|
350
450
|
assignedTo: options.assigned || "",
|
|
451
|
+
developer: options.developer || "",
|
|
351
452
|
sprintId: options.sprint || "",
|
|
352
453
|
parentId: options.parent || "",
|
|
353
454
|
requirementKey: options.requirementKey || "",
|
|
354
|
-
tags: options.tags ? options.tags.split(",").map(t => t.trim()) : [],
|
|
455
|
+
tags: options.tags ? options.tags.split(",").map(t => t.trim().toLowerCase()) : [],
|
|
355
456
|
});
|
|
356
457
|
console.log(`Created: ${result.data.type.toUpperCase()}-${result.data.bugNumber} (${result.data.id})`);
|
|
458
|
+
if (options.tags) console.log(` Tags: ${options.tags}`);
|
|
357
459
|
} catch (e) {
|
|
358
460
|
console.error("Failed:", e.message);
|
|
359
461
|
process.exit(1);
|
|
@@ -364,17 +466,19 @@ program
|
|
|
364
466
|
program
|
|
365
467
|
.command("templates")
|
|
366
468
|
.description("List templates and their review status")
|
|
469
|
+
.option("--project <id>", "Target project ID (overrides default)")
|
|
367
470
|
.option("--status <status>", "Filter: draft, pending_review, approved, rejected")
|
|
368
471
|
.option("-e, --email <email>", "Login email")
|
|
369
472
|
.option("-p, --password <password>", "Login password")
|
|
370
473
|
.action(async (options) => {
|
|
371
474
|
await authenticate(options);
|
|
372
475
|
const creds = getCredentials();
|
|
373
|
-
|
|
476
|
+
const projectId = options.project || creds?.projectId;
|
|
477
|
+
if (!projectId) { console.error("No project configured. Run: qatools setup, or pass --project <id>"); process.exit(1); }
|
|
374
478
|
|
|
375
479
|
const listTemplates = httpsCallable(functions, "listTemplates");
|
|
376
480
|
try {
|
|
377
|
-
const result = await listTemplates({ projectId
|
|
481
|
+
const result = await listTemplates({ projectId, status: options.status || null });
|
|
378
482
|
const templates = result.data.templates;
|
|
379
483
|
if (templates.length === 0) {
|
|
380
484
|
console.log("No templates found.");
|
|
@@ -392,19 +496,112 @@ program
|
|
|
392
496
|
process.exit(0);
|
|
393
497
|
});
|
|
394
498
|
|
|
499
|
+
program
|
|
500
|
+
.command("requirements")
|
|
501
|
+
.description("List requirements with rich filters. Superseded reqs are hidden by default.")
|
|
502
|
+
.option("--project <id>", "Target project ID (overrides default)")
|
|
503
|
+
.option("--status <statuses>", "Filter by status (comma-separated): draft,approved,rejected,implemented,tested,superseded")
|
|
504
|
+
.option("--epic <key>", "Only reqs linked to this epic")
|
|
505
|
+
.option("--no-epic", "Only reqs WITHOUT an epic")
|
|
506
|
+
.option("--type <type>", "Filter by type: functional, non-functional")
|
|
507
|
+
.option("--with-coverage", "Only reqs that have at least one test case in any template")
|
|
508
|
+
.option("--without-coverage", "Only reqs with NO test coverage")
|
|
509
|
+
.option("--include-superseded", "Include superseded reqs (hidden by default)")
|
|
510
|
+
.option("--json", "Output as JSON to stdout (machine-readable)")
|
|
511
|
+
.option("-o, --output <file>", "Write JSON output to file")
|
|
512
|
+
.option("-e, --email <email>", "Login email")
|
|
513
|
+
.option("-p, --password <password>", "Login password")
|
|
514
|
+
.action(async (options) => {
|
|
515
|
+
await authenticate(options);
|
|
516
|
+
const creds = getCredentials();
|
|
517
|
+
const projectId = options.project || creds?.projectId;
|
|
518
|
+
if (!projectId) { console.error("No project configured. Run: qatools setup, or pass --project <id>"); process.exit(1); }
|
|
519
|
+
|
|
520
|
+
const listReqs = httpsCallable(functions, "listRequirements");
|
|
521
|
+
try {
|
|
522
|
+
// Server-side status filter only works for a single status; do all filtering client-side for consistency
|
|
523
|
+
const result = await listReqs({ projectId, status: null });
|
|
524
|
+
let reqs = result.data.requirements;
|
|
525
|
+
|
|
526
|
+
// Default: hide superseded
|
|
527
|
+
if (!options.includeSuperseded) reqs = reqs.filter(r => r.status !== "superseded");
|
|
528
|
+
|
|
529
|
+
// Status (comma-separated)
|
|
530
|
+
if (options.status) {
|
|
531
|
+
const statuses = new Set(options.status.split(",").map(s => s.trim()));
|
|
532
|
+
reqs = reqs.filter(r => statuses.has(r.status));
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Epic
|
|
536
|
+
if (options.epic) reqs = reqs.filter(r => r.epicKey === options.epic);
|
|
537
|
+
// commander turns --no-epic into options.epic === false
|
|
538
|
+
if (options.epic === false) reqs = reqs.filter(r => !r.epicKey);
|
|
539
|
+
|
|
540
|
+
// Type
|
|
541
|
+
if (options.type) reqs = reqs.filter(r => r.type === options.type);
|
|
542
|
+
|
|
543
|
+
// Coverage filters require loading templates
|
|
544
|
+
if (options.withCoverage || options.withoutCoverage) {
|
|
545
|
+
const listTemplates = httpsCallable(functions, "listTemplates");
|
|
546
|
+
const tplResult = await listTemplates({ projectId, status: null });
|
|
547
|
+
// listTemplates returns summary only β we need full templates for test cases
|
|
548
|
+
// Use the full templates list via a different path: load each template (or rely on coverage server-side later)
|
|
549
|
+
// For now: scan template summaries for requirementKey if present, otherwise we have to fall back
|
|
550
|
+
const covered = new Set();
|
|
551
|
+
for (const t of (tplResult.data.templates || [])) {
|
|
552
|
+
// Some template responses include sections, others only summaries
|
|
553
|
+
for (const sec of (t.sections || [])) {
|
|
554
|
+
for (const test of (sec.tests || [])) {
|
|
555
|
+
if (test.requirementKey) covered.add(test.requirementKey);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
if (options.withCoverage) reqs = reqs.filter(r => covered.has(r.key));
|
|
560
|
+
if (options.withoutCoverage) reqs = reqs.filter(r => !covered.has(r.key));
|
|
561
|
+
if (covered.size === 0 && (options.withCoverage || options.withoutCoverage)) {
|
|
562
|
+
console.error("Note: listTemplates returned no test-case data. Coverage filter may be unreliable. Use the web app for accurate coverage.");
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Output
|
|
567
|
+
if (options.json) {
|
|
568
|
+
console.log(JSON.stringify(reqs, null, 2));
|
|
569
|
+
process.exit(0);
|
|
570
|
+
}
|
|
571
|
+
if (options.output) {
|
|
572
|
+
fs.writeFileSync(options.output, JSON.stringify(reqs, null, 2));
|
|
573
|
+
console.log(`Written ${reqs.length} requirements to ${options.output}`);
|
|
574
|
+
} else {
|
|
575
|
+
console.log(`\n${reqs.length} requirement${reqs.length !== 1 ? 's' : ''}:\n`);
|
|
576
|
+
for (const r of reqs) {
|
|
577
|
+
const badge = r.status === "rejected" ? "β" : r.status === "approved" ? "β
" : r.status === "tested" ? "π’" : r.status === "implemented" ? "π‘" : r.status === "superseded" ? "π" : "π";
|
|
578
|
+
const epic = r.epicKey ? ` epic:${r.epicKey}` : ' (no epic)';
|
|
579
|
+
console.log(` ${badge} [${r.key}]${epic} ${r.title} (${r.status})`);
|
|
580
|
+
if (r.reviewComment) console.log(` π¬ ${r.reviewComment}`);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
} catch (e) {
|
|
584
|
+
console.error("Failed:", e.message);
|
|
585
|
+
process.exit(1);
|
|
586
|
+
}
|
|
587
|
+
process.exit(0);
|
|
588
|
+
});
|
|
589
|
+
|
|
395
590
|
program
|
|
396
591
|
.command("requirement-keys")
|
|
397
592
|
.description("List all requirement keys in the project (from backlog + templates)")
|
|
593
|
+
.option("--project <id>", "Target project ID (overrides default)")
|
|
398
594
|
.option("-e, --email <email>", "Login email")
|
|
399
595
|
.option("-p, --password <password>", "Login password")
|
|
400
596
|
.action(async (options) => {
|
|
401
597
|
await authenticate(options);
|
|
402
598
|
const creds = getCredentials();
|
|
403
|
-
|
|
599
|
+
const projectId = options.project || creds?.projectId;
|
|
600
|
+
if (!projectId) { console.error("No project configured. Run: qatools setup, or pass --project <id>"); process.exit(1); }
|
|
404
601
|
|
|
405
602
|
const listKeys = httpsCallable(functions, "listRequirementKeys");
|
|
406
603
|
try {
|
|
407
|
-
const result = await listKeys({ projectId
|
|
604
|
+
const result = await listKeys({ projectId });
|
|
408
605
|
const keys = result.data.keys;
|
|
409
606
|
if (keys.length === 0) {
|
|
410
607
|
console.log("No requirement keys found.");
|
|
@@ -425,16 +622,372 @@ program
|
|
|
425
622
|
process.exit(0);
|
|
426
623
|
});
|
|
427
624
|
|
|
625
|
+
program
|
|
626
|
+
.command("requirement-create")
|
|
627
|
+
.description("Upload requirements from JSON file. Each req may include a 'status' field (draft/approved/rejected/implemented/tested); defaults to draft.")
|
|
628
|
+
.requiredOption("-f, --file <path>", "Path to requirements JSON file")
|
|
629
|
+
.option("--project <id>", "Target project ID (overrides default)")
|
|
630
|
+
.option("--force", "Skip semantic similarity check (allow create even if similar key/title exists)")
|
|
631
|
+
.option("-e, --email <email>", "Login email")
|
|
632
|
+
.option("-p, --password <password>", "Login password")
|
|
633
|
+
.action(async (options) => {
|
|
634
|
+
await authenticate(options);
|
|
635
|
+
const creds = getCredentials();
|
|
636
|
+
const projectId = options.project || creds?.projectId;
|
|
637
|
+
if (!projectId) { console.error("No project configured. Run: qatools setup, or pass --project <id>"); process.exit(1); }
|
|
638
|
+
|
|
639
|
+
const fs = require("fs");
|
|
640
|
+
const data = JSON.parse(fs.readFileSync(options.file, "utf8"));
|
|
641
|
+
const requirements = Array.isArray(data) ? data : (data.requirements || [data]);
|
|
642
|
+
|
|
643
|
+
const createReq = httpsCallable(functions, "createRequirement");
|
|
644
|
+
let created = 0, skipped = 0, similar = 0;
|
|
645
|
+
const similarReports = [];
|
|
646
|
+
for (const req of requirements) {
|
|
647
|
+
try {
|
|
648
|
+
const payload = { projectId, ...req };
|
|
649
|
+
if (options.force) payload.force = true;
|
|
650
|
+
const result = await createReq(payload);
|
|
651
|
+
if (result.data.error === "duplicate") {
|
|
652
|
+
skipped++;
|
|
653
|
+
process.stdout.write(`\r Created ${created}, skipped ${skipped} (dup), similar ${similar} / ${requirements.length}`);
|
|
654
|
+
} else if (result.data.error === "similar_exists") {
|
|
655
|
+
similar++;
|
|
656
|
+
similarReports.push({ attempted: req, matches: result.data.matches });
|
|
657
|
+
process.stdout.write(`\r Created ${created}, skipped ${skipped} (dup), similar ${similar} / ${requirements.length}`);
|
|
658
|
+
} else {
|
|
659
|
+
created++;
|
|
660
|
+
process.stdout.write(`\r Created ${created}, skipped ${skipped} (dup), similar ${similar} / ${requirements.length}`);
|
|
661
|
+
}
|
|
662
|
+
} catch (e) {
|
|
663
|
+
console.error(`\nFailed to create ${req.key}: ${e.message}`);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
console.log(`\n\nDone. ${created} created, ${skipped} skipped (exact dup), ${similar} blocked (semantically similar).`);
|
|
667
|
+
if (similar > 0) {
|
|
668
|
+
console.log(`\nSimilar matches found β review before deciding:\n`);
|
|
669
|
+
for (const rep of similarReports) {
|
|
670
|
+
console.log(` Attempted: [${rep.attempted.key}] ${rep.attempted.title}`);
|
|
671
|
+
for (const m of rep.matches) {
|
|
672
|
+
console.log(` β³ similar (${m.matchedOn}): [${m.key}] ${m.title} (${m.status}, id=${m.id})`);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
console.log(`\nResolve by either:\n - editing your input to align with the existing key/title\n - re-running with --force if you really mean a separate requirement`);
|
|
676
|
+
process.exit(2);
|
|
677
|
+
}
|
|
678
|
+
process.exit(0);
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
program
|
|
682
|
+
.command("sync")
|
|
683
|
+
.description("Sync App Context from qa-tools into a CLAUDE.md block")
|
|
684
|
+
.option("--project <id>", "Target project ID (overrides default)")
|
|
685
|
+
.option("-o, --output <path>", "Path to file to update (auto: .claude/CLAUDE.md if it exists, else CLAUDE.md)")
|
|
686
|
+
.option("--check", "Exit 1 if file is out of sync (does not modify)")
|
|
687
|
+
.option("-e, --email <email>", "Login email")
|
|
688
|
+
.option("-p, --password <password>", "Login password")
|
|
689
|
+
.action(async (options) => {
|
|
690
|
+
await authenticate(options);
|
|
691
|
+
const creds = getCredentials();
|
|
692
|
+
const projectId = options.project || creds?.projectId;
|
|
693
|
+
if (!projectId) { console.error("No project configured. Run: qatools setup, or pass --project <id>"); process.exit(1); }
|
|
694
|
+
// Default output: prefer .claude/CLAUDE.md if it exists, else CLAUDE.md in cwd
|
|
695
|
+
if (!options.output) {
|
|
696
|
+
options.output = fs.existsSync(".claude/CLAUDE.md") ? ".claude/CLAUDE.md" : "CLAUDE.md";
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const getCtx = httpsCallable(functions, "getAppContext");
|
|
700
|
+
let ctx;
|
|
701
|
+
try {
|
|
702
|
+
const result = await getCtx({ projectId });
|
|
703
|
+
ctx = result.data.appContext;
|
|
704
|
+
} catch (e) {
|
|
705
|
+
console.error("Failed to fetch App Context:", e.message);
|
|
706
|
+
process.exit(1);
|
|
707
|
+
}
|
|
708
|
+
if (!ctx) {
|
|
709
|
+
console.error(`No App Context defined for project ${projectId}.`);
|
|
710
|
+
process.exit(1);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
const md = renderAppContextMarkdown(ctx, projectId);
|
|
714
|
+
const beginMarker = "<!-- BEGIN qatools-context (auto-synced from qa-tools, do not edit by hand) -->";
|
|
715
|
+
const endMarker = "<!-- END qatools-context -->";
|
|
716
|
+
const block = `${beginMarker}\n${md}\n${endMarker}`;
|
|
717
|
+
|
|
718
|
+
const filePath = path.resolve(options.output);
|
|
719
|
+
let existing = "";
|
|
720
|
+
if (fs.existsSync(filePath)) existing = fs.readFileSync(filePath, "utf8");
|
|
721
|
+
|
|
722
|
+
let updated;
|
|
723
|
+
const beginIdx = existing.indexOf(beginMarker);
|
|
724
|
+
const endIdx = existing.indexOf(endMarker);
|
|
725
|
+
if (beginIdx >= 0 && endIdx > beginIdx) {
|
|
726
|
+
updated = existing.slice(0, beginIdx) + block + existing.slice(endIdx + endMarker.length);
|
|
727
|
+
} else {
|
|
728
|
+
const sep = existing && !existing.endsWith("\n") ? "\n\n" : (existing ? "\n" : "");
|
|
729
|
+
updated = existing + sep + block + "\n";
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
if (options.check) {
|
|
733
|
+
if (existing === updated) {
|
|
734
|
+
console.log(`β ${options.output} is in sync with project ${projectId}`);
|
|
735
|
+
process.exit(0);
|
|
736
|
+
} else {
|
|
737
|
+
console.error(`β ${options.output} is out of sync with project ${projectId}. Run: qatools sync`);
|
|
738
|
+
process.exit(1);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
if (existing === updated) {
|
|
743
|
+
console.log(`β ${options.output} already in sync (no changes)`);
|
|
744
|
+
} else {
|
|
745
|
+
fs.writeFileSync(filePath, updated);
|
|
746
|
+
console.log(`β ${options.output} updated from project ${projectId} (${ctx.name})`);
|
|
747
|
+
}
|
|
748
|
+
process.exit(0);
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
function renderAppContextMarkdown(ctx, projectId) {
|
|
752
|
+
const lines = [];
|
|
753
|
+
const ts = new Date().toISOString();
|
|
754
|
+
lines.push(`<!-- Last synced: ${ts} from project ${projectId} -->`);
|
|
755
|
+
lines.push("");
|
|
756
|
+
lines.push(`## ${ctx.name || "Project Context"}`);
|
|
757
|
+
lines.push("");
|
|
758
|
+
if (ctx.purpose) { lines.push("### Purpose"); lines.push(ctx.purpose); lines.push(""); }
|
|
759
|
+
if (ctx.domainObjects?.length) {
|
|
760
|
+
lines.push("### Domain Objects");
|
|
761
|
+
lines.push(ctx.domainObjects.map(o => `- ${o}`).join("\n"));
|
|
762
|
+
lines.push("");
|
|
763
|
+
}
|
|
764
|
+
if (ctx.actors?.length) {
|
|
765
|
+
lines.push("### Actors");
|
|
766
|
+
lines.push(ctx.actors.map(a => `- ${a}`).join("\n"));
|
|
767
|
+
lines.push("");
|
|
768
|
+
}
|
|
769
|
+
if (ctx.stack?.length) {
|
|
770
|
+
lines.push("### Tech Stack");
|
|
771
|
+
for (const s of ctx.stack) {
|
|
772
|
+
const v = s.version ? ` ${s.version}` : "";
|
|
773
|
+
const n = s.notes ? ` β ${s.notes}` : "";
|
|
774
|
+
lines.push(`- **${s.layer}**: ${s.technology}${v}${n}`);
|
|
775
|
+
}
|
|
776
|
+
lines.push("");
|
|
777
|
+
}
|
|
778
|
+
if (ctx.architecture) { lines.push("### Architecture"); lines.push(ctx.architecture); lines.push(""); }
|
|
779
|
+
if (ctx.security?.length) {
|
|
780
|
+
lines.push("### Security");
|
|
781
|
+
lines.push(ctx.security.map(s => `- ${s}`).join("\n"));
|
|
782
|
+
lines.push("");
|
|
783
|
+
}
|
|
784
|
+
if (ctx.aiRules?.length) {
|
|
785
|
+
lines.push("### AI Rules");
|
|
786
|
+
ctx.aiRules.forEach((r, i) => lines.push(`${i + 1}. ${r}`));
|
|
787
|
+
lines.push("");
|
|
788
|
+
}
|
|
789
|
+
if (ctx.constraints?.length) {
|
|
790
|
+
lines.push("### Constraints");
|
|
791
|
+
lines.push(ctx.constraints.map(c => `- ${c}`).join("\n"));
|
|
792
|
+
lines.push("");
|
|
793
|
+
}
|
|
794
|
+
if (ctx.conventions?.length) {
|
|
795
|
+
lines.push("### Conventions");
|
|
796
|
+
for (const c of ctx.conventions) lines.push(`- **${c.area}**: ${c.rule}`);
|
|
797
|
+
lines.push("");
|
|
798
|
+
}
|
|
799
|
+
if (ctx.environments?.length) {
|
|
800
|
+
lines.push("### Environments");
|
|
801
|
+
for (const e of ctx.environments) {
|
|
802
|
+
const url = e.url ? ` β ${e.url}` : "";
|
|
803
|
+
const notes = e.notes ? ` (${e.notes})` : "";
|
|
804
|
+
lines.push(`- **${e.name}**${url}${notes}`);
|
|
805
|
+
}
|
|
806
|
+
lines.push("");
|
|
807
|
+
}
|
|
808
|
+
if (ctx.integrations?.length) {
|
|
809
|
+
lines.push("### Integrations");
|
|
810
|
+
for (const i of ctx.integrations) {
|
|
811
|
+
const purpose = i.purpose ? ` β ${i.purpose}` : "";
|
|
812
|
+
lines.push(`- **${i.name}**${purpose}`);
|
|
813
|
+
}
|
|
814
|
+
lines.push("");
|
|
815
|
+
}
|
|
816
|
+
if (ctx.sections?.length) {
|
|
817
|
+
for (const s of ctx.sections) {
|
|
818
|
+
lines.push(`### ${s.title}`);
|
|
819
|
+
// defensive: escape end-comment markers only in user content so it can't break our END marker
|
|
820
|
+
lines.push((s.content || "").replace(/-->/g, "--\\>"));
|
|
821
|
+
lines.push("");
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
return lines.join("\n");
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
program
|
|
828
|
+
.command("next")
|
|
829
|
+
.description("Show what Claude should work on next in this project")
|
|
830
|
+
.option("--project <id>", "Target project ID (overrides default)")
|
|
831
|
+
.option("--json", "Output raw JSON instead of formatted text")
|
|
832
|
+
.option("-e, --email <email>", "Login email")
|
|
833
|
+
.option("-p, --password <password>", "Login password")
|
|
834
|
+
.action(async (options) => {
|
|
835
|
+
await authenticate(options);
|
|
836
|
+
const creds = getCredentials();
|
|
837
|
+
const projectId = options.project || creds?.projectId;
|
|
838
|
+
if (!projectId) { console.error("No project configured. Run: qatools setup, or pass --project <id>"); process.exit(1); }
|
|
839
|
+
const getAgenda = httpsCallable(functions, "getProjectAgenda");
|
|
840
|
+
let result;
|
|
841
|
+
try {
|
|
842
|
+
result = await getAgenda({ projectId });
|
|
843
|
+
} catch (e) {
|
|
844
|
+
console.error("Failed:", e.message);
|
|
845
|
+
process.exit(1);
|
|
846
|
+
}
|
|
847
|
+
const data = result.data;
|
|
848
|
+
if (options.json) {
|
|
849
|
+
console.log(JSON.stringify(data, null, 2));
|
|
850
|
+
process.exit(0);
|
|
851
|
+
}
|
|
852
|
+
// Formatted output
|
|
853
|
+
console.log(`\n ${data.projectName} (${data.projectId})`);
|
|
854
|
+
if (data.activeSprint) console.log(` Active sprint: ${data.activeSprint.name}`);
|
|
855
|
+
console.log("");
|
|
856
|
+
|
|
857
|
+
const sections = [];
|
|
858
|
+
if (data.agenda.pendingSessions.length) {
|
|
859
|
+
const items = data.agenda.pendingSessions.map(s => ` β’ "${s.templateName}" (sess: ${s.id})`);
|
|
860
|
+
sections.push({
|
|
861
|
+
icon: "π΄",
|
|
862
|
+
title: `URGENT β ${data.agenda.pendingSessions.length} session${data.agenda.pendingSessions.length > 1 ? 's' : ''} waiting for analysis`,
|
|
863
|
+
items,
|
|
864
|
+
next: `qatools read-session <id> β qatools upload-analysis <file>`,
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
if (data.agenda.approvedNoCoverage.length) {
|
|
868
|
+
const items = data.agenda.approvedNoCoverage.slice(0, 10).map(r => ` β’ [${r.key}] ${r.title}`);
|
|
869
|
+
if (data.agenda.approvedNoCoverage.length > 10) items.push(` β’ ... and ${data.agenda.approvedNoCoverage.length - 10} more`);
|
|
870
|
+
sections.push({
|
|
871
|
+
icon: "π‘",
|
|
872
|
+
title: `${data.agenda.approvedNoCoverage.length} approved requirement${data.agenda.approvedNoCoverage.length > 1 ? 's' : ''} without test coverage`,
|
|
873
|
+
items,
|
|
874
|
+
next: `write a template covering these β qatools template-upload <file>`,
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
if (data.agenda.continuousUnverified.length) {
|
|
878
|
+
const items = data.agenda.continuousUnverified.map(r => ` β’ [${r.key}] ${r.title}`);
|
|
879
|
+
sections.push({
|
|
880
|
+
icon: "π’",
|
|
881
|
+
title: `${data.agenda.continuousUnverified.length} continuous compliance req${data.agenda.continuousUnverified.length > 1 ? 's' : ''} need verification for "${data.agenda.continuousUnverified[0].sprintName}"`,
|
|
882
|
+
items,
|
|
883
|
+
next: `verify each against this sprint's diff and record result via UI Sprintar tab`,
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
if (data.agenda.sprintReqsNotTested.length) {
|
|
887
|
+
const items = data.agenda.sprintReqsNotTested.slice(0, 10).map(r => ` β’ [${r.key}] ${r.title} (${r.status})`);
|
|
888
|
+
if (data.agenda.sprintReqsNotTested.length > 10) items.push(` β’ ... and ${data.agenda.sprintReqsNotTested.length - 10} more`);
|
|
889
|
+
sections.push({
|
|
890
|
+
icon: "π’",
|
|
891
|
+
title: `${data.agenda.sprintReqsNotTested.length} requirement${data.agenda.sprintReqsNotTested.length > 1 ? 's' : ''} in active sprint not yet tested`,
|
|
892
|
+
items,
|
|
893
|
+
next: `implement, test, then qatools requirement-update -i <id> --status tested`,
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
if (data.agenda.draftReqs.length) {
|
|
897
|
+
const items = data.agenda.draftReqs.slice(0, 10).map(r => ` β’ [${r.key}] ${r.title}${r.hasAC ? '' : ' (no AC)'}`);
|
|
898
|
+
if (data.agenda.draftReqs.length > 10) items.push(` β’ ... and ${data.agenda.draftReqs.length - 10} more`);
|
|
899
|
+
sections.push({
|
|
900
|
+
icon: "βͺ",
|
|
901
|
+
title: `${data.agenda.draftReqs.length} draft requirement${data.agenda.draftReqs.length > 1 ? 's' : ''} need acceptance criteria / approval`,
|
|
902
|
+
items,
|
|
903
|
+
next: `qatools requirement-update -i <id> --status approved (or refine AC first)`,
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
if (sections.length === 0) {
|
|
908
|
+
console.log(" β Nothing on the agenda. The project is in a clean state.\n");
|
|
909
|
+
process.exit(0);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
for (const sec of sections) {
|
|
913
|
+
console.log(`${sec.icon} ${sec.title}`);
|
|
914
|
+
sec.items.forEach(i => console.log(i));
|
|
915
|
+
console.log(` β ${sec.next}`);
|
|
916
|
+
console.log("");
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
console.log(` ββββββββββββββββββββ`);
|
|
920
|
+
console.log(` ${data.totalCount} thing${data.totalCount !== 1 ? 's' : ''} on the agenda. Start with the ${sections[0].icon} row.\n`);
|
|
921
|
+
process.exit(0);
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
program
|
|
925
|
+
.command("requirement-update")
|
|
926
|
+
.description("Update one or more requirements (by id, optionally from a JSON file)")
|
|
927
|
+
.option("-f, --file <path>", "JSON file with updates: { '<requirementId>': { status, epicKey, ... } }")
|
|
928
|
+
.option("-i, --id <id>", "Single requirement ID to update")
|
|
929
|
+
.option("--status <status>", "New status (draft/approved/rejected/implemented/tested/superseded)")
|
|
930
|
+
.option("--epic <key>", "Set epicKey")
|
|
931
|
+
.option("--sprint <id>", "Set sprintId (use empty string to clear)")
|
|
932
|
+
.option("--type <type>", "Set type (functional/non-functional)")
|
|
933
|
+
.option("--title <text>", "Set title")
|
|
934
|
+
.option("--description <text>", "Set description")
|
|
935
|
+
.option("--ac <text>", "Set acceptance criteria")
|
|
936
|
+
.option("--project <id>", "Target project ID (overrides default; requires access to that project)")
|
|
937
|
+
.option("-e, --email <email>", "Login email")
|
|
938
|
+
.option("-p, --password <password>", "Login password")
|
|
939
|
+
.action(async (options) => {
|
|
940
|
+
await authenticate(options);
|
|
941
|
+
const creds = getCredentials();
|
|
942
|
+
const projectId = options.project || creds?.projectId;
|
|
943
|
+
if (!projectId) { console.error("No project configured. Run: qatools setup, or pass --project <id>"); process.exit(1); }
|
|
944
|
+
|
|
945
|
+
let updates = {};
|
|
946
|
+
if (options.file) {
|
|
947
|
+
updates = JSON.parse(fs.readFileSync(path.resolve(options.file), "utf8"));
|
|
948
|
+
} else if (options.id) {
|
|
949
|
+
const u = {};
|
|
950
|
+
if (options.status) u.status = options.status;
|
|
951
|
+
if (options.epic !== undefined) u.epicKey = options.epic;
|
|
952
|
+
if (options.sprint !== undefined) u.sprintId = options.sprint;
|
|
953
|
+
if (options.type) u.type = options.type;
|
|
954
|
+
if (options.title) u.title = options.title;
|
|
955
|
+
if (options.description) u.description = options.description;
|
|
956
|
+
if (options.ac) u.acceptanceCriteria = options.ac;
|
|
957
|
+
updates[options.id] = u;
|
|
958
|
+
} else {
|
|
959
|
+
console.error("Provide --file or --id");
|
|
960
|
+
process.exit(1);
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
const updateReq = httpsCallable(functions, "updateRequirement");
|
|
964
|
+
let ok = 0, failed = 0;
|
|
965
|
+
for (const [requirementId, fields] of Object.entries(updates)) {
|
|
966
|
+
try {
|
|
967
|
+
await updateReq({ projectId, requirementId, updates: fields });
|
|
968
|
+
ok++;
|
|
969
|
+
process.stdout.write(`\r Updated ${ok}, failed ${failed} / ${Object.keys(updates).length}`);
|
|
970
|
+
} catch (e) {
|
|
971
|
+
failed++;
|
|
972
|
+
console.error(`\nFailed ${requirementId}: ${e.message}`);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
console.log(`\n\nDone. ${ok} updated, ${failed} failed.`);
|
|
976
|
+
process.exit(0);
|
|
977
|
+
});
|
|
978
|
+
|
|
428
979
|
program
|
|
429
980
|
.command("template-upload")
|
|
430
981
|
.description("Upload a test template JSON to the project")
|
|
431
982
|
.requiredOption("-f, --file <path>", "Path to template JSON file")
|
|
983
|
+
.option("--project <id>", "Target project ID (overrides default)")
|
|
432
984
|
.option("-e, --email <email>", "Login email")
|
|
433
985
|
.option("-p, --password <password>", "Login password")
|
|
434
986
|
.action(async (options) => {
|
|
435
987
|
await authenticate(options);
|
|
436
988
|
const creds = getCredentials();
|
|
437
|
-
|
|
989
|
+
const projectId = options.project || creds?.projectId;
|
|
990
|
+
if (!projectId) { console.error("No project configured. Run: qatools setup, or pass --project <id>"); process.exit(1); }
|
|
438
991
|
|
|
439
992
|
const filePath = path.resolve(options.file);
|
|
440
993
|
if (!fs.existsSync(filePath)) { console.error(`File not found: ${filePath}`); process.exit(1); }
|
|
@@ -445,7 +998,7 @@ program
|
|
|
445
998
|
const uploadTemplate = httpsCallable(functions, "uploadTemplate");
|
|
446
999
|
try {
|
|
447
1000
|
const result = await uploadTemplate({
|
|
448
|
-
projectId
|
|
1001
|
+
projectId,
|
|
449
1002
|
name: data.name,
|
|
450
1003
|
version: data.version || "",
|
|
451
1004
|
sections: data.sections,
|
|
@@ -459,6 +1012,85 @@ program
|
|
|
459
1012
|
process.exit(0);
|
|
460
1013
|
});
|
|
461
1014
|
|
|
1015
|
+
program
|
|
1016
|
+
.command("backlog-tags")
|
|
1017
|
+
.description("List all existing tags in the backlog")
|
|
1018
|
+
.option("--project <id>", "Target project ID (overrides default)")
|
|
1019
|
+
.option("-e, --email <email>", "Login email")
|
|
1020
|
+
.option("-p, --password <password>", "Login password")
|
|
1021
|
+
.action(async (options) => {
|
|
1022
|
+
await authenticate(options);
|
|
1023
|
+
const creds = getCredentials();
|
|
1024
|
+
const projectId = options.project || creds?.projectId;
|
|
1025
|
+
if (!projectId) { console.error("No project configured. Run: qatools setup, or pass --project <id>"); process.exit(1); }
|
|
1026
|
+
const listBacklog = httpsCallable(functions, "listBacklog");
|
|
1027
|
+
const result = await listBacklog({ projectId });
|
|
1028
|
+
const tags = new Set();
|
|
1029
|
+
result.data.items.forEach(i => (i.tags || []).forEach(t => tags.add(t)));
|
|
1030
|
+
const sorted = [...tags].sort();
|
|
1031
|
+
if (sorted.length === 0) { console.log("No tags found."); }
|
|
1032
|
+
else { sorted.forEach(t => console.log(t)); console.log(`\n${sorted.length} tag(s)`); }
|
|
1033
|
+
process.exit(0);
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
program
|
|
1037
|
+
.command("backlog-duplicates")
|
|
1038
|
+
.description("Find potential duplicate backlog items using AI")
|
|
1039
|
+
.option("--project <id>", "Target project ID (overrides default)")
|
|
1040
|
+
.option("--local", "Output items for local AI analysis (no API key needed)")
|
|
1041
|
+
.option("-e, --email <email>", "Login email")
|
|
1042
|
+
.option("-p, --password <password>", "Login password")
|
|
1043
|
+
.action(async (options) => {
|
|
1044
|
+
await authenticate(options);
|
|
1045
|
+
const creds = getCredentials();
|
|
1046
|
+
const projectId = options.project || creds?.projectId;
|
|
1047
|
+
if (!projectId) { console.error("No project configured. Run: qatools setup, or pass --project <id>"); process.exit(1); }
|
|
1048
|
+
|
|
1049
|
+
if (options.local) {
|
|
1050
|
+
// Fetch items with full context for local analysis
|
|
1051
|
+
const listBacklog = httpsCallable(functions, "listBacklog");
|
|
1052
|
+
const result = await listBacklog({ projectId });
|
|
1053
|
+
const prefix = t => ({ bug: "AVV", improvement: "IMP", requirement_gap: "GAP", technical_defect: "DEF" }[t] || "BUG");
|
|
1054
|
+
const items = result.data.items.filter(i => i.status !== "rejected");
|
|
1055
|
+
console.log(`Find duplicates among these ${items.length} backlog items. Items that describe the same issue, feature, or task β even if worded differently.\n`);
|
|
1056
|
+
console.log(`Strategy: compare semantically, check shared requirementKey/testId, read comments for context. Would fixing one make the other unnecessary?\n`);
|
|
1057
|
+
for (const i of items) {
|
|
1058
|
+
let line = `${prefix(i.type)}-${i.bugNumber} [${i.type}] (${i.status})`;
|
|
1059
|
+
if (i.requirementKey) line += ` req:${i.requirementKey}`;
|
|
1060
|
+
if (i.testId) line += ` test:${i.testId}`;
|
|
1061
|
+
if (i.tags?.length) line += ` tags:[${i.tags.join(",")}]`;
|
|
1062
|
+
if (i.duplicateOf) line += ` duplicateOf:${i.duplicateOf}`;
|
|
1063
|
+
line += `\n "${i.description}"`;
|
|
1064
|
+
if (i.comments?.length) line += `\n Comments: ${i.comments.slice(0, 3).map(c => `"${(c.text || '').substring(0, 100)}"`).join(" | ")}`;
|
|
1065
|
+
console.log(line + "\n");
|
|
1066
|
+
}
|
|
1067
|
+
console.log(`For each duplicate group: items, confidence (high/medium/low), reason.`);
|
|
1068
|
+
process.exit(0);
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
console.log("Analyzing backlog for duplicates...");
|
|
1072
|
+
const findDuplicates = httpsCallable(functions, "findDuplicates");
|
|
1073
|
+
try {
|
|
1074
|
+
const result = await findDuplicates({ projectId });
|
|
1075
|
+
const { duplicates, itemCount } = result.data;
|
|
1076
|
+
console.log(`Analyzed ${itemCount} items.\n`);
|
|
1077
|
+
if (duplicates.length === 0) {
|
|
1078
|
+
console.log("No duplicates found.");
|
|
1079
|
+
} else {
|
|
1080
|
+
for (const group of duplicates) {
|
|
1081
|
+
const conf = group.confidence === 'high' ? 'π΄' : group.confidence === 'medium' ? 'π‘' : 'π’';
|
|
1082
|
+
console.log(`${conf} ${group.items.join(' β ')} [${group.confidence}]`);
|
|
1083
|
+
console.log(` ${group.reason}\n`);
|
|
1084
|
+
}
|
|
1085
|
+
console.log(`Found ${duplicates.length} potential duplicate group(s).`);
|
|
1086
|
+
}
|
|
1087
|
+
} catch (e) {
|
|
1088
|
+
console.error("Failed:", e.message);
|
|
1089
|
+
process.exit(1);
|
|
1090
|
+
}
|
|
1091
|
+
process.exit(0);
|
|
1092
|
+
});
|
|
1093
|
+
|
|
462
1094
|
program
|
|
463
1095
|
.command("backlog-update")
|
|
464
1096
|
.description("Update one or more backlog items (JSON file or inline)")
|
|
@@ -467,7 +1099,12 @@ program
|
|
|
467
1099
|
.option("--status <status>", "New status")
|
|
468
1100
|
.option("--comment <text>", "Add comment")
|
|
469
1101
|
.option("--assigned <email>", "Assign to")
|
|
470
|
-
.option("--
|
|
1102
|
+
.option("--sprint <id>", "Set sprint ID")
|
|
1103
|
+
.option("--priority <n>", "Priority: 1 (high), 2 (medium), 3 (low)")
|
|
1104
|
+
.option("--developer <email>", "Set developer")
|
|
1105
|
+
.option("--requirement-key <key>", "Set requirement key")
|
|
1106
|
+
.option("--tags <tags>", "Set tags (comma-separated, replaces existing)")
|
|
1107
|
+
.option("--description <text>", "Update description")
|
|
471
1108
|
.option("--author <name>", "Author name for comments (default: your email)", "Claude")
|
|
472
1109
|
.option("-e, --email <email>", "Login email")
|
|
473
1110
|
.option("-p, --password <password>", "Login password")
|
|
@@ -486,7 +1123,12 @@ program
|
|
|
486
1123
|
if (options.status) u.status = options.status;
|
|
487
1124
|
if (options.comment) { u.comment = options.comment; u.authorName = options.author; }
|
|
488
1125
|
if (options.assigned) u.assignedTo = options.assigned;
|
|
489
|
-
if (options.
|
|
1126
|
+
if (options.developer) u.developer = options.developer;
|
|
1127
|
+
if (options.sprint) u.sprintId = options.sprint;
|
|
1128
|
+
if (options.priority) u.priority = parseInt(options.priority, 10);
|
|
1129
|
+
if (options.requirementKey) u.requirementKey = options.requirementKey;
|
|
1130
|
+
if (options.tags) u.tags = options.tags.split(",").map(t => t.trim().toLowerCase());
|
|
1131
|
+
if (options.description) u.description = options.description;
|
|
490
1132
|
updates[options.id] = u;
|
|
491
1133
|
} else {
|
|
492
1134
|
console.error("Provide --file or --id");
|
|
@@ -504,15 +1146,703 @@ program
|
|
|
504
1146
|
process.exit(0);
|
|
505
1147
|
});
|
|
506
1148
|
|
|
1149
|
+
program
|
|
1150
|
+
.command("security-import")
|
|
1151
|
+
.description("Import security scan results (SARIF format) as backlog items")
|
|
1152
|
+
.requiredOption("-f, --file <path>", "Path to SARIF JSON file")
|
|
1153
|
+
.option("--project <id>", "Target project ID (overrides default)")
|
|
1154
|
+
.option("-s, --source <name>", "Scanner name (e.g. semgrep, snyk, sonarqube)")
|
|
1155
|
+
.action(async (options) => {
|
|
1156
|
+
await authenticate(options);
|
|
1157
|
+
const creds = getCredentials();
|
|
1158
|
+
const projectId = options.project || creds?.projectId;
|
|
1159
|
+
if (!projectId) { console.error("No project configured. Run: qatools setup, or pass --project <id>"); process.exit(1); }
|
|
1160
|
+
|
|
1161
|
+
const sarif = JSON.parse(fs.readFileSync(path.resolve(options.file), "utf-8"));
|
|
1162
|
+
const importScan = httpsCallable(functions, "importSecurityScan");
|
|
1163
|
+
try {
|
|
1164
|
+
const result = await importScan({ projectId, sarif, source: options.source || "" });
|
|
1165
|
+
const data = result.data;
|
|
1166
|
+
console.log(`\nSecurity scan imported:`);
|
|
1167
|
+
console.log(` Total findings: ${data.total}`);
|
|
1168
|
+
console.log(` Created: ${data.created} backlog items`);
|
|
1169
|
+
console.log(` Skipped: ${data.skipped} (duplicates)`);
|
|
1170
|
+
} catch (e) {
|
|
1171
|
+
console.error("Failed:", e.message);
|
|
1172
|
+
}
|
|
1173
|
+
process.exit(0);
|
|
1174
|
+
});
|
|
1175
|
+
|
|
1176
|
+
program
|
|
1177
|
+
.command("context")
|
|
1178
|
+
.description("Show App Context for the project")
|
|
1179
|
+
.option("-o, --output <path>", "Save to JSON file")
|
|
1180
|
+
.option("--project <id>", "Target project ID (overrides default; requires access to that project)")
|
|
1181
|
+
.option("-e, --email <email>", "Login email")
|
|
1182
|
+
.option("-p, --password <password>", "Login password")
|
|
1183
|
+
.action(async (options) => {
|
|
1184
|
+
await authenticate(options);
|
|
1185
|
+
const creds = getCredentials();
|
|
1186
|
+
const projectId = options.project || creds?.projectId;
|
|
1187
|
+
if (!projectId) { console.error("No project configured. Run: qatools setup, or pass --project <id>"); process.exit(1); }
|
|
1188
|
+
const getCtx = httpsCallable(functions, "getAppContext");
|
|
1189
|
+
const result = await getCtx({ projectId });
|
|
1190
|
+
const ctx = result.data.appContext;
|
|
1191
|
+
if (!ctx) { console.log("No App Context defined for this project."); process.exit(0); }
|
|
1192
|
+
if (options.output) {
|
|
1193
|
+
fs.writeFileSync(path.resolve(options.output), JSON.stringify(ctx, null, 2));
|
|
1194
|
+
console.log(`Saved to ${options.output}`);
|
|
1195
|
+
} else {
|
|
1196
|
+
console.log(`\n${ctx.name}`);
|
|
1197
|
+
if (ctx.description) console.log(ctx.description);
|
|
1198
|
+
if (ctx.stack?.length) {
|
|
1199
|
+
console.log("\nTech Stack:");
|
|
1200
|
+
ctx.stack.forEach(s => console.log(` ${s.layer}: ${s.technology}${s.version ? ' ' + s.version : ''}${s.notes ? ' β ' + s.notes : ''}`));
|
|
1201
|
+
}
|
|
1202
|
+
if (ctx.architecture) console.log(`\nArchitecture: ${ctx.architecture}`);
|
|
1203
|
+
if (ctx.constraints?.length) { console.log("\nConstraints:"); ctx.constraints.forEach(c => console.log(` - ${c}`)); }
|
|
1204
|
+
if (ctx.conventions?.length) { console.log("\nConventions:"); ctx.conventions.forEach(c => console.log(` - ${c.area}: ${c.rule}`)); }
|
|
1205
|
+
if (ctx.sections?.length) { ctx.sections.forEach(s => console.log(`\n${s.title}:\n ${s.content}`)); }
|
|
1206
|
+
}
|
|
1207
|
+
process.exit(0);
|
|
1208
|
+
});
|
|
1209
|
+
|
|
1210
|
+
program
|
|
1211
|
+
.command("context-set")
|
|
1212
|
+
.description("Upload App Context from JSON file")
|
|
1213
|
+
.requiredOption("-f, --file <path>", "Path to App Context JSON file")
|
|
1214
|
+
.option("--project <id>", "Target project ID (overrides default; requires access to that project)")
|
|
1215
|
+
.option("-e, --email <email>", "Login email")
|
|
1216
|
+
.option("-p, --password <password>", "Login password")
|
|
1217
|
+
.action(async (options) => {
|
|
1218
|
+
await authenticate(options);
|
|
1219
|
+
const creds = getCredentials();
|
|
1220
|
+
const projectId = options.project || creds?.projectId;
|
|
1221
|
+
if (!projectId) { console.error("No project configured. Run: qatools setup, or pass --project <id>"); process.exit(1); }
|
|
1222
|
+
const data = JSON.parse(fs.readFileSync(path.resolve(options.file), "utf8"));
|
|
1223
|
+
const saveCtx = httpsCallable(functions, "saveAppContext");
|
|
1224
|
+
const result = await saveCtx({ projectId, context: data });
|
|
1225
|
+
console.log(`App Context saved (${result.data.id}) β project ${projectId}`);
|
|
1226
|
+
process.exit(0);
|
|
1227
|
+
});
|
|
1228
|
+
|
|
1229
|
+
program
|
|
1230
|
+
.command("epics")
|
|
1231
|
+
.description("List epics for the project")
|
|
1232
|
+
.option("--project <id>", "Target project ID (overrides default)")
|
|
1233
|
+
.option("-o, --output <path>", "Save to JSON file")
|
|
1234
|
+
.option("-e, --email <email>", "Login email")
|
|
1235
|
+
.option("-p, --password <password>", "Login password")
|
|
1236
|
+
.action(async (options) => {
|
|
1237
|
+
await authenticate(options);
|
|
1238
|
+
const creds = getCredentials();
|
|
1239
|
+
const projectId = options.project || creds?.projectId;
|
|
1240
|
+
if (!projectId) { console.error("No project configured. Run: qatools setup, or pass --project <id>"); process.exit(1); }
|
|
1241
|
+
const listEpics = httpsCallable(functions, "listEpics");
|
|
1242
|
+
const result = await listEpics({ projectId });
|
|
1243
|
+
const epics = result.data.epics;
|
|
1244
|
+
if (options.output) {
|
|
1245
|
+
fs.writeFileSync(path.resolve(options.output), JSON.stringify(epics, null, 2));
|
|
1246
|
+
console.log(`Saved ${epics.length} epics to ${options.output}`);
|
|
1247
|
+
} else {
|
|
1248
|
+
if (epics.length === 0) { console.log("No epics defined."); }
|
|
1249
|
+
else { epics.forEach(e => console.log(` [${e.key}] ${e.title} (${e.status})`)); }
|
|
1250
|
+
}
|
|
1251
|
+
process.exit(0);
|
|
1252
|
+
});
|
|
1253
|
+
|
|
1254
|
+
program
|
|
1255
|
+
.command("epic-update")
|
|
1256
|
+
.description("Update an epic (status, title, description, etc)")
|
|
1257
|
+
.requiredOption("-i, --id <id>", "Epic ID")
|
|
1258
|
+
.option("--status <status>", "draft/active/completed")
|
|
1259
|
+
.option("--title <text>", "Title")
|
|
1260
|
+
.option("--description <text>", "Description")
|
|
1261
|
+
.option("--value <text>", "Value statement")
|
|
1262
|
+
.option("--scope <items>", "Scope items (semicolon-separated, replaces existing)")
|
|
1263
|
+
.option("--priority <n>", "Priority: 1/2/3")
|
|
1264
|
+
.option("--project <id>", "Target project ID")
|
|
1265
|
+
.option("-e, --email <email>", "Login email")
|
|
1266
|
+
.option("-p, --password <password>", "Login password")
|
|
1267
|
+
.action(async (options) => {
|
|
1268
|
+
await authenticate(options);
|
|
1269
|
+
const creds = getCredentials();
|
|
1270
|
+
const projectId = options.project || creds?.projectId;
|
|
1271
|
+
if (!projectId) { console.error("No project configured."); process.exit(1); }
|
|
1272
|
+
const updates = {};
|
|
1273
|
+
if (options.status) updates.status = options.status;
|
|
1274
|
+
if (options.title) updates.title = options.title;
|
|
1275
|
+
if (options.description) updates.description = options.description;
|
|
1276
|
+
if (options.value) updates.value = options.value;
|
|
1277
|
+
if (options.scope) updates.scope = options.scope.split(";").map(s => s.trim()).filter(s => s);
|
|
1278
|
+
if (options.priority) updates.priority = parseInt(options.priority, 10);
|
|
1279
|
+
if (Object.keys(updates).length === 0) { console.error("No updates provided."); process.exit(1); }
|
|
1280
|
+
const updateEpic = httpsCallable(functions, "updateEpic");
|
|
1281
|
+
try {
|
|
1282
|
+
await updateEpic({ projectId, epicId: options.id, updates });
|
|
1283
|
+
console.log(`Epic ${options.id} updated:`, JSON.stringify(updates));
|
|
1284
|
+
} catch (e) {
|
|
1285
|
+
console.error("Failed:", e.message);
|
|
1286
|
+
process.exit(1);
|
|
1287
|
+
}
|
|
1288
|
+
process.exit(0);
|
|
1289
|
+
});
|
|
1290
|
+
|
|
1291
|
+
program
|
|
1292
|
+
.command("epic-create")
|
|
1293
|
+
.description("Create a new epic")
|
|
1294
|
+
.requiredOption("-k, --key <key>", "Epic key (e.g. auth, billing)")
|
|
1295
|
+
.requiredOption("-t, --title <title>", "Epic title")
|
|
1296
|
+
.option("--project <id>", "Target project ID (overrides default)")
|
|
1297
|
+
.option("-d, --description <text>", "Epic description")
|
|
1298
|
+
.option("--value <text>", "Why this capability exists")
|
|
1299
|
+
.option("--scope <items>", "Scope items (semicolon-separated)")
|
|
1300
|
+
.option("--depends-on <keys>", "Epic keys this depends on (comma-separated)")
|
|
1301
|
+
.option("--priority <n>", "Priority: 1 (high), 2 (medium), 3 (low)", "2")
|
|
1302
|
+
.option("-e, --email <email>", "Login email")
|
|
1303
|
+
.option("-p, --password <password>", "Login password")
|
|
1304
|
+
.action(async (options) => {
|
|
1305
|
+
await authenticate(options);
|
|
1306
|
+
const creds = getCredentials();
|
|
1307
|
+
const projectId = options.project || creds?.projectId;
|
|
1308
|
+
if (!projectId) { console.error("No project configured. Run: qatools setup, or pass --project <id>"); process.exit(1); }
|
|
1309
|
+
const createEpic = httpsCallable(functions, "createEpic");
|
|
1310
|
+
try {
|
|
1311
|
+
const result = await createEpic({
|
|
1312
|
+
projectId,
|
|
1313
|
+
key: options.key,
|
|
1314
|
+
title: options.title,
|
|
1315
|
+
description: options.description || "",
|
|
1316
|
+
value: options.value || "",
|
|
1317
|
+
scope: options.scope ? options.scope.split(";").map(s => s.trim()).filter(s => s) : [],
|
|
1318
|
+
dependsOn: options.dependsOn ? options.dependsOn.split(",").map(s => s.trim()).filter(s => s) : [],
|
|
1319
|
+
priority: parseInt(options.priority, 10),
|
|
1320
|
+
});
|
|
1321
|
+
if (result.data.error === "duplicate") { console.log(`Epic '${options.key}' already exists.`); }
|
|
1322
|
+
else { console.log(`Created epic: ${options.key} (${result.data.id})`); }
|
|
1323
|
+
} catch (e) {
|
|
1324
|
+
console.error("Failed:", e.message);
|
|
1325
|
+
process.exit(1);
|
|
1326
|
+
}
|
|
1327
|
+
process.exit(0);
|
|
1328
|
+
});
|
|
1329
|
+
|
|
1330
|
+
program
|
|
1331
|
+
.command("epic-delete")
|
|
1332
|
+
.description("Delete an epic (only if no requirements are linked)")
|
|
1333
|
+
.requiredOption("-k, --key <key>", "Epic key to delete")
|
|
1334
|
+
.option("--project <id>", "Target project ID (overrides default)")
|
|
1335
|
+
.option("-e, --email <email>", "Login email")
|
|
1336
|
+
.option("-p, --password <password>", "Login password")
|
|
1337
|
+
.action(async (options) => {
|
|
1338
|
+
await authenticate(options);
|
|
1339
|
+
const creds = getCredentials();
|
|
1340
|
+
const projectId = options.project || creds?.projectId;
|
|
1341
|
+
if (!projectId) { console.error("No project configured. Run: qatools setup, or pass --project <id>"); process.exit(1); }
|
|
1342
|
+
// Find epic ID by key
|
|
1343
|
+
const listEpics = httpsCallable(functions, "listEpics");
|
|
1344
|
+
const epicsResult = await listEpics({ projectId });
|
|
1345
|
+
const epic = epicsResult.data.epics.find(e => e.key === options.key);
|
|
1346
|
+
if (!epic) { console.error(`Epic '${options.key}' not found.`); process.exit(1); }
|
|
1347
|
+
const deleteEpic = httpsCallable(functions, "deleteEpic");
|
|
1348
|
+
try {
|
|
1349
|
+
await deleteEpic({ projectId, epicId: epic.id });
|
|
1350
|
+
console.log(`Deleted epic: ${options.key}`);
|
|
1351
|
+
} catch (e) {
|
|
1352
|
+
console.error("Failed:", e.message);
|
|
1353
|
+
process.exit(1);
|
|
1354
|
+
}
|
|
1355
|
+
process.exit(0);
|
|
1356
|
+
});
|
|
1357
|
+
|
|
1358
|
+
program
|
|
1359
|
+
.command("init")
|
|
1360
|
+
.description("Initialize QA-Tools in current project (creates skills, CLAUDE.md section)")
|
|
1361
|
+
.option("-k, --api-key <key>", "Claude API key (from QA-Tools β Members β Claude AI Integration)")
|
|
1362
|
+
.action(async (options) => {
|
|
1363
|
+
const cwd = process.cwd();
|
|
1364
|
+
const apiKey = options.apiKey || getCredentials()?.apiKey || "";
|
|
1365
|
+
|
|
1366
|
+
// 1. Create .claude/skills/ directory
|
|
1367
|
+
const skillsDir = path.join(cwd, ".claude", "skills");
|
|
1368
|
+
fs.mkdirSync(skillsDir, { recursive: true });
|
|
1369
|
+
console.log("Created .claude/skills/");
|
|
1370
|
+
|
|
1371
|
+
// 2. Write skills files
|
|
1372
|
+
const skills = {
|
|
1373
|
+
"qa-loop.md": `---
|
|
1374
|
+
description: Overview of the QA-Tools delivery loop β from requirement to verified release. Read this first.
|
|
1375
|
+
---
|
|
1376
|
+
|
|
1377
|
+
# QA-Tools: The Quality Loop
|
|
1378
|
+
|
|
1379
|
+
This project uses QA-Tools (https://qa-tools.com) for requirement-driven development.
|
|
1380
|
+
|
|
1381
|
+
\`\`\`
|
|
1382
|
+
Requirement β Build β Test β Backlog β Fix β Verify
|
|
1383
|
+
\`\`\`
|
|
1384
|
+
|
|
1385
|
+
## How it works
|
|
1386
|
+
|
|
1387
|
+
1. **Requirement defined** β Acceptance criteria (Given/When/Then) and negative scenarios. Each requirement has a semantic key (e.g. \`auth.login-trial\`).
|
|
1388
|
+
2. **Feature built** β AI or human implements the feature. Traceable to the requirement via requirementKey.
|
|
1389
|
+
3. **Test runs** β Tests are generated from acceptance criteria. Each test links to a requirementKey.
|
|
1390
|
+
4. **Backlog updates automatically** β Failed tests create backlog items (bugs). Each item is traceable to requirement, feature, and test.
|
|
1391
|
+
5. **AI analyzes and fixes** β AI identifies root cause: is it the code or the requirement? Fixes code or suggests requirement adjustment.
|
|
1392
|
+
6. **Retest verifies** β The loop closes only when the requirement is verified.
|
|
1393
|
+
|
|
1394
|
+
## CLI quick reference
|
|
1395
|
+
|
|
1396
|
+
\`\`\`bash
|
|
1397
|
+
# Auth
|
|
1398
|
+
qatools login --api-key <key>
|
|
1399
|
+
|
|
1400
|
+
# App Context & Epics
|
|
1401
|
+
qatools context # View app context (stack, architecture, constraints)
|
|
1402
|
+
qatools context-set -f context.json # Upload app context from JSON
|
|
1403
|
+
qatools epics # List epics
|
|
1404
|
+
qatools epic-create -k auth -t "Authentication" # Create epic
|
|
1405
|
+
|
|
1406
|
+
# Requirements
|
|
1407
|
+
qatools requirements
|
|
1408
|
+
qatools requirements --status approved -o reqs.json
|
|
1409
|
+
qatools requirement-keys
|
|
1410
|
+
qatools requirement-create -f requirements.json
|
|
1411
|
+
|
|
1412
|
+
# Build & backlog
|
|
1413
|
+
qatools backlog
|
|
1414
|
+
qatools backlog-tags # List existing tags (MUST check before tagging)
|
|
1415
|
+
qatools backlog-create -d "..." -t bug --requirement-key <key> --assigned <email> --developer <email> --sprint <id> --tags tag1,tag2
|
|
1416
|
+
qatools backlog-update -i <id> --status resolved --sprint <id> --priority 1 --requirement-key <key> --developer <email> --tags tag1,tag2
|
|
1417
|
+
qatools backlog-duplicates --local # Output for local AI analysis (no API key needed)
|
|
1418
|
+
qatools backlog-duplicates # AI-powered duplicate detection (requires API key)
|
|
1419
|
+
|
|
1420
|
+
# Test
|
|
1421
|
+
qatools templates
|
|
1422
|
+
qatools template-upload -f tests.json
|
|
1423
|
+
qatools pending
|
|
1424
|
+
qatools read-session -s <id> -o session.json
|
|
1425
|
+
|
|
1426
|
+
# Analyze & fix
|
|
1427
|
+
qatools upload-analysis -s <id> -f analysis.json
|
|
1428
|
+
|
|
1429
|
+
# Sprints
|
|
1430
|
+
qatools sprints
|
|
1431
|
+
qatools sprint-items --sprint <id>
|
|
1432
|
+
\`\`\`
|
|
1433
|
+
|
|
1434
|
+
## Key rules
|
|
1435
|
+
- Start with \`qatools context\` to understand the app's tech stack, architecture, and constraints
|
|
1436
|
+
- Epics describe capabilities (what + why + scope). Requirements describe testable behaviors. Break down epics by asking: "What must the system do to fulfill this?"
|
|
1437
|
+
- A requirement MUST be expressible in Given/When/Then. If it can't β it's an epic or too vague
|
|
1438
|
+
- Every requirement MUST link to an epic via epicKey
|
|
1439
|
+
- DEPENDENCY HYGIENE: Before creating or modifying epic dependencies, ALWAYS run \`qatools epics\` to review the full dependency graph. Validate: (1) no circular deps, (2) existing deps still make sense, (3) the graph stays minimal β remove deps that are no longer needed. The model evolves β stale dependencies are worse than missing ones.
|
|
1440
|
+
- Every test, bug, and fix MUST trace back to a requirementKey
|
|
1441
|
+
- Never create duplicate requirements β run \`qatools requirement-keys\` first
|
|
1442
|
+
- The loop runs until every requirement is verified
|
|
1443
|
+
- BACKLOG = SIGNALS ONLY: AVV (deviation from req), GAP (missing req), DEF (technical issue), IMP (idea). Never create requirements as backlog items. Backlog items are observations β requirements are what you build from.
|
|
1444
|
+
- PLANNING FLOW: App Context β Epics β Requirements β Sprint. Backlog feeds back into requirements via GAP/IMP conversion.
|
|
1445
|
+
- Tags: ONLY use existing tags β run \`qatools backlog-tags\` first. New tags can only be created by humans in the UI
|
|
1446
|
+
- Member roles: developer, tester, project_lead, architect, stakeholder. Stakeholders cannot be assigned items. Use \`--developer <email>\` to set the responsible developer (separate from assignee)
|
|
1447
|
+
- Sprint status is automatic: planning/active/overdue/completed based on start and end dates. Only manual actions: complete and reopen
|
|
1448
|
+
- Each project has a \`language\` setting (sv/en). Write ALL comments, analyses, and backlog updates in the project's language. Check \`project.language\` in the response from \`qatools read-session\` or \`qatools backlog\`
|
|
1449
|
+
- EVIDENCE-FIRST: Never speculate. Every analysis, verdict, and duplicate claim must cite specific evidence (code, test output, error messages, requirement text). If unsure, say so.
|
|
1450
|
+
`,
|
|
1451
|
+
|
|
1452
|
+
"qa-requirement-analyst.md": `---
|
|
1453
|
+
description: Break down epics into testable requirements. Use when creating or refining requirements from epics.
|
|
1454
|
+
---
|
|
1455
|
+
|
|
1456
|
+
# QA-Tools: Requirement Analyst
|
|
1457
|
+
|
|
1458
|
+
You break down epics into testable requirements with acceptance criteria.
|
|
1459
|
+
|
|
1460
|
+
## Intent
|
|
1461
|
+
Each epic describes a capability. Your job is to break it down into testable requirements that describe specific behaviors. Ask: "What different things must the system do to fulfill this epic?" β each answer becomes a requirement.
|
|
1462
|
+
|
|
1463
|
+
## Workflow
|
|
1464
|
+
|
|
1465
|
+
### Step 1: Read epics and app context
|
|
1466
|
+
\`qatools context\` β understand the tech stack, architecture, constraints
|
|
1467
|
+
\`qatools epics\` β list all epics with their scope
|
|
1468
|
+
|
|
1469
|
+
### Step 2: Break down one epic at a time
|
|
1470
|
+
For each epic, look at its scope items. Each scope item should become one or more requirements:
|
|
1471
|
+
- key (semantic, prefixed with epic key: \`locksmith.create\`, \`locksmith.permissions\`)
|
|
1472
|
+
- title β what the system must do
|
|
1473
|
+
- description β context and detail
|
|
1474
|
+
- acceptanceCriteria (Given/When/Then β ALWAYS)
|
|
1475
|
+
- negativeCriteria (ALWAYS β what must NOT happen)
|
|
1476
|
+
- epicKey β links to the epic
|
|
1477
|
+
- type, priority, category
|
|
1478
|
+
|
|
1479
|
+
Upload: \`qatools requirement-create -f <epic>-requirements.json\`
|
|
1480
|
+
Wait for approval before next epic.
|
|
1481
|
+
|
|
1482
|
+
### Step 3: Handle feedback
|
|
1483
|
+
\`qatools requirements --status rejected\`
|
|
1484
|
+
Improve rejected requirements and re-upload.
|
|
1485
|
+
|
|
1486
|
+
## Rules
|
|
1487
|
+
- A requirement MUST be expressible in Given/When/Then. If it can't, it's either an epic (too broad) or too vague (needs clarification)
|
|
1488
|
+
- A requirement describes a TESTABLE BEHAVIOR, not a UI element or implementation detail
|
|
1489
|
+
- ALWAYS Given/When/Then format
|
|
1490
|
+
- ALWAYS include negative scenarios
|
|
1491
|
+
- Every requirement MUST link to an epic via epicKey
|
|
1492
|
+
- One epic at a time β get approval before continuing
|
|
1493
|
+
- Run \`qatools requirement-keys\` before creating to avoid duplicates
|
|
1494
|
+
- EVIDENCE-FIRST: base requirements on observable behavior, not assumptions
|
|
1495
|
+
- Before creating new epics: run \`qatools epics\` and review the dependency graph. Remove stale dependencies. Keep the model clean β it evolves over time.
|
|
1496
|
+
`,
|
|
1497
|
+
|
|
1498
|
+
"qa-developer.md": `---
|
|
1499
|
+
description: Implement features based on approved requirements. Use when building new functionality.
|
|
1500
|
+
---
|
|
1501
|
+
|
|
1502
|
+
# QA-Tools: Developer
|
|
1503
|
+
|
|
1504
|
+
You implement code based on approved requirements from QA-Tools.
|
|
1505
|
+
|
|
1506
|
+
## The model
|
|
1507
|
+
|
|
1508
|
+
\\\`\\\`\\\`
|
|
1509
|
+
App Context β Epics β Requirements β Sprint β Implementation β Test
|
|
1510
|
+
β
|
|
1511
|
+
Backlog (signals only)
|
|
1512
|
+
\\\`\\\`\\\`
|
|
1513
|
+
|
|
1514
|
+
- **Requirements** drive what you build. They come from epics.
|
|
1515
|
+
- **Backlog** is NOT your task list. It contains observations: AVV (deviations), GAP (missing reqs), DEF (technical defects), IMP (improvement ideas).
|
|
1516
|
+
- You build from REQUIREMENTS, not backlog items.
|
|
1517
|
+
|
|
1518
|
+
## Workflow
|
|
1519
|
+
|
|
1520
|
+
1. Read app context and current sprint:
|
|
1521
|
+
\`qatools context\`
|
|
1522
|
+
\`qatools requirements --status approved -o requirements.json\`
|
|
1523
|
+
|
|
1524
|
+
2. Implement according to acceptanceCriteria AND negativeCriteria.
|
|
1525
|
+
Every piece of code must be traceable to a requirementKey.
|
|
1526
|
+
|
|
1527
|
+
3. If you discover something unexpected during implementation:
|
|
1528
|
+
- Behavior doesn't match a requirement β create AVV: \`qatools backlog-create -d "..." -t bug --requirement-key <key>\`
|
|
1529
|
+
- Requirement is missing or unclear β create GAP: \`qatools backlog-create -d "..." -t requirement_gap --requirement-key <key>\`
|
|
1530
|
+
- Technical issue (infra, runtime) β create DEF: \`qatools backlog-create -d "..." -t technical_defect\`
|
|
1531
|
+
- Improvement idea β create IMP: \`qatools backlog-create -d "..." -t improvement\`
|
|
1532
|
+
|
|
1533
|
+
4. NEVER create requirements directly as backlog items. If a GAP needs to become a requirement, flag it β the user decides.
|
|
1534
|
+
|
|
1535
|
+
5. Check for pending test sessions:
|
|
1536
|
+
\`qatools pending\`
|
|
1537
|
+
\`qatools read-session -s <id> -o session.json\`
|
|
1538
|
+
|
|
1539
|
+
6. If tests fail, analyze and fix:
|
|
1540
|
+
\`qatools upload-analysis -s <id> -f analysis.json\`
|
|
1541
|
+
|
|
1542
|
+
## Rules
|
|
1543
|
+
- Build from REQUIREMENTS, not backlog
|
|
1544
|
+
- Backlog items are SIGNALS β observations during work, not tasks to implement
|
|
1545
|
+
- Implement EXACTLY what the requirement says β ask if unclear
|
|
1546
|
+
- If the requirement is ambiguous, create a GAP β don't guess
|
|
1547
|
+
- Include commitId when uploading analysis
|
|
1548
|
+
`,
|
|
1549
|
+
|
|
1550
|
+
"qa-test-generator.md": `---
|
|
1551
|
+
description: Generate test protocols from approved requirements. Use after requirements are approved.
|
|
1552
|
+
---
|
|
1553
|
+
|
|
1554
|
+
# QA-Tools: Test Generator
|
|
1555
|
+
|
|
1556
|
+
You generate test protocols (templates) from approved requirements.
|
|
1557
|
+
|
|
1558
|
+
## Workflow
|
|
1559
|
+
|
|
1560
|
+
1. Fetch approved requirements:
|
|
1561
|
+
\`qatools requirements --status approved -o requirements.json\`
|
|
1562
|
+
|
|
1563
|
+
2. Check existing keys:
|
|
1564
|
+
\`qatools requirement-keys\`
|
|
1565
|
+
|
|
1566
|
+
3. Generate test JSON. Each test must have:
|
|
1567
|
+
- id (prefix + section.number, e.g. P-1.1)
|
|
1568
|
+
- requirementKey (links to the requirement)
|
|
1569
|
+
- requirementVersion (e.g. "1.0")
|
|
1570
|
+
- purpose (what this test verifies β locked in session)
|
|
1571
|
+
- name, steps, expected
|
|
1572
|
+
|
|
1573
|
+
4. Upload:
|
|
1574
|
+
\`qatools template-upload -f test-protocol.json\`
|
|
1575
|
+
|
|
1576
|
+
## JSON format
|
|
1577
|
+
\`\`\`json
|
|
1578
|
+
{
|
|
1579
|
+
"name": "Test Protocol v1.0",
|
|
1580
|
+
"sections": [{
|
|
1581
|
+
"title": "1. Authentication",
|
|
1582
|
+
"tests": [{
|
|
1583
|
+
"id": "P-1.1",
|
|
1584
|
+
"requirementKey": "auth.login-trial",
|
|
1585
|
+
"requirementVersion": "1.0",
|
|
1586
|
+
"purpose": "Verifies trial account login per acceptance criteria",
|
|
1587
|
+
"name": "Login with trial account",
|
|
1588
|
+
"steps": "1. Go to /login\n2. Click Try free\n3. Enter test@example.com",
|
|
1589
|
+
"expected": "User logged in, redirected to dashboard"
|
|
1590
|
+
}]
|
|
1591
|
+
}]
|
|
1592
|
+
}
|
|
1593
|
+
\`\`\`
|
|
1594
|
+
|
|
1595
|
+
## Rules
|
|
1596
|
+
- Every test MUST have requirementKey, requirementVersion, and purpose
|
|
1597
|
+
- Generate from BOTH acceptance criteria AND negative scenarios
|
|
1598
|
+
`,
|
|
1599
|
+
|
|
1600
|
+
"qa-test-analyst.md": `---
|
|
1601
|
+
description: Analyze failed test sessions and fix bugs. Use when test sessions are pending analysis.
|
|
1602
|
+
---
|
|
1603
|
+
|
|
1604
|
+
# QA-Tools: Test Analyst
|
|
1605
|
+
|
|
1606
|
+
You analyze test sessions and fix bugs found during testing.
|
|
1607
|
+
|
|
1608
|
+
## Workflow
|
|
1609
|
+
|
|
1610
|
+
1. Check for pending sessions:
|
|
1611
|
+
\`qatools pending\`
|
|
1612
|
+
|
|
1613
|
+
2. Read session details:
|
|
1614
|
+
\`qatools read-session -s <id> -o session.json\`
|
|
1615
|
+
|
|
1616
|
+
3. For each FAILED test:
|
|
1617
|
+
- Analyze root cause: is it the code or the requirement?
|
|
1618
|
+
- Determine verdict: bugg, troligen-db, testprotokoll, design, ux, ej-aktuell, krΓ€ver-genomgΓ₯ng
|
|
1619
|
+
- Fix the bug if possible (fixStatus: "fixed", list filesChanged)
|
|
1620
|
+
- If the requirement is wrong, set verdict to "testprotokoll"
|
|
1621
|
+
|
|
1622
|
+
4. Upload analysis:
|
|
1623
|
+
\`qatools upload-analysis -s <id> -f analysis.json\`
|
|
1624
|
+
|
|
1625
|
+
5. Update backlog items:
|
|
1626
|
+
\`qatools backlog-update -f updates.json\`
|
|
1627
|
+
|
|
1628
|
+
## Analysis JSON format
|
|
1629
|
+
\`\`\`json
|
|
1630
|
+
{
|
|
1631
|
+
"sessionId": "<id>",
|
|
1632
|
+
"results": {
|
|
1633
|
+
"P-1.6": {
|
|
1634
|
+
"verdict": "bugg",
|
|
1635
|
+
"comment": "Race condition β auth0Id never set",
|
|
1636
|
+
"fixStatus": "fixed",
|
|
1637
|
+
"commitId": "a1b2c3d",
|
|
1638
|
+
"filesChanged": ["AuthService.cs", "OpenController.cs"]
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
\`\`\`
|
|
1643
|
+
|
|
1644
|
+
## Rules
|
|
1645
|
+
- EVIDENCE-FIRST: Base every verdict and comment on observable evidence. Cite specific code, test steps, or error messages. Never speculate.
|
|
1646
|
+
- Always identify whether the problem is in the code or the requirement
|
|
1647
|
+
- If you fix code, list all changed files and include the commitId (short hash from \`git rev-parse --short HEAD\`)
|
|
1648
|
+
- When creating backlog items from test failures, use the correct type:
|
|
1649
|
+
- Code violates requirement β AVV (bug): \`qatools backlog-create -t bug -d "..." --requirement-key <key>\`
|
|
1650
|
+
- Requirement missing/unclear β GAP: \`qatools backlog-create -t requirement_gap -d "..." --requirement-key <key>\`
|
|
1651
|
+
- Technical/runtime issue β DEF: \`qatools backlog-create -t technical_defect -d "..."\`
|
|
1652
|
+
- NEVER create requirements as backlog items β backlog is for signals only
|
|
1653
|
+
`,
|
|
1654
|
+
|
|
1655
|
+
"qa-release-manager.md": `---
|
|
1656
|
+
description: Manage sprints, backlog, and release readiness. Use for sprint planning and release verification.
|
|
1657
|
+
---
|
|
1658
|
+
|
|
1659
|
+
# QA-Tools: Release Manager
|
|
1660
|
+
|
|
1661
|
+
You manage sprints, backlog, and verify release readiness.
|
|
1662
|
+
|
|
1663
|
+
## Workflow
|
|
1664
|
+
|
|
1665
|
+
1. Review sprints:
|
|
1666
|
+
\`qatools sprints\`
|
|
1667
|
+
\`qatools sprint-items --sprint <id> -o sprint.json\`
|
|
1668
|
+
|
|
1669
|
+
2. Break down requirements into stories/tasks:
|
|
1670
|
+
\`qatools backlog-create -d "Implement login flow" -t story --requirement-key auth.login-trial\`
|
|
1671
|
+
|
|
1672
|
+
3. Track progress:
|
|
1673
|
+
\`qatools backlog --sprint <id>\`
|
|
1674
|
+
\`qatools backlog-update -i <id> --status resolved\`
|
|
1675
|
+
|
|
1676
|
+
4. Verify requirement coverage:
|
|
1677
|
+
\`qatools requirements --status approved\`
|
|
1678
|
+
Ensure every approved requirement has tests and all tests pass.
|
|
1679
|
+
|
|
1680
|
+
5. Release when every requirement in the sprint is verified.
|
|
1681
|
+
|
|
1682
|
+
## Rules
|
|
1683
|
+
- A sprint is ready ONLY when all requirements are verified
|
|
1684
|
+
- Every backlog item must link to a requirementKey
|
|
1685
|
+
- Track: requirement β test β bug β fix β retest β verified
|
|
1686
|
+
`,
|
|
1687
|
+
"qa-evidence-first.md": `---
|
|
1688
|
+
description: Core principle β never speculate, always cite evidence. Applies to ALL work.
|
|
1689
|
+
---
|
|
1690
|
+
|
|
1691
|
+
# Evidence-First Principle
|
|
1692
|
+
|
|
1693
|
+
This is a non-negotiable rule for all AI work in this project.
|
|
1694
|
+
|
|
1695
|
+
## The rule
|
|
1696
|
+
Every claim, verdict, analysis, and recommendation MUST cite specific evidence:
|
|
1697
|
+
- Code references (file, function, line)
|
|
1698
|
+
- Test output (actual vs expected)
|
|
1699
|
+
- Error messages (exact text)
|
|
1700
|
+
- Requirement text (quote the GWT)
|
|
1701
|
+
|
|
1702
|
+
## What this means in practice
|
|
1703
|
+
|
|
1704
|
+
### When analyzing test failures
|
|
1705
|
+
β "This is probably a timing issue"
|
|
1706
|
+
β
"AuthService.cs:42 sets auth0Id after the response is sent (line 45), causing a race condition. Test P-1.6 expects auth0Id to be set before redirect."
|
|
1707
|
+
|
|
1708
|
+
### When creating backlog items
|
|
1709
|
+
β "Login seems broken"
|
|
1710
|
+
β
"Given a user with valid credentials, When clicking login, Then the response is 500. Error: 'NullReferenceException at AuthController.cs:78'. Requirement auth.login expects redirect to dashboard."
|
|
1711
|
+
|
|
1712
|
+
### When suggesting fixes
|
|
1713
|
+
β "Try adding a null check"
|
|
1714
|
+
β
"auth0Id is null because createUserResponse (line 42) is awaited but its result is not assigned. Fix: \`var user = await createUserResponse;\` then \`auth0Id = user.Id;\`"
|
|
1715
|
+
|
|
1716
|
+
### When flagging requirement issues
|
|
1717
|
+
β "This requirement is unclear"
|
|
1718
|
+
β
"Requirement auth.login says 'user is redirected' but doesn't specify WHERE. The test expects /dashboard but the code redirects to /home."
|
|
1719
|
+
|
|
1720
|
+
## If you're unsure
|
|
1721
|
+
Say "I'm unsure" β that's better than guessing. Suggest what evidence would resolve the uncertainty.
|
|
1722
|
+
`
|
|
1723
|
+
};
|
|
1724
|
+
|
|
1725
|
+
for (const [filename, content] of Object.entries(skills)) {
|
|
1726
|
+
fs.writeFileSync(path.join(skillsDir, filename), content);
|
|
1727
|
+
}
|
|
1728
|
+
console.log(" β qa-loop.md (start here)");
|
|
1729
|
+
console.log(" β qa-requirement-analyst.md");
|
|
1730
|
+
console.log(" β qa-developer.md");
|
|
1731
|
+
console.log(" β qa-test-generator.md");
|
|
1732
|
+
console.log(" β qa-test-analyst.md");
|
|
1733
|
+
console.log(" β qa-release-manager.md");
|
|
1734
|
+
console.log(" β qa-evidence-first.md");
|
|
1735
|
+
|
|
1736
|
+
// 4. Generate app context skill from API
|
|
1737
|
+
try {
|
|
1738
|
+
await authenticate(options);
|
|
1739
|
+
const creds = getCredentials();
|
|
1740
|
+
if (creds?.projectId) {
|
|
1741
|
+
const getCtx = httpsCallable(functions, "getAppContext");
|
|
1742
|
+
const ctxResult = await getCtx({ projectId: creds.projectId });
|
|
1743
|
+
const ctx = ctxResult.data.appContext;
|
|
1744
|
+
if (ctx) {
|
|
1745
|
+
let ctxSkill = `---\ndescription: App-specific context for ${ctx.name}. Auto-generated from QA-Tools App Context.\n---\n\n# ${ctx.name}\n\n`;
|
|
1746
|
+
if (ctx.purpose) ctxSkill += `## Purpose\n${ctx.purpose}\n\n`;
|
|
1747
|
+
if (ctx.domainObjects?.length) ctxSkill += `## Domain Objects\n${ctx.domainObjects.map(d => '- ' + d).join('\n')}\n\n`;
|
|
1748
|
+
if (ctx.actors?.length) ctxSkill += `## Actors\n${ctx.actors.map(a => '- ' + a).join('\n')}\n\n`;
|
|
1749
|
+
if (ctx.stack?.length) ctxSkill += `## Tech Stack\n${ctx.stack.map(s => '- **' + s.layer + ':** ' + s.technology + (s.version ? ' ' + s.version : '') + (s.notes ? ' β ' + s.notes : '')).join('\n')}\n\n`;
|
|
1750
|
+
if (ctx.architecture) ctxSkill += `## Architecture\n${ctx.architecture}\n\n`;
|
|
1751
|
+
if (ctx.security?.length) ctxSkill += `## Security\n${ctx.security.map(s => '- ' + s).join('\n')}\n\n`;
|
|
1752
|
+
if (ctx.aiRules?.length) ctxSkill += `## AI Rules\n${ctx.aiRules.map(r => '- ' + r).join('\n')}\n\n`;
|
|
1753
|
+
if (ctx.conventions?.length) ctxSkill += `## Design Principles\n${ctx.conventions.map(c => '- **' + c.area + ':** ' + c.rule).join('\n')}\n\n`;
|
|
1754
|
+
if (ctx.constraints?.length) ctxSkill += `## Constraints\n${ctx.constraints.map(c => '- ' + c).join('\n')}\n\n`;
|
|
1755
|
+
fs.writeFileSync(path.join(skillsDir, "app-context.md"), ctxSkill);
|
|
1756
|
+
console.log(" β app-context.md (from project)");
|
|
1757
|
+
|
|
1758
|
+
// Generate custom skills from App Context
|
|
1759
|
+
if (ctx.skills?.length) {
|
|
1760
|
+
for (const skill of ctx.skills) {
|
|
1761
|
+
const fname = skill.filename.endsWith('.md') ? skill.filename : skill.filename + '.md';
|
|
1762
|
+
const content = `---\ndescription: ${skill.title}\n---\n\n# ${skill.title}\n\n${skill.content}\n`;
|
|
1763
|
+
fs.writeFileSync(path.join(skillsDir, fname), content);
|
|
1764
|
+
console.log(" β " + fname + " (custom)");
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
} catch (e) {
|
|
1770
|
+
// No auth or no context β skip, not critical
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
// 3. Update or create CLAUDE.md
|
|
1774
|
+
const claudeMdPath = path.join(cwd, "CLAUDE.md");
|
|
1775
|
+
const qaSection = `
|
|
1776
|
+
## QA-Tools Integration
|
|
1777
|
+
|
|
1778
|
+
\`\`\`bash
|
|
1779
|
+
export QATOOLS_API_KEY=${apiKey || "<din-nyckel>"}
|
|
1780
|
+
\`\`\`
|
|
1781
|
+
|
|
1782
|
+
### Kommandon
|
|
1783
|
+
- \`qatools requirements\` β lista krav
|
|
1784
|
+
- \`qatools backlog\` β lista backlog
|
|
1785
|
+
- \`qatools pending\` β vΓ€ntande testsessioner
|
|
1786
|
+
- \`qatools read-session -s <id>\` β lΓ€s session
|
|
1787
|
+
- \`qatools upload-analysis -s <id> -f analysis.json\` β ladda upp analys
|
|
1788
|
+
- \`qatools requirement-create -f file.json\` β skapa krav
|
|
1789
|
+
- \`qatools template-upload -f file.json\` β ladda upp testprotokoll
|
|
1790
|
+
- \`qatools backlog-create -d "..." -t task\` β skapa Γ€rende
|
|
1791
|
+
- \`qatools backlog-update -i <id> --status resolved\` β uppdatera
|
|
1792
|
+
|
|
1793
|
+
### Roller (skills)
|
|
1794
|
+
Se \`.claude/skills/\` fΓΆr instruktioner per roll.
|
|
1795
|
+
`;
|
|
1796
|
+
|
|
1797
|
+
if (fs.existsSync(claudeMdPath)) {
|
|
1798
|
+
const existing = fs.readFileSync(claudeMdPath, "utf8");
|
|
1799
|
+
if (!existing.includes("QA-Tools Integration")) {
|
|
1800
|
+
fs.appendFileSync(claudeMdPath, "\n" + qaSection);
|
|
1801
|
+
console.log("\nβ Added QA-Tools section to CLAUDE.md");
|
|
1802
|
+
} else {
|
|
1803
|
+
console.log("\nβ CLAUDE.md already has QA-Tools section (skipped)");
|
|
1804
|
+
}
|
|
1805
|
+
} else {
|
|
1806
|
+
fs.writeFileSync(claudeMdPath, "# Project\n" + qaSection);
|
|
1807
|
+
console.log("\nβ Created CLAUDE.md with QA-Tools section");
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
console.log("\nπ QA-Tools initialized! Claude is ready.");
|
|
1811
|
+
if (!apiKey) console.log("\nβ No API key set. Add it to CLAUDE.md: export QATOOLS_API_KEY=<key>");
|
|
1812
|
+
process.exit(0);
|
|
1813
|
+
});
|
|
1814
|
+
|
|
507
1815
|
program
|
|
508
1816
|
.command("login")
|
|
509
1817
|
.description("Save credentials to ~/.qatools (email/password or --google)")
|
|
510
1818
|
.option("-e, --email <email>", "Login email")
|
|
511
1819
|
.option("-p, --password <password>", "Login password")
|
|
512
1820
|
.option("-g, --google", "Login with Google (opens browser)")
|
|
1821
|
+
.option("-k, --api-key <key>", "Login with Claude API key (permanent, no browser)")
|
|
513
1822
|
.action(async (options) => {
|
|
514
1823
|
const configPath = path.join(process.env.HOME || process.env.USERPROFILE, ".qatools");
|
|
515
1824
|
|
|
1825
|
+
if (options.apiKey) {
|
|
1826
|
+
// API key login β no browser, no expiry
|
|
1827
|
+
console.log("Authenticating with API key...");
|
|
1828
|
+
const authRes = await fetch("https://us-central1-qatools-app.cloudfunctions.net/authenticateWithApiKey", {
|
|
1829
|
+
method: "POST", headers: { "Content-Type": "application/json" },
|
|
1830
|
+
body: JSON.stringify({ data: { apiKey: options.apiKey } }),
|
|
1831
|
+
});
|
|
1832
|
+
const authData = await authRes.json();
|
|
1833
|
+
if (authData.error || !authData.result) {
|
|
1834
|
+
console.error("Invalid API key.");
|
|
1835
|
+
process.exit(1);
|
|
1836
|
+
}
|
|
1837
|
+
const { customToken, projectId, projectName, email } = authData.result;
|
|
1838
|
+
// Save config
|
|
1839
|
+
const config = { apiKey: options.apiKey, projectId, projectName, email };
|
|
1840
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
1841
|
+
console.log(`Logged in as Claude for project: ${projectName}`);
|
|
1842
|
+
console.log(`Project ID: ${projectId}`);
|
|
1843
|
+
process.exit(0);
|
|
1844
|
+
}
|
|
1845
|
+
|
|
516
1846
|
if (options.google) {
|
|
517
1847
|
// Google login via browser
|
|
518
1848
|
console.log("Opening browser for Google sign-in...\n");
|