@formigio/fazemos-cli 0.10.47 → 0.10.50
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 +371 -34
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
@@ -2358,7 +2391,7 @@ ws
|
|
|
2358
2391
|
});
|
|
2359
2392
|
ws
|
|
2360
2393
|
.command('archive')
|
|
2361
|
-
.description('Archive a worksheet. Archived worksheets are hidden from "ws list" by default but can be found with "ws list -s archived".
|
|
2394
|
+
.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>".')
|
|
2362
2395
|
.argument('<id>', 'Worksheet ID')
|
|
2363
2396
|
.action(async (id) => {
|
|
2364
2397
|
try {
|
|
@@ -2370,6 +2403,38 @@ ws
|
|
|
2370
2403
|
process.exit(1);
|
|
2371
2404
|
}
|
|
2372
2405
|
});
|
|
2406
|
+
// W-TOOL-4: first-class active → completed transition (owner-only). Completed
|
|
2407
|
+
// worksheets leave the default active list; find them with "ws list -s completed".
|
|
2408
|
+
ws
|
|
2409
|
+
.command('complete')
|
|
2410
|
+
.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>".')
|
|
2411
|
+
.argument('<id>', 'Worksheet ID')
|
|
2412
|
+
.action(async (id) => {
|
|
2413
|
+
try {
|
|
2414
|
+
await api('POST', `/api/worksheets/${id}/complete`);
|
|
2415
|
+
console.log(chalk.green('Worksheet completed'));
|
|
2416
|
+
}
|
|
2417
|
+
catch (err) {
|
|
2418
|
+
console.error(chalk.red(err.message));
|
|
2419
|
+
process.exit(1);
|
|
2420
|
+
}
|
|
2421
|
+
});
|
|
2422
|
+
// W-TOOL-4: symmetric inverse of complete/archive (completed | archived → active,
|
|
2423
|
+
// owner-only). Also serves as the un-archive path.
|
|
2424
|
+
ws
|
|
2425
|
+
.command('reopen')
|
|
2426
|
+
.description('Reopen a completed or archived worksheet back to active (also un-archives). Owner-only.')
|
|
2427
|
+
.argument('<id>', 'Worksheet ID')
|
|
2428
|
+
.action(async (id) => {
|
|
2429
|
+
try {
|
|
2430
|
+
await api('POST', `/api/worksheets/${id}/reopen`);
|
|
2431
|
+
console.log(chalk.green('Worksheet reopened'));
|
|
2432
|
+
}
|
|
2433
|
+
catch (err) {
|
|
2434
|
+
console.error(chalk.red(err.message));
|
|
2435
|
+
process.exit(1);
|
|
2436
|
+
}
|
|
2437
|
+
});
|
|
2373
2438
|
ws
|
|
2374
2439
|
.command('progress')
|
|
2375
2440
|
.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.')
|
|
@@ -2415,15 +2480,18 @@ ws
|
|
|
2415
2480
|
});
|
|
2416
2481
|
ws
|
|
2417
2482
|
.command('show')
|
|
2418
|
-
.description('Show worksheet detail')
|
|
2483
|
+
.description('Show worksheet detail. Pass --project <slug> (or --all-projects) to read a worksheet that lives in a sibling project.')
|
|
2419
2484
|
.argument('<id>', 'Worksheet ID')
|
|
2420
|
-
.
|
|
2485
|
+
.option('--project <slug>', 'Read a worksheet in this project (resolves cross-project 404s)')
|
|
2486
|
+
.option('--all-projects', 'Read org-wide without pinning to the active project', false)
|
|
2487
|
+
.action(async (id, opts) => {
|
|
2421
2488
|
try {
|
|
2489
|
+
const scope = readScopeOpts(opts);
|
|
2422
2490
|
// G4 (AC15): fetch enriched actions in parallel so we can display
|
|
2423
2491
|
// linked_pipeline status chips next to bound actions.
|
|
2424
2492
|
const [data, actionsResult] = await Promise.all([
|
|
2425
|
-
api('GET', `/api/worksheets/${id}
|
|
2426
|
-
api('GET', `/api/worksheets/${id}/actions
|
|
2493
|
+
api('GET', `/api/worksheets/${id}`, undefined, scope),
|
|
2494
|
+
api('GET', `/api/worksheets/${id}/actions`, undefined, scope).catch(() => null),
|
|
2427
2495
|
]);
|
|
2428
2496
|
const w = data.worksheet;
|
|
2429
2497
|
// Build a lookup map from action id → linked_pipeline (from the enriched endpoint)
|
|
@@ -2487,6 +2555,110 @@ ws
|
|
|
2487
2555
|
process.exit(1);
|
|
2488
2556
|
}
|
|
2489
2557
|
});
|
|
2558
|
+
// W-TOOL-2 — `ws report <id>`: one-call progress digest.
|
|
2559
|
+
//
|
|
2560
|
+
// Retires Ollie's manual Progress-Read / Check-in-Prep assembly, which today
|
|
2561
|
+
// stitches 3–4 separate `oc list` / `ac list` / `cm list` reads by hand. The
|
|
2562
|
+
// single GET /api/worksheets/:id already returns the full bundle (worksheet +
|
|
2563
|
+
// outcomes + actions + commitments + milestones), so the digest is composed
|
|
2564
|
+
// client-side from ONE call — no new endpoint required.
|
|
2565
|
+
//
|
|
2566
|
+
// (The existing /progress-board endpoint was evaluated first per the cheapest-
|
|
2567
|
+
// correct-implementation rule, but it omits actions entirely and caps outcomes
|
|
2568
|
+
// at 6, so it cannot render "action velocity" or a complete outcome scoreboard.
|
|
2569
|
+
// See the Test Implementation Brief for the API-aggregate follow-up flagged to
|
|
2570
|
+
// Atlas.)
|
|
2571
|
+
ws
|
|
2572
|
+
.command('report')
|
|
2573
|
+
.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.')
|
|
2574
|
+
.argument('<id>', 'Worksheet ID')
|
|
2575
|
+
.option('--json', 'Print the assembled report as JSON (machine-readable)')
|
|
2576
|
+
.option('--project <slug>', 'Read a worksheet in this project (resolves cross-project 404s)')
|
|
2577
|
+
.option('--all-projects', 'Read org-wide without pinning to the active project', false)
|
|
2578
|
+
.action(async (id, opts) => {
|
|
2579
|
+
try {
|
|
2580
|
+
const data = await api('GET', `/api/worksheets/${id}`, undefined, readScopeOpts(opts));
|
|
2581
|
+
const w = data.worksheet;
|
|
2582
|
+
const outcomes = data.outcomes || [];
|
|
2583
|
+
const actions = data.actions || [];
|
|
2584
|
+
const commitments = data.commitments || [];
|
|
2585
|
+
// Commitment roll-up: a commitment is "missed" if it is persisted missed
|
|
2586
|
+
// OR still open past its due date (mirrors the API's own progress-board
|
|
2587
|
+
// math: `status = 'missed' OR (status = 'open' AND due_date < today)`).
|
|
2588
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
2589
|
+
const isMissed = (c) => c.status === 'missed' || (c.status === 'open' && c.due_date && c.due_date < today);
|
|
2590
|
+
const isKept = (c) => c.status === 'completed';
|
|
2591
|
+
const isPending = (c) => c.status === 'open' && !isMissed(c);
|
|
2592
|
+
const kept = commitments.filter(isKept);
|
|
2593
|
+
const missed = commitments.filter(isMissed);
|
|
2594
|
+
const pending = commitments.filter(isPending);
|
|
2595
|
+
if (opts.json) {
|
|
2596
|
+
console.log(JSON.stringify({
|
|
2597
|
+
worksheet: { id: w.id, name: w.name, purpose: w.purpose, status: w.status },
|
|
2598
|
+
outcomes: outcomes.map(o => ({
|
|
2599
|
+
id: o.id,
|
|
2600
|
+
name: o.name,
|
|
2601
|
+
currentValue: o.current_value ?? null,
|
|
2602
|
+
targetValue: o.target_value ?? null,
|
|
2603
|
+
status: o.status,
|
|
2604
|
+
})),
|
|
2605
|
+
actions: actions.map(a => ({
|
|
2606
|
+
id: a.id,
|
|
2607
|
+
description: a.description,
|
|
2608
|
+
currentValue: a.current_value ?? null,
|
|
2609
|
+
targetValue: a.target_value ?? null,
|
|
2610
|
+
member: a.member_name ?? null,
|
|
2611
|
+
})),
|
|
2612
|
+
commitments: {
|
|
2613
|
+
kept: kept.length,
|
|
2614
|
+
missed: missed.length,
|
|
2615
|
+
pending: pending.length,
|
|
2616
|
+
total: commitments.length,
|
|
2617
|
+
items: commitments.map(c => ({
|
|
2618
|
+
id: c.id,
|
|
2619
|
+
description: c.description,
|
|
2620
|
+
dueDate: c.due_date,
|
|
2621
|
+
status: isMissed(c) ? 'missed' : isKept(c) ? 'kept' : 'pending',
|
|
2622
|
+
})),
|
|
2623
|
+
},
|
|
2624
|
+
}, null, 2));
|
|
2625
|
+
return;
|
|
2626
|
+
}
|
|
2627
|
+
console.log(chalk.cyan.bold(w.name) + chalk.gray(` (${w.status})`));
|
|
2628
|
+
console.log(` Purpose: ${w.purpose || chalk.gray('(none)')}`);
|
|
2629
|
+
console.log(chalk.cyan(`\n Outcomes (${outcomes.length}):`));
|
|
2630
|
+
if (outcomes.length === 0) {
|
|
2631
|
+
console.log(chalk.gray(' (none)'));
|
|
2632
|
+
}
|
|
2633
|
+
else {
|
|
2634
|
+
for (const o of outcomes) {
|
|
2635
|
+
const progress = o.target_value != null ? ` ${o.current_value ?? 0}/${o.target_value}` : '';
|
|
2636
|
+
const icon = o.status === 'achieved' ? chalk.green('✓') : '○';
|
|
2637
|
+
console.log(` ${icon} ${o.name}${progress} — ${chalk.yellow(o.status || 'unknown')}`);
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
console.log(chalk.cyan(`\n Action velocity (${actions.length}):`));
|
|
2641
|
+
if (actions.length === 0) {
|
|
2642
|
+
console.log(chalk.gray(' (none)'));
|
|
2643
|
+
}
|
|
2644
|
+
else {
|
|
2645
|
+
for (const a of actions) {
|
|
2646
|
+
const progress = a.target_value != null ? ` ${a.current_value ?? 0}/${a.target_value}` : '';
|
|
2647
|
+
console.log(` ${a.description}${progress} — ${a.member_name || 'unassigned'}`);
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
console.log(chalk.cyan(`\n Commitments: ${chalk.green(kept.length + ' kept')}, ${chalk.red(missed.length + ' missed')}, ${pending.length} pending (${commitments.length} total)`));
|
|
2651
|
+
for (const c of commitments) {
|
|
2652
|
+
const icon = isKept(c) ? chalk.green('✓') : isMissed(c) ? chalk.red('✗') : '○';
|
|
2653
|
+
const label = isMissed(c) ? 'missed' : isKept(c) ? 'kept' : 'pending';
|
|
2654
|
+
console.log(` ${icon} ${c.description} — due ${c.due_date} (${label})`);
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2657
|
+
catch (err) {
|
|
2658
|
+
console.error(chalk.red(err.message));
|
|
2659
|
+
process.exit(1);
|
|
2660
|
+
}
|
|
2661
|
+
});
|
|
2490
2662
|
// ── Outcomes ────────────────────────────────────────────────
|
|
2491
2663
|
const outcomes = program.command('outcomes').alias('oc').description('Outcome commands');
|
|
2492
2664
|
outcomes
|
|
@@ -2523,11 +2695,13 @@ outcomes
|
|
|
2523
2695
|
});
|
|
2524
2696
|
outcomes
|
|
2525
2697
|
.command('list')
|
|
2526
|
-
.description('List outcomes on a worksheet')
|
|
2698
|
+
.description('List outcomes on a worksheet. Pass --project <slug> (or --all-projects) to read a worksheet in a sibling project.')
|
|
2527
2699
|
.requiredOption('-w, --worksheet <id>', 'Worksheet ID')
|
|
2700
|
+
.option('--project <slug>', 'Read a worksheet in this project (resolves cross-project 404s)')
|
|
2701
|
+
.option('--all-projects', 'Read org-wide without pinning to the active project', false)
|
|
2528
2702
|
.action(async (opts) => {
|
|
2529
2703
|
try {
|
|
2530
|
-
const data = await api('GET', `/api/worksheets/${opts.worksheet}
|
|
2704
|
+
const data = await api('GET', `/api/worksheets/${opts.worksheet}`, undefined, readScopeOpts(opts));
|
|
2531
2705
|
if (!data.outcomes?.length) {
|
|
2532
2706
|
console.log(chalk.yellow('No outcomes'));
|
|
2533
2707
|
return;
|
|
@@ -2587,7 +2761,7 @@ outcomes
|
|
|
2587
2761
|
.option('-d, --description <desc>', 'New description')
|
|
2588
2762
|
.option('-m, --measurement <method>', 'How it is measured (e.g., "Count of active users")')
|
|
2589
2763
|
.option('-t, --target <value>', 'Target value (numeric)', parseNumber)
|
|
2590
|
-
.option('-s, --status <status>', 'Status:
|
|
2764
|
+
.option('-s, --status <status>', 'Status: on_track, at_risk, behind, or achieved')
|
|
2591
2765
|
.action(async (opts) => {
|
|
2592
2766
|
try {
|
|
2593
2767
|
const body = {};
|
|
@@ -2952,28 +3126,135 @@ members
|
|
|
2952
3126
|
const actions = program.command('actions').alias('ac').description('Action (lead measure) commands');
|
|
2953
3127
|
actions
|
|
2954
3128
|
.command('add')
|
|
2955
|
-
.description('Add
|
|
3129
|
+
.description('Add one or more actions to a worksheet.\n' +
|
|
3130
|
+
' Single: ac add -w <id> -n "<desc>" [-m <measurement>] [-t <target>] [-c <current>] [-o <outcomeId>]\n' +
|
|
3131
|
+
' Batch: ac add -w <id> --action "<desc>" --action "<desc>" ... (repeatable, description-only)\n' +
|
|
3132
|
+
' Batch: ac add -w <id> --file actions.json (JSON array of full action objects)\n' +
|
|
3133
|
+
' Preview: add --dry-run to print exactly what would be written without creating anything.')
|
|
2956
3134
|
.requiredOption('-w, --worksheet <id>', 'Worksheet ID')
|
|
2957
|
-
.
|
|
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)
|
|
3135
|
+
.option('-n, --name <name>', 'Action description (single-add form)')
|
|
3136
|
+
.option('-o, --outcome <id>', 'Linked outcome ID (single-add form)')
|
|
3137
|
+
.option('-m, --measurement <method>', 'How it is measured (single-add form)')
|
|
3138
|
+
.option('-t, --target <value>', 'Target value (single-add form)', parseNumber)
|
|
3139
|
+
.option('-c, --current <value>', 'Current value (single-add form)', parseNumber)
|
|
3140
|
+
.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; })
|
|
3141
|
+
.option('--file <path>', 'Batch: path to a JSON array of action objects ({ description|name, measurement?, target?, current?, outcomeId? }). "@file.json" is also accepted.')
|
|
3142
|
+
.option('--dry-run', 'Preview the action(s) that would be created without writing anything', false)
|
|
2962
3143
|
.action(async (opts) => {
|
|
2963
3144
|
try {
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
body
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
3145
|
+
// Normalize a raw item (from --file or a flag) into an API body.
|
|
3146
|
+
const toBody = (item) => {
|
|
3147
|
+
const description = item.description ?? item.name;
|
|
3148
|
+
if (!description || typeof description !== 'string' || description.trim().length === 0) {
|
|
3149
|
+
throw new Error('each action needs a non-empty "description" (or "name")');
|
|
3150
|
+
}
|
|
3151
|
+
const body = { description };
|
|
3152
|
+
const outcomeId = item.outcomeId ?? item.outcome;
|
|
3153
|
+
const measurement = item.measurement;
|
|
3154
|
+
const target = item.targetValue ?? item.target;
|
|
3155
|
+
const current = item.currentValue ?? item.current;
|
|
3156
|
+
if (outcomeId != null)
|
|
3157
|
+
body.outcomeId = outcomeId;
|
|
3158
|
+
if (measurement != null)
|
|
3159
|
+
body.measurement = measurement;
|
|
3160
|
+
if (target != null)
|
|
3161
|
+
body.targetValue = typeof target === 'number' ? target : parseNumber(String(target));
|
|
3162
|
+
if (current != null)
|
|
3163
|
+
body.currentValue = typeof current === 'number' ? current : parseNumber(String(current));
|
|
3164
|
+
return body;
|
|
3165
|
+
};
|
|
3166
|
+
// Resolve the source of truth for what to create. Exactly one of:
|
|
3167
|
+
// --file | --action (repeatable) | -n/--name (single form)
|
|
3168
|
+
const batchActions = opts.action;
|
|
3169
|
+
let filePath = opts.file;
|
|
3170
|
+
// Support the "@file.json" convention (a bare @-prefixed --file value).
|
|
3171
|
+
if (filePath && filePath.startsWith('@'))
|
|
3172
|
+
filePath = filePath.slice(1);
|
|
3173
|
+
const sources = [
|
|
3174
|
+
filePath ? 'file' : null,
|
|
3175
|
+
batchActions && batchActions.length ? 'action' : null,
|
|
3176
|
+
opts.name ? 'name' : null,
|
|
3177
|
+
].filter(Boolean);
|
|
3178
|
+
if (sources.length === 0) {
|
|
3179
|
+
console.error(chalk.red('Provide -n/--name (single), --action (repeatable), or --file (JSON batch).'));
|
|
3180
|
+
process.exit(1);
|
|
3181
|
+
}
|
|
3182
|
+
if (sources.length > 1) {
|
|
3183
|
+
console.error(chalk.red(`Choose one input form — got ${sources.join(' + ')}. Combine measurement/target with the --file form.`));
|
|
3184
|
+
process.exit(1);
|
|
3185
|
+
}
|
|
3186
|
+
// Build the list of bodies.
|
|
3187
|
+
let bodies;
|
|
3188
|
+
if (filePath) {
|
|
3189
|
+
let parsed;
|
|
3190
|
+
try {
|
|
3191
|
+
parsed = JSON.parse(readFileSync(resolve(filePath), 'utf-8'));
|
|
3192
|
+
}
|
|
3193
|
+
catch (e) {
|
|
3194
|
+
console.error(chalk.red(`Could not read/parse ${filePath}: ${e.message}`));
|
|
3195
|
+
process.exit(1);
|
|
3196
|
+
}
|
|
3197
|
+
const arr = Array.isArray(parsed) ? parsed : (Array.isArray(parsed?.actions) ? parsed.actions : null);
|
|
3198
|
+
if (!arr) {
|
|
3199
|
+
console.error(chalk.red('The --file must contain a JSON array of action objects (or an { "actions": [...] } wrapper).'));
|
|
3200
|
+
process.exit(1);
|
|
3201
|
+
}
|
|
3202
|
+
if (arr.length === 0) {
|
|
3203
|
+
console.error(chalk.red('The --file action array is empty.'));
|
|
3204
|
+
process.exit(1);
|
|
3205
|
+
}
|
|
3206
|
+
bodies = arr.map(toBody);
|
|
3207
|
+
}
|
|
3208
|
+
else if (batchActions && batchActions.length) {
|
|
3209
|
+
bodies = batchActions.map((desc) => toBody({ description: desc }));
|
|
3210
|
+
}
|
|
3211
|
+
else {
|
|
3212
|
+
bodies = [toBody({ description: opts.name, outcome: opts.outcome, measurement: opts.measurement, target: opts.target, current: opts.current })];
|
|
3213
|
+
}
|
|
3214
|
+
// Dry-run preview — no network calls.
|
|
3215
|
+
if (opts.dryRun) {
|
|
3216
|
+
console.log(chalk.cyan(`Dry run — ${bodies.length} action(s) would be created on worksheet ${opts.worksheet}:`));
|
|
3217
|
+
bodies.forEach((b, i) => {
|
|
3218
|
+
const extras = [];
|
|
3219
|
+
if (b.measurement)
|
|
3220
|
+
extras.push(`measurement="${b.measurement}"`);
|
|
3221
|
+
if (b.targetValue != null)
|
|
3222
|
+
extras.push(`target=${b.targetValue}`);
|
|
3223
|
+
if (b.currentValue != null)
|
|
3224
|
+
extras.push(`current=${b.currentValue}`);
|
|
3225
|
+
if (b.outcomeId)
|
|
3226
|
+
extras.push(`outcome=${b.outcomeId}`);
|
|
3227
|
+
const tail = extras.length ? chalk.gray(` [${extras.join(', ')}]`) : '';
|
|
3228
|
+
console.log(` ${i + 1}. ${b.description}${tail}`);
|
|
3229
|
+
});
|
|
3230
|
+
console.log(chalk.gray('Nothing was written. Re-run without --dry-run to create these actions.'));
|
|
3231
|
+
return;
|
|
3232
|
+
}
|
|
3233
|
+
// Real create. Single form keeps its original one-line success output;
|
|
3234
|
+
// batch form reports each created ID and a final summary.
|
|
3235
|
+
if (bodies.length === 1 && sources[0] === 'name') {
|
|
3236
|
+
const data = await api('POST', `/api/worksheets/${opts.worksheet}/actions`, bodies[0]);
|
|
3237
|
+
const a = data.action;
|
|
3238
|
+
console.log(chalk.green(`Added action: ${a.description}`));
|
|
3239
|
+
console.log(` ID: ${a.id}`);
|
|
3240
|
+
return;
|
|
3241
|
+
}
|
|
3242
|
+
let created = 0;
|
|
3243
|
+
for (let i = 0; i < bodies.length; i++) {
|
|
3244
|
+
const b = bodies[i];
|
|
3245
|
+
try {
|
|
3246
|
+
const data = await api('POST', `/api/worksheets/${opts.worksheet}/actions`, b);
|
|
3247
|
+
const a = data.action;
|
|
3248
|
+
created++;
|
|
3249
|
+
console.log(chalk.green(` ✓ [${i + 1}/${bodies.length}] ${a.description} — ${a.id}`));
|
|
3250
|
+
}
|
|
3251
|
+
catch (e) {
|
|
3252
|
+
console.error(chalk.red(` ✗ [${i + 1}/${bodies.length}] ${b.description} — ${e.message}`));
|
|
3253
|
+
console.error(chalk.yellow(`Stopped after ${created} of ${bodies.length} created. Fix the failing item and re-run for the remainder.`));
|
|
3254
|
+
process.exit(1);
|
|
3255
|
+
}
|
|
3256
|
+
}
|
|
3257
|
+
console.log(chalk.green(`Created ${created} action(s).`));
|
|
2977
3258
|
}
|
|
2978
3259
|
catch (err) {
|
|
2979
3260
|
console.error(chalk.red(err.message));
|
|
@@ -2982,11 +3263,13 @@ actions
|
|
|
2982
3263
|
});
|
|
2983
3264
|
actions
|
|
2984
3265
|
.command('list')
|
|
2985
|
-
.description('List actions on a worksheet')
|
|
3266
|
+
.description('List actions on a worksheet. Pass --project <slug> (or --all-projects) to read a worksheet in a sibling project.')
|
|
2986
3267
|
.requiredOption('-w, --worksheet <id>', 'Worksheet ID')
|
|
3268
|
+
.option('--project <slug>', 'Read a worksheet in this project (resolves cross-project 404s)')
|
|
3269
|
+
.option('--all-projects', 'Read org-wide without pinning to the active project', false)
|
|
2987
3270
|
.action(async (opts) => {
|
|
2988
3271
|
try {
|
|
2989
|
-
const data = await api('GET', `/api/worksheets/${opts.worksheet}/actions
|
|
3272
|
+
const data = await api('GET', `/api/worksheets/${opts.worksheet}/actions`, undefined, readScopeOpts(opts));
|
|
2990
3273
|
if (!data.actions?.length) {
|
|
2991
3274
|
console.log(chalk.yellow('No actions'));
|
|
2992
3275
|
return;
|
|
@@ -3049,6 +3332,21 @@ actions
|
|
|
3049
3332
|
process.exit(1);
|
|
3050
3333
|
}
|
|
3051
3334
|
});
|
|
3335
|
+
actions
|
|
3336
|
+
.command('remove')
|
|
3337
|
+
.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.')
|
|
3338
|
+
.requiredOption('-w, --worksheet <id>', 'Worksheet ID')
|
|
3339
|
+
.requiredOption('-a, --action <id>', 'Action ID (from "ac list" or "ws show" output)')
|
|
3340
|
+
.action(async (opts) => {
|
|
3341
|
+
try {
|
|
3342
|
+
await api('DELETE', `/api/worksheets/${opts.worksheet}/actions/${opts.action}`);
|
|
3343
|
+
console.log(chalk.green('Action deleted'));
|
|
3344
|
+
}
|
|
3345
|
+
catch (err) {
|
|
3346
|
+
console.error(chalk.red(err.message));
|
|
3347
|
+
process.exit(1);
|
|
3348
|
+
}
|
|
3349
|
+
});
|
|
3052
3350
|
actions
|
|
3053
3351
|
.command('execute')
|
|
3054
3352
|
.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.')
|
|
@@ -3075,11 +3373,17 @@ commitments
|
|
|
3075
3373
|
.requiredOption('-d, --description <desc>', 'What you commit to do')
|
|
3076
3374
|
.option('-a, --action <id>', 'Linked action ID')
|
|
3077
3375
|
.requiredOption('--due <date>', 'Due date (YYYY-MM-DD)')
|
|
3376
|
+
.option('--allow-past-due', 'Back-date a still-open commitment (admin/owner only)')
|
|
3078
3377
|
.action(async (opts) => {
|
|
3079
3378
|
try {
|
|
3080
3379
|
const body = { description: opts.description, dueDate: opts.due };
|
|
3081
3380
|
if (opts.action)
|
|
3082
3381
|
body.actionId = opts.action;
|
|
3382
|
+
// W-TOOL-7: opt-in back-dating of a still-open commitment. Gated to org
|
|
3383
|
+
// admin/owner server-side (403 otherwise); omit the flag to leave the
|
|
3384
|
+
// default forward-dating behavior unchanged.
|
|
3385
|
+
if (opts.allowPastDue)
|
|
3386
|
+
body.allowPastDue = true;
|
|
3083
3387
|
const data = await api('POST', `/api/worksheets/${opts.worksheet}/commitments`, body);
|
|
3084
3388
|
const c = data.commitment;
|
|
3085
3389
|
console.log(chalk.green(`Commitment made: ${c.description}`));
|
|
@@ -3093,11 +3397,13 @@ commitments
|
|
|
3093
3397
|
});
|
|
3094
3398
|
commitments
|
|
3095
3399
|
.command('list')
|
|
3096
|
-
.description('List commitments on a worksheet')
|
|
3400
|
+
.description('List commitments on a worksheet. Pass --project <slug> (or --all-projects) to read a worksheet in a sibling project.')
|
|
3097
3401
|
.requiredOption('-w, --worksheet <id>', 'Worksheet ID')
|
|
3402
|
+
.option('--project <slug>', 'Read a worksheet in this project (resolves cross-project 404s)')
|
|
3403
|
+
.option('--all-projects', 'Read org-wide without pinning to the active project', false)
|
|
3098
3404
|
.action(async (opts) => {
|
|
3099
3405
|
try {
|
|
3100
|
-
const data = await api('GET', `/api/worksheets/${opts.worksheet}/commitments
|
|
3406
|
+
const data = await api('GET', `/api/worksheets/${opts.worksheet}/commitments`, undefined, readScopeOpts(opts));
|
|
3101
3407
|
if (!data.commitments?.length) {
|
|
3102
3408
|
console.log(chalk.yellow('No commitments'));
|
|
3103
3409
|
return;
|
|
@@ -4768,19 +5074,50 @@ templates
|
|
|
4768
5074
|
});
|
|
4769
5075
|
// ── Pipelines ──────────────────────────────────────────────
|
|
4770
5076
|
const pipelines = program.command('pipelines').alias('pl').description('Pipeline instance commands');
|
|
5077
|
+
// Keep this in sync with the `pl list --status` help text below. A typo like
|
|
5078
|
+
// `--status runing` would otherwise pass straight through to the API, exact-
|
|
5079
|
+
// match nothing, and print "No pipeline instances" — reading as "nothing
|
|
5080
|
+
// running" rather than "you mistyped". We reject unknown values up front with
|
|
5081
|
+
// the same allow-list pattern used by `orgs members` / `orgs invites list`.
|
|
5082
|
+
const PL_LIST_STATUSES = [
|
|
5083
|
+
'active',
|
|
5084
|
+
'all',
|
|
5085
|
+
'created',
|
|
5086
|
+
'running',
|
|
5087
|
+
'paused',
|
|
5088
|
+
'completed',
|
|
5089
|
+
'failed',
|
|
5090
|
+
'cancelled',
|
|
5091
|
+
'archived',
|
|
5092
|
+
];
|
|
4771
5093
|
pipelines
|
|
4772
5094
|
.command('list')
|
|
4773
5095
|
.description('List pipeline instances in the active project (or all projects with --all-projects)')
|
|
4774
|
-
.option('-s, --status <status>', 'Filter by status (
|
|
5096
|
+
.option('-s, --status <status>', 'Filter by status: active (created/running/paused, the default), running, created, paused, completed, failed, cancelled, archived, or all', 'active')
|
|
4775
5097
|
.option('--search <term>', 'Search by name or ID')
|
|
4776
5098
|
.option('--expand', 'Include steps inline (avoids N+1)')
|
|
4777
5099
|
.option('--project <slug>', 'Override active project for this call')
|
|
4778
5100
|
.option('--all-projects', 'List pipelines across every project in the active org', false)
|
|
4779
5101
|
.action(async (opts) => {
|
|
4780
5102
|
try {
|
|
5103
|
+
const status = opts.status || 'active';
|
|
5104
|
+
if (!PL_LIST_STATUSES.includes(status)) {
|
|
5105
|
+
console.error(chalk.red(`Invalid --status "${status}". Allowed values: ${PL_LIST_STATUSES.join(', ')}`));
|
|
5106
|
+
process.exit(1);
|
|
5107
|
+
}
|
|
4781
5108
|
const params = [];
|
|
4782
|
-
|
|
4783
|
-
|
|
5109
|
+
// Status → API mapping. The API treats a bare `status` value as an exact
|
|
5110
|
+
// DB status match, and omitting it entirely as the "active" group
|
|
5111
|
+
// (created/running/paused). In-flight instances carry DB status `running`,
|
|
5112
|
+
// which no literal `active` value would ever match — so `active` (and the
|
|
5113
|
+
// default) must map to "omit the param" rather than `status=active`, or
|
|
5114
|
+
// `pl list` reports "No instances" while pipelines are actually running.
|
|
5115
|
+
// `all` → `status=all` (API disables the filter). Every real DB status
|
|
5116
|
+
// (running, created, paused, completed, failed, cancelled, archived) is
|
|
5117
|
+
// passed through verbatim.
|
|
5118
|
+
if (status && status !== 'active') {
|
|
5119
|
+
params.push(`status=${status}`);
|
|
5120
|
+
}
|
|
4784
5121
|
if (opts.search)
|
|
4785
5122
|
params.push(`search=${encodeURIComponent(opts.search)}`);
|
|
4786
5123
|
if (opts.expand)
|