typescript-virtual-container 1.4.1 → 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 (67) hide show
  1. package/.vscode/settings.json +2 -0
  2. package/README.md +77 -36
  3. package/benchmark-virtualshell.ts +3 -11
  4. package/builds/self-standalone.js +224 -224
  5. package/builds/self-standalone.js.map +4 -4
  6. package/builds/standalone-wo-sftp.js +23 -23
  7. package/builds/standalone-wo-sftp.js.map +3 -3
  8. package/builds/standalone.js +23 -23
  9. package/builds/standalone.js.map +3 -3
  10. package/dist/VirtualFileSystem/index.d.ts +47 -0
  11. package/dist/VirtualFileSystem/index.d.ts.map +1 -1
  12. package/dist/VirtualFileSystem/index.js +159 -0
  13. package/dist/VirtualShell/index.d.ts +29 -0
  14. package/dist/VirtualShell/index.d.ts.map +1 -1
  15. package/dist/VirtualShell/index.js +29 -0
  16. package/dist/VirtualShell/shell.d.ts.map +1 -1
  17. package/dist/VirtualShell/shell.js +6 -10
  18. package/dist/VirtualShell/shellParser.js +28 -1
  19. package/dist/VirtualUserManager/index.d.ts.map +1 -1
  20. package/dist/VirtualUserManager/index.js +4 -4
  21. package/dist/commands/export.d.ts.map +1 -1
  22. package/dist/commands/export.js +5 -3
  23. package/dist/commands/helpers.js +1 -1
  24. package/dist/commands/history.js +2 -2
  25. package/dist/commands/registry.d.ts.map +1 -1
  26. package/dist/commands/registry.js +2 -0
  27. package/dist/commands/runtime.d.ts.map +1 -1
  28. package/dist/commands/runtime.js +28 -3
  29. package/dist/commands/seq.d.ts +4 -0
  30. package/dist/commands/seq.d.ts.map +1 -0
  31. package/dist/commands/seq.js +50 -0
  32. package/dist/commands/sh.d.ts +0 -6
  33. package/dist/commands/sh.d.ts.map +1 -1
  34. package/dist/commands/sh.js +153 -10
  35. package/dist/modules/linuxRootfs.d.ts +1 -1
  36. package/dist/modules/linuxRootfs.d.ts.map +1 -1
  37. package/dist/modules/linuxRootfs.js +5 -5
  38. package/dist/self-standalone.js +149 -102
  39. package/dist/types/pipeline.d.ts +6 -0
  40. package/dist/types/pipeline.d.ts.map +1 -1
  41. package/dist/types/vfs.d.ts +15 -0
  42. package/dist/types/vfs.d.ts.map +1 -1
  43. package/dist/utils/expand.d.ts +9 -0
  44. package/dist/utils/expand.d.ts.map +1 -1
  45. package/dist/utils/expand.js +84 -2
  46. package/dist/utils/tokenize.d.ts.map +1 -1
  47. package/dist/utils/tokenize.js +40 -0
  48. package/package.json +1 -1
  49. package/src/VirtualFileSystem/index.ts +164 -1
  50. package/src/VirtualShell/index.ts +36 -0
  51. package/src/VirtualShell/shell.ts +6 -11
  52. package/src/VirtualShell/shellParser.ts +26 -1
  53. package/src/VirtualUserManager/index.ts +4 -4
  54. package/src/commands/export.ts +5 -3
  55. package/src/commands/helpers.ts +1 -1
  56. package/src/commands/history.ts +2 -2
  57. package/src/commands/registry.ts +2 -0
  58. package/src/commands/runtime.ts +30 -3
  59. package/src/commands/seq.ts +43 -0
  60. package/src/commands/sh.ts +144 -19
  61. package/src/modules/linuxRootfs.ts +6 -6
  62. package/src/self-standalone.ts +190 -141
  63. package/src/types/pipeline.ts +6 -0
  64. package/src/types/vfs.ts +17 -0
  65. package/src/utils/expand.ts +75 -2
  66. package/src/utils/tokenize.ts +20 -0
  67. package/tests/helpers.test.ts +3 -3
@@ -1,14 +1,18 @@
1
1
  import { readFile, unlink, writeFile } from "node:fs/promises";
2
+ import * as path from "node:path";
2
3
  import { basename } from "node:path";
3
4
  import { stdin, stdout } from "node:process";
4
5
  import { createInterface } from "node:readline";
6
+ import { getCommandNames } from "./commands/registry";
5
7
  import { makeDefaultEnv, runCommand } from "./commands/runtime";
6
8
  import { spawnNanoEditorProcess } from "./modules/shellInteractive";
9
+ import { resolvePath } from "./modules/shellRuntime";
7
10
  import { buildLoginBanner } from "./SSHMimic/loginBanner";
8
11
  import { buildPrompt } from "./SSHMimic/prompt";
9
12
  import { VirtualShell } from "./VirtualShell";
10
13
  const hostname = process.env.SSH_MIMIC_HOSTNAME ?? "typescript-vm";
11
14
  const argv = process.argv.slice(2);
15
+ // ── CLI args ──────────────────────────────────────────────────────────────────
12
16
  function readUserArg() {
13
17
  for (let index = 0; index < argv.length; index += 1) {
14
18
  const current = argv[index];
@@ -30,11 +34,11 @@ const virtualShell = new VirtualShell(hostname, undefined, {
30
34
  mode: "fs",
31
35
  snapshotPath: ".vfs",
32
36
  });
37
+ // ── VFS helpers ───────────────────────────────────────────────────────────────
33
38
  function readLastLogin(username) {
34
- const lastlogPath = `/virtual-env-js/.lastlog/${username}.json`;
35
- if (!virtualShell.vfs.exists(lastlogPath)) {
39
+ const lastlogPath = `/home/${username}/.lastlog`;
40
+ if (!virtualShell.vfs.exists(lastlogPath))
36
41
  return null;
37
- }
38
42
  try {
39
43
  return JSON.parse(virtualShell.vfs.readFile(lastlogPath));
40
44
  }
@@ -42,6 +46,63 @@ function readLastLogin(username) {
42
46
  return null;
43
47
  }
44
48
  }
49
+ function writeLastLogin(username, from) {
50
+ virtualShell.vfs.writeFile(`/home/${username}/.lastlog`, JSON.stringify({ at: new Date().toISOString(), from }));
51
+ }
52
+ async function flushVfs() {
53
+ await virtualShell.vfs.flushMirror();
54
+ }
55
+ function loadHistory(authUser) {
56
+ const historyPath = `/home/${authUser}/.bash_history`;
57
+ if (!virtualShell.vfs.exists(historyPath)) {
58
+ virtualShell.vfs.writeFile(historyPath, "");
59
+ return [];
60
+ }
61
+ return virtualShell.vfs
62
+ .readFile(historyPath)
63
+ .split("\n")
64
+ .map((line) => line.trim())
65
+ .filter((line) => line.length > 0);
66
+ }
67
+ function saveHistory(history, authUser) {
68
+ const data = history.length > 0 ? `${history.join("\n")}\n` : "";
69
+ virtualShell.vfs.writeFile(`/home/${authUser}/.bash_history`, data);
70
+ }
71
+ // ── Tab completion ────────────────────────────────────────────────────────────
72
+ function listPathCompletions(vfs, cwd, prefix) {
73
+ const slashIndex = prefix.lastIndexOf("/");
74
+ const dirPart = slashIndex >= 0 ? prefix.slice(0, slashIndex + 1) : "";
75
+ const namePart = slashIndex >= 0 ? prefix.slice(slashIndex + 1) : prefix;
76
+ const basePath = resolvePath(cwd, dirPart || ".");
77
+ try {
78
+ return vfs
79
+ .list(basePath)
80
+ .filter((e) => !e.startsWith(".") && e.startsWith(namePart))
81
+ .map((e) => {
82
+ const fullPath = path.posix.join(basePath, e);
83
+ const st = vfs.stat(fullPath);
84
+ return `${dirPart}${e}${st.type === "directory" ? "/" : ""}`;
85
+ })
86
+ .sort();
87
+ }
88
+ catch {
89
+ return [];
90
+ }
91
+ }
92
+ function makeCompleter(getState) {
93
+ const commandNames = Array.from(new Set(getCommandNames())).sort();
94
+ return (line, cb) => {
95
+ const { cwd } = getState();
96
+ // Extract the token under/before cursor (last whitespace-separated word)
97
+ const token = line.split(/\s+/).at(-1) ?? "";
98
+ const isFirstToken = line.trimStart() === token;
99
+ const cmdHits = isFirstToken ? commandNames.filter((n) => n.startsWith(token)) : [];
100
+ const pathHits = listPathCompletions(virtualShell.vfs, cwd, token);
101
+ const hits = Array.from(new Set([...cmdHits, ...pathHits])).sort();
102
+ cb(null, [hits, token]);
103
+ };
104
+ }
105
+ // ── Hidden password input ─────────────────────────────────────────────────────
45
106
  function askHiddenQuestion(rl, promptText) {
46
107
  return new Promise((resolve) => {
47
108
  if (!stdin.isTTY || !stdout.isTTY) {
@@ -52,10 +113,8 @@ function askHiddenQuestion(rl, promptText) {
52
113
  let buffer = "";
53
114
  const cleanup = () => {
54
115
  stdin.off("data", onData);
55
- if (!wasRawMode) {
116
+ if (!wasRawMode)
56
117
  stdin.setRawMode(false);
57
- }
58
- rl.resume();
59
118
  };
60
119
  const finish = (value) => {
61
120
  cleanup();
@@ -64,8 +123,8 @@ function askHiddenQuestion(rl, promptText) {
64
123
  };
65
124
  const onData = (chunk) => {
66
125
  const input = chunk.toString("utf8");
67
- for (let index = 0; index < input.length; index += 1) {
68
- const ch = input[index];
126
+ for (let i = 0; i < input.length; i += 1) {
127
+ const ch = input[i];
69
128
  if (ch === "\r" || ch === "\n") {
70
129
  finish(buffer);
71
130
  return;
@@ -74,46 +133,20 @@ function askHiddenQuestion(rl, promptText) {
74
133
  buffer = buffer.slice(0, -1);
75
134
  continue;
76
135
  }
77
- if (ch >= " ") {
136
+ if (ch >= " ")
78
137
  buffer += ch;
79
- }
80
138
  }
81
139
  };
140
+ // Pause readline so it doesn't eat our raw keystrokes
82
141
  rl.pause();
83
142
  stdout.write(promptText);
84
- if (!wasRawMode) {
143
+ if (!wasRawMode)
85
144
  stdin.setRawMode(true);
86
- }
87
145
  stdin.resume();
88
146
  stdin.on("data", onData);
89
147
  });
90
148
  }
91
- function writeLastLogin(username, from) {
92
- const dir = "/virtual-env-js/.lastlog";
93
- if (!virtualShell.vfs.exists(dir)) {
94
- virtualShell.vfs.mkdir(dir, 0o700);
95
- }
96
- virtualShell.vfs.writeFile(`/virtual-env-js/.lastlog/${username}.json`, JSON.stringify({ at: new Date().toISOString(), from }));
97
- }
98
- async function flushVfs() {
99
- await virtualShell.vfs.flushMirror();
100
- }
101
- function loadHistory() {
102
- const historyPath = "/virtual-env-js/.bash_history";
103
- if (!virtualShell.vfs.exists(historyPath)) {
104
- virtualShell.vfs.writeFile(historyPath, "");
105
- return [];
106
- }
107
- return virtualShell.vfs
108
- .readFile(historyPath)
109
- .split("\n")
110
- .map((line) => line.trim())
111
- .filter((line) => line.length > 0);
112
- }
113
- function saveHistory(history) {
114
- const data = history.length > 0 ? `${history.join("\n")}\n` : "";
115
- virtualShell.vfs.writeFile("/virtual-env-js/.bash_history", data);
116
- }
149
+ // ── Session state helper ──────────────────────────────────────────────────────
117
150
  function applySessionState(authUserState, cwdState, result, shellEnvState) {
118
151
  let authUser = authUserState;
119
152
  let cwd = cwdState;
@@ -131,21 +164,16 @@ function applySessionState(authUserState, cwdState, result, shellEnvState) {
131
164
  }
132
165
  return { authUser, cwd };
133
166
  }
134
- virtualShell.addCommand("demo", [], () => {
135
- return {
136
- stdout: "This is a demo command. It does nothing useful.",
137
- exitCode: 0,
138
- };
139
- });
167
+ // ── Demo command ──────────────────────────────────────────────────────────────
168
+ virtualShell.addCommand("demo", [], () => ({
169
+ stdout: "This is a demo command. It does nothing useful.",
170
+ exitCode: 0,
171
+ }));
172
+ // ── Main shell ────────────────────────────────────────────────────────────────
140
173
  async function runReadlineShell() {
141
- const rl = createInterface({ input: stdin, output: stdout, terminal: true });
142
174
  await virtualShell.ensureInitialized();
143
- let history = loadHistory();
144
- const rlWithHistory = rl;
145
- rlWithHistory.history = [...history].reverse();
146
175
  const selectedUser = initialUser.trim() || "root";
147
- const userExists = virtualShell.users.getPasswordHash(selectedUser) !== null;
148
- if (!userExists) {
176
+ if (virtualShell.users.getPasswordHash(selectedUser) === null) {
149
177
  process.stderr.write(`self-standalone: user '${selectedUser}' does not exist\n`);
150
178
  process.exit(1);
151
179
  }
@@ -154,10 +182,19 @@ async function runReadlineShell() {
154
182
  let cwd = `/home/${authUser}`;
155
183
  shellEnv.vars.PWD = cwd;
156
184
  const remoteAddress = "localhost";
157
- const terminalSize = {
158
- cols: stdout.columns ?? 80,
159
- rows: stdout.rows ?? 24,
160
- };
185
+ const terminalSize = { cols: stdout.columns ?? 80, rows: stdout.rows ?? 24 };
186
+ let history = loadHistory(authUser);
187
+ // completer reads cwd via closure — always current
188
+ const rl = createInterface({
189
+ input: stdin,
190
+ output: stdout,
191
+ terminal: true,
192
+ completer: makeCompleter(() => ({ cwd })),
193
+ });
194
+ // Sync readline's internal history with our VFS history
195
+ const rlWithHistory = rl;
196
+ rlWithHistory.history = [...history].reverse();
197
+ // ── nano editor ────────────────────────────────────────────────────────────
161
198
  async function startNanoEditor(targetPath, initialContent, tempPath) {
162
199
  if (virtualShell.vfs.exists(targetPath)) {
163
200
  await writeFile(tempPath, initialContent, "utf8");
@@ -169,20 +206,16 @@ async function runReadlineShell() {
169
206
  end: () => undefined,
170
207
  });
171
208
  const wasRawMode = Boolean(stdin.isRaw);
172
- const forwardInput = (chunk) => {
173
- editor.stdin.write(chunk);
174
- };
209
+ const forwardInput = (chunk) => { editor.stdin.write(chunk); };
175
210
  stdin.resume();
176
- if (!wasRawMode) {
211
+ if (!wasRawMode)
177
212
  stdin.setRawMode(true);
178
- }
179
213
  stdin.on("data", forwardInput);
180
214
  await new Promise((resolve) => {
181
215
  const cleanup = () => {
182
216
  stdin.off("data", forwardInput);
183
- if (!wasRawMode) {
217
+ if (!wasRawMode)
184
218
  stdin.setRawMode(false);
185
- }
186
219
  rl.resume();
187
220
  };
188
221
  editor.on("error", (error) => {
@@ -199,7 +232,7 @@ async function runReadlineShell() {
199
232
  await flushVfs();
200
233
  }
201
234
  catch {
202
- // Save skipped or temp file missing.
235
+ // save skipped or temp file missing
203
236
  }
204
237
  await unlink(tempPath).catch(() => undefined);
205
238
  stdout.write("\r\n");
@@ -207,6 +240,7 @@ async function runReadlineShell() {
207
240
  });
208
241
  });
209
242
  }
243
+ // ── challenge handlers ─────────────────────────────────────────────────────
210
244
  async function handleSudoChallenge(challenge) {
211
245
  if (challenge.onPassword) {
212
246
  let promptText = challenge.prompt;
@@ -275,6 +309,7 @@ async function runReadlineShell() {
275
309
  break;
276
310
  }
277
311
  }
312
+ // handleCommandResult must be declared before the "line" handler
278
313
  async function handleCommandResult(result) {
279
314
  if (result.openEditor) {
280
315
  await startNanoEditor(result.openEditor.targetPath, result.openEditor.initialContent, result.openEditor.tempPath);
@@ -288,32 +323,26 @@ async function runReadlineShell() {
288
323
  await handlePasswordChallenge(result.passwordChallenge);
289
324
  return;
290
325
  }
326
+ if (result.clearScreen) {
327
+ stdout.write("\u001b[2J\u001b[H");
328
+ console.clear();
329
+ }
291
330
  if (result.stdout) {
292
331
  stdout.write(result.stdout.endsWith("\n") ? result.stdout : `${result.stdout}\n`);
293
332
  }
294
333
  if (result.stderr) {
295
334
  process.stderr.write(result.stderr.endsWith("\n") ? result.stderr : `${result.stderr}\n`);
296
335
  }
297
- if (result.clearScreen) {
298
- stdout.write("\u001b[2J\u001b[H");
299
- console.clear();
300
- }
301
- const updatedState = applySessionState(authUser, cwd, result, shellEnv);
302
- authUser = updatedState.authUser;
303
- cwd = updatedState.cwd;
336
+ const updated = applySessionState(authUser, cwd, result, shellEnv);
337
+ authUser = updated.authUser;
338
+ cwd = updated.cwd;
304
339
  if (result.closeSession) {
305
340
  await flushVfs();
306
341
  rl.close();
307
342
  process.exit(result.exitCode ?? 0);
308
343
  }
309
344
  }
310
- if (process.env.USER !== "root" && virtualShell.users.hasPassword(authUser)) {
311
- const password = await askHiddenQuestion(rl, `Password for ${authUser}: `);
312
- if (!virtualShell.users.verifyPassword(authUser, password)) {
313
- process.stderr.write("self-standalone: authentication failed\n");
314
- process.exit(1);
315
- }
316
- }
345
+ // ── Prompt helper ──────────────────────────────────────────────────────────
317
346
  const renderPrompt = () => {
318
347
  const cwdLabel = cwd === `/home/${authUser}` ? "~" : basename(cwd) || "/";
319
348
  return buildPrompt(authUser, hostname, cwdLabel);
@@ -322,50 +351,68 @@ async function runReadlineShell() {
322
351
  rl.setPrompt(renderPrompt());
323
352
  rl.prompt();
324
353
  };
325
- rl.on("SIGINT", () => {
326
- stdout.write("^C\n");
327
- rl.write("", { ctrl: true, name: "u" });
328
- prompt();
329
- });
330
- rl.on("close", () => {
331
- void (async () => {
332
- await flushVfs();
333
- console.log("");
334
- process.exit(0);
335
- })();
336
- });
354
+ // ── Auth (password gate) ───────────────────────────────────────────────────
355
+ if (process.env.USER !== "root" && virtualShell.users.hasPassword(authUser)) {
356
+ const password = await askHiddenQuestion(rl, `Password for ${authUser}: `);
357
+ if (!virtualShell.users.verifyPassword(authUser, password)) {
358
+ process.stderr.write("self-standalone: authentication failed\n");
359
+ process.exit(1);
360
+ }
361
+ }
362
+ // ── Login banner ───────────────────────────────────────────────────────────
337
363
  stdout.write(buildLoginBanner(hostname, virtualShell.properties, readLastLogin(authUser)));
338
364
  writeLastLogin(authUser, remoteAddress);
339
365
  await flushVfs();
340
- prompt();
341
- while (true) {
342
- const inputLine = await new Promise((resolve) => {
343
- rl.once("line", (line) => resolve(line));
344
- });
366
+ // ── Event-driven line handler (enables completer) ──────────────────────────
367
+ //
368
+ // Key insight: readline's completer only fires when readline itself owns
369
+ // stdin (i.e. rl is not paused). We use the event-driven "line" pattern
370
+ // instead of a while(true)+rl.once("line") loop so readline stays active
371
+ // between commands. We pause only while awaiting async work, then resume
372
+ // immediately before re-prompting so the next Tab press is caught.
373
+ let busy = false;
374
+ rl.on("line", async (inputLine) => {
375
+ if (busy)
376
+ return; // shouldn't happen but guard re-entrancy
377
+ busy = true;
345
378
  rl.pause();
346
- if (inputLine.trim().length > 0) {
379
+ const trimmed = inputLine.trim();
380
+ if (trimmed.length > 0) {
347
381
  history.push(inputLine);
348
- if (history.length > 500) {
382
+ if (history.length > 500)
349
383
  history = history.slice(history.length - 500);
350
- }
351
- saveHistory(history);
384
+ saveHistory(history, authUser);
352
385
  rlWithHistory.history = [...history].reverse();
353
386
  }
354
387
  const result = await runCommand(inputLine, authUser, hostname, "shell", cwd, virtualShell, undefined, shellEnv);
355
388
  await handleCommandResult(result);
356
389
  await flushVfs();
357
- prompt();
390
+ busy = false;
391
+ // Resume before prompt so readline can handle Tab on the next input
358
392
  rl.resume();
359
- }
393
+ prompt();
394
+ });
395
+ rl.on("SIGINT", () => {
396
+ stdout.write("^C\n");
397
+ rl.write("", { ctrl: true, name: "u" });
398
+ prompt();
399
+ });
400
+ rl.on("close", () => {
401
+ void flushVfs().then(() => {
402
+ console.log("");
403
+ process.exit(0);
404
+ });
405
+ });
406
+ // Initial prompt — readline is already active, completer live from first keystroke
407
+ prompt();
360
408
  }
361
409
  runReadlineShell().catch((error) => {
362
410
  console.error("Failed to start readline SSH emulation:", error);
363
411
  process.exit(1);
364
412
  });
365
413
  process.on("uncaughtException", (error) => {
366
- console.log("Oh my god, something terrible happened: ", error);
414
+ console.error("Uncaught exception:", error);
367
415
  });
368
416
  process.on("unhandledRejection", (error, promise) => {
369
- console.log(" Oh Lord! We forgot to handle a promise rejection here: ", promise);
370
- console.log(" The error was: ", error);
417
+ console.error("Unhandled rejection at:", promise, "error:", error);
371
418
  });
@@ -10,6 +10,12 @@ export interface PipelineCommand {
10
10
  outputFile?: string;
11
11
  /** Append to output file (>> file) */
12
12
  appendOutput?: boolean;
13
+ /** Stderr redirection file path (2> file) */
14
+ stderrFile?: string;
15
+ /** Append stderr to file (2>> file) */
16
+ stderrAppend?: boolean;
17
+ /** Redirect stderr to stdout (2>&1) */
18
+ stderrToStdout?: boolean;
13
19
  }
14
20
  /** Logical operator connecting two statement groups. */
15
21
  export type LogicalOp = "&&" | "||" | ";";
@@ -1 +1 @@
1
- {"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../../src/types/pipeline.ts"],"names":[],"mappings":"AAAA,iDAAiD;AACjD,MAAM,WAAW,eAAe;IAC/B,mBAAmB;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,wBAAwB;IACxB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,2CAA2C;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4CAA4C;IAC5C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,sCAAsC;IACtC,YAAY,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,wDAAwD;AACxD,MAAM,MAAM,SAAS,GAAG,IAAI,GAAG,IAAI,GAAG,GAAG,CAAC;AAE1C,yCAAyC;AACzC,MAAM,WAAW,QAAQ;IACxB,uCAAuC;IACvC,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,uCAAuC;IACvC,OAAO,EAAE,OAAO,CAAC;IACjB,sCAAsC;IACtC,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAED,0FAA0F;AAC1F,MAAM,WAAW,SAAS;IACzB,8CAA8C;IAC9C,QAAQ,EAAE,QAAQ,CAAC;IACnB,0DAA0D;IAC1D,EAAE,CAAC,EAAE,SAAS,CAAC;IACf,2CAA2C;IAC3C,IAAI,CAAC,EAAE,SAAS,CAAC;CACjB;AAED,2CAA2C;AAC3C,MAAM,WAAW,MAAM;IACtB,0CAA0C;IAC1C,UAAU,EAAE,SAAS,EAAE,CAAC;IACxB,kDAAkD;IAClD,OAAO,EAAE,OAAO,CAAC;IACjB,oCAAoC;IACpC,KAAK,CAAC,EAAE,MAAM,CAAC;CACf"}
1
+ {"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../../src/types/pipeline.ts"],"names":[],"mappings":"AAAA,iDAAiD;AACjD,MAAM,WAAW,eAAe;IAC/B,mBAAmB;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,wBAAwB;IACxB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,2CAA2C;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4CAA4C;IAC5C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,sCAAsC;IACtC,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,6CAA6C;IAC7C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,uCAAuC;IACvC,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,uCAAuC;IACvC,cAAc,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,wDAAwD;AACxD,MAAM,MAAM,SAAS,GAAG,IAAI,GAAG,IAAI,GAAG,GAAG,CAAC;AAE1C,yCAAyC;AACzC,MAAM,WAAW,QAAQ;IACxB,uCAAuC;IACvC,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,uCAAuC;IACvC,OAAO,EAAE,OAAO,CAAC;IACjB,sCAAsC;IACtC,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAED,0FAA0F;AAC1F,MAAM,WAAW,SAAS;IACzB,8CAA8C;IAC9C,QAAQ,EAAE,QAAQ,CAAC;IACnB,0DAA0D;IAC1D,EAAE,CAAC,EAAE,SAAS,CAAC;IACf,2CAA2C;IAC3C,IAAI,CAAC,EAAE,SAAS,CAAC;CACjB;AAED,2CAA2C;AAC3C,MAAM,WAAW,MAAM;IACtB,0CAA0C;IAC1C,UAAU,EAAE,SAAS,EAAE,CAAC;IACxB,kDAAkD;IAClD,OAAO,EAAE,OAAO,CAAC;IACjB,oCAAoC;IACpC,KAAK,CAAC,EAAE,MAAM,CAAC;CACf"}
@@ -68,4 +68,19 @@ export type VfsSnapshotNode = VfsSnapshotFileNode | VfsSnapshotDirectoryNode;
68
68
  export interface VfsSnapshot {
69
69
  root: VfsSnapshotDirectoryNode;
70
70
  }
71
+ /** Options for mounting a host directory into the VFS. */
72
+ export interface MountOptions {
73
+ /** Absolute path inside the VM (e.g. `"/app"`). */
74
+ vPath: string;
75
+ /** Path on the host filesystem. Relative paths resolved from `process.cwd()`. */
76
+ hostPath: string;
77
+ /** When `true` (default), write operations inside the mount throw `EROFS`. */
78
+ readOnly?: boolean;
79
+ }
80
+ /** Describes an active mount point. */
81
+ export interface MountPoint {
82
+ vPath: string;
83
+ hostPath: string;
84
+ readOnly: boolean;
85
+ }
71
86
  //# sourceMappingURL=vfs.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"vfs.d.ts","sourceRoot":"","sources":["../../src/types/vfs.ts"],"names":[],"mappings":"AAAA,oCAAoC;AACpC,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,WAAW,CAAC;AAE/C,oEAAoE;AACpE,MAAM,WAAW,WAAW;IAC3B,qCAAqC;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,qCAAqC;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,4BAA4B;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,+BAA+B;IAC/B,SAAS,EAAE,IAAI,CAAC;IAChB,6BAA6B;IAC7B,SAAS,EAAE,IAAI,CAAC;CAChB;AAED,0CAA0C;AAC1C,MAAM,WAAW,WAAY,SAAQ,WAAW;IAC/C,IAAI,EAAE,MAAM,CAAC;IACb,mDAAmD;IACnD,UAAU,EAAE,OAAO,CAAC;IACpB,4DAA4D;IAC5D,IAAI,EAAE,MAAM,CAAC;CACb;AAED,+CAA+C;AAC/C,MAAM,WAAW,gBAAiB,SAAQ,WAAW;IACpD,IAAI,EAAE,WAAW,CAAC;IAClB,8CAA8C;IAC9C,aAAa,EAAE,MAAM,CAAC;CACtB;AAED,kDAAkD;AAClD,MAAM,MAAM,YAAY,GAAG,WAAW,GAAG,gBAAgB,CAAC;AAE1D,wDAAwD;AACxD,MAAM,WAAW,gBAAgB;IAChC,uDAAuD;IACvD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,0CAA0C;IAC1C,QAAQ,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,qDAAqD;AACrD,MAAM,WAAW,aAAa;IAC7B,gDAAgD;IAChD,SAAS,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,gEAAgE;AAChE,MAAM,WAAW,mBAAmB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,mCAAmC;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,iCAAiC;IACjC,SAAS,EAAE,MAAM,CAAC;CAClB;AAED,gDAAgD;AAChD,MAAM,WAAW,mBAAoB,SAAQ,mBAAmB;IAC/D,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,OAAO,CAAC;IACpB,qCAAqC;IACrC,aAAa,EAAE,MAAM,CAAC;CACtB;AAED,qDAAqD;AACrD,MAAM,WAAW,wBAAyB,SAAQ,mBAAmB;IACpE,IAAI,EAAE,WAAW,CAAC;IAClB,QAAQ,EAAE,eAAe,EAAE,CAAC;CAC5B;AAED,kDAAkD;AAClD,MAAM,MAAM,eAAe,GAAG,mBAAmB,GAAG,wBAAwB,CAAC;AAE7E,gDAAgD;AAChD,MAAM,WAAW,WAAW;IAC3B,IAAI,EAAE,wBAAwB,CAAC;CAC/B"}
1
+ {"version":3,"file":"vfs.d.ts","sourceRoot":"","sources":["../../src/types/vfs.ts"],"names":[],"mappings":"AAAA,oCAAoC;AACpC,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,WAAW,CAAC;AAE/C,oEAAoE;AACpE,MAAM,WAAW,WAAW;IAC3B,qCAAqC;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,qCAAqC;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,4BAA4B;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,+BAA+B;IAC/B,SAAS,EAAE,IAAI,CAAC;IAChB,6BAA6B;IAC7B,SAAS,EAAE,IAAI,CAAC;CAChB;AAED,0CAA0C;AAC1C,MAAM,WAAW,WAAY,SAAQ,WAAW;IAC/C,IAAI,EAAE,MAAM,CAAC;IACb,mDAAmD;IACnD,UAAU,EAAE,OAAO,CAAC;IACpB,4DAA4D;IAC5D,IAAI,EAAE,MAAM,CAAC;CACb;AAED,+CAA+C;AAC/C,MAAM,WAAW,gBAAiB,SAAQ,WAAW;IACpD,IAAI,EAAE,WAAW,CAAC;IAClB,8CAA8C;IAC9C,aAAa,EAAE,MAAM,CAAC;CACtB;AAED,kDAAkD;AAClD,MAAM,MAAM,YAAY,GAAG,WAAW,GAAG,gBAAgB,CAAC;AAE1D,wDAAwD;AACxD,MAAM,WAAW,gBAAgB;IAChC,uDAAuD;IACvD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,0CAA0C;IAC1C,QAAQ,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,qDAAqD;AACrD,MAAM,WAAW,aAAa;IAC7B,gDAAgD;IAChD,SAAS,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,gEAAgE;AAChE,MAAM,WAAW,mBAAmB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,mCAAmC;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,iCAAiC;IACjC,SAAS,EAAE,MAAM,CAAC;CAClB;AAED,gDAAgD;AAChD,MAAM,WAAW,mBAAoB,SAAQ,mBAAmB;IAC/D,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,OAAO,CAAC;IACpB,qCAAqC;IACrC,aAAa,EAAE,MAAM,CAAC;CACtB;AAED,qDAAqD;AACrD,MAAM,WAAW,wBAAyB,SAAQ,mBAAmB;IACpE,IAAI,EAAE,WAAW,CAAC;IAClB,QAAQ,EAAE,eAAe,EAAE,CAAC;CAC5B;AAED,kDAAkD;AAClD,MAAM,MAAM,eAAe,GAAG,mBAAmB,GAAG,wBAAwB,CAAC;AAE7E,gDAAgD;AAChD,MAAM,WAAW,WAAW;IAC3B,IAAI,EAAE,wBAAwB,CAAC;CAC/B;AAED,0DAA0D;AAC1D,MAAM,WAAW,YAAY;IAC5B,mDAAmD;IACnD,KAAK,EAAE,MAAM,CAAC;IACd,iFAAiF;IACjF,QAAQ,EAAE,MAAM,CAAC;IACjB,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,uCAAuC;AACvC,MAAM,WAAW,UAAU;IAC1B,KAAK,EAAK,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,OAAO,CAAC;CAClB"}
@@ -34,6 +34,15 @@ export declare function evalArith(expr: string, env: Record<string, string>): nu
34
34
  * @param lastExit Last command exit code (for `$?`).
35
35
  * @param home Home directory path (for `~`).
36
36
  */
37
+ /**
38
+ * Expand brace expressions in a single token.
39
+ * - `{a,b,c}` → `["a", "b", "c"]`
40
+ * - `{1..5}` → `["1", "2", "3", "4", "5"]`
41
+ * - `{a..e}` → `["a", "b", "c", "d", "e"]`
42
+ * - `prefix{a,b}suffix` → `["prefixasuffix", "prefixbsuffix"]`
43
+ * Returns a single-element array when no brace expansion applies.
44
+ */
45
+ export declare function expandBraces(token: string): string[];
37
46
  export declare function expandSync(input: string, env: Record<string, string>, lastExit?: number, home?: string): string;
38
47
  /**
39
48
  * Expand all shell forms including `$(cmd)` command substitution.
@@ -1 +1 @@
1
- {"version":3,"file":"expand.d.ts","sourceRoot":"","sources":["../../src/utils/expand.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAIH;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAuB3E;AAoCD;;;;;;;;;GASG;AACH,wBAAgB,UAAU,CACzB,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC3B,QAAQ,SAAI,EACZ,IAAI,CAAC,EAAE,MAAM,GACX,MAAM,CA4DR;AAID;;;;;;;;;;GAUG;AACH,wBAAsB,WAAW,CAChC,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC3B,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,GACtC,OAAO,CAAC,MAAM,CAAC,CAuDjB"}
1
+ {"version":3,"file":"expand.d.ts","sourceRoot":"","sources":["../../src/utils/expand.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAIH;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAuB3E;AAoCD;;;;;;;;;GASG;AAEH;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CA8DpD;AAED,wBAAgB,UAAU,CACzB,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC3B,QAAQ,SAAI,EACZ,IAAI,CAAC,EAAE,MAAM,GACX,MAAM,CA4DR;AAID;;;;;;;;;;GAUG;AACH,wBAAsB,WAAW,CAChC,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC3B,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,GACtC,OAAO,CAAC,MAAM,CAAC,CAuDjB"}
@@ -82,6 +82,88 @@ function outsideSingleQuotes(input, replacer) {
82
82
  * @param lastExit Last command exit code (for `$?`).
83
83
  * @param home Home directory path (for `~`).
84
84
  */
85
+ /**
86
+ * Expand brace expressions in a single token.
87
+ * - `{a,b,c}` → `["a", "b", "c"]`
88
+ * - `{1..5}` → `["1", "2", "3", "4", "5"]`
89
+ * - `{a..e}` → `["a", "b", "c", "d", "e"]`
90
+ * - `prefix{a,b}suffix` → `["prefixasuffix", "prefixbsuffix"]`
91
+ * Returns a single-element array when no brace expansion applies.
92
+ */
93
+ export function expandBraces(token) {
94
+ // Find the first { not preceded by $
95
+ let depth = 0;
96
+ let start = -1;
97
+ for (let i = 0; i < token.length; i++) {
98
+ const ch = token[i];
99
+ if (ch === "{" && token[i - 1] !== "$") {
100
+ if (depth === 0)
101
+ start = i;
102
+ depth++;
103
+ }
104
+ else if (ch === "}") {
105
+ depth--;
106
+ if (depth === 0 && start !== -1) {
107
+ const prefix = token.slice(0, start);
108
+ const inner = token.slice(start + 1, i);
109
+ const suffix = token.slice(i + 1);
110
+ // Range: {1..5} or {a..e}
111
+ const rangeMatch = inner.match(/^(-?\d+)\.\.(-?\d+)(?:\.\.-?(\d+))?$/) ||
112
+ inner.match(/^([a-z])\.\.([a-z])$/);
113
+ if (rangeMatch) {
114
+ const items = [];
115
+ if (/\d/.test(rangeMatch[1])) {
116
+ const from = parseInt(rangeMatch[1], 10);
117
+ const to = parseInt(rangeMatch[2], 10);
118
+ const step = rangeMatch[3] ? parseInt(rangeMatch[3], 10) : 1;
119
+ const inc = from <= to ? step : -step;
120
+ for (let n = from; from <= to ? n <= to : n >= to; n += inc) {
121
+ items.push(String(n));
122
+ }
123
+ }
124
+ else {
125
+ const from = rangeMatch[1].charCodeAt(0);
126
+ const to = rangeMatch[2].charCodeAt(0);
127
+ const inc = from <= to ? 1 : -1;
128
+ for (let c = from; from <= to ? c <= to : c >= to; c += inc) {
129
+ items.push(String.fromCharCode(c));
130
+ }
131
+ }
132
+ const expanded = items.map((v) => `${prefix}${v}${suffix}`);
133
+ return expanded.flatMap(expandBraces);
134
+ }
135
+ // Comma list: {a,b,c} — split respecting nested braces
136
+ const parts = [];
137
+ let cur = "";
138
+ let d2 = 0;
139
+ for (const ch2 of inner) {
140
+ if (ch2 === "{") {
141
+ d2++;
142
+ cur += ch2;
143
+ }
144
+ else if (ch2 === "}") {
145
+ d2--;
146
+ cur += ch2;
147
+ }
148
+ else if (ch2 === "," && d2 === 0) {
149
+ parts.push(cur);
150
+ cur = "";
151
+ }
152
+ else {
153
+ cur += ch2;
154
+ }
155
+ }
156
+ parts.push(cur);
157
+ if (parts.length > 1) {
158
+ const expanded = parts.map((p) => `${prefix}${p}${suffix}`);
159
+ return expanded.flatMap(expandBraces);
160
+ }
161
+ break;
162
+ }
163
+ }
164
+ }
165
+ return [token];
166
+ }
85
167
  export function expandSync(input, env, lastExit = 0, home) {
86
168
  const homePath = home ?? env.HOME ?? "/home/user";
87
169
  return outsideSingleQuotes(input, (chunk) => {
@@ -111,8 +193,8 @@ export function expandSync(input, env, lastExit = 0, home) {
111
193
  s = s.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*):\+([^}]*)\}/g, (_, name, alt) => env[name] !== undefined && env[name] !== "" ? alt : "");
112
194
  // ${VAR}
113
195
  s = s.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_, name) => env[name] ?? "");
114
- // $VAR
115
- s = s.replace(/\$([A-Za-z_][A-Za-z0-9_]*)/g, (_, name) => env[name] ?? "");
196
+ // $VAR and positional params $1 $2 ...
197
+ s = s.replace(/\$([A-Za-z_][A-Za-z0-9_]*|\d+)/g, (_, name) => env[name] ?? "");
116
198
  return s;
117
199
  });
118
200
  }
@@ -1 +1 @@
1
- {"version":3,"file":"tokenize.d.ts","sourceRoot":"","sources":["../../src/utils/tokenize.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CA0DvD"}
1
+ {"version":3,"file":"tokenize.d.ts","sourceRoot":"","sources":["../../src/utils/tokenize.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CA8EvD"}
@@ -50,6 +50,46 @@ export function tokenizeCommand(input) {
50
50
  i++;
51
51
  continue;
52
52
  }
53
+ // Handle 2>&1, 2>>, 2>, >&, >>
54
+ if (!inQ && ch === "2" && (next === ">")) {
55
+ const rest = input.slice(i + 1);
56
+ if (rest.startsWith(">>&1") || rest.startsWith(">> &1")) {
57
+ if (current) {
58
+ tokens.push(current);
59
+ current = "";
60
+ }
61
+ tokens.push("2>>&1");
62
+ i += 5;
63
+ continue;
64
+ }
65
+ if (rest.startsWith(">&1")) {
66
+ if (current) {
67
+ tokens.push(current);
68
+ current = "";
69
+ }
70
+ tokens.push("2>&1");
71
+ i += 4;
72
+ continue;
73
+ }
74
+ if (rest.startsWith(">>")) {
75
+ if (current) {
76
+ tokens.push(current);
77
+ current = "";
78
+ }
79
+ tokens.push("2>>");
80
+ i += 3;
81
+ continue;
82
+ }
83
+ if (rest.startsWith(">")) {
84
+ if (current) {
85
+ tokens.push(current);
86
+ current = "";
87
+ }
88
+ tokens.push("2>");
89
+ i += 2;
90
+ continue;
91
+ }
92
+ }
53
93
  if ((ch === ">" || ch === "<") && !inQ) {
54
94
  if (current) {
55
95
  tokens.push(current);
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.4.1",
7
+ "version": "1.4.3",
8
8
  "license": "MIT",
9
9
  "repository": {
10
10
  "type": "git",