qatools-cli 1.1.5 → 1.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/qatools.js +117 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qatools-cli",
3
- "version": "1.1.5",
3
+ "version": "1.1.6",
4
4
  "description": "CLI for QA Tools — requirements, epics, sprints, sessions, App Context sync, project agenda. The companion to qa-tools.com.",
5
5
  "bin": {
6
6
  "qatools": "./qatools.js"
package/qatools.js CHANGED
@@ -684,6 +684,7 @@ program
684
684
  .option("--project <id>", "Target project ID (overrides default)")
685
685
  .option("-o, --output <path>", "Path to file to update (auto: .claude/CLAUDE.md if it exists, else CLAUDE.md)")
686
686
  .option("--check", "Exit 1 if file is out of sync (does not modify)")
687
+ .option("--dry-run", "Show what would change without writing the file")
687
688
  .option("-e, --email <email>", "Login email")
688
689
  .option("-p, --password <password>", "Login password")
689
690
  .action(async (options) => {
@@ -739,6 +740,49 @@ program
739
740
  }
740
741
  }
741
742
 
743
+ if (options.dryRun) {
744
+ if (existing === updated) {
745
+ console.log(`✓ ${options.output} already in sync — nothing would change.`);
746
+ process.exit(0);
747
+ }
748
+ const beginIdxNow = existing.indexOf(beginMarker);
749
+ if (beginIdxNow < 0) {
750
+ console.log(`Would CREATE qatools-context block in ${options.output}:`);
751
+ console.log(` + ${md.split('\n').length} lines (${md.length} chars)`);
752
+ console.log(` Block markers: ${beginMarker} ... ${endMarker}`);
753
+ console.log(``);
754
+ console.log(`First 10 lines of new content:`);
755
+ md.split('\n').slice(0, 10).forEach(l => console.log(` + ${l}`));
756
+ process.exit(0);
757
+ }
758
+ // Compute a simple line-level diff between old block and new block
759
+ const oldEnd = existing.indexOf(endMarker);
760
+ const oldBlockLines = existing.slice(beginIdxNow, oldEnd + endMarker.length).split('\n');
761
+ const newBlockLines = block.split('\n');
762
+ console.log(`Would UPDATE qatools-context block in ${options.output}:`);
763
+ console.log(` Old: ${oldBlockLines.length} lines`);
764
+ console.log(` New: ${newBlockLines.length} lines`);
765
+ console.log(``);
766
+ // Show added/removed lines (simple set diff — ignores reordering)
767
+ const oldSet = new Set(oldBlockLines.map(l => l.trim()).filter(Boolean));
768
+ const newSet = new Set(newBlockLines.map(l => l.trim()).filter(Boolean));
769
+ const added = [...newSet].filter(l => !oldSet.has(l));
770
+ const removed = [...oldSet].filter(l => !newSet.has(l));
771
+ if (removed.length) {
772
+ console.log(` ${removed.length} line(s) removed:`);
773
+ removed.slice(0, 15).forEach(l => console.log(` - ${l.substring(0, 100)}`));
774
+ if (removed.length > 15) console.log(` ... and ${removed.length - 15} more`);
775
+ }
776
+ if (added.length) {
777
+ console.log(` ${added.length} line(s) added:`);
778
+ added.slice(0, 15).forEach(l => console.log(` + ${l.substring(0, 100)}`));
779
+ if (added.length > 15) console.log(` ... and ${added.length - 15} more`);
780
+ }
781
+ console.log(``);
782
+ console.log(`Run without --dry-run to write the changes.`);
783
+ process.exit(0);
784
+ }
785
+
742
786
  if (existing === updated) {
743
787
  console.log(`✓ ${options.output} already in sync (no changes)`);
744
788
  } else {
@@ -1359,9 +1403,74 @@ program
1359
1403
  process.exit(0);
1360
1404
  });
1361
1405
 
1406
+ program
1407
+ .command("context-template")
1408
+ .description("Print an empty App Context JSON template with all fields and example values. Pipe to a file, edit, then context-set.")
1409
+ .option("-o, --output <path>", "Write to file instead of stdout")
1410
+ .action((options) => {
1411
+ const template = {
1412
+ name: "Your project name",
1413
+ purpose: "One sentence: what does this app do and why does it exist?",
1414
+ domainObjects: [
1415
+ "User",
1416
+ "Order",
1417
+ "Product"
1418
+ ],
1419
+ actors: [
1420
+ "End user",
1421
+ "Admin",
1422
+ "External system"
1423
+ ],
1424
+ stack: [
1425
+ { layer: "Frontend", technology: "React", version: "18", notes: "with TypeScript" },
1426
+ { layer: "Backend", technology: "Node.js", version: "20", notes: "Express" },
1427
+ { layer: "Database", technology: "PostgreSQL", version: "16", notes: "" }
1428
+ ],
1429
+ architecture: "Short description of how the system is structured. Where does data flow? What's the main pattern?",
1430
+ security: [
1431
+ "All endpoints require authentication",
1432
+ "Data is isolated per tenant via tenantId column"
1433
+ ],
1434
+ aiRules: [
1435
+ "Build from requirements — never from backlog items",
1436
+ "Evidence-first: cite specific code, test output, or error message",
1437
+ "Never guess — say 'I'm unsure' if evidence is missing",
1438
+ "Acceptance criteria in Given/When/Then on the project's language (sv: Givet/När/Då, en: Given/When/Then) — never mix"
1439
+ ],
1440
+ constraints: [
1441
+ "Hard constraint #1 (e.g. must run offline)",
1442
+ "Hard constraint #2 (e.g. no third-party AI calls)"
1443
+ ],
1444
+ conventions: [
1445
+ { area: "Naming", rule: "camelCase for variables, PascalCase for components" },
1446
+ { area: "Errors", rule: "Always throw with a Result type, never silent fail" }
1447
+ ],
1448
+ environments: [
1449
+ { name: "Production", url: "https://app.example.com", notes: "" },
1450
+ { name: "Staging", url: "https://staging.example.com", notes: "" }
1451
+ ],
1452
+ integrations: [
1453
+ { name: "Stripe", purpose: "Payments" },
1454
+ { name: "SendGrid", purpose: "Transactional email" }
1455
+ ],
1456
+ sections: [
1457
+ { title: "Custom section title", content: "Free-text content for anything that doesn't fit the structured fields above. Use this for project-specific docs, runbooks, glossaries, etc." }
1458
+ ]
1459
+ };
1460
+ const json = JSON.stringify(template, null, 2);
1461
+ if (options.output) {
1462
+ fs.writeFileSync(path.resolve(options.output), json);
1463
+ console.error(`Template written to ${options.output}`);
1464
+ console.error(`Edit it, then upload with: qatools context-set -f ${options.output}`);
1465
+ } else {
1466
+ console.log(json);
1467
+ }
1468
+ process.exit(0);
1469
+ });
1470
+
1362
1471
  program
1363
1472
  .command("context-set")
1364
- .description("Upload App Context from JSON file")
1473
+ .description("Upload App Context from JSON file. Run 'qatools context-template' to see the expected schema.")
1365
1474
  .requiredOption("-f, --file <path>", "Path to App Context JSON file")
1366
1475
  .option("--project <id>", "Target project ID (overrides default; requires access to that project)")
1367
1476
  .option("-e, --email <email>", "Login email")
@@ -1375,6 +1484,13 @@ program
1375
1484
  const saveCtx = httpsCallable(functions, "saveAppContext");
1376
1485
  const result = await saveCtx({ projectId, context: data });
1377
1486
  console.log(`App Context saved (${result.data.id}) — project ${projectId}`);
1487
+ if (result.data.warnings && result.data.warnings.length > 0) {
1488
+ console.error(``);
1489
+ console.error(`⚠ Schema warnings (${result.data.warnings.length}):`);
1490
+ for (const w of result.data.warnings) console.error(` ${w}`);
1491
+ console.error(``);
1492
+ console.error(` These fields were saved but may be ignored. Run 'qatools context-template' to see the schema.`);
1493
+ }
1378
1494
  process.exit(0);
1379
1495
  });
1380
1496