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,6 +1,7 @@
1
1
  import * as path from "node:path";
2
2
  import { applyUserSwitch, getCommandNames, makeDefaultEnv, runCommand, userHome } from "../commands";
3
3
  import { NanoEditor } from "../modules/nanoEditor";
4
+ import { PacmanGame } from "../modules/pacmanGame";
4
5
  import { spawnHtopProcess, } from "../modules/shellInteractive";
5
6
  import { getVisibleHtopPidList, toTtyLines, } from "../modules/shellRuntime";
6
7
  import { buildLoginBanner } from "../SSHMimic/loginBanner";
@@ -15,6 +16,11 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
15
16
  let cwd = userHome(authUser);
16
17
  let pendingHeredoc = null;
17
18
  const shellEnv = makeDefaultEnv(authUser, hostname);
19
+ if (sessionId) {
20
+ const sess = shell.users.listActiveSessions().find((s) => s.id === sessionId);
21
+ if (sess)
22
+ shellEnv.vars.__TTY = sess.tty;
23
+ }
18
24
  const sessionStack = [];
19
25
  let nanoSession = null;
20
26
  let pendingSudo = null;
@@ -60,7 +66,7 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
60
66
  })();
61
67
  function renderLine() {
62
68
  const prompt = buildCurrentPrompt();
63
- stream.write(`\r${prompt}${lineBuffer}\u001b[K`);
69
+ stream.write(`\r\x1b[0m${prompt}${lineBuffer}\u001b[K`);
64
70
  const moveLeft = lineBuffer.length - cursorPos;
65
71
  if (moveLeft > 0) {
66
72
  stream.write(`\u001b[${moveLeft}D`);
@@ -110,6 +116,10 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
110
116
  await startHtop();
111
117
  return;
112
118
  }
119
+ if (result.openPacman) {
120
+ startPacman();
121
+ return;
122
+ }
113
123
  if (result.clearScreen) {
114
124
  stream.write("\u001b[2J\u001b[H");
115
125
  }
@@ -132,10 +142,15 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
132
142
  // WAL: checkpoint handled by auto-flush timer
133
143
  renderLine();
134
144
  }
145
+ let interactivePid = -1;
135
146
  function finishInteractiveSession(savedContent, targetPath) {
136
147
  if (savedContent !== undefined && targetPath) {
137
148
  shell.writeFileAsUser(authUser, targetPath, savedContent);
138
149
  }
150
+ if (interactivePid !== -1) {
151
+ shell.users.unregisterProcess(interactivePid);
152
+ interactivePid = -1;
153
+ }
139
154
  nanoSession = null;
140
155
  lineBuffer = "";
141
156
  cursorPos = 0;
@@ -144,6 +159,7 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
144
159
  renderLine();
145
160
  }
146
161
  function startNanoEditor(targetPath, initialContent, _tempPath) {
162
+ interactivePid = shell.users.registerProcess(authUser, "nano", ["nano", targetPath], shellEnv.vars.__TTY ?? "?");
147
163
  const editor = new NanoEditor({
148
164
  stream,
149
165
  terminalSize,
@@ -167,6 +183,7 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
167
183
  stream.write("htop: no child_process processes to display\r\n");
168
184
  return;
169
185
  }
186
+ interactivePid = shell.users.registerProcess(authUser, "htop", ["htop"], shellEnv.vars.__TTY ?? "?");
170
187
  const monitor = spawnHtopProcess(pidList, terminalSize, stream);
171
188
  monitor.on("error", (error) => {
172
189
  stream.write(`htop: ${error.message}\r\n`);
@@ -177,6 +194,26 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
177
194
  });
178
195
  nanoSession = { kind: "htop", process: monitor };
179
196
  }
197
+ function startPacman() {
198
+ interactivePid = shell.users.registerProcess(authUser, "pacman", ["pacman"], shellEnv.vars.__TTY ?? "?");
199
+ const game = new PacmanGame({
200
+ stream,
201
+ terminalSize,
202
+ onExit: () => {
203
+ if (interactivePid !== -1) {
204
+ shell.users.unregisterProcess(interactivePid);
205
+ interactivePid = -1;
206
+ }
207
+ nanoSession = null;
208
+ lineBuffer = "";
209
+ cursorPos = 0;
210
+ stream.write("\x1b[2J\x1b[H\x1b[0m");
211
+ renderLine();
212
+ },
213
+ });
214
+ nanoSession = { kind: "pacman", game };
215
+ game.start();
216
+ }
180
217
  function applyHistoryLine(nextLine) {
181
218
  lineBuffer = nextLine;
182
219
  cursorPos = lineBuffer.length;
@@ -247,6 +284,9 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
247
284
  if (nanoSession.kind === "nano") {
248
285
  nanoSession.editor.handleInput(chunk);
249
286
  }
287
+ else if (nanoSession.kind === "pacman") {
288
+ nanoSession.game.handleInput(chunk);
289
+ }
250
290
  else {
251
291
  nanoSession.process.stdin.write(chunk);
252
292
  }
@@ -552,6 +592,10 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
552
592
  await startHtop();
553
593
  return;
554
594
  }
595
+ if (result.openPacman) {
596
+ startPacman();
597
+ return;
598
+ }
555
599
  if (result.sudoChallenge) {
556
600
  startSudoPrompt(result.sudoChallenge);
557
601
  return;
@@ -617,6 +661,9 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
617
661
  if (nanoSession.kind === "htop") {
618
662
  nanoSession.process.kill("SIGTERM");
619
663
  }
664
+ else if (nanoSession.kind === "pacman") {
665
+ nanoSession.game.stop();
666
+ }
620
667
  nanoSession = null;
621
668
  }
622
669
  });
@@ -45,7 +45,7 @@ export function expandGlob(pattern, entries) {
45
45
  }
46
46
  // ── Internal parser ───────────────────────────────────────────────────────────
47
47
  function parseStatements(input) {
48
- // Split by ;, &&, || — respecting quotes and parens
48
+ // Split by ;, &&, ||, & — respecting quotes and parens
49
49
  const segments = splitByLogicalOps(input);
50
50
  const statements = [];
51
51
  for (const seg of segments) {
@@ -53,6 +53,8 @@ function parseStatements(input) {
53
53
  const stmt = { pipeline: { commands, isValid: true } };
54
54
  if (seg.op)
55
55
  stmt.op = seg.op;
56
+ if (seg.background)
57
+ stmt.background = true;
56
58
  statements.push(stmt);
57
59
  }
58
60
  return statements;
@@ -64,9 +66,9 @@ function splitByLogicalOps(input) {
64
66
  let inQ = false;
65
67
  let qChar = "";
66
68
  let i = 0;
67
- const flush = (op) => {
69
+ const flush = (op, background) => {
68
70
  if (current.trim())
69
- segments.push({ text: current, op });
71
+ segments.push({ text: current, op, background });
70
72
  current = "";
71
73
  };
72
74
  while (i < input.length) {
@@ -117,6 +119,25 @@ function splitByLogicalOps(input) {
117
119
  i += 2;
118
120
  continue;
119
121
  }
122
+ if (ch === "&" && input[i + 1] !== "&") {
123
+ // &> redirect (stdout+stderr) — keep in current segment, not a background op
124
+ if (input[i + 1] === ">") {
125
+ current += ch;
126
+ i++;
127
+ continue;
128
+ }
129
+ // 2>&1 — the & is part of a redirection target, not a background op
130
+ const trimmed = current.trimEnd();
131
+ if (trimmed.endsWith(">") || trimmed.endsWith("2>") || trimmed.endsWith(">>")) {
132
+ current += ch;
133
+ i++;
134
+ continue;
135
+ }
136
+ // trailing & → background job; treat like ; for sequencing
137
+ flush(";", true);
138
+ i++;
139
+ continue;
140
+ }
120
141
  if (ch === ";") {
121
142
  flush(";");
122
143
  i++;
@@ -209,6 +230,17 @@ function parseCommandWithRedirections(token) {
209
230
  appendOutput = false;
210
231
  i++;
211
232
  }
233
+ else if (part === "&>" || part === "&>>") {
234
+ // &> file — redirect both stdout and stderr to file
235
+ const append = part === "&>>";
236
+ i++;
237
+ if (i >= parts.length)
238
+ throw new Error(`Syntax error: expected filename after ${part}`);
239
+ outputFile = parts[i];
240
+ appendOutput = append;
241
+ stderrToStdout = true;
242
+ i++;
243
+ }
212
244
  else if (part === "2>&1") {
213
245
  stderrToStdout = true;
214
246
  i++;
@@ -12,6 +12,21 @@ export interface VirtualUserRecord {
12
12
  /** Scrypt-derived password hash in hex encoding. */
13
13
  passwordHash: string;
14
14
  }
15
+ /** Runtime representation of a command currently executing in a session. */
16
+ export interface VirtualProcess {
17
+ /** Unique process identifier (auto-incremented). */
18
+ pid: number;
19
+ /** Username running the process. */
20
+ username: string;
21
+ /** Command name (argv[0]). */
22
+ command: string;
23
+ /** Full argument list (command + args). */
24
+ argv: string[];
25
+ /** TTY identifier of the owning session, or "?" for background jobs. */
26
+ tty: string;
27
+ /** ISO-8601 start timestamp. */
28
+ startedAt: string;
29
+ }
15
30
  /** Runtime representation of authenticated SSH session. */
16
31
  export interface VirtualActiveSession {
17
32
  /** Stable session identifier (UUID). */
@@ -43,7 +58,9 @@ export declare class VirtualUserManager extends EventEmitter {
43
58
  private readonly sudoers;
44
59
  private readonly quotas;
45
60
  private readonly activeSessions;
61
+ private readonly activeProcesses;
46
62
  private nextTty;
63
+ private nextPid;
47
64
  /**
48
65
  * Creates a user manager instance backed by a virtual filesystem.
49
66
  *
@@ -196,6 +213,15 @@ export declare class VirtualUserManager extends EventEmitter {
196
213
  * @returns Array of username strings sorted alphabetically.
197
214
  */
198
215
  listUsers(): string[];
216
+ /**
217
+ * Registers a running command as a virtual process.
218
+ * Returns the assigned PID so the caller can deregister on completion.
219
+ */
220
+ registerProcess(username: string, command: string, argv: string[], tty: string): number;
221
+ /** Removes a process record when the command exits. */
222
+ unregisterProcess(pid: number): void;
223
+ /** Returns all currently running processes sorted by PID. */
224
+ listProcesses(): VirtualProcess[];
199
225
  private loadFromVfs;
200
226
  private loadSudoersFromVfs;
201
227
  private loadQuotasFromVfs;
@@ -26,7 +26,9 @@ export class VirtualUserManager extends EventEmitter {
26
26
  sudoers = new Set();
27
27
  quotas = new Map();
28
28
  activeSessions = new Map();
29
+ activeProcesses = new Map();
29
30
  nextTty = 0;
31
+ nextPid = 1000;
30
32
  /**
31
33
  * Creates a user manager instance backed by a virtual filesystem.
32
34
  *
@@ -401,6 +403,30 @@ export class VirtualUserManager extends EventEmitter {
401
403
  listUsers() {
402
404
  return Array.from(this.users.keys()).sort();
403
405
  }
406
+ /**
407
+ * Registers a running command as a virtual process.
408
+ * Returns the assigned PID so the caller can deregister on completion.
409
+ */
410
+ registerProcess(username, command, argv, tty) {
411
+ const pid = this.nextPid++;
412
+ this.activeProcesses.set(pid, {
413
+ pid,
414
+ username,
415
+ command,
416
+ argv,
417
+ tty,
418
+ startedAt: new Date().toISOString(),
419
+ });
420
+ return pid;
421
+ }
422
+ /** Removes a process record when the command exits. */
423
+ unregisterProcess(pid) {
424
+ this.activeProcesses.delete(pid);
425
+ }
426
+ /** Returns all currently running processes sorted by PID. */
427
+ listProcesses() {
428
+ return Array.from(this.activeProcesses.values()).sort((a, b) => a.pid - b.pid);
429
+ }
404
430
  loadFromVfs() {
405
431
  this.users.clear();
406
432
  if (!this.vfs.exists(this.usersPath)) {
@@ -0,0 +1,55 @@
1
+ import type { ShellModule } from "../types/commands";
2
+ /**
3
+ * timeout — run command with time limit (simulated: just runs the command)
4
+ * @category shell
5
+ * @params ["<duration> <command> [args...]"]
6
+ */
7
+ export declare const timeoutCommand: ShellModule;
8
+ /**
9
+ * mktemp — create a temporary file or directory
10
+ * @category shell
11
+ * @params ["[TEMPLATE]"]
12
+ */
13
+ export declare const mktempCommand: ShellModule;
14
+ /**
15
+ * nproc — print number of processing units
16
+ * @category system
17
+ * @params ["[--all]"]
18
+ */
19
+ export declare const nprocCommand: ShellModule;
20
+ /**
21
+ * wait — wait for background jobs (no-op: background jobs are fire-and-forget)
22
+ * @category shell
23
+ * @params ["[job_id...]"]
24
+ */
25
+ export declare const waitCommand: ShellModule;
26
+ /**
27
+ * shuf — shuffle lines of input
28
+ * @category text
29
+ * @params ["[-n count] [-i lo-hi] [file]"]
30
+ */
31
+ export declare const shufCommand: ShellModule;
32
+ /**
33
+ * paste — merge lines of files side by side
34
+ * @category text
35
+ * @params ["[-d delimiter] file..."]
36
+ */
37
+ export declare const pasteCommand: ShellModule;
38
+ /**
39
+ * tac — concatenate files in reverse (line order)
40
+ * @category text
41
+ * @params ["[file...]"]
42
+ */
43
+ export declare const tacCommand: ShellModule;
44
+ /**
45
+ * nl — number lines of files
46
+ * @category text
47
+ * @params ["[file]"]
48
+ */
49
+ export declare const nlCommand: ShellModule;
50
+ /**
51
+ * column — columnate lists
52
+ * @category text
53
+ * @params ["[-t] [-s sep] [file]"]
54
+ */
55
+ export declare const columnCommand: ShellModule;
@@ -0,0 +1,271 @@
1
+ import { resolvePath } from "./helpers";
2
+ /**
3
+ * timeout — run command with time limit (simulated: just runs the command)
4
+ * @category shell
5
+ * @params ["<duration> <command> [args...]"]
6
+ */
7
+ export const timeoutCommand = {
8
+ name: "timeout",
9
+ description: "Run command with time limit",
10
+ category: "shell",
11
+ params: ["<duration>", "<command>", "[args...]"],
12
+ run: async ({ args, authUser, hostname, mode, cwd, shell, env, stdin }) => {
13
+ // First arg is duration (ignored in simulation), rest is the command
14
+ if (args.length < 2)
15
+ return { stderr: "timeout: missing operand", exitCode: 1 };
16
+ const { runCommand } = await import("./runtime");
17
+ const cmd = args.slice(1).join(" ");
18
+ return runCommand(cmd, authUser, hostname, mode, cwd, shell, stdin, env);
19
+ },
20
+ };
21
+ /**
22
+ * mktemp — create a temporary file or directory
23
+ * @category shell
24
+ * @params ["[TEMPLATE]"]
25
+ */
26
+ export const mktempCommand = {
27
+ name: "mktemp",
28
+ description: "Create a temporary file or directory",
29
+ category: "shell",
30
+ params: ["[-d]", "[TEMPLATE]"],
31
+ run: ({ args, shell }) => {
32
+ const isDir = args.includes("-d");
33
+ const templateArg = args.find((a) => !a.startsWith("-")) ?? "tmp.XXXXXXXXXX";
34
+ const suffix = templateArg.replace(/X+$/, "") || "tmp.";
35
+ const rand = Math.random().toString(36).slice(2, 10);
36
+ const name = `${suffix}${rand}`;
37
+ const path = name.startsWith("/") ? name : `/tmp/${name}`;
38
+ try {
39
+ if (!shell.vfs.exists("/tmp"))
40
+ shell.vfs.mkdir("/tmp");
41
+ if (isDir) {
42
+ shell.vfs.mkdir(path);
43
+ }
44
+ else {
45
+ shell.vfs.writeFile(path, "");
46
+ }
47
+ }
48
+ catch {
49
+ return { stderr: `mktemp: failed to create ${isDir ? "directory" : "file"} via template '${templateArg}'`, exitCode: 1 };
50
+ }
51
+ return { stdout: path, exitCode: 0 };
52
+ },
53
+ };
54
+ /**
55
+ * nproc — print number of processing units
56
+ * @category system
57
+ * @params ["[--all]"]
58
+ */
59
+ export const nprocCommand = {
60
+ name: "nproc",
61
+ description: "Print number of processing units",
62
+ category: "system",
63
+ params: ["[--all]"],
64
+ run: () => ({ stdout: "4", exitCode: 0 }),
65
+ };
66
+ /**
67
+ * wait — wait for background jobs (no-op: background jobs are fire-and-forget)
68
+ * @category shell
69
+ * @params ["[job_id...]"]
70
+ */
71
+ export const waitCommand = {
72
+ name: "wait",
73
+ description: "Wait for background jobs to finish",
74
+ category: "shell",
75
+ params: ["[job_id...]"],
76
+ run: () => ({ exitCode: 0 }),
77
+ };
78
+ /**
79
+ * shuf — shuffle lines of input
80
+ * @category text
81
+ * @params ["[-n count] [-i lo-hi] [file]"]
82
+ */
83
+ export const shufCommand = {
84
+ name: "shuf",
85
+ description: "Shuffle lines of input randomly",
86
+ category: "text",
87
+ params: ["[-n count]", "[-i lo-hi]", "[file]"],
88
+ run: ({ args, stdin, shell, cwd }) => {
89
+ // -i lo-hi: generate range
90
+ const iIdx = args.indexOf("-i");
91
+ if (iIdx !== -1) {
92
+ const range = args[iIdx + 1] ?? "";
93
+ const m = range.match(/^(-?\d+)-(-?\d+)$/);
94
+ if (!m)
95
+ return { stderr: "shuf: invalid range", exitCode: 1 };
96
+ const lo = parseInt(m[1], 10);
97
+ const hi = parseInt(m[2], 10);
98
+ const nums = [];
99
+ for (let n = lo; n <= hi; n++)
100
+ nums.push(n);
101
+ for (let i = nums.length - 1; i > 0; i--) {
102
+ const j = Math.floor(Math.random() * (i + 1));
103
+ [nums[i], nums[j]] = [nums[j], nums[i]];
104
+ }
105
+ const nIdx = args.indexOf("-n");
106
+ const count = nIdx !== -1 ? parseInt(args[nIdx + 1] ?? "0", 10) : nums.length;
107
+ return { stdout: nums.slice(0, count).join("\n"), exitCode: 0 };
108
+ }
109
+ // file or stdin
110
+ let input = stdin ?? "";
111
+ const fileArg = args.find((a) => !a.startsWith("-"));
112
+ if (fileArg) {
113
+ const p = resolvePath(cwd ?? "/", fileArg);
114
+ if (!shell.vfs.exists(p))
115
+ return { stderr: `shuf: ${fileArg}: No such file or directory`, exitCode: 1 };
116
+ input = shell.vfs.readFile(p);
117
+ }
118
+ const lines = input.split("\n").filter((l) => l !== "");
119
+ for (let i = lines.length - 1; i > 0; i--) {
120
+ const j = Math.floor(Math.random() * (i + 1));
121
+ [lines[i], lines[j]] = [lines[j], lines[i]];
122
+ }
123
+ const nIdx = args.indexOf("-n");
124
+ const count = nIdx !== -1 ? parseInt(args[nIdx + 1] ?? "0", 10) : lines.length;
125
+ return { stdout: lines.slice(0, count).join("\n"), exitCode: 0 };
126
+ },
127
+ };
128
+ /**
129
+ * paste — merge lines of files side by side
130
+ * @category text
131
+ * @params ["[-d delimiter] file..."]
132
+ */
133
+ export const pasteCommand = {
134
+ name: "paste",
135
+ description: "Merge lines of files",
136
+ category: "text",
137
+ params: ["[-d delimiter]", "file..."],
138
+ run: ({ args, stdin, shell, cwd }) => {
139
+ let delim = "\t";
140
+ const files = [];
141
+ let i = 0;
142
+ while (i < args.length) {
143
+ if (args[i] === "-d" && args[i + 1]) {
144
+ delim = args[i + 1];
145
+ i += 2;
146
+ }
147
+ else {
148
+ files.push(args[i]);
149
+ i++;
150
+ }
151
+ }
152
+ // serial mode (-s not implemented; basic merge)
153
+ let sources;
154
+ if (files.length === 0 || files[0] === "-") {
155
+ sources = [(stdin ?? "").split("\n")];
156
+ }
157
+ else {
158
+ sources = files.map((f) => {
159
+ const p = resolvePath(cwd ?? "/", f);
160
+ if (!shell.vfs.exists(p))
161
+ return [];
162
+ return shell.vfs.readFile(p).split("\n");
163
+ });
164
+ }
165
+ const maxLen = Math.max(...sources.map((s) => s.length));
166
+ const out = [];
167
+ for (let row = 0; row < maxLen; row++) {
168
+ out.push(sources.map((s) => s[row] ?? "").join(delim));
169
+ }
170
+ return { stdout: out.join("\n"), exitCode: 0 };
171
+ },
172
+ };
173
+ /**
174
+ * tac — concatenate files in reverse (line order)
175
+ * @category text
176
+ * @params ["[file...]"]
177
+ */
178
+ export const tacCommand = {
179
+ name: "tac",
180
+ description: "Concatenate files in reverse line order",
181
+ category: "text",
182
+ params: ["[file...]"],
183
+ run: ({ args, stdin, shell, cwd }) => {
184
+ let input = "";
185
+ if (args.length === 0 || (args.length === 1 && args[0] === "-")) {
186
+ input = stdin ?? "";
187
+ }
188
+ else {
189
+ for (const f of args) {
190
+ const p = resolvePath(cwd ?? "/", f);
191
+ if (!shell.vfs.exists(p))
192
+ return { stderr: `tac: ${f}: No such file or directory`, exitCode: 1 };
193
+ input += shell.vfs.readFile(p);
194
+ }
195
+ }
196
+ const lines = input.split("\n");
197
+ // preserve trailing newline behaviour
198
+ if (lines[lines.length - 1] === "")
199
+ lines.pop();
200
+ return { stdout: lines.reverse().join("\n"), exitCode: 0 };
201
+ },
202
+ };
203
+ /**
204
+ * nl — number lines of files
205
+ * @category text
206
+ * @params ["[file]"]
207
+ */
208
+ export const nlCommand = {
209
+ name: "nl",
210
+ description: "Number lines of files",
211
+ category: "text",
212
+ params: ["[-ba] [-nrz] [file]"],
213
+ run: ({ args, stdin, shell, cwd }) => {
214
+ const fileArg = args.find((a) => !a.startsWith("-"));
215
+ let input = stdin ?? "";
216
+ if (fileArg) {
217
+ const p = resolvePath(cwd ?? "/", fileArg);
218
+ if (!shell.vfs.exists(p))
219
+ return { stderr: `nl: ${fileArg}: No such file or directory`, exitCode: 1 };
220
+ input = shell.vfs.readFile(p);
221
+ }
222
+ const lines = input.split("\n");
223
+ if (lines[lines.length - 1] === "")
224
+ lines.pop();
225
+ let n = 1;
226
+ const out = lines.map((l) => {
227
+ if (l.trim() === "")
228
+ return `\t${l}`;
229
+ return `${String(n++).padStart(6)}\t${l}`;
230
+ });
231
+ return { stdout: out.join("\n"), exitCode: 0 };
232
+ },
233
+ };
234
+ /**
235
+ * column — columnate lists
236
+ * @category text
237
+ * @params ["[-t] [-s sep] [file]"]
238
+ */
239
+ export const columnCommand = {
240
+ name: "column",
241
+ description: "Columnate lists",
242
+ category: "text",
243
+ params: ["[-t]", "[-s sep]", "[file]"],
244
+ run: ({ args, stdin, shell, cwd }) => {
245
+ const tableMode = args.includes("-t");
246
+ const sIdx = args.indexOf("-s");
247
+ const sep = sIdx !== -1 ? (args[sIdx + 1] ?? "\t") : /\s+/;
248
+ const fileArg = args.find((a) => !a.startsWith("-") && a !== args[sIdx + 1]);
249
+ let input = stdin ?? "";
250
+ if (fileArg) {
251
+ const p = resolvePath(cwd ?? "/", fileArg);
252
+ if (!shell.vfs.exists(p))
253
+ return { stderr: `column: ${fileArg}: No such file or directory`, exitCode: 1 };
254
+ input = shell.vfs.readFile(p);
255
+ }
256
+ const lines = input.split("\n").filter((l) => l !== "");
257
+ if (tableMode) {
258
+ const rows = lines.map((l) => (typeof sep === "string" ? l.split(sep) : l.split(sep)));
259
+ const colWidths = [];
260
+ for (const row of rows) {
261
+ row.forEach((cell, ci) => {
262
+ colWidths[ci] = Math.max(colWidths[ci] ?? 0, cell.length);
263
+ });
264
+ }
265
+ const out = rows.map((row) => row.map((cell, ci) => cell.padEnd(colWidths[ci] ?? 0)).join(" ").trimEnd());
266
+ return { stdout: out.join("\n"), exitCode: 0 };
267
+ }
268
+ // Default: fill columns (simple: just output as-is)
269
+ return { stdout: lines.join("\n"), exitCode: 0 };
270
+ },
271
+ };
@@ -1,7 +1,7 @@
1
1
  import type { ShellModule } from "../types/commands";
2
2
  /**
3
- * Interactive system monitor (requires terminal interaction).
3
+ * Interactive system monitor full ANSI output in exec/ssh mode, interactive panel in shell mode.
4
4
  * @category system
5
- * @params []
5
+ * @params ["[-d delay] [-p pid]"]
6
6
  */
7
7
  export declare const htopCommand: ShellModule;