@formigio/fazemos-cli 0.10.47 → 0.10.49

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
@@ -1261,6 +1261,39 @@ function projectOpts(opts) {
1261
1261
  allProjects: opts.allProjects,
1262
1262
  };
1263
1263
  }
1264
+ /**
1265
+ * W-TOOL-3 — scope options for by-ID worksheet READ verbs (`ws show`,
1266
+ * `ws report`, `oc list`, `ac list`, `cm list`).
1267
+ *
1268
+ * `GET /api/worksheets/:id` (and `/progress-board`) strictly scope the read
1269
+ * to the caller's project: when an `X-Fazemos-Project-Id` header is present
1270
+ * and does not equal the worksheet's `project_id`, the API returns 404 (an
1271
+ * enumeration guard). Because `api()` always attaches the *active* project's
1272
+ * header by default, an org steward reading a worksheet that lives in a
1273
+ * sibling project gets a spurious 404 — which is exactly the raw-API drop
1274
+ * Ollie reported.
1275
+ *
1276
+ * --project <slug> → send THAT project's header (resolves the sibling read)
1277
+ * --all-projects → OMIT the project header entirely, so an owner/admin
1278
+ * gets an org-wide read. The API only enforces the
1279
+ * header/worksheet match when a header is present; with
1280
+ * no header an owner/admin resolves any worksheet in the
1281
+ * org, while a plain member still needs a
1282
+ * `project_members` row for the worksheet's project
1283
+ * (authorization is unchanged — only the accidental
1284
+ * active-project pin is removed).
1285
+ *
1286
+ * Deliberately different from projectOpts(): on a LIST endpoint --all-projects
1287
+ * appends ?view=all to widen the list; on a by-ID read there is no list to
1288
+ * widen — the fix is to stop pinning the header to the active project.
1289
+ */
1290
+ function readScopeOpts(opts) {
1291
+ if (opts.allProjects)
1292
+ return { noProjectHeader: true };
1293
+ if (opts.project)
1294
+ return { projectSlug: opts.project };
1295
+ return {};
1296
+ }
1264
1297
  /**
1265
1298
  * Uniform error handler for scoped commands. When the API emits
1266
1299
  * MISSING_PROJECT_CONTEXT (§7.3.1), the api helper has already re-shaped
@@ -2415,15 +2448,18 @@ ws
2415
2448
  });
2416
2449
  ws
2417
2450
  .command('show')
2418
- .description('Show worksheet detail')
2451
+ .description('Show worksheet detail. Pass --project <slug> (or --all-projects) to read a worksheet that lives in a sibling project.')
2419
2452
  .argument('<id>', 'Worksheet ID')
2420
- .action(async (id) => {
2453
+ .option('--project <slug>', 'Read a worksheet in this project (resolves cross-project 404s)')
2454
+ .option('--all-projects', 'Read org-wide without pinning to the active project', false)
2455
+ .action(async (id, opts) => {
2421
2456
  try {
2457
+ const scope = readScopeOpts(opts);
2422
2458
  // G4 (AC15): fetch enriched actions in parallel so we can display
2423
2459
  // linked_pipeline status chips next to bound actions.
2424
2460
  const [data, actionsResult] = await Promise.all([
2425
- api('GET', `/api/worksheets/${id}`),
2426
- api('GET', `/api/worksheets/${id}/actions`).catch(() => null),
2461
+ api('GET', `/api/worksheets/${id}`, undefined, scope),
2462
+ api('GET', `/api/worksheets/${id}/actions`, undefined, scope).catch(() => null),
2427
2463
  ]);
2428
2464
  const w = data.worksheet;
2429
2465
  // Build a lookup map from action id → linked_pipeline (from the enriched endpoint)
@@ -2487,6 +2523,110 @@ ws
2487
2523
  process.exit(1);
2488
2524
  }
2489
2525
  });
2526
+ // W-TOOL-2 — `ws report <id>`: one-call progress digest.
2527
+ //
2528
+ // Retires Ollie's manual Progress-Read / Check-in-Prep assembly, which today
2529
+ // stitches 3–4 separate `oc list` / `ac list` / `cm list` reads by hand. The
2530
+ // single GET /api/worksheets/:id already returns the full bundle (worksheet +
2531
+ // outcomes + actions + commitments + milestones), so the digest is composed
2532
+ // client-side from ONE call — no new endpoint required.
2533
+ //
2534
+ // (The existing /progress-board endpoint was evaluated first per the cheapest-
2535
+ // correct-implementation rule, but it omits actions entirely and caps outcomes
2536
+ // at 6, so it cannot render "action velocity" or a complete outcome scoreboard.
2537
+ // See the Test Implementation Brief for the API-aggregate follow-up flagged to
2538
+ // Atlas.)
2539
+ ws
2540
+ .command('report')
2541
+ .description('One-call progress digest for a worksheet: Purpose, outcome scoreboard (current/target + status), action velocity, and kept/missed/pending commitments. Composed from a single read. Pass --project <slug> (or --all-projects) for a sibling-project worksheet.')
2542
+ .argument('<id>', 'Worksheet ID')
2543
+ .option('--json', 'Print the assembled report as JSON (machine-readable)')
2544
+ .option('--project <slug>', 'Read a worksheet in this project (resolves cross-project 404s)')
2545
+ .option('--all-projects', 'Read org-wide without pinning to the active project', false)
2546
+ .action(async (id, opts) => {
2547
+ try {
2548
+ const data = await api('GET', `/api/worksheets/${id}`, undefined, readScopeOpts(opts));
2549
+ const w = data.worksheet;
2550
+ const outcomes = data.outcomes || [];
2551
+ const actions = data.actions || [];
2552
+ const commitments = data.commitments || [];
2553
+ // Commitment roll-up: a commitment is "missed" if it is persisted missed
2554
+ // OR still open past its due date (mirrors the API's own progress-board
2555
+ // math: `status = 'missed' OR (status = 'open' AND due_date < today)`).
2556
+ const today = new Date().toISOString().slice(0, 10);
2557
+ const isMissed = (c) => c.status === 'missed' || (c.status === 'open' && c.due_date && c.due_date < today);
2558
+ const isKept = (c) => c.status === 'completed';
2559
+ const isPending = (c) => c.status === 'open' && !isMissed(c);
2560
+ const kept = commitments.filter(isKept);
2561
+ const missed = commitments.filter(isMissed);
2562
+ const pending = commitments.filter(isPending);
2563
+ if (opts.json) {
2564
+ console.log(JSON.stringify({
2565
+ worksheet: { id: w.id, name: w.name, purpose: w.purpose, status: w.status },
2566
+ outcomes: outcomes.map(o => ({
2567
+ id: o.id,
2568
+ name: o.name,
2569
+ currentValue: o.current_value ?? null,
2570
+ targetValue: o.target_value ?? null,
2571
+ status: o.status,
2572
+ })),
2573
+ actions: actions.map(a => ({
2574
+ id: a.id,
2575
+ description: a.description,
2576
+ currentValue: a.current_value ?? null,
2577
+ targetValue: a.target_value ?? null,
2578
+ member: a.member_name ?? null,
2579
+ })),
2580
+ commitments: {
2581
+ kept: kept.length,
2582
+ missed: missed.length,
2583
+ pending: pending.length,
2584
+ total: commitments.length,
2585
+ items: commitments.map(c => ({
2586
+ id: c.id,
2587
+ description: c.description,
2588
+ dueDate: c.due_date,
2589
+ status: isMissed(c) ? 'missed' : isKept(c) ? 'kept' : 'pending',
2590
+ })),
2591
+ },
2592
+ }, null, 2));
2593
+ return;
2594
+ }
2595
+ console.log(chalk.cyan.bold(w.name) + chalk.gray(` (${w.status})`));
2596
+ console.log(` Purpose: ${w.purpose || chalk.gray('(none)')}`);
2597
+ console.log(chalk.cyan(`\n Outcomes (${outcomes.length}):`));
2598
+ if (outcomes.length === 0) {
2599
+ console.log(chalk.gray(' (none)'));
2600
+ }
2601
+ else {
2602
+ for (const o of outcomes) {
2603
+ const progress = o.target_value != null ? ` ${o.current_value ?? 0}/${o.target_value}` : '';
2604
+ const icon = o.status === 'achieved' ? chalk.green('✓') : '○';
2605
+ console.log(` ${icon} ${o.name}${progress} — ${chalk.yellow(o.status || 'unknown')}`);
2606
+ }
2607
+ }
2608
+ console.log(chalk.cyan(`\n Action velocity (${actions.length}):`));
2609
+ if (actions.length === 0) {
2610
+ console.log(chalk.gray(' (none)'));
2611
+ }
2612
+ else {
2613
+ for (const a of actions) {
2614
+ const progress = a.target_value != null ? ` ${a.current_value ?? 0}/${a.target_value}` : '';
2615
+ console.log(` ${a.description}${progress} — ${a.member_name || 'unassigned'}`);
2616
+ }
2617
+ }
2618
+ console.log(chalk.cyan(`\n Commitments: ${chalk.green(kept.length + ' kept')}, ${chalk.red(missed.length + ' missed')}, ${pending.length} pending (${commitments.length} total)`));
2619
+ for (const c of commitments) {
2620
+ const icon = isKept(c) ? chalk.green('✓') : isMissed(c) ? chalk.red('✗') : '○';
2621
+ const label = isMissed(c) ? 'missed' : isKept(c) ? 'kept' : 'pending';
2622
+ console.log(` ${icon} ${c.description} — due ${c.due_date} (${label})`);
2623
+ }
2624
+ }
2625
+ catch (err) {
2626
+ console.error(chalk.red(err.message));
2627
+ process.exit(1);
2628
+ }
2629
+ });
2490
2630
  // ── Outcomes ────────────────────────────────────────────────
2491
2631
  const outcomes = program.command('outcomes').alias('oc').description('Outcome commands');
2492
2632
  outcomes
@@ -2523,11 +2663,13 @@ outcomes
2523
2663
  });
2524
2664
  outcomes
2525
2665
  .command('list')
2526
- .description('List outcomes on a worksheet')
2666
+ .description('List outcomes on a worksheet. Pass --project <slug> (or --all-projects) to read a worksheet in a sibling project.')
2527
2667
  .requiredOption('-w, --worksheet <id>', 'Worksheet ID')
2668
+ .option('--project <slug>', 'Read a worksheet in this project (resolves cross-project 404s)')
2669
+ .option('--all-projects', 'Read org-wide without pinning to the active project', false)
2528
2670
  .action(async (opts) => {
2529
2671
  try {
2530
- const data = await api('GET', `/api/worksheets/${opts.worksheet}`);
2672
+ const data = await api('GET', `/api/worksheets/${opts.worksheet}`, undefined, readScopeOpts(opts));
2531
2673
  if (!data.outcomes?.length) {
2532
2674
  console.log(chalk.yellow('No outcomes'));
2533
2675
  return;
@@ -2587,7 +2729,7 @@ outcomes
2587
2729
  .option('-d, --description <desc>', 'New description')
2588
2730
  .option('-m, --measurement <method>', 'How it is measured (e.g., "Count of active users")')
2589
2731
  .option('-t, --target <value>', 'Target value (numeric)', parseNumber)
2590
- .option('-s, --status <status>', 'Status: active, achieved, or dropped')
2732
+ .option('-s, --status <status>', 'Status: on_track, at_risk, behind, or achieved')
2591
2733
  .action(async (opts) => {
2592
2734
  try {
2593
2735
  const body = {};
@@ -2952,28 +3094,135 @@ members
2952
3094
  const actions = program.command('actions').alias('ac').description('Action (lead measure) commands');
2953
3095
  actions
2954
3096
  .command('add')
2955
- .description('Add an action to a worksheet')
3097
+ .description('Add one or more actions to a worksheet.\n' +
3098
+ ' Single: ac add -w <id> -n "<desc>" [-m <measurement>] [-t <target>] [-c <current>] [-o <outcomeId>]\n' +
3099
+ ' Batch: ac add -w <id> --action "<desc>" --action "<desc>" ... (repeatable, description-only)\n' +
3100
+ ' Batch: ac add -w <id> --file actions.json (JSON array of full action objects)\n' +
3101
+ ' Preview: add --dry-run to print exactly what would be written without creating anything.')
2956
3102
  .requiredOption('-w, --worksheet <id>', 'Worksheet ID')
2957
- .requiredOption('-n, --name <name>', 'Action description')
2958
- .option('-o, --outcome <id>', 'Linked outcome ID')
2959
- .option('-m, --measurement <method>', 'How it is measured')
2960
- .option('-t, --target <value>', 'Target value', parseNumber)
2961
- .option('-c, --current <value>', 'Current value', parseNumber)
3103
+ .option('-n, --name <name>', 'Action description (single-add form)')
3104
+ .option('-o, --outcome <id>', 'Linked outcome ID (single-add form)')
3105
+ .option('-m, --measurement <method>', 'How it is measured (single-add form)')
3106
+ .option('-t, --target <value>', 'Target value (single-add form)', parseNumber)
3107
+ .option('-c, --current <value>', 'Current value (single-add form)', parseNumber)
3108
+ .option('--action <desc>', 'Batch: an action description (repeatable). Use --file for actions that need measurement/target.', (v, acc) => { const list = Array.isArray(acc) ? acc : []; list.push(v); return list; })
3109
+ .option('--file <path>', 'Batch: path to a JSON array of action objects ({ description|name, measurement?, target?, current?, outcomeId? }). "@file.json" is also accepted.')
3110
+ .option('--dry-run', 'Preview the action(s) that would be created without writing anything', false)
2962
3111
  .action(async (opts) => {
2963
3112
  try {
2964
- const body = { description: opts.name };
2965
- if (opts.outcome)
2966
- body.outcomeId = opts.outcome;
2967
- if (opts.measurement)
2968
- body.measurement = opts.measurement;
2969
- if (opts.target !== undefined)
2970
- body.targetValue = opts.target;
2971
- if (opts.current !== undefined)
2972
- body.currentValue = opts.current;
2973
- const data = await api('POST', `/api/worksheets/${opts.worksheet}/actions`, body);
2974
- const a = data.action;
2975
- console.log(chalk.green(`Added action: ${a.description}`));
2976
- console.log(` ID: ${a.id}`);
3113
+ // Normalize a raw item (from --file or a flag) into an API body.
3114
+ const toBody = (item) => {
3115
+ const description = item.description ?? item.name;
3116
+ if (!description || typeof description !== 'string' || description.trim().length === 0) {
3117
+ throw new Error('each action needs a non-empty "description" (or "name")');
3118
+ }
3119
+ const body = { description };
3120
+ const outcomeId = item.outcomeId ?? item.outcome;
3121
+ const measurement = item.measurement;
3122
+ const target = item.targetValue ?? item.target;
3123
+ const current = item.currentValue ?? item.current;
3124
+ if (outcomeId != null)
3125
+ body.outcomeId = outcomeId;
3126
+ if (measurement != null)
3127
+ body.measurement = measurement;
3128
+ if (target != null)
3129
+ body.targetValue = typeof target === 'number' ? target : parseNumber(String(target));
3130
+ if (current != null)
3131
+ body.currentValue = typeof current === 'number' ? current : parseNumber(String(current));
3132
+ return body;
3133
+ };
3134
+ // Resolve the source of truth for what to create. Exactly one of:
3135
+ // --file | --action (repeatable) | -n/--name (single form)
3136
+ const batchActions = opts.action;
3137
+ let filePath = opts.file;
3138
+ // Support the "@file.json" convention (a bare @-prefixed --file value).
3139
+ if (filePath && filePath.startsWith('@'))
3140
+ filePath = filePath.slice(1);
3141
+ const sources = [
3142
+ filePath ? 'file' : null,
3143
+ batchActions && batchActions.length ? 'action' : null,
3144
+ opts.name ? 'name' : null,
3145
+ ].filter(Boolean);
3146
+ if (sources.length === 0) {
3147
+ console.error(chalk.red('Provide -n/--name (single), --action (repeatable), or --file (JSON batch).'));
3148
+ process.exit(1);
3149
+ }
3150
+ if (sources.length > 1) {
3151
+ console.error(chalk.red(`Choose one input form — got ${sources.join(' + ')}. Combine measurement/target with the --file form.`));
3152
+ process.exit(1);
3153
+ }
3154
+ // Build the list of bodies.
3155
+ let bodies;
3156
+ if (filePath) {
3157
+ let parsed;
3158
+ try {
3159
+ parsed = JSON.parse(readFileSync(resolve(filePath), 'utf-8'));
3160
+ }
3161
+ catch (e) {
3162
+ console.error(chalk.red(`Could not read/parse ${filePath}: ${e.message}`));
3163
+ process.exit(1);
3164
+ }
3165
+ const arr = Array.isArray(parsed) ? parsed : (Array.isArray(parsed?.actions) ? parsed.actions : null);
3166
+ if (!arr) {
3167
+ console.error(chalk.red('The --file must contain a JSON array of action objects (or an { "actions": [...] } wrapper).'));
3168
+ process.exit(1);
3169
+ }
3170
+ if (arr.length === 0) {
3171
+ console.error(chalk.red('The --file action array is empty.'));
3172
+ process.exit(1);
3173
+ }
3174
+ bodies = arr.map(toBody);
3175
+ }
3176
+ else if (batchActions && batchActions.length) {
3177
+ bodies = batchActions.map((desc) => toBody({ description: desc }));
3178
+ }
3179
+ else {
3180
+ bodies = [toBody({ description: opts.name, outcome: opts.outcome, measurement: opts.measurement, target: opts.target, current: opts.current })];
3181
+ }
3182
+ // Dry-run preview — no network calls.
3183
+ if (opts.dryRun) {
3184
+ console.log(chalk.cyan(`Dry run — ${bodies.length} action(s) would be created on worksheet ${opts.worksheet}:`));
3185
+ bodies.forEach((b, i) => {
3186
+ const extras = [];
3187
+ if (b.measurement)
3188
+ extras.push(`measurement="${b.measurement}"`);
3189
+ if (b.targetValue != null)
3190
+ extras.push(`target=${b.targetValue}`);
3191
+ if (b.currentValue != null)
3192
+ extras.push(`current=${b.currentValue}`);
3193
+ if (b.outcomeId)
3194
+ extras.push(`outcome=${b.outcomeId}`);
3195
+ const tail = extras.length ? chalk.gray(` [${extras.join(', ')}]`) : '';
3196
+ console.log(` ${i + 1}. ${b.description}${tail}`);
3197
+ });
3198
+ console.log(chalk.gray('Nothing was written. Re-run without --dry-run to create these actions.'));
3199
+ return;
3200
+ }
3201
+ // Real create. Single form keeps its original one-line success output;
3202
+ // batch form reports each created ID and a final summary.
3203
+ if (bodies.length === 1 && sources[0] === 'name') {
3204
+ const data = await api('POST', `/api/worksheets/${opts.worksheet}/actions`, bodies[0]);
3205
+ const a = data.action;
3206
+ console.log(chalk.green(`Added action: ${a.description}`));
3207
+ console.log(` ID: ${a.id}`);
3208
+ return;
3209
+ }
3210
+ let created = 0;
3211
+ for (let i = 0; i < bodies.length; i++) {
3212
+ const b = bodies[i];
3213
+ try {
3214
+ const data = await api('POST', `/api/worksheets/${opts.worksheet}/actions`, b);
3215
+ const a = data.action;
3216
+ created++;
3217
+ console.log(chalk.green(` ✓ [${i + 1}/${bodies.length}] ${a.description} — ${a.id}`));
3218
+ }
3219
+ catch (e) {
3220
+ console.error(chalk.red(` ✗ [${i + 1}/${bodies.length}] ${b.description} — ${e.message}`));
3221
+ console.error(chalk.yellow(`Stopped after ${created} of ${bodies.length} created. Fix the failing item and re-run for the remainder.`));
3222
+ process.exit(1);
3223
+ }
3224
+ }
3225
+ console.log(chalk.green(`Created ${created} action(s).`));
2977
3226
  }
2978
3227
  catch (err) {
2979
3228
  console.error(chalk.red(err.message));
@@ -2982,11 +3231,13 @@ actions
2982
3231
  });
2983
3232
  actions
2984
3233
  .command('list')
2985
- .description('List actions on a worksheet')
3234
+ .description('List actions on a worksheet. Pass --project <slug> (or --all-projects) to read a worksheet in a sibling project.')
2986
3235
  .requiredOption('-w, --worksheet <id>', 'Worksheet ID')
3236
+ .option('--project <slug>', 'Read a worksheet in this project (resolves cross-project 404s)')
3237
+ .option('--all-projects', 'Read org-wide without pinning to the active project', false)
2987
3238
  .action(async (opts) => {
2988
3239
  try {
2989
- const data = await api('GET', `/api/worksheets/${opts.worksheet}/actions`);
3240
+ const data = await api('GET', `/api/worksheets/${opts.worksheet}/actions`, undefined, readScopeOpts(opts));
2990
3241
  if (!data.actions?.length) {
2991
3242
  console.log(chalk.yellow('No actions'));
2992
3243
  return;
@@ -3049,6 +3300,21 @@ actions
3049
3300
  process.exit(1);
3050
3301
  }
3051
3302
  });
3303
+ actions
3304
+ .command('remove')
3305
+ .description('Permanently delete an action from a worksheet. This cannot be undone. Use "ac list -w <id>" (or "ws show <id>") to find action IDs.')
3306
+ .requiredOption('-w, --worksheet <id>', 'Worksheet ID')
3307
+ .requiredOption('-a, --action <id>', 'Action ID (from "ac list" or "ws show" output)')
3308
+ .action(async (opts) => {
3309
+ try {
3310
+ await api('DELETE', `/api/worksheets/${opts.worksheet}/actions/${opts.action}`);
3311
+ console.log(chalk.green('Action deleted'));
3312
+ }
3313
+ catch (err) {
3314
+ console.error(chalk.red(err.message));
3315
+ process.exit(1);
3316
+ }
3317
+ });
3052
3318
  actions
3053
3319
  .command('execute')
3054
3320
  .description('Trigger an agent execution for an action. The API determines which agent to use based on the action\'s configuration. For more control over agent selection and parameters, use the top-level "execute" command instead.')
@@ -3093,11 +3359,13 @@ commitments
3093
3359
  });
3094
3360
  commitments
3095
3361
  .command('list')
3096
- .description('List commitments on a worksheet')
3362
+ .description('List commitments on a worksheet. Pass --project <slug> (or --all-projects) to read a worksheet in a sibling project.')
3097
3363
  .requiredOption('-w, --worksheet <id>', 'Worksheet ID')
3364
+ .option('--project <slug>', 'Read a worksheet in this project (resolves cross-project 404s)')
3365
+ .option('--all-projects', 'Read org-wide without pinning to the active project', false)
3098
3366
  .action(async (opts) => {
3099
3367
  try {
3100
- const data = await api('GET', `/api/worksheets/${opts.worksheet}/commitments`);
3368
+ const data = await api('GET', `/api/worksheets/${opts.worksheet}/commitments`, undefined, readScopeOpts(opts));
3101
3369
  if (!data.commitments?.length) {
3102
3370
  console.log(chalk.yellow('No commitments'));
3103
3371
  return;
@@ -4768,19 +5036,50 @@ templates
4768
5036
  });
4769
5037
  // ── Pipelines ──────────────────────────────────────────────
4770
5038
  const pipelines = program.command('pipelines').alias('pl').description('Pipeline instance commands');
5039
+ // Keep this in sync with the `pl list --status` help text below. A typo like
5040
+ // `--status runing` would otherwise pass straight through to the API, exact-
5041
+ // match nothing, and print "No pipeline instances" — reading as "nothing
5042
+ // running" rather than "you mistyped". We reject unknown values up front with
5043
+ // the same allow-list pattern used by `orgs members` / `orgs invites list`.
5044
+ const PL_LIST_STATUSES = [
5045
+ 'active',
5046
+ 'all',
5047
+ 'created',
5048
+ 'running',
5049
+ 'paused',
5050
+ 'completed',
5051
+ 'failed',
5052
+ 'cancelled',
5053
+ 'archived',
5054
+ ];
4771
5055
  pipelines
4772
5056
  .command('list')
4773
5057
  .description('List pipeline instances in the active project (or all projects with --all-projects)')
4774
- .option('-s, --status <status>', 'Filter by status (active, completed, archived)', 'active')
5058
+ .option('-s, --status <status>', 'Filter by status: active (created/running/paused, the default), running, created, paused, completed, failed, cancelled, archived, or all', 'active')
4775
5059
  .option('--search <term>', 'Search by name or ID')
4776
5060
  .option('--expand', 'Include steps inline (avoids N+1)')
4777
5061
  .option('--project <slug>', 'Override active project for this call')
4778
5062
  .option('--all-projects', 'List pipelines across every project in the active org', false)
4779
5063
  .action(async (opts) => {
4780
5064
  try {
5065
+ const status = opts.status || 'active';
5066
+ if (!PL_LIST_STATUSES.includes(status)) {
5067
+ console.error(chalk.red(`Invalid --status "${status}". Allowed values: ${PL_LIST_STATUSES.join(', ')}`));
5068
+ process.exit(1);
5069
+ }
4781
5070
  const params = [];
4782
- if (opts.status)
4783
- params.push(`status=${opts.status}`);
5071
+ // Status → API mapping. The API treats a bare `status` value as an exact
5072
+ // DB status match, and omitting it entirely as the "active" group
5073
+ // (created/running/paused). In-flight instances carry DB status `running`,
5074
+ // which no literal `active` value would ever match — so `active` (and the
5075
+ // default) must map to "omit the param" rather than `status=active`, or
5076
+ // `pl list` reports "No instances" while pipelines are actually running.
5077
+ // `all` → `status=all` (API disables the filter). Every real DB status
5078
+ // (running, created, paused, completed, failed, cancelled, archived) is
5079
+ // passed through verbatim.
5080
+ if (status && status !== 'active') {
5081
+ params.push(`status=${status}`);
5082
+ }
4784
5083
  if (opts.search)
4785
5084
  params.push(`search=${encodeURIComponent(opts.search)}`);
4786
5085
  if (opts.expand)