@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
@@ -29,9 +29,9 @@ export function getActiveProfile() {
29
29
  */
30
30
  export function setActiveProfile(name) {
31
31
  if (!fs.existsSync(CONFIG_DIR)) {
32
- fs.mkdirSync(CONFIG_DIR, { recursive: true });
32
+ fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
33
33
  }
34
- fs.writeFileSync(ACTIVE_PROFILE_FILE, name + '\n');
34
+ fs.writeFileSync(ACTIVE_PROFILE_FILE, name + '\n', { mode: 0o600 });
35
35
  }
36
36
  /**
37
37
  * Loads a profile's configuration. Returns an empty object if the profile
@@ -54,11 +54,11 @@ export function loadProfile(name) {
54
54
  */
55
55
  export function setProfileValue(name, key, value) {
56
56
  if (!fs.existsSync(PROFILES_DIR)) {
57
- fs.mkdirSync(PROFILES_DIR, { recursive: true });
57
+ fs.mkdirSync(PROFILES_DIR, { recursive: true, mode: 0o700 });
58
58
  }
59
59
  const config = loadProfile(name);
60
60
  config[key] = value;
61
- fs.writeFileSync(getProfilePath(name), JSON.stringify(config, null, 2) + '\n');
61
+ fs.writeFileSync(getProfilePath(name), JSON.stringify(config, null, 2) + '\n', { mode: 0o600 });
62
62
  }
63
63
  /**
64
64
  * Gets a single value from a profile.
@@ -31,9 +31,9 @@ export function loadSyncConfigs() {
31
31
  */
32
32
  export function saveSyncConfigs(configs) {
33
33
  if (!fs.existsSync(CONFIG_DIR)) {
34
- fs.mkdirSync(CONFIG_DIR, { recursive: true });
34
+ fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
35
35
  }
36
- fs.writeFileSync(SYNCS_FILE, JSON.stringify(configs, null, 2) + '\n');
36
+ fs.writeFileSync(SYNCS_FILE, JSON.stringify(configs, null, 2) + '\n', { mode: 0o600 });
37
37
  }
38
38
  /**
39
39
  * Find a sync config by its ID.
@@ -8,7 +8,7 @@ import { resolveIgnorePatterns } from './ignore.js';
8
8
  import { createWatcher } from './watcher.js';
9
9
  import { createRemotePoller } from './remote-poller.js';
10
10
  import { removePid } from './daemon.js';
11
- import { loadConfig } from '../config.js';
11
+ import { loadConfigAsync } from '../config.js';
12
12
  import { LifestreamVaultClient } from '@lifestreamdynamics/vault-sdk';
13
13
  import { scanLocalFiles, scanRemoteFiles, computePushDiff, computePullDiff, executePush, executePull } from './engine.js';
14
14
  import { loadSyncState } from './state.js';
@@ -17,10 +17,17 @@ function log(msg) {
17
17
  const ts = new Date().toISOString();
18
18
  process.stdout.write(`[${ts}] ${msg}\n`);
19
19
  }
20
- function createClient() {
21
- const config = loadConfig();
22
- if (!config.apiKey) {
23
- throw new Error('No API key configured. Run `lsvault auth login` first.');
20
+ async function createClient() {
21
+ const config = await loadConfigAsync();
22
+ if (!config.apiKey && !config.accessToken) {
23
+ throw new Error('No credentials configured. Run `lsvault auth login` first.');
24
+ }
25
+ if (config.accessToken) {
26
+ return new LifestreamVaultClient({
27
+ baseUrl: config.apiUrl,
28
+ accessToken: config.accessToken,
29
+ refreshToken: config.refreshToken,
30
+ });
24
31
  }
25
32
  return new LifestreamVaultClient({
26
33
  baseUrl: config.apiUrl,
@@ -36,7 +43,7 @@ async function start() {
36
43
  process.exit(0);
37
44
  }
38
45
  log(`Found ${configs.length} auto-sync configuration(s)`);
39
- const client = createClient();
46
+ const client = await createClient();
40
47
  // Startup reconciliation: catch changes made while daemon was stopped
41
48
  for (const config of configs) {
42
49
  try {
@@ -137,7 +137,8 @@ export function startDaemon(logFile) {
137
137
  rotateLogIfNeeded(targetLog);
138
138
  const logFd = fs.openSync(targetLog, 'a');
139
139
  // Spawn the daemon worker as a detached process
140
- const workerPath = path.join(import.meta.dirname, 'daemon-worker.js');
140
+ // Use URL constructor for Node 20.0-20.10 compatibility (import.meta.dirname was added in 20.11).
141
+ const workerPath = path.join(path.dirname(new URL(import.meta.url).pathname), 'daemon-worker.js');
141
142
  const child = spawn(process.execPath, [workerPath], {
142
143
  detached: true,
143
144
  stdio: ['ignore', logFd, logFd],
@@ -63,7 +63,9 @@ export function createRemotePoller(client, config, options) {
63
63
  if (resolution === 'remote') {
64
64
  conflictFile = createConflictFile(config.localPath, doc.path, localContent, 'local');
65
65
  onLocalWrite?.(doc.path);
66
- fs.writeFileSync(localFile, content, 'utf-8');
66
+ const tmpConflict = localFile + '.tmp';
67
+ fs.writeFileSync(tmpConflict, content, 'utf-8');
68
+ fs.renameSync(tmpConflict, localFile);
67
69
  log(`Conflict: ${doc.path} — used remote, saved local as ${conflictFile}`);
68
70
  }
69
71
  else {
@@ -80,13 +82,15 @@ export function createRemotePoller(client, config, options) {
80
82
  continue;
81
83
  }
82
84
  }
83
- // No conflict — download the file
85
+ // No conflict — download the file atomically
84
86
  const dir = path.dirname(localFile);
85
87
  if (!fs.existsSync(dir)) {
86
88
  fs.mkdirSync(dir, { recursive: true });
87
89
  }
88
90
  onLocalWrite?.(doc.path);
89
- fs.writeFileSync(localFile, content, 'utf-8');
91
+ const tmpFile = localFile + '.tmp';
92
+ fs.writeFileSync(tmpFile, content, 'utf-8');
93
+ fs.renameSync(tmpFile, localFile);
90
94
  log(`Pulled: ${doc.path}`);
91
95
  changes++;
92
96
  state.local[doc.path] = {
@@ -43,10 +43,10 @@ export function loadSyncState(syncId) {
43
43
  */
44
44
  export function saveSyncState(state) {
45
45
  if (!fs.existsSync(STATE_DIR)) {
46
- fs.mkdirSync(STATE_DIR, { recursive: true });
46
+ fs.mkdirSync(STATE_DIR, { recursive: true, mode: 0o700 });
47
47
  }
48
48
  state.updatedAt = new Date().toISOString();
49
- fs.writeFileSync(stateFilePath(state.syncId), JSON.stringify(state, null, 2) + '\n');
49
+ fs.writeFileSync(stateFilePath(state.syncId), JSON.stringify(state, null, 2) + '\n', { mode: 0o600 });
50
50
  }
51
51
  /**
52
52
  * Delete sync state for a given sync configuration.
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Prompt the user to confirm a destructive action.
3
+ *
4
+ * @param message - The confirmation message to display (without " [y/N] " suffix)
5
+ * @param options.yes - If true, skip the prompt and return true immediately (e.g. --yes flag)
6
+ * @returns Promise resolving to true if confirmed, false if declined
7
+ * @throws Error if stdin is not a TTY and --yes was not provided
8
+ */
9
+ export declare function confirmAction(message: string, options?: {
10
+ yes?: boolean;
11
+ }): Promise<boolean>;
@@ -0,0 +1,23 @@
1
+ import readline from 'node:readline';
2
+ /**
3
+ * Prompt the user to confirm a destructive action.
4
+ *
5
+ * @param message - The confirmation message to display (without " [y/N] " suffix)
6
+ * @param options.yes - If true, skip the prompt and return true immediately (e.g. --yes flag)
7
+ * @returns Promise resolving to true if confirmed, false if declined
8
+ * @throws Error if stdin is not a TTY and --yes was not provided
9
+ */
10
+ export async function confirmAction(message, options) {
11
+ if (options?.yes)
12
+ return true;
13
+ if (!process.stdin.isTTY) {
14
+ throw new Error('Cannot prompt for confirmation in non-interactive mode. Use --yes to skip confirmation.');
15
+ }
16
+ const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
17
+ return new Promise((resolve) => {
18
+ rl.question(`${message} [y/N] `, (answer) => {
19
+ rl.close();
20
+ resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
21
+ });
22
+ });
23
+ }
@@ -1,5 +1,5 @@
1
1
  export function formatBytes(bytes) {
2
- if (bytes === 0)
2
+ if (bytes <= 0)
3
3
  return '0 B';
4
4
  const units = ['B', 'KB', 'MB', 'GB', 'TB'];
5
5
  const i = Math.floor(Math.log(bytes) / Math.log(1024));
@@ -108,12 +108,15 @@ export class Output {
108
108
  list(data, options) {
109
109
  if (data.length === 0) {
110
110
  if (this.flags.output === 'json') {
111
- // empty json array: no output
111
+ process.stdout.write('[]\n');
112
112
  return;
113
113
  }
114
114
  if (options?.emptyMessage && !this.flags.quiet) {
115
115
  this.status(options.emptyMessage);
116
116
  }
117
+ else if (!this.flags.quiet && !options?.emptyMessage) {
118
+ process.stdout.write('No results found.\n');
119
+ }
117
120
  return;
118
121
  }
119
122
  switch (this.flags.output) {
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Shared interactive prompt utilities.
3
+ *
4
+ * These helpers write prompts to stderr so they do not corrupt stdout piping,
5
+ * and they disable terminal echo so passwords are never visible on screen or
6
+ * in scroll-back buffers.
7
+ */
8
+ /**
9
+ * Prompt for a password from stdin (non-echoing).
10
+ *
11
+ * @param prompt - Label written to stderr before the user types (default: "Password: ")
12
+ * @returns The entered password, or null if stdin is not a TTY (non-interactive).
13
+ */
14
+ export declare function promptPassword(prompt?: string): Promise<string | null>;
15
+ /**
16
+ * Read a password from stdin in non-TTY / CI mode (i.e. piped input).
17
+ *
18
+ * Reads the first line of stdin and trims whitespace. Callers should
19
+ * gate this behind a `--password-stdin` flag so the intent is explicit.
20
+ *
21
+ * @returns The password string, or null if stdin is empty / already ended.
22
+ */
23
+ export declare function readPasswordFromStdin(): Promise<string | null>;
24
+ /**
25
+ * Prompt for an MFA code from stdin (6 digits, non-echoing).
26
+ *
27
+ * @returns The entered code, or null if stdin is not a TTY.
28
+ */
29
+ export declare function promptMfaCode(): Promise<string | null>;
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Shared interactive prompt utilities.
3
+ *
4
+ * These helpers write prompts to stderr so they do not corrupt stdout piping,
5
+ * and they disable terminal echo so passwords are never visible on screen or
6
+ * in scroll-back buffers.
7
+ */
8
+ /**
9
+ * Prompt for a password from stdin (non-echoing).
10
+ *
11
+ * @param prompt - Label written to stderr before the user types (default: "Password: ")
12
+ * @returns The entered password, or null if stdin is not a TTY (non-interactive).
13
+ */
14
+ export async function promptPassword(prompt = 'Password: ') {
15
+ if (!process.stdin.isTTY) {
16
+ return null;
17
+ }
18
+ const readline = await import('node:readline');
19
+ return new Promise((resolve) => {
20
+ const rl = readline.createInterface({
21
+ input: process.stdin,
22
+ output: process.stderr,
23
+ terminal: true,
24
+ });
25
+ process.stderr.write(prompt);
26
+ process.stdin.setRawMode?.(true);
27
+ let password = '';
28
+ const onData = (chunk) => {
29
+ const char = chunk.toString('utf-8');
30
+ if (char === '\n' || char === '\r' || char === '\u0004') {
31
+ process.stderr.write('\n');
32
+ process.stdin.setRawMode?.(false);
33
+ process.stdin.removeListener('data', onData);
34
+ rl.close();
35
+ resolve(password);
36
+ }
37
+ else if (char === '\u0003') {
38
+ // Ctrl+C
39
+ process.stderr.write('\n');
40
+ process.stdin.setRawMode?.(false);
41
+ process.stdin.removeListener('data', onData);
42
+ rl.close();
43
+ resolve(null);
44
+ }
45
+ else if (char === '\u007F' || char === '\b') {
46
+ // Backspace
47
+ if (password.length > 0) {
48
+ password = password.slice(0, -1);
49
+ }
50
+ }
51
+ else {
52
+ password += char;
53
+ }
54
+ };
55
+ process.stdin.on('data', onData);
56
+ process.stdin.resume();
57
+ });
58
+ }
59
+ /**
60
+ * Read a password from stdin in non-TTY / CI mode (i.e. piped input).
61
+ *
62
+ * Reads the first line of stdin and trims whitespace. Callers should
63
+ * gate this behind a `--password-stdin` flag so the intent is explicit.
64
+ *
65
+ * @returns The password string, or null if stdin is empty / already ended.
66
+ */
67
+ export async function readPasswordFromStdin() {
68
+ return new Promise((resolve, reject) => {
69
+ let data = '';
70
+ const onData = (chunk) => {
71
+ data += chunk.toString('utf-8');
72
+ // Stop after the first newline — we only want one line.
73
+ if (data.includes('\n')) {
74
+ cleanup();
75
+ resolve(data.split('\n')[0].trim() || null);
76
+ }
77
+ };
78
+ const onEnd = () => {
79
+ cleanup();
80
+ resolve(data.trim() || null);
81
+ };
82
+ const onError = (err) => {
83
+ cleanup();
84
+ reject(err);
85
+ };
86
+ const cleanup = () => {
87
+ process.stdin.removeListener('data', onData);
88
+ process.stdin.removeListener('end', onEnd);
89
+ process.stdin.removeListener('error', onError);
90
+ };
91
+ process.stdin.on('data', onData);
92
+ process.stdin.once('end', onEnd);
93
+ process.stdin.once('error', onError);
94
+ process.stdin.resume();
95
+ });
96
+ }
97
+ /**
98
+ * Prompt for an MFA code from stdin (6 digits, non-echoing).
99
+ *
100
+ * @returns The entered code, or null if stdin is not a TTY.
101
+ */
102
+ export async function promptMfaCode() {
103
+ if (!process.stdin.isTTY) {
104
+ return null;
105
+ }
106
+ const readline = await import('node:readline');
107
+ return new Promise((resolve) => {
108
+ const rl = readline.createInterface({
109
+ input: process.stdin,
110
+ output: process.stderr,
111
+ terminal: true,
112
+ });
113
+ process.stderr.write('MFA code: ');
114
+ process.stdin.setRawMode?.(true);
115
+ let code = '';
116
+ const onData = (chunk) => {
117
+ const char = chunk.toString('utf-8');
118
+ if (char === '\n' || char === '\r' || char === '\u0004') {
119
+ process.stderr.write('\n');
120
+ process.stdin.setRawMode?.(false);
121
+ process.stdin.removeListener('data', onData);
122
+ rl.close();
123
+ resolve(code);
124
+ }
125
+ else if (char === '\u0003') {
126
+ // Ctrl+C
127
+ process.stderr.write('\n');
128
+ process.stdin.setRawMode?.(false);
129
+ process.stdin.removeListener('data', onData);
130
+ rl.close();
131
+ resolve(null);
132
+ }
133
+ else if (char === '\u007F' || char === '\b') {
134
+ // Backspace
135
+ if (code.length > 0) {
136
+ code = code.slice(0, -1);
137
+ }
138
+ }
139
+ else {
140
+ code += char;
141
+ }
142
+ };
143
+ process.stdin.on('data', onData);
144
+ process.stdin.resume();
145
+ });
146
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lifestreamdynamics/vault-cli",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Command-line interface for Lifestream Vault",
5
5
  "engines": {
6
6
  "node": ">=20"
@@ -44,7 +44,7 @@
44
44
  "prepublishOnly": "npm run build && npm test"
45
45
  },
46
46
  "dependencies": {
47
- "@lifestreamdynamics/vault-sdk": "^1.2.0",
47
+ "@lifestreamdynamics/vault-sdk": "^2.1.0",
48
48
  "chalk": "^5.4.0",
49
49
  "chokidar": "^4.0.3",
50
50
  "commander": "^13.0.0",