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
@@ -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;
@@ -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 at startup
27
- void (async () => {
28
- const sourceFile = async (filePath, isEnvFile = false) => {
29
- if (!shell.vfs.exists(filePath))
30
- return;
31
- try {
32
- const content = shell.vfs.readFile(filePath);
33
- for (const line of content.split("\n")) {
34
- const l = line.trim();
35
- if (!l || l.startsWith("#"))
36
- continue;
37
- if (isEnvFile) {
38
- // /etc/environment: KEY=VALUE pairs only, no shell syntax
39
- const m = l.match(/^([A-Za-z_][A-Za-z0-9_]*)=["']?(.+?)["']?\s*$/);
40
- if (m)
41
- shellEnv.vars[m[1]] = m[2];
42
- }
43
- else {
44
- await runCommand(l, authUser, hostname, "shell", cwd, shell, undefined, shellEnv);
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
- catch { /* ignore */ }
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
- async function finishNanoEditor() {
126
- if (!nanoSession) {
127
- return;
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
- 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");
145
144
  renderLine();
146
145
  }
147
- async function startNanoEditor(targetPath, initialContent, tempPath) {
148
- if (shell.vfs.exists(targetPath)) {
149
- await writeFile(tempPath, initialContent, "utf8");
150
- }
151
- const editor = spawnNanoEditorProcess(tempPath, terminalSize, stream);
152
- editor.on("error", (error) => {
153
- stream.write(`nano: ${error.message}\r\n`);
154
- void finishNanoEditor();
155
- });
156
- editor.on("close", () => {
157
- 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
+ },
158
160
  });
159
- nanoSession = {
160
- kind: "nano",
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
- void finishNanoEditor();
173
+ finishInteractiveSession();
176
174
  });
177
175
  monitor.on("close", () => {
178
- void finishNanoEditor();
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(nowIso);
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.process.stdin.write(chunk);
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
- stream.exit(0);
404
- stream.end();
405
- return;
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
- stream.exit(result.exitCode ?? 0);
602
- stream.end();
603
- return;
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.process.kill("SIGTERM");
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
@@ -5,7 +5,7 @@
5
5
  */
6
6
  export const exitCommand = {
7
7
  name: "exit",
8
- aliases: ["bye"],
8
+ aliases: ["bye", "logout"],
9
9
  description: "Exit the shell session",
10
10
  category: "shell",
11
11
  params: ["[code]"],
@@ -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;
@@ -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
  }
@@ -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
@@ -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
- let processed = code
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 $_
@@ -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
- for (let i = 0; i < right; i++)
698
- arr.push(...left);
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;