typescript-virtual-container 1.2.7 → 1.2.9

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 (130) hide show
  1. package/README.md +457 -42
  2. package/dist/SSHMimic/executor.js +3 -5
  3. package/dist/VirtualFileSystem/binaryPack.d.ts +49 -0
  4. package/dist/VirtualFileSystem/binaryPack.d.ts.map +1 -0
  5. package/dist/VirtualFileSystem/binaryPack.js +193 -0
  6. package/dist/VirtualFileSystem/index.d.ts +7 -5
  7. package/dist/VirtualFileSystem/index.d.ts.map +1 -1
  8. package/dist/VirtualFileSystem/index.js +20 -9
  9. package/dist/VirtualPackageManager/index.d.ts +202 -0
  10. package/dist/VirtualPackageManager/index.d.ts.map +1 -0
  11. package/dist/VirtualPackageManager/index.js +676 -0
  12. package/dist/VirtualShell/index.d.ts +87 -12
  13. package/dist/VirtualShell/index.d.ts.map +1 -1
  14. package/dist/VirtualShell/index.js +83 -12
  15. package/dist/VirtualUserManager/index.d.ts +52 -20
  16. package/dist/VirtualUserManager/index.d.ts.map +1 -1
  17. package/dist/VirtualUserManager/index.js +54 -20
  18. package/dist/commands/alias.d.ts +4 -0
  19. package/dist/commands/alias.d.ts.map +1 -0
  20. package/dist/commands/alias.js +58 -0
  21. package/dist/commands/apt.d.ts +4 -0
  22. package/dist/commands/apt.d.ts.map +1 -0
  23. package/dist/commands/apt.js +182 -0
  24. package/dist/commands/cat.d.ts.map +1 -1
  25. package/dist/commands/cat.js +27 -8
  26. package/dist/commands/chmod.d.ts.map +1 -1
  27. package/dist/commands/chmod.js +52 -3
  28. package/dist/commands/command-helpers.d.ts +78 -4
  29. package/dist/commands/command-helpers.d.ts.map +1 -1
  30. package/dist/commands/command-helpers.js +78 -4
  31. package/dist/commands/curl.d.ts.map +1 -1
  32. package/dist/commands/curl.js +81 -29
  33. package/dist/commands/dpkg.d.ts +4 -0
  34. package/dist/commands/dpkg.d.ts.map +1 -0
  35. package/dist/commands/dpkg.js +144 -0
  36. package/dist/commands/echo.d.ts.map +1 -1
  37. package/dist/commands/echo.js +24 -12
  38. package/dist/commands/free.d.ts +3 -0
  39. package/dist/commands/free.d.ts.map +1 -0
  40. package/dist/commands/free.js +38 -0
  41. package/dist/commands/helpers.d.ts +3 -0
  42. package/dist/commands/helpers.d.ts.map +1 -1
  43. package/dist/commands/helpers.js +3 -0
  44. package/dist/commands/history.d.ts +3 -0
  45. package/dist/commands/history.d.ts.map +1 -0
  46. package/dist/commands/history.js +21 -0
  47. package/dist/commands/index.d.ts +8 -1
  48. package/dist/commands/index.d.ts.map +1 -1
  49. package/dist/commands/index.js +120 -11
  50. package/dist/commands/ls.d.ts.map +1 -1
  51. package/dist/commands/ls.js +4 -3
  52. package/dist/commands/lsb-release.d.ts +3 -0
  53. package/dist/commands/lsb-release.d.ts.map +1 -0
  54. package/dist/commands/lsb-release.js +50 -0
  55. package/dist/commands/man.d.ts +3 -0
  56. package/dist/commands/man.d.ts.map +1 -0
  57. package/dist/commands/man.js +155 -0
  58. package/dist/commands/neofetch.d.ts.map +1 -1
  59. package/dist/commands/neofetch.js +5 -0
  60. package/dist/commands/ping.d.ts.map +1 -1
  61. package/dist/commands/ping.js +5 -2
  62. package/dist/commands/ps.d.ts.map +1 -1
  63. package/dist/commands/ps.js +27 -6
  64. package/dist/commands/sh.d.ts.map +1 -1
  65. package/dist/commands/sh.js +29 -11
  66. package/dist/commands/source.d.ts +3 -0
  67. package/dist/commands/source.d.ts.map +1 -0
  68. package/dist/commands/source.js +31 -0
  69. package/dist/commands/test.d.ts +3 -0
  70. package/dist/commands/test.d.ts.map +1 -0
  71. package/dist/commands/test.js +92 -0
  72. package/dist/commands/type.d.ts +3 -0
  73. package/dist/commands/type.d.ts.map +1 -0
  74. package/dist/commands/type.js +34 -0
  75. package/dist/commands/uptime.d.ts +3 -0
  76. package/dist/commands/uptime.d.ts.map +1 -0
  77. package/dist/commands/uptime.js +40 -0
  78. package/dist/commands/wget.d.ts.map +1 -1
  79. package/dist/commands/wget.js +71 -100
  80. package/dist/commands/which.d.ts +3 -0
  81. package/dist/commands/which.d.ts.map +1 -0
  82. package/dist/commands/which.js +32 -0
  83. package/dist/index.d.ts +5 -2
  84. package/dist/index.d.ts.map +1 -1
  85. package/dist/index.js +2 -1
  86. package/dist/modules/linuxRootfs.d.ts +24 -0
  87. package/dist/modules/linuxRootfs.d.ts.map +1 -0
  88. package/dist/modules/linuxRootfs.js +297 -0
  89. package/dist/modules/neofetch.d.ts.map +1 -1
  90. package/dist/modules/neofetch.js +1 -0
  91. package/dist/standalone.js +4 -1
  92. package/package.json +2 -1
  93. package/src/SSHMimic/executor.ts +3 -5
  94. package/src/VirtualFileSystem/binaryPack.ts +219 -0
  95. package/src/VirtualFileSystem/index.ts +21 -11
  96. package/src/VirtualPackageManager/index.ts +820 -0
  97. package/src/VirtualShell/index.ts +104 -13
  98. package/src/VirtualUserManager/index.ts +55 -20
  99. package/src/commands/alias.ts +60 -0
  100. package/src/commands/apt.ts +198 -0
  101. package/src/commands/cat.ts +32 -8
  102. package/src/commands/chmod.ts +48 -3
  103. package/src/commands/command-helpers.ts +78 -4
  104. package/src/commands/curl.ts +78 -37
  105. package/src/commands/dpkg.ts +158 -0
  106. package/src/commands/echo.ts +30 -14
  107. package/src/commands/free.ts +40 -0
  108. package/src/commands/helpers.ts +8 -0
  109. package/src/commands/history.ts +29 -0
  110. package/src/commands/index.ts +116 -11
  111. package/src/commands/ls.ts +5 -4
  112. package/src/commands/lsb-release.ts +52 -0
  113. package/src/commands/man.ts +166 -0
  114. package/src/commands/neofetch.ts +5 -0
  115. package/src/commands/ping.ts +5 -2
  116. package/src/commands/ps.ts +28 -6
  117. package/src/commands/sh.ts +33 -11
  118. package/src/commands/source.ts +35 -0
  119. package/src/commands/test.ts +100 -0
  120. package/src/commands/type.ts +40 -0
  121. package/src/commands/uptime.ts +46 -0
  122. package/src/commands/wget.ts +70 -123
  123. package/src/commands/which.ts +34 -0
  124. package/src/index.ts +10 -0
  125. package/src/modules/linuxRootfs.ts +439 -0
  126. package/src/modules/neofetch.ts +1 -0
  127. package/src/standalone.ts +4 -1
  128. package/standalone.js +418 -103
  129. package/standalone.js.map +4 -4
  130. package/tests/new-features.test.ts +626 -0
@@ -1,6 +1,42 @@
1
1
  import type { ShellModule } from "../types/commands";
2
2
  import { assertPathAccess, resolvePath } from "./helpers";
3
3
 
4
+ /**
5
+ * Parse a symbolic chmod mode string (e.g. "+x", "u+x", "go-w", "a+rx")
6
+ * and apply it to the existing mode bits.
7
+ * Returns null if the string is not a valid symbolic mode.
8
+ */
9
+ function applySymbolicMode(existing: number, modeStr: string): number | null {
10
+ const pattern = /^([ugoa]*)([+\-=])([rwx]*)$/;
11
+ const parts = modeStr.split(",");
12
+ let mode = existing;
13
+ for (const part of parts) {
14
+ const m = part.trim().match(pattern);
15
+ if (!m) return null;
16
+ const [, who = "a", op, perms = ""] = m;
17
+ const targets = who === "" || who === "a" ? ["u", "g", "o"] : who.split("");
18
+ const bits: Record<string, Record<string, number>> = {
19
+ u: { r: 0o400, w: 0o200, x: 0o100 },
20
+ g: { r: 0o040, w: 0o020, x: 0o010 },
21
+ o: { r: 0o004, w: 0o002, x: 0o001 },
22
+ };
23
+ for (const t of targets) {
24
+ for (const p of perms.split("")) {
25
+ const bit = bits[t]?.[p];
26
+ if (bit === undefined) continue;
27
+ if (op === "+") mode |= bit;
28
+ else if (op === "-") mode &= ~bit;
29
+ else if (op === "=") {
30
+ // clear all bits for this target, then set requested
31
+ const mask = Object.values(bits[t] ?? {}).reduce((a, b) => a | b, 0);
32
+ mode = (mode & ~mask) | bit;
33
+ }
34
+ }
35
+ }
36
+ }
37
+ return mode;
38
+ }
39
+
4
40
  export const chmodCommand: ShellModule = {
5
41
  name: "chmod",
6
42
  description: "Change file permissions",
@@ -21,9 +57,18 @@ export const chmodCommand: ShellModule = {
21
57
  exitCode: 1,
22
58
  };
23
59
  }
24
- const mode = parseInt(modeArg, 8);
25
- if (Number.isNaN(mode)) {
26
- return { stderr: `chmod: invalid mode: ${modeArg}`, exitCode: 1 };
60
+ let mode: number;
61
+ const octal = parseInt(modeArg, 8);
62
+ if (!Number.isNaN(octal) && /^[0-7]+$/.test(modeArg)) {
63
+ mode = octal;
64
+ } else {
65
+ // symbolic mode
66
+ const existing = shell.vfs.stat(filePath).mode;
67
+ const result = applySymbolicMode(existing, modeArg);
68
+ if (result === null) {
69
+ return { stderr: `chmod: invalid mode: ${modeArg}`, exitCode: 1 };
70
+ }
71
+ mode = result;
27
72
  }
28
73
  shell.vfs.chmod(filePath, mode);
29
74
  return { exitCode: 0 };
@@ -80,6 +80,23 @@ function collectPositionals(
80
80
  return positionals;
81
81
  }
82
82
 
83
+ /**
84
+ * Returns `true` when any of the given flags appear in `args`.
85
+ *
86
+ * Matches both standalone tokens (`-s`, `--silent`) and inline forms
87
+ * (`--output=file`). Useful for simple boolean flag checks inside command
88
+ * `run` handlers.
89
+ *
90
+ * @param args Tokenized argument array from `CommandContext.args`.
91
+ * @param flags Single flag string or array of equivalent flag strings.
92
+ * @returns `true` if at least one flag is present, otherwise `false`.
93
+ *
94
+ * @example
95
+ * ```ts
96
+ * ifFlag(args, "-r") // single flag
97
+ * ifFlag(args, ["-r", "--recursive"]) // aliases
98
+ * ```
99
+ */
83
100
  export function ifFlag(args: string[], flags: string | string[]): boolean {
84
101
  const allFlags = toFlagList(flags);
85
102
 
@@ -94,6 +111,25 @@ export function ifFlag(args: string[], flags: string | string[]): boolean {
94
111
  return false;
95
112
  }
96
113
 
114
+ /**
115
+ * Returns the value associated with a flag, or `true` if the flag is present
116
+ * but has no associated value, or `undefined` if the flag is absent.
117
+ *
118
+ * Handles three forms:
119
+ * - `--output file` → returns `"file"` (next token)
120
+ * - `--output=file` → returns `"file"` (inline `=` form)
121
+ * - `--verbose` → returns `true` (flag with no value)
122
+ *
123
+ * @param args Tokenized argument array from `CommandContext.args`.
124
+ * @param flags Single flag string or array of equivalent flag strings.
125
+ * @returns The flag value string, `true` when valueless, or `undefined`.
126
+ *
127
+ * @example
128
+ * ```ts
129
+ * const output = getFlag(args, ["-o", "--output"]);
130
+ * if (typeof output === "string") { /* use path *\/ }
131
+ * ```
132
+ */
97
133
  export function getFlag(
98
134
  args: string[],
99
135
  flags: string | string[],
@@ -125,6 +161,26 @@ export function getFlag(
125
161
  return undefined;
126
162
  }
127
163
 
164
+ /**
165
+ * Returns the positional argument at the given zero-based index, skipping
166
+ * known flags and their values.
167
+ *
168
+ * Flags declared in `options.flags` are treated as boolean and skipped.
169
+ * Flags declared in `options.flagsWithValue` consume the next token too.
170
+ * Tokens after `--` are always treated as positionals.
171
+ *
172
+ * @param args Tokenized argument array from `CommandContext.args`.
173
+ * @param index Zero-based positional index to retrieve.
174
+ * @param options Optional flag declarations to skip during positional collection.
175
+ * @returns The positional value, or `undefined` if the index is out of range.
176
+ *
177
+ * @example
178
+ * ```ts
179
+ * // args = ["-r", "src", "dest"]
180
+ * getArg(args, 0, { flags: ["-r"] }) // "src"
181
+ * getArg(args, 1, { flags: ["-r"] }) // "dest"
182
+ * ```
183
+ */
128
184
  export function getArg(
129
185
  args: string[],
130
186
  index: number,
@@ -135,10 +191,28 @@ export function getArg(
135
191
  }
136
192
 
137
193
  /**
138
- * Parse arguments into flags, flags with values, and positionals.
139
- * @param args - Array of arguments to parse.
140
- * @param options - Parsing options for flags and flags with values.
141
- * @returns Parsed arguments as { flags, flagsWithValues, positionals }.
194
+ * Parses an argument array into structured flags, flag values, and positionals.
195
+ *
196
+ * - `options.flags` boolean flags (e.g. `["-r", "--recursive"]`); collected
197
+ * into a `Set<string>` and not treated as positionals.
198
+ * - `options.flagsWithValue` — flags that consume the next token or an inline
199
+ * `=value`; collected into a `Map<string, string>`.
200
+ * - All remaining tokens are positionals.
201
+ * - Tokens after `--` are always positionals, regardless of `-` prefix.
202
+ *
203
+ * @param args Tokenized argument array from `CommandContext.args`.
204
+ * @param options Flag declaration lists.
205
+ * @returns `{ flags, flagsWithValues, positionals }`.
206
+ *
207
+ * @example
208
+ * ```ts
209
+ * const { flags, flagsWithValues, positionals } = parseArgs(args, {
210
+ * flags: ["-r", "--recursive"],
211
+ * flagsWithValue: ["-o", "--output"],
212
+ * });
213
+ * const recursive = flags.has("-r");
214
+ * const output = flagsWithValues.get("-o");
215
+ * ```
142
216
  */
143
217
  export function parseArgs(
144
218
  args: string[],
@@ -1,61 +1,102 @@
1
1
  import type { ShellModule } from "../types/commands";
2
- import { parseArgs } from "./command-helpers";
3
- import {
4
- assertPathAccess,
5
- normalizeTerminalOutput,
6
- resolvePath,
7
- runHostCommand,
8
- } from "./helpers";
2
+ import { ifFlag, parseArgs } from "./command-helpers";
3
+ import { assertPathAccess, resolvePath } from "./helpers";
9
4
 
10
5
  export const curlCommand: ShellModule = {
11
6
  name: "curl",
12
- description: "HTTP client",
7
+ description: "Transfer data from or to a server (pure fetch)",
13
8
  category: "network",
14
- params: ["[-o file] <url>"],
9
+ params: ["[options] <url>"],
15
10
  run: async ({ authUser, cwd, args, shell }) => {
16
11
  const { flagsWithValues, positionals } = parseArgs(args, {
17
- flagsWithValue: ["-o", "--output"],
12
+ flagsWithValue: ["-o", "--output", "-X", "--request", "-d", "--data", "-H", "--header", "-u", "--user"],
18
13
  });
19
- const outputPath =
20
- flagsWithValues.get("-o") || flagsWithValues.get("--output") || null;
14
+
15
+ if (ifFlag(args, ["--help", "-h"])) {
16
+ return {
17
+ stdout: [
18
+ "Usage: curl [options] <url>",
19
+ " -o, --output <file> Write to file",
20
+ " -X, --request <method> HTTP method",
21
+ " -d, --data <data> POST data",
22
+ " -H, --header <hdr> Extra header",
23
+ " -s, --silent Silent mode",
24
+ " -I, --head Fetch headers only",
25
+ " -L, --location Follow redirects",
26
+ " -v, --verbose Verbose",
27
+ ].join("\n"),
28
+ exitCode: 0,
29
+ };
30
+ }
31
+
21
32
  const url = positionals[0];
33
+ if (!url) return { stderr: "curl: no URL specified", exitCode: 1 };
22
34
 
23
- if (!url) {
24
- return { stderr: "curl: missing URL", exitCode: 1 };
35
+ const outputPath = flagsWithValues.get("-o") ?? flagsWithValues.get("--output") ?? null;
36
+ const method = (flagsWithValues.get("-X") ?? flagsWithValues.get("--request") ?? "GET").toUpperCase();
37
+ const postData = flagsWithValues.get("-d") ?? flagsWithValues.get("--data") ?? null;
38
+ const headerRaw = flagsWithValues.get("-H") ?? flagsWithValues.get("--header") ?? null;
39
+ const silent = ifFlag(args, ["-s", "--silent"]);
40
+ const headOnly = ifFlag(args, ["-I", "--head"]);
41
+ const followRedirects = ifFlag(args, ["-L", "--location"]);
42
+ const verbose = ifFlag(args, ["-v", "--verbose"]);
43
+
44
+ const extraHeaders: Record<string, string> = { "User-Agent": "curl/7.88.1" };
45
+ if (headerRaw) {
46
+ const idx = headerRaw.indexOf(":");
47
+ if (idx !== -1) extraHeaders[headerRaw.slice(0, idx).trim()] = headerRaw.slice(idx + 1).trim();
25
48
  }
26
49
 
27
- const passthroughArgs = outputPath
28
- ? [...positionals, "-o", "-"]
29
- : positionals;
30
- const result = await runHostCommand("curl", passthroughArgs);
50
+ const finalMethod = postData && method === "GET" ? "POST" : method;
51
+ const fetchOpts: RequestInit = {
52
+ method: finalMethod,
53
+ headers: extraHeaders,
54
+ redirect: followRedirects ? "follow" : "manual",
55
+ };
56
+ if (postData) {
57
+ extraHeaders["Content-Type"] ??= "application/x-www-form-urlencoded";
58
+ fetchOpts.body = postData;
59
+ }
31
60
 
32
- if (result.exitCode !== 0) {
33
- return {
34
- stderr: normalizeTerminalOutput(
35
- result.stderr || `curl: exited with code ${result.exitCode}`,
36
- ),
37
- exitCode: result.exitCode,
38
- };
61
+ const stderrLines: string[] = [];
62
+ if (verbose) {
63
+ stderrLines.push(`* Trying ${url}...`, `* Connected`);
64
+ stderrLines.push(`> ${finalMethod} / HTTP/1.1`, `> Host: ${new URL(url).host}`);
39
65
  }
40
66
 
67
+ let response: Response;
68
+ try {
69
+ response = await fetch(url, fetchOpts);
70
+ } catch (err) {
71
+ const msg = err instanceof Error ? err.message : String(err);
72
+ return { stderr: `curl: (6) Could not resolve host: ${msg}`, exitCode: 6 };
73
+ }
74
+
75
+ if (verbose) {
76
+ stderrLines.push(`< HTTP/1.1 ${response.status} ${response.statusText}`);
77
+ }
78
+
79
+ if (headOnly) {
80
+ const lines = [`HTTP/1.1 ${response.status} ${response.statusText}`];
81
+ for (const [k, v] of response.headers.entries()) lines.push(`${k}: ${v}`);
82
+ return { stdout: `${lines.join("\r\n")}\r\n`, exitCode: 0 };
83
+ }
84
+
85
+ let body: string;
86
+ try { body = await response.text(); } catch { return { stderr: "curl: failed to read response body", exitCode: 1 }; }
87
+
41
88
  if (outputPath) {
42
89
  const target = resolvePath(cwd, outputPath);
43
90
  assertPathAccess(authUser, target, "curl");
44
- shell.writeFileAsUser(authUser, target, result.stdout);
45
- return {
46
- stderr: result.stderr
47
- ? normalizeTerminalOutput(result.stderr)
48
- : undefined,
49
- exitCode: 0,
50
- };
91
+ shell.writeFileAsUser(authUser, target, body);
92
+ if (!silent) stderrLines.push(` % Total % Received\n100 ${body.length} 100 ${body.length}`);
93
+ return { stderr: stderrLines.join("\n") || undefined, exitCode: response.ok ? 0 : 22 };
51
94
  }
52
95
 
53
96
  return {
54
- stdout: result.stdout,
55
- stderr: result.stderr
56
- ? normalizeTerminalOutput(result.stderr)
57
- : undefined,
58
- exitCode: 0,
97
+ stdout: body,
98
+ stderr: stderrLines.length > 0 ? stderrLines.join("\n") : undefined,
99
+ exitCode: response.ok ? 0 : 22,
59
100
  };
60
101
  },
61
102
  };
@@ -0,0 +1,158 @@
1
+ import type { ShellModule } from "../types/commands";
2
+ import { ifFlag, parseArgs } from "./command-helpers";
3
+ import { getPackageManager } from "./helpers";
4
+
5
+ export const dpkgCommand: ShellModule = {
6
+ name: "dpkg",
7
+ description: "Debian package manager low-level tool",
8
+ category: "system",
9
+ params: ["[-l] [-s pkg] [-L pkg] [-i pkg] [--remove pkg]"],
10
+ run: ({ args, authUser, shell }) => {
11
+ const pm = getPackageManager(shell);
12
+ if (!pm) return { stderr: "dpkg: package manager not initialised", exitCode: 1 };
13
+
14
+ const listFlag = ifFlag(args, ["-l", "--list"]);
15
+ const statusFlag = ifFlag(args, ["-s", "--status"]);
16
+ const listFilesFlag = ifFlag(args, ["-L", "--listfiles"]);
17
+ const removeFlag = ifFlag(args, ["-r", "--remove"]);
18
+ const purgeFlag = ifFlag(args, ["-P", "--purge"]);
19
+
20
+ const { positionals } = parseArgs(args, {
21
+ flags: ["-l", "--list", "-s", "--status", "-L", "--listfiles", "-r", "--remove", "-P", "--purge"],
22
+ });
23
+
24
+ if (listFlag) {
25
+ const pkgList = pm.listInstalled();
26
+ if (pkgList.length === 0) {
27
+ return {
28
+ stdout: [
29
+ "Desired=Unknown/Install/Remove/Purge/Hold",
30
+ "|Status=Not/Inst/Conf-files/Unpacked/halF-conf/Half-inst/trig-aWait/Trig-pend",
31
+ "|/ Err?=(none)/Reinst-required (Status,Err: uppercase=bad)",
32
+ "||/ Name Version Architecture Description",
33
+ "+++-==============-===============-============-========================================",
34
+ "(no packages installed)",
35
+ ].join("\n"),
36
+ exitCode: 0,
37
+ };
38
+ }
39
+
40
+ const header = [
41
+ "Desired=Unknown/Install/Remove/Purge/Hold",
42
+ "|Status=Not/Inst/Conf-files/Unpacked/halF-conf/Half-inst/trig-aWait/Trig-pend",
43
+ "|/ Err?=(none)/Reinst-required (Status,Err: uppercase=bad)",
44
+ "||/ Name Version Architecture Description",
45
+ "+++-==============-===============-============-========================================",
46
+ ];
47
+
48
+ const rows = pkgList.map((p) => {
49
+ const name = p.name.padEnd(14).slice(0, 14);
50
+ const ver = p.version.padEnd(15).slice(0, 15);
51
+ const arch = p.architecture.padEnd(12).slice(0, 12);
52
+ const desc = (p.description || "").slice(0, 40);
53
+ return `ii ${name} ${ver} ${arch} ${desc}`;
54
+ });
55
+
56
+ return { stdout: [...header, ...rows].join("\n"), exitCode: 0 };
57
+ }
58
+
59
+ if (statusFlag) {
60
+ const pkgName = positionals[0];
61
+ if (!pkgName) return { stderr: "dpkg: -s needs a package name", exitCode: 1 };
62
+ const info = pm.show(pkgName);
63
+ if (!info)
64
+ return {
65
+ stderr: `dpkg-query: package '${pkgName}' is not installed and no information is available`,
66
+ exitCode: 1,
67
+ };
68
+ return { stdout: info, exitCode: 0 };
69
+ }
70
+
71
+ if (listFilesFlag) {
72
+ const pkgName = positionals[0];
73
+ if (!pkgName) return { stderr: "dpkg: -L needs a package name", exitCode: 1 };
74
+ const installed = pm.listInstalled().find((p) => p.name === pkgName);
75
+ if (!installed)
76
+ return {
77
+ stderr: `dpkg-query: package '${pkgName}' is not installed`,
78
+ exitCode: 1,
79
+ };
80
+ if (installed.files.length === 0)
81
+ return { stdout: "/.keep", exitCode: 0 };
82
+ return { stdout: installed.files.join("\n"), exitCode: 0 };
83
+ }
84
+
85
+ if (removeFlag || purgeFlag) {
86
+ if (authUser !== "root")
87
+ return {
88
+ stderr: "dpkg: error: requested operation requires superuser privilege",
89
+ exitCode: 2,
90
+ };
91
+ if (positionals.length === 0)
92
+ return { stderr: "dpkg: error: need an action option", exitCode: 2 };
93
+ const { output, exitCode } = pm.remove(positionals, { purge: purgeFlag });
94
+ return { stdout: output || undefined, exitCode };
95
+ }
96
+
97
+ // Default: show help
98
+ return {
99
+ stdout: [
100
+ "Usage: dpkg [<option>...] <command>",
101
+ "",
102
+ "Commands:",
103
+ " -l, --list List packages matching given pattern",
104
+ " -s, --status <pkg>... Report status of specified package",
105
+ " -L, --listfiles <pkg>... List files owned by package",
106
+ " -r, --remove <pkg>... Remove <pkg> but leave its configuration",
107
+ " -P, --purge <pkg>... Remove <pkg> and its configuration",
108
+ ].join("\n"),
109
+ exitCode: 0,
110
+ };
111
+ },
112
+ };
113
+
114
+ export const dpkgQueryCommand: ShellModule = {
115
+ name: "dpkg-query",
116
+ description: "Show information about installed packages",
117
+ category: "system",
118
+ params: ["-W [pkg] | -l [pattern]"],
119
+ run: ({ args, shell }) => {
120
+ const pm = getPackageManager(shell);
121
+ if (!pm) return { stderr: "dpkg-query: package manager not initialised", exitCode: 1 };
122
+
123
+ const listFlag = ifFlag(args, ["-l"]);
124
+ const showFlag = ifFlag(args, ["-W", "--show"]);
125
+ const { positionals } = parseArgs(args, {
126
+ flags: ["-l", "-W", "--show"],
127
+ });
128
+
129
+ if (listFlag || showFlag) {
130
+ const pkgList = pm.listInstalled();
131
+ const pattern = positionals[0];
132
+ const filtered = pattern
133
+ ? pkgList.filter((p) => p.name.includes(pattern))
134
+ : pkgList;
135
+
136
+ if (showFlag) {
137
+ return {
138
+ stdout: filtered
139
+ .map((p) => `${p.name}\t${p.version}`)
140
+ .join("\n"),
141
+ exitCode: 0,
142
+ };
143
+ }
144
+
145
+ const rows = filtered.map((p) => {
146
+ const name = p.name.padEnd(14).slice(0, 14);
147
+ const ver = p.version.padEnd(15).slice(0, 15);
148
+ return `ii ${name} ${ver} amd64 ${(p.description || "").slice(0, 40)}`;
149
+ });
150
+ return {
151
+ stdout: rows.join("\n") || "(no packages match)",
152
+ exitCode: 0,
153
+ };
154
+ }
155
+
156
+ return { stderr: "dpkg-query: need a flag (-l, -W)", exitCode: 1 };
157
+ },
158
+ };
@@ -1,28 +1,44 @@
1
1
  import type { ShellModule } from "../types/commands";
2
2
  import { parseArgs } from "./command-helpers";
3
- import { getAllEnvVars } from "./set";
4
3
 
5
- function expandEnvVars(input: string, env: Record<string, string>): string {
6
- return input.replace(/\$([A-Za-z_][A-Za-z0-9_]*)/g, (_, name: string) => {
7
- return env[name] ?? "";
8
- });
4
+ /**
5
+ * Expand escape sequences for `echo -e`.
6
+ * Handles \n \t \r \\ \a \b \f \v and \0NNN (octal).
7
+ */
8
+ function expandEscapes(text: string): string {
9
+ return text
10
+ .replace(/\\n/g, "\n")
11
+ .replace(/\\t/g, "\t")
12
+ .replace(/\\r/g, "\r")
13
+ .replace(/\\\\/g, "\\")
14
+ .replace(/\\a/g, "\x07")
15
+ .replace(/\\b/g, "\x08")
16
+ .replace(/\\f/g, "\x0C")
17
+ .replace(/\\v/g, "\x0B")
18
+ .replace(/\\0(\d{1,3})/g, (_, oct) => String.fromCharCode(parseInt(oct, 8)));
9
19
  }
10
20
 
11
21
  export const echoCommand: ShellModule = {
12
22
  name: "echo",
13
23
  description: "Display text",
14
24
  category: "shell",
15
- params: ["[options] [text...]"],
16
- run: ({ args, authUser, stdin }) => {
17
- const { flags, positionals } = parseArgs(args, { flags: ["-n"] });
18
- const newline = !flags.has("-n");
19
- const rawText =
20
- positionals.length > 0 ? positionals.join(" ") : (stdin ?? "");
21
- const env = getAllEnvVars(authUser);
22
- const text = expandEnvVars(rawText, env);
25
+ params: ["[-n] [-e] [text...]"],
26
+ run: ({ args, stdin, env }) => {
27
+ const { flags, positionals } = parseArgs(args, { flags: ["-n", "-e", "-E"] });
28
+ const noNewline = flags.has("-n");
29
+ const escapes = flags.has("-e");
30
+
31
+ const rawText = positionals.length > 0 ? positionals.join(" ") : (stdin ?? "");
32
+
33
+ // Expand $VAR references using the session env (not the legacy global store)
34
+ const varsExpanded = rawText.replace(/\$([A-Za-z_][A-Za-z0-9_]*)/g, (_, name: string) =>
35
+ env?.vars[name] ?? "",
36
+ );
37
+
38
+ const text = escapes ? expandEscapes(varsExpanded) : varsExpanded;
23
39
 
24
40
  return {
25
- stdout: newline ? text : text.trimEnd(),
41
+ stdout: noNewline ? text : `${text}\n`,
26
42
  exitCode: 0,
27
43
  };
28
44
  },
@@ -0,0 +1,40 @@
1
+ import * as os from "node:os";
2
+ import type { ShellModule } from "../types/commands";
3
+ import { ifFlag } from "./command-helpers";
4
+
5
+ export const freeCommand: ShellModule = {
6
+ name: "free",
7
+ description: "Display amount of free and used memory",
8
+ category: "system",
9
+ params: ["[-h] [-m] [-g]"],
10
+ run: ({ args }) => {
11
+ const human = ifFlag(args, ["-h", "--human"]);
12
+ const mb = ifFlag(args, ["-m"]);
13
+ const gb = ifFlag(args, ["-g"]);
14
+
15
+ const osTotalB = os.totalmem();
16
+ const osFreeB = os.freemem();
17
+ const usedB = osTotalB - osFreeB;
18
+ const sharedB = Math.floor(osTotalB * 0.02);
19
+ const buffersB = Math.floor(osTotalB * 0.05);
20
+ const availableB = Math.floor(osFreeB * 0.95);
21
+ const swapB = Math.floor(osTotalB * 0.5);
22
+
23
+ const fmt = (bytes: number): string => {
24
+ if (human) {
25
+ if (bytes >= 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)}G`;
26
+ if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)}M`;
27
+ return `${(bytes / 1024).toFixed(1)}K`;
28
+ }
29
+ if (gb) return String(Math.floor(bytes / (1024 * 1024 * 1024)));
30
+ if (mb) return String(Math.floor(bytes / (1024 * 1024)));
31
+ return String(Math.floor(bytes / 1024));
32
+ };
33
+
34
+ const header = ` total used free shared buff/cache available`;
35
+ const memRow = `Mem: ${fmt(osTotalB).padStart(12)} ${fmt(usedB).padStart(11)} ${fmt(osFreeB).padStart(11)} ${fmt(sharedB).padStart(11)} ${fmt(buffersB).padStart(11)} ${fmt(availableB).padStart(11)}`;
36
+ const swapRow = `Swap: ${fmt(swapB).padStart(12)} ${fmt(0).padStart(11)} ${fmt(swapB).padStart(11)}`;
37
+
38
+ return { stdout: [header, memRow, swapRow].join("\n"), exitCode: 0 };
39
+ },
40
+ };
@@ -1,6 +1,8 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import * as path from "node:path";
3
3
  import type VirtualFileSystem from "../VirtualFileSystem";
4
+ import type { VirtualPackageManager } from "../VirtualPackageManager";
5
+ import type { VirtualShell } from "../VirtualShell";
4
6
 
5
7
  const PROTECTED_PREFIXES = ["/virtual-env-js/.auth"] as const;
6
8
 
@@ -220,3 +222,9 @@ export function joinListWithType(
220
222
  })
221
223
  .join(" ");
222
224
  }
225
+
226
+ export function getPackageManager(
227
+ shell: VirtualShell,
228
+ ): VirtualPackageManager | undefined {
229
+ return shell.packageManager;
230
+ }
@@ -0,0 +1,29 @@
1
+ import type { ShellModule } from "../types/commands";
2
+
3
+ export const historyCommand: ShellModule = {
4
+ name: "history",
5
+ description: "Display command history",
6
+ category: "shell",
7
+ params: ["[n]"],
8
+ run: ({ args, shell }) => {
9
+ // History is persisted in the VFS by the interactive shell
10
+ const histPath = "/virtual-env-js/.bash_history";
11
+ if (!shell.vfs.exists(histPath)) {
12
+ return { stdout: "", exitCode: 0 };
13
+ }
14
+
15
+ const raw = shell.vfs.readFile(histPath);
16
+ const lines = raw.split("\n").filter(Boolean);
17
+
18
+ const nArg = args[0];
19
+ const n = nArg ? parseInt(nArg, 10) : null;
20
+ const slice = n && !Number.isNaN(n) ? lines.slice(-n) : lines;
21
+
22
+ const offset = lines.length - slice.length + 1;
23
+ const numbered = slice.map((line, i) =>
24
+ `${String(offset + i).padStart(5)} ${line}`
25
+ );
26
+
27
+ return { stdout: numbered.join("\n"), exitCode: 0 };
28
+ },
29
+ };