typescript-virtual-container 1.0.5 → 1.0.6
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.
- package/modules/neofetch.ts +349 -0
- package/package.json +1 -1
- package/src/SSHMimic/exec.ts +2 -0
- package/src/VirtualShell/commands/index.ts +8 -0
- package/src/VirtualShell/commands/neofetch.ts +37 -0
- package/src/VirtualShell/commands/sh.ts +11 -1
- package/src/VirtualShell/commands/sudo.ts +2 -0
- package/src/VirtualShell/index.ts +17 -0
- package/src/VirtualShell/shell.ts +18 -11
- package/src/types/commands.ts +3 -0
|
@@ -0,0 +1,349 @@
|
|
|
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
|
+
console.log("Resolving neofetch info with shellProps:", shellProps);
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
user: info.user,
|
|
259
|
+
host: info.host,
|
|
260
|
+
osName: shellProps?.os ?? info.osName ?? `${readOsPrettyName() ?? os.type()} ${os.arch()}`,
|
|
261
|
+
kernel: shellProps?.kernel ?? info.kernel ?? os.release(),
|
|
262
|
+
uptimeSeconds: info.uptimeSeconds ?? os.uptime(),
|
|
263
|
+
packages: info.packages ?? resolvePackagesLabel(),
|
|
264
|
+
shell: resolveShellLabel(info.shell),
|
|
265
|
+
shellProps: info.shellProps as ShellProperties ?? {
|
|
266
|
+
kernel: info.kernel ?? os.release(),
|
|
267
|
+
os: info.osName ?? `${readOsPrettyName() ?? os.type()} ${os.arch()}`,
|
|
268
|
+
arch: os.arch(),
|
|
269
|
+
},
|
|
270
|
+
resolution: info.resolution ?? "n/a (ssh)",
|
|
271
|
+
terminal: info.terminal ?? "unknown",
|
|
272
|
+
cpu: info.cpu ?? resolveCpuLabel(),
|
|
273
|
+
gpu: info.gpu ?? "n/a",
|
|
274
|
+
memoryUsedMiB: info.memoryUsedMiB ?? toMiB(usedMem),
|
|
275
|
+
memoryTotalMiB: info.memoryTotalMiB ?? toMiB(totalMem),
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function buildNeofetchOutput(info: NeofetchInfo): string {
|
|
280
|
+
const fields = resolveDefaults(info);
|
|
281
|
+
const uptime = formatUptime(fields.uptimeSeconds);
|
|
282
|
+
const colorBars = buildColorBars();
|
|
283
|
+
|
|
284
|
+
const distroLogo = [
|
|
285
|
+
" .. .:. ",
|
|
286
|
+
" .::.. .. .. ",
|
|
287
|
+
". .... ... .. ",
|
|
288
|
+
": .... .:. .. ",
|
|
289
|
+
": .:.:........:. .. ",
|
|
290
|
+
": .. ",
|
|
291
|
+
". : ",
|
|
292
|
+
". : ",
|
|
293
|
+
".. : ",
|
|
294
|
+
" :. .. ",
|
|
295
|
+
" .. .. ",
|
|
296
|
+
" :-. :: ",
|
|
297
|
+
" .:. :. ",
|
|
298
|
+
" ..: ... ",
|
|
299
|
+
" ..: :.. ",
|
|
300
|
+
" :... :....",
|
|
301
|
+
" .. ....",
|
|
302
|
+
" . .. ",
|
|
303
|
+
" .:. .: ",
|
|
304
|
+
" :. .. ",
|
|
305
|
+
" ::. .. ",
|
|
306
|
+
"..... ..:... ",
|
|
307
|
+
"...:. .. ",
|
|
308
|
+
".:...:. ::. .. ",
|
|
309
|
+
" ... ..:::::.. ..:::::::.. ",
|
|
310
|
+
]
|
|
311
|
+
|
|
312
|
+
const details = [
|
|
313
|
+
`${fields.user}@${fields.host}`,
|
|
314
|
+
"-------------------------",
|
|
315
|
+
`OS: ${fields.osName}`,
|
|
316
|
+
`Host: ${resolveHostLabel(fields.host)}`,
|
|
317
|
+
`Kernel: ${fields.kernel}`,
|
|
318
|
+
`Uptime: ${uptime}`,
|
|
319
|
+
`Packages: ${fields.packages}`,
|
|
320
|
+
`Shell: ${fields.shell}`,
|
|
321
|
+
// `Shell Props: ${fields.shellProps}`,
|
|
322
|
+
`Resolution: ${fields.resolution}`,
|
|
323
|
+
`Terminal: ${fields.terminal}`,
|
|
324
|
+
`CPU: ${fields.cpu}`,
|
|
325
|
+
`GPU: ${fields.gpu}`,
|
|
326
|
+
`Memory: ${fields.memoryUsedMiB}MiB / ${fields.memoryTotalMiB}MiB`,
|
|
327
|
+
"",
|
|
328
|
+
colorBars[0],
|
|
329
|
+
colorBars[1],
|
|
330
|
+
];
|
|
331
|
+
|
|
332
|
+
const width = Math.max(distroLogo.length, details.length);
|
|
333
|
+
const lines: string[] = [];
|
|
334
|
+
|
|
335
|
+
for (let i = 0; i < width; i += 1) {
|
|
336
|
+
const rawLeft = distroLogo[i] ?? "";
|
|
337
|
+
const right = details[i] ?? "";
|
|
338
|
+
if (right.length > 0) {
|
|
339
|
+
const left = colorizeLogoLine(rawLeft.padEnd(31, " "), i, distroLogo.length);
|
|
340
|
+
const coloredRight = colorizeDetailLine(right);
|
|
341
|
+
lines.push(`${left} ${coloredRight}`);
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
lines.push(colorizeLogoLine(rawLeft, i, distroLogo.length));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return lines.join("\n");
|
|
349
|
+
}
|
package/package.json
CHANGED
package/src/SSHMimic/exec.ts
CHANGED
|
@@ -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,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(
|
|
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(
|
|
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
|
|
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
|
-
"
|
|
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(
|
|
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);
|
package/src/types/commands.ts
CHANGED
|
@@ -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. */
|