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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seq.d.ts","sourceRoot":"","sources":["../../src/commands/seq.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD,0FAA0F;AAC1F,eAAO,MAAM,UAAU,EAAE,WAuCxB,CAAC"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** Generate sequences of numbers. seq LAST / seq FIRST LAST / seq FIRST INCREMENT LAST */
|
|
2
|
+
export const seqCommand = {
|
|
3
|
+
name: "seq",
|
|
4
|
+
description: "Print a sequence of numbers",
|
|
5
|
+
category: "text",
|
|
6
|
+
params: ["[FIRST [INCREMENT]] LAST"],
|
|
7
|
+
run: ({ args }) => {
|
|
8
|
+
const nums = args.filter((a) => !a.startsWith("-") || /^-[\d.]/.test(a)).map(Number);
|
|
9
|
+
const sep = (() => { const i = args.indexOf("-s"); return i !== -1 ? (args[i + 1] ?? "\n") : "\n"; })();
|
|
10
|
+
const fmt = (() => { const i = args.indexOf("-f"); return i !== -1 ? (args[i + 1] ?? "%g") : null; })();
|
|
11
|
+
const width = args.includes("-w");
|
|
12
|
+
let first = 1, inc = 1, last;
|
|
13
|
+
if (nums.length === 1) {
|
|
14
|
+
last = nums[0];
|
|
15
|
+
}
|
|
16
|
+
else if (nums.length === 2) {
|
|
17
|
+
first = nums[0];
|
|
18
|
+
last = nums[1];
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
first = nums[0];
|
|
22
|
+
inc = nums[1];
|
|
23
|
+
last = nums[2];
|
|
24
|
+
}
|
|
25
|
+
if (inc === 0)
|
|
26
|
+
return { stderr: "seq: zero increment\n", exitCode: 1 };
|
|
27
|
+
if ((inc > 0 && first > last) || (inc < 0 && first < last))
|
|
28
|
+
return { stdout: "", exitCode: 0 };
|
|
29
|
+
const results = [];
|
|
30
|
+
const maxSteps = 100000;
|
|
31
|
+
let steps = 0;
|
|
32
|
+
for (let n = first; inc > 0 ? n <= last : n >= last; n = Math.round((n + inc) * 1e10) / 1e10) {
|
|
33
|
+
if (++steps > maxSteps)
|
|
34
|
+
break;
|
|
35
|
+
let s;
|
|
36
|
+
if (fmt) {
|
|
37
|
+
s = fmt.replace("%g", String(n)).replace("%f", n.toFixed(6)).replace("%d", String(Math.trunc(n)));
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
s = Number.isInteger(n) ? String(n) : n.toPrecision(12).replace(/\.?0+$/, "");
|
|
41
|
+
}
|
|
42
|
+
if (width) {
|
|
43
|
+
const maxLen = String(Math.trunc(last)).length;
|
|
44
|
+
s = s.padStart(maxLen, "0");
|
|
45
|
+
}
|
|
46
|
+
results.push(s);
|
|
47
|
+
}
|
|
48
|
+
return { stdout: `${results.join(sep)}\n`, exitCode: 0 };
|
|
49
|
+
},
|
|
50
|
+
};
|
package/dist/commands/sh.d.ts
CHANGED
|
@@ -1,9 +1,3 @@
|
|
|
1
1
|
import type { ShellModule } from "../types/commands";
|
|
2
|
-
/**
|
|
3
|
-
* Execute shell scripts or commands with a minimal shell interpreter.
|
|
4
|
-
* Supports if/elif/else, for loops, while loops, and variable expansion.
|
|
5
|
-
* @category shell
|
|
6
|
-
* @params ["-c <script>", "[<file>]"]
|
|
7
|
-
*/
|
|
8
2
|
export declare const shCommand: ShellModule;
|
|
9
3
|
//# sourceMappingURL=sh.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sh.d.ts","sourceRoot":"","sources":["../../src/commands/sh.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAGX,WAAW,EACX,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"sh.d.ts","sourceRoot":"","sources":["../../src/commands/sh.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAGX,WAAW,EACX,MAAM,mBAAmB,CAAC;AAqa3B,eAAO,MAAM,SAAS,EAAE,WAsCvB,CAAC"}
|
package/dist/commands/sh.js
CHANGED
|
@@ -19,6 +19,38 @@ function parseBlocks(lines) {
|
|
|
19
19
|
i++;
|
|
20
20
|
continue;
|
|
21
21
|
}
|
|
22
|
+
// Function definition: name() { or function name { or name() { body }
|
|
23
|
+
const funcMatchInline = line.match(/^(?:function\s+)?(\w+)\s*\(\s*\)\s*\{(.+)\}\s*$/);
|
|
24
|
+
const funcMatch = funcMatchInline ?? (line.match(/^(?:function\s+)?(\w+)\s*\(\s*\)\s*\{?\s*$/) ||
|
|
25
|
+
line.match(/^function\s+(\w+)\s*\{?\s*$/));
|
|
26
|
+
if (funcMatch) {
|
|
27
|
+
const funcName = funcMatch[1];
|
|
28
|
+
const body = [];
|
|
29
|
+
// Inline: name() { cmd; } — single-line form
|
|
30
|
+
if (funcMatchInline) {
|
|
31
|
+
body.push(...funcMatchInline[2].split(";").map((s) => s.trim()).filter(Boolean));
|
|
32
|
+
blocks.push({ type: "func", name: funcName, body });
|
|
33
|
+
i++;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
i++;
|
|
37
|
+
while (i < lines.length && lines[i]?.trim() !== "}" && i < lines.length + 1) {
|
|
38
|
+
const l = lines[i].trim().replace(/^do\s+/, "");
|
|
39
|
+
if (l && l !== "{")
|
|
40
|
+
body.push(l);
|
|
41
|
+
i++;
|
|
42
|
+
}
|
|
43
|
+
i++; // skip closing }
|
|
44
|
+
blocks.push({ type: "func", name: funcName, body });
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
// (( expr )) arithmetic statement
|
|
48
|
+
const arithMatch = line.match(/^\(\(\s*(.+?)\s*\)\)$/);
|
|
49
|
+
if (arithMatch) {
|
|
50
|
+
blocks.push({ type: "arith", expr: arithMatch[1] });
|
|
51
|
+
i++;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
22
54
|
if (line.startsWith("if ") || line === "if") {
|
|
23
55
|
const cond = line
|
|
24
56
|
.replace(/^if\s+/, "")
|
|
@@ -174,7 +206,26 @@ async function runBlocks(blocks, ctx) {
|
|
|
174
206
|
continue;
|
|
175
207
|
}
|
|
176
208
|
}
|
|
177
|
-
const r = await
|
|
209
|
+
const r = await (async () => {
|
|
210
|
+
// Check if expanded matches a registered function
|
|
211
|
+
const cmdName = expanded.trim().split(/\s+/)[0] ?? "";
|
|
212
|
+
const funcBody = ctx.env.vars[`__func_${cmdName}`];
|
|
213
|
+
if (funcBody) {
|
|
214
|
+
// Set positional params $1 $2 ... from remaining args
|
|
215
|
+
const funcArgs = expanded.trim().split(/\s+/).slice(1);
|
|
216
|
+
const savedVars = { ...ctx.env.vars };
|
|
217
|
+
funcArgs.forEach((a, i) => { ctx.env.vars[String(i + 1)] = a; });
|
|
218
|
+
ctx.env.vars["0"] = cmdName;
|
|
219
|
+
const funcLines = funcBody.split("\n");
|
|
220
|
+
const funcResult = await runBlocks(parseBlocks(funcLines), ctx);
|
|
221
|
+
// Restore positional params
|
|
222
|
+
for (let pi = 1; pi <= funcArgs.length; pi++)
|
|
223
|
+
delete ctx.env.vars[String(pi)];
|
|
224
|
+
Object.assign(ctx.env.vars, { ...savedVars, ...ctx.env.vars });
|
|
225
|
+
return funcResult;
|
|
226
|
+
}
|
|
227
|
+
return runCommand(expanded, ctx.authUser, ctx.hostname, ctx.mode, ctx.cwd, ctx.shell, undefined, ctx.env);
|
|
228
|
+
})();
|
|
178
229
|
ctx.env.lastExitCode = r.exitCode ?? 0;
|
|
179
230
|
if (r.stdout)
|
|
180
231
|
output += `${r.stdout}\n`;
|
|
@@ -207,9 +258,35 @@ async function runBlocks(blocks, ctx) {
|
|
|
207
258
|
}
|
|
208
259
|
}
|
|
209
260
|
}
|
|
261
|
+
else if (block.type === "func") {
|
|
262
|
+
// Register function in env vars as __func_<name>=<body>
|
|
263
|
+
ctx.env.vars[`__func_${block.name}`] = block.body.join("\n");
|
|
264
|
+
}
|
|
265
|
+
else if (block.type === "arith") {
|
|
266
|
+
// (( expr )) — evaluate arithmetic, update vars
|
|
267
|
+
const { expandSync } = await import("../utils/expand");
|
|
268
|
+
const expr = expandSync(block.expr, ctx.env.vars, ctx.env.lastExitCode);
|
|
269
|
+
// Handle i++ / i-- / i+=N / i-=N
|
|
270
|
+
const incMatch = expr.match(/^(\w+)\s*(\+\+|--)$/);
|
|
271
|
+
if (incMatch) {
|
|
272
|
+
const val = parseInt(ctx.env.vars[incMatch[1]] ?? "0", 10);
|
|
273
|
+
ctx.env.vars[incMatch[1]] = String(incMatch[2] === "++" ? val + 1 : val - 1);
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
const assignMatch = expr.match(/^(\w+)\s*([+\-*/])=\s*(.+)$/);
|
|
277
|
+
if (assignMatch) {
|
|
278
|
+
const lhs = parseInt(ctx.env.vars[assignMatch[1]] ?? "0", 10);
|
|
279
|
+
const rhs = parseInt(assignMatch[3], 10);
|
|
280
|
+
const ops = { "+": lhs + rhs, "-": lhs - rhs, "*": lhs * rhs, "/": Math.floor(lhs / rhs) };
|
|
281
|
+
ctx.env.vars[assignMatch[1]] = String(ops[assignMatch[2]] ?? lhs);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
210
285
|
else if (block.type === "for") {
|
|
211
286
|
const listExpanded = await expandVars(block.list, ctx.env.vars, ctx.env.lastExitCode, ctx);
|
|
212
|
-
|
|
287
|
+
// Apply brace expansion to each token in the list
|
|
288
|
+
const { expandBraces } = await import("../utils/expand");
|
|
289
|
+
const items = listExpanded.trim().split(/\s+/).flatMap(expandBraces);
|
|
213
290
|
for (const item of items) {
|
|
214
291
|
ctx.env.vars[block.var] = item;
|
|
215
292
|
const sub = await runBlocks(parseBlocks(block.body), ctx);
|
|
@@ -239,6 +316,78 @@ async function runBlocks(blocks, ctx) {
|
|
|
239
316
|
* @category shell
|
|
240
317
|
* @params ["-c <script>", "[<file>]"]
|
|
241
318
|
*/
|
|
319
|
+
/**
|
|
320
|
+
* Split a sh script into logical lines, respecting:
|
|
321
|
+
* - `{...}` braces (function bodies)
|
|
322
|
+
* - Newlines and semicolons at depth 0 only
|
|
323
|
+
*/
|
|
324
|
+
function splitShScript(script) {
|
|
325
|
+
const lines = [];
|
|
326
|
+
let current = "";
|
|
327
|
+
let depth = 0;
|
|
328
|
+
let inSingleQ = false;
|
|
329
|
+
let inDoubleQ = false;
|
|
330
|
+
let i = 0;
|
|
331
|
+
while (i < script.length) {
|
|
332
|
+
const ch = script[i];
|
|
333
|
+
if (!inSingleQ && !inDoubleQ) {
|
|
334
|
+
if (ch === "'") {
|
|
335
|
+
inSingleQ = true;
|
|
336
|
+
current += ch;
|
|
337
|
+
i++;
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
if (ch === '"') {
|
|
341
|
+
inDoubleQ = true;
|
|
342
|
+
current += ch;
|
|
343
|
+
i++;
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
if (ch === "{") {
|
|
347
|
+
depth++;
|
|
348
|
+
current += ch;
|
|
349
|
+
i++;
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
if (ch === "}") {
|
|
353
|
+
depth--;
|
|
354
|
+
current += ch;
|
|
355
|
+
i++;
|
|
356
|
+
// At depth 0, closing } ends the function body line
|
|
357
|
+
if (depth === 0) {
|
|
358
|
+
const t = current.trim();
|
|
359
|
+
if (t)
|
|
360
|
+
lines.push(t);
|
|
361
|
+
current = "";
|
|
362
|
+
// Skip trailing ; or whitespace
|
|
363
|
+
while (i < script.length && (script[i] === ";" || script[i] === " "))
|
|
364
|
+
i++;
|
|
365
|
+
}
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
if (depth === 0 && (ch === ";" || ch === "\n")) {
|
|
369
|
+
const t = current.trim();
|
|
370
|
+
if (t && !t.startsWith("#"))
|
|
371
|
+
lines.push(t);
|
|
372
|
+
current = "";
|
|
373
|
+
i++;
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
else if (inSingleQ && ch === "'") {
|
|
378
|
+
inSingleQ = false;
|
|
379
|
+
}
|
|
380
|
+
else if (inDoubleQ && ch === '"') {
|
|
381
|
+
inDoubleQ = false;
|
|
382
|
+
}
|
|
383
|
+
current += ch;
|
|
384
|
+
i++;
|
|
385
|
+
}
|
|
386
|
+
const t = current.trim();
|
|
387
|
+
if (t && !t.startsWith("#"))
|
|
388
|
+
lines.push(t);
|
|
389
|
+
return lines;
|
|
390
|
+
}
|
|
242
391
|
export const shCommand = {
|
|
243
392
|
name: "sh",
|
|
244
393
|
aliases: ["bash"],
|
|
@@ -252,10 +401,7 @@ export const shCommand = {
|
|
|
252
401
|
const script = args[args.indexOf("-c") + 1] ?? "";
|
|
253
402
|
if (!script)
|
|
254
403
|
return { stderr: "sh: -c requires a script", exitCode: 1 };
|
|
255
|
-
const lines = script
|
|
256
|
-
.split(/[;\n]/)
|
|
257
|
-
.map((l) => l.trim())
|
|
258
|
-
.filter((l) => l && !l.startsWith("#"));
|
|
404
|
+
const lines = splitShScript(script);
|
|
259
405
|
const blocks = parseBlocks(lines);
|
|
260
406
|
return runBlocks(blocks, ctx);
|
|
261
407
|
}
|
|
@@ -269,10 +415,7 @@ export const shCommand = {
|
|
|
269
415
|
exitCode: 1,
|
|
270
416
|
};
|
|
271
417
|
const content = shell.vfs.readFile(p);
|
|
272
|
-
const lines = content
|
|
273
|
-
.split("\n")
|
|
274
|
-
.map((l) => l.trim())
|
|
275
|
-
.filter((l) => l && !l.startsWith("#"));
|
|
418
|
+
const lines = splitShScript(content);
|
|
276
419
|
const blocks = parseBlocks(lines);
|
|
277
420
|
return runBlocks(blocks, ctx);
|
|
278
421
|
}
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* Called once during VirtualShell initialization. Idempotent — skips
|
|
7
7
|
* paths that already exist so FS-mode snapshots survive restarts.
|
|
8
8
|
*/
|
|
9
|
-
import type { ShellProperties } from "../VirtualShell";
|
|
10
9
|
import type VirtualFileSystem from "../VirtualFileSystem";
|
|
10
|
+
import type { ShellProperties } from "../VirtualShell";
|
|
11
11
|
import type { VirtualUserManager } from "../VirtualUserManager";
|
|
12
12
|
/**
|
|
13
13
|
* Sync `/etc/passwd`, `/etc/group`, and `/etc/shadow` from the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"linuxRootfs.d.ts","sourceRoot":"","sources":["../../src/modules/linuxRootfs.ts"],"names":[],"mappings":"AAAA,oEAAoE;AACpE;;;;;;GAMG;AAGH,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"linuxRootfs.d.ts","sourceRoot":"","sources":["../../src/modules/linuxRootfs.ts"],"names":[],"mappings":"AAAA,oEAAoE;AACpE;;;;;;GAMG;AAGH,OAAO,KAAK,iBAAiB,MAAM,sBAAsB,CAAC;AAC1D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACvD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAgHhE;;;;;GAKG;AACH,wBAAgB,aAAa,CAC5B,GAAG,EAAE,iBAAiB,EACtB,KAAK,EAAE,kBAAkB,GACvB,IAAI,CAsCN;AAgED;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAC1B,GAAG,EAAE,iBAAiB,EACtB,KAAK,EAAE,eAAe,EACtB,QAAQ,EAAE,MAAM,EAChB,cAAc,EAAE,MAAM,EACtB,QAAQ,CAAC,EAAE,OAAO,uBAAuB,EAAE,oBAAoB,EAAE,GAC/D,IAAI,CAmJN;AAuND;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CACnC,GAAG,EAAE,iBAAiB,EACtB,KAAK,EAAE,kBAAkB,EACzB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,eAAe,EACtB,cAAc,EAAE,MAAM,GACpB,IAAI,CAYN"}
|
|
@@ -394,18 +394,18 @@ function bootstrapTmp(vfs) {
|
|
|
394
394
|
// ─── /root ────────────────────────────────────────────────────────────────────
|
|
395
395
|
function bootstrapRoot(vfs) {
|
|
396
396
|
ensureDir(vfs, "/root", 0o700);
|
|
397
|
-
ensureFile(vfs, "/root/.bashrc", `${[
|
|
397
|
+
ensureFile(vfs, "/home/root/.bashrc", `${[
|
|
398
398
|
"# root .bashrc",
|
|
399
399
|
"export PS1='\\[\\033[0;31m\\]\\u@\\h\\[\\033[0m\\]:\\[\\033[0;34m\\]\\w\\[\\033[0m\\]# '",
|
|
400
400
|
"export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
|
401
401
|
"alias ll='ls -la'",
|
|
402
402
|
"alias la='ls -A'",
|
|
403
403
|
].join("\n")}\n`);
|
|
404
|
-
ensureFile(vfs, "/root/.profile", "[ -f ~/.bashrc ] && . ~/.bashrc\n");
|
|
404
|
+
ensureFile(vfs, "/home/root/.profile", "[ -f ~/.bashrc ] && . ~/.bashrc\n");
|
|
405
405
|
// Fix: /home/root should map to /root for root user
|
|
406
|
-
if (!vfs.exists("/home/root")) {
|
|
407
|
-
|
|
408
|
-
}
|
|
406
|
+
// if (!vfs.exists("/home/root")) {
|
|
407
|
+
// vfs.symlink("/root", "/home/root");
|
|
408
|
+
// }
|
|
409
409
|
}
|
|
410
410
|
// ─── /opt + /srv + /mnt + /media ─────────────────────────────────────────────
|
|
411
411
|
function bootstrapMisc(vfs) {
|