@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.
- package/README.md +140 -30
- package/dist/client.d.ts +4 -0
- package/dist/client.js +12 -11
- package/dist/commands/admin.js +5 -5
- package/dist/commands/ai.js +17 -4
- package/dist/commands/auth.js +10 -105
- package/dist/commands/booking.d.ts +2 -0
- package/dist/commands/booking.js +739 -0
- package/dist/commands/calendar.js +725 -6
- package/dist/commands/completion.d.ts +5 -0
- package/dist/commands/completion.js +60 -0
- package/dist/commands/config.js +17 -16
- package/dist/commands/connectors.js +12 -1
- package/dist/commands/custom-domains.js +6 -1
- package/dist/commands/docs.js +12 -5
- package/dist/commands/hooks.js +6 -1
- package/dist/commands/links.js +9 -2
- package/dist/commands/mfa.js +1 -70
- package/dist/commands/plugins.d.ts +2 -0
- package/dist/commands/plugins.js +172 -0
- package/dist/commands/publish.js +13 -3
- package/dist/commands/saml.d.ts +2 -0
- package/dist/commands/saml.js +220 -0
- package/dist/commands/scim.d.ts +2 -0
- package/dist/commands/scim.js +238 -0
- package/dist/commands/shares.js +25 -3
- package/dist/commands/subscription.js +9 -2
- package/dist/commands/sync.js +3 -0
- package/dist/commands/teams.js +141 -8
- package/dist/commands/user.js +122 -9
- package/dist/commands/vaults.js +17 -8
- package/dist/commands/webhooks.js +6 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.js +7 -3
- package/dist/index.js +20 -1
- package/dist/lib/credential-manager.js +32 -7
- package/dist/lib/migration.js +2 -2
- package/dist/lib/profiles.js +4 -4
- package/dist/sync/config.js +2 -2
- package/dist/sync/daemon-worker.js +13 -6
- package/dist/sync/daemon.js +2 -1
- package/dist/sync/remote-poller.js +7 -3
- package/dist/sync/state.js +2 -2
- package/dist/utils/confirm.d.ts +11 -0
- package/dist/utils/confirm.js +23 -0
- package/dist/utils/format.js +1 -1
- package/dist/utils/output.js +4 -1
- package/dist/utils/prompt.d.ts +29 -0
- package/dist/utils/prompt.js +146 -0
- package/package.json +2 -2
package/dist/lib/profiles.js
CHANGED
|
@@ -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.
|
package/dist/sync/config.js
CHANGED
|
@@ -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 {
|
|
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 =
|
|
22
|
-
if (!config.apiKey) {
|
|
23
|
-
throw new Error('No
|
|
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 {
|
package/dist/sync/daemon.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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] = {
|
package/dist/sync/state.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/utils/format.js
CHANGED
package/dist/utils/output.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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.
|
|
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",
|