typescript-virtual-container 1.2.7 → 1.2.9

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 (130) hide show
  1. package/README.md +457 -42
  2. package/dist/SSHMimic/executor.js +3 -5
  3. package/dist/VirtualFileSystem/binaryPack.d.ts +49 -0
  4. package/dist/VirtualFileSystem/binaryPack.d.ts.map +1 -0
  5. package/dist/VirtualFileSystem/binaryPack.js +193 -0
  6. package/dist/VirtualFileSystem/index.d.ts +7 -5
  7. package/dist/VirtualFileSystem/index.d.ts.map +1 -1
  8. package/dist/VirtualFileSystem/index.js +20 -9
  9. package/dist/VirtualPackageManager/index.d.ts +202 -0
  10. package/dist/VirtualPackageManager/index.d.ts.map +1 -0
  11. package/dist/VirtualPackageManager/index.js +676 -0
  12. package/dist/VirtualShell/index.d.ts +87 -12
  13. package/dist/VirtualShell/index.d.ts.map +1 -1
  14. package/dist/VirtualShell/index.js +83 -12
  15. package/dist/VirtualUserManager/index.d.ts +52 -20
  16. package/dist/VirtualUserManager/index.d.ts.map +1 -1
  17. package/dist/VirtualUserManager/index.js +54 -20
  18. package/dist/commands/alias.d.ts +4 -0
  19. package/dist/commands/alias.d.ts.map +1 -0
  20. package/dist/commands/alias.js +58 -0
  21. package/dist/commands/apt.d.ts +4 -0
  22. package/dist/commands/apt.d.ts.map +1 -0
  23. package/dist/commands/apt.js +182 -0
  24. package/dist/commands/cat.d.ts.map +1 -1
  25. package/dist/commands/cat.js +27 -8
  26. package/dist/commands/chmod.d.ts.map +1 -1
  27. package/dist/commands/chmod.js +52 -3
  28. package/dist/commands/command-helpers.d.ts +78 -4
  29. package/dist/commands/command-helpers.d.ts.map +1 -1
  30. package/dist/commands/command-helpers.js +78 -4
  31. package/dist/commands/curl.d.ts.map +1 -1
  32. package/dist/commands/curl.js +81 -29
  33. package/dist/commands/dpkg.d.ts +4 -0
  34. package/dist/commands/dpkg.d.ts.map +1 -0
  35. package/dist/commands/dpkg.js +144 -0
  36. package/dist/commands/echo.d.ts.map +1 -1
  37. package/dist/commands/echo.js +24 -12
  38. package/dist/commands/free.d.ts +3 -0
  39. package/dist/commands/free.d.ts.map +1 -0
  40. package/dist/commands/free.js +38 -0
  41. package/dist/commands/helpers.d.ts +3 -0
  42. package/dist/commands/helpers.d.ts.map +1 -1
  43. package/dist/commands/helpers.js +3 -0
  44. package/dist/commands/history.d.ts +3 -0
  45. package/dist/commands/history.d.ts.map +1 -0
  46. package/dist/commands/history.js +21 -0
  47. package/dist/commands/index.d.ts +8 -1
  48. package/dist/commands/index.d.ts.map +1 -1
  49. package/dist/commands/index.js +120 -11
  50. package/dist/commands/ls.d.ts.map +1 -1
  51. package/dist/commands/ls.js +4 -3
  52. package/dist/commands/lsb-release.d.ts +3 -0
  53. package/dist/commands/lsb-release.d.ts.map +1 -0
  54. package/dist/commands/lsb-release.js +50 -0
  55. package/dist/commands/man.d.ts +3 -0
  56. package/dist/commands/man.d.ts.map +1 -0
  57. package/dist/commands/man.js +155 -0
  58. package/dist/commands/neofetch.d.ts.map +1 -1
  59. package/dist/commands/neofetch.js +5 -0
  60. package/dist/commands/ping.d.ts.map +1 -1
  61. package/dist/commands/ping.js +5 -2
  62. package/dist/commands/ps.d.ts.map +1 -1
  63. package/dist/commands/ps.js +27 -6
  64. package/dist/commands/sh.d.ts.map +1 -1
  65. package/dist/commands/sh.js +29 -11
  66. package/dist/commands/source.d.ts +3 -0
  67. package/dist/commands/source.d.ts.map +1 -0
  68. package/dist/commands/source.js +31 -0
  69. package/dist/commands/test.d.ts +3 -0
  70. package/dist/commands/test.d.ts.map +1 -0
  71. package/dist/commands/test.js +92 -0
  72. package/dist/commands/type.d.ts +3 -0
  73. package/dist/commands/type.d.ts.map +1 -0
  74. package/dist/commands/type.js +34 -0
  75. package/dist/commands/uptime.d.ts +3 -0
  76. package/dist/commands/uptime.d.ts.map +1 -0
  77. package/dist/commands/uptime.js +40 -0
  78. package/dist/commands/wget.d.ts.map +1 -1
  79. package/dist/commands/wget.js +71 -100
  80. package/dist/commands/which.d.ts +3 -0
  81. package/dist/commands/which.d.ts.map +1 -0
  82. package/dist/commands/which.js +32 -0
  83. package/dist/index.d.ts +5 -2
  84. package/dist/index.d.ts.map +1 -1
  85. package/dist/index.js +2 -1
  86. package/dist/modules/linuxRootfs.d.ts +24 -0
  87. package/dist/modules/linuxRootfs.d.ts.map +1 -0
  88. package/dist/modules/linuxRootfs.js +297 -0
  89. package/dist/modules/neofetch.d.ts.map +1 -1
  90. package/dist/modules/neofetch.js +1 -0
  91. package/dist/standalone.js +4 -1
  92. package/package.json +2 -1
  93. package/src/SSHMimic/executor.ts +3 -5
  94. package/src/VirtualFileSystem/binaryPack.ts +219 -0
  95. package/src/VirtualFileSystem/index.ts +21 -11
  96. package/src/VirtualPackageManager/index.ts +820 -0
  97. package/src/VirtualShell/index.ts +104 -13
  98. package/src/VirtualUserManager/index.ts +55 -20
  99. package/src/commands/alias.ts +60 -0
  100. package/src/commands/apt.ts +198 -0
  101. package/src/commands/cat.ts +32 -8
  102. package/src/commands/chmod.ts +48 -3
  103. package/src/commands/command-helpers.ts +78 -4
  104. package/src/commands/curl.ts +78 -37
  105. package/src/commands/dpkg.ts +158 -0
  106. package/src/commands/echo.ts +30 -14
  107. package/src/commands/free.ts +40 -0
  108. package/src/commands/helpers.ts +8 -0
  109. package/src/commands/history.ts +29 -0
  110. package/src/commands/index.ts +116 -11
  111. package/src/commands/ls.ts +5 -4
  112. package/src/commands/lsb-release.ts +52 -0
  113. package/src/commands/man.ts +166 -0
  114. package/src/commands/neofetch.ts +5 -0
  115. package/src/commands/ping.ts +5 -2
  116. package/src/commands/ps.ts +28 -6
  117. package/src/commands/sh.ts +33 -11
  118. package/src/commands/source.ts +35 -0
  119. package/src/commands/test.ts +100 -0
  120. package/src/commands/type.ts +40 -0
  121. package/src/commands/uptime.ts +46 -0
  122. package/src/commands/wget.ts +70 -123
  123. package/src/commands/which.ts +34 -0
  124. package/src/index.ts +10 -0
  125. package/src/modules/linuxRootfs.ts +439 -0
  126. package/src/modules/neofetch.ts +1 -0
  127. package/src/standalone.ts +4 -1
  128. package/standalone.js +418 -103
  129. package/standalone.js.map +4 -4
  130. package/tests/new-features.test.ts +626 -0
@@ -6,11 +6,32 @@ import type { PerfLogger } from "../utils/perfLogger";
6
6
  import { createPerfLogger } from "../utils/perfLogger";
7
7
  import VirtualFileSystem, { type VfsOptions } from "../VirtualFileSystem";
8
8
  import { VirtualUserManager } from "../VirtualUserManager";
9
+ import { VirtualPackageManager } from "../VirtualPackageManager";
10
+ import { bootstrapLinuxRootfs, refreshProc, syncEtcPasswd } from "../modules/linuxRootfs";
9
11
  import { startShell } from "./shell";
10
12
 
13
+ /**
14
+ * Virtual machine identity strings surfaced by system-info commands
15
+ * (`uname`, `neofetch`, `lsb_release`, `/proc/version`, `/etc/os-release`).
16
+ *
17
+ * Pass this as the second argument to `new VirtualShell()` to customise the
18
+ * distro name, kernel version, and CPU architecture reported inside the shell.
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * const shell = new VirtualShell("my-vm", {
23
+ * kernel: "6.1.0+custom-amd64",
24
+ * os: "Acme GNU/Linux x64",
25
+ * arch: "x86_64",
26
+ * });
27
+ * ```
28
+ */
11
29
  export interface ShellProperties {
30
+ /** Kernel version string (e.g. `"1.0.0+itsrealfortune+1-amd64"`). */
12
31
  kernel: string;
32
+ /** Full OS description (e.g. `"Fortune GNU/Linux x64"`). */
13
33
  os: string;
34
+ /** CPU architecture label (e.g. `"x86_64"`, `"aarch64"`). */
14
35
  arch: string;
15
36
  }
16
37
 
@@ -32,16 +53,40 @@ function resolveAutoSudoForNewUsers(): boolean {
32
53
  }
33
54
 
34
55
  /**
35
- * Coordinates the virtual filesystem, user manager, and command runtime.
56
+ * Coordinates the virtual filesystem, user manager, package manager, and
57
+ * command runtime for a single isolated shell environment.
58
+ *
59
+ * Each instance owns its own VFS tree, user database, package registry, and
60
+ * session state — multiple instances are fully independent.
61
+ *
62
+ * Instances are consumed both by the SSH/SFTP server facades and directly via
63
+ * the programmatic `SshClient` API.
36
64
  *
37
- * Instances are used both by the SSH server facade and by the programmatic
38
- * client API.
65
+ * @example
66
+ * ```ts
67
+ * const shell = new VirtualShell("my-vm");
68
+ * await shell.ensureInitialized();
69
+ * const client = new SshClient(shell, "root");
70
+ * const result = await client.exec("uname -a");
71
+ * ```
72
+ *
73
+ * @fires VirtualShell#initialized Emitted once the VFS and users are ready.
74
+ * @fires VirtualShell#command Emitted after every command execution.
75
+ * @fires VirtualShell#session:start Emitted when an interactive session opens.
39
76
  */
40
77
  class VirtualShell extends EventEmitter {
78
+ /** Backing virtual filesystem — use for direct path operations. */
41
79
  vfs: VirtualFileSystem;
80
+ /** Virtual user database — use for auth, quotas, and session tracking. */
42
81
  users: VirtualUserManager;
82
+ /** APT/dpkg package manager backed by the built-in package registry. */
83
+ packageManager: VirtualPackageManager;
84
+ /** Hostname shown in the shell prompt and SSH ident string. */
43
85
  hostname: string;
86
+ /** Distro identity strings surfaced by `uname`, `neofetch`, etc. */
44
87
  properties: ShellProperties;
88
+ /** Unix ms timestamp of shell creation — used by `uptime` and `/proc/uptime`. */
89
+ startTime: number;
45
90
  private initialized: Promise<void>;
46
91
 
47
92
  /**
@@ -60,17 +105,27 @@ class VirtualShell extends EventEmitter {
60
105
  perf.mark("constructor");
61
106
  this.hostname = hostname;
62
107
  this.properties = properties || defaultShellProperties;
108
+ this.startTime = Date.now();
63
109
  this.vfs = new VirtualFileSystem(vfsOptions ?? {});
64
110
  this.users = new VirtualUserManager(this.vfs, resolveAutoSudoForNewUsers());
111
+ this.packageManager = new VirtualPackageManager(this.vfs, this.users);
65
112
 
66
113
  // Store references to avoid TypeScript "used before assigned" errors
67
114
  const vfs = this.vfs;
68
115
  const users = this.users;
116
+ const pm = this.packageManager;
117
+ const shellProps = this.properties;
118
+ const shellHostname = this.hostname;
119
+ const startTime = this.startTime;
69
120
 
70
121
  // Initialize both VFS mirror and users, ensuring all is ready before auth
71
122
  this.initialized = (async () => {
72
123
  await vfs.restoreMirror();
73
124
  await users.initialize();
125
+ // Bootstrap Linux rootfs (idempotent)
126
+ bootstrapLinuxRootfs(vfs, users, shellHostname, shellProps, startTime);
127
+ // Load installed packages from dpkg status
128
+ pm.load();
74
129
  this.emit("initialized");
75
130
  })();
76
131
  }
@@ -105,11 +160,16 @@ class VirtualShell extends EventEmitter {
105
160
  }
106
161
 
107
162
  /**
108
- * Executes a command line string in the context of this shell instance.
163
+ * Executes a raw command line string programmatically.
164
+ *
165
+ * Supports the full shell operator set (`&&`, `||`, `;`, `|`, `>`, `<`,
166
+ * `$(cmd)`) and alias expansion. The result is emitted via the
167
+ * `"command"` event but not returned — use `SshClient.exec()` for a
168
+ * result-returning wrapper.
109
169
  *
110
- * @param rawInput
111
- * @param authUser
112
- * @param cwd
170
+ * @param rawInput Unparsed command line (e.g. `"ls -la /tmp"`).
171
+ * @param authUser Username to run the command as.
172
+ * @param cwd Current working directory for path resolution.
113
173
  */
114
174
  executeCommand(rawInput: string, authUser: string, cwd: string): void {
115
175
  perf.mark("executeCommand");
@@ -118,14 +178,19 @@ class VirtualShell extends EventEmitter {
118
178
  }
119
179
 
120
180
  /**
121
- * Starts an interactive session with the shell.
181
+ * Attaches an interactive PTY session to this shell instance.
182
+ *
183
+ * Called internally by `SshMimic` when a client opens a shell channel.
184
+ * The session reads from `stream` (user keystrokes) and writes back ANSI
185
+ * output. History, `.bashrc` sourcing, and Ctrl+W/Ctrl+U line editing are
186
+ * handled automatically.
122
187
  *
123
- * @param stream The stream for the interactive session.
124
- * @param authUser The authenticated user for the session.
125
- * @param sessionId The ID of the session.
126
- * @param remoteAddress The address of the remote client.
188
+ * @param stream Bidirectional SSH channel stream.
189
+ * @param authUser Authenticated username bound to this session.
190
+ * @param sessionId Stable session UUID (used for `who` output), or `null`.
191
+ * @param remoteAddress IP or hostname of the connecting client.
192
+ * @param terminalSize Initial terminal dimensions in columns and rows.
127
193
  */
128
-
129
194
  startInteractiveSession(
130
195
  stream: ShellStream,
131
196
  authUser: string,
@@ -148,6 +213,32 @@ class VirtualShell extends EventEmitter {
148
213
  );
149
214
  }
150
215
 
216
+ /**
217
+ * Refreshes the `/proc` virtual filesystem with current system state.
218
+ *
219
+ * Updates `/proc/uptime`, `/proc/meminfo`, `/proc/cpuinfo`,
220
+ * `/proc/version`, and `/proc/loadavg` from live host data.
221
+ *
222
+ * Called automatically during `bootstrapLinuxRootfs`. Call again before
223
+ * reading `/proc` files for up-to-date values (e.g. before `neofetch`
224
+ * or `free` in long-running processes).
225
+ */
226
+ public refreshProcFs(): void {
227
+ refreshProc(this.vfs, this.properties, this.hostname, this.startTime);
228
+ }
229
+
230
+ /**
231
+ * Syncs `/etc/passwd`, `/etc/group`, and `/etc/shadow` from the current
232
+ * `VirtualUserManager` state.
233
+ *
234
+ * Called automatically during `bootstrapLinuxRootfs`. Call again after
235
+ * `users.addUser()`, `users.deleteUser()`, or `users.addSudoer()` to keep
236
+ * the classic Unix credential files in sync with the user manager.
237
+ */
238
+ public syncPasswd(): void {
239
+ syncEtcPasswd(this.vfs, this.users);
240
+ }
241
+
151
242
  /**
152
243
  * Returns virtual filesystem instance after server started.
153
244
  *
@@ -290,10 +290,11 @@ export class VirtualUserManager extends EventEmitter {
290
290
  }
291
291
 
292
292
  /**
293
- * Updates password for an existing user account.
293
+ * Updates the password for an existing user account.
294
294
  *
295
295
  * @param username Username to update.
296
- * @param password New plaintext password.
296
+ * @param password New plaintext password (must be non-empty).
297
+ * @throws When the user does not exist or the password is empty.
297
298
  */
298
299
  public async setPassword(username: string, password: string): Promise<void> {
299
300
  perf.mark("setPassword");
@@ -309,9 +310,10 @@ export class VirtualUserManager extends EventEmitter {
309
310
  }
310
311
 
311
312
  /**
312
- * Deletes existing non-root user account.
313
+ * Deletes an existing non-root user account and revokes sudo access.
313
314
  *
314
315
  * @param username Username to remove.
316
+ * @throws When `username` is `"root"` or the user does not exist.
315
317
  */
316
318
  public async deleteUser(username: string): Promise<void> {
317
319
  perf.mark("deleteUser");
@@ -343,9 +345,10 @@ export class VirtualUserManager extends EventEmitter {
343
345
  }
344
346
 
345
347
  /**
346
- * Grants sudo access to existing user.
348
+ * Grants sudo privileges to an existing user.
347
349
  *
348
350
  * @param username Username to promote.
351
+ * @throws When the user does not exist.
349
352
  */
350
353
  public async addSudoer(username: string): Promise<void> {
351
354
  perf.mark("addSudoer");
@@ -359,9 +362,10 @@ export class VirtualUserManager extends EventEmitter {
359
362
  }
360
363
 
361
364
  /**
362
- * Revokes sudo access from user.
365
+ * Revokes sudo privileges from a user. Root cannot be demoted.
363
366
  *
364
367
  * @param username Username to demote.
368
+ * @throws When `username` is `"root"`.
365
369
  */
366
370
  public async removeSudoer(username: string): Promise<void> {
367
371
  perf.mark("removeSudoer");
@@ -375,11 +379,14 @@ export class VirtualUserManager extends EventEmitter {
375
379
  }
376
380
 
377
381
  /**
378
- * Registers active session and allocates tty id.
382
+ * Registers a new active session and allocates a virtual TTY identifier.
379
383
  *
380
- * @param username Session username.
381
- * @param remoteAddress Session source address.
382
- * @returns Registered session descriptor.
384
+ * Called by the SSH server when a client is authenticated. The returned
385
+ * descriptor is visible in `who` output and `listActiveSessions()`.
386
+ *
387
+ * @param username Authenticated username bound to the session.
388
+ * @param remoteAddress IP address or hostname of the connecting client.
389
+ * @returns The newly created `VirtualActiveSession` descriptor.
383
390
  */
384
391
  public registerSession(
385
392
  username: string,
@@ -403,9 +410,11 @@ export class VirtualUserManager extends EventEmitter {
403
410
  }
404
411
 
405
412
  /**
406
- * Unregisters active session when connection closes.
413
+ * Removes an active session record when the connection closes.
414
+ *
415
+ * Safe to call with a `null` or `undefined` session ID — it will be a no-op.
407
416
  *
408
- * @param sessionId Session identifier; ignored when nullish.
417
+ * @param sessionId Session UUID returned by `registerSession()`, or nullish.
409
418
  */
410
419
  public unregisterSession(sessionId: string | null | undefined): void {
411
420
  perf.mark("unregisterSession");
@@ -425,11 +434,15 @@ export class VirtualUserManager extends EventEmitter {
425
434
  }
426
435
 
427
436
  /**
428
- * Updates username/address metadata for existing session.
437
+ * Updates the username and remote address metadata for an active session.
429
438
  *
430
- * @param sessionId Session identifier; ignored when nullish.
431
- * @param username New username value.
432
- * @param remoteAddress New remote address value.
439
+ * Called internally by `su` and `sudo` when the effective user changes
440
+ * within a session. Silently ignored when the session ID is nullish or
441
+ * unknown.
442
+ *
443
+ * @param sessionId Session UUID to update, or nullish for no-op.
444
+ * @param username New effective username.
445
+ * @param remoteAddress New remote address (usually unchanged).
433
446
  */
434
447
  public updateSession(
435
448
  sessionId: string | null | undefined,
@@ -454,9 +467,11 @@ export class VirtualUserManager extends EventEmitter {
454
467
  }
455
468
 
456
469
  /**
457
- * Lists active sessions sorted by start time.
470
+ * Returns a snapshot of all currently active sessions, sorted by start time.
471
+ *
472
+ * Used by `who`, `ps`, `uptime`, and the `HoneyPot` auditor.
458
473
  *
459
- * @returns Snapshot of active session descriptors.
474
+ * @returns Array of `VirtualActiveSession` descriptors.
460
475
  */
461
476
  public listActiveSessions(): VirtualActiveSession[] {
462
477
  perf.mark("listActiveSessions");
@@ -465,6 +480,15 @@ export class VirtualUserManager extends EventEmitter {
465
480
  );
466
481
  }
467
482
 
483
+ /**
484
+ * Returns a sorted list of all registered usernames.
485
+ *
486
+ * @returns Array of username strings sorted alphabetically.
487
+ */
488
+ public listUsers(): string[] {
489
+ return Array.from(this.users.keys()).sort();
490
+ }
491
+
468
492
  private loadFromVfs(): void {
469
493
  this.users.clear();
470
494
 
@@ -610,6 +634,14 @@ export class VirtualUserManager extends EventEmitter {
610
634
  return record;
611
635
  }
612
636
 
637
+ /**
638
+ * Returns `true` when the user has a non-empty password set.
639
+ *
640
+ * A user with no password (or whose password hash matches the empty-string
641
+ * hash) is allowed to authenticate without a credential check.
642
+ *
643
+ * @param username Target username.
644
+ */
613
645
  public hasPassword(username: string): boolean {
614
646
  perf.mark("hasPassword");
615
647
  if (this.getPasswordHash(username) === this.hashPassword("")) {
@@ -620,10 +652,13 @@ export class VirtualUserManager extends EventEmitter {
620
652
  }
621
653
 
622
654
  /**
623
- * Hashes plaintext password with per-user salt using scrypt.
655
+ * Hashes a plaintext password using scrypt (or SHA-256 in fast-hash mode).
656
+ *
657
+ * Set `SSH_MIMIC_FAST_PASSWORD_HASH=1` to switch to SHA-256 for test
658
+ * environments where scrypt latency is undesirable.
624
659
  *
625
- * @param password Plaintext password.
626
- * @returns Hex-encoded password hash.
660
+ * @param password Plaintext password string.
661
+ * @returns Hex-encoded hash string.
627
662
  */
628
663
  public hashPassword(password: string): string {
629
664
  if (VirtualUserManager.fastPasswordHash) {
@@ -0,0 +1,60 @@
1
+ import type { ShellModule } from "../types/commands";
2
+ import { ifFlag } from "./command-helpers";
3
+
4
+ export const aliasCommand: ShellModule = {
5
+ name: "alias",
6
+ description: "Define or display aliases",
7
+ category: "shell",
8
+ params: ["[name[=value] ...]"],
9
+ run: ({ args, env }) => {
10
+ if (!env) return { exitCode: 0 };
11
+
12
+ // Aliases stored in env.vars under prefix __alias_
13
+ if (args.length === 0) {
14
+ const aliases = Object.entries(env.vars)
15
+ .filter(([k]) => k.startsWith("__alias_"))
16
+ .map(([k, v]) => `alias ${k.slice("__alias_".length)}='${v}'`);
17
+ return { stdout: aliases.join("\n") || "", exitCode: 0 };
18
+ }
19
+
20
+ const lines: string[] = [];
21
+ for (const arg of args) {
22
+ const eq = arg.indexOf("=");
23
+ if (eq === -1) {
24
+ // Display single alias
25
+ const val = env.vars[`__alias_${arg}`];
26
+ if (val) lines.push(`alias ${arg}='${val}'`);
27
+ else return { stderr: `alias: ${arg}: not found`, exitCode: 1 };
28
+ } else {
29
+ // Set alias
30
+ const name = arg.slice(0, eq);
31
+ const val = arg.slice(eq + 1).replace(/^['"]|['"]$/g, "");
32
+ env.vars[`__alias_${name}`] = val;
33
+ }
34
+ }
35
+
36
+ return { stdout: lines.join("\n") || undefined, exitCode: 0 };
37
+ },
38
+ };
39
+
40
+ export const unaliasCommand: ShellModule = {
41
+ name: "unalias",
42
+ description: "Remove alias definitions",
43
+ category: "shell",
44
+ params: ["<name...> | -a"],
45
+ run: ({ args, env }) => {
46
+ if (!env) return { exitCode: 0 };
47
+
48
+ if (ifFlag(args, ["-a"])) {
49
+ for (const k of Object.keys(env.vars)) {
50
+ if (k.startsWith("__alias_")) delete env.vars[k];
51
+ }
52
+ return { exitCode: 0 };
53
+ }
54
+
55
+ for (const name of args) {
56
+ delete env.vars[`__alias_${name}`];
57
+ }
58
+ return { exitCode: 0 };
59
+ },
60
+ };
@@ -0,0 +1,198 @@
1
+ import type { ShellModule } from "../types/commands";
2
+ import { ifFlag, } from "./command-helpers";
3
+ import { getPackageManager } from "./helpers";
4
+
5
+ export const aptCommand: ShellModule = {
6
+ name: "apt",
7
+ aliases: ["apt-get"],
8
+ description: "Package manager",
9
+ category: "system",
10
+ params: ["<install|remove|update|upgrade|search|show|list> [pkg...]"],
11
+ run: ({ args, shell, authUser }) => {
12
+ const pm = getPackageManager(shell);
13
+ if (!pm) return { stderr: "apt: package manager not initialised", exitCode: 1 };
14
+
15
+ const sub = args[0]?.toLowerCase();
16
+ const rest = args.slice(1);
17
+
18
+ const quiet = ifFlag(rest, ["-q", "--quiet", "-qq"]);
19
+ const purge = ifFlag(rest, ["--purge"]);
20
+ const pkgs = rest.filter((a) => !a.startsWith("-"));
21
+
22
+ // Non-root check
23
+ const restricted = ["install", "remove", "purge", "upgrade", "update"];
24
+ if (restricted.includes(sub ?? "") && authUser !== "root") {
25
+ return {
26
+ stderr: "E: Could not open lock file /var/lib/dpkg/lock-frontend - open (13: Permission denied)\nE: Unable to acquire the dpkg frontend lock, are you root?",
27
+ exitCode: 100,
28
+ };
29
+ }
30
+
31
+ switch (sub) {
32
+ case "install": {
33
+ if (pkgs.length === 0)
34
+ return { stderr: "apt: no packages specified", exitCode: 1 };
35
+ const { output, exitCode } = pm.install(pkgs, { quiet });
36
+ return { stdout: output || undefined, exitCode };
37
+ }
38
+
39
+ case "remove":
40
+ case "purge": {
41
+ if (pkgs.length === 0)
42
+ return { stderr: "apt: no packages specified", exitCode: 1 };
43
+ const { output, exitCode } = pm.remove(pkgs, {
44
+ purge: sub === "purge" || purge,
45
+ quiet,
46
+ });
47
+ return { stdout: output || undefined, exitCode };
48
+ }
49
+
50
+ case "update": {
51
+ return {
52
+ stdout: [
53
+ "Hit:1 fortune://packages.fortune.local aurora InRelease",
54
+ "Hit:2 fortune://security.fortune.local aurora-security InRelease",
55
+ "Reading package lists... Done",
56
+ "Building dependency tree... Done",
57
+ "Reading state information... Done",
58
+ `All packages are up to date.`,
59
+ ].join("\n"),
60
+ exitCode: 0,
61
+ };
62
+ }
63
+
64
+ case "upgrade": {
65
+ return {
66
+ stdout: [
67
+ "Reading package lists... Done",
68
+ "Building dependency tree... Done",
69
+ "Reading state information... Done",
70
+ "Calculating upgrade... Done",
71
+ "0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.",
72
+ ].join("\n"),
73
+ exitCode: 0,
74
+ };
75
+ }
76
+
77
+ case "search": {
78
+ const term = pkgs[0];
79
+ if (!term)
80
+ return { stderr: "apt: search requires a term", exitCode: 1 };
81
+ const results = pm.search(term);
82
+ if (results.length === 0)
83
+ return {
84
+ stdout: `Sorting... Done\nFull Text Search... Done\n(no results)`,
85
+ exitCode: 0,
86
+ };
87
+ const lines = results.map(
88
+ (p) => `${p.name}/${p.section ?? "misc"} ${p.version} amd64\n ${p.shortDesc ?? p.description}`,
89
+ );
90
+ return {
91
+ stdout: `Sorting... Done\nFull Text Search... Done\n${lines.join("\n")}`,
92
+ exitCode: 0,
93
+ };
94
+ }
95
+
96
+ case "show": {
97
+ const name = pkgs[0];
98
+ if (!name) return { stderr: "apt: show requires a package name", exitCode: 1 };
99
+ const info = pm.show(name);
100
+ if (!info)
101
+ return { stderr: `N: Unable to locate package ${name}`, exitCode: 100 };
102
+ return { stdout: info, exitCode: 0 };
103
+ }
104
+
105
+ case "list": {
106
+ const installedFlag = ifFlag(rest, ["--installed"]);
107
+ if (installedFlag) {
108
+ const pkgList = pm.listInstalled();
109
+ if (pkgList.length === 0)
110
+ return { stdout: "Listing... Done\n(no packages installed)", exitCode: 0 };
111
+ const lines = pkgList.map(
112
+ (p) => `${p.name}/${p.section} ${p.version} ${p.architecture} [installed]`,
113
+ );
114
+ return { stdout: `Listing... Done\n${lines.join("\n")}`, exitCode: 0 };
115
+ }
116
+ // all available
117
+ const all = pm.listAvailable();
118
+ const lines = all.map(
119
+ (p) => `${p.name}/${p.section ?? "misc"} ${p.version} amd64`,
120
+ );
121
+ return { stdout: `Listing... Done\n${lines.join("\n")}`, exitCode: 0 };
122
+ }
123
+
124
+ default: {
125
+ return {
126
+ stdout: [
127
+ "Usage: apt [options] command",
128
+ "",
129
+ "Commands:",
130
+ " install <pkg...> Install packages",
131
+ " remove <pkg...> Remove packages",
132
+ " purge <pkg...> Remove packages and config files",
133
+ " update Refresh package index",
134
+ " upgrade Upgrade all packages",
135
+ " search <term> Search in package descriptions",
136
+ " show <pkg> Show package details",
137
+ " list [--installed] List packages",
138
+ ].join("\n"),
139
+ exitCode: 0,
140
+ };
141
+ }
142
+ }
143
+ },
144
+ };
145
+
146
+ export const aptCacheCommand: ShellModule = {
147
+ name: "apt-cache",
148
+ description: "Query the package cache",
149
+ category: "system",
150
+ params: ["<search|show|policy> [pkg]"],
151
+ run: ({ args, shell }) => {
152
+ const pm = getPackageManager(shell);
153
+ if (!pm) return { stderr: "apt-cache: package manager not initialised", exitCode: 1 };
154
+
155
+ const sub = args[0]?.toLowerCase();
156
+ const pkgName = args[1];
157
+
158
+ switch (sub) {
159
+ case "search": {
160
+ if (!pkgName) return { stderr: "Need a search term", exitCode: 1 };
161
+ const results = pm.search(pkgName);
162
+ return {
163
+ stdout: results
164
+ .map((p) => `${p.name} - ${p.shortDesc ?? p.description}`)
165
+ .join("\n") || "(no results)",
166
+ exitCode: 0,
167
+ };
168
+ }
169
+ case "show": {
170
+ if (!pkgName) return { stderr: "Need a package name", exitCode: 1 };
171
+ const info = pm.show(pkgName);
172
+ return info
173
+ ? { stdout: info, exitCode: 0 }
174
+ : { stderr: `N: Unable to locate package ${pkgName}`, exitCode: 100 };
175
+ }
176
+ case "policy": {
177
+ if (!pkgName) return { stderr: "Need a package name", exitCode: 1 };
178
+ const def = pm.findInRegistry(pkgName);
179
+ if (!def)
180
+ return { stderr: `N: Unable to locate package ${pkgName}`, exitCode: 100 };
181
+ const inst = pm.isInstalled(pkgName);
182
+ return {
183
+ stdout: [
184
+ `${pkgName}:`,
185
+ ` Installed: ${inst ? def.version : "(none)"}`,
186
+ ` Candidate: ${def.version}`,
187
+ ` Version table:`,
188
+ ` ${def.version} 500`,
189
+ ` 500 fortune://packages.fortune.local aurora/main amd64 Packages`,
190
+ ].join("\n"),
191
+ exitCode: 0,
192
+ };
193
+ }
194
+ default:
195
+ return { stderr: `apt-cache: unknown command '${sub ?? ""}'`, exitCode: 1 };
196
+ }
197
+ },
198
+ };
@@ -1,20 +1,44 @@
1
1
  import type { ShellModule } from "../types/commands";
2
- import { getArg } from "./command-helpers";
2
+ import { ifFlag } from "./command-helpers";
3
3
  import { assertPathAccess, resolveReadablePath } from "./helpers";
4
4
 
5
5
  export const catCommand: ShellModule = {
6
6
  name: "cat",
7
7
  description: "Concatenate and print files",
8
8
  category: "files",
9
- params: ["<file>"],
10
- run: ({ authUser, shell, cwd, args }) => {
11
- const fileArg = getArg(args, 0);
12
- if (!fileArg) {
9
+ params: ["[-n] [-b] <file...>"],
10
+ run: ({ authUser, shell, cwd, args, stdin }) => {
11
+ const numberAll = ifFlag(args, ["-n", "--number"]);
12
+ const numberNonBlank = ifFlag(args, ["-b", "--number-nonblank"]);
13
+ const fileArgs = args.filter((a) => !a.startsWith("-"));
14
+
15
+ if (fileArgs.length === 0 && stdin !== undefined) {
16
+ return { stdout: stdin, exitCode: 0 };
17
+ }
18
+
19
+ if (fileArgs.length === 0) {
13
20
  return { stderr: "cat: missing file operand", exitCode: 1 };
14
21
  }
15
22
 
16
- const target = resolveReadablePath(shell.vfs, cwd, fileArg);
17
- assertPathAccess(authUser, target, "cat");
18
- return { stdout: shell.vfs.readFile(target), exitCode: 0 };
23
+ const parts: string[] = [];
24
+ for (const fileArg of fileArgs) {
25
+ const target = resolveReadablePath(shell.vfs, cwd, fileArg);
26
+ assertPathAccess(authUser, target, "cat");
27
+ parts.push(shell.vfs.readFile(target));
28
+ }
29
+
30
+ const combined = parts.join("");
31
+
32
+ if (!numberAll && !numberNonBlank) {
33
+ return { stdout: combined, exitCode: 0 };
34
+ }
35
+
36
+ let lineNum = 1;
37
+ const numbered = combined.split("\n").map((line) => {
38
+ if (numberNonBlank && line.trim() === "") return line;
39
+ return `${String(lineNum++).padStart(6)}\t${line}`;
40
+ }).join("\n");
41
+
42
+ return { stdout: numbered, exitCode: 0 };
19
43
  },
20
44
  };