@formigio/fazemos-cli 0.2.4 → 0.4.0
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 +1373 -138
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -224,6 +224,32 @@ orgs
|
|
|
224
224
|
process.exit(1);
|
|
225
225
|
}
|
|
226
226
|
});
|
|
227
|
+
orgs
|
|
228
|
+
.command('show')
|
|
229
|
+
.description('Show organization details. Defaults to the active org if no ID is provided. Use "orgs list" to find org IDs.')
|
|
230
|
+
.argument('[id]', 'Organization ID (defaults to active org)')
|
|
231
|
+
.action(async (id) => {
|
|
232
|
+
try {
|
|
233
|
+
const orgId = id || getActiveOrgId();
|
|
234
|
+
if (!orgId) {
|
|
235
|
+
console.error(chalk.red('No active org. Run: fazemos orgs switch <slug>'));
|
|
236
|
+
process.exit(1);
|
|
237
|
+
}
|
|
238
|
+
const data = await api('GET', `/api/organizations/${orgId}`);
|
|
239
|
+
const org = data.organization;
|
|
240
|
+
console.log(chalk.cyan(org.name));
|
|
241
|
+
console.log(` ID: ${org.id}`);
|
|
242
|
+
console.log(` Slug: ${org.slug}`);
|
|
243
|
+
if (org.description)
|
|
244
|
+
console.log(` Desc: ${org.description}`);
|
|
245
|
+
if (org.member_count != null)
|
|
246
|
+
console.log(` Members: ${org.member_count}`);
|
|
247
|
+
}
|
|
248
|
+
catch (err) {
|
|
249
|
+
console.error(chalk.red(err.message));
|
|
250
|
+
process.exit(1);
|
|
251
|
+
}
|
|
252
|
+
});
|
|
227
253
|
orgs
|
|
228
254
|
.command('switch')
|
|
229
255
|
.description('Switch active organization')
|
|
@@ -264,6 +290,82 @@ orgs
|
|
|
264
290
|
process.exit(1);
|
|
265
291
|
}
|
|
266
292
|
});
|
|
293
|
+
orgs
|
|
294
|
+
.command('update')
|
|
295
|
+
.description('Update the active organization. Provide at least one of --name or --description. Requires an active org (use "orgs switch" first).')
|
|
296
|
+
.option('-n, --name <name>', 'New organization name')
|
|
297
|
+
.option('-d, --description <desc>', 'New description')
|
|
298
|
+
.action(async (opts) => {
|
|
299
|
+
try {
|
|
300
|
+
if (!opts.name && opts.description == null) {
|
|
301
|
+
console.error(chalk.red('Provide --name and/or --description'));
|
|
302
|
+
process.exit(1);
|
|
303
|
+
}
|
|
304
|
+
const orgId = getActiveOrgId();
|
|
305
|
+
if (!orgId) {
|
|
306
|
+
console.error(chalk.red('No active org. Run: fazemos orgs switch <slug>'));
|
|
307
|
+
process.exit(1);
|
|
308
|
+
}
|
|
309
|
+
const body = {};
|
|
310
|
+
if (opts.name)
|
|
311
|
+
body.name = opts.name;
|
|
312
|
+
if (opts.description != null)
|
|
313
|
+
body.description = opts.description;
|
|
314
|
+
const data = await api('PATCH', `/api/organizations/${orgId}`, body);
|
|
315
|
+
console.log(chalk.green(`Updated: ${data.organization.name}`));
|
|
316
|
+
}
|
|
317
|
+
catch (err) {
|
|
318
|
+
console.error(chalk.red(err.message));
|
|
319
|
+
process.exit(1);
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
orgs
|
|
323
|
+
.command('invite')
|
|
324
|
+
.description('Invite a human member to the organization by email. The invitee receives an email to join. Use "orgs add-agent" for AI agents instead.')
|
|
325
|
+
.requiredOption('-e, --email <email>', 'Email address to invite')
|
|
326
|
+
.option('-r, --role <role>', 'Organization role: admin (full access) or member (default)', 'member')
|
|
327
|
+
.action(async (opts) => {
|
|
328
|
+
try {
|
|
329
|
+
const orgId = getActiveOrgId();
|
|
330
|
+
if (!orgId) {
|
|
331
|
+
console.error(chalk.red('No active org. Run: fazemos orgs switch <slug>'));
|
|
332
|
+
process.exit(1);
|
|
333
|
+
}
|
|
334
|
+
const data = await api('POST', `/api/organizations/${orgId}/members/invite`, {
|
|
335
|
+
email: opts.email,
|
|
336
|
+
role: opts.role,
|
|
337
|
+
});
|
|
338
|
+
console.log(chalk.green(`Invited ${opts.email} as ${opts.role}`));
|
|
339
|
+
}
|
|
340
|
+
catch (err) {
|
|
341
|
+
console.error(chalk.red(err.message));
|
|
342
|
+
process.exit(1);
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
orgs
|
|
346
|
+
.command('add-agent')
|
|
347
|
+
.description('Create an AI agent member in the organization. The agent can then be assigned work via actions, commitments, or pipeline steps. Use "agents register" for bulk registration with roles and config.')
|
|
348
|
+
.requiredOption('-n, --name <name>', 'Agent display name (e.g., "kate", "marco")')
|
|
349
|
+
.option('-r, --role <role>', 'Organization role (admin, member)', 'member')
|
|
350
|
+
.action(async (opts) => {
|
|
351
|
+
try {
|
|
352
|
+
const orgId = getActiveOrgId();
|
|
353
|
+
if (!orgId) {
|
|
354
|
+
console.error(chalk.red('No active org. Run: fazemos orgs switch <slug>'));
|
|
355
|
+
process.exit(1);
|
|
356
|
+
}
|
|
357
|
+
const data = await api('POST', `/api/organizations/${orgId}/members`, {
|
|
358
|
+
displayName: opts.name,
|
|
359
|
+
role: opts.role,
|
|
360
|
+
});
|
|
361
|
+
console.log(chalk.green(`Created agent: ${data.member.display_name}`));
|
|
362
|
+
console.log(` ID: ${data.member.id}`);
|
|
363
|
+
}
|
|
364
|
+
catch (err) {
|
|
365
|
+
console.error(chalk.red(err.message));
|
|
366
|
+
process.exit(1);
|
|
367
|
+
}
|
|
368
|
+
});
|
|
267
369
|
orgs
|
|
268
370
|
.command('members')
|
|
269
371
|
.description('List org members')
|
|
@@ -339,6 +441,91 @@ ws
|
|
|
339
441
|
process.exit(1);
|
|
340
442
|
}
|
|
341
443
|
});
|
|
444
|
+
ws
|
|
445
|
+
.command('update')
|
|
446
|
+
.description('Update worksheet properties. Provide at least one field. Use "ws list" to find IDs, "ws show <id>" to see current values.')
|
|
447
|
+
.argument('<id>', 'Worksheet ID')
|
|
448
|
+
.option('-n, --name <name>', 'New worksheet name')
|
|
449
|
+
.option('-p, --purpose <purpose>', 'New purpose (e.g., "From X to Y by When")')
|
|
450
|
+
.option('--cadence <cadence>', 'Check-in cadence: weekly, biweekly, or monthly')
|
|
451
|
+
.action(async (id, opts) => {
|
|
452
|
+
try {
|
|
453
|
+
if (!opts.name && !opts.purpose && !opts.cadence) {
|
|
454
|
+
console.error(chalk.red('Provide --name, --purpose, and/or --cadence'));
|
|
455
|
+
process.exit(1);
|
|
456
|
+
}
|
|
457
|
+
const body = {};
|
|
458
|
+
if (opts.name)
|
|
459
|
+
body.name = opts.name;
|
|
460
|
+
if (opts.purpose)
|
|
461
|
+
body.purpose = opts.purpose;
|
|
462
|
+
if (opts.cadence)
|
|
463
|
+
body.checkInCadence = opts.cadence;
|
|
464
|
+
const data = await api('PATCH', `/api/worksheets/${id}`, body);
|
|
465
|
+
console.log(chalk.green(`Updated: ${data.worksheet.name}`));
|
|
466
|
+
}
|
|
467
|
+
catch (err) {
|
|
468
|
+
console.error(chalk.red(err.message));
|
|
469
|
+
process.exit(1);
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
ws
|
|
473
|
+
.command('archive')
|
|
474
|
+
.description('Archive a worksheet. Archived worksheets are hidden from "ws list" by default but can be found with "ws list -s archived". This is not reversible from the CLI.')
|
|
475
|
+
.argument('<id>', 'Worksheet ID')
|
|
476
|
+
.action(async (id) => {
|
|
477
|
+
try {
|
|
478
|
+
await api('POST', `/api/worksheets/${id}/archive`);
|
|
479
|
+
console.log(chalk.green('Worksheet archived'));
|
|
480
|
+
}
|
|
481
|
+
catch (err) {
|
|
482
|
+
console.error(chalk.red(err.message));
|
|
483
|
+
process.exit(1);
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
ws
|
|
487
|
+
.command('progress')
|
|
488
|
+
.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.')
|
|
489
|
+
.argument('<id>', 'Worksheet ID')
|
|
490
|
+
.action(async (id) => {
|
|
491
|
+
try {
|
|
492
|
+
const data = await api('GET', `/api/worksheets/${id}/progress-board`);
|
|
493
|
+
const board = data.progressBoard || data;
|
|
494
|
+
console.log(chalk.cyan('Progress Board'));
|
|
495
|
+
if (board.outcomes?.length) {
|
|
496
|
+
console.log(chalk.cyan('\n Outcomes:'));
|
|
497
|
+
for (const o of board.outcomes) {
|
|
498
|
+
const progress = o.target_value ? ` ${o.current_value ?? 0}/${o.target_value}` : '';
|
|
499
|
+
console.log(` ${o.status === 'achieved' ? '✓' : '○'} ${o.name}${progress}`);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
if (board.milestones?.length) {
|
|
503
|
+
console.log(chalk.cyan('\n Milestones:'));
|
|
504
|
+
for (const m of board.milestones) {
|
|
505
|
+
const icon = m.status === 'reached' ? '✓' : m.status === 'missed' ? '✗' : '○';
|
|
506
|
+
console.log(` ${icon} ${m.name}${m.target_date ? ` (${m.target_date})` : ''}`);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
if (board.commitments?.length) {
|
|
510
|
+
console.log(chalk.cyan('\n Commitments:'));
|
|
511
|
+
for (const c of board.commitments) {
|
|
512
|
+
const icon = c.status === 'completed' ? chalk.green('✓') : c.status === 'missed' ? chalk.red('✗') : '○';
|
|
513
|
+
console.log(` ${icon} ${c.description} — ${c.status}`);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
if (board.actions?.length) {
|
|
517
|
+
console.log(chalk.cyan('\n Actions:'));
|
|
518
|
+
for (const a of board.actions) {
|
|
519
|
+
const progress = a.target_value ? ` ${a.current_value ?? 0}/${a.target_value}` : '';
|
|
520
|
+
console.log(` ${a.description}${progress}`);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
catch (err) {
|
|
525
|
+
console.error(chalk.red(err.message));
|
|
526
|
+
process.exit(1);
|
|
527
|
+
}
|
|
528
|
+
});
|
|
342
529
|
ws
|
|
343
530
|
.command('show')
|
|
344
531
|
.description('Show worksheet detail')
|
|
@@ -483,6 +670,41 @@ outcomes
|
|
|
483
670
|
process.exit(1);
|
|
484
671
|
}
|
|
485
672
|
});
|
|
673
|
+
outcomes
|
|
674
|
+
.command('update')
|
|
675
|
+
.description('Update an outcome. Provide at least one field to change. Use "oc update-value" to change just the current value, or "oc link/unlink" to manage parent linkage. Use "ws show <id>" to find outcome IDs.')
|
|
676
|
+
.requiredOption('-w, --worksheet <id>', 'Worksheet ID')
|
|
677
|
+
.requiredOption('-o, --outcome <id>', 'Outcome ID (from "ws show" output)')
|
|
678
|
+
.option('-n, --name <name>', 'New outcome name')
|
|
679
|
+
.option('-d, --description <desc>', 'New description')
|
|
680
|
+
.option('-m, --measurement <method>', 'How it is measured (e.g., "Count of active users")')
|
|
681
|
+
.option('-t, --target <value>', 'Target value (numeric)', parseNumber)
|
|
682
|
+
.option('-s, --status <status>', 'Status: active, achieved, or dropped')
|
|
683
|
+
.action(async (opts) => {
|
|
684
|
+
try {
|
|
685
|
+
const body = {};
|
|
686
|
+
if (opts.name)
|
|
687
|
+
body.name = opts.name;
|
|
688
|
+
if (opts.description != null)
|
|
689
|
+
body.description = opts.description;
|
|
690
|
+
if (opts.measurement)
|
|
691
|
+
body.measurement = opts.measurement;
|
|
692
|
+
if (opts.target !== undefined)
|
|
693
|
+
body.targetValue = opts.target;
|
|
694
|
+
if (opts.status)
|
|
695
|
+
body.status = opts.status;
|
|
696
|
+
if (Object.keys(body).length === 0) {
|
|
697
|
+
console.error(chalk.red('Provide at least one field to update'));
|
|
698
|
+
process.exit(1);
|
|
699
|
+
}
|
|
700
|
+
await api('PATCH', `/api/worksheets/${opts.worksheet}/outcomes/${opts.outcome}`, body);
|
|
701
|
+
console.log(chalk.green('Outcome updated'));
|
|
702
|
+
}
|
|
703
|
+
catch (err) {
|
|
704
|
+
console.error(chalk.red(err.message));
|
|
705
|
+
process.exit(1);
|
|
706
|
+
}
|
|
707
|
+
});
|
|
486
708
|
outcomes
|
|
487
709
|
.command('update-value')
|
|
488
710
|
.description('Update an outcome current value')
|
|
@@ -499,6 +721,101 @@ outcomes
|
|
|
499
721
|
process.exit(1);
|
|
500
722
|
}
|
|
501
723
|
});
|
|
724
|
+
outcomes
|
|
725
|
+
.command('remove')
|
|
726
|
+
.description('Permanently delete an outcome from a worksheet. If the outcome has linked children, those links will be broken. This cannot be undone.')
|
|
727
|
+
.requiredOption('-w, --worksheet <id>', 'Worksheet ID')
|
|
728
|
+
.requiredOption('-o, --outcome <id>', 'Outcome ID (from "ws show" output)')
|
|
729
|
+
.action(async (opts) => {
|
|
730
|
+
try {
|
|
731
|
+
await api('DELETE', `/api/worksheets/${opts.worksheet}/outcomes/${opts.outcome}`);
|
|
732
|
+
console.log(chalk.green('Outcome deleted'));
|
|
733
|
+
}
|
|
734
|
+
catch (err) {
|
|
735
|
+
console.error(chalk.red(err.message));
|
|
736
|
+
process.exit(1);
|
|
737
|
+
}
|
|
738
|
+
});
|
|
739
|
+
outcomes
|
|
740
|
+
.command('reorder')
|
|
741
|
+
.description('Reorder outcomes on a worksheet. Pass all outcome IDs in the desired display order. IDs not included will be appended at the end.')
|
|
742
|
+
.requiredOption('-w, --worksheet <id>', 'Worksheet ID')
|
|
743
|
+
.requiredOption('--ids <ids>', 'Comma-separated outcome IDs in desired order (e.g., --ids id1,id2,id3)', (v) => v.split(','))
|
|
744
|
+
.action(async (opts) => {
|
|
745
|
+
try {
|
|
746
|
+
await api('PATCH', `/api/worksheets/${opts.worksheet}/outcomes/reorder`, { outcomeIds: opts.ids });
|
|
747
|
+
console.log(chalk.green('Outcomes reordered'));
|
|
748
|
+
}
|
|
749
|
+
catch (err) {
|
|
750
|
+
console.error(chalk.red(err.message));
|
|
751
|
+
process.exit(1);
|
|
752
|
+
}
|
|
753
|
+
});
|
|
754
|
+
outcomes
|
|
755
|
+
.command('linkable')
|
|
756
|
+
.description('List outcomes from other worksheets that can be linked to as parents. Use this to discover valid --parent IDs for "oc link". Only shows outcomes that would not create a circular reference.')
|
|
757
|
+
.requiredOption('-w, --worksheet <id>', 'Worksheet ID whose outcomes you want to link from')
|
|
758
|
+
.action(async (opts) => {
|
|
759
|
+
try {
|
|
760
|
+
const data = await api('GET', `/api/outcomes/linkable?worksheetId=${opts.worksheet}`);
|
|
761
|
+
if (!data.outcomes?.length) {
|
|
762
|
+
console.log(chalk.yellow('No linkable outcomes'));
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
for (const o of data.outcomes) {
|
|
766
|
+
const ws = o.worksheet_name ? ` on ${o.worksheet_name}` : '';
|
|
767
|
+
console.log(` ${o.name}${ws} — ${o.id}`);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
catch (err) {
|
|
771
|
+
console.error(chalk.red(err.message));
|
|
772
|
+
process.exit(1);
|
|
773
|
+
}
|
|
774
|
+
});
|
|
775
|
+
outcomes
|
|
776
|
+
.command('children')
|
|
777
|
+
.description('Show outcomes from other worksheets that are linked to this outcome as children. These are outcomes where "oc link --parent <this-id>" was used.')
|
|
778
|
+
.argument('<id>', 'Parent outcome ID')
|
|
779
|
+
.action(async (id) => {
|
|
780
|
+
try {
|
|
781
|
+
const data = await api('GET', `/api/outcomes/${id}/children`);
|
|
782
|
+
if (!data.children?.length) {
|
|
783
|
+
console.log(chalk.yellow('No child outcomes'));
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
for (const c of data.children) {
|
|
787
|
+
const progress = c.target_value ? ` ${c.current_value ?? 0}/${c.target_value}` : '';
|
|
788
|
+
const ws = c.worksheet_name ? ` on ${c.worksheet_name}` : '';
|
|
789
|
+
console.log(` ${c.status === 'achieved' ? '✓' : '○'} ${c.name}${progress}${ws} — ${c.id}`);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
catch (err) {
|
|
793
|
+
console.error(chalk.red(err.message));
|
|
794
|
+
process.exit(1);
|
|
795
|
+
}
|
|
796
|
+
});
|
|
797
|
+
outcomes
|
|
798
|
+
.command('tree')
|
|
799
|
+
.description('Show the full hierarchy tree for an outcome, including all descendant outcomes across worksheets. Useful for seeing how team-level outcomes roll up to org-level goals.')
|
|
800
|
+
.argument('<id>', 'Root outcome ID')
|
|
801
|
+
.action(async (id) => {
|
|
802
|
+
try {
|
|
803
|
+
const data = await api('GET', `/api/outcomes/${id}/tree`);
|
|
804
|
+
const printNode = (node, indent) => {
|
|
805
|
+
const progress = node.target_value ? ` ${node.current_value ?? 0}/${node.target_value}` : '';
|
|
806
|
+
const ws = node.worksheet_name ? ` on ${node.worksheet_name}` : '';
|
|
807
|
+
console.log(`${indent}${node.status === 'achieved' ? '✓' : '○'} ${node.name}${progress}${ws}`);
|
|
808
|
+
for (const child of node.children || []) {
|
|
809
|
+
printNode(child, indent + ' ');
|
|
810
|
+
}
|
|
811
|
+
};
|
|
812
|
+
printNode(data.tree || data, ' ');
|
|
813
|
+
}
|
|
814
|
+
catch (err) {
|
|
815
|
+
console.error(chalk.red(err.message));
|
|
816
|
+
process.exit(1);
|
|
817
|
+
}
|
|
818
|
+
});
|
|
502
819
|
// ── Milestones ──────────────────────────────────────────────
|
|
503
820
|
const milestones = program.command('milestones').alias('ms').description('Milestone commands');
|
|
504
821
|
milestones
|
|
@@ -527,8 +844,71 @@ milestones
|
|
|
527
844
|
process.exit(1);
|
|
528
845
|
}
|
|
529
846
|
});
|
|
847
|
+
milestones
|
|
848
|
+
.command('update')
|
|
849
|
+
.description('Update a milestone. Provide at least one field to change. Use "ws show <id>" to find milestone IDs and see current values.')
|
|
850
|
+
.argument('<id>', 'Milestone ID (from "ws show" output)')
|
|
851
|
+
.option('-n, --name <name>', 'New milestone name')
|
|
852
|
+
.option('-d, --description <desc>', 'New description')
|
|
853
|
+
.option('-t, --target-date <date>', 'New target date (YYYY-MM-DD)')
|
|
854
|
+
.option('-s, --status <status>', 'Status: pending (default), reached, or missed')
|
|
855
|
+
.action(async (id, opts) => {
|
|
856
|
+
try {
|
|
857
|
+
const body = {};
|
|
858
|
+
if (opts.name)
|
|
859
|
+
body.name = opts.name;
|
|
860
|
+
if (opts.description != null)
|
|
861
|
+
body.description = opts.description;
|
|
862
|
+
if (opts.targetDate)
|
|
863
|
+
body.targetDate = opts.targetDate;
|
|
864
|
+
if (opts.status)
|
|
865
|
+
body.status = opts.status;
|
|
866
|
+
if (Object.keys(body).length === 0) {
|
|
867
|
+
console.error(chalk.red('Provide at least one field to update'));
|
|
868
|
+
process.exit(1);
|
|
869
|
+
}
|
|
870
|
+
await api('PATCH', `/api/milestones/${id}`, body);
|
|
871
|
+
console.log(chalk.green('Milestone updated'));
|
|
872
|
+
}
|
|
873
|
+
catch (err) {
|
|
874
|
+
console.error(chalk.red(err.message));
|
|
875
|
+
process.exit(1);
|
|
876
|
+
}
|
|
877
|
+
});
|
|
878
|
+
milestones
|
|
879
|
+
.command('remove')
|
|
880
|
+
.description('Permanently delete a milestone. This cannot be undone.')
|
|
881
|
+
.argument('<id>', 'Milestone ID (from "ws show" output)')
|
|
882
|
+
.action(async (id) => {
|
|
883
|
+
try {
|
|
884
|
+
await api('DELETE', `/api/milestones/${id}`);
|
|
885
|
+
console.log(chalk.green('Milestone deleted'));
|
|
886
|
+
}
|
|
887
|
+
catch (err) {
|
|
888
|
+
console.error(chalk.red(err.message));
|
|
889
|
+
process.exit(1);
|
|
890
|
+
}
|
|
891
|
+
});
|
|
892
|
+
milestones
|
|
893
|
+
.command('reorder')
|
|
894
|
+
.description('Reorder milestones on a worksheet. Pass all milestone IDs in the desired display order.')
|
|
895
|
+
.requiredOption('-w, --worksheet <id>', 'Worksheet ID')
|
|
896
|
+
.requiredOption('--ids <ids>', 'Comma-separated milestone IDs in desired order (e.g., --ids id1,id2,id3)', (v) => v.split(','))
|
|
897
|
+
.action(async (opts) => {
|
|
898
|
+
try {
|
|
899
|
+
await api('PATCH', `/api/worksheets/${opts.worksheet}/milestones/reorder`, { milestoneIds: opts.ids });
|
|
900
|
+
console.log(chalk.green('Milestones reordered'));
|
|
901
|
+
}
|
|
902
|
+
catch (err) {
|
|
903
|
+
console.error(chalk.red(err.message));
|
|
904
|
+
process.exit(1);
|
|
905
|
+
}
|
|
906
|
+
});
|
|
530
907
|
// ── Members ─────────────────────────────────────────────────
|
|
531
|
-
const members = program.command('members').description('
|
|
908
|
+
const members = program.command('members').description('Member commands (worksheet and org level).\n\n' +
|
|
909
|
+
' Worksheet members: add, list, update, remove — manage who works on a worksheet.\n' +
|
|
910
|
+
' Org members: show, update-org, delete — manage org-level member records.\n' +
|
|
911
|
+
' Use "orgs members" to list all org members and find member IDs.');
|
|
532
912
|
members
|
|
533
913
|
.command('add')
|
|
534
914
|
.description('Add a member to a worksheet')
|
|
@@ -568,32 +948,124 @@ members
|
|
|
568
948
|
process.exit(1);
|
|
569
949
|
}
|
|
570
950
|
});
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
.command('add')
|
|
575
|
-
.description('Add an action to a worksheet')
|
|
951
|
+
members
|
|
952
|
+
.command('update')
|
|
953
|
+
.description('Change a worksheet member\'s role. Use "members list -w <id>" to find worksheet member IDs.')
|
|
576
954
|
.requiredOption('-w, --worksheet <id>', 'Worksheet ID')
|
|
577
|
-
.requiredOption('-
|
|
578
|
-
.
|
|
579
|
-
.option('-m, --measurement <method>', 'How it is measured')
|
|
580
|
-
.option('-t, --target <value>', 'Target value', parseNumber)
|
|
581
|
-
.option('-c, --current <value>', 'Current value', parseNumber)
|
|
955
|
+
.requiredOption('-m, --member <id>', 'Worksheet member ID (from "members list" output)')
|
|
956
|
+
.requiredOption('-r, --role <role>', 'New role: lead or contributor')
|
|
582
957
|
.action(async (opts) => {
|
|
583
958
|
try {
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
959
|
+
await api('PATCH', `/api/worksheets/${opts.worksheet}/members/${opts.member}`, { role: opts.role });
|
|
960
|
+
console.log(chalk.green(`Member role updated to ${opts.role}`));
|
|
961
|
+
}
|
|
962
|
+
catch (err) {
|
|
963
|
+
console.error(chalk.red(err.message));
|
|
964
|
+
process.exit(1);
|
|
965
|
+
}
|
|
966
|
+
});
|
|
967
|
+
members
|
|
968
|
+
.command('remove')
|
|
969
|
+
.description('Remove a member from a worksheet. The member remains in the organization but loses access to this worksheet.')
|
|
970
|
+
.requiredOption('-w, --worksheet <id>', 'Worksheet ID')
|
|
971
|
+
.requiredOption('-m, --member <id>', 'Worksheet member ID (from "members list" output)')
|
|
972
|
+
.action(async (opts) => {
|
|
973
|
+
try {
|
|
974
|
+
await api('DELETE', `/api/worksheets/${opts.worksheet}/members/${opts.member}`);
|
|
975
|
+
console.log(chalk.green('Member removed from worksheet'));
|
|
976
|
+
}
|
|
977
|
+
catch (err) {
|
|
978
|
+
console.error(chalk.red(err.message));
|
|
979
|
+
process.exit(1);
|
|
980
|
+
}
|
|
981
|
+
});
|
|
982
|
+
members
|
|
983
|
+
.command('show')
|
|
984
|
+
.description('Show an organization member\'s detail, including role, type, and agent status. Use "orgs members" to find member IDs.')
|
|
985
|
+
.argument('<id>', 'Member ID (from "orgs members" output)')
|
|
986
|
+
.action(async (id) => {
|
|
987
|
+
try {
|
|
988
|
+
const data = await api('GET', `/api/members/${id}`);
|
|
989
|
+
const m = data.member;
|
|
990
|
+
console.log(chalk.cyan(m.display_name));
|
|
991
|
+
console.log(` ID: ${m.id}`);
|
|
992
|
+
console.log(` Role: ${m.role}`);
|
|
993
|
+
console.log(` Type: ${m.member_type}`);
|
|
994
|
+
if (m.email)
|
|
995
|
+
console.log(` Email: ${m.email}`);
|
|
996
|
+
if (m.agent_status)
|
|
997
|
+
console.log(` Status: ${m.agent_status}`);
|
|
998
|
+
}
|
|
999
|
+
catch (err) {
|
|
1000
|
+
console.error(chalk.red(err.message));
|
|
1001
|
+
process.exit(1);
|
|
1002
|
+
}
|
|
1003
|
+
});
|
|
1004
|
+
members
|
|
1005
|
+
.command('update-org')
|
|
1006
|
+
.description('Update an organization member\'s role or display name. This is the org-level role, not the worksheet role. Use "members update" for worksheet roles instead.')
|
|
1007
|
+
.argument('<id>', 'Member ID (from "orgs members" output)')
|
|
1008
|
+
.option('-r, --role <role>', 'New org role: admin or member')
|
|
1009
|
+
.option('-n, --name <name>', 'New display name')
|
|
1010
|
+
.action(async (id, opts) => {
|
|
1011
|
+
try {
|
|
1012
|
+
const body = {};
|
|
1013
|
+
if (opts.role)
|
|
1014
|
+
body.role = opts.role;
|
|
1015
|
+
if (opts.name)
|
|
1016
|
+
body.displayName = opts.name;
|
|
1017
|
+
if (Object.keys(body).length === 0) {
|
|
1018
|
+
console.error(chalk.red('Provide --role and/or --name'));
|
|
1019
|
+
process.exit(1);
|
|
1020
|
+
}
|
|
1021
|
+
await api('PATCH', `/api/members/${id}`, body);
|
|
1022
|
+
console.log(chalk.green('Member updated'));
|
|
1023
|
+
}
|
|
1024
|
+
catch (err) {
|
|
1025
|
+
console.error(chalk.red(err.message));
|
|
1026
|
+
process.exit(1);
|
|
1027
|
+
}
|
|
1028
|
+
});
|
|
1029
|
+
members
|
|
1030
|
+
.command('delete')
|
|
1031
|
+
.description('Permanently remove a member from the organization. This removes them from all worksheets and revokes access. Cannot be undone.')
|
|
1032
|
+
.argument('<id>', 'Member ID (from "orgs members" output)')
|
|
1033
|
+
.action(async (id) => {
|
|
1034
|
+
try {
|
|
1035
|
+
await api('DELETE', `/api/members/${id}`);
|
|
1036
|
+
console.log(chalk.green('Member removed from organization'));
|
|
1037
|
+
}
|
|
1038
|
+
catch (err) {
|
|
1039
|
+
console.error(chalk.red(err.message));
|
|
1040
|
+
process.exit(1);
|
|
1041
|
+
}
|
|
1042
|
+
});
|
|
1043
|
+
// ── Actions ─────────────────────────────────────────────────
|
|
1044
|
+
const actions = program.command('actions').alias('ac').description('Action (lead measure) commands');
|
|
1045
|
+
actions
|
|
1046
|
+
.command('add')
|
|
1047
|
+
.description('Add an action to a worksheet')
|
|
1048
|
+
.requiredOption('-w, --worksheet <id>', 'Worksheet ID')
|
|
1049
|
+
.requiredOption('-n, --name <name>', 'Action description')
|
|
1050
|
+
.option('-o, --outcome <id>', 'Linked outcome ID')
|
|
1051
|
+
.option('-m, --measurement <method>', 'How it is measured')
|
|
1052
|
+
.option('-t, --target <value>', 'Target value', parseNumber)
|
|
1053
|
+
.option('-c, --current <value>', 'Current value', parseNumber)
|
|
1054
|
+
.action(async (opts) => {
|
|
1055
|
+
try {
|
|
1056
|
+
const body = { description: opts.name };
|
|
1057
|
+
if (opts.outcome)
|
|
1058
|
+
body.outcomeId = opts.outcome;
|
|
1059
|
+
if (opts.measurement)
|
|
1060
|
+
body.measurement = opts.measurement;
|
|
1061
|
+
if (opts.target !== undefined)
|
|
1062
|
+
body.targetValue = opts.target;
|
|
1063
|
+
if (opts.current !== undefined)
|
|
1064
|
+
body.currentValue = opts.current;
|
|
1065
|
+
const data = await api('POST', `/api/worksheets/${opts.worksheet}/actions`, body);
|
|
1066
|
+
const a = data.action;
|
|
1067
|
+
console.log(chalk.green(`Added action: ${a.description}`));
|
|
1068
|
+
console.log(` ID: ${a.id}`);
|
|
597
1069
|
}
|
|
598
1070
|
catch (err) {
|
|
599
1071
|
console.error(chalk.red(err.message));
|
|
@@ -637,6 +1109,55 @@ actions
|
|
|
637
1109
|
process.exit(1);
|
|
638
1110
|
}
|
|
639
1111
|
});
|
|
1112
|
+
actions
|
|
1113
|
+
.command('update')
|
|
1114
|
+
.description('Update an action. Provide at least one field to change. Use "ac update-value" to change just the current value. Use "ac list -w <id>" to find action IDs.')
|
|
1115
|
+
.requiredOption('-w, --worksheet <id>', 'Worksheet ID')
|
|
1116
|
+
.requiredOption('-a, --action <id>', 'Action ID (from "ac list" or "ws show" output)')
|
|
1117
|
+
.option('-n, --name <name>', 'New action description')
|
|
1118
|
+
.option('-m, --measurement <method>', 'How it is measured (e.g., "PRs merged per week")')
|
|
1119
|
+
.option('-t, --target <value>', 'Target value (numeric)', parseNumber)
|
|
1120
|
+
.option('-o, --outcome <id>', 'Link to a different outcome ID')
|
|
1121
|
+
.action(async (opts) => {
|
|
1122
|
+
try {
|
|
1123
|
+
const body = {};
|
|
1124
|
+
if (opts.name)
|
|
1125
|
+
body.description = opts.name;
|
|
1126
|
+
if (opts.measurement)
|
|
1127
|
+
body.measurement = opts.measurement;
|
|
1128
|
+
if (opts.target !== undefined)
|
|
1129
|
+
body.targetValue = opts.target;
|
|
1130
|
+
if (opts.outcome)
|
|
1131
|
+
body.outcomeId = opts.outcome;
|
|
1132
|
+
if (Object.keys(body).length === 0) {
|
|
1133
|
+
console.error(chalk.red('Provide at least one field to update'));
|
|
1134
|
+
process.exit(1);
|
|
1135
|
+
}
|
|
1136
|
+
await api('PATCH', `/api/worksheets/${opts.worksheet}/actions/${opts.action}`, body);
|
|
1137
|
+
console.log(chalk.green('Action updated'));
|
|
1138
|
+
}
|
|
1139
|
+
catch (err) {
|
|
1140
|
+
console.error(chalk.red(err.message));
|
|
1141
|
+
process.exit(1);
|
|
1142
|
+
}
|
|
1143
|
+
});
|
|
1144
|
+
actions
|
|
1145
|
+
.command('execute')
|
|
1146
|
+
.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.')
|
|
1147
|
+
.requiredOption('-w, --worksheet <id>', 'Worksheet ID')
|
|
1148
|
+
.requiredOption('-a, --action <id>', 'Action ID (from "ac list" or "ws show" output)')
|
|
1149
|
+
.action(async (opts) => {
|
|
1150
|
+
try {
|
|
1151
|
+
const data = await api('POST', `/api/worksheets/${opts.worksheet}/actions/${opts.action}/execute`);
|
|
1152
|
+
console.log(chalk.green('Execution triggered'));
|
|
1153
|
+
if (data.execution?.id)
|
|
1154
|
+
console.log(` Execution ID: ${data.execution.id}`);
|
|
1155
|
+
}
|
|
1156
|
+
catch (err) {
|
|
1157
|
+
console.error(chalk.red(err.message));
|
|
1158
|
+
process.exit(1);
|
|
1159
|
+
}
|
|
1160
|
+
});
|
|
640
1161
|
// ── Commitments ─────────────────────────────────────────────
|
|
641
1162
|
const commitments = program.command('commitments').alias('cm').description('Commitment commands');
|
|
642
1163
|
commitments
|
|
@@ -697,6 +1218,243 @@ commitments
|
|
|
697
1218
|
process.exit(1);
|
|
698
1219
|
}
|
|
699
1220
|
});
|
|
1221
|
+
commitments
|
|
1222
|
+
.command('execute')
|
|
1223
|
+
.description('Trigger an agent execution for a commitment. The API determines which agent to use based on the commitment\'s configuration. For more control, use the top-level "execute" command instead.')
|
|
1224
|
+
.requiredOption('-w, --worksheet <id>', 'Worksheet ID')
|
|
1225
|
+
.requiredOption('-c, --commitment <id>', 'Commitment ID (from "cm list" or "ws show" output)')
|
|
1226
|
+
.action(async (opts) => {
|
|
1227
|
+
try {
|
|
1228
|
+
const data = await api('POST', `/api/worksheets/${opts.worksheet}/commitments/${opts.commitment}/execute`);
|
|
1229
|
+
console.log(chalk.green('Execution triggered'));
|
|
1230
|
+
if (data.execution?.id)
|
|
1231
|
+
console.log(` Execution ID: ${data.execution.id}`);
|
|
1232
|
+
}
|
|
1233
|
+
catch (err) {
|
|
1234
|
+
console.error(chalk.red(err.message));
|
|
1235
|
+
process.exit(1);
|
|
1236
|
+
}
|
|
1237
|
+
});
|
|
1238
|
+
// ── Notes ───────────────────────────────────────────────────
|
|
1239
|
+
const notes = program.command('notes').alias('nt').description('Worksheet note commands. Notes are freeform text attached to a worksheet for capturing context, decisions, or observations.');
|
|
1240
|
+
notes
|
|
1241
|
+
.command('add')
|
|
1242
|
+
.description('Add a note to a worksheet. Notes are timestamped and attributed to the current user. Good for recording decisions, context, or observations that don\'t fit into outcomes or actions.')
|
|
1243
|
+
.requiredOption('-w, --worksheet <id>', 'Worksheet ID')
|
|
1244
|
+
.requiredOption('-t, --text <text>', 'Note text (e.g., "Decided to defer mobile until Q3")')
|
|
1245
|
+
.action(async (opts) => {
|
|
1246
|
+
try {
|
|
1247
|
+
const data = await api('POST', `/api/worksheets/${opts.worksheet}/notes`, { text: opts.text });
|
|
1248
|
+
const n = data.note;
|
|
1249
|
+
console.log(chalk.green('Note added'));
|
|
1250
|
+
console.log(` ID: ${n.id}`);
|
|
1251
|
+
}
|
|
1252
|
+
catch (err) {
|
|
1253
|
+
console.error(chalk.red(err.message));
|
|
1254
|
+
process.exit(1);
|
|
1255
|
+
}
|
|
1256
|
+
});
|
|
1257
|
+
notes
|
|
1258
|
+
.command('list')
|
|
1259
|
+
.description('List notes on a worksheet')
|
|
1260
|
+
.requiredOption('-w, --worksheet <id>', 'Worksheet ID')
|
|
1261
|
+
.action(async (opts) => {
|
|
1262
|
+
try {
|
|
1263
|
+
const data = await api('GET', `/api/worksheets/${opts.worksheet}/notes`);
|
|
1264
|
+
if (!data.notes?.length) {
|
|
1265
|
+
console.log(chalk.yellow('No notes'));
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
for (const n of data.notes) {
|
|
1269
|
+
const time = n.created_at ? new Date(n.created_at).toLocaleString() : '';
|
|
1270
|
+
const author = n.author_name || '';
|
|
1271
|
+
console.log(` ${chalk.gray(time)} ${author ? chalk.cyan(author) + ' — ' : ''}${n.text}`);
|
|
1272
|
+
console.log(` ID: ${n.id}`);
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
catch (err) {
|
|
1276
|
+
console.error(chalk.red(err.message));
|
|
1277
|
+
process.exit(1);
|
|
1278
|
+
}
|
|
1279
|
+
});
|
|
1280
|
+
notes
|
|
1281
|
+
.command('remove')
|
|
1282
|
+
.description('Permanently delete a note. Use "nt list -w <id>" to find note IDs.')
|
|
1283
|
+
.argument('<id>', 'Note ID (from "nt list" output)')
|
|
1284
|
+
.action(async (id) => {
|
|
1285
|
+
try {
|
|
1286
|
+
await api('DELETE', `/api/notes/${id}`);
|
|
1287
|
+
console.log(chalk.green('Note deleted'));
|
|
1288
|
+
}
|
|
1289
|
+
catch (err) {
|
|
1290
|
+
console.error(chalk.red(err.message));
|
|
1291
|
+
process.exit(1);
|
|
1292
|
+
}
|
|
1293
|
+
});
|
|
1294
|
+
// ── Check-ins ───────────────────────────────────────────────
|
|
1295
|
+
const checkins = program.command('check-ins').alias('ci').description('Check-in commands.\n\n' +
|
|
1296
|
+
' Check-ins are periodic reviews of worksheet progress (weekly, biweekly, etc.).\n' +
|
|
1297
|
+
' During a check-in, members report on commitments and make new ones.\n\n' +
|
|
1298
|
+
' Typical flow:\n' +
|
|
1299
|
+
' 1. ci create -w <id> Start a new check-in\n' +
|
|
1300
|
+
' 2. ci report <id> --reports [...] Report on past commitments\n' +
|
|
1301
|
+
' 3. ci add-commitment <id> -d "..." Make new commitments\n' +
|
|
1302
|
+
' 4. ci complete <id> Close the check-in');
|
|
1303
|
+
checkins
|
|
1304
|
+
.command('create')
|
|
1305
|
+
.description('Start a new check-in for a worksheet. This creates a check-in record where members can report on past commitments and make new ones. Only one check-in is typically open at a time.')
|
|
1306
|
+
.requiredOption('-w, --worksheet <id>', 'Worksheet ID')
|
|
1307
|
+
.option('-n, --name <name>', 'Check-in name (e.g., "Week of 2026-03-23")')
|
|
1308
|
+
.action(async (opts) => {
|
|
1309
|
+
try {
|
|
1310
|
+
const body = {};
|
|
1311
|
+
if (opts.name)
|
|
1312
|
+
body.name = opts.name;
|
|
1313
|
+
const data = await api('POST', `/api/worksheets/${opts.worksheet}/check-ins`, body);
|
|
1314
|
+
const ci = data.checkIn;
|
|
1315
|
+
console.log(chalk.green('Check-in created'));
|
|
1316
|
+
console.log(` ID: ${ci.id}`);
|
|
1317
|
+
if (ci.status)
|
|
1318
|
+
console.log(` Status: ${ci.status}`);
|
|
1319
|
+
}
|
|
1320
|
+
catch (err) {
|
|
1321
|
+
console.error(chalk.red(err.message));
|
|
1322
|
+
process.exit(1);
|
|
1323
|
+
}
|
|
1324
|
+
});
|
|
1325
|
+
checkins
|
|
1326
|
+
.command('list')
|
|
1327
|
+
.description('List check-ins for a worksheet')
|
|
1328
|
+
.requiredOption('-w, --worksheet <id>', 'Worksheet ID')
|
|
1329
|
+
.action(async (opts) => {
|
|
1330
|
+
try {
|
|
1331
|
+
const data = await api('GET', `/api/worksheets/${opts.worksheet}/check-ins`);
|
|
1332
|
+
if (!data.checkIns?.length) {
|
|
1333
|
+
console.log(chalk.yellow('No check-ins'));
|
|
1334
|
+
return;
|
|
1335
|
+
}
|
|
1336
|
+
for (const ci of data.checkIns) {
|
|
1337
|
+
const icon = ci.status === 'completed' ? chalk.green('✓') : '○';
|
|
1338
|
+
const date = ci.created_at ? new Date(ci.created_at).toLocaleDateString() : '';
|
|
1339
|
+
console.log(` ${icon} ${ci.name || date} (${ci.status}) — ${ci.id}`);
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
catch (err) {
|
|
1343
|
+
console.error(chalk.red(err.message));
|
|
1344
|
+
process.exit(1);
|
|
1345
|
+
}
|
|
1346
|
+
});
|
|
1347
|
+
checkins
|
|
1348
|
+
.command('show')
|
|
1349
|
+
.description('Show check-in detail including commitment reports and new commitments made during the check-in.')
|
|
1350
|
+
.argument('<id>', 'Check-in ID (from "ci list" output)')
|
|
1351
|
+
.action(async (id) => {
|
|
1352
|
+
try {
|
|
1353
|
+
const data = await api('GET', `/api/check-ins/${id}`);
|
|
1354
|
+
const ci = data.checkIn;
|
|
1355
|
+
console.log(chalk.cyan(ci.name || `Check-in ${ci.id}`));
|
|
1356
|
+
console.log(` ID: ${ci.id}`);
|
|
1357
|
+
console.log(` Status: ${ci.status}`);
|
|
1358
|
+
if (ci.created_at)
|
|
1359
|
+
console.log(` Created: ${ci.created_at}`);
|
|
1360
|
+
if (ci.reports?.length) {
|
|
1361
|
+
console.log(chalk.cyan('\n Reports:'));
|
|
1362
|
+
for (const r of ci.reports) {
|
|
1363
|
+
const icon = r.status === 'completed' ? chalk.green('✓') : r.status === 'missed' ? chalk.red('✗') : '○';
|
|
1364
|
+
console.log(` ${icon} ${r.commitment_description || r.commitment_id} — ${r.status}`);
|
|
1365
|
+
if (r.notes)
|
|
1366
|
+
console.log(` ${r.notes}`);
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
if (ci.commitments?.length) {
|
|
1370
|
+
console.log(chalk.cyan('\n Commitments:'));
|
|
1371
|
+
for (const c of ci.commitments) {
|
|
1372
|
+
const icon = c.status === 'completed' ? chalk.green('✓') : '○';
|
|
1373
|
+
console.log(` ${icon} ${c.description} — due ${c.due_date} — ${c.id}`);
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
catch (err) {
|
|
1378
|
+
console.error(chalk.red(err.message));
|
|
1379
|
+
process.exit(1);
|
|
1380
|
+
}
|
|
1381
|
+
});
|
|
1382
|
+
checkins
|
|
1383
|
+
.command('update')
|
|
1384
|
+
.description('Update a check-in\'s name or notes. The check-in must still be open (not completed).')
|
|
1385
|
+
.argument('<id>', 'Check-in ID (from "ci list" output)')
|
|
1386
|
+
.option('-n, --name <name>', 'New check-in name')
|
|
1387
|
+
.option('--notes <text>', 'General notes for this check-in')
|
|
1388
|
+
.action(async (id, opts) => {
|
|
1389
|
+
try {
|
|
1390
|
+
const body = {};
|
|
1391
|
+
if (opts.name)
|
|
1392
|
+
body.name = opts.name;
|
|
1393
|
+
if (opts.notes != null)
|
|
1394
|
+
body.notes = opts.notes;
|
|
1395
|
+
if (Object.keys(body).length === 0) {
|
|
1396
|
+
console.error(chalk.red('Provide at least one field to update'));
|
|
1397
|
+
process.exit(1);
|
|
1398
|
+
}
|
|
1399
|
+
await api('PATCH', `/api/check-ins/${id}`, body);
|
|
1400
|
+
console.log(chalk.green('Check-in updated'));
|
|
1401
|
+
}
|
|
1402
|
+
catch (err) {
|
|
1403
|
+
console.error(chalk.red(err.message));
|
|
1404
|
+
process.exit(1);
|
|
1405
|
+
}
|
|
1406
|
+
});
|
|
1407
|
+
checkins
|
|
1408
|
+
.command('report')
|
|
1409
|
+
.description('Submit commitment reports for a check-in. Each report records whether a past commitment was completed, missed, or is still in progress.\n\n Example:\n ci report <id> --reports \'[{"commitmentId":"abc","status":"completed","notes":"Shipped it"}]\'')
|
|
1410
|
+
.argument('<id>', 'Check-in ID (from "ci list" output)')
|
|
1411
|
+
.requiredOption('--reports <json>', 'JSON array of reports. Each: {commitmentId, status (completed|missed|in_progress), notes?}')
|
|
1412
|
+
.action(async (id, opts) => {
|
|
1413
|
+
try {
|
|
1414
|
+
const reports = JSON.parse(opts.reports);
|
|
1415
|
+
await api('POST', `/api/check-ins/${id}/reports`, { reports });
|
|
1416
|
+
console.log(chalk.green(`Submitted ${reports.length} report(s)`));
|
|
1417
|
+
}
|
|
1418
|
+
catch (err) {
|
|
1419
|
+
console.error(chalk.red(err.message));
|
|
1420
|
+
process.exit(1);
|
|
1421
|
+
}
|
|
1422
|
+
});
|
|
1423
|
+
checkins
|
|
1424
|
+
.command('add-commitment')
|
|
1425
|
+
.description('Make a new commitment during a check-in. This is how team members pledge specific deliverables for the next period. The commitment will appear in "cm list" and be reportable in the next check-in.')
|
|
1426
|
+
.argument('<id>', 'Check-in ID (from "ci list" output)')
|
|
1427
|
+
.requiredOption('-d, --description <desc>', 'What you commit to do (e.g., "Ship the new API endpoint")')
|
|
1428
|
+
.requiredOption('--due <date>', 'Due date (YYYY-MM-DD)')
|
|
1429
|
+
.option('-a, --action <id>', 'Link to an action ID for tracking')
|
|
1430
|
+
.action(async (id, opts) => {
|
|
1431
|
+
try {
|
|
1432
|
+
const body = { description: opts.description, dueDate: opts.due };
|
|
1433
|
+
if (opts.action)
|
|
1434
|
+
body.actionId = opts.action;
|
|
1435
|
+
const data = await api('POST', `/api/check-ins/${id}/commitments`, body);
|
|
1436
|
+
console.log(chalk.green(`Commitment added: ${data.commitment.description}`));
|
|
1437
|
+
console.log(` ID: ${data.commitment.id}`);
|
|
1438
|
+
}
|
|
1439
|
+
catch (err) {
|
|
1440
|
+
console.error(chalk.red(err.message));
|
|
1441
|
+
process.exit(1);
|
|
1442
|
+
}
|
|
1443
|
+
});
|
|
1444
|
+
checkins
|
|
1445
|
+
.command('complete')
|
|
1446
|
+
.description('Close a check-in. Once completed, no more reports or commitments can be added. This finalizes the check-in record.')
|
|
1447
|
+
.argument('<id>', 'Check-in ID (from "ci list" output)')
|
|
1448
|
+
.action(async (id) => {
|
|
1449
|
+
try {
|
|
1450
|
+
await api('POST', `/api/check-ins/${id}/complete`);
|
|
1451
|
+
console.log(chalk.green('Check-in completed'));
|
|
1452
|
+
}
|
|
1453
|
+
catch (err) {
|
|
1454
|
+
console.error(chalk.red(err.message));
|
|
1455
|
+
process.exit(1);
|
|
1456
|
+
}
|
|
1457
|
+
});
|
|
700
1458
|
// ── Template I/O helpers ───────────────────────────────────
|
|
701
1459
|
function allSteps(definition) {
|
|
702
1460
|
const steps = [];
|
|
@@ -717,8 +1475,24 @@ function requireDraftStatus(template) {
|
|
|
717
1475
|
process.exit(1);
|
|
718
1476
|
}
|
|
719
1477
|
}
|
|
1478
|
+
function findPhaseById(definition, phaseId) {
|
|
1479
|
+
return (definition?.phases || []).find((p) => p.id === phaseId);
|
|
1480
|
+
}
|
|
1481
|
+
const VALID_STEP_TYPES = ['human', 'agent', 'script', 'gate'];
|
|
720
1482
|
// ── Templates ──────────────────────────────────────────────
|
|
721
|
-
const templates = program.command('templates').alias('tpl').description('Pipeline template commands'
|
|
1483
|
+
const templates = program.command('templates').alias('tpl').description('Pipeline template commands.\n\n' +
|
|
1484
|
+
' Templates define multi-step workflows: template → phases → steps → I/O.\n' +
|
|
1485
|
+
' Lifecycle: draft → active → archived (archived → draft to unarchive).\n' +
|
|
1486
|
+
' Structural edits (add/remove/edit phases, steps, I/O) require draft status.\n\n' +
|
|
1487
|
+
' Typical workflow:\n' +
|
|
1488
|
+
' 1. tpl create -n "Name" Create empty draft template\n' +
|
|
1489
|
+
' 2. tpl add-phase <id> --name ... Add phases\n' +
|
|
1490
|
+
' 3. tpl add-step <id> --phase ... Add steps to phases\n' +
|
|
1491
|
+
' 4. tpl add-output / add-input Wire I/O between steps\n' +
|
|
1492
|
+
' 5. tpl validate <id> Check for errors\n' +
|
|
1493
|
+
' 6. tpl activate <id> Make available for instances\n\n' +
|
|
1494
|
+
' Use "tpl steps <id>" to list step IDs needed by --step options.\n' +
|
|
1495
|
+
' Use "tpl show <id>" to see full structure with I/O declarations.');
|
|
722
1496
|
templates
|
|
723
1497
|
.command('list')
|
|
724
1498
|
.description('List pipeline templates')
|
|
@@ -744,8 +1518,8 @@ templates
|
|
|
744
1518
|
});
|
|
745
1519
|
templates
|
|
746
1520
|
.command('show')
|
|
747
|
-
.description('Show template detail')
|
|
748
|
-
.argument('<id>', 'Template ID')
|
|
1521
|
+
.description('Show template detail including phases, steps, I/O declarations, and pipeline inputs. Use this to inspect the full structure of a template and discover phase/step IDs needed by other commands.')
|
|
1522
|
+
.argument('<id>', 'Template ID (use "tpl list" to find IDs)')
|
|
749
1523
|
.action(async (id) => {
|
|
750
1524
|
try {
|
|
751
1525
|
const data = await api('GET', `/api/pipeline-templates/${id}`);
|
|
@@ -803,19 +1577,243 @@ templates
|
|
|
803
1577
|
}
|
|
804
1578
|
});
|
|
805
1579
|
templates
|
|
806
|
-
.command('create')
|
|
807
|
-
.description('Create
|
|
808
|
-
.requiredOption('-n, --name <name>', 'Template name')
|
|
809
|
-
.option('-d, --description <desc>', 'Description')
|
|
810
|
-
.action(async (opts) => {
|
|
1580
|
+
.command('create')
|
|
1581
|
+
.description('Create an empty pipeline template in draft status. After creation, use add-phase, add-step, and I/O commands to build the structure, then activate.')
|
|
1582
|
+
.requiredOption('-n, --name <name>', 'Template name')
|
|
1583
|
+
.option('-d, --description <desc>', 'Description')
|
|
1584
|
+
.action(async (opts) => {
|
|
1585
|
+
try {
|
|
1586
|
+
const body = { name: opts.name, definition: { phases: [] } };
|
|
1587
|
+
if (opts.description)
|
|
1588
|
+
body.description = opts.description;
|
|
1589
|
+
const data = await api('POST', '/api/pipeline-templates', body);
|
|
1590
|
+
const t = data.template;
|
|
1591
|
+
console.log(chalk.green(`Created: ${t.name}`));
|
|
1592
|
+
console.log(` ID: ${t.id}`);
|
|
1593
|
+
}
|
|
1594
|
+
catch (err) {
|
|
1595
|
+
console.error(chalk.red(err.message));
|
|
1596
|
+
process.exit(1);
|
|
1597
|
+
}
|
|
1598
|
+
});
|
|
1599
|
+
templates
|
|
1600
|
+
.command('import')
|
|
1601
|
+
.description('Import a JOE template JSON file as a Fazemos pipeline template. Imports steps, outputs, inputs, and pipeline-level inputs. Step IDs are remapped to new UUIDs and input references are updated accordingly.')
|
|
1602
|
+
.argument('<file>', 'Path to JOE template JSON file')
|
|
1603
|
+
.action(async (file) => {
|
|
1604
|
+
try {
|
|
1605
|
+
const raw = JSON.parse(readFileSync(resolve(file), 'utf-8'));
|
|
1606
|
+
// Map JOE template to Fazemos format
|
|
1607
|
+
const joeSteps = raw.steps || [];
|
|
1608
|
+
// Build ID mapping so input source_step_id references survive UUID reassignment
|
|
1609
|
+
const idMap = new Map();
|
|
1610
|
+
for (const s of joeSteps) {
|
|
1611
|
+
if (s.id)
|
|
1612
|
+
idMap.set(s.id, crypto.randomUUID());
|
|
1613
|
+
}
|
|
1614
|
+
const definition = {
|
|
1615
|
+
inputs: (raw.inputs || []).map((i) => ({
|
|
1616
|
+
name: i.name,
|
|
1617
|
+
type: i.type || 'text',
|
|
1618
|
+
required: i.required !== false,
|
|
1619
|
+
...(i.description ? { description: i.description } : {}),
|
|
1620
|
+
...(i.default_value != null ? { default_value: i.default_value } : {}),
|
|
1621
|
+
})),
|
|
1622
|
+
phases: [{
|
|
1623
|
+
id: crypto.randomUUID(),
|
|
1624
|
+
name: raw.name || 'Main',
|
|
1625
|
+
description: raw.description || '',
|
|
1626
|
+
deliverables: [],
|
|
1627
|
+
steps: joeSteps.map((s, i) => ({
|
|
1628
|
+
id: idMap.get(s.id) || crypto.randomUUID(),
|
|
1629
|
+
name: s.name,
|
|
1630
|
+
description: s.description || '',
|
|
1631
|
+
step_type: s.executionMode === 'script' ? 'script' : (s.agent ? 'agent' : 'human'),
|
|
1632
|
+
role: s.role || s.agent || 'unassigned',
|
|
1633
|
+
inputs: (s.inputs || []).map((inp) => ({
|
|
1634
|
+
name: inp.name,
|
|
1635
|
+
...(inp.source_step_id ? { source_step_id: idMap.get(inp.source_step_id) || inp.source_step_id, source_output_name: inp.source_output_name } : {}),
|
|
1636
|
+
...(inp.pipeline_input ? { pipeline_input: inp.pipeline_input } : {}),
|
|
1637
|
+
...(inp.description ? { description: inp.description } : {}),
|
|
1638
|
+
required: inp.required !== false,
|
|
1639
|
+
})),
|
|
1640
|
+
outputs: (s.outputs || []).map((o) => ({
|
|
1641
|
+
name: o.name,
|
|
1642
|
+
type: o.type || 'text',
|
|
1643
|
+
...(o.description ? { description: o.description } : {}),
|
|
1644
|
+
required: o.required !== false,
|
|
1645
|
+
...(o.format ? { format: o.format } : {}),
|
|
1646
|
+
})),
|
|
1647
|
+
sections: s.sections || '',
|
|
1648
|
+
reviewer: s.reviewer || null,
|
|
1649
|
+
max_review_cycles: s.maxReviewCycles || 0,
|
|
1650
|
+
execution_config: s.executionMode === 'script' ? {
|
|
1651
|
+
image: s.image || '',
|
|
1652
|
+
command: s.command || '',
|
|
1653
|
+
} : null,
|
|
1654
|
+
parallel_group: null,
|
|
1655
|
+
sort_order: i,
|
|
1656
|
+
})),
|
|
1657
|
+
}],
|
|
1658
|
+
};
|
|
1659
|
+
const body = {
|
|
1660
|
+
name: raw.name || file,
|
|
1661
|
+
description: raw.description || '',
|
|
1662
|
+
definition,
|
|
1663
|
+
};
|
|
1664
|
+
const data = await api('POST', '/api/pipeline-templates', body);
|
|
1665
|
+
const t = data.template;
|
|
1666
|
+
console.log(chalk.green(`Imported: ${t.name} (${joeSteps.length} steps)`));
|
|
1667
|
+
console.log(` ID: ${t.id}`);
|
|
1668
|
+
}
|
|
1669
|
+
catch (err) {
|
|
1670
|
+
console.error(chalk.red(err.message));
|
|
1671
|
+
process.exit(1);
|
|
1672
|
+
}
|
|
1673
|
+
});
|
|
1674
|
+
templates
|
|
1675
|
+
.command('update-definition')
|
|
1676
|
+
.description('Update a template definition from a JSON file')
|
|
1677
|
+
.argument('<id>', 'Template ID')
|
|
1678
|
+
.argument('<file>', 'Path to definition JSON file')
|
|
1679
|
+
.action(async (id, file) => {
|
|
1680
|
+
try {
|
|
1681
|
+
const definition = JSON.parse(readFileSync(resolve(file), 'utf-8'));
|
|
1682
|
+
await api('PUT', `/api/pipeline-templates/${id}`, { definition });
|
|
1683
|
+
console.log(chalk.green('Template definition updated'));
|
|
1684
|
+
}
|
|
1685
|
+
catch (err) {
|
|
1686
|
+
console.error(chalk.red(err.message));
|
|
1687
|
+
process.exit(1);
|
|
1688
|
+
}
|
|
1689
|
+
});
|
|
1690
|
+
templates
|
|
1691
|
+
.command('activate')
|
|
1692
|
+
.description('Activate a draft template (required before creating pipeline instances). Template must have at least one phase with at least one step. Status changes from draft → active.')
|
|
1693
|
+
.argument('<id>', 'Template ID')
|
|
1694
|
+
.action(async (id) => {
|
|
1695
|
+
try {
|
|
1696
|
+
await api('PATCH', `/api/pipeline-templates/${id}/status`, { status: 'active' });
|
|
1697
|
+
console.log(chalk.green('Template activated'));
|
|
1698
|
+
}
|
|
1699
|
+
catch (err) {
|
|
1700
|
+
console.error(chalk.red(err.message));
|
|
1701
|
+
process.exit(1);
|
|
1702
|
+
}
|
|
1703
|
+
});
|
|
1704
|
+
// ── Template structure commands ─────────────────────────────
|
|
1705
|
+
templates
|
|
1706
|
+
.command('update')
|
|
1707
|
+
.description('Update template name or description. Works on any status (draft, active, or archived). Does not modify the definition or bump version.')
|
|
1708
|
+
.argument('<id>', 'Template ID')
|
|
1709
|
+
.option('-n, --name <name>', 'New name')
|
|
1710
|
+
.option('-d, --description <desc>', 'New description')
|
|
1711
|
+
.action(async (id, opts) => {
|
|
1712
|
+
try {
|
|
1713
|
+
if (!opts.name && opts.description == null) {
|
|
1714
|
+
console.error(chalk.red('Provide --name and/or --description'));
|
|
1715
|
+
process.exit(1);
|
|
1716
|
+
}
|
|
1717
|
+
const body = {};
|
|
1718
|
+
if (opts.name)
|
|
1719
|
+
body.name = opts.name;
|
|
1720
|
+
if (opts.description != null)
|
|
1721
|
+
body.description = opts.description;
|
|
1722
|
+
const data = await api('PUT', `/api/pipeline-templates/${id}`, body);
|
|
1723
|
+
console.log(chalk.green(`Updated: ${data.template.name}`));
|
|
1724
|
+
}
|
|
1725
|
+
catch (err) {
|
|
1726
|
+
console.error(chalk.red(err.message));
|
|
1727
|
+
process.exit(1);
|
|
1728
|
+
}
|
|
1729
|
+
});
|
|
1730
|
+
templates
|
|
1731
|
+
.command('archive')
|
|
1732
|
+
.description('Archive an active template. Archived templates cannot be used to create new instances. Use "tpl unarchive" to move back to draft for editing.')
|
|
1733
|
+
.argument('<id>', 'Template ID')
|
|
1734
|
+
.action(async (id) => {
|
|
1735
|
+
try {
|
|
1736
|
+
await api('PATCH', `/api/pipeline-templates/${id}/status`, { status: 'archived' });
|
|
1737
|
+
console.log(chalk.green('Template archived'));
|
|
1738
|
+
}
|
|
1739
|
+
catch (err) {
|
|
1740
|
+
console.error(chalk.red(err.message));
|
|
1741
|
+
process.exit(1);
|
|
1742
|
+
}
|
|
1743
|
+
});
|
|
1744
|
+
templates
|
|
1745
|
+
.command('unarchive')
|
|
1746
|
+
.description('Move an archived template back to draft status for editing. After edits, use "tpl activate" to make it available again.')
|
|
1747
|
+
.argument('<id>', 'Template ID')
|
|
1748
|
+
.action(async (id) => {
|
|
1749
|
+
try {
|
|
1750
|
+
await api('PATCH', `/api/pipeline-templates/${id}/status`, { status: 'draft' });
|
|
1751
|
+
console.log(chalk.green('Template moved to draft'));
|
|
1752
|
+
}
|
|
1753
|
+
catch (err) {
|
|
1754
|
+
console.error(chalk.red(err.message));
|
|
1755
|
+
process.exit(1);
|
|
1756
|
+
}
|
|
1757
|
+
});
|
|
1758
|
+
templates
|
|
1759
|
+
.command('add-phase')
|
|
1760
|
+
.description('Add a phase to a template. Phases group steps into logical stages (e.g., "Design", "Build", "Test"). Template must be in draft status. Phase names must be unique within the template. Returns the generated phase ID needed by add-step.')
|
|
1761
|
+
.argument('<templateId>', 'Template ID')
|
|
1762
|
+
.requiredOption('--name <name>', 'Phase name (must be unique within the template)')
|
|
1763
|
+
.option('--description <desc>', 'Phase description')
|
|
1764
|
+
.action(async (templateId, opts) => {
|
|
1765
|
+
try {
|
|
1766
|
+
const data = await api('GET', `/api/pipeline-templates/${templateId}`);
|
|
1767
|
+
const t = data.template;
|
|
1768
|
+
requireDraftStatus(t);
|
|
1769
|
+
if (!t.definition.phases)
|
|
1770
|
+
t.definition.phases = [];
|
|
1771
|
+
if (t.definition.phases.find((p) => p.name === opts.name)) {
|
|
1772
|
+
console.error(chalk.red(`Phase "${opts.name}" already exists`));
|
|
1773
|
+
process.exit(1);
|
|
1774
|
+
}
|
|
1775
|
+
const phase = {
|
|
1776
|
+
id: crypto.randomUUID(),
|
|
1777
|
+
name: opts.name,
|
|
1778
|
+
description: opts.description || '',
|
|
1779
|
+
deliverables: [],
|
|
1780
|
+
steps: [],
|
|
1781
|
+
};
|
|
1782
|
+
t.definition.phases.push(phase);
|
|
1783
|
+
await api('PUT', `/api/pipeline-templates/${templateId}`, { definition: t.definition });
|
|
1784
|
+
console.log(chalk.green(`Added phase: ${opts.name}`));
|
|
1785
|
+
console.log(` ID: ${phase.id}`);
|
|
1786
|
+
}
|
|
1787
|
+
catch (err) {
|
|
1788
|
+
console.error(chalk.red(err.message));
|
|
1789
|
+
process.exit(1);
|
|
1790
|
+
}
|
|
1791
|
+
});
|
|
1792
|
+
templates
|
|
1793
|
+
.command('remove-phase')
|
|
1794
|
+
.description('Remove a phase from a template. Blocked if the phase contains steps unless --force is used. Template must be in draft status. Use "tpl show" to find phase IDs.')
|
|
1795
|
+
.argument('<templateId>', 'Template ID')
|
|
1796
|
+
.requiredOption('--phase <phaseId>', 'Phase ID (from "tpl show" output)')
|
|
1797
|
+
.option('--force', 'Remove even if phase contains steps')
|
|
1798
|
+
.action(async (templateId, opts) => {
|
|
811
1799
|
try {
|
|
812
|
-
const
|
|
813
|
-
if (opts.description)
|
|
814
|
-
body.description = opts.description;
|
|
815
|
-
const data = await api('POST', '/api/pipeline-templates', body);
|
|
1800
|
+
const data = await api('GET', `/api/pipeline-templates/${templateId}`);
|
|
816
1801
|
const t = data.template;
|
|
817
|
-
|
|
818
|
-
|
|
1802
|
+
requireDraftStatus(t);
|
|
1803
|
+
const idx = (t.definition.phases || []).findIndex((p) => p.id === opts.phase);
|
|
1804
|
+
if (idx === -1) {
|
|
1805
|
+
console.error(chalk.red(`Phase "${opts.phase}" not found`));
|
|
1806
|
+
process.exit(1);
|
|
1807
|
+
}
|
|
1808
|
+
const phase = t.definition.phases[idx];
|
|
1809
|
+
if (phase.steps?.length && !opts.force) {
|
|
1810
|
+
console.error(chalk.yellow(`Phase "${phase.name}" has ${phase.steps.length} steps`));
|
|
1811
|
+
console.error(chalk.yellow('Use --force to remove anyway'));
|
|
1812
|
+
process.exit(1);
|
|
1813
|
+
}
|
|
1814
|
+
t.definition.phases.splice(idx, 1);
|
|
1815
|
+
await api('PUT', `/api/pipeline-templates/${templateId}`, { definition: t.definition });
|
|
1816
|
+
console.log(chalk.green(`Removed phase: ${phase.name}`));
|
|
819
1817
|
}
|
|
820
1818
|
catch (err) {
|
|
821
1819
|
console.error(chalk.red(err.message));
|
|
@@ -823,74 +1821,137 @@ templates
|
|
|
823
1821
|
}
|
|
824
1822
|
});
|
|
825
1823
|
templates
|
|
826
|
-
.command('
|
|
827
|
-
.description('
|
|
828
|
-
.argument('<
|
|
829
|
-
.
|
|
1824
|
+
.command('edit-phase')
|
|
1825
|
+
.description('Edit a phase name or description. Template must be in draft status. Provide at least one of --name or --description.')
|
|
1826
|
+
.argument('<templateId>', 'Template ID')
|
|
1827
|
+
.requiredOption('--phase <phaseId>', 'Phase ID (from "tpl show" output)')
|
|
1828
|
+
.option('--name <name>', 'New phase name (must be unique within the template)')
|
|
1829
|
+
.option('--description <desc>', 'New phase description')
|
|
1830
|
+
.action(async (templateId, opts) => {
|
|
830
1831
|
try {
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
// Build ID mapping so input source_step_id references survive UUID reassignment
|
|
835
|
-
const idMap = new Map();
|
|
836
|
-
for (const s of joeSteps) {
|
|
837
|
-
if (s.id)
|
|
838
|
-
idMap.set(s.id, crypto.randomUUID());
|
|
1832
|
+
if (!opts.name && opts.description == null) {
|
|
1833
|
+
console.error(chalk.red('Provide --name and/or --description'));
|
|
1834
|
+
process.exit(1);
|
|
839
1835
|
}
|
|
840
|
-
const
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
1836
|
+
const data = await api('GET', `/api/pipeline-templates/${templateId}`);
|
|
1837
|
+
const t = data.template;
|
|
1838
|
+
requireDraftStatus(t);
|
|
1839
|
+
const phase = findPhaseById(t.definition, opts.phase);
|
|
1840
|
+
if (!phase) {
|
|
1841
|
+
console.error(chalk.red(`Phase "${opts.phase}" not found`));
|
|
1842
|
+
process.exit(1);
|
|
1843
|
+
}
|
|
1844
|
+
if (opts.name) {
|
|
1845
|
+
if (t.definition.phases.find((p) => p.id !== opts.phase && p.name === opts.name)) {
|
|
1846
|
+
console.error(chalk.red(`Phase name "${opts.name}" already exists`));
|
|
1847
|
+
process.exit(1);
|
|
1848
|
+
}
|
|
1849
|
+
phase.name = opts.name;
|
|
1850
|
+
}
|
|
1851
|
+
if (opts.description != null)
|
|
1852
|
+
phase.description = opts.description;
|
|
1853
|
+
await api('PUT', `/api/pipeline-templates/${templateId}`, { definition: t.definition });
|
|
1854
|
+
console.log(chalk.green(`Updated phase: ${phase.name}`));
|
|
1855
|
+
}
|
|
1856
|
+
catch (err) {
|
|
1857
|
+
console.error(chalk.red(err.message));
|
|
1858
|
+
process.exit(1);
|
|
1859
|
+
}
|
|
1860
|
+
});
|
|
1861
|
+
templates
|
|
1862
|
+
.command('add-step')
|
|
1863
|
+
.description('Add a step to a phase. Template must be in draft status. Step names must be unique within their phase. Returns the generated step ID needed by I/O commands (add-output, add-input, etc.). Use "tpl show" or "tpl steps" to find existing step IDs.')
|
|
1864
|
+
.argument('<templateId>', 'Template ID')
|
|
1865
|
+
.requiredOption('--phase <phaseId>', 'Phase ID (from "tpl show" or "tpl add-phase" output)')
|
|
1866
|
+
.requiredOption('--name <name>', 'Step name (must be unique within the phase)')
|
|
1867
|
+
.option('--type <type>', 'Step type: human (manual task), agent (AI agent), script (automated), gate (approval checkpoint)', 'human')
|
|
1868
|
+
.option('--role <role>', 'Role or agent name (e.g., "kate", "marco", "dev-team")')
|
|
1869
|
+
.option('--description <desc>', 'Step description')
|
|
1870
|
+
.option('--reviewer <reviewer>', 'Reviewer role for review steps')
|
|
1871
|
+
.option('--max-review-cycles <n>', 'Max review cycles before auto-approval', '0')
|
|
1872
|
+
.option('--parallel-group <group>', 'Group name for parallel execution (steps in the same group run concurrently)')
|
|
1873
|
+
.action(async (templateId, opts) => {
|
|
1874
|
+
try {
|
|
1875
|
+
if (!VALID_STEP_TYPES.includes(opts.type)) {
|
|
1876
|
+
console.error(chalk.red(`Invalid step type "${opts.type}". Valid: ${VALID_STEP_TYPES.join(', ')}`));
|
|
1877
|
+
process.exit(1);
|
|
1878
|
+
}
|
|
1879
|
+
const data = await api('GET', `/api/pipeline-templates/${templateId}`);
|
|
1880
|
+
const t = data.template;
|
|
1881
|
+
requireDraftStatus(t);
|
|
1882
|
+
const phase = findPhaseById(t.definition, opts.phase);
|
|
1883
|
+
if (!phase) {
|
|
1884
|
+
console.error(chalk.red(`Phase "${opts.phase}" not found`));
|
|
1885
|
+
process.exit(1);
|
|
1886
|
+
}
|
|
1887
|
+
if (!phase.steps)
|
|
1888
|
+
phase.steps = [];
|
|
1889
|
+
if (phase.steps.find((s) => s.name === opts.name)) {
|
|
1890
|
+
console.error(chalk.red(`Step "${opts.name}" already exists in phase "${phase.name}"`));
|
|
1891
|
+
process.exit(1);
|
|
1892
|
+
}
|
|
1893
|
+
const step = {
|
|
1894
|
+
id: crypto.randomUUID(),
|
|
1895
|
+
name: opts.name,
|
|
1896
|
+
description: opts.description || '',
|
|
1897
|
+
step_type: opts.type,
|
|
1898
|
+
role: opts.role || 'unassigned',
|
|
1899
|
+
inputs: [],
|
|
1900
|
+
outputs: [],
|
|
1901
|
+
sections: '',
|
|
1902
|
+
reviewer: opts.reviewer || null,
|
|
1903
|
+
max_review_cycles: parseInt(opts.maxReviewCycles) || 0,
|
|
1904
|
+
execution_config: null,
|
|
1905
|
+
parallel_group: opts.parallelGroup || null,
|
|
1906
|
+
sort_order: phase.steps.length,
|
|
889
1907
|
};
|
|
890
|
-
|
|
1908
|
+
phase.steps.push(step);
|
|
1909
|
+
await api('PUT', `/api/pipeline-templates/${templateId}`, { definition: t.definition });
|
|
1910
|
+
console.log(chalk.green(`Added step: ${opts.name} (${opts.type}) to phase ${phase.name}`));
|
|
1911
|
+
console.log(` ID: ${step.id}`);
|
|
1912
|
+
}
|
|
1913
|
+
catch (err) {
|
|
1914
|
+
console.error(chalk.red(err.message));
|
|
1915
|
+
process.exit(1);
|
|
1916
|
+
}
|
|
1917
|
+
});
|
|
1918
|
+
templates
|
|
1919
|
+
.command('remove-step')
|
|
1920
|
+
.description('Remove a step from a template. Blocked if other steps reference this step\'s outputs as inputs unless --force is used. Template must be in draft status. Use "tpl steps" to find step IDs.')
|
|
1921
|
+
.argument('<templateId>', 'Template ID')
|
|
1922
|
+
.requiredOption('--step <stepId>', 'Step ID (from "tpl steps" output)')
|
|
1923
|
+
.option('--force', 'Remove even if other steps reference this step\'s outputs')
|
|
1924
|
+
.action(async (templateId, opts) => {
|
|
1925
|
+
try {
|
|
1926
|
+
const data = await api('GET', `/api/pipeline-templates/${templateId}`);
|
|
891
1927
|
const t = data.template;
|
|
892
|
-
|
|
893
|
-
|
|
1928
|
+
requireDraftStatus(t);
|
|
1929
|
+
// Find the step and its phase
|
|
1930
|
+
let targetPhase = null;
|
|
1931
|
+
let stepIdx = -1;
|
|
1932
|
+
for (const phase of t.definition.phases || []) {
|
|
1933
|
+
const idx = (phase.steps || []).findIndex((s) => s.id === opts.step);
|
|
1934
|
+
if (idx !== -1) {
|
|
1935
|
+
targetPhase = phase;
|
|
1936
|
+
stepIdx = idx;
|
|
1937
|
+
break;
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
if (!targetPhase) {
|
|
1941
|
+
console.error(chalk.red(`Step "${opts.step}" not found`));
|
|
1942
|
+
process.exit(1);
|
|
1943
|
+
}
|
|
1944
|
+
const step = targetPhase.steps[stepIdx];
|
|
1945
|
+
// Check for references from other steps
|
|
1946
|
+
const refs = allSteps(t.definition).filter((s) => s.id !== step.id && (s.inputs || []).some((inp) => inp.source_step_id === step.id));
|
|
1947
|
+
if (refs.length && !opts.force) {
|
|
1948
|
+
console.error(chalk.yellow(`Step "${step.name}" is referenced by: ${refs.map((r) => r.name).join(', ')}`));
|
|
1949
|
+
console.error(chalk.yellow('Use --force to remove anyway'));
|
|
1950
|
+
process.exit(1);
|
|
1951
|
+
}
|
|
1952
|
+
targetPhase.steps.splice(stepIdx, 1);
|
|
1953
|
+
await api('PUT', `/api/pipeline-templates/${templateId}`, { definition: t.definition });
|
|
1954
|
+
console.log(chalk.green(`Removed step: ${step.name}`));
|
|
894
1955
|
}
|
|
895
1956
|
catch (err) {
|
|
896
1957
|
console.error(chalk.red(err.message));
|
|
@@ -898,13 +1959,64 @@ templates
|
|
|
898
1959
|
}
|
|
899
1960
|
});
|
|
900
1961
|
templates
|
|
901
|
-
.command('
|
|
902
|
-
.description('
|
|
903
|
-
.argument('<
|
|
904
|
-
.
|
|
1962
|
+
.command('edit-step')
|
|
1963
|
+
.description('Edit step properties. Template must be in draft status. Provide at least one field to update. Use "tpl steps" to find step IDs.')
|
|
1964
|
+
.argument('<templateId>', 'Template ID')
|
|
1965
|
+
.requiredOption('--step <stepId>', 'Step ID (from "tpl steps" output)')
|
|
1966
|
+
.option('--name <name>', 'New step name (must be unique within the phase)')
|
|
1967
|
+
.option('--type <type>', 'Step type: human, agent, script, gate')
|
|
1968
|
+
.option('--role <role>', 'Role or agent name')
|
|
1969
|
+
.option('--description <desc>', 'Description')
|
|
1970
|
+
.option('--reviewer <reviewer>', 'Reviewer role (empty string to clear)')
|
|
1971
|
+
.option('--max-review-cycles <n>', 'Max review cycles')
|
|
1972
|
+
.option('--parallel-group <group>', 'Parallel execution group (empty string to clear)')
|
|
1973
|
+
.action(async (templateId, opts) => {
|
|
905
1974
|
try {
|
|
906
|
-
|
|
907
|
-
|
|
1975
|
+
const hasUpdate = opts.name || opts.type || opts.role || opts.description != null
|
|
1976
|
+
|| opts.reviewer != null || opts.maxReviewCycles != null || opts.parallelGroup != null;
|
|
1977
|
+
if (!hasUpdate) {
|
|
1978
|
+
console.error(chalk.red('Provide at least one field to update'));
|
|
1979
|
+
process.exit(1);
|
|
1980
|
+
}
|
|
1981
|
+
if (opts.type && !VALID_STEP_TYPES.includes(opts.type)) {
|
|
1982
|
+
console.error(chalk.red(`Invalid step type "${opts.type}". Valid: ${VALID_STEP_TYPES.join(', ')}`));
|
|
1983
|
+
process.exit(1);
|
|
1984
|
+
}
|
|
1985
|
+
const data = await api('GET', `/api/pipeline-templates/${templateId}`);
|
|
1986
|
+
const t = data.template;
|
|
1987
|
+
requireDraftStatus(t);
|
|
1988
|
+
const step = findStepById(t.definition, opts.step);
|
|
1989
|
+
if (!step) {
|
|
1990
|
+
console.error(chalk.red(`Step "${opts.step}" not found`));
|
|
1991
|
+
process.exit(1);
|
|
1992
|
+
}
|
|
1993
|
+
if (opts.name) {
|
|
1994
|
+
// Check uniqueness within the step's phase
|
|
1995
|
+
for (const phase of t.definition.phases || []) {
|
|
1996
|
+
if (phase.steps?.find((s) => s.id === step.id)) {
|
|
1997
|
+
if (phase.steps.find((s) => s.id !== step.id && s.name === opts.name)) {
|
|
1998
|
+
console.error(chalk.red(`Step name "${opts.name}" already exists in phase "${phase.name}"`));
|
|
1999
|
+
process.exit(1);
|
|
2000
|
+
}
|
|
2001
|
+
break;
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
step.name = opts.name;
|
|
2005
|
+
}
|
|
2006
|
+
if (opts.type)
|
|
2007
|
+
step.step_type = opts.type;
|
|
2008
|
+
if (opts.role)
|
|
2009
|
+
step.role = opts.role;
|
|
2010
|
+
if (opts.description != null)
|
|
2011
|
+
step.description = opts.description;
|
|
2012
|
+
if (opts.reviewer != null)
|
|
2013
|
+
step.reviewer = opts.reviewer || null;
|
|
2014
|
+
if (opts.maxReviewCycles != null)
|
|
2015
|
+
step.max_review_cycles = parseInt(opts.maxReviewCycles) || 0;
|
|
2016
|
+
if (opts.parallelGroup != null)
|
|
2017
|
+
step.parallel_group = opts.parallelGroup || null;
|
|
2018
|
+
await api('PUT', `/api/pipeline-templates/${templateId}`, { definition: t.definition });
|
|
2019
|
+
console.log(chalk.green(`Updated step: ${step.name}`));
|
|
908
2020
|
}
|
|
909
2021
|
catch (err) {
|
|
910
2022
|
console.error(chalk.red(err.message));
|
|
@@ -914,7 +2026,7 @@ templates
|
|
|
914
2026
|
// ── Template I/O commands ──────────────────────────────────
|
|
915
2027
|
templates
|
|
916
2028
|
.command('steps')
|
|
917
|
-
.description('List step IDs and names in a template')
|
|
2029
|
+
.description('List step IDs and names in a template. Use this to discover step IDs needed by --step options in add-output, add-input, remove-step, edit-step, etc.')
|
|
918
2030
|
.argument('<id>', 'Template ID')
|
|
919
2031
|
.action(async (id) => {
|
|
920
2032
|
try {
|
|
@@ -935,14 +2047,14 @@ templates
|
|
|
935
2047
|
});
|
|
936
2048
|
templates
|
|
937
2049
|
.command('add-output')
|
|
938
|
-
.description('
|
|
2050
|
+
.description('Declare what a step produces. Outputs can be consumed by downstream steps via add-input. Template must be in draft status. Output names must be unique within the step.')
|
|
939
2051
|
.argument('<templateId>', 'Template ID')
|
|
940
|
-
.requiredOption('--step <stepId>', 'Step ID')
|
|
941
|
-
.requiredOption('--name <name>', 'Output name')
|
|
942
|
-
.requiredOption('--type <type>',
|
|
943
|
-
.option('--description <desc>', '
|
|
944
|
-
.option('--optional', 'Mark as not required')
|
|
945
|
-
.option('--format <format>', 'Format hint
|
|
2052
|
+
.requiredOption('--step <stepId>', 'Step ID (from "tpl steps" output)')
|
|
2053
|
+
.requiredOption('--name <name>', 'Output name (unique within the step, e.g., "requirements", "metrics_data")')
|
|
2054
|
+
.requiredOption('--type <type>', 'Data type: text, markdown, number, boolean, url, json, object, array')
|
|
2055
|
+
.option('--description <desc>', 'Human-readable description of what this output contains')
|
|
2056
|
+
.option('--optional', 'Mark as not required (default: required)')
|
|
2057
|
+
.option('--format <format>', 'Format hint: file_path, commit_sha, semver, date, datetime, email, integer, percentage')
|
|
946
2058
|
.action(async (templateId, opts) => {
|
|
947
2059
|
try {
|
|
948
2060
|
if (!VALID_IO_TYPES.includes(opts.type)) {
|
|
@@ -979,10 +2091,10 @@ templates
|
|
|
979
2091
|
});
|
|
980
2092
|
templates
|
|
981
2093
|
.command('remove-output')
|
|
982
|
-
.description('Remove an output declaration from a step')
|
|
2094
|
+
.description('Remove an output declaration from a step. Blocked if downstream steps consume this output via add-input unless --force is used. Template must be in draft status.')
|
|
983
2095
|
.argument('<templateId>', 'Template ID')
|
|
984
|
-
.requiredOption('--step <stepId>', 'Step ID')
|
|
985
|
-
.requiredOption('--name <name>', 'Output name')
|
|
2096
|
+
.requiredOption('--step <stepId>', 'Step ID (from "tpl steps" output)')
|
|
2097
|
+
.requiredOption('--name <name>', 'Output name to remove')
|
|
986
2098
|
.option('--force', 'Remove even if downstream steps reference this output')
|
|
987
2099
|
.action(async (templateId, opts) => {
|
|
988
2100
|
try {
|
|
@@ -1017,15 +2129,18 @@ templates
|
|
|
1017
2129
|
});
|
|
1018
2130
|
templates
|
|
1019
2131
|
.command('add-input')
|
|
1020
|
-
.description('
|
|
2132
|
+
.description('Declare what a step consumes. Two modes (mutually exclusive):\n' +
|
|
2133
|
+
' Step-sourced: --source-step + --source-output (consumes output from another step)\n' +
|
|
2134
|
+
' Pipeline-sourced: --pipeline-input (consumes a template-level input)\n' +
|
|
2135
|
+
'Validates that the source step/output/pipeline-input exists. Template must be in draft status.')
|
|
1021
2136
|
.argument('<templateId>', 'Template ID')
|
|
1022
|
-
.requiredOption('--step <stepId>', 'Step ID')
|
|
1023
|
-
.requiredOption('--name <name>', '
|
|
1024
|
-
.option('--source-step <stepId>', '
|
|
1025
|
-
.option('--source-output <name>', '
|
|
1026
|
-
.option('--pipeline-input <name>', 'Pipeline-level input name (
|
|
1027
|
-
.option('--description <desc>', '
|
|
1028
|
-
.option('--optional', 'Mark as not required')
|
|
2137
|
+
.requiredOption('--step <stepId>', 'Step ID receiving the input (from "tpl steps" output)')
|
|
2138
|
+
.requiredOption('--name <name>', 'Local input name (unique within the step)')
|
|
2139
|
+
.option('--source-step <stepId>', 'Upstream step ID that produces the data (use with --source-output)')
|
|
2140
|
+
.option('--source-output <name>', 'Output name on the source step (use with --source-step)')
|
|
2141
|
+
.option('--pipeline-input <name>', 'Pipeline-level input name (use instead of --source-step/--source-output)')
|
|
2142
|
+
.option('--description <desc>', 'What this data is used for')
|
|
2143
|
+
.option('--optional', 'Mark as not required (default: required)')
|
|
1029
2144
|
.action(async (templateId, opts) => {
|
|
1030
2145
|
try {
|
|
1031
2146
|
const hasSrc = opts.sourceStep || opts.sourceOutput;
|
|
@@ -1106,10 +2221,10 @@ templates
|
|
|
1106
2221
|
});
|
|
1107
2222
|
templates
|
|
1108
2223
|
.command('remove-input')
|
|
1109
|
-
.description('Remove an input declaration from a step')
|
|
2224
|
+
.description('Remove an input declaration from a step. Template must be in draft status.')
|
|
1110
2225
|
.argument('<templateId>', 'Template ID')
|
|
1111
|
-
.requiredOption('--step <stepId>', 'Step ID')
|
|
1112
|
-
.requiredOption('--name <name>', 'Input name')
|
|
2226
|
+
.requiredOption('--step <stepId>', 'Step ID (from "tpl steps" output)')
|
|
2227
|
+
.requiredOption('--name <name>', 'Input name to remove')
|
|
1113
2228
|
.action(async (templateId, opts) => {
|
|
1114
2229
|
try {
|
|
1115
2230
|
const data = await api('GET', `/api/pipeline-templates/${templateId}`);
|
|
@@ -1136,12 +2251,12 @@ templates
|
|
|
1136
2251
|
});
|
|
1137
2252
|
templates
|
|
1138
2253
|
.command('add-pipeline-input')
|
|
1139
|
-
.description('Add a
|
|
2254
|
+
.description('Add a template-level input that is provided when creating a pipeline instance. Steps can consume these via "tpl add-input --pipeline-input <name>". Template must be in draft status.')
|
|
1140
2255
|
.argument('<templateId>', 'Template ID')
|
|
1141
|
-
.requiredOption('--name <name>', 'Input name')
|
|
1142
|
-
.requiredOption('--type <type>',
|
|
1143
|
-
.option('--description <desc>', '
|
|
1144
|
-
.option('--optional', 'Not required at instance creation')
|
|
2256
|
+
.requiredOption('--name <name>', 'Input name (unique within the template)')
|
|
2257
|
+
.requiredOption('--type <type>', 'Data type: text, markdown, number, boolean, url, json, object, array')
|
|
2258
|
+
.option('--description <desc>', 'Human-readable description')
|
|
2259
|
+
.option('--optional', 'Not required at instance creation (default: required)')
|
|
1145
2260
|
.option('--default <value>', 'Default value')
|
|
1146
2261
|
.action(async (templateId, opts) => {
|
|
1147
2262
|
try {
|
|
@@ -1174,9 +2289,9 @@ templates
|
|
|
1174
2289
|
});
|
|
1175
2290
|
templates
|
|
1176
2291
|
.command('remove-pipeline-input')
|
|
1177
|
-
.description('Remove a
|
|
2292
|
+
.description('Remove a template-level input. Blocked if steps reference this input via --pipeline-input unless --force is used. Template must be in draft status.')
|
|
1178
2293
|
.argument('<templateId>', 'Template ID')
|
|
1179
|
-
.requiredOption('--name <name>', '
|
|
2294
|
+
.requiredOption('--name <name>', 'Pipeline input name to remove')
|
|
1180
2295
|
.option('--force', 'Remove even if steps reference this input')
|
|
1181
2296
|
.action(async (templateId, opts) => {
|
|
1182
2297
|
try {
|
|
@@ -1207,7 +2322,7 @@ templates
|
|
|
1207
2322
|
});
|
|
1208
2323
|
templates
|
|
1209
2324
|
.command('validate')
|
|
1210
|
-
.description('Validate template I/O contracts')
|
|
2325
|
+
.description('Validate template I/O contracts. Checks for: circular dependencies between steps, missing source step/output references, self-references, duplicate input names per step. Also reports warnings for unreferenced outputs and info for steps with no outputs declared. Run before activating to catch errors early.')
|
|
1211
2326
|
.argument('<id>', 'Template ID')
|
|
1212
2327
|
.action(async (id) => {
|
|
1213
2328
|
try {
|
|
@@ -1639,6 +2754,50 @@ step
|
|
|
1639
2754
|
process.exit(1);
|
|
1640
2755
|
}
|
|
1641
2756
|
});
|
|
2757
|
+
step
|
|
2758
|
+
.command('approve')
|
|
2759
|
+
.description('Approve a submitted step. The step must be in "submitted" status (awaiting review). Approval marks the step as completed and may trigger downstream steps to be queued. Use "pl show <id>" to see step statuses.')
|
|
2760
|
+
.argument('<instanceId>', 'Pipeline instance ID')
|
|
2761
|
+
.argument('<stepId>', 'Step instance ID')
|
|
2762
|
+
.option('--notes <text>', 'Approval notes (visible to the step assignee)')
|
|
2763
|
+
.action(async (instanceId, stepId, opts) => {
|
|
2764
|
+
try {
|
|
2765
|
+
const body = {};
|
|
2766
|
+
if (opts.notes)
|
|
2767
|
+
body.notes = opts.notes;
|
|
2768
|
+
const data = await api('POST', `/api/pipeline-instances/${instanceId}/steps/${stepId}/approve`, body);
|
|
2769
|
+
console.log(chalk.green(`Step approved: ${data.step?.step_name || stepId}`));
|
|
2770
|
+
if (data.queued_steps?.length) {
|
|
2771
|
+
console.log(chalk.cyan(`\n Next steps queued: ${data.queued_steps.length}`));
|
|
2772
|
+
for (const qs of data.queued_steps) {
|
|
2773
|
+
console.log(` ○ ${qs.step_name} [${qs.role || '-'}]`);
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
catch (err) {
|
|
2778
|
+
console.error(chalk.red(err.message));
|
|
2779
|
+
process.exit(1);
|
|
2780
|
+
}
|
|
2781
|
+
});
|
|
2782
|
+
step
|
|
2783
|
+
.command('revise')
|
|
2784
|
+
.description('Request revision on a submitted step. Sends the step back to "in_progress" with feedback. The assignee can then resubmit. The step\'s review cycle counter increments; if max cycles are reached, the step auto-approves.')
|
|
2785
|
+
.argument('<instanceId>', 'Pipeline instance ID')
|
|
2786
|
+
.argument('<stepId>', 'Step instance ID')
|
|
2787
|
+
.requiredOption('--reason <text>', 'Revision feedback explaining what needs to change')
|
|
2788
|
+
.action(async (instanceId, stepId, opts) => {
|
|
2789
|
+
try {
|
|
2790
|
+
const data = await api('POST', `/api/pipeline-instances/${instanceId}/steps/${stepId}/revise`, {
|
|
2791
|
+
reason: opts.reason,
|
|
2792
|
+
});
|
|
2793
|
+
console.log(chalk.green(`Revision requested: ${data.step?.step_name || stepId}`));
|
|
2794
|
+
console.log(` Status: ${data.step?.status}`);
|
|
2795
|
+
}
|
|
2796
|
+
catch (err) {
|
|
2797
|
+
console.error(chalk.red(err.message));
|
|
2798
|
+
process.exit(1);
|
|
2799
|
+
}
|
|
2800
|
+
});
|
|
1642
2801
|
step
|
|
1643
2802
|
.command('skip')
|
|
1644
2803
|
.description('Skip a step')
|
|
@@ -2043,6 +3202,82 @@ executions
|
|
|
2043
3202
|
process.exit(1);
|
|
2044
3203
|
}
|
|
2045
3204
|
});
|
|
3205
|
+
executions
|
|
3206
|
+
.command('dispatch')
|
|
3207
|
+
.description('Dispatch a pending execution, claiming it for container launch. Moves the execution from "pending" to "dispatched". Typically called by the execution dispatcher, not manually.')
|
|
3208
|
+
.argument('<id>', 'Execution ID')
|
|
3209
|
+
.action(async (id) => {
|
|
3210
|
+
try {
|
|
3211
|
+
const data = await api('POST', `/api/executions/${id}/dispatch`);
|
|
3212
|
+
console.log(chalk.green(`Execution dispatched: ${data.execution?.id || id}`));
|
|
3213
|
+
console.log(` Status: ${data.execution?.status}`);
|
|
3214
|
+
}
|
|
3215
|
+
catch (err) {
|
|
3216
|
+
console.error(chalk.red(err.message));
|
|
3217
|
+
process.exit(1);
|
|
3218
|
+
}
|
|
3219
|
+
});
|
|
3220
|
+
executions
|
|
3221
|
+
.command('complete')
|
|
3222
|
+
.description('Mark an execution as completed with optional results. Typically called by the agent container when work is done. Use "ex update" for partial updates during execution.')
|
|
3223
|
+
.argument('<id>', 'Execution ID')
|
|
3224
|
+
.option('-o, --output <json>', 'Final output data as JSON')
|
|
3225
|
+
.option('--cost <usd>', 'Total cost in USD', parseNumber)
|
|
3226
|
+
.option('--input-tokens <n>', 'Total input token count', parseNumber)
|
|
3227
|
+
.option('--output-tokens <n>', 'Total output token count', parseNumber)
|
|
3228
|
+
.option('--duration <ms>', 'Total duration in milliseconds', parseNumber)
|
|
3229
|
+
.action(async (id, opts) => {
|
|
3230
|
+
try {
|
|
3231
|
+
const body = {};
|
|
3232
|
+
if (opts.output)
|
|
3233
|
+
body.outputData = JSON.parse(opts.output);
|
|
3234
|
+
if (opts.cost !== undefined)
|
|
3235
|
+
body.costUsd = opts.cost;
|
|
3236
|
+
if (opts.inputTokens !== undefined)
|
|
3237
|
+
body.inputTokens = opts.inputTokens;
|
|
3238
|
+
if (opts.outputTokens !== undefined)
|
|
3239
|
+
body.outputTokens = opts.outputTokens;
|
|
3240
|
+
if (opts.duration !== undefined)
|
|
3241
|
+
body.durationMs = opts.duration;
|
|
3242
|
+
await api('POST', `/api/executions/${id}/complete`, body);
|
|
3243
|
+
console.log(chalk.green('Execution completed'));
|
|
3244
|
+
}
|
|
3245
|
+
catch (err) {
|
|
3246
|
+
console.error(chalk.red(err.message));
|
|
3247
|
+
process.exit(1);
|
|
3248
|
+
}
|
|
3249
|
+
});
|
|
3250
|
+
executions
|
|
3251
|
+
.command('fail')
|
|
3252
|
+
.description('Mark an execution as failed with an error message. Typically called by the agent container on unrecoverable errors. Partial output and cost data can still be recorded.')
|
|
3253
|
+
.argument('<id>', 'Execution ID')
|
|
3254
|
+
.requiredOption('--error <text>', 'Error message describing the failure')
|
|
3255
|
+
.option('-o, --output <json>', 'Partial output data as JSON (any work completed before failure)')
|
|
3256
|
+
.option('--cost <usd>', 'Cost incurred in USD', parseNumber)
|
|
3257
|
+
.option('--input-tokens <n>', 'Input token count', parseNumber)
|
|
3258
|
+
.option('--output-tokens <n>', 'Output token count', parseNumber)
|
|
3259
|
+
.option('--duration <ms>', 'Duration in milliseconds before failure', parseNumber)
|
|
3260
|
+
.action(async (id, opts) => {
|
|
3261
|
+
try {
|
|
3262
|
+
const body = { error: opts.error };
|
|
3263
|
+
if (opts.output)
|
|
3264
|
+
body.outputData = JSON.parse(opts.output);
|
|
3265
|
+
if (opts.cost !== undefined)
|
|
3266
|
+
body.costUsd = opts.cost;
|
|
3267
|
+
if (opts.inputTokens !== undefined)
|
|
3268
|
+
body.inputTokens = opts.inputTokens;
|
|
3269
|
+
if (opts.outputTokens !== undefined)
|
|
3270
|
+
body.outputTokens = opts.outputTokens;
|
|
3271
|
+
if (opts.duration !== undefined)
|
|
3272
|
+
body.durationMs = opts.duration;
|
|
3273
|
+
await api('POST', `/api/executions/${id}/fail`, body);
|
|
3274
|
+
console.log(chalk.green('Execution marked as failed'));
|
|
3275
|
+
}
|
|
3276
|
+
catch (err) {
|
|
3277
|
+
console.error(chalk.red(err.message));
|
|
3278
|
+
process.exit(1);
|
|
3279
|
+
}
|
|
3280
|
+
});
|
|
2046
3281
|
executions
|
|
2047
3282
|
.command('cloudwatch-logs')
|
|
2048
3283
|
.alias('cw')
|