typescript-virtual-container 1.4.2 → 1.4.3

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 (48) hide show
  1. package/README.md +72 -35
  2. package/builds/self-standalone.js +160 -160
  3. package/builds/self-standalone.js.map +4 -4
  4. package/builds/standalone-wo-sftp.js +18 -18
  5. package/builds/standalone-wo-sftp.js.map +3 -3
  6. package/builds/standalone.js +46 -46
  7. package/builds/standalone.js.map +3 -3
  8. package/dist/VirtualFileSystem/index.d.ts +47 -0
  9. package/dist/VirtualFileSystem/index.d.ts.map +1 -1
  10. package/dist/VirtualFileSystem/index.js +159 -0
  11. package/dist/VirtualShell/index.d.ts +29 -0
  12. package/dist/VirtualShell/index.d.ts.map +1 -1
  13. package/dist/VirtualShell/index.js +29 -0
  14. package/dist/VirtualShell/shellParser.js +28 -1
  15. package/dist/commands/export.d.ts.map +1 -1
  16. package/dist/commands/export.js +5 -3
  17. package/dist/commands/registry.d.ts.map +1 -1
  18. package/dist/commands/registry.js +2 -0
  19. package/dist/commands/runtime.d.ts.map +1 -1
  20. package/dist/commands/runtime.js +28 -3
  21. package/dist/commands/seq.d.ts +4 -0
  22. package/dist/commands/seq.d.ts.map +1 -0
  23. package/dist/commands/seq.js +50 -0
  24. package/dist/commands/sh.d.ts +0 -6
  25. package/dist/commands/sh.d.ts.map +1 -1
  26. package/dist/commands/sh.js +153 -10
  27. package/dist/types/pipeline.d.ts +6 -0
  28. package/dist/types/pipeline.d.ts.map +1 -1
  29. package/dist/types/vfs.d.ts +15 -0
  30. package/dist/types/vfs.d.ts.map +1 -1
  31. package/dist/utils/expand.d.ts +9 -0
  32. package/dist/utils/expand.d.ts.map +1 -1
  33. package/dist/utils/expand.js +84 -2
  34. package/dist/utils/tokenize.d.ts.map +1 -1
  35. package/dist/utils/tokenize.js +40 -0
  36. package/package.json +1 -1
  37. package/src/VirtualFileSystem/index.ts +164 -1
  38. package/src/VirtualShell/index.ts +36 -0
  39. package/src/VirtualShell/shellParser.ts +26 -1
  40. package/src/commands/export.ts +5 -3
  41. package/src/commands/registry.ts +2 -0
  42. package/src/commands/runtime.ts +30 -3
  43. package/src/commands/seq.ts +43 -0
  44. package/src/commands/sh.ts +144 -19
  45. package/src/types/pipeline.ts +6 -0
  46. package/src/types/vfs.ts +17 -0
  47. package/src/utils/expand.ts +75 -2
  48. package/src/utils/tokenize.ts +20 -0
@@ -50,6 +50,10 @@ declare class VirtualFileSystem extends EventEmitter {
50
50
  private root;
51
51
  private readonly mode;
52
52
  private readonly snapshotFile;
53
+ /** Active host-directory mounts: vPath → { hostPath, readOnly } */
54
+ private readonly mounts;
55
+ /** True when running in a browser environment (no host FS access). */
56
+ private static readonly isBrowser;
53
57
  constructor(options?: VfsOptions);
54
58
  private makeDir;
55
59
  private makeFile;
@@ -75,6 +79,49 @@ declare class VirtualFileSystem extends EventEmitter {
75
79
  /** Returns the snapshot file path used in `"fs"` mode, or `null`. */
76
80
  getSnapshotPath(): string | null;
77
81
  /** Creates a directory (and any missing parents). */
82
+ /**
83
+ * Mount a host directory into the VFS at `vPath`.
84
+ *
85
+ * Files inside `vPath` are read directly from the host filesystem via
86
+ * `node:fs`. All standard VFS operations (`readFile`, `writeFile`,
87
+ * `exists`, `stat`, `list`) are transparently delegated.
88
+ *
89
+ * In browser environments the mount is silently ignored — `vPath` remains
90
+ * an empty in-memory directory.
91
+ *
92
+ * @param vPath Absolute path inside the VM (e.g. `"/app"`).
93
+ * @param hostPath Path on the host filesystem — relative paths are
94
+ * resolved from `process.cwd()`.
95
+ * @param readOnly When `true` (default), write operations inside the
96
+ * mount throw `EROFS: read-only file system`.
97
+ *
98
+ * @example
99
+ * ```ts
100
+ * shell.vfs.mount("/app", "./src", { readOnly: true });
101
+ * // cat /app/index.ts — reads ./src/index.ts from host
102
+ * ```
103
+ */
104
+ mount(vPath: string, hostPath: string, { readOnly }?: {
105
+ readOnly?: boolean;
106
+ }): void;
107
+ /**
108
+ * Unmount a previously mounted host directory.
109
+ * The in-memory VFS directory at `vPath` is preserved but the host
110
+ * delegation is removed.
111
+ */
112
+ unmount(vPath: string): void;
113
+ /** List all active mounts. */
114
+ getMounts(): Array<{
115
+ vPath: string;
116
+ hostPath: string;
117
+ readOnly: boolean;
118
+ }>;
119
+ /**
120
+ * If `targetPath` is inside a mount, return `{ hostPath, readOnly, relPath }`.
121
+ * `relPath` is the path relative to the mount's host directory.
122
+ * Returns `null` if the path is not under any mount.
123
+ */
124
+ private resolveMount;
78
125
  mkdir(targetPath: string, mode?: number): void;
79
126
  /**
80
127
  * Writes UTF-8 text or binary content into a file.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/VirtualFileSystem/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAW3C,OAAO,KAAK,EACX,aAAa,EACb,YAAY,EACZ,WAAW,EAIX,gBAAgB,EAChB,MAAM,cAAc,CAAC;AAItB;;;;;;GAMG;AACH,MAAM,MAAM,kBAAkB,GAAG,QAAQ,GAAG,IAAI,CAAC;AAEjD,MAAM,WAAW,UAAU;IAC1B;;;;;OAKG;IACH,IAAI,CAAC,EAAE,kBAAkB,CAAC;IAC1B;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACtB;AAID;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,cAAM,iBAAkB,SAAQ,YAAY;IAC3C,OAAO,CAAC,IAAI,CAAwB;IACpC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAqB;IAC1C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAgB;gBAEjC,OAAO,GAAE,UAAe;IAqBpC,OAAO,CAAC,OAAO;IAYf,OAAO,CAAC,QAAQ;IAkBhB,OAAO,CAAC,cAAc;IAwBtB;;;;;;OAMG;IACU,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC;IA4B3C;;;;;;OAMG;IACU,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;IAazC,4CAA4C;IACrC,OAAO,IAAI,kBAAkB;IAIpC,qEAAqE;IAC9D,eAAe,IAAI,MAAM,GAAG,IAAI;IAMvC,qDAAqD;IAC9C,KAAK,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,GAAE,MAAc,GAAG,IAAI;IAiB5D;;;OAGG;IACI,SAAS,CACf,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,GAAG,MAAM,EACxB,OAAO,GAAE,gBAAqB,GAC5B,IAAI;IAuCP;;;OAGG;IACI,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM;IAY3C,+DAA+D;IACxD,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM;IAY9C,4DAA4D;IACrD,MAAM,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO;IAS1C,mCAAmC;IAC5B,KAAK,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAIpD,gDAAgD;IACzC,IAAI,CAAC,UAAU,EAAE,MAAM,GAAG,YAAY;IA6B7C,2DAA2D;IACpD,IAAI,CAAC,OAAO,GAAE,MAAY,GAAG,MAAM,EAAE;IAS5C,wDAAwD;IACjD,IAAI,CAAC,OAAO,GAAE,MAAY,GAAG,MAAM;IAU1C,OAAO,CAAC,eAAe;IAqBvB,gDAAgD;IACzC,aAAa,CAAC,UAAU,GAAE,MAAY,GAAG,MAAM;IAItD,OAAO,CAAC,YAAY;IASpB,sDAAsD;IAC/C,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAY7C,oDAAoD;IAC7C,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAY/C;;;OAGG;IACI,OAAO,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IA2B1D,0DAA0D;IACnD,SAAS,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO;IAS7C;;;OAGG;IACI,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,SAAI,GAAG,MAAM;IAsB7D,wCAAwC;IACjC,MAAM,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,GAAE,aAAkB,GAAG,IAAI;IAsBpE,+BAA+B;IACxB,IAAI,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IA8BnD;;;;;OAKG;IACI,UAAU,IAAI,WAAW;IAIhC,OAAO,CAAC,YAAY;IAmBpB,OAAO,CAAC,aAAa;IAYrB;;;;;;;OAOG;WACW,YAAY,CAAC,QAAQ,EAAE,WAAW,GAAG,iBAAiB;IAMpE;;;;;;;;OAQG;IACI,cAAc,CAAC,QAAQ,EAAE,WAAW,GAAG,IAAI;IAKlD,OAAO,CAAC,cAAc;CAkCtB;AAED,eAAe,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/VirtualFileSystem/index.ts"],"names":[],"mappings":"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"}
@@ -32,6 +32,10 @@ class VirtualFileSystem extends EventEmitter {
32
32
  root;
33
33
  mode;
34
34
  snapshotFile;
35
+ /** Active host-directory mounts: vPath → { hostPath, readOnly } */
36
+ mounts = new Map();
37
+ /** True when running in a browser environment (no host FS access). */
38
+ static isBrowser = typeof process === "undefined" || typeof process.versions?.node === "undefined";
35
39
  constructor(options = {}) {
36
40
  super();
37
41
  this.mode = options.mode ?? "memory";
@@ -151,6 +155,80 @@ class VirtualFileSystem extends EventEmitter {
151
155
  }
152
156
  // ── Public filesystem API ─────────────────────────────────────────────────
153
157
  /** Creates a directory (and any missing parents). */
158
+ // ── Mount API ─────────────────────────────────────────────────────────────
159
+ /**
160
+ * Mount a host directory into the VFS at `vPath`.
161
+ *
162
+ * Files inside `vPath` are read directly from the host filesystem via
163
+ * `node:fs`. All standard VFS operations (`readFile`, `writeFile`,
164
+ * `exists`, `stat`, `list`) are transparently delegated.
165
+ *
166
+ * In browser environments the mount is silently ignored — `vPath` remains
167
+ * an empty in-memory directory.
168
+ *
169
+ * @param vPath Absolute path inside the VM (e.g. `"/app"`).
170
+ * @param hostPath Path on the host filesystem — relative paths are
171
+ * resolved from `process.cwd()`.
172
+ * @param readOnly When `true` (default), write operations inside the
173
+ * mount throw `EROFS: read-only file system`.
174
+ *
175
+ * @example
176
+ * ```ts
177
+ * shell.vfs.mount("/app", "./src", { readOnly: true });
178
+ * // cat /app/index.ts — reads ./src/index.ts from host
179
+ * ```
180
+ */
181
+ mount(vPath, hostPath, { readOnly = true } = {}) {
182
+ if (VirtualFileSystem.isBrowser)
183
+ return; // silently degrade in browser
184
+ const normalized = normalizePath(vPath);
185
+ const resolved = path.resolve(hostPath);
186
+ if (!fsSync.existsSync(resolved)) {
187
+ throw new Error(`VirtualFileSystem.mount: host path does not exist: "${resolved}"`);
188
+ }
189
+ if (!fsSync.statSync(resolved).isDirectory()) {
190
+ throw new Error(`VirtualFileSystem.mount: host path is not a directory: "${resolved}"`);
191
+ }
192
+ // Ensure the mount point exists in the VFS tree
193
+ this.mkdir(normalized);
194
+ this.mounts.set(normalized, { hostPath: resolved, readOnly });
195
+ this.emit("mount", { vPath: normalized, hostPath: resolved, readOnly });
196
+ }
197
+ /**
198
+ * Unmount a previously mounted host directory.
199
+ * The in-memory VFS directory at `vPath` is preserved but the host
200
+ * delegation is removed.
201
+ */
202
+ unmount(vPath) {
203
+ const normalized = normalizePath(vPath);
204
+ if (this.mounts.delete(normalized)) {
205
+ this.emit("unmount", { vPath: normalized });
206
+ }
207
+ }
208
+ /** List all active mounts. */
209
+ getMounts() {
210
+ return [...this.mounts.entries()].map(([vPath, opts]) => ({
211
+ vPath, ...opts,
212
+ }));
213
+ }
214
+ /**
215
+ * If `targetPath` is inside a mount, return `{ hostPath, readOnly, relPath }`.
216
+ * `relPath` is the path relative to the mount's host directory.
217
+ * Returns `null` if the path is not under any mount.
218
+ */
219
+ resolveMount(targetPath) {
220
+ const normalized = normalizePath(targetPath);
221
+ // Iterate mounts from most specific to least specific
222
+ const sorted = [...this.mounts.entries()].sort(([a], [b]) => b.length - a.length);
223
+ for (const [vBase, opts] of sorted) {
224
+ if (normalized === vBase || normalized.startsWith(`${vBase}/`)) {
225
+ const relPath = normalized.slice(vBase.length).replace(/^\//, "");
226
+ const fullHostPath = relPath ? path.join(opts.hostPath, relPath) : opts.hostPath;
227
+ return { hostPath: opts.hostPath, readOnly: opts.readOnly, relPath, fullHostPath };
228
+ }
229
+ }
230
+ return null;
231
+ }
154
232
  mkdir(targetPath, mode = 0o755) {
155
233
  const normalized = normalizePath(targetPath);
156
234
  const existing = (() => {
@@ -171,6 +249,17 @@ class VirtualFileSystem extends EventEmitter {
171
249
  * Parent directories are created when missing.
172
250
  */
173
251
  writeFile(targetPath, content, options = {}) {
252
+ // Delegate to host FS if inside a mount
253
+ const m = this.resolveMount(targetPath);
254
+ if (m) {
255
+ if (m.readOnly)
256
+ throw new Error(`EROFS: read-only file system, open '${m.fullHostPath}'`);
257
+ const dir = path.dirname(m.fullHostPath);
258
+ if (!fsSync.existsSync(dir))
259
+ fsSync.mkdirSync(dir, { recursive: true });
260
+ fsSync.writeFileSync(m.fullHostPath, Buffer.isBuffer(content) ? content : Buffer.from(content, "utf8"));
261
+ return;
262
+ }
174
263
  const normalized = normalizePath(targetPath);
175
264
  const { parent, name } = getParentDirectory(this.root, normalized, true, (p) => this.mkdirRecursive(p, 0o755));
176
265
  const existing = parent.children.get(name);
@@ -200,6 +289,12 @@ class VirtualFileSystem extends EventEmitter {
200
289
  * Gzip-compressed files are transparently decompressed.
201
290
  */
202
291
  readFile(targetPath) {
292
+ const m = this.resolveMount(targetPath);
293
+ if (m) {
294
+ if (!fsSync.existsSync(m.fullHostPath))
295
+ throw new Error(`ENOENT: no such file or directory, open '${m.fullHostPath}'`);
296
+ return fsSync.readFileSync(m.fullHostPath, "utf8");
297
+ }
203
298
  const normalized = normalizePath(targetPath);
204
299
  const node = getNode(this.root, normalized);
205
300
  if (node.type !== "file") {
@@ -212,6 +307,12 @@ class VirtualFileSystem extends EventEmitter {
212
307
  }
213
308
  /** Reads file content as a Buffer (decompresses if needed). */
214
309
  readFileRaw(targetPath) {
310
+ const m = this.resolveMount(targetPath);
311
+ if (m) {
312
+ if (!fsSync.existsSync(m.fullHostPath))
313
+ throw new Error(`ENOENT: no such file or directory, open '${m.fullHostPath}'`);
314
+ return fsSync.readFileSync(m.fullHostPath);
315
+ }
215
316
  const normalized = normalizePath(targetPath);
216
317
  const node = getNode(this.root, normalized);
217
318
  if (node.type !== "file") {
@@ -224,6 +325,9 @@ class VirtualFileSystem extends EventEmitter {
224
325
  }
225
326
  /** Returns true when a file or directory exists at path. */
226
327
  exists(targetPath) {
328
+ const m = this.resolveMount(targetPath);
329
+ if (m)
330
+ return fsSync.existsSync(m.fullHostPath);
227
331
  try {
228
332
  getNode(this.root, normalizePath(targetPath));
229
333
  return true;
@@ -238,6 +342,35 @@ class VirtualFileSystem extends EventEmitter {
238
342
  }
239
343
  /** Returns metadata for a file or directory. */
240
344
  stat(targetPath) {
345
+ const m = this.resolveMount(targetPath);
346
+ if (m) {
347
+ if (!fsSync.existsSync(m.fullHostPath))
348
+ throw new Error(`ENOENT: stat '${m.fullHostPath}'`);
349
+ const hst = fsSync.statSync(m.fullHostPath);
350
+ const name = m.relPath.split("/").pop() ?? m.fullHostPath.split("/").pop() ?? "";
351
+ const now = hst.mtime;
352
+ if (hst.isDirectory()) {
353
+ return {
354
+ type: "directory",
355
+ name,
356
+ path: normalizePath(targetPath),
357
+ mode: 0o755,
358
+ createdAt: hst.birthtime,
359
+ updatedAt: now,
360
+ childrenCount: fsSync.readdirSync(m.fullHostPath).length,
361
+ };
362
+ }
363
+ return {
364
+ type: "file",
365
+ name,
366
+ path: normalizePath(targetPath),
367
+ mode: m.readOnly ? 0o444 : 0o644,
368
+ createdAt: hst.birthtime,
369
+ updatedAt: now,
370
+ compressed: false,
371
+ size: hst.size,
372
+ };
373
+ }
241
374
  const normalized = normalizePath(targetPath);
242
375
  const node = getNode(this.root, normalized);
243
376
  const name = normalized === "/" ? "" : path.posix.basename(normalized);
@@ -267,6 +400,17 @@ class VirtualFileSystem extends EventEmitter {
267
400
  }
268
401
  /** Lists direct children names of a directory (sorted). */
269
402
  list(dirPath = "/") {
403
+ const m = this.resolveMount(dirPath);
404
+ if (m) {
405
+ if (!fsSync.existsSync(m.fullHostPath))
406
+ return [];
407
+ try {
408
+ return fsSync.readdirSync(m.fullHostPath).sort();
409
+ }
410
+ catch {
411
+ return [];
412
+ }
413
+ }
270
414
  const normalized = normalizePath(dirPath);
271
415
  const node = getNode(this.root, normalized);
272
416
  if (node.type !== "directory") {
@@ -402,6 +546,21 @@ class VirtualFileSystem extends EventEmitter {
402
546
  }
403
547
  /** Removes a file or directory node. */
404
548
  remove(targetPath, options = {}) {
549
+ const m = this.resolveMount(targetPath);
550
+ if (m) {
551
+ if (m.readOnly)
552
+ throw new Error(`EROFS: read-only file system, unlink '${m.fullHostPath}'`);
553
+ if (!fsSync.existsSync(m.fullHostPath))
554
+ throw new Error(`ENOENT: no such file or directory, unlink '${m.fullHostPath}'`);
555
+ const hst = fsSync.statSync(m.fullHostPath);
556
+ if (hst.isDirectory()) {
557
+ fsSync.rmSync(m.fullHostPath, { recursive: options.recursive ?? false });
558
+ }
559
+ else {
560
+ fsSync.unlinkSync(m.fullHostPath);
561
+ }
562
+ return;
563
+ }
405
564
  const normalized = normalizePath(targetPath);
406
565
  if (normalized === "/")
407
566
  throw new Error("Cannot remove root directory.");
@@ -147,6 +147,35 @@ declare class VirtualShell extends EventEmitter {
147
147
  * reading `/proc` files for up-to-date values.
148
148
  */
149
149
  refreshProcFs(): void;
150
+ /**
151
+ * Mount a host directory into the VFS at `vPath`.
152
+ *
153
+ * Delegates file operations inside `vPath` to the host filesystem via
154
+ * `node:fs`. Silently ignored in browser environments.
155
+ *
156
+ * @param vPath Absolute path inside the VM (e.g. `"/app"`).
157
+ * @param hostPath Path on the host — relative paths are resolved from `process.cwd()`.
158
+ * @param options `{ readOnly?: boolean }` — default `true`.
159
+ *
160
+ * @example
161
+ * ```ts
162
+ * const shell = new VirtualShell("dev-vm");
163
+ * await shell.ensureInitialized();
164
+ * shell.mount("/workspace", "./my-project");
165
+ * // shell commands can now read ./my-project files via /workspace
166
+ * ```
167
+ */
168
+ mount(vPath: string, hostPath: string, options?: {
169
+ readOnly?: boolean;
170
+ }): void;
171
+ /** Remove a previously mounted host directory. */
172
+ unmount(vPath: string): void;
173
+ /** List all active mounts. */
174
+ getMounts(): Array<{
175
+ vPath: string;
176
+ hostPath: string;
177
+ readOnly: boolean;
178
+ }>;
150
179
  /**
151
180
  * Updates only the session-dependent `/proc` entries (`/proc/<pid>`,
152
181
  * `/proc/self`). Cheaper than a full `refreshProcFs()` — call this
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/VirtualShell/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/VirtualShell/index.ts"],"names":[],"mappings":"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"}
@@ -196,6 +196,35 @@ class VirtualShell extends EventEmitter {
196
196
  refreshProcFs() {
197
197
  refreshProc(this.vfs, this.properties, this.hostname, this.startTime, this.users.listActiveSessions());
198
198
  }
199
+ /**
200
+ * Mount a host directory into the VFS at `vPath`.
201
+ *
202
+ * Delegates file operations inside `vPath` to the host filesystem via
203
+ * `node:fs`. Silently ignored in browser environments.
204
+ *
205
+ * @param vPath Absolute path inside the VM (e.g. `"/app"`).
206
+ * @param hostPath Path on the host — relative paths are resolved from `process.cwd()`.
207
+ * @param options `{ readOnly?: boolean }` — default `true`.
208
+ *
209
+ * @example
210
+ * ```ts
211
+ * const shell = new VirtualShell("dev-vm");
212
+ * await shell.ensureInitialized();
213
+ * shell.mount("/workspace", "./my-project");
214
+ * // shell commands can now read ./my-project files via /workspace
215
+ * ```
216
+ */
217
+ mount(vPath, hostPath, options = {}) {
218
+ this.vfs.mount(vPath, hostPath, options);
219
+ }
220
+ /** Remove a previously mounted host directory. */
221
+ unmount(vPath) {
222
+ this.vfs.unmount(vPath);
223
+ }
224
+ /** List all active mounts. */
225
+ getMounts() {
226
+ return this.vfs.getMounts();
227
+ }
199
228
  /**
200
229
  * Updates only the session-dependent `/proc` entries (`/proc/<pid>`,
201
230
  * `/proc/self`). Cheaper than a full `refreshProcFs()` — call this
@@ -202,6 +202,9 @@ function parseCommandWithRedirections(token) {
202
202
  let outputFile;
203
203
  let appendOutput = false;
204
204
  let i = 0;
205
+ let stderrFile;
206
+ let stderrAppend = false;
207
+ let stderrToStdout = false;
205
208
  while (i < parts.length) {
206
209
  const part = parts[i];
207
210
  if (part === "<") {
@@ -227,11 +230,35 @@ function parseCommandWithRedirections(token) {
227
230
  appendOutput = false;
228
231
  i++;
229
232
  }
233
+ else if (part === "2>&1") {
234
+ stderrToStdout = true;
235
+ i++;
236
+ }
237
+ else if (part === "2>>") {
238
+ i++;
239
+ if (i >= parts.length)
240
+ throw new Error("Syntax error: expected filename after 2>>");
241
+ stderrFile = parts[i];
242
+ stderrAppend = true;
243
+ i++;
244
+ }
245
+ else if (part === "2>") {
246
+ i++;
247
+ if (i >= parts.length)
248
+ throw new Error("Syntax error: expected filename after 2>");
249
+ stderrFile = parts[i];
250
+ stderrAppend = false;
251
+ i++;
252
+ }
230
253
  else {
231
254
  cmdParts.push(part);
232
255
  i++;
233
256
  }
234
257
  }
235
258
  const name = (cmdParts[0] ?? "").toLowerCase();
236
- return { name, args: cmdParts.slice(1), inputFile, outputFile, appendOutput };
259
+ return {
260
+ name, args: cmdParts.slice(1),
261
+ inputFile, outputFile, appendOutput,
262
+ stderrFile, stderrAppend, stderrToStdout,
263
+ };
237
264
  }
@@ -1 +1 @@
1
- {"version":3,"file":"export.d.ts","sourceRoot":"","sources":["../../src/commands/export.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD;;;;GAIG;AACH,eAAO,MAAM,aAAa,EAAE,WAwB3B,CAAC"}
1
+ {"version":3,"file":"export.d.ts","sourceRoot":"","sources":["../../src/commands/export.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD;;;;GAIG;AACH,eAAO,MAAM,aAAa,EAAE,WA0B3B,CAAC"}
@@ -9,13 +9,15 @@ export const exportCommand = {
9
9
  category: "shell",
10
10
  params: ["[VAR=value]"],
11
11
  run: ({ args, env }) => {
12
- if (args.length === 0) {
12
+ // export -p or export with no args list all exported vars
13
+ if (args.length === 0 || (args.length === 1 && args[0] === "-p")) {
13
14
  const out = Object.entries(env.vars)
15
+ .filter(([k]) => k && /^[A-Za-z_][A-Za-z0-9_]*$/.test(k))
14
16
  .map(([k, v]) => `declare -x ${k}="${v}"`)
15
17
  .join("\n");
16
- return { stdout: out, exitCode: 0 };
18
+ return { stdout: out ? `${out}\n` : "", exitCode: 0 };
17
19
  }
18
- for (const arg of args) {
20
+ for (const arg of args.filter((a) => a !== "-p")) {
19
21
  if (arg.includes("=")) {
20
22
  const eq = arg.indexOf("=");
21
23
  const name = arg.slice(0, eq);
@@ -1 +1 @@
1
- {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/commands/registry.ts"],"names":[],"mappings":"AAAA,qEAAqE;AACrE,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAmNpF,wBAAgB,eAAe,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,CAYzD;AAED,wBAAgB,mBAAmB,CAClC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EAAE,EAChB,GAAG,EAAE,CAAC,GAAG,EAAE,cAAc,KAAK,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC,GAClE,WAAW,CAEb;AAED,wBAAgB,eAAe,IAAI,MAAM,EAAE,CAG1C;AAED,wBAAgB,uBAAuB,IAAI,WAAW,EAAE,CAEvD;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CAGnE"}
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/commands/registry.ts"],"names":[],"mappings":"AAAA,qEAAqE;AACrE,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAqNpF,wBAAgB,eAAe,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,CAYzD;AAED,wBAAgB,mBAAmB,CAClC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EAAE,EAChB,GAAG,EAAE,CAAC,GAAG,EAAE,cAAc,KAAK,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC,GAClE,WAAW,CAEb;AAED,wBAAgB,eAAe,IAAI,MAAM,EAAE,CAG1C;AAED,wBAAgB,uBAAuB,IAAI,WAAW,EAAE,CAEvD;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CAGnE"}
@@ -34,6 +34,7 @@ import { htopCommand } from "./htop";
34
34
  import { idCommand } from "./id";
35
35
  import { killCommand } from "./kill";
36
36
  import { lnCommand, readlinkCommand } from "./ln";
37
+ import { seqCommand } from "./seq";
37
38
  import { statCommand } from "./stat";
38
39
  import { lsCommand } from "./ls";
39
40
  import { lsbReleaseCommand } from "./lsb-release";
@@ -96,6 +97,7 @@ const BASE_COMMANDS = [
96
97
  lnCommand,
97
98
  readlinkCommand,
98
99
  chmodCommand,
100
+ seqCommand,
99
101
  statCommand,
100
102
  findCommand,
101
103
  // Text processing
@@ -1 +1 @@
1
- {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/commands/runtime.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAEpD,OAAO,KAAK,EACR,WAAW,EACX,aAAa,EACb,QAAQ,EACX,MAAM,mBAAmB,CAAC;AAK3B,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,QAAQ,CAc3E;AAyCD,wBAAsB,gBAAgB,CACrC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EAAE,EACd,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,WAAW,EACjB,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,YAAY,EACnB,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,GAAG,EAAE,QAAQ,GACX,OAAO,CAAC,aAAa,CAAC,CA4ExB;AAED,wBAAsB,UAAU,CAC/B,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,WAAW,EACjB,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,YAAY,EACnB,KAAK,CAAC,EAAE,MAAM,EACd,GAAG,CAAC,EAAE,QAAQ,GACZ,OAAO,CAAC,aAAa,CAAC,CA+HxB"}
1
+ {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/commands/runtime.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAEpD,OAAO,KAAK,EACR,WAAW,EACX,aAAa,EACb,QAAQ,EACX,MAAM,mBAAmB,CAAC;AAK3B,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,QAAQ,CAc3E;AAyCD,wBAAsB,gBAAgB,CACrC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EAAE,EACd,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,WAAW,EACjB,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,YAAY,EACnB,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,GAAG,EAAE,QAAQ,GACX,OAAO,CAAC,aAAa,CAAC,CA4ExB;AAED,wBAAsB,UAAU,CAC/B,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,WAAW,EACjB,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,YAAY,EACnB,KAAK,CAAC,EAAE,MAAM,EACd,GAAG,CAAC,EAAE,QAAQ,GACZ,OAAO,CAAC,aAAa,CAAC,CA0JxB"}
@@ -1,7 +1,7 @@
1
1
  /** biome-ignore-all lint/style/useNamingConvention: ENV VARIABLES */
2
2
  import { executeStatements } from "../SSHMimic/executor";
3
3
  import { parseScript } from "../VirtualShell/shellParser";
4
- import { expandAsync } from "../utils/expand";
4
+ import { expandAsync, expandBraces } from "../utils/expand";
5
5
  import { tokenizeCommand } from "../utils/tokenize";
6
6
  import { resolveModule } from "./registry";
7
7
  export function makeDefaultEnv(authUser, hostname) {
@@ -135,13 +135,37 @@ export async function runCommand(rawInput, authUser, hostname, mode, cwd, shell,
135
135
  const aliasExpanded = aliasVal
136
136
  ? trimmed.replace(rawFirstWord, aliasVal)
137
137
  : trimmed;
138
+ // Detect sh-syntax constructs that must be handled by the sh interpreter
139
+ const isShScript = /\bfor\s+\w+\s+in\b/.test(aliasExpanded) ||
140
+ /\bwhile\s+/.test(aliasExpanded) ||
141
+ /\bif\s+/.test(aliasExpanded) ||
142
+ /\w+\s*\(\s*\)\s*\{/.test(aliasExpanded) ||
143
+ /\bfunction\s+\w+/.test(aliasExpanded) ||
144
+ /\(\(\s*.+\s*\)\)/.test(aliasExpanded);
138
145
  const hasOperators = /(?<![|&])[|](?![|])/.test(aliasExpanded) ||
139
146
  aliasExpanded.includes(">") ||
140
147
  aliasExpanded.includes("<") ||
141
148
  aliasExpanded.includes("&&") ||
142
149
  aliasExpanded.includes("||") ||
143
150
  aliasExpanded.includes(";");
144
- if (hasOperators) {
151
+ if ((isShScript && rawFirstWord !== "sh" && rawFirstWord !== "bash") || hasOperators) {
152
+ // sh-syntax: route through sh interpreter to handle for/while/functions
153
+ if (isShScript && rawFirstWord !== "sh" && rawFirstWord !== "bash") {
154
+ const shMod = resolveModule("sh");
155
+ if (shMod) {
156
+ return await shMod.run({
157
+ authUser, hostname,
158
+ activeSessions: shell.users.listActiveSessions(),
159
+ rawInput: aliasExpanded,
160
+ mode,
161
+ args: ["-c", aliasExpanded],
162
+ stdin: undefined,
163
+ cwd,
164
+ shell,
165
+ env: shellEnv,
166
+ });
167
+ }
168
+ }
145
169
  const script = parseScript(aliasExpanded);
146
170
  if (!script.isValid)
147
171
  return { stderr: script.error || "Syntax error", exitCode: 1 };
@@ -158,7 +182,8 @@ export async function runCommand(rawInput, authUser, hostname, mode, cwd, shell,
158
182
  const expanded = await expandAsync(aliasExpanded, shellEnv.vars, shellEnv.lastExitCode, (sub) => runCommand(sub, authUser, hostname, mode, cwd, shell, undefined, shellEnv).then((r) => r.stdout ?? ""));
159
183
  const parts = tokenizeCommand(expanded.trim());
160
184
  const commandName = parts[0]?.toLowerCase() ?? "";
161
- const args = parts.slice(1);
185
+ // Apply brace expansion to each arg token
186
+ const args = parts.slice(1).flatMap(expandBraces);
162
187
  const mod = resolveModule(commandName);
163
188
  if (!mod) {
164
189
  const vfsBinary = resolveVfsBinary(commandName, shellEnv, shell, authUser);
@@ -0,0 +1,4 @@
1
+ import type { ShellModule } from "../types/commands";
2
+ /** Generate sequences of numbers. seq LAST / seq FIRST LAST / seq FIRST INCREMENT LAST */
3
+ export declare const seqCommand: ShellModule;
4
+ //# sourceMappingURL=seq.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"seq.d.ts","sourceRoot":"","sources":["../../src/commands/seq.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD,0FAA0F;AAC1F,eAAO,MAAM,UAAU,EAAE,WAuCxB,CAAC"}
@@ -0,0 +1,50 @@
1
+ /** Generate sequences of numbers. seq LAST / seq FIRST LAST / seq FIRST INCREMENT LAST */
2
+ export const seqCommand = {
3
+ name: "seq",
4
+ description: "Print a sequence of numbers",
5
+ category: "text",
6
+ params: ["[FIRST [INCREMENT]] LAST"],
7
+ run: ({ args }) => {
8
+ const nums = args.filter((a) => !a.startsWith("-") || /^-[\d.]/.test(a)).map(Number);
9
+ const sep = (() => { const i = args.indexOf("-s"); return i !== -1 ? (args[i + 1] ?? "\n") : "\n"; })();
10
+ const fmt = (() => { const i = args.indexOf("-f"); return i !== -1 ? (args[i + 1] ?? "%g") : null; })();
11
+ const width = args.includes("-w");
12
+ let first = 1, inc = 1, last;
13
+ if (nums.length === 1) {
14
+ last = nums[0];
15
+ }
16
+ else if (nums.length === 2) {
17
+ first = nums[0];
18
+ last = nums[1];
19
+ }
20
+ else {
21
+ first = nums[0];
22
+ inc = nums[1];
23
+ last = nums[2];
24
+ }
25
+ if (inc === 0)
26
+ return { stderr: "seq: zero increment\n", exitCode: 1 };
27
+ if ((inc > 0 && first > last) || (inc < 0 && first < last))
28
+ return { stdout: "", exitCode: 0 };
29
+ const results = [];
30
+ const maxSteps = 100000;
31
+ let steps = 0;
32
+ for (let n = first; inc > 0 ? n <= last : n >= last; n = Math.round((n + inc) * 1e10) / 1e10) {
33
+ if (++steps > maxSteps)
34
+ break;
35
+ let s;
36
+ if (fmt) {
37
+ s = fmt.replace("%g", String(n)).replace("%f", n.toFixed(6)).replace("%d", String(Math.trunc(n)));
38
+ }
39
+ else {
40
+ s = Number.isInteger(n) ? String(n) : n.toPrecision(12).replace(/\.?0+$/, "");
41
+ }
42
+ if (width) {
43
+ const maxLen = String(Math.trunc(last)).length;
44
+ s = s.padStart(maxLen, "0");
45
+ }
46
+ results.push(s);
47
+ }
48
+ return { stdout: `${results.join(sep)}\n`, exitCode: 0 };
49
+ },
50
+ };
@@ -1,9 +1,3 @@
1
1
  import type { ShellModule } from "../types/commands";
2
- /**
3
- * Execute shell scripts or commands with a minimal shell interpreter.
4
- * Supports if/elif/else, for loops, while loops, and variable expansion.
5
- * @category shell
6
- * @params ["-c <script>", "[<file>]"]
7
- */
8
2
  export declare const shCommand: ShellModule;
9
3
  //# sourceMappingURL=sh.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sh.d.ts","sourceRoot":"","sources":["../../src/commands/sh.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAGX,WAAW,EACX,MAAM,mBAAmB,CAAC;AA4R3B;;;;;GAKG;AACH,eAAO,MAAM,SAAS,EAAE,WA4CvB,CAAC"}
1
+ {"version":3,"file":"sh.d.ts","sourceRoot":"","sources":["../../src/commands/sh.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAGX,WAAW,EACX,MAAM,mBAAmB,CAAC;AAqa3B,eAAO,MAAM,SAAS,EAAE,WAsCvB,CAAC"}