typescript-virtual-container 1.5.6 → 1.5.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.
Files changed (58) hide show
  1. package/README.md +28 -20
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/SSHMimic/index.d.ts +5 -1
  4. package/dist/SSHMimic/index.js +27 -3
  5. package/dist/SSHMimic/prompt.d.ts +2 -1
  6. package/dist/SSHMimic/prompt.js +27 -5
  7. package/dist/SSHMimic/scp.d.ts +34 -0
  8. package/dist/SSHMimic/scp.js +285 -0
  9. package/dist/SSHMimic/sftp.d.ts +53 -3
  10. package/dist/SSHMimic/sftp.js +9 -3
  11. package/dist/VirtualFileSystem/binaryPack.d.ts +7 -0
  12. package/dist/VirtualFileSystem/binaryPack.js +37 -1
  13. package/dist/VirtualFileSystem/index.d.ts +7 -0
  14. package/dist/VirtualFileSystem/index.js +67 -27
  15. package/dist/VirtualFileSystem/internalTypes.d.ts +2 -0
  16. package/dist/VirtualFileSystem/path.d.ts +5 -0
  17. package/dist/VirtualFileSystem/path.js +24 -11
  18. package/dist/VirtualPackageManager/index.d.ts +4 -2
  19. package/dist/VirtualPackageManager/index.js +24 -4
  20. package/dist/VirtualShell/index.d.ts +6 -3
  21. package/dist/VirtualShell/index.js +3 -10
  22. package/dist/VirtualShell/shell.js +114 -140
  23. package/dist/VirtualShell/shellParser.js +1 -22
  24. package/dist/commands/exit.js +1 -1
  25. package/dist/commands/find.js +1 -4
  26. package/dist/commands/helpers.d.ts +0 -20
  27. package/dist/commands/helpers.js +0 -97
  28. package/dist/commands/id.js +8 -1
  29. package/dist/commands/index.d.ts +1 -1
  30. package/dist/commands/index.js +1 -1
  31. package/dist/commands/manuals-bundle.js +10 -1
  32. package/dist/commands/perl.js +1 -1
  33. package/dist/commands/python.js +5 -2
  34. package/dist/commands/registry.js +6 -1
  35. package/dist/commands/rm.d.ts +1 -1
  36. package/dist/commands/rm.js +48 -11
  37. package/dist/commands/runtime.d.ts +5 -0
  38. package/dist/commands/runtime.js +90 -88
  39. package/dist/commands/strace.js +1 -1
  40. package/dist/commands/tar.js +2 -2
  41. package/dist/commands/test.js +2 -2
  42. package/dist/modules/linuxRootfs.js +7 -6
  43. package/dist/modules/nanoEditor.d.ts +92 -0
  44. package/dist/modules/nanoEditor.js +956 -0
  45. package/dist/modules/neofetch.js +2 -2
  46. package/dist/modules/webTermRenderer.d.ts +42 -0
  47. package/dist/modules/webTermRenderer.js +291 -0
  48. package/dist/types/commands.d.ts +4 -0
  49. package/dist/utils/argv.d.ts +6 -0
  50. package/dist/utils/argv.js +32 -0
  51. package/dist/utils/expand.d.ts +5 -2
  52. package/dist/utils/expand.js +70 -67
  53. package/dist/utils/glob.d.ts +6 -0
  54. package/dist/utils/glob.js +34 -0
  55. package/dist/utils/shellSession.d.ts +10 -0
  56. package/dist/utils/shellSession.js +56 -0
  57. package/dist/utils/tokenize.js +13 -13
  58. package/package.json +7 -6
@@ -208,10 +208,10 @@ function resolveDefaults(info) {
208
208
  os: info.osName ?? `${readOsPrettyName() ?? os.type()} ${os.arch()}`,
209
209
  arch: os.arch(),
210
210
  },
211
- resolution: info.resolution ?? "n/a (ssh)",
211
+ resolution: info.resolution ?? shellProps?.resolution ?? "n/a (ssh)",
212
212
  terminal: info.terminal ?? "unknown",
213
213
  cpu: info.cpu ?? resolveCpuLabel(),
214
- gpu: info.gpu ?? "n/a",
214
+ gpu: info.gpu ?? shellProps?.gpu ?? "n/a",
215
215
  memoryUsedMiB: info.memoryUsedMiB ?? toMiB(usedMem),
216
216
  memoryTotalMiB: info.memoryTotalMiB ?? toMiB(totalMem),
217
217
  };
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Minimal VT100 screen buffer for browser-side rendering.
3
+ * Handles the subset of escape sequences emitted by NanoEditor:
4
+ * - CSI H / CSI row;colH (cursor position)
5
+ * - CSI K (erase to end of line)
6
+ * - CSI 2J (erase display)
7
+ * - CSI ?25l / ?25h (cursor hide/show)
8
+ * - CSI <n> m (SGR — bold, reverse, fg, bg, reset)
9
+ */
10
+ export interface Cell {
11
+ ch: string;
12
+ bold: boolean;
13
+ reverse: boolean;
14
+ fg: string | null;
15
+ bg: string | null;
16
+ }
17
+ export declare class WebTermRenderer {
18
+ private rows;
19
+ private cols;
20
+ private screen;
21
+ private curRow;
22
+ private curCol;
23
+ private cursorVisible;
24
+ private bold;
25
+ private reverse;
26
+ private fg;
27
+ private bg;
28
+ private buf;
29
+ constructor(rows: number, cols: number);
30
+ resize(rows: number, cols: number): void;
31
+ write(data: string): void;
32
+ private flush;
33
+ private handleCsi;
34
+ private handleSgr;
35
+ private putChar;
36
+ private makeScreen;
37
+ /** Render current screen state to an HTML string for a <pre> element. */
38
+ renderHtml(): string;
39
+ get cursorRow(): number;
40
+ get cursorCol(): number;
41
+ get isCursorVisible(): boolean;
42
+ }
@@ -0,0 +1,291 @@
1
+ /**
2
+ * Minimal VT100 screen buffer for browser-side rendering.
3
+ * Handles the subset of escape sequences emitted by NanoEditor:
4
+ * - CSI H / CSI row;colH (cursor position)
5
+ * - CSI K (erase to end of line)
6
+ * - CSI 2J (erase display)
7
+ * - CSI ?25l / ?25h (cursor hide/show)
8
+ * - CSI <n> m (SGR — bold, reverse, fg, bg, reset)
9
+ */
10
+ const DEFAULT_CELL = { ch: " ", bold: false, reverse: false, fg: null, bg: null };
11
+ function makeCell(partial) {
12
+ return { ...DEFAULT_CELL, ...partial };
13
+ }
14
+ export class WebTermRenderer {
15
+ rows;
16
+ cols;
17
+ screen;
18
+ curRow = 0;
19
+ curCol = 0;
20
+ cursorVisible = true;
21
+ // Current SGR state
22
+ bold = false;
23
+ reverse = false;
24
+ fg = null;
25
+ bg = null;
26
+ buf = "";
27
+ constructor(rows, cols) {
28
+ this.rows = rows;
29
+ this.cols = cols;
30
+ this.screen = this.makeScreen();
31
+ }
32
+ resize(rows, cols) {
33
+ const newScreen = this.makeScreen(rows, cols);
34
+ for (let r = 0; r < Math.min(rows, this.rows); r++) {
35
+ for (let c = 0; c < Math.min(cols, this.cols); c++) {
36
+ newScreen[r][c] = this.screen[r]?.[c] ?? makeCell();
37
+ }
38
+ }
39
+ this.rows = rows;
40
+ this.cols = cols;
41
+ this.screen = newScreen;
42
+ this.curRow = Math.min(this.curRow, rows - 1);
43
+ this.curCol = Math.min(this.curCol, cols - 1);
44
+ }
45
+ write(data) {
46
+ this.buf += data;
47
+ this.flush();
48
+ }
49
+ flush() {
50
+ let i = 0;
51
+ while (i < this.buf.length) {
52
+ const ch = this.buf[i];
53
+ if (ch === "\x1b") {
54
+ if (i + 1 >= this.buf.length)
55
+ break; // wait for more data
56
+ const next = this.buf[i + 1];
57
+ if (next === "[") {
58
+ // CSI — find terminator
59
+ let j = i + 2;
60
+ while (j < this.buf.length && (this.buf[j] < "@" || this.buf[j] > "~"))
61
+ j++;
62
+ if (j >= this.buf.length)
63
+ break; // incomplete
64
+ const seq = this.buf.slice(i + 2, j);
65
+ const cmd = this.buf[j];
66
+ this.handleCsi(seq, cmd);
67
+ i = j + 1;
68
+ }
69
+ else {
70
+ i += 2; // skip unknown ESC sequence
71
+ }
72
+ }
73
+ else if (ch === "\r") {
74
+ this.curCol = 0;
75
+ i++;
76
+ }
77
+ else if (ch === "\n") {
78
+ this.curRow = Math.min(this.curRow + 1, this.rows - 1);
79
+ i++;
80
+ }
81
+ else if (ch.charCodeAt(0) >= 32) {
82
+ this.putChar(ch);
83
+ i++;
84
+ }
85
+ else {
86
+ i++; // skip other control chars
87
+ }
88
+ }
89
+ this.buf = this.buf.slice(i);
90
+ }
91
+ handleCsi(seq, cmd) {
92
+ if (cmd === "H" || cmd === "f") {
93
+ // Cursor position: row;col (1-based)
94
+ const parts = seq.split(";").map((n) => Number.parseInt(n || "1", 10));
95
+ this.curRow = Math.max(0, Math.min((parts[0] ?? 1) - 1, this.rows - 1));
96
+ this.curCol = Math.max(0, Math.min((parts[1] ?? 1) - 1, this.cols - 1));
97
+ return;
98
+ }
99
+ if (cmd === "K") {
100
+ // Erase line from cursor to end
101
+ const mode = seq === "" ? 0 : Number.parseInt(seq, 10);
102
+ if (mode === 0) {
103
+ for (let c = this.curCol; c < this.cols; c++) {
104
+ this.screen[this.curRow][c] = makeCell();
105
+ }
106
+ }
107
+ else if (mode === 1) {
108
+ for (let c = 0; c <= this.curCol; c++) {
109
+ this.screen[this.curRow][c] = makeCell();
110
+ }
111
+ }
112
+ else if (mode === 2) {
113
+ for (let c = 0; c < this.cols; c++) {
114
+ this.screen[this.curRow][c] = makeCell();
115
+ }
116
+ }
117
+ return;
118
+ }
119
+ if (cmd === "J") {
120
+ const mode = seq === "" ? 0 : Number.parseInt(seq, 10);
121
+ if (mode === 2) {
122
+ this.screen = this.makeScreen();
123
+ this.curRow = 0;
124
+ this.curCol = 0;
125
+ }
126
+ return;
127
+ }
128
+ if (cmd === "m") {
129
+ this.handleSgr(seq);
130
+ return;
131
+ }
132
+ if (cmd === "l" && seq === "?25") {
133
+ this.cursorVisible = false;
134
+ return;
135
+ }
136
+ if (cmd === "h" && seq === "?25") {
137
+ this.cursorVisible = true;
138
+ return;
139
+ }
140
+ }
141
+ handleSgr(seq) {
142
+ const codes = seq === "" ? [0] : seq.split(";").map((n) => Number.parseInt(n || "0", 10));
143
+ let i = 0;
144
+ while (i < codes.length) {
145
+ const code = codes[i];
146
+ if (code === 0) {
147
+ this.bold = false;
148
+ this.reverse = false;
149
+ this.fg = null;
150
+ this.bg = null;
151
+ }
152
+ else if (code === 1) {
153
+ this.bold = true;
154
+ }
155
+ else if (code === 7) {
156
+ this.reverse = true;
157
+ }
158
+ else if (code === 22) {
159
+ this.bold = false;
160
+ }
161
+ else if (code === 27) {
162
+ this.reverse = false;
163
+ }
164
+ else if (code >= 30 && code <= 37) {
165
+ this.fg = ANSI_COLORS[code - 30];
166
+ }
167
+ else if (code === 38) {
168
+ if (codes[i + 1] === 5 && codes[i + 2] !== undefined) {
169
+ this.fg = xterm256(codes[i + 2]);
170
+ i += 2;
171
+ }
172
+ else if (codes[i + 1] === 2 && codes[i + 4] !== undefined) {
173
+ this.fg = `rgb(${codes[i + 2]},${codes[i + 3]},${codes[i + 4]})`;
174
+ i += 4;
175
+ }
176
+ }
177
+ else if (code === 39) {
178
+ this.fg = null;
179
+ }
180
+ else if (code >= 40 && code <= 47) {
181
+ this.bg = ANSI_COLORS[code - 40];
182
+ }
183
+ else if (code === 48) {
184
+ if (codes[i + 1] === 5 && codes[i + 2] !== undefined) {
185
+ this.bg = xterm256(codes[i + 2]);
186
+ i += 2;
187
+ }
188
+ else if (codes[i + 1] === 2 && codes[i + 4] !== undefined) {
189
+ this.bg = `rgb(${codes[i + 2]},${codes[i + 3]},${codes[i + 4]})`;
190
+ i += 4;
191
+ }
192
+ }
193
+ else if (code === 49) {
194
+ this.bg = null;
195
+ }
196
+ else if (code >= 90 && code <= 97) {
197
+ this.fg = ANSI_COLORS_BRIGHT[code - 90];
198
+ }
199
+ else if (code >= 100 && code <= 107) {
200
+ this.bg = ANSI_COLORS_BRIGHT[code - 100];
201
+ }
202
+ i++;
203
+ }
204
+ }
205
+ putChar(ch) {
206
+ if (this.curRow >= this.rows || this.curCol >= this.cols)
207
+ return;
208
+ this.screen[this.curRow][this.curCol] = makeCell({
209
+ ch,
210
+ bold: this.bold,
211
+ reverse: this.reverse,
212
+ fg: this.fg,
213
+ bg: this.bg,
214
+ });
215
+ this.curCol++;
216
+ if (this.curCol >= this.cols) {
217
+ this.curCol = 0;
218
+ if (this.curRow < this.rows - 1)
219
+ this.curRow++;
220
+ }
221
+ }
222
+ makeScreen(rows = this.rows, cols = this.cols) {
223
+ return Array.from({ length: rows }, () => Array.from({ length: cols }, () => makeCell()));
224
+ }
225
+ /** Render current screen state to an HTML string for a <pre> element. */
226
+ renderHtml() {
227
+ let html = "";
228
+ for (let r = 0; r < this.rows; r++) {
229
+ const row = this.screen[r];
230
+ let spanOpen = false;
231
+ let lastStyle = "";
232
+ for (let c = 0; c < this.cols; c++) {
233
+ const cell = row[c];
234
+ const isCursor = this.cursorVisible && r === this.curRow && c === this.curCol;
235
+ let fg = cell.fg ?? "#ccc";
236
+ let bg = cell.bg ?? "transparent";
237
+ if (cell.reverse) {
238
+ [fg, bg] = [bg === "transparent" ? "#000" : bg, fg];
239
+ }
240
+ if (cell.bold && !cell.fg)
241
+ fg = "#fff";
242
+ if (isCursor) {
243
+ [fg, bg] = [bg === "transparent" ? "#000" : bg, fg === "#ccc" ? "#ccc" : fg];
244
+ bg = "#ccc";
245
+ fg = "#000";
246
+ }
247
+ const style = `color:${fg};background:${bg};${cell.bold ? "font-weight:bold;" : ""}`;
248
+ if (style !== lastStyle) {
249
+ if (spanOpen)
250
+ html += "</span>";
251
+ html += `<span style="${style}">`;
252
+ spanOpen = true;
253
+ lastStyle = style;
254
+ }
255
+ html += escHtml(cell.ch);
256
+ }
257
+ if (spanOpen)
258
+ html += "</span>";
259
+ if (r < this.rows - 1)
260
+ html += "\n";
261
+ }
262
+ return html;
263
+ }
264
+ get cursorRow() { return this.curRow; }
265
+ get cursorCol() { return this.curCol; }
266
+ get isCursorVisible() { return this.cursorVisible; }
267
+ }
268
+ function escHtml(ch) {
269
+ if (ch === "&")
270
+ return "&amp;";
271
+ if (ch === "<")
272
+ return "&lt;";
273
+ if (ch === ">")
274
+ return "&gt;";
275
+ return ch;
276
+ }
277
+ const ANSI_COLORS = ["#000", "#c00", "#0c0", "#cc0", "#00c", "#c0c", "#0cc", "#ccc"];
278
+ const ANSI_COLORS_BRIGHT = ["#555", "#f55", "#5f5", "#ff5", "#55f", "#f5f", "#5ff", "#fff"];
279
+ function xterm256(n) {
280
+ if (n < 16)
281
+ return (n < 8 ? ANSI_COLORS : ANSI_COLORS_BRIGHT)[n < 8 ? n : n - 8];
282
+ if (n < 232) {
283
+ const i = n - 16;
284
+ const r = Math.floor(i / 36) * 51;
285
+ const g = Math.floor((i % 36) / 6) * 51;
286
+ const b = (i % 6) * 51;
287
+ return `rgb(${r},${g},${b})`;
288
+ }
289
+ const v = (n - 232) * 10 + 8;
290
+ return `rgb(${v},${v},${v})`;
291
+ }
@@ -94,6 +94,10 @@ export interface ShellEnv {
94
94
  vars: Record<string, string>;
95
95
  /** Exit status of the last executed command. */
96
96
  lastExitCode: number;
97
+ /** @internal Cached split of vars.PATH — invalidated when _pathRaw !== vars.PATH. */
98
+ _pathRaw?: string;
99
+ /** @internal Pre-split PATH directories for resolveVfsBinary hot-path. */
100
+ _pathDirs?: string[];
97
101
  }
98
102
  /** Runtime context object passed to each command module. */
99
103
  export interface CommandContext {
@@ -0,0 +1,6 @@
1
+ /** Returns true if `name` appears in `argv`. */
2
+ export declare function getFlag(argv: string[], name: string): boolean;
3
+ /** Returns the string value for `--name=VALUE` or `--name VALUE`, or `fallback`. */
4
+ export declare function getOptionString(argv: string[], name: string, fallback: string): string;
5
+ /** Returns the integer value for `--name=VALUE` or `--name VALUE`, or `fallback`. */
6
+ export declare function getOptionInt(argv: string[], name: string, fallback: number): number;
@@ -0,0 +1,32 @@
1
+ /** Returns true if `name` appears in `argv`. */
2
+ export function getFlag(argv, name) {
3
+ return argv.includes(name);
4
+ }
5
+ /** Returns the string value for `--name=VALUE` or `--name VALUE`, or `fallback`. */
6
+ export function getOptionString(argv, name, fallback) {
7
+ const prefix = `${name}=`;
8
+ for (let i = 0; i < argv.length; i++) {
9
+ const a = argv[i];
10
+ if (a.startsWith(prefix))
11
+ return a.slice(prefix.length);
12
+ if (a === name) {
13
+ const next = argv[i + 1];
14
+ return (next && !next.startsWith("--")) ? next : fallback;
15
+ }
16
+ }
17
+ return fallback;
18
+ }
19
+ /** Returns the integer value for `--name=VALUE` or `--name VALUE`, or `fallback`. */
20
+ export function getOptionInt(argv, name, fallback) {
21
+ const prefix = `${name}=`;
22
+ for (let i = 0; i < argv.length; i++) {
23
+ const a = argv[i];
24
+ if (a.startsWith(prefix))
25
+ return parseInt(a.slice(prefix.length), 10);
26
+ if (a === name) {
27
+ const next = argv[i + 1];
28
+ return next ? parseInt(next, 10) : fallback;
29
+ }
30
+ }
31
+ return fallback;
32
+ }
@@ -61,10 +61,13 @@ export declare function expandAsync(input: string, env: Record<string, string>,
61
61
  * Supports * (any chars in segment) and ** (any path).
62
62
  * Returns the original pattern if no matches found (bash behavior).
63
63
  */
64
- export declare function expandGlob(pattern: string, cwd: string, vfs: {
64
+ type GlobVfs = {
65
65
  list: (p: string) => string[];
66
66
  exists: (p: string) => boolean;
67
67
  stat: (p: string) => {
68
68
  type: string;
69
69
  };
70
- }): string[];
70
+ statType?: (p: string) => string | null;
71
+ };
72
+ export declare function expandGlob(pattern: string, cwd: string, vfs: GlobVfs): string[];
73
+ export {};
@@ -17,6 +17,24 @@
17
17
  * $VAR simple reference
18
18
  * $((expr)) arithmetic (integer)
19
19
  */
20
+ import { globToRegex } from "./glob";
21
+ // Memoized shell-pattern → RegExp for ${VAR//pat/rep} etc. forms.
22
+ // Key encodes anchor/greedy options to keep separate caches per form.
23
+ const _shellPatCache = new Map();
24
+ function shellPatToRegex(pat, anchor, greedy, global = false) {
25
+ const key = `${anchor}:${greedy ? "g" : "s"}:${global ? "G" : ""}:${pat}`;
26
+ let re = _shellPatCache.get(key);
27
+ if (re)
28
+ return re;
29
+ const esc = pat.replace(/[.+^${}()|[\]\\]/g, "\\$&");
30
+ const body = greedy
31
+ ? esc.replace(/\*/g, ".*").replace(/\?/g, ".")
32
+ : esc.replace(/\*/g, "[^/]*").replace(/\?/g, ".");
33
+ const src = anchor === "prefix" ? `^${body}` : anchor === "suffix" ? `${body}$` : body;
34
+ re = new RegExp(src, global ? "g" : "");
35
+ _shellPatCache.set(key, re);
36
+ return re;
37
+ }
20
38
  function tokenizeArith(expr, env) {
21
39
  const tokens = [];
22
40
  let i = 0;
@@ -193,25 +211,24 @@ export function evalArith(expr, env) {
193
211
  * Single-quoted content is passed through verbatim (POSIX sh behaviour).
194
212
  */
195
213
  function outsideSingleQuotes(input, replacer) {
214
+ // Fast path: no single quotes → apply replacer to whole string, no allocation
215
+ if (!input.includes("'"))
216
+ return replacer(input);
196
217
  const parts = [];
197
218
  let i = 0;
198
219
  while (i < input.length) {
199
220
  const sqIdx = input.indexOf("'", i);
200
221
  if (sqIdx === -1) {
201
- // No more single quotes — expand the rest
202
222
  parts.push(replacer(input.slice(i)));
203
223
  break;
204
224
  }
205
- // Expand the part before the single quote
206
225
  parts.push(replacer(input.slice(i, sqIdx)));
207
- // Find closing single quote — everything inside is literal
208
226
  const closeIdx = input.indexOf("'", sqIdx + 1);
209
227
  if (closeIdx === -1) {
210
- // Unclosed quote — treat rest as literal
211
228
  parts.push(input.slice(sqIdx));
212
229
  break;
213
230
  }
214
- parts.push(input.slice(sqIdx, closeIdx + 1)); // include quotes
231
+ parts.push(input.slice(sqIdx, closeIdx + 1));
215
232
  i = closeIdx + 1;
216
233
  }
217
234
  return parts.join("");
@@ -327,10 +344,14 @@ export function expandBraces(token) {
327
344
  return expandBracesInternal(token, 0);
328
345
  }
329
346
  function expandArithmeticChunks(input, env) {
347
+ if (!input.includes("$(("))
348
+ return input;
330
349
  let result = "";
331
350
  let index = 0;
351
+ let flush = 0;
332
352
  while (index < input.length) {
333
353
  if (input[index] === "$" && input[index + 1] === "(" && input[index + 2] === "(") {
354
+ result += input.slice(flush, index);
334
355
  let scan = index + 3;
335
356
  let depth = 0;
336
357
  while (scan < input.length) {
@@ -347,6 +368,7 @@ function expandArithmeticChunks(input, env) {
347
368
  const value = evalArith(expr, env);
348
369
  result += Number.isNaN(value) ? "0" : String(value);
349
370
  index = scan + 2;
371
+ flush = index;
350
372
  break;
351
373
  }
352
374
  }
@@ -354,16 +376,18 @@ function expandArithmeticChunks(input, env) {
354
376
  }
355
377
  if (scan >= input.length) {
356
378
  result += input.slice(index);
357
- break;
379
+ return result;
358
380
  }
359
381
  continue;
360
382
  }
361
- result += input[index];
362
383
  index++;
363
384
  }
364
- return result;
385
+ return result + input.slice(flush);
365
386
  }
366
387
  export function expandSync(input, env, lastExit = 0, home) {
388
+ // Fast path: nothing to expand (no $ and no ~ and no single quotes)
389
+ if (!input.includes("$") && !input.includes("~") && !input.includes("'"))
390
+ return input;
367
391
  const homePath = home ?? env.HOME ?? "/home/user";
368
392
  return outsideSingleQuotes(input, (chunk) => {
369
393
  let s = chunk;
@@ -413,7 +437,7 @@ export function expandSync(input, env, lastExit = 0, home) {
413
437
  s = s.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\/\/([^/}]*)\/([^}]*)\}/g, (_, name, pat, rep) => {
414
438
  const val = env[name] ?? "";
415
439
  try {
416
- return val.replace(new RegExp(pat.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, "."), "g"), rep);
440
+ return val.replace(shellPatToRegex(pat, "none", true, true), rep);
417
441
  }
418
442
  catch {
419
443
  return val;
@@ -423,36 +447,20 @@ export function expandSync(input, env, lastExit = 0, home) {
423
447
  s = s.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\/([^/}]*)\/([^}]*)\}/g, (_, name, pat, rep) => {
424
448
  const val = env[name] ?? "";
425
449
  try {
426
- return val.replace(new RegExp(pat.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".")), rep);
450
+ return val.replace(shellPatToRegex(pat, "none", true, false), rep);
427
451
  }
428
452
  catch {
429
453
  return val;
430
454
  }
431
455
  });
432
456
  // ${VAR##pattern} — strip longest prefix
433
- s = s.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)##([^}]+)\}/g, (_, name, pat) => {
434
- const val = env[name] ?? "";
435
- const re = new RegExp(`^${pat.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".")}`);
436
- return val.replace(re, "");
437
- });
457
+ s = s.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)##([^}]+)\}/g, (_, name, pat) => (env[name] ?? "").replace(shellPatToRegex(pat, "prefix", true), ""));
438
458
  // ${VAR#pattern} — strip shortest prefix
439
- s = s.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)#([^}]+)\}/g, (_, name, pat) => {
440
- const val = env[name] ?? "";
441
- const re = new RegExp(`^${pat.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*").replace(/\?/g, ".")}`);
442
- return val.replace(re, "");
443
- });
459
+ s = s.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)#([^}]+)\}/g, (_, name, pat) => (env[name] ?? "").replace(shellPatToRegex(pat, "prefix", false), ""));
444
460
  // ${VAR%%pattern} — strip longest suffix
445
- s = s.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)%%([^}]+)\}/g, (_, name, pat) => {
446
- const val = env[name] ?? "";
447
- const re = new RegExp(`${pat.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".")}$`);
448
- return val.replace(re, "");
449
- });
461
+ s = s.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)%%([^}]+)\}/g, (_, name, pat) => (env[name] ?? "").replace(shellPatToRegex(pat, "suffix", true), ""));
450
462
  // ${VAR%pattern} — strip shortest suffix
451
- s = s.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)%([^}]+)\}/g, (_, name, pat) => {
452
- const val = env[name] ?? "";
453
- const re = new RegExp(`${pat.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*").replace(/\?/g, ".")}$`);
454
- return val.replace(re, "");
455
- });
463
+ s = s.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)%([^}]+)\}/g, (_, name, pat) => (env[name] ?? "").replace(shellPatToRegex(pat, "suffix", false), ""));
456
464
  // ${VAR}
457
465
  s = s.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_, name) => env[name] ?? "");
458
466
  // $VAR and positional params $1 $2 ...
@@ -540,12 +548,16 @@ export async function expandAsync(input, env, lastExit, runCmd) {
540
548
  env[depthKey] = String(currentDepth);
541
549
  }
542
550
  }
543
- // ─── Glob expansion ──────────────────────────────────────────────────────────
544
- /**
545
- * Expand a glob pattern against a VirtualShell VFS.
546
- * Supports * (any chars in segment) and ** (any path).
547
- * Returns the original pattern if no matches found (bash behavior).
548
- */
551
+ function nodeType(vfs, p) {
552
+ if (vfs.statType)
553
+ return vfs.statType(p);
554
+ try {
555
+ return vfs.stat(p).type;
556
+ }
557
+ catch {
558
+ return null;
559
+ }
560
+ }
549
561
  export function expandGlob(pattern, cwd, vfs) {
550
562
  // No glob chars → return as-is
551
563
  if (!pattern.includes('*') && !pattern.includes('?'))
@@ -567,14 +579,14 @@ function matchGlob(dir, segments, vfs) {
567
579
  // ** matches zero or more path segments
568
580
  if (seg === '**') {
569
581
  const all = walkAll(dir, vfs);
570
- return rest.length === 0 ? all : all.flatMap(d => {
571
- try {
572
- if (vfs.stat(d).type === 'directory')
573
- return matchGlob(d, rest, vfs);
574
- }
575
- catch { }
576
- return [];
577
- });
582
+ if (rest.length === 0)
583
+ return all;
584
+ const out = [];
585
+ for (const d of all) {
586
+ if (nodeType(vfs, d) === 'directory')
587
+ out.push(...matchGlob(d, rest, vfs));
588
+ }
589
+ return out;
578
590
  }
579
591
  let entries = [];
580
592
  try {
@@ -584,20 +596,20 @@ function matchGlob(dir, segments, vfs) {
584
596
  return [];
585
597
  }
586
598
  const re = globToRegex(seg);
587
- return entries
588
- .filter(e => !e.startsWith('.') || seg.startsWith('.'))
589
- .filter(e => re.test(e))
590
- .flatMap(e => {
599
+ const showHidden = seg.startsWith('.');
600
+ const matched = [];
601
+ for (const e of entries) {
602
+ if ((!showHidden && e.startsWith('.')) || !re.test(e))
603
+ continue;
591
604
  const full = dir === '/' ? `/${e}` : `${dir}/${e}`;
592
- if (rest.length === 0)
593
- return [full];
594
- try {
595
- if (vfs.stat(full).type === 'directory')
596
- return matchGlob(full, rest, vfs);
605
+ if (rest.length === 0) {
606
+ matched.push(full);
607
+ continue;
597
608
  }
598
- catch { }
599
- return [];
600
- });
609
+ if (nodeType(vfs, full) === 'directory')
610
+ matched.push(...matchGlob(full, rest, vfs));
611
+ }
612
+ return matched;
601
613
  }
602
614
  function walkAll(dir, vfs) {
603
615
  const results = [dir];
@@ -610,17 +622,8 @@ function walkAll(dir, vfs) {
610
622
  }
611
623
  for (const e of entries) {
612
624
  const full = dir === '/' ? `/${e}` : `${dir}/${e}`;
613
- try {
614
- if (vfs.stat(full).type === 'directory')
615
- results.push(...walkAll(full, vfs));
616
- }
617
- catch { }
625
+ if (nodeType(vfs, full) === 'directory')
626
+ results.push(...walkAll(full, vfs));
618
627
  }
619
628
  return results;
620
629
  }
621
- function globToRegex(pattern) {
622
- const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&')
623
- .replace(/\*/g, '.*')
624
- .replace(/\?/g, '.');
625
- return new RegExp(`^${escaped}$`);
626
- }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Convert a shell glob pattern to a RegExp.
3
+ * Supports: * (any chars), ? (one char), [...] (char class), flags (e.g. "i").
4
+ * Results are memoized — same pattern+flags returns the cached instance.
5
+ */
6
+ export declare function globToRegex(pattern: string, flags?: string): RegExp;