@formigio/fazemos-cli 0.10.46 → 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/config.d.ts +11 -0
- package/dist/config.js +49 -6
- package/dist/config.js.map +1 -1
- package/dist/index.js +372 -41
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from 'commander';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
|
-
import { config, getEnv, getToken, getActiveOrgId, setActiveOrgId, addEnvironment, hasEnvironments,
|
|
4
|
+
import { config, getEnv, getToken, getActiveEnvName, getActiveEnvSource, setEnvOverride, getActiveOrgId, setActiveOrgId, addEnvironment, hasEnvironments,
|
|
5
5
|
// F15 — project context helpers
|
|
6
6
|
getActiveProjectId, setActiveProjectId, clearActiveProjectId, findProjectBySlug, findProjectById, findOrgById,
|
|
7
7
|
// Directory-scoped context override (.fazemos.json)
|
|
@@ -199,7 +199,12 @@ const program = new Command();
|
|
|
199
199
|
program
|
|
200
200
|
.name('fazemos')
|
|
201
201
|
.description('Fazemos CLI — Team Accomplishment Platform')
|
|
202
|
-
.version(pkg.version)
|
|
202
|
+
.version(pkg.version)
|
|
203
|
+
// Session-scoped environment override. Precedence: this flag > FAZEMOS_ENV
|
|
204
|
+
// env var > the persisted active env. Lets one command (or, via the env var,
|
|
205
|
+
// a whole session) target a different environment without a shared-disk
|
|
206
|
+
// `fazemos env` switch that would clobber a concurrent session.
|
|
207
|
+
.option('--env <name>', 'Environment to use for this invocation (overrides FAZEMOS_ENV and the persisted active env)');
|
|
203
208
|
// ── Directory-scoped default org/project (.fazemos.json) ─────
|
|
204
209
|
//
|
|
205
210
|
// Before any command action runs, discover the nearest `.fazemos.json`
|
|
@@ -266,6 +271,11 @@ async function applyDirContext(topCommand) {
|
|
|
266
271
|
}
|
|
267
272
|
}
|
|
268
273
|
program.hook('preAction', async (_thisCommand, actionCommand) => {
|
|
274
|
+
// Apply the global `--env` flag first so every downstream resolution (dir
|
|
275
|
+
// context slug lookups, auth, API calls) sees the right environment.
|
|
276
|
+
const envFlag = program.opts().env;
|
|
277
|
+
if (envFlag)
|
|
278
|
+
setEnvOverride(envFlag);
|
|
269
279
|
// Resolve the top-level command name (e.g. `worksheets list` → `worksheets`).
|
|
270
280
|
let c = actionCommand;
|
|
271
281
|
let top = c?.name?.();
|
|
@@ -332,8 +342,8 @@ program
|
|
|
332
342
|
// ── Environment ─────────────────────────────────────────────
|
|
333
343
|
program
|
|
334
344
|
.command('env')
|
|
335
|
-
.description('Show or switch environment')
|
|
336
|
-
.argument('[name]', 'Environment to switch to')
|
|
345
|
+
.description('Show or switch environment. The persisted switch is machine-global; for per-session isolation set FAZEMOS_ENV or pass --env.')
|
|
346
|
+
.argument('[name]', 'Environment to switch to (persisted, machine-global)')
|
|
337
347
|
.action((name) => {
|
|
338
348
|
if (!hasEnvironments()) {
|
|
339
349
|
console.log(chalk.yellow('No environments configured. Run: fazemos init <name> --api-url <url>'));
|
|
@@ -347,10 +357,25 @@ program
|
|
|
347
357
|
process.exit(1);
|
|
348
358
|
}
|
|
349
359
|
config.set('activeEnv', name);
|
|
350
|
-
console.log(chalk.green(`Switched to ${name}`));
|
|
360
|
+
console.log(chalk.green(`Switched persisted environment to ${name}`));
|
|
361
|
+
// A persisted switch is machine-global. If this session is pinned to a
|
|
362
|
+
// different env via the override chain, the switch won't take effect here
|
|
363
|
+
// until the override is cleared — say so rather than silently no-op'ing.
|
|
364
|
+
const src = getActiveEnvSource();
|
|
365
|
+
if ((src === 'session' || src === 'flag') && getActiveEnvName() !== name) {
|
|
366
|
+
const via = src === 'flag' ? '--env' : 'FAZEMOS_ENV';
|
|
367
|
+
console.log(chalk.yellow(`Note: this session is pinned to "${getActiveEnvName()}" via ${via}; ` +
|
|
368
|
+
`the persisted switch won't take effect here until that override is cleared.`));
|
|
369
|
+
}
|
|
351
370
|
}
|
|
352
371
|
const env = getEnv();
|
|
353
|
-
|
|
372
|
+
const src = getActiveEnvSource();
|
|
373
|
+
const srcLabel = src === 'flag'
|
|
374
|
+
? chalk.gray(' (this invocation, via --env)')
|
|
375
|
+
: src === 'session'
|
|
376
|
+
? chalk.gray(' (this session, via FAZEMOS_ENV)')
|
|
377
|
+
: '';
|
|
378
|
+
console.log(` Environment: ${chalk.cyan(env.name)}${srcLabel}`);
|
|
354
379
|
console.log(` API: ${env.apiUrl}`);
|
|
355
380
|
console.log(` Cognito: ${env.cognitoPoolId}`);
|
|
356
381
|
const token = getToken();
|
|
@@ -433,11 +458,11 @@ auth
|
|
|
433
458
|
.command('logout')
|
|
434
459
|
.description('Clear stored credentials')
|
|
435
460
|
.action(() => {
|
|
436
|
-
const env =
|
|
461
|
+
const env = getActiveEnvName();
|
|
437
462
|
const auths = config.get('auth');
|
|
438
463
|
delete auths[env];
|
|
439
464
|
config.set('auth', auths);
|
|
440
|
-
console.log(chalk.green(
|
|
465
|
+
console.log(chalk.green(`Logged out of ${env}`));
|
|
441
466
|
});
|
|
442
467
|
// ── Whoami ──────────────────────────────────────────────────
|
|
443
468
|
program
|
|
@@ -459,6 +484,13 @@ program
|
|
|
459
484
|
}
|
|
460
485
|
console.log(` User: ${chalk.cyan(data.user.email)}`);
|
|
461
486
|
console.log(` Member: ${data.member.displayName} (${data.member.role})`);
|
|
487
|
+
const envSrc = getActiveEnvSource();
|
|
488
|
+
const envSrcLabel = envSrc === 'flag'
|
|
489
|
+
? chalk.gray(' (this invocation, via --env)')
|
|
490
|
+
: envSrc === 'session'
|
|
491
|
+
? chalk.gray(' (this session, via FAZEMOS_ENV)')
|
|
492
|
+
: '';
|
|
493
|
+
console.log(` Env: ${chalk.cyan(getActiveEnvName())}${envSrcLabel}`);
|
|
462
494
|
const activeOrgId = getActiveOrgId() ?? data.activeOrgId;
|
|
463
495
|
const activeOrg = data.orgs.find(o => o.id === activeOrgId);
|
|
464
496
|
if (activeOrg) {
|
|
@@ -1229,6 +1261,39 @@ function projectOpts(opts) {
|
|
|
1229
1261
|
allProjects: opts.allProjects,
|
|
1230
1262
|
};
|
|
1231
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
|
+
}
|
|
1232
1297
|
/**
|
|
1233
1298
|
* Uniform error handler for scoped commands. When the API emits
|
|
1234
1299
|
* MISSING_PROJECT_CONTEXT (§7.3.1), the api helper has already re-shaped
|
|
@@ -2383,15 +2448,18 @@ ws
|
|
|
2383
2448
|
});
|
|
2384
2449
|
ws
|
|
2385
2450
|
.command('show')
|
|
2386
|
-
.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.')
|
|
2387
2452
|
.argument('<id>', 'Worksheet ID')
|
|
2388
|
-
.
|
|
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) => {
|
|
2389
2456
|
try {
|
|
2457
|
+
const scope = readScopeOpts(opts);
|
|
2390
2458
|
// G4 (AC15): fetch enriched actions in parallel so we can display
|
|
2391
2459
|
// linked_pipeline status chips next to bound actions.
|
|
2392
2460
|
const [data, actionsResult] = await Promise.all([
|
|
2393
|
-
api('GET', `/api/worksheets/${id}
|
|
2394
|
-
api('GET', `/api/worksheets/${id}/actions
|
|
2461
|
+
api('GET', `/api/worksheets/${id}`, undefined, scope),
|
|
2462
|
+
api('GET', `/api/worksheets/${id}/actions`, undefined, scope).catch(() => null),
|
|
2395
2463
|
]);
|
|
2396
2464
|
const w = data.worksheet;
|
|
2397
2465
|
// Build a lookup map from action id → linked_pipeline (from the enriched endpoint)
|
|
@@ -2455,6 +2523,110 @@ ws
|
|
|
2455
2523
|
process.exit(1);
|
|
2456
2524
|
}
|
|
2457
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
|
+
});
|
|
2458
2630
|
// ── Outcomes ────────────────────────────────────────────────
|
|
2459
2631
|
const outcomes = program.command('outcomes').alias('oc').description('Outcome commands');
|
|
2460
2632
|
outcomes
|
|
@@ -2491,11 +2663,13 @@ outcomes
|
|
|
2491
2663
|
});
|
|
2492
2664
|
outcomes
|
|
2493
2665
|
.command('list')
|
|
2494
|
-
.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.')
|
|
2495
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)
|
|
2496
2670
|
.action(async (opts) => {
|
|
2497
2671
|
try {
|
|
2498
|
-
const data = await api('GET', `/api/worksheets/${opts.worksheet}
|
|
2672
|
+
const data = await api('GET', `/api/worksheets/${opts.worksheet}`, undefined, readScopeOpts(opts));
|
|
2499
2673
|
if (!data.outcomes?.length) {
|
|
2500
2674
|
console.log(chalk.yellow('No outcomes'));
|
|
2501
2675
|
return;
|
|
@@ -2555,7 +2729,7 @@ outcomes
|
|
|
2555
2729
|
.option('-d, --description <desc>', 'New description')
|
|
2556
2730
|
.option('-m, --measurement <method>', 'How it is measured (e.g., "Count of active users")')
|
|
2557
2731
|
.option('-t, --target <value>', 'Target value (numeric)', parseNumber)
|
|
2558
|
-
.option('-s, --status <status>', 'Status:
|
|
2732
|
+
.option('-s, --status <status>', 'Status: on_track, at_risk, behind, or achieved')
|
|
2559
2733
|
.action(async (opts) => {
|
|
2560
2734
|
try {
|
|
2561
2735
|
const body = {};
|
|
@@ -2920,28 +3094,135 @@ members
|
|
|
2920
3094
|
const actions = program.command('actions').alias('ac').description('Action (lead measure) commands');
|
|
2921
3095
|
actions
|
|
2922
3096
|
.command('add')
|
|
2923
|
-
.description('Add
|
|
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.')
|
|
2924
3102
|
.requiredOption('-w, --worksheet <id>', 'Worksheet ID')
|
|
2925
|
-
.
|
|
2926
|
-
.option('-o, --outcome <id>', 'Linked outcome ID')
|
|
2927
|
-
.option('-m, --measurement <method>', 'How it is measured')
|
|
2928
|
-
.option('-t, --target <value>', 'Target value', parseNumber)
|
|
2929
|
-
.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)
|
|
2930
3111
|
.action(async (opts) => {
|
|
2931
3112
|
try {
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
body
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
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).`));
|
|
2945
3226
|
}
|
|
2946
3227
|
catch (err) {
|
|
2947
3228
|
console.error(chalk.red(err.message));
|
|
@@ -2950,11 +3231,13 @@ actions
|
|
|
2950
3231
|
});
|
|
2951
3232
|
actions
|
|
2952
3233
|
.command('list')
|
|
2953
|
-
.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.')
|
|
2954
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)
|
|
2955
3238
|
.action(async (opts) => {
|
|
2956
3239
|
try {
|
|
2957
|
-
const data = await api('GET', `/api/worksheets/${opts.worksheet}/actions
|
|
3240
|
+
const data = await api('GET', `/api/worksheets/${opts.worksheet}/actions`, undefined, readScopeOpts(opts));
|
|
2958
3241
|
if (!data.actions?.length) {
|
|
2959
3242
|
console.log(chalk.yellow('No actions'));
|
|
2960
3243
|
return;
|
|
@@ -3017,6 +3300,21 @@ actions
|
|
|
3017
3300
|
process.exit(1);
|
|
3018
3301
|
}
|
|
3019
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
|
+
});
|
|
3020
3318
|
actions
|
|
3021
3319
|
.command('execute')
|
|
3022
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.')
|
|
@@ -3061,11 +3359,13 @@ commitments
|
|
|
3061
3359
|
});
|
|
3062
3360
|
commitments
|
|
3063
3361
|
.command('list')
|
|
3064
|
-
.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.')
|
|
3065
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)
|
|
3066
3366
|
.action(async (opts) => {
|
|
3067
3367
|
try {
|
|
3068
|
-
const data = await api('GET', `/api/worksheets/${opts.worksheet}/commitments
|
|
3368
|
+
const data = await api('GET', `/api/worksheets/${opts.worksheet}/commitments`, undefined, readScopeOpts(opts));
|
|
3069
3369
|
if (!data.commitments?.length) {
|
|
3070
3370
|
console.log(chalk.yellow('No commitments'));
|
|
3071
3371
|
return;
|
|
@@ -4736,19 +5036,50 @@ templates
|
|
|
4736
5036
|
});
|
|
4737
5037
|
// ── Pipelines ──────────────────────────────────────────────
|
|
4738
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
|
+
];
|
|
4739
5055
|
pipelines
|
|
4740
5056
|
.command('list')
|
|
4741
5057
|
.description('List pipeline instances in the active project (or all projects with --all-projects)')
|
|
4742
|
-
.option('-s, --status <status>', 'Filter by status (
|
|
5058
|
+
.option('-s, --status <status>', 'Filter by status: active (created/running/paused, the default), running, created, paused, completed, failed, cancelled, archived, or all', 'active')
|
|
4743
5059
|
.option('--search <term>', 'Search by name or ID')
|
|
4744
5060
|
.option('--expand', 'Include steps inline (avoids N+1)')
|
|
4745
5061
|
.option('--project <slug>', 'Override active project for this call')
|
|
4746
5062
|
.option('--all-projects', 'List pipelines across every project in the active org', false)
|
|
4747
5063
|
.action(async (opts) => {
|
|
4748
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
|
+
}
|
|
4749
5070
|
const params = [];
|
|
4750
|
-
|
|
4751
|
-
|
|
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
|
+
}
|
|
4752
5083
|
if (opts.search)
|
|
4753
5084
|
params.push(`search=${encodeURIComponent(opts.search)}`);
|
|
4754
5085
|
if (opts.expand)
|