@lifestreamdynamics/vault-cli 1.0.0 → 1.1.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.
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import chalk from 'chalk';
4
- import { getClient } from '../client.js';
4
+ import { getClientAsync } from '../client.js';
5
5
  import { addGlobalFlags, resolveFlags } from '../utils/flags.js';
6
6
  import { createOutput, handleError } from '../utils/output.js';
7
7
  import { formatUptime } from '../utils/format.js';
@@ -30,7 +30,7 @@ export function registerSyncCommands(program) {
30
30
  const out = createOutput(flags);
31
31
  out.startSpinner('Initializing sync...');
32
32
  try {
33
- const client = getClient();
33
+ const client = await getClientAsync();
34
34
  const vault = await client.vaults.get(vaultId);
35
35
  const absPath = path.resolve(localPath);
36
36
  const mode = _opts.mode ?? 'sync';
@@ -144,7 +144,7 @@ export function registerSyncCommands(program) {
144
144
  process.exitCode = 1;
145
145
  return;
146
146
  }
147
- const client = getClient();
147
+ const client = await getClientAsync();
148
148
  const ignorePatterns = resolveIgnorePatterns(config.ignore, config.localPath);
149
149
  const lastState = loadSyncState(config.id);
150
150
  out.startSpinner('Scanning local files...');
@@ -220,7 +220,7 @@ export function registerSyncCommands(program) {
220
220
  process.exitCode = 1;
221
221
  return;
222
222
  }
223
- const client = getClient();
223
+ const client = await getClientAsync();
224
224
  const ignorePatterns = resolveIgnorePatterns(config.ignore, config.localPath);
225
225
  const lastState = loadSyncState(config.id);
226
226
  out.startSpinner('Scanning local files...');
@@ -296,7 +296,7 @@ export function registerSyncCommands(program) {
296
296
  process.exitCode = 1;
297
297
  return;
298
298
  }
299
- const client = getClient();
299
+ const client = await getClientAsync();
300
300
  const ignorePatterns = resolveIgnorePatterns(config.ignore, config.localPath);
301
301
  const lastState = loadSyncState(config.id);
302
302
  out.startSpinner('Scanning...');
@@ -373,7 +373,7 @@ export function registerSyncCommands(program) {
373
373
  process.exitCode = 1;
374
374
  return;
375
375
  }
376
- const client = getClient();
376
+ const client = await getClientAsync();
377
377
  const ignorePatterns = resolveIgnorePatterns(config.ignore, config.localPath);
378
378
  const pollInterval = parseInt(String(_opts.pollInterval ?? '30000'), 10);
379
379
  out.status(`Watching sync ${chalk.cyan(syncId.slice(0, 8))}...`);
@@ -447,7 +447,7 @@ export function registerSyncCommands(program) {
447
447
  process.exitCode = 1;
448
448
  return;
449
449
  }
450
- const client = getClient();
450
+ const client = await getClientAsync();
451
451
  const localFile = path.join(config.localPath, docPath);
452
452
  const state = loadSyncState(config.id);
453
453
  if (useVersion === 'local') {
@@ -501,8 +501,11 @@ export function registerSyncCommands(program) {
501
501
  const out = createOutput(flags);
502
502
  try {
503
503
  const logFile = _opts.logFile;
504
- const pid = startDaemon(logFile);
504
+ const { pid, lingerWarning } = startDaemon(logFile);
505
505
  out.success('Daemon started', { pid, status: 'running' });
506
+ if (lingerWarning) {
507
+ out.warn(`Warning: ${lingerWarning}`);
508
+ }
506
509
  }
507
510
  catch (err) {
508
511
  handleError(out, err, 'Failed to start daemon');
@@ -1,5 +1,5 @@
1
1
  import chalk from 'chalk';
2
- import { getClient } from '../client.js';
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
5
  export function registerTeamCommands(program) {
@@ -12,7 +12,7 @@ export function registerTeamCommands(program) {
12
12
  const out = createOutput(flags);
13
13
  out.startSpinner('Fetching teams...');
14
14
  try {
15
- const client = getClient();
15
+ const client = await getClientAsync();
16
16
  const teamList = await client.teams.list();
17
17
  out.stopSpinner();
18
18
  out.list(teamList.map(t => ({ name: t.name, id: t.id, description: t.description || 'No description' })), {
@@ -37,7 +37,7 @@ export function registerTeamCommands(program) {
37
37
  const out = createOutput(flags);
38
38
  out.startSpinner('Fetching team...');
39
39
  try {
40
- const client = getClient();
40
+ const client = await getClientAsync();
41
41
  const team = await client.teams.get(teamId);
42
42
  out.stopSpinner();
43
43
  out.record({
@@ -66,7 +66,7 @@ EXAMPLES
66
66
  const out = createOutput(flags);
67
67
  out.startSpinner('Creating team...');
68
68
  try {
69
- const client = getClient();
69
+ const client = await getClientAsync();
70
70
  const team = await client.teams.create({ name, description: _opts.description });
71
71
  out.success(`Team created: ${chalk.cyan(team.name)} (${team.id})`, { id: team.id, name: team.name });
72
72
  }
@@ -84,7 +84,7 @@ EXAMPLES
84
84
  const out = createOutput(flags);
85
85
  out.startSpinner('Updating team...');
86
86
  try {
87
- const client = getClient();
87
+ const client = await getClientAsync();
88
88
  const team = await client.teams.update(teamId, {
89
89
  name: _opts.name,
90
90
  description: _opts.description,
@@ -103,7 +103,7 @@ EXAMPLES
103
103
  const out = createOutput(flags);
104
104
  out.startSpinner('Deleting team...');
105
105
  try {
106
- const client = getClient();
106
+ const client = await getClientAsync();
107
107
  await client.teams.delete(teamId);
108
108
  out.success('Team deleted.', { id: teamId, deleted: true });
109
109
  }
@@ -121,7 +121,7 @@ EXAMPLES
121
121
  const out = createOutput(flags);
122
122
  out.startSpinner('Fetching members...');
123
123
  try {
124
- const client = getClient();
124
+ const client = await getClientAsync();
125
125
  const memberList = await client.teams.listMembers(teamId);
126
126
  out.stopSpinner();
127
127
  out.list(memberList.map(m => ({
@@ -154,7 +154,7 @@ EXAMPLES
154
154
  const role = String(_opts.role);
155
155
  out.startSpinner('Updating member role...');
156
156
  try {
157
- const client = getClient();
157
+ const client = await getClientAsync();
158
158
  const member = await client.teams.updateMemberRole(teamId, userId, role);
159
159
  out.success(`Role updated to ${chalk.magenta(member.role)} for ${member.user.email}`, {
160
160
  userId,
@@ -175,7 +175,7 @@ EXAMPLES
175
175
  const out = createOutput(flags);
176
176
  out.startSpinner('Removing member...');
177
177
  try {
178
- const client = getClient();
178
+ const client = await getClientAsync();
179
179
  await client.teams.removeMember(teamId, userId);
180
180
  out.success('Member removed.', { teamId, userId, removed: true });
181
181
  }
@@ -191,7 +191,7 @@ EXAMPLES
191
191
  const out = createOutput(flags);
192
192
  out.startSpinner('Leaving team...');
193
193
  try {
194
- const client = getClient();
194
+ const client = await getClientAsync();
195
195
  await client.teams.leave(teamId);
196
196
  out.success('Left the team.', { teamId, left: true });
197
197
  }
@@ -209,7 +209,7 @@ EXAMPLES
209
209
  const out = createOutput(flags);
210
210
  out.startSpinner('Fetching invitations...');
211
211
  try {
212
- const client = getClient();
212
+ const client = await getClientAsync();
213
213
  const invitationList = await client.teams.listInvitations(teamId);
214
214
  out.stopSpinner();
215
215
  out.list(invitationList.map(inv => ({
@@ -242,7 +242,7 @@ EXAMPLES
242
242
  const role = String(_opts.role);
243
243
  out.startSpinner('Sending invitation...');
244
244
  try {
245
- const client = getClient();
245
+ const client = await getClientAsync();
246
246
  const invitation = await client.teams.inviteMember(teamId, email, role);
247
247
  out.success(`Invited ${chalk.cyan(invitation.email)} as ${chalk.magenta(invitation.role)}`, {
248
248
  id: invitation.id,
@@ -263,7 +263,7 @@ EXAMPLES
263
263
  const out = createOutput(flags);
264
264
  out.startSpinner('Revoking invitation...');
265
265
  try {
266
- const client = getClient();
266
+ const client = await getClientAsync();
267
267
  await client.teams.revokeInvitation(teamId, invitationId);
268
268
  out.success('Invitation revoked.', { id: invitationId, revoked: true });
269
269
  }
@@ -281,7 +281,7 @@ EXAMPLES
281
281
  const out = createOutput(flags);
282
282
  out.startSpinner('Fetching team vaults...');
283
283
  try {
284
- const client = getClient();
284
+ const client = await getClientAsync();
285
285
  const vaultList = await client.teams.listVaults(teamId);
286
286
  out.stopSpinner();
287
287
  out.list(vaultList.map(v => ({ name: String(v.name), slug: String(v.slug), description: String(v.description) || 'No description' })), {
@@ -308,7 +308,7 @@ EXAMPLES
308
308
  const out = createOutput(flags);
309
309
  out.startSpinner('Creating team vault...');
310
310
  try {
311
- const client = getClient();
311
+ const client = await getClientAsync();
312
312
  const vault = await client.teams.createVault(teamId, { name, description: _opts.description });
313
313
  out.success(`Team vault created: ${chalk.cyan(String(vault.name))}`, {
314
314
  name: String(vault.name),
@@ -1,5 +1,5 @@
1
1
  import chalk from 'chalk';
2
- import { getClient } from '../client.js';
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
5
  import { formatBytes } from '../utils/format.js';
@@ -12,7 +12,7 @@ export function registerUserCommands(program) {
12
12
  const out = createOutput(flags);
13
13
  out.startSpinner('Fetching storage usage...');
14
14
  try {
15
- const client = getClient();
15
+ const client = await getClientAsync();
16
16
  const storage = await client.user.getStorage();
17
17
  out.stopSpinner();
18
18
  if (flags.output === 'json') {
@@ -1,5 +1,5 @@
1
1
  import chalk from 'chalk';
2
- import { getClient } from '../client.js';
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
5
  import { generateVaultKey } from '@lifestreamdynamics/vault-sdk';
@@ -13,7 +13,7 @@ export function registerVaultCommands(program) {
13
13
  const out = createOutput(flags);
14
14
  out.startSpinner('Fetching vaults...');
15
15
  try {
16
- const client = getClient();
16
+ const client = await getClientAsync();
17
17
  const vaultList = await client.vaults.list();
18
18
  out.stopSpinner();
19
19
  out.list(vaultList.map(v => ({ name: v.name, slug: v.slug, encrypted: v.encryptionEnabled ? 'yes' : 'no', description: v.description || 'No description', id: v.id })), {
@@ -43,7 +43,7 @@ export function registerVaultCommands(program) {
43
43
  const out = createOutput(flags);
44
44
  out.startSpinner('Fetching vault...');
45
45
  try {
46
- const client = getClient();
46
+ const client = await getClientAsync();
47
47
  const vault = await client.vaults.get(vaultId);
48
48
  out.stopSpinner();
49
49
  out.record({
@@ -75,7 +75,7 @@ EXAMPLES
75
75
  const out = createOutput(flags);
76
76
  out.startSpinner('Creating vault...');
77
77
  try {
78
- const client = getClient();
78
+ const client = await getClientAsync();
79
79
  const isEncrypted = _opts.encrypted === true;
80
80
  const vault = await client.vaults.create({
81
81
  name,
@@ -1,5 +1,5 @@
1
1
  import chalk from 'chalk';
2
- import { getClient } from '../client.js';
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
5
  export function registerVersionCommands(program) {
@@ -16,7 +16,7 @@ EXAMPLES
16
16
  const out = createOutput(flags);
17
17
  out.startSpinner('Fetching versions...');
18
18
  try {
19
- const client = getClient();
19
+ const client = await getClientAsync();
20
20
  const versionList = await client.documents.listVersions(vaultId, docPath);
21
21
  out.stopSpinner();
22
22
  out.list(versionList.map(v => ({
@@ -65,7 +65,7 @@ EXAMPLES
65
65
  return;
66
66
  }
67
67
  try {
68
- const client = getClient();
68
+ const client = await getClientAsync();
69
69
  const version = await client.documents.getVersion(vaultId, docPath, versionNum);
70
70
  if (version.content === null) {
71
71
  out.error('Version content is no longer available (expired or pruned)');
@@ -99,7 +99,7 @@ EXAMPLES
99
99
  }
100
100
  out.startSpinner('Computing diff...');
101
101
  try {
102
- const client = getClient();
102
+ const client = await getClientAsync();
103
103
  const diff = await client.documents.diffVersions(vaultId, docPath, from, to);
104
104
  out.stopSpinner();
105
105
  if (flags.output === 'json') {
@@ -143,7 +143,7 @@ EXAMPLES
143
143
  }
144
144
  out.startSpinner(`Restoring to version ${versionNum}...`);
145
145
  try {
146
- const client = getClient();
146
+ const client = await getClientAsync();
147
147
  const doc = await client.documents.restoreVersion(vaultId, docPath, versionNum);
148
148
  out.success(`Restored ${chalk.cyan(docPath)} to version ${versionNum}`, {
149
149
  path: doc.path,
@@ -173,7 +173,7 @@ EXAMPLES
173
173
  }
174
174
  out.startSpinner(`Pinning version ${versionNum}...`);
175
175
  try {
176
- const client = getClient();
176
+ const client = await getClientAsync();
177
177
  await client.documents.pinVersion(vaultId, docPath, versionNum);
178
178
  out.success(`Pinned version ${versionNum} of ${chalk.cyan(docPath)}`, {
179
179
  path: docPath,
@@ -204,7 +204,7 @@ EXAMPLES
204
204
  }
205
205
  out.startSpinner(`Unpinning version ${versionNum}...`);
206
206
  try {
207
- const client = getClient();
207
+ const client = await getClientAsync();
208
208
  await client.documents.unpinVersion(vaultId, docPath, versionNum);
209
209
  out.success(`Unpinned version ${versionNum} of ${chalk.cyan(docPath)}`, {
210
210
  path: docPath,
@@ -1,5 +1,5 @@
1
1
  import chalk from 'chalk';
2
- import { getClient } from '../client.js';
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
5
  export function registerWebhookCommands(program) {
@@ -12,7 +12,7 @@ export function registerWebhookCommands(program) {
12
12
  const out = createOutput(flags);
13
13
  out.startSpinner('Fetching webhooks...');
14
14
  try {
15
- const client = getClient();
15
+ const client = await getClientAsync();
16
16
  const webhookList = await client.webhooks.list(vaultId);
17
17
  out.stopSpinner();
18
18
  out.list(webhookList.map(wh => ({
@@ -50,7 +50,7 @@ export function registerWebhookCommands(program) {
50
50
  const out = createOutput(flags);
51
51
  out.startSpinner('Creating webhook...');
52
52
  try {
53
- const client = getClient();
53
+ const client = await getClientAsync();
54
54
  const params = {
55
55
  url,
56
56
  events: String(_opts.events || 'create,update,delete').split(',').map((e) => e.trim()),
@@ -95,7 +95,7 @@ export function registerWebhookCommands(program) {
95
95
  }
96
96
  out.startSpinner('Updating webhook...');
97
97
  try {
98
- const client = getClient();
98
+ const client = await getClientAsync();
99
99
  const params = {};
100
100
  if (_opts.url)
101
101
  params.url = String(_opts.url);
@@ -125,7 +125,7 @@ export function registerWebhookCommands(program) {
125
125
  const out = createOutput(flags);
126
126
  out.startSpinner('Deleting webhook...');
127
127
  try {
128
- const client = getClient();
128
+ const client = await getClientAsync();
129
129
  await client.webhooks.delete(vaultId, webhookId);
130
130
  out.success('Webhook deleted successfully', { id: webhookId, deleted: true });
131
131
  }
@@ -142,7 +142,7 @@ export function registerWebhookCommands(program) {
142
142
  const out = createOutput(flags);
143
143
  out.startSpinner('Fetching deliveries...');
144
144
  try {
145
- const client = getClient();
145
+ const client = await getClientAsync();
146
146
  const deliveries = await client.webhooks.listDeliveries(vaultId, webhookId);
147
147
  out.stopSpinner();
148
148
  out.list(deliveries.map(d => ({
package/dist/config.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { type CredentialManager } from './lib/credential-manager.js';
2
+ export declare const DEFAULT_API_URL = "https://vault.lifestreamdynamics.com";
2
3
  export interface CliConfig {
3
4
  apiUrl: string;
4
5
  apiKey?: string;
package/dist/config.js CHANGED
@@ -4,6 +4,7 @@ import os from 'node:os';
4
4
  import { createCredentialManager } from './lib/credential-manager.js';
5
5
  const CONFIG_DIR = path.join(os.homedir(), '.lsvault');
6
6
  const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
7
+ export const DEFAULT_API_URL = 'https://vault.lifestreamdynamics.com';
7
8
  // Singleton credential manager
8
9
  let _credentialManager;
9
10
  export function getCredentialManager() {
@@ -25,7 +26,7 @@ export function setCredentialManager(cm) {
25
26
  */
26
27
  export function loadConfig() {
27
28
  const config = {
28
- apiUrl: process.env.LSVAULT_API_URL || 'http://localhost:4660',
29
+ apiUrl: process.env.LSVAULT_API_URL || DEFAULT_API_URL,
29
30
  };
30
31
  if (process.env.LSVAULT_API_KEY) {
31
32
  config.apiKey = process.env.LSVAULT_API_KEY;
@@ -48,7 +49,7 @@ export async function loadConfigAsync() {
48
49
  const cm = getCredentialManager();
49
50
  const secureCreds = await cm.getCredentials();
50
51
  const config = {
51
- apiUrl: secureCreds.apiUrl || process.env.LSVAULT_API_URL || 'http://localhost:4660',
52
+ apiUrl: secureCreds.apiUrl || process.env.LSVAULT_API_URL || DEFAULT_API_URL,
52
53
  };
53
54
  // Load JWT tokens if available
54
55
  if (secureCreds.accessToken) {
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from 'commander';
3
3
  import { registerAuthCommands } from './commands/auth.js';
4
+ import { registerMfaCommands } from './commands/mfa.js';
4
5
  import { registerVaultCommands } from './commands/vaults.js';
5
6
  import { registerDocCommands } from './commands/docs.js';
6
7
  import { registerSearchCommands } from './commands/search.js';
@@ -18,6 +19,8 @@ import { registerWebhookCommands } from './commands/webhooks.js';
18
19
  import { registerConfigCommands } from './commands/config.js';
19
20
  import { registerSyncCommands } from './commands/sync.js';
20
21
  import { registerVersionCommands } from './commands/versions.js';
22
+ import { registerLinkCommands } from './commands/links.js';
23
+ import { registerCalendarCommands } from './commands/calendar.js';
21
24
  const program = new Command();
22
25
  program
23
26
  .name('lsvault')
@@ -43,6 +46,7 @@ LEARN MORE
43
46
  lsvault <command> --help Show help for a command
44
47
  lsvault <command> <subcommand> --help Show help for a subcommand`);
45
48
  registerAuthCommands(program);
49
+ registerMfaCommands(program);
46
50
  registerVaultCommands(program);
47
51
  registerDocCommands(program);
48
52
  registerSearchCommands(program);
@@ -60,4 +64,6 @@ registerWebhookCommands(program);
60
64
  registerConfigCommands(program);
61
65
  registerSyncCommands(program);
62
66
  registerVersionCommands(program);
67
+ registerLinkCommands(program);
68
+ registerCalendarCommands(program);
63
69
  program.parse();
@@ -1,6 +1,8 @@
1
1
  const SERVICE_NAME = 'lifestream-vault-cli';
2
2
  const ACCOUNT_API_KEY = 'api-key';
3
3
  const ACCOUNT_API_URL = 'api-url';
4
+ const ACCOUNT_ACCESS_TOKEN = 'access-token';
5
+ const ACCOUNT_REFRESH_TOKEN = 'refresh-token';
4
6
  /**
5
7
  * Dynamically loads keytar. Returns null if unavailable (not installed or
6
8
  * native module fails to load, e.g. missing libsecret on Linux).
@@ -48,6 +50,12 @@ export function createKeychainBackend() {
48
50
  const apiUrl = await kt.getPassword(SERVICE_NAME, ACCOUNT_API_URL);
49
51
  if (apiUrl)
50
52
  result.apiUrl = apiUrl;
53
+ const accessToken = await kt.getPassword(SERVICE_NAME, ACCOUNT_ACCESS_TOKEN);
54
+ if (accessToken)
55
+ result.accessToken = accessToken;
56
+ const refreshToken = await kt.getPassword(SERVICE_NAME, ACCOUNT_REFRESH_TOKEN);
57
+ if (refreshToken)
58
+ result.refreshToken = refreshToken;
51
59
  }
52
60
  catch {
53
61
  // Keychain access failed silently
@@ -64,6 +72,12 @@ export function createKeychainBackend() {
64
72
  if (config.apiUrl) {
65
73
  await kt.setPassword(SERVICE_NAME, ACCOUNT_API_URL, config.apiUrl);
66
74
  }
75
+ if (config.accessToken) {
76
+ await kt.setPassword(SERVICE_NAME, ACCOUNT_ACCESS_TOKEN, config.accessToken);
77
+ }
78
+ if (config.refreshToken) {
79
+ await kt.setPassword(SERVICE_NAME, ACCOUNT_REFRESH_TOKEN, config.refreshToken);
80
+ }
67
81
  },
68
82
  async clearCredentials() {
69
83
  const kt = await getKeytar();
@@ -77,6 +91,14 @@ export function createKeychainBackend() {
77
91
  await kt.deletePassword(SERVICE_NAME, ACCOUNT_API_URL);
78
92
  }
79
93
  catch { /* ignore */ }
94
+ try {
95
+ await kt.deletePassword(SERVICE_NAME, ACCOUNT_ACCESS_TOKEN);
96
+ }
97
+ catch { /* ignore */ }
98
+ try {
99
+ await kt.deletePassword(SERVICE_NAME, ACCOUNT_REFRESH_TOKEN);
100
+ }
101
+ catch { /* ignore */ }
80
102
  },
81
103
  };
82
104
  }
@@ -10,6 +10,8 @@ import { createRemotePoller } from './remote-poller.js';
10
10
  import { removePid } from './daemon.js';
11
11
  import { loadConfig } from '../config.js';
12
12
  import { LifestreamVaultClient } from '@lifestreamdynamics/vault-sdk';
13
+ import { scanLocalFiles, scanRemoteFiles, computePushDiff, computePullDiff, executePush, executePull } from './engine.js';
14
+ import { loadSyncState } from './state.js';
13
15
  const managed = [];
14
16
  function log(msg) {
15
17
  const ts = new Date().toISOString();
@@ -35,6 +37,66 @@ async function start() {
35
37
  }
36
38
  log(`Found ${configs.length} auto-sync configuration(s)`);
37
39
  const client = createClient();
40
+ // Startup reconciliation: catch changes made while daemon was stopped
41
+ for (const config of configs) {
42
+ try {
43
+ log(`Reconciling ${config.id.slice(0, 8)} (${config.mode} mode)...`);
44
+ const ignorePatterns = resolveIgnorePatterns(config.ignore, config.localPath);
45
+ const lastState = loadSyncState(config.id);
46
+ const localFiles = scanLocalFiles(config.localPath, ignorePatterns);
47
+ const remoteFiles = await scanRemoteFiles(client, config.vaultId, ignorePatterns);
48
+ let pushed = 0;
49
+ let pulled = 0;
50
+ let deleted = 0;
51
+ if (config.mode === 'push' || config.mode === 'sync') {
52
+ const pushDiff = computePushDiff(localFiles, remoteFiles, lastState);
53
+ const pushOps = pushDiff.uploads.length + pushDiff.deletes.length;
54
+ if (pushOps > 0) {
55
+ const result = await executePush(client, config, pushDiff);
56
+ pushed = result.filesUploaded;
57
+ deleted += result.filesDeleted;
58
+ if (result.errors.length > 0) {
59
+ for (const err of result.errors) {
60
+ log(` Push error: ${err.path}: ${err.error}`);
61
+ }
62
+ }
63
+ }
64
+ }
65
+ if (config.mode === 'pull' || config.mode === 'sync') {
66
+ const pullDiff = computePullDiff(localFiles, remoteFiles, lastState);
67
+ const pullOps = pullDiff.downloads.length + pullDiff.deletes.length;
68
+ if (pullOps > 0) {
69
+ const result = await executePull(client, config, pullDiff);
70
+ pulled = result.filesDownloaded;
71
+ deleted += result.filesDeleted;
72
+ if (result.errors.length > 0) {
73
+ for (const err of result.errors) {
74
+ log(` Pull error: ${err.path}: ${err.error}`);
75
+ }
76
+ }
77
+ }
78
+ }
79
+ const total = pushed + pulled + deleted;
80
+ if (total > 0) {
81
+ const parts = [];
82
+ if (pushed > 0)
83
+ parts.push(`${pushed} uploaded`);
84
+ if (pulled > 0)
85
+ parts.push(`${pulled} downloaded`);
86
+ if (deleted > 0)
87
+ parts.push(`${deleted} deleted`);
88
+ log(`Reconciled ${config.id.slice(0, 8)}: ${parts.join(', ')}`);
89
+ }
90
+ else {
91
+ log(`Reconciled ${config.id.slice(0, 8)}: up to date`);
92
+ }
93
+ }
94
+ catch (err) {
95
+ const msg = err instanceof Error ? err.message : String(err);
96
+ log(`Reconciliation failed for ${config.id.slice(0, 8)}: ${msg}`);
97
+ // Continue — still start the watcher even if reconciliation fails
98
+ }
99
+ }
38
100
  for (const config of configs) {
39
101
  try {
40
102
  const ignorePatterns = resolveIgnorePatterns(config.ignore, config.localPath);
@@ -36,9 +36,17 @@ export declare function rotateLogIfNeeded(logFile?: string): void;
36
36
  * Start the daemon as a detached child process.
37
37
  * Returns the PID of the spawned process.
38
38
  */
39
- export declare function startDaemon(logFile?: string): number;
39
+ export declare function startDaemon(logFile?: string): {
40
+ pid: number;
41
+ lingerWarning?: string;
42
+ };
40
43
  /**
41
44
  * Stop the running daemon.
42
45
  */
43
46
  export declare function stopDaemon(): boolean;
47
+ /**
48
+ * Check if systemd linger is enabled for the current user (Linux only).
49
+ * When linger is disabled, user services stop when the SSH session ends.
50
+ */
51
+ export declare function checkLingerStatus(): 'enabled' | 'disabled' | 'unknown';
44
52
  export { DAEMON_DIR, PID_FILE, LOG_FILE };
@@ -150,7 +150,12 @@ export function startDaemon(logFile) {
150
150
  writePid(child.pid);
151
151
  child.unref();
152
152
  fs.closeSync(logFd);
153
- return child.pid;
153
+ const result = { pid: child.pid };
154
+ const lingerStatus = checkLingerStatus();
155
+ if (lingerStatus === 'disabled') {
156
+ result.lingerWarning = 'systemd lingering is not enabled for your user. The daemon will stop when you log out. To fix:\n sudo loginctl enable-linger $(whoami)';
157
+ }
158
+ return result;
154
159
  }
155
160
  /**
156
161
  * Stop the running daemon.
@@ -171,4 +176,20 @@ export function stopDaemon() {
171
176
  return false;
172
177
  }
173
178
  }
179
+ /**
180
+ * Check if systemd linger is enabled for the current user (Linux only).
181
+ * When linger is disabled, user services stop when the SSH session ends.
182
+ */
183
+ export function checkLingerStatus() {
184
+ if (process.platform !== 'linux')
185
+ return 'unknown';
186
+ try {
187
+ const username = os.userInfo().username;
188
+ const lingerFile = `/var/lib/systemd/linger/${username}`;
189
+ return fs.existsSync(lingerFile) ? 'enabled' : 'disabled';
190
+ }
191
+ catch {
192
+ return 'unknown';
193
+ }
194
+ }
174
195
  export { DAEMON_DIR, PID_FILE, LOG_FILE };
package/package.json CHANGED
@@ -1,7 +1,10 @@
1
1
  {
2
2
  "name": "@lifestreamdynamics/vault-cli",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Command-line interface for Lifestream Vault",
5
+ "engines": {
6
+ "node": ">=20"
7
+ },
5
8
  "type": "module",
6
9
  "bin": {
7
10
  "lsvault": "./dist/index.js"
@@ -41,7 +44,7 @@
41
44
  "prepublishOnly": "npm run build && npm test"
42
45
  },
43
46
  "dependencies": {
44
- "@lifestreamdynamics/vault-sdk": "^1.0.0",
47
+ "@lifestreamdynamics/vault-sdk": "^1.1.0",
45
48
  "chalk": "^5.4.0",
46
49
  "chokidar": "^4.0.3",
47
50
  "commander": "^13.0.0",
@@ -53,10 +56,10 @@
53
56
  },
54
57
  "devDependencies": {
55
58
  "@types/node": "^22.0.0",
56
- "@vitest/coverage-v8": "^2.0.0",
57
- "@vitest/ui": "^2.0.0",
59
+ "@vitest/coverage-v8": "^4.0.18",
60
+ "@vitest/ui": "^4.0.18",
58
61
  "tsx": "^4.19.0",
59
62
  "typescript": "^5.7.0",
60
- "vitest": "^2.0.0"
63
+ "vitest": "^4.0.18"
61
64
  }
62
65
  }