typescript-virtual-container 1.5.6 → 1.5.7

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 (43) 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/scp.d.ts +34 -0
  6. package/dist/SSHMimic/scp.js +285 -0
  7. package/dist/SSHMimic/sftp.d.ts +53 -3
  8. package/dist/SSHMimic/sftp.js +9 -3
  9. package/dist/VirtualFileSystem/binaryPack.d.ts +7 -0
  10. package/dist/VirtualFileSystem/binaryPack.js +37 -1
  11. package/dist/VirtualFileSystem/index.d.ts +7 -0
  12. package/dist/VirtualFileSystem/index.js +67 -27
  13. package/dist/VirtualFileSystem/internalTypes.d.ts +2 -0
  14. package/dist/VirtualFileSystem/path.d.ts +5 -0
  15. package/dist/VirtualFileSystem/path.js +24 -11
  16. package/dist/VirtualPackageManager/index.d.ts +4 -2
  17. package/dist/VirtualPackageManager/index.js +24 -4
  18. package/dist/VirtualShell/index.d.ts +4 -0
  19. package/dist/VirtualShell/index.js +1 -7
  20. package/dist/VirtualShell/shell.js +40 -10
  21. package/dist/VirtualShell/shellParser.js +1 -22
  22. package/dist/commands/exit.js +1 -1
  23. package/dist/commands/find.js +1 -4
  24. package/dist/commands/helpers.d.ts +0 -20
  25. package/dist/commands/helpers.js +0 -97
  26. package/dist/commands/perl.js +1 -1
  27. package/dist/commands/python.js +5 -2
  28. package/dist/commands/registry.js +6 -1
  29. package/dist/commands/runtime.js +65 -87
  30. package/dist/commands/strace.js +1 -1
  31. package/dist/commands/tar.js +2 -2
  32. package/dist/commands/test.js +2 -2
  33. package/dist/modules/linuxRootfs.js +1 -4
  34. package/dist/modules/neofetch.js +2 -2
  35. package/dist/types/commands.d.ts +4 -0
  36. package/dist/utils/argv.d.ts +6 -0
  37. package/dist/utils/argv.js +32 -0
  38. package/dist/utils/expand.d.ts +5 -2
  39. package/dist/utils/expand.js +70 -67
  40. package/dist/utils/glob.d.ts +6 -0
  41. package/dist/utils/glob.js +34 -0
  42. package/dist/utils/tokenize.js +13 -13
  43. package/package.json +7 -6
@@ -1,3 +1,4 @@
1
+ import { globToRegex } from "../utils/glob";
1
2
  import { tokenizeCommand } from "../utils/tokenize";
2
3
  // ── Public API ───────────────────────────────────────────────────────────────
3
4
  /**
@@ -42,28 +43,6 @@ export function expandGlob(pattern, entries) {
42
43
  const matches = entries.filter((e) => regex.test(e));
43
44
  return matches.length > 0 ? matches.sort() : [pattern];
44
45
  }
45
- function globToRegex(pattern) {
46
- let re = "^";
47
- for (let i = 0; i < pattern.length; i++) {
48
- const c = pattern[i];
49
- if (c === "*")
50
- re += ".*";
51
- else if (c === "?")
52
- re += ".";
53
- else if (c === "[") {
54
- const close = pattern.indexOf("]", i + 1);
55
- if (close === -1)
56
- re += "\\[";
57
- else {
58
- re += `[${pattern.slice(i + 1, close)}]`;
59
- i = close;
60
- }
61
- }
62
- else
63
- re += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
64
- }
65
- return new RegExp(`${re}$`);
66
- }
67
46
  // ── Internal parser ───────────────────────────────────────────────────────────
68
47
  function parseStatements(input) {
69
48
  // Split by ;, &&, || — respecting quotes and parens
@@ -5,7 +5,7 @@
5
5
  */
6
6
  export const exitCommand = {
7
7
  name: "exit",
8
- aliases: ["bye"],
8
+ aliases: ["bye", "logout"],
9
9
  description: "Exit the shell session",
10
10
  category: "shell",
11
11
  params: ["[code]"],
@@ -1,3 +1,4 @@
1
+ import { globToRegex } from "../utils/glob";
1
2
  import { assertPathAccess, resolvePath } from "./helpers";
2
3
  import { runCommand } from "./runtime";
3
4
  /**
@@ -109,10 +110,6 @@ export const findCommand = {
109
110
  return [{ type: "true" }, pos + 1];
110
111
  }
111
112
  const pred = exprArgs.length > 0 ? parseExpr(exprArgs, 0)[0] : { type: "true" };
112
- function globToRegex(pat, flags = "") {
113
- const esc = pat.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
114
- return new RegExp(`^${esc}$`, flags);
115
- }
116
113
  function matchPred(p, fullPath, depth) {
117
114
  switch (p.type) {
118
115
  case "true": return true;
@@ -1,28 +1,8 @@
1
1
  import type VirtualFileSystem from "../VirtualFileSystem";
2
2
  import type { VirtualPackageManager } from "../VirtualPackageManager";
3
3
  import type { VirtualShell } from "../VirtualShell";
4
- export declare function normalizeTerminalOutput(text: string): string;
5
4
  export declare function resolvePath(cwd: string, inputPath: string, homeDir?: string): string;
6
5
  export declare function assertPathAccess(authUser: string, targetPath: string, operation: string): void;
7
6
  export declare function stripUrlFilename(url: string): string;
8
- export declare function fetchResource(url: string): Promise<{
9
- text: string;
10
- status: number;
11
- contentType: string | null;
12
- }>;
13
- /**
14
- * Run a host command like curl or wget and capture its output.
15
- * @param binary - The binary to execute (e.g., "curl", "wget").
16
- * @param args - Arguments to pass to the binary.
17
- * @returns Promise resolving with stdout, stderr, and exit code.
18
- */
19
- export declare function runHostCommand(binary: string, args: string[]): Promise<{
20
- stdout: string;
21
- stderr: string;
22
- exitCode: number;
23
- }>;
24
7
  export declare function resolveReadablePath(vfs: VirtualFileSystem, cwd: string, inputPath: string): string;
25
- export declare function joinListWithType(cwd: string, items: string[], statAt: (p: string) => {
26
- type: "file" | "directory";
27
- }): string;
28
8
  export declare function getPackageManager(shell: VirtualShell): VirtualPackageManager | undefined;
@@ -1,23 +1,5 @@
1
- import { spawn } from "node:child_process";
2
1
  import * as path from "node:path";
3
2
  const PROTECTED_PREFIXES = ["/.virtual-env-js/.auth", "/etc/htpasswd"];
4
- function normalizeFetchUrl(input) {
5
- if (/^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(input)) {
6
- return input;
7
- }
8
- return `http://${input}`;
9
- }
10
- export function normalizeTerminalOutput(text) {
11
- return text
12
- .replace(/\r\n/g, "\n")
13
- .replace(/\r/g, "\n")
14
- .replace(/\t/g, " ")
15
- .split("\n")
16
- .map((line) => line.replace(/^[ \u00A0]{8,}/, " ").replace(/[ \u00A0]{3,}/g, " "))
17
- .join("\n")
18
- .replace(/\n{3,}/g, "\n\n")
19
- .trimEnd();
20
- }
21
3
  export function resolvePath(cwd, inputPath, homeDir) {
22
4
  if (!inputPath || inputPath.trim() === "") {
23
5
  return cwd;
@@ -49,76 +31,6 @@ export function stripUrlFilename(url) {
49
31
  const lastPart = cleaned.split("/").filter(Boolean).pop();
50
32
  return lastPart && lastPart.length > 0 ? lastPart : "index.html";
51
33
  }
52
- export async function fetchResource(url) {
53
- const response = await fetch(normalizeFetchUrl(url));
54
- const contentType = response.headers.get("content-type");
55
- return {
56
- text: await response.text(),
57
- status: response.status,
58
- contentType,
59
- };
60
- }
61
- /**
62
- * Run a host command like curl or wget and capture its output.
63
- * @param binary - The binary to execute (e.g., "curl", "wget").
64
- * @param args - Arguments to pass to the binary.
65
- * @returns Promise resolving with stdout, stderr, and exit code.
66
- */
67
- export function runHostCommand(binary, args) {
68
- return new Promise((resolve) => {
69
- let childProcess;
70
- try {
71
- childProcess = spawn(binary, args, {
72
- stdio: ["ignore", "pipe", "pipe"],
73
- });
74
- }
75
- catch (error) {
76
- resolve({
77
- stdout: "",
78
- stderr: `${binary}: ${error instanceof Error ? error.message : String(error)}`,
79
- exitCode: 1,
80
- });
81
- return;
82
- }
83
- let stdout = "";
84
- let stderr = "";
85
- const stdoutStream = childProcess.stdout;
86
- const stderrStream = childProcess.stderr;
87
- if (!stdoutStream || !stderrStream) {
88
- resolve({
89
- stdout: "",
90
- stderr: `${binary}: failed to capture process output`,
91
- exitCode: 1,
92
- });
93
- return;
94
- }
95
- stdoutStream.setEncoding("utf8");
96
- stderrStream.setEncoding("utf8");
97
- stdoutStream.on("data", (chunk) => {
98
- stdout += chunk;
99
- });
100
- stderrStream.on("data", (chunk) => {
101
- stderr += chunk;
102
- });
103
- childProcess.on("error", (error) => {
104
- const errorCode = error instanceof Error && "code" in error
105
- ? String(error.code ?? "")
106
- : "";
107
- resolve({
108
- stdout: "",
109
- stderr: `${binary}: ${error.message}`,
110
- exitCode: errorCode === "ENOENT" ? 127 : 1,
111
- });
112
- });
113
- childProcess.on("close", (code) => {
114
- resolve({
115
- stdout,
116
- stderr,
117
- exitCode: code ?? 1,
118
- });
119
- });
120
- });
121
- }
122
34
  function levenshtein(a, b) {
123
35
  const dp = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0));
124
36
  for (let i = 0; i <= a.length; i += 1) {
@@ -153,15 +65,6 @@ export function resolveReadablePath(vfs, cwd, inputPath) {
153
65
  }
154
66
  return exactPath;
155
67
  }
156
- export function joinListWithType(cwd, items, statAt) {
157
- return items
158
- .map((name) => {
159
- const childPath = resolvePath(cwd, name);
160
- const stats = statAt(childPath);
161
- return stats.type === "directory" ? `${name}/` : name;
162
- })
163
- .join(" ");
164
- }
165
68
  export function getPackageManager(shell) {
166
69
  return shell.packageManager;
167
70
  }
@@ -25,7 +25,7 @@ export const perlCommand = {
25
25
  for (let li = 0; li < lines.length; li++) {
26
26
  let line = lines[li];
27
27
  // $_ = line, $. = line number
28
- let processed = code
28
+ const processed = code
29
29
  .replace(/\$_/g, JSON.stringify(line))
30
30
  .replace(/\$\./g, String(li + 1));
31
31
  // s/pat/rep/[g] substitution on $_
@@ -694,8 +694,11 @@ class Interpreter {
694
694
  return left.repeat(right);
695
695
  if (Array.isArray(left) && typeof right === "number") {
696
696
  const arr = [];
697
- for (let i = 0; i < right; i++)
698
- arr.push(...left);
697
+ const n = right | 0;
698
+ for (let i = 0; i < n; i++) {
699
+ for (let j = 0; j < left.length; j++)
700
+ arr.push(left[j]);
701
+ }
699
702
  return arr;
700
703
  }
701
704
  return left * right;
@@ -255,7 +255,12 @@ export function registerCommand(module) {
255
255
  throw new Error("Command names must be non-empty and contain no spaces");
256
256
  }
257
257
  customCommands.push(normalized);
258
- buildCache();
258
+ // Incremental insert — avoids full Map rebuild for every registerCommand call
259
+ commandRegistry.set(normalized.name, normalized);
260
+ for (const alias of normalized.aliases ?? [])
261
+ commandRegistry.set(alias, normalized);
262
+ // Invalidate sorted names cache; rebuilt lazily on next getCommandNames()
263
+ cachedCommandNames = null;
259
264
  }
260
265
  export function createCustomCommand(name, params, run) {
261
266
  return { name, params, run };
@@ -4,6 +4,16 @@ import { parseScript } from "../VirtualShell/shellParser";
4
4
  import { expandAsync, expandBraces, expandGlob } from "../utils/expand";
5
5
  import { tokenizeCommand } from "../utils/tokenize";
6
6
  import { resolveModule } from "./registry";
7
+ // Module-level compiled regexes — avoids recompilation on every runCommand call
8
+ const ASSIGN_RE = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/;
9
+ const RE_FOR = /\bfor\s+\w+\s+in\b/;
10
+ const RE_WHILE = /\bwhile\s+/;
11
+ const RE_IF = /\bif\s+/;
12
+ const RE_FUNC_BRACE = /\w+\s*\(\s*\)\s*\{/;
13
+ const RE_FUNC_KW = /\bfunction\s+\w+/;
14
+ const RE_ARITH = /\(\(\s*.+\s*\)\)/;
15
+ const RE_PIPE = /(?<![|&])[|](?![|])/;
16
+ const RE_OPERATORS = /[><;&]|\|\|/;
7
17
  /** Returns the home directory path for a given user. Root lives at /root. */
8
18
  export function userHome(authUser) {
9
19
  return authUser === "root" ? "/root" : `/home/${authUser}`;
@@ -43,7 +53,13 @@ function resolveVfsBinary(name, env, shell, authUser) {
43
53
  return null;
44
54
  }
45
55
  }
46
- const pathDirs = (env.vars.PATH ?? "/usr/local/bin:/usr/bin:/bin").split(":");
56
+ const rawPath = env.vars.PATH ?? "/usr/local/bin:/usr/bin:/bin";
57
+ // Cache split PATH on the env object to avoid re-splitting on every binary lookup
58
+ if (!env._pathDirs || env._pathRaw !== rawPath) {
59
+ env._pathRaw = rawPath;
60
+ env._pathDirs = rawPath.split(":");
61
+ }
62
+ const pathDirs = env._pathDirs;
47
63
  for (const dir of pathDirs) {
48
64
  if ((dir === "/sbin" || dir === "/usr/sbin") && authUser !== "root")
49
65
  continue;
@@ -63,6 +79,35 @@ function resolveVfsBinary(name, env, shell, authUser) {
63
79
  return null;
64
80
  }
65
81
  const MAX_CALL_DEPTH = 8;
82
+ /** Run a VFS stub file as a command, handling `exec builtin <name>` and `sh -c` stubs. */
83
+ async function runVfsStub(vfsBinary, cmdName, args, rawInput, authUser, hostname, mode, cwd, shell, env, stdin) {
84
+ const stubContent = shell.vfs.readFile(vfsBinary);
85
+ const builtinMatch = stubContent.match(/exec\s+builtin\s+(\S+)/);
86
+ if (builtinMatch) {
87
+ const builtinMod = resolveModule(builtinMatch[1]);
88
+ if (builtinMod) {
89
+ return builtinMod.run({
90
+ authUser, hostname,
91
+ activeSessions: shell.users.listActiveSessions(),
92
+ rawInput, mode, args, stdin, cwd, shell, env,
93
+ });
94
+ }
95
+ // Guard: missing builtin — stop here to avoid sh -c infinite loop
96
+ return { stderr: `${cmdName}: exec builtin '${builtinMatch[1]}' not found`, exitCode: 127 };
97
+ }
98
+ const shMod = resolveModule("sh");
99
+ if (shMod) {
100
+ return shMod.run({
101
+ authUser, hostname,
102
+ activeSessions: shell.users.listActiveSessions(),
103
+ rawInput: `sh -c ${JSON.stringify(stubContent)}`,
104
+ mode,
105
+ args: ["-c", stubContent, "--", ...args],
106
+ stdin, cwd, shell, env,
107
+ });
108
+ }
109
+ return { stderr: `${cmdName}: command not found`, exitCode: 127 };
110
+ }
66
111
  let _callDepth = 0;
67
112
  export async function runCommandDirect(name, args, authUser, hostname, mode, cwd, shell, stdin, env) {
68
113
  // Anti-loop guard: track call depth via env to avoid infinite recursion
@@ -81,7 +126,7 @@ export async function runCommandDirect(name, args, authUser, hostname, mode, cwd
81
126
  }
82
127
  }
83
128
  async function _runCommandDirectInner(name, args, authUser, hostname, mode, cwd, shell, stdin, env) {
84
- const assignRe = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/;
129
+ const assignRe = ASSIGN_RE;
85
130
  const invocation = [name, ...args];
86
131
  let assignCount = 0;
87
132
  while (assignCount < invocation.length && assignRe.test(invocation[assignCount])) {
@@ -119,42 +164,7 @@ async function _runCommandDirectInner(name, args, authUser, hostname, mode, cwd,
119
164
  if (!mod) {
120
165
  const vfsBinary = resolveVfsBinary(name, env, shell, authUser);
121
166
  if (vfsBinary) {
122
- const stubContent = shell.vfs.readFile(vfsBinary);
123
- const builtinMatch = stubContent.match(/exec\s+builtin\s+(\S+)/);
124
- if (builtinMatch) {
125
- const builtinMod = resolveModule(builtinMatch[1]);
126
- if (builtinMod) {
127
- return await builtinMod.run({
128
- authUser,
129
- hostname,
130
- activeSessions: shell.users.listActiveSessions(),
131
- rawInput: [name, ...args].join(" "),
132
- mode,
133
- args,
134
- stdin,
135
- cwd,
136
- shell,
137
- env,
138
- });
139
- }
140
- // builtin not found — stop here, don't fall through to sh -c (avoids infinite loop)
141
- return { stderr: `${name}: exec builtin '${builtinMatch[1]}' not found`, exitCode: 127 };
142
- }
143
- const shMod = resolveModule("sh");
144
- if (shMod) {
145
- return await shMod.run({
146
- authUser,
147
- hostname,
148
- activeSessions: shell.users.listActiveSessions(),
149
- rawInput: `sh -c ${JSON.stringify(stubContent)}`,
150
- mode,
151
- args: ["-c", stubContent, "--", ...args],
152
- stdin,
153
- cwd,
154
- shell,
155
- env,
156
- });
157
- }
167
+ return runVfsStub(vfsBinary, name, args, [name, ...args].join(" "), authUser, hostname, mode, cwd, shell, env, stdin);
158
168
  }
159
169
  return { stderr: `${name}: command not found`, exitCode: 127 };
160
170
  }
@@ -220,18 +230,13 @@ export async function runCommand(rawInput, authUser, hostname, mode, cwd, shell,
220
230
  ? trimmed.replace(rawFirstWord, aliasVal)
221
231
  : trimmed;
222
232
  // Detect sh-syntax constructs that must be handled by the sh interpreter
223
- const isShScript = /\bfor\s+\w+\s+in\b/.test(aliasExpanded) ||
224
- /\bwhile\s+/.test(aliasExpanded) ||
225
- /\bif\s+/.test(aliasExpanded) ||
226
- /\w+\s*\(\s*\)\s*\{/.test(aliasExpanded) ||
227
- /\bfunction\s+\w+/.test(aliasExpanded) ||
228
- /\(\(\s*.+\s*\)\)/.test(aliasExpanded);
229
- const hasOperators = /(?<![|&])[|](?![|])/.test(aliasExpanded) ||
230
- aliasExpanded.includes(">") ||
231
- aliasExpanded.includes("<") ||
232
- aliasExpanded.includes("&&") ||
233
- aliasExpanded.includes("||") ||
234
- aliasExpanded.includes(";");
233
+ const isShScript = RE_FOR.test(aliasExpanded) ||
234
+ RE_WHILE.test(aliasExpanded) ||
235
+ RE_IF.test(aliasExpanded) ||
236
+ RE_FUNC_BRACE.test(aliasExpanded) ||
237
+ RE_FUNC_KW.test(aliasExpanded) ||
238
+ RE_ARITH.test(aliasExpanded);
239
+ const hasOperators = RE_PIPE.test(aliasExpanded) || RE_OPERATORS.test(aliasExpanded);
235
240
  if ((isShScript && rawFirstWord !== "sh" && rawFirstWord !== "bash") || hasOperators) {
236
241
  // sh-syntax: route through sh interpreter to handle for/while/functions
237
242
  if (isShScript && rawFirstWord !== "sh" && rawFirstWord !== "bash") {
@@ -267,52 +272,25 @@ export async function runCommand(rawInput, authUser, hostname, mode, cwd, shell,
267
272
  const parts = tokenizeCommand(expanded.trim());
268
273
  if (parts.length === 0)
269
274
  return { exitCode: 0 };
270
- const assignRe = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/;
275
+ const assignRe = ASSIGN_RE;
271
276
  if (assignRe.test(parts[0])) {
272
277
  return runCommandDirect(parts[0], parts.slice(1), authUser, hostname, mode, cwd, shell, stdin, shellEnv);
273
278
  }
274
279
  const commandName = parts[0]?.toLowerCase() ?? "";
275
280
  // Apply brace expansion to each arg token
276
- const args = parts.slice(1).flatMap(expandBraces).flatMap(token => expandGlob(token, cwd, shell.vfs));
281
+ const rawArgs = parts.slice(1);
282
+ const args = [];
283
+ for (const token of rawArgs) {
284
+ for (const brace of expandBraces(token)) {
285
+ for (const glob of expandGlob(brace, cwd, shell.vfs))
286
+ args.push(glob);
287
+ }
288
+ }
277
289
  const mod = resolveModule(commandName);
278
290
  if (!mod) {
279
291
  const vfsBinary = resolveVfsBinary(commandName, shellEnv, shell, authUser);
280
292
  if (vfsBinary) {
281
- const stubContent = shell.vfs.readFile(vfsBinary);
282
- const builtinMatch = stubContent.match(/exec\s+builtin\s+(\S+)/);
283
- if (builtinMatch) {
284
- const builtinName = builtinMatch[1];
285
- const builtinMod = resolveModule(builtinName);
286
- if (builtinMod) {
287
- return await builtinMod.run({
288
- authUser,
289
- hostname,
290
- activeSessions: shell.users.listActiveSessions(),
291
- rawInput: [commandName, ...args].join(" "),
292
- mode,
293
- args,
294
- stdin,
295
- cwd,
296
- shell,
297
- env: shellEnv,
298
- });
299
- }
300
- }
301
- const shMod = resolveModule("sh");
302
- if (shMod) {
303
- return await shMod.run({
304
- authUser,
305
- hostname,
306
- activeSessions: shell.users.listActiveSessions(),
307
- rawInput: `sh -c ${JSON.stringify(stubContent)}`,
308
- mode,
309
- args: ["-c", stubContent, "--", ...args],
310
- stdin,
311
- cwd,
312
- shell,
313
- env: shellEnv,
314
- });
315
- }
293
+ return runVfsStub(vfsBinary, commandName, args, expanded, authUser, hostname, mode, cwd, shell, shellEnv, stdin);
316
294
  }
317
295
  return { stderr: `${commandName}: command not found`, exitCode: 127 };
318
296
  }
@@ -11,7 +11,7 @@ export const straceCommand = {
11
11
  const cmd = args.find((a) => !a.startsWith("-"));
12
12
  if (!cmd)
13
13
  return { stderr: "strace: must have PROG [ARGS] or -p PID", exitCode: 1 };
14
- const pid = Math.floor(Math.random() * 30000) + 1000;
14
+ const _pid = Math.floor(Math.random() * 30000) + 1000;
15
15
  const lines = [
16
16
  `execve("/usr/bin/${cmd}", ["${cmd}"${args.slice(1).map((a) => `, "${a}"`).join("")}], 0x... /* ... vars */) = 0`,
17
17
  `brk(NULL) = 0x${(Math.random() * 0xfffff | 0).toString(16)}000`,
@@ -11,8 +11,8 @@ function makeTarHeader(name, size, isDir) {
11
11
  enc(isDir ? "0000755\0" : "0000644\0", 100, 8);
12
12
  enc("0000000\0", 108, 8);
13
13
  enc("0000000\0", 116, 8);
14
- enc(size.toString(8).padStart(11, "0") + "\0", 124, 12);
15
- enc(Math.floor(Date.now() / 1000).toString(8).padStart(11, "0") + "\0", 136, 12);
14
+ enc(`${size.toString(8).padStart(11, "0")}\0`, 124, 12);
15
+ enc(`${Math.floor(Date.now() / 1000).toString(8).padStart(11, "0")}\0`, 136, 12);
16
16
  hdr[156] = isDir ? 0x35 : 0x30; // '5' dir, '0' file
17
17
  enc("ustar\0", 257, 6);
18
18
  enc("00", 263, 2);
@@ -1,3 +1,4 @@
1
+ import { resolvePath } from "./helpers";
1
2
  /**
2
3
  * Evaluate a POSIX test expression.
3
4
  * Supports: -f, -d, -e, -r, -w, -x, -s, -z, -n,
@@ -33,8 +34,7 @@ function evalTest(tokens, shell, cwd) {
33
34
  // Unary file tests
34
35
  if (tokens.length === 2) {
35
36
  const [flag, operand = ""] = tokens;
36
- const resolvePath = (p) => p.startsWith("/") ? p : `${cwd}/${p}`.replace(/\/+/g, "/");
37
- const path = resolvePath(operand);
37
+ const path = resolvePath(cwd, operand);
38
38
  switch (flag) {
39
39
  case "-e":
40
40
  return shell.vfs.exists(path);
@@ -1281,7 +1281,7 @@ Installed-Size: 6800
1281
1281
  Maintainer: Fortune Package Team <dpkg@fortune.local>
1282
1282
  Architecture: amd64
1283
1283
  Version: 1.22.6nyx1
1284
- Depends: libc6 (>= 2.17), libzstd1 (>= 1.5.6)
1284
+ Depends: libc6 (>= 2.17), libzstd1 (>= 1.5.7)
1285
1285
  Description: Fortune package management system
1286
1286
  This package provides the low-level infrastructure for handling the
1287
1287
  installation and removal of Fortune software packages.
@@ -1547,12 +1547,9 @@ export function bootstrapLinuxRootfs(vfs, users, hostname, props, shellStartTime
1547
1547
  const snapshot = getStaticRootfsSnapshot(hostname, props);
1548
1548
  const hasRestoredData = vfs.getMode() === "fs" && vfs.exists("/home");
1549
1549
  if (hasRestoredData) {
1550
- // Snapshot was already restored — merge static rootfs without
1551
- // clobbering user files and directories.
1552
1550
  vfs.mergeRootTree(decodeVfs(snapshot));
1553
1551
  }
1554
1552
  else {
1555
- // Fresh start — replace the empty tree with the full static rootfs.
1556
1553
  vfs.importRootTree(decodeVfs(snapshot));
1557
1554
  }
1558
1555
  bootstrapRoot(vfs);
@@ -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
  };
@@ -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 {};