typescript-virtual-container 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.yml +50 -0
  2. package/.github/ISSUE_TEMPLATE/feature_request.yml +31 -0
  3. package/.github/dependabot.yml +27 -0
  4. package/.github/pull_request_template.md +21 -0
  5. package/.github/workflows/create-pull-request.yml +83 -0
  6. package/.github/workflows/test-battery.yml +57 -0
  7. package/CHANGELOG.md +27 -0
  8. package/CODE_OF_CONDUCT.md +39 -0
  9. package/CONTRIBUTING.md +59 -0
  10. package/LICENSE +21 -0
  11. package/README.md +1283 -0
  12. package/SECURITY.md +33 -0
  13. package/biome.json +20 -0
  14. package/bun.lock +99 -0
  15. package/package.json +38 -0
  16. package/src/SSHMimic/client.ts +248 -0
  17. package/src/SSHMimic/commands/adduser.ts +22 -0
  18. package/src/SSHMimic/commands/cat.ts +16 -0
  19. package/src/SSHMimic/commands/cd.ts +20 -0
  20. package/src/SSHMimic/commands/clear.ts +7 -0
  21. package/src/SSHMimic/commands/curl.ts +27 -0
  22. package/src/SSHMimic/commands/deluser.ts +19 -0
  23. package/src/SSHMimic/commands/exit.ts +7 -0
  24. package/src/SSHMimic/commands/help.ts +9 -0
  25. package/src/SSHMimic/commands/helpers.ts +137 -0
  26. package/src/SSHMimic/commands/hostname.ts +7 -0
  27. package/src/SSHMimic/commands/htop.ts +13 -0
  28. package/src/SSHMimic/commands/index.ts +120 -0
  29. package/src/SSHMimic/commands/ls.ts +14 -0
  30. package/src/SSHMimic/commands/mkdir.ts +17 -0
  31. package/src/SSHMimic/commands/nano.ts +30 -0
  32. package/src/SSHMimic/commands/pwd.ts +7 -0
  33. package/src/SSHMimic/commands/rm.ts +26 -0
  34. package/src/SSHMimic/commands/su.ts +31 -0
  35. package/src/SSHMimic/commands/sudo.ts +90 -0
  36. package/src/SSHMimic/commands/touch.ts +20 -0
  37. package/src/SSHMimic/commands/tree.ts +11 -0
  38. package/src/SSHMimic/commands/wget.ts +33 -0
  39. package/src/SSHMimic/commands/who.ts +18 -0
  40. package/src/SSHMimic/commands/whoami.ts +7 -0
  41. package/src/SSHMimic/exec.ts +37 -0
  42. package/src/SSHMimic/hostKey.ts +21 -0
  43. package/src/SSHMimic/index.ts +203 -0
  44. package/src/SSHMimic/loginFormat.ts +10 -0
  45. package/src/SSHMimic/prompt.ts +14 -0
  46. package/src/SSHMimic/shell.ts +740 -0
  47. package/src/SSHMimic/users.ts +336 -0
  48. package/src/VirtualFileSystem.ts +420 -0
  49. package/src/index.ts +34 -0
  50. package/src/standalone.ts +14 -0
  51. package/src/types/commands.ts +98 -0
  52. package/src/types/streams.ts +32 -0
  53. package/src/types/tar-stream.d.ts +38 -0
  54. package/src/types/vfs.ts +81 -0
  55. package/src/vfs/archive.ts +74 -0
  56. package/src/vfs/internalTypes.ts +19 -0
  57. package/src/vfs/path.ts +74 -0
  58. package/src/vfs/snapshot.ts +84 -0
  59. package/src/vfs/tree.ts +34 -0
  60. package/tsconfig.json +31 -0
@@ -0,0 +1,420 @@
1
+ import { promises as fs } from "node:fs";
2
+ import * as path from "node:path";
3
+ import { gunzipSync, gzipSync } from "node:zlib";
4
+ import type {
5
+ RemoveOptions,
6
+ VfsNodeStats,
7
+ WriteFileOptions,
8
+ } from "./types/vfs";
9
+ import {
10
+ archiveExists,
11
+ createTarBuffer,
12
+ readSnapshotFromTar,
13
+ } from "./vfs/archive";
14
+ import type { InternalDirectoryNode, InternalNode } from "./vfs/internalTypes";
15
+ import {
16
+ getNode,
17
+ getParentDirectory,
18
+ normalizePath,
19
+ splitPath,
20
+ } from "./vfs/path";
21
+ import { applySnapshot, createSnapshot } from "./vfs/snapshot";
22
+ import { renderTree } from "./vfs/tree";
23
+
24
+ /**
25
+ * In-memory virtual filesystem with tar.gz mirror persistence.
26
+ *
27
+ * Paths are normalized to POSIX-like absolute paths. Use
28
+ * {@link VirtualFileSystem.restoreMirror} on startup and
29
+ * {@link VirtualFileSystem.flushMirror} to persist pending changes.
30
+ */
31
+ class VirtualFileSystem {
32
+ private readonly root: InternalDirectoryNode;
33
+ private readonly archivePath: string;
34
+ private dirty = false;
35
+
36
+ /**
37
+ * Creates a virtual filesystem instance.
38
+ *
39
+ * @param baseDir Base directory used to resolve mirror archive location.
40
+ */
41
+ constructor(baseDir: string = process.cwd()) {
42
+ const now = new Date();
43
+ this.archivePath = path.resolve(baseDir, ".vfs", "mirror.tar.gz");
44
+ this.root = {
45
+ type: "directory",
46
+ name: "",
47
+ mode: 0o755,
48
+ createdAt: now,
49
+ updatedAt: now,
50
+ children: new Map<string, InternalNode>(),
51
+ };
52
+ }
53
+
54
+ /**
55
+ * Restores filesystem state from mirror archive.
56
+ *
57
+ * If archive does not exist or cannot be read, creates fresh mirror file.
58
+ */
59
+ public async restoreMirror(): Promise<void> {
60
+ await fs.mkdir(path.dirname(this.archivePath), { recursive: true });
61
+
62
+ try {
63
+ const compressed = await fs.readFile(this.archivePath);
64
+ const tarBuffer = gunzipSync(compressed);
65
+ const snapshot = await readSnapshotFromTar(tarBuffer);
66
+ applySnapshot(this.root, snapshot);
67
+ this.dirty = false;
68
+ return;
69
+ } catch {
70
+ await this.flushMirror();
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Persists current filesystem state to mirror archive.
76
+ *
77
+ * No-op when nothing changed and archive already exists.
78
+ */
79
+ public async flushMirror(): Promise<void> {
80
+ if (!this.dirty && (await archiveExists(this.archivePath))) {
81
+ return;
82
+ }
83
+
84
+ await fs.mkdir(path.dirname(this.archivePath), { recursive: true });
85
+ const snapshotJson = JSON.stringify(createSnapshot(this.root), null, 2);
86
+ const tarBuffer = await createTarBuffer(snapshotJson);
87
+ const compressed = gzipSync(tarBuffer);
88
+ await fs.writeFile(this.archivePath, compressed);
89
+ this.dirty = false;
90
+ }
91
+
92
+ /**
93
+ * Creates directory and any missing parent directories.
94
+ *
95
+ * @param targetPath Absolute or relative path to directory.
96
+ * @param mode POSIX-like mode bits for new directories.
97
+ */
98
+ public mkdir(targetPath: string, mode: number = 0o755): void {
99
+ const normalized = normalizePath(targetPath);
100
+ const parts = splitPath(normalized);
101
+
102
+ let current = this.root;
103
+ for (const part of parts) {
104
+ const existing = current.children.get(part);
105
+ if (!existing) {
106
+ const now = new Date();
107
+ const nextDir: InternalDirectoryNode = {
108
+ type: "directory",
109
+ name: part,
110
+ mode,
111
+ createdAt: now,
112
+ updatedAt: now,
113
+ children: new Map<string, InternalNode>(),
114
+ };
115
+ current.children.set(part, nextDir);
116
+ current.updatedAt = now;
117
+ this.dirty = true;
118
+ current = nextDir;
119
+ continue;
120
+ }
121
+
122
+ if (existing.type !== "directory") {
123
+ throw new Error(
124
+ `Cannot create directory '${normalized}': '${part}' is a file.`,
125
+ );
126
+ }
127
+ current = existing;
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Writes UTF-8 text or binary content into file.
133
+ *
134
+ * Parent directories are created when missing.
135
+ *
136
+ * @param targetPath Destination file path.
137
+ * @param content File content as string or Buffer.
138
+ * @param options Optional write behavior (mode, compression).
139
+ */
140
+ public writeFile(
141
+ targetPath: string,
142
+ content: string | Buffer,
143
+ options: WriteFileOptions = {},
144
+ ): void {
145
+ const normalized = normalizePath(targetPath);
146
+ const { parent, name } = getParentDirectory(
147
+ this.root,
148
+ normalized,
149
+ true,
150
+ (pathToCreate) => this.mkdir(pathToCreate),
151
+ );
152
+ const now = new Date();
153
+
154
+ const rawContent = Buffer.isBuffer(content)
155
+ ? content
156
+ : Buffer.from(content, "utf8");
157
+ const shouldCompress = options.compress ?? false;
158
+ const storedContent = shouldCompress ? gzipSync(rawContent) : rawContent;
159
+ const existing = parent.children.get(name);
160
+
161
+ if (existing && existing.type === "directory") {
162
+ throw new Error(
163
+ `Cannot write file '${normalized}': path is a directory.`,
164
+ );
165
+ }
166
+
167
+ const createdAt = existing?.type === "file" ? existing.createdAt : now;
168
+ const mode =
169
+ options.mode ?? (existing?.type === "file" ? existing.mode : 0o644);
170
+
171
+ parent.children.set(name, {
172
+ type: "file",
173
+ name,
174
+ mode,
175
+ createdAt,
176
+ updatedAt: now,
177
+ content: storedContent,
178
+ compressed: shouldCompress,
179
+ });
180
+
181
+ parent.updatedAt = now;
182
+ this.dirty = true;
183
+ }
184
+
185
+ /**
186
+ * Reads file content as UTF-8 text.
187
+ *
188
+ * Compressed files are transparently decompressed.
189
+ *
190
+ * @param targetPath Path to file.
191
+ * @returns UTF-8 string content.
192
+ */
193
+ public readFile(targetPath: string): string {
194
+ const node = getNode(this.root, targetPath);
195
+ if (node.type !== "file") {
196
+ throw new Error(`Cannot read '${targetPath}': not a file.`);
197
+ }
198
+
199
+ const raw = node.compressed ? gunzipSync(node.content) : node.content;
200
+ return raw.toString("utf8");
201
+ }
202
+
203
+ /**
204
+ * Checks whether node exists at path.
205
+ *
206
+ * @param targetPath Node path.
207
+ * @returns True when file or directory exists.
208
+ */
209
+ public exists(targetPath: string): boolean {
210
+ try {
211
+ getNode(this.root, targetPath);
212
+ return true;
213
+ } catch {
214
+ return false;
215
+ }
216
+ }
217
+
218
+ /**
219
+ * Updates mode bits for file or directory.
220
+ *
221
+ * @param targetPath Node path.
222
+ * @param mode New POSIX-like mode.
223
+ */
224
+ public chmod(targetPath: string, mode: number): void {
225
+ const node = getNode(this.root, targetPath);
226
+ node.mode = mode;
227
+ node.updatedAt = new Date();
228
+ this.dirty = true;
229
+ }
230
+
231
+ /**
232
+ * Returns metadata for file or directory.
233
+ *
234
+ * @param targetPath Node path.
235
+ * @returns Typed stat object based on node type.
236
+ */
237
+ public stat(targetPath: string): VfsNodeStats {
238
+ const normalized = normalizePath(targetPath);
239
+ const node = getNode(this.root, normalized);
240
+
241
+ if (node.type === "file") {
242
+ return {
243
+ type: "file",
244
+ name: node.name,
245
+ path: normalized,
246
+ mode: node.mode,
247
+ createdAt: node.createdAt,
248
+ updatedAt: node.updatedAt,
249
+ compressed: node.compressed,
250
+ size: node.content.length,
251
+ };
252
+ }
253
+
254
+ return {
255
+ type: "directory",
256
+ name: node.name,
257
+ path: normalized,
258
+ mode: node.mode,
259
+ createdAt: node.createdAt,
260
+ updatedAt: node.updatedAt,
261
+ childrenCount: node.children.size,
262
+ };
263
+ }
264
+
265
+ /**
266
+ * Lists direct children names of directory.
267
+ *
268
+ * @param dirPath Directory path, defaults to root.
269
+ * @returns Sorted child names.
270
+ */
271
+ public list(dirPath: string = "/"): string[] {
272
+ const node = getNode(this.root, dirPath);
273
+ if (node.type !== "directory") {
274
+ throw new Error(`Cannot list '${dirPath}': not a directory.`);
275
+ }
276
+
277
+ return Array.from(node.children.keys()).sort();
278
+ }
279
+
280
+ /**
281
+ * Renders ASCII tree view of directory hierarchy.
282
+ *
283
+ * @param dirPath Directory path, defaults to root.
284
+ * @returns Multi-line tree string.
285
+ */
286
+ public tree(dirPath: string = "/"): string {
287
+ const node = getNode(this.root, dirPath);
288
+ if (node.type !== "directory") {
289
+ throw new Error(`Cannot render tree for '${dirPath}': not a directory.`);
290
+ }
291
+
292
+ const rootLabel =
293
+ dirPath === "/" ? "/" : path.posix.basename(normalizePath(dirPath));
294
+ return renderTree(node, rootLabel);
295
+ }
296
+
297
+ /**
298
+ * Compresses file content with gzip and flags node as compressed.
299
+ *
300
+ * @param targetPath Path to file.
301
+ */
302
+ public compressFile(targetPath: string): void {
303
+ const node = getNode(this.root, targetPath);
304
+ if (node.type !== "file") {
305
+ throw new Error(`Cannot compress '${targetPath}': not a file.`);
306
+ }
307
+
308
+ if (!node.compressed) {
309
+ node.content = gzipSync(node.content);
310
+ node.compressed = true;
311
+ node.updatedAt = new Date();
312
+ this.dirty = true;
313
+ }
314
+ }
315
+
316
+ /**
317
+ * Decompresses gzip-compressed file content.
318
+ *
319
+ * @param targetPath Path to file.
320
+ */
321
+ public decompressFile(targetPath: string): void {
322
+ const node = getNode(this.root, targetPath);
323
+ if (node.type !== "file") {
324
+ throw new Error(`Cannot decompress '${targetPath}': not a file.`);
325
+ }
326
+
327
+ if (node.compressed) {
328
+ node.content = gunzipSync(node.content);
329
+ node.compressed = false;
330
+ node.updatedAt = new Date();
331
+ this.dirty = true;
332
+ }
333
+ }
334
+
335
+ /**
336
+ * Removes file or directory node.
337
+ *
338
+ * @param targetPath Path to remove.
339
+ * @param options Removal options, including recursive delete.
340
+ */
341
+ public remove(targetPath: string, options: RemoveOptions = {}): void {
342
+ const normalized = normalizePath(targetPath);
343
+ if (normalized === "/") {
344
+ throw new Error("Cannot remove root directory.");
345
+ }
346
+
347
+ const { parent, name } = getParentDirectory(
348
+ this.root,
349
+ normalized,
350
+ false,
351
+ () => undefined,
352
+ );
353
+ const node = parent.children.get(name);
354
+
355
+ if (!node) {
356
+ throw new Error(`Path '${normalized}' does not exist.`);
357
+ }
358
+
359
+ if (
360
+ node.type === "directory" &&
361
+ node.children.size > 0 &&
362
+ !options.recursive
363
+ ) {
364
+ throw new Error(
365
+ `Directory '${normalized}' is not empty. Use recursive option.`,
366
+ );
367
+ }
368
+
369
+ parent.children.delete(name);
370
+ parent.updatedAt = new Date();
371
+ this.dirty = true;
372
+ }
373
+
374
+ /**
375
+ * Moves or renames node to destination path.
376
+ *
377
+ * @param fromPath Existing source path.
378
+ * @param toPath Destination path.
379
+ */
380
+ public move(fromPath: string, toPath: string): void {
381
+ const fromNormalized = normalizePath(fromPath);
382
+ const toNormalized = normalizePath(toPath);
383
+
384
+ if (fromNormalized === "/" || toNormalized === "/") {
385
+ throw new Error("Cannot move root directory.");
386
+ }
387
+
388
+ const { parent: fromParent, name: fromName } = getParentDirectory(
389
+ this.root,
390
+ fromNormalized,
391
+ false,
392
+ () => undefined,
393
+ );
394
+ const node = fromParent.children.get(fromName);
395
+
396
+ if (!node) {
397
+ throw new Error(`Path '${fromNormalized}' does not exist.`);
398
+ }
399
+
400
+ const { parent: toParent, name: toName } = getParentDirectory(
401
+ this.root,
402
+ toNormalized,
403
+ true,
404
+ (pathToCreate) => this.mkdir(pathToCreate),
405
+ );
406
+ if (toParent.children.has(toName)) {
407
+ throw new Error(`Destination '${toNormalized}' already exists.`);
408
+ }
409
+
410
+ fromParent.children.delete(fromName);
411
+ node.name = toName;
412
+ node.updatedAt = new Date();
413
+ toParent.children.set(toName, node);
414
+ fromParent.updatedAt = new Date();
415
+ toParent.updatedAt = new Date();
416
+ this.dirty = true;
417
+ }
418
+ }
419
+
420
+ export default VirtualFileSystem;
package/src/index.ts ADDED
@@ -0,0 +1,34 @@
1
+ import { SshClient } from "./SSHMimic/client";
2
+ import { SshMimic } from "./SSHMimic/index";
3
+ import { VirtualUserManager } from "./SSHMimic/users";
4
+ import VirtualFileSystem from "./VirtualFileSystem";
5
+
6
+ export type {
7
+ CommandContext,
8
+ CommandMode,
9
+ CommandOutcome,
10
+ CommandResult,
11
+ NanoEditorSession,
12
+ ShellModule,
13
+ SudoChallenge
14
+ } from "./types/commands";
15
+ export type { ExecStream, ShellStream } from "./types/streams";
16
+ export type {
17
+ RemoveOptions,
18
+ VfsBaseNode,
19
+ VfsDirectoryNode,
20
+ VfsFileNode,
21
+ VfsNodeStats,
22
+ VfsNodeType,
23
+ VfsSnapshot,
24
+ VfsSnapshotBaseNode,
25
+ VfsSnapshotDirectoryNode,
26
+ VfsSnapshotFileNode,
27
+ VfsSnapshotNode,
28
+ WriteFileOptions
29
+ } from "./types/vfs";
30
+
31
+ export {
32
+ SshClient, VirtualFileSystem, SshMimic as VirtualMachine, VirtualUserManager
33
+ };
34
+
@@ -0,0 +1,14 @@
1
+ import { VirtualMachine } from ".";
2
+
3
+ const sshHostname = process.env.SSH_MIMIC_HOSTNAME ?? "typescript-vm";
4
+ const sshMimic = new VirtualMachine({ port: 2222, hostname: sshHostname });
5
+
6
+ sshMimic
7
+ .start()
8
+ .then((port: number) => {
9
+ console.log(`SSH Mimic initialized. Listening on port ${port}.`);
10
+ })
11
+ .catch((error: unknown) => {
12
+ console.error("Failed to start SSH Mimic:", error);
13
+ process.exit(1);
14
+ });
@@ -0,0 +1,98 @@
1
+ /** Command invocation mode used by shell runtime. */
2
+ export type CommandMode = "shell" | "exec";
3
+
4
+ import type {
5
+ VirtualActiveSession,
6
+ VirtualUserManager,
7
+ } from "../SSHMimic/users";
8
+ import type VirtualFileSystem from "../VirtualFileSystem";
9
+
10
+ /**
11
+ * Normalized command execution output.
12
+ *
13
+ * A command can write text, control session lifecycle, request UI state
14
+ * transitions, and update active identity/cwd.
15
+ */
16
+ export interface CommandResult {
17
+ /** Standard output payload to append in terminal. */
18
+ stdout?: string;
19
+ /** Standard error payload to append in terminal. */
20
+ stderr?: string;
21
+ /** Request full terminal clear before next prompt. */
22
+ clearScreen?: boolean;
23
+ /** Request current shell/exec session close. */
24
+ closeSession?: boolean;
25
+ /** Optional exit code (default behavior handled by caller). */
26
+ exitCode?: number;
27
+ /** Optional cwd to apply for next prompt iteration. */
28
+ nextCwd?: string;
29
+ /** Optional user switch for current session state. */
30
+ switchUser?: string;
31
+ /** Request opening built-in nano editor workflow. */
32
+ openEditor?: NanoEditorSession;
33
+ /** Request opening built-in htop-like screen. */
34
+ openHtop?: boolean;
35
+ /** Request sudo password challenge flow. */
36
+ sudoChallenge?: SudoChallenge;
37
+ }
38
+
39
+ /** Deferred sudo challenge metadata returned by sudo command. */
40
+ export interface SudoChallenge {
41
+ /** User currently requesting elevation. */
42
+ username: string;
43
+ /** Target identity for elevated command. */
44
+ targetUser: string;
45
+ /** Command to execute after successful challenge; null for login shell. */
46
+ commandLine: string | null;
47
+ /** True when challenge targets interactive login shell. */
48
+ loginShell: boolean;
49
+ /** Prompt text shown before password input. */
50
+ prompt: string;
51
+ }
52
+
53
+ /** State payload used by nano command interactive editor flow. */
54
+ export interface NanoEditorSession {
55
+ /** Final destination path to write when save succeeds. */
56
+ targetPath: string;
57
+ /** Temporary scratch path used while editing. */
58
+ tempPath: string;
59
+ /** Initial editor content shown to user. */
60
+ initialContent: string;
61
+ }
62
+
63
+ /** Runtime context object passed to each command module. */
64
+ export interface CommandContext {
65
+ /** Authenticated user currently bound to stream. */
66
+ authUser: string;
67
+ /** Virtual hostname shown in prompt and banners. */
68
+ hostname: string;
69
+ /** User and session manager instance. */
70
+ users: VirtualUserManager;
71
+ /** Snapshot of currently active user sessions. */
72
+ activeSessions: VirtualActiveSession[];
73
+ /** Original unparsed command line input. */
74
+ rawInput: string;
75
+ /** Invocation mode (interactive shell or direct exec). */
76
+ mode: CommandMode;
77
+ /** Tokenized arguments excluding command name. */
78
+ args: string[];
79
+ /** Current working directory for command execution. */
80
+ cwd: string;
81
+ /** Virtual filesystem instance for IO operations. */
82
+ vfs: VirtualFileSystem;
83
+ }
84
+
85
+ /** Contract implemented by each shell command module. */
86
+ export interface ShellModule {
87
+ /** Primary command name used in CLI. */
88
+ name: string;
89
+ /** Parameter help snippets displayed by help command. */
90
+ params: string[];
91
+ /** Command handler implementation. */
92
+ run: (ctx: CommandContext) => CommandResult | Promise<CommandResult>;
93
+ /** Optional alternative command names. */
94
+ aliases?: string[];
95
+ }
96
+
97
+ /** Command return union allowing sync or async handlers. */
98
+ export type CommandOutcome = CommandResult | Promise<CommandResult>;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Minimal stream contract used by exec command handlers.
3
+ */
4
+ export interface ExecStream {
5
+ /** Writes text to stdout channel. */
6
+ write(data: string): void;
7
+ /** Signals output completion. */
8
+ end(): void;
9
+ /** Sets process-like exit code for exec response. */
10
+ exit(code: number): void;
11
+ /** Writable stderr channel. */
12
+ stderr: {
13
+ /** Writes text to stderr channel. */
14
+ write(data: string): void;
15
+ };
16
+ }
17
+
18
+ /**
19
+ * Minimal interactive stream contract used by shell mode.
20
+ */
21
+ export interface ShellStream {
22
+ /** Writes text to shell output channel. */
23
+ write(data: string): void;
24
+ /** Sets shell exit code on close. */
25
+ exit(code: number): void;
26
+ /** Ends shell stream. */
27
+ end(): void;
28
+ /** Subscribes to incoming user input chunks. */
29
+ on(event: "data", listener: (chunk: Buffer) => void): void;
30
+ /** Subscribes to stream close event. */
31
+ on(event: "close", listener: () => void): void;
32
+ }
@@ -0,0 +1,38 @@
1
+ declare module "tar-stream" {
2
+ export interface TarStreamEntryHeader {
3
+ name: string;
4
+ mode?: number;
5
+ size?: number;
6
+ mtime?: Date;
7
+ }
8
+
9
+ export interface TarStreamPack {
10
+ entry(
11
+ header: TarStreamEntryHeader,
12
+ data: string | Buffer,
13
+ callback?: (error?: Error | null) => void,
14
+ ): void;
15
+ finalize(): void;
16
+ destroy?(error?: Error): void;
17
+ on(event: "data", listener: (chunk: Buffer) => void): this;
18
+ on(event: "end", listener: () => void): this;
19
+ on(event: "error", listener: (error: Error) => void): this;
20
+ }
21
+
22
+ export interface TarStreamExtract {
23
+ on(
24
+ event: "entry",
25
+ listener: (
26
+ header: TarStreamEntryHeader,
27
+ stream: NodeJS.ReadableStream,
28
+ next: () => void,
29
+ ) => void,
30
+ ): this;
31
+ on(event: "finish", listener: () => void): this;
32
+ on(event: "error", listener: (error: Error) => void): this;
33
+ end(buffer: Buffer): void;
34
+ }
35
+
36
+ export function pack(): TarStreamPack;
37
+ export function extract(): TarStreamExtract;
38
+ }