sentron-cli 1.0.23 → 1.0.24
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/README.md +366 -29
- package/dist/api.d.ts +424 -0
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +369 -1
- package/dist/api.js.map +1 -1
- package/dist/commands/app.d.ts +6 -0
- package/dist/commands/app.d.ts.map +1 -0
- package/dist/commands/app.js +874 -0
- package/dist/commands/app.js.map +1 -0
- package/dist/commands/invitations.d.ts +7 -0
- package/dist/commands/invitations.d.ts.map +1 -0
- package/dist/commands/invitations.js +284 -0
- package/dist/commands/invitations.js.map +1 -0
- package/dist/commands/notifications.d.ts +6 -0
- package/dist/commands/notifications.d.ts.map +1 -0
- package/dist/commands/notifications.js +327 -0
- package/dist/commands/notifications.js.map +1 -0
- package/dist/commands/org.d.ts +6 -0
- package/dist/commands/org.d.ts.map +1 -0
- package/dist/commands/org.js +1406 -0
- package/dist/commands/org.js.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/utils/styling.d.ts +5 -0
- package/dist/utils/styling.d.ts.map +1 -1
- package/dist/utils/styling.js +16 -0
- package/dist/utils/styling.js.map +1 -1
- package/package.json +3 -1
|
@@ -0,0 +1,1406 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Organization Commands
|
|
3
|
+
*/
|
|
4
|
+
import * as crypto from 'crypto';
|
|
5
|
+
import * as readline from 'readline';
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
import { api } from '../api.js';
|
|
9
|
+
import { getStoredToken } from '../utils/storage.js';
|
|
10
|
+
import { printSection, printSuccess, printError, printErrorWithAction, printInfo, printBox, printLoading, printLoadingComplete, printLoadingError, printPrompt, brand, } from '../utils/styling.js';
|
|
11
|
+
/**
|
|
12
|
+
* Prompt for input
|
|
13
|
+
*/
|
|
14
|
+
function prompt(question) {
|
|
15
|
+
const rl = readline.createInterface({
|
|
16
|
+
input: process.stdin,
|
|
17
|
+
output: process.stdout,
|
|
18
|
+
});
|
|
19
|
+
return new Promise((resolve) => {
|
|
20
|
+
rl.question(question, (answer) => {
|
|
21
|
+
rl.close();
|
|
22
|
+
resolve(answer.trim());
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
export function setupOrgCommands(program) {
|
|
27
|
+
const orgCmd = program
|
|
28
|
+
.command('org')
|
|
29
|
+
.alias('organization')
|
|
30
|
+
.description('Manage organizations');
|
|
31
|
+
// List organizations
|
|
32
|
+
orgCmd
|
|
33
|
+
.command('list')
|
|
34
|
+
.alias('ls')
|
|
35
|
+
.description('List your organizations')
|
|
36
|
+
.option('--json', 'Output as JSON')
|
|
37
|
+
.action(async (options) => {
|
|
38
|
+
try {
|
|
39
|
+
const token = await getStoredToken();
|
|
40
|
+
if (!token) {
|
|
41
|
+
printError('Not signed in. Run "sentron auth signin" first.');
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
printLoading('Fetching organizations');
|
|
45
|
+
const orgs = await api.organizations.list();
|
|
46
|
+
printLoadingComplete();
|
|
47
|
+
if (options.json) {
|
|
48
|
+
console.log(JSON.stringify(orgs, null, 2));
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
console.log();
|
|
52
|
+
printSection('Organizations');
|
|
53
|
+
if (orgs.length === 0) {
|
|
54
|
+
printInfo('No organizations found. Create one with "sentron org create"');
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
orgs.forEach((org) => {
|
|
58
|
+
printBox([
|
|
59
|
+
`Name: ${brand.accent(org.name)}`,
|
|
60
|
+
`Slug: ${org.slug}`,
|
|
61
|
+
`Role: ${org.role}`,
|
|
62
|
+
`Members: ${org.memberCount}`,
|
|
63
|
+
`Apps: ${org.appCount}`,
|
|
64
|
+
org.description ? `Desc: ${org.description}` : '',
|
|
65
|
+
`ID: ${brand.muted(org.id)}`,
|
|
66
|
+
].filter(Boolean));
|
|
67
|
+
console.log();
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
printLoadingError();
|
|
74
|
+
console.log();
|
|
75
|
+
printError(error instanceof Error ? error.message : 'Failed to list organizations');
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
// Create organization
|
|
80
|
+
orgCmd
|
|
81
|
+
.command('create')
|
|
82
|
+
.description('Create a new organization (slug is auto-generated from name)')
|
|
83
|
+
.option('-n, --name <name>', 'Organization name')
|
|
84
|
+
.option('-d, --description <description>', 'Organization description')
|
|
85
|
+
.action(async (options) => {
|
|
86
|
+
try {
|
|
87
|
+
const token = await getStoredToken();
|
|
88
|
+
if (!token) {
|
|
89
|
+
printError('Not signed in. Run "sentron auth signin" first.');
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
const rawName = options.name || await prompt(printPrompt('Organization Name'));
|
|
93
|
+
const name = rawName?.trim();
|
|
94
|
+
if (!name) {
|
|
95
|
+
printError('Name is required and cannot be empty or whitespace only');
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
printLoading('Creating organization');
|
|
99
|
+
const org = await api.organizations.create({
|
|
100
|
+
name,
|
|
101
|
+
description: options.description,
|
|
102
|
+
});
|
|
103
|
+
printLoadingComplete();
|
|
104
|
+
console.log();
|
|
105
|
+
printSection('Organization Created');
|
|
106
|
+
printBox([
|
|
107
|
+
`Name: ${brand.accent(org.name)}`,
|
|
108
|
+
`Slug: ${org.slug}`,
|
|
109
|
+
`URL: sentron.dev/organizations/${org.slug}`,
|
|
110
|
+
`ID: ${brand.muted(org.id)}`,
|
|
111
|
+
]);
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
printLoadingError();
|
|
115
|
+
console.log();
|
|
116
|
+
printError(error instanceof Error ? error.message : 'Failed to create organization');
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
// Get organization details
|
|
121
|
+
orgCmd
|
|
122
|
+
.command('get <id>')
|
|
123
|
+
.alias('show')
|
|
124
|
+
.description('Get organization details (accepts UUID or slug)')
|
|
125
|
+
.option('--json', 'Output as JSON')
|
|
126
|
+
.action(async (id, options) => {
|
|
127
|
+
try {
|
|
128
|
+
const token = await getStoredToken();
|
|
129
|
+
if (!token) {
|
|
130
|
+
printError('Not signed in. Run "sentron auth signin" first.');
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
printLoading('Fetching organization');
|
|
134
|
+
const org = await api.organizations.get(id);
|
|
135
|
+
printLoadingComplete();
|
|
136
|
+
if (options.json) {
|
|
137
|
+
console.log(JSON.stringify(org, null, 2));
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
console.log();
|
|
141
|
+
printSection('Organization Details');
|
|
142
|
+
printBox([
|
|
143
|
+
`Name: ${brand.accent(org.name)}`,
|
|
144
|
+
`Slug: ${org.slug}`,
|
|
145
|
+
org.description ? `Desc: ${org.description}` : '',
|
|
146
|
+
org.website ? `Website: ${org.website}` : '',
|
|
147
|
+
`Billing: ${org.billingStatus}`,
|
|
148
|
+
`Members: ${org.memberCount}`,
|
|
149
|
+
`Apps: ${org.appCount}`,
|
|
150
|
+
`Created: ${new Date(org.createdAt).toLocaleDateString()}`,
|
|
151
|
+
`ID: ${brand.muted(org.id)}`,
|
|
152
|
+
].filter(Boolean));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
printLoadingError();
|
|
157
|
+
console.log();
|
|
158
|
+
printErrorWithAction(error, 'Failed to get organization');
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
// Update organization
|
|
163
|
+
orgCmd
|
|
164
|
+
.command('update <id>')
|
|
165
|
+
.description('Update organization details')
|
|
166
|
+
.option('-n, --name <name>', 'New name')
|
|
167
|
+
.option('-d, --description <description>', 'New description')
|
|
168
|
+
.option('-w, --website <website>', 'New website')
|
|
169
|
+
.action(async (id, options) => {
|
|
170
|
+
try {
|
|
171
|
+
const token = await getStoredToken();
|
|
172
|
+
if (!token) {
|
|
173
|
+
printError('Not signed in. Run "sentron auth signin" first.');
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
if (!options.name && !options.description && !options.website) {
|
|
177
|
+
printError('At least one field to update is required');
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
printLoading('Updating organization');
|
|
181
|
+
const org = await api.organizations.update(id, {
|
|
182
|
+
name: options.name,
|
|
183
|
+
description: options.description,
|
|
184
|
+
website: options.website,
|
|
185
|
+
});
|
|
186
|
+
printLoadingComplete();
|
|
187
|
+
console.log();
|
|
188
|
+
printSection('Organization Updated');
|
|
189
|
+
printSuccess(`Updated: ${org.name}`);
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
printLoadingError();
|
|
193
|
+
console.log();
|
|
194
|
+
printError(error instanceof Error ? error.message : 'Failed to update organization');
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
// Delete organization
|
|
199
|
+
orgCmd
|
|
200
|
+
.command('delete <id>')
|
|
201
|
+
.description('Delete an organization')
|
|
202
|
+
.option('-f, --force', 'Skip confirmation')
|
|
203
|
+
.action(async (id, options) => {
|
|
204
|
+
try {
|
|
205
|
+
const token = await getStoredToken();
|
|
206
|
+
if (!token) {
|
|
207
|
+
printError('Not signed in. Run "sentron auth signin" first.');
|
|
208
|
+
process.exit(1);
|
|
209
|
+
}
|
|
210
|
+
if (!options.force) {
|
|
211
|
+
const confirm = await prompt(printPrompt('Type "DELETE" to confirm'));
|
|
212
|
+
if (confirm !== 'DELETE') {
|
|
213
|
+
printInfo('Cancelled');
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
printLoading('Deleting organization');
|
|
218
|
+
await api.organizations.delete(id);
|
|
219
|
+
printLoadingComplete();
|
|
220
|
+
console.log();
|
|
221
|
+
printSuccess('Organization deleted successfully');
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
printLoadingError();
|
|
225
|
+
console.log();
|
|
226
|
+
printError(error instanceof Error ? error.message : 'Failed to delete organization');
|
|
227
|
+
process.exit(1);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
// Members subcommand
|
|
231
|
+
const membersCmd = orgCmd
|
|
232
|
+
.command('members')
|
|
233
|
+
.description('Manage organization members');
|
|
234
|
+
// List members
|
|
235
|
+
membersCmd
|
|
236
|
+
.command('list <orgId>')
|
|
237
|
+
.alias('ls')
|
|
238
|
+
.description('List organization members')
|
|
239
|
+
.option('--json', 'Output as JSON')
|
|
240
|
+
.action(async (orgId, options) => {
|
|
241
|
+
try {
|
|
242
|
+
const token = await getStoredToken();
|
|
243
|
+
if (!token) {
|
|
244
|
+
printError('Not signed in. Run "sentron auth signin" first.');
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
247
|
+
printLoading('Fetching members');
|
|
248
|
+
const members = await api.organizations.listMembers(orgId);
|
|
249
|
+
printLoadingComplete();
|
|
250
|
+
if (options.json) {
|
|
251
|
+
console.log(JSON.stringify(members, null, 2));
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
console.log();
|
|
255
|
+
printSection('Organization Members');
|
|
256
|
+
if (members.length === 0) {
|
|
257
|
+
printInfo('No members found');
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
members.forEach((member) => {
|
|
261
|
+
const name = [member.firstName, member.lastName].filter(Boolean).join(' ') || 'N/A';
|
|
262
|
+
printBox([
|
|
263
|
+
`Email: ${member.email}`,
|
|
264
|
+
`Name: ${name}`,
|
|
265
|
+
`Role: ${brand.accent(member.role)}`,
|
|
266
|
+
`Joined: ${new Date(member.joinedAt).toLocaleDateString()}`,
|
|
267
|
+
`ID: ${brand.muted(member.userId)}`,
|
|
268
|
+
]);
|
|
269
|
+
console.log();
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
printLoadingError();
|
|
276
|
+
console.log();
|
|
277
|
+
printErrorWithAction(error, 'Failed to list members');
|
|
278
|
+
process.exit(1);
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
// Update member role
|
|
282
|
+
membersCmd
|
|
283
|
+
.command('update-role <orgId> <userId> <role>')
|
|
284
|
+
.description('Update member role (owner, admin, manager, member)')
|
|
285
|
+
.action(async (orgId, userId, role) => {
|
|
286
|
+
try {
|
|
287
|
+
const token = await getStoredToken();
|
|
288
|
+
if (!token) {
|
|
289
|
+
printError('Not signed in. Run "sentron auth signin" first.');
|
|
290
|
+
process.exit(1);
|
|
291
|
+
}
|
|
292
|
+
const validRoles = ['owner', 'admin', 'manager', 'member'];
|
|
293
|
+
if (!validRoles.includes(role)) {
|
|
294
|
+
printError(`Invalid role. Must be one of: ${validRoles.join(', ')}`);
|
|
295
|
+
process.exit(1);
|
|
296
|
+
}
|
|
297
|
+
printLoading('Updating member role');
|
|
298
|
+
await api.organizations.updateMemberRole(orgId, userId, role);
|
|
299
|
+
printLoadingComplete();
|
|
300
|
+
console.log();
|
|
301
|
+
printSuccess(`Member role updated to: ${role}`);
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
printLoadingError();
|
|
305
|
+
console.log();
|
|
306
|
+
printErrorWithAction(error, 'Failed to update member role');
|
|
307
|
+
process.exit(1);
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
// Remove member
|
|
311
|
+
membersCmd
|
|
312
|
+
.command('remove <orgId> <userId>')
|
|
313
|
+
.description('Remove a member from the organization')
|
|
314
|
+
.action(async (orgId, userId) => {
|
|
315
|
+
try {
|
|
316
|
+
const token = await getStoredToken();
|
|
317
|
+
if (!token) {
|
|
318
|
+
printError('Not signed in. Run "sentron auth signin" first.');
|
|
319
|
+
process.exit(1);
|
|
320
|
+
}
|
|
321
|
+
printLoading('Removing member');
|
|
322
|
+
await api.organizations.removeMember(orgId, userId);
|
|
323
|
+
printLoadingComplete();
|
|
324
|
+
console.log();
|
|
325
|
+
printSuccess('Member removed successfully');
|
|
326
|
+
}
|
|
327
|
+
catch (error) {
|
|
328
|
+
printLoadingError();
|
|
329
|
+
console.log();
|
|
330
|
+
printError(error instanceof Error ? error.message : 'Failed to remove member');
|
|
331
|
+
process.exit(1);
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
// Invitations subcommand
|
|
335
|
+
const invitesCmd = orgCmd
|
|
336
|
+
.command('invites')
|
|
337
|
+
.alias('invitations')
|
|
338
|
+
.description('Manage organization invitations');
|
|
339
|
+
// List invitations
|
|
340
|
+
invitesCmd
|
|
341
|
+
.command('list <orgId>')
|
|
342
|
+
.alias('ls')
|
|
343
|
+
.description('List pending invitations')
|
|
344
|
+
.option('--json', 'Output as JSON')
|
|
345
|
+
.action(async (orgId, options) => {
|
|
346
|
+
try {
|
|
347
|
+
const token = await getStoredToken();
|
|
348
|
+
if (!token) {
|
|
349
|
+
printError('Not signed in. Run "sentron auth signin" first.');
|
|
350
|
+
process.exit(1);
|
|
351
|
+
}
|
|
352
|
+
printLoading('Fetching invitations');
|
|
353
|
+
const invites = await api.organizations.listInvitations(orgId);
|
|
354
|
+
printLoadingComplete();
|
|
355
|
+
if (options.json) {
|
|
356
|
+
console.log(JSON.stringify(invites, null, 2));
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
console.log();
|
|
360
|
+
printSection('Invitations');
|
|
361
|
+
if (invites.length === 0) {
|
|
362
|
+
printInfo('No pending invitations');
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
invites.forEach((inv) => {
|
|
366
|
+
printBox([
|
|
367
|
+
`Email: ${inv.email}`,
|
|
368
|
+
`Role: ${inv.role}`,
|
|
369
|
+
`Status: ${inv.status}`,
|
|
370
|
+
`Expires: ${new Date(inv.expiresAt).toLocaleDateString()}`,
|
|
371
|
+
`ID: ${brand.muted(inv.id)}`,
|
|
372
|
+
]);
|
|
373
|
+
console.log();
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
catch (error) {
|
|
379
|
+
printLoadingError();
|
|
380
|
+
console.log();
|
|
381
|
+
printError(error instanceof Error ? error.message : 'Failed to list invitations');
|
|
382
|
+
process.exit(1);
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
// Create invitation
|
|
386
|
+
invitesCmd
|
|
387
|
+
.command('create <orgId>')
|
|
388
|
+
.description('Invite someone to the organization')
|
|
389
|
+
.option('-e, --email <email>', 'Email to invite')
|
|
390
|
+
.option('-r, --role <role>', 'Role to assign (default: member)')
|
|
391
|
+
.action(async (orgId, options) => {
|
|
392
|
+
try {
|
|
393
|
+
const token = await getStoredToken();
|
|
394
|
+
if (!token) {
|
|
395
|
+
printError('Not signed in. Run "sentron auth signin" first.');
|
|
396
|
+
process.exit(1);
|
|
397
|
+
}
|
|
398
|
+
const email = options.email || await prompt(printPrompt('Email to invite'));
|
|
399
|
+
if (!email) {
|
|
400
|
+
printError('Email is required');
|
|
401
|
+
process.exit(1);
|
|
402
|
+
}
|
|
403
|
+
printLoading('Creating invitation');
|
|
404
|
+
const inv = await api.organizations.createInvitation(orgId, email, options.role);
|
|
405
|
+
printLoadingComplete();
|
|
406
|
+
console.log();
|
|
407
|
+
printSection('Invitation Sent');
|
|
408
|
+
printBox([
|
|
409
|
+
`Email: ${inv.email}`,
|
|
410
|
+
`Role: ${inv.role}`,
|
|
411
|
+
`Expires: ${new Date(inv.expiresAt).toLocaleDateString()}`,
|
|
412
|
+
]);
|
|
413
|
+
}
|
|
414
|
+
catch (error) {
|
|
415
|
+
printLoadingError();
|
|
416
|
+
console.log();
|
|
417
|
+
printError(error instanceof Error ? error.message : 'Failed to create invitation');
|
|
418
|
+
process.exit(1);
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
// Batch invite
|
|
422
|
+
invitesCmd
|
|
423
|
+
.command('batch <orgId>')
|
|
424
|
+
.description('Invite multiple people to the organization (max 50)')
|
|
425
|
+
.option('-e, --emails <emails>', 'Comma-separated list of emails')
|
|
426
|
+
.option('-f, --file <path>', 'CSV file with columns: email,role')
|
|
427
|
+
.option('-r, --role <role>', 'Default role for all (admin, manager, member)', 'member')
|
|
428
|
+
.option('--json', 'Output as JSON')
|
|
429
|
+
.action(async (orgId, options) => {
|
|
430
|
+
try {
|
|
431
|
+
const token = await getStoredToken();
|
|
432
|
+
if (!token) {
|
|
433
|
+
printError('Not signed in. Run "sentron auth signin" first.');
|
|
434
|
+
process.exit(1);
|
|
435
|
+
}
|
|
436
|
+
// Validate role
|
|
437
|
+
const validRoles = ['admin', 'manager', 'member'];
|
|
438
|
+
if (!validRoles.includes(options.role)) {
|
|
439
|
+
printError(`Invalid role. Must be one of: ${validRoles.join(', ')}`);
|
|
440
|
+
process.exit(1);
|
|
441
|
+
}
|
|
442
|
+
// Get invitations from either --emails or --file
|
|
443
|
+
let invitations = [];
|
|
444
|
+
if (options.file) {
|
|
445
|
+
// Read from CSV file
|
|
446
|
+
const filePath = path.resolve(options.file);
|
|
447
|
+
if (!fs.existsSync(filePath)) {
|
|
448
|
+
printError(`File not found: ${options.file}`);
|
|
449
|
+
process.exit(1);
|
|
450
|
+
}
|
|
451
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
452
|
+
const lines = content.trim().split('\n');
|
|
453
|
+
// Check if first line is header
|
|
454
|
+
const firstLine = lines[0].toLowerCase();
|
|
455
|
+
const hasHeader = firstLine.includes('email') || firstLine.includes('role');
|
|
456
|
+
const dataLines = hasHeader ? lines.slice(1) : lines;
|
|
457
|
+
for (const line of dataLines) {
|
|
458
|
+
const trimmed = line.trim();
|
|
459
|
+
if (!trimmed)
|
|
460
|
+
continue;
|
|
461
|
+
const parts = trimmed.split(',').map(s => s.trim());
|
|
462
|
+
const email = parts[0];
|
|
463
|
+
const role = parts[1] || options.role;
|
|
464
|
+
if (!email)
|
|
465
|
+
continue;
|
|
466
|
+
if (!validRoles.includes(role)) {
|
|
467
|
+
printError(`Invalid role "${role}" for email ${email}. Must be one of: ${validRoles.join(', ')}`);
|
|
468
|
+
process.exit(1);
|
|
469
|
+
}
|
|
470
|
+
invitations.push({ email, role });
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
else if (options.emails) {
|
|
474
|
+
// Parse from comma-separated string
|
|
475
|
+
const emails = options.emails.split(',').map((e) => e.trim()).filter(Boolean);
|
|
476
|
+
invitations = emails.map((email) => ({ email, role: options.role }));
|
|
477
|
+
}
|
|
478
|
+
else {
|
|
479
|
+
// Interactive mode - prompt for emails
|
|
480
|
+
const input = await prompt(printPrompt('Enter emails (comma-separated)'));
|
|
481
|
+
if (!input) {
|
|
482
|
+
printError('No emails provided');
|
|
483
|
+
process.exit(1);
|
|
484
|
+
}
|
|
485
|
+
const emails = input.split(',').map(e => e.trim()).filter(Boolean);
|
|
486
|
+
invitations = emails.map(email => ({ email, role: options.role }));
|
|
487
|
+
}
|
|
488
|
+
if (invitations.length === 0) {
|
|
489
|
+
printError('No invitations to send');
|
|
490
|
+
process.exit(1);
|
|
491
|
+
}
|
|
492
|
+
if (invitations.length > 50) {
|
|
493
|
+
printError('Maximum 50 invitations per batch. Split into multiple batches.');
|
|
494
|
+
process.exit(1);
|
|
495
|
+
}
|
|
496
|
+
printLoading(`Creating ${invitations.length} invitation(s)`);
|
|
497
|
+
const result = await api.organizations.batchInvite(orgId, invitations);
|
|
498
|
+
printLoadingComplete();
|
|
499
|
+
if (options.json) {
|
|
500
|
+
console.log(JSON.stringify(result, null, 2));
|
|
501
|
+
}
|
|
502
|
+
else {
|
|
503
|
+
console.log();
|
|
504
|
+
printSection('Batch Invite Results');
|
|
505
|
+
// Summary
|
|
506
|
+
printBox([
|
|
507
|
+
`Total: ${result.summary.total}`,
|
|
508
|
+
`Successful: ${brand.success(result.summary.successful.toString())}`,
|
|
509
|
+
`Failed: ${result.summary.failed > 0 ? brand.error(result.summary.failed.toString()) : result.summary.failed}`,
|
|
510
|
+
]);
|
|
511
|
+
// Show failures if any
|
|
512
|
+
if (result.failed.length > 0) {
|
|
513
|
+
console.log();
|
|
514
|
+
printSection('Failed Invitations');
|
|
515
|
+
for (const failed of result.failed) {
|
|
516
|
+
console.log(` ${brand.error('✗')} ${failed.email}`);
|
|
517
|
+
console.log(` ${brand.muted(failed.error?.message || 'Unknown error')}`);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
// Show successes
|
|
521
|
+
if (result.created.length > 0) {
|
|
522
|
+
console.log();
|
|
523
|
+
printSection('Invitations Sent');
|
|
524
|
+
for (const created of result.created) {
|
|
525
|
+
console.log(` ${brand.success('✓')} ${created.email}`);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
console.log();
|
|
529
|
+
printInfo(`Run "sentron org invites list ${orgId}" to see all pending invitations`);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
catch (error) {
|
|
533
|
+
printLoadingError();
|
|
534
|
+
console.log();
|
|
535
|
+
printError(error instanceof Error ? error.message : 'Failed to create batch invitations');
|
|
536
|
+
process.exit(1);
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
// Cancel invitation
|
|
540
|
+
invitesCmd
|
|
541
|
+
.command('cancel <orgId> <invitationId>')
|
|
542
|
+
.description('Cancel a pending invitation')
|
|
543
|
+
.action(async (orgId, invitationId) => {
|
|
544
|
+
try {
|
|
545
|
+
const token = await getStoredToken();
|
|
546
|
+
if (!token) {
|
|
547
|
+
printError('Not signed in. Run "sentron auth signin" first.');
|
|
548
|
+
process.exit(1);
|
|
549
|
+
}
|
|
550
|
+
printLoading('Cancelling invitation');
|
|
551
|
+
await api.organizations.cancelInvitation(orgId, invitationId);
|
|
552
|
+
printLoadingComplete();
|
|
553
|
+
console.log();
|
|
554
|
+
printSuccess('Invitation cancelled');
|
|
555
|
+
}
|
|
556
|
+
catch (error) {
|
|
557
|
+
printLoadingError();
|
|
558
|
+
console.log();
|
|
559
|
+
printError(error instanceof Error ? error.message : 'Failed to cancel invitation');
|
|
560
|
+
process.exit(1);
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
// Resend invitation
|
|
564
|
+
invitesCmd
|
|
565
|
+
.command('resend <orgId> <invitationId>')
|
|
566
|
+
.description('Resend a pending invitation (resets expiration)')
|
|
567
|
+
.action(async (orgId, invitationId) => {
|
|
568
|
+
try {
|
|
569
|
+
const token = await getStoredToken();
|
|
570
|
+
if (!token) {
|
|
571
|
+
printError('Not signed in. Run "sentron auth signin" first.');
|
|
572
|
+
process.exit(1);
|
|
573
|
+
}
|
|
574
|
+
printLoading('Resending invitation');
|
|
575
|
+
await api.organizations.resendInvitation(orgId, invitationId);
|
|
576
|
+
printLoadingComplete();
|
|
577
|
+
console.log();
|
|
578
|
+
printSuccess('Invitation resent');
|
|
579
|
+
}
|
|
580
|
+
catch (error) {
|
|
581
|
+
printLoadingError();
|
|
582
|
+
console.log();
|
|
583
|
+
printError(error instanceof Error ? error.message : 'Failed to resend invitation');
|
|
584
|
+
process.exit(1);
|
|
585
|
+
}
|
|
586
|
+
});
|
|
587
|
+
// Join requests subcommand
|
|
588
|
+
const requestsCmd = orgCmd
|
|
589
|
+
.command('requests')
|
|
590
|
+
.alias('join-requests')
|
|
591
|
+
.description('Manage join requests');
|
|
592
|
+
// List join requests
|
|
593
|
+
requestsCmd
|
|
594
|
+
.command('list <orgId>')
|
|
595
|
+
.alias('ls')
|
|
596
|
+
.description('List pending join requests')
|
|
597
|
+
.option('--json', 'Output as JSON')
|
|
598
|
+
.action(async (orgId, options) => {
|
|
599
|
+
try {
|
|
600
|
+
const token = await getStoredToken();
|
|
601
|
+
if (!token) {
|
|
602
|
+
printError('Not signed in. Run "sentron auth signin" first.');
|
|
603
|
+
process.exit(1);
|
|
604
|
+
}
|
|
605
|
+
printLoading('Fetching join requests');
|
|
606
|
+
const requests = await api.organizations.listJoinRequests(orgId);
|
|
607
|
+
printLoadingComplete();
|
|
608
|
+
if (options.json) {
|
|
609
|
+
console.log(JSON.stringify(requests, null, 2));
|
|
610
|
+
}
|
|
611
|
+
else {
|
|
612
|
+
console.log();
|
|
613
|
+
printSection('Join Requests');
|
|
614
|
+
if (requests.length === 0) {
|
|
615
|
+
printInfo('No pending join requests');
|
|
616
|
+
}
|
|
617
|
+
else {
|
|
618
|
+
requests.forEach((req) => {
|
|
619
|
+
printBox([
|
|
620
|
+
`Email: ${req.email}`,
|
|
621
|
+
`Status: ${req.status}`,
|
|
622
|
+
req.message ? `Message: ${req.message}` : '',
|
|
623
|
+
`Created: ${new Date(req.createdAt).toLocaleDateString()}`,
|
|
624
|
+
`ID: ${brand.muted(req.id)}`,
|
|
625
|
+
].filter(Boolean));
|
|
626
|
+
console.log();
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
catch (error) {
|
|
632
|
+
printLoadingError();
|
|
633
|
+
console.log();
|
|
634
|
+
printError(error instanceof Error ? error.message : 'Failed to list join requests');
|
|
635
|
+
process.exit(1);
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
// Approve join request
|
|
639
|
+
requestsCmd
|
|
640
|
+
.command('approve <orgId> <requestId>')
|
|
641
|
+
.description('Approve a join request')
|
|
642
|
+
.action(async (orgId, requestId) => {
|
|
643
|
+
try {
|
|
644
|
+
const token = await getStoredToken();
|
|
645
|
+
if (!token) {
|
|
646
|
+
printError('Not signed in. Run "sentron auth signin" first.');
|
|
647
|
+
process.exit(1);
|
|
648
|
+
}
|
|
649
|
+
printLoading('Approving join request');
|
|
650
|
+
await api.organizations.handleJoinRequest(orgId, requestId, 'approve');
|
|
651
|
+
printLoadingComplete();
|
|
652
|
+
console.log();
|
|
653
|
+
printSuccess('Join request approved');
|
|
654
|
+
}
|
|
655
|
+
catch (error) {
|
|
656
|
+
printLoadingError();
|
|
657
|
+
console.log();
|
|
658
|
+
printError(error instanceof Error ? error.message : 'Failed to approve join request');
|
|
659
|
+
process.exit(1);
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
// Reject join request
|
|
663
|
+
requestsCmd
|
|
664
|
+
.command('reject <orgId> <requestId>')
|
|
665
|
+
.description('Reject a join request')
|
|
666
|
+
.action(async (orgId, requestId) => {
|
|
667
|
+
try {
|
|
668
|
+
const token = await getStoredToken();
|
|
669
|
+
if (!token) {
|
|
670
|
+
printError('Not signed in. Run "sentron auth signin" first.');
|
|
671
|
+
process.exit(1);
|
|
672
|
+
}
|
|
673
|
+
printLoading('Rejecting join request');
|
|
674
|
+
await api.organizations.handleJoinRequest(orgId, requestId, 'reject');
|
|
675
|
+
printLoadingComplete();
|
|
676
|
+
console.log();
|
|
677
|
+
printSuccess('Join request rejected');
|
|
678
|
+
}
|
|
679
|
+
catch (error) {
|
|
680
|
+
printLoadingError();
|
|
681
|
+
console.log();
|
|
682
|
+
printError(error instanceof Error ? error.message : 'Failed to reject join request');
|
|
683
|
+
process.exit(1);
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
// Create join request
|
|
687
|
+
requestsCmd
|
|
688
|
+
.command('create <orgId>')
|
|
689
|
+
.description('Request to join an organization')
|
|
690
|
+
.option('-m, --message <message>', 'Optional message with the request')
|
|
691
|
+
.action(async (orgId, options) => {
|
|
692
|
+
try {
|
|
693
|
+
const token = await getStoredToken();
|
|
694
|
+
if (!token) {
|
|
695
|
+
printError('Not signed in. Run "sentron auth signin" first.');
|
|
696
|
+
process.exit(1);
|
|
697
|
+
}
|
|
698
|
+
printLoading('Submitting join request');
|
|
699
|
+
const request = await api.organizations.createJoinRequest(orgId, options.message);
|
|
700
|
+
printLoadingComplete();
|
|
701
|
+
console.log();
|
|
702
|
+
printSection('Join Request Submitted');
|
|
703
|
+
printSuccess(`Status: ${request.status}`);
|
|
704
|
+
printInfo('An admin will review your request');
|
|
705
|
+
}
|
|
706
|
+
catch (error) {
|
|
707
|
+
printLoadingError();
|
|
708
|
+
console.log();
|
|
709
|
+
printError(error instanceof Error ? error.message : 'Failed to create join request');
|
|
710
|
+
process.exit(1);
|
|
711
|
+
}
|
|
712
|
+
});
|
|
713
|
+
// Transfer ownership
|
|
714
|
+
orgCmd
|
|
715
|
+
.command('transfer <orgId> <newOwnerId>')
|
|
716
|
+
.description('Transfer organization ownership to another member')
|
|
717
|
+
.option('-f, --force', 'Skip confirmation')
|
|
718
|
+
.action(async (orgId, newOwnerId, options) => {
|
|
719
|
+
try {
|
|
720
|
+
const token = await getStoredToken();
|
|
721
|
+
if (!token) {
|
|
722
|
+
printError('Not signed in. Run "sentron auth signin" first.');
|
|
723
|
+
process.exit(1);
|
|
724
|
+
}
|
|
725
|
+
if (!options.force) {
|
|
726
|
+
const confirm = await prompt(printPrompt('Type "TRANSFER" to confirm'));
|
|
727
|
+
if (confirm !== 'TRANSFER') {
|
|
728
|
+
printInfo('Cancelled');
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
printLoading('Transferring ownership');
|
|
733
|
+
await api.organizations.transfer(orgId, newOwnerId);
|
|
734
|
+
printLoadingComplete();
|
|
735
|
+
console.log();
|
|
736
|
+
printSuccess('Ownership transferred successfully');
|
|
737
|
+
}
|
|
738
|
+
catch (error) {
|
|
739
|
+
printLoadingError();
|
|
740
|
+
console.log();
|
|
741
|
+
printError(error instanceof Error ? error.message : 'Failed to transfer ownership');
|
|
742
|
+
process.exit(1);
|
|
743
|
+
}
|
|
744
|
+
});
|
|
745
|
+
// Billing subcommand
|
|
746
|
+
const billingCmd = orgCmd
|
|
747
|
+
.command('billing')
|
|
748
|
+
.description('Manage organization billing');
|
|
749
|
+
// Get billing status
|
|
750
|
+
billingCmd
|
|
751
|
+
.command('status <orgId>')
|
|
752
|
+
.alias('get')
|
|
753
|
+
.description('Get billing status')
|
|
754
|
+
.option('--json', 'Output as JSON')
|
|
755
|
+
.action(async (orgId, options) => {
|
|
756
|
+
try {
|
|
757
|
+
const token = await getStoredToken();
|
|
758
|
+
if (!token) {
|
|
759
|
+
printError('Not signed in. Run "sentron auth signin" first.');
|
|
760
|
+
process.exit(1);
|
|
761
|
+
}
|
|
762
|
+
printLoading('Fetching billing status');
|
|
763
|
+
const status = await api.organizations.getBillingStatus(orgId);
|
|
764
|
+
printLoadingComplete();
|
|
765
|
+
if (options.json) {
|
|
766
|
+
console.log(JSON.stringify(status, null, 2));
|
|
767
|
+
}
|
|
768
|
+
else {
|
|
769
|
+
console.log();
|
|
770
|
+
printSection('Billing Status');
|
|
771
|
+
printBox([
|
|
772
|
+
`Status: ${status.billingStatus === 'active' ? brand.success('Active') : status.billingStatus === 'past_due' ? brand.error('Past Due') : status.billingStatus === 'canceled' ? brand.error('Canceled') : brand.muted('No subscription')}`,
|
|
773
|
+
`Payment Method: ${status.hasPaymentMethod ? brand.success('Yes') : brand.muted('No')}`,
|
|
774
|
+
status.stripeCustomerId ? `Stripe Customer: ${brand.muted(status.stripeCustomerId)}` : '',
|
|
775
|
+
].filter(Boolean));
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
catch (error) {
|
|
779
|
+
printLoadingError();
|
|
780
|
+
console.log();
|
|
781
|
+
printError(error instanceof Error ? error.message : 'Failed to get billing status');
|
|
782
|
+
process.exit(1);
|
|
783
|
+
}
|
|
784
|
+
});
|
|
785
|
+
// Add payment method (get checkout URL)
|
|
786
|
+
billingCmd
|
|
787
|
+
.command('add-payment <orgId>')
|
|
788
|
+
.description('Get URL to add payment method')
|
|
789
|
+
.action(async (orgId) => {
|
|
790
|
+
try {
|
|
791
|
+
const token = await getStoredToken();
|
|
792
|
+
if (!token) {
|
|
793
|
+
printError('Not signed in. Run "sentron auth signin" first.');
|
|
794
|
+
process.exit(1);
|
|
795
|
+
}
|
|
796
|
+
printLoading('Creating checkout session');
|
|
797
|
+
const result = await api.organizations.createCheckoutSession(orgId, 'https://sentron.dev/billing/success', 'https://sentron.dev/billing/cancel');
|
|
798
|
+
printLoadingComplete();
|
|
799
|
+
console.log();
|
|
800
|
+
printSection('Add Payment Method');
|
|
801
|
+
printInfo('Open this URL to add a payment method:');
|
|
802
|
+
console.log();
|
|
803
|
+
console.log(` ${brand.accent(result.url)}`);
|
|
804
|
+
console.log();
|
|
805
|
+
}
|
|
806
|
+
catch (error) {
|
|
807
|
+
printLoadingError();
|
|
808
|
+
console.log();
|
|
809
|
+
printError(error instanceof Error ? error.message : 'Failed to create checkout session');
|
|
810
|
+
process.exit(1);
|
|
811
|
+
}
|
|
812
|
+
});
|
|
813
|
+
// Manage billing (get portal URL)
|
|
814
|
+
billingCmd
|
|
815
|
+
.command('portal <orgId>')
|
|
816
|
+
.alias('manage')
|
|
817
|
+
.description('Get URL to billing portal')
|
|
818
|
+
.action(async (orgId) => {
|
|
819
|
+
try {
|
|
820
|
+
const token = await getStoredToken();
|
|
821
|
+
if (!token) {
|
|
822
|
+
printError('Not signed in. Run "sentron auth signin" first.');
|
|
823
|
+
process.exit(1);
|
|
824
|
+
}
|
|
825
|
+
printLoading('Creating portal session');
|
|
826
|
+
const result = await api.organizations.createPortalSession(orgId, 'https://sentron.dev/organizations');
|
|
827
|
+
printLoadingComplete();
|
|
828
|
+
console.log();
|
|
829
|
+
printSection('Billing Portal');
|
|
830
|
+
printInfo('Open this URL to manage billing:');
|
|
831
|
+
console.log();
|
|
832
|
+
console.log(` ${brand.accent(result.url)}`);
|
|
833
|
+
console.log();
|
|
834
|
+
}
|
|
835
|
+
catch (error) {
|
|
836
|
+
printLoadingError();
|
|
837
|
+
console.log();
|
|
838
|
+
printError(error instanceof Error ? error.message : 'Failed to create portal session');
|
|
839
|
+
process.exit(1);
|
|
840
|
+
}
|
|
841
|
+
});
|
|
842
|
+
// Get subscription for app type
|
|
843
|
+
billingCmd
|
|
844
|
+
.command('subscription <orgId> <appType>')
|
|
845
|
+
.alias('sub')
|
|
846
|
+
.description('Get subscription details for an app type')
|
|
847
|
+
.option('--json', 'Output as JSON')
|
|
848
|
+
.action(async (orgId, appType, options) => {
|
|
849
|
+
try {
|
|
850
|
+
const token = await getStoredToken();
|
|
851
|
+
if (!token) {
|
|
852
|
+
printError('Not signed in. Run "sentron auth signin" first.');
|
|
853
|
+
process.exit(1);
|
|
854
|
+
}
|
|
855
|
+
printLoading('Fetching subscription');
|
|
856
|
+
const sub = await api.organizations.getSubscription(orgId, appType);
|
|
857
|
+
printLoadingComplete();
|
|
858
|
+
if (options.json) {
|
|
859
|
+
console.log(JSON.stringify(sub, null, 2));
|
|
860
|
+
}
|
|
861
|
+
else {
|
|
862
|
+
console.log();
|
|
863
|
+
printSection(`${sub.appTypeDisplayName} Subscription`);
|
|
864
|
+
printBox([
|
|
865
|
+
`Tier: ${brand.accent(sub.tierDisplayName)} (${sub.tier})`,
|
|
866
|
+
`Status: ${sub.status === 'active' ? brand.success('Active') : sub.status === 'past_due' ? brand.error('Past Due') : brand.muted(sub.status)}`,
|
|
867
|
+
`Interval: ${sub.billingInterval || 'N/A'}`,
|
|
868
|
+
sub.currentPeriodEnd ? `Renews: ${new Date(sub.currentPeriodEnd).toLocaleDateString()}` : '',
|
|
869
|
+
sub.cancelAtPeriodEnd ? `Cancels: At period end` : '',
|
|
870
|
+
'',
|
|
871
|
+
`App Limit: ${sub.currentUsage.apps} / ${sub.enforcedLimits.apps ?? '∞'}`,
|
|
872
|
+
'',
|
|
873
|
+
`Features: ${sub.features.join(', ')}`,
|
|
874
|
+
].filter(line => line !== ''));
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
catch (error) {
|
|
878
|
+
printLoadingError();
|
|
879
|
+
console.log();
|
|
880
|
+
printError(error instanceof Error ? error.message : 'Failed to get subscription');
|
|
881
|
+
process.exit(1);
|
|
882
|
+
}
|
|
883
|
+
});
|
|
884
|
+
// Subscribe to a tier
|
|
885
|
+
billingCmd
|
|
886
|
+
.command('subscribe <orgId> <appType> <tier>')
|
|
887
|
+
.description('Subscribe to a paid tier (starter, team, platform)')
|
|
888
|
+
.option('-i, --interval <interval>', 'Billing interval (monthly, annual)', 'annual')
|
|
889
|
+
.action(async (orgId, appType, tier, options) => {
|
|
890
|
+
try {
|
|
891
|
+
const token = await getStoredToken();
|
|
892
|
+
if (!token) {
|
|
893
|
+
printError('Not signed in. Run "sentron auth signin" first.');
|
|
894
|
+
process.exit(1);
|
|
895
|
+
}
|
|
896
|
+
const validTiers = ['starter', 'team', 'platform'];
|
|
897
|
+
if (!validTiers.includes(tier)) {
|
|
898
|
+
printError(`Invalid tier. Must be one of: ${validTiers.join(', ')}`);
|
|
899
|
+
process.exit(1);
|
|
900
|
+
}
|
|
901
|
+
const validIntervals = ['monthly', 'annual'];
|
|
902
|
+
if (!validIntervals.includes(options.interval)) {
|
|
903
|
+
printError(`Invalid interval. Must be one of: ${validIntervals.join(', ')}`);
|
|
904
|
+
process.exit(1);
|
|
905
|
+
}
|
|
906
|
+
printLoading('Creating checkout session');
|
|
907
|
+
const requestId = crypto.randomUUID();
|
|
908
|
+
const result = await api.organizations.subscribe(orgId, appType, tier, options.interval, 'https://sentron.dev/billing/success', 'https://sentron.dev/billing/cancel', requestId);
|
|
909
|
+
printLoadingComplete();
|
|
910
|
+
console.log();
|
|
911
|
+
printSection('Subscribe');
|
|
912
|
+
printInfo('Open this URL to complete your subscription:');
|
|
913
|
+
console.log();
|
|
914
|
+
console.log(` ${brand.accent(result.checkoutUrl)}`);
|
|
915
|
+
console.log();
|
|
916
|
+
}
|
|
917
|
+
catch (error) {
|
|
918
|
+
printLoadingError();
|
|
919
|
+
console.log();
|
|
920
|
+
printError(error instanceof Error ? error.message : 'Failed to create checkout session');
|
|
921
|
+
process.exit(1);
|
|
922
|
+
}
|
|
923
|
+
});
|
|
924
|
+
// Preview plan change
|
|
925
|
+
billingCmd
|
|
926
|
+
.command('preview <orgId> <appType> <tier>')
|
|
927
|
+
.description('Preview what happens when you change plans')
|
|
928
|
+
.option('-i, --interval <interval>', 'Billing interval (monthly, annual)', 'annual')
|
|
929
|
+
.option('--json', 'Output as JSON')
|
|
930
|
+
.action(async (orgId, appType, tier, options) => {
|
|
931
|
+
try {
|
|
932
|
+
const token = await getStoredToken();
|
|
933
|
+
if (!token) {
|
|
934
|
+
printError('Not signed in. Run "sentron auth signin" first.');
|
|
935
|
+
process.exit(1);
|
|
936
|
+
}
|
|
937
|
+
const validTiers = ['starter', 'team', 'platform'];
|
|
938
|
+
if (!validTiers.includes(tier)) {
|
|
939
|
+
printError(`Invalid tier. Must be one of: ${validTiers.join(', ')}`);
|
|
940
|
+
process.exit(1);
|
|
941
|
+
}
|
|
942
|
+
printLoading('Calculating preview');
|
|
943
|
+
const preview = await api.organizations.previewPlanChange(orgId, appType, tier, options.interval);
|
|
944
|
+
printLoadingComplete();
|
|
945
|
+
if (options.json) {
|
|
946
|
+
console.log(JSON.stringify(preview, null, 2));
|
|
947
|
+
}
|
|
948
|
+
else {
|
|
949
|
+
console.log();
|
|
950
|
+
printSection('Plan Change Preview');
|
|
951
|
+
const formatPrice = (cents) => `$${(cents / 100).toFixed(2)}`;
|
|
952
|
+
const direction = preview.isUpgrade ? brand.success('↑ UPGRADE') : brand.error('↓ DOWNGRADE');
|
|
953
|
+
printBox([
|
|
954
|
+
`${preview.currentTier} → ${preview.newTier} ${direction}`,
|
|
955
|
+
``,
|
|
956
|
+
`Current Price: ${formatPrice(preview.currentPrice)} / ${preview.currentInterval || 'month'}`,
|
|
957
|
+
`New Price: ${formatPrice(preview.newPrice)} / ${preview.newInterval}`,
|
|
958
|
+
``,
|
|
959
|
+
preview.proratedAmount > 0
|
|
960
|
+
? `Amount Due Now: ${brand.accent(formatPrice(preview.proratedAmount))}`
|
|
961
|
+
: preview.proratedAmount < 0
|
|
962
|
+
? `Credit Applied: ${brand.success(formatPrice(Math.abs(preview.proratedAmount)))}`
|
|
963
|
+
: `Amount Due Now: ${brand.muted('$0.00')}`,
|
|
964
|
+
``,
|
|
965
|
+
`Effective: ${new Date(preview.effectiveDate).toLocaleDateString()}`,
|
|
966
|
+
`Next Billing: ${new Date(preview.nextBillingDate).toLocaleDateString()}`,
|
|
967
|
+
]);
|
|
968
|
+
console.log();
|
|
969
|
+
printInfo('To apply this change, run:');
|
|
970
|
+
console.log(` sentron org billing change ${orgId} ${appType} ${tier} -i ${options.interval}`);
|
|
971
|
+
console.log();
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
catch (error) {
|
|
975
|
+
printLoadingError();
|
|
976
|
+
console.log();
|
|
977
|
+
printError(error instanceof Error ? error.message : 'Failed to preview plan change');
|
|
978
|
+
process.exit(1);
|
|
979
|
+
}
|
|
980
|
+
});
|
|
981
|
+
// Confirm plan change
|
|
982
|
+
billingCmd
|
|
983
|
+
.command('change <orgId> <appType> <tier>')
|
|
984
|
+
.description('Change your plan (upgrade or downgrade)')
|
|
985
|
+
.option('-i, --interval <interval>', 'Billing interval (monthly, annual)', 'annual')
|
|
986
|
+
.option('-f, --force', 'Skip confirmation')
|
|
987
|
+
.action(async (orgId, appType, tier, options) => {
|
|
988
|
+
try {
|
|
989
|
+
const token = await getStoredToken();
|
|
990
|
+
if (!token) {
|
|
991
|
+
printError('Not signed in. Run "sentron auth signin" first.');
|
|
992
|
+
process.exit(1);
|
|
993
|
+
}
|
|
994
|
+
const validTiers = ['starter', 'team', 'platform'];
|
|
995
|
+
if (!validTiers.includes(tier)) {
|
|
996
|
+
printError(`Invalid tier. Must be one of: ${validTiers.join(', ')}`);
|
|
997
|
+
process.exit(1);
|
|
998
|
+
}
|
|
999
|
+
// Show preview first
|
|
1000
|
+
printLoading('Calculating preview');
|
|
1001
|
+
const preview = await api.organizations.previewPlanChange(orgId, appType, tier, options.interval);
|
|
1002
|
+
printLoadingComplete();
|
|
1003
|
+
console.log();
|
|
1004
|
+
const formatPrice = (cents) => `$${(cents / 100).toFixed(2)}`;
|
|
1005
|
+
printSection('Plan Change');
|
|
1006
|
+
printInfo(`${preview.currentTier} → ${preview.newTier}`);
|
|
1007
|
+
if (preview.proratedAmount > 0) {
|
|
1008
|
+
printInfo(`You will be charged ${brand.accent(formatPrice(preview.proratedAmount))} now`);
|
|
1009
|
+
}
|
|
1010
|
+
else if (preview.proratedAmount < 0) {
|
|
1011
|
+
printInfo(`You will receive a ${brand.success(formatPrice(Math.abs(preview.proratedAmount)))} credit`);
|
|
1012
|
+
}
|
|
1013
|
+
console.log();
|
|
1014
|
+
if (!options.force) {
|
|
1015
|
+
const confirm = await prompt(printPrompt('Type "CONFIRM" to proceed'));
|
|
1016
|
+
if (confirm !== 'CONFIRM') {
|
|
1017
|
+
printInfo('Cancelled');
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
printLoading('Changing plan');
|
|
1022
|
+
const requestId = crypto.randomUUID();
|
|
1023
|
+
const result = await api.organizations.confirmPlanChange(orgId, appType, tier, options.interval, requestId);
|
|
1024
|
+
printLoadingComplete();
|
|
1025
|
+
console.log();
|
|
1026
|
+
printSuccess('Plan changed successfully!');
|
|
1027
|
+
printBox([
|
|
1028
|
+
`New Tier: ${brand.accent(result.subscription.tier)}`,
|
|
1029
|
+
`Status: ${result.subscription.status}`,
|
|
1030
|
+
`Interval: ${result.subscription.billingInterval}`,
|
|
1031
|
+
result.chargedAmount > 0 ? `Charged: ${formatPrice(result.chargedAmount)}` : '',
|
|
1032
|
+
result.creditAmount > 0 ? `Credit: ${formatPrice(result.creditAmount)}` : '',
|
|
1033
|
+
].filter(Boolean));
|
|
1034
|
+
}
|
|
1035
|
+
catch (error) {
|
|
1036
|
+
printLoadingError();
|
|
1037
|
+
console.log();
|
|
1038
|
+
printError(error instanceof Error ? error.message : 'Failed to change plan');
|
|
1039
|
+
process.exit(1);
|
|
1040
|
+
}
|
|
1041
|
+
});
|
|
1042
|
+
// Cancel subscription
|
|
1043
|
+
billingCmd
|
|
1044
|
+
.command('cancel <orgId> <appType>')
|
|
1045
|
+
.description('Cancel a subscription (keeps access until end of billing period)')
|
|
1046
|
+
.option('--immediate', 'Cancel immediately with prorated refund')
|
|
1047
|
+
.option('--force', 'Skip confirmation prompt')
|
|
1048
|
+
.action(async (orgId, appType, options) => {
|
|
1049
|
+
try {
|
|
1050
|
+
const token = await getStoredToken();
|
|
1051
|
+
if (!token) {
|
|
1052
|
+
printError('Not signed in. Run "sentron auth signin" first.');
|
|
1053
|
+
process.exit(1);
|
|
1054
|
+
}
|
|
1055
|
+
// Get current subscription first
|
|
1056
|
+
printLoading('Fetching subscription');
|
|
1057
|
+
const sub = await api.organizations.getSubscription(orgId, appType);
|
|
1058
|
+
printLoadingComplete();
|
|
1059
|
+
if (sub.tier === 'free') {
|
|
1060
|
+
printError('Already on free tier');
|
|
1061
|
+
process.exit(1);
|
|
1062
|
+
}
|
|
1063
|
+
console.log();
|
|
1064
|
+
printSection('Cancel Subscription');
|
|
1065
|
+
printInfo(`Current tier: ${brand.accent(sub.tier)}`);
|
|
1066
|
+
if (options.immediate) {
|
|
1067
|
+
printInfo('Mode: Immediate cancellation with prorated refund');
|
|
1068
|
+
}
|
|
1069
|
+
else {
|
|
1070
|
+
printInfo(`Mode: Cancel at end of period (${sub.currentPeriodEnd ? new Date(sub.currentPeriodEnd).toLocaleDateString() : 'end of period'})`);
|
|
1071
|
+
}
|
|
1072
|
+
console.log();
|
|
1073
|
+
if (!options.force) {
|
|
1074
|
+
const confirm = await prompt(printPrompt('Type "CANCEL" to confirm'));
|
|
1075
|
+
if (confirm !== 'CANCEL') {
|
|
1076
|
+
printInfo('Cancelled');
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
printLoading('Cancelling subscription');
|
|
1081
|
+
const result = await api.organizations.cancelSubscription(orgId, appType, options.immediate || false);
|
|
1082
|
+
printLoadingComplete();
|
|
1083
|
+
console.log();
|
|
1084
|
+
if (options.immediate) {
|
|
1085
|
+
printSuccess('Subscription cancelled immediately');
|
|
1086
|
+
printInfo('Prorated refund will be applied to your account');
|
|
1087
|
+
}
|
|
1088
|
+
else {
|
|
1089
|
+
printSuccess('Subscription set to cancel');
|
|
1090
|
+
printInfo(`You will keep access until ${new Date(result.effectiveDate).toLocaleDateString()}`);
|
|
1091
|
+
printInfo('You can reactivate anytime before that date');
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
catch (error) {
|
|
1095
|
+
printLoadingError();
|
|
1096
|
+
console.log();
|
|
1097
|
+
printError(error instanceof Error ? error.message : 'Failed to cancel subscription');
|
|
1098
|
+
process.exit(1);
|
|
1099
|
+
}
|
|
1100
|
+
});
|
|
1101
|
+
// Reactivate subscription
|
|
1102
|
+
billingCmd
|
|
1103
|
+
.command('reactivate <orgId> <appType>')
|
|
1104
|
+
.description('Reactivate a subscription that was set to cancel')
|
|
1105
|
+
.action(async (orgId, appType) => {
|
|
1106
|
+
try {
|
|
1107
|
+
const token = await getStoredToken();
|
|
1108
|
+
if (!token) {
|
|
1109
|
+
printError('Not signed in. Run "sentron auth signin" first.');
|
|
1110
|
+
process.exit(1);
|
|
1111
|
+
}
|
|
1112
|
+
printLoading('Reactivating subscription');
|
|
1113
|
+
await api.organizations.reactivateSubscription(orgId, appType);
|
|
1114
|
+
printLoadingComplete();
|
|
1115
|
+
console.log();
|
|
1116
|
+
printSuccess('Subscription reactivated!');
|
|
1117
|
+
printInfo('Pending cancellation has been removed');
|
|
1118
|
+
}
|
|
1119
|
+
catch (error) {
|
|
1120
|
+
printLoadingError();
|
|
1121
|
+
console.log();
|
|
1122
|
+
printError(error instanceof Error ? error.message : 'Failed to reactivate subscription');
|
|
1123
|
+
process.exit(1);
|
|
1124
|
+
}
|
|
1125
|
+
});
|
|
1126
|
+
// Check if can create app
|
|
1127
|
+
billingCmd
|
|
1128
|
+
.command('can-create-app <orgId> <appType>')
|
|
1129
|
+
.description('Check if you can create a new app (tier limits)')
|
|
1130
|
+
.option('--json', 'Output as JSON')
|
|
1131
|
+
.action(async (orgId, appType, options) => {
|
|
1132
|
+
try {
|
|
1133
|
+
const token = await getStoredToken();
|
|
1134
|
+
if (!token) {
|
|
1135
|
+
printError('Not signed in. Run "sentron auth signin" first.');
|
|
1136
|
+
process.exit(1);
|
|
1137
|
+
}
|
|
1138
|
+
printLoading('Checking limits');
|
|
1139
|
+
const result = await api.organizations.canCreateApp(orgId, appType);
|
|
1140
|
+
printLoadingComplete();
|
|
1141
|
+
if (options.json) {
|
|
1142
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1143
|
+
}
|
|
1144
|
+
else {
|
|
1145
|
+
console.log();
|
|
1146
|
+
if (result.allowed) {
|
|
1147
|
+
printSuccess('✓ You can create a new app');
|
|
1148
|
+
}
|
|
1149
|
+
else {
|
|
1150
|
+
printError('✗ App limit reached');
|
|
1151
|
+
printInfo(`Current: ${result.current} / ${result.limit}`);
|
|
1152
|
+
printInfo(`Reason: ${result.reason}`);
|
|
1153
|
+
if (result.upgradeUrl) {
|
|
1154
|
+
printInfo(`Upgrade: ${result.upgradeUrl}`);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
catch (error) {
|
|
1160
|
+
printLoadingError();
|
|
1161
|
+
console.log();
|
|
1162
|
+
printError(error instanceof Error ? error.message : 'Failed to check limits');
|
|
1163
|
+
process.exit(1);
|
|
1164
|
+
}
|
|
1165
|
+
});
|
|
1166
|
+
// Default billing action - show status
|
|
1167
|
+
billingCmd
|
|
1168
|
+
.action(async () => {
|
|
1169
|
+
console.log();
|
|
1170
|
+
printSection('Billing Commands');
|
|
1171
|
+
printInfo('Usage: sentron org billing <command> <orgId> [options]');
|
|
1172
|
+
console.log();
|
|
1173
|
+
printInfo('General:');
|
|
1174
|
+
printInfo(' status <orgId> Get billing status');
|
|
1175
|
+
printInfo(' add-payment <orgId> Get URL to add payment method');
|
|
1176
|
+
printInfo(' portal <orgId> Get URL to billing portal');
|
|
1177
|
+
console.log();
|
|
1178
|
+
printInfo('Subscriptions (per app type):');
|
|
1179
|
+
printInfo(' subscription <orgId> <appType> Get subscription details');
|
|
1180
|
+
printInfo(' subscribe <orgId> <appType> <tier> Subscribe to a tier');
|
|
1181
|
+
printInfo(' preview <orgId> <appType> <tier> Preview plan change');
|
|
1182
|
+
printInfo(' change <orgId> <appType> <tier> Change plan (upgrade/downgrade)');
|
|
1183
|
+
console.log();
|
|
1184
|
+
printInfo('Limits:');
|
|
1185
|
+
printInfo(' can-create-app <orgId> <appType> Check app creation limit');
|
|
1186
|
+
console.log();
|
|
1187
|
+
printInfo('Tiers: free, starter, team, platform');
|
|
1188
|
+
printInfo('App Types: data_studio');
|
|
1189
|
+
console.log();
|
|
1190
|
+
});
|
|
1191
|
+
// Logo subcommand
|
|
1192
|
+
const logoCmd = orgCmd
|
|
1193
|
+
.command('logo')
|
|
1194
|
+
.description('Manage organization logo');
|
|
1195
|
+
// Upload logo
|
|
1196
|
+
logoCmd
|
|
1197
|
+
.command('upload <orgId> <filepath>')
|
|
1198
|
+
.description('Upload a logo for the organization')
|
|
1199
|
+
.action(async (orgId, filepath) => {
|
|
1200
|
+
try {
|
|
1201
|
+
const token = await getStoredToken();
|
|
1202
|
+
if (!token) {
|
|
1203
|
+
printError('Not signed in. Run "sentron auth signin" first.');
|
|
1204
|
+
process.exit(1);
|
|
1205
|
+
}
|
|
1206
|
+
// Resolve and validate file path
|
|
1207
|
+
const absolutePath = path.resolve(filepath);
|
|
1208
|
+
if (!fs.existsSync(absolutePath)) {
|
|
1209
|
+
printError(`File not found: ${filepath}`);
|
|
1210
|
+
process.exit(1);
|
|
1211
|
+
}
|
|
1212
|
+
// Check file size (2MB max)
|
|
1213
|
+
const stats = fs.statSync(absolutePath);
|
|
1214
|
+
if (stats.size > 2 * 1024 * 1024) {
|
|
1215
|
+
printError('File too large. Maximum size is 2MB');
|
|
1216
|
+
process.exit(1);
|
|
1217
|
+
}
|
|
1218
|
+
// Check file extension
|
|
1219
|
+
const ext = path.extname(filepath).toLowerCase();
|
|
1220
|
+
const validExts = ['.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp'];
|
|
1221
|
+
if (!validExts.includes(ext)) {
|
|
1222
|
+
printError(`Invalid file type. Supported: ${validExts.join(', ')}`);
|
|
1223
|
+
process.exit(1);
|
|
1224
|
+
}
|
|
1225
|
+
printLoading('Uploading logo');
|
|
1226
|
+
const result = await api.organizations.uploadLogo(orgId, absolutePath);
|
|
1227
|
+
printLoadingComplete();
|
|
1228
|
+
console.log();
|
|
1229
|
+
printSuccess('Logo uploaded successfully');
|
|
1230
|
+
if (result.logoUrl) {
|
|
1231
|
+
printInfo(`Logo URL: ${brand.muted(result.logoUrl.substring(0, 50) + '...')}`);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
catch (error) {
|
|
1235
|
+
printLoadingError();
|
|
1236
|
+
console.log();
|
|
1237
|
+
printError(error instanceof Error ? error.message : 'Failed to upload logo');
|
|
1238
|
+
process.exit(1);
|
|
1239
|
+
}
|
|
1240
|
+
});
|
|
1241
|
+
// Remove logo
|
|
1242
|
+
logoCmd
|
|
1243
|
+
.command('remove <orgId>')
|
|
1244
|
+
.alias('delete')
|
|
1245
|
+
.description('Remove the organization logo')
|
|
1246
|
+
.action(async (orgId) => {
|
|
1247
|
+
try {
|
|
1248
|
+
const token = await getStoredToken();
|
|
1249
|
+
if (!token) {
|
|
1250
|
+
printError('Not signed in. Run "sentron auth signin" first.');
|
|
1251
|
+
process.exit(1);
|
|
1252
|
+
}
|
|
1253
|
+
printLoading('Removing logo');
|
|
1254
|
+
await api.organizations.removeLogo(orgId);
|
|
1255
|
+
printLoadingComplete();
|
|
1256
|
+
console.log();
|
|
1257
|
+
printSuccess('Logo removed successfully');
|
|
1258
|
+
}
|
|
1259
|
+
catch (error) {
|
|
1260
|
+
printLoadingError();
|
|
1261
|
+
console.log();
|
|
1262
|
+
printError(error instanceof Error ? error.message : 'Failed to remove logo');
|
|
1263
|
+
process.exit(1);
|
|
1264
|
+
}
|
|
1265
|
+
});
|
|
1266
|
+
// Default logo action
|
|
1267
|
+
logoCmd
|
|
1268
|
+
.action(() => {
|
|
1269
|
+
console.log();
|
|
1270
|
+
printSection('Logo Commands');
|
|
1271
|
+
printInfo('Usage: sentron org logo <command> <orgId>');
|
|
1272
|
+
console.log();
|
|
1273
|
+
printInfo('Commands:');
|
|
1274
|
+
printInfo(' upload <orgId> <filepath> Upload a logo image');
|
|
1275
|
+
printInfo(' remove <orgId> Remove the logo');
|
|
1276
|
+
console.log();
|
|
1277
|
+
});
|
|
1278
|
+
// Default action - list organizations
|
|
1279
|
+
orgCmd
|
|
1280
|
+
.action(async () => {
|
|
1281
|
+
try {
|
|
1282
|
+
const token = await getStoredToken();
|
|
1283
|
+
if (!token) {
|
|
1284
|
+
printError('Not signed in. Run "sentron auth signin" first.');
|
|
1285
|
+
process.exit(1);
|
|
1286
|
+
}
|
|
1287
|
+
printLoading('Fetching organizations');
|
|
1288
|
+
const orgs = await api.organizations.list();
|
|
1289
|
+
printLoadingComplete();
|
|
1290
|
+
console.log();
|
|
1291
|
+
printSection('Your Organizations');
|
|
1292
|
+
if (orgs.length === 0) {
|
|
1293
|
+
printInfo('No organizations found. Create one with "sentron org create"');
|
|
1294
|
+
}
|
|
1295
|
+
else {
|
|
1296
|
+
orgs.forEach((org) => {
|
|
1297
|
+
printBox([
|
|
1298
|
+
`Name: ${brand.accent(org.name)}`,
|
|
1299
|
+
`Slug: ${org.slug}`,
|
|
1300
|
+
`Role: ${org.role}`,
|
|
1301
|
+
`Members: ${org.memberCount}`,
|
|
1302
|
+
`Apps: ${org.appCount}`,
|
|
1303
|
+
]);
|
|
1304
|
+
console.log();
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1307
|
+
console.log();
|
|
1308
|
+
printInfo('Run "sentron org --help" for more commands');
|
|
1309
|
+
}
|
|
1310
|
+
catch (error) {
|
|
1311
|
+
printLoadingError();
|
|
1312
|
+
console.log();
|
|
1313
|
+
printError(error instanceof Error ? error.message : 'Failed to list organizations');
|
|
1314
|
+
process.exit(1);
|
|
1315
|
+
}
|
|
1316
|
+
});
|
|
1317
|
+
// ============================================================================
|
|
1318
|
+
// AUDIT LOGS
|
|
1319
|
+
// ============================================================================
|
|
1320
|
+
const auditCmd = orgCmd
|
|
1321
|
+
.command('audit-logs')
|
|
1322
|
+
.alias('activity')
|
|
1323
|
+
.description('View organization activity logs');
|
|
1324
|
+
auditCmd
|
|
1325
|
+
.command('list <orgId>')
|
|
1326
|
+
.alias('ls')
|
|
1327
|
+
.description('List audit logs for an organization')
|
|
1328
|
+
.option('--limit <number>', 'Number of logs to return (default 50, max 100)', '50')
|
|
1329
|
+
.option('--offset <number>', 'Offset for pagination', '0')
|
|
1330
|
+
.option('--action <action>', 'Filter by action type (e.g., org.member.added)')
|
|
1331
|
+
.option('--user <userId>', 'Filter by actor user ID')
|
|
1332
|
+
.option('--from <date>', 'Filter from date (ISO 8601 format)')
|
|
1333
|
+
.option('--to <date>', 'Filter to date (ISO 8601 format)')
|
|
1334
|
+
.option('--json', 'Output as JSON')
|
|
1335
|
+
.action(async (orgId, options) => {
|
|
1336
|
+
try {
|
|
1337
|
+
const token = await getStoredToken();
|
|
1338
|
+
if (!token) {
|
|
1339
|
+
printError('Not signed in. Run "sentron auth signin" first.');
|
|
1340
|
+
process.exit(1);
|
|
1341
|
+
}
|
|
1342
|
+
printLoading('Fetching audit logs');
|
|
1343
|
+
const logs = await api.organizations.listAuditLogs(orgId, {
|
|
1344
|
+
limit: parseInt(options.limit, 10),
|
|
1345
|
+
offset: parseInt(options.offset, 10),
|
|
1346
|
+
action: options.action,
|
|
1347
|
+
userId: options.user,
|
|
1348
|
+
startDate: options.from,
|
|
1349
|
+
endDate: options.to,
|
|
1350
|
+
});
|
|
1351
|
+
printLoadingComplete();
|
|
1352
|
+
if (options.json) {
|
|
1353
|
+
console.log(JSON.stringify(logs, null, 2));
|
|
1354
|
+
return;
|
|
1355
|
+
}
|
|
1356
|
+
console.log();
|
|
1357
|
+
printSection('Audit Logs');
|
|
1358
|
+
if (logs.length === 0) {
|
|
1359
|
+
printInfo('No audit logs found matching your criteria.');
|
|
1360
|
+
}
|
|
1361
|
+
else {
|
|
1362
|
+
logs.forEach((log) => {
|
|
1363
|
+
const date = new Date(log.createdAt).toLocaleString();
|
|
1364
|
+
const action = formatAuditAction(log.action);
|
|
1365
|
+
const resource = log.resourceType + (log.resourceId ? ` (${log.resourceId.substring(0, 8)}...)` : '');
|
|
1366
|
+
printBox([
|
|
1367
|
+
`Action: ${brand.accent(action)}`,
|
|
1368
|
+
`Resource: ${resource}`,
|
|
1369
|
+
`Actor: ${log.userId.substring(0, 12)}...`,
|
|
1370
|
+
`Date: ${date}`,
|
|
1371
|
+
log.metadata ? `Details: ${JSON.stringify(log.metadata)}` : null,
|
|
1372
|
+
].filter(Boolean));
|
|
1373
|
+
console.log();
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
catch (error) {
|
|
1378
|
+
printLoadingError();
|
|
1379
|
+
console.log();
|
|
1380
|
+
if (error.action) {
|
|
1381
|
+
printErrorWithAction(error.message, error.action);
|
|
1382
|
+
}
|
|
1383
|
+
else {
|
|
1384
|
+
printError(error.message || 'Failed to fetch audit logs');
|
|
1385
|
+
}
|
|
1386
|
+
process.exit(1);
|
|
1387
|
+
}
|
|
1388
|
+
});
|
|
1389
|
+
}
|
|
1390
|
+
/**
|
|
1391
|
+
* Format audit action for display
|
|
1392
|
+
*/
|
|
1393
|
+
function formatAuditAction(action) {
|
|
1394
|
+
// Convert action like "org.member.added" to "Member Added"
|
|
1395
|
+
const parts = action.split('.');
|
|
1396
|
+
if (parts.length >= 2) {
|
|
1397
|
+
const subject = parts[parts.length - 2];
|
|
1398
|
+
const verb = parts[parts.length - 1];
|
|
1399
|
+
return `${capitalize(subject)} ${capitalize(verb)}`;
|
|
1400
|
+
}
|
|
1401
|
+
return action;
|
|
1402
|
+
}
|
|
1403
|
+
function capitalize(str) {
|
|
1404
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
1405
|
+
}
|
|
1406
|
+
//# sourceMappingURL=org.js.map
|