neozip-cli 0.75.0-beta → 0.75.1-beta

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,2288 @@
1
+ "use strict";
2
+ /**
3
+ * ZIP creation functions for NeoZip CLI
4
+ */
5
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
6
+ if (k2 === undefined) k2 = k;
7
+ var desc = Object.getOwnPropertyDescriptor(m, k);
8
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
9
+ desc = { enumerable: true, get: function() { return m[k]; } };
10
+ }
11
+ Object.defineProperty(o, k2, desc);
12
+ }) : (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ o[k2] = m[k];
15
+ }));
16
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
17
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
18
+ }) : function(o, v) {
19
+ o["default"] = v;
20
+ });
21
+ var __importStar = (this && this.__importStar) || (function () {
22
+ var ownKeys = function(o) {
23
+ ownKeys = Object.getOwnPropertyNames || function (o) {
24
+ var ar = [];
25
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
26
+ return ar;
27
+ };
28
+ return ownKeys(o);
29
+ };
30
+ return function (mod) {
31
+ if (mod && mod.__esModule) return mod;
32
+ var result = {};
33
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
34
+ __setModuleDefault(result, mod);
35
+ return result;
36
+ };
37
+ })();
38
+ var __importDefault = (this && this.__importDefault) || function (mod) {
39
+ return (mod && mod.__esModule) ? mod : { "default": mod };
40
+ };
41
+ Object.defineProperty(exports, "__esModule", { value: true });
42
+ exports.createZip = createZip;
43
+ exports.testArchiveIntegrity = testArchiveIntegrity;
44
+ exports.upgradeZipForTokenization = upgradeZipForTokenization;
45
+ const fs = __importStar(require("fs"));
46
+ const path = __importStar(require("path"));
47
+ const neozipkit_1 = require("neozipkit");
48
+ const node_1 = __importDefault(require("neozipkit/node"));
49
+ const blockchain_1 = require("neozipkit/blockchain");
50
+ const CommentManager_1 = require("./CommentManager");
51
+ const utils_1 = require("./utils");
52
+ const file_operations_1 = require("./file-operations");
53
+ const blockchain_2 = require("./blockchain");
54
+ const user_interaction_1 = require("./user-interaction");
55
+ const exit_codes_1 = require("../exit-codes");
56
+ const version_1 = require("../version");
57
+ /**
58
+ * BufferOutputWriter: Collects ZIP data in memory (for in-memory mode)
59
+ */
60
+ class BufferOutputWriter {
61
+ constructor() {
62
+ this.chunks = [];
63
+ this.position = 0;
64
+ this.headerPositions = new Map(); // Track header positions for updates
65
+ }
66
+ async writeHeader(header) {
67
+ const headerPosition = this.position;
68
+ this.headerPositions.set(this.chunks.length, headerPosition);
69
+ this.chunks.push(header);
70
+ this.position += header.length;
71
+ }
72
+ async writeData(data) {
73
+ this.chunks.push(data);
74
+ this.position += data.length;
75
+ }
76
+ async updateHeader(headerPosition, compressedSize, crc) {
77
+ // Find the chunk containing this header position
78
+ let currentPos = 0;
79
+ for (let i = 0; i < this.chunks.length; i++) {
80
+ const chunk = this.chunks[i];
81
+ const chunkStart = currentPos;
82
+ const chunkEnd = currentPos + chunk.length;
83
+ if (headerPosition >= chunkStart && headerPosition < chunkEnd) {
84
+ // This chunk contains the header
85
+ const offsetInChunk = headerPosition - chunkStart;
86
+ if (chunk.length >= offsetInChunk + 22 && chunk.readUInt32LE(offsetInChunk) === 0x04034b50) {
87
+ // Valid local file header signature
88
+ // Update CRC (offset 14) if provided
89
+ if (crc !== undefined) {
90
+ chunk.writeUInt32LE(crc, offsetInChunk + 14);
91
+ }
92
+ // Update compressed size (offset 18)
93
+ chunk.writeUInt32LE(compressedSize, offsetInChunk + 18);
94
+ }
95
+ break;
96
+ }
97
+ currentPos = chunkEnd;
98
+ }
99
+ }
100
+ getPosition() {
101
+ return this.position;
102
+ }
103
+ async close() {
104
+ // Nothing to close for buffer mode
105
+ }
106
+ getResult() {
107
+ return Buffer.concat(this.chunks);
108
+ }
109
+ }
110
+ /**
111
+ * FileOutputWriter: Writes ZIP data directly to file (for file-based mode)
112
+ */
113
+ class FileOutputWriter {
114
+ constructor(archiveName, outputFd, outputStream, positionRef) {
115
+ this.stream = outputStream;
116
+ this.fd = outputFd;
117
+ this.positionRef = positionRef;
118
+ }
119
+ async writeHeader(header) {
120
+ return new Promise((resolve, reject) => {
121
+ this.stream.write(header, (error) => {
122
+ if (error) {
123
+ reject(error);
124
+ }
125
+ else {
126
+ this.positionRef.current += header.length;
127
+ resolve();
128
+ }
129
+ });
130
+ });
131
+ }
132
+ async writeData(data) {
133
+ return new Promise((resolve, reject) => {
134
+ this.stream.write(data, (error) => {
135
+ if (error) {
136
+ reject(error);
137
+ }
138
+ else {
139
+ this.positionRef.current += data.length;
140
+ resolve();
141
+ }
142
+ });
143
+ });
144
+ }
145
+ async updateHeader(headerPosition, compressedSize, crc) {
146
+ // Update CRC (offset 14) if provided
147
+ if (crc !== undefined) {
148
+ const crcOffset = headerPosition + 14;
149
+ const crcBuffer = Buffer.alloc(4);
150
+ crcBuffer.writeUInt32LE(crc, 0);
151
+ fs.writeSync(this.fd, crcBuffer, 0, 4, crcOffset);
152
+ }
153
+ // Update compressed size (offset 18)
154
+ const compressedSizeOffset = headerPosition + 18;
155
+ const sizeBuffer = Buffer.alloc(4);
156
+ sizeBuffer.writeUInt32LE(compressedSize, 0);
157
+ fs.writeSync(this.fd, sizeBuffer, 0, 4, compressedSizeOffset);
158
+ }
159
+ getPosition() {
160
+ return this.positionRef.current;
161
+ }
162
+ async close() {
163
+ // Close is handled by ZipCreator.closeOutput()
164
+ // This method exists for interface compliance
165
+ }
166
+ getResult() {
167
+ return null; // File mode doesn't return buffer
168
+ }
169
+ }
170
+ /**
171
+ * Create ZIP archive using the EXACT working code from zip.ts
172
+ */
173
+ async function createZip(archiveName, files, options = { verbose: false, quiet: false }) {
174
+ const zipCreator = new ZipCreator(archiveName, files, options);
175
+ await zipCreator.create();
176
+ }
177
+ /**
178
+ * ZipCreator class that handles ZIP file creation with modular, reusable methods
179
+ *
180
+ * ZIP FILE CREATION PROCEDURE:
181
+ *
182
+ * This class follows a sequential, efficient ZIP file creation process:
183
+ *
184
+ * 1. FILE ANALYSIS (processFiles):
185
+ * - Collects files to be added to the ZIP archive
186
+ * - Creates ZipEntry objects using zipkit.createZipEntry() for each file
187
+ * - Each entry is automatically added to zip.zipEntries[] (single source of truth)
188
+ * - Entries are stored in processing order (files first)
189
+ * - Sets file metadata: timestamps, sizes, paths, etc.
190
+ * - Marks entries as updated (entry.isUpdated = true) for compression
191
+ *
192
+ * 2. ZIP HEADER & COMPRESSION (compressEntries):
193
+ * - Loops through zip.zipEntries[] directly (maintains exact processing order)
194
+ * - For each entry marked for update (entry.isUpdated):
195
+ * a. Reads file data from disk (for in-memory mode) or streams (for file-based mode)
196
+ * b. Applies line ending conversion if requested
197
+ * c. SEQUENTIAL WRITE PROCESS:
198
+ * - Step 1: Get current file position
199
+ * - Step 2: Create local file header with placeholder compressed size (0)
200
+ * - Step 3: Write local header to output (file or memory buffer)
201
+ * - Step 4: Compress file data using ZipCompress.compressFileBuffer()
202
+ * - Step 5: Write compressed data to output
203
+ * - Step 6: Seek back and update compressed size in local header
204
+ * d. Updates entry with compression results: compressedSize, crc, sha256
205
+ * e. Accumulates SHA-256 hashes for blockchain metadata (if enabled)
206
+ *
207
+ * 3. METADATA ADDITION (handleTokenization):
208
+ * - After all files are compressed, calculates blockchain metadata (if enabled)
209
+ * - Creates metadata entry using zipkit.createZipEntry() for:
210
+ * - META-INF/NZIP.TOKEN (blockchain token metadata)
211
+ * - META-INF/TIMESTAMP.METADATA (timestamp metadata)
212
+ * - META-INF/TS-SUBMIT.OTS (OpenTimestamp proof)
213
+ * - Each metadata entry is automatically added to zip.zipEntries[] (at the end)
214
+ * - Writes metadata to ZIP file using same sequential process:
215
+ * - Local header (with placeholder) → metadata data → update header
216
+ * - Metadata entries use STORED compression (cmpMethod = 0) since they're small
217
+ *
218
+ * 4. CENTRAL DIRECTORY GENERATION (finalize):
219
+ * - Uses zip.zipEntries[] directly (already in correct order: files first, metadata last)
220
+ * - For each entry in zip.zipEntries[]:
221
+ * - Updates localHdrOffset with actual file position
222
+ * - Generates central directory entry using entry.centralDirEntry()
223
+ * - Writes central directory entry to output
224
+ * - Calculates central directory size and offset
225
+ * - Creates and writes End of Central Directory record
226
+ * - Closes output stream/file
227
+ *
228
+ * KEY PRINCIPLES:
229
+ * - zip.zipEntries[] is the SINGLE SOURCE OF TRUTH for entry order
230
+ * - Entries are added to zip.zipEntries[] in processing order (files first, metadata last)
231
+ * - No reordering or separation logic needed - natural order is correct
232
+ * - Sequential write ensures efficient processing: header → data → update header
233
+ * - Local header management handled in neozipkit (ZipCompress)
234
+ * - Zipkit is used for loading existing ZIPs (buffer-based mode)
235
+ */
236
+ class ZipCreator {
237
+ constructor(archiveName, files, options) {
238
+ this.archiveName = archiveName;
239
+ this.files = files;
240
+ this.options = options;
241
+ this.tempArchiveName = '';
242
+ this.inputFiles = [];
243
+ this.entryToPath = new Map();
244
+ this.entryToDisplay = new Map();
245
+ this.entryToStats = new Map();
246
+ this.hashAccumulator = null; // Accumulate SHA-256 hashes as entries are added
247
+ this.zipWriter = null; // ZIP file writer (replaces outputStream/outputFd)
248
+ this.currentPosition = 0;
249
+ this.totalIn = 0;
250
+ this.totalOut = 0;
251
+ this.filesCount = 0;
252
+ this.fdCache = new Map();
253
+ this.compressionOffset = 0;
254
+ this.centralDirOffset = 0;
255
+ this.zip = new node_1.default(); // Use ZipkitNode for ZIP operations
256
+ // Initialize HashAccumulator if blockchain features are enabled
257
+ if (this.options.blockchain || this.options.blockchainOts) {
258
+ this.hashAccumulator = new neozipkit_1.HashCalculator({ enableAccumulation: true });
259
+ }
260
+ }
261
+ /**
262
+ * Initialize the output file stream for direct writing
263
+ * Uses ZipkitNode's initializeZipFile() method
264
+ * Always writes to temporary file first for atomic operations
265
+ */
266
+ async initializeOutput() {
267
+ // Ensure temp file name is set (should be set in initialize())
268
+ if (!this.tempArchiveName) {
269
+ throw new Error('Temp archive name not initialized. Call initialize() first.');
270
+ }
271
+ // For all modes (file-based or in-memory blockchain), create temp file first
272
+ // For in-memory blockchain mode, compression will write directly to temp file
273
+ // For file-based mode, compression will also write to temp file
274
+ // Final file will be moved atomically after successful completion
275
+ this.zipWriter = await this.zip.initializeZipFile(this.tempArchiveName);
276
+ this.currentPosition = 0;
277
+ if (this.options.verbose) {
278
+ (0, utils_1.log)(`📊 Created temporary output file: ${this.tempArchiveName}`, this.options);
279
+ }
280
+ }
281
+ /**
282
+ * Write a chunk of data directly to the output file stream or buffer
283
+ * Used for in-memory mode and metadata entries
284
+ */
285
+ async writeChunk(data) {
286
+ // For in-memory non-blockchain mode, write to buffer
287
+ if (this.options.inMemory && !this.options.blockchain && !this.options.blockchainOts) {
288
+ const bufferWriter = this.zip.bufferWriter;
289
+ if (bufferWriter) {
290
+ // Determine if this is a header or data by checking signature
291
+ if (data.length >= 4 && data.readUInt32LE(0) === 0x04034b50) {
292
+ // This is a local file header
293
+ await bufferWriter.writeHeader(data);
294
+ }
295
+ else {
296
+ // This is data
297
+ await bufferWriter.writeData(data);
298
+ }
299
+ this.currentPosition = bufferWriter.getPosition();
300
+ return;
301
+ }
302
+ }
303
+ // For file-based mode or in-memory blockchain mode, write to file stream
304
+ if (!this.zipWriter) {
305
+ throw new Error('ZIP writer not initialized');
306
+ }
307
+ return new Promise((resolve, reject) => {
308
+ this.zipWriter.outputStream.write(data, (error) => {
309
+ if (error) {
310
+ reject(error);
311
+ }
312
+ else {
313
+ this.zipWriter.currentPosition += data.length;
314
+ this.currentPosition = this.zipWriter.currentPosition;
315
+ resolve();
316
+ }
317
+ });
318
+ });
319
+ }
320
+ /**
321
+ * Close the output stream and file descriptor
322
+ * Uses ZipkitNode's finalizeZipFile() method
323
+ */
324
+ async closeOutput() {
325
+ if (this.zipWriter) {
326
+ await this.zip.finalizeZipFile(this.zipWriter);
327
+ this.zipWriter = null;
328
+ }
329
+ }
330
+ async create() {
331
+ try {
332
+ await this.initialize();
333
+ // Initialize output stream for file-based mode OR in-memory blockchain mode
334
+ // For in-memory blockchain mode, we need to write directly to file during compression
335
+ // For in-memory non-blockchain mode, output is written directly during compression
336
+ if (!this.options.inMemory || (this.options.blockchain || this.options.blockchainOts)) {
337
+ await this.initializeOutput();
338
+ }
339
+ await this.processFiles();
340
+ await this.setupCompression();
341
+ await this.handleComments();
342
+ await this.handleCompression();
343
+ await this.handleTokenization();
344
+ // Finalize ZIP file (write central directory and end record)
345
+ // For all modes (including in-memory non-blockchain), finalize after metadata is added
346
+ // This ensures metadata is included in the ZIP before central directory is written
347
+ await this.finalize();
348
+ // Test integrity AFTER finalization (so it includes metadata)
349
+ if (this.options.testIntegrity) {
350
+ const integrityPassed = await testArchiveIntegrity(this.archiveName, this.options);
351
+ if (!integrityPassed) {
352
+ (0, utils_1.logError)(`❌ Archive integrity test failed for: ${this.archiveName}`);
353
+ throw new Error('Archive integrity test failed');
354
+ }
355
+ }
356
+ // Display final statistics
357
+ this.displayStatistics();
358
+ }
359
+ catch (error) {
360
+ // On error, ensure temp file is cleaned up
361
+ if (this.tempArchiveName && fs.existsSync(this.tempArchiveName)) {
362
+ try {
363
+ fs.unlinkSync(this.tempArchiveName);
364
+ if (this.options.verbose) {
365
+ (0, utils_1.log)(`🧹 Cleaned up temporary file after error`, this.options);
366
+ }
367
+ }
368
+ catch (cleanupError) {
369
+ // Ignore cleanup errors
370
+ if (this.options.verbose) {
371
+ (0, utils_1.log)(`⚠️ Warning: Could not clean up temp file: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`, this.options);
372
+ }
373
+ }
374
+ }
375
+ // Re-throw error after cleanup
376
+ throw error;
377
+ }
378
+ }
379
+ displayStatistics() {
380
+ (0, utils_1.log)(`\n✅ ZIP archive created: ${this.archiveName}`, this.options);
381
+ (0, utils_1.log)(` Files: ${this.filesCount}`, this.options);
382
+ (0, utils_1.log)(` Total size: ${(0, utils_1.formatBytes)(this.totalIn)}`, this.options);
383
+ (0, utils_1.log)(` Compressed size: ${(0, utils_1.formatBytes)(this.totalOut)}`, this.options);
384
+ (0, utils_1.log)(` Compression ratio: ${this.totalIn > 0 ? Math.round((1 - this.totalOut / this.totalIn) * 100) : 0}%`, this.options);
385
+ if (this.options.debug) {
386
+ (0, utils_1.logDebug)(` Input files processed: ${this.filesCount}`, this.options);
387
+ (0, utils_1.logDebug)(` Total uncompressed size: ${this.totalIn} bytes`, this.options);
388
+ (0, utils_1.logDebug)(` Total compressed size: ${this.totalOut} bytes`, this.options);
389
+ (0, utils_1.logDebug)(` Compression ratio: ${this.totalIn > 0 ? Math.round((1 - this.totalOut / this.totalIn) * 100) : 0}%`, this.options);
390
+ (0, utils_1.logDebug)(` Archive file: ${this.archiveName}`, this.options);
391
+ if (fs.existsSync(this.archiveName)) {
392
+ (0, utils_1.logDebug)(` Archive file size: ${fs.statSync(this.archiveName).size} bytes`, this.options);
393
+ }
394
+ }
395
+ }
396
+ async initialize() {
397
+ // Display application information
398
+ (0, utils_1.log)(`🚀 NEOZIP v${version_1.APP_VERSION} (${version_1.APP_RELEASE_DATE})`, this.options);
399
+ (0, utils_1.log)(`📦 Creating: ${this.archiveName}`, this.options);
400
+ (0, utils_1.log)(`📁 Files: ${this.files.length} file(s)`, this.options);
401
+ const compressionDesc = this.options.level === 0 ? 'store (no compression)' : `${this.options.compression} (level ${this.options.level})`;
402
+ (0, utils_1.log)(`🔧 Compression: ${compressionDesc}`, this.options);
403
+ (0, utils_1.log)(`📊 Block size: ${(0, utils_1.formatBytes)(this.options.blockSize || 64 * 1024)}`, this.options);
404
+ (0, utils_1.log)(`🔗 Tokenization: ${this.options.blockchain ? 'enabled' : 'disabled'}`, this.options);
405
+ const encryptionMethod = this.options.encryptionMethod || 'pkzip';
406
+ (0, utils_1.log)(`🔐 Encryption: ${this.options.encrypt ? `${encryptionMethod.toUpperCase()} enabled` : 'disabled'}`, this.options);
407
+ (0, utils_1.log)('', this.options);
408
+ if (this.options.debug) {
409
+ (0, utils_1.logDebug)('Debug mode enabled', this.options);
410
+ (0, utils_1.logDebug)(`Command line arguments: ${process.argv.join(' ')}`, this.options);
411
+ (0, utils_1.logDebug)(`Working directory: ${process.cwd()}`, this.options);
412
+ (0, utils_1.logDebug)(`Node.js version: ${process.version}`, this.options);
413
+ (0, utils_1.logDebug)(`Platform: ${process.platform} ${process.arch}`, this.options);
414
+ (0, utils_1.log)('', this.options);
415
+ }
416
+ if (this.options.verbose) {
417
+ (0, utils_1.log)(`Creating ZIP archive: ${this.archiveName}`, this.options);
418
+ (0, utils_1.log)(`Files to compress: ${this.files.join(', ')}`, this.options);
419
+ if (this.options.blockchain) {
420
+ if (this.options.blockchainDefault) {
421
+ (0, utils_1.log)(`Blockchain: enabled (auto-select default)`, this.options);
422
+ }
423
+ else if (this.options.blockchainMint) {
424
+ (0, utils_1.log)(`Blockchain: enabled (force mint new)`, this.options);
425
+ }
426
+ else {
427
+ (0, utils_1.log)(`Blockchain: enabled (default)`, this.options);
428
+ }
429
+ (0, utils_1.log)(`Tokenization: enabled`, this.options);
430
+ (0, utils_1.log)(`Network: ${this.options.network || 'base-sepolia'}`, this.options);
431
+ }
432
+ else if (this.options.blockchainOts) {
433
+ (0, utils_1.log)(`Blockchain: enabled (OpenTimestamp)`, this.options);
434
+ (0, utils_1.log)(`Bitcoin blockchain: enabled`, this.options);
435
+ }
436
+ else {
437
+ (0, utils_1.log)(`Blockchain: disabled`, this.options);
438
+ (0, utils_1.log)(`Tokenization: disabled`, this.options);
439
+ }
440
+ if (this.options.toCrlf) {
441
+ (0, utils_1.log)(`Line ending conversion: LF to CRLF (Windows style)`, this.options);
442
+ }
443
+ if (this.options.fromCrlf) {
444
+ (0, utils_1.log)(`Line ending conversion: CRLF to LF (Unix style)`, this.options);
445
+ }
446
+ }
447
+ // Ensure parent directory exists
448
+ const parentDir = path.dirname(this.archiveName);
449
+ if (parentDir && parentDir !== '.' && !fs.existsSync(parentDir)) {
450
+ fs.mkdirSync(parentDir, { recursive: true });
451
+ if (this.options.verbose)
452
+ (0, utils_1.log)(`Created directory: ${parentDir}`, this.options);
453
+ }
454
+ // Create temporary ZIP file name
455
+ const tempDir = (0, utils_1.getTempDir)(this.options);
456
+ // Ensure temp directory exists
457
+ if (!fs.existsSync(tempDir)) {
458
+ fs.mkdirSync(tempDir, { recursive: true });
459
+ }
460
+ this.tempArchiveName = path.join(tempDir, `${path.basename(this.archiveName)}.tmp.${Date.now()}`);
461
+ if (this.options.tempPath && this.options.verbose) {
462
+ (0, utils_1.log)(`📁 Using custom temp directory: ${tempDir}`, this.options);
463
+ }
464
+ }
465
+ /**
466
+ * Collect and filter files for compression (unified for both modes)
467
+ */
468
+ async collectAndFilterFiles() {
469
+ const collectedFiles = await (0, file_operations_1.collectFilesRecursively)(this.files, this.options);
470
+ const filePaths = collectedFiles.map(f => f.displayPath);
471
+ const filteredFilePaths = (0, file_operations_1.filterFiles)(filePaths, this.options);
472
+ return collectedFiles.filter(f => filteredFilePaths.includes(f.displayPath)).map(f => ({
473
+ entryName: f.entryName,
474
+ absPath: f.absPath,
475
+ displayPath: f.displayPath,
476
+ stat: f.stat,
477
+ isSymlink: f.isSymlink,
478
+ linkTarget: f.linkTarget,
479
+ isHardLink: f.isHardLink,
480
+ originalEntry: f.originalEntry,
481
+ inode: f.inode
482
+ }));
483
+ }
484
+ async processFiles() {
485
+ // Load existing archive if update or freshen mode is enabled
486
+ let existingArchive = null;
487
+ if (this.options.update || this.options.freshen) {
488
+ try {
489
+ existingArchive = (0, file_operations_1.loadExistingArchive)(this.archiveName, this.options);
490
+ if (existingArchive) {
491
+ const mode = this.options.freshen ? 'Freshen' : 'Update';
492
+ const existingDir = existingArchive.getDirectory();
493
+ (0, utils_1.log)(`📦 ${mode} mode: Found existing archive with ${existingDir?.length || 0} files`, this.options);
494
+ }
495
+ else {
496
+ const mode = this.options.freshen ? 'Freshen' : 'Update';
497
+ (0, utils_1.log)(`📦 ${mode} mode: No existing archive found, creating new one`, this.options);
498
+ }
499
+ }
500
+ catch (error) {
501
+ (0, utils_1.logError)(`❌ Failed to load existing archive: ${error instanceof Error ? error.message : String(error)}`);
502
+ (0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.CANT_OPEN_ARCHIVE);
503
+ }
504
+ }
505
+ // Collect files recursively using unified method
506
+ const filteredFiles = await this.collectAndFilterFiles();
507
+ if (this.options.verbose && (this.options.include || this.options.exclude || this.options.suffixes)) {
508
+ const collectedFiles = await (0, file_operations_1.collectFilesRecursively)(this.files, this.options);
509
+ (0, utils_1.log)(`File filtering applied:`, this.options);
510
+ if (this.options.include) {
511
+ (0, utils_1.log)(` Include patterns: ${this.options.include.join(', ')}`, this.options);
512
+ }
513
+ if (this.options.exclude) {
514
+ (0, utils_1.log)(` Exclude patterns: ${this.options.exclude.join(', ')}`, this.options);
515
+ }
516
+ if (this.options.suffixes) {
517
+ (0, utils_1.log)(` Excluded suffixes: ${this.options.suffixes.join(', ')}`, this.options);
518
+ }
519
+ (0, utils_1.log)(` Original files: ${collectedFiles.length}, Filtered files: ${filteredFiles.length}`, this.options);
520
+ }
521
+ // Store filtered files for use in compression
522
+ this.inputFiles = filteredFiles;
523
+ // Filter files for update or freshen mode
524
+ if ((this.options.update || this.options.freshen) && existingArchive) {
525
+ const originalCount = this.inputFiles.length;
526
+ this.inputFiles = this.inputFiles.filter(file => {
527
+ const shouldUpdate = (0, file_operations_1.shouldUpdateFile)(file.entryName, file.stat, existingArchive, this.options.freshen);
528
+ if (!shouldUpdate && this.options.verbose) {
529
+ const reason = this.options.freshen ? 'not in archive' : 'up to date';
530
+ (0, utils_1.log)(`skipping: ${file.displayPath} (${reason})`, this.options);
531
+ }
532
+ return shouldUpdate;
533
+ });
534
+ const mode = this.options.freshen ? 'Freshen' : 'Update';
535
+ if (this.options.verbose) {
536
+ (0, utils_1.log)(`${mode} filtering: ${originalCount} files checked, ${this.inputFiles.length} files need updating`, this.options);
537
+ }
538
+ }
539
+ // Display planned files (optional)
540
+ if (this.options.verbose) {
541
+ (0, utils_1.log)('Files to compress:', this.options);
542
+ for (const it of this.inputFiles) {
543
+ (0, utils_1.log)(` - ${it.displayPath} (${(0, utils_1.formatBytes)(it.stat.size)})`, this.options);
544
+ }
545
+ }
546
+ // Handle existing archive entries for update mode
547
+ if (this.options.update && existingArchive) {
548
+ const existingEntries = await existingArchive.getDirectory() || [];
549
+ const updatedEntryNames = new Set(this.inputFiles.map(f => f.entryName));
550
+ // Copy existing entries that aren't being updated
551
+ const entriesToPreserve = existingEntries.filter(entry => !updatedEntryNames.has(entry.filename));
552
+ for (const existingEntry of entriesToPreserve) {
553
+ // Copy the existing entry to the new archive
554
+ const newEntry = this.zip.createZipEntry(existingEntry.filename);
555
+ newEntry.timeDateDOS = existingEntry.timeDateDOS;
556
+ newEntry.uncompressedSize = existingEntry.uncompressedSize;
557
+ newEntry.compressedSize = existingEntry.compressedSize;
558
+ newEntry.cmpMethod = existingEntry.cmpMethod;
559
+ newEntry.crc = existingEntry.crc;
560
+ // Copy the file data from the existing archive
561
+ try {
562
+ const existingData = await (async () => {
563
+ try {
564
+ const buf = fs.readFileSync(this.archiveName);
565
+ const { default: Zipkit } = require('neozipkit');
566
+ const kit = new Zipkit();
567
+ kit.loadZip(buf);
568
+ return await kit.extract(existingEntry);
569
+ }
570
+ catch {
571
+ return null;
572
+ }
573
+ })();
574
+ if (existingData) {
575
+ newEntry.fileBuffer = existingData;
576
+ newEntry.isUpdated = true;
577
+ }
578
+ }
579
+ catch (error) {
580
+ if (this.options.verbose) {
581
+ (0, utils_1.log)(`warning: Could not copy existing entry ${existingEntry.filename}`, this.options);
582
+ }
583
+ }
584
+ if (this.options.verbose)
585
+ (0, utils_1.log)(`preserved: ${existingEntry.filename} (existing)`, this.options);
586
+ }
587
+ }
588
+ // Create entries for input files
589
+ let processedFiles = 0;
590
+ const totalFiles = this.inputFiles.length;
591
+ for (const { entryName, absPath, stat, displayPath, isSymlink, linkTarget, isHardLink, originalEntry, inode } of this.inputFiles) {
592
+ if (this.options.debug) {
593
+ (0, utils_1.logDebug)(`Processing file: ${displayPath}`, this.options);
594
+ (0, utils_1.logDebug)(` Entry name: ${entryName}`, this.options);
595
+ (0, utils_1.logDebug)(` Absolute path: ${absPath}`, this.options);
596
+ (0, utils_1.logDebug)(` File size: ${stat.size} bytes`, this.options);
597
+ (0, utils_1.logDebug)(` File stats: ${JSON.stringify({
598
+ isFile: stat.isFile(),
599
+ isDirectory: stat.isDirectory(),
600
+ mode: stat.mode,
601
+ mtime: stat.mtime.toISOString(),
602
+ atime: stat.atime.toISOString(),
603
+ ctime: stat.ctime.toISOString()
604
+ })}`, this.options);
605
+ }
606
+ if (this.options.progress && !this.options.quiet) {
607
+ (0, utils_1.log)(`📦 Processing file ${processedFiles + 1}/${totalFiles}: ${displayPath}`, this.options);
608
+ }
609
+ if (this.options.showFiles && !this.options.quiet) {
610
+ (0, utils_1.log)(`adding: ${entryName}`, this.options);
611
+ }
612
+ const entry = this.zip.createZipEntry(entryName);
613
+ if (this.options.debug) {
614
+ (0, utils_1.logDebug)(` Creating ZIP entry for: ${entryName}`, this.options);
615
+ }
616
+ // Set the timestamp directly from file modification time
617
+ // @ts-ignore neozipkit ZipEntry has method setDateTime at runtime
618
+ entry.timeDateDOS = entry.setDateTime(new Date(stat.mtimeMs));
619
+ if (this.options.debug) {
620
+ (0, utils_1.logDebug)(` Set timestamp: ${new Date(stat.mtimeMs).toISOString()}`, this.options);
621
+ (0, utils_1.logDebug)(` Set DOS timestamp: ${entry.timeDateDOS}`, this.options);
622
+ }
623
+ // Also set lastModTimeDate for better compatibility (same as timeDateDOS)
624
+ entry.lastModTimeDate = entry.timeDateDOS;
625
+ // Set extended timestamps for better accuracy on Unix/Mac systems
626
+ // Try to set extended timestamps if the properties exist
627
+ try {
628
+ // @ts-ignore neozipkit ZipEntry may have extended timestamp properties
629
+ entry.ntfsTime = {
630
+ mtime: stat.mtimeMs,
631
+ atime: stat.atimeMs,
632
+ ctime: stat.ctimeMs
633
+ };
634
+ }
635
+ catch (e) {
636
+ // Property may not exist, that's okay
637
+ }
638
+ try {
639
+ // @ts-ignore neozipkit ZipEntry may have extended timestamp properties
640
+ entry.extendedTime = {
641
+ mtime: stat.mtimeMs,
642
+ atime: stat.atimeMs,
643
+ ctime: stat.ctimeMs
644
+ };
645
+ }
646
+ catch (e) {
647
+ // Property may not exist, that's okay
648
+ }
649
+ // Preserve UID/GID if requested (Unix-like systems only)
650
+ if (this.options.preservePerms && process.platform !== 'win32') {
651
+ try {
652
+ // @ts-ignore fs.Stats has uid and gid on Unix systems
653
+ entry.uid = stat.uid;
654
+ // @ts-ignore fs.Stats has uid and gid on Unix systems
655
+ entry.gid = stat.gid;
656
+ if (this.options.debug) {
657
+ (0, utils_1.logDebug)(` Preserved permissions: uid=${entry.uid}, gid=${entry.gid}`, this.options);
658
+ }
659
+ }
660
+ catch (e) {
661
+ if (this.options.debug) {
662
+ (0, utils_1.logDebug)(` Could not preserve permissions: ${e}`, this.options);
663
+ }
664
+ }
665
+ }
666
+ // Handle symbolic links
667
+ if (isSymlink && linkTarget) {
668
+ // Mark entry as symbolic link and store target
669
+ entry.isSymlink = true;
670
+ entry.linkTarget = linkTarget;
671
+ // Set Unix file attributes for symbolic link (S_IFLNK | permissions)
672
+ const S_IFLNK = 0o120000; // Symbolic link file type
673
+ const permissions = stat.mode & 0o777; // Extract permission bits
674
+ entry.extFileAttr = (S_IFLNK | permissions) << 16;
675
+ // Set the uncompressed size to the length of the target path
676
+ entry.uncompressedSize = Buffer.byteLength(linkTarget, 'utf8');
677
+ if (this.options.debug) {
678
+ (0, utils_1.logDebug)(` Symbolic link: ${linkTarget}`, this.options);
679
+ (0, utils_1.logDebug)(` Link target size: ${entry.uncompressedSize} bytes`, this.options);
680
+ }
681
+ }
682
+ // Handle hard links
683
+ if (isHardLink && originalEntry && inode) {
684
+ // Mark entry as hard link and store reference to original
685
+ entry.isHardLink = true;
686
+ entry.originalEntry = originalEntry;
687
+ entry.inode = inode;
688
+ // For hard links, we don't store the file content again
689
+ // Set uncompressed size to 0 to indicate this is a link entry
690
+ entry.uncompressedSize = 0;
691
+ if (this.options.debug) {
692
+ (0, utils_1.logDebug)(` Hard link: ${entryName} -> ${originalEntry} (inode: ${inode})`, this.options);
693
+ (0, utils_1.logDebug)(` Link entry (no content stored)`, this.options);
694
+ }
695
+ }
696
+ // Debug: Check what setDateTime actually returns
697
+ if (process.env.DEBUG_UPDATE) {
698
+ console.log(`Setting timestamp for ${entryName}:`);
699
+ console.log(` File mtime: ${stat.mtimeMs} (${new Date(stat.mtimeMs).toISOString()})`);
700
+ console.log(` timeDateDOS set to: ${Math.floor(stat.mtimeMs / 1000)}`);
701
+ console.log(` lastModTimeDate set to: ${Math.floor(stat.mtimeMs / 1000)}`);
702
+ console.log(` Extended timestamps set: ntfsTime=${!!entry.ntfsTime}, extendedTime=${!!entry.extendedTime}`);
703
+ }
704
+ entry.uncompressedSize = stat.size;
705
+ // Mark entry as updated so it gets processed
706
+ entry.isUpdated = true;
707
+ // Entry is automatically added to zip.zipEntries[] by createZipEntry()
708
+ this.entryToPath.set(entryName, absPath);
709
+ this.entryToDisplay.set(entryName, displayPath);
710
+ this.entryToStats.set(entryName, stat);
711
+ if (this.options.verbose)
712
+ (0, utils_1.log)(`Queued: ${entryName} (${stat.size} bytes)`, this.options);
713
+ processedFiles++;
714
+ }
715
+ }
716
+ async setupCompression() {
717
+ // Handle password prompting for encryption
718
+ if (this.options.encrypt && this.options.password === 'PROMPT') {
719
+ (0, utils_1.log)('🔐 Encryption enabled - password required', this.options);
720
+ this.options.password = await (0, user_interaction_1.promptPassword)('Enter password: ');
721
+ if (!this.options.password) {
722
+ (0, utils_1.logError)('❌ Password is required for encryption');
723
+ (0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.PARAMETER_ERROR);
724
+ }
725
+ (0, utils_1.log)('✅ Password set for encryption', this.options);
726
+ }
727
+ }
728
+ async handleComments() {
729
+ let commentOptions = {};
730
+ if (this.options.commentFile) {
731
+ (0, utils_1.log)('📝 Reading comments from file...', this.options);
732
+ const commentResult = await CommentManager_1.CommentManager.readCommentsFromFile(this.options.commentFile);
733
+ if (commentResult.success) {
734
+ commentOptions = {
735
+ archiveComment: commentResult.archiveComment,
736
+ fileComments: commentResult.fileComments
737
+ };
738
+ if (commentResult.archiveComment) {
739
+ (0, utils_1.log)(` Archive comment: ${commentResult.archiveComment.length} characters`, this.options);
740
+ }
741
+ if (commentResult.fileComments) {
742
+ (0, utils_1.log)(` File comments: ${Object.keys(commentResult.fileComments).length} files`, this.options);
743
+ }
744
+ }
745
+ else {
746
+ (0, utils_1.logError)(`❌ Error reading comment file: ${commentResult.error}`);
747
+ (0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.READ_ERROR);
748
+ }
749
+ }
750
+ else if (this.options.archiveComment) {
751
+ commentOptions.archiveComment = this.options.archiveComment;
752
+ (0, utils_1.log)(`📝 Archive comment: ${this.options.archiveComment.length} characters`, this.options);
753
+ }
754
+ if (this.options.interactiveComments) {
755
+ (0, utils_1.log)('📝 Interactive comment mode enabled', this.options);
756
+ if (!commentOptions.archiveComment) {
757
+ commentOptions.archiveComment = await CommentManager_1.CommentManager.promptForArchiveComment();
758
+ }
759
+ const filenames = this.inputFiles.map(f => f.entryName);
760
+ const fileComments = await CommentManager_1.CommentManager.promptForFileComments(filenames);
761
+ if (Object.keys(fileComments).length > 0) {
762
+ commentOptions.fileComments = fileComments;
763
+ }
764
+ }
765
+ if (this.options.fileCommentsOnly) {
766
+ (0, utils_1.log)('📝 File comment mode enabled', this.options);
767
+ const filenames = this.inputFiles.map(f => f.entryName);
768
+ const fileComments = await CommentManager_1.CommentManager.promptForFileCommentsOnly(filenames);
769
+ if (Object.keys(fileComments).length > 0) {
770
+ commentOptions.fileComments = fileComments;
771
+ }
772
+ }
773
+ // Set archive comment option (will be applied during finalization)
774
+ if (commentOptions.archiveComment) {
775
+ this.options.archiveComment = commentOptions.archiveComment;
776
+ (0, utils_1.log)('✅ Archive comment set (will be applied during finalization)', this.options);
777
+ }
778
+ // Apply file comments to entries
779
+ if (commentOptions.fileComments) {
780
+ const entries = await this.zip.getDirectory() || [];
781
+ for (const [filename, comment] of Object.entries(commentOptions.fileComments)) {
782
+ const entry = entries.find((e) => e.filename === filename);
783
+ if (entry) {
784
+ entry.comment = comment;
785
+ if (this.options.verbose)
786
+ (0, utils_1.log)(` File comment set: ${filename}`, this.options);
787
+ }
788
+ else {
789
+ (0, utils_1.logError)(`❌ File not found for comment: ${filename}`);
790
+ (0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.CANT_FIND_ARCHIVE);
791
+ }
792
+ }
793
+ (0, utils_1.log)('✅ File comments applied successfully', this.options);
794
+ }
795
+ }
796
+ async handleCompression() {
797
+ this.compressionOffset = await this.compressEntries();
798
+ }
799
+ /**
800
+ * Unified compression method for both in-memory and file-based modes
801
+ * Loops through zip.zipEntries[] directly (single source of truth for entry order)
802
+ */
803
+ async compressEntries() {
804
+ // Create compression options
805
+ const level = this.options.level ?? 6;
806
+ // Validate password exists when encryption is enabled
807
+ if (this.options.encrypt && !this.options.password) {
808
+ (0, utils_1.logError)('❌ Password is required for encryption');
809
+ throw new Error('Password is required for encryption');
810
+ }
811
+ const compressionOptions = {
812
+ level: level,
813
+ useZstd: level === 0 ? false : (this.options.compression || 'zstd') === 'zstd',
814
+ useSHA256: this.options.blockchain || this.options.blockchainOts || false,
815
+ password: this.options.encrypt ? this.options.password : null,
816
+ encryptionMethod: this.options.encryptionMethod || 'pkzip'
817
+ };
818
+ // Get entries from zip.getDirectory() (populated by processFiles())
819
+ // This is the single source of truth for entry order
820
+ const zipEntries = this.zip.getDirectory();
821
+ if (zipEntries.length === 0) {
822
+ (0, utils_1.log)('No files to compress', this.options);
823
+ return 0;
824
+ }
825
+ if (this.options.inMemory) {
826
+ // In-memory mode: use compressFileBuffer with BufferOutputWriter
827
+ (0, utils_1.log)(`🔧 Using in-memory compression (browser compatible)`, this.options);
828
+ (0, utils_1.log)(`🔄 Compressing ${zipEntries.length} files in memory...`, this.options);
829
+ // Create ZipkitNode instance for buffer-based compression
830
+ const zipkit = new node_1.default();
831
+ // Determine if we need to write to file (for blockchain mode)
832
+ const writeToFile = this.options.blockchain || this.options.blockchainOts;
833
+ // Create appropriate output writer
834
+ let writer;
835
+ const positionRef = { current: this.currentPosition };
836
+ if (writeToFile) {
837
+ // For blockchain mode, write directly to file
838
+ if (!this.zipWriter) {
839
+ throw new Error('ZIP writer not initialized for blockchain mode');
840
+ }
841
+ // Start from currentPosition (should be 0 for new file, or existing position for append)
842
+ positionRef.current = this.currentPosition;
843
+ writer = new FileOutputWriter(this.archiveName, this.zipWriter.outputFd, this.zipWriter.outputStream, positionRef);
844
+ }
845
+ else {
846
+ // For non-blockchain mode, collect in memory
847
+ writer = new BufferOutputWriter();
848
+ }
849
+ // Compress each file using SEQUENTIAL write: header -> compress -> data -> update header
850
+ // Loop through zip.zipEntries[] directly (entries in processing order)
851
+ for (const entry of zipEntries) {
852
+ // Skip entries that aren't marked for update (e.g., preserved entries in update mode)
853
+ if (!entry.isUpdated) {
854
+ continue;
855
+ }
856
+ // Read file data for this entry
857
+ const absPath = this.entryToPath.get(entry.filename);
858
+ if (!absPath) {
859
+ (0, utils_1.logError)(`❌ Cannot find source path for entry: ${entry.filename}`);
860
+ throw new Error(`Cannot find source path for entry: ${entry.filename}`);
861
+ }
862
+ try {
863
+ // Read file data into memory
864
+ let fileData = fs.readFileSync(absPath);
865
+ // Apply line ending conversion if requested and file is a text file
866
+ if ((this.options.toCrlf || this.options.fromCrlf) && (0, utils_1.isTextFile)(absPath)) {
867
+ const originalSize = fileData.length;
868
+ const convertedBuf = (0, utils_1.convertLineEndings)(fileData, this.options);
869
+ fileData = Buffer.from(convertedBuf);
870
+ if (this.options.verbose) {
871
+ (0, utils_1.log)(`📝 Converted line endings: ${entry.filename} (${originalSize} -> ${fileData.length} bytes)`, this.options);
872
+ }
873
+ }
874
+ // Step 1: Get header position BEFORE creating header (current file position)
875
+ const headerPosition = writer.getPosition();
876
+ entry.localHdrOffset = headerPosition;
877
+ // Step 2: Set compression method BEFORE creating header
878
+ // Determine compression method based on options
879
+ if (compressionOptions.level === 0) {
880
+ entry.cmpMethod = 0; // STORED
881
+ }
882
+ else if (compressionOptions.useZstd) {
883
+ entry.cmpMethod = 93; // ZSTD
884
+ }
885
+ else {
886
+ entry.cmpMethod = 8; // DEFLATED
887
+ }
888
+ // Step 3: Create local header with placeholder compressed size (0)
889
+ entry.compressedSize = 0; // Placeholder - will be updated after compression
890
+ const localHeader = entry.createLocalHdr();
891
+ // Step 4: Write local header FIRST (with placeholder compressed size)
892
+ await writer.writeHeader(localHeader);
893
+ // Track entry position for central directory (position where header was written)
894
+ // Note: For in-memory mode, positions are tracked in bufferWriter, not zipWriter
895
+ if (this.zipWriter) {
896
+ this.zipWriter.entryPositions.set(entry.filename || '', headerPosition);
897
+ }
898
+ // Step 5: Compress data (returns ONLY compressed data, no header)
899
+ const compressedData = await zipkit.compressFileBuffer(entry, fileData, compressionOptions);
900
+ // Update entry compressed size for statistics and central directory
901
+ entry.compressedSize = compressedData.length;
902
+ // Step 6: Write compressed data
903
+ await writer.writeData(compressedData);
904
+ // Step 7: Update CRC and compressed size in local header
905
+ await writer.updateHeader(headerPosition, compressedData.length, entry.crc);
906
+ // Update statistics
907
+ this.totalIn += entry.uncompressedSize || 0;
908
+ this.totalOut += entry.compressedSize || 0;
909
+ this.filesCount += 1;
910
+ // Accumulate SHA-256 hash as entries are added (for merkle root calculation)
911
+ if (this.hashAccumulator && entry.sha256) {
912
+ const filename = entry.filename || '';
913
+ // Skip blockchain metadata files
914
+ if (filename !== neozipkit_1.TOKENIZED_METADATA &&
915
+ filename !== neozipkit_1.TIMESTAMP_METADATA &&
916
+ filename !== neozipkit_1.TIMESTAMP_SUBMITTED) {
917
+ const hashBuffer = Buffer.from(entry.sha256, 'hex');
918
+ this.hashAccumulator.addHash(hashBuffer);
919
+ if (this.options.verbose) {
920
+ (0, utils_1.logDebug)(`Added SHA-256 hash to accumulator for ${filename}`, this.options);
921
+ }
922
+ }
923
+ }
924
+ // Entry is already in zip.zipEntries[] from processFiles(), updated with compression properties
925
+ if (this.options.verbose) {
926
+ const ratio = entry.uncompressedSize > 0 ? Math.round((1 - entry.compressedSize / entry.uncompressedSize) * 100) : 0;
927
+ (0, utils_1.log)(`📦 Compressed: ${entry.filename} (${(0, utils_1.formatBytes)(entry.uncompressedSize)} -> ${(0, utils_1.formatBytes)(entry.compressedSize)} bytes, ${ratio}%)`, this.options);
928
+ }
929
+ }
930
+ catch (error) {
931
+ (0, utils_1.logError)(`❌ Failed to compress file ${entry.filename}: ${error instanceof Error ? error.message : String(error)}`);
932
+ throw error;
933
+ }
934
+ }
935
+ // For non-blockchain mode, keep buffer open for metadata addition
936
+ // Metadata will be added before central directory in handleTokenization() and finalize()
937
+ if (!writeToFile) {
938
+ // Store buffer writer for later use (metadata addition and finalization)
939
+ // For in-memory non-blockchain mode, we'll write the complete ZIP file in finalize()
940
+ // after metadata is added
941
+ this.zip.bufferWriter = writer;
942
+ this.currentPosition = writer.getPosition();
943
+ }
944
+ else {
945
+ // For blockchain mode, update currentPosition from FileOutputWriter
946
+ // The FileOutputWriter updates positionRef.current as it writes
947
+ // Sync this.currentPosition with the actual file position
948
+ this.currentPosition = positionRef.current;
949
+ // Note: Do NOT close the stream here - it needs to remain open
950
+ // so that metadata can be added and finalize() can write central directory
951
+ if (this.options.verbose) {
952
+ (0, utils_1.log)(`📊 Compression completed at file position: ${this.currentPosition}`, this.options);
953
+ }
954
+ }
955
+ return writer.getPosition();
956
+ }
957
+ else {
958
+ // File-based mode: use ZipkitNode's writeZipEntry() method
959
+ // Get entries to compress from zip.getDirectory() (populated by processFiles())
960
+ const zipEntries = this.zip.getDirectory().length > 0 ? this.zip.getDirectory() : await this.zip.getDirectory() || [];
961
+ if (zipEntries.length === 0) {
962
+ (0, utils_1.logError)('❌ No files to compress');
963
+ (0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.NO_FILES_MATCHED);
964
+ }
965
+ if (!this.zipWriter) {
966
+ throw new Error('ZIP writer not initialized for file-based mode');
967
+ }
968
+ const cmpOptions = {
969
+ ...compressionOptions,
970
+ bufferSize: this.options.blockSize || 512 * 1024 // Default 512KB buffer
971
+ };
972
+ if (this.options.verbose) {
973
+ (0, utils_1.log)(`🔧 Using file-based compression with ${(0, utils_1.formatBytes)(cmpOptions.bufferSize)} buffer`, this.options);
974
+ }
975
+ try {
976
+ // Loop through entries and process each one
977
+ let entryCnt = zipEntries.length;
978
+ for (let index = 0; index < entryCnt; index++) {
979
+ const entry = zipEntries[index];
980
+ // Filter out metadata entries (handled separately in handleTokenization)
981
+ if (entry.filename === neozipkit_1.TOKENIZED_METADATA ||
982
+ entry.filename === neozipkit_1.TIMESTAMP_METADATA ||
983
+ entry.filename === neozipkit_1.TIMESTAMP_SUBMITTED) {
984
+ zipEntries.splice(index, 1);
985
+ entryCnt--;
986
+ index--;
987
+ if (this.options.verbose) {
988
+ (0, utils_1.log)(`Deleting Metadata entry: ${entry.filename}`, this.options);
989
+ }
990
+ continue;
991
+ }
992
+ // Handle non-updated entries: copy from existing ZIP
993
+ if (!entry.isUpdated) {
994
+ if (this.options.verbose) {
995
+ (0, utils_1.log)(`Copying Entry: ${entry.filename}`, this.options);
996
+ }
997
+ entry.localHdrOffset = this.zipWriter.currentPosition;
998
+ const inData = await this.zip.copyEntry(entry);
999
+ await this.writeChunk(inData);
1000
+ // Update entry position tracking
1001
+ this.zipWriter.entryPositions.set(entry.filename || '', entry.localHdrOffset);
1002
+ // Update statistics
1003
+ this.totalIn += entry.uncompressedSize || 0;
1004
+ this.totalOut += entry.compressedSize || entry.uncompressedSize || 0;
1005
+ this.filesCount += 1;
1006
+ if (this.options.verbose) {
1007
+ const actionPath = this.entryToDisplay.get(entry.filename) || entry.filename;
1008
+ (0, utils_1.log)(`adding: ${actionPath} (copied, no recompression)`, this.options);
1009
+ }
1010
+ continue;
1011
+ }
1012
+ // Handle updated entries: use writeZipEntry() for compression
1013
+ const filePath = this.entryToPath.get(entry.filename);
1014
+ if (!filePath) {
1015
+ throw new Error(`Cannot find source path for entry: ${entry.filename}`);
1016
+ }
1017
+ // Check for pre-loaded buffer (symlinks, hardlinks, line-ending conversion)
1018
+ // If we have a pre-loaded buffer, we need to compress it directly
1019
+ let fileBuffer = null;
1020
+ if (entry.fileBuffer) {
1021
+ fileBuffer = entry.fileBuffer;
1022
+ }
1023
+ // Use writeZipEntry() for regular file compression
1024
+ // For pre-loaded buffers, we'll need to handle them specially
1025
+ if (fileBuffer) {
1026
+ // For pre-loaded buffers, compress in memory and write manually
1027
+ // This handles symlinks, hardlinks, and line-ending conversion
1028
+ const zipkit = new node_1.default();
1029
+ const cmpData = await zipkit.compressData(entry, fileBuffer, cmpOptions);
1030
+ // Write header manually
1031
+ entry.compressedSize = 0; // Placeholder
1032
+ entry.localHdrOffset = this.zipWriter.currentPosition;
1033
+ const localHdr = entry.createLocalHdr();
1034
+ await this.writeChunk(localHdr);
1035
+ this.zipWriter.entryPositions.set(entry.filename || '', entry.localHdrOffset);
1036
+ // Write compressed data
1037
+ await this.writeChunk(cmpData);
1038
+ // Update header with actual compressed size
1039
+ entry.compressedSize = cmpData.length;
1040
+ const compressedSizeOffset = entry.localHdrOffset + 18;
1041
+ const sizeBuffer = Buffer.alloc(4);
1042
+ sizeBuffer.writeUInt32LE(entry.compressedSize, 0);
1043
+ fs.writeSync(this.zipWriter.outputFd, sizeBuffer, 0, 4, compressedSizeOffset);
1044
+ if (entry.crc !== undefined) {
1045
+ const crcOffset = entry.localHdrOffset + 14;
1046
+ const crcBuffer = Buffer.alloc(4);
1047
+ crcBuffer.writeUInt32LE(entry.crc, 0);
1048
+ fs.writeSync(this.zipWriter.outputFd, crcBuffer, 0, 4, crcOffset);
1049
+ }
1050
+ }
1051
+ else {
1052
+ // Use writeZipEntry() for regular file compression
1053
+ await this.zip.writeZipEntry(this.zipWriter, entry, filePath, cmpOptions, {
1054
+ onProgress: (entry, bytes) => {
1055
+ // Progress tracking if needed
1056
+ },
1057
+ onHashCalculated: (entry, hash) => {
1058
+ // Accumulate SHA-256 hash for blockchain
1059
+ if (this.hashAccumulator) {
1060
+ const filename = entry.filename || '';
1061
+ // Skip blockchain metadata files
1062
+ if (filename !== neozipkit_1.TOKENIZED_METADATA &&
1063
+ filename !== neozipkit_1.TIMESTAMP_METADATA &&
1064
+ filename !== neozipkit_1.TIMESTAMP_SUBMITTED) {
1065
+ this.hashAccumulator.addHash(hash);
1066
+ if (this.options.verbose) {
1067
+ (0, utils_1.logDebug)(`Added SHA-256 hash to accumulator for ${filename}`, this.options);
1068
+ }
1069
+ }
1070
+ }
1071
+ }
1072
+ });
1073
+ }
1074
+ // Update currentPosition from zipWriter
1075
+ this.currentPosition = this.zipWriter.currentPosition;
1076
+ // Update statistics
1077
+ this.totalIn += entry.uncompressedSize || 0;
1078
+ this.totalOut += entry.compressedSize || 0;
1079
+ this.filesCount += 1;
1080
+ // Log progress
1081
+ const compressed = entry.uncompressedSize && entry.uncompressedSize > 0
1082
+ ? Math.max(0, Math.round((1 - (entry.compressedSize / entry.uncompressedSize)) * 100))
1083
+ : 0;
1084
+ const method = entry.cmpMethod === 0 ? 'stored' : (entry.cmpMethod === 93 ? 'zstd' : 'deflated');
1085
+ const actionPath = this.entryToDisplay.get(entry.filename) || entry.filename;
1086
+ (0, utils_1.log)(`adding: ${actionPath} (${method} ${compressed}%)`, this.options);
1087
+ }
1088
+ }
1089
+ finally {
1090
+ // Always cleanup file descriptors
1091
+ await this.cleanupFileDescriptors();
1092
+ }
1093
+ if (this.options.verbose) {
1094
+ (0, utils_1.log)(`📊 Compression completed at file position: ${this.currentPosition}`, this.options);
1095
+ }
1096
+ // Return the current file position
1097
+ return this.currentPosition;
1098
+ }
1099
+ }
1100
+ async addTokenMetadataDirectly(tokenMeta) {
1101
+ // Create token metadata JSON
1102
+ const tokenContent = JSON.stringify(tokenMeta, null, 2);
1103
+ const tokenBuffer = Buffer.from(tokenContent, 'utf8');
1104
+ const tokenPath = 'META-INF/NZIP.TOKEN';
1105
+ console.log('✅ Token metadata prepared for ZIP');
1106
+ console.log(`📄 Token content:\n${tokenContent}`);
1107
+ // Create local file header for token metadata
1108
+ const tokenEntry = this.zip.createZipEntry(tokenPath);
1109
+ tokenEntry.timeDateDOS = tokenEntry.setDateTime(new Date());
1110
+ tokenEntry.compressedSize = tokenBuffer.length;
1111
+ tokenEntry.uncompressedSize = tokenBuffer.length;
1112
+ tokenEntry.cmpMethod = 0; // STORED (no compression for small metadata file)
1113
+ tokenEntry.crc = (0, neozipkit_1.crc32)(tokenBuffer);
1114
+ // Use current file position for local header offset (not compressionOffset)
1115
+ tokenEntry.localHdrOffset = this.currentPosition;
1116
+ // Track entry position for central directory
1117
+ if (this.zipWriter) {
1118
+ this.zipWriter.entryPositions.set(tokenPath, this.currentPosition);
1119
+ }
1120
+ // Create local header
1121
+ const localHdr = tokenEntry.createLocalHdr();
1122
+ // Write token metadata directly to file (at the end, just before central directory)
1123
+ await this.writeChunk(localHdr);
1124
+ await this.writeChunk(tokenBuffer);
1125
+ // Token entry is automatically added to zip.zipEntries[] by createZipEntry()
1126
+ // Since it's added last, it will appear last in the central directory
1127
+ console.log(`✅ Token metadata added to ZIP (${tokenBuffer.length} bytes)`);
1128
+ return tokenEntry;
1129
+ }
1130
+ /**
1131
+ * Calculate merkle root from ZIP entries with SHA-256 hashes
1132
+ * Excludes blockchain metadata files (META-INF/*) to ensure consistent calculation
1133
+ */
1134
+ calculateMerkleRootFromEntries(entries) {
1135
+ if (!entries || entries.length === 0) {
1136
+ return null;
1137
+ }
1138
+ const hashAccumulator = new neozipkit_1.HashCalculator({ enableAccumulation: true });
1139
+ // Filter out blockchain metadata files to ensure consistent Merkle Root calculation
1140
+ const contentEntries = entries.filter(entry => {
1141
+ const filename = entry.filename || '';
1142
+ return filename !== neozipkit_1.TOKENIZED_METADATA &&
1143
+ filename !== neozipkit_1.TIMESTAMP_METADATA &&
1144
+ filename !== neozipkit_1.TIMESTAMP_SUBMITTED;
1145
+ });
1146
+ // Add SHA-256 hashes to accumulator
1147
+ for (const entry of contentEntries) {
1148
+ if (entry.sha256) {
1149
+ // Convert hex string to Buffer
1150
+ const hashBuffer = Buffer.from(entry.sha256, 'hex');
1151
+ hashAccumulator.addHash(hashBuffer);
1152
+ }
1153
+ }
1154
+ const merkleRoot = hashAccumulator.merkleRoot();
1155
+ return merkleRoot;
1156
+ }
1157
+ async handleTokenization() {
1158
+ let tokenMeta = null;
1159
+ let tokenInfoEntry = null;
1160
+ let otsMetaEntry = null;
1161
+ let merkleRoot = null;
1162
+ if (this.options.blockchain) {
1163
+ // Check for wallet passkey AFTER compression is complete
1164
+ // Priority: -w option > wallet.json > environment variable
1165
+ // Don't show error yet - we'll prompt user if key is missing
1166
+ const walletPasskey = (0, blockchain_2.getWalletPasskey)(this.options.walletKey, false);
1167
+ if (!walletPasskey) {
1168
+ // Show error message and prompt user
1169
+ console.error('\n❌ Error: Wallet private key is required for blockchain operations');
1170
+ console.error('');
1171
+ console.error(' Option 1 (Recommended): Run interactive setup');
1172
+ console.error(' $ neozip init');
1173
+ console.error('');
1174
+ console.error(' Option 2: Use command-line flag');
1175
+ console.error(' $ neozip -b -w 0x... <archive> <files>');
1176
+ console.error('');
1177
+ console.error(' Option 3: Set environment variable');
1178
+ console.error(' $ export NEOZIP_WALLET_PASSKEY="0x..."');
1179
+ console.error('');
1180
+ console.error(' Get testnet ETH from: https://www.coinbase.com/faucets/base-ethereum-sepolia-faucet');
1181
+ console.error('');
1182
+ // Prompt user to continue without blockchain tokenization
1183
+ if (!this.options.nonInteractive) {
1184
+ const answer = await (0, user_interaction_1.promptUser)('Continue without blockchain tokenization? (y/n): ');
1185
+ if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
1186
+ (0, utils_1.log)('⚠️ Continuing without blockchain tokenization (ZIP file is already created)...', this.options);
1187
+ this.options.blockchain = false;
1188
+ this.options.blockchainOts = false;
1189
+ return; // Skip tokenization, ZIP is already complete
1190
+ }
1191
+ else {
1192
+ (0, utils_1.logError)('💡 Tip: Run "neozip init" to set up your wallet configuration');
1193
+ (0, utils_1.logError)('💡 Tip: Omit blockchain flags to create ZIP without blockchain operations');
1194
+ (0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.PARAMETER_ERROR);
1195
+ }
1196
+ }
1197
+ else {
1198
+ // Non-interactive mode - exit with error
1199
+ (0, utils_1.logError)('💡 Tip: Run "neozip init" to set up your wallet configuration');
1200
+ (0, utils_1.logError)('💡 Tip: Omit blockchain flags to create ZIP without blockchain operations');
1201
+ (0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.PARAMETER_ERROR);
1202
+ }
1203
+ }
1204
+ else {
1205
+ // Wallet key is available, store it for use
1206
+ this.options.walletPasskey = walletPasskey;
1207
+ }
1208
+ if (this.options.debug) {
1209
+ (0, utils_1.logDebug)('Tokenization process starting...', this.options);
1210
+ }
1211
+ // Calculate merkle root from accumulated hashes (if available) or from entries
1212
+ if (this.hashAccumulator && this.hashAccumulator.leafCount() > 0) {
1213
+ // Use accumulated hashes (calculated incrementally during compression)
1214
+ merkleRoot = this.hashAccumulator.merkleRoot();
1215
+ if (this.options.verbose) {
1216
+ console.log(`✅ Calculated merkle root from ${this.hashAccumulator.leafCount()} accumulated SHA-256 hashes`);
1217
+ }
1218
+ }
1219
+ else {
1220
+ // Fallback: Calculate from entries (for updates or in-memory mode)
1221
+ let entriesForMerkle = [];
1222
+ const zipEntries = this.zip.getDirectory();
1223
+ if (zipEntries.length > 0) {
1224
+ // New ZIP creation (file-based) - use entries we created (they have SHA-256 hashes from compression)
1225
+ entriesForMerkle = zipEntries;
1226
+ }
1227
+ else {
1228
+ // Update mode OR in-memory mode - load ZIP file and get entries
1229
+ // For in-memory mode, the ZIP file was already written to disk by createZipInMemory()
1230
+ try {
1231
+ if (fs.existsSync(this.archiveName)) {
1232
+ // Load the ZIP file to get entries with SHA-256 hashes
1233
+ const zipkit = new node_1.default();
1234
+ if (this.options.inMemory) {
1235
+ // For in-memory mode, load as buffer
1236
+ const zipBuffer = fs.readFileSync(this.archiveName);
1237
+ zipkit.loadZip(zipBuffer);
1238
+ }
1239
+ else {
1240
+ // For file-based mode
1241
+ await zipkit.loadZipFile(this.archiveName);
1242
+ }
1243
+ entriesForMerkle = zipkit.getDirectory() || [];
1244
+ }
1245
+ else {
1246
+ // Update mode - get entries from loaded ZIP
1247
+ entriesForMerkle = await this.zip.getDirectory() || [];
1248
+ }
1249
+ }
1250
+ catch (error) {
1251
+ console.error('❌ Error loading ZIP file for merkle root calculation:', error instanceof Error ? error.message : String(error));
1252
+ // Fallback to getting entries from this.zip
1253
+ entriesForMerkle = await this.zip.getDirectory() || [];
1254
+ }
1255
+ }
1256
+ // Calculate merkle root from entries
1257
+ merkleRoot = this.calculateMerkleRootFromEntries(entriesForMerkle);
1258
+ }
1259
+ if (!merkleRoot) {
1260
+ console.error('❌ Error: Could not calculate merkle root for tokenization');
1261
+ console.error(' This is required for blockchain operations.');
1262
+ console.error(' Make sure files have been compressed with SHA-256 hashes.');
1263
+ if (this.options.verbose) {
1264
+ const hashCount = this.hashAccumulator ? this.hashAccumulator.leafCount() : 0;
1265
+ console.error(` Found ${hashCount} accumulated SHA-256 hashes`);
1266
+ }
1267
+ (0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.BAD_ARCHIVE_FORMAT);
1268
+ }
1269
+ if (this.options.verbose && this.hashAccumulator) {
1270
+ console.log(`✅ Calculated merkle root from ${this.hashAccumulator.leafCount()} accumulated SHA-256 hashes`);
1271
+ }
1272
+ if (merkleRoot) {
1273
+ console.log(`\n📋 Merkle Root: ${merkleRoot}`);
1274
+ // Validate network
1275
+ const network = this.options.network || 'base-sepolia';
1276
+ // Always display network when tokenization is enabled
1277
+ console.log(`🌐 Network: ${network}`);
1278
+ // Get network config for contract address and version info
1279
+ const networkConfig = (0, blockchain_1.getNetworkByName)(network);
1280
+ let contractAddress;
1281
+ let networkChainId;
1282
+ let contractVersion;
1283
+ if (networkConfig) {
1284
+ contractAddress = networkConfig.address;
1285
+ networkChainId = networkConfig.chainId;
1286
+ // Determine contract version: v2.11+ has version in config, prior versions are "unknown"
1287
+ const versionStr = networkConfig.version || '0';
1288
+ const versionNum = parseFloat(versionStr);
1289
+ contractVersion = versionNum >= 2.11 ? versionStr : 'unknown';
1290
+ }
1291
+ else {
1292
+ // Fallback to default (Base Sepolia)
1293
+ const defaultConfig = (0, blockchain_1.getContractConfig)(84532);
1294
+ contractAddress = defaultConfig.address;
1295
+ networkChainId = defaultConfig.chainId;
1296
+ const versionStr = defaultConfig.version || '0';
1297
+ const versionNum = parseFloat(versionStr);
1298
+ contractVersion = versionNum >= 2.11 ? versionStr : 'unknown';
1299
+ }
1300
+ // Display contract address and version right after network
1301
+ console.log(`📄 Contract: ${contractAddress}`);
1302
+ console.log(`🔢 Version: ${contractVersion}`);
1303
+ if (!(0, blockchain_2.isSupportedNetwork)(network)) {
1304
+ const supportedNetworks = (0, blockchain_1.getSupportedNetworkNames)();
1305
+ console.error(`❌ Error: Unsupported network: ${network}`);
1306
+ console.error(` Currently supported networks: ${supportedNetworks.join(', ')}`);
1307
+ (0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.BLOCKCHAIN_CONFIG_ERROR);
1308
+ }
1309
+ let minter = null;
1310
+ try {
1311
+ // Initialize blockchain minter
1312
+ minter = new neozipkit_1.ZipkitMinter(merkleRoot, {
1313
+ network: network,
1314
+ walletPrivateKey: this.options.walletPasskey,
1315
+ verbose: this.options.verbose,
1316
+ debug: this.options.verbose || this.options.debug || process.env.NEOZIP_DEBUG === 'true' // Enable debug output
1317
+ });
1318
+ // Start the minting process (checks duplicates, gets wallet info, estimates gas)
1319
+ console.log('🔗 Connecting to blockchain network...');
1320
+ const processResult = await Promise.race([
1321
+ minter.processMinting(),
1322
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Network connection timeout after 30 seconds. The RPC endpoint may be slow or unreachable.')), 30000))
1323
+ ]);
1324
+ console.log(`💳 Wallet: ${processResult.walletInfo.address}`);
1325
+ console.log(`💰 Balance: ${processResult.walletInfo.balance} ETH`);
1326
+ // Network config already retrieved above (contractAddress and networkChainId are available)
1327
+ // Check if tokens already exist for this merkle root
1328
+ if (processResult.duplicateCheck.hasExistingTokens) {
1329
+ let userChoice;
1330
+ if (this.options.blockchainMint) {
1331
+ // Force mint new token
1332
+ console.log('\n🔨 Force minting new token (--blockchain-mint)');
1333
+ userChoice = { action: 'mint-new' };
1334
+ }
1335
+ else if (this.options.blockchainDefault) {
1336
+ // Auto-select default option
1337
+ if (processResult.duplicateCheck.userOwnedTokens.length > 0) {
1338
+ // Use the earliest token owned by user
1339
+ const earliestToken = processResult.duplicateCheck.userOwnedTokens[0];
1340
+ console.log(`\n✅ Auto-selecting existing token ID ${earliestToken.tokenId} (--blockchain-default)`);
1341
+ userChoice = { action: 'use-existing', selectedToken: earliestToken };
1342
+ }
1343
+ else {
1344
+ // No user-owned tokens, mint new
1345
+ console.log('\n🔨 No user-owned tokens found, minting new token (--blockchain-default)');
1346
+ userChoice = { action: 'mint-new' };
1347
+ }
1348
+ }
1349
+ else {
1350
+ // Present user with choices (interactive mode)
1351
+ userChoice = await (0, user_interaction_1.getUserTokenChoice)(processResult.duplicateCheck, {
1352
+ contractAddress,
1353
+ version: contractVersion
1354
+ });
1355
+ }
1356
+ if (userChoice.action === 'use-existing' && userChoice.selectedToken) {
1357
+ console.log(`\n✅ Using existing token ID ${userChoice.selectedToken.tokenId}`);
1358
+ // Create TokenMetadata from selected existing token
1359
+ // Use a fixed timestamp for existing tokens to ensure consistency
1360
+ // Network config already retrieved above
1361
+ tokenMeta = {
1362
+ contractVersion: contractVersion,
1363
+ tokenId: userChoice.selectedToken.tokenId,
1364
+ contractAddress: contractAddress,
1365
+ network: processResult.walletInfo.networkName,
1366
+ networkChainId: networkChainId,
1367
+ transactionHash: userChoice.selectedToken.tokenData?.transactionHash || '',
1368
+ merkleRoot: merkleRoot,
1369
+ encryptedHash: undefined, // Currently unused
1370
+ mintedAt: '2025-01-01T00:00:00.000Z' // Fixed timestamp for existing tokens
1371
+ };
1372
+ // Add token metadata to ZIP (after compression, at the end)
1373
+ console.log('\n📄 Adding token metadata to ZIP...');
1374
+ if (tokenMeta) {
1375
+ tokenInfoEntry = await this.addTokenMetadataDirectly(tokenMeta);
1376
+ // Token entry is automatically added to zip.zipEntries[] by addTokenMetadataDirectly() -> createZipEntry()
1377
+ }
1378
+ // Cleanup provider to allow process to exit
1379
+ minter.destroy();
1380
+ }
1381
+ else if (userChoice.action === 'mint-new') {
1382
+ // Mint new token
1383
+ const mintResult = await (0, blockchain_2.handleTokenMinting)(minter, this.zip, this.options.nonInteractive || false, 0);
1384
+ if (mintResult.success && mintResult.tokenInfo) {
1385
+ tokenMeta = mintResult.tokenInfo;
1386
+ // Add token metadata directly to ZIP (after compression, at the end)
1387
+ console.log('\n📄 Adding token metadata to ZIP...');
1388
+ tokenInfoEntry = await this.addTokenMetadataDirectly(tokenMeta);
1389
+ // Token entry is automatically added to zip.zipEntries[] by addTokenMetadataDirectly() -> createZipEntry()
1390
+ }
1391
+ else {
1392
+ console.error('❌ Failed to mint new token');
1393
+ console.error('⚠️ Continuing with ZIP creation without token metadata...');
1394
+ tokenMeta = null;
1395
+ tokenInfoEntry = null;
1396
+ }
1397
+ // Cleanup provider to allow process to exit
1398
+ minter.destroy();
1399
+ }
1400
+ else if (userChoice.action === 'skip-tokenization') {
1401
+ // Skip tokenization - create ZIP without token metadata
1402
+ console.log('\n⏭️ Skipping tokenization - creating ZIP without token metadata');
1403
+ tokenMeta = null;
1404
+ tokenInfoEntry = null;
1405
+ // Cleanup provider to allow process to exit
1406
+ if (minter) {
1407
+ minter.destroy();
1408
+ }
1409
+ }
1410
+ else {
1411
+ console.log('❌ Operation cancelled by user');
1412
+ if (minter) {
1413
+ minter.destroy();
1414
+ }
1415
+ (0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.TOKEN_MINT_USER_CANCELLED);
1416
+ }
1417
+ }
1418
+ else {
1419
+ // No existing tokens, mint new one
1420
+ const mintResult = await (0, blockchain_2.handleTokenMinting)(minter, this.zip, this.options.nonInteractive || false, 0);
1421
+ if (mintResult.success && mintResult.tokenInfo) {
1422
+ tokenMeta = mintResult.tokenInfo;
1423
+ // Add token metadata directly to ZIP (after compression, at the end)
1424
+ console.log('\n📄 Adding token metadata to ZIP...');
1425
+ tokenInfoEntry = await this.addTokenMetadataDirectly(tokenMeta);
1426
+ // Token entry is automatically added to zip.zipEntries[] by addTokenMetadataDirectly() -> createZipEntry()
1427
+ }
1428
+ else {
1429
+ console.error('❌ Failed to mint new token');
1430
+ console.error('⚠️ Continuing with ZIP creation without token metadata...');
1431
+ tokenMeta = null;
1432
+ tokenInfoEntry = null;
1433
+ }
1434
+ // Cleanup provider to allow process to exit
1435
+ minter.destroy();
1436
+ }
1437
+ }
1438
+ catch (error) {
1439
+ const errorMessage = error instanceof Error ? error.message : String(error);
1440
+ console.error(`❌ Blockchain error: ${errorMessage}`);
1441
+ // Provide helpful guidance for timeout errors
1442
+ if (errorMessage.includes('timeout')) {
1443
+ console.error('');
1444
+ console.error('💡 Troubleshooting tips:');
1445
+ console.error(' 1. Check your internet connection');
1446
+ console.error(' 2. The RPC endpoint may be slow or unreachable');
1447
+ console.error(' 3. Try using a different network or RPC endpoint');
1448
+ console.error(' 4. Use --verbose or --debug for more details');
1449
+ console.error('');
1450
+ }
1451
+ console.error('⚠️ Continuing with ZIP creation without token metadata...');
1452
+ tokenMeta = null;
1453
+ tokenInfoEntry = null;
1454
+ }
1455
+ finally {
1456
+ // Always cleanup provider to allow process to exit, even on error
1457
+ // Note: Some paths already call destroy() above, but this ensures cleanup on all paths
1458
+ if (minter) {
1459
+ try {
1460
+ minter.destroy();
1461
+ }
1462
+ catch (error) {
1463
+ // Ignore errors during cleanup
1464
+ }
1465
+ }
1466
+ }
1467
+ }
1468
+ }
1469
+ else if (this.options.blockchainOts) {
1470
+ // Handle OpenTimestamp for Bitcoin blockchain
1471
+ if (this.options.debug) {
1472
+ (0, utils_1.logDebug)('OpenTimestamp process starting...', this.options);
1473
+ }
1474
+ console.log('🔗 OpenTimestamp: Creating timestamp proof on Bitcoin blockchain...');
1475
+ // Calculate merkle root for OpenTimestamp from accumulated hashes (if available) or from entries
1476
+ let merkleRoot = null;
1477
+ if (this.hashAccumulator && this.hashAccumulator.leafCount() > 0) {
1478
+ // Use accumulated hashes (calculated incrementally during compression)
1479
+ merkleRoot = this.hashAccumulator.merkleRoot();
1480
+ if (this.options.verbose) {
1481
+ console.log(`✅ Calculated merkle root from ${this.hashAccumulator.leafCount()} accumulated SHA-256 hashes`);
1482
+ }
1483
+ }
1484
+ else {
1485
+ // Fallback: Calculate from entries (for updates or in-memory mode)
1486
+ let entriesForMerkle = [];
1487
+ const zipEntries = this.zip.getDirectory();
1488
+ if (zipEntries.length > 0) {
1489
+ // New ZIP creation (file-based) - use entries we created (they have SHA-256 hashes from compression)
1490
+ entriesForMerkle = zipEntries;
1491
+ }
1492
+ else {
1493
+ // Update mode OR in-memory mode - load ZIP file and get entries
1494
+ // For in-memory mode, the ZIP file was already written to disk by createZipInMemory()
1495
+ try {
1496
+ if (fs.existsSync(this.archiveName)) {
1497
+ // Load the ZIP file to get entries with SHA-256 hashes
1498
+ const zipkit = new node_1.default();
1499
+ if (this.options.inMemory) {
1500
+ // For in-memory mode, load as buffer
1501
+ const zipBuffer = fs.readFileSync(this.archiveName);
1502
+ zipkit.loadZip(zipBuffer);
1503
+ }
1504
+ else {
1505
+ // For file-based mode
1506
+ await zipkit.loadZipFile(this.archiveName);
1507
+ }
1508
+ entriesForMerkle = zipkit.getDirectory() || [];
1509
+ }
1510
+ else {
1511
+ // Update mode - get entries from loaded ZIP
1512
+ entriesForMerkle = await this.zip.getDirectory() || [];
1513
+ }
1514
+ }
1515
+ catch (error) {
1516
+ console.error('❌ Error loading ZIP file for merkle root calculation:', error instanceof Error ? error.message : String(error));
1517
+ // Fallback to getting entries from this.zip
1518
+ entriesForMerkle = await this.zip.getDirectory() || [];
1519
+ }
1520
+ }
1521
+ // Calculate merkle root from entries
1522
+ merkleRoot = this.calculateMerkleRootFromEntries(entriesForMerkle);
1523
+ }
1524
+ if (!merkleRoot) {
1525
+ console.error('❌ Error calculating merkle root for OpenTimestamp');
1526
+ console.error(' Make sure files have been compressed with SHA-256 hashes.');
1527
+ if (this.options.verbose) {
1528
+ const hashCount = this.hashAccumulator ? this.hashAccumulator.leafCount() : 0;
1529
+ console.error(` Found ${hashCount} accumulated SHA-256 hashes`);
1530
+ }
1531
+ (0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.BAD_ARCHIVE_FORMAT);
1532
+ }
1533
+ if (this.options.verbose && this.hashAccumulator) {
1534
+ console.log(`✅ Calculated merkle root from ${this.hashAccumulator.leafCount()} accumulated SHA-256 hashes`);
1535
+ }
1536
+ if (merkleRoot) {
1537
+ console.log(`📋 Merkle Root: ${merkleRoot}`);
1538
+ // Create actual OpenTimestamp proof
1539
+ try {
1540
+ const otsProof = await (0, neozipkit_1.createTimestamp)(merkleRoot, { debug: this.options.verbose });
1541
+ if (!otsProof) {
1542
+ throw new Error('Failed to create OpenTimestamp proof');
1543
+ }
1544
+ console.log('📄 OpenTimestamp metadata prepared');
1545
+ // Use the createOtsMetadataEntry function like in the example
1546
+ const metaEntry = (0, blockchain_1.createOtsMetadataEntry)(this.zip, otsProof);
1547
+ if (metaEntry) {
1548
+ // Ensure the filename is set correctly
1549
+ metaEntry.filename = neozipkit_1.TIMESTAMP_SUBMITTED;
1550
+ // Ensure OTS entry uses STORED compression (no compression for small metadata file)
1551
+ metaEntry.cmpMethod = 0; // STORED
1552
+ metaEntry.compressedSize = metaEntry.fileBuffer?.length || metaEntry.uncompressedSize || 0;
1553
+ if (this.options.verbose) {
1554
+ console.log(`📂 Final timestamp entry: ${metaEntry.filename}`);
1555
+ console.log(`📏 Final timestamp entry size: ${metaEntry.fileBuffer?.length || metaEntry.uncompressedSize || 0} bytes`);
1556
+ }
1557
+ console.log('✅ OpenTimestamp proof created and queued for inclusion');
1558
+ // Use current file position for local header offset
1559
+ metaEntry.localHdrOffset = this.currentPosition;
1560
+ // Record the position for this entry
1561
+ if (this.zipWriter) {
1562
+ this.zipWriter.entryPositions.set(metaEntry.filename, this.currentPosition);
1563
+ }
1564
+ // Create local header (must be done after setting all entry properties)
1565
+ const localHdr = metaEntry.createLocalHdr();
1566
+ if (this.options.verbose) {
1567
+ (0, utils_1.log)(`📝 Writing OTS entry: ${metaEntry.filename} at offset ${this.currentPosition}`, this.options);
1568
+ (0, utils_1.log)(`📊 OTS entry size: ${metaEntry.fileBuffer?.length || 0} bytes`, this.options);
1569
+ }
1570
+ // Write local header and file data
1571
+ await this.writeChunk(localHdr);
1572
+ if (metaEntry.fileBuffer) {
1573
+ await this.writeChunk(metaEntry.fileBuffer);
1574
+ }
1575
+ // OTS entry is automatically added to zip.zipEntries[] by createZipEntry()
1576
+ // Store the metadata entry
1577
+ otsMetaEntry = metaEntry;
1578
+ }
1579
+ else {
1580
+ console.warn('⚠️ Warning: Failed to create timestamp metadata entry');
1581
+ }
1582
+ }
1583
+ catch (error) {
1584
+ console.error('❌ Failed to create OpenTimestamp proof:', error instanceof Error ? error.message : String(error));
1585
+ throw error;
1586
+ }
1587
+ }
1588
+ }
1589
+ }
1590
+ async finalize() {
1591
+ // Check if this is in-memory non-blockchain mode (needs special handling)
1592
+ if (this.options.inMemory && !this.options.blockchain && !this.options.blockchainOts) {
1593
+ // For in-memory non-blockchain mode, write complete ZIP file from buffer
1594
+ // Metadata should already be in the buffer (added via writeChunk() in handleTokenization())
1595
+ const bufferWriter = this.zip.bufferWriter;
1596
+ if (bufferWriter) {
1597
+ const zipData = bufferWriter.getResult();
1598
+ if (zipData) {
1599
+ // Create central directory from zip.getDirectory() in the SAME order as data section
1600
+ // Files are written first during compression, then token metadata is added
1601
+ // So getDirectory() is already in the correct order (data section order)
1602
+ const zipEntries = this.zip.getDirectory();
1603
+ const centralDirChunks = [];
1604
+ for (const entry of zipEntries) {
1605
+ const centralDirEntry = entry.centralDirEntry();
1606
+ centralDirChunks.push(centralDirEntry);
1607
+ }
1608
+ const centralDir = Buffer.concat(centralDirChunks);
1609
+ // Create end of central directory record
1610
+ const endOfCentralDir = createEndOfCentralDirRecord(zipEntries.length, centralDir.length, zipData.length, this.options.archiveComment || '');
1611
+ // Combine everything: ZIP data (includes metadata) + Central Directory + End of Central Directory
1612
+ const completeZip = Buffer.concat([zipData, centralDir, endOfCentralDir]);
1613
+ // Write ZIP file to temporary file first (for atomic operation)
1614
+ if (!this.tempArchiveName) {
1615
+ throw new Error('Temp archive name not initialized');
1616
+ }
1617
+ fs.writeFileSync(this.tempArchiveName, completeZip);
1618
+ // Atomically move temp file to final location
1619
+ fs.renameSync(this.tempArchiveName, this.archiveName);
1620
+ // Apply old timestamp if requested
1621
+ if (this.options.oldTimestamp) {
1622
+ try {
1623
+ const oldestTime = await (0, file_operations_1.findOldestEntryTime)(this.zip);
1624
+ if (oldestTime) {
1625
+ fs.utimesSync(this.archiveName, oldestTime, oldestTime);
1626
+ if (this.options.verbose) {
1627
+ (0, utils_1.log)(`📅 Set archive timestamp to oldest entry: ${oldestTime.toISOString()}`, this.options);
1628
+ }
1629
+ }
1630
+ }
1631
+ catch (error) {
1632
+ if (this.options.verbose) {
1633
+ (0, utils_1.log)(`⚠️ Warning: Could not set old timestamp: ${error instanceof Error ? error.message : String(error)}`, this.options);
1634
+ }
1635
+ }
1636
+ }
1637
+ // Clean up temp file (should already be moved, but ensure it's gone)
1638
+ if (fs.existsSync(this.tempArchiveName)) {
1639
+ try {
1640
+ fs.unlinkSync(this.tempArchiveName);
1641
+ }
1642
+ catch {
1643
+ // Ignore cleanup errors
1644
+ }
1645
+ }
1646
+ // Done - no need to close stream for in-memory mode
1647
+ return;
1648
+ }
1649
+ }
1650
+ }
1651
+ // For file-based mode or in-memory blockchain mode, write central directory to file
1652
+ // Use ZipkitNode's writeCentralDirectory() and writeEndOfCentralDirectory() methods
1653
+ if (!this.zipWriter) {
1654
+ throw new Error('ZIP writer not initialized for finalization');
1655
+ }
1656
+ // Get entries - for new ZIPs, use zip.getDirectory(); for updates, use getDirectory()
1657
+ // IMPORTANT: getDirectory() contains entries in the EXACT order they were written to the ZIP file
1658
+ // Files are written first during compression, then token metadata is added at the end
1659
+ // So getDirectory() already has the correct order - use it as-is without reordering
1660
+ let zipEntries = [];
1661
+ const currentEntries = this.zip.getDirectory();
1662
+ if (currentEntries.length > 0) {
1663
+ // New ZIP creation - use getDirectory() in the exact order they were written
1664
+ // This matches the data section order: files first, then metadata last
1665
+ zipEntries = [...currentEntries];
1666
+ }
1667
+ else {
1668
+ // Update mode - get entries from loaded ZIP
1669
+ zipEntries = await this.zip.getDirectory() || [];
1670
+ }
1671
+ // Write central directory using ZipkitNode method
1672
+ const centralDirOffset = this.zipWriter.currentPosition;
1673
+ if (this.options.verbose) {
1674
+ (0, utils_1.log)(`📊 Central directory starts at offset: ${centralDirOffset}`, this.options);
1675
+ (0, utils_1.log)(`📁 Writing ${zipEntries.length} central directory entries`, this.options);
1676
+ }
1677
+ const centralDirSize = await this.zip.writeCentralDirectory(this.zipWriter, zipEntries, {
1678
+ archiveComment: this.options.archiveComment,
1679
+ onProgress: (entry) => {
1680
+ if (this.options.verbose) {
1681
+ (0, utils_1.log)(`📝 Writing central dir entry: ${entry.filename}`, this.options);
1682
+ (0, utils_1.log)(`📊 Entry localHdrOffset: ${entry.localHdrOffset}`, this.options);
1683
+ }
1684
+ }
1685
+ });
1686
+ // Update currentPosition from zipWriter
1687
+ this.currentPosition = this.zipWriter.currentPosition;
1688
+ // Write end of central directory using ZipkitNode method
1689
+ if (this.options.verbose) {
1690
+ (0, utils_1.log)(`📝 Writing end of central directory`, this.options);
1691
+ (0, utils_1.log)(`📊 Central directory size: ${centralDirSize} bytes`, this.options);
1692
+ (0, utils_1.log)(`📊 Total file size: ${this.currentPosition} bytes (before EOCD)`, this.options);
1693
+ }
1694
+ await this.zip.writeEndOfCentralDirectory(this.zipWriter, zipEntries.length, centralDirSize, centralDirOffset, this.options.archiveComment);
1695
+ // Update currentPosition from zipWriter
1696
+ this.currentPosition = this.zipWriter.currentPosition;
1697
+ // Close the output stream
1698
+ await this.closeOutput();
1699
+ // Atomically move temp file to final location
1700
+ if (!this.tempArchiveName) {
1701
+ throw new Error('Temp archive name not initialized');
1702
+ }
1703
+ if (!fs.existsSync(this.tempArchiveName)) {
1704
+ throw new Error(`Temporary file not found: ${this.tempArchiveName}`);
1705
+ }
1706
+ // Use fs.rename() for atomic move (same filesystem)
1707
+ // This will overwrite existing file atomically if it exists
1708
+ fs.renameSync(this.tempArchiveName, this.archiveName);
1709
+ // Clean up temp file immediately after successful move (should already be moved, but ensure)
1710
+ if (fs.existsSync(this.tempArchiveName)) {
1711
+ try {
1712
+ fs.unlinkSync(this.tempArchiveName);
1713
+ }
1714
+ catch {
1715
+ // Ignore cleanup errors - file may have already been moved
1716
+ }
1717
+ }
1718
+ // Apply old timestamp if requested (on final file)
1719
+ if (this.options.oldTimestamp) {
1720
+ try {
1721
+ const oldestTime = await (0, file_operations_1.findOldestEntryTime)(this.zip);
1722
+ if (oldestTime) {
1723
+ fs.utimesSync(this.archiveName, oldestTime, oldestTime);
1724
+ if (this.options.verbose) {
1725
+ (0, utils_1.log)(`📅 Set archive timestamp to oldest entry: ${oldestTime.toISOString()}`, this.options);
1726
+ }
1727
+ }
1728
+ }
1729
+ catch (error) {
1730
+ if (this.options.verbose) {
1731
+ (0, utils_1.log)(`⚠️ Warning: Could not set old timestamp: ${error instanceof Error ? error.message : String(error)}`, this.options);
1732
+ }
1733
+ }
1734
+ }
1735
+ }
1736
+ cleanupFileDescriptors() {
1737
+ for (const [filePath, fd] of this.fdCache.entries()) {
1738
+ try {
1739
+ fs.closeSync(fd);
1740
+ }
1741
+ catch (error) {
1742
+ // Ignore errors during cleanup
1743
+ }
1744
+ }
1745
+ this.fdCache.clear();
1746
+ }
1747
+ }
1748
+ /**
1749
+ * Test the integrity of a ZIP archive using neounzip
1750
+ */
1751
+ async function testArchiveIntegrity(archiveName, options) {
1752
+ try {
1753
+ // Always show when integrity testing starts
1754
+ (0, utils_1.log)(`\n🧪 Testing archive integrity: ${archiveName}`, options);
1755
+ // Import child_process for shelling out
1756
+ const { spawn } = require('child_process');
1757
+ // Use the installed neounzip command directly (works in both dev and installed)
1758
+ // In development, this will use the local bin, in installed package it uses global bin
1759
+ const neounzipCommand = 'neounzip';
1760
+ return new Promise((resolve) => {
1761
+ // Build neounzip command arguments
1762
+ const args = ['-t'];
1763
+ // Add password if encryption is enabled
1764
+ if (options.encrypt && options.password) {
1765
+ args.push('-P', options.password);
1766
+ }
1767
+ args.push(archiveName);
1768
+ // Run neounzip with -t (test) flag directly
1769
+ const neounzip = spawn(neounzipCommand, args, {
1770
+ stdio: ['pipe', 'pipe', 'pipe']
1771
+ });
1772
+ let stdout = '';
1773
+ let stderr = '';
1774
+ let fileCount = 0;
1775
+ let testResults = [];
1776
+ neounzip.stdout.on('data', (data) => {
1777
+ const output = data.toString();
1778
+ stdout += output;
1779
+ // Parse the output to extract file testing information
1780
+ const lines = output.split('\n');
1781
+ for (const line of lines) {
1782
+ // Look for lines like "testing: filename ...OK SHA-256" or "testing: filename ...OK"
1783
+ if (line.includes('testing:') && line.includes('...OK')) {
1784
+ fileCount++;
1785
+ testResults.push(line.trim());
1786
+ }
1787
+ }
1788
+ });
1789
+ neounzip.stderr.on('data', (data) => {
1790
+ stderr += data.toString();
1791
+ });
1792
+ neounzip.on('close', (code) => {
1793
+ if (code === 0) {
1794
+ // Show detailed integrity test results
1795
+ if (fileCount > 0) {
1796
+ (0, utils_1.log)(`📊 Testing ${fileCount} file(s):`, options);
1797
+ testResults.forEach(result => {
1798
+ (0, utils_1.log)(` ${result}`, options);
1799
+ });
1800
+ }
1801
+ // Look for summary line like "No errors detected in compressed data of X files"
1802
+ const summaryMatch = stdout.match(/No errors detected in compressed data of (\d+) files?/);
1803
+ if (summaryMatch) {
1804
+ (0, utils_1.log)(`✅ Archive integrity test passed - ${summaryMatch[1]} files verified`, options);
1805
+ }
1806
+ else {
1807
+ (0, utils_1.log)(`✅ Archive integrity test passed`, options);
1808
+ }
1809
+ resolve(true);
1810
+ }
1811
+ else {
1812
+ (0, utils_1.log)(`❌ Archive integrity test failed (exit code: ${code})`, options);
1813
+ if (stderr) {
1814
+ (0, utils_1.log)(`Error output: ${stderr}`, options);
1815
+ }
1816
+ resolve(false);
1817
+ }
1818
+ });
1819
+ neounzip.on('error', (error) => {
1820
+ (0, utils_1.log)(`❌ Failed to run integrity test: ${error.message}`, options);
1821
+ resolve(false);
1822
+ });
1823
+ });
1824
+ }
1825
+ catch (error) {
1826
+ (0, utils_1.log)(`❌ Error during integrity test: ${error instanceof Error ? error.message : String(error)}`, options);
1827
+ return false;
1828
+ }
1829
+ }
1830
+ /**
1831
+ * Creates the end of central directory record manually
1832
+ * @param totalEntries - Total number of entries in the ZIP
1833
+ * @param centralDirSize - Size of central directory
1834
+ * @param centralDirOffset - Offset to central directory
1835
+ * @param archiveComment - Optional archive comment
1836
+ * @returns Buffer containing end of central directory record
1837
+ */
1838
+ function createEndOfCentralDirRecord(totalEntries, centralDirSize, centralDirOffset, archiveComment = '') {
1839
+ const commentBytes = Buffer.from(archiveComment, 'utf8');
1840
+ const commentLength = Math.min(commentBytes.length, 0xFFFF); // Max 65535 bytes
1841
+ const buffer = Buffer.alloc(22 + commentLength);
1842
+ let offset = 0;
1843
+ // End of central directory signature (4 bytes)
1844
+ buffer.writeUInt32LE(0x06054b50, offset);
1845
+ offset += 4;
1846
+ // Number of this disk (2 bytes)
1847
+ buffer.writeUInt16LE(0, offset);
1848
+ offset += 2;
1849
+ // Number of the disk with the start of the central directory (2 bytes)
1850
+ buffer.writeUInt16LE(0, offset);
1851
+ offset += 2;
1852
+ // Total number of entries in the central directory on this disk (2 bytes)
1853
+ buffer.writeUInt16LE(totalEntries, offset);
1854
+ offset += 2;
1855
+ // Total number of entries in the central directory (2 bytes)
1856
+ buffer.writeUInt16LE(totalEntries, offset);
1857
+ offset += 2;
1858
+ // Size of the central directory (4 bytes)
1859
+ buffer.writeUInt32LE(centralDirSize, offset);
1860
+ offset += 4;
1861
+ // Offset of start of central directory with respect to the starting disk number (4 bytes)
1862
+ buffer.writeUInt32LE(centralDirOffset, offset);
1863
+ offset += 4;
1864
+ // ZIP file comment length (2 bytes)
1865
+ buffer.writeUInt16LE(commentLength, offset);
1866
+ offset += 2;
1867
+ // ZIP file comment (variable length)
1868
+ if (commentLength > 0) {
1869
+ commentBytes.copy(buffer, offset, 0, commentLength);
1870
+ }
1871
+ return buffer;
1872
+ }
1873
+ /**
1874
+ * Upgrade existing ZIP file for tokenization by adding SHA-256 hashes to entries
1875
+ * that don't have them, preserving original compression methods
1876
+ */
1877
+ async function upgradeZipForTokenization(inputZipPath, options) {
1878
+ const os = require('os');
1879
+ (0, utils_1.log)(`\n🔧 Upgrading ZIP file for tokenization: ${inputZipPath}`, options);
1880
+ // Validate input file exists
1881
+ if (!fs.existsSync(inputZipPath)) {
1882
+ (0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.CANT_FIND_ARCHIVE, `Error: Input ZIP file not found: ${inputZipPath}`);
1883
+ }
1884
+ // Determine output file path (final destination)
1885
+ let outputZipPath;
1886
+ if (options.overwrite) {
1887
+ outputZipPath = inputZipPath;
1888
+ (0, utils_1.log)(` Output: ${outputZipPath} (overwriting input)`, options);
1889
+ }
1890
+ else {
1891
+ const inputDir = path.dirname(inputZipPath);
1892
+ const inputBase = path.basename(inputZipPath, path.extname(inputZipPath));
1893
+ const inputExt = path.extname(inputZipPath);
1894
+ outputZipPath = path.join(inputDir, `${inputBase}-tokenized${inputExt}`);
1895
+ (0, utils_1.log)(` Output: ${outputZipPath}`, options);
1896
+ }
1897
+ // Create temporary file path for atomic operation (even when overwriting)
1898
+ const tempDir = options.tempPath || os.tmpdir();
1899
+ // Ensure temp directory exists
1900
+ if (!fs.existsSync(tempDir)) {
1901
+ fs.mkdirSync(tempDir, { recursive: true });
1902
+ }
1903
+ const tempOutputPath = path.join(tempDir, `${path.basename(outputZipPath)}.tmp.${Date.now()}`);
1904
+ // Load existing ZIP
1905
+ const sourceZip = new node_1.default();
1906
+ await sourceZip.loadZipFile(inputZipPath);
1907
+ const entries = await sourceZip.getDirectory();
1908
+ if (!entries || entries.length === 0) {
1909
+ (0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.BAD_ARCHIVE_FORMAT, 'Error: ZIP file is empty or invalid');
1910
+ }
1911
+ (0, utils_1.log)(` Found ${entries.length} entries in ZIP`, options);
1912
+ // Check for existing token metadata (ignore per requirement 3a)
1913
+ const hasTokenMetadata = entries.some((e) => e.filename === neozipkit_1.TOKENIZED_METADATA);
1914
+ if (hasTokenMetadata && options.verbose) {
1915
+ (0, utils_1.log)(` Note: ZIP already contains token metadata (will create new token)`, options);
1916
+ }
1917
+ // Process entries: extract if missing SHA-256, calculate hash, preserve compression
1918
+ const processedEntries = [];
1919
+ const hashAccumulator = new neozipkit_1.HashCalculator({ enableAccumulation: true });
1920
+ let tempFiles = [];
1921
+ try {
1922
+ for (const entry of entries) {
1923
+ const filename = entry.filename || '';
1924
+ // Skip metadata entries and directories
1925
+ if (filename === neozipkit_1.TOKENIZED_METADATA ||
1926
+ filename === neozipkit_1.TIMESTAMP_METADATA ||
1927
+ filename === neozipkit_1.TIMESTAMP_SUBMITTED ||
1928
+ entry.isDirectory) {
1929
+ continue;
1930
+ }
1931
+ // Check if entry needs SHA-256 hash
1932
+ let needsHash = !entry.sha256;
1933
+ if (needsHash) {
1934
+ (0, utils_1.log)(` Processing: ${filename} (calculating SHA-256...)`, options);
1935
+ // Check if encrypted (requirement 5c: error if cannot process)
1936
+ if (entry.isEncrypted) {
1937
+ if (!options.password) {
1938
+ (0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.BAD_ARCHIVE_FORMAT, `Error: Entry "${filename}" is encrypted but no password provided. Use -P or --password option.`);
1939
+ }
1940
+ // Set password on source ZIP for decryption
1941
+ sourceZip.password = options.password;
1942
+ }
1943
+ // Extract entry to get uncompressed data
1944
+ let uncompressedData;
1945
+ try {
1946
+ // Check if ZIP is file-based or buffer-based
1947
+ const isFileBased = sourceZip.fileHandle !== undefined;
1948
+ if (isFileBased) {
1949
+ // File-based: extract to temp file
1950
+ const tempFile = path.join(tempDir, `neozip-upgrade-${Date.now()}-${filename.replace(/[^a-zA-Z0-9]/g, '_')}`);
1951
+ tempFiles.push(tempFile);
1952
+ await sourceZip.extractToFile(entry, tempFile, { skipHashCheck: false });
1953
+ uncompressedData = fs.readFileSync(tempFile);
1954
+ }
1955
+ else {
1956
+ // Buffer-based: extract directly
1957
+ const extracted = await sourceZip.extract(entry, false);
1958
+ if (!extracted) {
1959
+ (0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.READ_ERROR, `Error: Failed to extract entry "${filename}"`);
1960
+ }
1961
+ uncompressedData = extracted;
1962
+ }
1963
+ }
1964
+ catch (error) {
1965
+ const errorMsg = error instanceof Error ? error.message : String(error);
1966
+ (0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.READ_ERROR, `Error: Failed to extract entry "${filename}": ${errorMsg}`);
1967
+ }
1968
+ // Calculate SHA-256 hash
1969
+ const hashHex = (0, neozipkit_1.sha256)(uncompressedData);
1970
+ entry.sha256 = hashHex;
1971
+ if (options.verbose) {
1972
+ (0, utils_1.log)(` SHA-256: ${hashHex.substring(0, 16)}...`, options);
1973
+ }
1974
+ }
1975
+ else {
1976
+ if (options.verbose) {
1977
+ (0, utils_1.log)(` Skipping: ${filename} (already has SHA-256)`, options);
1978
+ }
1979
+ }
1980
+ // Read compressed data from original ZIP (preserve compression)
1981
+ let compressedData;
1982
+ try {
1983
+ const isFileBased = sourceZip.fileHandle !== undefined;
1984
+ if (isFileBased) {
1985
+ // Read local header to find data offset
1986
+ const localHeaderBuffer = Buffer.alloc(30);
1987
+ await sourceZip.fileHandle.read(localHeaderBuffer, 0, 30, entry.localHdrOffset);
1988
+ if (localHeaderBuffer.readUInt32LE(0) !== 0x04034b50) {
1989
+ (0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.BAD_ARCHIVE_FORMAT, `Error: Invalid local header for entry "${filename}"`);
1990
+ }
1991
+ const filenameLength = localHeaderBuffer.readUInt16LE(26);
1992
+ const extraFieldLength = localHeaderBuffer.readUInt16LE(28);
1993
+ const dataStart = entry.localHdrOffset + 30 + filenameLength + extraFieldLength;
1994
+ // Read compressed data
1995
+ compressedData = Buffer.alloc(entry.compressedSize);
1996
+ await sourceZip.fileHandle.read(compressedData, 0, entry.compressedSize, dataStart);
1997
+ }
1998
+ else {
1999
+ // Buffer-based: parse local header and read compressed data
2000
+ const buffer = sourceZip.ensureBuffer();
2001
+ compressedData = sourceZip.parseLocalHeader(entry, buffer);
2002
+ }
2003
+ }
2004
+ catch (error) {
2005
+ const errorMsg = error instanceof Error ? error.message : String(error);
2006
+ (0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.READ_ERROR, `Error: Failed to read compressed data for entry "${filename}": ${errorMsg}`);
2007
+ }
2008
+ // Store processed entry with compressed data
2009
+ processedEntries.push({ entry, compressedData });
2010
+ // Add hash to merkle root calculation
2011
+ if (entry.sha256) {
2012
+ const hashBuffer = Buffer.from(entry.sha256, 'hex');
2013
+ hashAccumulator.addHash(hashBuffer);
2014
+ }
2015
+ }
2016
+ // Calculate merkle root
2017
+ const merkleRoot = hashAccumulator.merkleRoot();
2018
+ if (!merkleRoot) {
2019
+ (0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.BAD_ARCHIVE_FORMAT, 'Error: Failed to calculate merkle root (no content files found)');
2020
+ }
2021
+ (0, utils_1.log)(` Calculated merkle root: ${merkleRoot.substring(0, 16)}...`, options);
2022
+ // Rebuild ZIP with updated entries (write to temp file first for atomic operation)
2023
+ (0, utils_1.log)(` Rebuilding ZIP with updated entries...`, options);
2024
+ const outputZip = new node_1.default();
2025
+ const writer = await outputZip.initializeZipFile(tempOutputPath);
2026
+ try {
2027
+ let currentOffset = 0;
2028
+ // Write all entries with updated SHA-256 hashes
2029
+ for (const { entry, compressedData } of processedEntries) {
2030
+ // Create new entry with same properties but updated SHA-256
2031
+ const newEntry = new neozipkit_1.ZipEntry(entry.filename || '', null, false);
2032
+ Object.assign(newEntry, {
2033
+ filename: entry.filename,
2034
+ cmpMethod: entry.cmpMethod,
2035
+ compressedSize: entry.compressedSize,
2036
+ uncompressedSize: entry.uncompressedSize,
2037
+ crc: entry.crc,
2038
+ sha256: entry.sha256,
2039
+ timeDateDOS: entry.timeDateDOS,
2040
+ lastModTimeDate: entry.lastModTimeDate,
2041
+ comment: entry.comment,
2042
+ isDirectory: entry.isDirectory,
2043
+ isEncrypted: entry.isEncrypted,
2044
+ encryptionMethod: entry.encryptionMethod,
2045
+ uid: entry.uid,
2046
+ gid: entry.gid,
2047
+ isSymlink: entry.isSymlink,
2048
+ linkTarget: entry.linkTarget,
2049
+ universalTime: entry.universalTime
2050
+ });
2051
+ // Create local header
2052
+ const localHeader = newEntry.createLocalHdr();
2053
+ newEntry.localHdrOffset = currentOffset;
2054
+ // Write local header
2055
+ await new Promise((resolve, reject) => {
2056
+ writer.outputStream.write(localHeader, (error) => {
2057
+ if (error) {
2058
+ reject(error);
2059
+ }
2060
+ else {
2061
+ writer.currentPosition += localHeader.length;
2062
+ resolve();
2063
+ }
2064
+ });
2065
+ });
2066
+ currentOffset += localHeader.length;
2067
+ // Write compressed data (preserved from original)
2068
+ await new Promise((resolve, reject) => {
2069
+ writer.outputStream.write(compressedData, (error) => {
2070
+ if (error) {
2071
+ reject(error);
2072
+ }
2073
+ else {
2074
+ writer.currentPosition += compressedData.length;
2075
+ currentOffset += compressedData.length;
2076
+ resolve();
2077
+ }
2078
+ });
2079
+ });
2080
+ }
2081
+ // Mint new token before finalizing ZIP (so we can add metadata)
2082
+ // Check for wallet passkey - don't show error yet
2083
+ const walletPasskey = (0, blockchain_2.getWalletPasskey)(options.walletKey, false);
2084
+ if (!walletPasskey) {
2085
+ // Show error message and prompt user
2086
+ console.error('❌ Error: Wallet private key is required for blockchain operations');
2087
+ console.error('');
2088
+ console.error(' Option 1 (Recommended): Run interactive setup');
2089
+ console.error(' $ neozip init');
2090
+ console.error('');
2091
+ console.error(' Option 2: Use command-line flag');
2092
+ console.error(' $ neozip --upgrade -w 0x... <archive>');
2093
+ console.error('');
2094
+ console.error(' Option 3: Set environment variable');
2095
+ console.error(' $ export NEOZIP_WALLET_PASSKEY="0x..."');
2096
+ console.error('');
2097
+ console.error(' Get testnet ETH from: https://www.coinbase.com/faucets/base-ethereum-sepolia-faucet');
2098
+ console.error('');
2099
+ // Prompt user to continue without blockchain tokenization
2100
+ if (!options.nonInteractive) {
2101
+ const answer = await (0, user_interaction_1.promptUser)('Continue upgrade without blockchain tokenization? (y/n): ');
2102
+ if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
2103
+ (0, utils_1.log)('⚠️ Continuing upgrade without blockchain tokenization (SHA-256 hashes will be added)...', options);
2104
+ // Skip token minting - ZIP will be upgraded with SHA-256 hashes but no token metadata
2105
+ }
2106
+ else {
2107
+ (0, utils_1.logError)('💡 Tip: Run "neozip init" to set up your wallet configuration');
2108
+ (0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.BLOCKCHAIN_CONFIG_ERROR);
2109
+ }
2110
+ }
2111
+ else {
2112
+ // Non-interactive mode - exit with error
2113
+ (0, utils_1.logError)('💡 Tip: Run "neozip init" to set up your wallet configuration');
2114
+ (0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.BLOCKCHAIN_CONFIG_ERROR);
2115
+ }
2116
+ }
2117
+ // Only mint token if wallet passkey is available
2118
+ if (walletPasskey) {
2119
+ (0, utils_1.log)(` Minting new token on blockchain...`, options);
2120
+ const network = options.network || 'base-sepolia';
2121
+ if (!(0, blockchain_2.isSupportedNetwork)(network)) {
2122
+ (0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.BLOCKCHAIN_CONFIG_ERROR, `Error: Unsupported network: ${network}`);
2123
+ }
2124
+ const minter = new neozipkit_1.ZipkitMinter(merkleRoot, {
2125
+ network,
2126
+ walletPrivateKey: walletPasskey,
2127
+ verbose: options.verbose,
2128
+ debug: options.debug,
2129
+ rpcUrlIndex: 0
2130
+ });
2131
+ // Create a temporary ZipkitNode instance for handleTokenMinting
2132
+ // (it needs a zip instance, but we'll add metadata manually)
2133
+ const tempZip = new node_1.default();
2134
+ const mintResult = await (0, blockchain_2.handleTokenMinting)(minter, tempZip, options.nonInteractive || false, 0);
2135
+ if (mintResult.success && mintResult.tokenInfo) {
2136
+ // Add token metadata entry before central directory
2137
+ const tokenContent = JSON.stringify(mintResult.tokenInfo, null, 2);
2138
+ const tokenBuffer = Buffer.from(tokenContent, 'utf8');
2139
+ const tokenEntry = new neozipkit_1.ZipEntry(neozipkit_1.TOKENIZED_METADATA, null, false);
2140
+ tokenEntry.timeDateDOS = tokenEntry.setDateTime(new Date());
2141
+ tokenEntry.compressedSize = tokenBuffer.length;
2142
+ tokenEntry.uncompressedSize = tokenBuffer.length;
2143
+ tokenEntry.cmpMethod = 0; // STORED
2144
+ tokenEntry.crc = (0, neozipkit_1.crc32)(tokenBuffer);
2145
+ tokenEntry.localHdrOffset = currentOffset;
2146
+ // Write token metadata entry
2147
+ const tokenLocalHeader = tokenEntry.createLocalHdr();
2148
+ await new Promise((resolve, reject) => {
2149
+ writer.outputStream.write(tokenLocalHeader, (error) => {
2150
+ if (error) {
2151
+ reject(error);
2152
+ }
2153
+ else {
2154
+ writer.currentPosition += tokenLocalHeader.length;
2155
+ resolve();
2156
+ }
2157
+ });
2158
+ });
2159
+ currentOffset += tokenLocalHeader.length;
2160
+ await new Promise((resolve, reject) => {
2161
+ writer.outputStream.write(tokenBuffer, (error) => {
2162
+ if (error) {
2163
+ reject(error);
2164
+ }
2165
+ else {
2166
+ writer.currentPosition += tokenBuffer.length;
2167
+ currentOffset += tokenBuffer.length;
2168
+ resolve();
2169
+ }
2170
+ });
2171
+ });
2172
+ // Add token entry to processed entries for central directory
2173
+ processedEntries.push({ entry: tokenEntry, compressedData: tokenBuffer });
2174
+ // Clean up minter
2175
+ minter.destroy();
2176
+ (0, utils_1.log)(` ✓ Token minted successfully!`, options);
2177
+ (0, utils_1.log)(` ✓ Token ID: ${mintResult.tokenInfo.tokenId}`, options);
2178
+ (0, utils_1.log)(` ✓ Transaction: ${mintResult.tokenInfo.transactionHash}`, options);
2179
+ }
2180
+ else {
2181
+ // Clean up minter
2182
+ minter.destroy();
2183
+ (0, utils_1.log)(` ⚠️ Token minting failed, but ZIP upgrade will continue`, options);
2184
+ }
2185
+ }
2186
+ else {
2187
+ (0, utils_1.log)(` ⚠️ Skipping blockchain tokenization (no wallet key provided)`, options);
2188
+ }
2189
+ // Write central directory (including token entry if minting succeeded)
2190
+ const finalCentralDirOffset = currentOffset;
2191
+ let finalCentralDirSize = 0;
2192
+ for (const { entry } of processedEntries) {
2193
+ const newEntry = new neozipkit_1.ZipEntry(entry.filename || '', null, false);
2194
+ Object.assign(newEntry, {
2195
+ filename: entry.filename,
2196
+ cmpMethod: entry.cmpMethod,
2197
+ compressedSize: entry.compressedSize,
2198
+ uncompressedSize: entry.uncompressedSize,
2199
+ crc: entry.crc,
2200
+ sha256: entry.sha256,
2201
+ timeDateDOS: entry.timeDateDOS,
2202
+ lastModTimeDate: entry.lastModTimeDate,
2203
+ comment: entry.comment,
2204
+ isDirectory: entry.isDirectory,
2205
+ localHdrOffset: entry.localHdrOffset,
2206
+ uid: entry.uid,
2207
+ gid: entry.gid,
2208
+ isSymlink: entry.isSymlink,
2209
+ linkTarget: entry.linkTarget,
2210
+ universalTime: entry.universalTime
2211
+ });
2212
+ const centralDirEntry = newEntry.centralDirEntry();
2213
+ await new Promise((resolve, reject) => {
2214
+ writer.outputStream.write(centralDirEntry, (error) => {
2215
+ if (error) {
2216
+ reject(error);
2217
+ }
2218
+ else {
2219
+ writer.currentPosition += centralDirEntry.length;
2220
+ finalCentralDirSize += centralDirEntry.length;
2221
+ currentOffset += centralDirEntry.length;
2222
+ resolve();
2223
+ }
2224
+ });
2225
+ });
2226
+ }
2227
+ // Write end of central directory
2228
+ await outputZip.writeEndOfCentralDirectory(writer, processedEntries.length, finalCentralDirSize, finalCentralDirOffset, '' // No comment
2229
+ );
2230
+ // Finalize ZIP file
2231
+ await outputZip.finalizeZipFile(writer);
2232
+ // Atomically move temp file to final location
2233
+ if (!fs.existsSync(tempOutputPath)) {
2234
+ throw new Error(`Temporary file not found: ${tempOutputPath}`);
2235
+ }
2236
+ // Use fs.rename() for atomic move (same filesystem)
2237
+ // This will overwrite existing file atomically if it exists
2238
+ fs.renameSync(tempOutputPath, outputZipPath);
2239
+ // Clean up temp file immediately after successful move
2240
+ if (fs.existsSync(tempOutputPath)) {
2241
+ try {
2242
+ fs.unlinkSync(tempOutputPath);
2243
+ }
2244
+ catch {
2245
+ // Ignore cleanup errors - file may have already been moved
2246
+ }
2247
+ }
2248
+ (0, utils_1.log)(` ✓ ZIP rebuilt successfully`, options);
2249
+ }
2250
+ catch (error) {
2251
+ // Clean up temp output file on error
2252
+ if (fs.existsSync(tempOutputPath)) {
2253
+ try {
2254
+ fs.unlinkSync(tempOutputPath);
2255
+ }
2256
+ catch {
2257
+ // Ignore cleanup errors
2258
+ }
2259
+ }
2260
+ throw error;
2261
+ }
2262
+ (0, utils_1.log)(`\n✅ ZIP upgrade completed successfully!`, options);
2263
+ (0, utils_1.log)(` Output: ${outputZipPath}`, options);
2264
+ }
2265
+ finally {
2266
+ // Clean up temporary extraction files
2267
+ for (const tempFile of tempFiles) {
2268
+ try {
2269
+ if (fs.existsSync(tempFile)) {
2270
+ fs.unlinkSync(tempFile);
2271
+ }
2272
+ }
2273
+ catch {
2274
+ // Ignore cleanup errors
2275
+ }
2276
+ }
2277
+ // Ensure temp output file is cleaned up if it still exists (error case)
2278
+ if (fs.existsSync(tempOutputPath)) {
2279
+ try {
2280
+ fs.unlinkSync(tempOutputPath);
2281
+ }
2282
+ catch {
2283
+ // Ignore cleanup errors
2284
+ }
2285
+ }
2286
+ }
2287
+ }
2288
+ //# sourceMappingURL=createZip.js.map