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/chmod.ts
CHANGED
|
@@ -1,6 +1,42 @@
|
|
|
1
1
|
import type { ShellModule } from "../types/commands";
|
|
2
2
|
import { assertPathAccess, resolvePath } from "./helpers";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Parse a symbolic chmod mode string (e.g. "+x", "u+x", "go-w", "a+rx")
|
|
6
|
+
* and apply it to the existing mode bits.
|
|
7
|
+
* Returns null if the string is not a valid symbolic mode.
|
|
8
|
+
*/
|
|
9
|
+
function applySymbolicMode(existing: number, modeStr: string): number | null {
|
|
10
|
+
const pattern = /^([ugoa]*)([+\-=])([rwx]*)$/;
|
|
11
|
+
const parts = modeStr.split(",");
|
|
12
|
+
let mode = existing;
|
|
13
|
+
for (const part of parts) {
|
|
14
|
+
const m = part.trim().match(pattern);
|
|
15
|
+
if (!m) return null;
|
|
16
|
+
const [, who = "a", op, perms = ""] = m;
|
|
17
|
+
const targets = who === "" || who === "a" ? ["u", "g", "o"] : who.split("");
|
|
18
|
+
const bits: Record<string, Record<string, number>> = {
|
|
19
|
+
u: { r: 0o400, w: 0o200, x: 0o100 },
|
|
20
|
+
g: { r: 0o040, w: 0o020, x: 0o010 },
|
|
21
|
+
o: { r: 0o004, w: 0o002, x: 0o001 },
|
|
22
|
+
};
|
|
23
|
+
for (const t of targets) {
|
|
24
|
+
for (const p of perms.split("")) {
|
|
25
|
+
const bit = bits[t]?.[p];
|
|
26
|
+
if (bit === undefined) continue;
|
|
27
|
+
if (op === "+") mode |= bit;
|
|
28
|
+
else if (op === "-") mode &= ~bit;
|
|
29
|
+
else if (op === "=") {
|
|
30
|
+
// clear all bits for this target, then set requested
|
|
31
|
+
const mask = Object.values(bits[t] ?? {}).reduce((a, b) => a | b, 0);
|
|
32
|
+
mode = (mode & ~mask) | bit;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return mode;
|
|
38
|
+
}
|
|
39
|
+
|
|
4
40
|
export const chmodCommand: ShellModule = {
|
|
5
41
|
name: "chmod",
|
|
6
42
|
description: "Change file permissions",
|
|
@@ -21,9 +57,18 @@ export const chmodCommand: ShellModule = {
|
|
|
21
57
|
exitCode: 1,
|
|
22
58
|
};
|
|
23
59
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
60
|
+
let mode: number;
|
|
61
|
+
const octal = parseInt(modeArg, 8);
|
|
62
|
+
if (!Number.isNaN(octal) && /^[0-7]+$/.test(modeArg)) {
|
|
63
|
+
mode = octal;
|
|
64
|
+
} else {
|
|
65
|
+
// symbolic mode
|
|
66
|
+
const existing = shell.vfs.stat(filePath).mode;
|
|
67
|
+
const result = applySymbolicMode(existing, modeArg);
|
|
68
|
+
if (result === null) {
|
|
69
|
+
return { stderr: `chmod: invalid mode: ${modeArg}`, exitCode: 1 };
|
|
70
|
+
}
|
|
71
|
+
mode = result;
|
|
27
72
|
}
|
|
28
73
|
shell.vfs.chmod(filePath, mode);
|
|
29
74
|
return { exitCode: 0 };
|
|
@@ -80,6 +80,23 @@ function collectPositionals(
|
|
|
80
80
|
return positionals;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Returns `true` when any of the given flags appear in `args`.
|
|
85
|
+
*
|
|
86
|
+
* Matches both standalone tokens (`-s`, `--silent`) and inline forms
|
|
87
|
+
* (`--output=file`). Useful for simple boolean flag checks inside command
|
|
88
|
+
* `run` handlers.
|
|
89
|
+
*
|
|
90
|
+
* @param args Tokenized argument array from `CommandContext.args`.
|
|
91
|
+
* @param flags Single flag string or array of equivalent flag strings.
|
|
92
|
+
* @returns `true` if at least one flag is present, otherwise `false`.
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* ```ts
|
|
96
|
+
* ifFlag(args, "-r") // single flag
|
|
97
|
+
* ifFlag(args, ["-r", "--recursive"]) // aliases
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
83
100
|
export function ifFlag(args: string[], flags: string | string[]): boolean {
|
|
84
101
|
const allFlags = toFlagList(flags);
|
|
85
102
|
|
|
@@ -94,6 +111,25 @@ export function ifFlag(args: string[], flags: string | string[]): boolean {
|
|
|
94
111
|
return false;
|
|
95
112
|
}
|
|
96
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Returns the value associated with a flag, or `true` if the flag is present
|
|
116
|
+
* but has no associated value, or `undefined` if the flag is absent.
|
|
117
|
+
*
|
|
118
|
+
* Handles three forms:
|
|
119
|
+
* - `--output file` → returns `"file"` (next token)
|
|
120
|
+
* - `--output=file` → returns `"file"` (inline `=` form)
|
|
121
|
+
* - `--verbose` → returns `true` (flag with no value)
|
|
122
|
+
*
|
|
123
|
+
* @param args Tokenized argument array from `CommandContext.args`.
|
|
124
|
+
* @param flags Single flag string or array of equivalent flag strings.
|
|
125
|
+
* @returns The flag value string, `true` when valueless, or `undefined`.
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* ```ts
|
|
129
|
+
* const output = getFlag(args, ["-o", "--output"]);
|
|
130
|
+
* if (typeof output === "string") { /* use path *\/ }
|
|
131
|
+
* ```
|
|
132
|
+
*/
|
|
97
133
|
export function getFlag(
|
|
98
134
|
args: string[],
|
|
99
135
|
flags: string | string[],
|
|
@@ -125,6 +161,26 @@ export function getFlag(
|
|
|
125
161
|
return undefined;
|
|
126
162
|
}
|
|
127
163
|
|
|
164
|
+
/**
|
|
165
|
+
* Returns the positional argument at the given zero-based index, skipping
|
|
166
|
+
* known flags and their values.
|
|
167
|
+
*
|
|
168
|
+
* Flags declared in `options.flags` are treated as boolean and skipped.
|
|
169
|
+
* Flags declared in `options.flagsWithValue` consume the next token too.
|
|
170
|
+
* Tokens after `--` are always treated as positionals.
|
|
171
|
+
*
|
|
172
|
+
* @param args Tokenized argument array from `CommandContext.args`.
|
|
173
|
+
* @param index Zero-based positional index to retrieve.
|
|
174
|
+
* @param options Optional flag declarations to skip during positional collection.
|
|
175
|
+
* @returns The positional value, or `undefined` if the index is out of range.
|
|
176
|
+
*
|
|
177
|
+
* @example
|
|
178
|
+
* ```ts
|
|
179
|
+
* // args = ["-r", "src", "dest"]
|
|
180
|
+
* getArg(args, 0, { flags: ["-r"] }) // "src"
|
|
181
|
+
* getArg(args, 1, { flags: ["-r"] }) // "dest"
|
|
182
|
+
* ```
|
|
183
|
+
*/
|
|
128
184
|
export function getArg(
|
|
129
185
|
args: string[],
|
|
130
186
|
index: number,
|
|
@@ -135,10 +191,28 @@ export function getArg(
|
|
|
135
191
|
}
|
|
136
192
|
|
|
137
193
|
/**
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
194
|
+
* Parses an argument array into structured flags, flag values, and positionals.
|
|
195
|
+
*
|
|
196
|
+
* - `options.flags` — boolean flags (e.g. `["-r", "--recursive"]`); collected
|
|
197
|
+
* into a `Set<string>` and not treated as positionals.
|
|
198
|
+
* - `options.flagsWithValue` — flags that consume the next token or an inline
|
|
199
|
+
* `=value`; collected into a `Map<string, string>`.
|
|
200
|
+
* - All remaining tokens are positionals.
|
|
201
|
+
* - Tokens after `--` are always positionals, regardless of `-` prefix.
|
|
202
|
+
*
|
|
203
|
+
* @param args Tokenized argument array from `CommandContext.args`.
|
|
204
|
+
* @param options Flag declaration lists.
|
|
205
|
+
* @returns `{ flags, flagsWithValues, positionals }`.
|
|
206
|
+
*
|
|
207
|
+
* @example
|
|
208
|
+
* ```ts
|
|
209
|
+
* const { flags, flagsWithValues, positionals } = parseArgs(args, {
|
|
210
|
+
* flags: ["-r", "--recursive"],
|
|
211
|
+
* flagsWithValue: ["-o", "--output"],
|
|
212
|
+
* });
|
|
213
|
+
* const recursive = flags.has("-r");
|
|
214
|
+
* const output = flagsWithValues.get("-o");
|
|
215
|
+
* ```
|
|
142
216
|
*/
|
|
143
217
|
export function parseArgs(
|
|
144
218
|
args: string[],
|
package/src/commands/curl.ts
CHANGED
|
@@ -1,61 +1,102 @@
|
|
|
1
1
|
import type { ShellModule } from "../types/commands";
|
|
2
|
-
import { parseArgs } from "./command-helpers";
|
|
3
|
-
import {
|
|
4
|
-
assertPathAccess,
|
|
5
|
-
normalizeTerminalOutput,
|
|
6
|
-
resolvePath,
|
|
7
|
-
runHostCommand,
|
|
8
|
-
} from "./helpers";
|
|
2
|
+
import { ifFlag, parseArgs } from "./command-helpers";
|
|
3
|
+
import { assertPathAccess, resolvePath } from "./helpers";
|
|
9
4
|
|
|
10
5
|
export const curlCommand: ShellModule = {
|
|
11
6
|
name: "curl",
|
|
12
|
-
description: "
|
|
7
|
+
description: "Transfer data from or to a server (pure fetch)",
|
|
13
8
|
category: "network",
|
|
14
|
-
params: ["[
|
|
9
|
+
params: ["[options] <url>"],
|
|
15
10
|
run: async ({ authUser, cwd, args, shell }) => {
|
|
16
11
|
const { flagsWithValues, positionals } = parseArgs(args, {
|
|
17
|
-
flagsWithValue: ["-o", "--output"],
|
|
12
|
+
flagsWithValue: ["-o", "--output", "-X", "--request", "-d", "--data", "-H", "--header", "-u", "--user"],
|
|
18
13
|
});
|
|
19
|
-
|
|
20
|
-
|
|
14
|
+
|
|
15
|
+
if (ifFlag(args, ["--help", "-h"])) {
|
|
16
|
+
return {
|
|
17
|
+
stdout: [
|
|
18
|
+
"Usage: curl [options] <url>",
|
|
19
|
+
" -o, --output <file> Write to file",
|
|
20
|
+
" -X, --request <method> HTTP method",
|
|
21
|
+
" -d, --data <data> POST data",
|
|
22
|
+
" -H, --header <hdr> Extra header",
|
|
23
|
+
" -s, --silent Silent mode",
|
|
24
|
+
" -I, --head Fetch headers only",
|
|
25
|
+
" -L, --location Follow redirects",
|
|
26
|
+
" -v, --verbose Verbose",
|
|
27
|
+
].join("\n"),
|
|
28
|
+
exitCode: 0,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
21
32
|
const url = positionals[0];
|
|
33
|
+
if (!url) return { stderr: "curl: no URL specified", exitCode: 1 };
|
|
22
34
|
|
|
23
|
-
|
|
24
|
-
|
|
35
|
+
const outputPath = flagsWithValues.get("-o") ?? flagsWithValues.get("--output") ?? null;
|
|
36
|
+
const method = (flagsWithValues.get("-X") ?? flagsWithValues.get("--request") ?? "GET").toUpperCase();
|
|
37
|
+
const postData = flagsWithValues.get("-d") ?? flagsWithValues.get("--data") ?? null;
|
|
38
|
+
const headerRaw = flagsWithValues.get("-H") ?? flagsWithValues.get("--header") ?? null;
|
|
39
|
+
const silent = ifFlag(args, ["-s", "--silent"]);
|
|
40
|
+
const headOnly = ifFlag(args, ["-I", "--head"]);
|
|
41
|
+
const followRedirects = ifFlag(args, ["-L", "--location"]);
|
|
42
|
+
const verbose = ifFlag(args, ["-v", "--verbose"]);
|
|
43
|
+
|
|
44
|
+
const extraHeaders: Record<string, string> = { "User-Agent": "curl/7.88.1" };
|
|
45
|
+
if (headerRaw) {
|
|
46
|
+
const idx = headerRaw.indexOf(":");
|
|
47
|
+
if (idx !== -1) extraHeaders[headerRaw.slice(0, idx).trim()] = headerRaw.slice(idx + 1).trim();
|
|
25
48
|
}
|
|
26
49
|
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
:
|
|
30
|
-
|
|
50
|
+
const finalMethod = postData && method === "GET" ? "POST" : method;
|
|
51
|
+
const fetchOpts: RequestInit = {
|
|
52
|
+
method: finalMethod,
|
|
53
|
+
headers: extraHeaders,
|
|
54
|
+
redirect: followRedirects ? "follow" : "manual",
|
|
55
|
+
};
|
|
56
|
+
if (postData) {
|
|
57
|
+
extraHeaders["Content-Type"] ??= "application/x-www-form-urlencoded";
|
|
58
|
+
fetchOpts.body = postData;
|
|
59
|
+
}
|
|
31
60
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
),
|
|
37
|
-
exitCode: result.exitCode,
|
|
38
|
-
};
|
|
61
|
+
const stderrLines: string[] = [];
|
|
62
|
+
if (verbose) {
|
|
63
|
+
stderrLines.push(`* Trying ${url}...`, `* Connected`);
|
|
64
|
+
stderrLines.push(`> ${finalMethod} / HTTP/1.1`, `> Host: ${new URL(url).host}`);
|
|
39
65
|
}
|
|
40
66
|
|
|
67
|
+
let response: Response;
|
|
68
|
+
try {
|
|
69
|
+
response = await fetch(url, fetchOpts);
|
|
70
|
+
} catch (err) {
|
|
71
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
72
|
+
return { stderr: `curl: (6) Could not resolve host: ${msg}`, exitCode: 6 };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (verbose) {
|
|
76
|
+
stderrLines.push(`< HTTP/1.1 ${response.status} ${response.statusText}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (headOnly) {
|
|
80
|
+
const lines = [`HTTP/1.1 ${response.status} ${response.statusText}`];
|
|
81
|
+
for (const [k, v] of response.headers.entries()) lines.push(`${k}: ${v}`);
|
|
82
|
+
return { stdout: `${lines.join("\r\n")}\r\n`, exitCode: 0 };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let body: string;
|
|
86
|
+
try { body = await response.text(); } catch { return { stderr: "curl: failed to read response body", exitCode: 1 }; }
|
|
87
|
+
|
|
41
88
|
if (outputPath) {
|
|
42
89
|
const target = resolvePath(cwd, outputPath);
|
|
43
90
|
assertPathAccess(authUser, target, "curl");
|
|
44
|
-
shell.writeFileAsUser(authUser, target,
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
? normalizeTerminalOutput(result.stderr)
|
|
48
|
-
: undefined,
|
|
49
|
-
exitCode: 0,
|
|
50
|
-
};
|
|
91
|
+
shell.writeFileAsUser(authUser, target, body);
|
|
92
|
+
if (!silent) stderrLines.push(` % Total % Received\n100 ${body.length} 100 ${body.length}`);
|
|
93
|
+
return { stderr: stderrLines.join("\n") || undefined, exitCode: response.ok ? 0 : 22 };
|
|
51
94
|
}
|
|
52
95
|
|
|
53
96
|
return {
|
|
54
|
-
stdout:
|
|
55
|
-
stderr:
|
|
56
|
-
|
|
57
|
-
: undefined,
|
|
58
|
-
exitCode: 0,
|
|
97
|
+
stdout: body,
|
|
98
|
+
stderr: stderrLines.length > 0 ? stderrLines.join("\n") : undefined,
|
|
99
|
+
exitCode: response.ok ? 0 : 22,
|
|
59
100
|
};
|
|
60
101
|
},
|
|
61
102
|
};
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import type { ShellModule } from "../types/commands";
|
|
2
|
+
import { ifFlag, parseArgs } from "./command-helpers";
|
|
3
|
+
import { getPackageManager } from "./helpers";
|
|
4
|
+
|
|
5
|
+
export const dpkgCommand: ShellModule = {
|
|
6
|
+
name: "dpkg",
|
|
7
|
+
description: "Debian package manager low-level tool",
|
|
8
|
+
category: "system",
|
|
9
|
+
params: ["[-l] [-s pkg] [-L pkg] [-i pkg] [--remove pkg]"],
|
|
10
|
+
run: ({ args, authUser, shell }) => {
|
|
11
|
+
const pm = getPackageManager(shell);
|
|
12
|
+
if (!pm) return { stderr: "dpkg: package manager not initialised", exitCode: 1 };
|
|
13
|
+
|
|
14
|
+
const listFlag = ifFlag(args, ["-l", "--list"]);
|
|
15
|
+
const statusFlag = ifFlag(args, ["-s", "--status"]);
|
|
16
|
+
const listFilesFlag = ifFlag(args, ["-L", "--listfiles"]);
|
|
17
|
+
const removeFlag = ifFlag(args, ["-r", "--remove"]);
|
|
18
|
+
const purgeFlag = ifFlag(args, ["-P", "--purge"]);
|
|
19
|
+
|
|
20
|
+
const { positionals } = parseArgs(args, {
|
|
21
|
+
flags: ["-l", "--list", "-s", "--status", "-L", "--listfiles", "-r", "--remove", "-P", "--purge"],
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
if (listFlag) {
|
|
25
|
+
const pkgList = pm.listInstalled();
|
|
26
|
+
if (pkgList.length === 0) {
|
|
27
|
+
return {
|
|
28
|
+
stdout: [
|
|
29
|
+
"Desired=Unknown/Install/Remove/Purge/Hold",
|
|
30
|
+
"|Status=Not/Inst/Conf-files/Unpacked/halF-conf/Half-inst/trig-aWait/Trig-pend",
|
|
31
|
+
"|/ Err?=(none)/Reinst-required (Status,Err: uppercase=bad)",
|
|
32
|
+
"||/ Name Version Architecture Description",
|
|
33
|
+
"+++-==============-===============-============-========================================",
|
|
34
|
+
"(no packages installed)",
|
|
35
|
+
].join("\n"),
|
|
36
|
+
exitCode: 0,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const header = [
|
|
41
|
+
"Desired=Unknown/Install/Remove/Purge/Hold",
|
|
42
|
+
"|Status=Not/Inst/Conf-files/Unpacked/halF-conf/Half-inst/trig-aWait/Trig-pend",
|
|
43
|
+
"|/ Err?=(none)/Reinst-required (Status,Err: uppercase=bad)",
|
|
44
|
+
"||/ Name Version Architecture Description",
|
|
45
|
+
"+++-==============-===============-============-========================================",
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
const rows = pkgList.map((p) => {
|
|
49
|
+
const name = p.name.padEnd(14).slice(0, 14);
|
|
50
|
+
const ver = p.version.padEnd(15).slice(0, 15);
|
|
51
|
+
const arch = p.architecture.padEnd(12).slice(0, 12);
|
|
52
|
+
const desc = (p.description || "").slice(0, 40);
|
|
53
|
+
return `ii ${name} ${ver} ${arch} ${desc}`;
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return { stdout: [...header, ...rows].join("\n"), exitCode: 0 };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (statusFlag) {
|
|
60
|
+
const pkgName = positionals[0];
|
|
61
|
+
if (!pkgName) return { stderr: "dpkg: -s needs a package name", exitCode: 1 };
|
|
62
|
+
const info = pm.show(pkgName);
|
|
63
|
+
if (!info)
|
|
64
|
+
return {
|
|
65
|
+
stderr: `dpkg-query: package '${pkgName}' is not installed and no information is available`,
|
|
66
|
+
exitCode: 1,
|
|
67
|
+
};
|
|
68
|
+
return { stdout: info, exitCode: 0 };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (listFilesFlag) {
|
|
72
|
+
const pkgName = positionals[0];
|
|
73
|
+
if (!pkgName) return { stderr: "dpkg: -L needs a package name", exitCode: 1 };
|
|
74
|
+
const installed = pm.listInstalled().find((p) => p.name === pkgName);
|
|
75
|
+
if (!installed)
|
|
76
|
+
return {
|
|
77
|
+
stderr: `dpkg-query: package '${pkgName}' is not installed`,
|
|
78
|
+
exitCode: 1,
|
|
79
|
+
};
|
|
80
|
+
if (installed.files.length === 0)
|
|
81
|
+
return { stdout: "/.keep", exitCode: 0 };
|
|
82
|
+
return { stdout: installed.files.join("\n"), exitCode: 0 };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (removeFlag || purgeFlag) {
|
|
86
|
+
if (authUser !== "root")
|
|
87
|
+
return {
|
|
88
|
+
stderr: "dpkg: error: requested operation requires superuser privilege",
|
|
89
|
+
exitCode: 2,
|
|
90
|
+
};
|
|
91
|
+
if (positionals.length === 0)
|
|
92
|
+
return { stderr: "dpkg: error: need an action option", exitCode: 2 };
|
|
93
|
+
const { output, exitCode } = pm.remove(positionals, { purge: purgeFlag });
|
|
94
|
+
return { stdout: output || undefined, exitCode };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Default: show help
|
|
98
|
+
return {
|
|
99
|
+
stdout: [
|
|
100
|
+
"Usage: dpkg [<option>...] <command>",
|
|
101
|
+
"",
|
|
102
|
+
"Commands:",
|
|
103
|
+
" -l, --list List packages matching given pattern",
|
|
104
|
+
" -s, --status <pkg>... Report status of specified package",
|
|
105
|
+
" -L, --listfiles <pkg>... List files owned by package",
|
|
106
|
+
" -r, --remove <pkg>... Remove <pkg> but leave its configuration",
|
|
107
|
+
" -P, --purge <pkg>... Remove <pkg> and its configuration",
|
|
108
|
+
].join("\n"),
|
|
109
|
+
exitCode: 0,
|
|
110
|
+
};
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export const dpkgQueryCommand: ShellModule = {
|
|
115
|
+
name: "dpkg-query",
|
|
116
|
+
description: "Show information about installed packages",
|
|
117
|
+
category: "system",
|
|
118
|
+
params: ["-W [pkg] | -l [pattern]"],
|
|
119
|
+
run: ({ args, shell }) => {
|
|
120
|
+
const pm = getPackageManager(shell);
|
|
121
|
+
if (!pm) return { stderr: "dpkg-query: package manager not initialised", exitCode: 1 };
|
|
122
|
+
|
|
123
|
+
const listFlag = ifFlag(args, ["-l"]);
|
|
124
|
+
const showFlag = ifFlag(args, ["-W", "--show"]);
|
|
125
|
+
const { positionals } = parseArgs(args, {
|
|
126
|
+
flags: ["-l", "-W", "--show"],
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
if (listFlag || showFlag) {
|
|
130
|
+
const pkgList = pm.listInstalled();
|
|
131
|
+
const pattern = positionals[0];
|
|
132
|
+
const filtered = pattern
|
|
133
|
+
? pkgList.filter((p) => p.name.includes(pattern))
|
|
134
|
+
: pkgList;
|
|
135
|
+
|
|
136
|
+
if (showFlag) {
|
|
137
|
+
return {
|
|
138
|
+
stdout: filtered
|
|
139
|
+
.map((p) => `${p.name}\t${p.version}`)
|
|
140
|
+
.join("\n"),
|
|
141
|
+
exitCode: 0,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const rows = filtered.map((p) => {
|
|
146
|
+
const name = p.name.padEnd(14).slice(0, 14);
|
|
147
|
+
const ver = p.version.padEnd(15).slice(0, 15);
|
|
148
|
+
return `ii ${name} ${ver} amd64 ${(p.description || "").slice(0, 40)}`;
|
|
149
|
+
});
|
|
150
|
+
return {
|
|
151
|
+
stdout: rows.join("\n") || "(no packages match)",
|
|
152
|
+
exitCode: 0,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return { stderr: "dpkg-query: need a flag (-l, -W)", exitCode: 1 };
|
|
157
|
+
},
|
|
158
|
+
};
|
package/src/commands/echo.ts
CHANGED
|
@@ -1,28 +1,44 @@
|
|
|
1
1
|
import type { ShellModule } from "../types/commands";
|
|
2
2
|
import { parseArgs } from "./command-helpers";
|
|
3
|
-
import { getAllEnvVars } from "./set";
|
|
4
3
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Expand escape sequences for `echo -e`.
|
|
6
|
+
* Handles \n \t \r \\ \a \b \f \v and \0NNN (octal).
|
|
7
|
+
*/
|
|
8
|
+
function expandEscapes(text: string): string {
|
|
9
|
+
return text
|
|
10
|
+
.replace(/\\n/g, "\n")
|
|
11
|
+
.replace(/\\t/g, "\t")
|
|
12
|
+
.replace(/\\r/g, "\r")
|
|
13
|
+
.replace(/\\\\/g, "\\")
|
|
14
|
+
.replace(/\\a/g, "\x07")
|
|
15
|
+
.replace(/\\b/g, "\x08")
|
|
16
|
+
.replace(/\\f/g, "\x0C")
|
|
17
|
+
.replace(/\\v/g, "\x0B")
|
|
18
|
+
.replace(/\\0(\d{1,3})/g, (_, oct) => String.fromCharCode(parseInt(oct, 8)));
|
|
9
19
|
}
|
|
10
20
|
|
|
11
21
|
export const echoCommand: ShellModule = {
|
|
12
22
|
name: "echo",
|
|
13
23
|
description: "Display text",
|
|
14
24
|
category: "shell",
|
|
15
|
-
params: ["[
|
|
16
|
-
run: ({ args,
|
|
17
|
-
const { flags, positionals } = parseArgs(args, { flags: ["-n"] });
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
25
|
+
params: ["[-n] [-e] [text...]"],
|
|
26
|
+
run: ({ args, stdin, env }) => {
|
|
27
|
+
const { flags, positionals } = parseArgs(args, { flags: ["-n", "-e", "-E"] });
|
|
28
|
+
const noNewline = flags.has("-n");
|
|
29
|
+
const escapes = flags.has("-e");
|
|
30
|
+
|
|
31
|
+
const rawText = positionals.length > 0 ? positionals.join(" ") : (stdin ?? "");
|
|
32
|
+
|
|
33
|
+
// Expand $VAR references using the session env (not the legacy global store)
|
|
34
|
+
const varsExpanded = rawText.replace(/\$([A-Za-z_][A-Za-z0-9_]*)/g, (_, name: string) =>
|
|
35
|
+
env?.vars[name] ?? "",
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const text = escapes ? expandEscapes(varsExpanded) : varsExpanded;
|
|
23
39
|
|
|
24
40
|
return {
|
|
25
|
-
stdout:
|
|
41
|
+
stdout: noNewline ? text : `${text}\n`,
|
|
26
42
|
exitCode: 0,
|
|
27
43
|
};
|
|
28
44
|
},
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as os from "node:os";
|
|
2
|
+
import type { ShellModule } from "../types/commands";
|
|
3
|
+
import { ifFlag } from "./command-helpers";
|
|
4
|
+
|
|
5
|
+
export const freeCommand: ShellModule = {
|
|
6
|
+
name: "free",
|
|
7
|
+
description: "Display amount of free and used memory",
|
|
8
|
+
category: "system",
|
|
9
|
+
params: ["[-h] [-m] [-g]"],
|
|
10
|
+
run: ({ args }) => {
|
|
11
|
+
const human = ifFlag(args, ["-h", "--human"]);
|
|
12
|
+
const mb = ifFlag(args, ["-m"]);
|
|
13
|
+
const gb = ifFlag(args, ["-g"]);
|
|
14
|
+
|
|
15
|
+
const osTotalB = os.totalmem();
|
|
16
|
+
const osFreeB = os.freemem();
|
|
17
|
+
const usedB = osTotalB - osFreeB;
|
|
18
|
+
const sharedB = Math.floor(osTotalB * 0.02);
|
|
19
|
+
const buffersB = Math.floor(osTotalB * 0.05);
|
|
20
|
+
const availableB = Math.floor(osFreeB * 0.95);
|
|
21
|
+
const swapB = Math.floor(osTotalB * 0.5);
|
|
22
|
+
|
|
23
|
+
const fmt = (bytes: number): string => {
|
|
24
|
+
if (human) {
|
|
25
|
+
if (bytes >= 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)}G`;
|
|
26
|
+
if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)}M`;
|
|
27
|
+
return `${(bytes / 1024).toFixed(1)}K`;
|
|
28
|
+
}
|
|
29
|
+
if (gb) return String(Math.floor(bytes / (1024 * 1024 * 1024)));
|
|
30
|
+
if (mb) return String(Math.floor(bytes / (1024 * 1024)));
|
|
31
|
+
return String(Math.floor(bytes / 1024));
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const header = ` total used free shared buff/cache available`;
|
|
35
|
+
const memRow = `Mem: ${fmt(osTotalB).padStart(12)} ${fmt(usedB).padStart(11)} ${fmt(osFreeB).padStart(11)} ${fmt(sharedB).padStart(11)} ${fmt(buffersB).padStart(11)} ${fmt(availableB).padStart(11)}`;
|
|
36
|
+
const swapRow = `Swap: ${fmt(swapB).padStart(12)} ${fmt(0).padStart(11)} ${fmt(swapB).padStart(11)}`;
|
|
37
|
+
|
|
38
|
+
return { stdout: [header, memRow, swapRow].join("\n"), exitCode: 0 };
|
|
39
|
+
},
|
|
40
|
+
};
|
package/src/commands/helpers.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import type VirtualFileSystem from "../VirtualFileSystem";
|
|
4
|
+
import type { VirtualPackageManager } from "../VirtualPackageManager";
|
|
5
|
+
import type { VirtualShell } from "../VirtualShell";
|
|
4
6
|
|
|
5
7
|
const PROTECTED_PREFIXES = ["/virtual-env-js/.auth"] as const;
|
|
6
8
|
|
|
@@ -220,3 +222,9 @@ export function joinListWithType(
|
|
|
220
222
|
})
|
|
221
223
|
.join(" ");
|
|
222
224
|
}
|
|
225
|
+
|
|
226
|
+
export function getPackageManager(
|
|
227
|
+
shell: VirtualShell,
|
|
228
|
+
): VirtualPackageManager | undefined {
|
|
229
|
+
return shell.packageManager;
|
|
230
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ShellModule } from "../types/commands";
|
|
2
|
+
|
|
3
|
+
export const historyCommand: ShellModule = {
|
|
4
|
+
name: "history",
|
|
5
|
+
description: "Display command history",
|
|
6
|
+
category: "shell",
|
|
7
|
+
params: ["[n]"],
|
|
8
|
+
run: ({ args, shell }) => {
|
|
9
|
+
// History is persisted in the VFS by the interactive shell
|
|
10
|
+
const histPath = "/virtual-env-js/.bash_history";
|
|
11
|
+
if (!shell.vfs.exists(histPath)) {
|
|
12
|
+
return { stdout: "", exitCode: 0 };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const raw = shell.vfs.readFile(histPath);
|
|
16
|
+
const lines = raw.split("\n").filter(Boolean);
|
|
17
|
+
|
|
18
|
+
const nArg = args[0];
|
|
19
|
+
const n = nArg ? parseInt(nArg, 10) : null;
|
|
20
|
+
const slice = n && !Number.isNaN(n) ? lines.slice(-n) : lines;
|
|
21
|
+
|
|
22
|
+
const offset = lines.length - slice.length + 1;
|
|
23
|
+
const numbered = slice.map((line, i) =>
|
|
24
|
+
`${String(offset + i).padStart(5)} ${line}`
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
return { stdout: numbered.join("\n"), exitCode: 0 };
|
|
28
|
+
},
|
|
29
|
+
};
|