@zincapp/znvault-cli 2.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.
Files changed (122) hide show
  1. package/README.md +310 -0
  2. package/dist/commands/agent.d.ts +3 -0
  3. package/dist/commands/agent.d.ts.map +1 -0
  4. package/dist/commands/agent.js +660 -0
  5. package/dist/commands/agent.js.map +1 -0
  6. package/dist/commands/apikey.d.ts +3 -0
  7. package/dist/commands/apikey.d.ts.map +1 -0
  8. package/dist/commands/apikey.js +767 -0
  9. package/dist/commands/apikey.js.map +1 -0
  10. package/dist/commands/audit.d.ts +3 -0
  11. package/dist/commands/audit.d.ts.map +1 -0
  12. package/dist/commands/audit.js +147 -0
  13. package/dist/commands/audit.js.map +1 -0
  14. package/dist/commands/auth.d.ts +3 -0
  15. package/dist/commands/auth.d.ts.map +1 -0
  16. package/dist/commands/auth.js +426 -0
  17. package/dist/commands/auth.js.map +1 -0
  18. package/dist/commands/cert.d.ts +3 -0
  19. package/dist/commands/cert.d.ts.map +1 -0
  20. package/dist/commands/cert.js +398 -0
  21. package/dist/commands/cert.js.map +1 -0
  22. package/dist/commands/cluster.d.ts +3 -0
  23. package/dist/commands/cluster.d.ts.map +1 -0
  24. package/dist/commands/cluster.js +228 -0
  25. package/dist/commands/cluster.js.map +1 -0
  26. package/dist/commands/emergency.d.ts +3 -0
  27. package/dist/commands/emergency.d.ts.map +1 -0
  28. package/dist/commands/emergency.js +223 -0
  29. package/dist/commands/emergency.js.map +1 -0
  30. package/dist/commands/health.d.ts +3 -0
  31. package/dist/commands/health.d.ts.map +1 -0
  32. package/dist/commands/health.js +188 -0
  33. package/dist/commands/health.js.map +1 -0
  34. package/dist/commands/lockdown.d.ts +3 -0
  35. package/dist/commands/lockdown.d.ts.map +1 -0
  36. package/dist/commands/lockdown.js +232 -0
  37. package/dist/commands/lockdown.js.map +1 -0
  38. package/dist/commands/permissions.d.ts +3 -0
  39. package/dist/commands/permissions.d.ts.map +1 -0
  40. package/dist/commands/permissions.js +168 -0
  41. package/dist/commands/permissions.js.map +1 -0
  42. package/dist/commands/policy.d.ts +3 -0
  43. package/dist/commands/policy.d.ts.map +1 -0
  44. package/dist/commands/policy.js +660 -0
  45. package/dist/commands/policy.js.map +1 -0
  46. package/dist/commands/superadmin.d.ts +3 -0
  47. package/dist/commands/superadmin.d.ts.map +1 -0
  48. package/dist/commands/superadmin.js +203 -0
  49. package/dist/commands/superadmin.js.map +1 -0
  50. package/dist/commands/tenant.d.ts +3 -0
  51. package/dist/commands/tenant.d.ts.map +1 -0
  52. package/dist/commands/tenant.js +277 -0
  53. package/dist/commands/tenant.js.map +1 -0
  54. package/dist/commands/update.d.ts +9 -0
  55. package/dist/commands/update.d.ts.map +1 -0
  56. package/dist/commands/update.js +359 -0
  57. package/dist/commands/update.js.map +1 -0
  58. package/dist/commands/user.d.ts +3 -0
  59. package/dist/commands/user.d.ts.map +1 -0
  60. package/dist/commands/user.js +363 -0
  61. package/dist/commands/user.js.map +1 -0
  62. package/dist/index.d.ts +3 -0
  63. package/dist/index.d.ts.map +1 -0
  64. package/dist/index.js +82 -0
  65. package/dist/index.js.map +1 -0
  66. package/dist/lib/client.d.ts +246 -0
  67. package/dist/lib/client.d.ts.map +1 -0
  68. package/dist/lib/client.js +734 -0
  69. package/dist/lib/client.js.map +1 -0
  70. package/dist/lib/config.d.ts +130 -0
  71. package/dist/lib/config.d.ts.map +1 -0
  72. package/dist/lib/config.js +342 -0
  73. package/dist/lib/config.js.map +1 -0
  74. package/dist/lib/db.d.ts +111 -0
  75. package/dist/lib/db.d.ts.map +1 -0
  76. package/dist/lib/db.js +698 -0
  77. package/dist/lib/db.js.map +1 -0
  78. package/dist/lib/local.d.ts +41 -0
  79. package/dist/lib/local.d.ts.map +1 -0
  80. package/dist/lib/local.js +236 -0
  81. package/dist/lib/local.js.map +1 -0
  82. package/dist/lib/mode.d.ts +210 -0
  83. package/dist/lib/mode.d.ts.map +1 -0
  84. package/dist/lib/mode.js +389 -0
  85. package/dist/lib/mode.js.map +1 -0
  86. package/dist/lib/output.d.ts +61 -0
  87. package/dist/lib/output.d.ts.map +1 -0
  88. package/dist/lib/output.js +190 -0
  89. package/dist/lib/output.js.map +1 -0
  90. package/dist/lib/prompts.d.ts +32 -0
  91. package/dist/lib/prompts.d.ts.map +1 -0
  92. package/dist/lib/prompts.js +96 -0
  93. package/dist/lib/prompts.js.map +1 -0
  94. package/dist/services/auto-update-daemon.d.ts +48 -0
  95. package/dist/services/auto-update-daemon.d.ts.map +1 -0
  96. package/dist/services/auto-update-daemon.js +296 -0
  97. package/dist/services/auto-update-daemon.js.map +1 -0
  98. package/dist/services/signature-verifier.d.ts +38 -0
  99. package/dist/services/signature-verifier.d.ts.map +1 -0
  100. package/dist/services/signature-verifier.js +209 -0
  101. package/dist/services/signature-verifier.js.map +1 -0
  102. package/dist/services/update-checker.d.ts +39 -0
  103. package/dist/services/update-checker.d.ts.map +1 -0
  104. package/dist/services/update-checker.js +198 -0
  105. package/dist/services/update-checker.js.map +1 -0
  106. package/dist/services/update-installer.d.ts +54 -0
  107. package/dist/services/update-installer.d.ts.map +1 -0
  108. package/dist/services/update-installer.js +360 -0
  109. package/dist/services/update-installer.js.map +1 -0
  110. package/dist/types/index.d.ts +411 -0
  111. package/dist/types/index.d.ts.map +1 -0
  112. package/dist/types/index.js +2 -0
  113. package/dist/types/index.js.map +1 -0
  114. package/dist/types/update.d.ts +137 -0
  115. package/dist/types/update.d.ts.map +1 -0
  116. package/dist/types/update.js +27 -0
  117. package/dist/types/update.js.map +1 -0
  118. package/dist/utils/platform.d.ts +35 -0
  119. package/dist/utils/platform.d.ts.map +1 -0
  120. package/dist/utils/platform.js +115 -0
  121. package/dist/utils/platform.js.map +1 -0
  122. package/package.json +59 -0
@@ -0,0 +1,660 @@
1
+ import ora from 'ora';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import * as os from 'os';
5
+ import { spawn } from 'child_process';
6
+ import * as mode from '../lib/mode.js';
7
+ import * as output from '../lib/output.js';
8
+ import * as config from '../lib/config.js';
9
+ // Config locations - match standalone agent
10
+ const SYSTEM_CONFIG_DIR = '/etc/zn-vault-agent';
11
+ const SYSTEM_CONFIG_FILE = path.join(SYSTEM_CONFIG_DIR, 'config.json');
12
+ const USER_CONFIG_DIR = path.join(os.homedir(), '.config', 'zn-vault-agent');
13
+ const USER_CONFIG_FILE = path.join(USER_CONFIG_DIR, 'config.json');
14
+ /**
15
+ * Format relative time for display
16
+ */
17
+ function formatRelativeTime(dateStr) {
18
+ const date = new Date(dateStr);
19
+ const now = new Date();
20
+ const diffMs = now.getTime() - date.getTime();
21
+ const diffMins = Math.floor(diffMs / 60000);
22
+ const diffHours = Math.floor(diffMins / 60);
23
+ const diffDays = Math.floor(diffHours / 24);
24
+ if (diffMins < 1)
25
+ return 'just now';
26
+ if (diffMins < 60)
27
+ return `${diffMins}m ago`;
28
+ if (diffHours < 24)
29
+ return `${diffHours}h ago`;
30
+ return `${diffDays}d ago`;
31
+ }
32
+ // State file location
33
+ const STATE_DIR = path.join(os.homedir(), '.local', 'state', 'zn-vault-agent');
34
+ const DEFAULT_STATE_FILE = path.join(STATE_DIR, 'state.json');
35
+ /**
36
+ * Get the appropriate config file path based on privileges
37
+ */
38
+ function getConfigPath() {
39
+ // If running as root and system config exists, use it
40
+ if (process.getuid?.() === 0) {
41
+ return SYSTEM_CONFIG_FILE;
42
+ }
43
+ // Check if system config exists (for non-root reading)
44
+ if (fs.existsSync(SYSTEM_CONFIG_FILE)) {
45
+ return SYSTEM_CONFIG_FILE;
46
+ }
47
+ // Fall back to user config
48
+ return USER_CONFIG_FILE;
49
+ }
50
+ /**
51
+ * Load agent configuration
52
+ */
53
+ function loadConfig(configPath) {
54
+ const filePath = configPath || getConfigPath();
55
+ if (!fs.existsSync(filePath)) {
56
+ return null;
57
+ }
58
+ try {
59
+ return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
60
+ }
61
+ catch {
62
+ return null;
63
+ }
64
+ }
65
+ /**
66
+ * Save agent configuration
67
+ */
68
+ function saveConfig(config, configPath) {
69
+ const filePath = configPath || getConfigPath();
70
+ const dir = path.dirname(filePath);
71
+ if (!fs.existsSync(dir)) {
72
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
73
+ }
74
+ fs.writeFileSync(filePath, JSON.stringify(config, null, 2), { mode: 0o600 });
75
+ }
76
+ /**
77
+ * Create default config with current CLI credentials
78
+ */
79
+ async function createDefaultConfig() {
80
+ const cliConfig = config.getConfig();
81
+ const credentials = config.getCredentials();
82
+ const envCredentials = config.getEnvCredentials();
83
+ const apiKey = config.getApiKey();
84
+ // Get tenant from: env > stored credentials > default tenant
85
+ const tenantId = process.env.ZNVAULT_TENANT_ID ||
86
+ credentials?.tenantId ||
87
+ cliConfig.defaultTenant ||
88
+ '';
89
+ return {
90
+ vaultUrl: cliConfig.url || '',
91
+ tenantId,
92
+ auth: {
93
+ apiKey: apiKey,
94
+ username: envCredentials?.username,
95
+ password: envCredentials?.password,
96
+ },
97
+ insecure: cliConfig.insecure,
98
+ targets: [],
99
+ pollInterval: 3600,
100
+ };
101
+ }
102
+ export function registerAgentCommands(program) {
103
+ const agent = program
104
+ .command('agent')
105
+ .description('Certificate synchronization agent configuration and management');
106
+ // Initialize agent configuration
107
+ agent
108
+ .command('init')
109
+ .description('Initialize agent configuration')
110
+ .option('-c, --config <path>', 'Config file path')
111
+ .action(async (options) => {
112
+ const configPath = options.config || getConfigPath();
113
+ if (fs.existsSync(configPath)) {
114
+ output.error(`Config already exists at ${configPath}`);
115
+ output.info('Use "znvault agent add" to add certificates');
116
+ process.exit(1);
117
+ }
118
+ const config = await createDefaultConfig();
119
+ saveConfig(config, configPath);
120
+ console.log(`Agent configuration initialized at ${configPath}`);
121
+ console.log();
122
+ console.log('Next steps:');
123
+ console.log(' 1. Add certificates: znvault agent add <cert-id> --combined /path/to/cert.pem');
124
+ console.log(' 2. Start the agent: zn-vault-agent start');
125
+ console.log();
126
+ console.log('Or install as systemd service:');
127
+ console.log(' sudo systemctl enable --now zn-vault-agent');
128
+ });
129
+ // Add certificate to sync
130
+ agent
131
+ .command('add <cert-id>')
132
+ .description('Add a certificate to sync')
133
+ .option('-n, --name <name>', 'Human-readable name for the certificate')
134
+ .option('--combined <path>', 'Output path for combined cert+key file (HAProxy)')
135
+ .option('--cert <path>', 'Output path for certificate file')
136
+ .option('--key <path>', 'Output path for private key file')
137
+ .option('--chain <path>', 'Output path for CA chain file')
138
+ .option('--fullchain <path>', 'Output path for fullchain file (cert+chain)')
139
+ .option('--owner <user:group>', 'File ownership (e.g., haproxy:haproxy)')
140
+ .option('--mode <mode>', 'File permissions (e.g., 0640)', '0640')
141
+ .option('--reload <command>', 'Command to run after cert update')
142
+ .option('--health-check <command>', 'Health check command (must return 0)')
143
+ .option('-c, --config <path>', 'Config file path')
144
+ .action(async (certId, options) => {
145
+ const spinner = ora('Validating certificate...').start();
146
+ try {
147
+ // Validate the certificate exists
148
+ const cert = await mode.apiGet(`/v1/certificates/${certId}`);
149
+ spinner.stop();
150
+ // Load or create config
151
+ const configPath = options.config || getConfigPath();
152
+ let config = loadConfig(configPath);
153
+ if (!config) {
154
+ output.info('No config found, creating with current CLI credentials...');
155
+ config = await createDefaultConfig();
156
+ }
157
+ // Check if already added
158
+ if (config.targets.some(t => t.certId === certId)) {
159
+ output.error(`Certificate ${certId} is already configured`);
160
+ process.exit(1);
161
+ }
162
+ // Validate at least one output is specified
163
+ if (!options.combined && !options.cert && !options.key && !options.chain && !options.fullchain) {
164
+ output.error('At least one output path is required (--combined, --cert, --key, --chain, or --fullchain)');
165
+ process.exit(1);
166
+ }
167
+ const target = {
168
+ certId,
169
+ name: options.name || cert.alias,
170
+ outputs: {},
171
+ mode: options.mode,
172
+ };
173
+ if (options.combined)
174
+ target.outputs.combined = options.combined;
175
+ if (options.cert)
176
+ target.outputs.cert = options.cert;
177
+ if (options.key)
178
+ target.outputs.key = options.key;
179
+ if (options.chain)
180
+ target.outputs.chain = options.chain;
181
+ if (options.fullchain)
182
+ target.outputs.fullchain = options.fullchain;
183
+ if (options.owner)
184
+ target.owner = options.owner;
185
+ if (options.reload)
186
+ target.reloadCmd = options.reload;
187
+ if (options.healthCheck)
188
+ target.healthCheckCmd = options.healthCheck;
189
+ config.targets.push(target);
190
+ saveConfig(config, configPath);
191
+ console.log(`Added certificate: ${target.name} (${certId})`);
192
+ if (target.outputs.combined)
193
+ console.log(` Combined: ${target.outputs.combined}`);
194
+ if (target.outputs.cert)
195
+ console.log(` Certificate: ${target.outputs.cert}`);
196
+ if (target.outputs.key)
197
+ console.log(` Private key: ${target.outputs.key}`);
198
+ if (target.outputs.chain)
199
+ console.log(` Chain: ${target.outputs.chain}`);
200
+ if (target.outputs.fullchain)
201
+ console.log(` Fullchain: ${target.outputs.fullchain}`);
202
+ if (target.reloadCmd)
203
+ console.log(` Reload: ${target.reloadCmd}`);
204
+ }
205
+ catch (err) {
206
+ spinner.fail('Failed to add certificate');
207
+ output.error(err instanceof Error ? err.message : String(err));
208
+ process.exit(1);
209
+ }
210
+ finally {
211
+ await mode.closeLocalClient();
212
+ }
213
+ });
214
+ // Remove certificate from sync
215
+ agent
216
+ .command('remove <cert-id-or-name>')
217
+ .description('Remove a certificate from sync')
218
+ .option('-c, --config <path>', 'Config file path')
219
+ .action(async (certIdOrName, options) => {
220
+ const configPath = options.config || getConfigPath();
221
+ const config = loadConfig(configPath);
222
+ if (!config) {
223
+ output.error(`Config not found. Run 'znvault agent init' first.`);
224
+ process.exit(1);
225
+ }
226
+ const idx = config.targets.findIndex(t => t.certId === certIdOrName || t.name === certIdOrName);
227
+ if (idx === -1) {
228
+ output.error(`Certificate "${certIdOrName}" not found in configuration`);
229
+ process.exit(1);
230
+ }
231
+ const removed = config.targets.splice(idx, 1)[0];
232
+ saveConfig(config, configPath);
233
+ console.log(`Removed certificate: ${removed.name} (${removed.certId})`);
234
+ });
235
+ // List configured certificates
236
+ agent
237
+ .command('list')
238
+ .description('List configured certificates')
239
+ .option('-c, --config <path>', 'Config file path')
240
+ .option('--json', 'Output as JSON')
241
+ .action(async (options) => {
242
+ const configPath = options.config || getConfigPath();
243
+ const config = loadConfig(configPath);
244
+ if (!config) {
245
+ output.error(`Config not found. Run 'znvault agent init' first.`);
246
+ process.exit(1);
247
+ }
248
+ if (options.json) {
249
+ output.json(config);
250
+ return;
251
+ }
252
+ console.log(`Config: ${configPath}`);
253
+ console.log(`Vault: ${config.vaultUrl}`);
254
+ console.log(`Tenant: ${config.tenantId}`);
255
+ console.log(`Certificates: ${config.targets.length}`);
256
+ console.log();
257
+ if (config.targets.length === 0) {
258
+ console.log('No certificates configured. Use "znvault agent add <cert-id>" to add one.');
259
+ return;
260
+ }
261
+ output.table(['Name', 'Cert ID', 'Outputs', 'Reload'], config.targets.map(t => [
262
+ t.name,
263
+ t.certId.substring(0, 8) + '...',
264
+ Object.entries(t.outputs).filter(([, v]) => v).map(([k]) => k).join(', '),
265
+ t.reloadCmd ? t.reloadCmd.substring(0, 30) : '-',
266
+ ]));
267
+ });
268
+ // Sync certificates (one-time)
269
+ agent
270
+ .command('sync')
271
+ .description('Sync all configured certificates (one-time)')
272
+ .option('-c, --config <path>', 'Config file path')
273
+ .option('-s, --state <path>', 'State file path', DEFAULT_STATE_FILE)
274
+ .option('--force', 'Force sync even if unchanged')
275
+ .action(async (options) => {
276
+ const spinner = ora('Syncing certificates...').start();
277
+ try {
278
+ const configPath = options.config || getConfigPath();
279
+ const config = loadConfig(configPath);
280
+ if (!config) {
281
+ spinner.fail('Config not found');
282
+ output.error(`Run 'znvault agent init' first.`);
283
+ process.exit(1);
284
+ }
285
+ if (config.targets.length === 0) {
286
+ spinner.fail('No certificates configured');
287
+ output.error('Use "znvault agent add <cert-id>" to add certificates.');
288
+ process.exit(1);
289
+ }
290
+ // Load or create state
291
+ let state = { certificates: {}, lastUpdate: new Date().toISOString() };
292
+ if (fs.existsSync(options.state)) {
293
+ state = JSON.parse(fs.readFileSync(options.state, 'utf-8'));
294
+ }
295
+ let synced = 0;
296
+ let skipped = 0;
297
+ let failed = 0;
298
+ for (const target of config.targets) {
299
+ try {
300
+ // Get certificate with decrypted data
301
+ const cert = await mode.apiPost(`/v1/certificates/${target.certId}/decrypt`, { purpose: 'agent-sync' });
302
+ // Check if changed (use fingerprintSha256 as fingerprint may be null)
303
+ const certFingerprint = cert.fingerprintSha256 || cert.fingerprint;
304
+ const existingState = state.certificates[target.certId];
305
+ if (!options.force && existingState && existingState.fingerprint === certFingerprint) {
306
+ skipped++;
307
+ continue;
308
+ }
309
+ // Decode certificate data
310
+ const certData = Buffer.from(cert.certificateData, 'base64').toString('utf-8');
311
+ const keyData = cert.privateKeyData ? Buffer.from(cert.privateKeyData, 'base64').toString('utf-8') : null;
312
+ const chainData = cert.chainData ? Buffer.from(cert.chainData, 'base64').toString('utf-8') : null;
313
+ const fileMode = parseInt(target.mode || '0640', 8);
314
+ // Write certificate file
315
+ if (target.outputs.cert) {
316
+ ensureDir(path.dirname(target.outputs.cert));
317
+ fs.writeFileSync(target.outputs.cert, certData, { mode: fileMode });
318
+ }
319
+ // Write private key
320
+ if (target.outputs.key && keyData) {
321
+ ensureDir(path.dirname(target.outputs.key));
322
+ fs.writeFileSync(target.outputs.key, keyData, { mode: 0o600 });
323
+ }
324
+ // Write chain
325
+ if (target.outputs.chain && chainData) {
326
+ ensureDir(path.dirname(target.outputs.chain));
327
+ fs.writeFileSync(target.outputs.chain, chainData, { mode: fileMode });
328
+ }
329
+ // Write fullchain (cert + chain)
330
+ if (target.outputs.fullchain) {
331
+ let fullchain = certData;
332
+ if (chainData)
333
+ fullchain += '\n' + chainData;
334
+ ensureDir(path.dirname(target.outputs.fullchain));
335
+ fs.writeFileSync(target.outputs.fullchain, fullchain, { mode: fileMode });
336
+ }
337
+ // Write combined (cert + key + chain)
338
+ if (target.outputs.combined) {
339
+ let combined = certData;
340
+ if (keyData)
341
+ combined += '\n' + keyData;
342
+ if (chainData)
343
+ combined += '\n' + chainData;
344
+ ensureDir(path.dirname(target.outputs.combined));
345
+ fs.writeFileSync(target.outputs.combined, combined, { mode: 0o600 });
346
+ }
347
+ // Update state
348
+ state.certificates[target.certId] = {
349
+ id: target.certId,
350
+ alias: cert.alias,
351
+ lastSync: new Date().toISOString(),
352
+ version: cert.version || 1,
353
+ fingerprint: certFingerprint,
354
+ };
355
+ synced++;
356
+ }
357
+ catch (err) {
358
+ failed++;
359
+ console.error(`\nFailed to sync ${target.name}: ${err instanceof Error ? err.message : String(err)}`);
360
+ }
361
+ }
362
+ // Save state
363
+ state.lastUpdate = new Date().toISOString();
364
+ ensureDir(path.dirname(options.state));
365
+ fs.writeFileSync(options.state, JSON.stringify(state, null, 2));
366
+ spinner.stop();
367
+ console.log(`Sync complete: ${synced} synced, ${skipped} unchanged, ${failed} failed`);
368
+ }
369
+ catch (err) {
370
+ spinner.fail('Sync failed');
371
+ output.error(err instanceof Error ? err.message : String(err));
372
+ process.exit(1);
373
+ }
374
+ finally {
375
+ await mode.closeLocalClient();
376
+ }
377
+ });
378
+ // Start agent daemon (delegates to standalone agent)
379
+ agent
380
+ .command('start')
381
+ .description('Start the certificate sync agent daemon')
382
+ .option('-c, --config <path>', 'Config file path')
383
+ .option('-v, --verbose', 'Enable verbose logging')
384
+ .option('--health-port <port>', 'Enable health/metrics HTTP server')
385
+ .option('--foreground', 'Run in foreground')
386
+ .action(async (options) => {
387
+ const configPath = options.config || getConfigPath();
388
+ if (!fs.existsSync(configPath)) {
389
+ output.error(`Config not found at ${configPath}`);
390
+ output.info(`Run 'znvault agent init' first.`);
391
+ process.exit(1);
392
+ }
393
+ // Build command arguments
394
+ const args = ['start'];
395
+ if (options.verbose)
396
+ args.push('--verbose');
397
+ if (options.healthPort)
398
+ args.push('--health-port', options.healthPort);
399
+ // Set config path via environment if not default
400
+ const env = { ...process.env };
401
+ if (options.config) {
402
+ env.ZNVAULT_AGENT_CONFIG_DIR = path.dirname(options.config);
403
+ }
404
+ console.log('Starting zn-vault-agent daemon...');
405
+ console.log();
406
+ // Try to find the standalone agent
407
+ const agentPaths = [
408
+ '/usr/local/bin/zn-vault-agent',
409
+ '/usr/bin/zn-vault-agent',
410
+ path.join(os.homedir(), '.local', 'bin', 'zn-vault-agent'),
411
+ // Development: check sibling directory
412
+ path.resolve(__dirname, '..', '..', '..', '..', 'zn-vault-agent', 'dist', 'index.js'),
413
+ ];
414
+ let agentPath = null;
415
+ for (const p of agentPaths) {
416
+ if (fs.existsSync(p)) {
417
+ agentPath = p;
418
+ break;
419
+ }
420
+ }
421
+ if (!agentPath) {
422
+ output.error('zn-vault-agent not found');
423
+ console.log();
424
+ console.log('Install the standalone agent:');
425
+ console.log(' cd zn-vault-agent && npm install && npm run build');
426
+ console.log(' sudo ./deploy/install.sh');
427
+ console.log();
428
+ console.log('Or run directly:');
429
+ console.log(' cd zn-vault-agent && npm run start -- start');
430
+ process.exit(1);
431
+ }
432
+ // Determine how to run it
433
+ const isJsFile = agentPath.endsWith('.js');
434
+ const command = isJsFile ? 'node' : agentPath;
435
+ const spawnArgs = isJsFile ? [agentPath, ...args] : args;
436
+ // Spawn the agent
437
+ const child = spawn(command, spawnArgs, {
438
+ env,
439
+ stdio: 'inherit',
440
+ detached: !options.foreground,
441
+ });
442
+ if (!options.foreground) {
443
+ child.unref();
444
+ console.log(`Agent started with PID ${child.pid}`);
445
+ process.exit(0);
446
+ }
447
+ // In foreground mode, wait for the process
448
+ child.on('exit', (code) => {
449
+ process.exit(code || 0);
450
+ });
451
+ });
452
+ // Show agent status
453
+ agent
454
+ .command('status')
455
+ .description('Show agent configuration and sync status')
456
+ .option('-c, --config <path>', 'Config file path')
457
+ .option('-s, --state <path>', 'State file path', DEFAULT_STATE_FILE)
458
+ .option('--json', 'Output as JSON')
459
+ .action(async (options) => {
460
+ const configPath = options.config || getConfigPath();
461
+ const config = loadConfig(configPath);
462
+ if (!config) {
463
+ output.error(`Config not found. Run 'znvault agent init' first.`);
464
+ process.exit(1);
465
+ }
466
+ let state = { certificates: {}, lastUpdate: 'never' };
467
+ if (fs.existsSync(options.state)) {
468
+ state = JSON.parse(fs.readFileSync(options.state, 'utf-8'));
469
+ }
470
+ if (options.json) {
471
+ output.json({ config, state });
472
+ return;
473
+ }
474
+ console.log('Agent Configuration:');
475
+ console.log(` Config file: ${configPath}`);
476
+ console.log(` Vault URL: ${config.vaultUrl}`);
477
+ console.log(` Tenant: ${config.tenantId}`);
478
+ console.log(` Certificates: ${config.targets.length}`);
479
+ console.log(` Last sync: ${state.lastUpdate}`);
480
+ console.log();
481
+ if (config.targets.length === 0) {
482
+ console.log('No certificates configured.');
483
+ return;
484
+ }
485
+ output.table(['Name', 'Cert ID', 'Last Sync', 'Version', 'Fingerprint'], config.targets.map(t => {
486
+ const s = state.certificates[t.certId];
487
+ return [
488
+ t.name,
489
+ t.certId.substring(0, 8) + '...',
490
+ s ? new Date(s.lastSync).toLocaleString() : 'never',
491
+ s ? String(s.version) : '-',
492
+ s?.fingerprint ? s.fingerprint.substring(0, 16) + '...' : '-',
493
+ ];
494
+ }));
495
+ });
496
+ // ===== Remote Agent Management Commands =====
497
+ const remote = agent
498
+ .command('remote')
499
+ .description('Manage agents registered with the vault');
500
+ // List remote agents
501
+ remote
502
+ .command('list')
503
+ .description('List agents registered with the vault')
504
+ .option('--status <status>', 'Filter by status (online, offline)')
505
+ .option('--tenant <tenantId>', 'Filter by tenant (superadmin only)')
506
+ .option('--json', 'Output as JSON')
507
+ .action(async (options) => {
508
+ const spinner = ora('Fetching agents...').start();
509
+ try {
510
+ const params = new URLSearchParams();
511
+ if (options.status)
512
+ params.set('status', options.status);
513
+ if (options.tenant)
514
+ params.set('tenantId', options.tenant);
515
+ params.set('pageSize', '100');
516
+ const query = params.toString();
517
+ const response = await mode.apiGet(`/v1/agents${query ? `?${query}` : ''}`);
518
+ spinner.stop();
519
+ if (options.json) {
520
+ output.json(response);
521
+ return;
522
+ }
523
+ if (response.agents.length === 0) {
524
+ console.log('No agents registered');
525
+ return;
526
+ }
527
+ console.log(`Total agents: ${response.pagination.totalItems}`);
528
+ console.log();
529
+ output.table(['Hostname', 'Status', 'Last Seen', 'Version', 'Platform', 'Alerts'], response.agents.map(a => [
530
+ a.hostname,
531
+ a.status === 'online' ? '● online' : '○ offline',
532
+ formatRelativeTime(a.lastSeen),
533
+ a.version || '-',
534
+ a.platform || '-',
535
+ a.alertOnDisconnect ? 'enabled' : 'disabled',
536
+ ]));
537
+ }
538
+ catch (err) {
539
+ spinner.fail('Failed to fetch agents');
540
+ output.error(err instanceof Error ? err.message : String(err));
541
+ process.exit(1);
542
+ }
543
+ finally {
544
+ await mode.closeLocalClient();
545
+ }
546
+ });
547
+ // Show active connections
548
+ remote
549
+ .command('connections')
550
+ .description('Show active WebSocket connections')
551
+ .option('--tenant <tenantId>', 'Filter by tenant (superadmin only)')
552
+ .option('--json', 'Output as JSON')
553
+ .action(async (options) => {
554
+ const spinner = ora('Fetching connections...').start();
555
+ try {
556
+ const query = options.tenant ? `?tenantId=${encodeURIComponent(options.tenant)}` : '';
557
+ const response = await mode.apiGet(`/v1/agents/connections${query}`);
558
+ spinner.stop();
559
+ if (options.json) {
560
+ output.json(response);
561
+ return;
562
+ }
563
+ if (response.connections.length === 0) {
564
+ console.log('No active connections');
565
+ return;
566
+ }
567
+ console.log(`Active connections: ${response.totalConnections}`);
568
+ console.log();
569
+ output.table(['Hostname', 'Tenant', 'Version', 'Platform', 'Connected'], response.connections.map(c => [
570
+ c.hostname,
571
+ c.tenantId,
572
+ c.version,
573
+ c.platform,
574
+ formatRelativeTime(c.connectedAt),
575
+ ]));
576
+ }
577
+ catch (err) {
578
+ spinner.fail('Failed to fetch connections');
579
+ output.error(err instanceof Error ? err.message : String(err));
580
+ process.exit(1);
581
+ }
582
+ finally {
583
+ await mode.closeLocalClient();
584
+ }
585
+ });
586
+ // Configure agent alerts
587
+ remote
588
+ .command('alerts <agent-id>')
589
+ .description('Configure disconnect alerts for an agent')
590
+ .option('--enable', 'Enable disconnect alerts')
591
+ .option('--disable', 'Disable disconnect alerts')
592
+ .option('--threshold <seconds>', 'Set disconnect threshold in seconds', '600')
593
+ .action(async (agentId, options) => {
594
+ if (!options.enable && !options.disable) {
595
+ output.error('Specify --enable or --disable');
596
+ process.exit(1);
597
+ }
598
+ const spinner = ora('Updating agent alerts...').start();
599
+ try {
600
+ const payload = {};
601
+ if (options.enable)
602
+ payload.alertOnDisconnect = true;
603
+ if (options.disable)
604
+ payload.alertOnDisconnect = false;
605
+ if (options.threshold)
606
+ payload.disconnectThresholdSeconds = parseInt(options.threshold, 10);
607
+ const agent = await mode.apiPatch(`/v1/agents/${encodeURIComponent(agentId)}/alerts`, payload);
608
+ spinner.succeed(`Alerts ${agent.alertOnDisconnect ? 'enabled' : 'disabled'} for ${agent.hostname}`);
609
+ if (agent.alertOnDisconnect) {
610
+ console.log(` Threshold: ${agent.disconnectThresholdSeconds} seconds`);
611
+ }
612
+ }
613
+ catch (err) {
614
+ spinner.fail('Failed to update alerts');
615
+ output.error(err instanceof Error ? err.message : String(err));
616
+ process.exit(1);
617
+ }
618
+ finally {
619
+ await mode.closeLocalClient();
620
+ }
621
+ });
622
+ // Delete agent
623
+ remote
624
+ .command('delete <agent-id>')
625
+ .description('Remove an agent from the vault')
626
+ .option('-y, --yes', 'Skip confirmation')
627
+ .action(async (agentId, options) => {
628
+ if (!options.yes) {
629
+ const readline = await import('readline');
630
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
631
+ const answer = await new Promise(resolve => {
632
+ rl.question(`Delete agent ${agentId}? This will remove all activity history. [y/N] `, resolve);
633
+ });
634
+ rl.close();
635
+ if (answer.toLowerCase() !== 'y') {
636
+ console.log('Cancelled');
637
+ return;
638
+ }
639
+ }
640
+ const spinner = ora('Deleting agent...').start();
641
+ try {
642
+ await mode.apiDelete(`/v1/agents/${encodeURIComponent(agentId)}`);
643
+ spinner.succeed('Agent deleted');
644
+ }
645
+ catch (err) {
646
+ spinner.fail('Failed to delete agent');
647
+ output.error(err instanceof Error ? err.message : String(err));
648
+ process.exit(1);
649
+ }
650
+ finally {
651
+ await mode.closeLocalClient();
652
+ }
653
+ });
654
+ }
655
+ function ensureDir(dir) {
656
+ if (!fs.existsSync(dir)) {
657
+ fs.mkdirSync(dir, { recursive: true });
658
+ }
659
+ }
660
+ //# sourceMappingURL=agent.js.map