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.
@@ -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