@lifestreamdynamics/vault-cli 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +140 -30
- package/dist/client.d.ts +4 -0
- package/dist/client.js +12 -11
- package/dist/commands/admin.js +5 -5
- package/dist/commands/ai.d.ts +2 -0
- package/dist/commands/ai.js +124 -0
- package/dist/commands/analytics.d.ts +2 -0
- package/dist/commands/analytics.js +84 -0
- package/dist/commands/auth.js +10 -105
- package/dist/commands/booking.d.ts +2 -0
- package/dist/commands/booking.js +739 -0
- package/dist/commands/calendar.js +778 -6
- package/dist/commands/completion.d.ts +5 -0
- package/dist/commands/completion.js +60 -0
- package/dist/commands/config.js +17 -16
- package/dist/commands/connectors.js +12 -1
- package/dist/commands/custom-domains.d.ts +2 -0
- package/dist/commands/custom-domains.js +154 -0
- package/dist/commands/docs.js +152 -5
- package/dist/commands/hooks.js +6 -1
- package/dist/commands/links.js +9 -2
- package/dist/commands/mfa.js +1 -70
- package/dist/commands/plugins.d.ts +2 -0
- package/dist/commands/plugins.js +172 -0
- package/dist/commands/publish-vault.d.ts +2 -0
- package/dist/commands/publish-vault.js +117 -0
- package/dist/commands/publish.js +63 -2
- package/dist/commands/saml.d.ts +2 -0
- package/dist/commands/saml.js +220 -0
- package/dist/commands/scim.d.ts +2 -0
- package/dist/commands/scim.js +238 -0
- package/dist/commands/shares.js +25 -3
- package/dist/commands/subscription.js +9 -2
- package/dist/commands/sync.js +3 -0
- package/dist/commands/teams.js +233 -4
- package/dist/commands/user.js +444 -0
- package/dist/commands/vaults.js +240 -8
- package/dist/commands/webhooks.js +6 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.js +7 -3
- package/dist/index.js +28 -1
- package/dist/lib/credential-manager.js +32 -7
- package/dist/lib/migration.js +2 -2
- package/dist/lib/profiles.js +4 -4
- package/dist/sync/config.js +2 -2
- package/dist/sync/daemon-worker.js +13 -6
- package/dist/sync/daemon.js +2 -1
- package/dist/sync/remote-poller.js +7 -3
- package/dist/sync/state.js +2 -2
- package/dist/utils/confirm.d.ts +11 -0
- package/dist/utils/confirm.js +23 -0
- package/dist/utils/format.js +1 -1
- package/dist/utils/output.js +4 -1
- package/dist/utils/prompt.d.ts +29 -0
- package/dist/utils/prompt.js +146 -0
- package/package.json +2 -2
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { getClientAsync } from '../client.js';
|
|
3
|
+
import { addGlobalFlags, resolveFlags } from '../utils/flags.js';
|
|
4
|
+
import { createOutput, handleError } from '../utils/output.js';
|
|
5
|
+
export function registerScimCommands(program) {
|
|
6
|
+
const scim = program.command('scim').description('SCIM 2.0 user provisioning management (requires scimToken)');
|
|
7
|
+
// ── list-users ────────────────────────────────────────────────────────────
|
|
8
|
+
addGlobalFlags(scim.command('list-users')
|
|
9
|
+
.description('List SCIM-provisioned users')
|
|
10
|
+
.option('--filter <expr>', 'SCIM filter expression (e.g. \'userName eq "user@example.com"\')')
|
|
11
|
+
.option('--start-index <n>', 'Pagination start index (1-based)', parseInt)
|
|
12
|
+
.option('--count <n>', 'Number of results per page (max 100)', parseInt))
|
|
13
|
+
.action(async (_opts) => {
|
|
14
|
+
const flags = resolveFlags(_opts);
|
|
15
|
+
const out = createOutput(flags);
|
|
16
|
+
out.startSpinner('Fetching SCIM users...');
|
|
17
|
+
try {
|
|
18
|
+
const client = await getClientAsync();
|
|
19
|
+
if (!client.scim) {
|
|
20
|
+
out.error('SCIM resource is not configured. Provide a scimToken when creating the client.');
|
|
21
|
+
process.exitCode = 1;
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const result = await client.scim.listUsers({
|
|
25
|
+
filter: _opts.filter,
|
|
26
|
+
startIndex: _opts.startIndex,
|
|
27
|
+
count: _opts.count,
|
|
28
|
+
});
|
|
29
|
+
out.stopSpinner();
|
|
30
|
+
if (flags.output !== 'json') {
|
|
31
|
+
out.status(chalk.dim(`Total: ${result.totalResults} Start: ${result.startIndex} Page: ${result.itemsPerPage}`));
|
|
32
|
+
}
|
|
33
|
+
out.list(result.Resources.map((u) => ({
|
|
34
|
+
id: u.id,
|
|
35
|
+
userName: u.userName,
|
|
36
|
+
name: u.name.formatted,
|
|
37
|
+
active: u.active ? 'yes' : 'no',
|
|
38
|
+
externalId: u.externalId ?? '',
|
|
39
|
+
})), {
|
|
40
|
+
emptyMessage: 'No SCIM users found.',
|
|
41
|
+
columns: [
|
|
42
|
+
{ key: 'id', header: 'ID' },
|
|
43
|
+
{ key: 'userName', header: 'Username' },
|
|
44
|
+
{ key: 'name', header: 'Name' },
|
|
45
|
+
{ key: 'active', header: 'Active' },
|
|
46
|
+
{ key: 'externalId', header: 'External ID' },
|
|
47
|
+
],
|
|
48
|
+
textFn: (u) => `${chalk.cyan(String(u.userName))} ${chalk.dim(`(${String(u.id)})`)} — ${u.name} — ${u.active === 'yes' ? chalk.green('active') : chalk.red('inactive')}`,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
handleError(out, err, 'Failed to list SCIM users');
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
// ── get-user ──────────────────────────────────────────────────────────────
|
|
56
|
+
addGlobalFlags(scim.command('get-user')
|
|
57
|
+
.description('Get a SCIM user by internal ID')
|
|
58
|
+
.argument('<id>', 'Internal user ID'))
|
|
59
|
+
.action(async (id, _opts) => {
|
|
60
|
+
const flags = resolveFlags(_opts);
|
|
61
|
+
const out = createOutput(flags);
|
|
62
|
+
out.startSpinner('Fetching SCIM user...');
|
|
63
|
+
try {
|
|
64
|
+
const client = await getClientAsync();
|
|
65
|
+
if (!client.scim) {
|
|
66
|
+
out.error('SCIM resource is not configured. Provide a scimToken when creating the client.');
|
|
67
|
+
process.exitCode = 1;
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const user = await client.scim.getUser(id);
|
|
71
|
+
out.stopSpinner();
|
|
72
|
+
out.record({
|
|
73
|
+
id: user.id,
|
|
74
|
+
userName: user.userName,
|
|
75
|
+
givenName: user.name.givenName,
|
|
76
|
+
familyName: user.name.familyName,
|
|
77
|
+
email: user.emails[0]?.value ?? '',
|
|
78
|
+
active: user.active,
|
|
79
|
+
externalId: user.externalId,
|
|
80
|
+
created: user.meta.created,
|
|
81
|
+
lastModified: user.meta.lastModified,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
handleError(out, err, 'Failed to fetch SCIM user');
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
// ── create-user ───────────────────────────────────────────────────────────
|
|
89
|
+
addGlobalFlags(scim.command('create-user')
|
|
90
|
+
.description('Provision a new user via SCIM')
|
|
91
|
+
.requiredOption('--user-name <userName>', 'Login name (email address)')
|
|
92
|
+
.requiredOption('--email <email>', 'Primary email address')
|
|
93
|
+
.option('--given-name <name>', 'First/given name')
|
|
94
|
+
.option('--family-name <name>', 'Last/family name')
|
|
95
|
+
.option('--external-id <id>', 'External IdP subject ID'))
|
|
96
|
+
.action(async (_opts) => {
|
|
97
|
+
const flags = resolveFlags(_opts);
|
|
98
|
+
const out = createOutput(flags);
|
|
99
|
+
out.startSpinner('Creating SCIM user...');
|
|
100
|
+
try {
|
|
101
|
+
const client = await getClientAsync();
|
|
102
|
+
if (!client.scim) {
|
|
103
|
+
out.error('SCIM resource is not configured. Provide a scimToken when creating the client.');
|
|
104
|
+
process.exitCode = 1;
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const created = await client.scim.createUser({
|
|
108
|
+
userName: _opts.userName,
|
|
109
|
+
emails: [{ value: _opts.email, primary: true }],
|
|
110
|
+
name: {
|
|
111
|
+
givenName: _opts.givenName,
|
|
112
|
+
familyName: _opts.familyName,
|
|
113
|
+
},
|
|
114
|
+
externalId: _opts.externalId,
|
|
115
|
+
});
|
|
116
|
+
out.stopSpinner();
|
|
117
|
+
if (flags.output === 'json') {
|
|
118
|
+
out.raw(JSON.stringify(created, null, 2) + '\n');
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
out.raw(chalk.green(`SCIM user created: ${created.userName} (${created.id})`) + '\n');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
handleError(out, err, 'Failed to create SCIM user');
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
// ── update-user ───────────────────────────────────────────────────────────
|
|
129
|
+
addGlobalFlags(scim.command('update-user')
|
|
130
|
+
.description('Update a SCIM user (full replace)')
|
|
131
|
+
.argument('<id>', 'Internal user ID')
|
|
132
|
+
.option('--user-name <userName>', 'Updated login name')
|
|
133
|
+
.option('--email <email>', 'Updated primary email')
|
|
134
|
+
.option('--given-name <name>', 'Updated first/given name')
|
|
135
|
+
.option('--family-name <name>', 'Updated last/family name'))
|
|
136
|
+
.action(async (id, _opts) => {
|
|
137
|
+
const flags = resolveFlags(_opts);
|
|
138
|
+
const out = createOutput(flags);
|
|
139
|
+
const data = {};
|
|
140
|
+
if (_opts.userName)
|
|
141
|
+
data.userName = _opts.userName;
|
|
142
|
+
if (_opts.email)
|
|
143
|
+
data.emails = [{ value: _opts.email, primary: true }];
|
|
144
|
+
if (_opts.givenName || _opts.familyName) {
|
|
145
|
+
data.name = {
|
|
146
|
+
givenName: _opts.givenName,
|
|
147
|
+
familyName: _opts.familyName,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
if (Object.keys(data).length === 0) {
|
|
151
|
+
out.error('No updates specified. Use --user-name, --email, --given-name, or --family-name.');
|
|
152
|
+
process.exitCode = 2;
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
out.startSpinner('Updating SCIM user...');
|
|
156
|
+
try {
|
|
157
|
+
const client = await getClientAsync();
|
|
158
|
+
if (!client.scim) {
|
|
159
|
+
out.error('SCIM resource is not configured. Provide a scimToken when creating the client.');
|
|
160
|
+
process.exitCode = 1;
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const updated = await client.scim.updateUser(id, data);
|
|
164
|
+
out.stopSpinner();
|
|
165
|
+
if (flags.output === 'json') {
|
|
166
|
+
out.raw(JSON.stringify(updated, null, 2) + '\n');
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
out.raw(chalk.green(`SCIM user updated: ${updated.userName} (${updated.id})`) + '\n');
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
handleError(out, err, 'Failed to update SCIM user');
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
// ── delete-user ───────────────────────────────────────────────────────────
|
|
177
|
+
addGlobalFlags(scim.command('delete-user')
|
|
178
|
+
.description('Deprovision a SCIM user (removes SSO bindings)')
|
|
179
|
+
.argument('<id>', 'Internal user ID')
|
|
180
|
+
.option('--force', 'Skip confirmation prompt'))
|
|
181
|
+
.action(async (id, _opts) => {
|
|
182
|
+
const flags = resolveFlags(_opts);
|
|
183
|
+
const out = createOutput(flags);
|
|
184
|
+
if (!_opts.force) {
|
|
185
|
+
out.raw(chalk.yellow(`Pass --force to deprovision SCIM user ${id}.`) + '\n');
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
out.startSpinner('Deprovisioning SCIM user...');
|
|
189
|
+
try {
|
|
190
|
+
const client = await getClientAsync();
|
|
191
|
+
if (!client.scim) {
|
|
192
|
+
out.error('SCIM resource is not configured. Provide a scimToken when creating the client.');
|
|
193
|
+
process.exitCode = 1;
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
await client.scim.deleteUser(id);
|
|
197
|
+
out.stopSpinner();
|
|
198
|
+
out.raw(chalk.green(`SCIM user ${id} deprovisioned.`) + '\n');
|
|
199
|
+
}
|
|
200
|
+
catch (err) {
|
|
201
|
+
handleError(out, err, 'Failed to deprovision SCIM user');
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
// ── service-config ────────────────────────────────────────────────────────
|
|
205
|
+
addGlobalFlags(scim.command('service-config')
|
|
206
|
+
.description('Show SCIM service provider capabilities'))
|
|
207
|
+
.action(async (_opts) => {
|
|
208
|
+
const flags = resolveFlags(_opts);
|
|
209
|
+
const out = createOutput(flags);
|
|
210
|
+
out.startSpinner('Fetching SCIM service provider config...');
|
|
211
|
+
try {
|
|
212
|
+
const client = await getClientAsync();
|
|
213
|
+
if (!client.scim) {
|
|
214
|
+
out.error('SCIM resource is not configured. Provide a scimToken when creating the client.');
|
|
215
|
+
process.exitCode = 1;
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
const config = await client.scim.getServiceProviderConfig();
|
|
219
|
+
out.stopSpinner();
|
|
220
|
+
if (flags.output === 'json') {
|
|
221
|
+
out.raw(JSON.stringify(config, null, 2) + '\n');
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
out.record({
|
|
225
|
+
patch: String(config.patch.supported),
|
|
226
|
+
bulk: String(config.bulk.supported),
|
|
227
|
+
filter: String(config.filter.supported),
|
|
228
|
+
filterMaxResults: String(config.filter.maxResults),
|
|
229
|
+
changePassword: String(config.changePassword.supported),
|
|
230
|
+
sort: String(config.sort.supported),
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
catch (err) {
|
|
235
|
+
handleError(out, err, 'Failed to fetch SCIM service provider config');
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
}
|
package/dist/commands/shares.js
CHANGED
|
@@ -2,6 +2,7 @@ import chalk from 'chalk';
|
|
|
2
2
|
import { getClientAsync } from '../client.js';
|
|
3
3
|
import { addGlobalFlags, resolveFlags } from '../utils/flags.js';
|
|
4
4
|
import { createOutput, handleError } from '../utils/output.js';
|
|
5
|
+
import { promptPassword, readPasswordFromStdin } from '../utils/prompt.js';
|
|
5
6
|
export function registerShareCommands(program) {
|
|
6
7
|
const shares = program.command('shares').description('Create, list, and revoke document share links');
|
|
7
8
|
addGlobalFlags(shares.command('list')
|
|
@@ -55,20 +56,41 @@ export function registerShareCommands(program) {
|
|
|
55
56
|
.argument('<vaultId>', 'Vault ID')
|
|
56
57
|
.argument('<docPath>', 'Document path (e.g., notes/meeting.md)')
|
|
57
58
|
.option('--permission <perm>', 'Permission level: view or edit', 'view')
|
|
58
|
-
.option('--password
|
|
59
|
+
.option('--protect-with-password', 'Prompt for a password to protect the link (interactive TTY only)')
|
|
60
|
+
.option('--password-stdin', 'Read link password from stdin for CI usage')
|
|
59
61
|
.option('--expires <date>', 'Expiration date (ISO 8601)')
|
|
60
62
|
.option('--max-views <count>', 'Maximum number of views'))
|
|
61
63
|
.action(async (vaultId, docPath, _opts) => {
|
|
62
64
|
const flags = resolveFlags(_opts);
|
|
63
65
|
const out = createOutput(flags);
|
|
66
|
+
// Resolve optional link password without exposing it in the process list.
|
|
67
|
+
let linkPassword;
|
|
68
|
+
if (_opts.passwordStdin) {
|
|
69
|
+
const pw = await readPasswordFromStdin();
|
|
70
|
+
if (!pw) {
|
|
71
|
+
out.error('--password-stdin was set but no password was provided on stdin.');
|
|
72
|
+
process.exitCode = 1;
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
linkPassword = pw;
|
|
76
|
+
}
|
|
77
|
+
else if (_opts.protectWithPassword) {
|
|
78
|
+
const pw = await promptPassword('Share link password: ');
|
|
79
|
+
if (!pw) {
|
|
80
|
+
out.error('A password is required when --protect-with-password is set.');
|
|
81
|
+
process.exitCode = 1;
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
linkPassword = pw;
|
|
85
|
+
}
|
|
64
86
|
out.startSpinner('Creating share link...');
|
|
65
87
|
try {
|
|
66
88
|
const client = await getClientAsync();
|
|
67
89
|
const params = {};
|
|
68
90
|
if (_opts.permission)
|
|
69
91
|
params.permission = String(_opts.permission);
|
|
70
|
-
if (
|
|
71
|
-
params.password =
|
|
92
|
+
if (linkPassword)
|
|
93
|
+
params.password = linkPassword;
|
|
72
94
|
if (_opts.expires)
|
|
73
95
|
params.expiresAt = String(_opts.expires);
|
|
74
96
|
if (_opts.maxViews)
|
|
@@ -3,6 +3,7 @@ import { getClientAsync } from '../client.js';
|
|
|
3
3
|
import { addGlobalFlags, resolveFlags } from '../utils/flags.js';
|
|
4
4
|
import { createOutput, handleError } from '../utils/output.js';
|
|
5
5
|
import { formatBytes } from '../utils/format.js';
|
|
6
|
+
import { confirmAction } from '../utils/confirm.js';
|
|
6
7
|
export function registerSubscriptionCommands(program) {
|
|
7
8
|
const sub = program.command('subscription').description('View plans, manage subscription, and access billing');
|
|
8
9
|
addGlobalFlags(sub.command('status')
|
|
@@ -98,12 +99,18 @@ EXAMPLES
|
|
|
98
99
|
});
|
|
99
100
|
addGlobalFlags(sub.command('cancel')
|
|
100
101
|
.description('Cancel your current subscription')
|
|
101
|
-
.option('--reason <text>', 'Reason for cancellation')
|
|
102
|
+
.option('--reason <text>', 'Reason for cancellation')
|
|
103
|
+
.option('-y, --yes', 'Skip confirmation prompt'))
|
|
102
104
|
.action(async (_opts) => {
|
|
103
105
|
const flags = resolveFlags(_opts);
|
|
104
106
|
const out = createOutput(flags);
|
|
105
|
-
out.startSpinner('Cancelling subscription...');
|
|
106
107
|
try {
|
|
108
|
+
const confirmed = await confirmAction('Are you sure you want to cancel your subscription?', { yes: _opts.yes });
|
|
109
|
+
if (!confirmed) {
|
|
110
|
+
out.status('Cancellation aborted.');
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
out.startSpinner('Cancelling subscription...');
|
|
107
114
|
const client = await getClientAsync();
|
|
108
115
|
await client.subscription.cancel(_opts.reason);
|
|
109
116
|
out.success('Subscription cancelled', { cancelled: true });
|
package/dist/commands/sync.js
CHANGED
|
@@ -35,6 +35,9 @@ export function registerSyncCommands(program) {
|
|
|
35
35
|
const absPath = path.resolve(localPath);
|
|
36
36
|
const mode = _opts.mode ?? 'sync';
|
|
37
37
|
const onConflict = _opts.onConflict ?? 'newer';
|
|
38
|
+
if (!_opts.onConflict) {
|
|
39
|
+
out.warn('No --on-conflict strategy specified; defaulting to "newer" (keeps the file with the more recent modification time). Use --on-conflict local|remote|ask to override.');
|
|
40
|
+
}
|
|
38
41
|
const ignore = _opts.ignore;
|
|
39
42
|
const syncInterval = _opts.interval;
|
|
40
43
|
const autoSync = _opts.autoSync === true;
|
package/dist/commands/teams.js
CHANGED
|
@@ -97,10 +97,15 @@ EXAMPLES
|
|
|
97
97
|
});
|
|
98
98
|
addGlobalFlags(teams.command('delete')
|
|
99
99
|
.description('Permanently delete a team and all its data')
|
|
100
|
-
.argument('<teamId>', 'Team ID')
|
|
100
|
+
.argument('<teamId>', 'Team ID')
|
|
101
|
+
.option('-y, --yes', 'Skip confirmation prompt'))
|
|
101
102
|
.action(async (teamId, _opts) => {
|
|
102
103
|
const flags = resolveFlags(_opts);
|
|
103
104
|
const out = createOutput(flags);
|
|
105
|
+
if (!_opts.yes) {
|
|
106
|
+
out.status(chalk.yellow(`Pass --yes to permanently delete team ${teamId} and all its data.`));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
104
109
|
out.startSpinner('Deleting team...');
|
|
105
110
|
try {
|
|
106
111
|
const client = await getClientAsync();
|
|
@@ -125,7 +130,7 @@ EXAMPLES
|
|
|
125
130
|
const memberList = await client.teams.listMembers(teamId);
|
|
126
131
|
out.stopSpinner();
|
|
127
132
|
out.list(memberList.map(m => ({
|
|
128
|
-
name: m.user.
|
|
133
|
+
name: m.user.displayName || m.user.email,
|
|
129
134
|
userId: m.userId,
|
|
130
135
|
role: m.role,
|
|
131
136
|
email: m.user.email,
|
|
@@ -169,10 +174,15 @@ EXAMPLES
|
|
|
169
174
|
addGlobalFlags(members.command('remove')
|
|
170
175
|
.description('Remove a member from the team')
|
|
171
176
|
.argument('<teamId>', 'Team ID')
|
|
172
|
-
.argument('<userId>', 'User ID')
|
|
177
|
+
.argument('<userId>', 'User ID')
|
|
178
|
+
.option('-y, --yes', 'Skip confirmation prompt'))
|
|
173
179
|
.action(async (teamId, userId, _opts) => {
|
|
174
180
|
const flags = resolveFlags(_opts);
|
|
175
181
|
const out = createOutput(flags);
|
|
182
|
+
if (!_opts.yes) {
|
|
183
|
+
out.status(chalk.yellow(`Pass --yes to remove user ${userId} from team ${teamId}.`));
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
176
186
|
out.startSpinner('Removing member...');
|
|
177
187
|
try {
|
|
178
188
|
const client = await getClientAsync();
|
|
@@ -284,7 +294,7 @@ EXAMPLES
|
|
|
284
294
|
const client = await getClientAsync();
|
|
285
295
|
const vaultList = await client.teams.listVaults(teamId);
|
|
286
296
|
out.stopSpinner();
|
|
287
|
-
out.list(vaultList.map(v => ({ name: String(v.name), slug: String(v.slug), description:
|
|
297
|
+
out.list(vaultList.map(v => ({ name: String(v.name), slug: String(v.slug), description: v.description ?? 'No description' })), {
|
|
288
298
|
emptyMessage: 'No team vaults found.',
|
|
289
299
|
columns: [
|
|
290
300
|
{ key: 'name', header: 'Name' },
|
|
@@ -319,4 +329,223 @@ EXAMPLES
|
|
|
319
329
|
handleError(out, err, 'Failed to create team vault');
|
|
320
330
|
}
|
|
321
331
|
});
|
|
332
|
+
const teamCalendar = teams.command('calendar').description('Team calendar operations');
|
|
333
|
+
addGlobalFlags(teamCalendar.command('view')
|
|
334
|
+
.description('View team calendar')
|
|
335
|
+
.argument('<teamId>', 'Team ID')
|
|
336
|
+
.requiredOption('--start <date>', 'Start date (YYYY-MM-DD)')
|
|
337
|
+
.requiredOption('--end <date>', 'End date (YYYY-MM-DD)')
|
|
338
|
+
.option('--types <types>', 'Event types to include'))
|
|
339
|
+
.action(async (teamId, _opts) => {
|
|
340
|
+
const flags = resolveFlags(_opts);
|
|
341
|
+
const out = createOutput(flags);
|
|
342
|
+
out.startSpinner('Fetching team calendar...');
|
|
343
|
+
try {
|
|
344
|
+
const client = await getClientAsync();
|
|
345
|
+
const cal = await client.teams.getCalendar(teamId, {
|
|
346
|
+
start: _opts.start,
|
|
347
|
+
end: _opts.end,
|
|
348
|
+
types: _opts.types,
|
|
349
|
+
});
|
|
350
|
+
out.stopSpinner();
|
|
351
|
+
if (flags.output === 'json') {
|
|
352
|
+
out.raw(JSON.stringify(cal, null, 2) + '\n');
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
for (const [date, day] of Object.entries(cal.days ?? {})) {
|
|
356
|
+
if (day && day.activityCount) {
|
|
357
|
+
process.stdout.write(`${chalk.cyan(date)}: ${day.activityCount} events\n`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
catch (err) {
|
|
363
|
+
handleError(out, err, 'Failed to fetch team calendar');
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
addGlobalFlags(teamCalendar.command('activity')
|
|
367
|
+
.description('View team calendar activity')
|
|
368
|
+
.argument('<teamId>', 'Team ID')
|
|
369
|
+
.requiredOption('--start <date>', 'Start date (YYYY-MM-DD)')
|
|
370
|
+
.requiredOption('--end <date>', 'End date (YYYY-MM-DD)'))
|
|
371
|
+
.action(async (teamId, _opts) => {
|
|
372
|
+
const flags = resolveFlags(_opts);
|
|
373
|
+
const out = createOutput(flags);
|
|
374
|
+
out.startSpinner('Fetching team calendar activity...');
|
|
375
|
+
try {
|
|
376
|
+
const client = await getClientAsync();
|
|
377
|
+
const activity = await client.teams.getCalendarActivity(teamId, {
|
|
378
|
+
start: _opts.start,
|
|
379
|
+
end: _opts.end,
|
|
380
|
+
});
|
|
381
|
+
out.stopSpinner();
|
|
382
|
+
if (flags.output === 'json') {
|
|
383
|
+
out.raw(JSON.stringify(activity, null, 2) + '\n');
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
for (const day of activity.days ?? []) {
|
|
387
|
+
if (day.total) {
|
|
388
|
+
process.stdout.write(`${chalk.cyan(day.date)}: ${day.total} activities\n`);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
catch (err) {
|
|
394
|
+
handleError(out, err, 'Failed to fetch team calendar activity');
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
addGlobalFlags(teamCalendar.command('events')
|
|
398
|
+
.description('List team calendar events')
|
|
399
|
+
.argument('<teamId>', 'Team ID')
|
|
400
|
+
.option('--start <date>', 'Start date (YYYY-MM-DD)')
|
|
401
|
+
.option('--end <date>', 'End date (YYYY-MM-DD)'))
|
|
402
|
+
.action(async (teamId, _opts) => {
|
|
403
|
+
const flags = resolveFlags(_opts);
|
|
404
|
+
const out = createOutput(flags);
|
|
405
|
+
out.startSpinner('Fetching team calendar events...');
|
|
406
|
+
try {
|
|
407
|
+
const client = await getClientAsync();
|
|
408
|
+
const events = await client.teams.getCalendarEvents(teamId, {
|
|
409
|
+
start: _opts.start,
|
|
410
|
+
end: _opts.end,
|
|
411
|
+
});
|
|
412
|
+
out.stopSpinner();
|
|
413
|
+
out.list(events.map(e => ({ id: e.id, title: e.title, startDate: e.startDate, endDate: e.endDate ?? '' })), {
|
|
414
|
+
emptyMessage: 'No events found.',
|
|
415
|
+
columns: [
|
|
416
|
+
{ key: 'id', header: 'ID' },
|
|
417
|
+
{ key: 'title', header: 'Title' },
|
|
418
|
+
{ key: 'startDate', header: 'Start' },
|
|
419
|
+
{ key: 'endDate', header: 'End' },
|
|
420
|
+
],
|
|
421
|
+
textFn: (e) => `${chalk.cyan(String(e.title))} — ${String(e.startDate)}`,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
catch (err) {
|
|
425
|
+
handleError(out, err, 'Failed to fetch team calendar events');
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
addGlobalFlags(teamCalendar.command('upcoming')
|
|
429
|
+
.description('View upcoming events and due items for a team')
|
|
430
|
+
.argument('<teamId>', 'Team ID'))
|
|
431
|
+
.action(async (teamId, _opts) => {
|
|
432
|
+
const flags = resolveFlags(_opts);
|
|
433
|
+
const out = createOutput(flags);
|
|
434
|
+
out.startSpinner('Fetching upcoming items...');
|
|
435
|
+
try {
|
|
436
|
+
const client = await getClientAsync();
|
|
437
|
+
const upcoming = await client.teams.getUpcoming(teamId);
|
|
438
|
+
out.stopSpinner();
|
|
439
|
+
if (flags.output === 'json') {
|
|
440
|
+
out.raw(JSON.stringify(upcoming, null, 2) + '\n');
|
|
441
|
+
}
|
|
442
|
+
else {
|
|
443
|
+
if (upcoming.events && upcoming.events.length > 0) {
|
|
444
|
+
process.stdout.write(chalk.bold('Events:\n'));
|
|
445
|
+
for (const e of upcoming.events) {
|
|
446
|
+
process.stdout.write(` ${chalk.cyan(String(e.title))} — ${String(e.startDate)}\n`);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
process.stdout.write(chalk.dim('No upcoming events.\n'));
|
|
451
|
+
}
|
|
452
|
+
if (upcoming.dueDocs && upcoming.dueDocs.length > 0) {
|
|
453
|
+
process.stdout.write(chalk.bold('Due Documents:\n'));
|
|
454
|
+
for (const d of upcoming.dueDocs) {
|
|
455
|
+
process.stdout.write(` ${chalk.cyan(String(d.path))} — due ${String(d.dueAt)}\n`);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
else {
|
|
459
|
+
process.stdout.write(chalk.dim('No upcoming due documents.\n'));
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
catch (err) {
|
|
464
|
+
handleError(out, err, 'Failed to fetch upcoming items');
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
addGlobalFlags(teamCalendar.command('due')
|
|
468
|
+
.description('List due documents for a team')
|
|
469
|
+
.argument('<teamId>', 'Team ID')
|
|
470
|
+
.option('--status <status>', 'Filter by status (overdue|upcoming|all)'))
|
|
471
|
+
.action(async (teamId, _opts) => {
|
|
472
|
+
const flags = resolveFlags(_opts);
|
|
473
|
+
const out = createOutput(flags);
|
|
474
|
+
out.startSpinner('Fetching due documents...');
|
|
475
|
+
try {
|
|
476
|
+
const client = await getClientAsync();
|
|
477
|
+
const dueDocs = await client.teams.getDue(teamId, {
|
|
478
|
+
status: _opts.status,
|
|
479
|
+
});
|
|
480
|
+
out.stopSpinner();
|
|
481
|
+
out.list(dueDocs.map(d => ({
|
|
482
|
+
path: d.path,
|
|
483
|
+
title: d.title ?? '',
|
|
484
|
+
dueAt: d.dueAt,
|
|
485
|
+
overdue: d.overdue ? 'yes' : 'no',
|
|
486
|
+
})), {
|
|
487
|
+
emptyMessage: 'No due documents found.',
|
|
488
|
+
columns: [
|
|
489
|
+
{ key: 'path', header: 'Path' },
|
|
490
|
+
{ key: 'title', header: 'Title' },
|
|
491
|
+
{ key: 'dueAt', header: 'Due At' },
|
|
492
|
+
{ key: 'overdue', header: 'Overdue' },
|
|
493
|
+
],
|
|
494
|
+
textFn: (d) => `${chalk.cyan(String(d.path))} — due ${String(d.dueAt)} ${d.overdue === 'yes' ? chalk.red('[overdue]') : ''}`,
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
catch (err) {
|
|
498
|
+
handleError(out, err, 'Failed to fetch due documents');
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
addGlobalFlags(teamCalendar.command('agenda')
|
|
502
|
+
.description('View the team calendar agenda')
|
|
503
|
+
.argument('<teamId>', 'Team ID')
|
|
504
|
+
.option('--range <days>', 'Number of days to include')
|
|
505
|
+
.option('--group-by <period>', 'Group by period (day|week|month)'))
|
|
506
|
+
.action(async (teamId, _opts) => {
|
|
507
|
+
const flags = resolveFlags(_opts);
|
|
508
|
+
const out = createOutput(flags);
|
|
509
|
+
out.startSpinner('Fetching team agenda...');
|
|
510
|
+
try {
|
|
511
|
+
const client = await getClientAsync();
|
|
512
|
+
const agenda = await client.teams.getAgenda(teamId, {
|
|
513
|
+
range: _opts.range,
|
|
514
|
+
groupBy: _opts.groupBy,
|
|
515
|
+
});
|
|
516
|
+
out.stopSpinner();
|
|
517
|
+
if (flags.output === 'json') {
|
|
518
|
+
out.raw(JSON.stringify(agenda, null, 2) + '\n');
|
|
519
|
+
}
|
|
520
|
+
else {
|
|
521
|
+
process.stdout.write(`Total items: ${chalk.cyan(String(agenda.total))}\n`);
|
|
522
|
+
for (const group of agenda.groups ?? []) {
|
|
523
|
+
process.stdout.write(`\n${chalk.bold(String(group.label))}:\n`);
|
|
524
|
+
for (const item of group.items ?? []) {
|
|
525
|
+
process.stdout.write(` ${chalk.cyan(String(item.title ?? item.path))} — ${String(item.dueAt ?? '')}\n`);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
catch (err) {
|
|
531
|
+
handleError(out, err, 'Failed to fetch team agenda');
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
addGlobalFlags(teamCalendar.command('ical')
|
|
535
|
+
.description('Get the iCal feed for a team calendar')
|
|
536
|
+
.argument('<teamId>', 'Team ID'))
|
|
537
|
+
.action(async (teamId, _opts) => {
|
|
538
|
+
const flags = resolveFlags(_opts);
|
|
539
|
+
const out = createOutput(flags);
|
|
540
|
+
out.startSpinner('Fetching iCal feed...');
|
|
541
|
+
try {
|
|
542
|
+
const client = await getClientAsync();
|
|
543
|
+
const ical = await client.teams.getICalFeed(teamId);
|
|
544
|
+
out.stopSpinner();
|
|
545
|
+
out.raw(ical);
|
|
546
|
+
}
|
|
547
|
+
catch (err) {
|
|
548
|
+
handleError(out, err, 'Failed to fetch iCal feed');
|
|
549
|
+
}
|
|
550
|
+
});
|
|
322
551
|
}
|