cryptoserve 0.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.
@@ -0,0 +1,812 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * CryptoServe CLI — zero-dependency Node.js CLI.
5
+ *
6
+ * Usage:
7
+ * cryptoserve help
8
+ * cryptoserve version
9
+ * cryptoserve init [--insecure-storage]
10
+ * cryptoserve pqc [--profile P] [--format json] [--verbose]
11
+ * cryptoserve scan [path] [--format json]
12
+ * cryptoserve encrypt "text" [--context C | --algorithm A] [--password P]
13
+ * cryptoserve decrypt "blob" [--password P]
14
+ * cryptoserve encrypt --file in --output out [--context C | --algorithm A] [--password P]
15
+ * cryptoserve decrypt --file in --output out [--password P]
16
+ * cryptoserve hash-password [--algorithm scrypt|pbkdf2]
17
+ * cryptoserve context list | show NAME [--verbose] [--format json]
18
+ * cryptoserve vault init|set|get|list|delete|run|import|export
19
+ * cryptoserve login [--server URL]
20
+ * cryptoserve status
21
+ */
22
+
23
+ import { readFileSync } from 'node:fs';
24
+ import { resolve, dirname, join } from 'node:path';
25
+ import { fileURLToPath } from 'node:url';
26
+
27
+ const __dirname = dirname(fileURLToPath(import.meta.url));
28
+ const PKG = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Arg parsing helpers
32
+ // ---------------------------------------------------------------------------
33
+
34
+ function getFlag(args, name) {
35
+ const idx = args.indexOf(name);
36
+ return idx !== -1;
37
+ }
38
+
39
+ function getOption(args, name, defaultValue = null) {
40
+ const idx = args.indexOf(name);
41
+ if (idx === -1 || idx + 1 >= args.length) return defaultValue;
42
+ return args[idx + 1];
43
+ }
44
+
45
+ function getPositional(args, optionsWithValues = ['--password', '--algorithm', '--profile', '--format', '--file', '--output', '--server', '--context']) {
46
+ const result = [];
47
+ for (let i = 0; i < args.length; i++) {
48
+ if (args[i].startsWith('--')) {
49
+ // Skip the flag; if it takes a value, skip the next arg too
50
+ if (optionsWithValues.includes(args[i])) i++;
51
+ continue;
52
+ }
53
+ result.push(args[i]);
54
+ }
55
+ return result;
56
+ }
57
+
58
+ // ---------------------------------------------------------------------------
59
+ // Commands
60
+ // ---------------------------------------------------------------------------
61
+
62
+ async function cmdHelp() {
63
+ const { compactHeader, dim, bold, info } = await import('../lib/cli-style.mjs');
64
+ console.log(compactHeader());
65
+ console.log(` ${bold('CryptoServe CLI')} v${PKG.version}`);
66
+ console.log(` ${dim('Cryptographic scanning, PQC analysis, encryption, and key management')}\n`);
67
+ console.log(` ${bold('Scanning & Analysis')}`);
68
+ console.log(` ${info('pqc [--profile P] [--format json]')} Post-quantum readiness analysis`);
69
+ console.log(` ${info('scan [path] [--format json]')} Scan project for crypto & secrets`);
70
+ console.log();
71
+ console.log(` ${bold('Encryption')}`);
72
+ console.log(` ${info('encrypt "text" [--context C]')} Encrypt with context-aware algorithm selection`);
73
+ console.log(` ${info('encrypt "text" [--password P]')} Encrypt text (interactive password if omitted)`);
74
+ console.log(` ${info('decrypt "blob" [--password P]')} Decrypt text`);
75
+ console.log(` ${info('encrypt --file F --output O')} Encrypt file`);
76
+ console.log(` ${info('decrypt --file F --output O')} Decrypt file`);
77
+ console.log(` ${info('hash-password [--algorithm A]')} Hash a password (scrypt/pbkdf2)`);
78
+ console.log();
79
+ console.log(` ${bold('Contexts')}`);
80
+ console.log(` ${info('context list')} List available encryption contexts`);
81
+ console.log(` ${info('context show NAME [--verbose]')} Show context details and resolved algorithm`);
82
+ console.log();
83
+ console.log(` ${bold('Key Management')}`);
84
+ console.log(` ${info('init [--insecure-storage]')} Set up master key + AI tool protection`);
85
+ console.log(` ${info('vault init')} Create encrypted vault`);
86
+ console.log(` ${info('vault set KEY VALUE')} Store a secret`);
87
+ console.log(` ${info('vault get KEY')} Retrieve a secret`);
88
+ console.log(` ${info('vault list')} List stored secrets`);
89
+ console.log(` ${info('vault delete KEY')} Remove a secret`);
90
+ console.log(` ${info('vault run -- CMD [ARGS]')} Run command with secrets as env vars`);
91
+ console.log(` ${info('vault import .env')} Import .env file into vault`);
92
+ console.log();
93
+ console.log(` ${bold('Platform')}`);
94
+ console.log(` ${info('login [--server URL]')} Authenticate with CryptoServe server`);
95
+ console.log(` ${info('status')} Show configuration and server status`);
96
+ console.log(` ${info('version')} Show version`);
97
+ console.log(` ${info('help')} Show this help`);
98
+ console.log();
99
+ }
100
+
101
+ async function cmdVersion() {
102
+ console.log(`cryptoserve ${PKG.version}`);
103
+ }
104
+
105
+ async function cmdInit(args) {
106
+ const { compactHeader, success, info, warning, dim, labelValue } = await import('../lib/cli-style.mjs');
107
+ const { initProject } = await import('../lib/init.mjs');
108
+
109
+ console.log(compactHeader('init'));
110
+
111
+ const insecure = getFlag(args, '--insecure-storage');
112
+ const result = await initProject(process.cwd(), { insecureStorage: insecure });
113
+
114
+ // Key storage
115
+ if (result.keyStorage?.storage === 'keychain') {
116
+ console.log(success(`Master key stored in OS keychain (${result.keyStorage.platform})`));
117
+ } else if (result.keyStorage?.storage === 'encrypted-file') {
118
+ console.log(success(`Master key stored in encrypted file`));
119
+ } else if (result.keyStorage?.storage === 'plaintext-file') {
120
+ console.log(warning('Master key stored as plaintext (--insecure-storage)'));
121
+ } else if (result.keyStorage?.storage === 'existing') {
122
+ console.log(info('Master key already configured'));
123
+ }
124
+
125
+ // AI tools
126
+ if (result.toolsDetected.length > 0) {
127
+ console.log(`\n ${dim('Detected AI tools:')}`);
128
+ for (const tool of result.toolsDetected) {
129
+ console.log(` ${success(tool)}`);
130
+ }
131
+ }
132
+
133
+ if (result.toolsConfigured.length > 0) {
134
+ console.log(`\n ${dim('Configured protections:')}`);
135
+ for (const tool of result.toolsConfigured) {
136
+ console.log(` ${success(tool)}`);
137
+ }
138
+ }
139
+
140
+ if (result.filesCreated.length > 0) {
141
+ console.log(`\n ${dim('Created:')}`);
142
+ for (const f of result.filesCreated) console.log(` + ${f}`);
143
+ }
144
+ if (result.filesModified.length > 0) {
145
+ console.log(`\n ${dim('Modified:')}`);
146
+ for (const f of result.filesModified) console.log(` ~ ${f}`);
147
+ }
148
+
149
+ console.log();
150
+ }
151
+
152
+ async function cmdPqc(args) {
153
+ const { compactHeader, section, labelValue, tableHeader, tableRow, progressBar, statusBadge, divider, success, warning, error, info, dim, bold } = await import('../lib/cli-style.mjs');
154
+ const { analyzeOffline, DATA_PROFILES } = await import('../lib/pqc-engine.mjs');
155
+
156
+ const profile = getOption(args, '--profile', 'general');
157
+ const format = getOption(args, '--format', 'text');
158
+ const verbose = getFlag(args, '--verbose');
159
+
160
+ // Validate profile name
161
+ if (!DATA_PROFILES[profile]) {
162
+ const valid = Object.keys(DATA_PROFILES).join(', ');
163
+ if (format !== 'json') {
164
+ console.error(warning(`Unknown profile "${profile}", using default. Valid: ${valid}`));
165
+ }
166
+ }
167
+
168
+ // Use scanner results if available, otherwise use example libraries
169
+ let libraries = [];
170
+ try {
171
+ const { scanProject, toLibraryInventory } = await import('../lib/scanner.mjs');
172
+ const scanResults = scanProject(process.cwd());
173
+ libraries = toLibraryInventory(scanResults);
174
+ } catch { /* scanner not available, empty libraries */ }
175
+
176
+ const result = analyzeOffline(libraries, profile);
177
+
178
+ if (format === 'json') {
179
+ console.log(JSON.stringify(result, null, 2));
180
+ return;
181
+ }
182
+
183
+ console.log(compactHeader('pqc'));
184
+
185
+ // Data profile
186
+ console.log(section('Data Profile'));
187
+ console.log(labelValue('Profile', result.dataProfile.name));
188
+ console.log(labelValue('Protection needed', `${result.dataProfile.lifespanYears} years`));
189
+ console.log(labelValue('Urgency', result.dataProfile.urgency.toUpperCase()));
190
+
191
+ // Quantum readiness score
192
+ console.log(section('Quantum Readiness'));
193
+ console.log(` ${progressBar(result.quantumReadinessScore, 100)} ${bold(`${result.quantumReadinessScore}/100`)}`);
194
+
195
+ // SNDL assessment
196
+ const sndl = result.sndlAssessment;
197
+ console.log(section('SNDL Risk Assessment'));
198
+ console.log(labelValue('Risk level', statusBadge(sndl.riskLevel)));
199
+ console.log(labelValue('Vulnerable', sndl.vulnerable ? 'YES' : 'No'));
200
+ console.log(labelValue('Risk window', `${sndl.riskWindowYears} years`));
201
+ console.log(` ${dim(sndl.explanation)}`);
202
+
203
+ // KEM recommendations
204
+ if (result.kemRecommendations.length > 0) {
205
+ console.log(section('KEM Recommendations'));
206
+ console.log(tableHeader(['Algorithm', 'FIPS', 'Level', 'Score'], [20, 12, 14, 8]));
207
+ for (const rec of result.kemRecommendations) {
208
+ console.log(tableRow(
209
+ [rec.recommendedAlgorithm, rec.fipsStandard, rec.securityLevel, `${rec.score}%`],
210
+ [20, 12, 14, 8]
211
+ ));
212
+ }
213
+ }
214
+
215
+ // Signature recommendations
216
+ if (result.signatureRecommendations.length > 0) {
217
+ console.log(section('Signature Recommendations'));
218
+ console.log(tableHeader(['Algorithm', 'FIPS', 'Level', 'Score'], [20, 12, 14, 8]));
219
+ for (const rec of result.signatureRecommendations) {
220
+ console.log(tableRow(
221
+ [rec.recommendedAlgorithm, rec.fipsStandard, rec.securityLevel, `${rec.score}%`],
222
+ [20, 12, 14, 8]
223
+ ));
224
+ }
225
+ }
226
+
227
+ // Migration plan
228
+ if (result.migrationPlan.length > 0) {
229
+ console.log(section('Migration Plan'));
230
+ for (const step of result.migrationPlan) {
231
+ const icon = step.priority === 'CRITICAL' ? error(step.action)
232
+ : step.priority === 'HIGH' ? warning(step.action)
233
+ : info(step.action);
234
+ console.log(` ${step.step}. ${icon}`);
235
+ if (verbose) console.log(` ${dim(step.description)}`);
236
+ }
237
+ }
238
+
239
+ // Key findings
240
+ console.log(section('Key Findings'));
241
+ for (const finding of result.keyFindings) {
242
+ console.log(` ${info(finding)}`);
243
+ }
244
+
245
+ // Next steps
246
+ console.log(section('Next Steps'));
247
+ for (const step of result.nextSteps) {
248
+ console.log(` ${success(step)}`);
249
+ }
250
+
251
+ // Compliance (verbose only)
252
+ if (verbose && result.complianceReferences.length > 0) {
253
+ console.log(section('Compliance References'));
254
+ for (const ref of result.complianceReferences) {
255
+ console.log(labelValue(ref.framework, `${ref.authority} — ${ref.detail}`));
256
+ }
257
+ }
258
+
259
+ // Threat timelines (verbose only)
260
+ if (verbose && Object.keys(result.threatTimelines).length > 0) {
261
+ console.log(section('Threat Timelines'));
262
+ console.log(tableHeader(['Algorithm', 'Min', 'Median', 'Max', 'Status'], [14, 6, 8, 6, 10]));
263
+ for (const [, t] of Object.entries(result.threatTimelines)) {
264
+ console.log(tableRow(
265
+ [t.algorithm, `${t.minYears}y`, `${t.medianYears}y`, `${t.maxYears}y`, t.status],
266
+ [14, 6, 8, 6, 10]
267
+ ));
268
+ }
269
+ }
270
+
271
+ console.log();
272
+ }
273
+
274
+ async function cmdScan(args) {
275
+ const { compactHeader, section, tableHeader, tableRow, success, warning, error, info, dim, bold, labelValue } = await import('../lib/cli-style.mjs');
276
+ const { scanProject } = await import('../lib/scanner.mjs');
277
+
278
+ const positional = getPositional(args);
279
+ const scanDir = positional.length > 0 ? resolve(positional[0]) : process.cwd();
280
+ const format = getOption(args, '--format', 'text');
281
+
282
+ const results = scanProject(scanDir);
283
+
284
+ if (format === 'json') {
285
+ console.log(JSON.stringify(results, null, 2));
286
+ return;
287
+ }
288
+
289
+ console.log(compactHeader('scan'));
290
+ console.log(labelValue('Directory', scanDir));
291
+ console.log(labelValue('Files scanned', String(results.filesScanned)));
292
+
293
+ // Crypto libraries
294
+ if (results.libraries.length > 0) {
295
+ console.log(section('Crypto Libraries'));
296
+ console.log(tableHeader(['Library', 'Version', 'Risk', 'Algorithms'], [22, 10, 10, 30]));
297
+ for (const lib of results.libraries) {
298
+ const riskColor = lib.quantumRisk === 'high' ? `\x1b[91m${lib.quantumRisk}\x1b[0m`
299
+ : lib.quantumRisk === 'none' ? `\x1b[92m${lib.quantumRisk}\x1b[0m`
300
+ : lib.quantumRisk;
301
+ console.log(tableRow(
302
+ [lib.name, lib.version, lib.quantumRisk, lib.algorithms.join(', ')],
303
+ [22, 10, 10, 30]
304
+ ));
305
+ }
306
+ } else {
307
+ console.log(`\n ${dim('No crypto libraries detected')}`);
308
+ }
309
+
310
+ // Hardcoded secrets
311
+ if (results.secrets.length > 0) {
312
+ console.log(section('Hardcoded Secrets'));
313
+ for (const s of results.secrets) {
314
+ console.log(` ${error(`[CRIT] ${s.name}`)}`);
315
+ console.log(` ${dim(s.file)}`);
316
+ if (s.envVar) console.log(` ${info(`Use $${s.envVar} instead`)}`);
317
+ }
318
+ }
319
+
320
+ // Weak patterns
321
+ if (results.weakPatterns.length > 0) {
322
+ console.log(section('Weak Crypto Patterns'));
323
+ for (const w of results.weakPatterns) {
324
+ const icon = w.severity === 'critical' ? error(w.issue) : warning(w.issue);
325
+ console.log(` ${icon}`);
326
+ console.log(` ${dim(w.file)}`);
327
+ }
328
+ }
329
+
330
+ // Cert files
331
+ if (results.certFiles.length > 0) {
332
+ console.log(section('Certificate/Key Files'));
333
+ for (const f of results.certFiles) {
334
+ console.log(` ${info(f)}`);
335
+ }
336
+ }
337
+
338
+ // Summary
339
+ console.log(section('Summary'));
340
+ console.log(labelValue('Libraries', String(results.libraries.length)));
341
+ console.log(labelValue('Secrets found', results.secrets.length > 0 ? error(String(results.secrets.length)) : success('0')));
342
+ console.log(labelValue('Weak patterns', results.weakPatterns.length > 0 ? warning(String(results.weakPatterns.length)) : success('0')));
343
+ console.log(labelValue('Cert/key files', String(results.certFiles.length)));
344
+ console.log();
345
+ }
346
+
347
+ async function cmdEncrypt(args) {
348
+ const { promptPassword } = await import('../lib/keychain.mjs');
349
+ const { encryptString, encryptFile } = await import('../lib/local-crypto.mjs');
350
+
351
+ const file = getOption(args, '--file');
352
+ const output = getOption(args, '--output');
353
+ let password = getOption(args, '--password');
354
+ const contextName = getOption(args, '--context');
355
+ const verbose = getFlag(args, '--verbose');
356
+ let algorithm = getOption(args, '--algorithm', 'AES-256-GCM');
357
+
358
+ // Context-aware algorithm selection
359
+ if (contextName) {
360
+ const { resolveContext } = await import('../lib/context-resolver.mjs');
361
+ const resolved = resolveContext(contextName);
362
+ if (resolved.error) {
363
+ console.error(`${resolved.error}\nValid contexts: ${resolved.validContexts.join(', ')}`);
364
+ process.exit(1);
365
+ }
366
+ algorithm = resolved.algorithm;
367
+
368
+ if (verbose) {
369
+ const { dim, success, labelValue } = await import('../lib/cli-style.mjs');
370
+ console.error(labelValue('Context', `${contextName} → ${algorithm}`));
371
+ for (const f of resolved.factors) console.error(` ${dim(f)}`);
372
+ console.error();
373
+ }
374
+ }
375
+
376
+ // Interactive password prompt if not provided
377
+ if (!password) {
378
+ password = await promptPassword('Encryption password: ');
379
+ if (!password) { console.error('Password required.'); process.exit(1); }
380
+ }
381
+
382
+ if (file) {
383
+ const outPath = output || file + '.enc';
384
+ encryptFile(file, outPath, password, algorithm, contextName || 'file');
385
+ console.log(`Encrypted: ${outPath}`);
386
+ } else {
387
+ const positional = getPositional(args);
388
+ const text = positional[0];
389
+ if (!text) { console.error('Provide text to encrypt or use --file.'); process.exit(1); }
390
+ console.log(encryptString(text, password, algorithm, contextName || 'cli'));
391
+ }
392
+ }
393
+
394
+ async function cmdDecrypt(args) {
395
+ const { promptPassword } = await import('../lib/keychain.mjs');
396
+ const { decryptString, decryptFile } = await import('../lib/local-crypto.mjs');
397
+
398
+ const file = getOption(args, '--file');
399
+ const output = getOption(args, '--output');
400
+ let password = getOption(args, '--password');
401
+
402
+ if (!password) {
403
+ password = await promptPassword('Decryption password: ');
404
+ if (!password) { console.error('Password required.'); process.exit(1); }
405
+ }
406
+
407
+ try {
408
+ if (file) {
409
+ const outPath = output || file.replace(/\.enc$/, '.dec');
410
+ decryptFile(file, outPath, password);
411
+ console.log(`Decrypted: ${outPath}`);
412
+ } else {
413
+ const positional = getPositional(args);
414
+ const blob = positional[0];
415
+ if (!blob) { console.error('Provide encrypted text or use --file.'); process.exit(1); }
416
+ console.log(decryptString(blob, password));
417
+ }
418
+ } catch (e) {
419
+ console.error(`Decryption failed: ${e.message}`);
420
+ process.exit(1);
421
+ }
422
+ }
423
+
424
+ async function cmdHashPassword(args) {
425
+ const { promptPassword } = await import('../lib/keychain.mjs');
426
+ const { hashPassword } = await import('../lib/local-crypto.mjs');
427
+
428
+ const algorithm = getOption(args, '--algorithm', 'scrypt');
429
+ const positional = getPositional(args);
430
+ let password = positional[0];
431
+
432
+ if (!password) {
433
+ password = await promptPassword('Password to hash: ');
434
+ if (!password) { console.error('Password required.'); process.exit(1); }
435
+ }
436
+
437
+ console.log(hashPassword(password, algorithm));
438
+ }
439
+
440
+ async function cmdVault(args) {
441
+ const { compactHeader, success, error, warning, info, dim, labelValue, tableHeader, tableRow } = await import('../lib/cli-style.mjs');
442
+ const { promptPassword } = await import('../lib/keychain.mjs');
443
+
444
+ const subcommand = args[0];
445
+ const restArgs = args.slice(1);
446
+
447
+ if (!subcommand || subcommand === 'help') {
448
+ console.log(compactHeader('vault'));
449
+ console.log(' vault init Create new vault');
450
+ console.log(' vault set KEY VALUE Store a secret');
451
+ console.log(' vault get KEY Retrieve a secret');
452
+ console.log(' vault list List stored secrets');
453
+ console.log(' vault delete KEY Remove a secret');
454
+ console.log(' vault run -- CMD ARGS Run command with secrets as env vars');
455
+ console.log(' vault import .env Import .env file');
456
+ console.log(' vault export Export encrypted bundle');
457
+ console.log(' vault reset Delete vault');
458
+ console.log();
459
+ return;
460
+ }
461
+
462
+ const vault = await import('../lib/vault.mjs');
463
+
464
+ if (subcommand === 'init') {
465
+ if (vault.vaultExists()) {
466
+ console.log(warning('Vault already exists.'));
467
+ return;
468
+ }
469
+ const pw = await promptPassword('Set vault password: ');
470
+ const pw2 = await promptPassword('Confirm password: ');
471
+ if (pw !== pw2) { console.error('Passwords do not match.'); process.exit(1); }
472
+ vault.initVault(pw);
473
+ console.log(success('Vault created at ~/.cryptoserve/vault.enc'));
474
+ return;
475
+ }
476
+
477
+ if (subcommand === 'reset') {
478
+ vault.resetVault();
479
+ console.log(success('Vault deleted.'));
480
+ return;
481
+ }
482
+
483
+ // All other commands need the vault password
484
+ const pw = await promptPassword('Vault password: ');
485
+
486
+ try {
487
+ switch (subcommand) {
488
+ case 'set': {
489
+ const key = restArgs[0];
490
+ let value = restArgs[1];
491
+ if (!key) { console.error('Usage: vault set KEY VALUE'); process.exit(1); }
492
+ if (!value) {
493
+ // Read from stdin if no value provided
494
+ value = await promptPassword(`Value for ${key}: `);
495
+ }
496
+ vault.setSecret(pw, key, value);
497
+ console.log(success(`Stored: ${key}`));
498
+ break;
499
+ }
500
+ case 'get': {
501
+ const key = restArgs[0];
502
+ if (!key) { console.error('Usage: vault get KEY'); process.exit(1); }
503
+ const val = vault.getSecret(pw, key);
504
+ if (val === null) { console.error(`Not found: ${key}`); process.exit(1); }
505
+ console.log(val);
506
+ break;
507
+ }
508
+ case 'list': {
509
+ const secrets = vault.listSecrets(pw);
510
+ if (secrets.length === 0) {
511
+ console.log(dim(' Vault is empty'));
512
+ } else {
513
+ console.log(tableHeader(['Key', 'Updated'], [30, 24]));
514
+ for (const s of secrets) {
515
+ const ago = timeSince(new Date(s.updatedAt));
516
+ console.log(tableRow([s.key, ago], [30, 24]));
517
+ }
518
+ }
519
+ break;
520
+ }
521
+ case 'delete': {
522
+ const key = restArgs[0];
523
+ if (!key) { console.error('Usage: vault delete KEY'); process.exit(1); }
524
+ if (vault.deleteSecret(pw, key)) {
525
+ console.log(success(`Deleted: ${key}`));
526
+ } else {
527
+ console.error(`Not found: ${key}`);
528
+ process.exit(1);
529
+ }
530
+ break;
531
+ }
532
+ case 'run': {
533
+ const dashIdx = restArgs.indexOf('--');
534
+ const cmdArgs = dashIdx >= 0 ? restArgs.slice(dashIdx + 1) : restArgs;
535
+ if (cmdArgs.length === 0) {
536
+ console.error('Usage: vault run -- COMMAND [ARGS...]');
537
+ process.exit(1);
538
+ }
539
+ const exitCode = await vault.vaultRun(pw, cmdArgs[0], cmdArgs.slice(1));
540
+ process.exit(exitCode);
541
+ break;
542
+ }
543
+ case 'import': {
544
+ const envFile = restArgs[0] || '.env';
545
+ const count = vault.importEnvFile(pw, envFile);
546
+ console.log(success(`Imported ${count} secrets from ${envFile}`));
547
+ break;
548
+ }
549
+ case 'export': {
550
+ const bundle = vault.exportVault(pw);
551
+ console.log(bundle);
552
+ break;
553
+ }
554
+ default:
555
+ console.error(`Unknown vault command: ${subcommand}`);
556
+ process.exit(1);
557
+ }
558
+ } catch (e) {
559
+ console.error(error(e.message));
560
+ process.exit(1);
561
+ }
562
+ }
563
+
564
+ async function cmdContext(args) {
565
+ const { compactHeader, section, labelValue, tableHeader, tableRow, success, warning, dim, bold, info, statusBadge } = await import('../lib/cli-style.mjs');
566
+ const { resolveContext, listContexts } = await import('../lib/context-resolver.mjs');
567
+
568
+ const subcommand = args[0];
569
+ const format = getOption(args, '--format', 'text');
570
+ const verbose = getFlag(args, '--verbose');
571
+
572
+ if (!subcommand || subcommand === 'list') {
573
+ const contexts = listContexts();
574
+
575
+ if (format === 'json') {
576
+ console.log(JSON.stringify(contexts, null, 2));
577
+ return;
578
+ }
579
+
580
+ console.log(compactHeader('contexts'));
581
+ console.log(tableHeader(['Context', 'Sensitivity', 'Algorithm', 'Compliance'], [20, 12, 20, 20]));
582
+ for (const ctx of contexts) {
583
+ const badge = ctx.custom ? dim('(custom)') : '';
584
+ console.log(tableRow(
585
+ [ctx.name, ctx.sensitivity, ctx.algorithm, ctx.compliance.join(', ') || '—'],
586
+ [20, 12, 20, 20]
587
+ ));
588
+ }
589
+ console.log();
590
+ return;
591
+ }
592
+
593
+ if (subcommand === 'show') {
594
+ const name = args[1];
595
+ if (!name) { console.error('Usage: context show NAME [--verbose]'); process.exit(1); }
596
+
597
+ const resolved = resolveContext(name);
598
+ if (resolved.error) {
599
+ console.error(`${resolved.error}\nValid contexts: ${resolved.validContexts.join(', ')}`);
600
+ process.exit(1);
601
+ }
602
+
603
+ if (format === 'json') {
604
+ console.log(JSON.stringify(resolved, null, 2));
605
+ return;
606
+ }
607
+
608
+ console.log(compactHeader('context'));
609
+
610
+ // Identity
611
+ console.log(section(resolved.context.displayName));
612
+ console.log(labelValue('Context', resolved.context.name));
613
+ if (resolved.context.description) {
614
+ console.log(labelValue('Description', resolved.context.description));
615
+ }
616
+ if (resolved.context.custom) {
617
+ console.log(labelValue('Source', dim('custom (.cryptoserve.json)')));
618
+ }
619
+
620
+ // Resolved algorithm
621
+ console.log(section('Resolved Algorithm'));
622
+ console.log(labelValue('Algorithm', bold(resolved.algorithm)));
623
+ console.log(labelValue('Key size', `${resolved.keyBits} bits`));
624
+ console.log(labelValue('Key rotation', `${resolved.rotationDays} days`));
625
+ if (resolved.quantumRisk) {
626
+ console.log(labelValue('Quantum risk', warning('PQC migration recommended')));
627
+ }
628
+
629
+ // 5-layer summary
630
+ console.log(section('Context Layers'));
631
+ console.log(labelValue('1. Sensitivity', resolved.context.sensitivity.toUpperCase()));
632
+
633
+ const flags = [];
634
+ if (resolved.context.pii) flags.push('PII');
635
+ if (resolved.context.phi) flags.push('PHI');
636
+ if (resolved.context.pci) flags.push('PCI');
637
+ if (flags.length) console.log(labelValue(' Data flags', flags.join(', ')));
638
+
639
+ console.log(labelValue('2. Compliance', resolved.context.compliance.join(', ') || '—'));
640
+ console.log(labelValue('3. Threat model', resolved.context.adversaries.join(', ')));
641
+ console.log(labelValue(' Protection', `${resolved.context.protectionYears} years`));
642
+ console.log(labelValue('4. Access', `${resolved.context.frequency} frequency, ${resolved.context.usage}`));
643
+
644
+ // Examples
645
+ if (resolved.context.examples.length > 0) {
646
+ console.log(labelValue(' Examples', resolved.context.examples.join(', ')));
647
+ }
648
+
649
+ // Verbose: full rationale
650
+ if (verbose) {
651
+ console.log(section('Resolution Rationale'));
652
+ for (const f of resolved.factors) {
653
+ console.log(` ${dim(f)}`);
654
+ }
655
+
656
+ if (resolved.alternatives.length > 0) {
657
+ console.log(section('Alternatives'));
658
+ for (const alt of resolved.alternatives) {
659
+ console.log(` ${info(alt.algorithm)}`);
660
+ console.log(` ${dim(alt.reason)}`);
661
+ }
662
+ }
663
+ }
664
+
665
+ // Usage hint
666
+ console.log(section('Usage'));
667
+ console.log(` ${dim(`cryptoserve encrypt "data" --context ${name} --password P`)}`);
668
+ console.log();
669
+ return;
670
+ }
671
+
672
+ console.error(`Unknown context command: ${subcommand}`);
673
+ console.error('Usage: context list | context show NAME');
674
+ process.exit(1);
675
+ }
676
+
677
+ async function cmdLogin(args) {
678
+ const { login } = await import('../lib/client.mjs');
679
+ const server = getOption(args, '--server', 'https://localhost:8003');
680
+ try {
681
+ await login(server);
682
+ console.log('Login successful.');
683
+ } catch (e) {
684
+ console.error(`Login failed: ${e.message}`);
685
+ process.exit(1);
686
+ }
687
+ }
688
+
689
+ async function cmdStatus() {
690
+ const { compactHeader, section, labelValue, statusBadge, dim } = await import('../lib/cli-style.mjs');
691
+ const { loadToken, maskToken, parseJwtExpiry } = await import('../lib/credentials.mjs');
692
+ const { loadMasterKey, isKeychainAvailable } = await import('../lib/keychain.mjs');
693
+ const { vaultExists } = await import('../lib/vault.mjs');
694
+ const { getStatus } = await import('../lib/client.mjs');
695
+
696
+ console.log(compactHeader('status'));
697
+
698
+ // Key management
699
+ console.log(section('Key Management'));
700
+ const keychainOk = await isKeychainAvailable();
701
+ console.log(labelValue('OS keychain', keychainOk ? statusBadge('active') : statusBadge('unavailable')));
702
+
703
+ let hasKey = false;
704
+ try { hasKey = !!(await loadMasterKey()); } catch { /* no key */ }
705
+ console.log(labelValue('Master key', hasKey ? statusBadge('ready') : statusBadge('not configured')));
706
+ console.log(labelValue('Vault', vaultExists() ? statusBadge('ready') : statusBadge('not initialized')));
707
+
708
+ // Server connection
709
+ console.log(section('Server Connection'));
710
+ const creds = loadToken();
711
+ if (creds) {
712
+ console.log(labelValue('Server', creds.server));
713
+ console.log(labelValue('Token', maskToken(creds.token)));
714
+ const expiry = parseJwtExpiry(creds.token);
715
+ if (expiry) {
716
+ if (expiry.expired) {
717
+ console.log(labelValue('Expiry', statusBadge('expired')));
718
+ } else {
719
+ const mins = Math.floor(expiry.remainingMs / 60000);
720
+ console.log(labelValue('Expiry', `${mins} minutes remaining`));
721
+ }
722
+ if (expiry.subject) console.log(labelValue('Subject', expiry.subject));
723
+ }
724
+
725
+ const status = await getStatus();
726
+ console.log(labelValue('Connection', status.connected ? statusBadge('healthy') : statusBadge('error')));
727
+ if (status.latency) console.log(labelValue('Latency', `${status.latency}ms`));
728
+ } else {
729
+ console.log(labelValue('Status', dim('Not logged in')));
730
+ }
731
+
732
+ console.log();
733
+ }
734
+
735
+ // ---------------------------------------------------------------------------
736
+ // Utility
737
+ // ---------------------------------------------------------------------------
738
+
739
+ function timeSince(date) {
740
+ const seconds = Math.floor((Date.now() - date.getTime()) / 1000);
741
+ if (seconds < 60) return 'just now';
742
+ const minutes = Math.floor(seconds / 60);
743
+ if (minutes < 60) return `${minutes}m ago`;
744
+ const hours = Math.floor(minutes / 60);
745
+ if (hours < 24) return `${hours}h ago`;
746
+ const days = Math.floor(hours / 24);
747
+ return `${days}d ago`;
748
+ }
749
+
750
+ // ---------------------------------------------------------------------------
751
+ // Main router
752
+ // ---------------------------------------------------------------------------
753
+
754
+ const args = process.argv.slice(2);
755
+ const command = args[0];
756
+ const commandArgs = args.slice(1);
757
+
758
+ // Filter out the command from args for sub-parsers
759
+ // Strip flags that belong to the command router
760
+ const filteredArgs = commandArgs.filter(a => a !== command);
761
+
762
+ try {
763
+ switch (command) {
764
+ case 'help':
765
+ case '--help':
766
+ case '-h':
767
+ case undefined:
768
+ await cmdHelp();
769
+ break;
770
+ case 'version':
771
+ case '--version':
772
+ case '-v':
773
+ await cmdVersion();
774
+ break;
775
+ case 'init':
776
+ await cmdInit(commandArgs);
777
+ break;
778
+ case 'pqc':
779
+ await cmdPqc(commandArgs);
780
+ break;
781
+ case 'scan':
782
+ await cmdScan(commandArgs);
783
+ break;
784
+ case 'encrypt':
785
+ await cmdEncrypt(commandArgs);
786
+ break;
787
+ case 'decrypt':
788
+ await cmdDecrypt(commandArgs);
789
+ break;
790
+ case 'hash-password':
791
+ await cmdHashPassword(commandArgs);
792
+ break;
793
+ case 'context':
794
+ await cmdContext(commandArgs);
795
+ break;
796
+ case 'vault':
797
+ await cmdVault(commandArgs);
798
+ break;
799
+ case 'login':
800
+ await cmdLogin(commandArgs);
801
+ break;
802
+ case 'status':
803
+ await cmdStatus();
804
+ break;
805
+ default:
806
+ console.error(`Unknown command: ${command}\nRun "cryptoserve help" for usage.`);
807
+ process.exit(1);
808
+ }
809
+ } catch (e) {
810
+ console.error(`Error: ${e.message}`);
811
+ process.exit(1);
812
+ }