typescript-virtual-container 1.0.5 → 1.0.7

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.
@@ -0,0 +1,354 @@
1
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+ import type { ShellProperties } from "../src/VirtualShell";
5
+
6
+ function formatUptime(seconds: number): string {
7
+ const totalMinutes = Math.max(1, Math.floor(seconds / 60));
8
+ const days = Math.floor(totalMinutes / (24 * 60));
9
+ const hours = Math.floor((totalMinutes % (24 * 60)) / 60);
10
+ const minutes = totalMinutes % 60;
11
+
12
+ const parts: string[] = [];
13
+ if (days > 0) {
14
+ parts.push(`${days} day${days > 1 ? "s" : ""}`);
15
+ }
16
+ if (hours > 0) {
17
+ parts.push(`${hours} hour${hours > 1 ? "s" : ""}`);
18
+ }
19
+ if (minutes > 0 || parts.length === 0) {
20
+ parts.push(`${minutes} min${minutes > 1 ? "s" : ""}`);
21
+ }
22
+
23
+ return parts.join(", ");
24
+ }
25
+
26
+ function colorBlock(code: number): string {
27
+ return `\u001b[${code}m \u001b[0m`;
28
+ }
29
+
30
+ function buildColorBars(): string[] {
31
+ const normal = [40, 41, 42, 43, 44, 45, 46, 47].map(colorBlock).join("");
32
+ const bright = [100, 101, 102, 103, 104, 105, 106, 107]
33
+ .map(colorBlock)
34
+ .join("");
35
+ return [normal, bright];
36
+ }
37
+
38
+ function colorizeLogoLine(line: string, index: number, total: number): string {
39
+ if (line.trim().length === 0) {
40
+ return line;
41
+ }
42
+
43
+ const start = { r: 255, g: 255, b: 255 };
44
+ const end = { r: 168, g: 85, b: 247 };
45
+ const ratio = total <= 1 ? 0 : index / (total - 1);
46
+
47
+ const r = Math.round(start.r + (end.r - start.r) * ratio);
48
+ const g = Math.round(start.g + (end.g - start.g) * ratio);
49
+ const b = Math.round(start.b + (end.b - start.b) * ratio);
50
+
51
+ return `\u001b[38;2;${r};${g};${b}m${line}\u001b[0m`;
52
+ }
53
+
54
+ function colorizeDetailLine(line: string): string {
55
+ if (line.trim().length === 0) {
56
+ return line;
57
+ }
58
+
59
+ const colonIndex = line.indexOf(':');
60
+
61
+ if (colonIndex === -1) {
62
+ // Pas de ':', chercher '@' pour identifier user@host
63
+ if (line.includes('@')) {
64
+ // C'est user@host, appliquer dégradé horizontal
65
+ return applyHorizontalGradient(line);
66
+ }
67
+ // Sinon c'est un separator ou autre, laisser tel quel
68
+ return line;
69
+ }
70
+
71
+ // Il y a un ':', c'est titre: valeur
72
+ const title = line.substring(0, colonIndex + 1);
73
+ const value = line.substring(colonIndex + 1);
74
+
75
+ // Appliquer le dégradé seulement au titre
76
+ const colorized = applyHorizontalGradient(title);
77
+ return colorized + value;
78
+ }
79
+
80
+ function applyHorizontalGradient(text: string): string {
81
+ // Nettoyer les codes ANSI existants
82
+ const ansiRegex = new RegExp(`${String.fromCharCode(27)}\\[[\\d;]*m`, 'g');
83
+ const cleaned = text.replace(ansiRegex, '');
84
+
85
+ if (cleaned.trim().length === 0) {
86
+ return text;
87
+ }
88
+
89
+ const start = { r: 255, g: 255, b: 255 };
90
+ const end = { r: 168, g: 85, b: 247 };
91
+ let result = '';
92
+
93
+ for (let i = 0; i < cleaned.length; i += 1) {
94
+ const ratio = cleaned.length <= 1 ? 0 : i / (cleaned.length - 1);
95
+
96
+ const r = Math.round(start.r + (end.r - start.r) * ratio);
97
+ const g = Math.round(start.g + (end.g - start.g) * ratio);
98
+ const b = Math.round(start.b + (end.b - start.b) * ratio);
99
+
100
+ result += `\u001b[38;2;${r};${g};${b}m${cleaned[i]}\u001b[0m`;
101
+ }
102
+
103
+ return result;
104
+ }
105
+
106
+ export interface NeofetchInfo {
107
+ user: string;
108
+ host: string;
109
+ osName?: string;
110
+ kernel?: string;
111
+ uptimeSeconds?: number;
112
+ packages?: string;
113
+ shell?: string;
114
+ shellProps?: ShellProperties;
115
+ resolution?: string;
116
+ terminal?: string;
117
+ cpu?: string;
118
+ gpu?: string;
119
+ memoryUsedMiB?: number;
120
+ memoryTotalMiB?: number;
121
+ }
122
+
123
+ function toMiB(bytes: number): number {
124
+ return Math.max(0, Math.round(bytes / (1024 * 1024)));
125
+ }
126
+
127
+ function readOsPrettyName(): string | undefined {
128
+ try {
129
+ const data = readFileSync("/etc/os-release", "utf8");
130
+ for (const line of data.split("\n")) {
131
+ if (!line.startsWith("PRETTY_NAME=")) {
132
+ continue;
133
+ }
134
+
135
+ const value = line.slice("PRETTY_NAME=".length).trim();
136
+ return value.replace(/^"|"$/g, "");
137
+ }
138
+ } catch {
139
+ return undefined;
140
+ }
141
+
142
+ return undefined;
143
+ }
144
+
145
+ function readFirstLine(filePath: string): string | undefined {
146
+ try {
147
+ const data = readFileSync(filePath, "utf8").split("\n")[0]?.trim();
148
+ if (!data || data.length === 0) {
149
+ return undefined;
150
+ }
151
+ return data;
152
+ } catch {
153
+ return undefined;
154
+ }
155
+ }
156
+
157
+ function resolveHostLabel(fallback: string): string {
158
+ const vendor = readFirstLine("/sys/devices/virtual/dmi/id/sys_vendor");
159
+ const product = readFirstLine("/sys/devices/virtual/dmi/id/product_name");
160
+
161
+ if (vendor && product) {
162
+ return `${vendor} ${product}`;
163
+ }
164
+ if (product) {
165
+ return product;
166
+ }
167
+
168
+ return fallback;
169
+ }
170
+
171
+ function countDpkgPackages(): number | undefined {
172
+ const candidates = ["/var/lib/dpkg/status", "/usr/local/var/lib/dpkg/status"];
173
+
174
+ for (const filePath of candidates) {
175
+ if (!existsSync(filePath)) {
176
+ continue;
177
+ }
178
+
179
+ try {
180
+ const data = readFileSync(filePath, "utf8");
181
+ const matches = data.match(/^Package:\s+/gm);
182
+ return matches?.length ?? 0;
183
+ } catch {
184
+ }
185
+ }
186
+
187
+ return undefined;
188
+ }
189
+
190
+ function countSnapPackages(): number | undefined {
191
+ const candidates = ["/snap", "/var/lib/snapd/snaps"];
192
+
193
+ for (const dirPath of candidates) {
194
+ if (!existsSync(dirPath)) {
195
+ continue;
196
+ }
197
+
198
+ try {
199
+ const entries = readdirSync(dirPath, { withFileTypes: true });
200
+ const count = entries.filter((entry) => entry.isDirectory()).length;
201
+ return count;
202
+ } catch {
203
+ }
204
+ }
205
+
206
+ return undefined;
207
+ }
208
+
209
+ function resolvePackagesLabel(): string {
210
+ const dpkgCount = countDpkgPackages();
211
+ const snapCount = countSnapPackages();
212
+
213
+ if (dpkgCount !== undefined && snapCount !== undefined) {
214
+ return `${dpkgCount} (dpkg), ${snapCount} (snap)`;
215
+ }
216
+ if (dpkgCount !== undefined) {
217
+ return `${dpkgCount} (dpkg)`;
218
+ }
219
+ if (snapCount !== undefined) {
220
+ return `${snapCount} (snap)`;
221
+ }
222
+
223
+ return "n/a";
224
+ }
225
+
226
+ function resolveCpuLabel(): string {
227
+ const cpus = os.cpus();
228
+ if (cpus.length === 0) {
229
+ return "unknown";
230
+ }
231
+
232
+ const first = cpus[0];
233
+ if (!first) {
234
+ return "unknown";
235
+ }
236
+
237
+ const ghz = (first.speed / 1000).toFixed(2);
238
+ return `${first.model} (${cpus.length}) @ ${ghz}GHz`;
239
+ }
240
+
241
+ function resolveShellLabel(shell?: string): string {
242
+ if (!shell || shell.trim().length === 0) {
243
+ return "unknown";
244
+ }
245
+
246
+ return path.posix.basename(shell.trim());
247
+ }
248
+
249
+ function resolveDefaults(info: NeofetchInfo): Required<NeofetchInfo> {
250
+ const totalMem = os.totalmem();
251
+ const freeMem = os.freemem();
252
+ const usedMem = Math.max(0, totalMem - freeMem);
253
+ const shellProps = info.shellProps;
254
+
255
+ const processUptime = process.uptime();
256
+ if (info.uptimeSeconds === undefined) {
257
+ info.uptimeSeconds = Math.round(processUptime);
258
+ }
259
+
260
+ console.log("Resolving neofetch info with shellProps:", shellProps);
261
+
262
+ return {
263
+ user: info.user,
264
+ host: info.host,
265
+ osName: shellProps?.os ?? info.osName ?? `${readOsPrettyName() ?? os.type()} ${os.arch()}`,
266
+ kernel: shellProps?.kernel ?? info.kernel ?? os.release(),
267
+ uptimeSeconds: info.uptimeSeconds ?? os.uptime(),
268
+ packages: info.packages ?? resolvePackagesLabel(),
269
+ shell: resolveShellLabel(info.shell),
270
+ shellProps: info.shellProps as ShellProperties ?? {
271
+ kernel: info.kernel ?? os.release(),
272
+ os: info.osName ?? `${readOsPrettyName() ?? os.type()} ${os.arch()}`,
273
+ arch: os.arch(),
274
+ },
275
+ resolution: info.resolution ?? "n/a (ssh)",
276
+ terminal: info.terminal ?? "unknown",
277
+ cpu: info.cpu ?? resolveCpuLabel(),
278
+ gpu: info.gpu ?? "n/a",
279
+ memoryUsedMiB: info.memoryUsedMiB ?? toMiB(usedMem),
280
+ memoryTotalMiB: info.memoryTotalMiB ?? toMiB(totalMem),
281
+ };
282
+ }
283
+
284
+ export function buildNeofetchOutput(info: NeofetchInfo): string {
285
+ const fields = resolveDefaults(info);
286
+ const uptime = formatUptime(fields.uptimeSeconds);
287
+ const colorBars = buildColorBars();
288
+
289
+ const distroLogo = [
290
+ " .. .:. ",
291
+ " .::.. .. .. ",
292
+ ". .... ... .. ",
293
+ ": .... .:. .. ",
294
+ ": .:.:........:. .. ",
295
+ ": .. ",
296
+ ". : ",
297
+ ". : ",
298
+ ".. : ",
299
+ " :. .. ",
300
+ " .. .. ",
301
+ " :-. :: ",
302
+ " .:. :. ",
303
+ " ..: ... ",
304
+ " ..: :.. ",
305
+ " :... :....",
306
+ " .. ....",
307
+ " . .. ",
308
+ " .:. .: ",
309
+ " :. .. ",
310
+ " ::. .. ",
311
+ "..... ..:... ",
312
+ "...:. .. ",
313
+ ".:...:. ::. .. ",
314
+ " ... ..:::::.. ..:::::::.. ",
315
+ ]
316
+
317
+ const details = [
318
+ `${fields.user}@${fields.host}`,
319
+ "-------------------------",
320
+ `OS: ${fields.osName}`,
321
+ `Host: ${resolveHostLabel(fields.host)}`,
322
+ `Kernel: ${fields.kernel}`,
323
+ `Uptime: ${uptime}`,
324
+ // `Packages: ${fields.packages}`,
325
+ `Shell: ${fields.shell}`,
326
+ // `Shell Props: ${fields.shellProps}`,
327
+ `Resolution: ${fields.resolution}`,
328
+ `Terminal: ${fields.terminal}`,
329
+ `CPU: ${fields.cpu}`,
330
+ `GPU: ${fields.gpu}`,
331
+ `Memory: ${fields.memoryUsedMiB}MiB / ${fields.memoryTotalMiB}MiB`,
332
+ "",
333
+ colorBars[0],
334
+ colorBars[1],
335
+ ];
336
+
337
+ const width = Math.max(distroLogo.length, details.length);
338
+ const lines: string[] = [];
339
+
340
+ for (let i = 0; i < width; i += 1) {
341
+ const rawLeft = distroLogo[i] ?? "";
342
+ const right = details[i] ?? "";
343
+ if (right.length > 0) {
344
+ const left = colorizeLogoLine(rawLeft.padEnd(31, " "), i, distroLogo.length);
345
+ const coloredRight = colorizeDetailLine(right);
346
+ lines.push(`${left} ${coloredRight}`);
347
+ continue;
348
+ }
349
+
350
+ lines.push(colorizeLogoLine(rawLeft, i, distroLogo.length));
351
+ }
352
+
353
+ return lines.join("\n");
354
+ }
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "In-memory SSH server with virtual filesystem and typed programmatic API",
4
4
  "module": "src/index.ts",
5
5
  "type": "module",
6
- "version": "1.0.5",
6
+ "version": "1.0.7",
7
7
  "license": "MIT",
8
8
  "keywords": [
9
9
  "ssh",
@@ -1,5 +1,6 @@
1
1
  import type { ExecStream } from "../types/streams";
2
2
  import type VirtualFileSystem from "../VirtualFileSystem";
3
+ import { defaultShellProperties } from "../VirtualShell";
3
4
  import { runCommand } from "../VirtualShell/commands";
4
5
  import type { VirtualUserManager } from "./users";
5
6
 
@@ -26,6 +27,7 @@ export function runExec(
26
27
  users,
27
28
  "exec",
28
29
  `/home/${authUser}`,
30
+ defaultShellProperties,
29
31
  vfs,
30
32
  ),
31
33
  ).then((result) => {
@@ -1,6 +1,7 @@
1
1
  import type { CommandMode, CommandResult } from "../types/commands";
2
2
  import type { Pipeline, PipelineCommand } from "../types/pipeline";
3
3
  import type VirtualFileSystem from "../VirtualFileSystem";
4
+ import { defaultShellProperties } from "../VirtualShell";
4
5
  import { runCommand as runSingleCommand } from "../VirtualShell/commands";
5
6
  import { resolvePath } from "../VirtualShell/commands/helpers";
6
7
  import type { VirtualUserManager } from "./users";
@@ -84,6 +85,7 @@ async function executeSingleCommandWithRedirections(
84
85
  users,
85
86
  mode,
86
87
  cwd,
88
+ defaultShellProperties,
87
89
  vfs,
88
90
  stdin,
89
91
  );
@@ -159,6 +161,7 @@ async function executePipelineChain(
159
161
  users,
160
162
  mode,
161
163
  cwd,
164
+ defaultShellProperties,
162
165
  vfs,
163
166
  currentOutput,
164
167
  );
@@ -1,3 +1,4 @@
1
+ import type { ShellProperties } from "..";
1
2
  import type { VirtualUserManager } from "../../SSHMimic/users";
2
3
  import type {
3
4
  CommandContext,
@@ -24,6 +25,7 @@ import { htopCommand } from "./htop";
24
25
  import { lsCommand } from "./ls";
25
26
  import { mkdirCommand } from "./mkdir";
26
27
  import { nanoCommand } from "./nano";
28
+ import { neofetchCommand } from "./neofetch";
27
29
  import { pwdCommand } from "./pwd";
28
30
  import { rmCommand } from "./rm";
29
31
  import { setCommand } from "./set";
@@ -51,6 +53,7 @@ const BASE_COMMANDS: ShellModule[] = [
51
53
  rmCommand,
52
54
  treeCommand,
53
55
  nanoCommand,
56
+ neofetchCommand,
54
57
  htopCommand,
55
58
  adduserCommand,
56
59
  deluserCommand,
@@ -196,6 +199,7 @@ async function runCommandInternal(
196
199
  users: VirtualUserManager,
197
200
  mode: CommandMode,
198
201
  cwd: string,
202
+ shellProps: ShellProperties,
199
203
  vfs: VirtualFileSystem,
200
204
  stdin?: string,
201
205
  ): Promise<CommandResult> {
@@ -254,6 +258,7 @@ async function runCommandInternal(
254
258
  rawInput,
255
259
  mode,
256
260
  args,
261
+ shellProps,
257
262
  stdin,
258
263
  cwd,
259
264
  vfs,
@@ -273,6 +278,7 @@ export function runCommand(
273
278
  users: VirtualUserManager,
274
279
  mode: CommandMode,
275
280
  cwd: string,
281
+ shellProps: ShellProperties,
276
282
  vfs: VirtualFileSystem,
277
283
  stdin?: string,
278
284
  ): CommandOutcome {
@@ -291,6 +297,7 @@ export function runCommand(
291
297
  users,
292
298
  mode,
293
299
  cwd,
300
+ shellProps,
294
301
  vfs,
295
302
  stdin,
296
303
  );
@@ -319,6 +326,7 @@ export function runCommand(
319
326
  stdin,
320
327
  cwd,
321
328
  vfs,
329
+ shellProps,
322
330
  });
323
331
  } catch (error: unknown) {
324
332
  const message = error instanceof Error ? error.message : "Command failed";
@@ -0,0 +1,37 @@
1
+ import { buildNeofetchOutput } from "../../../modules/neofetch";
2
+ import type { ShellModule } from "../../types/commands";
3
+ import { ifFlag } from "./command-helpers";
4
+ import { getAllEnvVars } from "./set";
5
+
6
+ export const neofetchCommand: ShellModule = {
7
+ name: "neofetch",
8
+ params: ["[--off]"],
9
+ run: ({ args, authUser, hostname, shellProps }) => {
10
+ const env = getAllEnvVars(authUser);
11
+
12
+ if (ifFlag(args, "--help")) {
13
+ return {
14
+ stdout: "Usage: neofetch [--off]",
15
+ exitCode: 0,
16
+ };
17
+ }
18
+
19
+ if (ifFlag(args, "--off")) {
20
+ return {
21
+ stdout: `${authUser}@${hostname}`,
22
+ exitCode: 0,
23
+ };
24
+ }
25
+
26
+ return {
27
+ stdout: buildNeofetchOutput({
28
+ user: authUser,
29
+ host: hostname,
30
+ shell: env.SHELL,
31
+ shellProps: shellProps,
32
+ terminal: env.TERM,
33
+ }),
34
+ exitCode: 0,
35
+ };
36
+ },
37
+ };
@@ -1,3 +1,4 @@
1
+ import { defaultShellProperties } from "..";
1
2
  import type { CommandContext, ShellModule } from "../../types/commands";
2
3
  import { getArg, getFlag } from "./command-helpers";
3
4
  import { runCommand } from "./index";
@@ -38,7 +39,16 @@ export const shCommand: ShellModule = {
38
39
 
39
40
  // Execute the command
40
41
  const result = await Promise.resolve(
41
- runCommand(command, authUser, hostname, users, mode, cwd, vfs),
42
+ runCommand(
43
+ command,
44
+ authUser,
45
+ hostname,
46
+ users,
47
+ mode,
48
+ cwd,
49
+ defaultShellProperties,
50
+ vfs,
51
+ ),
42
52
  );
43
53
 
44
54
  if (result.stdout) {
@@ -1,3 +1,4 @@
1
+ import { defaultShellProperties } from "..";
1
2
  import type { ShellModule } from "../../types/commands";
2
3
  import { getArg, getFlag, ifFlag } from "./command-helpers";
3
4
  import { runCommand } from "./index";
@@ -62,6 +63,7 @@ export const sudoCommand: ShellModule = {
62
63
  users,
63
64
  mode,
64
65
  loginShell ? `/home/${effectiveUser}` : cwd,
66
+ defaultShellProperties,
65
67
  vfs,
66
68
  );
67
69
  }
@@ -5,19 +5,34 @@ import type VirtualFileSystem from "../VirtualFileSystem";
5
5
  import { createCustomCommand, registerCommand, runCommand } from "./commands";
6
6
  import { startShell } from "./shell";
7
7
 
8
+ export interface ShellProperties {
9
+ kernel: string;
10
+ os: "Fortune GNU/Linux x64";
11
+ arch: "x86_64";
12
+ }
13
+
14
+ export const defaultShellProperties: ShellProperties = {
15
+ kernel: "1.0.0+itsrealfortune+1-amd64",
16
+ os: "Fortune GNU/Linux x64",
17
+ arch: "x86_64",
18
+ };
19
+
8
20
  class VirtualShell {
9
21
  private vfs: VirtualFileSystem;
10
22
  private users: VirtualUserManager;
11
23
  private hostname: string;
24
+ public properties: ShellProperties;
12
25
 
13
26
  constructor(
14
27
  vfs: VirtualFileSystem,
15
28
  users: VirtualUserManager,
16
29
  hostname: string,
30
+ properties?: ShellProperties,
17
31
  ) {
18
32
  this.vfs = vfs;
19
33
  this.users = users;
20
34
  this.hostname = hostname;
35
+ this.properties = properties || defaultShellProperties;
21
36
  }
22
37
 
23
38
  addCommand(
@@ -41,6 +56,7 @@ class VirtualShell {
41
56
  this.users,
42
57
  "shell",
43
58
  cwd,
59
+ this.properties,
44
60
  this.vfs,
45
61
  );
46
62
  }
@@ -54,6 +70,7 @@ class VirtualShell {
54
70
  ): void {
55
71
  // Interactive shell logic
56
72
  startShell(
73
+ this.properties,
57
74
  stream,
58
75
  authUser,
59
76
  this.vfs!,
@@ -1,6 +1,7 @@
1
1
  import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process";
2
2
  import { readFile, unlink, writeFile } from "node:fs/promises";
3
3
  import * as path from "node:path";
4
+ import { defaultShellProperties, type ShellProperties } from ".";
4
5
  import { formatLoginDate } from "../SSHMimic/loginFormat";
5
6
  import { buildPrompt } from "../SSHMimic/prompt";
6
7
  import type { VirtualUserManager } from "../SSHMimic/users";
@@ -41,6 +42,7 @@ function toTtyLines(text: string): string {
41
42
  }
42
43
 
43
44
  export function startShell(
45
+ properties: ShellProperties,
44
46
  stream: ShellStream,
45
47
  authUser: string,
46
48
  vfs: VirtualFileSystem,
@@ -180,6 +182,7 @@ export function startShell(
180
182
  users,
181
183
  "shell",
182
184
  runCwd,
185
+ defaultShellProperties,
183
186
  vfs,
184
187
  ),
185
188
  );
@@ -467,20 +470,15 @@ export function startShell(
467
470
  }
468
471
 
469
472
  function renderLoginBanner(): void {
470
- // const kernel = os.release();
471
- // const arch = os.arch();
472
-
473
- // Our own kernel and arch strings to avoid leaking host info and to provide a more "Linux-like" feel
474
- const kernel = "5.15.0-1051-azure";
475
- const arch = "x86_64";
476
-
477
473
  const last = readLastLogin();
478
474
  const nowIso = new Date().toISOString();
479
475
 
480
- stream.write(`Linux ${hostname} ${kernel} ${arch}\r\n`);
476
+ stream.write(
477
+ `Linux ${hostname} ${properties.kernel} ${properties.arch}\r\n`,
478
+ );
481
479
  stream.write("\r\n");
482
480
  stream.write(
483
- "The programs included with the Debian GNU/Linux system are free software;\r\n",
481
+ "The programs included with the Fortune GNU/Linux system are free software;\r\n",
484
482
  );
485
483
  stream.write(
486
484
  "the exact distribution terms for each program are described in the\r\n",
@@ -488,7 +486,7 @@ export function startShell(
488
486
  stream.write("individual files in /usr/share/doc/*/copyright.\r\n");
489
487
  stream.write("\r\n");
490
488
  stream.write(
491
- "Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent\r\n",
489
+ "Fortune GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent\r\n",
492
490
  );
493
491
  stream.write("permitted by applicable law.\r\n");
494
492
 
@@ -649,7 +647,16 @@ export function startShell(
649
647
 
650
648
  if (line.length > 0) {
651
649
  const result = await Promise.resolve(
652
- runCommand(line, authUser, hostname, users, "shell", cwd, vfs),
650
+ runCommand(
651
+ line,
652
+ authUser,
653
+ hostname,
654
+ users,
655
+ "shell",
656
+ cwd,
657
+ defaultShellProperties,
658
+ vfs,
659
+ ),
653
660
  );
654
661
 
655
662
  pushHistory(line);
@@ -6,6 +6,7 @@ import type {
6
6
  VirtualUserManager,
7
7
  } from "../SSHMimic/users";
8
8
  import type VirtualFileSystem from "../VirtualFileSystem";
9
+ import type { ShellProperties } from "../VirtualShell";
9
10
 
10
11
  /**
11
12
  * Normalized command execution output.
@@ -76,6 +77,8 @@ export interface CommandContext {
76
77
  mode: CommandMode;
77
78
  /** Tokenized arguments excluding command name. */
78
79
  args: string[];
80
+ /** Virtual shell instance. */
81
+ shellProps: ShellProperties;
79
82
  /** Optional stdin payload (used by pipes/redirections). */
80
83
  stdin?: string;
81
84
  /** Current working directory for command execution. */