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.
- package/README.md +39 -29
- package/dist/.tsbuildinfo +1 -1
- package/dist/SSHMimic/executor.js +9 -0
- package/dist/SSHMimic/prompt.js +2 -2
- package/dist/VirtualShell/shell.js +48 -1
- package/dist/VirtualShell/shellParser.js +35 -3
- package/dist/VirtualUserManager/index.d.ts +26 -0
- package/dist/VirtualUserManager/index.js +26 -0
- package/dist/commands/coreutils.d.ts +55 -0
- package/dist/commands/coreutils.js +271 -0
- package/dist/commands/htop.d.ts +2 -2
- package/dist/commands/htop.js +143 -8
- package/dist/commands/manuals-bundle.js +227 -0
- package/dist/commands/pacman.d.ts +8 -0
- package/dist/commands/pacman.js +15 -0
- package/dist/commands/ps.js +22 -8
- package/dist/commands/registry.js +13 -0
- package/dist/commands/runtime.js +42 -2
- package/dist/commands/sh.js +10 -3
- package/dist/index.d.ts +1 -1
- package/dist/modules/linuxRootfs.js +4 -4
- package/dist/modules/nanoEditor.d.ts +1 -1
- package/dist/modules/nanoEditor.js +22 -4
- package/dist/modules/pacmanGame.d.ts +59 -0
- package/dist/modules/pacmanGame.js +655 -0
- package/dist/modules/webTermRenderer.d.ts +8 -0
- package/dist/modules/webTermRenderer.js +163 -29
- package/dist/types/commands.d.ts +2 -0
- package/dist/types/pipeline.d.ts +2 -0
- package/package.json +2 -2
|
@@ -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 ;, &&,
|
|
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
|
+
};
|
package/dist/commands/htop.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ShellModule } from "../types/commands";
|
|
2
2
|
/**
|
|
3
|
-
* Interactive system monitor
|
|
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;
|