acker-dacker 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -10
- package/package.json +34 -33
- package/src/cli/archive-name.ts +51 -0
- package/src/cli/cli-options.ts +166 -0
- package/src/{cli.ts → cli/cli.ts} +19 -8
- package/src/{git-handoff-selection.ts → git/git-handoff-selection.ts} +54 -2
- package/src/{git-handoff.ts → git/git-handoff.ts} +109 -28
- package/src/{git.ts → git/git.ts} +77 -0
- package/{index.ts → src/index.ts} +1 -1
- package/src/{file-policy.ts → pack/file-policy.ts} +1 -0
- package/src/pack/gitignore.ts +272 -0
- package/src/{repository-pack.ts → pack/repository-pack.ts} +149 -25
- package/src/zip/zip.ts +511 -0
- package/src/cli-options.ts +0 -93
- package/src/gitignore.ts +0 -139
- package/src/zip.ts +0 -327
- /package/src/{path-patterns.ts → pack/path-patterns.ts} +0 -0
package/src/zip/zip.ts
ADDED
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
import { lstat, mkdir, open, readdir, readlink, rename, rm } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { createDeflateRaw, deflateRawSync } from 'node:zlib';
|
|
5
|
+
import { compareStableStrings, normalizeArchivePath } from '../pack/path-patterns';
|
|
6
|
+
|
|
7
|
+
type ZipEntryKind = 'file' | 'symlink';
|
|
8
|
+
|
|
9
|
+
type ZipFileEntry = {
|
|
10
|
+
readonly absolutePath: string;
|
|
11
|
+
readonly archivePath: string;
|
|
12
|
+
readonly kind: ZipEntryKind;
|
|
13
|
+
readonly linkTarget?: string;
|
|
14
|
+
readonly mode?: number;
|
|
15
|
+
readonly size: number;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type WrittenZipEntry = ZipFileEntry & {
|
|
19
|
+
readonly compressedSize: number;
|
|
20
|
+
readonly compressionMethod: number;
|
|
21
|
+
readonly crc32: number;
|
|
22
|
+
readonly localHeaderOffset: number;
|
|
23
|
+
readonly pathBytes: Uint8Array;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type ZipCreationOptions = {
|
|
27
|
+
readonly preserveSymlinks: boolean;
|
|
28
|
+
readonly preserveUnixMetadata: boolean;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const ZIP_MAX_UINT32 = 0xffffffff;
|
|
32
|
+
export const ZIP_MAX_UINT16 = 0xffff;
|
|
33
|
+
const ZIP_DOS_DATE_1980_01_01 = 0x21;
|
|
34
|
+
const ZIP_DEFLATE_METHOD = 8;
|
|
35
|
+
const ZIP_STORE_METHOD = 0;
|
|
36
|
+
const ZIP_UNIX_CREATOR_VERSION = 0x0314;
|
|
37
|
+
const ZIP_UTF8_FLAG = 0x0800;
|
|
38
|
+
export const MAX_BUFFERED_DEFLATE_BYTES = 64 * 1024 * 1024;
|
|
39
|
+
|
|
40
|
+
const CRC32_TABLE = new Uint32Array(256);
|
|
41
|
+
for (let index = 0; index < CRC32_TABLE.length; index += 1) {
|
|
42
|
+
let value = index;
|
|
43
|
+
for (let bit = 0; bit < 8; bit += 1) {
|
|
44
|
+
value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1;
|
|
45
|
+
}
|
|
46
|
+
CRC32_TABLE[index] = value >>> 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const updateCrc32 = (crc: number, chunk: Uint8Array): number => {
|
|
50
|
+
let current = crc;
|
|
51
|
+
for (const byte of chunk) {
|
|
52
|
+
current = CRC32_TABLE[(current ^ byte) & 0xff]! ^ (current >>> 8);
|
|
53
|
+
}
|
|
54
|
+
return current >>> 0;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const validateZipEntryCount = (count: number): void => {
|
|
58
|
+
if (count > ZIP_MAX_UINT16) {
|
|
59
|
+
throw new Error(`Zip entry count (${count}) exceeds classic ZIP limit of ${ZIP_MAX_UINT16}`);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const validateZipEntrySize = (size: number, pathLabel: string): void => {
|
|
64
|
+
if (size > ZIP_MAX_UINT32) {
|
|
65
|
+
throw new Error(`Zip entry size exceeds classic ZIP limit (4 GB): ${pathLabel}`);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export const assertWithinZip32Limit = (currentOffset: number, bytesToAdd: number, description: string): void => {
|
|
70
|
+
if (currentOffset + bytesToAdd > ZIP_MAX_UINT32) {
|
|
71
|
+
throw new Error(`Zip archive size exceeds classic ZIP limit (4 GB) while writing ${description}`);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
const collectZipFiles = async ({
|
|
77
|
+
directory,
|
|
78
|
+
excludedAbsolutePaths,
|
|
79
|
+
preserveSymlinks,
|
|
80
|
+
preserveUnixMetadata,
|
|
81
|
+
rootDir,
|
|
82
|
+
}: {
|
|
83
|
+
readonly directory: string;
|
|
84
|
+
readonly excludedAbsolutePaths: ReadonlySet<string>;
|
|
85
|
+
readonly preserveSymlinks: boolean;
|
|
86
|
+
readonly preserveUnixMetadata: boolean;
|
|
87
|
+
readonly rootDir: string;
|
|
88
|
+
}): Promise<readonly ZipFileEntry[]> => {
|
|
89
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
90
|
+
const files: ZipFileEntry[] = [];
|
|
91
|
+
for (const entry of entries.sort((left, right) => compareStableStrings(left.name, right.name))) {
|
|
92
|
+
const absolutePath = path.join(directory, entry.name);
|
|
93
|
+
if (excludedAbsolutePaths.has(absolutePath)) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (entry.isSymbolicLink()) {
|
|
98
|
+
if (!preserveSymlinks) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const linkTarget = await readlink(absolutePath);
|
|
102
|
+
const size = new TextEncoder().encode(linkTarget).byteLength;
|
|
103
|
+
if (size > ZIP_MAX_UINT32) {
|
|
104
|
+
throw new Error(`Zip entry size exceeds classic ZIP limit (4 GB): ${absolutePath}`);
|
|
105
|
+
}
|
|
106
|
+
files.push({
|
|
107
|
+
absolutePath,
|
|
108
|
+
archivePath: normalizeArchivePath(path.relative(rootDir, absolutePath)),
|
|
109
|
+
kind: 'symlink',
|
|
110
|
+
linkTarget,
|
|
111
|
+
mode: 0o120777,
|
|
112
|
+
size,
|
|
113
|
+
});
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (entry.isDirectory()) {
|
|
118
|
+
files.push(
|
|
119
|
+
...(await collectZipFiles({
|
|
120
|
+
directory: absolutePath,
|
|
121
|
+
excludedAbsolutePaths,
|
|
122
|
+
preserveSymlinks,
|
|
123
|
+
preserveUnixMetadata,
|
|
124
|
+
rootDir,
|
|
125
|
+
})),
|
|
126
|
+
);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (!entry.isFile()) {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const fileStats = await lstat(absolutePath);
|
|
134
|
+
if (fileStats.size > ZIP_MAX_UINT32) {
|
|
135
|
+
throw new Error(`Zip entry size exceeds classic ZIP limit (4 GB): ${absolutePath}`);
|
|
136
|
+
}
|
|
137
|
+
files.push({
|
|
138
|
+
absolutePath,
|
|
139
|
+
archivePath: normalizeArchivePath(path.relative(rootDir, absolutePath)),
|
|
140
|
+
kind: 'file',
|
|
141
|
+
mode: preserveUnixMetadata ? fileStats.mode : undefined,
|
|
142
|
+
size: fileStats.size,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
return files;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
export const writeBuffer = async (
|
|
149
|
+
handle: { write: (buffer: Uint8Array, offset?: number, length?: number, position?: number | null) => Promise<{ bytesWritten: number }> },
|
|
150
|
+
buffer: Uint8Array,
|
|
151
|
+
offset: number,
|
|
152
|
+
): Promise<number> => {
|
|
153
|
+
let currentBufferOffset = 0;
|
|
154
|
+
let currentFileOffset = offset;
|
|
155
|
+
|
|
156
|
+
while (currentBufferOffset < buffer.length) {
|
|
157
|
+
const remaining = buffer.length - currentBufferOffset;
|
|
158
|
+
const result = await handle.write(buffer, currentBufferOffset, remaining, currentFileOffset);
|
|
159
|
+
if (result.bytesWritten <= 0) {
|
|
160
|
+
throw new Error(`Write made no progress at offset ${currentFileOffset}`);
|
|
161
|
+
}
|
|
162
|
+
currentBufferOffset += result.bytesWritten;
|
|
163
|
+
currentFileOffset += result.bytesWritten;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return currentFileOffset;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const writeZipLocalHeader = async (
|
|
170
|
+
handle: { write: (buffer: Uint8Array, offset?: number, length?: number, position?: number | null) => Promise<{ bytesWritten: number }> },
|
|
171
|
+
entry: ZipFileEntry,
|
|
172
|
+
pathBytes: Uint8Array,
|
|
173
|
+
crc32: number,
|
|
174
|
+
compressedSize: number,
|
|
175
|
+
compressionMethod: number,
|
|
176
|
+
offset: number,
|
|
177
|
+
): Promise<number> => {
|
|
178
|
+
assertWithinZip32Limit(offset, 30 + pathBytes.length, `local header for ${entry.archivePath}`);
|
|
179
|
+
const header = Buffer.alloc(30);
|
|
180
|
+
header.writeUInt32LE(0x04034b50, 0);
|
|
181
|
+
header.writeUInt16LE(20, 4);
|
|
182
|
+
header.writeUInt16LE(ZIP_UTF8_FLAG, 6);
|
|
183
|
+
header.writeUInt16LE(compressionMethod, 8);
|
|
184
|
+
header.writeUInt16LE(0, 10);
|
|
185
|
+
header.writeUInt16LE(ZIP_DOS_DATE_1980_01_01, 12);
|
|
186
|
+
header.writeUInt32LE(crc32, 14);
|
|
187
|
+
header.writeUInt32LE(compressedSize, 18);
|
|
188
|
+
header.writeUInt32LE(entry.size, 22);
|
|
189
|
+
header.writeUInt16LE(pathBytes.length, 26);
|
|
190
|
+
header.writeUInt16LE(0, 28);
|
|
191
|
+
const nextOffset = await writeBuffer(handle, header, offset);
|
|
192
|
+
return await writeBuffer(handle, pathBytes, nextOffset);
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const streamDeflateFile = async (
|
|
196
|
+
absolutePath: string,
|
|
197
|
+
spoolDir: string,
|
|
198
|
+
): Promise<{
|
|
199
|
+
readonly compressedSize: number;
|
|
200
|
+
readonly compressionMethod: number;
|
|
201
|
+
readonly crc32: number;
|
|
202
|
+
readonly spoolFile: string | null;
|
|
203
|
+
}> => {
|
|
204
|
+
const spoolFile = path.join(
|
|
205
|
+
spoolDir,
|
|
206
|
+
`.spool-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`,
|
|
207
|
+
);
|
|
208
|
+
const spoolHandle = await open(spoolFile, 'wx');
|
|
209
|
+
let spoolClosed = false;
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
const deflater = createDeflateRaw({ level: 9, memLevel: 9 });
|
|
213
|
+
let compressedSize = 0;
|
|
214
|
+
let spoolOffset = 0;
|
|
215
|
+
let writeQueue = Promise.resolve();
|
|
216
|
+
|
|
217
|
+
deflater.on('data', (chunk: Buffer) => {
|
|
218
|
+
compressedSize += chunk.length;
|
|
219
|
+
writeQueue = writeQueue.then(async () => {
|
|
220
|
+
await writeBuffer(spoolHandle, chunk, spoolOffset);
|
|
221
|
+
spoolOffset += chunk.length;
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const reader = Bun.file(absolutePath).stream().getReader();
|
|
226
|
+
let crc = 0xffffffff;
|
|
227
|
+
let sourceBytes = 0;
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
while (true) {
|
|
231
|
+
const { done, value } = await reader.read();
|
|
232
|
+
if (done) {
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
sourceBytes += value.length;
|
|
236
|
+
crc = updateCrc32(crc, value);
|
|
237
|
+
if (!deflater.write(value)) {
|
|
238
|
+
await new Promise((resolve) => deflater.once('drain', resolve));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
} finally {
|
|
242
|
+
reader.releaseLock();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
await new Promise<void>((resolve, reject) => {
|
|
246
|
+
deflater.on('error', reject);
|
|
247
|
+
deflater.on('end', resolve);
|
|
248
|
+
deflater.end();
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
await writeQueue;
|
|
252
|
+
await spoolHandle.close();
|
|
253
|
+
spoolClosed = true;
|
|
254
|
+
|
|
255
|
+
const finalCrc = (crc ^ 0xffffffff) >>> 0;
|
|
256
|
+
|
|
257
|
+
if (compressedSize < sourceBytes) {
|
|
258
|
+
return {
|
|
259
|
+
compressedSize,
|
|
260
|
+
compressionMethod: ZIP_DEFLATE_METHOD,
|
|
261
|
+
crc32: finalCrc,
|
|
262
|
+
spoolFile,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
await rm(spoolFile, { force: true });
|
|
267
|
+
return {
|
|
268
|
+
compressedSize: sourceBytes,
|
|
269
|
+
compressionMethod: ZIP_STORE_METHOD,
|
|
270
|
+
crc32: finalCrc,
|
|
271
|
+
spoolFile: null,
|
|
272
|
+
};
|
|
273
|
+
} catch (error) {
|
|
274
|
+
if (!spoolClosed) {
|
|
275
|
+
await spoolHandle.close().catch(() => {});
|
|
276
|
+
}
|
|
277
|
+
await rm(spoolFile, { force: true }).catch(() => {});
|
|
278
|
+
throw error;
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
type PreparedBytes = {
|
|
283
|
+
readonly bytes: Uint8Array | null;
|
|
284
|
+
readonly compressedSize: number;
|
|
285
|
+
readonly compressionMethod: number;
|
|
286
|
+
readonly crc32: number;
|
|
287
|
+
readonly spoolFile: string | null;
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
const prepareFileBytes = async (entry: ZipFileEntry, spoolDir: string): Promise<PreparedBytes> => {
|
|
291
|
+
if (entry.kind === 'symlink') {
|
|
292
|
+
const source = new TextEncoder().encode(entry.linkTarget ?? '');
|
|
293
|
+
const crc32 = (updateCrc32(0xffffffff, source) ^ 0xffffffff) >>> 0;
|
|
294
|
+
const compressed = deflateRawSync(source, { level: 9, memLevel: 9 });
|
|
295
|
+
if (compressed.byteLength < source.byteLength) {
|
|
296
|
+
return {
|
|
297
|
+
bytes: compressed,
|
|
298
|
+
compressedSize: compressed.byteLength,
|
|
299
|
+
compressionMethod: ZIP_DEFLATE_METHOD,
|
|
300
|
+
crc32,
|
|
301
|
+
spoolFile: null,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
return {
|
|
305
|
+
bytes: source,
|
|
306
|
+
compressedSize: source.byteLength,
|
|
307
|
+
compressionMethod: ZIP_STORE_METHOD,
|
|
308
|
+
crc32,
|
|
309
|
+
spoolFile: null,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (entry.size > MAX_BUFFERED_DEFLATE_BYTES) {
|
|
314
|
+
const streamed = await streamDeflateFile(entry.absolutePath, spoolDir);
|
|
315
|
+
return {
|
|
316
|
+
bytes: null,
|
|
317
|
+
compressedSize: streamed.compressedSize,
|
|
318
|
+
compressionMethod: streamed.compressionMethod,
|
|
319
|
+
crc32: streamed.crc32,
|
|
320
|
+
spoolFile: streamed.spoolFile,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const source = new Uint8Array(await Bun.file(entry.absolutePath).arrayBuffer());
|
|
325
|
+
const crc32 = (updateCrc32(0xffffffff, source) ^ 0xffffffff) >>> 0;
|
|
326
|
+
const compressed = deflateRawSync(source, { level: 9, memLevel: 9 });
|
|
327
|
+
if (compressed.byteLength < source.byteLength) {
|
|
328
|
+
return {
|
|
329
|
+
bytes: compressed,
|
|
330
|
+
compressedSize: compressed.byteLength,
|
|
331
|
+
compressionMethod: ZIP_DEFLATE_METHOD,
|
|
332
|
+
crc32,
|
|
333
|
+
spoolFile: null,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
return {
|
|
337
|
+
bytes: source,
|
|
338
|
+
compressedSize: source.byteLength,
|
|
339
|
+
compressionMethod: ZIP_STORE_METHOD,
|
|
340
|
+
crc32,
|
|
341
|
+
spoolFile: null,
|
|
342
|
+
};
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
export const createZipFile = async (
|
|
346
|
+
rootDir: string,
|
|
347
|
+
outputFile: string,
|
|
348
|
+
options: ZipCreationOptions = { preserveSymlinks: false, preserveUnixMetadata: false },
|
|
349
|
+
): Promise<void> => {
|
|
350
|
+
const absoluteOutputFile = path.resolve(outputFile);
|
|
351
|
+
const absoluteRootDir = path.resolve(rootDir);
|
|
352
|
+
const outputDir = path.dirname(absoluteOutputFile);
|
|
353
|
+
await mkdir(outputDir, { recursive: true });
|
|
354
|
+
|
|
355
|
+
const tempOutputFile = path.join(
|
|
356
|
+
outputDir,
|
|
357
|
+
`.${path.basename(absoluteOutputFile)}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`,
|
|
358
|
+
);
|
|
359
|
+
const handle = await open(tempOutputFile, 'wx');
|
|
360
|
+
let handleClosed = false;
|
|
361
|
+
|
|
362
|
+
try {
|
|
363
|
+
let offset = 0;
|
|
364
|
+
const writtenEntries: WrittenZipEntry[] = [];
|
|
365
|
+
const files = await collectZipFiles({
|
|
366
|
+
directory: absoluteRootDir,
|
|
367
|
+
excludedAbsolutePaths: new Set([absoluteOutputFile, tempOutputFile]),
|
|
368
|
+
preserveSymlinks: options.preserveSymlinks,
|
|
369
|
+
preserveUnixMetadata: options.preserveUnixMetadata,
|
|
370
|
+
rootDir: absoluteRootDir,
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
validateZipEntryCount(files.length);
|
|
374
|
+
|
|
375
|
+
for (const entry of files) {
|
|
376
|
+
const pathBytes = new TextEncoder().encode(entry.archivePath);
|
|
377
|
+
if (pathBytes.length > ZIP_MAX_UINT16) {
|
|
378
|
+
throw new Error(`Path exceeds classic ZIP limit of ${ZIP_MAX_UINT16} bytes: ${entry.archivePath}`);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
validateZipEntrySize(entry.size, entry.archivePath);
|
|
382
|
+
|
|
383
|
+
const prepared = await prepareFileBytes(entry, outputDir);
|
|
384
|
+
try {
|
|
385
|
+
validateZipEntrySize(prepared.compressedSize, entry.archivePath);
|
|
386
|
+
|
|
387
|
+
const localHeaderOffset = offset;
|
|
388
|
+
|
|
389
|
+
if (localHeaderOffset > ZIP_MAX_UINT32) {
|
|
390
|
+
throw new Error(`Local header offset exceeds classic ZIP limit (4 GB): ${localHeaderOffset}`);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
offset = await writeZipLocalHeader(
|
|
394
|
+
handle,
|
|
395
|
+
entry,
|
|
396
|
+
pathBytes,
|
|
397
|
+
prepared.crc32,
|
|
398
|
+
prepared.compressedSize,
|
|
399
|
+
prepared.compressionMethod,
|
|
400
|
+
offset,
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
if (prepared.bytes) {
|
|
404
|
+
assertWithinZip32Limit(offset, prepared.bytes.length, `payload for ${entry.archivePath}`);
|
|
405
|
+
offset = await writeBuffer(handle, prepared.bytes, offset);
|
|
406
|
+
} else if (prepared.spoolFile) {
|
|
407
|
+
const reader = Bun.file(prepared.spoolFile).stream().getReader();
|
|
408
|
+
try {
|
|
409
|
+
while (true) {
|
|
410
|
+
const { done, value } = await reader.read();
|
|
411
|
+
if (done) {
|
|
412
|
+
break;
|
|
413
|
+
}
|
|
414
|
+
assertWithinZip32Limit(offset, value.length, `payload chunk for ${entry.archivePath}`);
|
|
415
|
+
offset = await writeBuffer(handle, value, offset);
|
|
416
|
+
}
|
|
417
|
+
} finally {
|
|
418
|
+
reader.releaseLock();
|
|
419
|
+
}
|
|
420
|
+
} else {
|
|
421
|
+
const reader = Bun.file(entry.absolutePath).stream().getReader();
|
|
422
|
+
try {
|
|
423
|
+
while (true) {
|
|
424
|
+
const { done, value } = await reader.read();
|
|
425
|
+
if (done) {
|
|
426
|
+
break;
|
|
427
|
+
}
|
|
428
|
+
assertWithinZip32Limit(offset, value.length, `payload chunk for ${entry.archivePath}`);
|
|
429
|
+
offset = await writeBuffer(handle, value, offset);
|
|
430
|
+
}
|
|
431
|
+
} finally {
|
|
432
|
+
reader.releaseLock();
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
writtenEntries.push({
|
|
437
|
+
...entry,
|
|
438
|
+
compressedSize: prepared.compressedSize,
|
|
439
|
+
compressionMethod: prepared.compressionMethod,
|
|
440
|
+
crc32: prepared.crc32,
|
|
441
|
+
localHeaderOffset,
|
|
442
|
+
pathBytes,
|
|
443
|
+
});
|
|
444
|
+
} finally {
|
|
445
|
+
if (prepared.spoolFile) {
|
|
446
|
+
await rm(prepared.spoolFile, { force: true }).catch(() => {});
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const centralDirectoryOffset = offset;
|
|
452
|
+
if (centralDirectoryOffset > ZIP_MAX_UINT32) {
|
|
453
|
+
throw new Error(`Central directory offset exceeds classic ZIP limit (4 GB): ${centralDirectoryOffset}`);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
for (const entry of writtenEntries) {
|
|
457
|
+
assertWithinZip32Limit(offset, 46 + entry.pathBytes.length, `central directory entry for ${entry.archivePath}`);
|
|
458
|
+
const header = Buffer.alloc(46);
|
|
459
|
+
header.writeUInt32LE(0x02014b50, 0);
|
|
460
|
+
header.writeUInt16LE(options.preserveUnixMetadata ? ZIP_UNIX_CREATOR_VERSION : 20, 4);
|
|
461
|
+
header.writeUInt16LE(20, 6);
|
|
462
|
+
header.writeUInt16LE(ZIP_UTF8_FLAG, 8);
|
|
463
|
+
header.writeUInt16LE(entry.compressionMethod, 10);
|
|
464
|
+
header.writeUInt16LE(0, 12);
|
|
465
|
+
header.writeUInt16LE(ZIP_DOS_DATE_1980_01_01, 14);
|
|
466
|
+
header.writeUInt32LE(entry.crc32, 16);
|
|
467
|
+
header.writeUInt32LE(entry.compressedSize, 20);
|
|
468
|
+
header.writeUInt32LE(entry.size, 24);
|
|
469
|
+
header.writeUInt16LE(entry.pathBytes.length, 28);
|
|
470
|
+
header.writeUInt16LE(0, 30);
|
|
471
|
+
header.writeUInt16LE(0, 32);
|
|
472
|
+
header.writeUInt16LE(0, 34);
|
|
473
|
+
header.writeUInt16LE(0, 36);
|
|
474
|
+
const externalAttributes = options.preserveUnixMetadata
|
|
475
|
+
? ((((entry.mode ?? 0o100644) & 0xffff) << 16) >>> 0)
|
|
476
|
+
: 0;
|
|
477
|
+
header.writeUInt32LE(externalAttributes, 38);
|
|
478
|
+
header.writeUInt32LE(entry.localHeaderOffset, 42);
|
|
479
|
+
offset = await writeBuffer(handle, header, offset);
|
|
480
|
+
offset = await writeBuffer(handle, entry.pathBytes, offset);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const centralDirectorySize = offset - centralDirectoryOffset;
|
|
484
|
+
if (centralDirectorySize > ZIP_MAX_UINT32) {
|
|
485
|
+
throw new Error(`Central directory size exceeds classic ZIP limit (4 GB): ${centralDirectorySize}`);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
assertWithinZip32Limit(offset, 22, 'end of central directory record');
|
|
489
|
+
const endHeader = Buffer.alloc(22);
|
|
490
|
+
endHeader.writeUInt32LE(0x06054b50, 0);
|
|
491
|
+
endHeader.writeUInt16LE(0, 4);
|
|
492
|
+
endHeader.writeUInt16LE(0, 6);
|
|
493
|
+
endHeader.writeUInt16LE(writtenEntries.length, 8);
|
|
494
|
+
endHeader.writeUInt16LE(writtenEntries.length, 10);
|
|
495
|
+
endHeader.writeUInt32LE(centralDirectorySize, 12);
|
|
496
|
+
endHeader.writeUInt32LE(centralDirectoryOffset, 16);
|
|
497
|
+
endHeader.writeUInt16LE(0, 20);
|
|
498
|
+
await writeBuffer(handle, endHeader, offset);
|
|
499
|
+
|
|
500
|
+
await handle.close();
|
|
501
|
+
handleClosed = true;
|
|
502
|
+
|
|
503
|
+
await rename(tempOutputFile, absoluteOutputFile);
|
|
504
|
+
} catch (error) {
|
|
505
|
+
if (!handleClosed) {
|
|
506
|
+
await handle.close().catch(() => {});
|
|
507
|
+
}
|
|
508
|
+
await rm(tempOutputFile, { force: true }).catch(() => {});
|
|
509
|
+
throw error;
|
|
510
|
+
}
|
|
511
|
+
};
|
package/src/cli-options.ts
DELETED
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
export type CliOptions = {
|
|
2
|
-
readonly excludes: readonly string[];
|
|
3
|
-
readonly filters: readonly string[];
|
|
4
|
-
readonly gitHandoff: boolean;
|
|
5
|
-
readonly includes: readonly string[];
|
|
6
|
-
readonly outputFile: string | null;
|
|
7
|
-
readonly profile: string | null;
|
|
8
|
-
readonly reportFile: string | null;
|
|
9
|
-
readonly showHelp: boolean;
|
|
10
|
-
readonly targetDir: string;
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
export const parseCliArgs = (args: readonly string[]): CliOptions => {
|
|
14
|
-
const excludes: string[] = [];
|
|
15
|
-
const filters: string[] = [];
|
|
16
|
-
const includes: string[] = [];
|
|
17
|
-
let gitHandoff = false;
|
|
18
|
-
let outputFile: string | null = null;
|
|
19
|
-
let profile: string | null = null;
|
|
20
|
-
let reportFile: string | null = null;
|
|
21
|
-
let showHelp = false;
|
|
22
|
-
let targetDir = process.cwd();
|
|
23
|
-
|
|
24
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
25
|
-
const argument = args[index]!;
|
|
26
|
-
if (argument === '--help' || argument === '-h') {
|
|
27
|
-
showHelp = true;
|
|
28
|
-
} else if (argument === '--git-handoff') {
|
|
29
|
-
gitHandoff = true;
|
|
30
|
-
} else if (argument.startsWith('--profile=')) {
|
|
31
|
-
profile = argument.slice(10) || null;
|
|
32
|
-
} else if (argument === '--profile') {
|
|
33
|
-
index += 1;
|
|
34
|
-
profile = args[index] ?? null;
|
|
35
|
-
} else if (argument.startsWith('--exclude=')) {
|
|
36
|
-
excludes.push(argument.slice(10));
|
|
37
|
-
} else if (argument === '--exclude') {
|
|
38
|
-
index += 1;
|
|
39
|
-
if (args[index]) {
|
|
40
|
-
excludes.push(args[index]!);
|
|
41
|
-
}
|
|
42
|
-
} else if (argument.startsWith('--output=')) {
|
|
43
|
-
outputFile = argument.slice(9);
|
|
44
|
-
} else if (argument === '--output' || argument === '-o') {
|
|
45
|
-
index += 1;
|
|
46
|
-
outputFile = args[index] ?? null;
|
|
47
|
-
} else if (argument.startsWith('--include=')) {
|
|
48
|
-
includes.push(argument.slice(10));
|
|
49
|
-
} else if (argument === '--include' || argument === '-i') {
|
|
50
|
-
index += 1;
|
|
51
|
-
if (args[index]) {
|
|
52
|
-
includes.push(args[index]!);
|
|
53
|
-
}
|
|
54
|
-
} else if (argument.startsWith('--filter=')) {
|
|
55
|
-
filters.push(argument.slice(9));
|
|
56
|
-
} else if (argument === '--filter' || argument === '-f') {
|
|
57
|
-
index += 1;
|
|
58
|
-
if (args[index]) {
|
|
59
|
-
filters.push(args[index]!);
|
|
60
|
-
}
|
|
61
|
-
} else if (argument.startsWith('--report=')) {
|
|
62
|
-
reportFile = argument.slice(9);
|
|
63
|
-
} else if (argument === '--report') {
|
|
64
|
-
index += 1;
|
|
65
|
-
reportFile = args[index] ?? null;
|
|
66
|
-
} else if (!argument.startsWith('-')) {
|
|
67
|
-
targetDir = argument;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return { excludes, filters, gitHandoff, includes, outputFile, profile, reportFile, showHelp, targetDir };
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
export const printUsage = (): void => {
|
|
75
|
-
console.log(`
|
|
76
|
-
Usage: acker-dacker [targetRepoPath] [options]
|
|
77
|
-
|
|
78
|
-
Options:
|
|
79
|
-
--git-handoff Use Git's tracked + eligible-untracked set and preserve modes/symlinks
|
|
80
|
-
--profile <name> Select a named Git handoff profile (currently rust-stage3; requires --git-handoff)
|
|
81
|
-
--filter, -f <pattern> Include ONLY files matching pattern (e.g. *.md, *.ts; can be specified multiple times)
|
|
82
|
-
--include, -i <path> Additional folder path or repo name to include (can be specified multiple times)
|
|
83
|
-
--exclude <path|glob> Repeatable root-relative exclusion (only with --git-handoff)
|
|
84
|
-
--output, -o <path> Output zip file path (default: ./<targetRepoName>-pack.zip)
|
|
85
|
-
--report <path> Write a content-free Git handoff JSON report (only with --git-handoff)
|
|
86
|
-
--help, -h Show this help message
|
|
87
|
-
|
|
88
|
-
Examples:
|
|
89
|
-
acker-dacker ../ushman --filter "*.md"
|
|
90
|
-
acker-dacker ../ushman --include ../ushman-spector -f "*.md" -f "*.ts"
|
|
91
|
-
acker-dacker ../kodeguard --output=./kodeguard.zip
|
|
92
|
-
`);
|
|
93
|
-
};
|