typescript-virtual-container 1.5.11 → 1.6.0

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 (132) hide show
  1. package/README.md +236 -456
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/Honeypot/index.d.ts +9 -0
  4. package/dist/Honeypot/index.js +57 -0
  5. package/dist/SSHMimic/exec.d.ts +4 -0
  6. package/dist/SSHMimic/exec.js +4 -0
  7. package/dist/SSHMimic/executor.d.ts +10 -1
  8. package/dist/SSHMimic/executor.js +18 -8
  9. package/dist/SSHMimic/hostKey.d.ts +5 -0
  10. package/dist/SSHMimic/hostKey.js +5 -0
  11. package/dist/SSHMimic/loginBanner.d.ts +7 -0
  12. package/dist/SSHMimic/loginBanner.js +4 -0
  13. package/dist/SSHMimic/loginFormat.d.ts +4 -0
  14. package/dist/SSHMimic/loginFormat.js +4 -0
  15. package/dist/SSHMimic/prompt.d.ts +9 -0
  16. package/dist/SSHMimic/prompt.js +9 -0
  17. package/dist/SSHMimic/scp.d.ts +18 -0
  18. package/dist/SSHMimic/scp.js +14 -0
  19. package/dist/VirtualFileSystem/binaryPack.d.ts +7 -3
  20. package/dist/VirtualFileSystem/binaryPack.js +32 -10
  21. package/dist/VirtualFileSystem/index.d.ts +29 -0
  22. package/dist/VirtualFileSystem/index.js +126 -5
  23. package/dist/VirtualFileSystem/internalTypes.d.ts +4 -0
  24. package/dist/VirtualFileSystem/journal.d.ts +10 -4
  25. package/dist/VirtualFileSystem/journal.js +12 -2
  26. package/dist/VirtualFileSystem/path.d.ts +23 -1
  27. package/dist/VirtualFileSystem/path.js +23 -3
  28. package/dist/VirtualPackageManager/index.js +1 -1
  29. package/dist/VirtualShell/index.d.ts +3 -0
  30. package/dist/VirtualShell/index.js +12 -3
  31. package/dist/VirtualUserManager/index.d.ts +20 -1
  32. package/dist/VirtualUserManager/index.js +52 -15
  33. package/dist/commands/bc.d.ts +5 -0
  34. package/dist/commands/bc.js +5 -0
  35. package/dist/commands/cat.js +2 -2
  36. package/dist/commands/chgrp.d.ts +7 -0
  37. package/dist/commands/chgrp.js +42 -0
  38. package/dist/commands/chown.d.ts +7 -0
  39. package/dist/commands/chown.js +79 -0
  40. package/dist/commands/cp.js +4 -3
  41. package/dist/commands/dd.d.ts +7 -0
  42. package/dist/commands/dd.js +60 -0
  43. package/dist/commands/declare.js +0 -2
  44. package/dist/commands/expr.d.ts +7 -0
  45. package/dist/commands/expr.js +63 -0
  46. package/dist/commands/fun.d.ts +5 -0
  47. package/dist/commands/fun.js +5 -1
  48. package/dist/commands/help.d.ts +5 -0
  49. package/dist/commands/help.js +5 -0
  50. package/dist/commands/helpers.d.ts +43 -0
  51. package/dist/commands/helpers.js +61 -0
  52. package/dist/commands/id.d.ts +5 -0
  53. package/dist/commands/id.js +5 -0
  54. package/dist/commands/ip.d.ts +1 -0
  55. package/dist/commands/ip.js +50 -23
  56. package/dist/commands/jobs.js +43 -9
  57. package/dist/commands/kill.d.ts +1 -0
  58. package/dist/commands/kill.js +13 -5
  59. package/dist/commands/last.js +1 -1
  60. package/dist/commands/ln.d.ts +5 -0
  61. package/dist/commands/ln.js +5 -0
  62. package/dist/commands/ls.d.ts +5 -0
  63. package/dist/commands/ls.js +19 -4
  64. package/dist/commands/lsb-release.js +1 -1
  65. package/dist/commands/man.d.ts +5 -0
  66. package/dist/commands/man.js +5 -0
  67. package/dist/commands/manuals-bundle.js +242 -0
  68. package/dist/commands/miscutils.d.ts +43 -0
  69. package/dist/commands/miscutils.js +233 -0
  70. package/dist/commands/mkdir.js +3 -2
  71. package/dist/commands/mv.js +4 -3
  72. package/dist/commands/netcat.d.ts +7 -0
  73. package/dist/commands/netcat.js +64 -0
  74. package/dist/commands/nice.d.ts +7 -0
  75. package/dist/commands/nice.js +22 -0
  76. package/dist/commands/nohup.d.ts +7 -0
  77. package/dist/commands/nohup.js +18 -0
  78. package/dist/commands/ping.d.ts +2 -1
  79. package/dist/commands/ping.js +46 -8
  80. package/dist/commands/procUtils.d.ts +13 -0
  81. package/dist/commands/procUtils.js +72 -0
  82. package/dist/commands/pwd.d.ts +5 -0
  83. package/dist/commands/pwd.js +5 -0
  84. package/dist/commands/python.js +0 -4
  85. package/dist/commands/read.js +0 -1
  86. package/dist/commands/registry.d.ts +37 -0
  87. package/dist/commands/registry.js +73 -0
  88. package/dist/commands/rm.js +3 -2
  89. package/dist/commands/runtime.d.ts +47 -1
  90. package/dist/commands/runtime.js +60 -5
  91. package/dist/commands/sh.d.ts +5 -0
  92. package/dist/commands/sh.js +5 -0
  93. package/dist/commands/stat.js +3 -2
  94. package/dist/commands/strace.js +0 -1
  95. package/dist/commands/sysinfo.d.ts +19 -0
  96. package/dist/commands/sysinfo.js +73 -0
  97. package/dist/commands/test.d.ts +5 -0
  98. package/dist/commands/test.js +5 -0
  99. package/dist/commands/textutils.d.ts +25 -0
  100. package/dist/commands/textutils.js +171 -0
  101. package/dist/commands/top.d.ts +7 -0
  102. package/dist/commands/top.js +54 -0
  103. package/dist/commands/touch.js +6 -2
  104. package/dist/commands/tr.d.ts +5 -0
  105. package/dist/commands/tr.js +5 -0
  106. package/dist/commands/w.js +1 -1
  107. package/dist/commands/which.d.ts +5 -0
  108. package/dist/commands/which.js +5 -0
  109. package/dist/index.d.ts +10 -2
  110. package/dist/index.js +4 -0
  111. package/dist/modules/VirtualNetworkManager.d.ts +54 -0
  112. package/dist/modules/VirtualNetworkManager.js +144 -0
  113. package/dist/modules/linuxRootfs.d.ts +4 -3
  114. package/dist/modules/linuxRootfs.js +115 -74
  115. package/dist/modules/neofetch.d.ts +2 -0
  116. package/dist/modules/neofetch.js +3 -2
  117. package/dist/modules/pacmanGame.d.ts +2 -0
  118. package/dist/modules/pacmanGame.js +1 -0
  119. package/dist/modules/shellInteractive.d.ts +2 -0
  120. package/dist/modules/shellInteractive.js +2 -0
  121. package/dist/modules/shellRuntime.d.ts +7 -0
  122. package/dist/modules/shellRuntime.js +6 -0
  123. package/dist/modules/webTermRenderer.js +0 -7
  124. package/dist/types/commands.d.ts +1 -1
  125. package/dist/types/vfs.d.ts +8 -0
  126. package/dist/utils/argv.d.ts +22 -3
  127. package/dist/utils/argv.js +22 -3
  128. package/dist/utils/perfLogger.d.ts +10 -2
  129. package/dist/utils/perfLogger.js +7 -14
  130. package/dist/utils/shellSession.d.ts +35 -0
  131. package/dist/utils/shellSession.js +35 -0
  132. package/package.json +1 -1
@@ -29,6 +29,8 @@ export class VirtualUserManager extends EventEmitter {
29
29
  activeProcesses = new Map();
30
30
  nextTty = 0;
31
31
  nextPid = 1000;
32
+ nextUid = 1001;
33
+ nextGid = 1001;
32
34
  /**
33
35
  * Creates a user manager instance backed by a virtual filesystem.
34
36
  *
@@ -59,14 +61,6 @@ export class VirtualUserManager extends EventEmitter {
59
61
  changed = true;
60
62
  }
61
63
  this.sudoers.add("root");
62
- // Auto-create current system user for easier authentication
63
- // const currentUser = process.env.USER || process.env.USERNAME;
64
- // if (currentUser && currentUser !== "root" && !this.users.has(currentUser)) {
65
- // const userPassword = this.defaultRootPassword;
66
- // this.users.set(currentUser, this.createRecord(currentUser, userPassword));
67
- // this.sudoers.add(currentUser);
68
- // changed = true;
69
- // }
70
64
  const homePath = "/root";
71
65
  if (!this.vfs.exists(homePath)) {
72
66
  this.vfs.mkdir(homePath, 0o755);
@@ -403,11 +397,19 @@ export class VirtualUserManager extends EventEmitter {
403
397
  listUsers() {
404
398
  return Array.from(this.users.keys()).sort();
405
399
  }
400
+ /** Returns the numeric UID for a username, or 0 if unknown. */
401
+ getUid(username) {
402
+ return this.users.get(username)?.uid ?? 0;
403
+ }
404
+ /** Returns the primary GID for a username, or 0 if unknown. */
405
+ getGid(username) {
406
+ return this.users.get(username)?.gid ?? 0;
407
+ }
406
408
  /**
407
409
  * Registers a running command as a virtual process.
408
410
  * Returns the assigned PID so the caller can deregister on completion.
409
411
  */
410
- registerProcess(username, command, argv, tty) {
412
+ registerProcess(username, command, argv, tty, abortController) {
411
413
  const pid = this.nextPid++;
412
414
  this.activeProcesses.set(pid, {
413
415
  pid,
@@ -416,6 +418,8 @@ export class VirtualUserManager extends EventEmitter {
416
418
  argv,
417
419
  tty,
418
420
  startedAt: new Date().toISOString(),
421
+ status: "running",
422
+ abortController,
419
423
  });
420
424
  return pid;
421
425
  }
@@ -423,10 +427,27 @@ export class VirtualUserManager extends EventEmitter {
423
427
  unregisterProcess(pid) {
424
428
  this.activeProcesses.delete(pid);
425
429
  }
430
+ /** Marks a process as done (keeps it in the table briefly for jobs/ps). */
431
+ markProcessDone(pid) {
432
+ const proc = this.activeProcesses.get(pid);
433
+ if (proc)
434
+ proc.status = "done";
435
+ }
426
436
  /** Returns all currently running processes sorted by PID. */
427
437
  listProcesses() {
428
438
  return Array.from(this.activeProcesses.values()).sort((a, b) => a.pid - b.pid);
429
439
  }
440
+ /** Terminate a process by PID. Returns true if the process was found and signalled. */
441
+ killProcess(pid) {
442
+ const proc = this.activeProcesses.get(pid);
443
+ if (!proc)
444
+ return false;
445
+ if (proc.abortController) {
446
+ proc.abortController.abort();
447
+ }
448
+ proc.status = "stopped";
449
+ return true;
450
+ }
430
451
  loadFromVfs() {
431
452
  this.users.clear();
432
453
  if (!this.vfs.exists(this.usersPath)) {
@@ -442,11 +463,23 @@ export class VirtualUserManager extends EventEmitter {
442
463
  if (parts.length < 3) {
443
464
  continue;
444
465
  }
445
- const [username, salt, passwordHash] = parts;
446
- if (!username || !salt || !passwordHash) {
447
- continue;
466
+ // Format: username:uid:gid:salt:passwordHash (new) or username:salt:passwordHash (legacy)
467
+ if (parts.length >= 5) {
468
+ const [username, uidStr, gidStr, salt, passwordHash] = parts;
469
+ if (!username || !salt || !passwordHash)
470
+ continue;
471
+ const uid = parseInt(uidStr ?? "1001", 10);
472
+ const gid = parseInt(gidStr ?? "1001", 10);
473
+ this.users.set(username, { username, uid, gid, salt, passwordHash });
474
+ }
475
+ else {
476
+ const [username, salt, passwordHash] = parts;
477
+ if (!username || !salt || !passwordHash)
478
+ continue;
479
+ const uid = username === "root" ? 0 : this.nextUid++;
480
+ const gid = username === "root" ? 0 : this.nextGid++;
481
+ this.users.set(username, { username, uid, gid, salt, passwordHash });
448
482
  }
449
- this.users.set(username, { username, salt, passwordHash });
450
483
  }
451
484
  }
452
485
  loadSudoersFromVfs() {
@@ -487,7 +520,7 @@ export class VirtualUserManager extends EventEmitter {
487
520
  }
488
521
  const authContent = Array.from(this.users.values())
489
522
  .sort((left, right) => left.username.localeCompare(right.username))
490
- .map((record) => [record.username, record.salt, record.passwordHash].join(":"))
523
+ .map((record) => [record.username, record.uid, record.gid, record.salt, record.passwordHash].join(":"))
491
524
  .join("\n");
492
525
  const sudoersContent = Array.from(this.sudoers.values()).sort().join("\n");
493
526
  const quotasContent = Array.from(this.quotas.entries())
@@ -516,7 +549,9 @@ export class VirtualUserManager extends EventEmitter {
516
549
  this.vfs.writeFile(targetPath, content, { mode });
517
550
  return true;
518
551
  }
519
- createRecord(username, password) {
552
+ createRecord(username, password, uid, gid) {
553
+ const assignedUid = uid ?? (username === "root" ? 0 : this.nextUid++);
554
+ const assignedGid = gid ?? (username === "root" ? 0 : this.nextGid++);
520
555
  // Cache key is a hash of the inputs — never store plaintext password in memory
521
556
  const cacheKey = createHash("sha256").update(username).update(":").update(password).digest("hex");
522
557
  const cached = VirtualUserManager.recordCache.get(cacheKey);
@@ -526,6 +561,8 @@ export class VirtualUserManager extends EventEmitter {
526
561
  const salt = randomBytes(16).toString("hex");
527
562
  const record = {
528
563
  username,
564
+ uid: assignedUid,
565
+ gid: assignedGid,
529
566
  salt,
530
567
  // Hash uses the generated salt — verifyPassword must use record.salt
531
568
  passwordHash: this.hashPassword(password, salt),
@@ -1,2 +1,7 @@
1
1
  import type { ShellModule } from "../types/commands";
2
+ /**
3
+ * Arbitrary precision calculator language.
4
+ * @category system
5
+ * @params ["[expression]"]
6
+ */
2
7
  export declare const bcCommand: ShellModule;
@@ -1,4 +1,9 @@
1
1
  import { evalArith } from "../utils/expand";
2
+ /**
3
+ * Arbitrary precision calculator language.
4
+ * @category system
5
+ * @params ["[expression]"]
6
+ */
2
7
  export const bcCommand = {
3
8
  name: "bc",
4
9
  description: "Arbitrary precision calculator language",
@@ -1,5 +1,5 @@
1
1
  import { ifFlag } from "./command-helpers";
2
- import { assertPathAccess, resolveReadablePath } from "./helpers";
2
+ import { checkFilePermission, resolveReadablePath } from "./helpers";
3
3
  /**
4
4
  * Concatenate and print files to stdout.
5
5
  * @category files
@@ -23,7 +23,7 @@ export const catCommand = {
23
23
  const parts = [];
24
24
  for (const fileArg of fileArgs) {
25
25
  const target = resolveReadablePath(shell.vfs, cwd, fileArg);
26
- assertPathAccess(authUser, target, "cat");
26
+ checkFilePermission(shell.vfs, shell.users, authUser, target, 4);
27
27
  parts.push(shell.vfs.readFile(target));
28
28
  }
29
29
  const combined = parts.join("");
@@ -0,0 +1,7 @@
1
+ import type { ShellModule } from "../types/commands";
2
+ /**
3
+ * Change group ownership.
4
+ * @category files
5
+ * @params ["<group> <file>"]
6
+ */
7
+ export declare const chgrpCommand: ShellModule;
@@ -0,0 +1,42 @@
1
+ import { assertPathAccess, resolvePath } from "./helpers";
2
+ /**
3
+ * Change group ownership.
4
+ * @category files
5
+ * @params ["<group> <file>"]
6
+ */
7
+ export const chgrpCommand = {
8
+ name: "chgrp",
9
+ description: "Change group ownership",
10
+ category: "files",
11
+ params: ["<group> <file>"],
12
+ run: ({ authUser, shell, cwd, args }) => {
13
+ const [groupArg, fileArg] = args;
14
+ if (!groupArg || !fileArg) {
15
+ return { stderr: "chgrp: missing operand", exitCode: 1 };
16
+ }
17
+ if (authUser !== "root") {
18
+ return { stderr: "chgrp: changing group: Operation not permitted", exitCode: 1 };
19
+ }
20
+ const filePath = resolvePath(cwd, fileArg);
21
+ try {
22
+ assertPathAccess(authUser, filePath, "chgrp");
23
+ if (!shell.vfs.exists(filePath)) {
24
+ return {
25
+ stderr: `chgrp: ${fileArg}: No such file or directory`,
26
+ exitCode: 1,
27
+ };
28
+ }
29
+ const gid = parseInt(groupArg, 10);
30
+ if (Number.isNaN(gid)) {
31
+ return { stderr: `chgrp: invalid group: ${groupArg}`, exitCode: 1 };
32
+ }
33
+ const current = shell.vfs.getOwner(filePath);
34
+ shell.vfs.chown(filePath, current.uid, gid);
35
+ return { exitCode: 0 };
36
+ }
37
+ catch (err) {
38
+ const msg = err instanceof Error ? err.message : String(err);
39
+ return { stderr: `chgrp: ${msg}`, exitCode: 1 };
40
+ }
41
+ },
42
+ };
@@ -0,0 +1,7 @@
1
+ import type { ShellModule } from "../types/commands";
2
+ /**
3
+ * Change file owner and group.
4
+ * @category files
5
+ * @params ["<owner>[:<group>] <file>"]
6
+ */
7
+ export declare const chownCommand: ShellModule;
@@ -0,0 +1,79 @@
1
+ import { assertPathAccess, resolvePath } from "./helpers";
2
+ /**
3
+ * Change file owner and group.
4
+ * @category files
5
+ * @params ["<owner>[:<group>] <file>"]
6
+ */
7
+ export const chownCommand = {
8
+ name: "chown",
9
+ description: "Change file owner and group",
10
+ category: "files",
11
+ params: ["<owner>[:<group>] <file>"],
12
+ run: ({ authUser, shell, cwd, args }) => {
13
+ const [ownerArg, fileArg] = args;
14
+ if (!ownerArg || !fileArg) {
15
+ return { stderr: "chown: missing operand", exitCode: 1 };
16
+ }
17
+ if (authUser !== "root") {
18
+ return { stderr: "chown: changing ownership: Operation not permitted", exitCode: 1 };
19
+ }
20
+ const filePath = resolvePath(cwd, fileArg);
21
+ try {
22
+ assertPathAccess(authUser, filePath, "chown");
23
+ if (!shell.vfs.exists(filePath)) {
24
+ return {
25
+ stderr: `chown: ${fileArg}: No such file or directory`,
26
+ exitCode: 1,
27
+ };
28
+ }
29
+ let uid = null;
30
+ let gid = null;
31
+ const colonIdx = ownerArg.indexOf(":");
32
+ if (colonIdx === -1) {
33
+ // Just a user name
34
+ uid = resolveUser(shell, ownerArg);
35
+ if (uid === null) {
36
+ return { stderr: `chown: invalid user: ${ownerArg}`, exitCode: 1 };
37
+ }
38
+ }
39
+ else {
40
+ const userPart = ownerArg.slice(0, colonIdx);
41
+ const groupPart = ownerArg.slice(colonIdx + 1);
42
+ if (userPart) {
43
+ uid = resolveUser(shell, userPart);
44
+ if (uid === null) {
45
+ return { stderr: `chown: invalid user: ${userPart}`, exitCode: 1 };
46
+ }
47
+ }
48
+ if (groupPart) {
49
+ gid = resolveGroup(shell, groupPart);
50
+ if (gid === null) {
51
+ return { stderr: `chown: invalid group: ${groupPart}`, exitCode: 1 };
52
+ }
53
+ }
54
+ }
55
+ const current = shell.vfs.getOwner(filePath);
56
+ shell.vfs.chown(filePath, uid ?? current.uid, gid ?? current.gid);
57
+ return { exitCode: 0 };
58
+ }
59
+ catch (err) {
60
+ const msg = err instanceof Error ? err.message : String(err);
61
+ return { stderr: `chown: ${msg}`, exitCode: 1 };
62
+ }
63
+ },
64
+ };
65
+ function resolveUser(shell, name) {
66
+ const users = shell.users.listUsers();
67
+ if (users.includes(name))
68
+ return shell.users.getUid(name);
69
+ const num = parseInt(name, 10);
70
+ if (!Number.isNaN(num))
71
+ return num;
72
+ return null;
73
+ }
74
+ function resolveGroup(_shell, name) {
75
+ const num = parseInt(name, 10);
76
+ if (!Number.isNaN(num))
77
+ return num;
78
+ return 0; // fallback: root group
79
+ }
@@ -1,5 +1,6 @@
1
+ import * as path from "node:path";
1
2
  import { ifFlag } from "./command-helpers";
2
- import { assertPathAccess, resolvePath } from "./helpers";
3
+ import { checkFilePermission, resolvePath } from "./helpers";
3
4
  /**
4
5
  * Copy files or directories inside the virtual filesystem.
5
6
  * @category files
@@ -20,8 +21,8 @@ export const cpCommand = {
20
21
  const srcPath = resolvePath(cwd, srcArg);
21
22
  const destPath = resolvePath(cwd, destArg);
22
23
  try {
23
- assertPathAccess(authUser, srcPath, "cp");
24
- assertPathAccess(authUser, destPath, "cp");
24
+ checkFilePermission(shell.vfs, shell.users, authUser, srcPath, 4);
25
+ checkFilePermission(shell.vfs, shell.users, authUser, path.posix.dirname(destPath), 2);
25
26
  if (!shell.vfs.exists(srcPath)) {
26
27
  return {
27
28
  stderr: `cp: ${srcArg}: No such file or directory`,
@@ -0,0 +1,7 @@
1
+ import type { ShellModule } from "../types/commands";
2
+ /**
3
+ * Convert and copy a file
4
+ * @category files
5
+ * @params ["if=<file> of=<file> [bs=1024] [count=N]"]
6
+ */
7
+ export declare const ddCommand: ShellModule;
@@ -0,0 +1,60 @@
1
+ import { resolvePath } from "./helpers";
2
+ /**
3
+ * Convert and copy a file
4
+ * @category files
5
+ * @params ["if=<file> of=<file> [bs=1024] [count=N]"]
6
+ */
7
+ export const ddCommand = {
8
+ name: "dd",
9
+ description: "Convert and copy a file",
10
+ category: "files",
11
+ params: ["if=<file> of=<file> [bs=1024] [count=N]"],
12
+ run: ({ authUser, shell, cwd, args }) => {
13
+ const kv = {};
14
+ for (const arg of args) {
15
+ const eqIdx = arg.indexOf("=");
16
+ if (eqIdx !== -1) {
17
+ kv[arg.slice(0, eqIdx)] = arg.slice(eqIdx + 1);
18
+ }
19
+ }
20
+ const ifPath = kv.if ? resolvePath(cwd, kv.if) : undefined;
21
+ const ofPath = kv.of ? resolvePath(cwd, kv.of) : undefined;
22
+ if (!ifPath || !ofPath) {
23
+ return { stderr: "dd: missing 'if' or 'of' operand\n", exitCode: 1 };
24
+ }
25
+ if (!shell.vfs.exists(ifPath)) {
26
+ return { stderr: `dd: ${kv.if}: No such file or directory\n`, exitCode: 1 };
27
+ }
28
+ const bs = parseInt(kv.bs || "512", 10);
29
+ const content = shell.vfs.readFile(ifPath);
30
+ const skipBlocks = parseInt(kv.skip || "0", 10);
31
+ const seekBlocks = parseInt(kv.seek || "0", 10);
32
+ const countBlocks = kv.count !== undefined ? parseInt(kv.count, 10) : undefined;
33
+ const startByte = skipBlocks * bs;
34
+ const available = content.slice(startByte);
35
+ const totalBytes = countBlocks !== undefined
36
+ ? Math.min(available.length, countBlocks * bs)
37
+ : available.length;
38
+ const data = available.slice(0, totalBytes);
39
+ let output;
40
+ try {
41
+ output = shell.vfs.readFile(ofPath);
42
+ }
43
+ catch {
44
+ output = "";
45
+ }
46
+ const seekByte = seekBlocks * bs;
47
+ if (seekByte > 0) {
48
+ if (output.length < seekByte) {
49
+ output = output.padEnd(seekByte, "\0");
50
+ }
51
+ output = output.slice(0, seekByte) + data + output.slice(seekByte + data.length);
52
+ }
53
+ else {
54
+ output = data;
55
+ }
56
+ shell.writeFileAsUser(authUser, ofPath, output);
57
+ const recordsIn = Math.ceil(data.length / bs);
58
+ return { stdout: `${recordsIn}+0 records in\n${recordsIn}+0 records out\n`, exitCode: 0 };
59
+ },
60
+ };
@@ -14,8 +14,6 @@ export const declareCommand = {
14
14
  if (!env)
15
15
  return { exitCode: 0 };
16
16
  const integer = ifFlag(args, ["-i"]);
17
- const _readonly = ifFlag(args, ["-r"]);
18
- const _export_ = ifFlag(args, ["-x"]);
19
17
  const printAll = args.filter((a) => !a.startsWith("-")).length === 0;
20
18
  if (printAll) {
21
19
  const lines = Object.entries(env.vars).map(([k, v]) => `declare -- ${k}="${v}"`);
@@ -0,0 +1,7 @@
1
+ import type { ShellModule } from "../types/commands";
2
+ /**
3
+ * Evaluate expressions
4
+ * @category shell
5
+ * @params ["<expression>"]
6
+ */
7
+ export declare const exprCommand: ShellModule;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Evaluate expressions
3
+ * @category shell
4
+ * @params ["<expression>"]
5
+ */
6
+ export const exprCommand = {
7
+ name: "expr",
8
+ description: "Evaluate expressions",
9
+ category: "shell",
10
+ params: ["<expression>"],
11
+ run: ({ args }) => {
12
+ const colonIdx = args.indexOf(":");
13
+ if (colonIdx > 0 && colonIdx <= args.length - 2) {
14
+ const str = args[colonIdx - 1];
15
+ const pattern = args[colonIdx + 1];
16
+ try {
17
+ const re = new RegExp(pattern);
18
+ const match = str.match(re);
19
+ if (match && match.index !== undefined) {
20
+ return { stdout: `${match[0].length}\n`, exitCode: 0 };
21
+ }
22
+ return { stdout: "0\n", exitCode: 1 };
23
+ }
24
+ catch {
25
+ return { stderr: "expr: invalid regex\n", exitCode: 2 };
26
+ }
27
+ }
28
+ if (args.length >= 3) {
29
+ const left = parseInt(args[0], 10);
30
+ const op = args[1];
31
+ const right = parseInt(args[2], 10);
32
+ if (isNaN(left) || isNaN(right)) {
33
+ return { stderr: "expr: non-integer argument\n", exitCode: 1 };
34
+ }
35
+ let result;
36
+ switch (op) {
37
+ case "+":
38
+ result = left + right;
39
+ break;
40
+ case "-":
41
+ result = left - right;
42
+ break;
43
+ case "*":
44
+ result = left * right;
45
+ break;
46
+ case "/":
47
+ if (right === 0)
48
+ return { stderr: "expr: division by zero\n", exitCode: 2 };
49
+ result = Math.trunc(left / right);
50
+ break;
51
+ case "%":
52
+ if (right === 0)
53
+ return { stderr: "expr: division by zero\n", exitCode: 2 };
54
+ result = left % right;
55
+ break;
56
+ default:
57
+ return { stderr: "expr: syntax error\n", exitCode: 2 };
58
+ }
59
+ return { stdout: `${result}\n`, exitCode: 0 };
60
+ }
61
+ return { stderr: "expr: syntax error\n", exitCode: 2 };
62
+ },
63
+ };
@@ -17,6 +17,11 @@ export declare const fortuneCommand: ShellModule;
17
17
  * @params ["[message]"]
18
18
  */
19
19
  export declare const cowsayCommand: ShellModule;
20
+ /**
21
+ * Generate ASCII cow thinking.
22
+ * @category misc
23
+ * @params ["[message]"]
24
+ */
20
25
  export declare const cowthinkCommand: ShellModule;
21
26
  /**
22
27
  * Show falling characters in the terminal like the Matrix movie.
@@ -57,7 +57,6 @@ export const fortuneCommand = {
57
57
  function cowsay(message, dead = false) {
58
58
  const lines = message.split("\n");
59
59
  const maxLen = Math.max(...lines.map(l => l.length));
60
- const _border = "-".repeat(maxLen + 2);
61
60
  const body = lines.length === 1
62
61
  ? `< ${lines[0]} >`
63
62
  : lines.map((l, i) => {
@@ -95,6 +94,11 @@ export const cowsayCommand = {
95
94
  return { stdout: cowsay(msg), exitCode: 0 };
96
95
  },
97
96
  };
97
+ /**
98
+ * Generate ASCII cow thinking.
99
+ * @category misc
100
+ * @params ["[message]"]
101
+ */
98
102
  export const cowthinkCommand = {
99
103
  name: "cowthink",
100
104
  description: "Generate ASCII cow thinking",
@@ -1,2 +1,7 @@
1
1
  import type { ShellModule } from "../types/commands";
2
+ /**
3
+ * List all commands, or show usage for a specific command.
4
+ * @category shell
5
+ * @params ["[command]"]
6
+ */
2
7
  export declare function createHelpCommand(_getNames: () => string[]): ShellModule;
@@ -98,6 +98,11 @@ function renderDetail(mod) {
98
98
  return lines.join("\n");
99
99
  }
100
100
  // ─── export ───────────────────────────────────────────────────────────────────
101
+ /**
102
+ * List all commands, or show usage for a specific command.
103
+ * @category shell
104
+ * @params ["[command]"]
105
+ */
101
106
  export function createHelpCommand(_getNames) {
102
107
  return {
103
108
  name: "help",
@@ -1,8 +1,51 @@
1
1
  import type VirtualFileSystem from "../VirtualFileSystem";
2
2
  import type { VirtualPackageManager } from "../VirtualPackageManager";
3
3
  import type { VirtualShell } from "../VirtualShell";
4
+ /**
5
+ * Resolves a path string against the virtual file system.
6
+ * Supports `~` as shorthand for the home directory. If `inputPath` is
7
+ * absolute it is returned as-is; otherwise it is joined to `cwd`.
8
+ *
9
+ * @param cwd - The current working directory
10
+ * @param inputPath - The path string to resolve
11
+ * @param homeDir - The home directory to use for `~` expansion (defaults to `/root`)
12
+ * @returns The normalized absolute path
13
+ */
4
14
  export declare function resolvePath(cwd: string, inputPath: string, homeDir?: string): string;
5
15
  export declare function assertPathAccess(authUser: string, targetPath: string, operation: string): void;
16
+ /**
17
+ * Strips the filename from a URL path, extracting the last path segment.
18
+ * Removes query strings and fragments first. Returns `"index.html"` when
19
+ * no segment is found.
20
+ *
21
+ * @param url - The URL or path string to process
22
+ * @returns The extracted filename, or "index.html" as a fallback
23
+ */
6
24
  export declare function stripUrlFilename(url: string): string;
25
+ /**
26
+ * Resolves a path with readable error messages by attempting an exact match
27
+ * first, then falling back to case-insensitive matching, and finally to a
28
+ * Levenshtein-distance (≤1) fuzzy match against sibling entries.
29
+ *
30
+ * @param vfs - The virtual file system instance
31
+ * @param cwd - The current working directory
32
+ * @param inputPath - The path string to resolve
33
+ * @returns The best matching path (exact, case-insensitive, or fuzzy)
34
+ */
7
35
  export declare function resolveReadablePath(vfs: VirtualFileSystem, cwd: string, inputPath: string): string;
36
+ /**
37
+ * Returns the active package manager associated with the given shell.
38
+ *
39
+ * @param shell - The VirtualShell instance
40
+ * @returns The VirtualPackageManager, or undefined if none is configured
41
+ */
8
42
  export declare function getPackageManager(shell: VirtualShell): VirtualPackageManager | undefined;
43
+ /**
44
+ * POSIX-style permission check: does `authUser` have `want` access to `filePath`?
45
+ * `want` is a bitmask: 4=R_OK, 2=W_OK, 1=X_OK, 0=F_OK (check existence).
46
+ * Root bypasses all checks. Throws on EACCES.
47
+ */
48
+ export declare function checkFilePermission(vfs: VirtualFileSystem, users: {
49
+ getUid: (u: string) => number;
50
+ getGid: (u: string) => number;
51
+ }, authUser: string, filePath: string, want: number): void;