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,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
|