@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/bin/fazemos.js +2 -0
- package/dist/api.d.ts +1 -0
- package/dist/api.js +48 -0
- package/dist/api.js.map +1 -0
- package/dist/auth.d.ts +25 -0
- package/dist/auth.js +124 -0
- package/dist/auth.js.map +1 -0
- package/dist/config.d.ts +37 -0
- package/dist/config.js +56 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1335 -0
- package/dist/index.js.map +1 -0
- package/package.json +41 -0
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
|