ezshare-cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,75 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useEffect } from 'react';
3
+ import { Box, Text, useInput } from 'ink';
4
+ import { dirname } from 'node:path';
5
+ import { readDirectory, formatFileSize } from '../utils/fileSystem.js';
6
+ export function FileBrowser({ initialPath = process.cwd(), onSelect, onCancel, }) {
7
+ const [currentPath, setCurrentPath] = useState(initialPath);
8
+ const [files, setFiles] = useState([]);
9
+ const [selectedIndex, setSelectedIndex] = useState(0);
10
+ const [loading, setLoading] = useState(true);
11
+ useEffect(() => {
12
+ loadDirectory(currentPath);
13
+ }, [currentPath]);
14
+ const loadDirectory = async (path) => {
15
+ setLoading(true);
16
+ try {
17
+ const entries = await readDirectory(path);
18
+ // Add ".." entry to go up one level (unless we're at root)
19
+ const parentDir = dirname(path);
20
+ if (parentDir !== path) {
21
+ entries.unshift({
22
+ name: '..',
23
+ path: parentDir,
24
+ isDirectory: true,
25
+ });
26
+ }
27
+ setFiles(entries);
28
+ setSelectedIndex(0);
29
+ }
30
+ catch (error) {
31
+ console.error('Error loading directory:', error);
32
+ setFiles([]);
33
+ }
34
+ finally {
35
+ setLoading(false);
36
+ }
37
+ };
38
+ useInput((input, key) => {
39
+ if (loading)
40
+ return; // Ignore input while loading
41
+ if (key.upArrow) {
42
+ setSelectedIndex((prev) => Math.max(0, prev - 1));
43
+ }
44
+ else if (key.downArrow) {
45
+ setSelectedIndex((prev) => Math.min(files.length - 1, prev + 1));
46
+ }
47
+ else if (key.return) {
48
+ const selected = files[selectedIndex];
49
+ if (selected) {
50
+ if (selected.isDirectory) {
51
+ // Navigate into directory
52
+ setCurrentPath(selected.path);
53
+ }
54
+ else {
55
+ // File selected!
56
+ onSelect(selected.path);
57
+ }
58
+ }
59
+ }
60
+ else if (key.escape) {
61
+ onCancel();
62
+ }
63
+ else if (input === 's' && selectedIndex < files.length) {
64
+ // Quick shortcut: 's' to select current file/folder for sending
65
+ const selected = files[selectedIndex];
66
+ if (selected && selected.name !== '..') {
67
+ onSelect(selected.path);
68
+ }
69
+ }
70
+ });
71
+ if (loading) {
72
+ return (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { children: "Loading directory..." }) }));
73
+ }
74
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "cyan", children: ["Current: ", currentPath] }), _jsx(Text, { dimColor: true, children: "\u2191\u2193 to navigate | Enter to open/select | 's' to select | Esc to cancel" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: files.length === 0 ? (_jsx(Text, { dimColor: true, children: "Empty directory" })) : (files.map((file, idx) => (_jsx(Box, { children: _jsxs(Text, { color: idx === selectedIndex ? 'green' : undefined, children: [idx === selectedIndex ? '> ' : ' ', file.isDirectory ? '📁 ' : '📄 ', file.name, file.size !== undefined ? ` (${formatFileSize(file.size)})` : ''] }) }, file.path)))) }), files.length > 10 && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["Showing ", selectedIndex + 1, " of ", files.length, " items"] }) }))] }));
75
+ }
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text, useInput } from 'ink';
3
+ export function HelpScreen({ onBack }) {
4
+ useInput((input, key) => {
5
+ if (key.escape || input === 'q') {
6
+ onBack();
7
+ }
8
+ });
9
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "EzShare CLI - Help" }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Slash Commands:" }), _jsx(Text, { children: " /ezshare - Start interactive file transfer" }), _jsx(Text, { children: " /help - Show this help screen" }), _jsx(Text, { children: " /exit - Exit the shell" })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Direct CLI Mode:" }), _jsx(Text, { children: " hyperstream send <path> - Send file/folder" }), _jsx(Text, { children: " hyperstream receive <key> [-o dir] - Receive file/folder" })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Keyboard Shortcuts:" }), _jsx(Text, { children: " \u2191\u2193 - Navigate menus/files" }), _jsx(Text, { children: " Enter - Select/Confirm" }), _jsx(Text, { children: " Esc - Cancel/Go back" }), _jsx(Text, { children: " q - Quit (from command mode)" })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "How It Works:" }), _jsx(Text, { children: " 1. Sender selects a file/folder to share" }), _jsx(Text, { children: " 2. A unique share key is generated" }), _jsx(Text, { children: " 3. Receiver enters the share key" }), _jsx(Text, { children: " 4. Files are transferred peer-to-peer (P2P)" }), _jsx(Text, { children: " 5. Data is encrypted end-to-end with AES-256-GCM" })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Press Esc or 'q' to return" }) })] }));
10
+ }
@@ -0,0 +1,9 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { Select } from '@inkjs/ui';
4
+ export function MainMenu({ onSelect }) {
5
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "What would you like to do?" }), _jsx(Box, { marginTop: 1, children: _jsx(Select, { options: [
6
+ { label: '📤 Send file or folder', value: 'send' },
7
+ { label: '📥 Receive file or folder', value: 'receive' },
8
+ ], onChange: (value) => onSelect(value) }) })] }));
9
+ }
@@ -0,0 +1,88 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { Box, Text, useInput } from 'ink';
4
+ import { TextInput } from '@inkjs/ui';
5
+ import { HelpScreen } from './HelpScreen.js';
6
+ import { MainMenu } from './MainMenu.js';
7
+ import { FileBrowser } from './FileBrowser.js';
8
+ import { SendCommand } from '../commands/send.js';
9
+ import { ReceiveCommand } from '../commands/receive.js';
10
+ export function Shell() {
11
+ const [state, setState] = useState({
12
+ mode: 'command',
13
+ history: [],
14
+ });
15
+ const handleCommand = (cmd) => {
16
+ const trimmed = cmd.trim();
17
+ // Add to history
18
+ setState((prev) => ({
19
+ ...prev,
20
+ history: [...prev.history, trimmed],
21
+ }));
22
+ // Process slash commands
23
+ if (trimmed === '/ezshare') {
24
+ setState((prev) => ({ ...prev, mode: 'main-menu' }));
25
+ }
26
+ else if (trimmed === '/help') {
27
+ setState((prev) => ({ ...prev, mode: 'help' }));
28
+ }
29
+ else if (trimmed === '/exit') {
30
+ process.exit(0);
31
+ }
32
+ else if (trimmed.startsWith('/')) {
33
+ // Unknown command
34
+ setState((prev) => ({
35
+ ...prev,
36
+ history: [...prev.history, `Unknown command: ${trimmed}`],
37
+ }));
38
+ }
39
+ };
40
+ const handleMenuSelect = (choice) => {
41
+ if (choice === 'send') {
42
+ setState((prev) => ({ ...prev, mode: 'file-browser', transferMode: 'send' }));
43
+ }
44
+ else {
45
+ setState((prev) => ({ ...prev, mode: 'receive-key', transferMode: 'receive' }));
46
+ }
47
+ };
48
+ const handleFileSelect = (path) => {
49
+ setState((prev) => ({
50
+ ...prev,
51
+ mode: 'transfer',
52
+ selectedPath: path,
53
+ }));
54
+ };
55
+ const handleKeyInput = (key) => {
56
+ setState((prev) => ({
57
+ ...prev,
58
+ mode: 'transfer',
59
+ transferKey: key,
60
+ }));
61
+ };
62
+ const handleTransferComplete = () => {
63
+ setState((prev) => ({ ...prev, mode: 'done' }));
64
+ };
65
+ const handleTransferError = (error) => {
66
+ console.error('Transfer error:', error);
67
+ setState((prev) => ({ ...prev, mode: 'done' }));
68
+ };
69
+ const handleCancel = () => {
70
+ setState((prev) => ({ ...prev, mode: 'command' }));
71
+ };
72
+ const handleHelpBack = () => {
73
+ setState((prev) => ({ ...prev, mode: 'command' }));
74
+ };
75
+ // Global keyboard shortcuts
76
+ useInput((input, key) => {
77
+ if (input === 'q' && state.mode === 'command') {
78
+ process.exit(0);
79
+ }
80
+ if (key.escape && state.mode !== 'transfer') {
81
+ setState((prev) => ({ ...prev, mode: 'command' }));
82
+ }
83
+ if (key.escape && state.mode === 'done') {
84
+ setState((prev) => ({ ...prev, mode: 'command' }));
85
+ }
86
+ });
87
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "EzShare CLI - P2P File Transfer" }), _jsx(Text, { dimColor: true, children: "Type /ezshare to start or /help for commands" }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [state.mode === 'command' && (_jsx(TextInput, { placeholder: "Enter command...", onSubmit: handleCommand })), state.mode === 'main-menu' && _jsx(MainMenu, { onSelect: handleMenuSelect }), state.mode === 'file-browser' && (_jsx(FileBrowser, { onSelect: handleFileSelect, onCancel: handleCancel })), state.mode === 'receive-key' && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "Paste the share key you received:" }), _jsx(TextInput, { placeholder: "Enter share key...", onSubmit: handleKeyInput }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Press Esc to cancel" }) })] })), state.mode === 'transfer' && state.transferMode === 'send' && state.selectedPath && (_jsx(SendCommand, { path: state.selectedPath, onComplete: handleTransferComplete, onError: handleTransferError })), state.mode === 'transfer' && state.transferMode === 'receive' && state.transferKey && (_jsx(ReceiveCommand, { shareKey: state.transferKey, onComplete: handleTransferComplete, onError: handleTransferError })), state.mode === 'done' && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "green", bold: true, children: "\u2713 Transfer complete!" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Press Esc to return to command mode" }) })] })), state.mode === 'help' && _jsx(HelpScreen, { onBack: handleHelpBack })] }), state.history.length > 0 && state.mode === 'command' && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "Recent commands:" }), state.history.slice(-3).map((cmd, idx) => (_jsxs(Text, { dimColor: true, children: [' ', cmd] }, `cmd-${idx}-${cmd}`)))] }))] }));
88
+ }
@@ -0,0 +1,6 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { Spinner, ProgressBar } from '@inkjs/ui';
4
+ export function TransferUI({ mode, fileName, fileSize, progress, shareKey, }) {
5
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Text, { bold: true, color: mode === 'send' ? 'green' : 'blue', children: [mode === 'send' ? '📤 Sending' : '📥 Receiving', " ", fileName || 'file'] }), mode === 'send' && shareKey && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "cyan", children: "Share this key with receiver:" }), _jsx(Text, { bold: true, children: shareKey }), progress === 0 && (_jsx(Box, { marginTop: 1, children: _jsx(Spinner, { label: "Waiting for peer to connect..." }) }))] })), mode === 'receive' && progress === 0 && (_jsx(Box, { marginTop: 1, children: _jsx(Spinner, { label: "Connecting to peer..." }) })), progress > 0 && progress < 100 && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(ProgressBar, { value: progress }), _jsxs(Text, { children: [" ", progress, "%"] })] })), progress === 100 && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "green", bold: true, children: "\u2713 Transfer complete!" }), _jsx(Text, { dimColor: true, children: "Press Esc to return to command mode" })] }))] }));
6
+ }
@@ -0,0 +1,354 @@
1
+ /**
2
+ * Compression utilities for HyperStream
3
+ *
4
+ * Uses zstd for streaming compression with adaptive detection
5
+ * to skip already-compressed files.
6
+ *
7
+ * Stream format:
8
+ * [1 byte: flag] [payload...]
9
+ *
10
+ * flag = 0x00: raw data (no compression applied)
11
+ * flag = 0x01: zstd compressed data
12
+ *
13
+ * The flag makes streams self-describing, allowing the receiver
14
+ * to automatically detect and handle both cases.
15
+ */
16
+ import { Transform } from 'node:stream';
17
+ import { extname } from 'node:path';
18
+ import { execSync } from 'node:child_process';
19
+ import { compress, decompress } from 'simple-zstd';
20
+ // Protocol flags
21
+ const FLAG_RAW = 0x00;
22
+ const FLAG_COMPRESSED = 0x01;
23
+ // Default zstd compression level (1-22, 3 is default, good balance)
24
+ const COMPRESSION_LEVEL = 3;
25
+ /**
26
+ * File extensions that are already compressed.
27
+ * Compressing these again wastes CPU with minimal size benefit.
28
+ */
29
+ const COMPRESSED_EXTENSIONS = new Set([
30
+ // Archives
31
+ '.zip',
32
+ '.gz',
33
+ '.bz2',
34
+ '.xz',
35
+ '.7z',
36
+ '.rar',
37
+ '.zst',
38
+ '.lz4',
39
+ '.lzma',
40
+ '.tgz',
41
+ '.tbz2',
42
+ // Images
43
+ '.jpg',
44
+ '.jpeg',
45
+ '.png',
46
+ '.gif',
47
+ '.webp',
48
+ '.avif',
49
+ '.heic',
50
+ '.heif',
51
+ '.ico',
52
+ // Video
53
+ '.mp4',
54
+ '.mkv',
55
+ '.avi',
56
+ '.mov',
57
+ '.webm',
58
+ '.m4v',
59
+ '.wmv',
60
+ '.flv',
61
+ '.m2ts',
62
+ // Audio
63
+ '.mp3',
64
+ '.aac',
65
+ '.flac',
66
+ '.ogg',
67
+ '.m4a',
68
+ '.wma',
69
+ '.opus',
70
+ // Documents (Office formats use ZIP internally)
71
+ '.pdf',
72
+ '.docx',
73
+ '.xlsx',
74
+ '.pptx',
75
+ '.odt',
76
+ '.ods',
77
+ '.odp',
78
+ '.epub',
79
+ // Web assets
80
+ '.woff',
81
+ '.woff2',
82
+ '.br', // brotli
83
+ ]);
84
+ /**
85
+ * Check if the system has zstd installed
86
+ */
87
+ export function isZstdAvailable() {
88
+ try {
89
+ execSync('zstd --version', { stdio: 'ignore' });
90
+ return true;
91
+ }
92
+ catch {
93
+ return false;
94
+ }
95
+ }
96
+ /**
97
+ * Check if a file should be compressed based on its extension.
98
+ *
99
+ * Returns false for files that are already compressed (images, videos,
100
+ * archives, etc.) since re-compressing them wastes CPU with minimal benefit.
101
+ *
102
+ * @param filePath - Path to the file (only extension is checked)
103
+ * @returns true if the file should be compressed
104
+ */
105
+ export function shouldCompress(filePath) {
106
+ const ext = extname(filePath).toLowerCase();
107
+ return !COMPRESSED_EXTENSIONS.has(ext);
108
+ }
109
+ /**
110
+ * Create a transform that prepends a flag byte to the stream.
111
+ */
112
+ function createFlagPrepender(flag) {
113
+ let flagSent = false;
114
+ return new Transform({
115
+ transform(chunk, _encoding, callback) {
116
+ if (!flagSent) {
117
+ this.push(Buffer.from([flag]));
118
+ flagSent = true;
119
+ }
120
+ this.push(chunk);
121
+ callback();
122
+ },
123
+ });
124
+ }
125
+ /**
126
+ * Create a compression stream.
127
+ *
128
+ * When compress=true:
129
+ * - Outputs [0x01][zstd-compressed-data]
130
+ * - Uses zstd for compression
131
+ *
132
+ * When compress=false:
133
+ * - Outputs [0x00][raw-data]
134
+ * - Simple passthrough with flag prefix
135
+ *
136
+ * @param shouldCompress - Whether to actually compress (default: true)
137
+ * @returns Promise resolving to a Transform stream
138
+ * @throws Error if zstd is not available and compression is requested
139
+ */
140
+ export async function createCompressStream(shouldCompress = true) {
141
+ if (!shouldCompress) {
142
+ // Simple passthrough with raw flag
143
+ return createFlagPrepender(FLAG_RAW);
144
+ }
145
+ // Check zstd availability
146
+ if (!isZstdAvailable()) {
147
+ throw new Error('zstd is not installed. Please install zstd:\n' +
148
+ ' Ubuntu/Debian: sudo apt install zstd\n' +
149
+ ' macOS: brew install zstd\n' +
150
+ ' Windows: choco install zstd');
151
+ }
152
+ // Get zstd compression stream
153
+ const zstdStream = await compress(COMPRESSION_LEVEL);
154
+ // Track state
155
+ let flagSent = false;
156
+ const pendingData = [];
157
+ let zstdFinished = false;
158
+ let flushCallback = null;
159
+ // Collect compressed output from zstd
160
+ zstdStream.on('data', (chunk) => {
161
+ pendingData.push(chunk);
162
+ });
163
+ zstdStream.on('end', () => {
164
+ zstdFinished = true;
165
+ if (flushCallback) {
166
+ const cb = flushCallback;
167
+ flushCallback = null;
168
+ cb();
169
+ }
170
+ });
171
+ zstdStream.on('error', (err) => {
172
+ if (flushCallback) {
173
+ const cb = flushCallback;
174
+ flushCallback = null;
175
+ cb(err);
176
+ }
177
+ });
178
+ const wrapper = new Transform({
179
+ transform(chunk, _encoding, callback) {
180
+ // Write input to zstd
181
+ zstdStream.write(chunk, (err) => {
182
+ if (err) {
183
+ callback(err);
184
+ return;
185
+ }
186
+ // Push any pending compressed data
187
+ while (pendingData.length > 0) {
188
+ const data = pendingData.shift();
189
+ if (!flagSent) {
190
+ this.push(Buffer.from([FLAG_COMPRESSED]));
191
+ flagSent = true;
192
+ }
193
+ this.push(data);
194
+ }
195
+ callback();
196
+ });
197
+ },
198
+ flush(callback) {
199
+ // End the zstd stream and wait for all data
200
+ const pushRemaining = () => {
201
+ while (pendingData.length > 0) {
202
+ const data = pendingData.shift();
203
+ if (!flagSent) {
204
+ this.push(Buffer.from([FLAG_COMPRESSED]));
205
+ flagSent = true;
206
+ }
207
+ this.push(data);
208
+ }
209
+ // Handle edge case: empty input
210
+ if (!flagSent) {
211
+ this.push(Buffer.from([FLAG_COMPRESSED]));
212
+ }
213
+ callback();
214
+ };
215
+ // Wait for 'end' event which fires after all 'data' events
216
+ zstdStream.once('end', pushRemaining);
217
+ zstdStream.end();
218
+ },
219
+ });
220
+ return wrapper;
221
+ }
222
+ /**
223
+ * Create a decompression stream.
224
+ *
225
+ * Automatically detects the compression format based on the first byte:
226
+ * - 0x00: passthrough (no decompression)
227
+ * - 0x01: zstd decompression
228
+ *
229
+ * @returns Promise resolving to a Transform stream
230
+ */
231
+ export async function createDecompressStream() {
232
+ let flag = null;
233
+ let zstdStream = null;
234
+ let buffer = Buffer.alloc(0);
235
+ let destroyed = false;
236
+ const pendingOutput = [];
237
+ const wrapper = new Transform({
238
+ transform(chunk, _encoding, callback) {
239
+ if (destroyed) {
240
+ return callback();
241
+ }
242
+ buffer = Buffer.concat([buffer, chunk]);
243
+ // Read flag byte if not yet read
244
+ if (flag === null) {
245
+ if (buffer.length < 1) {
246
+ return callback(); // Need more data
247
+ }
248
+ flag = buffer[0];
249
+ buffer = buffer.subarray(1);
250
+ // Validate flag
251
+ if (flag !== FLAG_RAW && flag !== FLAG_COMPRESSED) {
252
+ return callback(new Error(`Invalid compression flag: 0x${flag.toString(16)}`));
253
+ }
254
+ // Initialize zstd if needed
255
+ if (flag === FLAG_COMPRESSED) {
256
+ if (!isZstdAvailable()) {
257
+ return callback(new Error('zstd is not installed but data is compressed'));
258
+ }
259
+ // Synchronously set up - decompress() returns Promise but we handle async carefully
260
+ decompress()
261
+ .then((stream) => {
262
+ if (destroyed) {
263
+ stream.destroy();
264
+ return callback();
265
+ }
266
+ zstdStream = stream;
267
+ // Collect decompressed output
268
+ stream.on('data', (data) => {
269
+ pendingOutput.push(data);
270
+ });
271
+ stream.on('error', (err) => {
272
+ wrapper.destroy(err);
273
+ });
274
+ // Write any buffered compressed data
275
+ if (buffer.length > 0) {
276
+ stream.write(buffer, (err) => {
277
+ buffer = Buffer.alloc(0);
278
+ // Push any output that arrived
279
+ while (pendingOutput.length > 0) {
280
+ this.push(pendingOutput.shift());
281
+ }
282
+ callback(err || undefined);
283
+ });
284
+ }
285
+ else {
286
+ callback();
287
+ }
288
+ })
289
+ .catch((err) => {
290
+ callback(err);
291
+ });
292
+ return; // Wait for async initialization
293
+ }
294
+ }
295
+ // Process buffered data
296
+ if (buffer.length > 0) {
297
+ if (flag === FLAG_COMPRESSED && zstdStream) {
298
+ zstdStream.write(buffer, (err) => {
299
+ buffer = Buffer.alloc(0);
300
+ // Push any output that arrived
301
+ while (pendingOutput.length > 0) {
302
+ this.push(pendingOutput.shift());
303
+ }
304
+ if (err && !destroyed) {
305
+ callback(err);
306
+ }
307
+ else {
308
+ callback();
309
+ }
310
+ });
311
+ }
312
+ else if (flag === FLAG_RAW) {
313
+ this.push(buffer);
314
+ buffer = Buffer.alloc(0);
315
+ callback();
316
+ }
317
+ else {
318
+ callback();
319
+ }
320
+ }
321
+ else {
322
+ // Push any pending output
323
+ while (pendingOutput.length > 0) {
324
+ this.push(pendingOutput.shift());
325
+ }
326
+ callback();
327
+ }
328
+ },
329
+ flush(callback) {
330
+ if (zstdStream) {
331
+ // Wait for 'end' event which fires after all 'data' events
332
+ zstdStream.once('end', () => {
333
+ // Push any remaining output
334
+ while (pendingOutput.length > 0) {
335
+ this.push(pendingOutput.shift());
336
+ }
337
+ callback();
338
+ });
339
+ zstdStream.end();
340
+ }
341
+ else {
342
+ callback();
343
+ }
344
+ },
345
+ destroy(err, callback) {
346
+ destroyed = true;
347
+ if (zstdStream) {
348
+ zstdStream.destroy();
349
+ }
350
+ callback(err);
351
+ },
352
+ });
353
+ return wrapper;
354
+ }