typescript-virtual-container 1.5.7 → 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 +21 -21
- package/dist/.tsbuildinfo +1 -1
- package/dist/SSHMimic/prompt.d.ts +2 -1
- package/dist/SSHMimic/prompt.js +27 -5
- 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 +77 -133
- 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/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 +25 -1
- package/dist/modules/linuxRootfs.js +7 -3
- package/dist/modules/nanoEditor.d.ts +92 -0
- package/dist/modules/nanoEditor.js +956 -0
- package/dist/modules/webTermRenderer.d.ts +42 -0
- package/dist/modules/webTermRenderer.js +291 -0
- package/dist/utils/shellSession.d.ts +10 -0
- package/dist/utils/shellSession.js +56 -0
- package/package.json +1 -1
|
@@ -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,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;
|
|
@@ -18,39 +19,44 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
|
|
|
18
19
|
let nanoSession = null;
|
|
19
20
|
let pendingSudo = null;
|
|
20
21
|
const buildCurrentPrompt = () => {
|
|
22
|
+
if (shellEnv.vars.PS1)
|
|
23
|
+
return buildPrompt(authUser, hostname, "", shellEnv.vars.PS1, cwd);
|
|
21
24
|
const homePath = userHome(authUser);
|
|
22
25
|
const cwdLabel = cwd === homePath ? "~" : path.posix.basename(cwd) || "/";
|
|
23
26
|
return buildPrompt(authUser, hostname, cwdLabel);
|
|
24
27
|
};
|
|
25
28
|
const commandNames = Array.from(new Set(getCommandNames())).sort();
|
|
26
29
|
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
|
-
|
|
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"));
|
|
47
50
|
}
|
|
48
51
|
}
|
|
49
|
-
|
|
50
|
-
}
|
|
52
|
+
}
|
|
53
|
+
catch { /* ignore */ }
|
|
54
|
+
};
|
|
55
|
+
const loginPromise = (async () => {
|
|
51
56
|
await sourceFile("/etc/environment", true);
|
|
52
57
|
await sourceFile(`${userHome(authUser)}/.profile`);
|
|
53
58
|
await sourceFile(`${userHome(authUser)}/.bashrc`);
|
|
59
|
+
loginReady = true;
|
|
54
60
|
})();
|
|
55
61
|
function renderLine() {
|
|
56
62
|
const prompt = buildCurrentPrompt();
|
|
@@ -88,6 +94,7 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
|
|
|
88
94
|
cwd = userHome(authUser);
|
|
89
95
|
}
|
|
90
96
|
shell.users.updateSession(sessionId, authUser, remoteAddress);
|
|
97
|
+
await applyUserSwitch(authUser, hostname, cwd, shellEnv, shell);
|
|
91
98
|
stream.write("\r\n");
|
|
92
99
|
renderLine();
|
|
93
100
|
return;
|
|
@@ -117,6 +124,7 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
|
|
|
117
124
|
authUser = result.switchUser;
|
|
118
125
|
cwd = result.nextCwd ?? userHome(authUser);
|
|
119
126
|
shell.users.updateSession(sessionId, authUser, remoteAddress);
|
|
127
|
+
await applyUserSwitch(authUser, hostname, cwd, shellEnv, shell);
|
|
120
128
|
}
|
|
121
129
|
else if (result.nextCwd) {
|
|
122
130
|
cwd = result.nextCwd;
|
|
@@ -124,46 +132,34 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
|
|
|
124
132
|
// WAL: checkpoint handled by auto-flush timer
|
|
125
133
|
renderLine();
|
|
126
134
|
}
|
|
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);
|
|
135
|
+
function finishInteractiveSession(savedContent, targetPath) {
|
|
136
|
+
if (savedContent !== undefined && targetPath) {
|
|
137
|
+
shell.writeFileAsUser(authUser, targetPath, savedContent);
|
|
142
138
|
}
|
|
143
139
|
nanoSession = null;
|
|
144
140
|
lineBuffer = "";
|
|
145
141
|
cursorPos = 0;
|
|
146
|
-
|
|
142
|
+
// Clear screen + reset SGR so nano residue is gone before next prompt
|
|
143
|
+
stream.write("\x1b[2J\x1b[H\x1b[0m");
|
|
147
144
|
renderLine();
|
|
148
145
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
+
},
|
|
160
160
|
});
|
|
161
|
-
nanoSession = {
|
|
162
|
-
|
|
163
|
-
targetPath,
|
|
164
|
-
tempPath,
|
|
165
|
-
process: editor,
|
|
166
|
-
};
|
|
161
|
+
nanoSession = { kind: "nano", targetPath, editor };
|
|
162
|
+
editor.start();
|
|
167
163
|
}
|
|
168
164
|
async function startHtop() {
|
|
169
165
|
const pidList = await getVisibleHtopPidList();
|
|
@@ -174,17 +170,12 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
|
|
|
174
170
|
const monitor = spawnHtopProcess(pidList, terminalSize, stream);
|
|
175
171
|
monitor.on("error", (error) => {
|
|
176
172
|
stream.write(`htop: ${error.message}\r\n`);
|
|
177
|
-
|
|
173
|
+
finishInteractiveSession();
|
|
178
174
|
});
|
|
179
175
|
monitor.on("close", () => {
|
|
180
|
-
|
|
176
|
+
finishInteractiveSession();
|
|
181
177
|
});
|
|
182
|
-
nanoSession = {
|
|
183
|
-
kind: "htop",
|
|
184
|
-
targetPath: "",
|
|
185
|
-
tempPath: "",
|
|
186
|
-
process: monitor,
|
|
187
|
-
};
|
|
178
|
+
nanoSession = { kind: "htop", process: monitor };
|
|
188
179
|
}
|
|
189
180
|
function applyHistoryLine(nextLine) {
|
|
190
181
|
lineBuffer = nextLine;
|
|
@@ -207,28 +198,6 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
|
|
|
207
198
|
}
|
|
208
199
|
return { start, end };
|
|
209
200
|
}
|
|
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
201
|
function handleTabCompletion() {
|
|
233
202
|
const { start, end } = getTokenRange(lineBuffer, cursorPos);
|
|
234
203
|
const token = lineBuffer.slice(start, cursorPos);
|
|
@@ -239,7 +208,7 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
|
|
|
239
208
|
const commandCandidates = firstToken
|
|
240
209
|
? commandNames.filter((name) => name.startsWith(token))
|
|
241
210
|
: [];
|
|
242
|
-
const pathCandidates = listPathCompletions(token);
|
|
211
|
+
const pathCandidates = listPathCompletions(shell.vfs, cwd, token);
|
|
243
212
|
const candidates = Array.from(new Set([...commandCandidates, ...pathCandidates])).sort();
|
|
244
213
|
if (candidates.length === 0) {
|
|
245
214
|
return;
|
|
@@ -257,43 +226,30 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
|
|
|
257
226
|
renderLine();
|
|
258
227
|
}
|
|
259
228
|
function pushHistory(cmd) {
|
|
260
|
-
if (cmd.length === 0)
|
|
229
|
+
if (cmd.length === 0)
|
|
261
230
|
return;
|
|
262
|
-
}
|
|
263
231
|
history.push(cmd);
|
|
264
|
-
if (history.length > 500)
|
|
232
|
+
if (history.length > 500)
|
|
265
233
|
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 }));
|
|
234
|
+
saveHistory(shell.vfs, authUser, history);
|
|
285
235
|
}
|
|
286
236
|
function renderLoginBanner() {
|
|
287
|
-
const last = readLastLogin();
|
|
288
|
-
const nowIso = new Date().toISOString();
|
|
237
|
+
const last = readLastLogin(shell.vfs, authUser);
|
|
289
238
|
stream.write(buildLoginBanner(hostname, properties, last));
|
|
290
|
-
writeLastLogin(
|
|
239
|
+
writeLastLogin(shell.vfs, authUser, remoteAddress);
|
|
291
240
|
}
|
|
292
241
|
renderLoginBanner();
|
|
293
|
-
renderLine();
|
|
242
|
+
void loginPromise.then(() => renderLine());
|
|
294
243
|
stream.on("data", async (chunk) => {
|
|
244
|
+
if (!loginReady)
|
|
245
|
+
return;
|
|
295
246
|
if (nanoSession) {
|
|
296
|
-
nanoSession.
|
|
247
|
+
if (nanoSession.kind === "nano") {
|
|
248
|
+
nanoSession.editor.handleInput(chunk);
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
nanoSession.process.stdin.write(chunk);
|
|
252
|
+
}
|
|
297
253
|
return;
|
|
298
254
|
}
|
|
299
255
|
if (pendingHeredoc) {
|
|
@@ -634,11 +590,9 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
|
|
|
634
590
|
sessionStack.push({ authUser, cwd });
|
|
635
591
|
authUser = result.switchUser;
|
|
636
592
|
cwd = result.nextCwd ?? userHome(authUser);
|
|
637
|
-
shellEnv.vars.USER = authUser;
|
|
638
|
-
shellEnv.vars.LOGNAME = authUser;
|
|
639
|
-
shellEnv.vars.HOME = userHome(authUser);
|
|
640
593
|
shellEnv.vars.PWD = cwd;
|
|
641
594
|
shell.users.updateSession(sessionId, authUser, remoteAddress);
|
|
595
|
+
await applyUserSwitch(authUser, hostname, cwd, shellEnv, shell);
|
|
642
596
|
lineBuffer = "";
|
|
643
597
|
cursorPos = 0;
|
|
644
598
|
}
|
|
@@ -660,20 +614,10 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
|
|
|
660
614
|
});
|
|
661
615
|
stream.on("close", () => {
|
|
662
616
|
if (nanoSession) {
|
|
663
|
-
nanoSession.
|
|
617
|
+
if (nanoSession.kind === "htop") {
|
|
618
|
+
nanoSession.process.kill("SIGTERM");
|
|
619
|
+
}
|
|
664
620
|
nanoSession = null;
|
|
665
621
|
}
|
|
666
622
|
});
|
|
667
623
|
}
|
|
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
|
-
}
|
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/rm.d.ts
CHANGED
package/dist/commands/rm.js
CHANGED
|
@@ -1,36 +1,73 @@
|
|
|
1
1
|
import { getArg, ifFlag } from "./command-helpers";
|
|
2
2
|
import { assertPathAccess, resolvePath } from "./helpers";
|
|
3
|
+
const FLAG_RECURSIVE = ["-r", "-R", "-rf", "-fr", "-rF", "-Fr"];
|
|
4
|
+
const FLAG_FORCE = ["-f", "-rf", "-fr", "-rF", "-Fr", "--force"];
|
|
3
5
|
/**
|
|
4
6
|
* Remove files or directories from the filesystem.
|
|
5
7
|
* @category files
|
|
6
|
-
* @params ["[-r|-rf] <path>"]
|
|
8
|
+
* @params ["[-r|-rf|-f] <path>"]
|
|
7
9
|
*/
|
|
8
10
|
export const rmCommand = {
|
|
9
11
|
name: "rm",
|
|
10
12
|
description: "Remove files or directories",
|
|
11
13
|
category: "files",
|
|
12
|
-
params: ["[-r|-rf] <path>"],
|
|
14
|
+
params: ["[-r|-rf|-f] <path>"],
|
|
13
15
|
run: ({ authUser, shell, cwd, args }) => {
|
|
14
16
|
if (args.length === 0) {
|
|
15
17
|
return { stderr: "rm: missing operand", exitCode: 1 };
|
|
16
18
|
}
|
|
17
|
-
const recursive = ifFlag(args,
|
|
19
|
+
const recursive = ifFlag(args, FLAG_RECURSIVE);
|
|
20
|
+
const force = ifFlag(args, FLAG_FORCE);
|
|
21
|
+
const allFlags = [...FLAG_RECURSIVE, ...FLAG_FORCE, "--force"];
|
|
18
22
|
const targets = [];
|
|
19
23
|
for (let index = 0;; index += 1) {
|
|
20
|
-
const target = getArg(args, index, { flags:
|
|
21
|
-
if (!target)
|
|
24
|
+
const target = getArg(args, index, { flags: allFlags });
|
|
25
|
+
if (!target)
|
|
22
26
|
break;
|
|
23
|
-
}
|
|
24
27
|
targets.push(target);
|
|
25
28
|
}
|
|
26
29
|
if (targets.length === 0) {
|
|
27
30
|
return { stderr: "rm: missing operand", exitCode: 1 };
|
|
28
31
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
assertPathAccess(authUser,
|
|
32
|
-
|
|
32
|
+
const resolved = targets.map((t) => resolvePath(cwd, t));
|
|
33
|
+
for (const r of resolved)
|
|
34
|
+
assertPathAccess(authUser, r, "rm");
|
|
35
|
+
for (const r of resolved) {
|
|
36
|
+
if (!shell.vfs.exists(r)) {
|
|
37
|
+
if (force)
|
|
38
|
+
continue;
|
|
39
|
+
return { stderr: `rm: cannot remove '${r}': No such file or directory`, exitCode: 1 };
|
|
40
|
+
}
|
|
33
41
|
}
|
|
34
|
-
|
|
42
|
+
const doRemove = (sh) => {
|
|
43
|
+
for (const r of resolved)
|
|
44
|
+
if (sh.vfs.exists(r))
|
|
45
|
+
sh.vfs.remove(r, { recursive });
|
|
46
|
+
return { exitCode: 0 };
|
|
47
|
+
};
|
|
48
|
+
if (force)
|
|
49
|
+
return doRemove(shell);
|
|
50
|
+
const label = targets.length === 1 ? `'${targets[0]}'` : `${targets.length} items`;
|
|
51
|
+
const prompt = recursive
|
|
52
|
+
? `rm: remove ${label} recursively? [y/N] `
|
|
53
|
+
: `rm: remove ${label}? [y/N] `;
|
|
54
|
+
return {
|
|
55
|
+
sudoChallenge: {
|
|
56
|
+
username: authUser,
|
|
57
|
+
targetUser: authUser,
|
|
58
|
+
commandLine: null,
|
|
59
|
+
loginShell: false,
|
|
60
|
+
prompt,
|
|
61
|
+
mode: "confirm",
|
|
62
|
+
onPassword: async (input, sh) => {
|
|
63
|
+
const answer = input.trim().toLowerCase();
|
|
64
|
+
if (answer !== "y" && answer !== "yes") {
|
|
65
|
+
return { result: { stdout: "rm: cancelled\n", exitCode: 1 } };
|
|
66
|
+
}
|
|
67
|
+
return { result: doRemove(sh) };
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
exitCode: 0,
|
|
71
|
+
};
|
|
35
72
|
},
|
|
36
73
|
};
|
|
@@ -2,6 +2,11 @@ import type { VirtualShell } from "../VirtualShell";
|
|
|
2
2
|
import type { CommandMode, CommandResult, ShellEnv } from "../types/commands";
|
|
3
3
|
/** Returns the home directory path for a given user. Root lives at /root. */
|
|
4
4
|
export declare function userHome(authUser: string): string;
|
|
5
|
+
/**
|
|
6
|
+
* Apply a user switch: reset PS1/USER/HOME/LOGNAME in shellEnv and re-source
|
|
7
|
+
* the new user's .bashrc. Call this after setting authUser = newUser.
|
|
8
|
+
*/
|
|
9
|
+
export declare function applyUserSwitch(newUser: string, hostname: string, cwd: string, shellEnv: ShellEnv, shell: VirtualShell): Promise<void>;
|
|
5
10
|
export declare function makeDefaultEnv(authUser: string, hostname: string): ShellEnv;
|
|
6
11
|
export declare function runCommandDirect(name: string, args: string[], authUser: string, hostname: string, mode: CommandMode, cwd: string, shell: VirtualShell, stdin: string | undefined, env: ShellEnv): Promise<CommandResult>;
|
|
7
12
|
export declare function runCommand(rawInput: string, authUser: string, hostname: string, mode: CommandMode, cwd: string, shell: VirtualShell, stdin?: string, env?: ShellEnv): Promise<CommandResult>;
|
package/dist/commands/runtime.js
CHANGED
|
@@ -18,6 +18,28 @@ const RE_OPERATORS = /[><;&]|\|\|/;
|
|
|
18
18
|
export function userHome(authUser) {
|
|
19
19
|
return authUser === "root" ? "/root" : `/home/${authUser}`;
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Apply a user switch: reset PS1/USER/HOME/LOGNAME in shellEnv and re-source
|
|
23
|
+
* the new user's .bashrc. Call this after setting authUser = newUser.
|
|
24
|
+
*/
|
|
25
|
+
export async function applyUserSwitch(newUser, hostname, cwd, shellEnv, shell) {
|
|
26
|
+
shellEnv.vars.USER = newUser;
|
|
27
|
+
shellEnv.vars.LOGNAME = newUser;
|
|
28
|
+
shellEnv.vars.HOME = userHome(newUser);
|
|
29
|
+
shellEnv.vars.PS1 = makeDefaultEnv(newUser, hostname).vars.PS1 ?? "";
|
|
30
|
+
const rcPath = `${userHome(newUser)}/.bashrc`;
|
|
31
|
+
if (!shell.vfs.exists(rcPath))
|
|
32
|
+
return;
|
|
33
|
+
for (const raw of shell.vfs.readFile(rcPath).split("\n")) {
|
|
34
|
+
const l = raw.trim();
|
|
35
|
+
if (!l || l.startsWith("#"))
|
|
36
|
+
continue;
|
|
37
|
+
try {
|
|
38
|
+
await runCommand(l, newUser, hostname, "shell", cwd, shell, undefined, shellEnv);
|
|
39
|
+
}
|
|
40
|
+
catch { /* ignore */ }
|
|
41
|
+
}
|
|
42
|
+
}
|
|
21
43
|
export function makeDefaultEnv(authUser, hostname) {
|
|
22
44
|
return {
|
|
23
45
|
vars: {
|
|
@@ -28,7 +50,9 @@ export function makeDefaultEnv(authUser, hostname) {
|
|
|
28
50
|
SHELL: "/bin/bash",
|
|
29
51
|
TERM: "xterm-256color",
|
|
30
52
|
HOSTNAME: hostname,
|
|
31
|
-
PS1:
|
|
53
|
+
PS1: authUser === "root"
|
|
54
|
+
? "\\[\\e[37;1m\\][\\[\\e[31;1m\\]\\u\\[\\e[37;1m\\]@\\[\\e[34;1m\\]\\h\\[\\e[0m\\] \\w\\[\\e[37;1m\\]]\\[\\e[31;1m\\]\\$\\[\\e[0m\\] "
|
|
55
|
+
: "\\[\\e[37;1m\\][\\[\\e[35;1m\\]\\u\\[\\e[37;1m\\]@\\[\\e[34;1m\\]\\h\\[\\e[0m\\] \\w\\[\\e[37;1m\\]]\\[\\e[0m\\]\\$ ",
|
|
32
56
|
"0": "/bin/bash",
|
|
33
57
|
},
|
|
34
58
|
lastExitCode: 0,
|
|
@@ -59,7 +59,11 @@ function bootstrapEtc(vfs, hostname, props) {
|
|
|
59
59
|
ensureFile(vfs, "/etc/shells", "/bin/sh\n/bin/bash\n/usr/bin/bash\n/bin/dash\n/usr/bin/dash\n");
|
|
60
60
|
ensureFile(vfs, "/etc/profile", `${[
|
|
61
61
|
"export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
|
62
|
-
"
|
|
62
|
+
"if [ \"$(id -u)\" -eq 0 ]; then",
|
|
63
|
+
" export PS1='\\[\\e[37;1m\\][\\[\\e[31;1m\\]\\u\\[\\e[37;1m\\]@\\[\\e[34;1m\\]\\h\\[\\e[0m\\] \\w\\[\\e[37;1m\\]]\\[\\e[31;1m\\]\\$\\[\\e[0m\\] '",
|
|
64
|
+
"else",
|
|
65
|
+
" export PS1='\\[\\e[37;1m\\][\\[\\e[35;1m\\]\\u\\[\\e[37;1m\\]@\\[\\e[34;1m\\]\\h\\[\\e[0m\\] \\w\\[\\e[37;1m\\]]\\[\\e[0m\\]\\$ '",
|
|
66
|
+
"fi",
|
|
63
67
|
].join("\n")}\n`);
|
|
64
68
|
ensureFile(vfs, "/etc/issue", "Fortune GNU/Linux 24.04 LTS \\n \\l\n");
|
|
65
69
|
ensureFile(vfs, "/etc/issue.net", "Fortune GNU/Linux 24.04 LTS\n");
|
|
@@ -1281,7 +1285,7 @@ Installed-Size: 6800
|
|
|
1281
1285
|
Maintainer: Fortune Package Team <dpkg@fortune.local>
|
|
1282
1286
|
Architecture: amd64
|
|
1283
1287
|
Version: 1.22.6nyx1
|
|
1284
|
-
Depends: libc6 (>= 2.17), libzstd1 (>= 1.5.
|
|
1288
|
+
Depends: libc6 (>= 2.17), libzstd1 (>= 1.5.8)
|
|
1285
1289
|
Description: Fortune package management system
|
|
1286
1290
|
This package provides the low-level infrastructure for handling the
|
|
1287
1291
|
installation and removal of Fortune software packages.
|
|
@@ -1449,7 +1453,7 @@ function bootstrapRoot(vfs) {
|
|
|
1449
1453
|
ensureDir(vfs, "/root/.local/share", 0o755);
|
|
1450
1454
|
ensureFile(vfs, "/root/.bashrc", `${[
|
|
1451
1455
|
"# root .bashrc",
|
|
1452
|
-
"export PS1='\\[\\
|
|
1456
|
+
"export PS1='\\[\\e[37;1m\\][\\[\\e[31;1m\\]\\u\\[\\e[37;1m\\]@\\[\\e[34;1m\\]\\h\\[\\e[0m\\] \\w\\[\\e[37;1m\\]]\\[\\e[31;1m\\]\\$\\[\\e[0m\\] '",
|
|
1453
1457
|
"export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
|
1454
1458
|
"export LANG=en_US.UTF-8",
|
|
1455
1459
|
"alias ll='ls -la'",
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { TerminalSize } from "./shellRuntime";
|
|
2
|
+
import type { ShellStream } from "../types/streams";
|
|
3
|
+
export type NanoExitReason = "saved" | "aborted";
|
|
4
|
+
export interface NanoEditorOptions {
|
|
5
|
+
stream: ShellStream;
|
|
6
|
+
terminalSize: TerminalSize;
|
|
7
|
+
content: string;
|
|
8
|
+
filename: string;
|
|
9
|
+
onExit: (reason: NanoExitReason, content: string) => void;
|
|
10
|
+
/** Called on ^S / silent save — save without closing nano. Optional. */
|
|
11
|
+
onSave?: (content: string) => void;
|
|
12
|
+
}
|
|
13
|
+
export declare class NanoEditor {
|
|
14
|
+
private lines;
|
|
15
|
+
private cursorRow;
|
|
16
|
+
private cursorCol;
|
|
17
|
+
private scrollTop;
|
|
18
|
+
private modified;
|
|
19
|
+
private filename;
|
|
20
|
+
private mode;
|
|
21
|
+
private inputBuffer;
|
|
22
|
+
private searchState;
|
|
23
|
+
private clipboard;
|
|
24
|
+
private undoStack;
|
|
25
|
+
private redoStack;
|
|
26
|
+
private markActive;
|
|
27
|
+
private readonly stream;
|
|
28
|
+
private terminalSize;
|
|
29
|
+
private readonly onExit;
|
|
30
|
+
private readonly onSave;
|
|
31
|
+
constructor(opts: NanoEditorOptions);
|
|
32
|
+
start(): void;
|
|
33
|
+
resize(size: TerminalSize): void;
|
|
34
|
+
handleInput(chunk: Buffer): void;
|
|
35
|
+
private consumeSequence;
|
|
36
|
+
private handleEscape;
|
|
37
|
+
private handleAlt;
|
|
38
|
+
private handleChar;
|
|
39
|
+
private handleControl;
|
|
40
|
+
private dispatch;
|
|
41
|
+
private handlePromptChar;
|
|
42
|
+
private moveCursor;
|
|
43
|
+
private moveCursorLeft;
|
|
44
|
+
private moveCursorRight;
|
|
45
|
+
private moveCursorHome;
|
|
46
|
+
private moveCursorEnd;
|
|
47
|
+
private movePage;
|
|
48
|
+
private moveWordRight;
|
|
49
|
+
private moveWordLeft;
|
|
50
|
+
private pushUndo;
|
|
51
|
+
private doInsertChar;
|
|
52
|
+
private doEnter;
|
|
53
|
+
private doBackspace;
|
|
54
|
+
private doDelete;
|
|
55
|
+
private doCutLine;
|
|
56
|
+
private doUncut;
|
|
57
|
+
private doUndo;
|
|
58
|
+
private doRedo;
|
|
59
|
+
private enterSearch;
|
|
60
|
+
private doSearch;
|
|
61
|
+
private doSearchNext;
|
|
62
|
+
private doSearchReplace;
|
|
63
|
+
private toggleMark;
|
|
64
|
+
private doExit;
|
|
65
|
+
private doSave;
|
|
66
|
+
private enterWriteout;
|
|
67
|
+
private showCursorPos;
|
|
68
|
+
private enterGotoLine;
|
|
69
|
+
private enterHelp;
|
|
70
|
+
private get cols();
|
|
71
|
+
private get rows();
|
|
72
|
+
private editAreaRows;
|
|
73
|
+
private editAreaStart;
|
|
74
|
+
private currentLine;
|
|
75
|
+
private clampScroll;
|
|
76
|
+
private getCurrentContent;
|
|
77
|
+
private pad;
|
|
78
|
+
fullRedraw(): void;
|
|
79
|
+
private renderTitleBar;
|
|
80
|
+
private renderEditArea;
|
|
81
|
+
private renderLine;
|
|
82
|
+
private renderCursor;
|
|
83
|
+
private renderStatusLine;
|
|
84
|
+
private renderStatusBar;
|
|
85
|
+
private buildTitleBar;
|
|
86
|
+
private buildEditArea;
|
|
87
|
+
private renderLineText;
|
|
88
|
+
private buildHelpBar;
|
|
89
|
+
private buildShortcutRow;
|
|
90
|
+
private buildCursorPosition;
|
|
91
|
+
private renderHelp;
|
|
92
|
+
}
|