@formigio/fazemos-cli 0.4.9 → 0.5.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/api.d.ts +13 -0
- package/dist/api.js +24 -4
- package/dist/api.js.map +1 -1
- package/dist/index.js +257 -7
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/api.d.ts
CHANGED
|
@@ -1 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error thrown when an API request returns a non-2xx response. Carries the
|
|
3
|
+
* `code` field from the API's error envelope (when present) so command
|
|
4
|
+
* handlers can branch on specific failure modes (e.g., EMAIL_SEND_FAILED).
|
|
5
|
+
* Also exposes the parsed body so callers can read extra fields like
|
|
6
|
+
* `inviteEmail`/`sessionEmail` on INVITE_EMAIL_MISMATCH.
|
|
7
|
+
*/
|
|
8
|
+
export declare class ApiError extends Error {
|
|
9
|
+
status: number;
|
|
10
|
+
code?: string;
|
|
11
|
+
body?: unknown;
|
|
12
|
+
constructor(message: string, status: number, code?: string, body?: unknown);
|
|
13
|
+
}
|
|
1
14
|
export declare function api(method: string, path: string, body?: unknown): Promise<unknown>;
|
package/dist/api.js
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
import { getEnv, getToken, getActiveOrgId } from './config.js';
|
|
2
2
|
import { refreshSession } from './auth.js';
|
|
3
|
+
/**
|
|
4
|
+
* Error thrown when an API request returns a non-2xx response. Carries the
|
|
5
|
+
* `code` field from the API's error envelope (when present) so command
|
|
6
|
+
* handlers can branch on specific failure modes (e.g., EMAIL_SEND_FAILED).
|
|
7
|
+
* Also exposes the parsed body so callers can read extra fields like
|
|
8
|
+
* `inviteEmail`/`sessionEmail` on INVITE_EMAIL_MISMATCH.
|
|
9
|
+
*/
|
|
10
|
+
export class ApiError extends Error {
|
|
11
|
+
status;
|
|
12
|
+
code;
|
|
13
|
+
body;
|
|
14
|
+
constructor(message, status, code, body) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = 'ApiError';
|
|
17
|
+
this.status = status;
|
|
18
|
+
this.code = code;
|
|
19
|
+
this.body = body;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
3
22
|
export async function api(method, path, body) {
|
|
4
23
|
const env = getEnv();
|
|
5
24
|
let token = getToken();
|
|
@@ -38,10 +57,11 @@ export async function api(method, path, body) {
|
|
|
38
57
|
data = text;
|
|
39
58
|
}
|
|
40
59
|
if (!response.ok) {
|
|
41
|
-
const
|
|
42
|
-
? data
|
|
43
|
-
:
|
|
44
|
-
|
|
60
|
+
const obj = typeof data === 'object' && data !== null
|
|
61
|
+
? data
|
|
62
|
+
: null;
|
|
63
|
+
const msg = obj?.error ?? `HTTP ${response.status}`;
|
|
64
|
+
throw new ApiError(msg, response.status, obj?.code, data);
|
|
45
65
|
}
|
|
46
66
|
return data;
|
|
47
67
|
}
|
package/dist/api.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC/D,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAE3C,MAAM,CAAC,KAAK,UAAU,GAAG,CAAC,MAAc,EAAE,IAAY,EAAE,IAAc;IACpE,MAAM,GAAG,GAAG,MAAM,EAAE,CAAC;IACrB,IAAI,KAAK,GAAG,QAAQ,EAAE,CAAC;IACvB,MAAM,KAAK,GAAG,cAAc,EAAE,CAAC;IAE/B,mCAAmC;IACnC,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,KAAK,GAAG,MAAM,cAAc,EAAE,CAAC;QAC/B,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,MAAM,IAAI,KAAK,CAAC,0CAA0C,CAAC,CAAC;QAC9D,CAAC;IACH,CAAC;IAED,MAAM,OAAO,GAA2B;QACtC,cAAc,EAAE,kBAAkB;KACnC,CAAC;IACF,IAAI,KAAK,EAAE,CAAC;QACV,OAAO,CAAC,eAAe,CAAC,GAAG,UAAU,KAAK,EAAE,CAAC;IAC/C,CAAC;IACD,IAAI,KAAK,EAAE,CAAC;QACV,OAAO,CAAC,UAAU,CAAC,GAAG,KAAK,CAAC;IAC9B,CAAC;IAED,MAAM,GAAG,GAAG,GAAG,GAAG,CAAC,MAAM,GAAG,IAAI,EAAE,CAAC;IACnC,MAAM,OAAO,GAAgB;QAC3B,MAAM;QACN,OAAO;KACR,CAAC;IAEF,IAAI,IAAI,IAAI,MAAM,KAAK,KAAK,EAAE,CAAC;QAC7B,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;IACtC,CAAC;IAED,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IAC3C,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;IAEnC,IAAI,IAAa,CAAC;IAClB,IAAI,CAAC;QACH,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC1B,CAAC;IAAC,MAAM,CAAC;QACP,IAAI,GAAG,IAAI,CAAC;IACd,CAAC;IAED,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,GAAG,
|
|
1
|
+
{"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC/D,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAE3C;;;;;;GAMG;AACH,MAAM,OAAO,QAAS,SAAQ,KAAK;IACjC,MAAM,CAAS;IACf,IAAI,CAAU;IACd,IAAI,CAAW;IACf,YAAY,OAAe,EAAE,MAAc,EAAE,IAAa,EAAE,IAAc;QACxE,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,UAAU,CAAC;QACvB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;CACF;AAED,MAAM,CAAC,KAAK,UAAU,GAAG,CAAC,MAAc,EAAE,IAAY,EAAE,IAAc;IACpE,MAAM,GAAG,GAAG,MAAM,EAAE,CAAC;IACrB,IAAI,KAAK,GAAG,QAAQ,EAAE,CAAC;IACvB,MAAM,KAAK,GAAG,cAAc,EAAE,CAAC;IAE/B,mCAAmC;IACnC,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,KAAK,GAAG,MAAM,cAAc,EAAE,CAAC;QAC/B,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,MAAM,IAAI,KAAK,CAAC,0CAA0C,CAAC,CAAC;QAC9D,CAAC;IACH,CAAC;IAED,MAAM,OAAO,GAA2B;QACtC,cAAc,EAAE,kBAAkB;KACnC,CAAC;IACF,IAAI,KAAK,EAAE,CAAC;QACV,OAAO,CAAC,eAAe,CAAC,GAAG,UAAU,KAAK,EAAE,CAAC;IAC/C,CAAC;IACD,IAAI,KAAK,EAAE,CAAC;QACV,OAAO,CAAC,UAAU,CAAC,GAAG,KAAK,CAAC;IAC9B,CAAC;IAED,MAAM,GAAG,GAAG,GAAG,GAAG,CAAC,MAAM,GAAG,IAAI,EAAE,CAAC;IACnC,MAAM,OAAO,GAAgB;QAC3B,MAAM;QACN,OAAO;KACR,CAAC;IAEF,IAAI,IAAI,IAAI,MAAM,KAAK,KAAK,EAAE,CAAC;QAC7B,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;IACtC,CAAC;IAED,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IAC3C,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;IAEnC,IAAI,IAAa,CAAC;IAClB,IAAI,CAAC;QACH,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC1B,CAAC;IAAC,MAAM,CAAC;QACP,IAAI,GAAG,IAAI,CAAC;IACd,CAAC;IAED,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,GAAG,GACP,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,IAAI;YACvC,CAAC,CAAE,IAA0C;YAC7C,CAAC,CAAC,IAAI,CAAC;QACX,MAAM,GAAG,GAAG,GAAG,EAAE,KAAK,IAAI,QAAQ,QAAQ,CAAC,MAAM,EAAE,CAAC;QACpD,MAAM,IAAI,QAAQ,CAAC,GAAG,EAAE,QAAQ,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;IAC5D,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { Command } from 'commander';
|
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import { config, getEnv, getToken, getActiveOrgId, setActiveOrgId, addEnvironment, hasEnvironments } from './config.js';
|
|
5
5
|
import { login, signup, confirmSignup, adminLogin } from './auth.js';
|
|
6
|
-
import { api } from './api.js';
|
|
6
|
+
import { api, ApiError } from './api.js';
|
|
7
7
|
import { execSync } from 'child_process';
|
|
8
8
|
import { readFileSync, readdirSync } from 'fs';
|
|
9
9
|
import { fileURLToPath } from 'url';
|
|
@@ -330,9 +330,10 @@ orgs
|
|
|
330
330
|
});
|
|
331
331
|
orgs
|
|
332
332
|
.command('invite')
|
|
333
|
-
.description('Invite a human member to the organization by email. The invitee receives an email to join. Use "orgs add-agent" for AI agents instead.')
|
|
333
|
+
.description('Invite a human member to the organization by email. The invitee receives an email with a one-time link to join. Use "orgs add-agent" for AI agents instead.')
|
|
334
334
|
.requiredOption('-e, --email <email>', 'Email address to invite')
|
|
335
335
|
.option('-r, --role <role>', 'Organization role: admin (full access) or member (default)', 'member')
|
|
336
|
+
.option('-d, --display-name <name>', 'Optional display name for the invitee')
|
|
336
337
|
.action(async (opts) => {
|
|
337
338
|
try {
|
|
338
339
|
const orgId = getActiveOrgId();
|
|
@@ -343,14 +344,81 @@ orgs
|
|
|
343
344
|
const data = await api('POST', `/api/organizations/${orgId}/members/invite`, {
|
|
344
345
|
email: opts.email,
|
|
345
346
|
role: opts.role,
|
|
347
|
+
displayName: opts.displayName,
|
|
348
|
+
});
|
|
349
|
+
printInviteSuccess({
|
|
350
|
+
email: opts.email,
|
|
351
|
+
role: opts.role,
|
|
352
|
+
member: data.member,
|
|
353
|
+
inviteLink: data.inviteLink,
|
|
354
|
+
expiresAt: data.expiresAt,
|
|
346
355
|
});
|
|
347
|
-
console.log(chalk.green(`Invited ${opts.email} as ${opts.role}`));
|
|
348
356
|
}
|
|
349
357
|
catch (err) {
|
|
350
|
-
|
|
358
|
+
// EMAIL_SEND_FAILED carries the member row + invite link in the body
|
|
359
|
+
// because the row was committed in 'failed' state for retry. Show
|
|
360
|
+
// the warning UX (yellow) instead of a hard error.
|
|
361
|
+
if (err instanceof ApiError && err.code === 'EMAIL_SEND_FAILED') {
|
|
362
|
+
const body = (err.body ?? {});
|
|
363
|
+
printInviteSendFailedWarning({
|
|
364
|
+
email: opts.email,
|
|
365
|
+
role: opts.role,
|
|
366
|
+
member: body.member,
|
|
367
|
+
inviteLink: body.inviteLink,
|
|
368
|
+
sendError: body.error || err.message,
|
|
369
|
+
});
|
|
370
|
+
process.exit(1);
|
|
371
|
+
}
|
|
372
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
373
|
+
console.error(chalk.red(`Error: ${msg}`));
|
|
351
374
|
process.exit(1);
|
|
352
375
|
}
|
|
353
376
|
});
|
|
377
|
+
function printInviteSuccess(args) {
|
|
378
|
+
const expiry = new Date(args.expiresAt);
|
|
379
|
+
const absoluteFmt = new Intl.DateTimeFormat('en-US', {
|
|
380
|
+
weekday: 'short',
|
|
381
|
+
month: 'short',
|
|
382
|
+
day: 'numeric',
|
|
383
|
+
year: 'numeric',
|
|
384
|
+
});
|
|
385
|
+
const days = Math.max(1, Math.round((expiry.getTime() - Date.now()) / (24 * 60 * 60 * 1000)));
|
|
386
|
+
console.log(chalk.bold(chalk.green(`Invited ${args.email} as ${args.role}.`)));
|
|
387
|
+
console.log('');
|
|
388
|
+
console.log(` Email sent to: ${args.email}`);
|
|
389
|
+
console.log(` Role: ${args.role}`);
|
|
390
|
+
if (args.member?.id) {
|
|
391
|
+
console.log(` Member ID: ${args.member.id}`);
|
|
392
|
+
}
|
|
393
|
+
console.log(` Expires: ${absoluteFmt.format(expiry)} (${days} days)`);
|
|
394
|
+
console.log('');
|
|
395
|
+
console.log(chalk.gray(' Invite link (for manual sharing):'));
|
|
396
|
+
console.log(chalk.gray(` ${args.inviteLink}`));
|
|
397
|
+
}
|
|
398
|
+
function printInviteSendFailedWarning(args) {
|
|
399
|
+
// HP5: warnings go to stderr so scripts piping stdout (e.g., for parsing
|
|
400
|
+
// the invite link or member id) don't get the recoverable warning text
|
|
401
|
+
// mixed into their parsed output. Exit code is already 1.
|
|
402
|
+
console.error(chalk.yellow('Warning: invite created but email delivery failed.'));
|
|
403
|
+
console.error('');
|
|
404
|
+
console.error(` Email: ${args.email}`);
|
|
405
|
+
console.error(` Role: ${args.role}`);
|
|
406
|
+
// HP4: Member ID is the row the operator needs to act on. Show it in the
|
|
407
|
+
// same labeled position as the success block, immediately after Role.
|
|
408
|
+
if (args.member?.id) {
|
|
409
|
+
console.error(` Member ID: ${args.member.id}`);
|
|
410
|
+
}
|
|
411
|
+
console.error(` Invite status: ${chalk.red('failed')} (${args.sendError})`);
|
|
412
|
+
if (args.inviteLink) {
|
|
413
|
+
console.error('');
|
|
414
|
+
console.error(chalk.gray(' Invite link (share manually or use `fazemos orgs invites resend`):'));
|
|
415
|
+
console.error(chalk.gray(` ${args.inviteLink}`));
|
|
416
|
+
}
|
|
417
|
+
if (args.member?.id) {
|
|
418
|
+
console.error('');
|
|
419
|
+
console.error(` To retry: fazemos orgs invites resend ${args.member.id}`);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
354
422
|
orgs
|
|
355
423
|
.command('add-agent')
|
|
356
424
|
.description('Create an AI agent member in the organization. The agent can then be assigned work via actions, commitments, or pipeline steps. Use "agents register" for bulk registration with roles and config.')
|
|
@@ -375,27 +443,44 @@ orgs
|
|
|
375
443
|
process.exit(1);
|
|
376
444
|
}
|
|
377
445
|
});
|
|
446
|
+
const ORGS_MEMBERS_STATUSES = ['active', 'invited', 'failed', 'all'];
|
|
378
447
|
orgs
|
|
379
448
|
.command('members')
|
|
380
449
|
.description('List org members')
|
|
381
450
|
.option('--agents', 'Show only agent members')
|
|
451
|
+
.option('--status <status>', 'Filter by status: active|invited|failed|all (default: active)', 'active')
|
|
382
452
|
.action(async (opts) => {
|
|
383
453
|
try {
|
|
454
|
+
const status = opts.status || 'active';
|
|
455
|
+
if (!ORGS_MEMBERS_STATUSES.includes(status)) {
|
|
456
|
+
console.error(chalk.red(`Invalid --status "${status}". Allowed values: ${ORGS_MEMBERS_STATUSES.join(', ')}`));
|
|
457
|
+
process.exit(1);
|
|
458
|
+
}
|
|
384
459
|
const orgId = getActiveOrgId();
|
|
385
460
|
if (!orgId) {
|
|
386
461
|
console.error(chalk.red('No active org. Run: fazemos orgs switch <slug>'));
|
|
387
462
|
process.exit(1);
|
|
388
463
|
}
|
|
389
|
-
const data = await api('GET', `/api/organizations/${orgId}/members`);
|
|
464
|
+
const data = await api('GET', `/api/organizations/${orgId}/members?status=${encodeURIComponent(status)}`);
|
|
390
465
|
if (!data.members?.length) {
|
|
391
|
-
console.log(chalk.yellow(
|
|
466
|
+
console.log(chalk.yellow(`No members (status=${status})`));
|
|
392
467
|
return;
|
|
393
468
|
}
|
|
469
|
+
if (status === 'invited' || status === 'failed') {
|
|
470
|
+
printPendingInvites(data.members);
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
// S6: when status !== 'active', the result set may contain a mix of
|
|
474
|
+
// statuses (active, invited, failed, revoked). Show the row's status
|
|
475
|
+
// inline so callers can tell them apart. The default `--status active`
|
|
476
|
+
// path stays unchanged (no status noise).
|
|
477
|
+
const showStatus = status !== 'active';
|
|
394
478
|
for (const m of data.members) {
|
|
395
479
|
if (opts.agents && m.member_type !== 'agent')
|
|
396
480
|
continue;
|
|
397
481
|
const type = m.member_type === 'agent' ? chalk.blue('agent') : 'human';
|
|
398
|
-
|
|
482
|
+
const statusBadge = showStatus ? ` ${formatMemberStatusBadge(m.status)}` : '';
|
|
483
|
+
console.log(` ${chalk.cyan(m.display_name)} (${m.role}, ${type})${statusBadge} — ${m.id}`);
|
|
399
484
|
}
|
|
400
485
|
}
|
|
401
486
|
catch (err) {
|
|
@@ -403,6 +488,171 @@ orgs
|
|
|
403
488
|
process.exit(1);
|
|
404
489
|
}
|
|
405
490
|
});
|
|
491
|
+
function printPendingInvites(members) {
|
|
492
|
+
console.log(chalk.bold(`Pending invites (${members.length}):`));
|
|
493
|
+
console.log('');
|
|
494
|
+
console.log(' EMAIL ROLE INVITED BY SENT EXPIRES STATUS ID');
|
|
495
|
+
for (const m of members) {
|
|
496
|
+
const email = (m.email || '').padEnd(30).slice(0, 30);
|
|
497
|
+
const role = (m.role || '').padEnd(8).slice(0, 8);
|
|
498
|
+
const inviter = (m.inviter_name || 'unknown').padEnd(17).slice(0, 17);
|
|
499
|
+
const sent = m.invite_last_sent_at
|
|
500
|
+
? formatRelative(new Date(m.invite_last_sent_at)).padEnd(16).slice(0, 16)
|
|
501
|
+
: 'never ';
|
|
502
|
+
let expires;
|
|
503
|
+
if (m.invite_expires_at) {
|
|
504
|
+
const e = formatExpiresAt(new Date(m.invite_expires_at));
|
|
505
|
+
// Pad the visible text first, then colorize. Colorizing before padding
|
|
506
|
+
// would break .padEnd because chalk's ANSI escape codes inflate .length.
|
|
507
|
+
expires = e.expired
|
|
508
|
+
? chalk.red(e.text.padEnd(15))
|
|
509
|
+
: e.text.padEnd(15);
|
|
510
|
+
}
|
|
511
|
+
else {
|
|
512
|
+
expires = 'unknown ';
|
|
513
|
+
}
|
|
514
|
+
const status = m.status === 'failed' ? chalk.red('failed ') : chalk.green('invited');
|
|
515
|
+
const resendNote = m.invite_resend_count
|
|
516
|
+
? chalk.gray(` (resent ${m.invite_resend_count}x)`)
|
|
517
|
+
: '';
|
|
518
|
+
console.log(` ${email} ${role} ${inviter} ${sent} ${expires} ${status} ${m.id}${resendNote}`);
|
|
519
|
+
}
|
|
520
|
+
console.log('');
|
|
521
|
+
console.log(chalk.gray('Use `fazemos orgs invites resend <member-id>` to resend.'));
|
|
522
|
+
console.log(chalk.gray('Use `fazemos orgs invites revoke <member-id>` to revoke.'));
|
|
523
|
+
}
|
|
524
|
+
function formatRelative(d) {
|
|
525
|
+
const diffMs = Date.now() - d.getTime();
|
|
526
|
+
const days = Math.floor(diffMs / (24 * 60 * 60 * 1000));
|
|
527
|
+
if (days <= 0)
|
|
528
|
+
return 'today';
|
|
529
|
+
if (days === 1)
|
|
530
|
+
return '1 day ago';
|
|
531
|
+
return `${days} days ago`;
|
|
532
|
+
}
|
|
533
|
+
function formatExpiresAt(d) {
|
|
534
|
+
const diffMs = d.getTime() - Date.now();
|
|
535
|
+
if (diffMs <= 0)
|
|
536
|
+
return { text: 'EXPIRED', expired: true };
|
|
537
|
+
const days = Math.ceil(diffMs / (24 * 60 * 60 * 1000));
|
|
538
|
+
if (days === 1)
|
|
539
|
+
return { text: 'in 1 day', expired: false };
|
|
540
|
+
return { text: `in ${days} days`, expired: false };
|
|
541
|
+
}
|
|
542
|
+
// S6: render a per-row status badge for the bullet member list. Used only
|
|
543
|
+
// when --status is not the default 'active' so a mixed result set (e.g. from
|
|
544
|
+
// --status all) is unambiguous about which row is active vs revoked vs etc.
|
|
545
|
+
function formatMemberStatusBadge(status) {
|
|
546
|
+
const s = (status || 'unknown').toLowerCase();
|
|
547
|
+
switch (s) {
|
|
548
|
+
case 'active': return chalk.green('[active]');
|
|
549
|
+
case 'invited': return chalk.yellow('[invited]');
|
|
550
|
+
case 'failed': return chalk.red('[failed]');
|
|
551
|
+
case 'revoked': return chalk.gray('[revoked]');
|
|
552
|
+
default: return chalk.gray(`[${s}]`);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
// ── orgs invites subcommands ────────────────────────────────
|
|
556
|
+
const invites = orgs.command('invites').description('Manage pending org invites');
|
|
557
|
+
const INVITES_LIST_STATUSES = ['invited', 'failed', 'all'];
|
|
558
|
+
invites
|
|
559
|
+
.command('list')
|
|
560
|
+
.description('List pending invites for the active org (alias of `orgs members --status invited`)')
|
|
561
|
+
.option('--status <status>', 'Filter: invited|failed|all (default: invited)', 'invited')
|
|
562
|
+
.action(async (opts) => {
|
|
563
|
+
try {
|
|
564
|
+
const status = opts.status || 'invited';
|
|
565
|
+
if (!INVITES_LIST_STATUSES.includes(status)) {
|
|
566
|
+
console.error(chalk.red(`Invalid --status "${status}". Allowed values: ${INVITES_LIST_STATUSES.join(', ')}`));
|
|
567
|
+
process.exit(1);
|
|
568
|
+
}
|
|
569
|
+
const orgId = getActiveOrgId();
|
|
570
|
+
if (!orgId) {
|
|
571
|
+
console.error(chalk.red('No active org. Run: fazemos orgs switch <slug>'));
|
|
572
|
+
process.exit(1);
|
|
573
|
+
}
|
|
574
|
+
// S6: `orgs invites list` is scoped to invites. When --status all is
|
|
575
|
+
// passed it should mean "all invite statuses" (invited + failed), NOT
|
|
576
|
+
// "all members regardless of invite status". The backend ?status=all
|
|
577
|
+
// returns every member, so we filter client-side here. We keep the
|
|
578
|
+
// single ?status=all call (rather than two calls for invited+failed)
|
|
579
|
+
// for simplicity — do not "optimize" this into the underlying status
|
|
580
|
+
// params without re-checking that the API supports a multi-value filter.
|
|
581
|
+
const data = await api('GET', `/api/organizations/${orgId}/members?status=${encodeURIComponent(status)}`);
|
|
582
|
+
let rows = data.members || [];
|
|
583
|
+
if (status === 'all') {
|
|
584
|
+
rows = rows.filter((m) => m.status === 'invited' || m.status === 'failed');
|
|
585
|
+
}
|
|
586
|
+
if (!rows.length) {
|
|
587
|
+
const label = status === 'all' ? 'pending or failed' : status;
|
|
588
|
+
console.log(chalk.yellow(`No ${label} invites.`));
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
printPendingInvites(rows);
|
|
592
|
+
}
|
|
593
|
+
catch (err) {
|
|
594
|
+
console.error(chalk.red(err.message));
|
|
595
|
+
process.exit(1);
|
|
596
|
+
}
|
|
597
|
+
});
|
|
598
|
+
invites
|
|
599
|
+
.command('resend <memberId>')
|
|
600
|
+
.description('Generate a fresh invite token and re-send the email')
|
|
601
|
+
.action(async (memberId) => {
|
|
602
|
+
try {
|
|
603
|
+
const orgId = getActiveOrgId();
|
|
604
|
+
if (!orgId) {
|
|
605
|
+
console.error(chalk.red('No active org. Run: fazemos orgs switch <slug>'));
|
|
606
|
+
process.exit(1);
|
|
607
|
+
}
|
|
608
|
+
const data = await api('POST', `/api/organizations/${orgId}/members/${memberId}/resend`, {});
|
|
609
|
+
console.log(chalk.green(`Invite resent to ${data.member.email}.`));
|
|
610
|
+
console.log('');
|
|
611
|
+
const expiry = new Date(data.expiresAt);
|
|
612
|
+
const days = Math.max(1, Math.round((expiry.getTime() - Date.now()) / (24 * 60 * 60 * 1000)));
|
|
613
|
+
const fmt = new Intl.DateTimeFormat('en-US', {
|
|
614
|
+
weekday: 'short', month: 'short', day: 'numeric', year: 'numeric',
|
|
615
|
+
});
|
|
616
|
+
console.log(` New expiry: ${fmt.format(expiry)} (${days} days)`);
|
|
617
|
+
console.log(chalk.gray(` Invite link: ${data.inviteLink}`));
|
|
618
|
+
}
|
|
619
|
+
catch (err) {
|
|
620
|
+
if (err instanceof ApiError && err.code === 'EMAIL_SEND_FAILED') {
|
|
621
|
+
const body = (err.body ?? {});
|
|
622
|
+
// HP5: warnings go to stderr so scripts piping stdout don't get
|
|
623
|
+
// recoverable warnings mixed into their parsed output.
|
|
624
|
+
console.error(chalk.yellow('Warning: invite resent but email delivery failed.'));
|
|
625
|
+
console.error('');
|
|
626
|
+
console.error(` Member ID: ${memberId}`);
|
|
627
|
+
console.error(` Reason: ${body.error || err.message}`);
|
|
628
|
+
if (body.inviteLink) {
|
|
629
|
+
console.error(chalk.gray(` Invite link: ${body.inviteLink}`));
|
|
630
|
+
}
|
|
631
|
+
process.exit(1);
|
|
632
|
+
}
|
|
633
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
634
|
+
console.error(chalk.red(`Error: ${msg}`));
|
|
635
|
+
process.exit(1);
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
invites
|
|
639
|
+
.command('revoke <memberId>')
|
|
640
|
+
.description('Revoke a pending invite immediately (invalidates the token)')
|
|
641
|
+
.action(async (memberId) => {
|
|
642
|
+
try {
|
|
643
|
+
const orgId = getActiveOrgId();
|
|
644
|
+
if (!orgId) {
|
|
645
|
+
console.error(chalk.red('No active org. Run: fazemos orgs switch <slug>'));
|
|
646
|
+
process.exit(1);
|
|
647
|
+
}
|
|
648
|
+
await api('POST', `/api/organizations/${orgId}/members/${memberId}/revoke`, {});
|
|
649
|
+
console.log(chalk.green(`Invite for member ${memberId} has been revoked.`));
|
|
650
|
+
}
|
|
651
|
+
catch (err) {
|
|
652
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
653
|
+
process.exit(1);
|
|
654
|
+
}
|
|
655
|
+
});
|
|
406
656
|
// ── Worksheets ──────────────────────────────────────────────
|
|
407
657
|
const ws = program.command('worksheets').alias('ws').description('Worksheet commands');
|
|
408
658
|
ws
|