typescript-virtual-container 1.5.7 → 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.d.ts +2 -1
- package/dist/SSHMimic/prompt.js +28 -6
- package/dist/SSHMimic/sftp.d.ts +1 -1
- package/dist/SSHMimic/sftp.js +1 -1
- package/dist/VirtualShell/index.d.ts +2 -3
- package/dist/VirtualShell/index.js +2 -3
- package/dist/VirtualShell/shell.js +108 -134
- 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/id.js +8 -1
- package/dist/commands/index.d.ts +1 -1
- package/dist/commands/index.js +1 -1
- package/dist/commands/manuals-bundle.js +237 -1
- 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/rm.d.ts +1 -1
- package/dist/commands/rm.js +48 -11
- package/dist/commands/runtime.d.ts +5 -0
- package/dist/commands/runtime.js +60 -1
- package/dist/commands/sh.js +5 -3
- package/dist/modules/linuxRootfs.js +7 -3
- package/dist/modules/nanoEditor.d.ts +92 -0
- package/dist/modules/nanoEditor.js +974 -0
- package/dist/modules/pacmanGame.d.ts +59 -0
- package/dist/modules/pacmanGame.js +655 -0
- package/dist/modules/webTermRenderer.d.ts +50 -0
- package/dist/modules/webTermRenderer.js +425 -0
- package/dist/types/commands.d.ts +2 -0
- package/dist/types/pipeline.d.ts +2 -0
- package/dist/utils/shellSession.d.ts +10 -0
- package/dist/utils/shellSession.js +56 -0
- package/package.json +2 -2
|
@@ -78,9 +78,8 @@ export interface VirtualShellVfsOptions {
|
|
|
78
78
|
* const result = await client.exec("uname -a");
|
|
79
79
|
* ```
|
|
80
80
|
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
* @fires VirtualShell#session:start Emitted when an interactive session opens.
|
|
81
|
+
* **Events:** `initialized` (VFS and users ready), `command` (after each execution),
|
|
82
|
+
* `session:start` (interactive session opened).
|
|
84
83
|
*/
|
|
85
84
|
declare class VirtualShell extends EventEmitter {
|
|
86
85
|
/** Backing virtual filesystem — use for direct path operations. */
|
|
@@ -60,9 +60,8 @@ function resolveAutoSudoForNewUsers() {
|
|
|
60
60
|
* const result = await client.exec("uname -a");
|
|
61
61
|
* ```
|
|
62
62
|
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* @fires VirtualShell#session:start Emitted when an interactive session opens.
|
|
63
|
+
* **Events:** `initialized` (VFS and users ready), `command` (after each execution),
|
|
64
|
+
* `session:start` (interactive session opened).
|
|
66
65
|
*/
|
|
67
66
|
class VirtualShell extends EventEmitter {
|
|
68
67
|
/** Backing virtual filesystem — use for direct path operations. */
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { readFile, unlink, writeFile } from "node:fs/promises";
|
|
2
1
|
import * as path from "node:path";
|
|
3
|
-
import { getCommandNames, makeDefaultEnv, runCommand, userHome } from "../commands";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
2
|
+
import { applyUserSwitch, getCommandNames, makeDefaultEnv, runCommand, userHome } from "../commands";
|
|
3
|
+
import { NanoEditor } from "../modules/nanoEditor";
|
|
4
|
+
import { PacmanGame } from "../modules/pacmanGame";
|
|
5
|
+
import { spawnHtopProcess, } from "../modules/shellInteractive";
|
|
6
|
+
import { getVisibleHtopPidList, toTtyLines, } from "../modules/shellRuntime";
|
|
6
7
|
import { buildLoginBanner } from "../SSHMimic/loginBanner";
|
|
7
8
|
import { buildPrompt } from "../SSHMimic/prompt";
|
|
9
|
+
import { listPathCompletions, loadHistory, readLastLogin, saveHistory, writeLastLogin } from "../utils/shellSession";
|
|
8
10
|
export function startShell(properties, stream, authUser, hostname, sessionId, remoteAddress = "unknown", terminalSize = { cols: 80, rows: 24 }, shell) {
|
|
9
11
|
let lineBuffer = "";
|
|
10
12
|
let cursorPos = 0;
|
|
@@ -18,43 +20,48 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
|
|
|
18
20
|
let nanoSession = null;
|
|
19
21
|
let pendingSudo = null;
|
|
20
22
|
const buildCurrentPrompt = () => {
|
|
23
|
+
if (shellEnv.vars.PS1)
|
|
24
|
+
return buildPrompt(authUser, hostname, "", shellEnv.vars.PS1, cwd);
|
|
21
25
|
const homePath = userHome(authUser);
|
|
22
26
|
const cwdLabel = cwd === homePath ? "~" : path.posix.basename(cwd) || "/";
|
|
23
27
|
return buildPrompt(authUser, hostname, cwdLabel);
|
|
24
28
|
};
|
|
25
29
|
const commandNames = Array.from(new Set(getCommandNames())).sort();
|
|
26
30
|
console.log(`[${sessionId}] Shell started for user '${authUser}' at ${remoteAddress}`);
|
|
27
|
-
// Source login/rc files
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
31
|
+
// Source login/rc files before first prompt.
|
|
32
|
+
let loginReady = false;
|
|
33
|
+
const sourceFile = async (filePath, isEnvFile = false) => {
|
|
34
|
+
if (!shell.vfs.exists(filePath))
|
|
35
|
+
return;
|
|
36
|
+
try {
|
|
37
|
+
const content = shell.vfs.readFile(filePath);
|
|
38
|
+
for (const line of content.split("\n")) {
|
|
39
|
+
const l = line.trim();
|
|
40
|
+
if (!l || l.startsWith("#"))
|
|
41
|
+
continue;
|
|
42
|
+
if (isEnvFile) {
|
|
43
|
+
const m = l.match(/^([A-Za-z_][A-Za-z0-9_]*)=["']?(.+?)["']?\s*$/);
|
|
44
|
+
if (m)
|
|
45
|
+
shellEnv.vars[m[1]] = m[2];
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
const r = await runCommand(l, authUser, hostname, "shell", cwd, shell, undefined, shellEnv);
|
|
49
|
+
if (r.stdout)
|
|
50
|
+
stream.write(r.stdout.replace(/\n/g, "\r\n"));
|
|
47
51
|
}
|
|
48
52
|
}
|
|
49
|
-
|
|
50
|
-
}
|
|
53
|
+
}
|
|
54
|
+
catch { /* ignore */ }
|
|
55
|
+
};
|
|
56
|
+
const loginPromise = (async () => {
|
|
51
57
|
await sourceFile("/etc/environment", true);
|
|
52
58
|
await sourceFile(`${userHome(authUser)}/.profile`);
|
|
53
59
|
await sourceFile(`${userHome(authUser)}/.bashrc`);
|
|
60
|
+
loginReady = true;
|
|
54
61
|
})();
|
|
55
62
|
function renderLine() {
|
|
56
63
|
const prompt = buildCurrentPrompt();
|
|
57
|
-
stream.write(`\r${prompt}${lineBuffer}\u001b[K`);
|
|
64
|
+
stream.write(`\r\x1b[0m${prompt}${lineBuffer}\u001b[K`);
|
|
58
65
|
const moveLeft = lineBuffer.length - cursorPos;
|
|
59
66
|
if (moveLeft > 0) {
|
|
60
67
|
stream.write(`\u001b[${moveLeft}D`);
|
|
@@ -88,6 +95,7 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
|
|
|
88
95
|
cwd = userHome(authUser);
|
|
89
96
|
}
|
|
90
97
|
shell.users.updateSession(sessionId, authUser, remoteAddress);
|
|
98
|
+
await applyUserSwitch(authUser, hostname, cwd, shellEnv, shell);
|
|
91
99
|
stream.write("\r\n");
|
|
92
100
|
renderLine();
|
|
93
101
|
return;
|
|
@@ -103,6 +111,10 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
|
|
|
103
111
|
await startHtop();
|
|
104
112
|
return;
|
|
105
113
|
}
|
|
114
|
+
if (result.openPacman) {
|
|
115
|
+
startPacman();
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
106
118
|
if (result.clearScreen) {
|
|
107
119
|
stream.write("\u001b[2J\u001b[H");
|
|
108
120
|
}
|
|
@@ -117,6 +129,7 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
|
|
|
117
129
|
authUser = result.switchUser;
|
|
118
130
|
cwd = result.nextCwd ?? userHome(authUser);
|
|
119
131
|
shell.users.updateSession(sessionId, authUser, remoteAddress);
|
|
132
|
+
await applyUserSwitch(authUser, hostname, cwd, shellEnv, shell);
|
|
120
133
|
}
|
|
121
134
|
else if (result.nextCwd) {
|
|
122
135
|
cwd = result.nextCwd;
|
|
@@ -124,46 +137,34 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
|
|
|
124
137
|
// WAL: checkpoint handled by auto-flush timer
|
|
125
138
|
renderLine();
|
|
126
139
|
}
|
|
127
|
-
|
|
128
|
-
if (
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
const activeSession = nanoSession;
|
|
132
|
-
if (activeSession.kind === "nano") {
|
|
133
|
-
try {
|
|
134
|
-
const updatedContent = await readFile(activeSession.tempPath, "utf8");
|
|
135
|
-
shell.writeFileAsUser(authUser, activeSession.targetPath, updatedContent);
|
|
136
|
-
// WAL: checkpoint handled by auto-flush timer
|
|
137
|
-
}
|
|
138
|
-
catch {
|
|
139
|
-
// If temp file does not exist, nano exited without writing.
|
|
140
|
-
}
|
|
141
|
-
await unlink(activeSession.tempPath).catch(() => undefined);
|
|
140
|
+
function finishInteractiveSession(savedContent, targetPath) {
|
|
141
|
+
if (savedContent !== undefined && targetPath) {
|
|
142
|
+
shell.writeFileAsUser(authUser, targetPath, savedContent);
|
|
142
143
|
}
|
|
143
144
|
nanoSession = null;
|
|
144
145
|
lineBuffer = "";
|
|
145
146
|
cursorPos = 0;
|
|
146
|
-
|
|
147
|
+
// Clear screen + reset SGR so nano residue is gone before next prompt
|
|
148
|
+
stream.write("\x1b[2J\x1b[H\x1b[0m");
|
|
147
149
|
renderLine();
|
|
148
150
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
151
|
+
function startNanoEditor(targetPath, initialContent, _tempPath) {
|
|
152
|
+
const editor = new NanoEditor({
|
|
153
|
+
stream,
|
|
154
|
+
terminalSize,
|
|
155
|
+
content: initialContent,
|
|
156
|
+
filename: path.posix.basename(targetPath),
|
|
157
|
+
onExit: (reason, content) => {
|
|
158
|
+
if (reason === "saved") {
|
|
159
|
+
finishInteractiveSession(content, targetPath);
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
finishInteractiveSession();
|
|
163
|
+
}
|
|
164
|
+
},
|
|
160
165
|
});
|
|
161
|
-
nanoSession = {
|
|
162
|
-
|
|
163
|
-
targetPath,
|
|
164
|
-
tempPath,
|
|
165
|
-
process: editor,
|
|
166
|
-
};
|
|
166
|
+
nanoSession = { kind: "nano", targetPath, editor };
|
|
167
|
+
editor.start();
|
|
167
168
|
}
|
|
168
169
|
async function startHtop() {
|
|
169
170
|
const pidList = await getVisibleHtopPidList();
|
|
@@ -174,17 +175,27 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
|
|
|
174
175
|
const monitor = spawnHtopProcess(pidList, terminalSize, stream);
|
|
175
176
|
monitor.on("error", (error) => {
|
|
176
177
|
stream.write(`htop: ${error.message}\r\n`);
|
|
177
|
-
|
|
178
|
+
finishInteractiveSession();
|
|
178
179
|
});
|
|
179
180
|
monitor.on("close", () => {
|
|
180
|
-
|
|
181
|
+
finishInteractiveSession();
|
|
181
182
|
});
|
|
182
|
-
nanoSession = {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
183
|
+
nanoSession = { kind: "htop", process: monitor };
|
|
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();
|
|
188
199
|
}
|
|
189
200
|
function applyHistoryLine(nextLine) {
|
|
190
201
|
lineBuffer = nextLine;
|
|
@@ -207,28 +218,6 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
|
|
|
207
218
|
}
|
|
208
219
|
return { start, end };
|
|
209
220
|
}
|
|
210
|
-
function listPathCompletions(prefix) {
|
|
211
|
-
const slashIndex = prefix.lastIndexOf("/");
|
|
212
|
-
const dirPart = slashIndex >= 0 ? prefix.slice(0, slashIndex + 1) : "";
|
|
213
|
-
const namePart = slashIndex >= 0 ? prefix.slice(slashIndex + 1) : prefix;
|
|
214
|
-
const basePath = resolvePath(cwd, dirPart || ".");
|
|
215
|
-
try {
|
|
216
|
-
return shell.vfs
|
|
217
|
-
.list(basePath)
|
|
218
|
-
.filter((entry) => !entry.startsWith("."))
|
|
219
|
-
.filter((entry) => entry.startsWith(namePart))
|
|
220
|
-
.map((entry) => {
|
|
221
|
-
const fullPath = path.posix.join(basePath, entry);
|
|
222
|
-
const st = shell.vfs.stat(fullPath);
|
|
223
|
-
const suffix = st.type === "directory" ? "/" : "";
|
|
224
|
-
return `${dirPart}${entry}${suffix}`;
|
|
225
|
-
})
|
|
226
|
-
.sort();
|
|
227
|
-
}
|
|
228
|
-
catch {
|
|
229
|
-
return [];
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
221
|
function handleTabCompletion() {
|
|
233
222
|
const { start, end } = getTokenRange(lineBuffer, cursorPos);
|
|
234
223
|
const token = lineBuffer.slice(start, cursorPos);
|
|
@@ -239,7 +228,7 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
|
|
|
239
228
|
const commandCandidates = firstToken
|
|
240
229
|
? commandNames.filter((name) => name.startsWith(token))
|
|
241
230
|
: [];
|
|
242
|
-
const pathCandidates = listPathCompletions(token);
|
|
231
|
+
const pathCandidates = listPathCompletions(shell.vfs, cwd, token);
|
|
243
232
|
const candidates = Array.from(new Set([...commandCandidates, ...pathCandidates])).sort();
|
|
244
233
|
if (candidates.length === 0) {
|
|
245
234
|
return;
|
|
@@ -257,43 +246,33 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
|
|
|
257
246
|
renderLine();
|
|
258
247
|
}
|
|
259
248
|
function pushHistory(cmd) {
|
|
260
|
-
if (cmd.length === 0)
|
|
249
|
+
if (cmd.length === 0)
|
|
261
250
|
return;
|
|
262
|
-
}
|
|
263
251
|
history.push(cmd);
|
|
264
|
-
if (history.length > 500)
|
|
252
|
+
if (history.length > 500)
|
|
265
253
|
history = history.slice(history.length - 500);
|
|
266
|
-
|
|
267
|
-
const data = history.length > 0 ? `${history.join("\n")}\n` : "";
|
|
268
|
-
shell.vfs.writeFile(`${userHome(authUser)}/.bash_history`, data);
|
|
269
|
-
}
|
|
270
|
-
function readLastLogin() {
|
|
271
|
-
const lastlogPath = `${userHome(authUser)}/.lastlog.json`;
|
|
272
|
-
if (!shell.vfs.exists(lastlogPath)) {
|
|
273
|
-
return null;
|
|
274
|
-
}
|
|
275
|
-
try {
|
|
276
|
-
return JSON.parse(shell.vfs.readFile(lastlogPath));
|
|
277
|
-
}
|
|
278
|
-
catch {
|
|
279
|
-
return null;
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
function writeLastLogin(nowIso) {
|
|
283
|
-
const lastlogPath = `${userHome(authUser)}/.lastlog`;
|
|
284
|
-
shell.vfs.writeFile(lastlogPath, JSON.stringify({ at: nowIso, from: remoteAddress }));
|
|
254
|
+
saveHistory(shell.vfs, authUser, history);
|
|
285
255
|
}
|
|
286
256
|
function renderLoginBanner() {
|
|
287
|
-
const last = readLastLogin();
|
|
288
|
-
const nowIso = new Date().toISOString();
|
|
257
|
+
const last = readLastLogin(shell.vfs, authUser);
|
|
289
258
|
stream.write(buildLoginBanner(hostname, properties, last));
|
|
290
|
-
writeLastLogin(
|
|
259
|
+
writeLastLogin(shell.vfs, authUser, remoteAddress);
|
|
291
260
|
}
|
|
292
261
|
renderLoginBanner();
|
|
293
|
-
renderLine();
|
|
262
|
+
void loginPromise.then(() => renderLine());
|
|
294
263
|
stream.on("data", async (chunk) => {
|
|
264
|
+
if (!loginReady)
|
|
265
|
+
return;
|
|
295
266
|
if (nanoSession) {
|
|
296
|
-
nanoSession.
|
|
267
|
+
if (nanoSession.kind === "nano") {
|
|
268
|
+
nanoSession.editor.handleInput(chunk);
|
|
269
|
+
}
|
|
270
|
+
else if (nanoSession.kind === "pacman") {
|
|
271
|
+
nanoSession.game.handleInput(chunk);
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
nanoSession.process.stdin.write(chunk);
|
|
275
|
+
}
|
|
297
276
|
return;
|
|
298
277
|
}
|
|
299
278
|
if (pendingHeredoc) {
|
|
@@ -596,6 +575,10 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
|
|
|
596
575
|
await startHtop();
|
|
597
576
|
return;
|
|
598
577
|
}
|
|
578
|
+
if (result.openPacman) {
|
|
579
|
+
startPacman();
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
599
582
|
if (result.sudoChallenge) {
|
|
600
583
|
startSudoPrompt(result.sudoChallenge);
|
|
601
584
|
return;
|
|
@@ -634,11 +617,9 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
|
|
|
634
617
|
sessionStack.push({ authUser, cwd });
|
|
635
618
|
authUser = result.switchUser;
|
|
636
619
|
cwd = result.nextCwd ?? userHome(authUser);
|
|
637
|
-
shellEnv.vars.USER = authUser;
|
|
638
|
-
shellEnv.vars.LOGNAME = authUser;
|
|
639
|
-
shellEnv.vars.HOME = userHome(authUser);
|
|
640
620
|
shellEnv.vars.PWD = cwd;
|
|
641
621
|
shell.users.updateSession(sessionId, authUser, remoteAddress);
|
|
622
|
+
await applyUserSwitch(authUser, hostname, cwd, shellEnv, shell);
|
|
642
623
|
lineBuffer = "";
|
|
643
624
|
cursorPos = 0;
|
|
644
625
|
}
|
|
@@ -660,20 +641,13 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
|
|
|
660
641
|
});
|
|
661
642
|
stream.on("close", () => {
|
|
662
643
|
if (nanoSession) {
|
|
663
|
-
nanoSession.
|
|
644
|
+
if (nanoSession.kind === "htop") {
|
|
645
|
+
nanoSession.process.kill("SIGTERM");
|
|
646
|
+
}
|
|
647
|
+
else if (nanoSession.kind === "pacman") {
|
|
648
|
+
nanoSession.game.stop();
|
|
649
|
+
}
|
|
664
650
|
nanoSession = null;
|
|
665
651
|
}
|
|
666
652
|
});
|
|
667
653
|
}
|
|
668
|
-
function loadHistory(vfs, authUser) {
|
|
669
|
-
const historyPath = `${userHome(authUser)}/.bash_history`;
|
|
670
|
-
if (!vfs.exists(historyPath)) {
|
|
671
|
-
vfs.writeFile(historyPath, "");
|
|
672
|
-
return [];
|
|
673
|
-
}
|
|
674
|
-
const raw = vfs.readFile(historyPath);
|
|
675
|
-
return raw
|
|
676
|
-
.split("\n")
|
|
677
|
-
.map((line) => line.trim())
|
|
678
|
-
.filter((line) => line.length > 0);
|
|
679
|
-
}
|
|
@@ -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;
|