@formigio/fazemos-cli 0.10.49 → 0.10.51

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/dist/index.js CHANGED
@@ -1294,6 +1294,72 @@ function readScopeOpts(opts) {
1294
1294
  return { projectSlug: opts.project };
1295
1295
  return {};
1296
1296
  }
1297
+ /**
1298
+ * P-TOOL-1 — render a rematerialize reconciliation plan (preview or applied).
1299
+ *
1300
+ * The plan has five step-disposition buckets:
1301
+ * preserved completed/skipped steps kept frozen
1302
+ * rematerialized pending/blocked/failed steps re-derived from the new def
1303
+ * inserted brand-new template steps
1304
+ * removed step_instances absent from the new def
1305
+ * reset completed downstream steps that --reset-downstream-of-new
1306
+ * will RE-RUN (empty unless the flag is set) — the
1307
+ * preview/apply-fidelity bucket, surfaced under "will RE-RUN".
1308
+ *
1309
+ * Each StepDisposition is { step_instance_id, template_step_id, step_name,
1310
+ * action, from_status, to_status }. Warnings render yellow; blockers render red.
1311
+ */
1312
+ function renderRematerializePlan(plan, warnings, blockers, render) {
1313
+ // process.stderr for the blocked-path so the plan travels with the error;
1314
+ // process.stdout otherwise. Commander's console.log/console.error split.
1315
+ const out = render.blockedHeader ? console.error : console.log;
1316
+ const p = plan ?? {};
1317
+ const buckets = [
1318
+ ['preserved', Array.isArray(p.preserved) ? p.preserved : []],
1319
+ ['rematerialized', Array.isArray(p.rematerialized) ? p.rematerialized : []],
1320
+ ['inserted', Array.isArray(p.inserted) ? p.inserted : []],
1321
+ ['removed', Array.isArray(p.removed) ? p.removed : []],
1322
+ ['reset', Array.isArray(p.reset) ? p.reset : []],
1323
+ ];
1324
+ // Count summary line.
1325
+ const counts = buckets.map(([name, rows]) => `${name}: ${rows.length}`).join(' | ');
1326
+ out(chalk.gray(` ${counts}`));
1327
+ // Per-step table, grouped by disposition. reset[] gets a distinct
1328
+ // "will RE-RUN" heading so an operator sees what apply will actually do.
1329
+ const rowLine = (d) => {
1330
+ const name = d?.step_name ?? d?.template_step_id ?? '(unknown)';
1331
+ const from = d?.from_status ?? '—';
1332
+ const to = d?.to_status ?? '—';
1333
+ return from === to ? ` ${name} [${to}]` : ` ${name} ${from} → ${to}`;
1334
+ };
1335
+ for (const [name, rows] of buckets) {
1336
+ if (rows.length === 0)
1337
+ continue;
1338
+ if (name === 'reset') {
1339
+ out(chalk.magenta(` will RE-RUN (reset ${rows.length}):`));
1340
+ }
1341
+ else {
1342
+ out(chalk.cyan(` ${name} (${rows.length}):`));
1343
+ }
1344
+ for (const d of rows)
1345
+ out(rowLine(d));
1346
+ }
1347
+ const warns = Array.isArray(warnings) ? warnings : [];
1348
+ if (warns.length) {
1349
+ out(chalk.yellow(` warnings (${warns.length}):`));
1350
+ for (const w of warns) {
1351
+ out(chalk.yellow(` - ${w?.code ?? '?'}${w?.template_step_id ? ` [${w.template_step_id}]` : ''}: ${w?.detail ?? ''}`));
1352
+ }
1353
+ }
1354
+ const blocks = Array.isArray(blockers) ? blockers : [];
1355
+ if (blocks.length) {
1356
+ out(chalk.red(` blockers (${blocks.length}):`));
1357
+ for (const b of blocks) {
1358
+ const offending = Array.isArray(b?.offending) && b.offending.length ? ` (${b.offending.join(', ')})` : '';
1359
+ out(chalk.red(` - ${b?.code ?? '?'}: ${b?.detail ?? ''}${offending}`));
1360
+ }
1361
+ }
1362
+ }
1297
1363
  /**
1298
1364
  * Uniform error handler for scoped commands. When the API emits
1299
1365
  * MISSING_PROJECT_CONTEXT (§7.3.1), the api helper has already re-shaped
@@ -2391,7 +2457,7 @@ ws
2391
2457
  });
2392
2458
  ws
2393
2459
  .command('archive')
2394
- .description('Archive a worksheet. Archived worksheets are hidden from "ws list" by default but can be found with "ws list -s archived". This is not reversible from the CLI.')
2460
+ .description('Archive a worksheet. Archived worksheets are hidden from "ws list" by default but can be found with "ws list -s archived". Reversible with "ws reopen <id>".')
2395
2461
  .argument('<id>', 'Worksheet ID')
2396
2462
  .action(async (id) => {
2397
2463
  try {
@@ -2403,6 +2469,38 @@ ws
2403
2469
  process.exit(1);
2404
2470
  }
2405
2471
  });
2472
+ // W-TOOL-4: first-class active → completed transition (owner-only). Completed
2473
+ // worksheets leave the default active list; find them with "ws list -s completed".
2474
+ ws
2475
+ .command('complete')
2476
+ .description('Mark a worksheet completed (active → completed). Owner-only. Completed worksheets are hidden from "ws list" by default but can be found with "ws list -s completed". Reversible with "ws reopen <id>".')
2477
+ .argument('<id>', 'Worksheet ID')
2478
+ .action(async (id) => {
2479
+ try {
2480
+ await api('POST', `/api/worksheets/${id}/complete`);
2481
+ console.log(chalk.green('Worksheet completed'));
2482
+ }
2483
+ catch (err) {
2484
+ console.error(chalk.red(err.message));
2485
+ process.exit(1);
2486
+ }
2487
+ });
2488
+ // W-TOOL-4: symmetric inverse of complete/archive (completed | archived → active,
2489
+ // owner-only). Also serves as the un-archive path.
2490
+ ws
2491
+ .command('reopen')
2492
+ .description('Reopen a completed or archived worksheet back to active (also un-archives). Owner-only.')
2493
+ .argument('<id>', 'Worksheet ID')
2494
+ .action(async (id) => {
2495
+ try {
2496
+ await api('POST', `/api/worksheets/${id}/reopen`);
2497
+ console.log(chalk.green('Worksheet reopened'));
2498
+ }
2499
+ catch (err) {
2500
+ console.error(chalk.red(err.message));
2501
+ process.exit(1);
2502
+ }
2503
+ });
2406
2504
  ws
2407
2505
  .command('progress')
2408
2506
  .description('Show the aggregated progress board for a worksheet. Combines outcomes, milestones, commitments, and actions into a single view. Use "ws show <id>" for the raw detail view instead.')
@@ -3341,11 +3439,17 @@ commitments
3341
3439
  .requiredOption('-d, --description <desc>', 'What you commit to do')
3342
3440
  .option('-a, --action <id>', 'Linked action ID')
3343
3441
  .requiredOption('--due <date>', 'Due date (YYYY-MM-DD)')
3442
+ .option('--allow-past-due', 'Back-date a still-open commitment (admin/owner only)')
3344
3443
  .action(async (opts) => {
3345
3444
  try {
3346
3445
  const body = { description: opts.description, dueDate: opts.due };
3347
3446
  if (opts.action)
3348
3447
  body.actionId = opts.action;
3448
+ // W-TOOL-7: opt-in back-dating of a still-open commitment. Gated to org
3449
+ // admin/owner server-side (403 otherwise); omit the flag to leave the
3450
+ // default forward-dating behavior unchanged.
3451
+ if (opts.allowPastDue)
3452
+ body.allowPastDue = true;
3349
3453
  const data = await api('POST', `/api/worksheets/${opts.worksheet}/commitments`, body);
3350
3454
  const c = data.commitment;
3351
3455
  console.log(chalk.green(`Commitment made: ${c.description}`));
@@ -5381,6 +5485,132 @@ pipelines
5381
5485
  process.exit(1);
5382
5486
  }
5383
5487
  });
5488
+ // ── P-TOOL-1 — `pl rematerialize <instanceId>` ──
5489
+ // Rebase a live pipeline instance onto the CURRENT template definition without
5490
+ // losing completed-step state. Safe-by-default: WITHOUT --apply the verb runs a
5491
+ // preview (dry_run=true) and prints the reconciliation plan, mutating nothing.
5492
+ //
5493
+ // POST /api/pipeline-instances/:id/rematerialize
5494
+ // Body : { dry_run | apply, reason?, force_inflight?, reset_downstream_of_new?, keep_failed? }
5495
+ // Header : If-Match: "<n>" (opt-in via --expect-version; default bypass)
5496
+ //
5497
+ // The API is itself safe-by-default (dry_run defaults to true server-side), so a
5498
+ // bare call previews. We are explicit anyway: send { dry_run: true } on preview
5499
+ // and { apply: true } on --apply. Owner/admin-only for apply and for the
5500
+ // dangerous --force-inflight / --reset-downstream-of-new flags (D5 SPLIT).
5501
+ pipelines
5502
+ .command('rematerialize')
5503
+ .description('Rebase a live pipeline instance onto the current template definition, preserving completed-step state. ' +
5504
+ 'Safe default: WITHOUT --apply it previews the reconciliation plan (mutates nothing). ' +
5505
+ 'With --apply it reconciles the instance in one transaction. Apply (and the --force-inflight / ' +
5506
+ '--reset-downstream-of-new flags) require org owner or admin.')
5507
+ .argument('<instanceId>', 'Pipeline instance ID')
5508
+ .option('--apply', 'Perform the mutation. WITHOUT it the verb previews (dry_run=true) and prints the plan.')
5509
+ .option('--force-inflight', 'Cancel in-flight step executions and reconcile anyway (D3 override). Owner/admin only.')
5510
+ .option('--reset-downstream-of-new', 'Cascade-reset completed steps downstream of a newly-inserted step so they re-run (D6 override). Owner/admin only.')
5511
+ .option('--keep-failed', 'Leave failed steps in "failed" instead of recovering them to "pending" (D4 opt-out).')
5512
+ .option('-r, --reason <reason>', 'Free-form audit reason (recorded in audit_log on apply).')
5513
+ .option('--expect-version <n>', 'Optimistic-concurrency guard: send If-Match: "<n>" so the API rejects the call (409 VERSION_CONFLICT) ' +
5514
+ 'if the current instance version differs. Omit to bypass version checking (default).')
5515
+ .option('--json', 'Print the raw API response as JSON (machine-readable)')
5516
+ .action(async (instanceId, opts) => {
5517
+ try {
5518
+ // Body field names match the API contract verbatim:
5519
+ // dry_run / apply — mode select (preview vs mutate)
5520
+ // reason — optional audit string (apply only, but harmless on preview)
5521
+ // force_inflight — D3 override
5522
+ // reset_downstream_of_new — D6 override
5523
+ // keep_failed — D4 opt-out
5524
+ const apply = opts.apply === true;
5525
+ const body = apply ? { apply: true } : { dry_run: true };
5526
+ if (opts.forceInflight)
5527
+ body.force_inflight = true;
5528
+ if (opts.resetDownstreamOfNew)
5529
+ body.reset_downstream_of_new = true;
5530
+ if (opts.keepFailed)
5531
+ body.keep_failed = true;
5532
+ if (opts.reason)
5533
+ body.reason = opts.reason;
5534
+ // --expect-version <n> opts the caller in to optimistic concurrency.
5535
+ // Default is bypass (no header), mirroring force-transition Decision #4.
5536
+ const apiOpts = {};
5537
+ if (opts.expectVersion !== undefined) {
5538
+ const n = Number(opts.expectVersion);
5539
+ if (!Number.isInteger(n)) {
5540
+ console.error(chalk.red(`--expect-version must be an integer; got "${opts.expectVersion}"`));
5541
+ process.exit(1);
5542
+ }
5543
+ apiOpts.headers = { 'If-Match': String(n) };
5544
+ }
5545
+ const path = `/api/pipeline-instances/${instanceId}/rematerialize`;
5546
+ let data;
5547
+ try {
5548
+ data = (await api('POST', path, body, apiOpts));
5549
+ }
5550
+ catch (err) {
5551
+ // 409 REMATERIALIZE_BLOCKED carries the plan + per-step blockers in its
5552
+ // body; surface them so the operator sees exactly what to fix.
5553
+ if (err instanceof ApiError && err.code === 'REMATERIALIZE_BLOCKED') {
5554
+ if (opts.json) {
5555
+ console.log(JSON.stringify(err.body ?? { code: err.code, error: err.message }, null, 2));
5556
+ process.exit(1);
5557
+ }
5558
+ const b = (err.body ?? {});
5559
+ console.error(chalk.red(`Rematerialize BLOCKED — instance ${instanceId} (template v${b.from_version} → v${b.to_version})`));
5560
+ renderRematerializePlan(b.plan, b.warnings, b.blockers, { blockedHeader: true });
5561
+ console.error(chalk.red('\nResolve the blockers above and retry.'));
5562
+ process.exit(1);
5563
+ }
5564
+ // 409 VERSION_CONFLICT — the If-Match pin did not match the live version.
5565
+ if (err instanceof ApiError && err.code === 'VERSION_CONFLICT') {
5566
+ console.error(chalk.red(`Version conflict: the instance was modified concurrently (--expect-version mismatch). ${err.message}`));
5567
+ process.exit(1);
5568
+ }
5569
+ // 403 owner/admin gate on apply / dangerous flags (or project-only agent).
5570
+ if (err instanceof ApiError && err.status === 403) {
5571
+ console.error(chalk.red(`Permission denied: ${err.message}`));
5572
+ process.exit(1);
5573
+ }
5574
+ throw err;
5575
+ }
5576
+ if (opts.json) {
5577
+ console.log(JSON.stringify(data, null, 2));
5578
+ return;
5579
+ }
5580
+ // 200 ALREADY_CURRENT — idempotent no-op (returned as 200, not an error).
5581
+ if (data?.already_current === true) {
5582
+ console.log(chalk.green(`Already current: instance ${instanceId} is already on template v${data.to_version}. No changes made.`));
5583
+ return;
5584
+ }
5585
+ if (data?.dry_run === true) {
5586
+ // Preview mode — print the plan and the run-again hint.
5587
+ console.log(chalk.bold(`Rematerialize preview — instance ${instanceId} (template v${data.from_version} → v${data.to_version})`));
5588
+ renderRematerializePlan(data.plan, data.warnings, data.blockers, {});
5589
+ if (Array.isArray(data.blockers) && data.blockers.length > 0) {
5590
+ console.log(chalk.yellow('\nThis plan has blockers — apply would be refused (409 REMATERIALIZE_BLOCKED) until they are resolved.'));
5591
+ }
5592
+ console.log(chalk.gray('\nRun again with --apply to execute.'));
5593
+ return;
5594
+ }
5595
+ // Applied — print the applied summary + forensic echo.
5596
+ console.log(chalk.green(`Rematerialized instance ${instanceId} (template v${data.from_version} → v${data.to_version})`));
5597
+ renderRematerializePlan(data.plan, data.warnings, data.blockers, {});
5598
+ if (data?.audit_log_id) {
5599
+ console.log(chalk.gray(` audit_log_id: ${data.audit_log_id}`));
5600
+ }
5601
+ const queued = Array.isArray(data?.queued_step_ids) ? data.queued_step_ids : [];
5602
+ if (queued.length) {
5603
+ console.log(chalk.gray(` queued_step_ids (${queued.length}): ${queued.join(', ')}`));
5604
+ }
5605
+ else {
5606
+ console.log(chalk.gray(' queued_step_ids: (none)'));
5607
+ }
5608
+ }
5609
+ catch (err) {
5610
+ console.error(chalk.red(err.message));
5611
+ process.exit(1);
5612
+ }
5613
+ });
5384
5614
  pipelines
5385
5615
  .command('set-params')
5386
5616
  .description('Set instance parameters (atomic update)')