@tearleads/cli 0.0.1

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/dist/index.js ADDED
@@ -0,0 +1,1320 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command as Command8 } from "commander";
5
+
6
+ // src/commands/backup.ts
7
+ import fs5 from "fs/promises";
8
+ import path3 from "path";
9
+ import { Command } from "commander";
10
+
11
+ // src/backup/constants.ts
12
+ var MAGIC_BYTES = new Uint8Array([
13
+ 82,
14
+ 65,
15
+ 80,
16
+ 73,
17
+ 68,
18
+ 66,
19
+ 65,
20
+ 75
21
+ ]);
22
+ var FORMAT_VERSION = 1;
23
+ var HEADER_SIZE = 32;
24
+ var MAGIC_SIZE = 8;
25
+ var SALT_SIZE = 16;
26
+ var IV_SIZE = 12;
27
+ var CHUNK_HEADER_SIZE = 20;
28
+ var PBKDF2_ITERATIONS = 6e5;
29
+ var AES_KEY_BITS = 256;
30
+ var MAX_BLOB_CHUNK_SIZE = 10 * 1024 * 1024;
31
+
32
+ // src/backup/compression.ts
33
+ async function compressNode(data) {
34
+ const { gzipSync } = await import("zlib");
35
+ return new Uint8Array(gzipSync(data));
36
+ }
37
+ async function decompressNode(data) {
38
+ const { gunzipSync } = await import("zlib");
39
+ return new Uint8Array(gunzipSync(data));
40
+ }
41
+ async function compress(data) {
42
+ return compressNode(data);
43
+ }
44
+ async function decompress(data) {
45
+ return decompressNode(data);
46
+ }
47
+
48
+ // src/backup/crypto.ts
49
+ function generateSalt() {
50
+ return crypto.getRandomValues(new Uint8Array(SALT_SIZE));
51
+ }
52
+ function generateIv() {
53
+ return crypto.getRandomValues(new Uint8Array(IV_SIZE));
54
+ }
55
+ async function deriveKey(password, salt, iterations = PBKDF2_ITERATIONS) {
56
+ const encoder = new TextEncoder();
57
+ const passwordBytes = encoder.encode(password);
58
+ const keyMaterial = await crypto.subtle.importKey(
59
+ "raw",
60
+ passwordBytes,
61
+ "PBKDF2",
62
+ false,
63
+ ["deriveBits", "deriveKey"]
64
+ );
65
+ const saltBuffer = new Uint8Array(salt).buffer;
66
+ return crypto.subtle.deriveKey(
67
+ {
68
+ name: "PBKDF2",
69
+ salt: saltBuffer,
70
+ iterations,
71
+ hash: "SHA-256"
72
+ },
73
+ keyMaterial,
74
+ { name: "AES-GCM", length: AES_KEY_BITS },
75
+ false,
76
+ ["encrypt", "decrypt"]
77
+ );
78
+ }
79
+ async function encrypt(data, key) {
80
+ const iv = generateIv();
81
+ const dataBuffer = new Uint8Array(data).buffer;
82
+ const ivBuffer = new Uint8Array(iv).buffer;
83
+ const ciphertext = await crypto.subtle.encrypt(
84
+ { name: "AES-GCM", iv: ivBuffer },
85
+ key,
86
+ dataBuffer
87
+ );
88
+ return {
89
+ iv,
90
+ ciphertext: new Uint8Array(ciphertext)
91
+ };
92
+ }
93
+ async function decrypt(ciphertext, key, iv) {
94
+ const ciphertextBuffer = new Uint8Array(ciphertext).buffer;
95
+ const ivBuffer = new Uint8Array(iv).buffer;
96
+ const plaintext = await crypto.subtle.decrypt(
97
+ { name: "AES-GCM", iv: ivBuffer },
98
+ key,
99
+ ciphertextBuffer
100
+ );
101
+ return new Uint8Array(plaintext);
102
+ }
103
+
104
+ // src/backup/types.ts
105
+ var ChunkType = {
106
+ MANIFEST: 0,
107
+ DATABASE: 1,
108
+ BLOB: 2
109
+ };
110
+
111
+ // src/backup/decoder.ts
112
+ var BackupDecodeError = class extends Error {
113
+ constructor(message) {
114
+ super(message);
115
+ this.name = "BackupDecodeError";
116
+ }
117
+ };
118
+ var InvalidPasswordError = class extends Error {
119
+ constructor() {
120
+ super("Invalid password or corrupted backup");
121
+ this.name = "InvalidPasswordError";
122
+ }
123
+ };
124
+ function readUint32LE(buffer, offset) {
125
+ const view = new DataView(
126
+ buffer.buffer,
127
+ buffer.byteOffset,
128
+ buffer.byteLength
129
+ );
130
+ return view.getUint32(offset, true);
131
+ }
132
+ function readUint16LE(buffer, offset) {
133
+ const view = new DataView(
134
+ buffer.buffer,
135
+ buffer.byteOffset,
136
+ buffer.byteLength
137
+ );
138
+ return view.getUint16(offset, true);
139
+ }
140
+ function parseHeader(data) {
141
+ if (data.length < HEADER_SIZE) {
142
+ throw new BackupDecodeError("File too small to be a valid backup");
143
+ }
144
+ const magic = data.slice(0, MAGIC_SIZE);
145
+ for (let i = 0; i < MAGIC_SIZE; i++) {
146
+ if (magic[i] !== MAGIC_BYTES[i]) {
147
+ throw new BackupDecodeError("Invalid backup file: wrong magic bytes");
148
+ }
149
+ }
150
+ const version = readUint16LE(data, MAGIC_SIZE);
151
+ if (version > FORMAT_VERSION) {
152
+ throw new BackupDecodeError(
153
+ `Unsupported backup version: ${version} (max supported: ${FORMAT_VERSION})`
154
+ );
155
+ }
156
+ const flags = readUint16LE(data, MAGIC_SIZE + 2);
157
+ const salt = data.slice(MAGIC_SIZE + 4, MAGIC_SIZE + 4 + SALT_SIZE);
158
+ return { magic, version, flags, salt };
159
+ }
160
+ function parseChunkHeader(data, offset) {
161
+ if (offset + CHUNK_HEADER_SIZE > data.length) {
162
+ throw new BackupDecodeError(
163
+ "Unexpected end of file while reading chunk header"
164
+ );
165
+ }
166
+ const payloadLength = readUint32LE(data, offset);
167
+ const chunkTypeValue = data[offset + 4] ?? -1;
168
+ if (!isChunkTypeValue(chunkTypeValue)) {
169
+ throw new BackupDecodeError(`Unknown chunk type: ${chunkTypeValue}`);
170
+ }
171
+ const reserved = data.slice(offset + 5, offset + 8);
172
+ const iv = data.slice(offset + 8, offset + CHUNK_HEADER_SIZE);
173
+ return { payloadLength, chunkType: chunkTypeValue, reserved, iv };
174
+ }
175
+ function isChunkTypeValue(value) {
176
+ return value === ChunkType.MANIFEST || value === ChunkType.DATABASE || value === ChunkType.BLOB;
177
+ }
178
+ async function decryptChunk(data, offset, header, key) {
179
+ const payloadStart = offset + CHUNK_HEADER_SIZE;
180
+ const payloadEnd = payloadStart + header.payloadLength;
181
+ if (payloadEnd > data.length) {
182
+ throw new BackupDecodeError(
183
+ "Unexpected end of file while reading chunk payload"
184
+ );
185
+ }
186
+ const ciphertext = data.slice(payloadStart, payloadEnd);
187
+ try {
188
+ const compressed = await decrypt(ciphertext, key, header.iv);
189
+ return decompress(compressed);
190
+ } catch {
191
+ throw new InvalidPasswordError();
192
+ }
193
+ }
194
+ function parseJsonChunk(data) {
195
+ const json = new TextDecoder().decode(data);
196
+ return JSON.parse(json);
197
+ }
198
+ function parseBlobChunk(data) {
199
+ const separatorIndex = data.indexOf(0);
200
+ if (separatorIndex === -1) {
201
+ throw new BackupDecodeError("Invalid blob chunk: missing separator");
202
+ }
203
+ const headerBytes = data.slice(0, separatorIndex);
204
+ const blobData = data.slice(separatorIndex + 1);
205
+ const header = JSON.parse(new TextDecoder().decode(headerBytes));
206
+ return { header, data: blobData };
207
+ }
208
+ async function decode(options) {
209
+ const { data, password, onProgress } = options;
210
+ const header = parseHeader(data);
211
+ const decodeWithKey = async (key2) => {
212
+ let offset = HEADER_SIZE;
213
+ let manifest = null;
214
+ let database = null;
215
+ const blobs = [];
216
+ const totalChunks = Math.max(
217
+ 1,
218
+ Math.floor((data.length - HEADER_SIZE) / CHUNK_HEADER_SIZE)
219
+ );
220
+ let currentChunk = 0;
221
+ const reportProgress = (phase, item) => {
222
+ if (!onProgress) return;
223
+ const event = {
224
+ phase,
225
+ current: currentChunk,
226
+ total: totalChunks,
227
+ currentItem: item
228
+ };
229
+ onProgress(event);
230
+ };
231
+ while (offset < data.length) {
232
+ const chunkHeader = parseChunkHeader(data, offset);
233
+ const chunkData = await decryptChunk(data, offset, chunkHeader, key2);
234
+ switch (chunkHeader.chunkType) {
235
+ case ChunkType.MANIFEST:
236
+ reportProgress("preparing", "manifest");
237
+ manifest = parseJsonChunk(chunkData);
238
+ break;
239
+ case ChunkType.DATABASE:
240
+ reportProgress("database", "database");
241
+ database = parseJsonChunk(chunkData);
242
+ break;
243
+ case ChunkType.BLOB:
244
+ reportProgress("blobs", "blob");
245
+ blobs.push(parseBlobChunk(chunkData));
246
+ break;
247
+ default:
248
+ throw new BackupDecodeError(
249
+ `Unknown chunk type: ${chunkHeader.chunkType}`
250
+ );
251
+ }
252
+ offset += CHUNK_HEADER_SIZE + chunkHeader.payloadLength;
253
+ currentChunk++;
254
+ }
255
+ if (!manifest || !database) {
256
+ throw new BackupDecodeError("Backup missing required chunks");
257
+ }
258
+ reportProgress("finalizing", "complete");
259
+ return { manifest, database, blobs };
260
+ };
261
+ const key = await deriveKey(password, header.salt, PBKDF2_ITERATIONS);
262
+ return decodeWithKey(key);
263
+ }
264
+
265
+ // src/backup/encoder.ts
266
+ function writeUint32LE(buffer, value, offset) {
267
+ const view = new DataView(
268
+ buffer.buffer,
269
+ buffer.byteOffset,
270
+ buffer.byteLength
271
+ );
272
+ view.setUint32(offset, value, true);
273
+ }
274
+ function writeUint16LE(buffer, value, offset) {
275
+ const view = new DataView(
276
+ buffer.buffer,
277
+ buffer.byteOffset,
278
+ buffer.byteLength
279
+ );
280
+ view.setUint16(offset, value, true);
281
+ }
282
+ function createHeader(salt, flags = 0) {
283
+ const header = new Uint8Array(HEADER_SIZE);
284
+ header.set(MAGIC_BYTES, 0);
285
+ writeUint16LE(header, FORMAT_VERSION, MAGIC_SIZE);
286
+ writeUint16LE(header, flags, MAGIC_SIZE + 2);
287
+ header.set(salt, MAGIC_SIZE + 4);
288
+ return header;
289
+ }
290
+ async function createChunk(data, chunkType, key) {
291
+ const compressed = await compress(data);
292
+ const { iv, ciphertext } = await encrypt(compressed, key);
293
+ const chunk = new Uint8Array(CHUNK_HEADER_SIZE + ciphertext.length);
294
+ writeUint32LE(chunk, ciphertext.length, 0);
295
+ chunk[4] = chunkType;
296
+ chunk.set(iv, 8);
297
+ chunk.set(ciphertext, CHUNK_HEADER_SIZE);
298
+ return chunk;
299
+ }
300
+ async function encodeJsonChunk(data, chunkType, key) {
301
+ const json = JSON.stringify(data);
302
+ const bytes = new TextEncoder().encode(json);
303
+ return createChunk(bytes, chunkType, key);
304
+ }
305
+ async function* encodeBlobChunks(blob, data, key) {
306
+ const totalParts = Math.ceil(data.length / MAX_BLOB_CHUNK_SIZE) || 1;
307
+ for (let partIndex = 0; partIndex < totalParts; partIndex++) {
308
+ const start = partIndex * MAX_BLOB_CHUNK_SIZE;
309
+ const end = Math.min(start + MAX_BLOB_CHUNK_SIZE, data.length);
310
+ const partData = data.slice(start, end);
311
+ const header = {
312
+ path: blob.path,
313
+ mimeType: blob.mimeType,
314
+ size: blob.size,
315
+ ...totalParts > 1 && { partIndex, totalParts }
316
+ };
317
+ const headerBytes = new TextEncoder().encode(JSON.stringify(header));
318
+ const chunkData = new Uint8Array(headerBytes.length + 1 + partData.length);
319
+ chunkData.set(headerBytes, 0);
320
+ chunkData[headerBytes.length] = 0;
321
+ chunkData.set(partData, headerBytes.length + 1);
322
+ yield createChunk(chunkData, ChunkType.BLOB, key);
323
+ }
324
+ }
325
+ async function encode(options) {
326
+ const { password, manifest, database, blobs, readBlob, onProgress } = options;
327
+ const salt = generateSalt();
328
+ const key = await deriveKey(password, salt);
329
+ const header = createHeader(salt);
330
+ const chunks = [header];
331
+ const totalSteps = 2 + blobs.length;
332
+ let currentStep = 0;
333
+ const reportProgress = (phase, item) => {
334
+ onProgress?.({
335
+ phase,
336
+ current: currentStep,
337
+ total: totalSteps,
338
+ currentItem: item
339
+ });
340
+ };
341
+ reportProgress("preparing", "manifest");
342
+ const manifestChunk = await encodeJsonChunk(
343
+ manifest,
344
+ ChunkType.MANIFEST,
345
+ key
346
+ );
347
+ chunks.push(manifestChunk);
348
+ currentStep++;
349
+ reportProgress("database", "database");
350
+ const databaseChunk = await encodeJsonChunk(
351
+ database,
352
+ ChunkType.DATABASE,
353
+ key
354
+ );
355
+ chunks.push(databaseChunk);
356
+ currentStep++;
357
+ for (const blob of blobs) {
358
+ reportProgress("blobs", blob.path);
359
+ const blobData = await readBlob(blob.path);
360
+ for await (const chunk of encodeBlobChunks(blob, blobData, key)) {
361
+ chunks.push(chunk);
362
+ }
363
+ currentStep++;
364
+ }
365
+ const totalSize = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
366
+ const output = new Uint8Array(totalSize);
367
+ let offset = 0;
368
+ for (const chunk of chunks) {
369
+ output.set(chunk, offset);
370
+ offset += chunk.length;
371
+ }
372
+ reportProgress("finalizing", "complete");
373
+ return output;
374
+ }
375
+
376
+ // src/crypto/key-manager.ts
377
+ import fs2 from "fs/promises";
378
+
379
+ // ../shared/dist/crypto/asymmetric.js
380
+ var HKDF_INFO = new TextEncoder().encode("rapid-vfs-hybrid-v1");
381
+
382
+ // ../shared/dist/crypto/web-crypto.js
383
+ var ALGORITHM = "AES-GCM";
384
+ var KEY_LENGTH = 256;
385
+ var SALT_LENGTH = 32;
386
+ var PBKDF2_ITERATIONS2 = 6e5;
387
+ function generateSalt2() {
388
+ return crypto.getRandomValues(new Uint8Array(SALT_LENGTH));
389
+ }
390
+ async function deriveKeyFromPassword(password, salt) {
391
+ const encoder = new TextEncoder();
392
+ const passwordBuffer = encoder.encode(password);
393
+ const keyMaterial = await crypto.subtle.importKey("raw", passwordBuffer, "PBKDF2", false, ["deriveBits", "deriveKey"]);
394
+ assertPlainArrayBuffer(salt);
395
+ return crypto.subtle.deriveKey({
396
+ name: "PBKDF2",
397
+ salt,
398
+ iterations: PBKDF2_ITERATIONS2,
399
+ hash: "SHA-256"
400
+ }, keyMaterial, { name: ALGORITHM, length: KEY_LENGTH }, true, ["encrypt", "decrypt"]);
401
+ }
402
+ async function exportKey(key) {
403
+ const exported = await crypto.subtle.exportKey("raw", key);
404
+ return new Uint8Array(exported);
405
+ }
406
+ async function importKey(keyBytes) {
407
+ assertPlainArrayBuffer(keyBytes);
408
+ return crypto.subtle.importKey("raw", keyBytes, { name: ALGORITHM, length: KEY_LENGTH }, true, ["encrypt", "decrypt"]);
409
+ }
410
+ function secureZero(buffer) {
411
+ crypto.getRandomValues(buffer);
412
+ buffer.fill(0);
413
+ }
414
+ async function generateExtractableWrappingKey() {
415
+ return crypto.subtle.generateKey({ name: "AES-KW", length: 256 }, true, [
416
+ "wrapKey",
417
+ "unwrapKey"
418
+ ]);
419
+ }
420
+ async function exportWrappingKey(key) {
421
+ const exported = await crypto.subtle.exportKey("raw", key);
422
+ return new Uint8Array(exported);
423
+ }
424
+ async function importWrappingKey(keyBytes) {
425
+ assertPlainArrayBuffer(keyBytes);
426
+ return crypto.subtle.importKey("raw", keyBytes, { name: "AES-KW", length: 256 }, true, ["wrapKey", "unwrapKey"]);
427
+ }
428
+ async function wrapKey(keyToWrap, wrappingKey) {
429
+ assertPlainArrayBuffer(keyToWrap);
430
+ const cryptoKey = await crypto.subtle.importKey("raw", keyToWrap, { name: ALGORITHM, length: KEY_LENGTH }, true, ["encrypt", "decrypt"]);
431
+ const wrapped = await crypto.subtle.wrapKey("raw", cryptoKey, wrappingKey, {
432
+ name: "AES-KW"
433
+ });
434
+ return new Uint8Array(wrapped);
435
+ }
436
+ async function unwrapKey(wrappedKey, wrappingKey) {
437
+ assertPlainArrayBuffer(wrappedKey);
438
+ const unwrappedCryptoKey = await crypto.subtle.unwrapKey("raw", wrappedKey, wrappingKey, { name: "AES-KW" }, { name: ALGORITHM, length: KEY_LENGTH }, true, ["encrypt", "decrypt"]);
439
+ const exported = await crypto.subtle.exportKey("raw", unwrappedCryptoKey);
440
+ return new Uint8Array(exported);
441
+ }
442
+
443
+ // ../shared/dist/index.js
444
+ function assertPlainArrayBuffer(arr) {
445
+ if (typeof SharedArrayBuffer !== "undefined" && arr.buffer instanceof SharedArrayBuffer) {
446
+ throw new Error("Unexpected SharedArrayBuffer backing Uint8Array. This should never occur in normal operation.");
447
+ }
448
+ }
449
+
450
+ // src/config/index.ts
451
+ import fs from "fs/promises";
452
+ import os from "os";
453
+ import path from "path";
454
+ var configRoot = null;
455
+ function getConfigPaths() {
456
+ const root = configRoot ?? path.join(os.homedir(), ".tearleads");
457
+ return {
458
+ root,
459
+ database: path.join(root, "tearleads.db"),
460
+ keyData: path.join(root, "keydata.json"),
461
+ session: path.join(root, ".session")
462
+ };
463
+ }
464
+ async function ensureConfigDir() {
465
+ const paths = getConfigPaths();
466
+ await fs.mkdir(paths.root, { recursive: true, mode: 448 });
467
+ }
468
+ async function hasSession() {
469
+ const paths = getConfigPaths();
470
+ try {
471
+ await fs.access(paths.session);
472
+ return true;
473
+ } catch {
474
+ return false;
475
+ }
476
+ }
477
+ async function clearSession() {
478
+ const paths = getConfigPaths();
479
+ try {
480
+ await fs.unlink(paths.session);
481
+ } catch {
482
+ }
483
+ }
484
+
485
+ // src/crypto/key-manager.ts
486
+ var currentKey = null;
487
+ async function hasExistingKey() {
488
+ const paths = getConfigPaths();
489
+ try {
490
+ await fs2.access(paths.keyData);
491
+ return true;
492
+ } catch {
493
+ return false;
494
+ }
495
+ }
496
+ async function readKeyData() {
497
+ const paths = getConfigPaths();
498
+ try {
499
+ const content = await fs2.readFile(paths.keyData, "utf-8");
500
+ return JSON.parse(content);
501
+ } catch {
502
+ return null;
503
+ }
504
+ }
505
+ async function writeKeyData(data) {
506
+ await ensureConfigDir();
507
+ const paths = getConfigPaths();
508
+ await fs2.writeFile(paths.keyData, JSON.stringify(data), {
509
+ mode: 384
510
+ });
511
+ }
512
+ async function readSessionData() {
513
+ const paths = getConfigPaths();
514
+ try {
515
+ const content = await fs2.readFile(paths.session, "utf-8");
516
+ return JSON.parse(content);
517
+ } catch {
518
+ return null;
519
+ }
520
+ }
521
+ async function writeSessionData(data) {
522
+ await ensureConfigDir();
523
+ const paths = getConfigPaths();
524
+ await fs2.writeFile(paths.session, JSON.stringify(data), {
525
+ mode: 384
526
+ });
527
+ }
528
+ async function createKeyCheckValue(keyBytes) {
529
+ const checkData = new TextEncoder().encode("TEARLEADS_KEY_CHECK");
530
+ const key = await importKey(keyBytes);
531
+ const iv = new Uint8Array(12);
532
+ const encrypted = await crypto.subtle.encrypt(
533
+ { name: "AES-GCM", iv },
534
+ key,
535
+ checkData
536
+ );
537
+ const bytes = new Uint8Array(encrypted).slice(0, 16);
538
+ return btoa(String.fromCharCode(...bytes));
539
+ }
540
+ async function setupNewKey(password) {
541
+ const salt = generateSalt2();
542
+ const key = await deriveKeyFromPassword(password, salt);
543
+ const keyBytes = await exportKey(key);
544
+ const kcv = await createKeyCheckValue(keyBytes);
545
+ await writeKeyData({
546
+ salt: Array.from(salt),
547
+ keyCheckValue: kcv
548
+ });
549
+ currentKey = new Uint8Array(keyBytes);
550
+ return keyBytes;
551
+ }
552
+ async function unlockWithPassword(password) {
553
+ const keyData = await readKeyData();
554
+ if (!keyData) {
555
+ throw new Error("No existing key found. Use setupNewKey instead.");
556
+ }
557
+ const salt = new Uint8Array(keyData.salt);
558
+ const key = await deriveKeyFromPassword(password, salt);
559
+ const keyBytes = await exportKey(key);
560
+ const computedKcv = await createKeyCheckValue(keyBytes);
561
+ if (keyData.keyCheckValue !== computedKcv) {
562
+ secureZero(keyBytes);
563
+ return null;
564
+ }
565
+ currentKey = new Uint8Array(keyBytes);
566
+ return keyBytes;
567
+ }
568
+ async function changePassword(oldPassword, newPassword) {
569
+ const oldKeyResult = await unlockWithPassword(oldPassword);
570
+ if (!oldKeyResult) return null;
571
+ const oldKey = new Uint8Array(oldKeyResult);
572
+ const newSalt = generateSalt2();
573
+ const newCryptoKey = await deriveKeyFromPassword(newPassword, newSalt);
574
+ const newKey = await exportKey(newCryptoKey);
575
+ const newKcv = await createKeyCheckValue(newKey);
576
+ await writeKeyData({
577
+ salt: Array.from(newSalt),
578
+ keyCheckValue: newKcv
579
+ });
580
+ currentKey = new Uint8Array(newKey);
581
+ return { oldKey, newKey };
582
+ }
583
+ function clearKey() {
584
+ if (currentKey) {
585
+ secureZero(currentKey);
586
+ currentKey = null;
587
+ }
588
+ }
589
+ async function persistSession() {
590
+ if (!currentKey) return false;
591
+ try {
592
+ const wrappingKey = await generateExtractableWrappingKey();
593
+ const wrappedKey = await wrapKey(currentKey, wrappingKey);
594
+ const wrappingKeyBytes = await exportWrappingKey(wrappingKey);
595
+ await writeSessionData({
596
+ wrappedKey: Array.from(wrappedKey),
597
+ wrappingKey: Array.from(wrappingKeyBytes)
598
+ });
599
+ return true;
600
+ } catch (err) {
601
+ console.error("Failed to persist session:", err);
602
+ return false;
603
+ }
604
+ }
605
+ async function hasPersistedSession() {
606
+ return hasSession();
607
+ }
608
+ async function restoreSession() {
609
+ try {
610
+ const sessionData = await readSessionData();
611
+ if (!sessionData) return null;
612
+ const wrappingKeyBytes = new Uint8Array(sessionData.wrappingKey);
613
+ const wrappedKey = new Uint8Array(sessionData.wrappedKey);
614
+ const wrappingKey = await importWrappingKey(wrappingKeyBytes);
615
+ const keyBytes = await unwrapKey(wrappedKey, wrappingKey);
616
+ currentKey = new Uint8Array(keyBytes);
617
+ return keyBytes;
618
+ } catch (err) {
619
+ console.error("Failed to restore session:", err);
620
+ await clearPersistedSession();
621
+ return null;
622
+ }
623
+ }
624
+ async function clearPersistedSession() {
625
+ await clearSession();
626
+ }
627
+
628
+ // src/db/index.ts
629
+ import fs4 from "fs/promises";
630
+
631
+ // src/db/adapter.ts
632
+ import fs3 from "fs/promises";
633
+ import path2 from "path";
634
+ import Database from "better-sqlite3-multiple-ciphers";
635
+ var SCHEMA = `
636
+ CREATE TABLE IF NOT EXISTS contacts (
637
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
638
+ name TEXT NOT NULL,
639
+ email TEXT,
640
+ phone TEXT,
641
+ notes TEXT,
642
+ created_at TEXT DEFAULT (datetime('now')),
643
+ updated_at TEXT DEFAULT (datetime('now'))
644
+ );
645
+
646
+ CREATE TABLE IF NOT EXISTS events (
647
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
648
+ contact_id INTEGER REFERENCES contacts(id),
649
+ type TEXT NOT NULL,
650
+ description TEXT,
651
+ event_date TEXT,
652
+ created_at TEXT DEFAULT (datetime('now'))
653
+ );
654
+
655
+ CREATE TABLE IF NOT EXISTS settings (
656
+ key TEXT PRIMARY KEY,
657
+ value TEXT NOT NULL
658
+ );
659
+ `;
660
+ function isRecord2(value) {
661
+ return typeof value === "object" && value !== null;
662
+ }
663
+ function getStringField(value, key) {
664
+ const field = value[key];
665
+ return typeof field === "string" ? field : null;
666
+ }
667
+ function bytesToHex(bytes) {
668
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
669
+ }
670
+ var NativeSqliteAdapter = class {
671
+ db = null;
672
+ dbPath;
673
+ constructor() {
674
+ const configPaths = getConfigPaths();
675
+ this.dbPath = configPaths.database;
676
+ }
677
+ /**
678
+ * Initialize a new encrypted database with the given key.
679
+ * Creates the database file and schema.
680
+ */
681
+ async initialize(key) {
682
+ await fs3.mkdir(path2.dirname(this.dbPath), { recursive: true, mode: 448 });
683
+ this.db = new Database(this.dbPath);
684
+ this.db.pragma(`key = "x'${bytesToHex(key)}'"`);
685
+ this.db.pragma("cipher_compatibility = 4");
686
+ this.db.exec(SCHEMA);
687
+ }
688
+ /**
689
+ * Open an existing encrypted database with the given key.
690
+ */
691
+ async open(key) {
692
+ try {
693
+ await fs3.access(this.dbPath);
694
+ } catch {
695
+ throw new Error("Database file not found");
696
+ }
697
+ this.db = new Database(this.dbPath);
698
+ this.db.pragma(`key = "x'${bytesToHex(key)}'"`);
699
+ this.db.pragma("cipher_compatibility = 4");
700
+ try {
701
+ this.db.prepare("SELECT count(*) FROM sqlite_master").get();
702
+ } catch {
703
+ this.db.close();
704
+ this.db = null;
705
+ throw new Error("Invalid encryption key");
706
+ }
707
+ }
708
+ /**
709
+ * Check if the database is currently open.
710
+ */
711
+ isOpen() {
712
+ return this.db !== null;
713
+ }
714
+ /**
715
+ * Close the database connection.
716
+ */
717
+ close() {
718
+ if (this.db) {
719
+ this.db.close();
720
+ this.db = null;
721
+ }
722
+ }
723
+ /**
724
+ * Re-key the database with a new encryption key.
725
+ */
726
+ async rekeyDatabase(newKey) {
727
+ if (!this.db) {
728
+ throw new Error("Database not open");
729
+ }
730
+ this.db.pragma(`rekey = "x'${bytesToHex(newKey)}'"`);
731
+ }
732
+ /**
733
+ * Export the database to a JSON structure for backup.
734
+ */
735
+ exportToJson() {
736
+ if (!this.db) {
737
+ throw new Error("Database not open");
738
+ }
739
+ const result = {};
740
+ const tables = this.db.prepare(
741
+ "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"
742
+ ).all();
743
+ for (const { name } of tables) {
744
+ const rows = this.db.prepare(`SELECT * FROM ${name}`).all();
745
+ result[name] = rows;
746
+ }
747
+ return result;
748
+ }
749
+ /**
750
+ * Export the database to a backup-friendly structure with schema and data.
751
+ */
752
+ exportToBackupDatabase() {
753
+ if (!this.db) {
754
+ throw new Error("Database not open");
755
+ }
756
+ const tables = this.db.prepare(
757
+ "SELECT name, sql FROM sqlite_master WHERE type='table' AND sql IS NOT NULL ORDER BY name"
758
+ ).all();
759
+ const tableSchemas = [];
760
+ for (const row of tables) {
761
+ if (!isRecord2(row)) continue;
762
+ const name = getStringField(row, "name");
763
+ const sql = getStringField(row, "sql");
764
+ if (!name || !sql) continue;
765
+ if (name.startsWith("sqlite_") || name === "__drizzle_migrations") {
766
+ continue;
767
+ }
768
+ tableSchemas.push({ name, sql });
769
+ }
770
+ const indexes = this.db.prepare(
771
+ "SELECT name, tbl_name, sql FROM sqlite_master WHERE type='index' AND sql IS NOT NULL ORDER BY name"
772
+ ).all();
773
+ const indexSchemas = [];
774
+ for (const row of indexes) {
775
+ if (!isRecord2(row)) continue;
776
+ const name = getStringField(row, "name");
777
+ const tableName = getStringField(row, "tbl_name");
778
+ const sql = getStringField(row, "sql");
779
+ if (!name || !tableName || !sql) continue;
780
+ if (name.startsWith("sqlite_")) continue;
781
+ indexSchemas.push({ name, tableName, sql });
782
+ }
783
+ const data = {};
784
+ for (const table of tableSchemas) {
785
+ const rows = this.db.prepare(`SELECT * FROM "${table.name}"`).all();
786
+ data[table.name] = rows;
787
+ }
788
+ return { tables: tableSchemas, indexes: indexSchemas, data };
789
+ }
790
+ /**
791
+ * Import data from a JSON structure (restore from backup).
792
+ */
793
+ importFromJson(data) {
794
+ if (!this.db) {
795
+ throw new Error("Database not open");
796
+ }
797
+ const db = this.db;
798
+ const transaction = db.transaction(() => {
799
+ for (const [tableName, rows] of Object.entries(data)) {
800
+ if (!Array.isArray(rows) || rows.length === 0) continue;
801
+ db.prepare(`DELETE FROM ${tableName}`).run();
802
+ const firstRow = rows[0];
803
+ const columns = Object.keys(firstRow);
804
+ const placeholders = columns.map(() => "?").join(", ");
805
+ const insertStmt = db.prepare(
806
+ `INSERT INTO ${tableName} (${columns.join(", ")}) VALUES (${placeholders})`
807
+ );
808
+ for (const row of rows) {
809
+ const rowData = row;
810
+ const values = columns.map((col) => rowData[col]);
811
+ insertStmt.run(...values);
812
+ }
813
+ }
814
+ });
815
+ transaction();
816
+ }
817
+ /**
818
+ * Import a backup database structure, recreating schema and data.
819
+ */
820
+ importFromBackupDatabase(database) {
821
+ if (!this.db) {
822
+ throw new Error("Database not open");
823
+ }
824
+ const db = this.db;
825
+ const transaction = db.transaction(() => {
826
+ const existingTables = db.prepare(
827
+ "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"
828
+ ).all();
829
+ for (const row of existingTables) {
830
+ if (!isRecord2(row)) continue;
831
+ const name = getStringField(row, "name");
832
+ if (!name) continue;
833
+ db.exec(`DROP TABLE IF EXISTS "${name}"`);
834
+ }
835
+ for (const table of database.tables) {
836
+ db.exec(table.sql);
837
+ }
838
+ for (const [tableName, rows] of Object.entries(database.data)) {
839
+ if (!Array.isArray(rows) || rows.length === 0) continue;
840
+ const firstRow = rows[0];
841
+ if (!isRecord2(firstRow)) continue;
842
+ const columns = Object.keys(firstRow);
843
+ if (columns.length === 0) continue;
844
+ const placeholders = columns.map(() => "?").join(", ");
845
+ const insertStmt = db.prepare(
846
+ `INSERT INTO "${tableName}" (${columns.join(", ")}) VALUES (${placeholders})`
847
+ );
848
+ for (const row of rows) {
849
+ if (!isRecord2(row)) continue;
850
+ const values = columns.map((col) => row[col]);
851
+ insertStmt.run(...values);
852
+ }
853
+ }
854
+ for (const index of database.indexes) {
855
+ if (index.sql.trim().length > 0) {
856
+ db.exec(index.sql);
857
+ }
858
+ }
859
+ });
860
+ transaction();
861
+ }
862
+ /**
863
+ * Execute a raw SQL query.
864
+ */
865
+ exec(sql) {
866
+ if (!this.db) {
867
+ throw new Error("Database not open");
868
+ }
869
+ this.db.exec(sql);
870
+ }
871
+ /**
872
+ * Get the database file path.
873
+ */
874
+ getPath() {
875
+ return this.dbPath;
876
+ }
877
+ };
878
+
879
+ // src/db/index.ts
880
+ var adapter = null;
881
+ async function isDatabaseSetUp() {
882
+ return hasExistingKey();
883
+ }
884
+ function isDatabaseUnlocked() {
885
+ return adapter?.isOpen() ?? false;
886
+ }
887
+ async function setupDatabase(password) {
888
+ const key = await setupNewKey(password);
889
+ adapter = new NativeSqliteAdapter();
890
+ await adapter.initialize(key);
891
+ await persistSession();
892
+ }
893
+ async function unlockDatabase(password) {
894
+ const key = await unlockWithPassword(password);
895
+ if (!key) {
896
+ return false;
897
+ }
898
+ adapter = new NativeSqliteAdapter();
899
+ await adapter.open(key);
900
+ await persistSession();
901
+ return true;
902
+ }
903
+ async function restoreDatabaseSession() {
904
+ const key = await restoreSession();
905
+ if (!key) {
906
+ return false;
907
+ }
908
+ adapter = new NativeSqliteAdapter();
909
+ await adapter.open(key);
910
+ return true;
911
+ }
912
+ function lockDatabase() {
913
+ if (adapter) {
914
+ adapter.close();
915
+ adapter = null;
916
+ }
917
+ clearKey();
918
+ }
919
+ function exportBackupDatabase() {
920
+ if (!adapter || !adapter.isOpen()) {
921
+ throw new Error("Database not open");
922
+ }
923
+ return adapter.exportToBackupDatabase();
924
+ }
925
+ function importBackupDatabase(database) {
926
+ if (!adapter || !adapter.isOpen()) {
927
+ throw new Error("Database not open");
928
+ }
929
+ adapter.importFromBackupDatabase(database);
930
+ }
931
+ async function changePassword2(oldPassword, newPassword) {
932
+ const result = await changePassword(oldPassword, newPassword);
933
+ if (!result) {
934
+ return false;
935
+ }
936
+ if (adapter?.isOpen()) {
937
+ await adapter.rekeyDatabase(result.newKey);
938
+ } else {
939
+ adapter = new NativeSqliteAdapter();
940
+ await adapter.open(result.oldKey);
941
+ await adapter.rekeyDatabase(result.newKey);
942
+ }
943
+ await persistSession();
944
+ return true;
945
+ }
946
+
947
+ // src/utils/prompt.ts
948
+ import { stdin, stdout } from "process";
949
+ import * as readline from "readline/promises";
950
+ async function promptPassword(prompt) {
951
+ const rl = readline.createInterface({ input: stdin, output: stdout });
952
+ try {
953
+ stdout.write(prompt);
954
+ return await new Promise((resolve) => {
955
+ let input = "";
956
+ if (stdin.isTTY) {
957
+ stdin.setRawMode(true);
958
+ }
959
+ stdin.resume();
960
+ stdin.setEncoding("utf8");
961
+ const onData = (char) => {
962
+ if (char === "\n" || char === "\r" || char === "") {
963
+ stdin.removeListener("data", onData);
964
+ if (stdin.isTTY) {
965
+ stdin.setRawMode(false);
966
+ }
967
+ stdout.write("\n");
968
+ resolve(input);
969
+ } else if (char === "") {
970
+ process.exit(1);
971
+ } else if (char === "\x7F" || char === "\b") {
972
+ if (input.length > 0) {
973
+ input = input.slice(0, -1);
974
+ }
975
+ } else {
976
+ input += char;
977
+ }
978
+ };
979
+ stdin.on("data", onData);
980
+ });
981
+ } finally {
982
+ rl.close();
983
+ }
984
+ }
985
+ async function promptConfirm(prompt) {
986
+ const rl = readline.createInterface({ input: stdin, output: stdout });
987
+ try {
988
+ const answer = await rl.question(prompt);
989
+ return answer.toLowerCase() === "y" || answer.toLowerCase() === "yes";
990
+ } finally {
991
+ rl.close();
992
+ }
993
+ }
994
+
995
+ // src/commands/backup.ts
996
+ async function resolveBackupPassword(options) {
997
+ if (options.password) {
998
+ return options.password;
999
+ }
1000
+ const password = await promptPassword("Backup password: ");
1001
+ const confirm = await promptPassword("Confirm backup password: ");
1002
+ if (password !== confirm) {
1003
+ console.error("Passwords do not match.");
1004
+ process.exit(1);
1005
+ }
1006
+ return password;
1007
+ }
1008
+ async function runBackup(file, options) {
1009
+ if (!await isDatabaseSetUp()) {
1010
+ console.error('Database not set up. Run "tearleads setup" first.');
1011
+ process.exit(1);
1012
+ }
1013
+ if (!isDatabaseUnlocked()) {
1014
+ if (await hasPersistedSession()) {
1015
+ const restored = await restoreDatabaseSession();
1016
+ if (!restored) {
1017
+ console.error('Session expired. Run "tearleads unlock" first.');
1018
+ process.exit(1);
1019
+ }
1020
+ } else {
1021
+ console.error('Database not unlocked. Run "tearleads unlock" first.');
1022
+ process.exit(1);
1023
+ }
1024
+ }
1025
+ const password = await resolveBackupPassword(options);
1026
+ const database = exportBackupDatabase();
1027
+ const manifest = {
1028
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1029
+ platform: "cli",
1030
+ appVersion: "cli",
1031
+ blobCount: 0,
1032
+ blobTotalSize: 0
1033
+ };
1034
+ const data = await encode({
1035
+ password,
1036
+ manifest,
1037
+ database,
1038
+ blobs: [],
1039
+ readBlob: async () => {
1040
+ throw new Error("No blob storage configured for CLI");
1041
+ }
1042
+ });
1043
+ const filePath = path3.resolve(file);
1044
+ await fs5.writeFile(filePath, data);
1045
+ console.log(`Backup saved to ${filePath}`);
1046
+ }
1047
+ var backupCommand = new Command("backup").description("Export database to an encrypted .rbu backup file").argument("<file>", "Output file path").option("-p, --password <password>", "Backup password").action(runBackup);
1048
+
1049
+ // src/commands/dump.ts
1050
+ import fs6 from "fs/promises";
1051
+ import path4 from "path";
1052
+ import { Command as Command2 } from "commander";
1053
+ async function runDump(folder, options) {
1054
+ let database;
1055
+ if (options.inputFile) {
1056
+ const filePath = path4.resolve(options.inputFile);
1057
+ const password = options.password ? options.password : await promptPassword("Backup password: ");
1058
+ try {
1059
+ const backupData = await fs6.readFile(filePath);
1060
+ const decoded = await decode({
1061
+ data: new Uint8Array(backupData),
1062
+ password
1063
+ });
1064
+ database = decoded.database;
1065
+ if (decoded.blobs.length > 0) {
1066
+ console.warn(
1067
+ `Warning: backup contains ${decoded.blobs.length} blobs that will be ignored.`
1068
+ );
1069
+ }
1070
+ } catch (err) {
1071
+ if (err.code === "ENOENT") {
1072
+ console.error(`File not found: ${filePath}`);
1073
+ } else {
1074
+ console.error(
1075
+ err instanceof Error ? err.message : "Failed to read or decode backup file."
1076
+ );
1077
+ }
1078
+ process.exit(1);
1079
+ }
1080
+ } else {
1081
+ if (!await isDatabaseSetUp()) {
1082
+ console.error('Database not set up. Run "tearleads setup" first.');
1083
+ process.exit(1);
1084
+ }
1085
+ if (!isDatabaseUnlocked()) {
1086
+ const canRestore = await hasPersistedSession();
1087
+ if (!canRestore || !await restoreDatabaseSession()) {
1088
+ const message = canRestore ? 'Session expired. Run "tearleads unlock" first.' : 'Database not unlocked. Run "tearleads unlock" first.';
1089
+ console.error(message);
1090
+ process.exit(1);
1091
+ }
1092
+ }
1093
+ database = exportBackupDatabase();
1094
+ }
1095
+ const outputPath = path4.resolve(folder);
1096
+ try {
1097
+ const stat = await fs6.stat(outputPath);
1098
+ if (stat.isDirectory()) {
1099
+ if (!options.force) {
1100
+ const confirmed = await promptConfirm(
1101
+ `Folder ${outputPath} exists. Overwrite? (y/n): `
1102
+ );
1103
+ if (!confirmed) {
1104
+ console.log("Dump cancelled.");
1105
+ return;
1106
+ }
1107
+ }
1108
+ await fs6.rm(outputPath, { recursive: true });
1109
+ } else {
1110
+ console.error(`${outputPath} exists and is not a directory.`);
1111
+ process.exit(1);
1112
+ }
1113
+ } catch (err) {
1114
+ if (err.code !== "ENOENT") {
1115
+ console.error(
1116
+ `Error checking output path "${outputPath}": ${err.message}`
1117
+ );
1118
+ process.exit(1);
1119
+ }
1120
+ }
1121
+ await fs6.mkdir(outputPath, { recursive: true });
1122
+ await fs6.mkdir(path4.join(outputPath, "tables"), { recursive: true });
1123
+ if (options.blobs !== false) {
1124
+ await fs6.mkdir(path4.join(outputPath, "files"), { recursive: true });
1125
+ }
1126
+ const manifest = {
1127
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1128
+ platform: "cli",
1129
+ appVersion: "cli",
1130
+ exportedTables: database.tables.map((t) => t.name),
1131
+ blobCount: 0,
1132
+ blobTotalSize: 0
1133
+ };
1134
+ await fs6.writeFile(
1135
+ path4.join(outputPath, "manifest.json"),
1136
+ JSON.stringify(manifest, null, 2)
1137
+ );
1138
+ const schema = {
1139
+ tables: database.tables,
1140
+ indexes: database.indexes
1141
+ };
1142
+ await fs6.writeFile(
1143
+ path4.join(outputPath, "schema.json"),
1144
+ JSON.stringify(schema, null, 2)
1145
+ );
1146
+ for (const table of database.tables) {
1147
+ const tableData = database.data[table.name] ?? [];
1148
+ await fs6.writeFile(
1149
+ path4.join(outputPath, "tables", `${table.name}.json`),
1150
+ JSON.stringify(tableData, null, 2)
1151
+ );
1152
+ }
1153
+ console.log(`Database dumped to ${outputPath}`);
1154
+ console.log(` Tables: ${database.tables.length}`);
1155
+ }
1156
+ var dumpCommand = new Command2("dump").description("Export database to unencrypted JSON files").argument("<folder>", "Output folder path").option(
1157
+ "-f, --input-file <file>",
1158
+ "Read from .rbu backup file instead of live database"
1159
+ ).option(
1160
+ "-p, --password <password>",
1161
+ "Backup file password (used with --input-file)"
1162
+ ).option("--force", "Overwrite existing folder without confirmation").option("--no-blobs", "Skip files directory").action(runDump);
1163
+
1164
+ // src/commands/lock.ts
1165
+ import { Command as Command3 } from "commander";
1166
+ async function runLock() {
1167
+ await lockDatabase();
1168
+ console.log("Database locked.");
1169
+ }
1170
+ var lockCommand = new Command3("lock").description("Lock the database").action(runLock);
1171
+
1172
+ // src/commands/password.ts
1173
+ import { Command as Command4 } from "commander";
1174
+ async function runPassword() {
1175
+ if (!await isDatabaseSetUp()) {
1176
+ console.error('Database not set up. Run "tearleads setup" first.');
1177
+ process.exit(1);
1178
+ }
1179
+ const oldPassword = await promptPassword("Current password: ");
1180
+ const newPassword = await promptPassword("New password: ");
1181
+ if (!newPassword) {
1182
+ console.error("Password cannot be empty.");
1183
+ process.exit(1);
1184
+ }
1185
+ const confirm = await promptPassword("Confirm new password: ");
1186
+ if (newPassword !== confirm) {
1187
+ console.error("Passwords do not match.");
1188
+ process.exit(1);
1189
+ }
1190
+ const success = await changePassword2(oldPassword, newPassword);
1191
+ if (!success) {
1192
+ console.error("Incorrect current password.");
1193
+ process.exit(1);
1194
+ }
1195
+ console.log("Password changed successfully.");
1196
+ }
1197
+ var passwordCommand = new Command4("password").description("Change database password").action(runPassword);
1198
+
1199
+ // src/commands/restore.ts
1200
+ import fs7 from "fs/promises";
1201
+ import path5 from "path";
1202
+ import { Command as Command5 } from "commander";
1203
+ async function runRestore(file, options) {
1204
+ const filePath = path5.resolve(file);
1205
+ try {
1206
+ await fs7.access(filePath);
1207
+ } catch {
1208
+ console.error(`File not found: ${filePath}`);
1209
+ process.exit(1);
1210
+ }
1211
+ if (!await isDatabaseSetUp()) {
1212
+ console.error('Database not set up. Run "tearleads setup" first.');
1213
+ process.exit(1);
1214
+ }
1215
+ if (!isDatabaseUnlocked()) {
1216
+ if (await hasPersistedSession()) {
1217
+ const restored = await restoreDatabaseSession();
1218
+ if (!restored) {
1219
+ console.error('Session expired. Run "tearleads unlock" first.');
1220
+ process.exit(1);
1221
+ }
1222
+ } else {
1223
+ console.error('Database not unlocked. Run "tearleads unlock" first.');
1224
+ process.exit(1);
1225
+ }
1226
+ }
1227
+ if (!options.force) {
1228
+ const confirmed = await promptConfirm(
1229
+ "This will overwrite existing data. Continue? (y/n): "
1230
+ );
1231
+ if (!confirmed) {
1232
+ console.log("Restore cancelled.");
1233
+ return;
1234
+ }
1235
+ }
1236
+ const password = options.password ? options.password : await promptPassword("Backup password: ");
1237
+ const backupData = await fs7.readFile(filePath);
1238
+ let decoded;
1239
+ try {
1240
+ decoded = await decode({ data: new Uint8Array(backupData), password });
1241
+ } catch (err) {
1242
+ console.error(
1243
+ err instanceof Error ? err.message : "Failed to decode backup file."
1244
+ );
1245
+ process.exit(1);
1246
+ }
1247
+ if (decoded.blobs.length > 0) {
1248
+ console.warn(
1249
+ `Warning: backup contains ${decoded.blobs.length} blobs that will be ignored in the CLI restore.`
1250
+ );
1251
+ }
1252
+ importBackupDatabase(decoded.database);
1253
+ console.log("Database restored successfully.");
1254
+ }
1255
+ var restoreCommand = new Command5("restore").description("Restore database from a backup file").argument("<file>", "Backup file path").option("-f, --force", "Overwrite without confirmation").option("-p, --password <password>", "Backup password").action(runRestore);
1256
+
1257
+ // src/commands/setup.ts
1258
+ import { Command as Command6 } from "commander";
1259
+ async function runSetup() {
1260
+ if (await isDatabaseSetUp()) {
1261
+ console.error(
1262
+ 'Database already set up. Use "tearleads password" to change password.'
1263
+ );
1264
+ process.exit(1);
1265
+ }
1266
+ const password = await promptPassword("Enter password: ");
1267
+ if (!password) {
1268
+ console.error("Password cannot be empty.");
1269
+ process.exit(1);
1270
+ }
1271
+ const confirm = await promptPassword("Confirm password: ");
1272
+ if (password !== confirm) {
1273
+ console.error("Passwords do not match.");
1274
+ process.exit(1);
1275
+ }
1276
+ await setupDatabase(password);
1277
+ console.log("Database initialized successfully.");
1278
+ }
1279
+ var setupCommand = new Command6("setup").description("Initialize a new encrypted database").action(runSetup);
1280
+
1281
+ // src/commands/unlock.ts
1282
+ import { Command as Command7 } from "commander";
1283
+ async function runUnlock() {
1284
+ if (!await isDatabaseSetUp()) {
1285
+ console.error('Database not set up. Run "tearleads setup" first.');
1286
+ process.exit(1);
1287
+ }
1288
+ if (isDatabaseUnlocked()) {
1289
+ console.log("Database already unlocked.");
1290
+ return;
1291
+ }
1292
+ if (await hasPersistedSession()) {
1293
+ const restored = await restoreDatabaseSession();
1294
+ if (restored) {
1295
+ console.log("Database unlocked (session restored).");
1296
+ return;
1297
+ }
1298
+ }
1299
+ const password = await promptPassword("Enter password: ");
1300
+ const success = await unlockDatabase(password);
1301
+ if (!success) {
1302
+ console.error("Incorrect password.");
1303
+ process.exit(1);
1304
+ }
1305
+ console.log("Database unlocked.");
1306
+ }
1307
+ var unlockCommand = new Command7("unlock").description("Unlock the database").action(runUnlock);
1308
+
1309
+ // src/index.ts
1310
+ var program = new Command8();
1311
+ program.name("tearleads").description("Tearleads CLI for database management").version("0.0.1");
1312
+ program.addCommand(setupCommand);
1313
+ program.addCommand(unlockCommand);
1314
+ program.addCommand(lockCommand);
1315
+ program.addCommand(backupCommand);
1316
+ program.addCommand(dumpCommand);
1317
+ program.addCommand(restoreCommand);
1318
+ program.addCommand(passwordCommand);
1319
+ program.parse();
1320
+ //# sourceMappingURL=index.js.map