typescript-virtual-container 1.5.5 → 1.5.7
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 +117 -35
- package/dist/.tsbuildinfo +1 -1
- package/dist/SSHMimic/index.d.ts +5 -1
- package/dist/SSHMimic/index.js +27 -3
- package/dist/SSHMimic/scp.d.ts +34 -0
- package/dist/SSHMimic/scp.js +285 -0
- package/dist/SSHMimic/sftp.d.ts +53 -3
- package/dist/SSHMimic/sftp.js +9 -3
- package/dist/VirtualFileSystem/binaryPack.d.ts +7 -0
- package/dist/VirtualFileSystem/binaryPack.js +37 -1
- package/dist/VirtualFileSystem/index.d.ts +7 -0
- package/dist/VirtualFileSystem/index.js +67 -27
- package/dist/VirtualFileSystem/internalTypes.d.ts +2 -0
- package/dist/VirtualFileSystem/path.d.ts +5 -0
- package/dist/VirtualFileSystem/path.js +24 -11
- package/dist/VirtualPackageManager/index.d.ts +4 -2
- package/dist/VirtualPackageManager/index.js +24 -4
- package/dist/VirtualShell/index.d.ts +4 -0
- package/dist/VirtualShell/index.js +1 -7
- package/dist/VirtualShell/shell.js +40 -10
- package/dist/VirtualShell/shellParser.js +1 -22
- package/dist/commands/awk.d.ts +6 -11
- package/dist/commands/awk.js +462 -109
- package/dist/commands/bzip2.d.ts +11 -0
- package/dist/commands/bzip2.js +91 -0
- package/dist/commands/exit.js +1 -1
- package/dist/commands/find.d.ts +2 -2
- package/dist/commands/find.js +209 -37
- package/dist/commands/helpers.d.ts +0 -20
- package/dist/commands/helpers.js +0 -97
- package/dist/commands/lsof.d.ts +6 -0
- package/dist/commands/lsof.js +30 -0
- package/dist/commands/perl.d.ts +6 -0
- package/dist/commands/perl.js +76 -0
- package/dist/commands/python.js +5 -2
- package/dist/commands/registry.js +19 -1
- package/dist/commands/runtime.js +65 -87
- package/dist/commands/sed.d.ts +2 -2
- package/dist/commands/sed.js +216 -34
- package/dist/commands/sh.js +42 -0
- package/dist/commands/strace.d.ts +6 -0
- package/dist/commands/strace.js +26 -0
- package/dist/commands/tar.d.ts +2 -1
- package/dist/commands/tar.js +138 -52
- package/dist/commands/test.js +2 -2
- package/dist/commands/zip.d.ts +11 -0
- package/dist/commands/zip.js +232 -0
- package/dist/modules/linuxRootfs.js +1 -4
- package/dist/modules/neofetch.js +2 -2
- package/dist/types/commands.d.ts +4 -0
- package/dist/utils/argv.d.ts +6 -0
- package/dist/utils/argv.js +32 -0
- package/dist/utils/expand.d.ts +5 -2
- package/dist/utils/expand.js +112 -45
- package/dist/utils/glob.d.ts +6 -0
- package/dist/utils/glob.js +34 -0
- package/dist/utils/tokenize.js +13 -13
- package/package.json +9 -7
- package/dist/self-standalone.d.ts +0 -1
- package/dist/self-standalone.js +0 -444
- package/dist/standalone-wo-sftp.d.ts +0 -1
- package/dist/standalone-wo-sftp.js +0 -30
- package/dist/standalone.d.ts +0 -1
- package/dist/standalone.js +0 -61
|
@@ -5,7 +5,7 @@ import * as path from "node:path";
|
|
|
5
5
|
import { gunzipSync, gzipSync } from "node:zlib";
|
|
6
6
|
import { decodeVfs, encodeVfs, isBinarySnapshot } from "./binaryPack";
|
|
7
7
|
import { appendJournalEntry, JournalOp, readJournal, truncateJournal } from "./journal";
|
|
8
|
-
import {
|
|
8
|
+
import { getNodeNormalized, getParentDirectory, normalizePath } from "./path";
|
|
9
9
|
// ── VirtualFileSystem ─────────────────────────────────────────────────────────
|
|
10
10
|
/**
|
|
11
11
|
* In-memory virtual filesystem with optional JSON-snapshot persistence.
|
|
@@ -48,6 +48,8 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
48
48
|
_dirty = false;
|
|
49
49
|
/** Active host-directory mounts: vPath → { hostPath, readOnly } */
|
|
50
50
|
mounts = new Map();
|
|
51
|
+
/** Sorted mounts cache (longest-path-first). Rebuilt lazily on mount/unmount. */
|
|
52
|
+
_sortedMounts = null;
|
|
51
53
|
/** True when running in a browser environment (no host FS access). */
|
|
52
54
|
static isBrowser = typeof process === "undefined" || typeof process.versions?.node === "undefined";
|
|
53
55
|
constructor(options = {}) {
|
|
@@ -93,6 +95,7 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
93
95
|
updatedAt: now,
|
|
94
96
|
children: Object.create(null),
|
|
95
97
|
_childCount: 0,
|
|
98
|
+
_sortedKeys: null,
|
|
96
99
|
};
|
|
97
100
|
}
|
|
98
101
|
makeFile(name, content, mode, compressed) {
|
|
@@ -127,8 +130,10 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
127
130
|
// Don't overwrite a real file or an already-promoted node
|
|
128
131
|
if (existing?.type === "file")
|
|
129
132
|
return;
|
|
130
|
-
if (!existing)
|
|
133
|
+
if (!existing) {
|
|
131
134
|
parent._childCount++;
|
|
135
|
+
parent._sortedKeys = null;
|
|
136
|
+
}
|
|
132
137
|
parent.children[name] = this.makeStub(name, content, mode);
|
|
133
138
|
}
|
|
134
139
|
mkdirRecursive(targetPath, mode) {
|
|
@@ -145,6 +150,7 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
145
150
|
child = this.makeDir(part, mode);
|
|
146
151
|
current.children[part] = child;
|
|
147
152
|
current._childCount++;
|
|
153
|
+
current._sortedKeys = null;
|
|
148
154
|
this.emit("dir:create", { path: builtPath, mode });
|
|
149
155
|
this._journal({ op: JournalOp.MKDIR, path: builtPath, mode });
|
|
150
156
|
}
|
|
@@ -316,21 +322,19 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
316
322
|
const existing = live.children[name];
|
|
317
323
|
if (node.type === "directory") {
|
|
318
324
|
if (!existing) {
|
|
319
|
-
// Dir doesn't exist yet — add it
|
|
320
325
|
live.children[name] = node;
|
|
321
326
|
live._childCount++;
|
|
327
|
+
live._sortedKeys = null;
|
|
322
328
|
}
|
|
323
329
|
else if (existing.type === "directory") {
|
|
324
|
-
// Both dirs — recurse
|
|
325
330
|
this._mergeDir(existing, node);
|
|
326
331
|
}
|
|
327
|
-
// existing is a file where dir expected — leave user file alone
|
|
328
332
|
}
|
|
329
333
|
else {
|
|
330
|
-
// File or stub — only add if not already present
|
|
331
334
|
if (!existing) {
|
|
332
335
|
live.children[name] = node;
|
|
333
336
|
live._childCount++;
|
|
337
|
+
live._sortedKeys = null;
|
|
334
338
|
}
|
|
335
339
|
}
|
|
336
340
|
}
|
|
@@ -495,6 +499,7 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
495
499
|
// Ensure the mount point exists in the VFS tree
|
|
496
500
|
this.mkdir(normalized);
|
|
497
501
|
this.mounts.set(normalized, { hostPath: resolved, readOnly });
|
|
502
|
+
this._sortedMounts = null;
|
|
498
503
|
this.emit("mount", { vPath: normalized, hostPath: resolved, readOnly });
|
|
499
504
|
}
|
|
500
505
|
/**
|
|
@@ -505,6 +510,7 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
505
510
|
unmount(vPath) {
|
|
506
511
|
const normalized = normalizePath(vPath);
|
|
507
512
|
if (this.mounts.delete(normalized)) {
|
|
513
|
+
this._sortedMounts = null;
|
|
508
514
|
this.emit("unmount", { vPath: normalized });
|
|
509
515
|
}
|
|
510
516
|
}
|
|
@@ -521,9 +527,11 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
521
527
|
*/
|
|
522
528
|
resolveMount(targetPath) {
|
|
523
529
|
const normalized = normalizePath(targetPath);
|
|
524
|
-
// Iterate mounts from most specific to least specific
|
|
525
|
-
|
|
526
|
-
|
|
530
|
+
// Iterate mounts from most specific to least specific (cached, rebuilt on mount/unmount)
|
|
531
|
+
if (!this._sortedMounts) {
|
|
532
|
+
this._sortedMounts = [...this.mounts.entries()].sort(([a], [b]) => b.length - a.length);
|
|
533
|
+
}
|
|
534
|
+
for (const [vBase, opts] of this._sortedMounts) {
|
|
527
535
|
if (normalized === vBase || normalized.startsWith(`${vBase}/`)) {
|
|
528
536
|
const relPath = normalized.slice(vBase.length).replace(/^\//, "");
|
|
529
537
|
const fullHostPath = relPath ? path.join(opts.hostPath, relPath) : opts.hostPath;
|
|
@@ -536,7 +544,7 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
536
544
|
const normalized = normalizePath(targetPath);
|
|
537
545
|
const existing = (() => {
|
|
538
546
|
try {
|
|
539
|
-
return
|
|
547
|
+
return getNodeNormalized(this.root, normalized);
|
|
540
548
|
}
|
|
541
549
|
catch {
|
|
542
550
|
return null;
|
|
@@ -585,8 +593,10 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
585
593
|
}
|
|
586
594
|
else {
|
|
587
595
|
// Create new real file — also promotes stubs (no _childCount change for stubs)
|
|
588
|
-
if (!existing)
|
|
596
|
+
if (!existing) {
|
|
589
597
|
parent._childCount++;
|
|
598
|
+
parent._sortedKeys = null;
|
|
599
|
+
}
|
|
590
600
|
parent.children[name] = this.makeFile(name, storedContent, mode, shouldCompress);
|
|
591
601
|
}
|
|
592
602
|
this.emit("file:write", { path: normalized, size: storedContent.length });
|
|
@@ -604,7 +614,7 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
604
614
|
return fsSync.readFileSync(m.fullHostPath, "utf8");
|
|
605
615
|
}
|
|
606
616
|
const normalized = normalizePath(targetPath);
|
|
607
|
-
const node =
|
|
617
|
+
const node = getNodeNormalized(this.root, normalized);
|
|
608
618
|
if (node.type === "stub") {
|
|
609
619
|
this.emit("file:read", { path: normalized, size: node.stubContent.length });
|
|
610
620
|
return node.stubContent;
|
|
@@ -628,7 +638,7 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
628
638
|
return fsSync.readFileSync(m.fullHostPath);
|
|
629
639
|
}
|
|
630
640
|
const normalized = normalizePath(targetPath);
|
|
631
|
-
const node =
|
|
641
|
+
const node = getNodeNormalized(this.root, normalized);
|
|
632
642
|
if (node.type === "stub") {
|
|
633
643
|
const buf = Buffer.from(node.stubContent, "utf8");
|
|
634
644
|
this.emit("file:read", { path: normalized, size: buf.length });
|
|
@@ -650,7 +660,7 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
650
660
|
if (m)
|
|
651
661
|
return fsSync.existsSync(m.fullHostPath);
|
|
652
662
|
try {
|
|
653
|
-
|
|
663
|
+
getNodeNormalized(this.root, normalizePath(targetPath));
|
|
654
664
|
return true;
|
|
655
665
|
}
|
|
656
666
|
catch {
|
|
@@ -660,7 +670,7 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
660
670
|
/** Updates mode bits on a node. */
|
|
661
671
|
chmod(targetPath, mode) {
|
|
662
672
|
const normalized = normalizePath(targetPath);
|
|
663
|
-
|
|
673
|
+
getNodeNormalized(this.root, normalized).mode = mode;
|
|
664
674
|
this._journal({ op: JournalOp.CHMOD, path: normalized, mode });
|
|
665
675
|
}
|
|
666
676
|
/** Returns metadata for a file or directory. */
|
|
@@ -695,7 +705,7 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
695
705
|
};
|
|
696
706
|
}
|
|
697
707
|
const normalized = normalizePath(targetPath);
|
|
698
|
-
const node =
|
|
708
|
+
const node = getNodeNormalized(this.root, normalized);
|
|
699
709
|
const name = normalized === "/" ? "" : path.posix.basename(normalized);
|
|
700
710
|
if (node.type === "stub") {
|
|
701
711
|
const s = node;
|
|
@@ -734,6 +744,26 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
734
744
|
childrenCount: d._childCount,
|
|
735
745
|
};
|
|
736
746
|
}
|
|
747
|
+
/**
|
|
748
|
+
* Fast type-only check — no Date/string allocation.
|
|
749
|
+
* Use instead of `stat().type` when that's all you need.
|
|
750
|
+
*/
|
|
751
|
+
statType(targetPath) {
|
|
752
|
+
try {
|
|
753
|
+
const m = this.resolveMount(targetPath);
|
|
754
|
+
if (m) {
|
|
755
|
+
const s = fsSync.statSync(m.fullHostPath, { throwIfNoEntry: false });
|
|
756
|
+
if (!s)
|
|
757
|
+
return null;
|
|
758
|
+
return s.isDirectory() ? "directory" : "file";
|
|
759
|
+
}
|
|
760
|
+
const node = getNodeNormalized(this.root, normalizePath(targetPath));
|
|
761
|
+
return node.type === "directory" ? "directory" : "file";
|
|
762
|
+
}
|
|
763
|
+
catch {
|
|
764
|
+
return null;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
737
767
|
/** Lists direct children names of a directory (sorted). */
|
|
738
768
|
list(dirPath = "/") {
|
|
739
769
|
const m = this.resolveMount(dirPath);
|
|
@@ -748,16 +778,19 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
748
778
|
}
|
|
749
779
|
}
|
|
750
780
|
const normalized = normalizePath(dirPath);
|
|
751
|
-
const node =
|
|
781
|
+
const node = getNodeNormalized(this.root, normalized);
|
|
752
782
|
if (node.type !== "directory") {
|
|
753
783
|
throw new Error(`Cannot list '${dirPath}': not a directory.`);
|
|
754
784
|
}
|
|
755
|
-
|
|
785
|
+
const dir = node;
|
|
786
|
+
if (!dir._sortedKeys)
|
|
787
|
+
dir._sortedKeys = Object.keys(dir.children).sort();
|
|
788
|
+
return dir._sortedKeys;
|
|
756
789
|
}
|
|
757
790
|
/** Renders ASCII tree view of a directory hierarchy. */
|
|
758
791
|
tree(dirPath = "/") {
|
|
759
792
|
const normalized = normalizePath(dirPath);
|
|
760
|
-
const node =
|
|
793
|
+
const node = getNodeNormalized(this.root, normalized);
|
|
761
794
|
if (node.type !== "directory") {
|
|
762
795
|
throw new Error(`Cannot render tree for '${dirPath}': not a directory.`);
|
|
763
796
|
}
|
|
@@ -766,7 +799,9 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
766
799
|
}
|
|
767
800
|
renderTreeLines(dir, label) {
|
|
768
801
|
const lines = [label];
|
|
769
|
-
|
|
802
|
+
if (!dir._sortedKeys)
|
|
803
|
+
dir._sortedKeys = Object.keys(dir.children).sort();
|
|
804
|
+
const entries = dir._sortedKeys;
|
|
770
805
|
for (let i = 0; i < entries.length; i++) {
|
|
771
806
|
const name = entries[i];
|
|
772
807
|
const child = dir.children[name];
|
|
@@ -786,7 +821,7 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
786
821
|
}
|
|
787
822
|
/** Computes total stored bytes under a path. */
|
|
788
823
|
getUsageBytes(targetPath = "/") {
|
|
789
|
-
return this.computeUsage(
|
|
824
|
+
return this.computeUsage(getNodeNormalized(this.root, normalizePath(targetPath)));
|
|
790
825
|
}
|
|
791
826
|
computeUsage(node) {
|
|
792
827
|
if (node.type === "file")
|
|
@@ -801,7 +836,7 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
801
836
|
}
|
|
802
837
|
/** Compresses a file's content with gzip in place. */
|
|
803
838
|
compressFile(targetPath) {
|
|
804
|
-
const node =
|
|
839
|
+
const node = getNodeNormalized(this.root, normalizePath(targetPath));
|
|
805
840
|
if (node.type !== "file")
|
|
806
841
|
throw new Error(`Cannot compress '${targetPath}': not a file.`);
|
|
807
842
|
const f = node;
|
|
@@ -813,7 +848,7 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
813
848
|
}
|
|
814
849
|
/** Decompresses a gzip-compressed file in place. */
|
|
815
850
|
decompressFile(targetPath) {
|
|
816
|
-
const node =
|
|
851
|
+
const node = getNodeNormalized(this.root, normalizePath(targetPath));
|
|
817
852
|
if (node.type !== "file")
|
|
818
853
|
throw new Error(`Cannot decompress '${targetPath}': not a file.`);
|
|
819
854
|
const f = node;
|
|
@@ -844,6 +879,7 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
844
879
|
};
|
|
845
880
|
parent.children[name] = symNode;
|
|
846
881
|
parent._childCount++;
|
|
882
|
+
parent._sortedKeys = null;
|
|
847
883
|
// Journal before emit
|
|
848
884
|
this._journal({ op: JournalOp.SYMLINK, path: normalizedLink, dest: normalizedTarget });
|
|
849
885
|
this.emit("symlink:create", {
|
|
@@ -854,7 +890,7 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
854
890
|
/** Returns true when the path is a symbolic link node. */
|
|
855
891
|
isSymlink(targetPath) {
|
|
856
892
|
try {
|
|
857
|
-
const node =
|
|
893
|
+
const node = getNodeNormalized(this.root, normalizePath(targetPath));
|
|
858
894
|
return node.type === "file" && node.mode === 0o120777;
|
|
859
895
|
}
|
|
860
896
|
catch {
|
|
@@ -869,7 +905,7 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
869
905
|
let current = normalizePath(linkPath);
|
|
870
906
|
for (let depth = 0; depth < maxDepth; depth++) {
|
|
871
907
|
try {
|
|
872
|
-
const node =
|
|
908
|
+
const node = getNodeNormalized(this.root, current);
|
|
873
909
|
if (node.type === "file" && node.mode === 0o120777) {
|
|
874
910
|
const target = node.content.toString("utf8");
|
|
875
911
|
current = target.startsWith("/")
|
|
@@ -905,7 +941,7 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
905
941
|
const normalized = normalizePath(targetPath);
|
|
906
942
|
if (normalized === "/")
|
|
907
943
|
throw new Error("Cannot remove root directory.");
|
|
908
|
-
const node =
|
|
944
|
+
const node = getNodeNormalized(this.root, normalized);
|
|
909
945
|
if (node.type === "directory") {
|
|
910
946
|
const dir = node;
|
|
911
947
|
if (!options.recursive && dir._childCount > 0) {
|
|
@@ -915,6 +951,7 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
915
951
|
const { parent, name } = getParentDirectory(this.root, normalized, false, () => { });
|
|
916
952
|
delete parent.children[name];
|
|
917
953
|
parent._childCount--;
|
|
954
|
+
parent._sortedKeys = null;
|
|
918
955
|
this.emit("node:remove", { path: normalized });
|
|
919
956
|
this._journal({ op: JournalOp.REMOVE, path: normalized });
|
|
920
957
|
}
|
|
@@ -925,7 +962,7 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
925
962
|
if (fromNormalized === "/" || toNormalized === "/") {
|
|
926
963
|
throw new Error("Cannot move root directory.");
|
|
927
964
|
}
|
|
928
|
-
const node =
|
|
965
|
+
const node = getNodeNormalized(this.root, fromNormalized);
|
|
929
966
|
if (this.exists(toNormalized)) {
|
|
930
967
|
throw new Error(`Destination '${toNormalized}' already exists.`);
|
|
931
968
|
}
|
|
@@ -934,9 +971,11 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
934
971
|
const { parent: srcParent, name: srcName } = getParentDirectory(this.root, fromNormalized, false, () => { });
|
|
935
972
|
delete srcParent.children[srcName];
|
|
936
973
|
srcParent._childCount--;
|
|
974
|
+
srcParent._sortedKeys = null;
|
|
937
975
|
node.name = destName;
|
|
938
976
|
destParent.children[destName] = node;
|
|
939
977
|
destParent._childCount++;
|
|
978
|
+
destParent._sortedKeys = null;
|
|
940
979
|
this._journal({ op: JournalOp.MOVE, path: fromNormalized, dest: toNormalized });
|
|
941
980
|
}
|
|
942
981
|
// ── Snapshot serialisation ─────────────────────────────────────────────────
|
|
@@ -1026,6 +1065,7 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
1026
1065
|
updatedAt: Date.parse(snap.updatedAt),
|
|
1027
1066
|
children: Object.create(null),
|
|
1028
1067
|
_childCount: 0,
|
|
1068
|
+
_sortedKeys: null,
|
|
1029
1069
|
};
|
|
1030
1070
|
for (const child of snap.children) {
|
|
1031
1071
|
if (child.type === "file") {
|
|
@@ -36,5 +36,7 @@ export interface InternalDirectoryNode extends InternalBaseNode {
|
|
|
36
36
|
children: Record<string, InternalNode>;
|
|
37
37
|
/** Cached child count — avoids O(n) Object.keys() on hot paths. */
|
|
38
38
|
_childCount: number;
|
|
39
|
+
/** Cached sorted child names — null means stale, rebuilt lazily by list(). */
|
|
40
|
+
_sortedKeys: string[] | null;
|
|
39
41
|
}
|
|
40
42
|
export {};
|
|
@@ -2,6 +2,11 @@ import type { InternalDirectoryNode, InternalNode } from "./internalTypes";
|
|
|
2
2
|
export declare function normalizePath(rawPath: string): string;
|
|
3
3
|
export declare function splitPath(normalizedPath: string): string[];
|
|
4
4
|
export declare function getNode(root: InternalDirectoryNode, targetPath: string): InternalNode;
|
|
5
|
+
/**
|
|
6
|
+
* Like getNode but skips normalization — caller must pass an already-normalized path.
|
|
7
|
+
* Avoids double normalizePath() when the caller has already normalized.
|
|
8
|
+
*/
|
|
9
|
+
export declare function getNodeNormalized(root: InternalDirectoryNode, normalized: string): InternalNode;
|
|
5
10
|
export declare function getParentDirectory(root: InternalDirectoryNode, targetPath: string, createIfMissing: boolean, createPath: (pathToCreate: string) => void): {
|
|
6
11
|
parent: InternalDirectoryNode;
|
|
7
12
|
name: string;
|
|
@@ -11,20 +11,33 @@ export function splitPath(normalizedPath) {
|
|
|
11
11
|
}
|
|
12
12
|
export function getNode(root, targetPath) {
|
|
13
13
|
const normalized = normalizePath(targetPath);
|
|
14
|
-
|
|
14
|
+
return getNodeNormalized(root, normalized);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Like getNode but skips normalization — caller must pass an already-normalized path.
|
|
18
|
+
* Avoids double normalizePath() when the caller has already normalized.
|
|
19
|
+
*/
|
|
20
|
+
export function getNodeNormalized(root, normalized) {
|
|
21
|
+
if (normalized === "/")
|
|
15
22
|
return root;
|
|
16
|
-
}
|
|
17
|
-
const parts = splitPath(normalized);
|
|
18
23
|
let current = root;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
if (
|
|
25
|
-
|
|
24
|
+
let i = 1; // skip leading "/"
|
|
25
|
+
while (i <= normalized.length) {
|
|
26
|
+
const slash = normalized.indexOf("/", i);
|
|
27
|
+
const end = slash === -1 ? normalized.length : slash;
|
|
28
|
+
const part = normalized.slice(i, end);
|
|
29
|
+
if (part) {
|
|
30
|
+
if (current.type !== "directory") {
|
|
31
|
+
throw new Error(`Path '${normalized}' does not exist.`);
|
|
32
|
+
}
|
|
33
|
+
const next = current.children[part];
|
|
34
|
+
if (!next)
|
|
35
|
+
throw new Error(`Path '${normalized}' does not exist.`);
|
|
36
|
+
current = next;
|
|
26
37
|
}
|
|
27
|
-
|
|
38
|
+
if (slash === -1)
|
|
39
|
+
break;
|
|
40
|
+
i = slash + 1;
|
|
28
41
|
}
|
|
29
42
|
return current;
|
|
30
43
|
}
|
|
@@ -94,18 +94,20 @@ export declare class VirtualPackageManager {
|
|
|
94
94
|
private readonly registryPath;
|
|
95
95
|
private readonly logPath;
|
|
96
96
|
private readonly aptLogPath;
|
|
97
|
+
private _loaded;
|
|
97
98
|
/**
|
|
98
99
|
* @param vfs Backing virtual filesystem for file I/O and dpkg status persistence.
|
|
99
100
|
* @param users User manager reference passed to `onInstall` hooks.
|
|
100
101
|
*/
|
|
101
102
|
constructor(vfs: VirtualFileSystem, users: VirtualUserManager);
|
|
103
|
+
/** Ensure dpkg/status is parsed. Called lazily on first package operation. */
|
|
104
|
+
private _ensureLoaded;
|
|
102
105
|
/**
|
|
103
106
|
* Loads installed package state from `/var/lib/dpkg/status` in the VFS.
|
|
104
|
-
*
|
|
105
|
-
* Called automatically by `VirtualShell` after `bootstrapLinuxRootfs`.
|
|
106
107
|
* Safe to call again to reload state after a snapshot restore.
|
|
107
108
|
*/
|
|
108
109
|
load(): void;
|
|
110
|
+
private _parseStatus;
|
|
109
111
|
/** Persist installed state to /var/lib/dpkg/status. */
|
|
110
112
|
private persist;
|
|
111
113
|
private parseFields;
|
|
@@ -502,6 +502,10 @@ const PACKAGE_REGISTRY = [
|
|
|
502
502
|
],
|
|
503
503
|
}
|
|
504
504
|
];
|
|
505
|
+
// O(1) name lookup — built once at module load, avoids O(n) linear scan per command
|
|
506
|
+
const _REGISTRY_MAP = new Map(PACKAGE_REGISTRY.map((p) => [p.name.toLowerCase(), p]));
|
|
507
|
+
// Pre-sorted for listAvailable() — avoids O(n log n) sort on every apt list/search
|
|
508
|
+
const _REGISTRY_SORTED = PACKAGE_REGISTRY.slice().sort((a, b) => a.name.localeCompare(b.name));
|
|
505
509
|
/**
|
|
506
510
|
* Pure-TypeScript APT/dpkg package manager backed by a built-in registry.
|
|
507
511
|
*
|
|
@@ -526,6 +530,7 @@ export class VirtualPackageManager {
|
|
|
526
530
|
registryPath = "/var/lib/dpkg/status";
|
|
527
531
|
logPath = "/var/log/dpkg.log";
|
|
528
532
|
aptLogPath = "/var/log/apt/history.log";
|
|
533
|
+
_loaded = false;
|
|
529
534
|
/**
|
|
530
535
|
* @param vfs Backing virtual filesystem for file I/O and dpkg status persistence.
|
|
531
536
|
* @param users User manager reference passed to `onInstall` hooks.
|
|
@@ -534,13 +539,22 @@ export class VirtualPackageManager {
|
|
|
534
539
|
this.vfs = vfs;
|
|
535
540
|
this.users = users;
|
|
536
541
|
}
|
|
542
|
+
/** Ensure dpkg/status is parsed. Called lazily on first package operation. */
|
|
543
|
+
_ensureLoaded() {
|
|
544
|
+
if (this._loaded)
|
|
545
|
+
return;
|
|
546
|
+
this._loaded = true;
|
|
547
|
+
this._parseStatus();
|
|
548
|
+
}
|
|
537
549
|
/**
|
|
538
550
|
* Loads installed package state from `/var/lib/dpkg/status` in the VFS.
|
|
539
|
-
*
|
|
540
|
-
* Called automatically by `VirtualShell` after `bootstrapLinuxRootfs`.
|
|
541
551
|
* Safe to call again to reload state after a snapshot restore.
|
|
542
552
|
*/
|
|
543
553
|
load() {
|
|
554
|
+
this._loaded = false;
|
|
555
|
+
this._ensureLoaded();
|
|
556
|
+
}
|
|
557
|
+
_parseStatus() {
|
|
544
558
|
if (!this.vfs.exists(this.registryPath))
|
|
545
559
|
return;
|
|
546
560
|
const status = this.vfs.readFile(this.registryPath);
|
|
@@ -626,7 +640,7 @@ export class VirtualPackageManager {
|
|
|
626
640
|
* @returns The matching `PackageDefinition`, or `undefined` if not found.
|
|
627
641
|
*/
|
|
628
642
|
findInRegistry(name) {
|
|
629
|
-
return
|
|
643
|
+
return _REGISTRY_MAP.get(name.toLowerCase());
|
|
630
644
|
}
|
|
631
645
|
/**
|
|
632
646
|
* Returns all packages in the built-in registry, sorted alphabetically.
|
|
@@ -634,7 +648,7 @@ export class VirtualPackageManager {
|
|
|
634
648
|
* @returns Array of `PackageDefinition` entries.
|
|
635
649
|
*/
|
|
636
650
|
listAvailable() {
|
|
637
|
-
return
|
|
651
|
+
return _REGISTRY_SORTED;
|
|
638
652
|
}
|
|
639
653
|
/**
|
|
640
654
|
* Returns all currently installed packages, sorted alphabetically.
|
|
@@ -642,6 +656,7 @@ export class VirtualPackageManager {
|
|
|
642
656
|
* @returns Array of `InstalledPackage` records.
|
|
643
657
|
*/
|
|
644
658
|
listInstalled() {
|
|
659
|
+
this._ensureLoaded();
|
|
645
660
|
return [...this.installed.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
646
661
|
}
|
|
647
662
|
/**
|
|
@@ -650,6 +665,7 @@ export class VirtualPackageManager {
|
|
|
650
665
|
* @param name Package name (case-insensitive).
|
|
651
666
|
*/
|
|
652
667
|
isInstalled(name) {
|
|
668
|
+
this._ensureLoaded();
|
|
653
669
|
return this.installed.has(name.toLowerCase());
|
|
654
670
|
}
|
|
655
671
|
/**
|
|
@@ -658,6 +674,7 @@ export class VirtualPackageManager {
|
|
|
658
674
|
* Used by `neofetch` to populate the `Packages:` field.
|
|
659
675
|
*/
|
|
660
676
|
installedCount() {
|
|
677
|
+
this._ensureLoaded();
|
|
661
678
|
return this.installed.size;
|
|
662
679
|
}
|
|
663
680
|
/**
|
|
@@ -675,6 +692,7 @@ export class VirtualPackageManager {
|
|
|
675
692
|
* (`0` on success, `100` when a package is not found).
|
|
676
693
|
*/
|
|
677
694
|
install(names, opts = {}) {
|
|
695
|
+
this._ensureLoaded();
|
|
678
696
|
const lines = [];
|
|
679
697
|
const toInstall = [];
|
|
680
698
|
const notFound = [];
|
|
@@ -770,6 +788,7 @@ export class VirtualPackageManager {
|
|
|
770
788
|
* @returns Terminal-style `output` string and exit code (`0` on success).
|
|
771
789
|
*/
|
|
772
790
|
remove(names, opts = {}) {
|
|
791
|
+
this._ensureLoaded();
|
|
773
792
|
const lines = [];
|
|
774
793
|
const toRemove = [];
|
|
775
794
|
for (const name of names) {
|
|
@@ -833,6 +852,7 @@ export class VirtualPackageManager {
|
|
|
833
852
|
* @returns Multi-line metadata string, or `null` if not in the registry.
|
|
834
853
|
*/
|
|
835
854
|
show(name) {
|
|
855
|
+
this._ensureLoaded();
|
|
836
856
|
const def = this.findInRegistry(name);
|
|
837
857
|
if (!def)
|
|
838
858
|
return null;
|
|
@@ -29,6 +29,10 @@ export interface ShellProperties {
|
|
|
29
29
|
os: string;
|
|
30
30
|
/** CPU architecture label (e.g. `"x86_64"`, `"aarch64"`). */
|
|
31
31
|
arch: string;
|
|
32
|
+
/** Display resolution (e.g. `"1920x1080"`). */
|
|
33
|
+
resolution?: string;
|
|
34
|
+
/** GPU label (e.g. `"WebGL Renderer"`). */
|
|
35
|
+
gpu?: string;
|
|
32
36
|
}
|
|
33
37
|
/**
|
|
34
38
|
* Minimal VFS interface accepted by {@link VirtualShell} as a drop-in replacement
|
|
@@ -27,10 +27,7 @@ function isVirtualShellVfsLike(value) {
|
|
|
27
27
|
typeof candidate.exists === "function" &&
|
|
28
28
|
typeof candidate.stat === "function" &&
|
|
29
29
|
typeof candidate.list === "function" &&
|
|
30
|
-
typeof candidate.remove === "function"
|
|
31
|
-
typeof candidate.copy === "function" &&
|
|
32
|
-
typeof candidate.move === "function" &&
|
|
33
|
-
typeof candidate.touch === "function");
|
|
30
|
+
typeof candidate.remove === "function");
|
|
34
31
|
}
|
|
35
32
|
const defaultShellProperties = {
|
|
36
33
|
kernel: "1.0.0+itsrealfortune+1-amd64",
|
|
@@ -110,7 +107,6 @@ class VirtualShell extends EventEmitter {
|
|
|
110
107
|
// Store references to avoid TypeScript "used before assigned" errors
|
|
111
108
|
const vfs = this.vfs;
|
|
112
109
|
const users = this.users;
|
|
113
|
-
const pm = this.packageManager;
|
|
114
110
|
const shellProps = this.properties;
|
|
115
111
|
const shellHostname = this.hostname;
|
|
116
112
|
const startTime = this.startTime;
|
|
@@ -120,8 +116,6 @@ class VirtualShell extends EventEmitter {
|
|
|
120
116
|
await users.initialize();
|
|
121
117
|
// Bootstrap Linux rootfs (idempotent)
|
|
122
118
|
bootstrapLinuxRootfs(vfs, users, shellHostname, shellProps, startTime);
|
|
123
|
-
// Load installed packages from dpkg status
|
|
124
|
-
pm.load();
|
|
125
119
|
this.emit("initialized");
|
|
126
120
|
})();
|
|
127
121
|
}
|
|
@@ -14,6 +14,7 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
|
|
|
14
14
|
let cwd = userHome(authUser);
|
|
15
15
|
let pendingHeredoc = null;
|
|
16
16
|
const shellEnv = makeDefaultEnv(authUser, hostname);
|
|
17
|
+
const sessionStack = [];
|
|
17
18
|
let nanoSession = null;
|
|
18
19
|
let pendingSudo = null;
|
|
19
20
|
const buildCurrentPrompt = () => {
|
|
@@ -112,6 +113,7 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
|
|
|
112
113
|
stream.write(`${toTtyLines(result.stderr)}\r\n`);
|
|
113
114
|
}
|
|
114
115
|
if (result.switchUser) {
|
|
116
|
+
sessionStack.push({ authUser, cwd });
|
|
115
117
|
authUser = result.switchUser;
|
|
116
118
|
cwd = result.nextCwd ?? userHome(authUser);
|
|
117
119
|
shell.users.updateSession(sessionId, authUser, remoteAddress);
|
|
@@ -396,13 +398,24 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
|
|
|
396
398
|
cursorPos = 0;
|
|
397
399
|
historyIndex = null;
|
|
398
400
|
historyDraft = "";
|
|
399
|
-
stream.write("bye\r\n");
|
|
400
|
-
pushHistory("bye");
|
|
401
|
-
// WAL: checkpoint handled by auto-flush timer
|
|
402
401
|
stream.write("logout\r\n");
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
402
|
+
if (sessionStack.length > 0) {
|
|
403
|
+
const prev = sessionStack.pop();
|
|
404
|
+
authUser = prev.authUser;
|
|
405
|
+
cwd = prev.cwd;
|
|
406
|
+
shellEnv.vars.USER = authUser;
|
|
407
|
+
shellEnv.vars.LOGNAME = authUser;
|
|
408
|
+
shellEnv.vars.HOME = userHome(authUser);
|
|
409
|
+
shellEnv.vars.PWD = cwd;
|
|
410
|
+
shell.users.updateSession(sessionId, authUser, remoteAddress);
|
|
411
|
+
renderLine();
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
414
|
+
stream.exit(0);
|
|
415
|
+
stream.end();
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
continue;
|
|
406
419
|
}
|
|
407
420
|
if (ch === "\t") {
|
|
408
421
|
handleTabCompletion();
|
|
@@ -598,16 +611,33 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
|
|
|
598
611
|
}
|
|
599
612
|
if (result.closeSession) {
|
|
600
613
|
stream.write("logout\r\n");
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
614
|
+
if (sessionStack.length > 0) {
|
|
615
|
+
const prev = sessionStack.pop();
|
|
616
|
+
authUser = prev.authUser;
|
|
617
|
+
cwd = prev.cwd;
|
|
618
|
+
shellEnv.vars.USER = authUser;
|
|
619
|
+
shellEnv.vars.LOGNAME = authUser;
|
|
620
|
+
shellEnv.vars.HOME = userHome(authUser);
|
|
621
|
+
shellEnv.vars.PWD = cwd;
|
|
622
|
+
shell.users.updateSession(sessionId, authUser, remoteAddress);
|
|
623
|
+
}
|
|
624
|
+
else {
|
|
625
|
+
stream.exit(result.exitCode ?? 0);
|
|
626
|
+
stream.end();
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
604
629
|
}
|
|
605
|
-
if (result.nextCwd) {
|
|
630
|
+
if (result.nextCwd && !result.closeSession) {
|
|
606
631
|
cwd = result.nextCwd;
|
|
607
632
|
}
|
|
608
633
|
if (result.switchUser) {
|
|
634
|
+
sessionStack.push({ authUser, cwd });
|
|
609
635
|
authUser = result.switchUser;
|
|
610
636
|
cwd = result.nextCwd ?? userHome(authUser);
|
|
637
|
+
shellEnv.vars.USER = authUser;
|
|
638
|
+
shellEnv.vars.LOGNAME = authUser;
|
|
639
|
+
shellEnv.vars.HOME = userHome(authUser);
|
|
640
|
+
shellEnv.vars.PWD = cwd;
|
|
611
641
|
shell.users.updateSession(sessionId, authUser, remoteAddress);
|
|
612
642
|
lineBuffer = "";
|
|
613
643
|
cursorPos = 0;
|