typescript-virtual-container 1.4.1 → 1.4.3
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/.vscode/settings.json +2 -0
- package/README.md +77 -36
- package/benchmark-virtualshell.ts +3 -11
- package/builds/self-standalone.js +224 -224
- package/builds/self-standalone.js.map +4 -4
- package/builds/standalone-wo-sftp.js +23 -23
- package/builds/standalone-wo-sftp.js.map +3 -3
- package/builds/standalone.js +23 -23
- package/builds/standalone.js.map +3 -3
- package/dist/VirtualFileSystem/index.d.ts +47 -0
- package/dist/VirtualFileSystem/index.d.ts.map +1 -1
- package/dist/VirtualFileSystem/index.js +159 -0
- package/dist/VirtualShell/index.d.ts +29 -0
- package/dist/VirtualShell/index.d.ts.map +1 -1
- package/dist/VirtualShell/index.js +29 -0
- package/dist/VirtualShell/shell.d.ts.map +1 -1
- package/dist/VirtualShell/shell.js +6 -10
- package/dist/VirtualShell/shellParser.js +28 -1
- package/dist/VirtualUserManager/index.d.ts.map +1 -1
- package/dist/VirtualUserManager/index.js +4 -4
- package/dist/commands/export.d.ts.map +1 -1
- package/dist/commands/export.js +5 -3
- package/dist/commands/helpers.js +1 -1
- package/dist/commands/history.js +2 -2
- package/dist/commands/registry.d.ts.map +1 -1
- package/dist/commands/registry.js +2 -0
- package/dist/commands/runtime.d.ts.map +1 -1
- package/dist/commands/runtime.js +28 -3
- package/dist/commands/seq.d.ts +4 -0
- package/dist/commands/seq.d.ts.map +1 -0
- package/dist/commands/seq.js +50 -0
- package/dist/commands/sh.d.ts +0 -6
- package/dist/commands/sh.d.ts.map +1 -1
- package/dist/commands/sh.js +153 -10
- package/dist/modules/linuxRootfs.d.ts +1 -1
- package/dist/modules/linuxRootfs.d.ts.map +1 -1
- package/dist/modules/linuxRootfs.js +5 -5
- package/dist/self-standalone.js +149 -102
- package/dist/types/pipeline.d.ts +6 -0
- package/dist/types/pipeline.d.ts.map +1 -1
- package/dist/types/vfs.d.ts +15 -0
- package/dist/types/vfs.d.ts.map +1 -1
- package/dist/utils/expand.d.ts +9 -0
- package/dist/utils/expand.d.ts.map +1 -1
- package/dist/utils/expand.js +84 -2
- package/dist/utils/tokenize.d.ts.map +1 -1
- package/dist/utils/tokenize.js +40 -0
- package/package.json +1 -1
- package/src/VirtualFileSystem/index.ts +164 -1
- package/src/VirtualShell/index.ts +36 -0
- package/src/VirtualShell/shell.ts +6 -11
- package/src/VirtualShell/shellParser.ts +26 -1
- package/src/VirtualUserManager/index.ts +4 -4
- package/src/commands/export.ts +5 -3
- package/src/commands/helpers.ts +1 -1
- package/src/commands/history.ts +2 -2
- package/src/commands/registry.ts +2 -0
- package/src/commands/runtime.ts +30 -3
- package/src/commands/seq.ts +43 -0
- package/src/commands/sh.ts +144 -19
- package/src/modules/linuxRootfs.ts +6 -6
- package/src/self-standalone.ts +190 -141
- package/src/types/pipeline.ts +6 -0
- package/src/types/vfs.ts +17 -0
- package/src/utils/expand.ts +75 -2
- package/src/utils/tokenize.ts +20 -0
- package/tests/helpers.test.ts +3 -3
package/src/commands/sh.ts
CHANGED
|
@@ -45,6 +45,8 @@ type Block =
|
|
|
45
45
|
}
|
|
46
46
|
| { type: "for"; var: string; list: string; body: string[] }
|
|
47
47
|
| { type: "while"; cond: string; body: string[] }
|
|
48
|
+
| { type: "func"; name: string; body: string[] }
|
|
49
|
+
| { type: "arith"; expr: string }
|
|
48
50
|
| { type: "cmd"; line: string };
|
|
49
51
|
|
|
50
52
|
/** Very small shell interpreter: supports if/elif/else/fi, for/do/done, while/do/done */
|
|
@@ -58,6 +60,41 @@ function parseBlocks(lines: string[]): Block[] {
|
|
|
58
60
|
continue;
|
|
59
61
|
}
|
|
60
62
|
|
|
63
|
+
// Function definition: name() { or function name { or name() { body }
|
|
64
|
+
const funcMatchInline = line.match(/^(?:function\s+)?(\w+)\s*\(\s*\)\s*\{(.+)\}\s*$/);
|
|
65
|
+
const funcMatch = funcMatchInline ?? (
|
|
66
|
+
line.match(/^(?:function\s+)?(\w+)\s*\(\s*\)\s*\{?\s*$/) ||
|
|
67
|
+
line.match(/^function\s+(\w+)\s*\{?\s*$/)
|
|
68
|
+
);
|
|
69
|
+
if (funcMatch) {
|
|
70
|
+
const funcName = funcMatch[1]!;
|
|
71
|
+
const body: string[] = [];
|
|
72
|
+
// Inline: name() { cmd; } — single-line form
|
|
73
|
+
if (funcMatchInline) {
|
|
74
|
+
body.push(...funcMatchInline[2]!.split(";").map((s: string) => s.trim()).filter(Boolean));
|
|
75
|
+
blocks.push({ type: "func", name: funcName, body });
|
|
76
|
+
i++;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
i++;
|
|
80
|
+
while (i < lines.length && lines[i]?.trim() !== "}" && i < lines.length + 1) {
|
|
81
|
+
const l = lines[i]!.trim().replace(/^do\s+/, "");
|
|
82
|
+
if (l && l !== "{") body.push(l);
|
|
83
|
+
i++;
|
|
84
|
+
}
|
|
85
|
+
i++; // skip closing }
|
|
86
|
+
blocks.push({ type: "func", name: funcName, body });
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// (( expr )) arithmetic statement
|
|
91
|
+
const arithMatch = line.match(/^\(\(\s*(.+?)\s*\)\)$/);
|
|
92
|
+
if (arithMatch) {
|
|
93
|
+
blocks.push({ type: "arith", expr: arithMatch[1]! });
|
|
94
|
+
i++;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
61
98
|
if (line.startsWith("if ") || line === "if") {
|
|
62
99
|
const cond = line
|
|
63
100
|
.replace(/^if\s+/, "")
|
|
@@ -225,16 +262,34 @@ async function runBlocks(
|
|
|
225
262
|
}
|
|
226
263
|
}
|
|
227
264
|
|
|
228
|
-
const r = await
|
|
229
|
-
expanded
|
|
230
|
-
|
|
231
|
-
ctx.
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
265
|
+
const r = await (async () => {
|
|
266
|
+
// Check if expanded matches a registered function
|
|
267
|
+
const cmdName = expanded.trim().split(/\s+/)[0] ?? "";
|
|
268
|
+
const funcBody = ctx.env.vars[`__func_${cmdName}`];
|
|
269
|
+
if (funcBody) {
|
|
270
|
+
// Set positional params $1 $2 ... from remaining args
|
|
271
|
+
const funcArgs = expanded.trim().split(/\s+/).slice(1);
|
|
272
|
+
const savedVars = { ...ctx.env.vars };
|
|
273
|
+
funcArgs.forEach((a, i) => { ctx.env.vars[String(i + 1)] = a; });
|
|
274
|
+
ctx.env.vars["0"] = cmdName;
|
|
275
|
+
const funcLines = funcBody.split("\n");
|
|
276
|
+
const funcResult = await runBlocks(parseBlocks(funcLines), ctx);
|
|
277
|
+
// Restore positional params
|
|
278
|
+
for (let pi = 1; pi <= funcArgs.length; pi++) delete ctx.env.vars[String(pi)];
|
|
279
|
+
Object.assign(ctx.env.vars, { ...savedVars, ...ctx.env.vars });
|
|
280
|
+
return funcResult;
|
|
281
|
+
}
|
|
282
|
+
return runCommand(
|
|
283
|
+
expanded,
|
|
284
|
+
ctx.authUser,
|
|
285
|
+
ctx.hostname,
|
|
286
|
+
ctx.mode,
|
|
287
|
+
ctx.cwd,
|
|
288
|
+
ctx.shell,
|
|
289
|
+
undefined,
|
|
290
|
+
ctx.env,
|
|
291
|
+
);
|
|
292
|
+
})();
|
|
238
293
|
ctx.env.lastExitCode = r.exitCode ?? 0;
|
|
239
294
|
if (r.stdout) output += `${r.stdout}\n`;
|
|
240
295
|
if (r.stderr) return { ...r, stdout: output.trim() };
|
|
@@ -259,6 +314,27 @@ async function runBlocks(
|
|
|
259
314
|
if (sub.stdout) output += `${sub.stdout}\n`;
|
|
260
315
|
}
|
|
261
316
|
}
|
|
317
|
+
} else if (block.type === "func") {
|
|
318
|
+
// Register function in env vars as __func_<name>=<body>
|
|
319
|
+
ctx.env.vars[`__func_${block.name}`] = block.body.join("\n");
|
|
320
|
+
} else if (block.type === "arith") {
|
|
321
|
+
// (( expr )) — evaluate arithmetic, update vars
|
|
322
|
+
const { expandSync } = await import("../utils/expand");
|
|
323
|
+
const expr = expandSync(block.expr, ctx.env.vars, ctx.env.lastExitCode);
|
|
324
|
+
// Handle i++ / i-- / i+=N / i-=N
|
|
325
|
+
const incMatch = expr.match(/^(\w+)\s*(\+\+|--)$/);
|
|
326
|
+
if (incMatch) {
|
|
327
|
+
const val = parseInt(ctx.env.vars[incMatch[1]!] ?? "0", 10);
|
|
328
|
+
ctx.env.vars[incMatch[1]!] = String(incMatch[2] === "++" ? val + 1 : val - 1);
|
|
329
|
+
} else {
|
|
330
|
+
const assignMatch = expr.match(/^(\w+)\s*([+\-*/])=\s*(.+)$/);
|
|
331
|
+
if (assignMatch) {
|
|
332
|
+
const lhs = parseInt(ctx.env.vars[assignMatch[1]!] ?? "0", 10);
|
|
333
|
+
const rhs = parseInt(assignMatch[3]!, 10);
|
|
334
|
+
const ops: Record<string, number> = { "+": lhs + rhs, "-": lhs - rhs, "*": lhs * rhs, "/": Math.floor(lhs / rhs) };
|
|
335
|
+
ctx.env.vars[assignMatch[1]!] = String(ops[assignMatch[2]!] ?? lhs);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
262
338
|
} else if (block.type === "for") {
|
|
263
339
|
const listExpanded = await expandVars(
|
|
264
340
|
block.list,
|
|
@@ -266,7 +342,9 @@ async function runBlocks(
|
|
|
266
342
|
ctx.env.lastExitCode,
|
|
267
343
|
ctx,
|
|
268
344
|
);
|
|
269
|
-
|
|
345
|
+
// Apply brace expansion to each token in the list
|
|
346
|
+
const { expandBraces } = await import("../utils/expand");
|
|
347
|
+
const items = listExpanded.trim().split(/\s+/).flatMap(expandBraces);
|
|
270
348
|
for (const item of items) {
|
|
271
349
|
ctx.env.vars[block.var] = item;
|
|
272
350
|
const sub = await runBlocks(parseBlocks(block.body), ctx);
|
|
@@ -292,6 +370,59 @@ async function runBlocks(
|
|
|
292
370
|
* @category shell
|
|
293
371
|
* @params ["-c <script>", "[<file>]"]
|
|
294
372
|
*/
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Split a sh script into logical lines, respecting:
|
|
376
|
+
* - `{...}` braces (function bodies)
|
|
377
|
+
* - Newlines and semicolons at depth 0 only
|
|
378
|
+
*/
|
|
379
|
+
function splitShScript(script: string): string[] {
|
|
380
|
+
const lines: string[] = [];
|
|
381
|
+
let current = "";
|
|
382
|
+
let depth = 0;
|
|
383
|
+
let inSingleQ = false;
|
|
384
|
+
let inDoubleQ = false;
|
|
385
|
+
let i = 0;
|
|
386
|
+
while (i < script.length) {
|
|
387
|
+
const ch = script[i]!;
|
|
388
|
+
if (!inSingleQ && !inDoubleQ) {
|
|
389
|
+
if (ch === "'") { inSingleQ = true; current += ch; i++; continue; }
|
|
390
|
+
if (ch === '"') { inDoubleQ = true; current += ch; i++; continue; }
|
|
391
|
+
if (ch === "{") { depth++; current += ch; i++; continue; }
|
|
392
|
+
if (ch === "}") {
|
|
393
|
+
depth--;
|
|
394
|
+
current += ch;
|
|
395
|
+
i++;
|
|
396
|
+
// At depth 0, closing } ends the function body line
|
|
397
|
+
if (depth === 0) {
|
|
398
|
+
const t = current.trim();
|
|
399
|
+
if (t) lines.push(t);
|
|
400
|
+
current = "";
|
|
401
|
+
// Skip trailing ; or whitespace
|
|
402
|
+
while (i < script.length && (script[i] === ";" || script[i] === " ")) i++;
|
|
403
|
+
}
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
if (depth === 0 && (ch === ";" || ch === "\n")) {
|
|
407
|
+
const t = current.trim();
|
|
408
|
+
if (t && !t.startsWith("#")) lines.push(t);
|
|
409
|
+
current = "";
|
|
410
|
+
i++;
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
} else if (inSingleQ && ch === "'") {
|
|
414
|
+
inSingleQ = false;
|
|
415
|
+
} else if (inDoubleQ && ch === '"') {
|
|
416
|
+
inDoubleQ = false;
|
|
417
|
+
}
|
|
418
|
+
current += ch;
|
|
419
|
+
i++;
|
|
420
|
+
}
|
|
421
|
+
const t = current.trim();
|
|
422
|
+
if (t && !t.startsWith("#")) lines.push(t);
|
|
423
|
+
return lines;
|
|
424
|
+
}
|
|
425
|
+
|
|
295
426
|
export const shCommand: ShellModule = {
|
|
296
427
|
name: "sh",
|
|
297
428
|
aliases: ["bash"],
|
|
@@ -305,10 +436,7 @@ export const shCommand: ShellModule = {
|
|
|
305
436
|
if (ifFlag(args, "-c")) {
|
|
306
437
|
const script = args[args.indexOf("-c") + 1] ?? "";
|
|
307
438
|
if (!script) return { stderr: "sh: -c requires a script", exitCode: 1 };
|
|
308
|
-
const lines = script
|
|
309
|
-
.split(/[;\n]/)
|
|
310
|
-
.map((l) => l.trim())
|
|
311
|
-
.filter((l) => l && !l.startsWith("#"));
|
|
439
|
+
const lines = splitShScript(script);
|
|
312
440
|
const blocks = parseBlocks(lines);
|
|
313
441
|
return runBlocks(blocks, ctx);
|
|
314
442
|
}
|
|
@@ -323,10 +451,7 @@ export const shCommand: ShellModule = {
|
|
|
323
451
|
exitCode: 1,
|
|
324
452
|
};
|
|
325
453
|
const content = shell.vfs.readFile(p);
|
|
326
|
-
const lines = content
|
|
327
|
-
.split("\n")
|
|
328
|
-
.map((l) => l.trim())
|
|
329
|
-
.filter((l) => l && !l.startsWith("#"));
|
|
454
|
+
const lines = splitShScript(content);
|
|
330
455
|
const blocks = parseBlocks(lines);
|
|
331
456
|
return runBlocks(blocks, ctx);
|
|
332
457
|
}
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import * as os from "node:os";
|
|
11
|
-
import type { ShellProperties } from "../VirtualShell";
|
|
12
11
|
import type VirtualFileSystem from "../VirtualFileSystem";
|
|
12
|
+
import type { ShellProperties } from "../VirtualShell";
|
|
13
13
|
import type { VirtualUserManager } from "../VirtualUserManager";
|
|
14
14
|
|
|
15
15
|
// ─── helpers ────────────────────────────────────────────────────────────────
|
|
@@ -584,7 +584,7 @@ function bootstrapRoot(vfs: VirtualFileSystem): void {
|
|
|
584
584
|
ensureDir(vfs, "/root", 0o700);
|
|
585
585
|
ensureFile(
|
|
586
586
|
vfs,
|
|
587
|
-
"/root/.bashrc",
|
|
587
|
+
"/home/root/.bashrc",
|
|
588
588
|
`${[
|
|
589
589
|
"# root .bashrc",
|
|
590
590
|
"export PS1='\\[\\033[0;31m\\]\\u@\\h\\[\\033[0m\\]:\\[\\033[0;34m\\]\\w\\[\\033[0m\\]# '",
|
|
@@ -593,11 +593,11 @@ function bootstrapRoot(vfs: VirtualFileSystem): void {
|
|
|
593
593
|
"alias la='ls -A'",
|
|
594
594
|
].join("\n")}\n`,
|
|
595
595
|
);
|
|
596
|
-
ensureFile(vfs, "/root/.profile", "[ -f ~/.bashrc ] && . ~/.bashrc\n");
|
|
596
|
+
ensureFile(vfs, "/home/root/.profile", "[ -f ~/.bashrc ] && . ~/.bashrc\n");
|
|
597
597
|
// Fix: /home/root should map to /root for root user
|
|
598
|
-
if (!vfs.exists("/home/root")) {
|
|
599
|
-
|
|
600
|
-
}
|
|
598
|
+
// if (!vfs.exists("/home/root")) {
|
|
599
|
+
// vfs.symlink("/root", "/home/root");
|
|
600
|
+
// }
|
|
601
601
|
}
|
|
602
602
|
|
|
603
603
|
// ─── /opt + /srv + /mnt + /media ─────────────────────────────────────────────
|