@lifestreamdynamics/vault-cli 1.2.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.
Files changed (50) hide show
  1. package/README.md +140 -30
  2. package/dist/client.d.ts +4 -0
  3. package/dist/client.js +12 -11
  4. package/dist/commands/admin.js +5 -5
  5. package/dist/commands/ai.js +17 -4
  6. package/dist/commands/auth.js +10 -105
  7. package/dist/commands/booking.d.ts +2 -0
  8. package/dist/commands/booking.js +739 -0
  9. package/dist/commands/calendar.js +725 -6
  10. package/dist/commands/completion.d.ts +5 -0
  11. package/dist/commands/completion.js +60 -0
  12. package/dist/commands/config.js +17 -16
  13. package/dist/commands/connectors.js +12 -1
  14. package/dist/commands/custom-domains.js +6 -1
  15. package/dist/commands/docs.js +12 -5
  16. package/dist/commands/hooks.js +6 -1
  17. package/dist/commands/links.js +9 -2
  18. package/dist/commands/mfa.js +1 -70
  19. package/dist/commands/plugins.d.ts +2 -0
  20. package/dist/commands/plugins.js +172 -0
  21. package/dist/commands/publish.js +13 -3
  22. package/dist/commands/saml.d.ts +2 -0
  23. package/dist/commands/saml.js +220 -0
  24. package/dist/commands/scim.d.ts +2 -0
  25. package/dist/commands/scim.js +238 -0
  26. package/dist/commands/shares.js +25 -3
  27. package/dist/commands/subscription.js +9 -2
  28. package/dist/commands/sync.js +3 -0
  29. package/dist/commands/teams.js +141 -8
  30. package/dist/commands/user.js +122 -9
  31. package/dist/commands/vaults.js +17 -8
  32. package/dist/commands/webhooks.js +6 -1
  33. package/dist/config.d.ts +2 -0
  34. package/dist/config.js +7 -3
  35. package/dist/index.js +20 -1
  36. package/dist/lib/credential-manager.js +32 -7
  37. package/dist/lib/migration.js +2 -2
  38. package/dist/lib/profiles.js +4 -4
  39. package/dist/sync/config.js +2 -2
  40. package/dist/sync/daemon-worker.js +13 -6
  41. package/dist/sync/daemon.js +2 -1
  42. package/dist/sync/remote-poller.js +7 -3
  43. package/dist/sync/state.js +2 -2
  44. package/dist/utils/confirm.d.ts +11 -0
  45. package/dist/utils/confirm.js +23 -0
  46. package/dist/utils/format.js +1 -1
  47. package/dist/utils/output.js +4 -1
  48. package/dist/utils/prompt.d.ts +29 -0
  49. package/dist/utils/prompt.js +146 -0
  50. package/package.json +2 -2
@@ -0,0 +1,220 @@
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 registerSamlCommands(program) {
6
+ const saml = program.command('saml').description('SAML SSO configuration management (requires admin role)');
7
+ // ── list-configs ─────────────────────────────────────────────────────────
8
+ addGlobalFlags(saml.command('list-configs')
9
+ .description('List all SSO configurations'))
10
+ .action(async (_opts) => {
11
+ const flags = resolveFlags(_opts);
12
+ const out = createOutput(flags);
13
+ out.startSpinner('Fetching SSO configs...');
14
+ try {
15
+ const client = await getClientAsync();
16
+ const configs = await client.saml.listConfigs();
17
+ out.stopSpinner();
18
+ if (configs.length === 0 && flags.output !== 'json') {
19
+ out.raw('No SSO configurations found.\n');
20
+ }
21
+ else {
22
+ out.list(configs.map((c) => ({
23
+ id: c.id,
24
+ domain: c.domain,
25
+ slug: c.slug,
26
+ entityId: c.entityId,
27
+ ssoUrl: c.ssoUrl,
28
+ })), {
29
+ columns: [
30
+ { key: 'id', header: 'ID' },
31
+ { key: 'domain', header: 'Domain' },
32
+ { key: 'slug', header: 'Slug' },
33
+ { key: 'entityId', header: 'Entity ID' },
34
+ { key: 'ssoUrl', header: 'SSO URL' },
35
+ ],
36
+ textFn: (c) => `${chalk.cyan(String(c.domain))} ${chalk.dim(`[${String(c.slug)}]`)} — ${chalk.dim(String(c.ssoUrl))}`,
37
+ });
38
+ }
39
+ }
40
+ catch (err) {
41
+ handleError(out, err, 'Failed to fetch SSO configs');
42
+ }
43
+ });
44
+ // ── get-config ───────────────────────────────────────────────────────────
45
+ addGlobalFlags(saml.command('get-config')
46
+ .description('Get a single SSO configuration by ID')
47
+ .argument('<id>', 'SSO config ID'))
48
+ .action(async (id, _opts) => {
49
+ const flags = resolveFlags(_opts);
50
+ const out = createOutput(flags);
51
+ out.startSpinner('Fetching SSO config...');
52
+ try {
53
+ const client = await getClientAsync();
54
+ const config = await client.saml.getConfig(id);
55
+ out.stopSpinner();
56
+ out.record({
57
+ id: config.id,
58
+ domain: config.domain,
59
+ slug: config.slug,
60
+ entityId: config.entityId,
61
+ ssoUrl: config.ssoUrl,
62
+ spEntityId: config.spEntityId,
63
+ createdAt: config.createdAt,
64
+ updatedAt: config.updatedAt,
65
+ });
66
+ }
67
+ catch (err) {
68
+ handleError(out, err, 'Failed to fetch SSO config');
69
+ }
70
+ });
71
+ // ── create-config ─────────────────────────────────────────────────────────
72
+ addGlobalFlags(saml.command('create-config')
73
+ .description('Create a new SSO configuration')
74
+ .requiredOption('--domain <domain>', 'Customer/tenant domain (e.g. acmecorp.com)')
75
+ .requiredOption('--slug <slug>', 'URL slug for SAML endpoints (e.g. acmecorp)')
76
+ .requiredOption('--entity-id <entityId>', 'Identity Provider entity ID URI')
77
+ .requiredOption('--sso-url <ssoUrl>', 'Identity Provider Single Sign-On URL')
78
+ .requiredOption('--certificate <cert>', 'X.509 certificate (PEM-encoded)')
79
+ .option('--sp-entity-id <spEntityId>', 'Optional Service Provider entity ID override'))
80
+ .action(async (_opts) => {
81
+ const flags = resolveFlags(_opts);
82
+ const out = createOutput(flags);
83
+ out.startSpinner('Creating SSO config...');
84
+ try {
85
+ const client = await getClientAsync();
86
+ const created = await client.saml.createConfig({
87
+ domain: _opts.domain,
88
+ slug: _opts.slug,
89
+ entityId: _opts.entityId,
90
+ ssoUrl: _opts.ssoUrl,
91
+ certificate: _opts.certificate,
92
+ spEntityId: _opts.spEntityId,
93
+ });
94
+ out.stopSpinner();
95
+ if (flags.output === 'json') {
96
+ out.raw(JSON.stringify(created, null, 2) + '\n');
97
+ }
98
+ else {
99
+ out.raw(chalk.green(`SSO config created for ${created.domain} (${created.id})`) + '\n');
100
+ }
101
+ }
102
+ catch (err) {
103
+ handleError(out, err, 'Failed to create SSO config');
104
+ }
105
+ });
106
+ // ── update-config ─────────────────────────────────────────────────────────
107
+ addGlobalFlags(saml.command('update-config')
108
+ .description('Update an existing SSO configuration')
109
+ .argument('<id>', 'SSO config ID')
110
+ .option('--domain <domain>', 'Updated customer domain')
111
+ .option('--slug <slug>', 'Updated URL slug')
112
+ .option('--entity-id <entityId>', 'Updated Identity Provider entity ID')
113
+ .option('--sso-url <ssoUrl>', 'Updated Identity Provider SSO URL')
114
+ .option('--certificate <cert>', 'Updated X.509 certificate')
115
+ .option('--sp-entity-id <spEntityId>', 'Updated Service Provider entity ID'))
116
+ .action(async (id, _opts) => {
117
+ const flags = resolveFlags(_opts);
118
+ const out = createOutput(flags);
119
+ const data = {};
120
+ if (_opts.domain)
121
+ data.domain = _opts.domain;
122
+ if (_opts.slug)
123
+ data.slug = _opts.slug;
124
+ if (_opts.entityId)
125
+ data.entityId = _opts.entityId;
126
+ if (_opts.ssoUrl)
127
+ data.ssoUrl = _opts.ssoUrl;
128
+ if (_opts.certificate)
129
+ data.certificate = _opts.certificate;
130
+ if (_opts.spEntityId)
131
+ data.spEntityId = _opts.spEntityId;
132
+ if (Object.keys(data).length === 0) {
133
+ out.error('No updates specified. Use --domain, --slug, --entity-id, --sso-url, --certificate, or --sp-entity-id.');
134
+ process.exitCode = 2;
135
+ return;
136
+ }
137
+ out.startSpinner('Updating SSO config...');
138
+ try {
139
+ const client = await getClientAsync();
140
+ const updated = await client.saml.updateConfig(id, data);
141
+ out.stopSpinner();
142
+ if (flags.output === 'json') {
143
+ out.raw(JSON.stringify(updated, null, 2) + '\n');
144
+ }
145
+ else {
146
+ out.raw(chalk.green(`SSO config updated: ${updated.domain} (${updated.id})`) + '\n');
147
+ }
148
+ }
149
+ catch (err) {
150
+ handleError(out, err, 'Failed to update SSO config');
151
+ }
152
+ });
153
+ // ── delete-config ─────────────────────────────────────────────────────────
154
+ addGlobalFlags(saml.command('delete-config')
155
+ .description('Delete an SSO configuration')
156
+ .argument('<id>', 'SSO config ID')
157
+ .option('--force', 'Skip confirmation prompt'))
158
+ .action(async (id, _opts) => {
159
+ const flags = resolveFlags(_opts);
160
+ const out = createOutput(flags);
161
+ if (!_opts.force) {
162
+ out.raw(chalk.yellow(`Pass --force to delete SSO config ${id}.`) + '\n');
163
+ return;
164
+ }
165
+ out.startSpinner('Deleting SSO config...');
166
+ try {
167
+ const client = await getClientAsync();
168
+ await client.saml.deleteConfig(id);
169
+ out.stopSpinner();
170
+ out.raw(chalk.green(`SSO config ${id} deleted.`) + '\n');
171
+ }
172
+ catch (err) {
173
+ handleError(out, err, 'Failed to delete SSO config');
174
+ }
175
+ });
176
+ // ── metadata ──────────────────────────────────────────────────────────────
177
+ addGlobalFlags(saml.command('metadata')
178
+ .description('Show Service Provider metadata XML for an IdP slug')
179
+ .argument('<slug>', 'IdP slug'))
180
+ .action(async (slug, _opts) => {
181
+ const flags = resolveFlags(_opts);
182
+ const out = createOutput(flags);
183
+ out.startSpinner('Fetching SP metadata...');
184
+ try {
185
+ const client = await getClientAsync();
186
+ const xml = await client.saml.getMetadata(slug);
187
+ out.stopSpinner();
188
+ if (flags.output === 'json') {
189
+ out.raw(JSON.stringify({ xml }, null, 2) + '\n');
190
+ }
191
+ else {
192
+ out.raw(xml + '\n');
193
+ }
194
+ }
195
+ catch (err) {
196
+ handleError(out, err, 'Failed to fetch SP metadata');
197
+ }
198
+ });
199
+ // ── login-url ─────────────────────────────────────────────────────────────
200
+ addGlobalFlags(saml.command('login-url')
201
+ .description('Show the IdP login redirect URL for a slug')
202
+ .argument('<slug>', 'IdP slug'))
203
+ .action(async (slug, _opts) => {
204
+ const flags = resolveFlags(_opts);
205
+ const out = createOutput(flags);
206
+ try {
207
+ const client = await getClientAsync();
208
+ const url = client.saml.getLoginUrl(slug);
209
+ if (flags.output === 'json') {
210
+ out.raw(JSON.stringify({ url }, null, 2) + '\n');
211
+ }
212
+ else {
213
+ out.raw(url + '\n');
214
+ }
215
+ }
216
+ catch (err) {
217
+ handleError(out, err, 'Failed to build login URL');
218
+ }
219
+ });
220
+ }
@@ -0,0 +1,2 @@
1
+ import type { Command } from 'commander';
2
+ export declare function registerScimCommands(program: Command): void;
@@ -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
+ }
@@ -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 <password>', 'Password to protect the link')
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 (_opts.password)
71
- params.password = String(_opts.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 });
@@ -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;