@lifestreamdynamics/vault-cli 1.0.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 (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +759 -0
  3. package/dist/client.d.ts +12 -0
  4. package/dist/client.js +79 -0
  5. package/dist/commands/admin.d.ts +2 -0
  6. package/dist/commands/admin.js +263 -0
  7. package/dist/commands/audit.d.ts +2 -0
  8. package/dist/commands/audit.js +119 -0
  9. package/dist/commands/auth.d.ts +2 -0
  10. package/dist/commands/auth.js +256 -0
  11. package/dist/commands/config.d.ts +2 -0
  12. package/dist/commands/config.js +130 -0
  13. package/dist/commands/connectors.d.ts +2 -0
  14. package/dist/commands/connectors.js +224 -0
  15. package/dist/commands/docs.d.ts +2 -0
  16. package/dist/commands/docs.js +194 -0
  17. package/dist/commands/hooks.d.ts +2 -0
  18. package/dist/commands/hooks.js +159 -0
  19. package/dist/commands/keys.d.ts +2 -0
  20. package/dist/commands/keys.js +165 -0
  21. package/dist/commands/publish.d.ts +2 -0
  22. package/dist/commands/publish.js +138 -0
  23. package/dist/commands/search.d.ts +2 -0
  24. package/dist/commands/search.js +61 -0
  25. package/dist/commands/shares.d.ts +2 -0
  26. package/dist/commands/shares.js +121 -0
  27. package/dist/commands/subscription.d.ts +2 -0
  28. package/dist/commands/subscription.js +166 -0
  29. package/dist/commands/sync.d.ts +2 -0
  30. package/dist/commands/sync.js +565 -0
  31. package/dist/commands/teams.d.ts +2 -0
  32. package/dist/commands/teams.js +322 -0
  33. package/dist/commands/user.d.ts +2 -0
  34. package/dist/commands/user.js +48 -0
  35. package/dist/commands/vaults.d.ts +2 -0
  36. package/dist/commands/vaults.js +157 -0
  37. package/dist/commands/versions.d.ts +2 -0
  38. package/dist/commands/versions.js +219 -0
  39. package/dist/commands/webhooks.d.ts +2 -0
  40. package/dist/commands/webhooks.js +181 -0
  41. package/dist/config.d.ts +24 -0
  42. package/dist/config.js +88 -0
  43. package/dist/index.d.ts +2 -0
  44. package/dist/index.js +63 -0
  45. package/dist/lib/credential-manager.d.ts +48 -0
  46. package/dist/lib/credential-manager.js +101 -0
  47. package/dist/lib/encrypted-config.d.ts +20 -0
  48. package/dist/lib/encrypted-config.js +102 -0
  49. package/dist/lib/keychain.d.ts +8 -0
  50. package/dist/lib/keychain.js +82 -0
  51. package/dist/lib/migration.d.ts +31 -0
  52. package/dist/lib/migration.js +92 -0
  53. package/dist/lib/profiles.d.ts +43 -0
  54. package/dist/lib/profiles.js +104 -0
  55. package/dist/sync/config.d.ts +32 -0
  56. package/dist/sync/config.js +100 -0
  57. package/dist/sync/conflict.d.ts +30 -0
  58. package/dist/sync/conflict.js +60 -0
  59. package/dist/sync/daemon-worker.d.ts +1 -0
  60. package/dist/sync/daemon-worker.js +128 -0
  61. package/dist/sync/daemon.d.ts +44 -0
  62. package/dist/sync/daemon.js +174 -0
  63. package/dist/sync/diff.d.ts +43 -0
  64. package/dist/sync/diff.js +166 -0
  65. package/dist/sync/engine.d.ts +41 -0
  66. package/dist/sync/engine.js +233 -0
  67. package/dist/sync/ignore.d.ts +16 -0
  68. package/dist/sync/ignore.js +72 -0
  69. package/dist/sync/remote-poller.d.ts +23 -0
  70. package/dist/sync/remote-poller.js +145 -0
  71. package/dist/sync/state.d.ts +32 -0
  72. package/dist/sync/state.js +98 -0
  73. package/dist/sync/types.d.ts +68 -0
  74. package/dist/sync/types.js +4 -0
  75. package/dist/sync/watcher.d.ts +23 -0
  76. package/dist/sync/watcher.js +207 -0
  77. package/dist/utils/flags.d.ts +18 -0
  78. package/dist/utils/flags.js +31 -0
  79. package/dist/utils/format.d.ts +2 -0
  80. package/dist/utils/format.js +22 -0
  81. package/dist/utils/output.d.ts +87 -0
  82. package/dist/utils/output.js +229 -0
  83. package/package.json +62 -0
@@ -0,0 +1,194 @@
1
+ import chalk from 'chalk';
2
+ import { getClient } from '../client.js';
3
+ import { addGlobalFlags, resolveFlags } from '../utils/flags.js';
4
+ import { createOutput, handleError } from '../utils/output.js';
5
+ import { createCredentialManager } from '../lib/credential-manager.js';
6
+ export function registerDocCommands(program) {
7
+ const docs = program.command('docs').description('Read, write, move, and delete documents in a vault');
8
+ addGlobalFlags(docs.command('list')
9
+ .description('List documents in a vault, optionally filtered by directory')
10
+ .argument('<vaultId>', 'Vault ID')
11
+ .option('--dir <path>', 'Filter by directory path')
12
+ .addHelpText('after', `
13
+ EXAMPLES
14
+ lsvault docs list abc123
15
+ lsvault docs list abc123 --dir notes/meetings`))
16
+ .action(async (vaultId, _opts) => {
17
+ const flags = resolveFlags(_opts);
18
+ const out = createOutput(flags);
19
+ out.startSpinner('Fetching documents...');
20
+ try {
21
+ const client = getClient();
22
+ const documents = await client.documents.list(vaultId, _opts.dir);
23
+ out.stopSpinner();
24
+ out.list(documents.map(doc => ({
25
+ path: doc.path,
26
+ title: doc.title || '',
27
+ tags: Array.isArray(doc.tags) ? doc.tags.join(', ') : '',
28
+ sizeBytes: doc.sizeBytes,
29
+ })), {
30
+ emptyMessage: 'No documents found.',
31
+ columns: [
32
+ { key: 'path', header: 'Path' },
33
+ { key: 'title', header: 'Title' },
34
+ { key: 'tags', header: 'Tags' },
35
+ { key: 'sizeBytes', header: 'Size' },
36
+ ],
37
+ textFn: (doc) => {
38
+ const title = doc.title ? chalk.dim(` -- ${String(doc.title)}`) : '';
39
+ const tags = doc.tags ? chalk.blue(` [${String(doc.tags)}]`) : '';
40
+ return `${chalk.cyan(String(doc.path))}${title}${tags}`;
41
+ },
42
+ });
43
+ if (flags.output === 'text' && documents.length > 0) {
44
+ out.status(chalk.dim(`\n${documents.length} document(s)`));
45
+ }
46
+ }
47
+ catch (err) {
48
+ handleError(out, err, 'Failed to fetch documents');
49
+ }
50
+ });
51
+ addGlobalFlags(docs.command('get')
52
+ .description('Print document content to stdout, or show metadata with --meta')
53
+ .argument('<vaultId>', 'Vault ID')
54
+ .argument('<path>', 'Document path (e.g., notes/todo.md)')
55
+ .option('--meta', 'Show metadata instead of content')
56
+ .addHelpText('after', `
57
+ EXAMPLES
58
+ lsvault docs get abc123 notes/todo.md
59
+ lsvault docs get abc123 notes/todo.md --meta
60
+ lsvault docs get abc123 notes/todo.md > local-copy.md`))
61
+ .action(async (vaultId, docPath, _opts) => {
62
+ const flags = resolveFlags(_opts);
63
+ const out = createOutput(flags);
64
+ try {
65
+ const client = getClient();
66
+ const result = await client.documents.get(vaultId, docPath);
67
+ // Auto-decrypt if the document is encrypted
68
+ if (result.document.encrypted && !_opts.meta) {
69
+ const credManager = createCredentialManager();
70
+ const vaultKey = await credManager.getVaultKey(vaultId);
71
+ if (!vaultKey) {
72
+ out.error('Document is encrypted but no vault key found.');
73
+ out.status(chalk.dim('Import the key with: lsvault vaults import-key ' + vaultId + ' --key <key>'));
74
+ process.exitCode = 1;
75
+ return;
76
+ }
77
+ const decrypted = await client.documents.getEncrypted(vaultId, docPath, vaultKey);
78
+ out.raw(decrypted.content);
79
+ return;
80
+ }
81
+ if (_opts.meta) {
82
+ const doc = result.document;
83
+ out.record({
84
+ path: doc.path,
85
+ title: doc.title,
86
+ sizeBytes: doc.sizeBytes,
87
+ tags: Array.isArray(doc.tags) ? doc.tags.join(', ') : '',
88
+ encrypted: doc.encrypted ? 'yes' : 'no',
89
+ contentHash: doc.contentHash,
90
+ fileModifiedAt: doc.fileModifiedAt,
91
+ createdAt: doc.createdAt,
92
+ updatedAt: doc.updatedAt,
93
+ });
94
+ }
95
+ else {
96
+ out.raw(result.content);
97
+ }
98
+ }
99
+ catch (err) {
100
+ handleError(out, err, 'Failed to get document');
101
+ }
102
+ });
103
+ addGlobalFlags(docs.command('put')
104
+ .description('Create or update a document by reading content from stdin')
105
+ .argument('<vaultId>', 'Vault ID')
106
+ .argument('<path>', 'Document path (must end with .md)')
107
+ .addHelpText('after', `
108
+ EXAMPLES
109
+ echo "# Hello" | lsvault docs put abc123 notes/hello.md
110
+ cat local-file.md | lsvault docs put abc123 docs/imported.md`))
111
+ .action(async (vaultId, docPath, _opts) => {
112
+ const flags = resolveFlags(_opts);
113
+ const out = createOutput(flags);
114
+ out.startSpinner('Reading stdin...');
115
+ try {
116
+ const content = await new Promise((resolve) => {
117
+ let data = '';
118
+ process.stdin.on('data', (chunk) => data += chunk);
119
+ process.stdin.on('end', () => resolve(data));
120
+ });
121
+ out.startSpinner('Uploading document...');
122
+ const client = getClient();
123
+ // Check if vault is encrypted and auto-encrypt
124
+ const vault = await client.vaults.get(vaultId);
125
+ let doc;
126
+ if (vault.encryptionEnabled) {
127
+ const credManager = createCredentialManager();
128
+ const vaultKey = await credManager.getVaultKey(vaultId);
129
+ if (!vaultKey) {
130
+ out.failSpinner('Failed to save document');
131
+ out.error('Vault is encrypted but no vault key found.');
132
+ out.status(chalk.dim('Import the key with: lsvault vaults import-key ' + vaultId + ' --key <key>'));
133
+ process.exitCode = 1;
134
+ return;
135
+ }
136
+ doc = await client.documents.putEncrypted(vaultId, docPath, content, vaultKey);
137
+ }
138
+ else {
139
+ doc = await client.documents.put(vaultId, docPath, content);
140
+ }
141
+ out.success(`Document saved: ${chalk.cyan(doc.path)} (${doc.sizeBytes} bytes)`, {
142
+ path: doc.path,
143
+ sizeBytes: doc.sizeBytes,
144
+ encrypted: doc.encrypted,
145
+ });
146
+ }
147
+ catch (err) {
148
+ handleError(out, err, 'Failed to save document');
149
+ }
150
+ });
151
+ addGlobalFlags(docs.command('delete')
152
+ .description('Permanently delete a document from a vault')
153
+ .argument('<vaultId>', 'Vault ID')
154
+ .argument('<path>', 'Document path to delete'))
155
+ .action(async (vaultId, docPath, _opts) => {
156
+ const flags = resolveFlags(_opts);
157
+ const out = createOutput(flags);
158
+ out.startSpinner('Deleting document...');
159
+ try {
160
+ const client = getClient();
161
+ await client.documents.delete(vaultId, docPath);
162
+ out.success(`Deleted: ${chalk.cyan(docPath)}`, { path: docPath, deleted: true });
163
+ }
164
+ catch (err) {
165
+ handleError(out, err, 'Failed to delete document');
166
+ }
167
+ });
168
+ addGlobalFlags(docs.command('move')
169
+ .description('Move or rename a document within a vault')
170
+ .argument('<vaultId>', 'Vault ID')
171
+ .argument('<source>', 'Current document path')
172
+ .argument('<dest>', 'New document path')
173
+ .option('--overwrite', 'Overwrite if destination already exists')
174
+ .addHelpText('after', `
175
+ EXAMPLES
176
+ lsvault docs move abc123 notes/old.md notes/new.md
177
+ lsvault docs move abc123 draft.md published/final.md --overwrite`))
178
+ .action(async (vaultId, source, dest, _opts) => {
179
+ const flags = resolveFlags(_opts);
180
+ const out = createOutput(flags);
181
+ out.startSpinner('Moving document...');
182
+ try {
183
+ const client = getClient();
184
+ const result = await client.documents.move(vaultId, source, dest, _opts.overwrite);
185
+ out.success(`Moved: ${chalk.cyan(result.source)} -> ${chalk.cyan(result.destination)}`, {
186
+ source: result.source,
187
+ destination: result.destination,
188
+ });
189
+ }
190
+ catch (err) {
191
+ handleError(out, err, 'Failed to move document');
192
+ }
193
+ });
194
+ }
@@ -0,0 +1,2 @@
1
+ import type { Command } from 'commander';
2
+ export declare function registerHookCommands(program: Command): void;
@@ -0,0 +1,159 @@
1
+ import chalk from 'chalk';
2
+ import { getClient } from '../client.js';
3
+ import { addGlobalFlags, resolveFlags } from '../utils/flags.js';
4
+ import { createOutput, handleError } from '../utils/output.js';
5
+ export function registerHookCommands(program) {
6
+ const hooks = program.command('hooks').description('Manage vault event hooks (auto-tag, template, etc.)');
7
+ addGlobalFlags(hooks.command('list')
8
+ .description('List all hooks for a vault')
9
+ .argument('<vaultId>', 'Vault ID'))
10
+ .action(async (vaultId, _opts) => {
11
+ const flags = resolveFlags(_opts);
12
+ const out = createOutput(flags);
13
+ out.startSpinner('Fetching hooks...');
14
+ try {
15
+ const client = getClient();
16
+ const hookList = await client.hooks.list(vaultId);
17
+ out.stopSpinner();
18
+ out.list(hookList.map(hook => ({
19
+ name: hook.name,
20
+ id: hook.id,
21
+ triggerEvent: hook.triggerEvent,
22
+ actionType: hook.actionType,
23
+ isActive: hook.isActive,
24
+ })), {
25
+ emptyMessage: 'No hooks found.',
26
+ columns: [
27
+ { key: 'name', header: 'Name' },
28
+ { key: 'triggerEvent', header: 'Trigger' },
29
+ { key: 'actionType', header: 'Action' },
30
+ { key: 'isActive', header: 'Active' },
31
+ ],
32
+ textFn: (hook) => {
33
+ const lines = [chalk.cyan(` ${String(hook.name)}`)];
34
+ lines.push(` ID: ${String(hook.id)}`);
35
+ lines.push(` Trigger: ${String(hook.triggerEvent)}`);
36
+ lines.push(` Action: ${String(hook.actionType)}`);
37
+ lines.push(` Active: ${hook.isActive ? chalk.green('Yes') : chalk.red('No')}`);
38
+ return lines.join('\n');
39
+ },
40
+ });
41
+ }
42
+ catch (err) {
43
+ handleError(out, err, 'Failed to fetch hooks');
44
+ }
45
+ });
46
+ addGlobalFlags(hooks.command('create')
47
+ .description('Create a new hook')
48
+ .argument('<vaultId>', 'Vault ID')
49
+ .argument('<name>', 'Hook name')
50
+ .requiredOption('--trigger <event>', 'Trigger event (e.g., document.create)')
51
+ .requiredOption('--action <type>', 'Action type (e.g., auto-tag, template)')
52
+ .requiredOption('--config <json>', 'Action configuration as JSON')
53
+ .option('--filter <json>', 'Trigger filter as JSON'))
54
+ .action(async (vaultId, name, _opts) => {
55
+ const flags = resolveFlags(_opts);
56
+ const out = createOutput(flags);
57
+ let actionConfig;
58
+ try {
59
+ actionConfig = JSON.parse(String(_opts.config));
60
+ }
61
+ catch {
62
+ out.error('--config must be valid JSON');
63
+ process.exitCode = 2;
64
+ return;
65
+ }
66
+ let triggerFilter;
67
+ if (_opts.filter) {
68
+ try {
69
+ triggerFilter = JSON.parse(String(_opts.filter));
70
+ }
71
+ catch {
72
+ out.error('--filter must be valid JSON');
73
+ process.exitCode = 2;
74
+ return;
75
+ }
76
+ }
77
+ out.startSpinner('Creating hook...');
78
+ try {
79
+ const client = getClient();
80
+ const params = {
81
+ name,
82
+ triggerEvent: String(_opts.trigger),
83
+ actionType: String(_opts.action),
84
+ actionConfig,
85
+ };
86
+ if (triggerFilter)
87
+ params.triggerFilter = triggerFilter;
88
+ const hook = await client.hooks.create(vaultId, params);
89
+ out.success('Hook created successfully!', {
90
+ id: hook.id,
91
+ name: hook.name,
92
+ triggerEvent: hook.triggerEvent,
93
+ actionType: hook.actionType,
94
+ });
95
+ }
96
+ catch (err) {
97
+ handleError(out, err, 'Failed to create hook');
98
+ }
99
+ });
100
+ addGlobalFlags(hooks.command('delete')
101
+ .description('Delete a hook')
102
+ .argument('<vaultId>', 'Vault ID')
103
+ .argument('<hookId>', 'Hook ID'))
104
+ .action(async (vaultId, hookId, _opts) => {
105
+ const flags = resolveFlags(_opts);
106
+ const out = createOutput(flags);
107
+ out.startSpinner('Deleting hook...');
108
+ try {
109
+ const client = getClient();
110
+ await client.hooks.delete(vaultId, hookId);
111
+ out.success('Hook deleted successfully', { id: hookId, deleted: true });
112
+ }
113
+ catch (err) {
114
+ handleError(out, err, 'Failed to delete hook');
115
+ }
116
+ });
117
+ addGlobalFlags(hooks.command('executions')
118
+ .description('List recent executions for a hook')
119
+ .argument('<vaultId>', 'Vault ID')
120
+ .argument('<hookId>', 'Hook ID'))
121
+ .action(async (vaultId, hookId, _opts) => {
122
+ const flags = resolveFlags(_opts);
123
+ const out = createOutput(flags);
124
+ out.startSpinner('Fetching executions...');
125
+ try {
126
+ const client = getClient();
127
+ const executions = await client.hooks.listExecutions(vaultId, hookId);
128
+ out.stopSpinner();
129
+ out.list(executions.map(exec => ({
130
+ id: exec.id,
131
+ status: exec.status,
132
+ durationMs: exec.durationMs,
133
+ error: exec.error || null,
134
+ createdAt: exec.createdAt,
135
+ })), {
136
+ emptyMessage: 'No executions found.',
137
+ columns: [
138
+ { key: 'status', header: 'Status' },
139
+ { key: 'id', header: 'ID' },
140
+ { key: 'durationMs', header: 'Duration (ms)' },
141
+ { key: 'createdAt', header: 'Time' },
142
+ ],
143
+ textFn: (exec) => {
144
+ const statusColor = String(exec.status) === 'success' ? chalk.green : chalk.red;
145
+ const lines = [` ${statusColor(String(exec.status).toUpperCase())} ${String(exec.id)}`];
146
+ if (exec.durationMs !== null)
147
+ lines.push(` Duration: ${String(exec.durationMs)}ms`);
148
+ if (exec.error)
149
+ lines.push(` Error: ${chalk.red(String(exec.error))}`);
150
+ lines.push(` Time: ${new Date(String(exec.createdAt)).toLocaleString()}`);
151
+ return lines.join('\n');
152
+ },
153
+ });
154
+ }
155
+ catch (err) {
156
+ handleError(out, err, 'Failed to fetch executions');
157
+ }
158
+ });
159
+ }
@@ -0,0 +1,2 @@
1
+ import type { Command } from 'commander';
2
+ export declare function registerKeyCommands(program: Command): void;
@@ -0,0 +1,165 @@
1
+ import chalk from 'chalk';
2
+ import { getClient } from '../client.js';
3
+ import { addGlobalFlags, resolveFlags } from '../utils/flags.js';
4
+ import { createOutput, handleError } from '../utils/output.js';
5
+ export function registerKeyCommands(program) {
6
+ const keys = program.command('keys').description('Create, list, update, and revoke API keys');
7
+ addGlobalFlags(keys.command('list')
8
+ .description('List all API keys for the current user'))
9
+ .action(async (_opts) => {
10
+ const flags = resolveFlags(_opts);
11
+ const out = createOutput(flags);
12
+ out.startSpinner('Fetching API keys...');
13
+ try {
14
+ const client = getClient();
15
+ const apiKeys = await client.apiKeys.list();
16
+ out.stopSpinner();
17
+ out.list(apiKeys.map(key => ({
18
+ name: key.name,
19
+ prefix: key.prefix,
20
+ scopes: key.scopes.join(', '),
21
+ isActive: key.isActive,
22
+ expiresAt: key.expiresAt || null,
23
+ lastUsedAt: key.lastUsedAt || null,
24
+ })), {
25
+ emptyMessage: 'No API keys found.',
26
+ columns: [
27
+ { key: 'name', header: 'Name' },
28
+ { key: 'prefix', header: 'Prefix' },
29
+ { key: 'scopes', header: 'Scopes' },
30
+ { key: 'isActive', header: 'Active' },
31
+ ],
32
+ textFn: (key) => {
33
+ const lines = [chalk.cyan(` ${String(key.name)}`)];
34
+ lines.push(` Prefix: ${String(key.prefix)}`);
35
+ lines.push(` Scopes: ${String(key.scopes)}`);
36
+ lines.push(` Active: ${key.isActive ? chalk.green('Yes') : chalk.red('No')}`);
37
+ if (key.expiresAt)
38
+ lines.push(` Expires: ${new Date(String(key.expiresAt)).toLocaleString()}`);
39
+ if (key.lastUsedAt)
40
+ lines.push(` Last used: ${new Date(String(key.lastUsedAt)).toLocaleString()}`);
41
+ return lines.join('\n');
42
+ },
43
+ });
44
+ }
45
+ catch (err) {
46
+ handleError(out, err, 'Failed to fetch API keys');
47
+ }
48
+ });
49
+ addGlobalFlags(keys.command('get')
50
+ .description('Show detailed information about an API key')
51
+ .argument('<keyId>', 'API key ID'))
52
+ .action(async (keyId, _opts) => {
53
+ const flags = resolveFlags(_opts);
54
+ const out = createOutput(flags);
55
+ out.startSpinner('Fetching API key...');
56
+ try {
57
+ const client = getClient();
58
+ const key = await client.apiKeys.get(keyId);
59
+ out.stopSpinner();
60
+ out.record({
61
+ id: key.id,
62
+ name: key.name,
63
+ prefix: key.prefix,
64
+ scopes: key.scopes.join(', '),
65
+ isActive: key.isActive,
66
+ vaultId: key.vaultId || null,
67
+ expiresAt: key.expiresAt || null,
68
+ lastUsedAt: key.lastUsedAt || null,
69
+ createdAt: key.createdAt,
70
+ });
71
+ }
72
+ catch (err) {
73
+ handleError(out, err, 'Failed to fetch API key');
74
+ }
75
+ });
76
+ addGlobalFlags(keys.command('create')
77
+ .description('Create a new API key and display it (shown only once)')
78
+ .argument('<name>', 'Descriptive name for the API key')
79
+ .option('--scopes <scopes>', 'Comma-separated scopes (e.g., read,write)', 'read,write')
80
+ .option('--vault <vaultId>', 'Restrict key to a specific vault')
81
+ .option('--expires <date>', 'Expiry date (ISO 8601 format, e.g., 2025-12-31)')
82
+ .addHelpText('after', `
83
+ EXAMPLES
84
+ lsvault keys create "CI Deploy Key" --scopes read,write
85
+ lsvault keys create "Read Only" --scopes read --vault abc123
86
+ lsvault keys create "Temp Key" --expires 2025-06-01`))
87
+ .action(async (name, _opts) => {
88
+ const flags = resolveFlags(_opts);
89
+ const out = createOutput(flags);
90
+ out.startSpinner('Creating API key...');
91
+ try {
92
+ const client = getClient();
93
+ const params = {
94
+ name,
95
+ scopes: String(_opts.scopes || 'read,write').split(',').map((s) => s.trim()),
96
+ };
97
+ if (_opts.vault)
98
+ params.vaultId = String(_opts.vault);
99
+ if (_opts.expires)
100
+ params.expiresAt = String(_opts.expires);
101
+ const apiKey = await client.apiKeys.create(params);
102
+ out.stopSpinner();
103
+ if (flags.output === 'json') {
104
+ out.record({ key: apiKey.key, name: apiKey.name, prefix: apiKey.prefix, scopes: apiKey.scopes.join(', ') });
105
+ }
106
+ else {
107
+ out.warn('\nIMPORTANT: Save this key securely. It cannot be retrieved later.\n');
108
+ process.stdout.write(chalk.green.bold(`API Key: ${apiKey.key}\n`));
109
+ process.stdout.write(`\nName: ${apiKey.name}\n`);
110
+ process.stdout.write(`Prefix: ${apiKey.prefix}\n`);
111
+ process.stdout.write(`Scopes: ${apiKey.scopes.join(', ')}\n`);
112
+ }
113
+ }
114
+ catch (err) {
115
+ handleError(out, err, 'Failed to create API key');
116
+ }
117
+ });
118
+ addGlobalFlags(keys.command('update')
119
+ .description('Update an API key name or active status')
120
+ .argument('<keyId>', 'API key ID')
121
+ .option('--name <name>', 'New name for the key')
122
+ .option('--active', 'Re-enable the key')
123
+ .option('--inactive', 'Disable the key without revoking it'))
124
+ .action(async (keyId, _opts) => {
125
+ const flags = resolveFlags(_opts);
126
+ const out = createOutput(flags);
127
+ if (!_opts.name && !_opts.active && !_opts.inactive) {
128
+ out.error('Must specify at least one update option (--name, --active, or --inactive)');
129
+ process.exitCode = 2;
130
+ return;
131
+ }
132
+ out.startSpinner('Updating API key...');
133
+ try {
134
+ const client = getClient();
135
+ const params = {};
136
+ if (_opts.name)
137
+ params.name = String(_opts.name);
138
+ if (_opts.active)
139
+ params.isActive = true;
140
+ if (_opts.inactive)
141
+ params.isActive = false;
142
+ const updated = await client.apiKeys.update(keyId, params);
143
+ out.success('API key updated successfully', { name: updated.name, isActive: updated.isActive });
144
+ }
145
+ catch (err) {
146
+ handleError(out, err, 'Failed to update API key');
147
+ }
148
+ });
149
+ addGlobalFlags(keys.command('revoke')
150
+ .description('Permanently revoke and delete an API key')
151
+ .argument('<keyId>', 'API key ID'))
152
+ .action(async (keyId, _opts) => {
153
+ const flags = resolveFlags(_opts);
154
+ const out = createOutput(flags);
155
+ out.startSpinner('Revoking API key...');
156
+ try {
157
+ const client = getClient();
158
+ await client.apiKeys.delete(keyId);
159
+ out.success('API key revoked successfully', { id: keyId, revoked: true });
160
+ }
161
+ catch (err) {
162
+ handleError(out, err, 'Failed to revoke API key');
163
+ }
164
+ });
165
+ }
@@ -0,0 +1,2 @@
1
+ import type { Command } from 'commander';
2
+ export declare function registerPublishCommands(program: Command): void;
@@ -0,0 +1,138 @@
1
+ import { getClient } from '../client.js';
2
+ import { addGlobalFlags, resolveFlags } from '../utils/flags.js';
3
+ import { createOutput, handleError } from '../utils/output.js';
4
+ import chalk from 'chalk';
5
+ export function registerPublishCommands(program) {
6
+ const publish = program.command('publish').description('Publish documents to public profile pages');
7
+ addGlobalFlags(publish.command('list')
8
+ .description('List your published documents')
9
+ .argument('<vaultId>', 'Vault ID (required by route)'))
10
+ .action(async (vaultId, _opts) => {
11
+ const flags = resolveFlags(_opts);
12
+ const out = createOutput(flags);
13
+ out.startSpinner('Fetching published documents...');
14
+ try {
15
+ const client = getClient();
16
+ const docs = await client.publish.listMine(vaultId);
17
+ out.stopSpinner();
18
+ out.list(docs.map(doc => ({
19
+ slug: doc.slug,
20
+ documentPath: doc.documentPath,
21
+ documentTitle: doc.documentTitle || '',
22
+ isPublished: doc.isPublished,
23
+ seoTitle: doc.seoTitle || '',
24
+ updatedAt: doc.updatedAt,
25
+ })), {
26
+ emptyMessage: 'No published documents found.',
27
+ columns: [
28
+ { key: 'slug', header: 'Slug' },
29
+ { key: 'documentPath', header: 'Path' },
30
+ { key: 'isPublished', header: 'Published' },
31
+ { key: 'seoTitle', header: 'SEO Title' },
32
+ ],
33
+ textFn: (doc) => {
34
+ const lines = [chalk.cyan(` ${String(doc.slug)}`)];
35
+ lines.push(` Path: ${String(doc.documentPath)}`);
36
+ if (doc.documentTitle)
37
+ lines.push(` Title: ${String(doc.documentTitle)}`);
38
+ lines.push(` Published: ${doc.isPublished ? chalk.green('Yes') : chalk.red('No')}`);
39
+ if (doc.seoTitle)
40
+ lines.push(` SEO Title: ${String(doc.seoTitle)}`);
41
+ lines.push(` Updated: ${new Date(String(doc.updatedAt)).toLocaleString()}`);
42
+ return lines.join('\n');
43
+ },
44
+ });
45
+ }
46
+ catch (err) {
47
+ handleError(out, err, 'Failed to fetch published documents');
48
+ }
49
+ });
50
+ addGlobalFlags(publish.command('create')
51
+ .description('Publish a document')
52
+ .argument('<vaultId>', 'Vault ID')
53
+ .argument('<docPath>', 'Document path (e.g., blog/post.md)')
54
+ .requiredOption('--slug <slug>', 'URL-friendly slug for the published page')
55
+ .option('--title <title>', 'SEO title')
56
+ .option('--description <description>', 'SEO description')
57
+ .option('--og-image <url>', 'Open Graph image URL'))
58
+ .action(async (vaultId, docPath, _opts) => {
59
+ const flags = resolveFlags(_opts);
60
+ const out = createOutput(flags);
61
+ out.startSpinner('Publishing document...');
62
+ try {
63
+ const client = getClient();
64
+ const params = {
65
+ slug: String(_opts.slug),
66
+ };
67
+ if (_opts.title)
68
+ params.seoTitle = String(_opts.title);
69
+ if (_opts.description)
70
+ params.seoDescription = String(_opts.description);
71
+ if (_opts.ogImage)
72
+ params.ogImage = String(_opts.ogImage);
73
+ const pub = await client.publish.create(vaultId, docPath, params);
74
+ out.success('Document published successfully!', {
75
+ slug: pub.slug,
76
+ isPublished: pub.isPublished,
77
+ seoTitle: pub.seoTitle || null,
78
+ seoDescription: pub.seoDescription || null,
79
+ publishedAt: pub.publishedAt,
80
+ });
81
+ }
82
+ catch (err) {
83
+ handleError(out, err, 'Failed to publish document');
84
+ }
85
+ });
86
+ addGlobalFlags(publish.command('update')
87
+ .description('Update a published document')
88
+ .argument('<vaultId>', 'Vault ID')
89
+ .argument('<docPath>', 'Document path (e.g., blog/post.md)')
90
+ .requiredOption('--slug <slug>', 'URL-friendly slug (required for updates)')
91
+ .option('--title <title>', 'SEO title')
92
+ .option('--description <description>', 'SEO description')
93
+ .option('--og-image <url>', 'Open Graph image URL'))
94
+ .action(async (vaultId, docPath, _opts) => {
95
+ const flags = resolveFlags(_opts);
96
+ const out = createOutput(flags);
97
+ out.startSpinner('Updating published document...');
98
+ try {
99
+ const client = getClient();
100
+ const params = {
101
+ slug: String(_opts.slug),
102
+ };
103
+ if (_opts.title)
104
+ params.seoTitle = String(_opts.title);
105
+ if (_opts.description)
106
+ params.seoDescription = String(_opts.description);
107
+ if (_opts.ogImage)
108
+ params.ogImage = String(_opts.ogImage);
109
+ const pub = await client.publish.update(vaultId, docPath, params);
110
+ out.success('Published document updated successfully', {
111
+ slug: pub.slug,
112
+ seoTitle: pub.seoTitle || null,
113
+ seoDescription: pub.seoDescription || null,
114
+ updatedAt: pub.updatedAt,
115
+ });
116
+ }
117
+ catch (err) {
118
+ handleError(out, err, 'Failed to update published document');
119
+ }
120
+ });
121
+ addGlobalFlags(publish.command('delete')
122
+ .description('Unpublish a document')
123
+ .argument('<vaultId>', 'Vault ID')
124
+ .argument('<docPath>', 'Document path (e.g., blog/post.md)'))
125
+ .action(async (vaultId, docPath, _opts) => {
126
+ const flags = resolveFlags(_opts);
127
+ const out = createOutput(flags);
128
+ out.startSpinner('Unpublishing document...');
129
+ try {
130
+ const client = getClient();
131
+ await client.publish.delete(vaultId, docPath);
132
+ out.success('Document unpublished successfully', { path: docPath, unpublished: true });
133
+ }
134
+ catch (err) {
135
+ handleError(out, err, 'Failed to unpublish document');
136
+ }
137
+ });
138
+ }
@@ -0,0 +1,2 @@
1
+ import type { Command } from 'commander';
2
+ export declare function registerSearchCommands(program: Command): void;