typescript-virtual-container 1.5.6 → 1.5.8
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 +28 -20
- package/dist/.tsbuildinfo +1 -1
- package/dist/SSHMimic/index.d.ts +5 -1
- package/dist/SSHMimic/index.js +27 -3
- package/dist/SSHMimic/prompt.d.ts +2 -1
- package/dist/SSHMimic/prompt.js +27 -5
- package/dist/SSHMimic/scp.d.ts +34 -0
- package/dist/SSHMimic/scp.js +285 -0
- package/dist/SSHMimic/sftp.d.ts +53 -3
- package/dist/SSHMimic/sftp.js +9 -3
- package/dist/VirtualFileSystem/binaryPack.d.ts +7 -0
- package/dist/VirtualFileSystem/binaryPack.js +37 -1
- package/dist/VirtualFileSystem/index.d.ts +7 -0
- package/dist/VirtualFileSystem/index.js +67 -27
- package/dist/VirtualFileSystem/internalTypes.d.ts +2 -0
- package/dist/VirtualFileSystem/path.d.ts +5 -0
- package/dist/VirtualFileSystem/path.js +24 -11
- package/dist/VirtualPackageManager/index.d.ts +4 -2
- package/dist/VirtualPackageManager/index.js +24 -4
- package/dist/VirtualShell/index.d.ts +6 -3
- package/dist/VirtualShell/index.js +3 -10
- package/dist/VirtualShell/shell.js +114 -140
- package/dist/VirtualShell/shellParser.js +1 -22
- package/dist/commands/exit.js +1 -1
- package/dist/commands/find.js +1 -4
- package/dist/commands/helpers.d.ts +0 -20
- package/dist/commands/helpers.js +0 -97
- 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 +10 -1
- package/dist/commands/perl.js +1 -1
- package/dist/commands/python.js +5 -2
- package/dist/commands/registry.js +6 -1
- 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 +90 -88
- package/dist/commands/strace.js +1 -1
- package/dist/commands/tar.js +2 -2
- package/dist/commands/test.js +2 -2
- package/dist/modules/linuxRootfs.js +7 -6
- package/dist/modules/nanoEditor.d.ts +92 -0
- package/dist/modules/nanoEditor.js +956 -0
- package/dist/modules/neofetch.js +2 -2
- package/dist/modules/webTermRenderer.d.ts +42 -0
- package/dist/modules/webTermRenderer.js +291 -0
- package/dist/types/commands.d.ts +4 -0
- package/dist/utils/argv.d.ts +6 -0
- package/dist/utils/argv.js +32 -0
- package/dist/utils/expand.d.ts +5 -2
- package/dist/utils/expand.js +70 -67
- package/dist/utils/glob.d.ts +6 -0
- package/dist/utils/glob.js +34 -0
- package/dist/utils/shellSession.d.ts +10 -0
- package/dist/utils/shellSession.js +56 -0
- package/dist/utils/tokenize.js +13 -13
- package/package.json +7 -6
|
@@ -1,10 +1,11 @@
|
|
|
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 { spawnHtopProcess, } from "../modules/shellInteractive";
|
|
5
|
+
import { getVisibleHtopPidList, toTtyLines, } from "../modules/shellRuntime";
|
|
6
6
|
import { buildLoginBanner } from "../SSHMimic/loginBanner";
|
|
7
7
|
import { buildPrompt } from "../SSHMimic/prompt";
|
|
8
|
+
import { listPathCompletions, loadHistory, readLastLogin, saveHistory, writeLastLogin } from "../utils/shellSession";
|
|
8
9
|
export function startShell(properties, stream, authUser, hostname, sessionId, remoteAddress = "unknown", terminalSize = { cols: 80, rows: 24 }, shell) {
|
|
9
10
|
let lineBuffer = "";
|
|
10
11
|
let cursorPos = 0;
|
|
@@ -14,42 +15,48 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
|
|
|
14
15
|
let cwd = userHome(authUser);
|
|
15
16
|
let pendingHeredoc = null;
|
|
16
17
|
const shellEnv = makeDefaultEnv(authUser, hostname);
|
|
18
|
+
const sessionStack = [];
|
|
17
19
|
let nanoSession = null;
|
|
18
20
|
let pendingSudo = null;
|
|
19
21
|
const buildCurrentPrompt = () => {
|
|
22
|
+
if (shellEnv.vars.PS1)
|
|
23
|
+
return buildPrompt(authUser, hostname, "", shellEnv.vars.PS1, cwd);
|
|
20
24
|
const homePath = userHome(authUser);
|
|
21
25
|
const cwdLabel = cwd === homePath ? "~" : path.posix.basename(cwd) || "/";
|
|
22
26
|
return buildPrompt(authUser, hostname, cwdLabel);
|
|
23
27
|
};
|
|
24
28
|
const commandNames = Array.from(new Set(getCommandNames())).sort();
|
|
25
29
|
console.log(`[${sessionId}] Shell started for user '${authUser}' at ${remoteAddress}`);
|
|
26
|
-
// Source login/rc files
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
30
|
+
// Source login/rc files before first prompt.
|
|
31
|
+
let loginReady = false;
|
|
32
|
+
const sourceFile = async (filePath, isEnvFile = false) => {
|
|
33
|
+
if (!shell.vfs.exists(filePath))
|
|
34
|
+
return;
|
|
35
|
+
try {
|
|
36
|
+
const content = shell.vfs.readFile(filePath);
|
|
37
|
+
for (const line of content.split("\n")) {
|
|
38
|
+
const l = line.trim();
|
|
39
|
+
if (!l || l.startsWith("#"))
|
|
40
|
+
continue;
|
|
41
|
+
if (isEnvFile) {
|
|
42
|
+
const m = l.match(/^([A-Za-z_][A-Za-z0-9_]*)=["']?(.+?)["']?\s*$/);
|
|
43
|
+
if (m)
|
|
44
|
+
shellEnv.vars[m[1]] = m[2];
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
const r = await runCommand(l, authUser, hostname, "shell", cwd, shell, undefined, shellEnv);
|
|
48
|
+
if (r.stdout)
|
|
49
|
+
stream.write(r.stdout.replace(/\n/g, "\r\n"));
|
|
46
50
|
}
|
|
47
51
|
}
|
|
48
|
-
|
|
49
|
-
}
|
|
52
|
+
}
|
|
53
|
+
catch { /* ignore */ }
|
|
54
|
+
};
|
|
55
|
+
const loginPromise = (async () => {
|
|
50
56
|
await sourceFile("/etc/environment", true);
|
|
51
57
|
await sourceFile(`${userHome(authUser)}/.profile`);
|
|
52
58
|
await sourceFile(`${userHome(authUser)}/.bashrc`);
|
|
59
|
+
loginReady = true;
|
|
53
60
|
})();
|
|
54
61
|
function renderLine() {
|
|
55
62
|
const prompt = buildCurrentPrompt();
|
|
@@ -87,6 +94,7 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
|
|
|
87
94
|
cwd = userHome(authUser);
|
|
88
95
|
}
|
|
89
96
|
shell.users.updateSession(sessionId, authUser, remoteAddress);
|
|
97
|
+
await applyUserSwitch(authUser, hostname, cwd, shellEnv, shell);
|
|
90
98
|
stream.write("\r\n");
|
|
91
99
|
renderLine();
|
|
92
100
|
return;
|
|
@@ -112,9 +120,11 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
|
|
|
112
120
|
stream.write(`${toTtyLines(result.stderr)}\r\n`);
|
|
113
121
|
}
|
|
114
122
|
if (result.switchUser) {
|
|
123
|
+
sessionStack.push({ authUser, cwd });
|
|
115
124
|
authUser = result.switchUser;
|
|
116
125
|
cwd = result.nextCwd ?? userHome(authUser);
|
|
117
126
|
shell.users.updateSession(sessionId, authUser, remoteAddress);
|
|
127
|
+
await applyUserSwitch(authUser, hostname, cwd, shellEnv, shell);
|
|
118
128
|
}
|
|
119
129
|
else if (result.nextCwd) {
|
|
120
130
|
cwd = result.nextCwd;
|
|
@@ -122,46 +132,34 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
|
|
|
122
132
|
// WAL: checkpoint handled by auto-flush timer
|
|
123
133
|
renderLine();
|
|
124
134
|
}
|
|
125
|
-
|
|
126
|
-
if (
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
const activeSession = nanoSession;
|
|
130
|
-
if (activeSession.kind === "nano") {
|
|
131
|
-
try {
|
|
132
|
-
const updatedContent = await readFile(activeSession.tempPath, "utf8");
|
|
133
|
-
shell.writeFileAsUser(authUser, activeSession.targetPath, updatedContent);
|
|
134
|
-
// WAL: checkpoint handled by auto-flush timer
|
|
135
|
-
}
|
|
136
|
-
catch {
|
|
137
|
-
// If temp file does not exist, nano exited without writing.
|
|
138
|
-
}
|
|
139
|
-
await unlink(activeSession.tempPath).catch(() => undefined);
|
|
135
|
+
function finishInteractiveSession(savedContent, targetPath) {
|
|
136
|
+
if (savedContent !== undefined && targetPath) {
|
|
137
|
+
shell.writeFileAsUser(authUser, targetPath, savedContent);
|
|
140
138
|
}
|
|
141
139
|
nanoSession = null;
|
|
142
140
|
lineBuffer = "";
|
|
143
141
|
cursorPos = 0;
|
|
144
|
-
|
|
142
|
+
// Clear screen + reset SGR so nano residue is gone before next prompt
|
|
143
|
+
stream.write("\x1b[2J\x1b[H\x1b[0m");
|
|
145
144
|
renderLine();
|
|
146
145
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
146
|
+
function startNanoEditor(targetPath, initialContent, _tempPath) {
|
|
147
|
+
const editor = new NanoEditor({
|
|
148
|
+
stream,
|
|
149
|
+
terminalSize,
|
|
150
|
+
content: initialContent,
|
|
151
|
+
filename: path.posix.basename(targetPath),
|
|
152
|
+
onExit: (reason, content) => {
|
|
153
|
+
if (reason === "saved") {
|
|
154
|
+
finishInteractiveSession(content, targetPath);
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
finishInteractiveSession();
|
|
158
|
+
}
|
|
159
|
+
},
|
|
158
160
|
});
|
|
159
|
-
nanoSession = {
|
|
160
|
-
|
|
161
|
-
targetPath,
|
|
162
|
-
tempPath,
|
|
163
|
-
process: editor,
|
|
164
|
-
};
|
|
161
|
+
nanoSession = { kind: "nano", targetPath, editor };
|
|
162
|
+
editor.start();
|
|
165
163
|
}
|
|
166
164
|
async function startHtop() {
|
|
167
165
|
const pidList = await getVisibleHtopPidList();
|
|
@@ -172,17 +170,12 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
|
|
|
172
170
|
const monitor = spawnHtopProcess(pidList, terminalSize, stream);
|
|
173
171
|
monitor.on("error", (error) => {
|
|
174
172
|
stream.write(`htop: ${error.message}\r\n`);
|
|
175
|
-
|
|
173
|
+
finishInteractiveSession();
|
|
176
174
|
});
|
|
177
175
|
monitor.on("close", () => {
|
|
178
|
-
|
|
176
|
+
finishInteractiveSession();
|
|
179
177
|
});
|
|
180
|
-
nanoSession = {
|
|
181
|
-
kind: "htop",
|
|
182
|
-
targetPath: "",
|
|
183
|
-
tempPath: "",
|
|
184
|
-
process: monitor,
|
|
185
|
-
};
|
|
178
|
+
nanoSession = { kind: "htop", process: monitor };
|
|
186
179
|
}
|
|
187
180
|
function applyHistoryLine(nextLine) {
|
|
188
181
|
lineBuffer = nextLine;
|
|
@@ -205,28 +198,6 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
|
|
|
205
198
|
}
|
|
206
199
|
return { start, end };
|
|
207
200
|
}
|
|
208
|
-
function listPathCompletions(prefix) {
|
|
209
|
-
const slashIndex = prefix.lastIndexOf("/");
|
|
210
|
-
const dirPart = slashIndex >= 0 ? prefix.slice(0, slashIndex + 1) : "";
|
|
211
|
-
const namePart = slashIndex >= 0 ? prefix.slice(slashIndex + 1) : prefix;
|
|
212
|
-
const basePath = resolvePath(cwd, dirPart || ".");
|
|
213
|
-
try {
|
|
214
|
-
return shell.vfs
|
|
215
|
-
.list(basePath)
|
|
216
|
-
.filter((entry) => !entry.startsWith("."))
|
|
217
|
-
.filter((entry) => entry.startsWith(namePart))
|
|
218
|
-
.map((entry) => {
|
|
219
|
-
const fullPath = path.posix.join(basePath, entry);
|
|
220
|
-
const st = shell.vfs.stat(fullPath);
|
|
221
|
-
const suffix = st.type === "directory" ? "/" : "";
|
|
222
|
-
return `${dirPart}${entry}${suffix}`;
|
|
223
|
-
})
|
|
224
|
-
.sort();
|
|
225
|
-
}
|
|
226
|
-
catch {
|
|
227
|
-
return [];
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
201
|
function handleTabCompletion() {
|
|
231
202
|
const { start, end } = getTokenRange(lineBuffer, cursorPos);
|
|
232
203
|
const token = lineBuffer.slice(start, cursorPos);
|
|
@@ -237,7 +208,7 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
|
|
|
237
208
|
const commandCandidates = firstToken
|
|
238
209
|
? commandNames.filter((name) => name.startsWith(token))
|
|
239
210
|
: [];
|
|
240
|
-
const pathCandidates = listPathCompletions(token);
|
|
211
|
+
const pathCandidates = listPathCompletions(shell.vfs, cwd, token);
|
|
241
212
|
const candidates = Array.from(new Set([...commandCandidates, ...pathCandidates])).sort();
|
|
242
213
|
if (candidates.length === 0) {
|
|
243
214
|
return;
|
|
@@ -255,43 +226,30 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
|
|
|
255
226
|
renderLine();
|
|
256
227
|
}
|
|
257
228
|
function pushHistory(cmd) {
|
|
258
|
-
if (cmd.length === 0)
|
|
229
|
+
if (cmd.length === 0)
|
|
259
230
|
return;
|
|
260
|
-
}
|
|
261
231
|
history.push(cmd);
|
|
262
|
-
if (history.length > 500)
|
|
232
|
+
if (history.length > 500)
|
|
263
233
|
history = history.slice(history.length - 500);
|
|
264
|
-
|
|
265
|
-
const data = history.length > 0 ? `${history.join("\n")}\n` : "";
|
|
266
|
-
shell.vfs.writeFile(`${userHome(authUser)}/.bash_history`, data);
|
|
267
|
-
}
|
|
268
|
-
function readLastLogin() {
|
|
269
|
-
const lastlogPath = `${userHome(authUser)}/.lastlog.json`;
|
|
270
|
-
if (!shell.vfs.exists(lastlogPath)) {
|
|
271
|
-
return null;
|
|
272
|
-
}
|
|
273
|
-
try {
|
|
274
|
-
return JSON.parse(shell.vfs.readFile(lastlogPath));
|
|
275
|
-
}
|
|
276
|
-
catch {
|
|
277
|
-
return null;
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
function writeLastLogin(nowIso) {
|
|
281
|
-
const lastlogPath = `${userHome(authUser)}/.lastlog`;
|
|
282
|
-
shell.vfs.writeFile(lastlogPath, JSON.stringify({ at: nowIso, from: remoteAddress }));
|
|
234
|
+
saveHistory(shell.vfs, authUser, history);
|
|
283
235
|
}
|
|
284
236
|
function renderLoginBanner() {
|
|
285
|
-
const last = readLastLogin();
|
|
286
|
-
const nowIso = new Date().toISOString();
|
|
237
|
+
const last = readLastLogin(shell.vfs, authUser);
|
|
287
238
|
stream.write(buildLoginBanner(hostname, properties, last));
|
|
288
|
-
writeLastLogin(
|
|
239
|
+
writeLastLogin(shell.vfs, authUser, remoteAddress);
|
|
289
240
|
}
|
|
290
241
|
renderLoginBanner();
|
|
291
|
-
renderLine();
|
|
242
|
+
void loginPromise.then(() => renderLine());
|
|
292
243
|
stream.on("data", async (chunk) => {
|
|
244
|
+
if (!loginReady)
|
|
245
|
+
return;
|
|
293
246
|
if (nanoSession) {
|
|
294
|
-
nanoSession.
|
|
247
|
+
if (nanoSession.kind === "nano") {
|
|
248
|
+
nanoSession.editor.handleInput(chunk);
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
nanoSession.process.stdin.write(chunk);
|
|
252
|
+
}
|
|
295
253
|
return;
|
|
296
254
|
}
|
|
297
255
|
if (pendingHeredoc) {
|
|
@@ -396,13 +354,24 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
|
|
|
396
354
|
cursorPos = 0;
|
|
397
355
|
historyIndex = null;
|
|
398
356
|
historyDraft = "";
|
|
399
|
-
stream.write("bye\r\n");
|
|
400
|
-
pushHistory("bye");
|
|
401
|
-
// WAL: checkpoint handled by auto-flush timer
|
|
402
357
|
stream.write("logout\r\n");
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
358
|
+
if (sessionStack.length > 0) {
|
|
359
|
+
const prev = sessionStack.pop();
|
|
360
|
+
authUser = prev.authUser;
|
|
361
|
+
cwd = prev.cwd;
|
|
362
|
+
shellEnv.vars.USER = authUser;
|
|
363
|
+
shellEnv.vars.LOGNAME = authUser;
|
|
364
|
+
shellEnv.vars.HOME = userHome(authUser);
|
|
365
|
+
shellEnv.vars.PWD = cwd;
|
|
366
|
+
shell.users.updateSession(sessionId, authUser, remoteAddress);
|
|
367
|
+
renderLine();
|
|
368
|
+
}
|
|
369
|
+
else {
|
|
370
|
+
stream.exit(0);
|
|
371
|
+
stream.end();
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
continue;
|
|
406
375
|
}
|
|
407
376
|
if (ch === "\t") {
|
|
408
377
|
handleTabCompletion();
|
|
@@ -598,17 +567,32 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
|
|
|
598
567
|
}
|
|
599
568
|
if (result.closeSession) {
|
|
600
569
|
stream.write("logout\r\n");
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
570
|
+
if (sessionStack.length > 0) {
|
|
571
|
+
const prev = sessionStack.pop();
|
|
572
|
+
authUser = prev.authUser;
|
|
573
|
+
cwd = prev.cwd;
|
|
574
|
+
shellEnv.vars.USER = authUser;
|
|
575
|
+
shellEnv.vars.LOGNAME = authUser;
|
|
576
|
+
shellEnv.vars.HOME = userHome(authUser);
|
|
577
|
+
shellEnv.vars.PWD = cwd;
|
|
578
|
+
shell.users.updateSession(sessionId, authUser, remoteAddress);
|
|
579
|
+
}
|
|
580
|
+
else {
|
|
581
|
+
stream.exit(result.exitCode ?? 0);
|
|
582
|
+
stream.end();
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
604
585
|
}
|
|
605
|
-
if (result.nextCwd) {
|
|
586
|
+
if (result.nextCwd && !result.closeSession) {
|
|
606
587
|
cwd = result.nextCwd;
|
|
607
588
|
}
|
|
608
589
|
if (result.switchUser) {
|
|
590
|
+
sessionStack.push({ authUser, cwd });
|
|
609
591
|
authUser = result.switchUser;
|
|
610
592
|
cwd = result.nextCwd ?? userHome(authUser);
|
|
593
|
+
shellEnv.vars.PWD = cwd;
|
|
611
594
|
shell.users.updateSession(sessionId, authUser, remoteAddress);
|
|
595
|
+
await applyUserSwitch(authUser, hostname, cwd, shellEnv, shell);
|
|
612
596
|
lineBuffer = "";
|
|
613
597
|
cursorPos = 0;
|
|
614
598
|
}
|
|
@@ -630,20 +614,10 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
|
|
|
630
614
|
});
|
|
631
615
|
stream.on("close", () => {
|
|
632
616
|
if (nanoSession) {
|
|
633
|
-
nanoSession.
|
|
617
|
+
if (nanoSession.kind === "htop") {
|
|
618
|
+
nanoSession.process.kill("SIGTERM");
|
|
619
|
+
}
|
|
634
620
|
nanoSession = null;
|
|
635
621
|
}
|
|
636
622
|
});
|
|
637
623
|
}
|
|
638
|
-
function loadHistory(vfs, authUser) {
|
|
639
|
-
const historyPath = `${userHome(authUser)}/.bash_history`;
|
|
640
|
-
if (!vfs.exists(historyPath)) {
|
|
641
|
-
vfs.writeFile(historyPath, "");
|
|
642
|
-
return [];
|
|
643
|
-
}
|
|
644
|
-
const raw = vfs.readFile(historyPath);
|
|
645
|
-
return raw
|
|
646
|
-
.split("\n")
|
|
647
|
-
.map((line) => line.trim())
|
|
648
|
-
.filter((line) => line.length > 0);
|
|
649
|
-
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { globToRegex } from "../utils/glob";
|
|
1
2
|
import { tokenizeCommand } from "../utils/tokenize";
|
|
2
3
|
// ── Public API ───────────────────────────────────────────────────────────────
|
|
3
4
|
/**
|
|
@@ -42,28 +43,6 @@ export function expandGlob(pattern, entries) {
|
|
|
42
43
|
const matches = entries.filter((e) => regex.test(e));
|
|
43
44
|
return matches.length > 0 ? matches.sort() : [pattern];
|
|
44
45
|
}
|
|
45
|
-
function globToRegex(pattern) {
|
|
46
|
-
let re = "^";
|
|
47
|
-
for (let i = 0; i < pattern.length; i++) {
|
|
48
|
-
const c = pattern[i];
|
|
49
|
-
if (c === "*")
|
|
50
|
-
re += ".*";
|
|
51
|
-
else if (c === "?")
|
|
52
|
-
re += ".";
|
|
53
|
-
else if (c === "[") {
|
|
54
|
-
const close = pattern.indexOf("]", i + 1);
|
|
55
|
-
if (close === -1)
|
|
56
|
-
re += "\\[";
|
|
57
|
-
else {
|
|
58
|
-
re += `[${pattern.slice(i + 1, close)}]`;
|
|
59
|
-
i = close;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
else
|
|
63
|
-
re += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
64
|
-
}
|
|
65
|
-
return new RegExp(`${re}$`);
|
|
66
|
-
}
|
|
67
46
|
// ── Internal parser ───────────────────────────────────────────────────────────
|
|
68
47
|
function parseStatements(input) {
|
|
69
48
|
// Split by ;, &&, || — respecting quotes and parens
|
package/dist/commands/exit.js
CHANGED
package/dist/commands/find.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { globToRegex } from "../utils/glob";
|
|
1
2
|
import { assertPathAccess, resolvePath } from "./helpers";
|
|
2
3
|
import { runCommand } from "./runtime";
|
|
3
4
|
/**
|
|
@@ -109,10 +110,6 @@ export const findCommand = {
|
|
|
109
110
|
return [{ type: "true" }, pos + 1];
|
|
110
111
|
}
|
|
111
112
|
const pred = exprArgs.length > 0 ? parseExpr(exprArgs, 0)[0] : { type: "true" };
|
|
112
|
-
function globToRegex(pat, flags = "") {
|
|
113
|
-
const esc = pat.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
114
|
-
return new RegExp(`^${esc}$`, flags);
|
|
115
|
-
}
|
|
116
113
|
function matchPred(p, fullPath, depth) {
|
|
117
114
|
switch (p.type) {
|
|
118
115
|
case "true": return true;
|
|
@@ -1,28 +1,8 @@
|
|
|
1
1
|
import type VirtualFileSystem from "../VirtualFileSystem";
|
|
2
2
|
import type { VirtualPackageManager } from "../VirtualPackageManager";
|
|
3
3
|
import type { VirtualShell } from "../VirtualShell";
|
|
4
|
-
export declare function normalizeTerminalOutput(text: string): string;
|
|
5
4
|
export declare function resolvePath(cwd: string, inputPath: string, homeDir?: string): string;
|
|
6
5
|
export declare function assertPathAccess(authUser: string, targetPath: string, operation: string): void;
|
|
7
6
|
export declare function stripUrlFilename(url: string): string;
|
|
8
|
-
export declare function fetchResource(url: string): Promise<{
|
|
9
|
-
text: string;
|
|
10
|
-
status: number;
|
|
11
|
-
contentType: string | null;
|
|
12
|
-
}>;
|
|
13
|
-
/**
|
|
14
|
-
* Run a host command like curl or wget and capture its output.
|
|
15
|
-
* @param binary - The binary to execute (e.g., "curl", "wget").
|
|
16
|
-
* @param args - Arguments to pass to the binary.
|
|
17
|
-
* @returns Promise resolving with stdout, stderr, and exit code.
|
|
18
|
-
*/
|
|
19
|
-
export declare function runHostCommand(binary: string, args: string[]): Promise<{
|
|
20
|
-
stdout: string;
|
|
21
|
-
stderr: string;
|
|
22
|
-
exitCode: number;
|
|
23
|
-
}>;
|
|
24
7
|
export declare function resolveReadablePath(vfs: VirtualFileSystem, cwd: string, inputPath: string): string;
|
|
25
|
-
export declare function joinListWithType(cwd: string, items: string[], statAt: (p: string) => {
|
|
26
|
-
type: "file" | "directory";
|
|
27
|
-
}): string;
|
|
28
8
|
export declare function getPackageManager(shell: VirtualShell): VirtualPackageManager | undefined;
|
package/dist/commands/helpers.js
CHANGED
|
@@ -1,23 +1,5 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
1
|
import * as path from "node:path";
|
|
3
2
|
const PROTECTED_PREFIXES = ["/.virtual-env-js/.auth", "/etc/htpasswd"];
|
|
4
|
-
function normalizeFetchUrl(input) {
|
|
5
|
-
if (/^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(input)) {
|
|
6
|
-
return input;
|
|
7
|
-
}
|
|
8
|
-
return `http://${input}`;
|
|
9
|
-
}
|
|
10
|
-
export function normalizeTerminalOutput(text) {
|
|
11
|
-
return text
|
|
12
|
-
.replace(/\r\n/g, "\n")
|
|
13
|
-
.replace(/\r/g, "\n")
|
|
14
|
-
.replace(/\t/g, " ")
|
|
15
|
-
.split("\n")
|
|
16
|
-
.map((line) => line.replace(/^[ \u00A0]{8,}/, " ").replace(/[ \u00A0]{3,}/g, " "))
|
|
17
|
-
.join("\n")
|
|
18
|
-
.replace(/\n{3,}/g, "\n\n")
|
|
19
|
-
.trimEnd();
|
|
20
|
-
}
|
|
21
3
|
export function resolvePath(cwd, inputPath, homeDir) {
|
|
22
4
|
if (!inputPath || inputPath.trim() === "") {
|
|
23
5
|
return cwd;
|
|
@@ -49,76 +31,6 @@ export function stripUrlFilename(url) {
|
|
|
49
31
|
const lastPart = cleaned.split("/").filter(Boolean).pop();
|
|
50
32
|
return lastPart && lastPart.length > 0 ? lastPart : "index.html";
|
|
51
33
|
}
|
|
52
|
-
export async function fetchResource(url) {
|
|
53
|
-
const response = await fetch(normalizeFetchUrl(url));
|
|
54
|
-
const contentType = response.headers.get("content-type");
|
|
55
|
-
return {
|
|
56
|
-
text: await response.text(),
|
|
57
|
-
status: response.status,
|
|
58
|
-
contentType,
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* Run a host command like curl or wget and capture its output.
|
|
63
|
-
* @param binary - The binary to execute (e.g., "curl", "wget").
|
|
64
|
-
* @param args - Arguments to pass to the binary.
|
|
65
|
-
* @returns Promise resolving with stdout, stderr, and exit code.
|
|
66
|
-
*/
|
|
67
|
-
export function runHostCommand(binary, args) {
|
|
68
|
-
return new Promise((resolve) => {
|
|
69
|
-
let childProcess;
|
|
70
|
-
try {
|
|
71
|
-
childProcess = spawn(binary, args, {
|
|
72
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
catch (error) {
|
|
76
|
-
resolve({
|
|
77
|
-
stdout: "",
|
|
78
|
-
stderr: `${binary}: ${error instanceof Error ? error.message : String(error)}`,
|
|
79
|
-
exitCode: 1,
|
|
80
|
-
});
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
let stdout = "";
|
|
84
|
-
let stderr = "";
|
|
85
|
-
const stdoutStream = childProcess.stdout;
|
|
86
|
-
const stderrStream = childProcess.stderr;
|
|
87
|
-
if (!stdoutStream || !stderrStream) {
|
|
88
|
-
resolve({
|
|
89
|
-
stdout: "",
|
|
90
|
-
stderr: `${binary}: failed to capture process output`,
|
|
91
|
-
exitCode: 1,
|
|
92
|
-
});
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
stdoutStream.setEncoding("utf8");
|
|
96
|
-
stderrStream.setEncoding("utf8");
|
|
97
|
-
stdoutStream.on("data", (chunk) => {
|
|
98
|
-
stdout += chunk;
|
|
99
|
-
});
|
|
100
|
-
stderrStream.on("data", (chunk) => {
|
|
101
|
-
stderr += chunk;
|
|
102
|
-
});
|
|
103
|
-
childProcess.on("error", (error) => {
|
|
104
|
-
const errorCode = error instanceof Error && "code" in error
|
|
105
|
-
? String(error.code ?? "")
|
|
106
|
-
: "";
|
|
107
|
-
resolve({
|
|
108
|
-
stdout: "",
|
|
109
|
-
stderr: `${binary}: ${error.message}`,
|
|
110
|
-
exitCode: errorCode === "ENOENT" ? 127 : 1,
|
|
111
|
-
});
|
|
112
|
-
});
|
|
113
|
-
childProcess.on("close", (code) => {
|
|
114
|
-
resolve({
|
|
115
|
-
stdout,
|
|
116
|
-
stderr,
|
|
117
|
-
exitCode: code ?? 1,
|
|
118
|
-
});
|
|
119
|
-
});
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
34
|
function levenshtein(a, b) {
|
|
123
35
|
const dp = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0));
|
|
124
36
|
for (let i = 0; i <= a.length; i += 1) {
|
|
@@ -153,15 +65,6 @@ export function resolveReadablePath(vfs, cwd, inputPath) {
|
|
|
153
65
|
}
|
|
154
66
|
return exactPath;
|
|
155
67
|
}
|
|
156
|
-
export function joinListWithType(cwd, items, statAt) {
|
|
157
|
-
return items
|
|
158
|
-
.map((name) => {
|
|
159
|
-
const childPath = resolvePath(cwd, name);
|
|
160
|
-
const stats = statAt(childPath);
|
|
161
|
-
return stats.type === "directory" ? `${name}/` : name;
|
|
162
|
-
})
|
|
163
|
-
.join(" ");
|
|
164
|
-
}
|
|
165
68
|
export function getPackageManager(shell) {
|
|
166
69
|
return shell.packageManager;
|
|
167
70
|
}
|
package/dist/commands/id.js
CHANGED
|
@@ -4,11 +4,18 @@ export const idCommand = {
|
|
|
4
4
|
category: "system",
|
|
5
5
|
params: ["[user]"],
|
|
6
6
|
run: ({ authUser, shell, args }) => {
|
|
7
|
-
const
|
|
7
|
+
const flagU = args.includes("-u");
|
|
8
|
+
const flagG = args.includes("-g");
|
|
9
|
+
const flagN = args.includes("-n");
|
|
10
|
+
const target = args.find(a => !a.startsWith("-")) ?? authUser;
|
|
8
11
|
const uid = target === "root" ? 0 : 1000;
|
|
9
12
|
const gid = uid;
|
|
10
13
|
const isSudo = shell.users.isSudoer(target);
|
|
11
14
|
const groups = isSudo ? `${gid}(${target}),0(root)` : `${gid}(${target})`;
|
|
15
|
+
if (flagU)
|
|
16
|
+
return { stdout: flagN ? target : String(uid), exitCode: 0 };
|
|
17
|
+
if (flagG)
|
|
18
|
+
return { stdout: flagN ? target : String(gid), exitCode: 0 };
|
|
12
19
|
return {
|
|
13
20
|
stdout: `uid=${uid}(${target}) gid=${gid}(${target}) groups=${groups}`,
|
|
14
21
|
exitCode: 0,
|
package/dist/commands/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { createCustomCommand, getCommandModulesPublic, getCommandNames, registerCommand, resolveModule } from "./registry";
|
|
2
|
-
export { makeDefaultEnv, runCommand, runCommandDirect, userHome } from "./runtime";
|
|
2
|
+
export { applyUserSwitch, makeDefaultEnv, runCommand, runCommandDirect, userHome } from "./runtime";
|
package/dist/commands/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { createCustomCommand, getCommandModulesPublic, getCommandNames, registerCommand, resolveModule } from "./registry";
|
|
2
|
-
export { makeDefaultEnv, runCommand, runCommandDirect, userHome } from "./runtime";
|
|
2
|
+
export { applyUserSwitch, makeDefaultEnv, runCommand, runCommandDirect, userHome } from "./runtime";
|
|
@@ -993,7 +993,16 @@ SYNOPSIS
|
|
|
993
993
|
rm [OPTION]... FILE...
|
|
994
994
|
|
|
995
995
|
OPTIONS
|
|
996
|
-
-r
|
|
996
|
+
-r, -R remove directories and their contents recursively
|
|
997
|
+
|
|
998
|
+
-f, --force
|
|
999
|
+
skip confirmation prompt, never prompt
|
|
1000
|
+
|
|
1001
|
+
-rf, -fr
|
|
1002
|
+
recursive and force combined
|
|
1003
|
+
|
|
1004
|
+
Without -f, rm prompts for confirmation before removing each target.
|
|
1005
|
+
Answer y or yes to confirm, anything else cancels.`,
|
|
997
1006
|
"sed": `SED(1) User Commands SED(1)
|
|
998
1007
|
|
|
999
1008
|
NAME
|
package/dist/commands/perl.js
CHANGED
|
@@ -25,7 +25,7 @@ export const perlCommand = {
|
|
|
25
25
|
for (let li = 0; li < lines.length; li++) {
|
|
26
26
|
let line = lines[li];
|
|
27
27
|
// $_ = line, $. = line number
|
|
28
|
-
|
|
28
|
+
const processed = code
|
|
29
29
|
.replace(/\$_/g, JSON.stringify(line))
|
|
30
30
|
.replace(/\$\./g, String(li + 1));
|
|
31
31
|
// s/pat/rep/[g] substitution on $_
|
package/dist/commands/python.js
CHANGED
|
@@ -694,8 +694,11 @@ class Interpreter {
|
|
|
694
694
|
return left.repeat(right);
|
|
695
695
|
if (Array.isArray(left) && typeof right === "number") {
|
|
696
696
|
const arr = [];
|
|
697
|
-
|
|
698
|
-
|
|
697
|
+
const n = right | 0;
|
|
698
|
+
for (let i = 0; i < n; i++) {
|
|
699
|
+
for (let j = 0; j < left.length; j++)
|
|
700
|
+
arr.push(left[j]);
|
|
701
|
+
}
|
|
699
702
|
return arr;
|
|
700
703
|
}
|
|
701
704
|
return left * right;
|