typescript-virtual-container 1.5.8 → 1.5.10

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.
@@ -1,17 +1,152 @@
1
+ import * as os from "node:os";
2
+ const C = {
3
+ reset: "\x1b[0m",
4
+ bold: "\x1b[1m",
5
+ rev: "\x1b[7m",
6
+ green: "\x1b[32m",
7
+ cyan: "\x1b[36m",
8
+ yellow: "\x1b[33m",
9
+ red: "\x1b[31m",
10
+ blue: "\x1b[34m",
11
+ magenta: "\x1b[35m",
12
+ white: "\x1b[97m",
13
+ bgBlue: "\x1b[44m",
14
+ bgGreen: "\x1b[42m",
15
+ bgRed: "\x1b[41m",
16
+ dim: "\x1b[2m",
17
+ };
18
+ function bar(ratio, width) {
19
+ const filled = Math.round(ratio * width);
20
+ const empty = width - filled;
21
+ const color = ratio > 0.8 ? C.red : ratio > 0.5 ? C.yellow : C.green;
22
+ return `${color}${"█".repeat(filled)}${C.dim}${"░".repeat(empty)}${C.reset}`;
23
+ }
24
+ function fmtBytes(b) {
25
+ if (b >= 1024 ** 3)
26
+ return `${(b / 1024 ** 3).toFixed(1)}G`;
27
+ if (b >= 1024 ** 2)
28
+ return `${(b / 1024 ** 2).toFixed(1)}M`;
29
+ if (b >= 1024)
30
+ return `${(b / 1024).toFixed(1)}K`;
31
+ return `${b}B`;
32
+ }
33
+ function fmtUptime(ms) {
34
+ const s = Math.floor(ms / 1000);
35
+ const d = Math.floor(s / 86400);
36
+ const h = Math.floor((s % 86400) / 3600);
37
+ const m = Math.floor((s % 3600) / 60);
38
+ const sec = s % 60;
39
+ if (d > 0)
40
+ return `${d}d ${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(sec).padStart(2, "0")}`;
41
+ return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(sec).padStart(2, "0")}`;
42
+ }
1
43
  /**
2
- * Interactive system monitor (requires terminal interaction).
44
+ * Interactive system monitor full ANSI output in exec/ssh mode, interactive panel in shell mode.
3
45
  * @category system
4
- * @params []
46
+ * @params ["[-d delay] [-p pid]"]
5
47
  */
6
48
  export const htopCommand = {
7
49
  name: "htop",
8
- description: "System monitor",
50
+ description: "Interactive system monitor",
9
51
  category: "system",
10
- params: [],
11
- run: ({ mode }) => {
12
- if (mode === "exec") {
13
- return { stderr: "htop: interactive terminal required", exitCode: 1 };
52
+ params: ["[-d delay]", "[-p pid]"],
53
+ run: ({ shell, authUser }) => {
54
+ // Render ANSI snapshot in all modes — real htop child_process unavailable in-process
55
+ const totalMem = os.totalmem();
56
+ const freeMem = os.freemem();
57
+ const usedMem = totalMem - freeMem;
58
+ const swapTotal = Math.floor(totalMem * 0.5);
59
+ const swapUsed = Math.floor(swapTotal * 0.02);
60
+ const cpus = os.cpus();
61
+ const cpuCount = cpus.length || 4;
62
+ const uptimeMs = Date.now() - shell.startTime;
63
+ const sessions = shell.users.listActiveSessions();
64
+ const taskCount = sessions.length + shell.users.listProcesses().length + 3; // bash sessions + running cmds + kernel threads
65
+ const now = new Date().toTimeString().slice(0, 8);
66
+ const memRatio = usedMem / totalMem;
67
+ const swapRatio = swapUsed / swapTotal;
68
+ const barWidth = 20;
69
+ const lines = [];
70
+ // ── Header ─────────────────────────────────────────────────────────────
71
+ const cpuLoads = [];
72
+ for (let i = 0; i < cpuCount; i++) {
73
+ cpuLoads.push(Math.random() * 0.3 + 0.02);
74
+ }
75
+ // CPU bars (up to 4 shown)
76
+ const shownCpus = Math.min(cpuCount, 4);
77
+ for (let i = 0; i < shownCpus; i++) {
78
+ const load = cpuLoads[i];
79
+ const pct = (load * 100).toFixed(1).padStart(5);
80
+ lines.push(`${C.bold}${C.cyan}${String(i + 1).padStart(3)}${C.reset}[${bar(load, barWidth)}${C.reset}] ${pct}%`);
81
+ }
82
+ if (cpuCount > 4) {
83
+ lines.push(`${C.dim} ... ${cpuCount - 4} more CPU(s) not shown${C.reset}`);
84
+ }
85
+ lines.push(`${C.bold}${C.cyan}Mem${C.reset}[${bar(memRatio, barWidth)}${C.reset}] ${fmtBytes(usedMem)}/${fmtBytes(totalMem)}`);
86
+ lines.push(`${C.bold}${C.cyan}Swp${C.reset}[${bar(swapRatio, barWidth)}${C.reset}] ${fmtBytes(swapUsed)}/${fmtBytes(swapTotal)}`);
87
+ lines.push("");
88
+ // ── Summary line ───────────────────────────────────────────────────────
89
+ const avgLoad = cpuLoads.slice(0, cpuCount).reduce((a, b) => a + b, 0) / cpuCount;
90
+ const load1 = (avgLoad * cpuCount).toFixed(2);
91
+ const load5 = (avgLoad * cpuCount * 0.9).toFixed(2);
92
+ const load15 = (avgLoad * cpuCount * 0.8).toFixed(2);
93
+ lines.push(`${C.bold}Tasks:${C.reset} ${C.green}${taskCount}${C.reset} total ` +
94
+ `${C.bold}Load average:${C.reset} ${load1} ${load5} ${load15} ` +
95
+ `${C.bold}Uptime:${C.reset} ${fmtUptime(uptimeMs)}`);
96
+ lines.push("");
97
+ // ── Process table header ───────────────────────────────────────────────
98
+ const hdr = `${C.bgBlue}${C.bold}${C.white}` +
99
+ " PID USER PRI NI VIRT RES SHR S CPU% MEM% TIME+ COMMAND" +
100
+ `${C.reset}`;
101
+ lines.push(hdr);
102
+ // Kernel / system processes
103
+ const sysProcs = [
104
+ { pid: 1, user: "root", cmd: "systemd", cpu: 0.0, mem: 0.1 },
105
+ { pid: 2, user: "root", cmd: "kthreadd", cpu: 0.0, mem: 0.0 },
106
+ { pid: 9, user: "root", cmd: "rcu_sched", cpu: (Math.random() * 0.2), mem: 0.0 },
107
+ { pid: 127, user: "root", cmd: "sshd", cpu: 0.0, mem: 0.2 },
108
+ ];
109
+ // Active sessions (bash processes)
110
+ let pid = 1000;
111
+ const sessionProcs = sessions.map((s) => ({
112
+ pid: pid++,
113
+ user: s.username,
114
+ cmd: "bash",
115
+ cpu: Math.random() * 0.5,
116
+ mem: (usedMem / totalMem * 100 / Math.max(sessions.length, 1) * 0.3),
117
+ }));
118
+ // Currently running commands
119
+ const runningProcs = shell.users.listProcesses().map((p) => ({
120
+ pid: p.pid,
121
+ user: p.username,
122
+ cmd: p.argv.join(" ").slice(0, 40),
123
+ cpu: Math.random() * 2.0 + 0.1,
124
+ mem: (usedMem / totalMem * 100 * 0.5),
125
+ }));
126
+ // htop itself
127
+ const htopProc = { pid: pid++, user: authUser, cmd: "htop", cpu: 0.1, mem: 0.1 };
128
+ const procs = [...sysProcs, ...sessionProcs, ...runningProcs, htopProc];
129
+ for (const p of procs) {
130
+ const virt = fmtBytes(Math.floor(Math.random() * 200 * 1024 * 1024 + 10 * 1024 * 1024));
131
+ const res = fmtBytes(Math.floor(Math.random() * 20 * 1024 * 1024 + 1024 * 1024));
132
+ const shr = fmtBytes(Math.floor(Math.random() * 5 * 1024 * 1024 + 512 * 1024));
133
+ const cpuPct = p.cpu.toFixed(1).padStart(5);
134
+ const memPct = p.mem.toFixed(1).padStart(5);
135
+ const time = `${String(Math.floor(Math.random() * 10)).padStart(2)}:${String(Math.floor(Math.random() * 60)).padStart(2, "0")}.${String(Math.floor(Math.random() * 100)).padStart(2, "0")}`;
136
+ const userColor = p.user === "root" ? C.red : p.user === authUser ? C.green : C.cyan;
137
+ const cmdColor = p.cmd === "htop" ? C.green : p.cmd === "bash" ? C.cyan : C.reset;
138
+ lines.push(`${String(p.pid).padStart(5)} ` +
139
+ `${userColor}${p.user.padEnd(10).slice(0, 10)}${C.reset} ` +
140
+ ` 20 0 ` +
141
+ `${virt.padStart(6)} ${res.padStart(6)} ${shr.padStart(5)} ` +
142
+ `S ${cpuPct} ${memPct} ` +
143
+ `${time.padStart(9)} ` +
144
+ `${cmdColor}${p.cmd}${C.reset}`);
14
145
  }
15
- return { openHtop: true, exitCode: 0 };
146
+ lines.push("");
147
+ // ── Footer ─────────────────────────────────────────────────────────────
148
+ lines.push(`${C.dim}${now} — htop snapshot (non-interactive mode) ` +
149
+ `press ${C.reset}${C.bold}q${C.reset}${C.dim} to quit in interactive mode${C.reset}`);
150
+ return { stdout: lines.join("\n"), exitCode: 0 };
16
151
  },
17
152
  };
@@ -187,6 +187,28 @@ OPTIONS
187
187
 
188
188
  EXAMPLES
189
189
  cmatrix`,
190
+ "column": `COLUMN(1) User Commands COLUMN(1)
191
+
192
+ NAME
193
+ column - columnate lists
194
+
195
+ SYNOPSIS
196
+ column [OPTION]... [FILE]...
197
+
198
+ DESCRIPTION
199
+ The column utility formats its input into multiple columns.
200
+ Rows are filled before columns. Input is taken from FILE or stdin.
201
+
202
+ OPTIONS
203
+ -t determine the number of columns the input contains and create
204
+ a table (useful for pretty-printing)
205
+ -s specify a set of characters to be used to delimit columns
206
+ (default: whitespace)
207
+
208
+ EXAMPLES
209
+ mount | column -t
210
+ cat /etc/passwd | column -t -s:
211
+ column -t file.txt`,
190
212
  "cowsay": `COWSAY(1) User Commands COWSAY(1)
191
213
 
192
214
  NAME
@@ -810,6 +832,31 @@ SYNOPSIS
810
832
 
811
833
  OPTIONS
812
834
  -p no error if existing, make parent directories as needed`,
835
+ "mktemp": `MKTEMP(1) User Commands MKTEMP(1)
836
+
837
+ NAME
838
+ mktemp - create a temporary file or directory
839
+
840
+ SYNOPSIS
841
+ mktemp [OPTION]... [TEMPLATE]
842
+
843
+ DESCRIPTION
844
+ Create a temporary file or directory, safely, and print its name.
845
+ TEMPLATE must contain at least 3 consecutive 'X's in last component.
846
+ If TEMPLATE is not specified, use tmp.XXXXXXXXXX.
847
+ Files are created in /tmp.
848
+
849
+ OPTIONS
850
+ -d create a directory, not a file
851
+
852
+ EXIT STATUS
853
+ 0 on success
854
+ 1 if the file/directory could not be created
855
+
856
+ EXAMPLES
857
+ mktemp
858
+ mktemp -d
859
+ mktemp /tmp/foo.XXXXXX`,
813
860
  "mv": `MV(1) User Commands MV(1)
814
861
 
815
862
  NAME
@@ -841,6 +888,30 @@ SYNOPSIS
841
888
 
842
889
  DESCRIPTION
843
890
  Print OS, kernel, uptime, package count, and related system details.`,
891
+ "nl": `NL(1) User Commands NL(1)
892
+
893
+ NAME
894
+ nl - number lines of files
895
+
896
+ SYNOPSIS
897
+ nl [OPTION]... [FILE]...
898
+
899
+ DESCRIPTION
900
+ Write each FILE to standard output, with line numbers added.
901
+ With no FILE, or when FILE is -, read standard input.
902
+
903
+ OPTIONS
904
+ -b, --body-numbering=STYLE use STYLE for numbering body lines
905
+ a number all lines
906
+ t number only non-empty lines (default)
907
+ -n, --number-format=FORMAT use FORMAT for line numbers
908
+ ln left justified, no leading zeros
909
+ rn right justified, no leading zeros (default)
910
+ rz right justified, leading zeros
911
+
912
+ EXAMPLES
913
+ nl /etc/passwd
914
+ cat file.txt | nl`,
844
915
  "node": `NODE(1) User Commands NODE(1)
845
916
 
846
917
  NAME
@@ -867,6 +938,24 @@ DESCRIPTION
867
938
 
868
939
  NOTES
869
940
  Requires package installation: apt install npm.`,
941
+ "nproc": `NPROC(1) User Commands NPROC(1)
942
+
943
+ NAME
944
+ nproc - print the number of processing units available
945
+
946
+ SYNOPSIS
947
+ nproc [OPTION]...
948
+
949
+ DESCRIPTION
950
+ Print the number of processing units available to the current process.
951
+ In this environment, always returns 4.
952
+
953
+ OPTIONS
954
+ --all print the number of installed processors
955
+
956
+ EXAMPLES
957
+ nproc
958
+ make -j$(nproc)`,
870
959
  "npx": `NPX(1) User Commands NPX(1)
871
960
 
872
961
  NAME
@@ -880,6 +969,41 @@ DESCRIPTION
880
969
 
881
970
  NOTES
882
971
  Requires package installation: apt install npm.`,
972
+ "pacman": `PACMAN(1) User Commands PACMAN(1)
973
+
974
+ NAME
975
+ pacman - play ASCII Pac-Man in the terminal
976
+
977
+ SYNOPSIS
978
+ pacman
979
+
980
+ DESCRIPTION
981
+ pacman launches an interactive ASCII Pac-Man game using myman
982
+ maze graphics. Eat all dots to win. Avoid ghosts or lose a life.
983
+ Eat a power pellet to enter fright mode and eat ghosts for points.
984
+
985
+ CONTROLS
986
+ W, Up move up
987
+ S, Down move down
988
+ A, Left move left
989
+ D, Right move right
990
+ Q, Ctrl+C quit
991
+
992
+ GHOSTS
993
+ Blinky (red) directly chases Pac-Man
994
+ Pinky (pink) targets 4 tiles ahead of Pac-Man
995
+ Inky (cyan) uses Blinky's position to compute target
996
+ Clyde (orange) chases when far, scatters when close
997
+
998
+ SCORING
999
+ Dot 10 points
1000
+ Power pellet 50 points
1001
+ Ghost 200 points (during fright mode)
1002
+
1003
+ NOTES
1004
+ Ghosts slow down during fright mode. Fright mode ends after
1005
+ a few seconds; ghosts flash before returning to normal.
1006
+ Left and right tunnel exits wrap around the maze.`,
883
1007
  "passwd": `PASSWD(1) User Commands PASSWD(1)
884
1008
 
885
1009
  NAME
@@ -891,6 +1015,26 @@ SYNOPSIS
891
1015
  DESCRIPTION
892
1016
  Update the authentication token (password) for USER.
893
1017
  Without USER, change the current user's password.`,
1018
+ "paste": `PASTE(1) User Commands PASTE(1)
1019
+
1020
+ NAME
1021
+ paste - merge lines of files
1022
+
1023
+ SYNOPSIS
1024
+ paste [OPTION]... [FILE]...
1025
+
1026
+ DESCRIPTION
1027
+ Write lines consisting of the sequentially corresponding lines from
1028
+ each FILE, separated by TABs, to standard output.
1029
+
1030
+ OPTIONS
1031
+ -d, --delimiters=LIST use characters from LIST instead of TABs
1032
+ -s, --serial paste one file at a time instead of in parallel
1033
+
1034
+ EXAMPLES
1035
+ paste file1 file2
1036
+ paste -d: /etc/passwd /etc/shadow
1037
+ paste -d, a.txt b.txt c.txt`,
894
1038
  "ping": `PING(8) User Commands PING(8)
895
1039
 
896
1040
  NAME
@@ -1080,6 +1224,28 @@ SYNOPSIS
1080
1224
 
1081
1225
  DESCRIPTION
1082
1226
  Rename positional parameters by discarding the first N arguments.`,
1227
+ "shuf": `SHUF(1) User Commands SHUF(1)
1228
+
1229
+ NAME
1230
+ shuf - generate random permutations
1231
+
1232
+ SYNOPSIS
1233
+ shuf [OPTION]... [FILE]
1234
+ shuf -i LO-HI [OPTION]...
1235
+ shuf -e [OPTION]... [ARG]...
1236
+
1237
+ DESCRIPTION
1238
+ Write a random permutation of the input lines to standard output.
1239
+
1240
+ OPTIONS
1241
+ -i LO-HI treat each number LO through HI as an input line
1242
+ -n COUNT output at most COUNT lines
1243
+ -e treat each ARG as an input line
1244
+
1245
+ EXAMPLES
1246
+ shuf /etc/passwd
1247
+ shuf -i 1-10
1248
+ shuf -n 3 /etc/hosts`,
1083
1249
  "sl": `SL(1) User Commands SL(1)
1084
1250
 
1085
1251
  NAME
@@ -1195,6 +1361,24 @@ SYNOPSIS
1195
1361
  OPTIONS
1196
1362
  -i run login shell as target user
1197
1363
  -u USER run command as USER`,
1364
+ "tac": `TAC(1) User Commands TAC(1)
1365
+
1366
+ NAME
1367
+ tac - concatenate and print files in reverse
1368
+
1369
+ SYNOPSIS
1370
+ tac [OPTION]... [FILE]...
1371
+
1372
+ DESCRIPTION
1373
+ Write each FILE to standard output, last line first.
1374
+ With no FILE, or when FILE is -, read standard input.
1375
+
1376
+ OPTIONS
1377
+ -s, --separator=STRING use STRING as the record separator
1378
+
1379
+ EXAMPLES
1380
+ tac /var/log/syslog
1381
+ echo -e "a\\nb\\nc" | tac`,
1198
1382
  "tail": `TAIL(1) User Commands TAIL(1)
1199
1383
 
1200
1384
  NAME
@@ -1245,6 +1429,29 @@ SYNOPSIS
1245
1429
 
1246
1430
  DESCRIPTION
1247
1431
  Evaluate conditional expressions for scripts and shell logic.`,
1432
+ "timeout": `TIMEOUT(1) User Commands TIMEOUT(1)
1433
+
1434
+ NAME
1435
+ timeout - run a command with a time limit
1436
+
1437
+ SYNOPSIS
1438
+ timeout DURATION COMMAND [ARG]...
1439
+
1440
+ DESCRIPTION
1441
+ Start COMMAND, and kill it if still running after DURATION seconds.
1442
+ In this environment, the time limit is simulated and the command
1443
+ always runs to completion.
1444
+
1445
+ OPTIONS
1446
+ DURATION An integer number of seconds (e.g. 5).
1447
+
1448
+ EXIT STATUS
1449
+ 124 if the command times out
1450
+ Otherwise the exit status of COMMAND.
1451
+
1452
+ EXAMPLES
1453
+ timeout 5 sleep 10
1454
+ timeout 30 curl http://example.com/`,
1248
1455
  "touch": `TOUCH(1) User Commands TOUCH(1)
1249
1456
 
1250
1457
  NAME
@@ -1406,6 +1613,26 @@ DESCRIPTION
1406
1613
  The following entries are displayed for each user: login name,
1407
1614
  the tty name, the remote host, login time, idle time, JCPU, PCPU,
1408
1615
  and the command line of the current process.`,
1616
+ "wait": `WAIT(1) Bash Builtin Commands WAIT(1)
1617
+
1618
+ NAME
1619
+ wait - wait for job completion
1620
+
1621
+ SYNOPSIS
1622
+ wait [jobspec or pid ...]
1623
+
1624
+ DESCRIPTION
1625
+ Wait for each specified process or job and return its termination
1626
+ status. If no arguments are given, wait for all currently active
1627
+ background jobs.
1628
+
1629
+ In this environment, background jobs are fire-and-forget; wait
1630
+ returns immediately with exit code 0.
1631
+
1632
+ EXAMPLES
1633
+ sleep 5 &
1634
+ wait
1635
+ echo "done"`,
1409
1636
  "wc": `WC(1) User Commands WC(1)
1410
1637
 
1411
1638
  NAME
@@ -0,0 +1,8 @@
1
+ import type { ShellModule } from "../types/commands";
2
+ /**
3
+ * Play ASCII Pac-Man in the terminal (myman-wip-2009-10-30 maze graphics).
4
+ * Controls: WASD or arrow keys to move, Q to quit.
5
+ * @category misc
6
+ * @params [""]
7
+ */
8
+ export declare const pacmanCommand: ShellModule;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Play ASCII Pac-Man in the terminal (myman-wip-2009-10-30 maze graphics).
3
+ * Controls: WASD or arrow keys to move, Q to quit.
4
+ * @category misc
5
+ * @params [""]
6
+ */
7
+ export const pacmanCommand = {
8
+ name: "pacman",
9
+ description: "Play ASCII Pac-Man (myman graphics, WASD/arrows)",
10
+ category: "misc",
11
+ params: [],
12
+ run: () => {
13
+ return { openPacman: true, exitCode: 0 };
14
+ },
15
+ };
@@ -11,36 +11,50 @@ export const psCommand = {
11
11
  params: ["[-a] [-u] [-x] [aux]"],
12
12
  run: ({ authUser, shell, args }) => {
13
13
  const sessions = shell.users.listActiveSessions();
14
+ const procs = shell.users.listProcesses();
14
15
  const showUser = ifFlag(args, ["-u"]) ||
15
16
  args.includes("u") ||
16
17
  args.includes("aux") ||
17
18
  args.includes("au");
18
19
  const showAll = ifFlag(args, ["-a", "-x"]) || args.includes("a") || args.includes("aux");
20
+ // Build bash-entry pid map from sessions (stable pids based on index)
21
+ const sessionPids = new Map(sessions.map((s, i) => [s.id, 1000 + i]));
22
+ const nextPid = 1000 + sessions.length;
19
23
  if (showUser) {
20
24
  const header = "USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND";
21
25
  const rows = [header];
22
- let pid = 1000;
23
26
  for (const s of sessions) {
24
27
  const user = s.username.padEnd(10).slice(0, 10);
25
28
  const mem = (Math.random() * 0.5).toFixed(1);
26
29
  const vsz = Math.floor(Math.random() * 20000 + 5000);
27
30
  const rss = Math.floor(Math.random() * 5000 + 1000);
28
- 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`);
29
- pid++;
31
+ rows.push(`${user} ${String(sessionPids.get(s.id)).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`);
30
32
  }
31
- rows.push(`root ${String(pid).padStart(6)} 0.0 0.0 0 0 ? S 00:00 0:00 ps`);
33
+ for (const p of procs) {
34
+ if (!showAll && p.username !== authUser)
35
+ continue;
36
+ const user = p.username.padEnd(10).slice(0, 10);
37
+ const mem = (Math.random() * 1.5).toFixed(1);
38
+ const vsz = Math.floor(Math.random() * 50000 + 10000);
39
+ const rss = Math.floor(Math.random() * 10000 + 2000);
40
+ rows.push(`${user} ${String(p.pid).padStart(6)} 0.1 ${mem.padStart(4)} ${String(vsz).padStart(6)} ${String(rss).padStart(5)} ${p.tty.padEnd(8)} S 00:00 0:00 ${p.command}`);
41
+ }
42
+ rows.push(`root ${String(nextPid).padStart(6)} 0.0 0.0 0 0 ? S 00:00 0:00 ps`);
32
43
  return { stdout: rows.join("\n"), exitCode: 0 };
33
44
  }
34
45
  const header = " PID TTY TIME CMD";
35
46
  const rows = [header];
36
- let pid = 1000;
37
47
  for (const s of sessions) {
38
48
  if (!showAll && s.username !== authUser)
39
49
  continue;
40
- rows.push(`${String(pid).padStart(5)} ${s.tty.padEnd(12)} 00:00:00 ${s.username === authUser ? "bash" : `bash (${s.username})`}`);
41
- pid++;
50
+ rows.push(`${String(sessionPids.get(s.id)).padStart(5)} ${s.tty.padEnd(12)} 00:00:00 ${s.username === authUser ? "bash" : `bash (${s.username})`}`);
51
+ }
52
+ for (const p of procs) {
53
+ if (!showAll && p.username !== authUser)
54
+ continue;
55
+ rows.push(`${String(p.pid).padStart(5)} ${p.tty.padEnd(12)} 00:00:00 ${p.command}`);
42
56
  }
43
- rows.push(`${String(pid).padStart(5)} pts/0 00:00:00 ps`);
57
+ rows.push(`${String(nextPid).padStart(5)} pts/0 00:00:00 ps`);
44
58
  return { stdout: rows.join("\n"), exitCode: 0 };
45
59
  },
46
60
  };
@@ -5,6 +5,7 @@ import { awkCommand } from "./awk";
5
5
  import { base64Command } from "./base64";
6
6
  import { basenameCommand, dirnameCommand } from "./basename";
7
7
  import { bcCommand } from "./bc";
8
+ import { columnCommand, mktempCommand, nlCommand, nprocCommand, pasteCommand, shufCommand, tacCommand, timeoutCommand, waitCommand } from "./coreutils";
8
9
  import { bunzip2Command, bzip2Command } from "./bzip2";
9
10
  import { lsofCommand } from "./lsof";
10
11
  import { perlCommand } from "./perl";
@@ -32,6 +33,7 @@ import { fileCommand } from "./file";
32
33
  import { findCommand } from "./find";
33
34
  import { freeCommand } from "./free";
34
35
  import { cmatrixCommand, cowsayCommand, cowthinkCommand, fortuneCommand, slCommand, yesCommand } from "./fun";
36
+ import { pacmanCommand } from "./pacman";
35
37
  import { grepCommand } from "./grep";
36
38
  import { groupsCommand } from "./groups";
37
39
  import { gunzipCommand, gzipCommand } from "./gzip";
@@ -177,6 +179,7 @@ const BASE_COMMANDS = [
177
179
  cowthinkCommand,
178
180
  cmatrixCommand,
179
181
  slCommand,
182
+ pacmanCommand,
180
183
  htopCommand,
181
184
  // Network
182
185
  curlCommand,
@@ -227,6 +230,16 @@ const BASE_COMMANDS = [
227
230
  straceCommand,
228
231
  // Scripting
229
232
  perlCommand,
233
+ // Coreutils (extended)
234
+ timeoutCommand,
235
+ mktempCommand,
236
+ nprocCommand,
237
+ waitCommand,
238
+ shufCommand,
239
+ pasteCommand,
240
+ tacCommand,
241
+ nlCommand,
242
+ columnCommand,
230
243
  ];
231
244
  const customCommands = [];
232
245
  const commandRegistry = new Map();
@@ -136,17 +136,22 @@ let _callDepth = 0;
136
136
  export async function runCommandDirect(name, args, authUser, hostname, mode, cwd, shell, stdin, env) {
137
137
  // Anti-loop guard: track call depth via env to avoid infinite recursion
138
138
  _callDepth++;
139
- // console.debug(`[depth=${_callDepth}] runCommandDirect: ${name}`);
140
139
  if (_callDepth > MAX_CALL_DEPTH) {
141
140
  _callDepth--;
142
- // console.debug(`[LOOP DETECTED] runCommandDirect blocked: ${name}`);
143
141
  return { stderr: `${name}: maximum call depth (${MAX_CALL_DEPTH}) exceeded`, exitCode: 126 };
144
142
  }
143
+ // Register as visible process only at the outermost call level
144
+ const isTopLevel = _callDepth === 1;
145
+ const pid = isTopLevel
146
+ ? shell.users.registerProcess(authUser, name, [name, ...args], env.vars.__TTY ?? "?")
147
+ : -1;
145
148
  try {
146
149
  return await _runCommandDirectInner(name, args, authUser, hostname, mode, cwd, shell, stdin, env);
147
150
  }
148
151
  finally {
149
152
  _callDepth--;
153
+ if (isTopLevel && pid !== -1)
154
+ shell.users.unregisterProcess(pid);
150
155
  }
151
156
  }
152
157
  async function _runCommandDirectInner(name, args, authUser, hostname, mode, cwd, shell, stdin, env) {
@@ -180,6 +185,41 @@ async function _runCommandDirectInner(name, args, authUser, hostname, mode, cwd,
180
185
  }
181
186
  }
182
187
  }
188
+ // Shell function defined via sh.ts (stored as __func_<name>)
189
+ const funcBody = env.vars[`__func_${name}`];
190
+ if (funcBody) {
191
+ const shMod = resolveModule("sh");
192
+ if (!shMod)
193
+ return { stderr: `${name}: sh not available`, exitCode: 127 };
194
+ const savedPositional = {};
195
+ args.forEach((a, i) => {
196
+ savedPositional[String(i + 1)] = env.vars[String(i + 1)];
197
+ env.vars[String(i + 1)] = a;
198
+ });
199
+ savedPositional["0"] = env.vars["0"];
200
+ env.vars["0"] = name;
201
+ try {
202
+ return await shMod.run({
203
+ authUser, hostname,
204
+ activeSessions: shell.users.listActiveSessions(),
205
+ rawInput: funcBody,
206
+ mode,
207
+ args: ["-c", funcBody],
208
+ stdin,
209
+ cwd,
210
+ shell,
211
+ env,
212
+ });
213
+ }
214
+ finally {
215
+ for (const [k, v] of Object.entries(savedPositional)) {
216
+ if (v === undefined)
217
+ delete env.vars[k];
218
+ else
219
+ env.vars[k] = v;
220
+ }
221
+ }
222
+ }
183
223
  const aliasVal = env.vars[`__alias_${name}`];
184
224
  if (aliasVal) {
185
225
  return runCommand(`${aliasVal} ${args.join(" ")}`, authUser, hostname, mode, cwd, shell, stdin, env);
@@ -2,6 +2,12 @@ import { evalArith, expandAsync, expandBraces } from "../utils/expand";
2
2
  import { ifFlag } from "./command-helpers";
3
3
  import { resolvePath } from "./helpers";
4
4
  import { runCommand } from "./runtime";
5
+ // Module-level compiled regexes for function definition matching.
6
+ // Rebuilt per-line inside parseBlocks would recompile on every script line — hoisted here instead.
7
+ const _funcNamePat = "[^\\s(){}]+";
8
+ const RE_FUNC_INLINE = new RegExp(`^(?:function\\s+)?(${_funcNamePat})\\s*\\(\\s*\\)\\s*\\{(.+)\\}\\s*$`);
9
+ const RE_FUNC_MULTI = new RegExp(`^(?:function\\s+)?(${_funcNamePat})\\s*\\(\\s*\\)\\s*\\{?\\s*$`);
10
+ const RE_FUNC_KW_ONLY = new RegExp(`^function\\s+(${_funcNamePat})\\s*\\{?\\s*$`);
5
11
  /**
6
12
  * Expand all shell forms including $(cmd) substitution.
7
13
  * Delegates to centralised expandAsync (single-quote-aware, depth-tracked).
@@ -20,9 +26,10 @@ function parseBlocks(lines) {
20
26
  continue;
21
27
  }
22
28
  // Function definition: name() { or function name { or name() { body }
23
- const funcMatchInline = line.match(/^(?:function\s+)?(\w+)\s*\(\s*\)\s*\{(.+)\}\s*$/);
24
- const funcMatch = funcMatchInline ?? (line.match(/^(?:function\s+)?(\w+)\s*\(\s*\)\s*\{?\s*$/) ||
25
- line.match(/^function\s+(\w+)\s*\{?\s*$/));
29
+ // Shell allows any non-whitespace identifier as function name (incl. ':')
30
+ const funcMatchInline = line.match(RE_FUNC_INLINE);
31
+ const funcMatch = funcMatchInline ?? (line.match(RE_FUNC_MULTI) ||
32
+ line.match(RE_FUNC_KW_ONLY));
26
33
  if (funcMatch) {
27
34
  const funcName = funcMatch[1];
28
35
  const body = [];
package/dist/index.d.ts CHANGED
@@ -6,7 +6,7 @@ export { default as VirtualFileSystem } from "./VirtualFileSystem/index";
6
6
  export { VirtualPackageManager } from "./VirtualPackageManager/index";
7
7
  export { VirtualShell } from "./VirtualShell/index";
8
8
  export { VirtualUserManager } from "./VirtualUserManager/index";
9
- export type { VirtualActiveSession } from "./VirtualUserManager/index";
9
+ export type { VirtualActiveSession, VirtualProcess } from "./VirtualUserManager/index";
10
10
  export { IdleManager } from "./VirtualShell/idleManager";
11
11
  export type { IdleManagerOptions, IdleState } from "./VirtualShell/idleManager";
12
12
  export type { AuditLogEntry, HoneyPotStats } from "./Honeypot/index";