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.
Files changed (58) hide show
  1. package/README.md +28 -20
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/SSHMimic/index.d.ts +5 -1
  4. package/dist/SSHMimic/index.js +27 -3
  5. package/dist/SSHMimic/prompt.d.ts +2 -1
  6. package/dist/SSHMimic/prompt.js +27 -5
  7. package/dist/SSHMimic/scp.d.ts +34 -0
  8. package/dist/SSHMimic/scp.js +285 -0
  9. package/dist/SSHMimic/sftp.d.ts +53 -3
  10. package/dist/SSHMimic/sftp.js +9 -3
  11. package/dist/VirtualFileSystem/binaryPack.d.ts +7 -0
  12. package/dist/VirtualFileSystem/binaryPack.js +37 -1
  13. package/dist/VirtualFileSystem/index.d.ts +7 -0
  14. package/dist/VirtualFileSystem/index.js +67 -27
  15. package/dist/VirtualFileSystem/internalTypes.d.ts +2 -0
  16. package/dist/VirtualFileSystem/path.d.ts +5 -0
  17. package/dist/VirtualFileSystem/path.js +24 -11
  18. package/dist/VirtualPackageManager/index.d.ts +4 -2
  19. package/dist/VirtualPackageManager/index.js +24 -4
  20. package/dist/VirtualShell/index.d.ts +6 -3
  21. package/dist/VirtualShell/index.js +3 -10
  22. package/dist/VirtualShell/shell.js +114 -140
  23. package/dist/VirtualShell/shellParser.js +1 -22
  24. package/dist/commands/exit.js +1 -1
  25. package/dist/commands/find.js +1 -4
  26. package/dist/commands/helpers.d.ts +0 -20
  27. package/dist/commands/helpers.js +0 -97
  28. package/dist/commands/id.js +8 -1
  29. package/dist/commands/index.d.ts +1 -1
  30. package/dist/commands/index.js +1 -1
  31. package/dist/commands/manuals-bundle.js +10 -1
  32. package/dist/commands/perl.js +1 -1
  33. package/dist/commands/python.js +5 -2
  34. package/dist/commands/registry.js +6 -1
  35. package/dist/commands/rm.d.ts +1 -1
  36. package/dist/commands/rm.js +48 -11
  37. package/dist/commands/runtime.d.ts +5 -0
  38. package/dist/commands/runtime.js +90 -88
  39. package/dist/commands/strace.js +1 -1
  40. package/dist/commands/tar.js +2 -2
  41. package/dist/commands/test.js +2 -2
  42. package/dist/modules/linuxRootfs.js +7 -6
  43. package/dist/modules/nanoEditor.d.ts +92 -0
  44. package/dist/modules/nanoEditor.js +956 -0
  45. package/dist/modules/neofetch.js +2 -2
  46. package/dist/modules/webTermRenderer.d.ts +42 -0
  47. package/dist/modules/webTermRenderer.js +291 -0
  48. package/dist/types/commands.d.ts +4 -0
  49. package/dist/utils/argv.d.ts +6 -0
  50. package/dist/utils/argv.js +32 -0
  51. package/dist/utils/expand.d.ts +5 -2
  52. package/dist/utils/expand.js +70 -67
  53. package/dist/utils/glob.d.ts +6 -0
  54. package/dist/utils/glob.js +34 -0
  55. package/dist/utils/shellSession.d.ts +10 -0
  56. package/dist/utils/shellSession.js +56 -0
  57. package/dist/utils/tokenize.js +13 -13
  58. package/package.json +7 -6
@@ -255,7 +255,12 @@ export function registerCommand(module) {
255
255
  throw new Error("Command names must be non-empty and contain no spaces");
256
256
  }
257
257
  customCommands.push(normalized);
258
- buildCache();
258
+ // Incremental insert — avoids full Map rebuild for every registerCommand call
259
+ commandRegistry.set(normalized.name, normalized);
260
+ for (const alias of normalized.aliases ?? [])
261
+ commandRegistry.set(alias, normalized);
262
+ // Invalidate sorted names cache; rebuilt lazily on next getCommandNames()
263
+ cachedCommandNames = null;
259
264
  }
260
265
  export function createCustomCommand(name, params, run) {
261
266
  return { name, params, run };
@@ -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>;
@@ -4,10 +4,42 @@ import { parseScript } from "../VirtualShell/shellParser";
4
4
  import { expandAsync, expandBraces, expandGlob } from "../utils/expand";
5
5
  import { tokenizeCommand } from "../utils/tokenize";
6
6
  import { resolveModule } from "./registry";
7
+ // Module-level compiled regexes — avoids recompilation on every runCommand call
8
+ const ASSIGN_RE = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/;
9
+ const RE_FOR = /\bfor\s+\w+\s+in\b/;
10
+ const RE_WHILE = /\bwhile\s+/;
11
+ const RE_IF = /\bif\s+/;
12
+ const RE_FUNC_BRACE = /\w+\s*\(\s*\)\s*\{/;
13
+ const RE_FUNC_KW = /\bfunction\s+\w+/;
14
+ const RE_ARITH = /\(\(\s*.+\s*\)\)/;
15
+ const RE_PIPE = /(?<![|&])[|](?![|])/;
16
+ const RE_OPERATORS = /[><;&]|\|\|/;
7
17
  /** Returns the home directory path for a given user. Root lives at /root. */
8
18
  export function userHome(authUser) {
9
19
  return authUser === "root" ? "/root" : `/home/${authUser}`;
10
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
+ }
11
43
  export function makeDefaultEnv(authUser, hostname) {
12
44
  return {
13
45
  vars: {
@@ -18,7 +50,9 @@ export function makeDefaultEnv(authUser, hostname) {
18
50
  SHELL: "/bin/bash",
19
51
  TERM: "xterm-256color",
20
52
  HOSTNAME: hostname,
21
- 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\\]\\$ ",
22
56
  "0": "/bin/bash",
23
57
  },
24
58
  lastExitCode: 0,
@@ -43,7 +77,13 @@ function resolveVfsBinary(name, env, shell, authUser) {
43
77
  return null;
44
78
  }
45
79
  }
46
- const pathDirs = (env.vars.PATH ?? "/usr/local/bin:/usr/bin:/bin").split(":");
80
+ const rawPath = env.vars.PATH ?? "/usr/local/bin:/usr/bin:/bin";
81
+ // Cache split PATH on the env object to avoid re-splitting on every binary lookup
82
+ if (!env._pathDirs || env._pathRaw !== rawPath) {
83
+ env._pathRaw = rawPath;
84
+ env._pathDirs = rawPath.split(":");
85
+ }
86
+ const pathDirs = env._pathDirs;
47
87
  for (const dir of pathDirs) {
48
88
  if ((dir === "/sbin" || dir === "/usr/sbin") && authUser !== "root")
49
89
  continue;
@@ -63,6 +103,35 @@ function resolveVfsBinary(name, env, shell, authUser) {
63
103
  return null;
64
104
  }
65
105
  const MAX_CALL_DEPTH = 8;
106
+ /** Run a VFS stub file as a command, handling `exec builtin <name>` and `sh -c` stubs. */
107
+ async function runVfsStub(vfsBinary, cmdName, args, rawInput, authUser, hostname, mode, cwd, shell, env, stdin) {
108
+ const stubContent = shell.vfs.readFile(vfsBinary);
109
+ const builtinMatch = stubContent.match(/exec\s+builtin\s+(\S+)/);
110
+ if (builtinMatch) {
111
+ const builtinMod = resolveModule(builtinMatch[1]);
112
+ if (builtinMod) {
113
+ return builtinMod.run({
114
+ authUser, hostname,
115
+ activeSessions: shell.users.listActiveSessions(),
116
+ rawInput, mode, args, stdin, cwd, shell, env,
117
+ });
118
+ }
119
+ // Guard: missing builtin — stop here to avoid sh -c infinite loop
120
+ return { stderr: `${cmdName}: exec builtin '${builtinMatch[1]}' not found`, exitCode: 127 };
121
+ }
122
+ const shMod = resolveModule("sh");
123
+ if (shMod) {
124
+ return shMod.run({
125
+ authUser, hostname,
126
+ activeSessions: shell.users.listActiveSessions(),
127
+ rawInput: `sh -c ${JSON.stringify(stubContent)}`,
128
+ mode,
129
+ args: ["-c", stubContent, "--", ...args],
130
+ stdin, cwd, shell, env,
131
+ });
132
+ }
133
+ return { stderr: `${cmdName}: command not found`, exitCode: 127 };
134
+ }
66
135
  let _callDepth = 0;
67
136
  export async function runCommandDirect(name, args, authUser, hostname, mode, cwd, shell, stdin, env) {
68
137
  // Anti-loop guard: track call depth via env to avoid infinite recursion
@@ -81,7 +150,7 @@ export async function runCommandDirect(name, args, authUser, hostname, mode, cwd
81
150
  }
82
151
  }
83
152
  async function _runCommandDirectInner(name, args, authUser, hostname, mode, cwd, shell, stdin, env) {
84
- const assignRe = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/;
153
+ const assignRe = ASSIGN_RE;
85
154
  const invocation = [name, ...args];
86
155
  let assignCount = 0;
87
156
  while (assignCount < invocation.length && assignRe.test(invocation[assignCount])) {
@@ -119,42 +188,7 @@ async function _runCommandDirectInner(name, args, authUser, hostname, mode, cwd,
119
188
  if (!mod) {
120
189
  const vfsBinary = resolveVfsBinary(name, env, shell, authUser);
121
190
  if (vfsBinary) {
122
- const stubContent = shell.vfs.readFile(vfsBinary);
123
- const builtinMatch = stubContent.match(/exec\s+builtin\s+(\S+)/);
124
- if (builtinMatch) {
125
- const builtinMod = resolveModule(builtinMatch[1]);
126
- if (builtinMod) {
127
- return await builtinMod.run({
128
- authUser,
129
- hostname,
130
- activeSessions: shell.users.listActiveSessions(),
131
- rawInput: [name, ...args].join(" "),
132
- mode,
133
- args,
134
- stdin,
135
- cwd,
136
- shell,
137
- env,
138
- });
139
- }
140
- // builtin not found — stop here, don't fall through to sh -c (avoids infinite loop)
141
- return { stderr: `${name}: exec builtin '${builtinMatch[1]}' not found`, exitCode: 127 };
142
- }
143
- const shMod = resolveModule("sh");
144
- if (shMod) {
145
- return await shMod.run({
146
- authUser,
147
- hostname,
148
- activeSessions: shell.users.listActiveSessions(),
149
- rawInput: `sh -c ${JSON.stringify(stubContent)}`,
150
- mode,
151
- args: ["-c", stubContent, "--", ...args],
152
- stdin,
153
- cwd,
154
- shell,
155
- env,
156
- });
157
- }
191
+ return runVfsStub(vfsBinary, name, args, [name, ...args].join(" "), authUser, hostname, mode, cwd, shell, env, stdin);
158
192
  }
159
193
  return { stderr: `${name}: command not found`, exitCode: 127 };
160
194
  }
@@ -220,18 +254,13 @@ export async function runCommand(rawInput, authUser, hostname, mode, cwd, shell,
220
254
  ? trimmed.replace(rawFirstWord, aliasVal)
221
255
  : trimmed;
222
256
  // Detect sh-syntax constructs that must be handled by the sh interpreter
223
- const isShScript = /\bfor\s+\w+\s+in\b/.test(aliasExpanded) ||
224
- /\bwhile\s+/.test(aliasExpanded) ||
225
- /\bif\s+/.test(aliasExpanded) ||
226
- /\w+\s*\(\s*\)\s*\{/.test(aliasExpanded) ||
227
- /\bfunction\s+\w+/.test(aliasExpanded) ||
228
- /\(\(\s*.+\s*\)\)/.test(aliasExpanded);
229
- const hasOperators = /(?<![|&])[|](?![|])/.test(aliasExpanded) ||
230
- aliasExpanded.includes(">") ||
231
- aliasExpanded.includes("<") ||
232
- aliasExpanded.includes("&&") ||
233
- aliasExpanded.includes("||") ||
234
- aliasExpanded.includes(";");
257
+ const isShScript = RE_FOR.test(aliasExpanded) ||
258
+ RE_WHILE.test(aliasExpanded) ||
259
+ RE_IF.test(aliasExpanded) ||
260
+ RE_FUNC_BRACE.test(aliasExpanded) ||
261
+ RE_FUNC_KW.test(aliasExpanded) ||
262
+ RE_ARITH.test(aliasExpanded);
263
+ const hasOperators = RE_PIPE.test(aliasExpanded) || RE_OPERATORS.test(aliasExpanded);
235
264
  if ((isShScript && rawFirstWord !== "sh" && rawFirstWord !== "bash") || hasOperators) {
236
265
  // sh-syntax: route through sh interpreter to handle for/while/functions
237
266
  if (isShScript && rawFirstWord !== "sh" && rawFirstWord !== "bash") {
@@ -267,52 +296,25 @@ export async function runCommand(rawInput, authUser, hostname, mode, cwd, shell,
267
296
  const parts = tokenizeCommand(expanded.trim());
268
297
  if (parts.length === 0)
269
298
  return { exitCode: 0 };
270
- const assignRe = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/;
299
+ const assignRe = ASSIGN_RE;
271
300
  if (assignRe.test(parts[0])) {
272
301
  return runCommandDirect(parts[0], parts.slice(1), authUser, hostname, mode, cwd, shell, stdin, shellEnv);
273
302
  }
274
303
  const commandName = parts[0]?.toLowerCase() ?? "";
275
304
  // Apply brace expansion to each arg token
276
- const args = parts.slice(1).flatMap(expandBraces).flatMap(token => expandGlob(token, cwd, shell.vfs));
305
+ const rawArgs = parts.slice(1);
306
+ const args = [];
307
+ for (const token of rawArgs) {
308
+ for (const brace of expandBraces(token)) {
309
+ for (const glob of expandGlob(brace, cwd, shell.vfs))
310
+ args.push(glob);
311
+ }
312
+ }
277
313
  const mod = resolveModule(commandName);
278
314
  if (!mod) {
279
315
  const vfsBinary = resolveVfsBinary(commandName, shellEnv, shell, authUser);
280
316
  if (vfsBinary) {
281
- const stubContent = shell.vfs.readFile(vfsBinary);
282
- const builtinMatch = stubContent.match(/exec\s+builtin\s+(\S+)/);
283
- if (builtinMatch) {
284
- const builtinName = builtinMatch[1];
285
- const builtinMod = resolveModule(builtinName);
286
- if (builtinMod) {
287
- return await builtinMod.run({
288
- authUser,
289
- hostname,
290
- activeSessions: shell.users.listActiveSessions(),
291
- rawInput: [commandName, ...args].join(" "),
292
- mode,
293
- args,
294
- stdin,
295
- cwd,
296
- shell,
297
- env: shellEnv,
298
- });
299
- }
300
- }
301
- const shMod = resolveModule("sh");
302
- if (shMod) {
303
- return await shMod.run({
304
- authUser,
305
- hostname,
306
- activeSessions: shell.users.listActiveSessions(),
307
- rawInput: `sh -c ${JSON.stringify(stubContent)}`,
308
- mode,
309
- args: ["-c", stubContent, "--", ...args],
310
- stdin,
311
- cwd,
312
- shell,
313
- env: shellEnv,
314
- });
315
- }
317
+ return runVfsStub(vfsBinary, commandName, args, expanded, authUser, hostname, mode, cwd, shell, shellEnv, stdin);
316
318
  }
317
319
  return { stderr: `${commandName}: command not found`, exitCode: 127 };
318
320
  }
@@ -11,7 +11,7 @@ export const straceCommand = {
11
11
  const cmd = args.find((a) => !a.startsWith("-"));
12
12
  if (!cmd)
13
13
  return { stderr: "strace: must have PROG [ARGS] or -p PID", exitCode: 1 };
14
- const pid = Math.floor(Math.random() * 30000) + 1000;
14
+ const _pid = Math.floor(Math.random() * 30000) + 1000;
15
15
  const lines = [
16
16
  `execve("/usr/bin/${cmd}", ["${cmd}"${args.slice(1).map((a) => `, "${a}"`).join("")}], 0x... /* ... vars */) = 0`,
17
17
  `brk(NULL) = 0x${(Math.random() * 0xfffff | 0).toString(16)}000`,
@@ -11,8 +11,8 @@ function makeTarHeader(name, size, isDir) {
11
11
  enc(isDir ? "0000755\0" : "0000644\0", 100, 8);
12
12
  enc("0000000\0", 108, 8);
13
13
  enc("0000000\0", 116, 8);
14
- enc(size.toString(8).padStart(11, "0") + "\0", 124, 12);
15
- enc(Math.floor(Date.now() / 1000).toString(8).padStart(11, "0") + "\0", 136, 12);
14
+ enc(`${size.toString(8).padStart(11, "0")}\0`, 124, 12);
15
+ enc(`${Math.floor(Date.now() / 1000).toString(8).padStart(11, "0")}\0`, 136, 12);
16
16
  hdr[156] = isDir ? 0x35 : 0x30; // '5' dir, '0' file
17
17
  enc("ustar\0", 257, 6);
18
18
  enc("00", 263, 2);
@@ -1,3 +1,4 @@
1
+ import { resolvePath } from "./helpers";
1
2
  /**
2
3
  * Evaluate a POSIX test expression.
3
4
  * Supports: -f, -d, -e, -r, -w, -x, -s, -z, -n,
@@ -33,8 +34,7 @@ function evalTest(tokens, shell, cwd) {
33
34
  // Unary file tests
34
35
  if (tokens.length === 2) {
35
36
  const [flag, operand = ""] = tokens;
36
- const resolvePath = (p) => p.startsWith("/") ? p : `${cwd}/${p}`.replace(/\/+/g, "/");
37
- const path = resolvePath(operand);
37
+ const path = resolvePath(cwd, operand);
38
38
  switch (flag) {
39
39
  case "-e":
40
40
  return shell.vfs.exists(path);
@@ -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.6)
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'",
@@ -1547,12 +1551,9 @@ export function bootstrapLinuxRootfs(vfs, users, hostname, props, shellStartTime
1547
1551
  const snapshot = getStaticRootfsSnapshot(hostname, props);
1548
1552
  const hasRestoredData = vfs.getMode() === "fs" && vfs.exists("/home");
1549
1553
  if (hasRestoredData) {
1550
- // Snapshot was already restored — merge static rootfs without
1551
- // clobbering user files and directories.
1552
1554
  vfs.mergeRootTree(decodeVfs(snapshot));
1553
1555
  }
1554
1556
  else {
1555
- // Fresh start — replace the empty tree with the full static rootfs.
1556
1557
  vfs.importRootTree(decodeVfs(snapshot));
1557
1558
  }
1558
1559
  bootstrapRoot(vfs);
@@ -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
+ }