@formigio/fazemos-cli 0.10.13 → 0.10.14

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
@@ -22,6 +22,56 @@ function parseNumber(val) {
22
22
  throw new Error(`Invalid number: ${val}`);
23
23
  return n;
24
24
  }
25
+ /**
26
+ * OPS-2 — Manual Interventions KPI cell formatter.
27
+ *
28
+ * Single source of CLI truth for the tri-state coloring rule encoded in
29
+ * rul_intervention_chip_thresholds_v1. Mirrors the web InterventionChip
30
+ * thresholds so the CLI MI column on `pl list`, the header line on
31
+ * `pl show`, and the `ops interventions` table all render the same way:
32
+ *
33
+ * null → `—` (no color) — pre-migration row or unknown state
34
+ * 0 → green — clean run
35
+ * 1-3 → amber — some operator help
36
+ * ≥4 → red — carried
37
+ * >999 → `999+` — cap per rul_intervention_chip_count_cap_999plus
38
+ *
39
+ * Width: 5 chars (right-padded inside the chalk color call so colors don't
40
+ * count against the visible width). Callers can wrap with additional text
41
+ * (e.g. `MI:${cell}`) without breaking alignment.
42
+ */
43
+ function formatManualInterventionCell(count) {
44
+ if (count === null || count === undefined) {
45
+ return chalk.gray('—'.padEnd(5));
46
+ }
47
+ const display = count > 999 ? '999+' : String(count);
48
+ const padded = display.padEnd(5);
49
+ if (count === 0)
50
+ return chalk.green(padded);
51
+ if (count <= 3)
52
+ return chalk.yellow(padded);
53
+ return chalk.red(padded);
54
+ }
55
+ /**
56
+ * OPS-2 — parse a duration string ('24h', '7d', '30d', '90d', etc.) into
57
+ * an ISO-8601 datetime `since` value for the GET /api/ops/manual-interventions
58
+ * `since` query param.
59
+ *
60
+ * Accepted units: `h` (hours), `d` (days), `w` (weeks). Unrecognised values
61
+ * throw and surface to the operator via the standard error path.
62
+ */
63
+ function parseSinceDuration(val) {
64
+ const m = /^(\d+)([hdw])$/.exec(val.trim());
65
+ if (!m) {
66
+ throw new Error(`Invalid --since "${val}". Expected forms like 24h, 7d, 30d, 4w.`);
67
+ }
68
+ const n = Number(m[1]);
69
+ const unit = m[2];
70
+ const ms = unit === 'h' ? n * 3_600_000
71
+ : unit === 'd' ? n * 86_400_000
72
+ : n * 604_800_000;
73
+ return new Date(Date.now() - ms).toISOString();
74
+ }
25
75
  function parseStreamSilenceAbortMs(val) {
26
76
  const n = Number(val);
27
77
  if (!Number.isInteger(n) || n < 30000 || n > 1800000) {
@@ -4000,7 +4050,18 @@ pipelines
4000
4050
  }
4001
4051
  for (const inst of data.instances) {
4002
4052
  const stepInfo = inst.total_steps ? ` — step ${inst.current_step ?? '?'}/${inst.total_steps}` : '';
4003
- console.log(` ${chalk.cyan(inst.name)} (${inst.status})${stepInfo}`);
4053
+ // OPS-2 — Manual Interventions KPI. The MI column renders inline next
4054
+ // to the instance status with the prescribed tri-state coloring:
4055
+ // null → `—` (no color; pre-migration row or unknown)
4056
+ // 0 → green
4057
+ // 1-3 → amber
4058
+ // ≥4 → red
4059
+ // Capped per rul_intervention_chip_count_cap_999plus: values > 999
4060
+ // collapse to `999+` while the underlying count remains intact in
4061
+ // the API response and JSON output for scripts.
4062
+ const miCell = formatManualInterventionCell(inst.manual_intervention_count);
4063
+ const envCell = inst.env_tag ? chalk.gray(` [${inst.env_tag}]`) : '';
4064
+ console.log(` ${chalk.cyan(inst.name)} (${inst.status}) MI:${miCell}${envCell}${stepInfo}`);
4004
4065
  console.log(` ID: ${inst.id}`);
4005
4066
  if (opts.expand && inst.steps?.length) {
4006
4067
  for (const s of inst.steps) {
@@ -4029,6 +4090,14 @@ pipelines
4029
4090
  console.log(` ID: ${inst.id}`);
4030
4091
  console.log(` Status: ${inst.status}`);
4031
4092
  console.log(` Template: ${inst.template_name || inst.template_id}`);
4093
+ // OPS-2 — Manual Interventions KPI header line. Same coloring rule as
4094
+ // `pl list` (null=—, 0=green, 1-3=amber, 4+=red). env_tag is surfaced
4095
+ // alongside so operators can read prod-vs-dev at a glance (the cycle
4096
+ // exit criterion is prod-only).
4097
+ console.log(` Manual Interventions: ${formatManualInterventionCell(inst.manual_intervention_count)}`);
4098
+ if (inst.env_tag) {
4099
+ console.log(` Env Tag: ${inst.env_tag}`);
4100
+ }
4032
4101
  if (inst.phases?.length) {
4033
4102
  for (const phase of inst.phases) {
4034
4103
  console.log(chalk.cyan(`\n Phase: ${phase.name}`));
@@ -6995,6 +7064,156 @@ ops
6995
7064
  process.exit(1);
6996
7065
  }
6997
7066
  });
7067
+ // ── OPS-2 — Manual Interventions KPI ─────────────────────────────────
7068
+ // Spec: specs/tech/platform/OPS-2-manual-intervention-kpi-tech-spec.md
7069
+ //
7070
+ // `fazemos ops interventions` has three operating modes:
7071
+ //
7072
+ // 1. bare — windowed summary table + current zero-intervention
7073
+ // streak. Calls GET /api/ops/manual-interventions/summary.
7074
+ // Prod-only by default (cycle exit criterion is prod);
7075
+ // --include-dev inverts the env_tag filter for parity
7076
+ // with the API.
7077
+ // 2. --pipeline — per-pipeline drill. Calls GET /api/pipeline-instances/:id
7078
+ // (response now carries manual_intervention_count +
7079
+ // env_tag) and surfaces the count + the env_tag fence so
7080
+ // operators can read the chip the same way the web does.
7081
+ // Forensic per-event detail lives behind `fazemos agents
7082
+ // events --type manual_intervention` (existing surface);
7083
+ // we print the hint so the operator doesn't have to
7084
+ // rediscover it.
7085
+ // 3. --since — windowed list. Calls GET /api/ops/manual-interventions
7086
+ // with the resolved `since` timestamp. Defaults to
7087
+ // include-dev (false) → prod-only-NO; the GET list
7088
+ // endpoint defaults exclude_dev=false (the broader view
7089
+ // is more useful when scanning recent activity). The
7090
+ // --exclude-dev flag tightens to prod-only.
7091
+ //
7092
+ // Admin/owner gating is enforced server-side (403 FORBIDDEN); the CLI
7093
+ // surfaces the API's error verbatim so non-admin operators see the
7094
+ // authoritative reason rather than a stale local guess.
7095
+ ops
7096
+ .command('interventions')
7097
+ .description('Manual-intervention KPI. Bare mode shows the windowed summary table + '
7098
+ + 'current zero-intervention streak (prod-only by default). '
7099
+ + '--pipeline <id> drills into one pipeline. '
7100
+ + '--since <duration> lists pipelines started in the window.')
7101
+ .option('-w, --window <window>', 'Summary window: 24h, 7d, 30d', '7d')
7102
+ .option('-p, --pipeline <id>', 'Drill into one pipeline_instance by ID')
7103
+ .option('--since <duration>', 'List mode: filter by started_at >= now - duration (e.g. 24h, 7d, 30d)')
7104
+ .option('-l, --limit <n>', 'List mode: max rows (default 50, max 200)', '50')
7105
+ .option('--include-dev', 'Include dev-tagged pipelines in the summary / list (default: exclude)', false)
7106
+ .option('--exclude-dev', 'Force exclude dev-tagged pipelines (default for summary; opt-in for list)', false)
7107
+ .option('--json', 'Print the raw API response as JSON (machine-readable)')
7108
+ .action(async (opts) => {
7109
+ try {
7110
+ // ── Mode 1: --pipeline <id> drill ────────────────────────────
7111
+ if (opts.pipeline) {
7112
+ const data = await api('GET', `/api/pipeline-instances/${opts.pipeline}`);
7113
+ if (opts.json) {
7114
+ console.log(JSON.stringify(data, null, 2));
7115
+ return;
7116
+ }
7117
+ const inst = data.instance;
7118
+ if (!inst) {
7119
+ console.error(chalk.red(`Pipeline ${opts.pipeline} not found`));
7120
+ process.exit(1);
7121
+ }
7122
+ console.log(chalk.cyan(`Manual interventions on ${inst.name}`));
7123
+ console.log(` ID: ${inst.id}`);
7124
+ console.log(` Status: ${inst.status}`);
7125
+ console.log(` Env Tag: ${inst.env_tag ?? chalk.gray('—')}`);
7126
+ console.log(` Count: ${formatManualInterventionCell(inst.manual_intervention_count)}`);
7127
+ if (inst.started_at) {
7128
+ console.log(` Started: ${new Date(inst.started_at).toLocaleString()}`);
7129
+ }
7130
+ if (inst.completed_at) {
7131
+ console.log(` Completed: ${new Date(inst.completed_at).toLocaleString()}`);
7132
+ }
7133
+ console.log('');
7134
+ console.log(chalk.gray(' Forensic per-event detail: query agent_events for the assignee/reviewer'));
7135
+ console.log(chalk.gray(' identities that drove each increment. Example:'));
7136
+ console.log(chalk.gray(` fazemos agents events <agent-id> --type manual_intervention`));
7137
+ return;
7138
+ }
7139
+ // ── Mode 2: --since <duration> windowed list ─────────────────
7140
+ if (opts.since) {
7141
+ const since = parseSinceDuration(opts.since);
7142
+ const params = [`since=${encodeURIComponent(since)}`];
7143
+ // For the list endpoint, the API default is exclude_dev=false (a
7144
+ // broader scan-recent-activity surface). --exclude-dev tightens
7145
+ // to prod-only. --include-dev is a no-op here (the default
7146
+ // already includes dev) but accepted so the flag is symmetric
7147
+ // across modes.
7148
+ if (opts.excludeDev)
7149
+ params.push('exclude_dev=true');
7150
+ const limitN = Math.max(1, Math.min(200, Number(opts.limit) || 50));
7151
+ params.push(`limit=${limitN}`);
7152
+ const qs = `?${params.join('&')}`;
7153
+ const data = await api('GET', `/api/ops/manual-interventions${qs}`);
7154
+ if (opts.json) {
7155
+ console.log(JSON.stringify(data, null, 2));
7156
+ return;
7157
+ }
7158
+ const rows = data.pipelines || [];
7159
+ if (!rows.length) {
7160
+ console.log(chalk.green(`No interventions in window since ${since}`));
7161
+ return;
7162
+ }
7163
+ const scope = opts.excludeDev ? 'prod-only' : 'all envs';
7164
+ console.log(chalk.cyan(`Manual interventions since ${since} (${scope}):`));
7165
+ console.log('');
7166
+ // Columns: MI | Env | Status | Started | Name (ID)
7167
+ for (const r of rows) {
7168
+ const mi = formatManualInterventionCell(r.manual_intervention_count);
7169
+ const env = (r.env_tag ?? '—').padEnd(4);
7170
+ const status = (r.status ?? '').padEnd(10);
7171
+ const started = r.started_at ? new Date(r.started_at).toLocaleString() : '';
7172
+ console.log(` MI:${mi} ${chalk.gray(env)} ${status} ${chalk.gray(started)}`);
7173
+ console.log(` ${chalk.cyan(r.name)} ${chalk.gray(r.pipeline_instance_id)}`);
7174
+ }
7175
+ return;
7176
+ }
7177
+ // ── Mode 3: bare summary ─────────────────────────────────────
7178
+ // The summary endpoint defaults exclude_dev=true (cycle exit
7179
+ // criterion is prod-only by spec). --include-dev inverts. The CLI
7180
+ // is explicit either way so log lines are unambiguous.
7181
+ const excludeDev = opts.includeDev ? false : true;
7182
+ const window = opts.window || '7d';
7183
+ const validWindows = ['24h', '7d', '30d'];
7184
+ if (!validWindows.includes(window)) {
7185
+ console.error(chalk.red(`Invalid --window "${window}". Must be one of: ${validWindows.join(', ')}`));
7186
+ process.exit(1);
7187
+ }
7188
+ const qs = `?window=${window}&exclude_dev=${excludeDev}`;
7189
+ const data = await api('GET', `/api/ops/manual-interventions/summary${qs}`);
7190
+ if (opts.json) {
7191
+ console.log(JSON.stringify(data, null, 2));
7192
+ return;
7193
+ }
7194
+ const scope = excludeDev ? 'prod-only' : 'all envs';
7195
+ console.log(chalk.cyan(`Manual interventions — ${data.window} (${scope})`));
7196
+ console.log('');
7197
+ console.log(` Total runs: ${data.total_runs}`);
7198
+ console.log(` Total interventions: ${data.total_interventions}`);
7199
+ console.log(` Avg per run: ${typeof data.avg_per_run === 'number' ? data.avg_per_run.toFixed(2) : data.avg_per_run}`);
7200
+ console.log(` p95 per run: ${data.p95_per_run}`);
7201
+ console.log(` Zero-intervention: ${data.zero_intervention_runs} of ${data.total_runs} runs`);
7202
+ // Streak gets the same green/amber/grey emphasis as the web chip:
7203
+ // ≥5 is a celebration moment per Sage's spec; 1-4 amber; 0 grey.
7204
+ const streak = data.zero_intervention_streak ?? 0;
7205
+ const streakDisplay = streak >= 5
7206
+ ? chalk.green(`${streak} 🎉`)
7207
+ : streak >= 1
7208
+ ? chalk.yellow(String(streak))
7209
+ : chalk.gray('0');
7210
+ console.log(` Current streak: ${streakDisplay}`);
7211
+ }
7212
+ catch (err) {
7213
+ console.error(chalk.red(err.message));
7214
+ process.exit(1);
7215
+ }
7216
+ });
6998
7217
  // ── F18 — Project-scoped docs surface ────────────────────────────────
6999
7218
  // Spec: specs/tech/platform/F18-project-scoped-docs-surface-tech-spec.md §8
7000
7219
  //