typescript-virtual-container 1.5.8 → 1.5.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.
@@ -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";
@@ -60,7 +61,7 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
60
61
  })();
61
62
  function renderLine() {
62
63
  const prompt = buildCurrentPrompt();
63
- stream.write(`\r${prompt}${lineBuffer}\u001b[K`);
64
+ stream.write(`\r\x1b[0m${prompt}${lineBuffer}\u001b[K`);
64
65
  const moveLeft = lineBuffer.length - cursorPos;
65
66
  if (moveLeft > 0) {
66
67
  stream.write(`\u001b[${moveLeft}D`);
@@ -110,6 +111,10 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
110
111
  await startHtop();
111
112
  return;
112
113
  }
114
+ if (result.openPacman) {
115
+ startPacman();
116
+ return;
117
+ }
113
118
  if (result.clearScreen) {
114
119
  stream.write("\u001b[2J\u001b[H");
115
120
  }
@@ -177,6 +182,21 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
177
182
  });
178
183
  nanoSession = { kind: "htop", process: monitor };
179
184
  }
185
+ function startPacman() {
186
+ const game = new PacmanGame({
187
+ stream,
188
+ terminalSize,
189
+ onExit: () => {
190
+ nanoSession = null;
191
+ lineBuffer = "";
192
+ cursorPos = 0;
193
+ stream.write("\x1b[2J\x1b[H\x1b[0m");
194
+ renderLine();
195
+ },
196
+ });
197
+ nanoSession = { kind: "pacman", game };
198
+ game.start();
199
+ }
180
200
  function applyHistoryLine(nextLine) {
181
201
  lineBuffer = nextLine;
182
202
  cursorPos = lineBuffer.length;
@@ -247,6 +267,9 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
247
267
  if (nanoSession.kind === "nano") {
248
268
  nanoSession.editor.handleInput(chunk);
249
269
  }
270
+ else if (nanoSession.kind === "pacman") {
271
+ nanoSession.game.handleInput(chunk);
272
+ }
250
273
  else {
251
274
  nanoSession.process.stdin.write(chunk);
252
275
  }
@@ -552,6 +575,10 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
552
575
  await startHtop();
553
576
  return;
554
577
  }
578
+ if (result.openPacman) {
579
+ startPacman();
580
+ return;
581
+ }
555
582
  if (result.sudoChallenge) {
556
583
  startSudoPrompt(result.sudoChallenge);
557
584
  return;
@@ -617,6 +644,9 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
617
644
  if (nanoSession.kind === "htop") {
618
645
  nanoSession.process.kill("SIGTERM");
619
646
  }
647
+ else if (nanoSession.kind === "pacman") {
648
+ nanoSession.game.stop();
649
+ }
620
650
  nanoSession = null;
621
651
  }
622
652
  });
@@ -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++;
@@ -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,275 @@
1
+ /**
2
+ * timeout — run command with time limit (simulated: just runs the command)
3
+ * @category shell
4
+ * @params ["<duration> <command> [args...]"]
5
+ */
6
+ export const timeoutCommand = {
7
+ name: "timeout",
8
+ description: "Run command with time limit",
9
+ category: "shell",
10
+ params: ["<duration>", "<command>", "[args...]"],
11
+ run: async ({ args, authUser, hostname, mode, cwd, shell, env, stdin }) => {
12
+ // First arg is duration (ignored in simulation), rest is the command
13
+ if (args.length < 2)
14
+ return { stderr: "timeout: missing operand", exitCode: 1 };
15
+ const { runCommand } = await import("./runtime");
16
+ const cmd = args.slice(1).join(" ");
17
+ return runCommand(cmd, authUser, hostname, mode, cwd, shell, stdin, env);
18
+ },
19
+ };
20
+ /**
21
+ * mktemp — create a temporary file or directory
22
+ * @category shell
23
+ * @params ["[TEMPLATE]"]
24
+ */
25
+ export const mktempCommand = {
26
+ name: "mktemp",
27
+ description: "Create a temporary file or directory",
28
+ category: "shell",
29
+ params: ["[-d]", "[TEMPLATE]"],
30
+ run: ({ args, shell }) => {
31
+ const isDir = args.includes("-d");
32
+ const templateArg = args.find((a) => !a.startsWith("-")) ?? "tmp.XXXXXXXXXX";
33
+ const suffix = templateArg.replace(/X+$/, "") || "tmp.";
34
+ const rand = Math.random().toString(36).slice(2, 10);
35
+ const name = `${suffix}${rand}`;
36
+ const path = name.startsWith("/") ? name : `/tmp/${name}`;
37
+ try {
38
+ if (!shell.vfs.exists("/tmp"))
39
+ shell.vfs.mkdir("/tmp");
40
+ if (isDir) {
41
+ shell.vfs.mkdir(path);
42
+ }
43
+ else {
44
+ shell.vfs.writeFile(path, "");
45
+ }
46
+ }
47
+ catch {
48
+ return { stderr: `mktemp: failed to create ${isDir ? "directory" : "file"} via template '${templateArg}'`, exitCode: 1 };
49
+ }
50
+ return { stdout: path, exitCode: 0 };
51
+ },
52
+ };
53
+ /**
54
+ * nproc — print number of processing units
55
+ * @category system
56
+ * @params ["[--all]"]
57
+ */
58
+ export const nprocCommand = {
59
+ name: "nproc",
60
+ description: "Print number of processing units",
61
+ category: "system",
62
+ params: ["[--all]"],
63
+ run: () => ({ stdout: "4", exitCode: 0 }),
64
+ };
65
+ /**
66
+ * wait — wait for background jobs (no-op: background jobs are fire-and-forget)
67
+ * @category shell
68
+ * @params ["[job_id...]"]
69
+ */
70
+ export const waitCommand = {
71
+ name: "wait",
72
+ description: "Wait for background jobs to finish",
73
+ category: "shell",
74
+ params: ["[job_id...]"],
75
+ run: () => ({ exitCode: 0 }),
76
+ };
77
+ /**
78
+ * shuf — shuffle lines of input
79
+ * @category text
80
+ * @params ["[-n count] [-i lo-hi] [file]"]
81
+ */
82
+ export const shufCommand = {
83
+ name: "shuf",
84
+ description: "Shuffle lines of input randomly",
85
+ category: "text",
86
+ params: ["[-n count]", "[-i lo-hi]", "[file]"],
87
+ run: ({ args, stdin, shell, cwd }) => {
88
+ const { resolvePath } = require("./helpers");
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
+ const { resolvePath } = require("./helpers");
140
+ let delim = "\t";
141
+ const files = [];
142
+ let i = 0;
143
+ while (i < args.length) {
144
+ if (args[i] === "-d" && args[i + 1]) {
145
+ delim = args[i + 1];
146
+ i += 2;
147
+ }
148
+ else {
149
+ files.push(args[i]);
150
+ i++;
151
+ }
152
+ }
153
+ // serial mode (-s not implemented; basic merge)
154
+ let sources;
155
+ if (files.length === 0 || files[0] === "-") {
156
+ sources = [(stdin ?? "").split("\n")];
157
+ }
158
+ else {
159
+ sources = files.map((f) => {
160
+ const p = resolvePath(cwd ?? "/", f);
161
+ if (!shell.vfs.exists(p))
162
+ return [];
163
+ return shell.vfs.readFile(p).split("\n");
164
+ });
165
+ }
166
+ const maxLen = Math.max(...sources.map((s) => s.length));
167
+ const out = [];
168
+ for (let row = 0; row < maxLen; row++) {
169
+ out.push(sources.map((s) => s[row] ?? "").join(delim));
170
+ }
171
+ return { stdout: out.join("\n"), exitCode: 0 };
172
+ },
173
+ };
174
+ /**
175
+ * tac — concatenate files in reverse (line order)
176
+ * @category text
177
+ * @params ["[file...]"]
178
+ */
179
+ export const tacCommand = {
180
+ name: "tac",
181
+ description: "Concatenate files in reverse line order",
182
+ category: "text",
183
+ params: ["[file...]"],
184
+ run: ({ args, stdin, shell, cwd }) => {
185
+ const { resolvePath } = require("./helpers");
186
+ let input = "";
187
+ if (args.length === 0 || (args.length === 1 && args[0] === "-")) {
188
+ input = stdin ?? "";
189
+ }
190
+ else {
191
+ for (const f of args) {
192
+ const p = resolvePath(cwd ?? "/", f);
193
+ if (!shell.vfs.exists(p))
194
+ return { stderr: `tac: ${f}: No such file or directory`, exitCode: 1 };
195
+ input += shell.vfs.readFile(p);
196
+ }
197
+ }
198
+ const lines = input.split("\n");
199
+ // preserve trailing newline behaviour
200
+ if (lines[lines.length - 1] === "")
201
+ lines.pop();
202
+ return { stdout: lines.reverse().join("\n"), exitCode: 0 };
203
+ },
204
+ };
205
+ /**
206
+ * nl — number lines of files
207
+ * @category text
208
+ * @params ["[file]"]
209
+ */
210
+ export const nlCommand = {
211
+ name: "nl",
212
+ description: "Number lines of files",
213
+ category: "text",
214
+ params: ["[-ba] [-nrz] [file]"],
215
+ run: ({ args, stdin, shell, cwd }) => {
216
+ const { resolvePath } = require("./helpers");
217
+ const fileArg = args.find((a) => !a.startsWith("-"));
218
+ let input = stdin ?? "";
219
+ if (fileArg) {
220
+ const p = resolvePath(cwd ?? "/", fileArg);
221
+ if (!shell.vfs.exists(p))
222
+ return { stderr: `nl: ${fileArg}: No such file or directory`, exitCode: 1 };
223
+ input = shell.vfs.readFile(p);
224
+ }
225
+ const lines = input.split("\n");
226
+ if (lines[lines.length - 1] === "")
227
+ lines.pop();
228
+ let n = 1;
229
+ const out = lines.map((l) => {
230
+ if (l.trim() === "")
231
+ return `\t${l}`;
232
+ return `${String(n++).padStart(6)}\t${l}`;
233
+ });
234
+ return { stdout: out.join("\n"), exitCode: 0 };
235
+ },
236
+ };
237
+ /**
238
+ * column — columnate lists
239
+ * @category text
240
+ * @params ["[-t] [-s sep] [file]"]
241
+ */
242
+ export const columnCommand = {
243
+ name: "column",
244
+ description: "Columnate lists",
245
+ category: "text",
246
+ params: ["[-t]", "[-s sep]", "[file]"],
247
+ run: ({ args, stdin, shell, cwd }) => {
248
+ const { resolvePath } = require("./helpers");
249
+ const tableMode = args.includes("-t");
250
+ const sIdx = args.indexOf("-s");
251
+ const sep = sIdx !== -1 ? (args[sIdx + 1] ?? "\t") : /\s+/;
252
+ const fileArg = args.find((a) => !a.startsWith("-") && a !== args[sIdx + 1]);
253
+ let input = stdin ?? "";
254
+ if (fileArg) {
255
+ const p = resolvePath(cwd ?? "/", fileArg);
256
+ if (!shell.vfs.exists(p))
257
+ return { stderr: `column: ${fileArg}: No such file or directory`, exitCode: 1 };
258
+ input = shell.vfs.readFile(p);
259
+ }
260
+ const lines = input.split("\n").filter((l) => l !== "");
261
+ if (tableMode) {
262
+ const rows = lines.map((l) => (typeof sep === "string" ? l.split(sep) : l.split(sep)));
263
+ const colWidths = [];
264
+ for (const row of rows) {
265
+ row.forEach((cell, ci) => {
266
+ colWidths[ci] = Math.max(colWidths[ci] ?? 0, cell.length);
267
+ });
268
+ }
269
+ const out = rows.map((row) => row.map((cell, ci) => cell.padEnd(colWidths[ci] ?? 0)).join(" ").trimEnd());
270
+ return { stdout: out.join("\n"), exitCode: 0 };
271
+ }
272
+ // Default: fill columns (simple: just output as-is)
273
+ return { stdout: lines.join("\n"), exitCode: 0 };
274
+ },
275
+ };