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,81 @@
1
+ /** Supported virtual node kinds. */
2
+ export type VfsNodeType = "file" | "directory";
3
+
4
+ /** Shared metadata fields available on file and directory stats. */
5
+ export interface VfsBaseNode {
6
+ /** Node name without parent path. */
7
+ name: string;
8
+ /** Absolute normalized node path. */
9
+ path: string;
10
+ /** POSIX-like mode bits. */
11
+ mode: number;
12
+ /** Node creation timestamp. */
13
+ createdAt: Date;
14
+ /** Last update timestamp. */
15
+ updatedAt: Date;
16
+ }
17
+
18
+ /** Stat shape returned for file nodes. */
19
+ export interface VfsFileNode extends VfsBaseNode {
20
+ type: "file";
21
+ /** True when file content stored as gzip bytes. */
22
+ compressed: boolean;
23
+ /** Stored byte length (compressed when compressed=true). */
24
+ size: number;
25
+ }
26
+
27
+ /** Stat shape returned for directory nodes. */
28
+ export interface VfsDirectoryNode extends VfsBaseNode {
29
+ type: "directory";
30
+ /** Number of direct children in directory. */
31
+ childrenCount: number;
32
+ }
33
+
34
+ /** Union of file and directory stat responses. */
35
+ export type VfsNodeStats = VfsFileNode | VfsDirectoryNode;
36
+
37
+ /** Optional behavior flags for writeFile operations. */
38
+ export interface WriteFileOptions {
39
+ /** POSIX-like mode to apply on create or overwrite. */
40
+ mode?: number;
41
+ /** Store content compressed with gzip. */
42
+ compress?: boolean;
43
+ }
44
+
45
+ /** Optional behavior flags for remove operations. */
46
+ export interface RemoveOptions {
47
+ /** Allow deleting non-empty directory trees. */
48
+ recursive?: boolean;
49
+ }
50
+
51
+ /** Base snapshot node schema used for archive serialization. */
52
+ export interface VfsSnapshotBaseNode {
53
+ name: string;
54
+ mode: number;
55
+ /** ISO-8601 creation timestamp. */
56
+ createdAt: string;
57
+ /** ISO-8601 update timestamp. */
58
+ updatedAt: string;
59
+ }
60
+
61
+ /** Serialized snapshot shape for file nodes. */
62
+ export interface VfsSnapshotFileNode extends VfsSnapshotBaseNode {
63
+ type: "file";
64
+ compressed: boolean;
65
+ /** Base64-encoded raw file bytes. */
66
+ contentBase64: string;
67
+ }
68
+
69
+ /** Serialized snapshot shape for directory nodes. */
70
+ export interface VfsSnapshotDirectoryNode extends VfsSnapshotBaseNode {
71
+ type: "directory";
72
+ children: VfsSnapshotNode[];
73
+ }
74
+
75
+ /** Union of serialized snapshot node variants. */
76
+ export type VfsSnapshotNode = VfsSnapshotFileNode | VfsSnapshotDirectoryNode;
77
+
78
+ /** Top-level serialized filesystem snapshot. */
79
+ export interface VfsSnapshot {
80
+ root: VfsSnapshotDirectoryNode;
81
+ }
@@ -0,0 +1,74 @@
1
+ import { promises as fs } from "node:fs";
2
+ import * as tarStream from "tar-stream";
3
+ import type { VfsSnapshot } from "../types/vfs";
4
+
5
+ export async function archiveExists(archivePath: string): Promise<boolean> {
6
+ try {
7
+ await fs.access(archivePath);
8
+ return true;
9
+ } catch {
10
+ return false;
11
+ }
12
+ }
13
+
14
+ export async function createTarBuffer(snapshotJson: string): Promise<Buffer> {
15
+ const pack = tarStream.pack();
16
+ const chunks: Buffer[] = [];
17
+
18
+ const finished = new Promise<Buffer>((resolve, reject) => {
19
+ pack.on("data", (chunk: Buffer) => chunks.push(Buffer.from(chunk)));
20
+ pack.on("error", reject);
21
+ pack.on("end", () => resolve(Buffer.concat(chunks)));
22
+ });
23
+
24
+ pack.entry(
25
+ { name: "snapshot.json", mode: 0o600 },
26
+ snapshotJson,
27
+ (error?: Error | null) => {
28
+ if (error) {
29
+ return;
30
+ }
31
+
32
+ pack.finalize();
33
+ },
34
+ );
35
+
36
+ return finished;
37
+ }
38
+
39
+ export async function readSnapshotFromTar(
40
+ tarBuffer: Buffer,
41
+ ): Promise<VfsSnapshot> {
42
+ return new Promise<VfsSnapshot>((resolve, reject) => {
43
+ const extract = tarStream.extract();
44
+ let snapshotText = "";
45
+ let found = false;
46
+
47
+ extract.on("entry", (header, stream, next) => {
48
+ if (header.name === "snapshot.json") {
49
+ found = true;
50
+ stream.on("data", (chunk: Buffer) => {
51
+ snapshotText += chunk.toString("utf8");
52
+ });
53
+ stream.on("end", next);
54
+ stream.resume();
55
+ return;
56
+ }
57
+
58
+ stream.resume();
59
+ stream.on("end", next);
60
+ });
61
+
62
+ extract.on("finish", () => {
63
+ if (!found) {
64
+ reject(new Error("snapshot.json missing from archive"));
65
+ return;
66
+ }
67
+
68
+ resolve(JSON.parse(snapshotText) as VfsSnapshot);
69
+ });
70
+
71
+ extract.on("error", reject);
72
+ extract.end(tarBuffer);
73
+ });
74
+ }
@@ -0,0 +1,19 @@
1
+ export type InternalNode = InternalFileNode | InternalDirectoryNode;
2
+
3
+ interface InternalBaseNode {
4
+ name: string;
5
+ mode: number;
6
+ createdAt: Date;
7
+ updatedAt: Date;
8
+ }
9
+
10
+ export interface InternalFileNode extends InternalBaseNode {
11
+ type: "file";
12
+ content: Buffer;
13
+ compressed: boolean;
14
+ }
15
+
16
+ export interface InternalDirectoryNode extends InternalBaseNode {
17
+ type: "directory";
18
+ children: Map<string, InternalNode>;
19
+ }
@@ -0,0 +1,74 @@
1
+ import * as path from "node:path";
2
+ import type { InternalDirectoryNode, InternalNode } from "./internalTypes";
3
+
4
+ export function normalizePath(rawPath: string): string {
5
+ if (!rawPath || rawPath.trim() === "") {
6
+ return "/";
7
+ }
8
+
9
+ const normalized = path.posix.normalize(
10
+ rawPath.startsWith("/") ? rawPath : `/${rawPath}`,
11
+ );
12
+ return normalized === "" ? "/" : normalized;
13
+ }
14
+
15
+ export function splitPath(normalizedPath: string): string[] {
16
+ return normalizedPath.split("/").filter(Boolean);
17
+ }
18
+
19
+ export function getNode(
20
+ root: InternalDirectoryNode,
21
+ targetPath: string,
22
+ ): InternalNode {
23
+ const normalized = normalizePath(targetPath);
24
+ if (normalized === "/") {
25
+ return root;
26
+ }
27
+
28
+ const parts = splitPath(normalized);
29
+ let current: InternalNode = root;
30
+
31
+ for (const part of parts) {
32
+ if (current.type !== "directory") {
33
+ throw new Error(`Path '${normalized}' does not exist.`);
34
+ }
35
+
36
+ const next = current.children.get(part);
37
+ if (!next) {
38
+ throw new Error(`Path '${normalized}' does not exist.`);
39
+ }
40
+ current = next;
41
+ }
42
+
43
+ return current;
44
+ }
45
+
46
+ export function getParentDirectory(
47
+ root: InternalDirectoryNode,
48
+ targetPath: string,
49
+ createIfMissing: boolean,
50
+ createPath: (pathToCreate: string) => void,
51
+ ): { parent: InternalDirectoryNode; name: string } {
52
+ const normalized = normalizePath(targetPath);
53
+ if (normalized === "/") {
54
+ throw new Error("Root path has no parent directory.");
55
+ }
56
+
57
+ const parentPath = path.posix.dirname(normalized);
58
+ const name = path.posix.basename(normalized);
59
+
60
+ if (!name) {
61
+ throw new Error(`Invalid path '${targetPath}'.`);
62
+ }
63
+
64
+ if (createIfMissing) {
65
+ createPath(parentPath);
66
+ }
67
+
68
+ const parentNode = getNode(root, parentPath);
69
+ if (parentNode.type !== "directory") {
70
+ throw new Error(`Parent path '${parentPath}' is not a directory.`);
71
+ }
72
+
73
+ return { parent: parentNode, name };
74
+ }
@@ -0,0 +1,84 @@
1
+ import type {
2
+ VfsSnapshot,
3
+ VfsSnapshotDirectoryNode,
4
+ VfsSnapshotNode,
5
+ } from "../types/vfs";
6
+ import type { InternalDirectoryNode, InternalNode } from "./internalTypes";
7
+
8
+ function serializeNode(node: InternalNode): VfsSnapshotNode {
9
+ if (node.type === "file") {
10
+ return {
11
+ type: "file",
12
+ name: node.name,
13
+ mode: node.mode,
14
+ createdAt: node.createdAt.toISOString(),
15
+ updatedAt: node.updatedAt.toISOString(),
16
+ compressed: node.compressed,
17
+ contentBase64: node.content.toString("base64"),
18
+ };
19
+ }
20
+
21
+ return serializeDirectory(node);
22
+ }
23
+
24
+ function serializeDirectory(
25
+ node: InternalDirectoryNode,
26
+ ): VfsSnapshotDirectoryNode {
27
+ return {
28
+ type: "directory",
29
+ name: node.name,
30
+ mode: node.mode,
31
+ createdAt: node.createdAt.toISOString(),
32
+ updatedAt: node.updatedAt.toISOString(),
33
+ children: Array.from(node.children.values()).map((child) =>
34
+ serializeNode(child),
35
+ ),
36
+ };
37
+ }
38
+
39
+ function deserializeNode(node: VfsSnapshotNode): InternalNode {
40
+ if (node.type === "file") {
41
+ return {
42
+ type: "file",
43
+ name: node.name,
44
+ mode: node.mode,
45
+ createdAt: new Date(node.createdAt),
46
+ updatedAt: new Date(node.updatedAt),
47
+ content: Buffer.from(node.contentBase64, "base64"),
48
+ compressed: node.compressed,
49
+ };
50
+ }
51
+
52
+ return deserializeDirectory(node);
53
+ }
54
+
55
+ function deserializeDirectory(
56
+ node: VfsSnapshotDirectoryNode,
57
+ ): InternalDirectoryNode {
58
+ return {
59
+ type: "directory",
60
+ name: node.name,
61
+ mode: node.mode,
62
+ createdAt: new Date(node.createdAt),
63
+ updatedAt: new Date(node.updatedAt),
64
+ children: new Map<string, InternalNode>(
65
+ node.children.map((child) => [child.name, deserializeNode(child)]),
66
+ ),
67
+ };
68
+ }
69
+
70
+ export function createSnapshot(root: InternalDirectoryNode): VfsSnapshot {
71
+ return { root: serializeDirectory(root) };
72
+ }
73
+
74
+ export function applySnapshot(
75
+ rootTarget: InternalDirectoryNode,
76
+ snapshot: VfsSnapshot,
77
+ ): void {
78
+ const root = deserializeDirectory(snapshot.root);
79
+ rootTarget.name = root.name;
80
+ rootTarget.mode = root.mode;
81
+ rootTarget.createdAt = root.createdAt;
82
+ rootTarget.updatedAt = root.updatedAt;
83
+ rootTarget.children = root.children;
84
+ }
@@ -0,0 +1,34 @@
1
+ import type { InternalDirectoryNode } from "./internalTypes";
2
+
3
+ function walkTree(
4
+ node: InternalDirectoryNode,
5
+ indent: string,
6
+ lines: string[],
7
+ ): void {
8
+ const entries = Array.from(node.children.entries()).sort(([a], [b]) =>
9
+ a.localeCompare(b),
10
+ );
11
+
12
+ entries.forEach(([name, child], index) => {
13
+ const isLast = index === entries.length - 1;
14
+ const branch = isLast ? "`-- " : "|-- ";
15
+ const nextIndent = indent + (isLast ? " " : "| ");
16
+
17
+ if (child.type === "file") {
18
+ lines.push(`${indent}${branch}${name}${child.compressed ? " [gz]" : ""}`);
19
+ return;
20
+ }
21
+
22
+ lines.push(`${indent}${branch}${name}/`);
23
+ walkTree(child, nextIndent, lines);
24
+ });
25
+ }
26
+
27
+ export function renderTree(
28
+ node: InternalDirectoryNode,
29
+ rootLabel: string,
30
+ ): string {
31
+ const lines: string[] = [rootLabel];
32
+ walkTree(node, "", lines);
33
+ return lines.join("\n");
34
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "compilerOptions": {
3
+ // Environment setup & latest features
4
+ "lib": ["ESNext"],
5
+ "target": "ESNext",
6
+ "module": "Preserve",
7
+ "moduleDetection": "force",
8
+ "jsx": "react-jsx",
9
+ "allowJs": true,
10
+
11
+ // Bundler mode
12
+ "moduleResolution": "bundler",
13
+ "allowImportingTsExtensions": true,
14
+ "verbatimModuleSyntax": true,
15
+ "noEmit": true,
16
+
17
+ // Best practices
18
+ "strict": true,
19
+ "skipLibCheck": true,
20
+ "noFallthroughCasesInSwitch": true,
21
+ "noUncheckedIndexedAccess": true,
22
+ "noImplicitOverride": true,
23
+
24
+ // Some stricter flags (disabled by default)
25
+ "noUnusedLocals": false,
26
+ "noUnusedParameters": false,
27
+ "noPropertyAccessFromIndexSignature": false,
28
+
29
+ "types": ["node"]
30
+ }
31
+ }