qatools-cli 1.1.3 → 1.1.4

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 +118 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qatools-cli",
3
- "version": "1.1.3",
3
+ "version": "1.1.4",
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
@@ -903,6 +903,14 @@ program
903
903
  next: `qatools requirement-update -i <id> --status approved (or refine AC first)`,
904
904
  });
905
905
  }
906
+ if (data.agenda.accessModelMissing) {
907
+ sections.push({
908
+ icon: "⚪",
909
+ title: "No access model defined for this project",
910
+ items: [" • Who can sign in? Which roles exist? What's protected? How is data isolated?"],
911
+ next: `qatools access show (shows a minimal example) → qatools access set -f model.json`,
912
+ });
913
+ }
906
914
 
907
915
  if (sections.length === 0) {
908
916
  console.log(" ✓ Nothing on the agenda. The project is in a clean state.\n");
@@ -1370,6 +1378,116 @@ program
1370
1378
  process.exit(0);
1371
1379
  });
1372
1380
 
1381
+ // access subcommands grouped under 'access'
1382
+ const accessCmd = program
1383
+ .command("access")
1384
+ .description("Manage the project's Access Model (who-can-do-what, structured)");
1385
+
1386
+ accessCmd
1387
+ .command("show")
1388
+ .description("Show the access model for the project")
1389
+ .option("--project <id>", "Target project ID (overrides default)")
1390
+ .option("-o, --output <path>", "Save to JSON file")
1391
+ .option("--json", "Output JSON to stdout")
1392
+ .option("-e, --email <email>", "Login email")
1393
+ .option("-p, --password <password>", "Login password")
1394
+ .action(async (options) => {
1395
+ await authenticate(options);
1396
+ const creds = getCredentials();
1397
+ const projectId = options.project || creds?.projectId;
1398
+ if (!projectId) { console.error("No project configured. Run: qatools setup, or pass --project <id>"); process.exit(1); }
1399
+ const getModel = httpsCallable(functions, "getAccessModel");
1400
+ let result;
1401
+ try { result = await getModel({ projectId }); }
1402
+ catch (e) { console.error("Failed:", e.message); process.exit(1); }
1403
+ const model = result.data.accessModel;
1404
+ if (!model) {
1405
+ console.log(`No access model defined for this project.`);
1406
+ console.log(`Define one by writing a JSON file and running: qatools access set -f model.json`);
1407
+ console.log(``);
1408
+ console.log(`Minimal shape:`);
1409
+ console.log(JSON.stringify({
1410
+ authMethods: [{ key: "google", description: "Google OAuth", primary: true }],
1411
+ roles: [{ key: "owner", description: "Project owner with full control" }],
1412
+ resources: [{ key: "project", description: "Project metadata", ownedBy: "project", permissions: [{ role: "owner", actions: ["read","create","update","delete"] }] }],
1413
+ isolationRules: [{ scope: "project", description: "Data scoped by projectId field", enforcedBy: ["firestore.rules", "checkProjectAccess()"] }],
1414
+ }, null, 2));
1415
+ process.exit(0);
1416
+ }
1417
+ if (options.output) {
1418
+ fs.writeFileSync(path.resolve(options.output), JSON.stringify(model, null, 2));
1419
+ console.log(`Saved to ${options.output}`);
1420
+ process.exit(0);
1421
+ }
1422
+ if (options.json) {
1423
+ console.log(JSON.stringify(model, null, 2));
1424
+ process.exit(0);
1425
+ }
1426
+ // Pretty text output
1427
+ console.log(``);
1428
+ console.log(`Access Model — project ${projectId}`);
1429
+ console.log(``);
1430
+ if (model.authMethods?.length) {
1431
+ console.log(`AUTH METHODS`);
1432
+ for (const a of model.authMethods) console.log(` ${a.primary ? "*" : " "} ${a.key.padEnd(15)} ${a.description}`);
1433
+ console.log(``);
1434
+ }
1435
+ if (model.roles?.length) {
1436
+ console.log(`ROLES`);
1437
+ for (const r of model.roles) {
1438
+ const inh = r.inherits?.length ? ` (inherits: ${r.inherits.join(", ")})` : "";
1439
+ console.log(` ${r.key.padEnd(15)} ${r.description}${inh}`);
1440
+ }
1441
+ console.log(``);
1442
+ }
1443
+ if (model.resources?.length) {
1444
+ console.log(`RESOURCES`);
1445
+ for (const r of model.resources) {
1446
+ const coll = r.collection ? ` [${r.collection}]` : "";
1447
+ console.log(` ${r.key}${coll} — owned by ${r.ownedBy}`);
1448
+ console.log(` ${r.description}`);
1449
+ for (const p of (r.permissions || [])) {
1450
+ const cond = p.condition ? ` (${p.condition})` : "";
1451
+ console.log(` ${p.role.padEnd(15)} ${p.actions.join(",")}${cond}`);
1452
+ }
1453
+ }
1454
+ console.log(``);
1455
+ }
1456
+ if (model.isolationRules?.length) {
1457
+ console.log(`ISOLATION RULES`);
1458
+ for (const i of model.isolationRules) {
1459
+ console.log(` [${i.scope}] ${i.description}`);
1460
+ if (i.enforcedBy?.length) console.log(` enforced by: ${i.enforcedBy.join(", ")}`);
1461
+ }
1462
+ console.log(``);
1463
+ }
1464
+ process.exit(0);
1465
+ });
1466
+
1467
+ accessCmd
1468
+ .command("set")
1469
+ .description("Upload (replace) the entire access model from a JSON file")
1470
+ .requiredOption("-f, --file <path>", "Path to access model JSON")
1471
+ .option("--project <id>", "Target project ID (overrides default)")
1472
+ .option("-e, --email <email>", "Login email")
1473
+ .option("-p, --password <password>", "Login password")
1474
+ .action(async (options) => {
1475
+ await authenticate(options);
1476
+ const creds = getCredentials();
1477
+ const projectId = options.project || creds?.projectId;
1478
+ if (!projectId) { console.error("No project configured. Run: qatools setup, or pass --project <id>"); process.exit(1); }
1479
+ const model = JSON.parse(fs.readFileSync(path.resolve(options.file), "utf8"));
1480
+ const saveModel = httpsCallable(functions, "saveAccessModel");
1481
+ try {
1482
+ const result = await saveModel({ projectId, model });
1483
+ console.log(`Access model saved (${result.data.id}) — project ${projectId}`);
1484
+ } catch (e) {
1485
+ console.error("Failed:", e.message);
1486
+ process.exit(1);
1487
+ }
1488
+ process.exit(0);
1489
+ });
1490
+
1373
1491
  program
1374
1492
  .command("epics")
1375
1493
  .description("List epics for the project")