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,5 +1,7 @@
1
1
  /** biome-ignore-all lint/style/useNamingConvention: ENV VARIABLES */
2
+ import { executeStatements } from "../SSHMimic/executor";
2
3
  import type { VirtualShell } from "../VirtualShell";
4
+ import { parseScript } from "../VirtualShell/shellParser";
3
5
  import type {
4
6
  CommandContext,
5
7
  CommandMode,
@@ -8,6 +10,11 @@ import type {
8
10
  ShellModule,
9
11
  } from "../types/commands";
10
12
  import { adduserCommand } from "./adduser";
13
+ import { aliasCommand, unaliasCommand } from "./alias";
14
+ import { testCommand } from "./test";
15
+ import { sourceCommand } from "./source";
16
+ import { historyCommand } from "./history";
17
+ import { aptCacheCommand, aptCommand } from "./apt";
11
18
  import { awkCommand } from "./awk";
12
19
  import { base64Command } from "./base64";
13
20
  import { catCommand } from "./cat";
@@ -21,12 +28,14 @@ import { dateCommand } from "./date";
21
28
  import { deluserCommand } from "./deluser";
22
29
  import { dfCommand } from "./df";
23
30
  import { diffCommand } from "./diff";
31
+ import { dpkgCommand, dpkgQueryCommand } from "./dpkg";
24
32
  import { duCommand } from "./du";
25
33
  import { echoCommand } from "./echo";
26
34
  import { envCommand } from "./env";
27
35
  import { exitCommand } from "./exit";
28
36
  import { exportCommand } from "./export";
29
37
  import { findCommand } from "./find";
38
+ import { freeCommand } from "./free";
30
39
  import { grepCommand } from "./grep";
31
40
  import { groupsCommand } from "./groups";
32
41
  import { gunzipCommand, gzipCommand } from "./gzip";
@@ -38,6 +47,8 @@ import { idCommand } from "./id";
38
47
  import { killCommand } from "./kill";
39
48
  import { lnCommand } from "./ln";
40
49
  import { lsCommand } from "./ls";
50
+ import { lsbReleaseCommand } from "./lsb-release";
51
+ import { manCommand } from "./man";
41
52
  import { mkdirCommand } from "./mkdir";
42
53
  import { mvCommand } from "./mv";
43
54
  import { nanoCommand } from "./nano";
@@ -60,11 +71,14 @@ import { teeCommand } from "./tee";
60
71
  import { touchCommand } from "./touch";
61
72
  import { trCommand } from "./tr";
62
73
  import { treeCommand } from "./tree";
74
+ import { typeCommand } from "./type";
63
75
  import { unameCommand } from "./uname";
64
76
  import { uniqCommand } from "./uniq";
65
77
  import { unsetCommand } from "./unset";
78
+ import { uptimeCommand } from "./uptime";
66
79
  import { wcCommand } from "./wc";
67
80
  import { wgetCommand } from "./wget";
81
+ import { whichCommand } from "./which";
68
82
  import { whoCommand } from "./who";
69
83
  import { whoamiCommand } from "./whoami";
70
84
  import { xargsCommand } from "./xargs";
@@ -95,6 +109,13 @@ const BASE_COMMANDS: ShellModule[] = [
95
109
  adduserCommand, passwdCommand, deluserCommand, sudoCommand, suCommand,
96
110
  // Misc
97
111
  neofetchCommand,
112
+ // Package management
113
+ aptCommand, aptCacheCommand, dpkgCommand, dpkgQueryCommand,
114
+ // Shell (extended)
115
+ whichCommand, typeCommand, manCommand, aliasCommand, unaliasCommand,
116
+ testCommand, sourceCommand, historyCommand,
117
+ // System (extended)
118
+ uptimeCommand, freeCommand, lsbReleaseCommand,
98
119
  ];
99
120
 
100
121
  const customCommands: ShellModule[] = [];
@@ -196,6 +217,52 @@ export function makeDefaultEnv(authUser: string, hostname: string): ShellEnv {
196
217
  };
197
218
  }
198
219
 
220
+ /**
221
+ * Execute a pre-parsed command directly by name and argument list.
222
+ *
223
+ * Unlike `runCommand`, this function does NOT re-join name+args into a string
224
+ * and re-parse — so arguments that contain special characters (`;`, `|`, `>`,
225
+ * quotes) are passed through verbatim. Use this from the pipeline executor.
226
+ */
227
+ export async function runCommandDirect(
228
+ name: string,
229
+ args: string[],
230
+ authUser: string,
231
+ hostname: string,
232
+ mode: CommandMode,
233
+ cwd: string,
234
+ shell: VirtualShell,
235
+ stdin: string | undefined,
236
+ env: ShellEnv,
237
+ ): Promise<CommandResult> {
238
+ // Alias expansion on the command name
239
+ const aliasVal = env.vars[`__alias_${name}`];
240
+ if (aliasVal) {
241
+ // Alias may expand to a multi-word command — re-route through runCommand
242
+ return runCommand(`${aliasVal} ${args.join(" ")}`, authUser, hostname, mode, cwd, shell, stdin, env);
243
+ }
244
+
245
+ const mod = resolveModule(name);
246
+ if (!mod) return { stderr: `${name}: command not found`, exitCode: 127 };
247
+
248
+ try {
249
+ return await mod.run({
250
+ authUser,
251
+ hostname,
252
+ activeSessions: shell.users.listActiveSessions(),
253
+ rawInput: [name, ...args].join(" "),
254
+ mode,
255
+ args,
256
+ stdin,
257
+ cwd,
258
+ shell,
259
+ env,
260
+ });
261
+ } catch (error: unknown) {
262
+ return { stderr: error instanceof Error ? error.message : "Command failed", exitCode: 1 };
263
+ }
264
+ }
265
+
199
266
  export async function runCommand(
200
267
  rawInput: string,
201
268
  authUser: string,
@@ -211,18 +278,56 @@ export async function runCommand(
211
278
 
212
279
  const shellEnv: ShellEnv = env ?? makeDefaultEnv(authUser, hostname);
213
280
 
281
+ // ── $(cmd) command substitution ──────────────────────────────────────────
282
+ let expanded = trimmed;
283
+ if (expanded.includes("$(")) {
284
+ // Only substitute $(…) that are NOT inside single quotes
285
+ // Strategy: walk char by char, track single-quote state
286
+ let result = "";
287
+ let inSingle = false;
288
+ let i = 0;
289
+ while (i < expanded.length) {
290
+ const ch = expanded[i]!;
291
+ if (ch === "'" && !inSingle) { inSingle = true; result += ch; i++; continue; }
292
+ if (ch === "'" && inSingle) { inSingle = false; result += ch; i++; continue; }
293
+ if (!inSingle && ch === "$" && expanded[i + 1] === "(") {
294
+ // Find matching closing )
295
+ let depth = 0;
296
+ let j = i + 1;
297
+ while (j < expanded.length) {
298
+ if (expanded[j] === "(") depth++;
299
+ else if (expanded[j] === ")") { depth--; if (depth === 0) break; }
300
+ j++;
301
+ }
302
+ const sub = expanded.slice(i + 2, j).trim();
303
+ const subResult = await runCommand(sub, authUser, hostname, mode, cwd, shell, undefined, shellEnv);
304
+ const subOut = (subResult.stdout ?? "").replace(/\n$/, "");
305
+ result += subOut;
306
+ i = j + 1;
307
+ continue;
308
+ }
309
+ result += ch; i++;
310
+ }
311
+ expanded = result;
312
+ }
313
+
314
+ // ── alias expansion ───────────────────────────────────────────────────────
315
+ const firstWord = expanded.split(/\s+/)[0] ?? "";
316
+ const aliasVal = shellEnv.vars[`__alias_${firstWord}`];
317
+ if (aliasVal) {
318
+ expanded = expanded.replace(firstWord, aliasVal);
319
+ }
320
+
214
321
  // Detect shell operators
215
322
  if (
216
- /(?<![|&])[|](?![|])/.test(trimmed) ||
217
- trimmed.includes(">") ||
218
- trimmed.includes("<") ||
219
- trimmed.includes("&&") ||
220
- trimmed.includes("||") ||
221
- trimmed.includes(";")
323
+ /(?<![|&])[|](?![|])/.test(expanded) ||
324
+ expanded.includes(">") ||
325
+ expanded.includes("<") ||
326
+ expanded.includes("&&") ||
327
+ expanded.includes("||") ||
328
+ expanded.includes(";")
222
329
  ) {
223
- const { parseScript } = await import("../VirtualShell/shellParser");
224
- const { executeStatements } = await import("../SSHMimic/executor");
225
- const script = parseScript(trimmed);
330
+ const script = parseScript(expanded);
226
331
  if (!script.isValid) return { stderr: script.error || "Syntax error", exitCode: 1 };
227
332
  try {
228
333
  return await executeStatements(script.statements, authUser, hostname, mode, cwd, shell, shellEnv);
@@ -231,7 +336,7 @@ export async function runCommand(
231
336
  }
232
337
  }
233
338
 
234
- const { commandName, args } = parseInput(trimmed);
339
+ const { commandName, args } = parseInput(expanded);
235
340
  const mod = resolveModule(commandName);
236
341
 
237
342
  if (!mod) return { stderr: `${commandName}: command not found`, exitCode: 127 };
@@ -241,7 +346,7 @@ export async function runCommand(
241
346
  authUser,
242
347
  hostname,
243
348
  activeSessions: shell.users.listActiveSessions(),
244
- rawInput: trimmed,
349
+ rawInput: expanded,
245
350
  mode,
246
351
  args,
247
352
  stdin,
@@ -30,15 +30,16 @@ export const lsCommand: ShellModule = {
30
30
  name: "ls",
31
31
  description: "List directory contents",
32
32
  category: "navigation",
33
- params: ["[path]"],
33
+ params: ["[-la] [path]"],
34
34
  run: ({ authUser, shell, cwd, args }) => {
35
- const longFormat = ifFlag(args, ["-l", "--long"]);
36
- const targetArg = getArg(args, 0, { flags: ["-l", "--long"] });
35
+ const longFormat = ifFlag(args, ["-l", "--long"]);
36
+ const showHidden = ifFlag(args, ["-a", "--all"]);
37
+ const targetArg = getArg(args, 0, { flags: ["-l", "--long", "-a", "--all", "-la", "-al"] });
37
38
  const target = resolvePath(cwd, targetArg ?? cwd);
38
39
  assertPathAccess(authUser, target, "ls");
39
40
  const items = shell.vfs
40
41
  .list(target)
41
- .filter((name) => !name.startsWith("."));
42
+ .filter((name) => showHidden || !name.startsWith("."));
42
43
  const rendered = longFormat
43
44
  ? items
44
45
  .map((name) => {
@@ -0,0 +1,52 @@
1
+ import type { ShellModule } from "../types/commands";
2
+ import { ifFlag } from "./command-helpers";
3
+
4
+ export const lsbReleaseCommand: ShellModule = {
5
+ name: "lsb_release",
6
+ description: "Print distribution-specific information",
7
+ category: "system",
8
+ params: ["[-a] [-i] [-d] [-r] [-c]"],
9
+ run: ({ args, shell }) => {
10
+ let osName = shell.properties?.os ?? "Fortune GNU/Linux x64";
11
+ let codename = "aurora";
12
+ let version = "1.0";
13
+
14
+ try {
15
+ const content = shell.vfs.readFile("/etc/os-release");
16
+ for (const line of content.split("\n")) {
17
+ if (line.startsWith("PRETTY_NAME="))
18
+ osName = line.slice("PRETTY_NAME=".length).replace(/^"|"$/g, "").trim();
19
+ if (line.startsWith("VERSION_CODENAME="))
20
+ codename = line.slice("VERSION_CODENAME=".length).trim();
21
+ if (line.startsWith("VERSION_ID="))
22
+ version = line.slice("VERSION_ID=".length).replace(/^"|"$/g, "").trim();
23
+ }
24
+ } catch {}
25
+
26
+ const all = ifFlag(args, ["-a", "--all"]);
27
+ const showId = ifFlag(args, ["-i", "--id"]);
28
+ const showDesc = ifFlag(args, ["-d", "--description"]);
29
+ const showRelease = ifFlag(args, ["-r", "--release"]);
30
+ const showCodename = ifFlag(args, ["-c", "--codename"]);
31
+
32
+ if (all || args.length === 0) {
33
+ return {
34
+ stdout: [
35
+ `Distributor ID:\tFortune`,
36
+ `Description:\t${osName}`,
37
+ `Release:\t${version}`,
38
+ `Codename:\t${codename}`,
39
+ ].join("\n"),
40
+ exitCode: 0,
41
+ };
42
+ }
43
+
44
+ const lines: string[] = [];
45
+ if (showId) lines.push(`Distributor ID:\tFortune`);
46
+ if (showDesc) lines.push(`Description:\t${osName}`);
47
+ if (showRelease) lines.push(`Release:\t${version}`);
48
+ if (showCodename) lines.push(`Codename:\t${codename}`);
49
+
50
+ return { stdout: lines.join("\n"), exitCode: 0 };
51
+ },
52
+ };
@@ -0,0 +1,166 @@
1
+ import type { ShellModule } from "../types/commands";
2
+
3
+ const MAN_PAGES: Record<string, string> = {
4
+ ls: `LS(1) User Commands LS(1)
5
+
6
+ NAME
7
+ ls - list directory contents
8
+
9
+ SYNOPSIS
10
+ ls [OPTION]... [FILE]...
11
+
12
+ DESCRIPTION
13
+ List information about the FILEs (the current directory by default).
14
+
15
+ OPTIONS
16
+ -l use a long listing format
17
+ -a do not ignore entries starting with .
18
+ -h with -l, print human readable sizes
19
+ -r reverse order while sorting
20
+ -t sort by modification time
21
+
22
+ AUTHOR
23
+ Written by Richard M. Stallman and David MacKenzie.`,
24
+
25
+ cat: `CAT(1) User Commands CAT(1)
26
+
27
+ NAME
28
+ cat - concatenate files and print on the standard output
29
+
30
+ SYNOPSIS
31
+ cat [OPTION]... [FILE]...
32
+
33
+ DESCRIPTION
34
+ Concatenate FILE(s) to standard output.
35
+
36
+ OPTIONS
37
+ -n, --number number all output lines
38
+ -b, --number-nonblank number nonempty output lines`,
39
+
40
+ grep: `GREP(1) User Commands GREP(1)
41
+
42
+ NAME
43
+ grep, egrep, fgrep - print lines that match patterns
44
+
45
+ SYNOPSIS
46
+ grep [OPTION]... PATTERNS [FILE]...
47
+
48
+ OPTIONS
49
+ -i, --ignore-case ignore case distinctions in patterns and data
50
+ -v, --invert-match select non-matching lines
51
+ -n, --line-number print line number with output lines
52
+ -r, --recursive read all files under each directory, recursively`,
53
+
54
+ apt: `APT(8) APT APT(8)
55
+
56
+ NAME
57
+ apt - command-line interface
58
+
59
+ SYNOPSIS
60
+ apt [options] command
61
+
62
+ DESCRIPTION
63
+ apt provides a high-level commandline interface for the package
64
+ management system.
65
+
66
+ COMMANDS
67
+ install pkg... Install packages
68
+ remove pkg... Remove packages
69
+ update Download package information
70
+ upgrade Upgrade installed packages
71
+ search term Search in package descriptions
72
+ show pkg Show package information
73
+ list List packages`,
74
+
75
+ ssh: `SSH(1) OpenSSH SSH(1)
76
+
77
+ NAME
78
+ ssh - OpenSSH remote login client
79
+
80
+ SYNOPSIS
81
+ ssh [-p port] [user@]hostname [command]
82
+
83
+ DESCRIPTION
84
+ ssh (SSH client) is a program for logging into a remote machine and
85
+ for executing commands on a remote machine.`,
86
+
87
+ curl: `CURL(1) User Commands CURL(1)
88
+
89
+ NAME
90
+ curl - transfer a URL
91
+
92
+ SYNOPSIS
93
+ curl [options / URLs]
94
+
95
+ DESCRIPTION
96
+ curl is a tool for transferring data with URL syntax.
97
+
98
+ OPTIONS
99
+ -o, --output <file> Write output to <file>
100
+ -X, --request <method> Specify request method
101
+ -d, --data <data> HTTP POST data
102
+ -H, --header <header> Pass custom header
103
+ -s, --silent Silent mode
104
+ -I, --head Show document info only
105
+ -L, --location Follow redirects
106
+ -v, --verbose Make the operation more talkative`,
107
+
108
+ chmod: `CHMOD(1) User Commands CHMOD(1)
109
+
110
+ NAME
111
+ chmod - change file mode bits
112
+
113
+ SYNOPSIS
114
+ chmod [OPTION]... MODE[,MODE]... FILE...
115
+ chmod [OPTION]... OCTAL-MODE FILE...
116
+
117
+ DESCRIPTION
118
+ Change the file mode bits of each given file according to MODE.
119
+
120
+ EXAMPLES
121
+ chmod 755 script.sh rwxr-xr-x
122
+ chmod 644 file.txt rw-r--r--
123
+ chmod +x script.sh add execute permission`,
124
+
125
+ tar: `TAR(1) GNU tar Manual TAR(1)
126
+
127
+ NAME
128
+ tar - an archiving utility
129
+
130
+ SYNOPSIS
131
+ tar [OPTION...] [FILE]...
132
+
133
+ DESCRIPTION
134
+ tar saves many files together into a single tape or disk archive,
135
+ and can restore individual files from the archive.
136
+
137
+ OPTIONS
138
+ -c, --create create a new archive
139
+ -x, --extract extract files from an archive
140
+ -z, --gzip filter the archive through gzip
141
+ -f, --file=ARCHIVE use archive file or device ARCHIVE
142
+ -v, --verbose verbosely list files processed
143
+ -t, --list list the contents of an archive`,
144
+ };
145
+
146
+ export const manCommand: ShellModule = {
147
+ name: "man",
148
+ description: "Interface to the system reference manuals",
149
+ category: "shell",
150
+ params: ["<command>"],
151
+ run: ({ args, shell }) => {
152
+ const name = args[0];
153
+ if (!name) return { stderr: "What manual page do you want?", exitCode: 1 };
154
+
155
+ // VFS-installed man pages take priority
156
+ const manPath = `/usr/share/man/man1/${name}.1`;
157
+ if (shell.vfs.exists(manPath)) {
158
+ return { stdout: shell.vfs.readFile(manPath), exitCode: 0 };
159
+ }
160
+
161
+ const page = MAN_PAGES[name.toLowerCase()];
162
+ if (page) return { stdout: page, exitCode: 0 };
163
+
164
+ return { stderr: `No manual entry for ${name}`, exitCode: 16 };
165
+ },
166
+ };
@@ -32,6 +32,11 @@ export const neofetchCommand: ShellModule = {
32
32
  shell: env.SHELL,
33
33
  shellProps: shell.properties,
34
34
  terminal: env.TERM,
35
+ uptimeSeconds: Math.floor((Date.now() - shell.startTime) / 1000),
36
+ packages: (() => {
37
+ const count = shell.packageManager?.installedCount() ?? 0;
38
+ return `${count} (dpkg)`;
39
+ })(),
35
40
  }),
36
41
  exitCode: 0,
37
42
  };
@@ -1,4 +1,5 @@
1
1
  import type { ShellModule } from "../types/commands";
2
+ import { parseArgs } from "./command-helpers";
2
3
 
3
4
  export const pingCommand: ShellModule = {
4
5
  name: "ping",
@@ -6,8 +7,10 @@ export const pingCommand: ShellModule = {
6
7
  category: "network",
7
8
  params: ["[-c <count>] <host>"],
8
9
  run: ({ args }) => {
9
- const host = args.find((a) => !a.startsWith("-")) ?? "localhost";
10
- const count = 4;
10
+ const { flagsWithValues, positionals } = parseArgs(args, { flagsWithValue: ["-c", "-i", "-W"] });
11
+ const host = positionals[0] ?? "localhost";
12
+ const countArg = flagsWithValues.get("-c");
13
+ const count = countArg ? Math.max(1, parseInt(countArg, 10) || 4) : 4;
11
14
  const lines = [`PING ${host}: 56 data bytes`];
12
15
  for (let i = 0; i < count; i++) {
13
16
  const ms = (Math.random() * 10 + 1).toFixed(3);
@@ -1,19 +1,41 @@
1
1
  import type { ShellModule } from "../types/commands";
2
+ import { ifFlag } from "./command-helpers";
2
3
 
3
4
  export const psCommand: ShellModule = {
4
5
  name: "ps",
5
6
  description: "Report process status",
6
7
  category: "system",
7
- params: ["[-a] [-u] [-x]"],
8
- run: ({ authUser, shell }) => {
8
+ params: ["[-a] [-u] [-x] [aux]"],
9
+ run: ({ authUser, shell, args }) => {
9
10
  const sessions = shell.users.listActiveSessions();
10
- const lines = [" PID TTY TIME CMD"];
11
+ const showUser = ifFlag(args, ["-u"]) || args.includes("u") || args.includes("aux") || args.includes("au");
12
+ const showAll = ifFlag(args, ["-a", "-x"]) || args.includes("a") || args.includes("aux");
13
+
14
+ if (showUser) {
15
+ const header = "USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND";
16
+ const rows: string[] = [header];
17
+ let pid = 1000;
18
+ for (const s of sessions) {
19
+ const user = s.username.padEnd(10).slice(0, 10);
20
+ const mem = (Math.random() * 0.5).toFixed(1);
21
+ const vsz = Math.floor(Math.random() * 20000 + 5000);
22
+ const rss = Math.floor(Math.random() * 5000 + 1000);
23
+ rows.push(`${user} ${String(pid).padStart(6)} 0.0 ${mem.padStart(4)} ${String(vsz).padStart(6)} ${String(rss).padStart(5)} ${s.tty.padEnd(8)} Ss 00:00 0:00 bash`);
24
+ pid++;
25
+ }
26
+ rows.push(`root ${String(pid).padStart(6)} 0.0 0.0 0 0 ? S 00:00 0:00 ps`);
27
+ return { stdout: rows.join("\n"), exitCode: 0 };
28
+ }
29
+
30
+ const header = " PID TTY TIME CMD";
31
+ const rows: string[] = [header];
11
32
  let pid = 1000;
12
33
  for (const s of sessions) {
13
- lines.push(`${String(pid).padStart(5)} ${s.tty.padEnd(12)} 00:00:00 ${s.username === authUser ? "bash" : `bash (${s.username})`}`);
34
+ if (!showAll && s.username !== authUser) continue;
35
+ rows.push(`${String(pid).padStart(5)} ${s.tty.padEnd(12)} 00:00:00 ${s.username === authUser ? "bash" : `bash (${s.username})`}`);
14
36
  pid++;
15
37
  }
16
- lines.push(`${String(pid).padStart(5)} pts/0 00:00:00 ps`);
17
- return { stdout: lines.join("\n"), exitCode: 0 };
38
+ rows.push(`${String(pid).padStart(5)} pts/0 00:00:00 ps`);
39
+ return { stdout: rows.join("\n"), exitCode: 0 };
18
40
  },
19
41
  };
@@ -1,10 +1,13 @@
1
1
  import type { CommandContext, CommandResult, ShellModule } from "../types/commands";
2
- import { getArg, ifFlag } from "./command-helpers";
2
+ import { ifFlag } from "./command-helpers";
3
3
  import { resolvePath } from "./helpers";
4
4
  import { runCommand } from "./index";
5
5
 
6
- /** Expand $VAR and ${VAR:-default} in a line using the current env */
7
- function expandVars(line: string, env: Record<string, string>, lastExit: number): string {
6
+ /** Alias for clarity inside sh.ts */
7
+ type ShellContext = CommandContext;
8
+
9
+ /** Expand $VAR and ${VAR:-default} in a line using the current env (sync, no $(cmd)) */
10
+ function expandVarsSync(line: string, env: Record<string, string>, lastExit: number): string {
8
11
  return line
9
12
  .replace(/\$\?/g, String(lastExit))
10
13
  .replace(/\$\{([^}:]+):-([^}]*)\}/g, (_, n, d) => env[n] ?? d)
@@ -13,6 +16,25 @@ function expandVars(line: string, env: Record<string, string>, lastExit: number)
13
16
  .replace(/^~(\/|$)/, `${env.HOME ?? "/home/user"}$1`);
14
17
  }
15
18
 
19
+ /**
20
+ * Expand $VAR, ${VAR:-default}, and $(cmd) substitution asynchronously.
21
+ * Used before executing each line in sh script context.
22
+ */
23
+ async function expandVars(line: string, env: Record<string, string>, lastExit: number, ctx: ShellContext): Promise<string> {
24
+ // $(cmd) substitution first
25
+ if (line.includes("$(")) {
26
+ const subRe = /\$\(([^)]+)\)/g;
27
+ const matches = [...line.matchAll(subRe)];
28
+ for (const m of matches) {
29
+ const sub = m[1]?.trim() ?? "";
30
+ const subResult = await runCommand(sub, ctx.authUser, ctx.hostname, ctx.mode, ctx.cwd, ctx.shell, undefined, ctx.env);
31
+ const subOut = (subResult.stdout ?? "").replace(/\n$/, "");
32
+ line = line.replace(m[0], subOut);
33
+ }
34
+ }
35
+ return expandVarsSync(line, env, lastExit);
36
+ }
37
+
16
38
  type Block =
17
39
  | { type: "if"; cond: string; then: string[]; elif: Array<{ cond: string; body: string[] }>; else_: string[] }
18
40
  | { type: "for"; var: string; list: string; body: string[] }
@@ -54,8 +76,8 @@ function parseBlocks(lines: string[]): Block[] {
54
76
  const body: string[] = [];
55
77
  i++;
56
78
  while (i < lines.length && lines[i]?.trim() !== "done") {
57
- const l = lines[i]!.trim();
58
- if (l !== "do") body.push(l);
79
+ const l = lines[i]!.trim().replace(/^do\s+/, "");
80
+ if (l && l !== "do") body.push(l);
59
81
  i++;
60
82
  }
61
83
  blocks.push({ type: "for", var: m[1]!, list: m[2]!, body });
@@ -65,8 +87,8 @@ function parseBlocks(lines: string[]): Block[] {
65
87
  const body: string[] = [];
66
88
  i++;
67
89
  while (i < lines.length && lines[i]?.trim() !== "done") {
68
- const l = lines[i]!.trim();
69
- if (l !== "do") body.push(l);
90
+ const l = lines[i]!.trim().replace(/^do\s+/, "");
91
+ if (l && l !== "do") body.push(l);
70
92
  i++;
71
93
  }
72
94
  blocks.push({ type: "while", cond, body });
@@ -79,7 +101,7 @@ function parseBlocks(lines: string[]): Block[] {
79
101
  }
80
102
 
81
103
  async function evalCondition(cond: string, ctx: CommandContext): Promise<boolean> {
82
- const expanded = expandVars(cond, ctx.env.vars, ctx.env.lastExitCode);
104
+ const expanded = await expandVars(cond, ctx.env.vars, ctx.env.lastExitCode, ctx);
83
105
  // test -f / test -d / [ ... ]
84
106
  const testMatch = expanded.match(/^\[?\s*(.+?)\s*\]?$/);
85
107
  if (testMatch) {
@@ -126,7 +148,7 @@ async function runBlocks(blocks: Block[], ctx: CommandContext): Promise<CommandR
126
148
 
127
149
  for (const block of blocks) {
128
150
  if (block.type === "cmd") {
129
- const expanded = expandVars(block.line, ctx.env.vars, ctx.env.lastExitCode);
151
+ const expanded = await expandVars(block.line, ctx.env.vars, ctx.env.lastExitCode, ctx);
130
152
  const r = await runCommand(expanded, ctx.authUser, ctx.hostname, ctx.mode, ctx.cwd, ctx.shell, undefined, ctx.env);
131
153
  ctx.env.lastExitCode = r.exitCode ?? 0;
132
154
  if (r.stdout) output += `${r.stdout}\n`;
@@ -152,7 +174,7 @@ async function runBlocks(blocks: Block[], ctx: CommandContext): Promise<CommandR
152
174
  }
153
175
  }
154
176
  } else if (block.type === "for") {
155
- const listExpanded = expandVars(block.list, ctx.env.vars, ctx.env.lastExitCode);
177
+ const listExpanded = await expandVars(block.list, ctx.env.vars, ctx.env.lastExitCode, ctx);
156
178
  const items = listExpanded.trim().split(/\s+/);
157
179
  for (const item of items) {
158
180
  ctx.env.vars[block.var] = item;
@@ -184,7 +206,7 @@ export const shCommand: ShellModule = {
184
206
 
185
207
  // sh -c "inline script"
186
208
  if (ifFlag(args, "-c")) {
187
- const script = getArg(args, 1) ?? "";
209
+ const script = args[args.indexOf("-c") + 1] ?? "";
188
210
  if (!script) return { stderr: "sh: -c requires a script", exitCode: 1 };
189
211
  const lines = script.split(/[;\n]/).map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
190
212
  const blocks = parseBlocks(lines);
@@ -0,0 +1,35 @@
1
+ import type { ShellModule } from "../types/commands";
2
+ import { resolvePath } from "./helpers";
3
+ import { runCommand } from ".";
4
+
5
+ export const sourceCommand: ShellModule = {
6
+ name: "source",
7
+ aliases: ["."],
8
+ description: "Execute commands from a file in the current shell environment",
9
+ category: "shell",
10
+ params: ["<file> [args...]"],
11
+ run: async ({ args, authUser, hostname, cwd, shell, env }) => {
12
+ const fileArg = args[0];
13
+ if (!fileArg) {
14
+ return { stderr: "source: missing filename", exitCode: 1 };
15
+ }
16
+
17
+ const filePath = resolvePath(cwd, fileArg);
18
+ if (!shell.vfs.exists(filePath)) {
19
+ return { stderr: `source: ${fileArg}: No such file or directory`, exitCode: 1 };
20
+ }
21
+
22
+ const content = shell.vfs.readFile(filePath);
23
+ let lastExitCode = 0;
24
+
25
+ for (const line of content.split("\n")) {
26
+ const l = line.trim();
27
+ if (!l || l.startsWith("#")) continue;
28
+ const result = await runCommand(l, authUser, hostname, "shell", cwd, shell, undefined, env);
29
+ lastExitCode = result.exitCode ?? 0;
30
+ if (result.closeSession || result.switchUser) return result;
31
+ }
32
+
33
+ return { exitCode: lastExitCode };
34
+ },
35
+ };