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,874 @@
1
+ /**
2
+ * App Commands
3
+ */
4
+ import * as readline from 'readline';
5
+ import { api } from '../api.js';
6
+ import { getStoredToken } from '../utils/storage.js';
7
+ import { printSection, printSuccess, printError, printInfo, printBox, printLoading, printLoadingComplete, printLoadingError, printPrompt, brand, } from '../utils/styling.js';
8
+ /**
9
+ * Prompt for input
10
+ */
11
+ function prompt(question) {
12
+ const rl = readline.createInterface({
13
+ input: process.stdin,
14
+ output: process.stdout,
15
+ });
16
+ return new Promise((resolve) => {
17
+ rl.question(question, (answer) => {
18
+ rl.close();
19
+ resolve(answer.trim());
20
+ });
21
+ });
22
+ }
23
+ export function setupAppCommands(program) {
24
+ const appCmd = program
25
+ .command('app')
26
+ .description('Manage apps');
27
+ // List apps in an organization
28
+ appCmd
29
+ .command('list <orgId>')
30
+ .alias('ls')
31
+ .description('List apps in an organization')
32
+ .option('--json', 'Output as JSON')
33
+ .action(async (orgId, options) => {
34
+ try {
35
+ const token = await getStoredToken();
36
+ if (!token) {
37
+ printError('Not signed in. Run "sentron auth signin" first.');
38
+ process.exit(1);
39
+ }
40
+ printLoading('Fetching apps');
41
+ const apps = await api.apps.list(orgId);
42
+ printLoadingComplete();
43
+ if (options.json) {
44
+ console.log(JSON.stringify(apps, null, 2));
45
+ }
46
+ else {
47
+ console.log();
48
+ printSection('Apps');
49
+ if (apps.length === 0) {
50
+ printInfo('No apps found. Create one with "sentron app create <orgId>"');
51
+ }
52
+ else {
53
+ apps.forEach((app) => {
54
+ printBox([
55
+ `Name: ${brand.accent(app.name)}`,
56
+ `Slug: ${app.slug}`,
57
+ `Members: ${app.memberCount}`,
58
+ app.description ? `Desc: ${app.description}` : '',
59
+ `Created: ${new Date(app.createdAt).toLocaleDateString()}`,
60
+ `ID: ${brand.muted(app.id)}`,
61
+ ].filter(Boolean));
62
+ console.log();
63
+ });
64
+ }
65
+ }
66
+ }
67
+ catch (error) {
68
+ printLoadingError();
69
+ console.log();
70
+ printError(error instanceof Error ? error.message : 'Failed to list apps');
71
+ process.exit(1);
72
+ }
73
+ });
74
+ // Create app
75
+ appCmd
76
+ .command('create <orgId>')
77
+ .description('Create a new app (slug is auto-generated from name based on app type)')
78
+ .option('-n, --name <name>', 'App name')
79
+ .option('-d, --description <description>', 'App description')
80
+ .action(async (orgId, options) => {
81
+ try {
82
+ const token = await getStoredToken();
83
+ if (!token) {
84
+ printError('Not signed in. Run "sentron auth signin" first.');
85
+ process.exit(1);
86
+ }
87
+ const name = options.name || await prompt(printPrompt('App Name'));
88
+ if (!name) {
89
+ printError('Name is required');
90
+ process.exit(1);
91
+ }
92
+ printLoading('Creating app');
93
+ const app = await api.apps.create(orgId, {
94
+ name,
95
+ description: options.description,
96
+ });
97
+ printLoadingComplete();
98
+ console.log();
99
+ printSection('App Created');
100
+ printBox([
101
+ `Name: ${brand.accent(app.name)}`,
102
+ `Slug: ${app.slug}`,
103
+ `Type: ${app.appType}`,
104
+ `ID: ${brand.muted(app.id)}`,
105
+ ]);
106
+ }
107
+ catch (error) {
108
+ printLoadingError();
109
+ console.log();
110
+ printError(error instanceof Error ? error.message : 'Failed to create app');
111
+ process.exit(1);
112
+ }
113
+ });
114
+ // Get app details
115
+ appCmd
116
+ .command('get <appId>')
117
+ .alias('show')
118
+ .description('Get app details')
119
+ .option('--json', 'Output as JSON')
120
+ .action(async (appId, options) => {
121
+ try {
122
+ const token = await getStoredToken();
123
+ if (!token) {
124
+ printError('Not signed in. Run "sentron auth signin" first.');
125
+ process.exit(1);
126
+ }
127
+ printLoading('Fetching app');
128
+ const app = await api.apps.get(appId);
129
+ printLoadingComplete();
130
+ if (options.json) {
131
+ console.log(JSON.stringify(app, null, 2));
132
+ }
133
+ else {
134
+ console.log();
135
+ printSection('App Details');
136
+ printBox([
137
+ `Name: ${brand.accent(app.name)}`,
138
+ `Slug: ${app.slug}`,
139
+ app.description ? `Desc: ${app.description}` : '',
140
+ `Members: ${app.memberCount}`,
141
+ `Created: ${new Date(app.createdAt).toLocaleDateString()}`,
142
+ `Org ID: ${brand.muted(app.organizationId)}`,
143
+ `ID: ${brand.muted(app.id)}`,
144
+ ].filter(Boolean));
145
+ }
146
+ }
147
+ catch (error) {
148
+ printLoadingError();
149
+ console.log();
150
+ printError(error instanceof Error ? error.message : 'Failed to get app');
151
+ process.exit(1);
152
+ }
153
+ });
154
+ // Update app
155
+ appCmd
156
+ .command('update <appId>')
157
+ .description('Update app details')
158
+ .option('-n, --name <name>', 'New name')
159
+ .option('-d, --description <description>', 'New description')
160
+ .action(async (appId, options) => {
161
+ try {
162
+ const token = await getStoredToken();
163
+ if (!token) {
164
+ printError('Not signed in. Run "sentron auth signin" first.');
165
+ process.exit(1);
166
+ }
167
+ if (!options.name && !options.description) {
168
+ printError('At least one field to update is required');
169
+ process.exit(1);
170
+ }
171
+ printLoading('Updating app');
172
+ const app = await api.apps.update(appId, {
173
+ name: options.name,
174
+ description: options.description,
175
+ });
176
+ printLoadingComplete();
177
+ console.log();
178
+ printSection('App Updated');
179
+ printSuccess(`Updated: ${app.name}`);
180
+ }
181
+ catch (error) {
182
+ printLoadingError();
183
+ console.log();
184
+ printError(error instanceof Error ? error.message : 'Failed to update app');
185
+ process.exit(1);
186
+ }
187
+ });
188
+ // Delete app
189
+ appCmd
190
+ .command('delete <appId>')
191
+ .description('Delete an app')
192
+ .option('-f, --force', 'Skip confirmation')
193
+ .action(async (appId, options) => {
194
+ try {
195
+ const token = await getStoredToken();
196
+ if (!token) {
197
+ printError('Not signed in. Run "sentron auth signin" first.');
198
+ process.exit(1);
199
+ }
200
+ if (!options.force) {
201
+ const confirm = await prompt(printPrompt('Type "DELETE" to confirm'));
202
+ if (confirm !== 'DELETE') {
203
+ printInfo('Cancelled');
204
+ return;
205
+ }
206
+ }
207
+ printLoading('Deleting app');
208
+ await api.apps.delete(appId);
209
+ printLoadingComplete();
210
+ console.log();
211
+ printSuccess('App deleted successfully');
212
+ }
213
+ catch (error) {
214
+ printLoadingError();
215
+ console.log();
216
+ printError(error instanceof Error ? error.message : 'Failed to delete app');
217
+ process.exit(1);
218
+ }
219
+ });
220
+ // Members subcommand
221
+ const membersCmd = appCmd
222
+ .command('members')
223
+ .description('Manage app members');
224
+ // List members
225
+ membersCmd
226
+ .command('list <appId>')
227
+ .alias('ls')
228
+ .description('List app members')
229
+ .option('--json', 'Output as JSON')
230
+ .action(async (appId, options) => {
231
+ try {
232
+ const token = await getStoredToken();
233
+ if (!token) {
234
+ printError('Not signed in. Run "sentron auth signin" first.');
235
+ process.exit(1);
236
+ }
237
+ printLoading('Fetching members');
238
+ const members = await api.apps.listMembers(appId);
239
+ printLoadingComplete();
240
+ if (options.json) {
241
+ console.log(JSON.stringify(members, null, 2));
242
+ }
243
+ else {
244
+ console.log();
245
+ printSection('App Members');
246
+ if (members.length === 0) {
247
+ printInfo('No members found');
248
+ }
249
+ else {
250
+ members.forEach((member) => {
251
+ const name = [member.firstName, member.lastName].filter(Boolean).join(' ') || 'N/A';
252
+ printBox([
253
+ `Email: ${member.email}`,
254
+ `Name: ${name}`,
255
+ `Role: ${brand.accent(member.role)}`,
256
+ `Joined: ${new Date(member.joinedAt).toLocaleDateString()}`,
257
+ `ID: ${brand.muted(member.userId)}`,
258
+ ]);
259
+ console.log();
260
+ });
261
+ }
262
+ }
263
+ }
264
+ catch (error) {
265
+ printLoadingError();
266
+ console.log();
267
+ printError(error instanceof Error ? error.message : 'Failed to list members');
268
+ process.exit(1);
269
+ }
270
+ });
271
+ // Add member
272
+ membersCmd
273
+ .command('add <appId> <userId>')
274
+ .description('Add a member to the app')
275
+ .option('-r, --role <role>', 'Role to assign (default: app_member)')
276
+ .action(async (appId, userId, options) => {
277
+ try {
278
+ const token = await getStoredToken();
279
+ if (!token) {
280
+ printError('Not signed in. Run "sentron auth signin" first.');
281
+ process.exit(1);
282
+ }
283
+ printLoading('Adding member');
284
+ await api.apps.addMember(appId, userId, options.role);
285
+ printLoadingComplete();
286
+ console.log();
287
+ printSuccess('Member added to app');
288
+ }
289
+ catch (error) {
290
+ printLoadingError();
291
+ console.log();
292
+ printError(error instanceof Error ? error.message : 'Failed to add member');
293
+ process.exit(1);
294
+ }
295
+ });
296
+ // Update member role
297
+ membersCmd
298
+ .command('update-role <appId> <userId> <role>')
299
+ .description('Update member role (app_admin, app_member, app_viewer)')
300
+ .action(async (appId, userId, role) => {
301
+ try {
302
+ const token = await getStoredToken();
303
+ if (!token) {
304
+ printError('Not signed in. Run "sentron auth signin" first.');
305
+ process.exit(1);
306
+ }
307
+ const validRoles = ['app_admin', 'app_member', 'app_viewer'];
308
+ if (!validRoles.includes(role)) {
309
+ printError(`Invalid role. Must be one of: ${validRoles.join(', ')}`);
310
+ process.exit(1);
311
+ }
312
+ printLoading('Updating member role');
313
+ await api.apps.updateMemberRole(appId, userId, role);
314
+ printLoadingComplete();
315
+ console.log();
316
+ printSuccess(`Member role updated to: ${role}`);
317
+ }
318
+ catch (error) {
319
+ printLoadingError();
320
+ console.log();
321
+ printError(error instanceof Error ? error.message : 'Failed to update member role');
322
+ process.exit(1);
323
+ }
324
+ });
325
+ // Remove member
326
+ membersCmd
327
+ .command('remove <appId> <userId>')
328
+ .description('Remove a member from the app')
329
+ .action(async (appId, userId) => {
330
+ try {
331
+ const token = await getStoredToken();
332
+ if (!token) {
333
+ printError('Not signed in. Run "sentron auth signin" first.');
334
+ process.exit(1);
335
+ }
336
+ printLoading('Removing member');
337
+ await api.apps.removeMember(appId, userId);
338
+ printLoadingComplete();
339
+ console.log();
340
+ printSuccess('Member removed from app');
341
+ }
342
+ catch (error) {
343
+ printLoadingError();
344
+ console.log();
345
+ printError(error instanceof Error ? error.message : 'Failed to remove member');
346
+ process.exit(1);
347
+ }
348
+ });
349
+ // ============================================================================
350
+ // INVITATIONS SUBCOMMAND
351
+ // ============================================================================
352
+ const invitesCmd = appCmd
353
+ .command('invites')
354
+ .description('Manage app invitations');
355
+ // List invitations
356
+ invitesCmd
357
+ .command('list <appId>')
358
+ .alias('ls')
359
+ .description('List app invitations')
360
+ .option('--json', 'Output as JSON')
361
+ .action(async (appId, options) => {
362
+ try {
363
+ const token = await getStoredToken();
364
+ if (!token) {
365
+ printError('Not signed in. Run "sentron auth signin" first.');
366
+ process.exit(1);
367
+ }
368
+ printLoading('Fetching invitations');
369
+ const invitations = await api.apps.listInvitations(appId);
370
+ printLoadingComplete();
371
+ if (options.json) {
372
+ console.log(JSON.stringify(invitations, null, 2));
373
+ }
374
+ else {
375
+ console.log();
376
+ printSection('App Invitations');
377
+ if (invitations.length === 0) {
378
+ printInfo('No invitations found. Create one with "sentron app invites create <appId>"');
379
+ }
380
+ else {
381
+ invitations.forEach((inv) => {
382
+ printBox([
383
+ `Email: ${inv.email || 'N/A'}`,
384
+ `Role: ${brand.accent(inv.role)}`,
385
+ `Status: ${inv.status}`,
386
+ `Expires: ${new Date(inv.expiresAt).toLocaleDateString()}`,
387
+ `ID: ${brand.muted(inv.id)}`,
388
+ ]);
389
+ console.log();
390
+ });
391
+ }
392
+ }
393
+ }
394
+ catch (error) {
395
+ printLoadingError();
396
+ console.log();
397
+ printError(error instanceof Error ? error.message : 'Failed to list invitations');
398
+ process.exit(1);
399
+ }
400
+ });
401
+ // Create invitation
402
+ invitesCmd
403
+ .command('create <appId>')
404
+ .description('Invite a user to the app (will also invite to org if not a member)')
405
+ .option('-e, --email <email>', 'Email to invite')
406
+ .option('-r, --role <role>', 'App role (app_admin, app_member, app_viewer)', 'app_member')
407
+ .option('-o, --org-role <orgRole>', 'Organization role if not a member (admin, manager, member)', 'member')
408
+ .action(async (appId, options) => {
409
+ try {
410
+ const token = await getStoredToken();
411
+ if (!token) {
412
+ printError('Not signed in. Run "sentron auth signin" first.');
413
+ process.exit(1);
414
+ }
415
+ const email = options.email || await prompt(printPrompt('Email to invite'));
416
+ if (!email) {
417
+ printError('Email is required');
418
+ process.exit(1);
419
+ }
420
+ const validRoles = ['app_admin', 'app_member', 'app_viewer'];
421
+ if (!validRoles.includes(options.role)) {
422
+ printError(`Invalid role. Must be one of: ${validRoles.join(', ')}`);
423
+ process.exit(1);
424
+ }
425
+ const validOrgRoles = ['admin', 'manager', 'member'];
426
+ if (!validOrgRoles.includes(options.orgRole)) {
427
+ printError(`Invalid org role. Must be one of: ${validOrgRoles.join(', ')}`);
428
+ process.exit(1);
429
+ }
430
+ printLoading('Creating invitation');
431
+ const invitation = await api.apps.createInvitation(appId, email, options.role, options.orgRole);
432
+ printLoadingComplete();
433
+ console.log();
434
+ printSection('Invitation Sent');
435
+ printBox([
436
+ `Email: ${email}`,
437
+ `Role: ${brand.accent(options.role)}`,
438
+ `Status: ${invitation.status}`,
439
+ invitation.status === 'pending_org'
440
+ ? 'Note: User will need to accept organization invitation first'
441
+ : '',
442
+ ].filter(Boolean));
443
+ }
444
+ catch (error) {
445
+ printLoadingError();
446
+ console.log();
447
+ printError(error instanceof Error ? error.message : 'Failed to create invitation');
448
+ process.exit(1);
449
+ }
450
+ });
451
+ // Batch invite
452
+ invitesCmd
453
+ .command('batch <appId>')
454
+ .description('Invite multiple people to the app (max 50)')
455
+ .option('-e, --emails <emails>', 'Comma-separated list of emails')
456
+ .option('-f, --file <path>', 'CSV file with columns: email,role,organizationRole')
457
+ .option('-r, --role <role>', 'Default app role for all (app_admin, app_member, app_viewer)', 'app_member')
458
+ .option('-o, --org-role <orgRole>', 'Default org role if not a member (admin, manager, member)', 'member')
459
+ .option('--json', 'Output as JSON')
460
+ .action(async (appId, options) => {
461
+ try {
462
+ const token = await getStoredToken();
463
+ if (!token) {
464
+ printError('Not signed in. Run "sentron auth signin" first.');
465
+ process.exit(1);
466
+ }
467
+ // Validate roles
468
+ const validAppRoles = ['app_admin', 'app_member', 'app_viewer'];
469
+ const validOrgRoles = ['admin', 'manager', 'member'];
470
+ if (!validAppRoles.includes(options.role)) {
471
+ printError(`Invalid app role. Must be one of: ${validAppRoles.join(', ')}`);
472
+ process.exit(1);
473
+ }
474
+ if (!validOrgRoles.includes(options.orgRole)) {
475
+ printError(`Invalid org role. Must be one of: ${validOrgRoles.join(', ')}`);
476
+ process.exit(1);
477
+ }
478
+ // Get invitations from either --emails or --file
479
+ let invitations = [];
480
+ if (options.file) {
481
+ // Read from CSV file
482
+ const fs = await import('fs');
483
+ const path = await import('path');
484
+ const filePath = path.resolve(options.file);
485
+ if (!fs.existsSync(filePath)) {
486
+ printError(`File not found: ${options.file}`);
487
+ process.exit(1);
488
+ }
489
+ const content = fs.readFileSync(filePath, 'utf-8');
490
+ const lines = content.trim().split('\n');
491
+ // Check if first line is header
492
+ const firstLine = lines[0].toLowerCase();
493
+ const hasHeader = firstLine.includes('email') || firstLine.includes('role');
494
+ const dataLines = hasHeader ? lines.slice(1) : lines;
495
+ for (const line of dataLines) {
496
+ const trimmed = line.trim();
497
+ if (!trimmed)
498
+ continue;
499
+ const parts = trimmed.split(',').map(s => s.trim());
500
+ const email = parts[0];
501
+ const role = parts[1] || options.role;
502
+ const organizationRole = parts[2] || options.orgRole;
503
+ if (!email)
504
+ continue;
505
+ if (!validAppRoles.includes(role)) {
506
+ printError(`Invalid app role "${role}" for email ${email}. Must be one of: ${validAppRoles.join(', ')}`);
507
+ process.exit(1);
508
+ }
509
+ invitations.push({ email, role, organizationRole });
510
+ }
511
+ }
512
+ else if (options.emails) {
513
+ // Parse from comma-separated string
514
+ const emails = options.emails.split(',').map((e) => e.trim()).filter(Boolean);
515
+ invitations = emails.map((email) => ({
516
+ email,
517
+ role: options.role,
518
+ organizationRole: options.orgRole
519
+ }));
520
+ }
521
+ else {
522
+ // Interactive mode - prompt for emails
523
+ const input = await prompt(printPrompt('Enter emails (comma-separated)'));
524
+ if (!input) {
525
+ printError('No emails provided');
526
+ process.exit(1);
527
+ }
528
+ const emails = input.split(',').map(e => e.trim()).filter(Boolean);
529
+ invitations = emails.map(email => ({
530
+ email,
531
+ role: options.role,
532
+ organizationRole: options.orgRole
533
+ }));
534
+ }
535
+ if (invitations.length === 0) {
536
+ printError('No invitations to send');
537
+ process.exit(1);
538
+ }
539
+ if (invitations.length > 50) {
540
+ printError('Maximum 50 invitations per batch. Split into multiple batches.');
541
+ process.exit(1);
542
+ }
543
+ printLoading(`Creating ${invitations.length} invitation(s)`);
544
+ const result = await api.apps.batchInvite(appId, invitations);
545
+ printLoadingComplete();
546
+ if (options.json) {
547
+ console.log(JSON.stringify(result, null, 2));
548
+ }
549
+ else {
550
+ console.log();
551
+ printSection('Batch App Invite Results');
552
+ // Summary
553
+ printBox([
554
+ `Total: ${result.summary.total}`,
555
+ `Successful: ${brand.success(result.summary.successful.toString())}`,
556
+ `Failed: ${result.summary.failed > 0 ? brand.error(result.summary.failed.toString()) : result.summary.failed}`,
557
+ ]);
558
+ // Show chained invites info
559
+ const chainedCount = result.created.filter(c => c.orgInviteCreated).length;
560
+ if (chainedCount > 0) {
561
+ console.log();
562
+ printInfo(`${chainedCount} user(s) will also be invited to the organization`);
563
+ }
564
+ // Show failures if any
565
+ if (result.failed.length > 0) {
566
+ console.log();
567
+ printSection('Failed Invitations');
568
+ for (const failed of result.failed) {
569
+ console.log(` ${brand.error('✗')} ${failed.email}`);
570
+ console.log(` ${brand.muted(failed.error?.message || 'Unknown error')}`);
571
+ }
572
+ }
573
+ // Show successes
574
+ if (result.created.length > 0) {
575
+ console.log();
576
+ printSection('Invitations Sent');
577
+ for (const created of result.created) {
578
+ const note = created.orgInviteCreated ? ' (+ org invite)' : '';
579
+ console.log(` ${brand.success('✓')} ${created.email}${brand.muted(note)}`);
580
+ }
581
+ }
582
+ console.log();
583
+ printInfo(`Run "sentron app invites list ${appId}" to see all pending invitations`);
584
+ }
585
+ }
586
+ catch (error) {
587
+ printLoadingError();
588
+ console.log();
589
+ printError(error instanceof Error ? error.message : 'Failed to create batch invitations');
590
+ process.exit(1);
591
+ }
592
+ });
593
+ // Cancel invitation
594
+ invitesCmd
595
+ .command('cancel <appId> <invitationId>')
596
+ .alias('revoke')
597
+ .description('Cancel/revoke an app invitation')
598
+ .action(async (appId, invitationId) => {
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('Cancelling invitation');
606
+ await api.apps.revokeInvitation(appId, invitationId);
607
+ printLoadingComplete();
608
+ console.log();
609
+ printSuccess('Invitation cancelled');
610
+ }
611
+ catch (error) {
612
+ printLoadingError();
613
+ console.log();
614
+ printError(error instanceof Error ? error.message : 'Failed to cancel invitation');
615
+ process.exit(1);
616
+ }
617
+ });
618
+ // Resend invitation
619
+ invitesCmd
620
+ .command('resend <appId> <invitationId>')
621
+ .description('Resend an app invitation (resets expiration)')
622
+ .action(async (appId, invitationId) => {
623
+ try {
624
+ const token = await getStoredToken();
625
+ if (!token) {
626
+ printError('Not signed in. Run "sentron auth signin" first.');
627
+ process.exit(1);
628
+ }
629
+ printLoading('Resending invitation');
630
+ await api.apps.resendInvitation(appId, invitationId);
631
+ printLoadingComplete();
632
+ console.log();
633
+ printSuccess('Invitation resent');
634
+ }
635
+ catch (error) {
636
+ printLoadingError();
637
+ console.log();
638
+ printError(error instanceof Error ? error.message : 'Failed to resend invitation');
639
+ process.exit(1);
640
+ }
641
+ });
642
+ // ============================================================================
643
+ // JOIN REQUESTS SUBCOMMAND
644
+ // ============================================================================
645
+ const requestsCmd = appCmd
646
+ .command('requests')
647
+ .description('Manage app join requests');
648
+ // List join requests
649
+ requestsCmd
650
+ .command('list <appId>')
651
+ .alias('ls')
652
+ .description('List join requests for an app')
653
+ .option('--json', 'Output as JSON')
654
+ .action(async (appId, options) => {
655
+ try {
656
+ const token = await getStoredToken();
657
+ if (!token) {
658
+ printError('Not signed in. Run "sentron auth signin" first.');
659
+ process.exit(1);
660
+ }
661
+ printLoading('Fetching join requests');
662
+ const requests = await api.apps.listJoinRequests(appId);
663
+ printLoadingComplete();
664
+ if (options.json) {
665
+ console.log(JSON.stringify(requests, null, 2));
666
+ }
667
+ else {
668
+ console.log();
669
+ printSection('App Join Requests');
670
+ if (requests.length === 0) {
671
+ printInfo('No join requests found');
672
+ }
673
+ else {
674
+ requests.forEach((req) => {
675
+ printBox([
676
+ `Email: ${req.email || 'N/A'}`,
677
+ `Status: ${req.status}`,
678
+ req.message ? `Message: ${req.message}` : '',
679
+ `Requested: ${new Date(req.createdAt).toLocaleDateString()}`,
680
+ `ID: ${brand.muted(req.id)}`,
681
+ `User ID: ${brand.muted(req.userId)}`,
682
+ ].filter(Boolean));
683
+ console.log();
684
+ });
685
+ }
686
+ }
687
+ }
688
+ catch (error) {
689
+ printLoadingError();
690
+ console.log();
691
+ printError(error instanceof Error ? error.message : 'Failed to list join requests');
692
+ process.exit(1);
693
+ }
694
+ });
695
+ // Create join request
696
+ requestsCmd
697
+ .command('create <appId>')
698
+ .description('Request to join an app')
699
+ .option('-m, --message <message>', 'Optional message with the request')
700
+ .action(async (appId, options) => {
701
+ try {
702
+ const token = await getStoredToken();
703
+ if (!token) {
704
+ printError('Not signed in. Run "sentron auth signin" first.');
705
+ process.exit(1);
706
+ }
707
+ printLoading('Submitting join request');
708
+ const request = await api.apps.createJoinRequest(appId, options.message);
709
+ printLoadingComplete();
710
+ console.log();
711
+ printSection('Join Request Submitted');
712
+ printSuccess(`Status: ${request.status}`);
713
+ printInfo('An admin will review your request');
714
+ }
715
+ catch (error) {
716
+ printLoadingError();
717
+ console.log();
718
+ printError(error instanceof Error ? error.message : 'Failed to create join request');
719
+ process.exit(1);
720
+ }
721
+ });
722
+ // Approve join request
723
+ requestsCmd
724
+ .command('approve <appId> <requestId>')
725
+ .description('Approve a join request')
726
+ .option('-r, --role <role>', 'Role to assign (app_admin, app_member, app_viewer)', 'app_member')
727
+ .action(async (appId, requestId, options) => {
728
+ try {
729
+ const token = await getStoredToken();
730
+ if (!token) {
731
+ printError('Not signed in. Run "sentron auth signin" first.');
732
+ process.exit(1);
733
+ }
734
+ const validRoles = ['app_admin', 'app_member', 'app_viewer'];
735
+ if (!validRoles.includes(options.role)) {
736
+ printError(`Invalid role. Must be one of: ${validRoles.join(', ')}`);
737
+ process.exit(1);
738
+ }
739
+ printLoading('Approving join request');
740
+ await api.apps.handleJoinRequest(appId, requestId, 'approve', options.role);
741
+ printLoadingComplete();
742
+ console.log();
743
+ printSuccess(`Join request approved with role: ${options.role}`);
744
+ }
745
+ catch (error) {
746
+ printLoadingError();
747
+ console.log();
748
+ printError(error instanceof Error ? error.message : 'Failed to approve join request');
749
+ process.exit(1);
750
+ }
751
+ });
752
+ // Reject join request
753
+ requestsCmd
754
+ .command('reject <appId> <requestId>')
755
+ .alias('decline')
756
+ .description('Reject a join request')
757
+ .action(async (appId, requestId) => {
758
+ try {
759
+ const token = await getStoredToken();
760
+ if (!token) {
761
+ printError('Not signed in. Run "sentron auth signin" first.');
762
+ process.exit(1);
763
+ }
764
+ printLoading('Rejecting join request');
765
+ await api.apps.handleJoinRequest(appId, requestId, 'decline');
766
+ printLoadingComplete();
767
+ console.log();
768
+ printSuccess('Join request rejected');
769
+ }
770
+ catch (error) {
771
+ printLoadingError();
772
+ console.log();
773
+ printError(error instanceof Error ? error.message : 'Failed to reject join request');
774
+ process.exit(1);
775
+ }
776
+ });
777
+ // ============================================================================
778
+ // AUDIT LOGS
779
+ // ============================================================================
780
+ const auditCmd = appCmd
781
+ .command('audit-logs')
782
+ .alias('activity')
783
+ .description('View app activity logs');
784
+ auditCmd
785
+ .command('list <appId>')
786
+ .alias('ls')
787
+ .description('List audit logs for an app')
788
+ .option('--limit <number>', 'Number of logs to return (default 50, max 100)', '50')
789
+ .option('--offset <number>', 'Offset for pagination', '0')
790
+ .option('--action <action>', 'Filter by action type (e.g., app.member.added)')
791
+ .option('--user <userId>', 'Filter by actor user ID')
792
+ .option('--from <date>', 'Filter from date (ISO 8601 format)')
793
+ .option('--to <date>', 'Filter to date (ISO 8601 format)')
794
+ .option('--json', 'Output as JSON')
795
+ .action(async (appId, options) => {
796
+ try {
797
+ const token = await getStoredToken();
798
+ if (!token) {
799
+ printError('Not signed in. Run "sentron auth signin" first.');
800
+ process.exit(1);
801
+ }
802
+ printLoading('Fetching audit logs');
803
+ const logs = await api.apps.listAuditLogs(appId, {
804
+ limit: parseInt(options.limit, 10),
805
+ offset: parseInt(options.offset, 10),
806
+ action: options.action,
807
+ userId: options.user,
808
+ startDate: options.from,
809
+ endDate: options.to,
810
+ });
811
+ printLoadingComplete();
812
+ if (options.json) {
813
+ console.log(JSON.stringify(logs, null, 2));
814
+ return;
815
+ }
816
+ console.log();
817
+ printSection('App Audit Logs');
818
+ if (logs.length === 0) {
819
+ printInfo('No audit logs found matching your criteria.');
820
+ }
821
+ else {
822
+ logs.forEach((log) => {
823
+ const date = new Date(log.createdAt).toLocaleString();
824
+ const action = formatAuditAction(log.action);
825
+ const resource = log.resourceType + (log.resourceId ? ` (${log.resourceId.substring(0, 8)}...)` : '');
826
+ printBox([
827
+ `Action: ${brand.accent(action)}`,
828
+ `Resource: ${resource}`,
829
+ `Actor: ${log.userId.substring(0, 12)}...`,
830
+ `Date: ${date}`,
831
+ log.metadata ? `Details: ${JSON.stringify(log.metadata)}` : null,
832
+ ].filter(Boolean));
833
+ console.log();
834
+ });
835
+ }
836
+ }
837
+ catch (error) {
838
+ printLoadingError();
839
+ console.log();
840
+ printError(error.message || 'Failed to fetch audit logs');
841
+ process.exit(1);
842
+ }
843
+ });
844
+ // Default action - show help
845
+ appCmd
846
+ .action(() => {
847
+ console.log();
848
+ printSection('App Commands');
849
+ printInfo('Use "sentron app list <orgId>" to list apps');
850
+ printInfo('Use "sentron app create <orgId>" to create an app');
851
+ printInfo('Use "sentron app members list <appId>" to list members');
852
+ printInfo('Use "sentron app invites list <appId>" to list invitations');
853
+ printInfo('Use "sentron app requests list <appId>" to list join requests');
854
+ printInfo('Use "sentron app audit-logs list <appId>" to view activity');
855
+ printInfo('Use "sentron app --help" for all options');
856
+ });
857
+ }
858
+ /**
859
+ * Format audit action for display
860
+ */
861
+ function formatAuditAction(action) {
862
+ // Convert action like "app.member.added" to "Member Added"
863
+ const parts = action.split('.');
864
+ if (parts.length >= 2) {
865
+ const subject = parts[parts.length - 2];
866
+ const verb = parts[parts.length - 1];
867
+ return `${capitalize(subject)} ${capitalize(verb)}`;
868
+ }
869
+ return action;
870
+ }
871
+ function capitalize(str) {
872
+ return str.charAt(0).toUpperCase() + str.slice(1);
873
+ }
874
+ //# sourceMappingURL=app.js.map