typescript-virtual-container 1.4.1 → 1.4.3
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/.vscode/settings.json +2 -0
- package/README.md +77 -36
- package/benchmark-virtualshell.ts +3 -11
- package/builds/self-standalone.js +224 -224
- package/builds/self-standalone.js.map +4 -4
- package/builds/standalone-wo-sftp.js +23 -23
- package/builds/standalone-wo-sftp.js.map +3 -3
- package/builds/standalone.js +23 -23
- package/builds/standalone.js.map +3 -3
- package/dist/VirtualFileSystem/index.d.ts +47 -0
- package/dist/VirtualFileSystem/index.d.ts.map +1 -1
- package/dist/VirtualFileSystem/index.js +159 -0
- package/dist/VirtualShell/index.d.ts +29 -0
- package/dist/VirtualShell/index.d.ts.map +1 -1
- package/dist/VirtualShell/index.js +29 -0
- package/dist/VirtualShell/shell.d.ts.map +1 -1
- package/dist/VirtualShell/shell.js +6 -10
- package/dist/VirtualShell/shellParser.js +28 -1
- package/dist/VirtualUserManager/index.d.ts.map +1 -1
- package/dist/VirtualUserManager/index.js +4 -4
- package/dist/commands/export.d.ts.map +1 -1
- package/dist/commands/export.js +5 -3
- package/dist/commands/helpers.js +1 -1
- package/dist/commands/history.js +2 -2
- package/dist/commands/registry.d.ts.map +1 -1
- package/dist/commands/registry.js +2 -0
- package/dist/commands/runtime.d.ts.map +1 -1
- package/dist/commands/runtime.js +28 -3
- package/dist/commands/seq.d.ts +4 -0
- package/dist/commands/seq.d.ts.map +1 -0
- package/dist/commands/seq.js +50 -0
- package/dist/commands/sh.d.ts +0 -6
- package/dist/commands/sh.d.ts.map +1 -1
- package/dist/commands/sh.js +153 -10
- package/dist/modules/linuxRootfs.d.ts +1 -1
- package/dist/modules/linuxRootfs.d.ts.map +1 -1
- package/dist/modules/linuxRootfs.js +5 -5
- package/dist/self-standalone.js +149 -102
- package/dist/types/pipeline.d.ts +6 -0
- package/dist/types/pipeline.d.ts.map +1 -1
- package/dist/types/vfs.d.ts +15 -0
- package/dist/types/vfs.d.ts.map +1 -1
- package/dist/utils/expand.d.ts +9 -0
- package/dist/utils/expand.d.ts.map +1 -1
- package/dist/utils/expand.js +84 -2
- package/dist/utils/tokenize.d.ts.map +1 -1
- package/dist/utils/tokenize.js +40 -0
- package/package.json +1 -1
- package/src/VirtualFileSystem/index.ts +164 -1
- package/src/VirtualShell/index.ts +36 -0
- package/src/VirtualShell/shell.ts +6 -11
- package/src/VirtualShell/shellParser.ts +26 -1
- package/src/VirtualUserManager/index.ts +4 -4
- package/src/commands/export.ts +5 -3
- package/src/commands/helpers.ts +1 -1
- package/src/commands/history.ts +2 -2
- package/src/commands/registry.ts +2 -0
- package/src/commands/runtime.ts +30 -3
- package/src/commands/seq.ts +43 -0
- package/src/commands/sh.ts +144 -19
- package/src/modules/linuxRootfs.ts +6 -6
- package/src/self-standalone.ts +190 -141
- package/src/types/pipeline.ts +6 -0
- package/src/types/vfs.ts +17 -0
- package/src/utils/expand.ts +75 -2
- package/src/utils/tokenize.ts +20 -0
- package/tests/helpers.test.ts +3 -3
package/dist/self-standalone.js
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import { readFile, unlink, writeFile } from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
2
3
|
import { basename } from "node:path";
|
|
3
4
|
import { stdin, stdout } from "node:process";
|
|
4
5
|
import { createInterface } from "node:readline";
|
|
6
|
+
import { getCommandNames } from "./commands/registry";
|
|
5
7
|
import { makeDefaultEnv, runCommand } from "./commands/runtime";
|
|
6
8
|
import { spawnNanoEditorProcess } from "./modules/shellInteractive";
|
|
9
|
+
import { resolvePath } from "./modules/shellRuntime";
|
|
7
10
|
import { buildLoginBanner } from "./SSHMimic/loginBanner";
|
|
8
11
|
import { buildPrompt } from "./SSHMimic/prompt";
|
|
9
12
|
import { VirtualShell } from "./VirtualShell";
|
|
10
13
|
const hostname = process.env.SSH_MIMIC_HOSTNAME ?? "typescript-vm";
|
|
11
14
|
const argv = process.argv.slice(2);
|
|
15
|
+
// ── CLI args ──────────────────────────────────────────────────────────────────
|
|
12
16
|
function readUserArg() {
|
|
13
17
|
for (let index = 0; index < argv.length; index += 1) {
|
|
14
18
|
const current = argv[index];
|
|
@@ -30,11 +34,11 @@ const virtualShell = new VirtualShell(hostname, undefined, {
|
|
|
30
34
|
mode: "fs",
|
|
31
35
|
snapshotPath: ".vfs",
|
|
32
36
|
});
|
|
37
|
+
// ── VFS helpers ───────────────────────────────────────────────────────────────
|
|
33
38
|
function readLastLogin(username) {
|
|
34
|
-
const lastlogPath = `/
|
|
35
|
-
if (!virtualShell.vfs.exists(lastlogPath))
|
|
39
|
+
const lastlogPath = `/home/${username}/.lastlog`;
|
|
40
|
+
if (!virtualShell.vfs.exists(lastlogPath))
|
|
36
41
|
return null;
|
|
37
|
-
}
|
|
38
42
|
try {
|
|
39
43
|
return JSON.parse(virtualShell.vfs.readFile(lastlogPath));
|
|
40
44
|
}
|
|
@@ -42,6 +46,63 @@ function readLastLogin(username) {
|
|
|
42
46
|
return null;
|
|
43
47
|
}
|
|
44
48
|
}
|
|
49
|
+
function writeLastLogin(username, from) {
|
|
50
|
+
virtualShell.vfs.writeFile(`/home/${username}/.lastlog`, JSON.stringify({ at: new Date().toISOString(), from }));
|
|
51
|
+
}
|
|
52
|
+
async function flushVfs() {
|
|
53
|
+
await virtualShell.vfs.flushMirror();
|
|
54
|
+
}
|
|
55
|
+
function loadHistory(authUser) {
|
|
56
|
+
const historyPath = `/home/${authUser}/.bash_history`;
|
|
57
|
+
if (!virtualShell.vfs.exists(historyPath)) {
|
|
58
|
+
virtualShell.vfs.writeFile(historyPath, "");
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
return virtualShell.vfs
|
|
62
|
+
.readFile(historyPath)
|
|
63
|
+
.split("\n")
|
|
64
|
+
.map((line) => line.trim())
|
|
65
|
+
.filter((line) => line.length > 0);
|
|
66
|
+
}
|
|
67
|
+
function saveHistory(history, authUser) {
|
|
68
|
+
const data = history.length > 0 ? `${history.join("\n")}\n` : "";
|
|
69
|
+
virtualShell.vfs.writeFile(`/home/${authUser}/.bash_history`, data);
|
|
70
|
+
}
|
|
71
|
+
// ── Tab completion ────────────────────────────────────────────────────────────
|
|
72
|
+
function listPathCompletions(vfs, cwd, prefix) {
|
|
73
|
+
const slashIndex = prefix.lastIndexOf("/");
|
|
74
|
+
const dirPart = slashIndex >= 0 ? prefix.slice(0, slashIndex + 1) : "";
|
|
75
|
+
const namePart = slashIndex >= 0 ? prefix.slice(slashIndex + 1) : prefix;
|
|
76
|
+
const basePath = resolvePath(cwd, dirPart || ".");
|
|
77
|
+
try {
|
|
78
|
+
return vfs
|
|
79
|
+
.list(basePath)
|
|
80
|
+
.filter((e) => !e.startsWith(".") && e.startsWith(namePart))
|
|
81
|
+
.map((e) => {
|
|
82
|
+
const fullPath = path.posix.join(basePath, e);
|
|
83
|
+
const st = vfs.stat(fullPath);
|
|
84
|
+
return `${dirPart}${e}${st.type === "directory" ? "/" : ""}`;
|
|
85
|
+
})
|
|
86
|
+
.sort();
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function makeCompleter(getState) {
|
|
93
|
+
const commandNames = Array.from(new Set(getCommandNames())).sort();
|
|
94
|
+
return (line, cb) => {
|
|
95
|
+
const { cwd } = getState();
|
|
96
|
+
// Extract the token under/before cursor (last whitespace-separated word)
|
|
97
|
+
const token = line.split(/\s+/).at(-1) ?? "";
|
|
98
|
+
const isFirstToken = line.trimStart() === token;
|
|
99
|
+
const cmdHits = isFirstToken ? commandNames.filter((n) => n.startsWith(token)) : [];
|
|
100
|
+
const pathHits = listPathCompletions(virtualShell.vfs, cwd, token);
|
|
101
|
+
const hits = Array.from(new Set([...cmdHits, ...pathHits])).sort();
|
|
102
|
+
cb(null, [hits, token]);
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
// ── Hidden password input ─────────────────────────────────────────────────────
|
|
45
106
|
function askHiddenQuestion(rl, promptText) {
|
|
46
107
|
return new Promise((resolve) => {
|
|
47
108
|
if (!stdin.isTTY || !stdout.isTTY) {
|
|
@@ -52,10 +113,8 @@ function askHiddenQuestion(rl, promptText) {
|
|
|
52
113
|
let buffer = "";
|
|
53
114
|
const cleanup = () => {
|
|
54
115
|
stdin.off("data", onData);
|
|
55
|
-
if (!wasRawMode)
|
|
116
|
+
if (!wasRawMode)
|
|
56
117
|
stdin.setRawMode(false);
|
|
57
|
-
}
|
|
58
|
-
rl.resume();
|
|
59
118
|
};
|
|
60
119
|
const finish = (value) => {
|
|
61
120
|
cleanup();
|
|
@@ -64,8 +123,8 @@ function askHiddenQuestion(rl, promptText) {
|
|
|
64
123
|
};
|
|
65
124
|
const onData = (chunk) => {
|
|
66
125
|
const input = chunk.toString("utf8");
|
|
67
|
-
for (let
|
|
68
|
-
const ch = input[
|
|
126
|
+
for (let i = 0; i < input.length; i += 1) {
|
|
127
|
+
const ch = input[i];
|
|
69
128
|
if (ch === "\r" || ch === "\n") {
|
|
70
129
|
finish(buffer);
|
|
71
130
|
return;
|
|
@@ -74,46 +133,20 @@ function askHiddenQuestion(rl, promptText) {
|
|
|
74
133
|
buffer = buffer.slice(0, -1);
|
|
75
134
|
continue;
|
|
76
135
|
}
|
|
77
|
-
if (ch >= " ")
|
|
136
|
+
if (ch >= " ")
|
|
78
137
|
buffer += ch;
|
|
79
|
-
}
|
|
80
138
|
}
|
|
81
139
|
};
|
|
140
|
+
// Pause readline so it doesn't eat our raw keystrokes
|
|
82
141
|
rl.pause();
|
|
83
142
|
stdout.write(promptText);
|
|
84
|
-
if (!wasRawMode)
|
|
143
|
+
if (!wasRawMode)
|
|
85
144
|
stdin.setRawMode(true);
|
|
86
|
-
}
|
|
87
145
|
stdin.resume();
|
|
88
146
|
stdin.on("data", onData);
|
|
89
147
|
});
|
|
90
148
|
}
|
|
91
|
-
|
|
92
|
-
const dir = "/virtual-env-js/.lastlog";
|
|
93
|
-
if (!virtualShell.vfs.exists(dir)) {
|
|
94
|
-
virtualShell.vfs.mkdir(dir, 0o700);
|
|
95
|
-
}
|
|
96
|
-
virtualShell.vfs.writeFile(`/virtual-env-js/.lastlog/${username}.json`, JSON.stringify({ at: new Date().toISOString(), from }));
|
|
97
|
-
}
|
|
98
|
-
async function flushVfs() {
|
|
99
|
-
await virtualShell.vfs.flushMirror();
|
|
100
|
-
}
|
|
101
|
-
function loadHistory() {
|
|
102
|
-
const historyPath = "/virtual-env-js/.bash_history";
|
|
103
|
-
if (!virtualShell.vfs.exists(historyPath)) {
|
|
104
|
-
virtualShell.vfs.writeFile(historyPath, "");
|
|
105
|
-
return [];
|
|
106
|
-
}
|
|
107
|
-
return virtualShell.vfs
|
|
108
|
-
.readFile(historyPath)
|
|
109
|
-
.split("\n")
|
|
110
|
-
.map((line) => line.trim())
|
|
111
|
-
.filter((line) => line.length > 0);
|
|
112
|
-
}
|
|
113
|
-
function saveHistory(history) {
|
|
114
|
-
const data = history.length > 0 ? `${history.join("\n")}\n` : "";
|
|
115
|
-
virtualShell.vfs.writeFile("/virtual-env-js/.bash_history", data);
|
|
116
|
-
}
|
|
149
|
+
// ── Session state helper ──────────────────────────────────────────────────────
|
|
117
150
|
function applySessionState(authUserState, cwdState, result, shellEnvState) {
|
|
118
151
|
let authUser = authUserState;
|
|
119
152
|
let cwd = cwdState;
|
|
@@ -131,21 +164,16 @@ function applySessionState(authUserState, cwdState, result, shellEnvState) {
|
|
|
131
164
|
}
|
|
132
165
|
return { authUser, cwd };
|
|
133
166
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
167
|
+
// ── Demo command ──────────────────────────────────────────────────────────────
|
|
168
|
+
virtualShell.addCommand("demo", [], () => ({
|
|
169
|
+
stdout: "This is a demo command. It does nothing useful.",
|
|
170
|
+
exitCode: 0,
|
|
171
|
+
}));
|
|
172
|
+
// ── Main shell ────────────────────────────────────────────────────────────────
|
|
140
173
|
async function runReadlineShell() {
|
|
141
|
-
const rl = createInterface({ input: stdin, output: stdout, terminal: true });
|
|
142
174
|
await virtualShell.ensureInitialized();
|
|
143
|
-
let history = loadHistory();
|
|
144
|
-
const rlWithHistory = rl;
|
|
145
|
-
rlWithHistory.history = [...history].reverse();
|
|
146
175
|
const selectedUser = initialUser.trim() || "root";
|
|
147
|
-
|
|
148
|
-
if (!userExists) {
|
|
176
|
+
if (virtualShell.users.getPasswordHash(selectedUser) === null) {
|
|
149
177
|
process.stderr.write(`self-standalone: user '${selectedUser}' does not exist\n`);
|
|
150
178
|
process.exit(1);
|
|
151
179
|
}
|
|
@@ -154,10 +182,19 @@ async function runReadlineShell() {
|
|
|
154
182
|
let cwd = `/home/${authUser}`;
|
|
155
183
|
shellEnv.vars.PWD = cwd;
|
|
156
184
|
const remoteAddress = "localhost";
|
|
157
|
-
const terminalSize = {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
185
|
+
const terminalSize = { cols: stdout.columns ?? 80, rows: stdout.rows ?? 24 };
|
|
186
|
+
let history = loadHistory(authUser);
|
|
187
|
+
// completer reads cwd via closure — always current
|
|
188
|
+
const rl = createInterface({
|
|
189
|
+
input: stdin,
|
|
190
|
+
output: stdout,
|
|
191
|
+
terminal: true,
|
|
192
|
+
completer: makeCompleter(() => ({ cwd })),
|
|
193
|
+
});
|
|
194
|
+
// Sync readline's internal history with our VFS history
|
|
195
|
+
const rlWithHistory = rl;
|
|
196
|
+
rlWithHistory.history = [...history].reverse();
|
|
197
|
+
// ── nano editor ────────────────────────────────────────────────────────────
|
|
161
198
|
async function startNanoEditor(targetPath, initialContent, tempPath) {
|
|
162
199
|
if (virtualShell.vfs.exists(targetPath)) {
|
|
163
200
|
await writeFile(tempPath, initialContent, "utf8");
|
|
@@ -169,20 +206,16 @@ async function runReadlineShell() {
|
|
|
169
206
|
end: () => undefined,
|
|
170
207
|
});
|
|
171
208
|
const wasRawMode = Boolean(stdin.isRaw);
|
|
172
|
-
const forwardInput = (chunk) => {
|
|
173
|
-
editor.stdin.write(chunk);
|
|
174
|
-
};
|
|
209
|
+
const forwardInput = (chunk) => { editor.stdin.write(chunk); };
|
|
175
210
|
stdin.resume();
|
|
176
|
-
if (!wasRawMode)
|
|
211
|
+
if (!wasRawMode)
|
|
177
212
|
stdin.setRawMode(true);
|
|
178
|
-
}
|
|
179
213
|
stdin.on("data", forwardInput);
|
|
180
214
|
await new Promise((resolve) => {
|
|
181
215
|
const cleanup = () => {
|
|
182
216
|
stdin.off("data", forwardInput);
|
|
183
|
-
if (!wasRawMode)
|
|
217
|
+
if (!wasRawMode)
|
|
184
218
|
stdin.setRawMode(false);
|
|
185
|
-
}
|
|
186
219
|
rl.resume();
|
|
187
220
|
};
|
|
188
221
|
editor.on("error", (error) => {
|
|
@@ -199,7 +232,7 @@ async function runReadlineShell() {
|
|
|
199
232
|
await flushVfs();
|
|
200
233
|
}
|
|
201
234
|
catch {
|
|
202
|
-
//
|
|
235
|
+
// save skipped or temp file missing
|
|
203
236
|
}
|
|
204
237
|
await unlink(tempPath).catch(() => undefined);
|
|
205
238
|
stdout.write("\r\n");
|
|
@@ -207,6 +240,7 @@ async function runReadlineShell() {
|
|
|
207
240
|
});
|
|
208
241
|
});
|
|
209
242
|
}
|
|
243
|
+
// ── challenge handlers ─────────────────────────────────────────────────────
|
|
210
244
|
async function handleSudoChallenge(challenge) {
|
|
211
245
|
if (challenge.onPassword) {
|
|
212
246
|
let promptText = challenge.prompt;
|
|
@@ -275,6 +309,7 @@ async function runReadlineShell() {
|
|
|
275
309
|
break;
|
|
276
310
|
}
|
|
277
311
|
}
|
|
312
|
+
// handleCommandResult must be declared before the "line" handler
|
|
278
313
|
async function handleCommandResult(result) {
|
|
279
314
|
if (result.openEditor) {
|
|
280
315
|
await startNanoEditor(result.openEditor.targetPath, result.openEditor.initialContent, result.openEditor.tempPath);
|
|
@@ -288,32 +323,26 @@ async function runReadlineShell() {
|
|
|
288
323
|
await handlePasswordChallenge(result.passwordChallenge);
|
|
289
324
|
return;
|
|
290
325
|
}
|
|
326
|
+
if (result.clearScreen) {
|
|
327
|
+
stdout.write("\u001b[2J\u001b[H");
|
|
328
|
+
console.clear();
|
|
329
|
+
}
|
|
291
330
|
if (result.stdout) {
|
|
292
331
|
stdout.write(result.stdout.endsWith("\n") ? result.stdout : `${result.stdout}\n`);
|
|
293
332
|
}
|
|
294
333
|
if (result.stderr) {
|
|
295
334
|
process.stderr.write(result.stderr.endsWith("\n") ? result.stderr : `${result.stderr}\n`);
|
|
296
335
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
}
|
|
301
|
-
const updatedState = applySessionState(authUser, cwd, result, shellEnv);
|
|
302
|
-
authUser = updatedState.authUser;
|
|
303
|
-
cwd = updatedState.cwd;
|
|
336
|
+
const updated = applySessionState(authUser, cwd, result, shellEnv);
|
|
337
|
+
authUser = updated.authUser;
|
|
338
|
+
cwd = updated.cwd;
|
|
304
339
|
if (result.closeSession) {
|
|
305
340
|
await flushVfs();
|
|
306
341
|
rl.close();
|
|
307
342
|
process.exit(result.exitCode ?? 0);
|
|
308
343
|
}
|
|
309
344
|
}
|
|
310
|
-
|
|
311
|
-
const password = await askHiddenQuestion(rl, `Password for ${authUser}: `);
|
|
312
|
-
if (!virtualShell.users.verifyPassword(authUser, password)) {
|
|
313
|
-
process.stderr.write("self-standalone: authentication failed\n");
|
|
314
|
-
process.exit(1);
|
|
315
|
-
}
|
|
316
|
-
}
|
|
345
|
+
// ── Prompt helper ──────────────────────────────────────────────────────────
|
|
317
346
|
const renderPrompt = () => {
|
|
318
347
|
const cwdLabel = cwd === `/home/${authUser}` ? "~" : basename(cwd) || "/";
|
|
319
348
|
return buildPrompt(authUser, hostname, cwdLabel);
|
|
@@ -322,50 +351,68 @@ async function runReadlineShell() {
|
|
|
322
351
|
rl.setPrompt(renderPrompt());
|
|
323
352
|
rl.prompt();
|
|
324
353
|
};
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
process.exit(0);
|
|
335
|
-
})();
|
|
336
|
-
});
|
|
354
|
+
// ── Auth (password gate) ───────────────────────────────────────────────────
|
|
355
|
+
if (process.env.USER !== "root" && virtualShell.users.hasPassword(authUser)) {
|
|
356
|
+
const password = await askHiddenQuestion(rl, `Password for ${authUser}: `);
|
|
357
|
+
if (!virtualShell.users.verifyPassword(authUser, password)) {
|
|
358
|
+
process.stderr.write("self-standalone: authentication failed\n");
|
|
359
|
+
process.exit(1);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
// ── Login banner ───────────────────────────────────────────────────────────
|
|
337
363
|
stdout.write(buildLoginBanner(hostname, virtualShell.properties, readLastLogin(authUser)));
|
|
338
364
|
writeLastLogin(authUser, remoteAddress);
|
|
339
365
|
await flushVfs();
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
366
|
+
// ── Event-driven line handler (enables completer) ──────────────────────────
|
|
367
|
+
//
|
|
368
|
+
// Key insight: readline's completer only fires when readline itself owns
|
|
369
|
+
// stdin (i.e. rl is not paused). We use the event-driven "line" pattern
|
|
370
|
+
// instead of a while(true)+rl.once("line") loop so readline stays active
|
|
371
|
+
// between commands. We pause only while awaiting async work, then resume
|
|
372
|
+
// immediately before re-prompting so the next Tab press is caught.
|
|
373
|
+
let busy = false;
|
|
374
|
+
rl.on("line", async (inputLine) => {
|
|
375
|
+
if (busy)
|
|
376
|
+
return; // shouldn't happen but guard re-entrancy
|
|
377
|
+
busy = true;
|
|
345
378
|
rl.pause();
|
|
346
|
-
|
|
379
|
+
const trimmed = inputLine.trim();
|
|
380
|
+
if (trimmed.length > 0) {
|
|
347
381
|
history.push(inputLine);
|
|
348
|
-
if (history.length > 500)
|
|
382
|
+
if (history.length > 500)
|
|
349
383
|
history = history.slice(history.length - 500);
|
|
350
|
-
|
|
351
|
-
saveHistory(history);
|
|
384
|
+
saveHistory(history, authUser);
|
|
352
385
|
rlWithHistory.history = [...history].reverse();
|
|
353
386
|
}
|
|
354
387
|
const result = await runCommand(inputLine, authUser, hostname, "shell", cwd, virtualShell, undefined, shellEnv);
|
|
355
388
|
await handleCommandResult(result);
|
|
356
389
|
await flushVfs();
|
|
357
|
-
|
|
390
|
+
busy = false;
|
|
391
|
+
// Resume before prompt so readline can handle Tab on the next input
|
|
358
392
|
rl.resume();
|
|
359
|
-
|
|
393
|
+
prompt();
|
|
394
|
+
});
|
|
395
|
+
rl.on("SIGINT", () => {
|
|
396
|
+
stdout.write("^C\n");
|
|
397
|
+
rl.write("", { ctrl: true, name: "u" });
|
|
398
|
+
prompt();
|
|
399
|
+
});
|
|
400
|
+
rl.on("close", () => {
|
|
401
|
+
void flushVfs().then(() => {
|
|
402
|
+
console.log("");
|
|
403
|
+
process.exit(0);
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
// Initial prompt — readline is already active, completer live from first keystroke
|
|
407
|
+
prompt();
|
|
360
408
|
}
|
|
361
409
|
runReadlineShell().catch((error) => {
|
|
362
410
|
console.error("Failed to start readline SSH emulation:", error);
|
|
363
411
|
process.exit(1);
|
|
364
412
|
});
|
|
365
413
|
process.on("uncaughtException", (error) => {
|
|
366
|
-
console.
|
|
414
|
+
console.error("Uncaught exception:", error);
|
|
367
415
|
});
|
|
368
416
|
process.on("unhandledRejection", (error, promise) => {
|
|
369
|
-
console.
|
|
370
|
-
console.log(" The error was: ", error);
|
|
417
|
+
console.error("Unhandled rejection at:", promise, "error:", error);
|
|
371
418
|
});
|
package/dist/types/pipeline.d.ts
CHANGED
|
@@ -10,6 +10,12 @@ export interface PipelineCommand {
|
|
|
10
10
|
outputFile?: string;
|
|
11
11
|
/** Append to output file (>> file) */
|
|
12
12
|
appendOutput?: boolean;
|
|
13
|
+
/** Stderr redirection file path (2> file) */
|
|
14
|
+
stderrFile?: string;
|
|
15
|
+
/** Append stderr to file (2>> file) */
|
|
16
|
+
stderrAppend?: boolean;
|
|
17
|
+
/** Redirect stderr to stdout (2>&1) */
|
|
18
|
+
stderrToStdout?: boolean;
|
|
13
19
|
}
|
|
14
20
|
/** Logical operator connecting two statement groups. */
|
|
15
21
|
export type LogicalOp = "&&" | "||" | ";";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../../src/types/pipeline.ts"],"names":[],"mappings":"AAAA,iDAAiD;AACjD,MAAM,WAAW,eAAe;IAC/B,mBAAmB;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,wBAAwB;IACxB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,2CAA2C;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4CAA4C;IAC5C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,sCAAsC;IACtC,YAAY,CAAC,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../../src/types/pipeline.ts"],"names":[],"mappings":"AAAA,iDAAiD;AACjD,MAAM,WAAW,eAAe;IAC/B,mBAAmB;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,wBAAwB;IACxB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,2CAA2C;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4CAA4C;IAC5C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,sCAAsC;IACtC,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,6CAA6C;IAC7C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,uCAAuC;IACvC,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,uCAAuC;IACvC,cAAc,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,wDAAwD;AACxD,MAAM,MAAM,SAAS,GAAG,IAAI,GAAG,IAAI,GAAG,GAAG,CAAC;AAE1C,yCAAyC;AACzC,MAAM,WAAW,QAAQ;IACxB,uCAAuC;IACvC,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,uCAAuC;IACvC,OAAO,EAAE,OAAO,CAAC;IACjB,sCAAsC;IACtC,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAED,0FAA0F;AAC1F,MAAM,WAAW,SAAS;IACzB,8CAA8C;IAC9C,QAAQ,EAAE,QAAQ,CAAC;IACnB,0DAA0D;IAC1D,EAAE,CAAC,EAAE,SAAS,CAAC;IACf,2CAA2C;IAC3C,IAAI,CAAC,EAAE,SAAS,CAAC;CACjB;AAED,2CAA2C;AAC3C,MAAM,WAAW,MAAM;IACtB,0CAA0C;IAC1C,UAAU,EAAE,SAAS,EAAE,CAAC;IACxB,kDAAkD;IAClD,OAAO,EAAE,OAAO,CAAC;IACjB,oCAAoC;IACpC,KAAK,CAAC,EAAE,MAAM,CAAC;CACf"}
|
package/dist/types/vfs.d.ts
CHANGED
|
@@ -68,4 +68,19 @@ export type VfsSnapshotNode = VfsSnapshotFileNode | VfsSnapshotDirectoryNode;
|
|
|
68
68
|
export interface VfsSnapshot {
|
|
69
69
|
root: VfsSnapshotDirectoryNode;
|
|
70
70
|
}
|
|
71
|
+
/** Options for mounting a host directory into the VFS. */
|
|
72
|
+
export interface MountOptions {
|
|
73
|
+
/** Absolute path inside the VM (e.g. `"/app"`). */
|
|
74
|
+
vPath: string;
|
|
75
|
+
/** Path on the host filesystem. Relative paths resolved from `process.cwd()`. */
|
|
76
|
+
hostPath: string;
|
|
77
|
+
/** When `true` (default), write operations inside the mount throw `EROFS`. */
|
|
78
|
+
readOnly?: boolean;
|
|
79
|
+
}
|
|
80
|
+
/** Describes an active mount point. */
|
|
81
|
+
export interface MountPoint {
|
|
82
|
+
vPath: string;
|
|
83
|
+
hostPath: string;
|
|
84
|
+
readOnly: boolean;
|
|
85
|
+
}
|
|
71
86
|
//# sourceMappingURL=vfs.d.ts.map
|
package/dist/types/vfs.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vfs.d.ts","sourceRoot":"","sources":["../../src/types/vfs.ts"],"names":[],"mappings":"AAAA,oCAAoC;AACpC,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,WAAW,CAAC;AAE/C,oEAAoE;AACpE,MAAM,WAAW,WAAW;IAC3B,qCAAqC;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,qCAAqC;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,4BAA4B;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,+BAA+B;IAC/B,SAAS,EAAE,IAAI,CAAC;IAChB,6BAA6B;IAC7B,SAAS,EAAE,IAAI,CAAC;CAChB;AAED,0CAA0C;AAC1C,MAAM,WAAW,WAAY,SAAQ,WAAW;IAC/C,IAAI,EAAE,MAAM,CAAC;IACb,mDAAmD;IACnD,UAAU,EAAE,OAAO,CAAC;IACpB,4DAA4D;IAC5D,IAAI,EAAE,MAAM,CAAC;CACb;AAED,+CAA+C;AAC/C,MAAM,WAAW,gBAAiB,SAAQ,WAAW;IACpD,IAAI,EAAE,WAAW,CAAC;IAClB,8CAA8C;IAC9C,aAAa,EAAE,MAAM,CAAC;CACtB;AAED,kDAAkD;AAClD,MAAM,MAAM,YAAY,GAAG,WAAW,GAAG,gBAAgB,CAAC;AAE1D,wDAAwD;AACxD,MAAM,WAAW,gBAAgB;IAChC,uDAAuD;IACvD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,0CAA0C;IAC1C,QAAQ,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,qDAAqD;AACrD,MAAM,WAAW,aAAa;IAC7B,gDAAgD;IAChD,SAAS,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,gEAAgE;AAChE,MAAM,WAAW,mBAAmB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,mCAAmC;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,iCAAiC;IACjC,SAAS,EAAE,MAAM,CAAC;CAClB;AAED,gDAAgD;AAChD,MAAM,WAAW,mBAAoB,SAAQ,mBAAmB;IAC/D,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,OAAO,CAAC;IACpB,qCAAqC;IACrC,aAAa,EAAE,MAAM,CAAC;CACtB;AAED,qDAAqD;AACrD,MAAM,WAAW,wBAAyB,SAAQ,mBAAmB;IACpE,IAAI,EAAE,WAAW,CAAC;IAClB,QAAQ,EAAE,eAAe,EAAE,CAAC;CAC5B;AAED,kDAAkD;AAClD,MAAM,MAAM,eAAe,GAAG,mBAAmB,GAAG,wBAAwB,CAAC;AAE7E,gDAAgD;AAChD,MAAM,WAAW,WAAW;IAC3B,IAAI,EAAE,wBAAwB,CAAC;CAC/B"}
|
|
1
|
+
{"version":3,"file":"vfs.d.ts","sourceRoot":"","sources":["../../src/types/vfs.ts"],"names":[],"mappings":"AAAA,oCAAoC;AACpC,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,WAAW,CAAC;AAE/C,oEAAoE;AACpE,MAAM,WAAW,WAAW;IAC3B,qCAAqC;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,qCAAqC;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,4BAA4B;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,+BAA+B;IAC/B,SAAS,EAAE,IAAI,CAAC;IAChB,6BAA6B;IAC7B,SAAS,EAAE,IAAI,CAAC;CAChB;AAED,0CAA0C;AAC1C,MAAM,WAAW,WAAY,SAAQ,WAAW;IAC/C,IAAI,EAAE,MAAM,CAAC;IACb,mDAAmD;IACnD,UAAU,EAAE,OAAO,CAAC;IACpB,4DAA4D;IAC5D,IAAI,EAAE,MAAM,CAAC;CACb;AAED,+CAA+C;AAC/C,MAAM,WAAW,gBAAiB,SAAQ,WAAW;IACpD,IAAI,EAAE,WAAW,CAAC;IAClB,8CAA8C;IAC9C,aAAa,EAAE,MAAM,CAAC;CACtB;AAED,kDAAkD;AAClD,MAAM,MAAM,YAAY,GAAG,WAAW,GAAG,gBAAgB,CAAC;AAE1D,wDAAwD;AACxD,MAAM,WAAW,gBAAgB;IAChC,uDAAuD;IACvD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,0CAA0C;IAC1C,QAAQ,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,qDAAqD;AACrD,MAAM,WAAW,aAAa;IAC7B,gDAAgD;IAChD,SAAS,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,gEAAgE;AAChE,MAAM,WAAW,mBAAmB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,mCAAmC;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,iCAAiC;IACjC,SAAS,EAAE,MAAM,CAAC;CAClB;AAED,gDAAgD;AAChD,MAAM,WAAW,mBAAoB,SAAQ,mBAAmB;IAC/D,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,OAAO,CAAC;IACpB,qCAAqC;IACrC,aAAa,EAAE,MAAM,CAAC;CACtB;AAED,qDAAqD;AACrD,MAAM,WAAW,wBAAyB,SAAQ,mBAAmB;IACpE,IAAI,EAAE,WAAW,CAAC;IAClB,QAAQ,EAAE,eAAe,EAAE,CAAC;CAC5B;AAED,kDAAkD;AAClD,MAAM,MAAM,eAAe,GAAG,mBAAmB,GAAG,wBAAwB,CAAC;AAE7E,gDAAgD;AAChD,MAAM,WAAW,WAAW;IAC3B,IAAI,EAAE,wBAAwB,CAAC;CAC/B;AAED,0DAA0D;AAC1D,MAAM,WAAW,YAAY;IAC5B,mDAAmD;IACnD,KAAK,EAAE,MAAM,CAAC;IACd,iFAAiF;IACjF,QAAQ,EAAE,MAAM,CAAC;IACjB,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,uCAAuC;AACvC,MAAM,WAAW,UAAU;IAC1B,KAAK,EAAK,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,OAAO,CAAC;CAClB"}
|
package/dist/utils/expand.d.ts
CHANGED
|
@@ -34,6 +34,15 @@ export declare function evalArith(expr: string, env: Record<string, string>): nu
|
|
|
34
34
|
* @param lastExit Last command exit code (for `$?`).
|
|
35
35
|
* @param home Home directory path (for `~`).
|
|
36
36
|
*/
|
|
37
|
+
/**
|
|
38
|
+
* Expand brace expressions in a single token.
|
|
39
|
+
* - `{a,b,c}` → `["a", "b", "c"]`
|
|
40
|
+
* - `{1..5}` → `["1", "2", "3", "4", "5"]`
|
|
41
|
+
* - `{a..e}` → `["a", "b", "c", "d", "e"]`
|
|
42
|
+
* - `prefix{a,b}suffix` → `["prefixasuffix", "prefixbsuffix"]`
|
|
43
|
+
* Returns a single-element array when no brace expansion applies.
|
|
44
|
+
*/
|
|
45
|
+
export declare function expandBraces(token: string): string[];
|
|
37
46
|
export declare function expandSync(input: string, env: Record<string, string>, lastExit?: number, home?: string): string;
|
|
38
47
|
/**
|
|
39
48
|
* Expand all shell forms including `$(cmd)` command substitution.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"expand.d.ts","sourceRoot":"","sources":["../../src/utils/expand.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAIH;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAuB3E;AAoCD;;;;;;;;;GASG;AACH,wBAAgB,UAAU,CACzB,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC3B,QAAQ,SAAI,EACZ,IAAI,CAAC,EAAE,MAAM,GACX,MAAM,CA4DR;AAID;;;;;;;;;;GAUG;AACH,wBAAsB,WAAW,CAChC,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC3B,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,GACtC,OAAO,CAAC,MAAM,CAAC,CAuDjB"}
|
|
1
|
+
{"version":3,"file":"expand.d.ts","sourceRoot":"","sources":["../../src/utils/expand.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAIH;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAuB3E;AAoCD;;;;;;;;;GASG;AAEH;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CA8DpD;AAED,wBAAgB,UAAU,CACzB,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC3B,QAAQ,SAAI,EACZ,IAAI,CAAC,EAAE,MAAM,GACX,MAAM,CA4DR;AAID;;;;;;;;;;GAUG;AACH,wBAAsB,WAAW,CAChC,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC3B,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,GACtC,OAAO,CAAC,MAAM,CAAC,CAuDjB"}
|
package/dist/utils/expand.js
CHANGED
|
@@ -82,6 +82,88 @@ function outsideSingleQuotes(input, replacer) {
|
|
|
82
82
|
* @param lastExit Last command exit code (for `$?`).
|
|
83
83
|
* @param home Home directory path (for `~`).
|
|
84
84
|
*/
|
|
85
|
+
/**
|
|
86
|
+
* Expand brace expressions in a single token.
|
|
87
|
+
* - `{a,b,c}` → `["a", "b", "c"]`
|
|
88
|
+
* - `{1..5}` → `["1", "2", "3", "4", "5"]`
|
|
89
|
+
* - `{a..e}` → `["a", "b", "c", "d", "e"]`
|
|
90
|
+
* - `prefix{a,b}suffix` → `["prefixasuffix", "prefixbsuffix"]`
|
|
91
|
+
* Returns a single-element array when no brace expansion applies.
|
|
92
|
+
*/
|
|
93
|
+
export function expandBraces(token) {
|
|
94
|
+
// Find the first { not preceded by $
|
|
95
|
+
let depth = 0;
|
|
96
|
+
let start = -1;
|
|
97
|
+
for (let i = 0; i < token.length; i++) {
|
|
98
|
+
const ch = token[i];
|
|
99
|
+
if (ch === "{" && token[i - 1] !== "$") {
|
|
100
|
+
if (depth === 0)
|
|
101
|
+
start = i;
|
|
102
|
+
depth++;
|
|
103
|
+
}
|
|
104
|
+
else if (ch === "}") {
|
|
105
|
+
depth--;
|
|
106
|
+
if (depth === 0 && start !== -1) {
|
|
107
|
+
const prefix = token.slice(0, start);
|
|
108
|
+
const inner = token.slice(start + 1, i);
|
|
109
|
+
const suffix = token.slice(i + 1);
|
|
110
|
+
// Range: {1..5} or {a..e}
|
|
111
|
+
const rangeMatch = inner.match(/^(-?\d+)\.\.(-?\d+)(?:\.\.-?(\d+))?$/) ||
|
|
112
|
+
inner.match(/^([a-z])\.\.([a-z])$/);
|
|
113
|
+
if (rangeMatch) {
|
|
114
|
+
const items = [];
|
|
115
|
+
if (/\d/.test(rangeMatch[1])) {
|
|
116
|
+
const from = parseInt(rangeMatch[1], 10);
|
|
117
|
+
const to = parseInt(rangeMatch[2], 10);
|
|
118
|
+
const step = rangeMatch[3] ? parseInt(rangeMatch[3], 10) : 1;
|
|
119
|
+
const inc = from <= to ? step : -step;
|
|
120
|
+
for (let n = from; from <= to ? n <= to : n >= to; n += inc) {
|
|
121
|
+
items.push(String(n));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
const from = rangeMatch[1].charCodeAt(0);
|
|
126
|
+
const to = rangeMatch[2].charCodeAt(0);
|
|
127
|
+
const inc = from <= to ? 1 : -1;
|
|
128
|
+
for (let c = from; from <= to ? c <= to : c >= to; c += inc) {
|
|
129
|
+
items.push(String.fromCharCode(c));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const expanded = items.map((v) => `${prefix}${v}${suffix}`);
|
|
133
|
+
return expanded.flatMap(expandBraces);
|
|
134
|
+
}
|
|
135
|
+
// Comma list: {a,b,c} — split respecting nested braces
|
|
136
|
+
const parts = [];
|
|
137
|
+
let cur = "";
|
|
138
|
+
let d2 = 0;
|
|
139
|
+
for (const ch2 of inner) {
|
|
140
|
+
if (ch2 === "{") {
|
|
141
|
+
d2++;
|
|
142
|
+
cur += ch2;
|
|
143
|
+
}
|
|
144
|
+
else if (ch2 === "}") {
|
|
145
|
+
d2--;
|
|
146
|
+
cur += ch2;
|
|
147
|
+
}
|
|
148
|
+
else if (ch2 === "," && d2 === 0) {
|
|
149
|
+
parts.push(cur);
|
|
150
|
+
cur = "";
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
cur += ch2;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
parts.push(cur);
|
|
157
|
+
if (parts.length > 1) {
|
|
158
|
+
const expanded = parts.map((p) => `${prefix}${p}${suffix}`);
|
|
159
|
+
return expanded.flatMap(expandBraces);
|
|
160
|
+
}
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return [token];
|
|
166
|
+
}
|
|
85
167
|
export function expandSync(input, env, lastExit = 0, home) {
|
|
86
168
|
const homePath = home ?? env.HOME ?? "/home/user";
|
|
87
169
|
return outsideSingleQuotes(input, (chunk) => {
|
|
@@ -111,8 +193,8 @@ export function expandSync(input, env, lastExit = 0, home) {
|
|
|
111
193
|
s = s.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*):\+([^}]*)\}/g, (_, name, alt) => env[name] !== undefined && env[name] !== "" ? alt : "");
|
|
112
194
|
// ${VAR}
|
|
113
195
|
s = s.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_, name) => env[name] ?? "");
|
|
114
|
-
// $VAR
|
|
115
|
-
s = s.replace(/\$([A-Za-z_][A-Za-z0-9_]
|
|
196
|
+
// $VAR and positional params $1 $2 ...
|
|
197
|
+
s = s.replace(/\$([A-Za-z_][A-Za-z0-9_]*|\d+)/g, (_, name) => env[name] ?? "");
|
|
116
198
|
return s;
|
|
117
199
|
});
|
|
118
200
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tokenize.d.ts","sourceRoot":"","sources":["../../src/utils/tokenize.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,
|
|
1
|
+
{"version":3,"file":"tokenize.d.ts","sourceRoot":"","sources":["../../src/utils/tokenize.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CA8EvD"}
|
package/dist/utils/tokenize.js
CHANGED
|
@@ -50,6 +50,46 @@ export function tokenizeCommand(input) {
|
|
|
50
50
|
i++;
|
|
51
51
|
continue;
|
|
52
52
|
}
|
|
53
|
+
// Handle 2>&1, 2>>, 2>, >&, >>
|
|
54
|
+
if (!inQ && ch === "2" && (next === ">")) {
|
|
55
|
+
const rest = input.slice(i + 1);
|
|
56
|
+
if (rest.startsWith(">>&1") || rest.startsWith(">> &1")) {
|
|
57
|
+
if (current) {
|
|
58
|
+
tokens.push(current);
|
|
59
|
+
current = "";
|
|
60
|
+
}
|
|
61
|
+
tokens.push("2>>&1");
|
|
62
|
+
i += 5;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (rest.startsWith(">&1")) {
|
|
66
|
+
if (current) {
|
|
67
|
+
tokens.push(current);
|
|
68
|
+
current = "";
|
|
69
|
+
}
|
|
70
|
+
tokens.push("2>&1");
|
|
71
|
+
i += 4;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (rest.startsWith(">>")) {
|
|
75
|
+
if (current) {
|
|
76
|
+
tokens.push(current);
|
|
77
|
+
current = "";
|
|
78
|
+
}
|
|
79
|
+
tokens.push("2>>");
|
|
80
|
+
i += 3;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (rest.startsWith(">")) {
|
|
84
|
+
if (current) {
|
|
85
|
+
tokens.push(current);
|
|
86
|
+
current = "";
|
|
87
|
+
}
|
|
88
|
+
tokens.push("2>");
|
|
89
|
+
i += 2;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
53
93
|
if ((ch === ">" || ch === "<") && !inQ) {
|
|
54
94
|
if (current) {
|
|
55
95
|
tokens.push(current);
|