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.
- package/README.md +457 -42
- package/dist/SSHMimic/executor.js +3 -5
- package/dist/VirtualFileSystem/binaryPack.d.ts +49 -0
- package/dist/VirtualFileSystem/binaryPack.d.ts.map +1 -0
- package/dist/VirtualFileSystem/binaryPack.js +193 -0
- package/dist/VirtualFileSystem/index.d.ts +7 -5
- package/dist/VirtualFileSystem/index.d.ts.map +1 -1
- package/dist/VirtualFileSystem/index.js +20 -9
- package/dist/VirtualPackageManager/index.d.ts +202 -0
- package/dist/VirtualPackageManager/index.d.ts.map +1 -0
- package/dist/VirtualPackageManager/index.js +676 -0
- package/dist/VirtualShell/index.d.ts +87 -12
- package/dist/VirtualShell/index.d.ts.map +1 -1
- package/dist/VirtualShell/index.js +83 -12
- package/dist/VirtualUserManager/index.d.ts +52 -20
- package/dist/VirtualUserManager/index.d.ts.map +1 -1
- package/dist/VirtualUserManager/index.js +54 -20
- package/dist/commands/alias.d.ts +4 -0
- package/dist/commands/alias.d.ts.map +1 -0
- package/dist/commands/alias.js +58 -0
- package/dist/commands/apt.d.ts +4 -0
- package/dist/commands/apt.d.ts.map +1 -0
- package/dist/commands/apt.js +182 -0
- package/dist/commands/cat.d.ts.map +1 -1
- package/dist/commands/cat.js +27 -8
- package/dist/commands/chmod.d.ts.map +1 -1
- package/dist/commands/chmod.js +52 -3
- package/dist/commands/command-helpers.d.ts +78 -4
- package/dist/commands/command-helpers.d.ts.map +1 -1
- package/dist/commands/command-helpers.js +78 -4
- package/dist/commands/curl.d.ts.map +1 -1
- package/dist/commands/curl.js +81 -29
- package/dist/commands/dpkg.d.ts +4 -0
- package/dist/commands/dpkg.d.ts.map +1 -0
- package/dist/commands/dpkg.js +144 -0
- package/dist/commands/echo.d.ts.map +1 -1
- package/dist/commands/echo.js +24 -12
- package/dist/commands/free.d.ts +3 -0
- package/dist/commands/free.d.ts.map +1 -0
- package/dist/commands/free.js +38 -0
- package/dist/commands/helpers.d.ts +3 -0
- package/dist/commands/helpers.d.ts.map +1 -1
- package/dist/commands/helpers.js +3 -0
- package/dist/commands/history.d.ts +3 -0
- package/dist/commands/history.d.ts.map +1 -0
- package/dist/commands/history.js +21 -0
- package/dist/commands/index.d.ts +8 -1
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +120 -11
- package/dist/commands/ls.d.ts.map +1 -1
- package/dist/commands/ls.js +4 -3
- package/dist/commands/lsb-release.d.ts +3 -0
- package/dist/commands/lsb-release.d.ts.map +1 -0
- package/dist/commands/lsb-release.js +50 -0
- package/dist/commands/man.d.ts +3 -0
- package/dist/commands/man.d.ts.map +1 -0
- package/dist/commands/man.js +155 -0
- package/dist/commands/neofetch.d.ts.map +1 -1
- package/dist/commands/neofetch.js +5 -0
- package/dist/commands/ping.d.ts.map +1 -1
- package/dist/commands/ping.js +5 -2
- package/dist/commands/ps.d.ts.map +1 -1
- package/dist/commands/ps.js +27 -6
- package/dist/commands/sh.d.ts.map +1 -1
- package/dist/commands/sh.js +29 -11
- package/dist/commands/source.d.ts +3 -0
- package/dist/commands/source.d.ts.map +1 -0
- package/dist/commands/source.js +31 -0
- package/dist/commands/test.d.ts +3 -0
- package/dist/commands/test.d.ts.map +1 -0
- package/dist/commands/test.js +92 -0
- package/dist/commands/type.d.ts +3 -0
- package/dist/commands/type.d.ts.map +1 -0
- package/dist/commands/type.js +34 -0
- package/dist/commands/uptime.d.ts +3 -0
- package/dist/commands/uptime.d.ts.map +1 -0
- package/dist/commands/uptime.js +40 -0
- package/dist/commands/wget.d.ts.map +1 -1
- package/dist/commands/wget.js +71 -100
- package/dist/commands/which.d.ts +3 -0
- package/dist/commands/which.d.ts.map +1 -0
- package/dist/commands/which.js +32 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/modules/linuxRootfs.d.ts +24 -0
- package/dist/modules/linuxRootfs.d.ts.map +1 -0
- package/dist/modules/linuxRootfs.js +297 -0
- package/dist/modules/neofetch.d.ts.map +1 -1
- package/dist/modules/neofetch.js +1 -0
- package/dist/standalone.js +4 -1
- package/package.json +2 -1
- package/src/SSHMimic/executor.ts +3 -5
- package/src/VirtualFileSystem/binaryPack.ts +219 -0
- package/src/VirtualFileSystem/index.ts +21 -11
- package/src/VirtualPackageManager/index.ts +820 -0
- package/src/VirtualShell/index.ts +104 -13
- package/src/VirtualUserManager/index.ts +55 -20
- package/src/commands/alias.ts +60 -0
- package/src/commands/apt.ts +198 -0
- package/src/commands/cat.ts +32 -8
- package/src/commands/chmod.ts +48 -3
- package/src/commands/command-helpers.ts +78 -4
- package/src/commands/curl.ts +78 -37
- package/src/commands/dpkg.ts +158 -0
- package/src/commands/echo.ts +30 -14
- package/src/commands/free.ts +40 -0
- package/src/commands/helpers.ts +8 -0
- package/src/commands/history.ts +29 -0
- package/src/commands/index.ts +116 -11
- package/src/commands/ls.ts +5 -4
- package/src/commands/lsb-release.ts +52 -0
- package/src/commands/man.ts +166 -0
- package/src/commands/neofetch.ts +5 -0
- package/src/commands/ping.ts +5 -2
- package/src/commands/ps.ts +28 -6
- package/src/commands/sh.ts +33 -11
- package/src/commands/source.ts +35 -0
- package/src/commands/test.ts +100 -0
- package/src/commands/type.ts +40 -0
- package/src/commands/uptime.ts +46 -0
- package/src/commands/wget.ts +70 -123
- package/src/commands/which.ts +34 -0
- package/src/index.ts +10 -0
- package/src/modules/linuxRootfs.ts +439 -0
- package/src/modules/neofetch.ts +1 -0
- package/src/standalone.ts +4 -1
- package/standalone.js +418 -103
- package/standalone.js.map +4 -4
- package/tests/new-features.test.ts +626 -0
package/src/commands/index.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/** biome-ignore-all lint/style/useNamingConvention: ENV VARIABLES */
|
|
2
|
+
import { executeStatements } from "../SSHMimic/executor";
|
|
2
3
|
import type { VirtualShell } from "../VirtualShell";
|
|
4
|
+
import { parseScript } from "../VirtualShell/shellParser";
|
|
3
5
|
import type {
|
|
4
6
|
CommandContext,
|
|
5
7
|
CommandMode,
|
|
@@ -8,6 +10,11 @@ import type {
|
|
|
8
10
|
ShellModule,
|
|
9
11
|
} from "../types/commands";
|
|
10
12
|
import { adduserCommand } from "./adduser";
|
|
13
|
+
import { aliasCommand, unaliasCommand } from "./alias";
|
|
14
|
+
import { testCommand } from "./test";
|
|
15
|
+
import { sourceCommand } from "./source";
|
|
16
|
+
import { historyCommand } from "./history";
|
|
17
|
+
import { aptCacheCommand, aptCommand } from "./apt";
|
|
11
18
|
import { awkCommand } from "./awk";
|
|
12
19
|
import { base64Command } from "./base64";
|
|
13
20
|
import { catCommand } from "./cat";
|
|
@@ -21,12 +28,14 @@ import { dateCommand } from "./date";
|
|
|
21
28
|
import { deluserCommand } from "./deluser";
|
|
22
29
|
import { dfCommand } from "./df";
|
|
23
30
|
import { diffCommand } from "./diff";
|
|
31
|
+
import { dpkgCommand, dpkgQueryCommand } from "./dpkg";
|
|
24
32
|
import { duCommand } from "./du";
|
|
25
33
|
import { echoCommand } from "./echo";
|
|
26
34
|
import { envCommand } from "./env";
|
|
27
35
|
import { exitCommand } from "./exit";
|
|
28
36
|
import { exportCommand } from "./export";
|
|
29
37
|
import { findCommand } from "./find";
|
|
38
|
+
import { freeCommand } from "./free";
|
|
30
39
|
import { grepCommand } from "./grep";
|
|
31
40
|
import { groupsCommand } from "./groups";
|
|
32
41
|
import { gunzipCommand, gzipCommand } from "./gzip";
|
|
@@ -38,6 +47,8 @@ import { idCommand } from "./id";
|
|
|
38
47
|
import { killCommand } from "./kill";
|
|
39
48
|
import { lnCommand } from "./ln";
|
|
40
49
|
import { lsCommand } from "./ls";
|
|
50
|
+
import { lsbReleaseCommand } from "./lsb-release";
|
|
51
|
+
import { manCommand } from "./man";
|
|
41
52
|
import { mkdirCommand } from "./mkdir";
|
|
42
53
|
import { mvCommand } from "./mv";
|
|
43
54
|
import { nanoCommand } from "./nano";
|
|
@@ -60,11 +71,14 @@ import { teeCommand } from "./tee";
|
|
|
60
71
|
import { touchCommand } from "./touch";
|
|
61
72
|
import { trCommand } from "./tr";
|
|
62
73
|
import { treeCommand } from "./tree";
|
|
74
|
+
import { typeCommand } from "./type";
|
|
63
75
|
import { unameCommand } from "./uname";
|
|
64
76
|
import { uniqCommand } from "./uniq";
|
|
65
77
|
import { unsetCommand } from "./unset";
|
|
78
|
+
import { uptimeCommand } from "./uptime";
|
|
66
79
|
import { wcCommand } from "./wc";
|
|
67
80
|
import { wgetCommand } from "./wget";
|
|
81
|
+
import { whichCommand } from "./which";
|
|
68
82
|
import { whoCommand } from "./who";
|
|
69
83
|
import { whoamiCommand } from "./whoami";
|
|
70
84
|
import { xargsCommand } from "./xargs";
|
|
@@ -95,6 +109,13 @@ const BASE_COMMANDS: ShellModule[] = [
|
|
|
95
109
|
adduserCommand, passwdCommand, deluserCommand, sudoCommand, suCommand,
|
|
96
110
|
// Misc
|
|
97
111
|
neofetchCommand,
|
|
112
|
+
// Package management
|
|
113
|
+
aptCommand, aptCacheCommand, dpkgCommand, dpkgQueryCommand,
|
|
114
|
+
// Shell (extended)
|
|
115
|
+
whichCommand, typeCommand, manCommand, aliasCommand, unaliasCommand,
|
|
116
|
+
testCommand, sourceCommand, historyCommand,
|
|
117
|
+
// System (extended)
|
|
118
|
+
uptimeCommand, freeCommand, lsbReleaseCommand,
|
|
98
119
|
];
|
|
99
120
|
|
|
100
121
|
const customCommands: ShellModule[] = [];
|
|
@@ -196,6 +217,52 @@ export function makeDefaultEnv(authUser: string, hostname: string): ShellEnv {
|
|
|
196
217
|
};
|
|
197
218
|
}
|
|
198
219
|
|
|
220
|
+
/**
|
|
221
|
+
* Execute a pre-parsed command directly by name and argument list.
|
|
222
|
+
*
|
|
223
|
+
* Unlike `runCommand`, this function does NOT re-join name+args into a string
|
|
224
|
+
* and re-parse — so arguments that contain special characters (`;`, `|`, `>`,
|
|
225
|
+
* quotes) are passed through verbatim. Use this from the pipeline executor.
|
|
226
|
+
*/
|
|
227
|
+
export async function runCommandDirect(
|
|
228
|
+
name: string,
|
|
229
|
+
args: string[],
|
|
230
|
+
authUser: string,
|
|
231
|
+
hostname: string,
|
|
232
|
+
mode: CommandMode,
|
|
233
|
+
cwd: string,
|
|
234
|
+
shell: VirtualShell,
|
|
235
|
+
stdin: string | undefined,
|
|
236
|
+
env: ShellEnv,
|
|
237
|
+
): Promise<CommandResult> {
|
|
238
|
+
// Alias expansion on the command name
|
|
239
|
+
const aliasVal = env.vars[`__alias_${name}`];
|
|
240
|
+
if (aliasVal) {
|
|
241
|
+
// Alias may expand to a multi-word command — re-route through runCommand
|
|
242
|
+
return runCommand(`${aliasVal} ${args.join(" ")}`, authUser, hostname, mode, cwd, shell, stdin, env);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const mod = resolveModule(name);
|
|
246
|
+
if (!mod) return { stderr: `${name}: command not found`, exitCode: 127 };
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
return await mod.run({
|
|
250
|
+
authUser,
|
|
251
|
+
hostname,
|
|
252
|
+
activeSessions: shell.users.listActiveSessions(),
|
|
253
|
+
rawInput: [name, ...args].join(" "),
|
|
254
|
+
mode,
|
|
255
|
+
args,
|
|
256
|
+
stdin,
|
|
257
|
+
cwd,
|
|
258
|
+
shell,
|
|
259
|
+
env,
|
|
260
|
+
});
|
|
261
|
+
} catch (error: unknown) {
|
|
262
|
+
return { stderr: error instanceof Error ? error.message : "Command failed", exitCode: 1 };
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
199
266
|
export async function runCommand(
|
|
200
267
|
rawInput: string,
|
|
201
268
|
authUser: string,
|
|
@@ -211,18 +278,56 @@ export async function runCommand(
|
|
|
211
278
|
|
|
212
279
|
const shellEnv: ShellEnv = env ?? makeDefaultEnv(authUser, hostname);
|
|
213
280
|
|
|
281
|
+
// ── $(cmd) command substitution ──────────────────────────────────────────
|
|
282
|
+
let expanded = trimmed;
|
|
283
|
+
if (expanded.includes("$(")) {
|
|
284
|
+
// Only substitute $(…) that are NOT inside single quotes
|
|
285
|
+
// Strategy: walk char by char, track single-quote state
|
|
286
|
+
let result = "";
|
|
287
|
+
let inSingle = false;
|
|
288
|
+
let i = 0;
|
|
289
|
+
while (i < expanded.length) {
|
|
290
|
+
const ch = expanded[i]!;
|
|
291
|
+
if (ch === "'" && !inSingle) { inSingle = true; result += ch; i++; continue; }
|
|
292
|
+
if (ch === "'" && inSingle) { inSingle = false; result += ch; i++; continue; }
|
|
293
|
+
if (!inSingle && ch === "$" && expanded[i + 1] === "(") {
|
|
294
|
+
// Find matching closing )
|
|
295
|
+
let depth = 0;
|
|
296
|
+
let j = i + 1;
|
|
297
|
+
while (j < expanded.length) {
|
|
298
|
+
if (expanded[j] === "(") depth++;
|
|
299
|
+
else if (expanded[j] === ")") { depth--; if (depth === 0) break; }
|
|
300
|
+
j++;
|
|
301
|
+
}
|
|
302
|
+
const sub = expanded.slice(i + 2, j).trim();
|
|
303
|
+
const subResult = await runCommand(sub, authUser, hostname, mode, cwd, shell, undefined, shellEnv);
|
|
304
|
+
const subOut = (subResult.stdout ?? "").replace(/\n$/, "");
|
|
305
|
+
result += subOut;
|
|
306
|
+
i = j + 1;
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
result += ch; i++;
|
|
310
|
+
}
|
|
311
|
+
expanded = result;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ── alias expansion ───────────────────────────────────────────────────────
|
|
315
|
+
const firstWord = expanded.split(/\s+/)[0] ?? "";
|
|
316
|
+
const aliasVal = shellEnv.vars[`__alias_${firstWord}`];
|
|
317
|
+
if (aliasVal) {
|
|
318
|
+
expanded = expanded.replace(firstWord, aliasVal);
|
|
319
|
+
}
|
|
320
|
+
|
|
214
321
|
// Detect shell operators
|
|
215
322
|
if (
|
|
216
|
-
/(?<![|&])[|](?![|])/.test(
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
323
|
+
/(?<![|&])[|](?![|])/.test(expanded) ||
|
|
324
|
+
expanded.includes(">") ||
|
|
325
|
+
expanded.includes("<") ||
|
|
326
|
+
expanded.includes("&&") ||
|
|
327
|
+
expanded.includes("||") ||
|
|
328
|
+
expanded.includes(";")
|
|
222
329
|
) {
|
|
223
|
-
const
|
|
224
|
-
const { executeStatements } = await import("../SSHMimic/executor");
|
|
225
|
-
const script = parseScript(trimmed);
|
|
330
|
+
const script = parseScript(expanded);
|
|
226
331
|
if (!script.isValid) return { stderr: script.error || "Syntax error", exitCode: 1 };
|
|
227
332
|
try {
|
|
228
333
|
return await executeStatements(script.statements, authUser, hostname, mode, cwd, shell, shellEnv);
|
|
@@ -231,7 +336,7 @@ export async function runCommand(
|
|
|
231
336
|
}
|
|
232
337
|
}
|
|
233
338
|
|
|
234
|
-
const { commandName, args } = parseInput(
|
|
339
|
+
const { commandName, args } = parseInput(expanded);
|
|
235
340
|
const mod = resolveModule(commandName);
|
|
236
341
|
|
|
237
342
|
if (!mod) return { stderr: `${commandName}: command not found`, exitCode: 127 };
|
|
@@ -241,7 +346,7 @@ export async function runCommand(
|
|
|
241
346
|
authUser,
|
|
242
347
|
hostname,
|
|
243
348
|
activeSessions: shell.users.listActiveSessions(),
|
|
244
|
-
rawInput:
|
|
349
|
+
rawInput: expanded,
|
|
245
350
|
mode,
|
|
246
351
|
args,
|
|
247
352
|
stdin,
|
package/src/commands/ls.ts
CHANGED
|
@@ -30,15 +30,16 @@ export const lsCommand: ShellModule = {
|
|
|
30
30
|
name: "ls",
|
|
31
31
|
description: "List directory contents",
|
|
32
32
|
category: "navigation",
|
|
33
|
-
params: ["[path]"],
|
|
33
|
+
params: ["[-la] [path]"],
|
|
34
34
|
run: ({ authUser, shell, cwd, args }) => {
|
|
35
|
-
const longFormat
|
|
36
|
-
const
|
|
35
|
+
const longFormat = ifFlag(args, ["-l", "--long"]);
|
|
36
|
+
const showHidden = ifFlag(args, ["-a", "--all"]);
|
|
37
|
+
const targetArg = getArg(args, 0, { flags: ["-l", "--long", "-a", "--all", "-la", "-al"] });
|
|
37
38
|
const target = resolvePath(cwd, targetArg ?? cwd);
|
|
38
39
|
assertPathAccess(authUser, target, "ls");
|
|
39
40
|
const items = shell.vfs
|
|
40
41
|
.list(target)
|
|
41
|
-
.filter((name) => !name.startsWith("."));
|
|
42
|
+
.filter((name) => showHidden || !name.startsWith("."));
|
|
42
43
|
const rendered = longFormat
|
|
43
44
|
? items
|
|
44
45
|
.map((name) => {
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { ShellModule } from "../types/commands";
|
|
2
|
+
import { ifFlag } from "./command-helpers";
|
|
3
|
+
|
|
4
|
+
export const lsbReleaseCommand: ShellModule = {
|
|
5
|
+
name: "lsb_release",
|
|
6
|
+
description: "Print distribution-specific information",
|
|
7
|
+
category: "system",
|
|
8
|
+
params: ["[-a] [-i] [-d] [-r] [-c]"],
|
|
9
|
+
run: ({ args, shell }) => {
|
|
10
|
+
let osName = shell.properties?.os ?? "Fortune GNU/Linux x64";
|
|
11
|
+
let codename = "aurora";
|
|
12
|
+
let version = "1.0";
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const content = shell.vfs.readFile("/etc/os-release");
|
|
16
|
+
for (const line of content.split("\n")) {
|
|
17
|
+
if (line.startsWith("PRETTY_NAME="))
|
|
18
|
+
osName = line.slice("PRETTY_NAME=".length).replace(/^"|"$/g, "").trim();
|
|
19
|
+
if (line.startsWith("VERSION_CODENAME="))
|
|
20
|
+
codename = line.slice("VERSION_CODENAME=".length).trim();
|
|
21
|
+
if (line.startsWith("VERSION_ID="))
|
|
22
|
+
version = line.slice("VERSION_ID=".length).replace(/^"|"$/g, "").trim();
|
|
23
|
+
}
|
|
24
|
+
} catch {}
|
|
25
|
+
|
|
26
|
+
const all = ifFlag(args, ["-a", "--all"]);
|
|
27
|
+
const showId = ifFlag(args, ["-i", "--id"]);
|
|
28
|
+
const showDesc = ifFlag(args, ["-d", "--description"]);
|
|
29
|
+
const showRelease = ifFlag(args, ["-r", "--release"]);
|
|
30
|
+
const showCodename = ifFlag(args, ["-c", "--codename"]);
|
|
31
|
+
|
|
32
|
+
if (all || args.length === 0) {
|
|
33
|
+
return {
|
|
34
|
+
stdout: [
|
|
35
|
+
`Distributor ID:\tFortune`,
|
|
36
|
+
`Description:\t${osName}`,
|
|
37
|
+
`Release:\t${version}`,
|
|
38
|
+
`Codename:\t${codename}`,
|
|
39
|
+
].join("\n"),
|
|
40
|
+
exitCode: 0,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const lines: string[] = [];
|
|
45
|
+
if (showId) lines.push(`Distributor ID:\tFortune`);
|
|
46
|
+
if (showDesc) lines.push(`Description:\t${osName}`);
|
|
47
|
+
if (showRelease) lines.push(`Release:\t${version}`);
|
|
48
|
+
if (showCodename) lines.push(`Codename:\t${codename}`);
|
|
49
|
+
|
|
50
|
+
return { stdout: lines.join("\n"), exitCode: 0 };
|
|
51
|
+
},
|
|
52
|
+
};
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import type { ShellModule } from "../types/commands";
|
|
2
|
+
|
|
3
|
+
const MAN_PAGES: Record<string, string> = {
|
|
4
|
+
ls: `LS(1) User Commands LS(1)
|
|
5
|
+
|
|
6
|
+
NAME
|
|
7
|
+
ls - list directory contents
|
|
8
|
+
|
|
9
|
+
SYNOPSIS
|
|
10
|
+
ls [OPTION]... [FILE]...
|
|
11
|
+
|
|
12
|
+
DESCRIPTION
|
|
13
|
+
List information about the FILEs (the current directory by default).
|
|
14
|
+
|
|
15
|
+
OPTIONS
|
|
16
|
+
-l use a long listing format
|
|
17
|
+
-a do not ignore entries starting with .
|
|
18
|
+
-h with -l, print human readable sizes
|
|
19
|
+
-r reverse order while sorting
|
|
20
|
+
-t sort by modification time
|
|
21
|
+
|
|
22
|
+
AUTHOR
|
|
23
|
+
Written by Richard M. Stallman and David MacKenzie.`,
|
|
24
|
+
|
|
25
|
+
cat: `CAT(1) User Commands CAT(1)
|
|
26
|
+
|
|
27
|
+
NAME
|
|
28
|
+
cat - concatenate files and print on the standard output
|
|
29
|
+
|
|
30
|
+
SYNOPSIS
|
|
31
|
+
cat [OPTION]... [FILE]...
|
|
32
|
+
|
|
33
|
+
DESCRIPTION
|
|
34
|
+
Concatenate FILE(s) to standard output.
|
|
35
|
+
|
|
36
|
+
OPTIONS
|
|
37
|
+
-n, --number number all output lines
|
|
38
|
+
-b, --number-nonblank number nonempty output lines`,
|
|
39
|
+
|
|
40
|
+
grep: `GREP(1) User Commands GREP(1)
|
|
41
|
+
|
|
42
|
+
NAME
|
|
43
|
+
grep, egrep, fgrep - print lines that match patterns
|
|
44
|
+
|
|
45
|
+
SYNOPSIS
|
|
46
|
+
grep [OPTION]... PATTERNS [FILE]...
|
|
47
|
+
|
|
48
|
+
OPTIONS
|
|
49
|
+
-i, --ignore-case ignore case distinctions in patterns and data
|
|
50
|
+
-v, --invert-match select non-matching lines
|
|
51
|
+
-n, --line-number print line number with output lines
|
|
52
|
+
-r, --recursive read all files under each directory, recursively`,
|
|
53
|
+
|
|
54
|
+
apt: `APT(8) APT APT(8)
|
|
55
|
+
|
|
56
|
+
NAME
|
|
57
|
+
apt - command-line interface
|
|
58
|
+
|
|
59
|
+
SYNOPSIS
|
|
60
|
+
apt [options] command
|
|
61
|
+
|
|
62
|
+
DESCRIPTION
|
|
63
|
+
apt provides a high-level commandline interface for the package
|
|
64
|
+
management system.
|
|
65
|
+
|
|
66
|
+
COMMANDS
|
|
67
|
+
install pkg... Install packages
|
|
68
|
+
remove pkg... Remove packages
|
|
69
|
+
update Download package information
|
|
70
|
+
upgrade Upgrade installed packages
|
|
71
|
+
search term Search in package descriptions
|
|
72
|
+
show pkg Show package information
|
|
73
|
+
list List packages`,
|
|
74
|
+
|
|
75
|
+
ssh: `SSH(1) OpenSSH SSH(1)
|
|
76
|
+
|
|
77
|
+
NAME
|
|
78
|
+
ssh - OpenSSH remote login client
|
|
79
|
+
|
|
80
|
+
SYNOPSIS
|
|
81
|
+
ssh [-p port] [user@]hostname [command]
|
|
82
|
+
|
|
83
|
+
DESCRIPTION
|
|
84
|
+
ssh (SSH client) is a program for logging into a remote machine and
|
|
85
|
+
for executing commands on a remote machine.`,
|
|
86
|
+
|
|
87
|
+
curl: `CURL(1) User Commands CURL(1)
|
|
88
|
+
|
|
89
|
+
NAME
|
|
90
|
+
curl - transfer a URL
|
|
91
|
+
|
|
92
|
+
SYNOPSIS
|
|
93
|
+
curl [options / URLs]
|
|
94
|
+
|
|
95
|
+
DESCRIPTION
|
|
96
|
+
curl is a tool for transferring data with URL syntax.
|
|
97
|
+
|
|
98
|
+
OPTIONS
|
|
99
|
+
-o, --output <file> Write output to <file>
|
|
100
|
+
-X, --request <method> Specify request method
|
|
101
|
+
-d, --data <data> HTTP POST data
|
|
102
|
+
-H, --header <header> Pass custom header
|
|
103
|
+
-s, --silent Silent mode
|
|
104
|
+
-I, --head Show document info only
|
|
105
|
+
-L, --location Follow redirects
|
|
106
|
+
-v, --verbose Make the operation more talkative`,
|
|
107
|
+
|
|
108
|
+
chmod: `CHMOD(1) User Commands CHMOD(1)
|
|
109
|
+
|
|
110
|
+
NAME
|
|
111
|
+
chmod - change file mode bits
|
|
112
|
+
|
|
113
|
+
SYNOPSIS
|
|
114
|
+
chmod [OPTION]... MODE[,MODE]... FILE...
|
|
115
|
+
chmod [OPTION]... OCTAL-MODE FILE...
|
|
116
|
+
|
|
117
|
+
DESCRIPTION
|
|
118
|
+
Change the file mode bits of each given file according to MODE.
|
|
119
|
+
|
|
120
|
+
EXAMPLES
|
|
121
|
+
chmod 755 script.sh rwxr-xr-x
|
|
122
|
+
chmod 644 file.txt rw-r--r--
|
|
123
|
+
chmod +x script.sh add execute permission`,
|
|
124
|
+
|
|
125
|
+
tar: `TAR(1) GNU tar Manual TAR(1)
|
|
126
|
+
|
|
127
|
+
NAME
|
|
128
|
+
tar - an archiving utility
|
|
129
|
+
|
|
130
|
+
SYNOPSIS
|
|
131
|
+
tar [OPTION...] [FILE]...
|
|
132
|
+
|
|
133
|
+
DESCRIPTION
|
|
134
|
+
tar saves many files together into a single tape or disk archive,
|
|
135
|
+
and can restore individual files from the archive.
|
|
136
|
+
|
|
137
|
+
OPTIONS
|
|
138
|
+
-c, --create create a new archive
|
|
139
|
+
-x, --extract extract files from an archive
|
|
140
|
+
-z, --gzip filter the archive through gzip
|
|
141
|
+
-f, --file=ARCHIVE use archive file or device ARCHIVE
|
|
142
|
+
-v, --verbose verbosely list files processed
|
|
143
|
+
-t, --list list the contents of an archive`,
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
export const manCommand: ShellModule = {
|
|
147
|
+
name: "man",
|
|
148
|
+
description: "Interface to the system reference manuals",
|
|
149
|
+
category: "shell",
|
|
150
|
+
params: ["<command>"],
|
|
151
|
+
run: ({ args, shell }) => {
|
|
152
|
+
const name = args[0];
|
|
153
|
+
if (!name) return { stderr: "What manual page do you want?", exitCode: 1 };
|
|
154
|
+
|
|
155
|
+
// VFS-installed man pages take priority
|
|
156
|
+
const manPath = `/usr/share/man/man1/${name}.1`;
|
|
157
|
+
if (shell.vfs.exists(manPath)) {
|
|
158
|
+
return { stdout: shell.vfs.readFile(manPath), exitCode: 0 };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const page = MAN_PAGES[name.toLowerCase()];
|
|
162
|
+
if (page) return { stdout: page, exitCode: 0 };
|
|
163
|
+
|
|
164
|
+
return { stderr: `No manual entry for ${name}`, exitCode: 16 };
|
|
165
|
+
},
|
|
166
|
+
};
|
package/src/commands/neofetch.ts
CHANGED
|
@@ -32,6 +32,11 @@ export const neofetchCommand: ShellModule = {
|
|
|
32
32
|
shell: env.SHELL,
|
|
33
33
|
shellProps: shell.properties,
|
|
34
34
|
terminal: env.TERM,
|
|
35
|
+
uptimeSeconds: Math.floor((Date.now() - shell.startTime) / 1000),
|
|
36
|
+
packages: (() => {
|
|
37
|
+
const count = shell.packageManager?.installedCount() ?? 0;
|
|
38
|
+
return `${count} (dpkg)`;
|
|
39
|
+
})(),
|
|
35
40
|
}),
|
|
36
41
|
exitCode: 0,
|
|
37
42
|
};
|
package/src/commands/ping.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ShellModule } from "../types/commands";
|
|
2
|
+
import { parseArgs } from "./command-helpers";
|
|
2
3
|
|
|
3
4
|
export const pingCommand: ShellModule = {
|
|
4
5
|
name: "ping",
|
|
@@ -6,8 +7,10 @@ export const pingCommand: ShellModule = {
|
|
|
6
7
|
category: "network",
|
|
7
8
|
params: ["[-c <count>] <host>"],
|
|
8
9
|
run: ({ args }) => {
|
|
9
|
-
const
|
|
10
|
-
const
|
|
10
|
+
const { flagsWithValues, positionals } = parseArgs(args, { flagsWithValue: ["-c", "-i", "-W"] });
|
|
11
|
+
const host = positionals[0] ?? "localhost";
|
|
12
|
+
const countArg = flagsWithValues.get("-c");
|
|
13
|
+
const count = countArg ? Math.max(1, parseInt(countArg, 10) || 4) : 4;
|
|
11
14
|
const lines = [`PING ${host}: 56 data bytes`];
|
|
12
15
|
for (let i = 0; i < count; i++) {
|
|
13
16
|
const ms = (Math.random() * 10 + 1).toFixed(3);
|
package/src/commands/ps.ts
CHANGED
|
@@ -1,19 +1,41 @@
|
|
|
1
1
|
import type { ShellModule } from "../types/commands";
|
|
2
|
+
import { ifFlag } from "./command-helpers";
|
|
2
3
|
|
|
3
4
|
export const psCommand: ShellModule = {
|
|
4
5
|
name: "ps",
|
|
5
6
|
description: "Report process status",
|
|
6
7
|
category: "system",
|
|
7
|
-
params: ["[-a] [-u] [-x]"],
|
|
8
|
-
run: ({ authUser, shell }) => {
|
|
8
|
+
params: ["[-a] [-u] [-x] [aux]"],
|
|
9
|
+
run: ({ authUser, shell, args }) => {
|
|
9
10
|
const sessions = shell.users.listActiveSessions();
|
|
10
|
-
const
|
|
11
|
+
const showUser = ifFlag(args, ["-u"]) || args.includes("u") || args.includes("aux") || args.includes("au");
|
|
12
|
+
const showAll = ifFlag(args, ["-a", "-x"]) || args.includes("a") || args.includes("aux");
|
|
13
|
+
|
|
14
|
+
if (showUser) {
|
|
15
|
+
const header = "USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND";
|
|
16
|
+
const rows: string[] = [header];
|
|
17
|
+
let pid = 1000;
|
|
18
|
+
for (const s of sessions) {
|
|
19
|
+
const user = s.username.padEnd(10).slice(0, 10);
|
|
20
|
+
const mem = (Math.random() * 0.5).toFixed(1);
|
|
21
|
+
const vsz = Math.floor(Math.random() * 20000 + 5000);
|
|
22
|
+
const rss = Math.floor(Math.random() * 5000 + 1000);
|
|
23
|
+
rows.push(`${user} ${String(pid).padStart(6)} 0.0 ${mem.padStart(4)} ${String(vsz).padStart(6)} ${String(rss).padStart(5)} ${s.tty.padEnd(8)} Ss 00:00 0:00 bash`);
|
|
24
|
+
pid++;
|
|
25
|
+
}
|
|
26
|
+
rows.push(`root ${String(pid).padStart(6)} 0.0 0.0 0 0 ? S 00:00 0:00 ps`);
|
|
27
|
+
return { stdout: rows.join("\n"), exitCode: 0 };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const header = " PID TTY TIME CMD";
|
|
31
|
+
const rows: string[] = [header];
|
|
11
32
|
let pid = 1000;
|
|
12
33
|
for (const s of sessions) {
|
|
13
|
-
|
|
34
|
+
if (!showAll && s.username !== authUser) continue;
|
|
35
|
+
rows.push(`${String(pid).padStart(5)} ${s.tty.padEnd(12)} 00:00:00 ${s.username === authUser ? "bash" : `bash (${s.username})`}`);
|
|
14
36
|
pid++;
|
|
15
37
|
}
|
|
16
|
-
|
|
17
|
-
return { stdout:
|
|
38
|
+
rows.push(`${String(pid).padStart(5)} pts/0 00:00:00 ps`);
|
|
39
|
+
return { stdout: rows.join("\n"), exitCode: 0 };
|
|
18
40
|
},
|
|
19
41
|
};
|
package/src/commands/sh.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import type { CommandContext, CommandResult, ShellModule } from "../types/commands";
|
|
2
|
-
import {
|
|
2
|
+
import { ifFlag } from "./command-helpers";
|
|
3
3
|
import { resolvePath } from "./helpers";
|
|
4
4
|
import { runCommand } from "./index";
|
|
5
5
|
|
|
6
|
-
/**
|
|
7
|
-
|
|
6
|
+
/** Alias for clarity inside sh.ts */
|
|
7
|
+
type ShellContext = CommandContext;
|
|
8
|
+
|
|
9
|
+
/** Expand $VAR and ${VAR:-default} in a line using the current env (sync, no $(cmd)) */
|
|
10
|
+
function expandVarsSync(line: string, env: Record<string, string>, lastExit: number): string {
|
|
8
11
|
return line
|
|
9
12
|
.replace(/\$\?/g, String(lastExit))
|
|
10
13
|
.replace(/\$\{([^}:]+):-([^}]*)\}/g, (_, n, d) => env[n] ?? d)
|
|
@@ -13,6 +16,25 @@ function expandVars(line: string, env: Record<string, string>, lastExit: number)
|
|
|
13
16
|
.replace(/^~(\/|$)/, `${env.HOME ?? "/home/user"}$1`);
|
|
14
17
|
}
|
|
15
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Expand $VAR, ${VAR:-default}, and $(cmd) substitution asynchronously.
|
|
21
|
+
* Used before executing each line in sh script context.
|
|
22
|
+
*/
|
|
23
|
+
async function expandVars(line: string, env: Record<string, string>, lastExit: number, ctx: ShellContext): Promise<string> {
|
|
24
|
+
// $(cmd) substitution first
|
|
25
|
+
if (line.includes("$(")) {
|
|
26
|
+
const subRe = /\$\(([^)]+)\)/g;
|
|
27
|
+
const matches = [...line.matchAll(subRe)];
|
|
28
|
+
for (const m of matches) {
|
|
29
|
+
const sub = m[1]?.trim() ?? "";
|
|
30
|
+
const subResult = await runCommand(sub, ctx.authUser, ctx.hostname, ctx.mode, ctx.cwd, ctx.shell, undefined, ctx.env);
|
|
31
|
+
const subOut = (subResult.stdout ?? "").replace(/\n$/, "");
|
|
32
|
+
line = line.replace(m[0], subOut);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return expandVarsSync(line, env, lastExit);
|
|
36
|
+
}
|
|
37
|
+
|
|
16
38
|
type Block =
|
|
17
39
|
| { type: "if"; cond: string; then: string[]; elif: Array<{ cond: string; body: string[] }>; else_: string[] }
|
|
18
40
|
| { type: "for"; var: string; list: string; body: string[] }
|
|
@@ -54,8 +76,8 @@ function parseBlocks(lines: string[]): Block[] {
|
|
|
54
76
|
const body: string[] = [];
|
|
55
77
|
i++;
|
|
56
78
|
while (i < lines.length && lines[i]?.trim() !== "done") {
|
|
57
|
-
const l = lines[i]!.trim();
|
|
58
|
-
if (l !== "do") body.push(l);
|
|
79
|
+
const l = lines[i]!.trim().replace(/^do\s+/, "");
|
|
80
|
+
if (l && l !== "do") body.push(l);
|
|
59
81
|
i++;
|
|
60
82
|
}
|
|
61
83
|
blocks.push({ type: "for", var: m[1]!, list: m[2]!, body });
|
|
@@ -65,8 +87,8 @@ function parseBlocks(lines: string[]): Block[] {
|
|
|
65
87
|
const body: string[] = [];
|
|
66
88
|
i++;
|
|
67
89
|
while (i < lines.length && lines[i]?.trim() !== "done") {
|
|
68
|
-
const l = lines[i]!.trim();
|
|
69
|
-
if (l !== "do") body.push(l);
|
|
90
|
+
const l = lines[i]!.trim().replace(/^do\s+/, "");
|
|
91
|
+
if (l && l !== "do") body.push(l);
|
|
70
92
|
i++;
|
|
71
93
|
}
|
|
72
94
|
blocks.push({ type: "while", cond, body });
|
|
@@ -79,7 +101,7 @@ function parseBlocks(lines: string[]): Block[] {
|
|
|
79
101
|
}
|
|
80
102
|
|
|
81
103
|
async function evalCondition(cond: string, ctx: CommandContext): Promise<boolean> {
|
|
82
|
-
const expanded = expandVars(cond, ctx.env.vars, ctx.env.lastExitCode);
|
|
104
|
+
const expanded = await expandVars(cond, ctx.env.vars, ctx.env.lastExitCode, ctx);
|
|
83
105
|
// test -f / test -d / [ ... ]
|
|
84
106
|
const testMatch = expanded.match(/^\[?\s*(.+?)\s*\]?$/);
|
|
85
107
|
if (testMatch) {
|
|
@@ -126,7 +148,7 @@ async function runBlocks(blocks: Block[], ctx: CommandContext): Promise<CommandR
|
|
|
126
148
|
|
|
127
149
|
for (const block of blocks) {
|
|
128
150
|
if (block.type === "cmd") {
|
|
129
|
-
const expanded = expandVars(block.line, ctx.env.vars, ctx.env.lastExitCode);
|
|
151
|
+
const expanded = await expandVars(block.line, ctx.env.vars, ctx.env.lastExitCode, ctx);
|
|
130
152
|
const r = await runCommand(expanded, ctx.authUser, ctx.hostname, ctx.mode, ctx.cwd, ctx.shell, undefined, ctx.env);
|
|
131
153
|
ctx.env.lastExitCode = r.exitCode ?? 0;
|
|
132
154
|
if (r.stdout) output += `${r.stdout}\n`;
|
|
@@ -152,7 +174,7 @@ async function runBlocks(blocks: Block[], ctx: CommandContext): Promise<CommandR
|
|
|
152
174
|
}
|
|
153
175
|
}
|
|
154
176
|
} else if (block.type === "for") {
|
|
155
|
-
const listExpanded = expandVars(block.list, ctx.env.vars, ctx.env.lastExitCode);
|
|
177
|
+
const listExpanded = await expandVars(block.list, ctx.env.vars, ctx.env.lastExitCode, ctx);
|
|
156
178
|
const items = listExpanded.trim().split(/\s+/);
|
|
157
179
|
for (const item of items) {
|
|
158
180
|
ctx.env.vars[block.var] = item;
|
|
@@ -184,7 +206,7 @@ export const shCommand: ShellModule = {
|
|
|
184
206
|
|
|
185
207
|
// sh -c "inline script"
|
|
186
208
|
if (ifFlag(args, "-c")) {
|
|
187
|
-
const script =
|
|
209
|
+
const script = args[args.indexOf("-c") + 1] ?? "";
|
|
188
210
|
if (!script) return { stderr: "sh: -c requires a script", exitCode: 1 };
|
|
189
211
|
const lines = script.split(/[;\n]/).map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
|
|
190
212
|
const blocks = parseBlocks(lines);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ShellModule } from "../types/commands";
|
|
2
|
+
import { resolvePath } from "./helpers";
|
|
3
|
+
import { runCommand } from ".";
|
|
4
|
+
|
|
5
|
+
export const sourceCommand: ShellModule = {
|
|
6
|
+
name: "source",
|
|
7
|
+
aliases: ["."],
|
|
8
|
+
description: "Execute commands from a file in the current shell environment",
|
|
9
|
+
category: "shell",
|
|
10
|
+
params: ["<file> [args...]"],
|
|
11
|
+
run: async ({ args, authUser, hostname, cwd, shell, env }) => {
|
|
12
|
+
const fileArg = args[0];
|
|
13
|
+
if (!fileArg) {
|
|
14
|
+
return { stderr: "source: missing filename", exitCode: 1 };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const filePath = resolvePath(cwd, fileArg);
|
|
18
|
+
if (!shell.vfs.exists(filePath)) {
|
|
19
|
+
return { stderr: `source: ${fileArg}: No such file or directory`, exitCode: 1 };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const content = shell.vfs.readFile(filePath);
|
|
23
|
+
let lastExitCode = 0;
|
|
24
|
+
|
|
25
|
+
for (const line of content.split("\n")) {
|
|
26
|
+
const l = line.trim();
|
|
27
|
+
if (!l || l.startsWith("#")) continue;
|
|
28
|
+
const result = await runCommand(l, authUser, hostname, "shell", cwd, shell, undefined, env);
|
|
29
|
+
lastExitCode = result.exitCode ?? 0;
|
|
30
|
+
if (result.closeSession || result.switchUser) return result;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return { exitCode: lastExitCode };
|
|
34
|
+
},
|
|
35
|
+
};
|