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.
@@ -60,9 +60,8 @@ function resolveAutoSudoForNewUsers() {
60
60
  * const result = await client.exec("uname -a");
61
61
  * ```
62
62
  *
63
- * @fires VirtualShell#initialized Emitted once the VFS and users are ready.
64
- * @fires VirtualShell#command Emitted after every command execution.
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 { spawnHtopProcess, spawnNanoEditorProcess, } from "../modules/shellInteractive";
5
- import { getVisibleHtopPidList, resolvePath, toTtyLines, } from "../modules/shellRuntime";
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 at startup
28
- void (async () => {
29
- const sourceFile = async (filePath, isEnvFile = false) => {
30
- if (!shell.vfs.exists(filePath))
31
- return;
32
- try {
33
- const content = shell.vfs.readFile(filePath);
34
- for (const line of content.split("\n")) {
35
- const l = line.trim();
36
- if (!l || l.startsWith("#"))
37
- continue;
38
- if (isEnvFile) {
39
- // /etc/environment: KEY=VALUE pairs only, no shell syntax
40
- const m = l.match(/^([A-Za-z_][A-Za-z0-9_]*)=["']?(.+?)["']?\s*$/);
41
- if (m)
42
- shellEnv.vars[m[1]] = m[2];
43
- }
44
- else {
45
- await runCommand(l, authUser, hostname, "shell", cwd, shell, undefined, shellEnv);
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
- catch { /* ignore */ }
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
- async function finishNanoEditor() {
128
- if (!nanoSession) {
129
- return;
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
- stream.write("\r\n");
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
- async function startNanoEditor(targetPath, initialContent, tempPath) {
150
- if (shell.vfs.exists(targetPath)) {
151
- await writeFile(tempPath, initialContent, "utf8");
152
- }
153
- const editor = spawnNanoEditorProcess(tempPath, terminalSize, stream);
154
- editor.on("error", (error) => {
155
- stream.write(`nano: ${error.message}\r\n`);
156
- void finishNanoEditor();
157
- });
158
- editor.on("close", () => {
159
- void finishNanoEditor();
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
- kind: "nano",
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
- void finishNanoEditor();
173
+ finishInteractiveSession();
178
174
  });
179
175
  monitor.on("close", () => {
180
- void finishNanoEditor();
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(nowIso);
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.process.stdin.write(chunk);
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.process.kill("SIGTERM");
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
- }
@@ -4,11 +4,18 @@ export const idCommand = {
4
4
  category: "system",
5
5
  params: ["[user]"],
6
6
  run: ({ authUser, shell, args }) => {
7
- const target = args[0] ?? authUser;
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,
@@ -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";
@@ -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 remove directories and their contents recursively`,
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
@@ -2,6 +2,6 @@ import type { ShellModule } from "../types/commands";
2
2
  /**
3
3
  * Remove files or directories from the filesystem.
4
4
  * @category files
5
- * @params ["[-r|-rf] <path>"]
5
+ * @params ["[-r|-rf|-f] <path>"]
6
6
  */
7
7
  export declare const rmCommand: ShellModule;
@@ -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, ["-r", "-rf", "-fr"]);
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: ["-r", "-rf", "-fr"] });
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
- for (const target of targets) {
30
- const resolvedTarget = resolvePath(cwd, target);
31
- assertPathAccess(authUser, resolvedTarget, "rm");
32
- shell.vfs.remove(resolvedTarget, { recursive });
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
- return { exitCode: 0 };
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>;
@@ -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: "\\u@\\h:\\w\\$ ",
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
- "export PS1='\\u@\\h:\\w\\$ '",
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.7)
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='\\[\\033[0;31m\\]\\u@\\h\\[\\033[0m\\]:\\[\\033[0;34m\\]\\w\\[\\033[0m\\]# '",
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
+ }