typescript-virtual-container 1.5.4 → 1.5.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.
Files changed (40) hide show
  1. package/README.md +107 -541
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/VirtualShell/shell.js +158 -11
  4. package/dist/commands/awk.d.ts +6 -11
  5. package/dist/commands/awk.js +462 -109
  6. package/dist/commands/bc.d.ts +2 -0
  7. package/dist/commands/bc.js +28 -0
  8. package/dist/commands/bzip2.d.ts +11 -0
  9. package/dist/commands/bzip2.js +91 -0
  10. package/dist/commands/find.d.ts +2 -2
  11. package/dist/commands/find.js +212 -37
  12. package/dist/commands/{ifconfig.d.ts → ip.d.ts} +1 -1
  13. package/dist/commands/{ifconfig.js → ip.js} +3 -3
  14. package/dist/commands/jobs.d.ts +4 -0
  15. package/dist/commands/jobs.js +27 -0
  16. package/dist/commands/lsof.d.ts +6 -0
  17. package/dist/commands/lsof.js +30 -0
  18. package/dist/commands/perl.d.ts +6 -0
  19. package/dist/commands/perl.js +76 -0
  20. package/dist/commands/registry.js +22 -3
  21. package/dist/commands/runtime.js +2 -1
  22. package/dist/commands/sed.d.ts +2 -2
  23. package/dist/commands/sed.js +216 -34
  24. package/dist/commands/set.js +20 -0
  25. package/dist/commands/sh.js +111 -1
  26. package/dist/commands/strace.d.ts +6 -0
  27. package/dist/commands/strace.js +26 -0
  28. package/dist/commands/tar.d.ts +2 -1
  29. package/dist/commands/tar.js +138 -52
  30. package/dist/commands/zip.d.ts +11 -0
  31. package/dist/commands/zip.js +232 -0
  32. package/dist/modules/linuxRootfs.js +1 -1
  33. package/dist/utils/expand.js +67 -1
  34. package/package.json +5 -4
  35. package/dist/self-standalone.d.ts +0 -1
  36. package/dist/self-standalone.js +0 -444
  37. package/dist/standalone-wo-sftp.d.ts +0 -1
  38. package/dist/standalone-wo-sftp.js +0 -30
  39. package/dist/standalone.d.ts +0 -1
  40. package/dist/standalone.js +0 -61
@@ -1,7 +1,7 @@
1
1
  import type { ShellModule } from "../types/commands";
2
2
  /**
3
- * Stream editor for filtering and transforming text lines.
3
+ * Stream editor supports s/pat/rep/[gI], d, p, q, =, addresses (N, /re/, N,M, /re/,/re/, $).
4
4
  * @category text
5
- * @params ["-e <expr> [file]", "s/pattern/replace/[g]"]
5
+ * @params ["[-n] [-e <expr>] [file]"]
6
6
  */
7
7
  export declare const sedCommand: ShellModule;
@@ -1,22 +1,74 @@
1
- import { getFlag, ifFlag } from "./command-helpers";
1
+ import { ifFlag } from "./command-helpers";
2
2
  import { resolvePath } from "./helpers";
3
3
  /**
4
- * Stream editor for filtering and transforming text lines.
4
+ * Stream editor supports s/pat/rep/[gI], d, p, q, =, addresses (N, /re/, N,M, /re/,/re/, $).
5
5
  * @category text
6
- * @params ["-e <expr> [file]", "s/pattern/replace/[g]"]
6
+ * @params ["[-n] [-e <expr>] [file]"]
7
7
  */
8
8
  export const sedCommand = {
9
9
  name: "sed",
10
10
  description: "Stream editor for filtering and transforming text",
11
11
  category: "text",
12
- params: ["-e <expr> [file]", "s/pattern/replace/[g]"],
12
+ params: ["[-n] [-e <expr>] [file]"],
13
13
  run: ({ authUser, shell, cwd, args, stdin }) => {
14
14
  const inPlace = ifFlag(args, ["-i"]);
15
- const expr = getFlag(args, ["-e"]) ??
16
- args.find((a) => !a.startsWith("-"));
17
- const fileArg = args.filter((a) => !a.startsWith("-") && a !== expr).pop();
18
- if (!expr)
15
+ const suppressAuto = ifFlag(args, ["-n"]);
16
+ // Collect all -e expressions and the first non-flag positional
17
+ const exprs = [];
18
+ let fileArg;
19
+ let i = 0;
20
+ while (i < args.length) {
21
+ const a = args[i];
22
+ if (a === "-e" || a === "--expression") {
23
+ i++;
24
+ if (args[i])
25
+ exprs.push(args[i]);
26
+ i++;
27
+ }
28
+ else if (a === "-n" || a === "-i") {
29
+ i++;
30
+ }
31
+ else if (a.startsWith("-e")) {
32
+ exprs.push(a.slice(2));
33
+ i++;
34
+ }
35
+ else if (!a.startsWith("-")) {
36
+ if (exprs.length === 0)
37
+ exprs.push(a);
38
+ else
39
+ fileArg = a;
40
+ i++;
41
+ }
42
+ else {
43
+ i++;
44
+ }
45
+ }
46
+ // If only one positional collected as expr and no file yet, check for file after
47
+ // Re-parse: first non-flag that follows all -e is the file
48
+ if (exprs.length === 0)
19
49
  return { stderr: "sed: no expression", exitCode: 1 };
50
+ // Re-check: if exprs[0] was set from positional, remaining positionals are files
51
+ {
52
+ let foundExprFromFlag = false;
53
+ let j = 0;
54
+ while (j < args.length) {
55
+ const a = args[j];
56
+ if (a === "-e" || a === "--expression") {
57
+ foundExprFromFlag = true;
58
+ j += 2;
59
+ }
60
+ else if (a.startsWith("-e")) {
61
+ foundExprFromFlag = true;
62
+ j++;
63
+ }
64
+ else
65
+ j++;
66
+ }
67
+ if (!foundExprFromFlag) {
68
+ // expr is first positional, file is second
69
+ fileArg = args.filter((a) => !a.startsWith("-")).slice(1)[0];
70
+ }
71
+ }
20
72
  let content = stdin ?? "";
21
73
  if (fileArg) {
22
74
  const p = resolvePath(cwd, fileArg);
@@ -24,32 +76,162 @@ export const sedCommand = {
24
76
  content = shell.vfs.readFile(p);
25
77
  }
26
78
  catch {
27
- return {
28
- stderr: `sed: ${fileArg}: No such file or directory`,
29
- exitCode: 1,
30
- };
31
- }
32
- }
33
- // Parse s/from/to/[g]
34
- const sMatch = expr.match(/^s([^a-zA-Z0-9])(.+?)\1(.*?)\1([gi]*)$/);
35
- if (!sMatch)
36
- return { stderr: `sed: unrecognized command: ${expr}`, exitCode: 1 };
37
- const [, , from, to, flags] = sMatch;
38
- const regexFlags = (flags ?? "").includes("i")
39
- ? "gi"
40
- : (flags ?? "").includes("g")
41
- ? "g"
42
- : "";
43
- let regex;
44
- try {
45
- regex = new RegExp(from, regexFlags || "");
46
- }
47
- catch (_e) {
48
- return { stderr: `sed: invalid regex: ${from}`, exitCode: 1 };
49
- }
50
- const result = (flags ?? "").includes("g") || regexFlags.includes("g")
51
- ? content.replace(regex, to ?? "")
52
- : content.replace(regex, to ?? "");
79
+ return { stderr: `sed: ${fileArg}: No such file or directory`, exitCode: 1 };
80
+ }
81
+ }
82
+ function parseAddr(s) {
83
+ if (!s)
84
+ return [undefined, s];
85
+ if (s[0] === "$")
86
+ return [{ type: "last" }, s.slice(1)];
87
+ if (/^\d/.test(s)) {
88
+ const m = s.match(/^(\d+)(.*)/s);
89
+ if (m)
90
+ return [{ type: "line", n: parseInt(m[1], 10) }, m[2]];
91
+ }
92
+ if (s[0] === "/") {
93
+ const end = s.indexOf("/", 1);
94
+ if (end !== -1) {
95
+ try {
96
+ const re = new RegExp(s.slice(1, end));
97
+ return [{ type: "regex", re }, s.slice(end + 1)];
98
+ }
99
+ catch { /* bad regex */ }
100
+ }
101
+ }
102
+ return [undefined, s];
103
+ }
104
+ function parseInstrs(expr) {
105
+ const instrs = [];
106
+ // Split on unquoted semicolons or newlines
107
+ const parts = expr.split(/\n|(?<=^|[^\\]);/);
108
+ for (const raw of parts) {
109
+ const part = raw.trim();
110
+ if (!part || part.startsWith("#"))
111
+ continue;
112
+ let rest = part;
113
+ const [addr1, after1] = parseAddr(rest);
114
+ rest = after1.trim();
115
+ let addr2;
116
+ if (rest[0] === ",") {
117
+ rest = rest.slice(1).trim();
118
+ const [a2, after2] = parseAddr(rest);
119
+ addr2 = a2;
120
+ rest = after2.trim();
121
+ }
122
+ const op = rest[0];
123
+ if (!op)
124
+ continue;
125
+ if (op === "s") {
126
+ // s/from/to/flags
127
+ const delim = rest[1] ?? "/";
128
+ const sRe = new RegExp(`^s${re(delim)}((?:[^${re(delim)}\\\\]|\\\\.)*)${re(delim)}((?:[^${re(delim)}\\\\]|\\\\.)*)${re(delim)}([gGiIp]*)$`);
129
+ const m = rest.match(sRe);
130
+ if (!m) {
131
+ instrs.push({ op: "d", addr1, addr2 });
132
+ continue;
133
+ } // bad expr, skip
134
+ const flags = m[3] ?? "";
135
+ let from;
136
+ try {
137
+ from = new RegExp(m[1], flags.includes("i") || flags.includes("I") ? "i" : "");
138
+ }
139
+ catch {
140
+ continue;
141
+ }
142
+ instrs.push({ op: "s", addr1, addr2, from, to: m[2], global: flags.includes("g") || flags.includes("G"), print: flags.includes("p") });
143
+ }
144
+ else if (op === "d") {
145
+ instrs.push({ op: "d", addr1, addr2 });
146
+ }
147
+ else if (op === "p") {
148
+ instrs.push({ op: "p", addr1, addr2 });
149
+ }
150
+ else if (op === "q") {
151
+ instrs.push({ op: "q", addr1 });
152
+ }
153
+ else if (op === "=") {
154
+ instrs.push({ op: "=", addr1, addr2 });
155
+ }
156
+ }
157
+ return instrs;
158
+ }
159
+ function re(c) {
160
+ return c.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
161
+ }
162
+ const allInstrs = exprs.flatMap(parseInstrs);
163
+ const lines = content.split("\n");
164
+ // Remove trailing empty string from trailing newline
165
+ if (lines[lines.length - 1] === "")
166
+ lines.pop();
167
+ const total = lines.length;
168
+ function matchesAddr(addr, lineNo, line) {
169
+ if (!addr)
170
+ return true;
171
+ if (addr.type === "line")
172
+ return lineNo === addr.n;
173
+ if (addr.type === "last")
174
+ return lineNo === total;
175
+ return addr.re.test(line);
176
+ }
177
+ function inRange(instr, lineNo, line, rangeActive) {
178
+ const { addr1, addr2 } = instr;
179
+ if (!addr1)
180
+ return true;
181
+ if (!addr2)
182
+ return matchesAddr(addr1, lineNo, line);
183
+ // Two-address range
184
+ let active = rangeActive.get(instr) ?? false;
185
+ if (!active && matchesAddr(addr1, lineNo, line)) {
186
+ active = true;
187
+ rangeActive.set(instr, true);
188
+ }
189
+ if (active && matchesAddr(addr2, lineNo, line)) {
190
+ rangeActive.set(instr, false);
191
+ return true;
192
+ }
193
+ if (active)
194
+ return true;
195
+ return false;
196
+ }
197
+ const out = [];
198
+ const rangeActive = new Map();
199
+ let quit = false;
200
+ for (let li = 0; li < lines.length && !quit; li++) {
201
+ let line = lines[li];
202
+ const lineNo = li + 1;
203
+ let deleted = false;
204
+ for (const instr of allInstrs) {
205
+ if (!inRange(instr, lineNo, line, rangeActive))
206
+ continue;
207
+ if (instr.op === "d") {
208
+ deleted = true;
209
+ break;
210
+ }
211
+ if (instr.op === "p") {
212
+ out.push(line);
213
+ }
214
+ if (instr.op === "=") {
215
+ out.push(String(lineNo));
216
+ }
217
+ if (instr.op === "q") {
218
+ quit = true;
219
+ }
220
+ if (instr.op === "s") {
221
+ const replaced = instr.global
222
+ ? line.replace(new RegExp(instr.from.source, instr.from.flags.includes("i") ? "gi" : "g"), instr.to)
223
+ : line.replace(instr.from, instr.to);
224
+ if (replaced !== line) {
225
+ line = replaced;
226
+ if (instr.print && suppressAuto)
227
+ out.push(line);
228
+ }
229
+ }
230
+ }
231
+ if (!deleted && !suppressAuto)
232
+ out.push(line);
233
+ }
234
+ const result = out.join("\n") + (out.length > 0 ? "\n" : "");
53
235
  if (inPlace && fileArg) {
54
236
  const p = resolvePath(cwd, fileArg);
55
237
  shell.writeFileAsUser(authUser, p, result);
@@ -11,11 +11,31 @@ export const setCommand = {
11
11
  run: ({ args, env }) => {
12
12
  if (args.length === 0) {
13
13
  const out = Object.entries(env.vars)
14
+ .filter(([k]) => !k.startsWith("__"))
14
15
  .map(([k, v]) => `${k}=${v}`)
15
16
  .join("\n");
16
17
  return { stdout: out, exitCode: 0 };
17
18
  }
18
19
  for (const arg of args) {
20
+ const flagMatch = arg.match(/^([+-])([a-zA-Z]+)$/);
21
+ if (flagMatch) {
22
+ const on = flagMatch[1] === "-";
23
+ for (const flag of flagMatch[2]) {
24
+ if (flag === "e") {
25
+ if (on)
26
+ env.vars.__errexit = "1";
27
+ else
28
+ delete env.vars.__errexit;
29
+ }
30
+ if (flag === "x") {
31
+ if (on)
32
+ env.vars.__xtrace = "1";
33
+ else
34
+ delete env.vars.__xtrace;
35
+ }
36
+ }
37
+ continue;
38
+ }
19
39
  if (arg.includes("=")) {
20
40
  const eq = arg.indexOf("=");
21
41
  env.vars[arg.slice(0, eq)] = arg.slice(eq + 1);
@@ -125,6 +125,69 @@ function parseBlocks(lines) {
125
125
  }
126
126
  blocks.push({ type: "while", cond, body });
127
127
  }
128
+ else if (line.startsWith("until ")) {
129
+ const cond = line
130
+ .replace(/^until\s+/, "")
131
+ .replace(/;\s*do\s*$/, "")
132
+ .trim();
133
+ const body = [];
134
+ i++;
135
+ while (i < lines.length && lines[i]?.trim() !== "done") {
136
+ const l = lines[i].trim().replace(/^do\s+/, "");
137
+ if (l && l !== "do")
138
+ body.push(l);
139
+ i++;
140
+ }
141
+ blocks.push({ type: "until", cond, body });
142
+ }
143
+ else if (/^[A-Za-z_][A-Za-z0-9_]*=\s*\(/.test(line)) {
144
+ // Array assignment: arr=(elem1 elem2 ...)
145
+ const arrMatch = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=\s*\(([^)]*)\)$/);
146
+ if (arrMatch) {
147
+ const elems = arrMatch[2].trim().split(/\s+/).filter(Boolean);
148
+ blocks.push({ type: "array", name: arrMatch[1], elements: elems });
149
+ }
150
+ else {
151
+ blocks.push({ type: "cmd", line });
152
+ }
153
+ }
154
+ else if (line.startsWith("case ") && line.endsWith(" in") || line.match(/^case\s+.+\s+in$/)) {
155
+ const caseExpr = line.replace(/^case\s+/, "").replace(/\s+in$/, "").trim();
156
+ const patterns = [];
157
+ i++;
158
+ while (i < lines.length && lines[i]?.trim() !== "esac") {
159
+ const pl = lines[i].trim();
160
+ if (!pl || pl === "esac") {
161
+ i++;
162
+ continue;
163
+ }
164
+ // pattern) or pattern1|pattern2)
165
+ const patMatch = pl.match(/^(.+?)\)\s*(.*)$/);
166
+ if (patMatch) {
167
+ const pat = patMatch[1].trim();
168
+ const body = [];
169
+ if (patMatch[2]?.trim() && patMatch[2].trim() !== ";;") {
170
+ body.push(patMatch[2].trim());
171
+ }
172
+ i++;
173
+ while (i < lines.length) {
174
+ const bl = lines[i].trim();
175
+ if (bl === ";;" || bl === "esac")
176
+ break;
177
+ if (bl)
178
+ body.push(bl);
179
+ i++;
180
+ }
181
+ if (lines[i]?.trim() === ";;")
182
+ i++; // skip ;;
183
+ patterns.push({ pattern: pat, body });
184
+ }
185
+ else {
186
+ i++;
187
+ }
188
+ }
189
+ blocks.push({ type: "case", expr: caseExpr, patterns });
190
+ }
128
191
  else {
129
192
  blocks.push({ type: "cmd", line });
130
193
  }
@@ -189,9 +252,12 @@ async function evalCondition(cond, ctx) {
189
252
  async function runBlocks(blocks, ctx) {
190
253
  let lastResult = { exitCode: 0 };
191
254
  let output = "";
255
+ let traceOutput = "";
192
256
  for (const block of blocks) {
193
257
  if (block.type === "cmd") {
194
258
  const expanded = await expandVars(block.line, ctx.env.vars, ctx.env.lastExitCode, ctx);
259
+ if (ctx.env.vars.__xtrace)
260
+ traceOutput += `+ ${expanded}\n`;
195
261
  // Bare VAR=val assignment(s) — handle before dispatching to runCommand
196
262
  const assignRe = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)/;
197
263
  const tokens = expanded.trim().split(/\s+/);
@@ -231,6 +297,8 @@ async function runBlocks(blocks, ctx) {
231
297
  output += `${r.stdout}\n`;
232
298
  if (r.stderr)
233
299
  return { ...r, stdout: output.trim() };
300
+ if (ctx.env.vars.__errexit && (r.exitCode ?? 0) !== 0)
301
+ return { ...r, stdout: output.trim() };
234
302
  lastResult = r;
235
303
  }
236
304
  else if (block.type === "if") {
@@ -311,8 +379,50 @@ async function runBlocks(blocks, ctx) {
311
379
  iterations++;
312
380
  }
313
381
  }
382
+ else if (block.type === "until") {
383
+ let iterations = 0;
384
+ while (iterations < 1000 && !(await evalCondition(block.cond, ctx))) {
385
+ const sub = await runBlocks(parseBlocks(block.body), ctx);
386
+ if (sub.stdout)
387
+ output += `${sub.stdout}\n`;
388
+ if (sub.closeSession)
389
+ return sub;
390
+ iterations++;
391
+ }
392
+ }
393
+ else if (block.type === "array") {
394
+ // Store array: arr[0]=e0, arr[1]=e1, ..., arr=space-joined (for ${arr[@]})
395
+ block.elements.forEach((el, idx) => { ctx.env.vars[`${block.name}[${idx}]`] = el; });
396
+ ctx.env.vars[block.name] = block.elements.join(" ");
397
+ }
398
+ else if (block.type === "case") {
399
+ const expanded = await expandVars(block.expr, ctx.env.vars, ctx.env.lastExitCode, ctx);
400
+ for (const pat of block.patterns) {
401
+ const alts = pat.pattern.split("|").map((p) => p.trim());
402
+ const matched = alts.some((p) => {
403
+ if (p === "*")
404
+ return true;
405
+ if (p.includes("*") || p.includes("?")) {
406
+ const re = new RegExp(`^${p.replace(/\./g, "\\.").replace(/\*/g, ".*").replace(/\?/g, ".")}$`);
407
+ return re.test(expanded);
408
+ }
409
+ return p === expanded;
410
+ });
411
+ if (matched) {
412
+ const sub = await runBlocks(parseBlocks(pat.body), ctx);
413
+ if (sub.stdout)
414
+ output += `${sub.stdout}\n`;
415
+ break;
416
+ }
417
+ }
418
+ }
419
+ }
420
+ const finalStdout = output.trim() || lastResult.stdout;
421
+ if (traceOutput) {
422
+ const traceStderr = (lastResult.stderr ? `${lastResult.stderr}\n` : "") + traceOutput.trim();
423
+ return { ...lastResult, stdout: finalStdout, stderr: traceStderr || lastResult.stderr };
314
424
  }
315
- return { ...lastResult, stdout: output.trim() || lastResult.stdout };
425
+ return { ...lastResult, stdout: finalStdout };
316
426
  }
317
427
  /**
318
428
  * Execute shell scripts or commands with a minimal shell interpreter.
@@ -0,0 +1,6 @@
1
+ import type { ShellModule } from "../types/commands";
2
+ /**
3
+ * Trace system calls and signals (stub — runs command, emits fake strace output).
4
+ * @category system
5
+ */
6
+ export declare const straceCommand: ShellModule;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Trace system calls and signals (stub — runs command, emits fake strace output).
3
+ * @category system
4
+ */
5
+ export const straceCommand = {
6
+ name: "strace",
7
+ description: "Trace system calls and signals",
8
+ category: "system",
9
+ params: ["[-e <expr>] [-o <file>] <command> [args]"],
10
+ run: ({ args }) => {
11
+ const cmd = args.find((a) => !a.startsWith("-"));
12
+ if (!cmd)
13
+ return { stderr: "strace: must have PROG [ARGS] or -p PID", exitCode: 1 };
14
+ const pid = Math.floor(Math.random() * 30000) + 1000;
15
+ const lines = [
16
+ `execve("/usr/bin/${cmd}", ["${cmd}"${args.slice(1).map((a) => `, "${a}"`).join("")}], 0x... /* ... vars */) = 0`,
17
+ `brk(NULL) = 0x${(Math.random() * 0xfffff | 0).toString(16)}000`,
18
+ `access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)`,
19
+ `openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3`,
20
+ `fstat(3, {st_mode=S_IFREG|0644, st_size=...}) = 0`,
21
+ `close(3) = 0`,
22
+ `+++ exited with 0 +++`,
23
+ ];
24
+ return { stderr: lines.join("\n"), exitCode: 0 };
25
+ },
26
+ };
@@ -1,6 +1,7 @@
1
1
  import type { ShellModule } from "../types/commands";
2
2
  /**
3
- * Archive or extract files with tar and optional gzip compression.
3
+ * Archive or extract files with tar writes real POSIX ustar binary format.
4
+ * Supports -c/-x/-t, -z (gzip), -j (bzip2 stub), -v (verbose), -f.
4
5
  * @category archive
5
6
  * @params ["[-czf|-xzf|-tf] <archive> [files...]"]
6
7
  */