@formigio/fazemos-cli 0.1.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 ADDED
@@ -0,0 +1,1335 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import chalk from 'chalk';
4
+ import { config, getEnv, getToken, getActiveOrgId, setActiveOrgId, addEnvironment, hasEnvironments } from './config.js';
5
+ import { login, signup, confirmSignup, adminLogin } from './auth.js';
6
+ import { api } from './api.js';
7
+ import { execSync } from 'child_process';
8
+ function parseNumber(val) {
9
+ const n = parseFloat(val);
10
+ if (isNaN(n))
11
+ throw new Error(`Invalid number: ${val}`);
12
+ return n;
13
+ }
14
+ const program = new Command();
15
+ program
16
+ .name('fazemos')
17
+ .description('Fazemos CLI — Team Accomplishment Platform')
18
+ .version('0.1.0');
19
+ // ── Init ────────────────────────────────────────────────────
20
+ program
21
+ .command('init')
22
+ .description('Configure an environment (auto-discovers Cognito settings from the API)')
23
+ .argument('<name>', 'Environment name (e.g., dev, prod, local)')
24
+ .requiredOption('--api-url <url>', 'API base URL')
25
+ .option('--cognito-pool <id>', 'Cognito User Pool ID (auto-discovered if omitted)')
26
+ .option('--cognito-client <id>', 'Cognito Client ID (auto-discovered if omitted)')
27
+ .option('--cognito-region <region>', 'Cognito region (auto-discovered if omitted)')
28
+ .action(async (name, opts) => {
29
+ let poolId = opts.cognitoPool;
30
+ let clientId = opts.cognitoClient;
31
+ let region = opts.cognitoRegion;
32
+ // Auto-discover from API if not provided
33
+ if (!poolId || !clientId) {
34
+ try {
35
+ console.log(chalk.cyan('Discovering auth config...'));
36
+ const res = await fetch(`${opts.apiUrl}/auth/config`);
37
+ if (res.ok) {
38
+ const data = await res.json();
39
+ poolId = poolId || data.cognitoPoolId || data.userPoolId;
40
+ clientId = clientId || data.cognitoClientId || data.clientId;
41
+ region = region || data.cognitoRegion || data.region;
42
+ console.log(chalk.green('Auto-discovered Cognito settings'));
43
+ }
44
+ else {
45
+ console.log(chalk.yellow('Auto-discovery not available — provide --cognito-pool and --cognito-client'));
46
+ process.exit(1);
47
+ }
48
+ }
49
+ catch {
50
+ console.error(chalk.red('Could not reach API for auto-discovery. Provide --cognito-pool and --cognito-client manually.'));
51
+ process.exit(1);
52
+ }
53
+ }
54
+ if (!poolId || !clientId) {
55
+ console.error(chalk.red('Missing Cognito Pool ID or Client ID'));
56
+ process.exit(1);
57
+ }
58
+ addEnvironment(name, {
59
+ apiUrl: opts.apiUrl.replace(/\/$/, ''),
60
+ cognitoPoolId: poolId,
61
+ cognitoClientId: clientId,
62
+ cognitoRegion: region || 'us-west-2',
63
+ });
64
+ console.log(chalk.green(`Environment "${name}" configured`));
65
+ console.log(` API: ${opts.apiUrl}`);
66
+ console.log(` Pool: ${poolId}`);
67
+ console.log(` Client: ${clientId}`);
68
+ console.log(` Region: ${region || 'us-west-2'}`);
69
+ if (config.get('activeEnv') === name) {
70
+ console.log(chalk.cyan(` Active: yes`));
71
+ }
72
+ });
73
+ // ── Environment ─────────────────────────────────────────────
74
+ program
75
+ .command('env')
76
+ .description('Show or switch environment')
77
+ .argument('[name]', 'Environment to switch to')
78
+ .action((name) => {
79
+ if (!hasEnvironments()) {
80
+ console.log(chalk.yellow('No environments configured. Run: fazemos init <name> --api-url <url>'));
81
+ return;
82
+ }
83
+ if (name) {
84
+ const envs = config.get('environments');
85
+ if (!envs[name]) {
86
+ console.error(chalk.red(`Unknown environment: ${name}`));
87
+ console.log('Available:', Object.keys(envs).join(', '));
88
+ process.exit(1);
89
+ }
90
+ config.set('activeEnv', name);
91
+ console.log(chalk.green(`Switched to ${name}`));
92
+ }
93
+ const env = getEnv();
94
+ console.log(` Environment: ${chalk.cyan(env.name)}`);
95
+ console.log(` API: ${env.apiUrl}`);
96
+ console.log(` Cognito: ${env.cognitoPoolId}`);
97
+ const token = getToken();
98
+ console.log(` Auth: ${token ? chalk.green('authenticated') : chalk.yellow('not logged in')}`);
99
+ });
100
+ // ── Auth ────────────────────────────────────────────────────
101
+ const auth = program.command('auth').description('Authentication commands');
102
+ auth
103
+ .command('login')
104
+ .description('Login with email and password')
105
+ .requiredOption('-e, --email <email>', 'Email address')
106
+ .requiredOption('-p, --password <password>', 'Password')
107
+ .action(async (opts) => {
108
+ try {
109
+ const result = await login(opts.email, opts.password);
110
+ console.log(chalk.green(`Logged in as ${result.email}`));
111
+ }
112
+ catch (err) {
113
+ console.error(chalk.red(`Login failed: ${err.message}`));
114
+ process.exit(1);
115
+ }
116
+ });
117
+ auth
118
+ .command('signup')
119
+ .description('Create a new account')
120
+ .requiredOption('-e, --email <email>', 'Email address')
121
+ .requiredOption('-p, --password <password>', 'Password')
122
+ .action(async (opts) => {
123
+ try {
124
+ const result = await signup(opts.email, opts.password);
125
+ console.log(chalk.green(result.message));
126
+ }
127
+ catch (err) {
128
+ console.error(chalk.red(`Signup failed: ${err.message}`));
129
+ process.exit(1);
130
+ }
131
+ });
132
+ auth
133
+ .command('confirm')
134
+ .description('Confirm email with verification code')
135
+ .requiredOption('-e, --email <email>', 'Email address')
136
+ .requiredOption('-c, --code <code>', 'Verification code')
137
+ .action(async (opts) => {
138
+ try {
139
+ const result = await confirmSignup(opts.email, opts.code);
140
+ console.log(chalk.green(result.message));
141
+ }
142
+ catch (err) {
143
+ console.error(chalk.red(`Confirmation failed: ${err.message}`));
144
+ process.exit(1);
145
+ }
146
+ });
147
+ auth
148
+ .command('admin-login')
149
+ .description('Admin login (creates test user, requires AWS credentials)')
150
+ .requiredOption('-e, --email <email>', 'Email address')
151
+ .requiredOption('-p, --password <password>', 'Password')
152
+ .action(async (opts) => {
153
+ try {
154
+ const result = await adminLogin(opts.email, opts.password);
155
+ console.log(chalk.green(`Admin logged in as ${result.email}`));
156
+ }
157
+ catch (err) {
158
+ console.error(chalk.red(`Admin login failed: ${err.message}`));
159
+ process.exit(1);
160
+ }
161
+ });
162
+ auth
163
+ .command('token')
164
+ .description('Print the current JWT token')
165
+ .action(() => {
166
+ const token = getToken();
167
+ if (!token) {
168
+ console.error(chalk.yellow('Not logged in. Run: fazemos auth login'));
169
+ process.exit(1);
170
+ }
171
+ console.log(token);
172
+ });
173
+ auth
174
+ .command('logout')
175
+ .description('Clear stored credentials')
176
+ .action(() => {
177
+ const env = config.get('activeEnv');
178
+ const auths = config.get('auth');
179
+ delete auths[env];
180
+ config.set('auth', auths);
181
+ console.log(chalk.green('Logged out'));
182
+ });
183
+ // ── Whoami ──────────────────────────────────────────────────
184
+ program
185
+ .command('whoami')
186
+ .description('Show current user and org context')
187
+ .action(async () => {
188
+ try {
189
+ const data = await api('GET', '/auth/me');
190
+ console.log(` User: ${chalk.cyan(data.user.email)}`);
191
+ console.log(` Member: ${data.member.displayName} (${data.member.role})`);
192
+ console.log(` Org: ${data.member.orgId}`);
193
+ }
194
+ catch (err) {
195
+ console.error(chalk.red(err.message));
196
+ process.exit(1);
197
+ }
198
+ });
199
+ // ── Orgs ────────────────────────────────────────────────────
200
+ const orgs = program.command('orgs').description('Organization commands');
201
+ orgs
202
+ .command('list')
203
+ .description('List your organizations')
204
+ .action(async () => {
205
+ try {
206
+ const data = await api('GET', '/api/organizations/mine');
207
+ if (data.organizations.length === 0) {
208
+ console.log(chalk.yellow('No organizations. Create one with: fazemos orgs create'));
209
+ return;
210
+ }
211
+ const activeOrgId = getActiveOrgId();
212
+ for (const org of data.organizations) {
213
+ const active = org.id === activeOrgId ? chalk.green(' ✓') : '';
214
+ console.log(` ${chalk.cyan(org.name)} (${org.slug}) — ${org.role}${active}`);
215
+ }
216
+ }
217
+ catch (err) {
218
+ console.error(chalk.red(err.message));
219
+ process.exit(1);
220
+ }
221
+ });
222
+ orgs
223
+ .command('switch')
224
+ .description('Switch active organization')
225
+ .argument('<slug>', 'Organization slug')
226
+ .action(async (slug) => {
227
+ try {
228
+ const data = await api('GET', '/api/organizations/mine');
229
+ const org = data.organizations.find((o) => o.slug === slug);
230
+ if (!org) {
231
+ console.error(chalk.red(`No organization with slug "${slug}"`));
232
+ console.log('Your organizations:');
233
+ for (const o of data.organizations) {
234
+ console.log(` ${o.name} (${o.slug})`);
235
+ }
236
+ process.exit(1);
237
+ }
238
+ setActiveOrgId(org.id);
239
+ console.log(chalk.green(`Switched to ${org.name} (${org.slug})`));
240
+ }
241
+ catch (err) {
242
+ console.error(chalk.red(err.message));
243
+ process.exit(1);
244
+ }
245
+ });
246
+ orgs
247
+ .command('create')
248
+ .description('Create an organization')
249
+ .requiredOption('-n, --name <name>', 'Organization name')
250
+ .requiredOption('-s, --slug <slug>', 'URL slug')
251
+ .action(async (opts) => {
252
+ try {
253
+ const data = await api('POST', '/api/organizations', { name: opts.name, slug: opts.slug });
254
+ console.log(chalk.green(`Created: ${data.organization.name} (${data.organization.slug})`));
255
+ console.log(` Org ID: ${data.organization.id}`);
256
+ }
257
+ catch (err) {
258
+ console.error(chalk.red(err.message));
259
+ process.exit(1);
260
+ }
261
+ });
262
+ orgs
263
+ .command('members')
264
+ .description('List org members')
265
+ .option('--agents', 'Show only agent members')
266
+ .action(async (opts) => {
267
+ try {
268
+ const orgId = getActiveOrgId();
269
+ if (!orgId) {
270
+ console.error(chalk.red('No active org. Run: fazemos orgs switch <slug>'));
271
+ process.exit(1);
272
+ }
273
+ const data = await api('GET', `/api/organizations/${orgId}/members`);
274
+ if (!data.members?.length) {
275
+ console.log(chalk.yellow('No members'));
276
+ return;
277
+ }
278
+ for (const m of data.members) {
279
+ if (opts.agents && m.member_type !== 'agent')
280
+ continue;
281
+ const type = m.member_type === 'agent' ? chalk.blue('agent') : 'human';
282
+ console.log(` ${chalk.cyan(m.display_name)} (${m.role}, ${type}) — ${m.id}`);
283
+ }
284
+ }
285
+ catch (err) {
286
+ console.error(chalk.red(err.message));
287
+ process.exit(1);
288
+ }
289
+ });
290
+ // ── Worksheets ──────────────────────────────────────────────
291
+ const ws = program.command('worksheets').alias('ws').description('Worksheet commands');
292
+ ws
293
+ .command('list')
294
+ .description('List worksheets')
295
+ .option('-s, --status <status>', 'Filter by status', 'active')
296
+ .action(async (opts) => {
297
+ try {
298
+ const data = await api('GET', `/api/worksheets?status=${opts.status}`);
299
+ if (data.worksheets.length === 0) {
300
+ console.log(chalk.yellow('No worksheets'));
301
+ return;
302
+ }
303
+ for (const w of data.worksheets) {
304
+ console.log(` ${chalk.cyan(w.name)} (${w.status}) — ${w.owner_name}`);
305
+ }
306
+ }
307
+ catch (err) {
308
+ console.error(chalk.red(err.message));
309
+ process.exit(1);
310
+ }
311
+ });
312
+ ws
313
+ .command('create')
314
+ .description('Create a worksheet')
315
+ .requiredOption('-n, --name <name>', 'Worksheet name')
316
+ .option('-p, --purpose <purpose>', 'Purpose (e.g., "From X to Y by When")')
317
+ .option('--cadence <cadence>', 'Check-in cadence (weekly, biweekly, monthly)', 'weekly')
318
+ .action(async (opts) => {
319
+ try {
320
+ const body = { name: opts.name };
321
+ if (opts.purpose)
322
+ body.purpose = opts.purpose;
323
+ if (opts.cadence)
324
+ body.checkInCadence = opts.cadence;
325
+ const data = await api('POST', '/api/worksheets', body);
326
+ const w = data.worksheet;
327
+ console.log(chalk.green(`Created: ${w.name}`));
328
+ console.log(` ID: ${w.id}`);
329
+ console.log(` Purpose: ${w.purpose || '(none)'}`);
330
+ console.log(` Cadence: ${w.check_in_cadence}`);
331
+ }
332
+ catch (err) {
333
+ console.error(chalk.red(err.message));
334
+ process.exit(1);
335
+ }
336
+ });
337
+ ws
338
+ .command('show')
339
+ .description('Show worksheet detail')
340
+ .argument('<id>', 'Worksheet ID')
341
+ .action(async (id) => {
342
+ try {
343
+ const data = await api('GET', `/api/worksheets/${id}`);
344
+ const w = data.worksheet;
345
+ console.log(chalk.cyan(w.name));
346
+ console.log(` Purpose: ${w.purpose || '(none)'}`);
347
+ console.log(` Status: ${w.status}`);
348
+ console.log(` Cadence: ${w.check_in_cadence}`);
349
+ console.log(` Owner: ${w.owner_name}`);
350
+ if (data.outcomes?.length) {
351
+ console.log(chalk.cyan(`\n Outcomes (${data.outcomes.length}):`));
352
+ for (const o of data.outcomes) {
353
+ const progress = o.target_value ? ` ${o.current_value ?? 0}/${o.target_value}` : '';
354
+ console.log(` ${o.status === 'achieved' ? '✓' : '○'} ${o.name}${progress} — ${o.id}`);
355
+ if (o.linked_outcome_id) {
356
+ const linkName = o.linked_outcome_name || o.linked_outcome_id;
357
+ const linkWs = o.linked_worksheet_name ? ` on ${o.linked_worksheet_name}` : '';
358
+ console.log(` ↳ linked to: ${linkName}${linkWs}`);
359
+ }
360
+ }
361
+ }
362
+ if (data.milestones?.length) {
363
+ console.log(chalk.cyan(`\n Milestones (${data.milestones.length}):`));
364
+ for (const m of data.milestones) {
365
+ const icon = m.status === 'reached' ? '✓' : m.status === 'missed' ? '✗' : '○';
366
+ console.log(` ${icon} ${m.name}${m.target_date ? ` (${m.target_date})` : ''}`);
367
+ }
368
+ }
369
+ if (data.actions?.length) {
370
+ console.log(chalk.cyan(`\n Actions (${data.actions.length}):`));
371
+ for (const a of data.actions) {
372
+ const progress = a.target_value ? ` ${a.current_value ?? 0}/${a.target_value}` : '';
373
+ console.log(` ${chalk.cyan(a.description)}${progress} — ${a.member_name || 'unassigned'}`);
374
+ }
375
+ }
376
+ if (data.commitments?.length) {
377
+ console.log(chalk.cyan(`\n Commitments (${data.commitments.length}):`));
378
+ for (const c of data.commitments) {
379
+ const icon = c.status === 'completed' ? chalk.green('✓') : c.status === 'missed' ? chalk.red('✗') : '○';
380
+ console.log(` ${icon} ${c.description} — due ${c.due_date} (${c.status})`);
381
+ }
382
+ }
383
+ }
384
+ catch (err) {
385
+ console.error(chalk.red(err.message));
386
+ process.exit(1);
387
+ }
388
+ });
389
+ // ── Outcomes ────────────────────────────────────────────────
390
+ const outcomes = program.command('outcomes').alias('oc').description('Outcome commands');
391
+ outcomes
392
+ .command('add')
393
+ .description('Add an outcome to a worksheet')
394
+ .requiredOption('-w, --worksheet <id>', 'Worksheet ID')
395
+ .requiredOption('-n, --name <name>', 'Outcome name')
396
+ .option('-d, --description <desc>', 'Description')
397
+ .option('-m, --measurement <method>', 'How it is measured')
398
+ .option('-t, --target <value>', 'Target value', parseNumber)
399
+ .option('-c, --current <value>', 'Current value', parseNumber)
400
+ .action(async (opts) => {
401
+ try {
402
+ const body = { name: opts.name };
403
+ if (opts.description)
404
+ body.description = opts.description;
405
+ if (opts.measurement)
406
+ body.measurement = opts.measurement;
407
+ if (opts.target !== undefined)
408
+ body.targetValue = opts.target;
409
+ if (opts.current !== undefined)
410
+ body.currentValue = opts.current;
411
+ const data = await api('POST', `/api/worksheets/${opts.worksheet}/outcomes`, body);
412
+ const o = data.outcome;
413
+ console.log(chalk.green(`Added outcome: ${o.name}`));
414
+ console.log(` ID: ${o.id}`);
415
+ if (o.target_value)
416
+ console.log(` Target: ${o.target_value}`);
417
+ }
418
+ catch (err) {
419
+ console.error(chalk.red(err.message));
420
+ process.exit(1);
421
+ }
422
+ });
423
+ outcomes
424
+ .command('list')
425
+ .description('List outcomes on a worksheet')
426
+ .requiredOption('-w, --worksheet <id>', 'Worksheet ID')
427
+ .action(async (opts) => {
428
+ try {
429
+ const data = await api('GET', `/api/worksheets/${opts.worksheet}`);
430
+ if (!data.outcomes?.length) {
431
+ console.log(chalk.yellow('No outcomes'));
432
+ return;
433
+ }
434
+ for (const o of data.outcomes) {
435
+ const progress = o.target_value ? ` ${o.current_value ?? 0}/${o.target_value}` : '';
436
+ console.log(` ${o.status === 'achieved' ? '✓' : '○'} ${o.name}${progress} — ${o.id}`);
437
+ if (o.linked_outcome_id) {
438
+ const linkName = o.linked_outcome_name || o.linked_outcome_id;
439
+ const linkWs = o.linked_worksheet_name ? ` on ${o.linked_worksheet_name}` : '';
440
+ console.log(` ↳ linked to: ${linkName}${linkWs}`);
441
+ }
442
+ }
443
+ }
444
+ catch (err) {
445
+ console.error(chalk.red(err.message));
446
+ process.exit(1);
447
+ }
448
+ });
449
+ outcomes
450
+ .command('link')
451
+ .description('Link an outcome to a parent outcome')
452
+ .requiredOption('-w, --worksheet <id>', 'Worksheet ID')
453
+ .requiredOption('-o, --outcome <id>', 'Outcome ID')
454
+ .requiredOption('--parent <id>', 'Parent outcome ID')
455
+ .action(async (opts) => {
456
+ try {
457
+ await api('PATCH', `/api/worksheets/${opts.worksheet}/outcomes/${opts.outcome}`, { linkedOutcomeId: opts.parent });
458
+ console.log(chalk.green('Outcome linked'));
459
+ }
460
+ catch (err) {
461
+ console.error(chalk.red(err.message));
462
+ process.exit(1);
463
+ }
464
+ });
465
+ outcomes
466
+ .command('unlink')
467
+ .description('Remove link from an outcome')
468
+ .requiredOption('-w, --worksheet <id>', 'Worksheet ID')
469
+ .requiredOption('-o, --outcome <id>', 'Outcome ID')
470
+ .action(async (opts) => {
471
+ try {
472
+ await api('PATCH', `/api/worksheets/${opts.worksheet}/outcomes/${opts.outcome}`, { linkedOutcomeId: null });
473
+ console.log(chalk.green('Outcome unlinked'));
474
+ }
475
+ catch (err) {
476
+ console.error(chalk.red(err.message));
477
+ process.exit(1);
478
+ }
479
+ });
480
+ outcomes
481
+ .command('update-value')
482
+ .description('Update an outcome current value')
483
+ .requiredOption('-w, --worksheet <id>', 'Worksheet ID')
484
+ .requiredOption('-o, --outcome <id>', 'Outcome ID')
485
+ .requiredOption('-v, --value <value>', 'New current value', parseNumber)
486
+ .action(async (opts) => {
487
+ try {
488
+ await api('PATCH', `/api/worksheets/${opts.worksheet}/outcomes/${opts.outcome}/value`, { currentValue: opts.value });
489
+ console.log(chalk.green(`Updated value to ${opts.value}`));
490
+ }
491
+ catch (err) {
492
+ console.error(chalk.red(err.message));
493
+ process.exit(1);
494
+ }
495
+ });
496
+ // ── Milestones ──────────────────────────────────────────────
497
+ const milestones = program.command('milestones').alias('ms').description('Milestone commands');
498
+ milestones
499
+ .command('add')
500
+ .description('Add a milestone to a worksheet')
501
+ .requiredOption('-w, --worksheet <id>', 'Worksheet ID')
502
+ .requiredOption('-n, --name <name>', 'Milestone name')
503
+ .option('-d, --description <desc>', 'Description')
504
+ .option('-t, --target-date <date>', 'Target date (YYYY-MM-DD)')
505
+ .action(async (opts) => {
506
+ try {
507
+ const body = { name: opts.name };
508
+ if (opts.description)
509
+ body.description = opts.description;
510
+ if (opts.targetDate)
511
+ body.targetDate = opts.targetDate;
512
+ const data = await api('POST', `/api/worksheets/${opts.worksheet}/milestones`, body);
513
+ const m = data.milestone;
514
+ console.log(chalk.green(`Added milestone: ${m.name}`));
515
+ console.log(` ID: ${m.id}`);
516
+ if (m.target_date)
517
+ console.log(` Target: ${m.target_date}`);
518
+ }
519
+ catch (err) {
520
+ console.error(chalk.red(err.message));
521
+ process.exit(1);
522
+ }
523
+ });
524
+ // ── Members ─────────────────────────────────────────────────
525
+ const members = program.command('members').description('Worksheet member commands');
526
+ members
527
+ .command('add')
528
+ .description('Add a member to a worksheet')
529
+ .requiredOption('-w, --worksheet <id>', 'Worksheet ID')
530
+ .requiredOption('-m, --member <id>', 'Member ID')
531
+ .option('-r, --role <role>', 'Worksheet role (lead, contributor)', 'contributor')
532
+ .action(async (opts) => {
533
+ try {
534
+ const data = await api('POST', `/api/worksheets/${opts.worksheet}/members`, {
535
+ memberId: opts.member,
536
+ role: opts.role,
537
+ });
538
+ console.log(chalk.green(`Added member: ${data.worksheetMember.display_name} (${opts.role})`));
539
+ }
540
+ catch (err) {
541
+ console.error(chalk.red(err.message));
542
+ process.exit(1);
543
+ }
544
+ });
545
+ members
546
+ .command('list')
547
+ .description('List worksheet members')
548
+ .requiredOption('-w, --worksheet <id>', 'Worksheet ID')
549
+ .action(async (opts) => {
550
+ try {
551
+ const data = await api('GET', `/api/worksheets/${opts.worksheet}/members`);
552
+ if (!data.members?.length) {
553
+ console.log(chalk.yellow('No members'));
554
+ return;
555
+ }
556
+ for (const m of data.members) {
557
+ console.log(` ${chalk.cyan(m.display_name)} (${m.worksheet_role}) — ${m.member_id}`);
558
+ }
559
+ }
560
+ catch (err) {
561
+ console.error(chalk.red(err.message));
562
+ process.exit(1);
563
+ }
564
+ });
565
+ // ── Actions ─────────────────────────────────────────────────
566
+ const actions = program.command('actions').alias('ac').description('Action (lead measure) commands');
567
+ actions
568
+ .command('add')
569
+ .description('Add an action to a worksheet')
570
+ .requiredOption('-w, --worksheet <id>', 'Worksheet ID')
571
+ .requiredOption('-n, --name <name>', 'Action description')
572
+ .option('-o, --outcome <id>', 'Linked outcome ID')
573
+ .option('-m, --measurement <method>', 'How it is measured')
574
+ .option('-t, --target <value>', 'Target value', parseNumber)
575
+ .option('-c, --current <value>', 'Current value', parseNumber)
576
+ .action(async (opts) => {
577
+ try {
578
+ const body = { description: opts.name };
579
+ if (opts.outcome)
580
+ body.outcomeId = opts.outcome;
581
+ if (opts.measurement)
582
+ body.measurement = opts.measurement;
583
+ if (opts.target !== undefined)
584
+ body.targetValue = opts.target;
585
+ if (opts.current !== undefined)
586
+ body.currentValue = opts.current;
587
+ const data = await api('POST', `/api/worksheets/${opts.worksheet}/actions`, body);
588
+ const a = data.action;
589
+ console.log(chalk.green(`Added action: ${a.description}`));
590
+ console.log(` ID: ${a.id}`);
591
+ }
592
+ catch (err) {
593
+ console.error(chalk.red(err.message));
594
+ process.exit(1);
595
+ }
596
+ });
597
+ actions
598
+ .command('list')
599
+ .description('List actions on a worksheet')
600
+ .requiredOption('-w, --worksheet <id>', 'Worksheet ID')
601
+ .action(async (opts) => {
602
+ try {
603
+ const data = await api('GET', `/api/worksheets/${opts.worksheet}/actions`);
604
+ if (!data.actions?.length) {
605
+ console.log(chalk.yellow('No actions'));
606
+ return;
607
+ }
608
+ for (const a of data.actions) {
609
+ const progress = a.target_value ? ` ${a.current_value ?? 0}/${a.target_value}` : '';
610
+ console.log(` ${chalk.cyan(a.description)}${progress} — ${a.member_name || 'unassigned'} — ${a.id}`);
611
+ }
612
+ }
613
+ catch (err) {
614
+ console.error(chalk.red(err.message));
615
+ process.exit(1);
616
+ }
617
+ });
618
+ actions
619
+ .command('update-value')
620
+ .description('Update an action current value')
621
+ .requiredOption('-w, --worksheet <id>', 'Worksheet ID')
622
+ .requiredOption('-a, --action <id>', 'Action ID')
623
+ .requiredOption('-v, --value <value>', 'New current value', parseNumber)
624
+ .action(async (opts) => {
625
+ try {
626
+ await api('PATCH', `/api/worksheets/${opts.worksheet}/actions/${opts.action}/value`, { currentValue: opts.value });
627
+ console.log(chalk.green(`Updated action value to ${opts.value}`));
628
+ }
629
+ catch (err) {
630
+ console.error(chalk.red(err.message));
631
+ process.exit(1);
632
+ }
633
+ });
634
+ // ── Commitments ─────────────────────────────────────────────
635
+ const commitments = program.command('commitments').alias('cm').description('Commitment commands');
636
+ commitments
637
+ .command('add')
638
+ .description('Make a commitment')
639
+ .requiredOption('-w, --worksheet <id>', 'Worksheet ID')
640
+ .requiredOption('-d, --description <desc>', 'What you commit to do')
641
+ .option('-a, --action <id>', 'Linked action ID')
642
+ .requiredOption('--due <date>', 'Due date (YYYY-MM-DD)')
643
+ .action(async (opts) => {
644
+ try {
645
+ const body = { description: opts.description, dueDate: opts.due };
646
+ if (opts.action)
647
+ body.actionId = opts.action;
648
+ const data = await api('POST', `/api/worksheets/${opts.worksheet}/commitments`, body);
649
+ const c = data.commitment;
650
+ console.log(chalk.green(`Commitment made: ${c.description}`));
651
+ console.log(` ID: ${c.id}`);
652
+ console.log(` Due: ${c.due_date}`);
653
+ }
654
+ catch (err) {
655
+ console.error(chalk.red(err.message));
656
+ process.exit(1);
657
+ }
658
+ });
659
+ commitments
660
+ .command('list')
661
+ .description('List commitments on a worksheet')
662
+ .requiredOption('-w, --worksheet <id>', 'Worksheet ID')
663
+ .action(async (opts) => {
664
+ try {
665
+ const data = await api('GET', `/api/worksheets/${opts.worksheet}/commitments`);
666
+ if (!data.commitments?.length) {
667
+ console.log(chalk.yellow('No commitments'));
668
+ return;
669
+ }
670
+ for (const c of data.commitments) {
671
+ const icon = c.status === 'completed' ? chalk.green('✓') : c.status === 'missed' ? chalk.red('✗') : '○';
672
+ console.log(` ${icon} ${c.description} — due ${c.due_date} (${c.status}) — ${c.id}`);
673
+ }
674
+ }
675
+ catch (err) {
676
+ console.error(chalk.red(err.message));
677
+ process.exit(1);
678
+ }
679
+ });
680
+ commitments
681
+ .command('done')
682
+ .description('Mark a commitment as done')
683
+ .requiredOption('-c, --commitment <id>', 'Commitment ID')
684
+ .action(async (opts) => {
685
+ try {
686
+ await api('PATCH', `/api/commitments/${opts.commitment}/complete`, {});
687
+ console.log(chalk.green('Commitment marked as done'));
688
+ }
689
+ catch (err) {
690
+ console.error(chalk.red(err.message));
691
+ process.exit(1);
692
+ }
693
+ });
694
+ // ── Templates ──────────────────────────────────────────────
695
+ const templates = program.command('templates').alias('tpl').description('Pipeline template commands');
696
+ templates
697
+ .command('list')
698
+ .description('List pipeline templates')
699
+ .option('-s, --status <status>', 'Filter by status (draft, active, archived)')
700
+ .action(async (opts) => {
701
+ try {
702
+ const qs = opts.status ? `?status=${opts.status}` : '';
703
+ const data = await api('GET', `/api/pipeline-templates${qs}`);
704
+ if (!data.templates?.length) {
705
+ console.log(chalk.yellow('No templates'));
706
+ return;
707
+ }
708
+ for (const t of data.templates) {
709
+ const phases = t.phase_count ?? '?';
710
+ const steps = t.step_count ?? '?';
711
+ console.log(` ${chalk.cyan(t.name)} (${t.status}) — ${phases} phases, ${steps} steps — ${t.id}`);
712
+ }
713
+ }
714
+ catch (err) {
715
+ console.error(chalk.red(err.message));
716
+ process.exit(1);
717
+ }
718
+ });
719
+ templates
720
+ .command('show')
721
+ .description('Show template detail')
722
+ .argument('<id>', 'Template ID')
723
+ .action(async (id) => {
724
+ try {
725
+ const data = await api('GET', `/api/pipeline-templates/${id}`);
726
+ const t = data.template;
727
+ console.log(chalk.cyan(t.name));
728
+ console.log(` ID: ${t.id}`);
729
+ console.log(` Status: ${t.status}`);
730
+ console.log(` Version: ${t.version}`);
731
+ if (t.definition?.phases) {
732
+ for (const phase of t.definition.phases) {
733
+ console.log(chalk.cyan(`\n Phase: ${phase.name}`));
734
+ for (const step of phase.steps || []) {
735
+ console.log(` ${step.name} (${step.stepType || 'human'}) — role: ${step.role || 'unassigned'}`);
736
+ }
737
+ }
738
+ }
739
+ }
740
+ catch (err) {
741
+ console.error(chalk.red(err.message));
742
+ process.exit(1);
743
+ }
744
+ });
745
+ templates
746
+ .command('create')
747
+ .description('Create a pipeline template')
748
+ .requiredOption('-n, --name <name>', 'Template name')
749
+ .option('-d, --description <desc>', 'Description')
750
+ .action(async (opts) => {
751
+ try {
752
+ const body = { name: opts.name, definition: { phases: [] } };
753
+ if (opts.description)
754
+ body.description = opts.description;
755
+ const data = await api('POST', '/api/pipeline-templates', body);
756
+ const t = data.template;
757
+ console.log(chalk.green(`Created: ${t.name}`));
758
+ console.log(` ID: ${t.id}`);
759
+ }
760
+ catch (err) {
761
+ console.error(chalk.red(err.message));
762
+ process.exit(1);
763
+ }
764
+ });
765
+ templates
766
+ .command('import')
767
+ .description('Import a JOE template JSON file as a Fazemos pipeline template')
768
+ .argument('<file>', 'Path to JOE template JSON file')
769
+ .action(async (file) => {
770
+ try {
771
+ const { readFileSync } = await import('fs');
772
+ const { resolve } = await import('path');
773
+ const raw = JSON.parse(readFileSync(resolve(file), 'utf-8'));
774
+ // Map JOE template to Fazemos format
775
+ const steps = raw.steps || [];
776
+ // Group sequential steps into a single phase for now
777
+ const definition = {
778
+ phases: [{
779
+ id: crypto.randomUUID(),
780
+ name: raw.name || 'Main',
781
+ description: raw.description || '',
782
+ deliverables: [],
783
+ steps: steps.map((s, i) => ({
784
+ id: crypto.randomUUID(),
785
+ name: s.name,
786
+ description: s.description || '',
787
+ step_type: s.executionMode === 'script' ? 'script' : (s.agent ? 'agent' : 'human'),
788
+ role: s.role || s.agent || 'unassigned',
789
+ inputs: [],
790
+ outputs: [],
791
+ sections: s.sections || '',
792
+ reviewer: s.reviewer || null,
793
+ max_review_cycles: s.maxReviewCycles || 0,
794
+ execution_config: s.executionMode === 'script' ? {
795
+ image: s.image || '',
796
+ command: s.command || '',
797
+ } : null,
798
+ parallel_group: null,
799
+ sort_order: i,
800
+ })),
801
+ }],
802
+ };
803
+ const body = {
804
+ name: raw.name || file,
805
+ description: raw.description || '',
806
+ definition,
807
+ };
808
+ const data = await api('POST', '/api/pipeline-templates', body);
809
+ const t = data.template;
810
+ console.log(chalk.green(`Imported: ${t.name} (${steps.length} steps)`));
811
+ console.log(` ID: ${t.id}`);
812
+ }
813
+ catch (err) {
814
+ console.error(chalk.red(err.message));
815
+ process.exit(1);
816
+ }
817
+ });
818
+ templates
819
+ .command('activate')
820
+ .description('Activate a template (required before creating instances)')
821
+ .argument('<id>', 'Template ID')
822
+ .action(async (id) => {
823
+ try {
824
+ await api('PATCH', `/api/pipeline-templates/${id}/status`, { status: 'active' });
825
+ console.log(chalk.green('Template activated'));
826
+ }
827
+ catch (err) {
828
+ console.error(chalk.red(err.message));
829
+ process.exit(1);
830
+ }
831
+ });
832
+ // ── Pipelines ──────────────────────────────────────────────
833
+ const pipelines = program.command('pipelines').alias('pl').description('Pipeline instance commands');
834
+ pipelines
835
+ .command('list')
836
+ .description('List pipeline instances')
837
+ .option('-s, --status <status>', 'Filter by status (active, completed, archived)', 'active')
838
+ .option('--search <term>', 'Search by name or ID')
839
+ .option('--expand', 'Include steps inline (avoids N+1)')
840
+ .action(async (opts) => {
841
+ try {
842
+ const params = [];
843
+ if (opts.status)
844
+ params.push(`status=${opts.status}`);
845
+ if (opts.search)
846
+ params.push(`search=${encodeURIComponent(opts.search)}`);
847
+ if (opts.expand)
848
+ params.push('expand=steps');
849
+ const qs = params.length ? `?${params.join('&')}` : '';
850
+ const data = await api('GET', `/api/pipeline-instances${qs}`);
851
+ if (!data.instances?.length) {
852
+ console.log(chalk.yellow('No pipeline instances'));
853
+ return;
854
+ }
855
+ for (const inst of data.instances) {
856
+ const stepInfo = inst.total_steps ? ` — step ${inst.current_step ?? '?'}/${inst.total_steps}` : '';
857
+ console.log(` ${chalk.cyan(inst.name)} (${inst.status})${stepInfo}`);
858
+ console.log(` ID: ${inst.id}`);
859
+ if (opts.expand && inst.steps?.length) {
860
+ for (const s of inst.steps) {
861
+ const icon = s.status === 'completed' ? chalk.green('✓')
862
+ : s.status === 'in_progress' ? chalk.yellow('▸')
863
+ : s.status === 'failed' ? chalk.red('✗')
864
+ : '○';
865
+ console.log(` ${icon} ${s.step_name} [${s.role || '-'}] (${s.status})`);
866
+ }
867
+ }
868
+ }
869
+ }
870
+ catch (err) {
871
+ console.error(chalk.red(err.message));
872
+ process.exit(1);
873
+ }
874
+ });
875
+ pipelines
876
+ .command('show')
877
+ .description('Show pipeline instance detail')
878
+ .argument('<id>', 'Instance ID')
879
+ .action(async (id) => {
880
+ try {
881
+ const data = await api('GET', `/api/pipeline-instances/${id}`);
882
+ const inst = data.instance;
883
+ console.log(chalk.cyan(inst.name));
884
+ console.log(` ID: ${inst.id}`);
885
+ console.log(` Status: ${inst.status}`);
886
+ console.log(` Template: ${inst.template_name || inst.template_id}`);
887
+ if (inst.phases?.length) {
888
+ for (const phase of inst.phases) {
889
+ console.log(chalk.cyan(`\n Phase: ${phase.name}`));
890
+ for (const s of phase.steps || []) {
891
+ const icon = s.status === 'completed' ? chalk.green('✓')
892
+ : s.status === 'in_progress' ? chalk.yellow('▸')
893
+ : s.status === 'failed' ? chalk.red('✗')
894
+ : '○';
895
+ console.log(` ${icon} ${s.step_name} [${s.role || '-'}] (${s.status})`);
896
+ }
897
+ }
898
+ }
899
+ }
900
+ catch (err) {
901
+ console.error(chalk.red(err.message));
902
+ process.exit(1);
903
+ }
904
+ });
905
+ pipelines
906
+ .command('create')
907
+ .description('Create a pipeline instance from a template')
908
+ .argument('<templateId>', 'Template ID')
909
+ .requiredOption('-n, --name <name>', 'Instance name')
910
+ .option('--params <json>', 'Parameters as JSON string')
911
+ .action(async (templateId, opts) => {
912
+ try {
913
+ const body = { templateId, name: opts.name };
914
+ if (opts.params)
915
+ body.parameters = JSON.parse(opts.params);
916
+ const data = await api('POST', '/api/pipeline-instances', body);
917
+ const inst = data.instance;
918
+ console.log(chalk.green(`Created: ${inst.name}`));
919
+ console.log(` ID: ${inst.id}`);
920
+ }
921
+ catch (err) {
922
+ console.error(chalk.red(err.message));
923
+ process.exit(1);
924
+ }
925
+ });
926
+ pipelines
927
+ .command('archive')
928
+ .description('Archive a pipeline instance')
929
+ .argument('<id>', 'Instance ID')
930
+ .action(async (id) => {
931
+ try {
932
+ const data = await api('POST', `/api/pipeline-instances/${id}/archive`);
933
+ console.log(chalk.green(`Archived: ${data.instance?.name || id}`));
934
+ }
935
+ catch (err) {
936
+ console.error(chalk.red(err.message));
937
+ process.exit(1);
938
+ }
939
+ });
940
+ pipelines
941
+ .command('set-params')
942
+ .description('Set instance parameters (atomic update)')
943
+ .argument('<id>', 'Instance ID')
944
+ .requiredOption('--params <json>', 'Parameters as JSON string')
945
+ .action(async (id, opts) => {
946
+ try {
947
+ const body = JSON.parse(opts.params);
948
+ const data = await api('PATCH', `/api/pipeline-instances/${id}/parameters`, body);
949
+ console.log(chalk.green(`Parameters updated: ${data.instance?.name || id}`));
950
+ }
951
+ catch (err) {
952
+ console.error(chalk.red(err.message));
953
+ process.exit(1);
954
+ }
955
+ });
956
+ pipelines
957
+ .command('set-status')
958
+ .description('Update instance status (running, paused, completed, failed, cancelled)')
959
+ .argument('<id>', 'Instance ID')
960
+ .requiredOption('--status <status>', 'New status')
961
+ .action(async (id, opts) => {
962
+ try {
963
+ const valid = ['running', 'paused', 'completed', 'failed', 'cancelled'];
964
+ if (!valid.includes(opts.status)) {
965
+ console.error(chalk.red(`Invalid status. Must be one of: ${valid.join(', ')}`));
966
+ process.exit(1);
967
+ }
968
+ const data = await api('PATCH', `/api/pipeline-instances/${id}`, { status: opts.status });
969
+ console.log(chalk.green(`Status set to ${opts.status}: ${data.instance?.name || id}`));
970
+ }
971
+ catch (err) {
972
+ console.error(chalk.red(err.message));
973
+ process.exit(1);
974
+ }
975
+ });
976
+ // ── Queue ──────────────────────────────────────────────────
977
+ const queue = program.command('queue').alias('q').description('Work queue commands');
978
+ queue
979
+ .command('summary')
980
+ .description('Show queue depth per agent')
981
+ .action(async () => {
982
+ try {
983
+ const data = await api('GET', '/api/work-queue/summary');
984
+ const entries = Object.entries(data.depths ?? {});
985
+ if (entries.length === 0) {
986
+ console.log(chalk.yellow('No agents in queue'));
987
+ return;
988
+ }
989
+ let total = 0;
990
+ for (const [agent, count] of entries) {
991
+ total += count;
992
+ console.log(` ${agent.padEnd(12)} ${count} steps queued`);
993
+ }
994
+ console.log(' ───────────────');
995
+ console.log(` ${'Total'.padEnd(12)} ${total} steps`);
996
+ }
997
+ catch (err) {
998
+ console.error(chalk.red(err.message));
999
+ process.exit(1);
1000
+ }
1001
+ });
1002
+ queue
1003
+ .command('list')
1004
+ .description('List queued and in-progress steps')
1005
+ .option('-a, --agent <name>', 'Filter by agent name')
1006
+ .option('--message-id <id>', 'Filter by JOE messageId in step metadata')
1007
+ .action(async (opts) => {
1008
+ try {
1009
+ const params = [];
1010
+ if (opts.agent)
1011
+ params.push(`agent=${encodeURIComponent(opts.agent)}`);
1012
+ if (opts.messageId)
1013
+ params.push(`messageId=${encodeURIComponent(opts.messageId)}`);
1014
+ const qs = params.length ? `?${params.join('&')}` : '';
1015
+ const data = await api('GET', `/api/work-queue${qs}`);
1016
+ if (!data.items?.length) {
1017
+ console.log(chalk.yellow('No queued steps'));
1018
+ return;
1019
+ }
1020
+ for (const s of data.items) {
1021
+ const icon = s.status === 'in_progress' ? chalk.yellow('▸') : '○';
1022
+ const agent = s.agent ? ` [${s.agent}]` : '';
1023
+ console.log(` ${icon} ${s.step_name}${agent} — ${s.pipeline_name || s.pipeline_instance_id} (${s.status})`);
1024
+ }
1025
+ }
1026
+ catch (err) {
1027
+ console.error(chalk.red(err.message));
1028
+ process.exit(1);
1029
+ }
1030
+ });
1031
+ // ── Executions ─────────────────────────────────────────────
1032
+ program
1033
+ .command('execute')
1034
+ .description('Trigger an agent execution')
1035
+ .argument('<source-id>', 'Action, commitment, or pipeline step ID')
1036
+ .requiredOption('--agent <name>', 'Agent name or member ID')
1037
+ .option('--source-type <type>', 'Source type (action, commitment, pipeline_step)', 'action')
1038
+ .option('--prompt <prompt>', 'Additional prompt context')
1039
+ .action(async (sourceId, opts) => {
1040
+ try {
1041
+ const validTypes = ['action', 'commitment', 'pipeline_step'];
1042
+ if (!validTypes.includes(opts.sourceType)) {
1043
+ console.error(chalk.red(`Invalid source type. Must be one of: ${validTypes.join(', ')}`));
1044
+ process.exit(1);
1045
+ }
1046
+ const body = {
1047
+ sourceType: opts.sourceType,
1048
+ sourceId,
1049
+ agent: opts.agent,
1050
+ };
1051
+ if (opts.prompt)
1052
+ body.prompt = opts.prompt;
1053
+ const data = await api('POST', '/api/executions', body);
1054
+ const exec = data.execution;
1055
+ console.log(chalk.green(`Execution created: ${exec.id}`));
1056
+ console.log(` Agent: ${exec.agent_name}`);
1057
+ console.log(` Source: ${exec.source_type} ${exec.source_id}`);
1058
+ console.log(` Status: ${exec.status}`);
1059
+ }
1060
+ catch (err) {
1061
+ console.error(chalk.red(err.message));
1062
+ process.exit(1);
1063
+ }
1064
+ });
1065
+ const executions = program.command('executions').alias('ex').description('Execution commands');
1066
+ executions
1067
+ .command('list')
1068
+ .description('List executions')
1069
+ .option('-s, --status <status>', 'Filter by status (pending, running, completed, failed)')
1070
+ .option('-a, --agent <name>', 'Filter by agent')
1071
+ .option('--source-type <type>', 'Filter by source type')
1072
+ .action(async (opts) => {
1073
+ try {
1074
+ const params = [];
1075
+ if (opts.status)
1076
+ params.push(`status=${opts.status}`);
1077
+ if (opts.agent)
1078
+ params.push(`agent=${encodeURIComponent(opts.agent)}`);
1079
+ if (opts.sourceType)
1080
+ params.push(`source_type=${opts.sourceType}`);
1081
+ const qs = params.length ? `?${params.join('&')}` : '';
1082
+ const data = await api('GET', `/api/executions${qs}`);
1083
+ if (!data.executions?.length) {
1084
+ console.log(chalk.yellow('No executions'));
1085
+ return;
1086
+ }
1087
+ for (const e of data.executions) {
1088
+ const icon = e.status === 'completed' ? chalk.green('✓')
1089
+ : e.status === 'running' ? chalk.yellow('▸')
1090
+ : e.status === 'failed' ? chalk.red('✗')
1091
+ : '○';
1092
+ const cost = e.cost_usd ? ` $${e.cost_usd}` : '';
1093
+ console.log(` ${icon} ${e.agent_name} — ${e.source_type} (${e.status})${cost} — ${e.id}`);
1094
+ }
1095
+ }
1096
+ catch (err) {
1097
+ console.error(chalk.red(err.message));
1098
+ process.exit(1);
1099
+ }
1100
+ });
1101
+ executions
1102
+ .command('show')
1103
+ .description('Show execution detail')
1104
+ .argument('<id>', 'Execution ID')
1105
+ .action(async (id) => {
1106
+ try {
1107
+ const data = await api('GET', `/api/executions/${id}`);
1108
+ const e = data.execution;
1109
+ console.log(chalk.cyan(`Execution: ${e.id}`));
1110
+ console.log(` Agent: ${e.agent_name}`);
1111
+ console.log(` Source: ${e.source_type} ${e.source_id}`);
1112
+ console.log(` Status: ${e.status}`);
1113
+ if (e.fargate_task_arn)
1114
+ console.log(` Task ARN: ${e.fargate_task_arn}`);
1115
+ if (e.cost_usd)
1116
+ console.log(` Cost: $${e.cost_usd}`);
1117
+ if (e.duration_ms)
1118
+ console.log(` Duration: ${(e.duration_ms / 1000).toFixed(1)}s`);
1119
+ if (e.input_tokens)
1120
+ console.log(` Tokens: ${e.input_tokens} in / ${e.output_tokens} out`);
1121
+ if (e.error)
1122
+ console.log(` Error: ${chalk.red(e.error)}`);
1123
+ if (e.queued_at)
1124
+ console.log(` Queued: ${e.queued_at}`);
1125
+ if (e.started_at)
1126
+ console.log(` Started: ${e.started_at}`);
1127
+ if (e.completed_at)
1128
+ console.log(` Completed: ${e.completed_at}`);
1129
+ }
1130
+ catch (err) {
1131
+ console.error(chalk.red(err.message));
1132
+ process.exit(1);
1133
+ }
1134
+ });
1135
+ executions
1136
+ .command('cancel')
1137
+ .description('Cancel a running execution')
1138
+ .argument('<id>', 'Execution ID')
1139
+ .action(async (id) => {
1140
+ try {
1141
+ await api('POST', `/api/executions/${id}/cancel`);
1142
+ console.log(chalk.green('Execution cancelled'));
1143
+ }
1144
+ catch (err) {
1145
+ console.error(chalk.red(err.message));
1146
+ process.exit(1);
1147
+ }
1148
+ });
1149
+ // ── My Work ─────────────────────────────────────────────────
1150
+ program
1151
+ .command('my-work')
1152
+ .description('Show pending work across all worksheets')
1153
+ .action(async () => {
1154
+ try {
1155
+ const data = await api('GET', '/api/my-work');
1156
+ const c = data.commitments;
1157
+ const total = c.overdue.length + c.due_today.length + c.due_this_week.length + c.upcoming.length;
1158
+ console.log(chalk.cyan(`Commitments: ${total}`));
1159
+ if (c.overdue.length)
1160
+ console.log(chalk.red(` Overdue: ${c.overdue.length}`));
1161
+ if (c.due_today.length)
1162
+ console.log(chalk.yellow(` Due today: ${c.due_today.length}`));
1163
+ if (c.due_this_week.length)
1164
+ console.log(` This week: ${c.due_this_week.length}`);
1165
+ if (c.upcoming.length)
1166
+ console.log(` Upcoming: ${c.upcoming.length}`);
1167
+ console.log(chalk.cyan(`\nActions: ${data.actions.length}`));
1168
+ console.log(chalk.cyan(`Worksheets: ${data.worksheets.length}`));
1169
+ console.log(chalk.cyan(`Pipeline steps: ${data.pipeline_steps.length}`));
1170
+ }
1171
+ catch (err) {
1172
+ console.error(chalk.red(err.message));
1173
+ process.exit(1);
1174
+ }
1175
+ });
1176
+ // ── Test ────────────────────────────────────────────────────
1177
+ program
1178
+ .command('test')
1179
+ .description('Run API tests via Bruno CLI')
1180
+ .option('-f, --folder <folder>', 'Run only a specific folder')
1181
+ .option('--html <path>', 'Generate HTML report')
1182
+ .option('-c, --collection <path>', 'Path to Bruno collection', '')
1183
+ .action(async (opts) => {
1184
+ const token = getToken();
1185
+ if (!token) {
1186
+ console.error(chalk.yellow('Not logged in. Run: fazemos auth login'));
1187
+ process.exit(1);
1188
+ }
1189
+ const env = getEnv();
1190
+ // Find collection: explicit path > ./tests/api > ../fazemos-api/tests/api
1191
+ let collectionPath = opts.collection;
1192
+ if (!collectionPath) {
1193
+ const candidates = [
1194
+ 'tests/api',
1195
+ '../fazemos-api/tests/api',
1196
+ ];
1197
+ for (const c of candidates) {
1198
+ try {
1199
+ const { statSync } = await import('fs');
1200
+ const resolved = new URL(c, `file://${process.cwd()}/`).pathname;
1201
+ if (statSync(resolved + '/opencollection.yml').isFile()) {
1202
+ collectionPath = resolved;
1203
+ break;
1204
+ }
1205
+ }
1206
+ catch { /* skip */ }
1207
+ }
1208
+ }
1209
+ if (!collectionPath) {
1210
+ console.error(chalk.red('Bruno collection not found. Run from the fazemos-api directory or use --collection <path>'));
1211
+ process.exit(1);
1212
+ }
1213
+ const folder = opts.folder ? opts.folder + '/ ' : '';
1214
+ let cmd = `bru run ${folder}--env ${env.name} --env-var "token=${token}" --env-var "baseUrl=${env.apiUrl}"`;
1215
+ if (opts.html) {
1216
+ cmd += ` --reporter-html ${opts.html}`;
1217
+ }
1218
+ console.log(chalk.cyan(`Running tests against ${env.name} (${collectionPath})...`));
1219
+ try {
1220
+ execSync(cmd, { stdio: 'inherit', cwd: collectionPath });
1221
+ }
1222
+ catch {
1223
+ process.exit(1);
1224
+ }
1225
+ });
1226
+ // ── Agents ──────────────────────────────────────────────────
1227
+ const agentsCmd = program.command('agents').description('Agent management');
1228
+ agentsCmd
1229
+ .command('register')
1230
+ .description('Register an agent')
1231
+ .requiredOption('-n, --name <name>', 'Agent display name')
1232
+ .option('-r, --roles <roles>', 'Comma-separated roles', (v) => v.split(','))
1233
+ .option('--model <model>', 'Model (e.g., opus, sonnet, system)', 'sonnet')
1234
+ .action(async (opts) => {
1235
+ try {
1236
+ const data = await api('POST', '/api/agents/register', {
1237
+ agents: [{
1238
+ displayName: opts.name,
1239
+ roles: opts.roles || [opts.name],
1240
+ config: { model: opts.model },
1241
+ }],
1242
+ });
1243
+ for (const a of data.agents) {
1244
+ const icon = a.status === 'created' ? chalk.green('✓') : a.status === 'updated' ? chalk.yellow('↻') : '○';
1245
+ console.log(` ${icon} ${a.display_name} (${a.status}) — ${a.member_id}`);
1246
+ }
1247
+ }
1248
+ catch (err) {
1249
+ console.error(chalk.red(err.message));
1250
+ process.exit(1);
1251
+ }
1252
+ });
1253
+ // ── Health ──────────────────────────────────────────────────
1254
+ program
1255
+ .command('health')
1256
+ .description('Check API health')
1257
+ .action(async () => {
1258
+ try {
1259
+ const env = getEnv();
1260
+ const fetchJson = async (path) => {
1261
+ const res = await fetch(`${env.apiUrl}${path}`);
1262
+ return res.json();
1263
+ };
1264
+ const [health, deep] = await Promise.all([
1265
+ fetchJson('/health'),
1266
+ fetchJson('/health/deep').catch(() => ({ status: 'error', database: 'unreachable' })),
1267
+ ]);
1268
+ console.log(` API: ${chalk.green(health.status)} (${env.apiUrl})`);
1269
+ console.log(` Database: ${deep.database === 'connected' ? chalk.green('connected') : chalk.red(deep.database)}`);
1270
+ }
1271
+ catch (err) {
1272
+ console.error(chalk.red(`API unreachable: ${err.message}`));
1273
+ process.exit(1);
1274
+ }
1275
+ });
1276
+ // ── API Keys ──────────────────────────────────────────────
1277
+ const apiKeys = program.command('api-keys').description('API key management');
1278
+ apiKeys
1279
+ .command('create')
1280
+ .description('Create an API key')
1281
+ .requiredOption('-n, --name <name>', 'Key name (e.g., joe-orchestrator)')
1282
+ .requiredOption('-m, --member <id>', 'Member ID to bind the key to')
1283
+ .option('-d, --description <desc>', 'Description')
1284
+ .action(async (opts) => {
1285
+ try {
1286
+ const body = { name: opts.name, memberId: opts.member };
1287
+ if (opts.description)
1288
+ body.description = opts.description;
1289
+ const data = await api('POST', '/api/api-keys', body);
1290
+ console.log(chalk.green(`API key created: ${opts.name}`));
1291
+ console.log(` Key: ${chalk.yellow(data.rawKey)}`);
1292
+ console.log(` ID: ${data.apiKey.id}`);
1293
+ console.log('');
1294
+ console.log(chalk.red(' ⚠ Save this key now — it will not be shown again.'));
1295
+ }
1296
+ catch (err) {
1297
+ console.error(chalk.red(err.message));
1298
+ process.exit(1);
1299
+ }
1300
+ });
1301
+ apiKeys
1302
+ .command('list')
1303
+ .description('List API keys')
1304
+ .action(async () => {
1305
+ try {
1306
+ const data = await api('GET', '/api/api-keys');
1307
+ if (!data.apiKeys?.length) {
1308
+ console.log(chalk.yellow('No API keys'));
1309
+ return;
1310
+ }
1311
+ for (const k of data.apiKeys) {
1312
+ console.log(` ${chalk.cyan(k.name)} — ${k.id} (created ${k.created_at})`);
1313
+ }
1314
+ }
1315
+ catch (err) {
1316
+ console.error(chalk.red(err.message));
1317
+ process.exit(1);
1318
+ }
1319
+ });
1320
+ apiKeys
1321
+ .command('revoke')
1322
+ .description('Revoke an API key')
1323
+ .argument('<id>', 'API key ID')
1324
+ .action(async (id) => {
1325
+ try {
1326
+ await api('DELETE', `/api/api-keys/${id}`);
1327
+ console.log(chalk.green('API key revoked'));
1328
+ }
1329
+ catch (err) {
1330
+ console.error(chalk.red(err.message));
1331
+ process.exit(1);
1332
+ }
1333
+ });
1334
+ program.parse();
1335
+ //# sourceMappingURL=index.js.map