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.
- 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 +31 -1
- package/dist/VirtualShell/shellParser.js +35 -3
- package/dist/commands/coreutils.d.ts +55 -0
- package/dist/commands/coreutils.js +275 -0
- 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/registry.js +13 -0
- package/dist/commands/runtime.js +35 -0
- package/dist/commands/sh.js +5 -3
- 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";
|
|
@@ -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 ;, &&,
|
|
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
|
+
};
|