typescript-virtual-container 1.2.6 → 1.2.8

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.
@@ -2,6 +2,7 @@ import { EventEmitter } from "node:events";
2
2
  import * as fsSync from "node:fs";
3
3
  import * as path from "node:path";
4
4
  import { gunzipSync, gzipSync } from "node:zlib";
5
+ import { decodeVfs, encodeVfs, isBinarySnapshot } from "./binaryPack";
5
6
  import { getNode, getParentDirectory, normalizePath } from "./path";
6
7
  // ── VirtualFileSystem ─────────────────────────────────────────────────────────
7
8
  /**
@@ -10,7 +11,7 @@ import { getNode, getParentDirectory, normalizePath } from "./path";
10
11
  * **Memory mode** (default) — all state lives in a fast recursive tree.
11
12
  * Use `toSnapshot()` / `fromSnapshot()` / `importSnapshot()` for serialisation.
12
13
  *
13
- * **FS mode** — same in-memory tree, but `restoreMirror()` loads a JSON
14
+ * **FS mode** — same in-memory tree, but `restoreMirror()` loads a binary
14
15
  * snapshot from disk and `flushMirror()` writes it back. This gives you
15
16
  * persistent VFS state across process restarts without any real POSIX filesystem
16
17
  * semantics leaking through.
@@ -38,7 +39,7 @@ class VirtualFileSystem extends EventEmitter {
38
39
  if (!options.snapshotPath) {
39
40
  throw new Error('VirtualFileSystem: "snapshotPath" is required when mode is "fs".');
40
41
  }
41
- this.snapshotFile = path.resolve(options.snapshotPath, "vfs-snapshot.json");
42
+ this.snapshotFile = path.resolve(options.snapshotPath, "vfs-snapshot.vfsb");
42
43
  }
43
44
  else {
44
45
  this.snapshotFile = null;
@@ -92,7 +93,8 @@ class VirtualFileSystem extends EventEmitter {
92
93
  }
93
94
  // ── Persistence ───────────────────────────────────────────────────────────
94
95
  /**
95
- * In `"fs"` mode: reads the JSON snapshot from disk and hydrates the tree.
96
+ * In `"fs"` mode: reads the binary snapshot (`vfs-snapshot.vfsb`) from disk.
97
+ * Automatically falls back to legacy JSON format for backward compatibility.
96
98
  * Silently succeeds when the snapshot file does not exist yet.
97
99
  *
98
100
  * In `"memory"` mode: no-op (kept for API compatibility).
@@ -103,9 +105,17 @@ class VirtualFileSystem extends EventEmitter {
103
105
  if (!fsSync.existsSync(this.snapshotFile))
104
106
  return;
105
107
  try {
106
- const raw = fsSync.readFileSync(this.snapshotFile, "utf8");
107
- const snapshot = JSON.parse(raw);
108
- this.root = this.deserializeDir(snapshot.root, "");
108
+ const raw = fsSync.readFileSync(this.snapshotFile);
109
+ if (isBinarySnapshot(raw)) {
110
+ // Fast binary format (current)
111
+ this.root = decodeVfs(raw);
112
+ }
113
+ else {
114
+ // Legacy JSON fallback — auto-migrates on next flushMirror()
115
+ const snapshot = JSON.parse(raw.toString("utf8"));
116
+ this.root = this.deserializeDir(snapshot.root, "");
117
+ console.info("[VirtualFileSystem] Migrating legacy JSON snapshot to binary format.");
118
+ }
109
119
  this.emit("snapshot:restore", { path: this.snapshotFile });
110
120
  }
111
121
  catch (err) {
@@ -114,7 +124,8 @@ class VirtualFileSystem extends EventEmitter {
114
124
  }
115
125
  }
116
126
  /**
117
- * In `"fs"` mode: serialises the in-memory tree to a JSON snapshot on disk.
127
+ * In `"fs"` mode: serialises the in-memory tree to a binary snapshot on disk
128
+ * (`vfs-snapshot.vfsb`). ~27% smaller and significantly faster than JSON+base64.
118
129
  * The directory is created if it does not exist.
119
130
  *
120
131
  * In `"memory"` mode: emits `"mirror:flush"` and returns (no disk write).
@@ -126,8 +137,8 @@ class VirtualFileSystem extends EventEmitter {
126
137
  }
127
138
  const dir = path.dirname(this.snapshotFile);
128
139
  fsSync.mkdirSync(dir, { recursive: true });
129
- const snapshot = this.toSnapshot();
130
- fsSync.writeFileSync(this.snapshotFile, JSON.stringify(snapshot), "utf8");
140
+ const binary = encodeVfs(this.root);
141
+ fsSync.writeFileSync(this.snapshotFile, binary);
131
142
  this.emit("mirror:flush", { path: this.snapshotFile });
132
143
  }
133
144
  /** Returns the current persistence mode. */
@@ -1,3 +1,4 @@
1
+ /** biome-ignore-all lint/style/useNamingConvention: ENV VARS */
1
2
  import type { ShellModule } from "../types/commands";
2
3
  export declare const envCommand: ShellModule;
3
4
  //# sourceMappingURL=env.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../../src/commands/env.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD,eAAO,MAAM,UAAU,EAAE,WASxB,CAAC"}
1
+ {"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../../src/commands/env.ts"],"names":[],"mappings":"AAAA,gEAAgE;AAChE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD,eAAO,MAAM,UAAU,EAAE,WASxB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"help.d.ts","sourceRoot":"","sources":["../../src/commands/help.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAoBrD,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,MAAM,EAAE,GAAG,WAAW,CAkExE"}
1
+ {"version":3,"file":"help.d.ts","sourceRoot":"","sources":["../../src/commands/help.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAoBrD,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,MAAM,EAAE,GAAG,WAAW,CAyDxE"}
@@ -59,20 +59,11 @@ export function createHelpCommand(_getNames) {
59
59
  if (!mods || mods.length === 0)
60
60
  continue;
61
61
  lines.push(`\x1b[33m${CATEGORY_LABELS[cat] ?? cat}\x1b[0m`);
62
- // Two-column layout
63
62
  const sorted = [...mods].sort((a, b) => a.name.localeCompare(b.name));
64
- for (let i = 0; i < sorted.length; i += 2) {
65
- const left = sorted[i];
66
- const right = sorted[i + 1];
67
- const leftStr = ` \x1b[36m${padRight(left.name, 14)}\x1b[0m ${left.description ?? ""}`;
68
- const rightStr = right
69
- ? ` \x1b[36m${padRight(right.name, 14)}\x1b[0m ${right.description ?? ""}`
70
- : "";
71
- lines.push(rightStr ? `${leftStr.padEnd(44)}${rightStr}` : leftStr);
63
+ for (const mod of sorted) {
64
+ lines.push(` \x1b[36m${padRight(mod.name, 14)}\x1b[0m ${mod.description ?? ""}`);
72
65
  }
73
- lines.push("");
74
66
  }
75
- lines.push("Type \x1b[1mhelp <command>\x1b[0m for usage details.");
76
67
  return { stdout: lines.join("\n"), exitCode: 0 };
77
68
  },
78
69
  };
@@ -1 +1 @@
1
- {"version":3,"file":"sh.d.ts","sourceRoot":"","sources":["../../src/commands/sh.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiC,WAAW,EAAE,MAAM,mBAAmB,CAAC;AA8KpF,eAAO,MAAM,SAAS,EAAE,WA+BvB,CAAC"}
1
+ {"version":3,"file":"sh.d.ts","sourceRoot":"","sources":["../../src/commands/sh.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiC,WAAW,EAAE,MAAM,mBAAmB,CAAC;AA+KpF,eAAO,MAAM,SAAS,EAAE,WA+BvB,CAAC"}
@@ -48,6 +48,7 @@ function parseBlocks(lines) {
48
48
  }
49
49
  i++;
50
50
  }
51
+ // biome-ignore lint/suspicious/noThenProperty: expected behavior for if/elif parsing
51
52
  blocks.push({ type: "if", cond, then: thenLines, elif: elifBlocks, else_: elseLines });
52
53
  }
53
54
  else if (line.startsWith("for ")) {
@@ -212,7 +213,7 @@ export const shCommand = {
212
213
  category: "shell",
213
214
  params: ["-c <script>", "[<file>]"],
214
215
  run: async (ctx) => {
215
- const { args, authUser, shell, cwd } = ctx;
216
+ const { args, shell, cwd } = ctx;
216
217
  // sh -c "inline script"
217
218
  if (ifFlag(args, "-c")) {
218
219
  const script = getArg(args, 1) ?? "";
@@ -1,6 +1,9 @@
1
1
  import { VirtualSftpServer, VirtualShell, VirtualSshServer } from ".";
2
2
  const hostname = process.env.SSH_MIMIC_HOSTNAME ?? "typescript-vm";
3
- const virtualShell = new VirtualShell(hostname);
3
+ const virtualShell = new VirtualShell(hostname, undefined, {
4
+ mode: "fs",
5
+ snapshotPath: ".vfs",
6
+ });
4
7
  virtualShell.addCommand("demo", [], () => {
5
8
  return {
6
9
  stdout: "This is a demo command. It does nothing useful.",
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
7
- "version": "1.2.6",
7
+ "version": "1.2.8",
8
8
  "license": "MIT",
9
9
  "repository": {
10
10
  "type": "git",
@@ -32,8 +32,8 @@
32
32
  "standalone-build": "bunx esbuild src/standalone.ts --bundle --platform=node --target=node18 --outfile=standalone.js --tree-shaking=true --minify --sourcemap"
33
33
  },
34
34
  "devDependencies": {
35
- "@biomejs/biome": "^2.4.11",
36
- "@types/bun": "latest",
35
+ "@biomejs/biome": "^2.4.13",
36
+ "@types/bun": "^1.3.13",
37
37
  "@types/node": "^25.6.0",
38
38
  "@types/ssh2": "^1.15.5",
39
39
  "typescript": "^6.0.3"
@@ -0,0 +1,219 @@
1
+ /**
2
+ * Binary serialisation format for VirtualFileSystem snapshots.
3
+ *
4
+ * Replaces the JSON+base64 approach. No external dependencies.
5
+ *
6
+ * Wire format (little-endian throughout):
7
+ *
8
+ * File header:
9
+ * [4] magic = 0x56 0x46 0x53 0x21 ("VFS!")
10
+ * [1] version = 0x01
11
+ *
12
+ * Node (recursive):
13
+ * [1] type = 0x01 (file) | 0x02 (directory)
14
+ * [2] name length (uint16)
15
+ * [N] name bytes (utf8)
16
+ * [4] mode (uint32)
17
+ * [8] createdAt ms (float64)
18
+ * [8] updatedAt ms (float64)
19
+ *
20
+ * File node extra:
21
+ * [1] compressed flag (0x00 | 0x01)
22
+ * [4] content length (uint32)
23
+ * [N] content bytes (raw — no base64)
24
+ *
25
+ * Directory node extra:
26
+ * [4] children count (uint32)
27
+ * [N] children nodes (recursive)
28
+ *
29
+ * Total overhead vs JSON+base64 for 1 MB of file data:
30
+ * JSON+base64 : ~1.37 MB (base64 33% bloat) + JSON string wrapping
31
+ * Binary pack : ~1.00 MB + ~40 bytes/node header → ~27% smaller, no string parsing
32
+ */
33
+
34
+ import type { InternalDirectoryNode, InternalFileNode, InternalNode } from "./internalTypes";
35
+
36
+ const MAGIC = Buffer.from([0x56, 0x46, 0x53, 0x21]); // "VFS!"
37
+ const VERSION = 0x01;
38
+ const TYPE_FILE = 0x01;
39
+ const TYPE_DIR = 0x02;
40
+
41
+ // ── Encoder ───────────────────────────────────────────────────────────────────
42
+
43
+ class Encoder {
44
+ private chunks: Buffer[] = [];
45
+
46
+ write(buf: Buffer): void { this.chunks.push(buf); }
47
+
48
+ writeUint8(n: number): void {
49
+ const b = Buffer.allocUnsafe(1);
50
+ b.writeUInt8(n, 0);
51
+ this.chunks.push(b);
52
+ }
53
+
54
+ writeUint16(n: number): void {
55
+ const b = Buffer.allocUnsafe(2);
56
+ b.writeUInt16LE(n, 0);
57
+ this.chunks.push(b);
58
+ }
59
+
60
+ writeUint32(n: number): void {
61
+ const b = Buffer.allocUnsafe(4);
62
+ b.writeUInt32LE(n, 0);
63
+ this.chunks.push(b);
64
+ }
65
+
66
+ writeFloat64(n: number): void {
67
+ const b = Buffer.allocUnsafe(8);
68
+ b.writeDoubleBE(n, 0);
69
+ this.chunks.push(b);
70
+ }
71
+
72
+ writeString(s: string): void {
73
+ const encoded = Buffer.from(s, "utf8");
74
+ this.writeUint16(encoded.length);
75
+ this.chunks.push(encoded);
76
+ }
77
+
78
+ writeBytes(bytes: Buffer): void {
79
+ this.writeUint32(bytes.length);
80
+ this.chunks.push(bytes);
81
+ }
82
+
83
+ toBuffer(): Buffer { return Buffer.concat(this.chunks); }
84
+ }
85
+
86
+ function encodeNode(enc: Encoder, node: InternalNode): void {
87
+ if (node.type === "file") {
88
+ const f = node as InternalFileNode;
89
+ enc.writeUint8(TYPE_FILE);
90
+ enc.writeString(f.name);
91
+ enc.writeUint32(f.mode);
92
+ enc.writeFloat64(f.createdAt.getTime());
93
+ enc.writeFloat64(f.updatedAt.getTime());
94
+ enc.writeUint8(f.compressed ? 0x01 : 0x00);
95
+ enc.writeBytes(f.content);
96
+ } else {
97
+ const d = node as InternalDirectoryNode;
98
+ enc.writeUint8(TYPE_DIR);
99
+ enc.writeString(d.name);
100
+ enc.writeUint32(d.mode);
101
+ enc.writeFloat64(d.createdAt.getTime());
102
+ enc.writeFloat64(d.updatedAt.getTime());
103
+ const children = Array.from(d.children.values());
104
+ enc.writeUint32(children.length);
105
+ for (const child of children) encodeNode(enc, child);
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Serialise an in-memory VFS root to a compact binary Buffer.
111
+ * No base64, no JSON. ~27% smaller than the JSON+base64 format for typical VFS trees.
112
+ */
113
+ export function encodeVfs(root: InternalDirectoryNode): Buffer {
114
+ const enc = new Encoder();
115
+ enc.write(MAGIC);
116
+ enc.writeUint8(VERSION);
117
+ encodeNode(enc, root);
118
+ return enc.toBuffer();
119
+ }
120
+
121
+ // ── Decoder ───────────────────────────────────────────────────────────────────
122
+
123
+ class Decoder {
124
+ private pos = 0;
125
+ constructor(private readonly buf: Buffer) {}
126
+
127
+ readUint8(): number { return this.buf.readUInt8(this.pos++); }
128
+
129
+ readUint16(): number {
130
+ const v = this.buf.readUInt16LE(this.pos);
131
+ this.pos += 2;
132
+ return v;
133
+ }
134
+
135
+ readUint32(): number {
136
+ const v = this.buf.readUInt32LE(this.pos);
137
+ this.pos += 4;
138
+ return v;
139
+ }
140
+
141
+ readFloat64(): number {
142
+ const v = this.buf.readDoubleBE(this.pos);
143
+ this.pos += 8;
144
+ return v;
145
+ }
146
+
147
+ readString(): string {
148
+ const len = this.readUint16();
149
+ const s = this.buf.toString("utf8", this.pos, this.pos + len);
150
+ this.pos += len;
151
+ return s;
152
+ }
153
+
154
+ readBytes(): Buffer {
155
+ const len = this.readUint32();
156
+ const b = this.buf.slice(this.pos, this.pos + len);
157
+ this.pos += len;
158
+ return b;
159
+ }
160
+
161
+ remaining(): number { return this.buf.length - this.pos; }
162
+ }
163
+
164
+ function decodeNode(dec: Decoder): InternalNode {
165
+ const type = dec.readUint8();
166
+ const name = dec.readString();
167
+ const mode = dec.readUint32();
168
+ const createdAt = new Date(dec.readFloat64());
169
+ const updatedAt = new Date(dec.readFloat64());
170
+
171
+ if (type === TYPE_FILE) {
172
+ const compressed = dec.readUint8() === 0x01;
173
+ const content = dec.readBytes();
174
+ return { type: "file", name, mode, createdAt, updatedAt, compressed, content } satisfies InternalFileNode;
175
+ }
176
+
177
+ if (type === TYPE_DIR) {
178
+ const count = dec.readUint32();
179
+ const children = new Map<string, InternalNode>();
180
+ for (let i = 0; i < count; i++) {
181
+ const child = decodeNode(dec);
182
+ children.set(child.name, child);
183
+ }
184
+ return { type: "directory", name, mode, createdAt, updatedAt, children } satisfies InternalDirectoryNode;
185
+ }
186
+
187
+ throw new Error(`[VFS binary] Unknown node type: 0x${type.toString(16)}`);
188
+ }
189
+
190
+ /**
191
+ * Deserialise a binary Buffer produced by {@link encodeVfs} back into an
192
+ * InternalDirectoryNode tree. Throws on magic/version mismatch or truncation.
193
+ */
194
+ export function decodeVfs(buf: Buffer): InternalDirectoryNode {
195
+ if (buf.length < 5) throw new Error("[VFS binary] Buffer too short");
196
+
197
+ const magic = buf.slice(0, 4);
198
+ if (!magic.equals(MAGIC)) {
199
+ throw new Error("[VFS binary] Invalid magic — not a VFS binary snapshot");
200
+ }
201
+
202
+ const dec = new Decoder(buf);
203
+ // skip magic (4) + version (1)
204
+ for (let i = 0; i < 5; i++) dec.readUint8();
205
+
206
+ const root = decodeNode(dec);
207
+ if (root.type !== "directory") {
208
+ throw new Error("[VFS binary] Root node must be a directory");
209
+ }
210
+ return root as InternalDirectoryNode;
211
+ }
212
+
213
+ /**
214
+ * Returns true if `buf` looks like a VFS binary snapshot (starts with magic bytes).
215
+ * Used to auto-detect format when loading from disk.
216
+ */
217
+ export function isBinarySnapshot(buf: Buffer): boolean {
218
+ return buf.length >= 4 && buf.slice(0, 4).equals(MAGIC);
219
+ }
@@ -7,6 +7,7 @@ import type {
7
7
  InternalFileNode,
8
8
  InternalNode,
9
9
  } from "./internalTypes";
10
+ import { decodeVfs, encodeVfs, isBinarySnapshot } from "./binaryPack";
10
11
  import { getNode, getParentDirectory, normalizePath } from "./path";
11
12
  import type {
12
13
  RemoveOptions,
@@ -24,8 +25,8 @@ import type {
24
25
  * "memory" — pure in-memory, no disk I/O (default).
25
26
  *
26
27
  * "fs" — mirrors the VFS tree to a directory on the host filesystem.
27
- * `snapshotPath` must be set to the directory where the JSON
28
- * snapshot file will be read/written.
28
+ * `snapshotPath` must be set to the directory where the binary
29
+ * snapshot file will be read/written (`vfs-snapshot.vfsb`).
29
30
  */
30
31
  export type VfsPersistenceMode = "memory" | "fs";
31
32
 
@@ -53,7 +54,7 @@ export interface VfsOptions {
53
54
  * **Memory mode** (default) — all state lives in a fast recursive tree.
54
55
  * Use `toSnapshot()` / `fromSnapshot()` / `importSnapshot()` for serialisation.
55
56
  *
56
- * **FS mode** — same in-memory tree, but `restoreMirror()` loads a JSON
57
+ * **FS mode** — same in-memory tree, but `restoreMirror()` loads a binary
57
58
  * snapshot from disk and `flushMirror()` writes it back. This gives you
58
59
  * persistent VFS state across process restarts without any real POSIX filesystem
59
60
  * semantics leaking through.
@@ -86,7 +87,7 @@ class VirtualFileSystem extends EventEmitter {
86
87
  }
87
88
  this.snapshotFile = path.resolve(
88
89
  options.snapshotPath,
89
- "vfs-snapshot.json",
90
+ "vfs-snapshot.vfsb",
90
91
  );
91
92
  } else {
92
93
  this.snapshotFile = null;
@@ -151,7 +152,8 @@ class VirtualFileSystem extends EventEmitter {
151
152
  // ── Persistence ───────────────────────────────────────────────────────────
152
153
 
153
154
  /**
154
- * In `"fs"` mode: reads the JSON snapshot from disk and hydrates the tree.
155
+ * In `"fs"` mode: reads the binary snapshot (`vfs-snapshot.vfsb`) from disk.
156
+ * Automatically falls back to legacy JSON format for backward compatibility.
155
157
  * Silently succeeds when the snapshot file does not exist yet.
156
158
  *
157
159
  * In `"memory"` mode: no-op (kept for API compatibility).
@@ -162,9 +164,16 @@ class VirtualFileSystem extends EventEmitter {
162
164
  if (!fsSync.existsSync(this.snapshotFile)) return;
163
165
 
164
166
  try {
165
- const raw = fsSync.readFileSync(this.snapshotFile, "utf8");
166
- const snapshot: VfsSnapshot = JSON.parse(raw);
167
- this.root = this.deserializeDir(snapshot.root, "");
167
+ const raw = fsSync.readFileSync(this.snapshotFile);
168
+ if (isBinarySnapshot(raw)) {
169
+ // Fast binary format (current)
170
+ this.root = decodeVfs(raw);
171
+ } else {
172
+ // Legacy JSON fallback — auto-migrates on next flushMirror()
173
+ const snapshot: VfsSnapshot = JSON.parse(raw.toString("utf8"));
174
+ this.root = this.deserializeDir(snapshot.root, "");
175
+ console.info("[VirtualFileSystem] Migrating legacy JSON snapshot to binary format.");
176
+ }
168
177
  this.emit("snapshot:restore", { path: this.snapshotFile });
169
178
  } catch (err) {
170
179
  // Corrupt or unreadable snapshot — start fresh and warn
@@ -176,7 +185,8 @@ class VirtualFileSystem extends EventEmitter {
176
185
  }
177
186
 
178
187
  /**
179
- * In `"fs"` mode: serialises the in-memory tree to a JSON snapshot on disk.
188
+ * In `"fs"` mode: serialises the in-memory tree to a binary snapshot on disk
189
+ * (`vfs-snapshot.vfsb`). ~27% smaller and significantly faster than JSON+base64.
180
190
  * The directory is created if it does not exist.
181
191
  *
182
192
  * In `"memory"` mode: emits `"mirror:flush"` and returns (no disk write).
@@ -189,8 +199,8 @@ class VirtualFileSystem extends EventEmitter {
189
199
 
190
200
  const dir = path.dirname(this.snapshotFile);
191
201
  fsSync.mkdirSync(dir, { recursive: true });
192
- const snapshot = this.toSnapshot();
193
- fsSync.writeFileSync(this.snapshotFile, JSON.stringify(snapshot), "utf8");
202
+ const binary = encodeVfs(this.root);
203
+ fsSync.writeFileSync(this.snapshotFile, binary);
194
204
  this.emit("mirror:flush", { path: this.snapshotFile });
195
205
  }
196
206
 
@@ -1,3 +1,4 @@
1
+ /** biome-ignore-all lint/style/useNamingConvention: ENV VARS */
1
2
  import type { ShellModule } from "../types/commands";
2
3
 
3
4
  export const envCommand: ShellModule = {
@@ -66,22 +66,13 @@ export function createHelpCommand(_getNames: () => string[]): ShellModule {
66
66
  if (!mods || mods.length === 0) continue;
67
67
  lines.push(`\x1b[33m${CATEGORY_LABELS[cat] ?? cat}\x1b[0m`);
68
68
 
69
- // Two-column layout
70
69
  const sorted = [...mods].sort((a, b) => a.name.localeCompare(b.name));
71
- for (let i = 0; i < sorted.length; i += 2) {
72
- const left = sorted[i]!;
73
- const right = sorted[i + 1];
74
- const leftStr = ` \x1b[36m${padRight(left.name, 14)}\x1b[0m ${left.description ?? ""}`;
75
- const rightStr = right
76
- ? ` \x1b[36m${padRight(right.name, 14)}\x1b[0m ${right.description ?? ""}`
77
- : "";
78
- lines.push(rightStr ? `${leftStr.padEnd(44)}${rightStr}` : leftStr);
70
+ for (const mod of sorted) {
71
+ lines.push(` \x1b[36m${padRight(mod.name, 14)}\x1b[0m ${mod.description ?? ""}`);
79
72
  }
80
- lines.push("");
81
73
  }
82
74
 
83
- lines.push("Type \x1b[1mhelp <command>\x1b[0m for usage details.");
84
- return { stdout: lines.join("\n"), exitCode: 0 };
75
+ return { stdout: lines.join("\n"), exitCode: 0 };
85
76
  },
86
77
  };
87
78
  }
@@ -46,6 +46,7 @@ function parseBlocks(lines: string[]): Block[] {
46
46
  }
47
47
  i++;
48
48
  }
49
+ // biome-ignore lint/suspicious/noThenProperty: expected behavior for if/elif parsing
49
50
  blocks.push({ type: "if", cond, then: thenLines, elif: elifBlocks, else_: elseLines });
50
51
  } else if (line.startsWith("for ")) {
51
52
  const m = line.match(/^for\s+(\w+)\s+in\s+(.+?)(?:\s*;\s*do)?$/);
@@ -179,7 +180,7 @@ export const shCommand: ShellModule = {
179
180
  category: "shell",
180
181
  params: ["-c <script>", "[<file>]"],
181
182
  run: async (ctx: CommandContext) => {
182
- const { args, authUser, shell, cwd } = ctx;
183
+ const { args, shell, cwd } = ctx;
183
184
 
184
185
  // sh -c "inline script"
185
186
  if (ifFlag(args, "-c")) {
package/src/standalone.ts CHANGED
@@ -1,7 +1,10 @@
1
1
  import { VirtualSftpServer, VirtualShell, VirtualSshServer } from ".";
2
2
 
3
3
  const hostname = process.env.SSH_MIMIC_HOSTNAME ?? "typescript-vm";
4
- const virtualShell = new VirtualShell(hostname);
4
+ const virtualShell = new VirtualShell(hostname, undefined, {
5
+ mode: "fs",
6
+ snapshotPath: ".vfs",
7
+ });
5
8
 
6
9
  virtualShell.addCommand("demo", [], () => {
7
10
  return {