@lifestreamdynamics/vault-cli 1.3.8 → 1.3.10

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.
@@ -5,6 +5,30 @@ import { createOutput, handleError } from '../utils/output.js';
5
5
  export function registerAiCommands(program) {
6
6
  const ai = program.command('ai').description('AI chat and document summarization');
7
7
  const sessions = ai.command('sessions').description('AI chat session management');
8
+ addGlobalFlags(sessions.command('create')
9
+ .description('Create a new AI chat session')
10
+ .option('--title <title>', 'Session title')
11
+ .option('--vault <vaultId>', 'Vault ID to scope the session'))
12
+ .action(async (_opts) => {
13
+ const flags = resolveFlags(_opts);
14
+ const out = createOutput(flags);
15
+ out.startSpinner('Creating AI session...');
16
+ try {
17
+ const client = await getClientAsync();
18
+ const session = await client.ai.createSession({
19
+ title: _opts.title ? String(_opts.title) : undefined,
20
+ vaultId: _opts.vault ? String(_opts.vault) : undefined,
21
+ });
22
+ out.success(`Session created: ${session.id}`, {
23
+ id: session.id,
24
+ title: session.title ?? 'Untitled',
25
+ vaultId: session.vaultId ?? null,
26
+ });
27
+ }
28
+ catch (err) {
29
+ handleError(out, err, 'Failed to create AI session');
30
+ }
31
+ });
8
32
  addGlobalFlags(sessions.command('list')
9
33
  .description('List AI chat sessions'))
10
34
  .action(async (_opts) => {
@@ -80,7 +104,11 @@ export function registerAiCommands(program) {
80
104
  addGlobalFlags(ai.command('chat')
81
105
  .description('Send a message in an AI chat session')
82
106
  .argument('<sessionId>', 'Session ID')
83
- .argument('<message>', 'Message to send'))
107
+ .argument('<message>', 'Message to send')
108
+ .addHelpText('after', `
109
+ EXAMPLES
110
+ lsvault ai chat <session-id> "What are the key points in my notes?"
111
+ lsvault ai chat <session-id> "Summarize recent changes" -o json`))
84
112
  .action(async (sessionId, message, _opts) => {
85
113
  const flags = resolveFlags(_opts);
86
114
  const out = createOutput(flags);
@@ -43,6 +43,7 @@ EXAMPLES
43
43
  const logPath = String(_opts.logPath || DEFAULT_LOG_PATH);
44
44
  if (!fs.existsSync(logPath)) {
45
45
  out.warn(`Audit log file not found at ${logPath}`);
46
+ process.exitCode = 1;
46
47
  if (flags.output === 'json') {
47
48
  process.stdout.write('[]\n');
48
49
  }
@@ -90,8 +91,8 @@ EXAMPLES
90
91
  }
91
92
  });
92
93
  addGlobalFlags(audit.command('export')
93
- .description('Export audit log entries to a CSV file or stdout')
94
- .option('--format <format>', 'Export format (csv)', 'csv')
94
+ .description('Export audit log entries to a CSV or JSON file or stdout')
95
+ .option('--format <format>', 'Export format (csv or json)', 'csv')
95
96
  .option('--file <file>', 'Output file path')
96
97
  .option('--status <code>', 'Filter by HTTP status code', parseInt)
97
98
  .option('--since <date>', 'Show entries since date (ISO 8601)')
@@ -101,8 +102,8 @@ EXAMPLES
101
102
  const flags = resolveFlags(_opts);
102
103
  const out = createOutput(flags);
103
104
  try {
104
- if (_opts.format !== 'csv') {
105
- out.error(`Unsupported format: ${String(_opts.format)}. Only 'csv' is supported.`);
105
+ if (_opts.format !== 'csv' && _opts.format !== 'json') {
106
+ out.error(`Unsupported format: ${String(_opts.format)}. Supported: csv, json`);
106
107
  process.exitCode = 2;
107
108
  return;
108
109
  }
@@ -140,6 +141,26 @@ EXAMPLES
140
141
  out.status('No audit log entries to export.');
141
142
  return;
142
143
  }
144
+ if (_opts.format === 'json') {
145
+ const jsonOutput = JSON.stringify(entries, null, 2);
146
+ if (_opts.file) {
147
+ const outputPath = String(_opts.file);
148
+ const outputDir = path.dirname(outputPath);
149
+ if (!fs.existsSync(outputDir)) {
150
+ fs.mkdirSync(outputDir, { recursive: true });
151
+ }
152
+ fs.writeFileSync(outputPath, jsonOutput, 'utf-8');
153
+ out.success(`Exported ${entries.length} entries to ${outputPath}`, {
154
+ entries: entries.length,
155
+ path: outputPath,
156
+ format: 'json',
157
+ });
158
+ }
159
+ else {
160
+ out.raw(jsonOutput + '\n');
161
+ }
162
+ return;
163
+ }
143
164
  const csv = logger.exportCsv(entries);
144
165
  if (_opts.file) {
145
166
  const outputPath = String(_opts.file);
@@ -5,6 +5,8 @@ import { loadConfigAsync, getCredentialManager } from '../config.js';
5
5
  import { getClientAsync } from '../client.js';
6
6
  import { migrateCredentials, hasPlaintextCredentials, checkAndPromptMigration } from '../lib/migration.js';
7
7
  import { promptPassword, promptMfaCode } from '../utils/prompt.js';
8
+ import { addGlobalFlags, resolveFlags } from '../utils/flags.js';
9
+ import { createOutput, handleError } from '../utils/output.js';
8
10
  export function registerAuthCommands(program) {
9
11
  const auth = program.command('auth').description('Authentication and credential management');
10
12
  auth.command('login')
@@ -186,41 +188,47 @@ EXAMPLES
186
188
  spinner.info('Migration skipped.');
187
189
  }
188
190
  });
189
- auth.command('whoami')
190
- .description('Show the currently authenticated user, plan, and API URL')
191
- .action(async () => {
191
+ addGlobalFlags(auth.command('whoami')
192
+ .description('Show the currently authenticated user, plan, and API URL'))
193
+ .action(async (_opts) => {
194
+ const flags = resolveFlags(_opts);
195
+ const out = createOutput(flags);
192
196
  const config = await loadConfigAsync();
193
- console.log(`API URL: ${config.apiUrl}`);
194
- console.log(`API Key: ${config.apiKey ? config.apiKey.slice(0, 12) + '...' : chalk.yellow('not set')}`);
195
- if (config.accessToken) {
196
- console.log(`Auth: ${chalk.green('JWT (email/password)')}`);
197
- }
198
197
  // Warn about plaintext credentials
199
198
  await checkAndPromptMigration(getCredentialManager());
200
- if (config.apiKey || config.accessToken) {
201
- const spinner = ora('Fetching user info...').start();
202
- try {
203
- const client = await getClientAsync();
204
- const user = await client.user.me();
205
- spinner.stop();
206
- console.log(`User: ${chalk.cyan(user.email)}`);
207
- console.log(`Name: ${user.displayName || chalk.dim('not set')}`);
208
- console.log(`Role: ${user.role}`);
209
- let plan = user.subscriptionTier;
210
- if (!plan) {
211
- try {
212
- const sub = await client.subscription.get();
213
- plan = sub.subscription.tier;
214
- }
215
- catch { /* API key may not have scope */ }
199
+ if (!config.apiKey && !config.accessToken) {
200
+ out.record({
201
+ apiUrl: config.apiUrl,
202
+ apiKey: null,
203
+ auth: 'none',
204
+ });
205
+ return;
206
+ }
207
+ out.startSpinner('Fetching user info...');
208
+ try {
209
+ const client = await getClientAsync();
210
+ const user = await client.user.me();
211
+ out.stopSpinner();
212
+ let plan = user.subscriptionTier;
213
+ if (!plan) {
214
+ try {
215
+ const sub = await client.subscription.get();
216
+ plan = sub.subscription.tier;
216
217
  }
217
- console.log(`Plan: ${plan ? chalk.green(plan) : chalk.dim('unknown')}`);
218
- }
219
- catch (err) {
220
- spinner.fail('Could not fetch user info');
221
- console.error(err instanceof Error ? err.message : String(err));
222
- process.exitCode = 1;
218
+ catch { /* API key may not have scope */ }
223
219
  }
220
+ out.record({
221
+ apiUrl: config.apiUrl,
222
+ apiKey: config.apiKey ? config.apiKey.slice(0, 12) + '...' : null,
223
+ auth: config.accessToken ? 'JWT (email/password)' : 'API key',
224
+ email: user.email,
225
+ displayName: user.displayName || null,
226
+ role: user.role,
227
+ plan: plan || 'unknown',
228
+ });
229
+ }
230
+ catch (err) {
231
+ handleError(out, err, 'Could not fetch user info');
224
232
  }
225
233
  });
226
234
  }
@@ -170,7 +170,7 @@ export function registerBookingCommands(program) {
170
170
  const client = await getClientAsync();
171
171
  await client.booking.deleteSlot(vaultId, slotId);
172
172
  out.stopSpinner();
173
- out.status(chalk.green(`Slot ${slotId} deleted.`));
173
+ out.success(`Slot ${slotId} deleted.`, { id: slotId, deleted: true });
174
174
  }
175
175
  catch (err) {
176
176
  handleError(out, err, 'Delete slot failed');
@@ -31,6 +31,7 @@ export function registerCalendarCommands(program) {
31
31
  out.startSpinner('Loading calendar...');
32
32
  try {
33
33
  vaultId = await resolveVaultId(vaultId);
34
+ out.debug(`API: GET calendar events for vault ${vaultId}`);
34
35
  const client = await getClientAsync();
35
36
  const response = await client.calendar.getActivity(vaultId, {
36
37
  start: _opts.start ?? getDefaultStart(),
@@ -134,9 +135,7 @@ export function registerCalendarCommands(program) {
134
135
  recurrence: _opts.recurrence || null,
135
136
  });
136
137
  out.stopSpinner();
137
- out.status(dateStr === 'clear'
138
- ? chalk.green(`Due date cleared for ${path}`)
139
- : chalk.green(`Due date set to ${dateStr} for ${path}`));
138
+ out.success(dateStr === 'clear' ? `Due date cleared for ${path}` : `Due date set to ${dateStr} for ${path}`, { path, dueDate: dateStr === 'clear' ? null : dateStr });
140
139
  }
141
140
  catch (err) {
142
141
  handleError(out, err, 'Set due date failed');
@@ -290,7 +289,7 @@ export function registerCalendarCommands(program) {
290
289
  const client = await getClientAsync();
291
290
  await client.calendar.deleteEvent(vaultId, eventId);
292
291
  out.stopSpinner();
293
- out.status(chalk.green(`Event ${eventId} deleted.`));
292
+ out.success(`Event ${eventId} deleted.`, { id: eventId, deleted: true });
294
293
  }
295
294
  catch (err) {
296
295
  handleError(out, err, 'Delete event failed');
@@ -809,7 +808,7 @@ NOTE
809
808
  const client = await getClientAsync();
810
809
  await client.calendar.removeParticipant(vaultId, eventId, participantId);
811
810
  out.stopSpinner();
812
- out.status(chalk.green(`Participant ${participantId} removed.`));
811
+ out.success(`Participant ${participantId} removed.`, { id: participantId, eventId, removed: true });
813
812
  }
814
813
  catch (err) {
815
814
  handleError(out, err, 'Remove participant failed');
@@ -84,6 +84,13 @@ EXAMPLES
84
84
  lsvault config use prod
85
85
  lsvault config use dev`)
86
86
  .action((name) => {
87
+ const profiles = listProfiles();
88
+ if (!profiles.includes(name)) {
89
+ process.stderr.write(chalk.red(`Profile '${name}' does not exist.`) + '\n');
90
+ process.stderr.write(chalk.dim('Available profiles: ' + (profiles.length ? profiles.join(', ') : 'none')) + '\n');
91
+ process.exitCode = 1;
92
+ return;
93
+ }
87
94
  setActiveProfile(name);
88
95
  process.stdout.write(chalk.green(`Active profile set to ${chalk.bold(name)}`) + '\n');
89
96
  });
@@ -14,6 +14,7 @@ export function registerConnectorCommands(program) {
14
14
  const out = createOutput(flags);
15
15
  out.startSpinner('Fetching connectors...');
16
16
  try {
17
+ out.debug('API: GET connectors');
17
18
  if (_opts.vault)
18
19
  _opts.vault = await resolveVaultId(String(_opts.vault));
19
20
  const client = await getClientAsync();
@@ -205,12 +206,17 @@ VALID PROVIDERS
205
206
  const out = createOutput(flags);
206
207
  out.startSpinner('Triggering sync...');
207
208
  try {
209
+ out.debug(`API: POST connectors/${connectorId}/sync`);
208
210
  const client = await getClientAsync();
209
211
  const result = await client.connectors.sync(connectorId);
210
212
  out.success(result.message, { message: result.message });
211
213
  }
212
214
  catch (err) {
213
215
  handleError(out, err, 'Failed to trigger sync');
216
+ const msg = err instanceof Error ? err.message : String(err);
217
+ if (/oauth|token|not authorized|unauthorized/i.test(msg)) {
218
+ out.status(chalk.dim('Hint: Ensure the connector has valid OAuth credentials. Run "lsvault connectors create" to set up.'));
219
+ }
214
220
  }
215
221
  });
216
222
  addGlobalFlags(connectors.command('logs')
@@ -100,9 +100,37 @@ export function registerLinkCommands(program) {
100
100
  process.stdout.write(JSON.stringify({ nodes: graph.nodes, edges: graph.edges }) + '\n');
101
101
  }
102
102
  else {
103
- process.stdout.write(chalk.bold(`Nodes: ${graph.nodes.length} Edges: ${graph.edges.length}\n`));
103
+ process.stdout.write(chalk.bold(`Nodes: ${graph.nodes.length} Edges: ${graph.edges.length}\n\n`));
104
+ // Most connected nodes (top 5)
105
+ const connectionCounts = new Map();
104
106
  for (const node of graph.nodes) {
105
- process.stdout.write(` ${chalk.cyan(String(node.path ?? node.id))}\n`);
107
+ connectionCounts.set(node.id, 0);
108
+ }
109
+ for (const edge of graph.edges) {
110
+ connectionCounts.set(edge.source, (connectionCounts.get(edge.source) ?? 0) + 1);
111
+ connectionCounts.set(edge.target, (connectionCounts.get(edge.target) ?? 0) + 1);
112
+ }
113
+ const sorted = [...connectionCounts.entries()].sort((a, b) => b[1] - a[1]);
114
+ const topConnected = sorted.slice(0, 5).filter(([, count]) => count > 0);
115
+ if (topConnected.length > 0) {
116
+ process.stdout.write(chalk.bold('Most connected:\n'));
117
+ for (const [nodeId, count] of topConnected) {
118
+ const node = graph.nodes.find((n) => n.id === nodeId);
119
+ process.stdout.write(` ${chalk.cyan(String(node?.path ?? nodeId))} (${count} links)\n`);
120
+ }
121
+ process.stdout.write('\n');
122
+ }
123
+ // Orphan nodes (no connections)
124
+ const orphans = sorted.filter(([, count]) => count === 0);
125
+ if (orphans.length > 0) {
126
+ process.stdout.write(chalk.bold(`Orphan nodes (${orphans.length}):\n`));
127
+ for (const [nodeId] of orphans.slice(0, 10)) {
128
+ const node = graph.nodes.find((n) => n.id === nodeId);
129
+ process.stdout.write(` ${chalk.dim(String(node?.path ?? nodeId))}\n`);
130
+ }
131
+ if (orphans.length > 10) {
132
+ process.stdout.write(chalk.dim(` ... and ${orphans.length - 10} more\n`));
133
+ }
106
134
  }
107
135
  }
108
136
  }
@@ -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 { resolveVaultId } from '../utils/resolve-vault.js';
6
+ import { loadConfigAsync } from '../config.js';
6
7
  export function registerPublishCommands(program) {
7
8
  const publish = program.command('publish').description('Publish documents to public profile pages');
8
9
  addGlobalFlags(publish.command('list')
@@ -63,6 +64,7 @@ export function registerPublishCommands(program) {
63
64
  out.startSpinner('Publishing document...');
64
65
  try {
65
66
  vaultId = await resolveVaultId(vaultId);
67
+ out.debug(`API: POST publish ${vaultId}/${docPath}`);
66
68
  const client = await getClientAsync();
67
69
  const params = {
68
70
  slug: String(_opts.slug),
@@ -74,9 +76,11 @@ export function registerPublishCommands(program) {
74
76
  if (_opts.ogImage)
75
77
  params.ogImage = String(_opts.ogImage);
76
78
  const pub = await client.publish.create(vaultId, docPath, params);
79
+ const config = await loadConfigAsync();
80
+ const baseUrl = config.apiUrl.replace(/\/api\/v\d+\/?$/, '');
77
81
  out.success('Document published successfully!', {
78
82
  slug: pub.slug,
79
- url: `/${pub.publishedBy}/${pub.slug}`,
83
+ url: `${baseUrl}/${pub.publishedBy}/${pub.slug}`,
80
84
  isPublished: pub.isPublished,
81
85
  seoTitle: pub.seoTitle || null,
82
86
  seoDescription: pub.seoDescription || null,
@@ -160,13 +164,16 @@ export function registerPublishCommands(program) {
160
164
  const result = await client.publish.getSubdomain(vaultId);
161
165
  out.stopSpinner();
162
166
  if (flags.output === 'json') {
163
- out.record({ subdomain: result.subdomain });
167
+ out.raw(JSON.stringify(result, null, 2) + '\n');
164
168
  }
165
169
  else if (result.subdomain == null) {
166
170
  out.status('No subdomain configured.');
167
171
  }
168
172
  else {
169
- out.record({ subdomain: result.subdomain });
173
+ out.record({
174
+ subdomain: result.subdomain,
175
+ url: `https://${result.subdomain}.lifestreamdynamics.com`,
176
+ });
170
177
  }
171
178
  }
172
179
  catch (err) {
@@ -156,12 +156,14 @@ export function registerSamlCommands(program) {
156
156
  addGlobalFlags(saml.command('delete-config')
157
157
  .description('Delete an SSO configuration')
158
158
  .argument('<id>', 'SSO config ID')
159
- .option('--force', 'Skip confirmation prompt'))
159
+ .option('--force', 'Skip confirmation prompt')
160
+ .option('-y, --yes', 'Alias for --force'))
160
161
  .action(async (id, _opts) => {
161
162
  const flags = resolveFlags(_opts);
162
163
  const out = createOutput(flags);
163
- if (!_opts.force) {
164
- out.raw(chalk.yellow(`Pass --force to delete SSO config ${id}.`) + '\n');
164
+ if (!_opts.force && !_opts.yes) {
165
+ out.warn(`Pass --force to delete SSO config ${id}.`);
166
+ process.exitCode = 1;
165
167
  return;
166
168
  }
167
169
  out.startSpinner('Deleting SSO config...');
@@ -205,6 +207,11 @@ export function registerSamlCommands(program) {
205
207
  .action(async (slug, _opts) => {
206
208
  const flags = resolveFlags(_opts);
207
209
  const out = createOutput(flags);
210
+ if (!slug || !slug.trim()) {
211
+ out.error('Slug cannot be empty.');
212
+ process.exitCode = 1;
213
+ return;
214
+ }
208
215
  try {
209
216
  const client = await getClientAsync();
210
217
  const url = client.saml.getLoginUrl(slug);
@@ -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 { confirmAction } from '../utils/confirm.js';
5
6
  export function registerTeamCommands(program) {
6
7
  const teams = program.command('teams').description('Manage teams, members, invitations, and shared vaults');
7
8
  // ── Team CRUD ──────────────────────────────────────────────────────
@@ -12,8 +13,10 @@ export function registerTeamCommands(program) {
12
13
  const out = createOutput(flags);
13
14
  out.startSpinner('Fetching teams...');
14
15
  try {
16
+ out.debug('API: GET teams');
15
17
  const client = await getClientAsync();
16
18
  const teamList = await client.teams.list();
19
+ out.debug(`Response: ${teamList.length} teams`);
17
20
  out.stopSpinner();
18
21
  out.list(teamList.map(t => ({ name: t.name, id: t.id, description: t.description || 'No description' })), {
19
22
  emptyMessage: 'No teams found.',
@@ -152,7 +155,7 @@ EXAMPLES
152
155
  .description('Update a member role')
153
156
  .argument('<teamId>', 'Team ID')
154
157
  .argument('<userId>', 'User ID')
155
- .requiredOption('-r, --role <role>', 'New role (admin or member)'))
158
+ .requiredOption('-r, --role <role>', 'New role: admin, editor, or viewer'))
156
159
  .action(async (teamId, userId, _opts) => {
157
160
  const flags = resolveFlags(_opts);
158
161
  const out = createOutput(flags);
@@ -195,10 +198,16 @@ EXAMPLES
195
198
  });
196
199
  addGlobalFlags(teams.command('leave')
197
200
  .description('Leave a team')
198
- .argument('<teamId>', 'Team ID'))
201
+ .argument('<teamId>', 'Team ID')
202
+ .option('-y, --yes', 'Skip confirmation prompt'))
199
203
  .action(async (teamId, _opts) => {
200
204
  const flags = resolveFlags(_opts);
201
205
  const out = createOutput(flags);
206
+ const confirmed = await confirmAction(`Are you sure you want to leave team ${teamId}?`, { yes: !!_opts.yes });
207
+ if (!confirmed) {
208
+ out.status('Cancelled.');
209
+ return;
210
+ }
202
211
  out.startSpinner('Leaving team...');
203
212
  try {
204
213
  const client = await getClientAsync();
@@ -245,11 +254,11 @@ EXAMPLES
245
254
  .description('Invite a user to the team')
246
255
  .argument('<teamId>', 'Team ID')
247
256
  .argument('<email>', 'Email address')
248
- .requiredOption('-r, --role <role>', 'Role (admin or member)'))
257
+ .requiredOption('-r, --role <role>', 'Role to assign: admin, editor, or viewer (default: editor)'))
249
258
  .action(async (teamId, email, _opts) => {
250
259
  const flags = resolveFlags(_opts);
251
260
  const out = createOutput(flags);
252
- const role = String(_opts.role);
261
+ const role = (String(_opts.role) || 'editor');
253
262
  out.startSpinner('Sending invitation...');
254
263
  try {
255
264
  const client = await getClientAsync();
@@ -267,10 +276,16 @@ EXAMPLES
267
276
  addGlobalFlags(invitations.command('revoke')
268
277
  .description('Revoke a pending invitation')
269
278
  .argument('<teamId>', 'Team ID')
270
- .argument('<invitationId>', 'Invitation ID'))
279
+ .argument('<invitationId>', 'Invitation ID')
280
+ .option('-y, --yes', 'Skip confirmation prompt'))
271
281
  .action(async (teamId, invitationId, _opts) => {
272
282
  const flags = resolveFlags(_opts);
273
283
  const out = createOutput(flags);
284
+ const confirmed = await confirmAction(`Revoke invitation ${invitationId}?`, { yes: !!_opts.yes });
285
+ if (!confirmed) {
286
+ out.status('Cancelled.');
287
+ return;
288
+ }
274
289
  out.startSpinner('Revoking invitation...');
275
290
  try {
276
291
  const client = await getClientAsync();
@@ -16,11 +16,13 @@ export function registerVaultCommands(program) {
16
16
  const out = createOutput(flags);
17
17
  out.startSpinner('Fetching vaults...');
18
18
  try {
19
+ out.debug('API: GET vaults');
19
20
  const client = await getClientAsync();
20
21
  const vaultList = await client.vaults.list({
21
22
  includeArchived: _opts.includeArchived === true,
22
23
  });
23
24
  out.stopSpinner();
25
+ out.debug(`Response: ${vaultList.length} vaults`);
24
26
  out.list(vaultList.map(v => ({ name: v.name, slug: v.slug, encrypted: v.encryptionEnabled ? 'yes' : 'no', description: v.description ?? null, id: v.id })), {
25
27
  emptyMessage: 'No vaults found.',
26
28
  columns: [
@@ -167,10 +169,12 @@ EXAMPLES
167
169
  // vault tree
168
170
  addGlobalFlags(vaults.command('tree')
169
171
  .description('Show vault file tree')
170
- .argument('<vaultId>', 'Vault ID or slug'))
172
+ .argument('<vaultId>', 'Vault ID or slug')
173
+ .option('--depth <n>', 'Maximum display depth (0 = root only)', parseInt))
171
174
  .action(async (vaultId, _opts) => {
172
175
  const flags = resolveFlags(_opts);
173
176
  const out = createOutput(flags);
177
+ const maxDepth = _opts.depth;
174
178
  out.startSpinner('Fetching vault tree...');
175
179
  try {
176
180
  vaultId = await resolveVaultId(vaultId);
@@ -182,6 +186,8 @@ EXAMPLES
182
186
  }
183
187
  else {
184
188
  function printNode(node, depth) {
189
+ if (maxDepth !== undefined && depth > maxDepth)
190
+ return;
185
191
  const indent = ' '.repeat(depth);
186
192
  const icon = node.type === 'directory' ? chalk.yellow('📁') : chalk.cyan('📄');
187
193
  process.stdout.write(`${indent}${icon} ${node.name}\n`);
@@ -146,7 +146,8 @@ EXAMPLES
146
146
  .argument('<version>', 'Version number to restore')
147
147
  .addHelpText('after', `
148
148
  EXAMPLES
149
- lsvault versions restore abc123 notes/todo.md 2`))
149
+ lsvault versions restore abc123 notes/todo.md 2
150
+ lsvault versions restore abc123 notes/todo.md 2 --dry-run`))
150
151
  .action(async (vaultId, docPath, versionStr, _opts) => {
151
152
  const flags = resolveFlags(_opts);
152
153
  const out = createOutput(flags);
@@ -156,6 +157,31 @@ EXAMPLES
156
157
  process.exitCode = 1;
157
158
  return;
158
159
  }
160
+ if (flags.dryRun) {
161
+ out.startSpinner(`Fetching version ${versionNum} preview...`);
162
+ try {
163
+ vaultId = await resolveVaultId(vaultId);
164
+ const client = await getClientAsync();
165
+ const version = await client.documents.getVersion(vaultId, docPath, versionNum);
166
+ out.stopSpinner();
167
+ if (flags.output === 'json') {
168
+ out.raw(JSON.stringify({ dryRun: true, version: { version: version.versionNum, createdAt: version.createdAt, size: version.content?.length ?? 0 } }, null, 2) + '\n');
169
+ }
170
+ else {
171
+ process.stdout.write(chalk.bold('Dry run — no changes made\n\n'));
172
+ process.stdout.write(`Version: ${version.versionNum}\n`);
173
+ process.stdout.write(`Created: ${version.createdAt}\n`);
174
+ if (version.content) {
175
+ const preview = version.content.slice(0, 200);
176
+ process.stdout.write(`Content preview:\n${chalk.dim(preview)}${version.content.length > 200 ? '...' : ''}\n`);
177
+ }
178
+ }
179
+ }
180
+ catch (err) {
181
+ handleError(out, err, 'Failed to preview version');
182
+ }
183
+ return;
184
+ }
159
185
  out.startSpinner(`Restoring to version ${versionNum}...`);
160
186
  try {
161
187
  vaultId = await resolveVaultId(vaultId);
@@ -118,9 +118,9 @@ export class Output {
118
118
  * - table: prints an ASCII table
119
119
  */
120
120
  list(data, options) {
121
+ if (this.flags.quiet)
122
+ return;
121
123
  if (data.length === 0) {
122
- if (this.flags.quiet)
123
- return;
124
124
  if (this.flags.output === 'json') {
125
125
  process.stdout.write('[]\n');
126
126
  return;
@@ -133,8 +133,6 @@ export class Output {
133
133
  }
134
134
  return;
135
135
  }
136
- if (this.flags.quiet)
137
- return;
138
136
  switch (this.flags.output) {
139
137
  case 'json':
140
138
  process.stdout.write(JSON.stringify(data) + '\n');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lifestreamdynamics/vault-cli",
3
- "version": "1.3.8",
3
+ "version": "1.3.10",
4
4
  "description": "Command-line interface for Lifestream Vault",
5
5
  "engines": {
6
6
  "node": ">=22"