@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 +231 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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".
|
|
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)')
|