typescript-virtual-container 0.1.0
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/.github/ISSUE_TEMPLATE/bug_report.yml +50 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +31 -0
- package/.github/dependabot.yml +27 -0
- package/.github/pull_request_template.md +21 -0
- package/.github/workflows/create-pull-request.yml +83 -0
- package/.github/workflows/test-battery.yml +57 -0
- package/CHANGELOG.md +27 -0
- package/CODE_OF_CONDUCT.md +39 -0
- package/CONTRIBUTING.md +59 -0
- package/LICENSE +21 -0
- package/README.md +1283 -0
- package/SECURITY.md +33 -0
- package/biome.json +20 -0
- package/bun.lock +99 -0
- package/package.json +38 -0
- package/src/SSHMimic/client.ts +248 -0
- package/src/SSHMimic/commands/adduser.ts +22 -0
- package/src/SSHMimic/commands/cat.ts +16 -0
- package/src/SSHMimic/commands/cd.ts +20 -0
- package/src/SSHMimic/commands/clear.ts +7 -0
- package/src/SSHMimic/commands/curl.ts +27 -0
- package/src/SSHMimic/commands/deluser.ts +19 -0
- package/src/SSHMimic/commands/exit.ts +7 -0
- package/src/SSHMimic/commands/help.ts +9 -0
- package/src/SSHMimic/commands/helpers.ts +137 -0
- package/src/SSHMimic/commands/hostname.ts +7 -0
- package/src/SSHMimic/commands/htop.ts +13 -0
- package/src/SSHMimic/commands/index.ts +120 -0
- package/src/SSHMimic/commands/ls.ts +14 -0
- package/src/SSHMimic/commands/mkdir.ts +17 -0
- package/src/SSHMimic/commands/nano.ts +30 -0
- package/src/SSHMimic/commands/pwd.ts +7 -0
- package/src/SSHMimic/commands/rm.ts +26 -0
- package/src/SSHMimic/commands/su.ts +31 -0
- package/src/SSHMimic/commands/sudo.ts +90 -0
- package/src/SSHMimic/commands/touch.ts +20 -0
- package/src/SSHMimic/commands/tree.ts +11 -0
- package/src/SSHMimic/commands/wget.ts +33 -0
- package/src/SSHMimic/commands/who.ts +18 -0
- package/src/SSHMimic/commands/whoami.ts +7 -0
- package/src/SSHMimic/exec.ts +37 -0
- package/src/SSHMimic/hostKey.ts +21 -0
- package/src/SSHMimic/index.ts +203 -0
- package/src/SSHMimic/loginFormat.ts +10 -0
- package/src/SSHMimic/prompt.ts +14 -0
- package/src/SSHMimic/shell.ts +740 -0
- package/src/SSHMimic/users.ts +336 -0
- package/src/VirtualFileSystem.ts +420 -0
- package/src/index.ts +34 -0
- package/src/standalone.ts +14 -0
- package/src/types/commands.ts +98 -0
- package/src/types/streams.ts +32 -0
- package/src/types/tar-stream.d.ts +38 -0
- package/src/types/vfs.ts +81 -0
- package/src/vfs/archive.ts +74 -0
- package/src/vfs/internalTypes.ts +19 -0
- package/src/vfs/path.ts +74 -0
- package/src/vfs/snapshot.ts +84 -0
- package/src/vfs/tree.ts +34 -0
- package/tsconfig.json +31 -0
package/SECURITY.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Supported Versions
|
|
4
|
+
|
|
5
|
+
Security fixes are prioritized for the latest published release.
|
|
6
|
+
|
|
7
|
+
## Reporting a Vulnerability
|
|
8
|
+
|
|
9
|
+
Please report vulnerabilities privately.
|
|
10
|
+
|
|
11
|
+
Preferred channel:
|
|
12
|
+
|
|
13
|
+
- Open a private GitHub security advisory for this repository.
|
|
14
|
+
|
|
15
|
+
If advisory tooling is unavailable, open an issue with minimal details and request private contact.
|
|
16
|
+
Do not include exploit details or sensitive information in a public issue.
|
|
17
|
+
|
|
18
|
+
## What to Include
|
|
19
|
+
|
|
20
|
+
Please include:
|
|
21
|
+
|
|
22
|
+
- A clear description of the issue
|
|
23
|
+
- Impact assessment
|
|
24
|
+
- Reproduction steps or proof of concept
|
|
25
|
+
- Affected versions
|
|
26
|
+
- Suggested remediation if available
|
|
27
|
+
|
|
28
|
+
## Disclosure Process
|
|
29
|
+
|
|
30
|
+
- We will acknowledge receipt as soon as possible.
|
|
31
|
+
- We will validate, triage, and work on a fix.
|
|
32
|
+
- We may request additional technical details.
|
|
33
|
+
- We will publish a patch and release notes after remediation.
|
package/biome.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"linter": {
|
|
3
|
+
"rules": {
|
|
4
|
+
"suspicious": {
|
|
5
|
+
"noDebugger": "off"
|
|
6
|
+
},
|
|
7
|
+
"style": {
|
|
8
|
+
"noShoutyConstants": "warn",
|
|
9
|
+
"useNamingConvention": "error",
|
|
10
|
+
"noNonNullAssertion": "off"
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"assist": {
|
|
15
|
+
"source": {
|
|
16
|
+
"autoImport": "on",
|
|
17
|
+
"importModuleSpecifierPreference": "relative"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
package/bun.lock
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lockfileVersion": 1,
|
|
3
|
+
"configVersion": 1,
|
|
4
|
+
"workspaces": {
|
|
5
|
+
"": {
|
|
6
|
+
"name": "virtual-env-js",
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"ssh2": "^1.17.0",
|
|
9
|
+
"tar-stream": "^3.1.8",
|
|
10
|
+
},
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"@biomejs/biome": "^2.4.11",
|
|
13
|
+
"@types/bun": "latest",
|
|
14
|
+
"@types/node": "^25.6.0",
|
|
15
|
+
"@types/ssh2": "^1.15.5",
|
|
16
|
+
},
|
|
17
|
+
"peerDependencies": {
|
|
18
|
+
"typescript": "^5",
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
"packages": {
|
|
23
|
+
"@biomejs/biome": ["@biomejs/biome@2.4.11", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.11", "@biomejs/cli-darwin-x64": "2.4.11", "@biomejs/cli-linux-arm64": "2.4.11", "@biomejs/cli-linux-arm64-musl": "2.4.11", "@biomejs/cli-linux-x64": "2.4.11", "@biomejs/cli-linux-x64-musl": "2.4.11", "@biomejs/cli-win32-arm64": "2.4.11", "@biomejs/cli-win32-x64": "2.4.11" }, "bin": { "biome": "bin/biome" } }, "sha512-nWxHX8tf3Opb/qRgZpBbsTOqOodkbrkJ7S+JxJAruxOReaDPPmPuLBAGQ8vigyUgo0QBB+oQltNEAvalLcjggA=="],
|
|
24
|
+
|
|
25
|
+
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-wOt+ed+L2dgZanWyL6i29qlXMc088N11optzpo10peayObBaAshbTcxKUchzEMp9QSY8rh5h6VfAFE3WTS1rqg=="],
|
|
26
|
+
|
|
27
|
+
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-gZ6zR8XmZlExfi/Pz/PffmdpWOQ8Qhy7oBztgkR8/ylSRyLwfRPSadmiVCV8WQ8PoJ2MWUy2fgID9zmtgUUJmw=="],
|
|
28
|
+
|
|
29
|
+
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-avdJaEElXrKceK0va9FkJ4P5ci3N01TGkc6ni3P8l3BElqbOz42Wg2IyX3gbh0ZLEd4HVKEIrmuVu/AMuSeFFA=="],
|
|
30
|
+
|
|
31
|
+
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-+Sbo1OAmlegtdwqFE8iOxFIWLh1B3OEgsuZfBpyyN/kWuqZ8dx9ZEes6zVnDMo+zRHF2wLynRVhoQmV7ohxl2Q=="],
|
|
32
|
+
|
|
33
|
+
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.11", "", { "os": "linux", "cpu": "x64" }, "sha512-TagWV0iomp5LnEnxWFg4nQO+e52Fow349vaX0Q/PIcX6Zhk4GGBgp3qqZ8PVkpC+cuehRctMf3+6+FgQ8jCEFQ=="],
|
|
34
|
+
|
|
35
|
+
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.11", "", { "os": "linux", "cpu": "x64" }, "sha512-bexd2IklK7ZgPhrz6jXzpIL6dEAH9MlJU1xGTrypx+FICxrXUp4CqtwfiuoDKse+UlgAlWtzML3jrMqeEAHEhA=="],
|
|
36
|
+
|
|
37
|
+
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-RJhaTnY8byzxDt4bDVb7AFPHkPcjOPK3xBip4ZRTrN3TEfyhjLRm3r3mqknqydgVTB74XG8l4jMLwEACEeihVg=="],
|
|
38
|
+
|
|
39
|
+
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.11", "", { "os": "win32", "cpu": "x64" }, "sha512-A8D3JM/00C2KQgUV3oj8Ba15EHEYwebAGCy5Sf9GAjr5Y3+kJIYOiESoqRDeuRZueuMdCsbLZIUqmPhpYXJE9A=="],
|
|
40
|
+
|
|
41
|
+
"@types/bun": ["@types/bun@1.3.12", "", { "dependencies": { "bun-types": "1.3.12" } }, "sha512-DBv81elK+/VSwXHDlnH3Qduw+KxkTIWi7TXkAeh24zpi5l0B2kUg9Ga3tb4nJaPcOFswflgi/yAvMVBPrxMB+A=="],
|
|
42
|
+
|
|
43
|
+
"@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="],
|
|
44
|
+
|
|
45
|
+
"@types/ssh2": ["@types/ssh2@1.15.5", "", { "dependencies": { "@types/node": "^18.11.18" } }, "sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ=="],
|
|
46
|
+
|
|
47
|
+
"asn1": ["asn1@0.2.6", "", { "dependencies": { "safer-buffer": "~2.1.0" } }, "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ=="],
|
|
48
|
+
|
|
49
|
+
"b4a": ["b4a@1.8.0", "", { "peerDependencies": { "react-native-b4a": "*" }, "optionalPeers": ["react-native-b4a"] }, "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg=="],
|
|
50
|
+
|
|
51
|
+
"bare-events": ["bare-events@2.8.2", "", { "peerDependencies": { "bare-abort-controller": "*" }, "optionalPeers": ["bare-abort-controller"] }, "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ=="],
|
|
52
|
+
|
|
53
|
+
"bare-fs": ["bare-fs@4.7.0", "", { "dependencies": { "bare-events": "^2.5.4", "bare-path": "^3.0.0", "bare-stream": "^2.6.4", "bare-url": "^2.2.2", "fast-fifo": "^1.3.2" }, "peerDependencies": { "bare-buffer": "*" }, "optionalPeers": ["bare-buffer"] }, "sha512-xzqKsCFxAek9aezYhjJuJRXBIaYlg/0OGDTZp+T8eYmYMlm66cs6cYko02drIyjN2CBbi+I6L7YfXyqpqtKRXA=="],
|
|
54
|
+
|
|
55
|
+
"bare-os": ["bare-os@3.8.7", "", {}, "sha512-G4Gr1UsGeEy2qtDTZwL7JFLo2wapUarz7iTMcYcMFdS89AIQuBoyjgXZz0Utv7uHs3xA9LckhVbeBi8lEQrC+w=="],
|
|
56
|
+
|
|
57
|
+
"bare-path": ["bare-path@3.0.0", "", { "dependencies": { "bare-os": "^3.0.1" } }, "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw=="],
|
|
58
|
+
|
|
59
|
+
"bare-stream": ["bare-stream@2.13.0", "", { "dependencies": { "streamx": "^2.25.0", "teex": "^1.0.1" }, "peerDependencies": { "bare-abort-controller": "*", "bare-buffer": "*", "bare-events": "*" }, "optionalPeers": ["bare-abort-controller", "bare-buffer", "bare-events"] }, "sha512-3zAJRZMDFGjdn+RVnNpF9kuELw+0Fl3lpndM4NcEOhb9zwtSo/deETfuIwMSE5BXanA0FrN1qVjffGwAg2Y7EA=="],
|
|
60
|
+
|
|
61
|
+
"bare-url": ["bare-url@2.4.0", "", { "dependencies": { "bare-path": "^3.0.0" } }, "sha512-NSTU5WN+fy/L0DDenfE8SXQna4voXuW0FHM7wH8i3/q9khUSchfPbPezO4zSFMnDGIf9YE+mt/RWhZgNRKRIXA=="],
|
|
62
|
+
|
|
63
|
+
"bcrypt-pbkdf": ["bcrypt-pbkdf@1.0.2", "", { "dependencies": { "tweetnacl": "^0.14.3" } }, "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w=="],
|
|
64
|
+
|
|
65
|
+
"buildcheck": ["buildcheck@0.0.7", "", {}, "sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA=="],
|
|
66
|
+
|
|
67
|
+
"bun-types": ["bun-types@1.3.12", "", { "dependencies": { "@types/node": "*" } }, "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA=="],
|
|
68
|
+
|
|
69
|
+
"cpu-features": ["cpu-features@0.0.10", "", { "dependencies": { "buildcheck": "~0.0.6", "nan": "^2.19.0" } }, "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA=="],
|
|
70
|
+
|
|
71
|
+
"events-universal": ["events-universal@1.0.1", "", { "dependencies": { "bare-events": "^2.7.0" } }, "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw=="],
|
|
72
|
+
|
|
73
|
+
"fast-fifo": ["fast-fifo@1.3.2", "", {}, "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="],
|
|
74
|
+
|
|
75
|
+
"nan": ["nan@2.26.2", "", {}, "sha512-0tTvBTYkt3tdGw22nrAy50x7gpbGCCFH3AFcyS5WiUu7Eu4vWlri1woE6qHBSfy11vksDqkiwjOnlR7WV8G1Hw=="],
|
|
76
|
+
|
|
77
|
+
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
|
78
|
+
|
|
79
|
+
"ssh2": ["ssh2@1.17.0", "", { "dependencies": { "asn1": "^0.2.6", "bcrypt-pbkdf": "^1.0.2" }, "optionalDependencies": { "cpu-features": "~0.0.10", "nan": "^2.23.0" } }, "sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ=="],
|
|
80
|
+
|
|
81
|
+
"streamx": ["streamx@2.25.0", "", { "dependencies": { "events-universal": "^1.0.0", "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" } }, "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg=="],
|
|
82
|
+
|
|
83
|
+
"tar-stream": ["tar-stream@3.1.8", "", { "dependencies": { "b4a": "^1.6.4", "bare-fs": "^4.5.5", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ=="],
|
|
84
|
+
|
|
85
|
+
"teex": ["teex@1.0.1", "", { "dependencies": { "streamx": "^2.12.5" } }, "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg=="],
|
|
86
|
+
|
|
87
|
+
"text-decoder": ["text-decoder@1.2.7", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ=="],
|
|
88
|
+
|
|
89
|
+
"tweetnacl": ["tweetnacl@0.14.5", "", {}, "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="],
|
|
90
|
+
|
|
91
|
+
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
|
92
|
+
|
|
93
|
+
"undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="],
|
|
94
|
+
|
|
95
|
+
"@types/ssh2/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="],
|
|
96
|
+
|
|
97
|
+
"@types/ssh2/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
|
|
98
|
+
}
|
|
99
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "typescript-virtual-container",
|
|
3
|
+
"description": "In-memory SSH server with virtual filesystem and typed programmatic API",
|
|
4
|
+
"module": "src/index.ts",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"version": "0.1.0",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"ssh",
|
|
10
|
+
"virtual-filesystem",
|
|
11
|
+
"typescript",
|
|
12
|
+
"testing",
|
|
13
|
+
"automation",
|
|
14
|
+
"shell"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"postinstall": "rm -f node_modules/ssh2/lib/protocol/crypto/build/Release/sshcrypto.node && rm -rf node_modules/cpu-features",
|
|
18
|
+
"format": "bunx --bun @biomejs/biome format --write ./src",
|
|
19
|
+
"check": "bunx --bun @biomejs/biome check ./src",
|
|
20
|
+
"lint": "bunx --bun @biomejs/biome lint ./src",
|
|
21
|
+
"lint:write": "bunx --bun @biomejs/biome lint --write ./src",
|
|
22
|
+
"test": "bunx --bun @biomejs/biome test ./src",
|
|
23
|
+
"deploy:npm": "npm publish --access public"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@biomejs/biome": "^2.4.11",
|
|
27
|
+
"@types/bun": "latest",
|
|
28
|
+
"@types/node": "^25.6.0",
|
|
29
|
+
"@types/ssh2": "^1.15.5"
|
|
30
|
+
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"typescript": "^5"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"ssh2": "^1.17.0",
|
|
36
|
+
"tar-stream": "^3.1.8"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import type { CommandResult } from "../types/commands";
|
|
2
|
+
import { runCommand } from "./commands";
|
|
3
|
+
import type { SshMimic } from "./index";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Programmatic SSH client to execute shell commands as a specific user.
|
|
7
|
+
*
|
|
8
|
+
* Maintains connection state (cwd) across multiple command invocations.
|
|
9
|
+
* All commands execute with implicit authentication (no password required).
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* const ssh = new SshMimic(2222, "myhost");
|
|
14
|
+
* await ssh.start();
|
|
15
|
+
*
|
|
16
|
+
* const client = new SshClient(ssh, "alice");
|
|
17
|
+
* const result = await client.cd("/tmp");
|
|
18
|
+
* const list = await client.ls();
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export class SshClient {
|
|
22
|
+
private currentCwd = "/";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Creates SSH client bound to user.
|
|
26
|
+
*
|
|
27
|
+
* @param ssh Parent SSH server instance (must be started).
|
|
28
|
+
* @param username Login user for all commands.
|
|
29
|
+
*/
|
|
30
|
+
constructor(
|
|
31
|
+
private ssh: SshMimic,
|
|
32
|
+
private username: string,
|
|
33
|
+
) {}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Executes raw shell command.
|
|
37
|
+
*
|
|
38
|
+
* @param command Unparsed command line.
|
|
39
|
+
* @returns Command result with stdout/stderr/exitCode.
|
|
40
|
+
*/
|
|
41
|
+
async exec(command: string): Promise<CommandResult> {
|
|
42
|
+
const vfs = this.ssh.getVfs();
|
|
43
|
+
const users = this.ssh.getUsers();
|
|
44
|
+
const hostname = this.ssh.getHostname();
|
|
45
|
+
|
|
46
|
+
if (!vfs || !users) {
|
|
47
|
+
throw new Error("SSH client not started");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const result = runCommand(
|
|
51
|
+
command,
|
|
52
|
+
this.username,
|
|
53
|
+
hostname,
|
|
54
|
+
users,
|
|
55
|
+
"exec",
|
|
56
|
+
this.currentCwd,
|
|
57
|
+
vfs,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// Handle async results
|
|
61
|
+
if (result instanceof Promise) {
|
|
62
|
+
return await result;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Lists directory contents.
|
|
70
|
+
*
|
|
71
|
+
* @param path Target directory, defaults to cwd.
|
|
72
|
+
* @returns Result with directory listing in stdout.
|
|
73
|
+
*/
|
|
74
|
+
async ls(path?: string): Promise<CommandResult> {
|
|
75
|
+
const target = path ?? ".";
|
|
76
|
+
return this.exec(`ls ${target}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Prints current working directory.
|
|
81
|
+
*
|
|
82
|
+
* @returns Result with cwd path in stdout.
|
|
83
|
+
*/
|
|
84
|
+
async pwd(): Promise<CommandResult> {
|
|
85
|
+
return this.exec("pwd");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Changes working directory.
|
|
90
|
+
*
|
|
91
|
+
* @param path Target directory path.
|
|
92
|
+
* @returns Result; updates internal cwd on success.
|
|
93
|
+
*/
|
|
94
|
+
async cd(path: string): Promise<CommandResult> {
|
|
95
|
+
const result = await this.exec(`cd ${path}`);
|
|
96
|
+
if (result.nextCwd && result.exitCode !== 1) {
|
|
97
|
+
this.currentCwd = result.nextCwd;
|
|
98
|
+
}
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Reads file content.
|
|
104
|
+
*
|
|
105
|
+
* @param path File path.
|
|
106
|
+
* @returns Result with file content in stdout.
|
|
107
|
+
*/
|
|
108
|
+
async cat(path: string): Promise<CommandResult> {
|
|
109
|
+
return this.exec(`cat ${path}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Creates directory.
|
|
114
|
+
*
|
|
115
|
+
* @param path Directory path.
|
|
116
|
+
* @param recursive When true, create parents.
|
|
117
|
+
* @returns Result from mkdir command.
|
|
118
|
+
*/
|
|
119
|
+
async mkdir(path: string, recursive = false): Promise<CommandResult> {
|
|
120
|
+
const flag = recursive ? "-p " : "";
|
|
121
|
+
return this.exec(`mkdir ${flag}${path}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Creates file (empty).
|
|
126
|
+
*
|
|
127
|
+
* @param path File path.
|
|
128
|
+
* @returns Result from touch command.
|
|
129
|
+
*/
|
|
130
|
+
async touch(path: string): Promise<CommandResult> {
|
|
131
|
+
return this.exec(`touch ${path}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Removes file or directory.
|
|
136
|
+
*
|
|
137
|
+
* @param path Target path.
|
|
138
|
+
* @param recursive When true, delete directory tree.
|
|
139
|
+
* @returns Result from rm command.
|
|
140
|
+
*/
|
|
141
|
+
async rm(path: string, recursive = false): Promise<CommandResult> {
|
|
142
|
+
const flag = recursive ? "-r " : "";
|
|
143
|
+
return this.exec(`rm ${flag}${path}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Writes file content.
|
|
148
|
+
*
|
|
149
|
+
* @param path Target file path.
|
|
150
|
+
* @param content Text to write.
|
|
151
|
+
* @returns Result from touch/write simulation.
|
|
152
|
+
*/
|
|
153
|
+
async writeFile(path: string, content: string): Promise<CommandResult> {
|
|
154
|
+
const vfs = this.ssh.getVfs();
|
|
155
|
+
if (!vfs) {
|
|
156
|
+
throw new Error("SSH client not started");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
vfs.writeFile(path, content);
|
|
161
|
+
return { stdout: `File '${path}' written`, exitCode: 0 };
|
|
162
|
+
} catch (error) {
|
|
163
|
+
return {
|
|
164
|
+
stderr: `Failed to write '${path}': ${error instanceof Error ? error.message : String(error)}`,
|
|
165
|
+
exitCode: 1,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Reads file content programmatically.
|
|
172
|
+
*
|
|
173
|
+
* @param path Target file path.
|
|
174
|
+
* @returns File content as string or error in result.
|
|
175
|
+
*/
|
|
176
|
+
async readFile(path: string): Promise<CommandResult> {
|
|
177
|
+
const vfs = this.ssh.getVfs();
|
|
178
|
+
if (!vfs) {
|
|
179
|
+
throw new Error("SSH client not started");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
const content = vfs.readFile(path);
|
|
184
|
+
return { stdout: content, exitCode: 0 };
|
|
185
|
+
} catch (error) {
|
|
186
|
+
return {
|
|
187
|
+
stderr: `Failed to read '${path}': ${error instanceof Error ? error.message : String(error)}`,
|
|
188
|
+
exitCode: 1,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Gets current working directory.
|
|
195
|
+
*
|
|
196
|
+
* @returns Normalized cwd path.
|
|
197
|
+
*/
|
|
198
|
+
getCwd(): string {
|
|
199
|
+
return this.currentCwd;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Gets logged-in username.
|
|
204
|
+
*
|
|
205
|
+
* @returns Associated username.
|
|
206
|
+
*/
|
|
207
|
+
getUsername(): string {
|
|
208
|
+
return this.username;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Renders tree view of directory.
|
|
213
|
+
*
|
|
214
|
+
* @param path Target directory, defaults to cwd.
|
|
215
|
+
* @returns Result with ASCII tree in stdout.
|
|
216
|
+
*/
|
|
217
|
+
async tree(path?: string): Promise<CommandResult> {
|
|
218
|
+
const target = path ?? ".";
|
|
219
|
+
return this.exec(`tree ${target}`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Shows current user.
|
|
224
|
+
*
|
|
225
|
+
* @returns Result from whoami command.
|
|
226
|
+
*/
|
|
227
|
+
async whoami(): Promise<CommandResult> {
|
|
228
|
+
return this.exec("whoami");
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Shows hostname.
|
|
233
|
+
*
|
|
234
|
+
* @returns Result from hostname command.
|
|
235
|
+
*/
|
|
236
|
+
async hostname(): Promise<CommandResult> {
|
|
237
|
+
return this.exec("hostname");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Lists active users/sessions.
|
|
242
|
+
*
|
|
243
|
+
* @returns Result from who command.
|
|
244
|
+
*/
|
|
245
|
+
async who(): Promise<CommandResult> {
|
|
246
|
+
return this.exec("who");
|
|
247
|
+
}
|
|
248
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ShellModule } from "../../types/commands";
|
|
2
|
+
|
|
3
|
+
export const adduserCommand: ShellModule = {
|
|
4
|
+
name: "adduser",
|
|
5
|
+
params: ["<username> <password>"],
|
|
6
|
+
run: async ({ authUser, users, args }) => {
|
|
7
|
+
if (authUser !== "root") {
|
|
8
|
+
return { stderr: "adduser: permission denied", exitCode: 1 };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const [username, password] = args;
|
|
12
|
+
if (!username || !password) {
|
|
13
|
+
return {
|
|
14
|
+
stderr: "adduser: usage: adduser <username> <password>",
|
|
15
|
+
exitCode: 1,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
await users.addUser(username, password);
|
|
20
|
+
return { stdout: `adduser: user '${username}' created`, exitCode: 0 };
|
|
21
|
+
},
|
|
22
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ShellModule } from "../../types/commands";
|
|
2
|
+
import { resolveReadablePath } from "./helpers";
|
|
3
|
+
|
|
4
|
+
export const catCommand: ShellModule = {
|
|
5
|
+
name: "cat",
|
|
6
|
+
params: ["<file>"],
|
|
7
|
+
run: ({ vfs, cwd, args }) => {
|
|
8
|
+
const fileArg = args[0];
|
|
9
|
+
if (!fileArg) {
|
|
10
|
+
return { stderr: "cat: missing file operand", exitCode: 1 };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const target = resolveReadablePath(vfs, cwd, fileArg);
|
|
14
|
+
return { stdout: vfs.readFile(target), exitCode: 0 };
|
|
15
|
+
},
|
|
16
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ShellModule } from "../../types/commands";
|
|
2
|
+
import { resolvePath } from "./helpers";
|
|
3
|
+
|
|
4
|
+
export const cdCommand: ShellModule = {
|
|
5
|
+
name: "cd",
|
|
6
|
+
params: ["[path]"],
|
|
7
|
+
run: ({ vfs, cwd, args, mode }) => {
|
|
8
|
+
const target = resolvePath(cwd, args[0] ?? "/virtual-env-js");
|
|
9
|
+
const stats = vfs.stat(target);
|
|
10
|
+
if (stats.type !== "directory") {
|
|
11
|
+
return { stderr: `cd: not a directory: ${target}`, exitCode: 1 };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (mode === "exec") {
|
|
15
|
+
return { exitCode: 0 };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return { nextCwd: target, exitCode: 0 };
|
|
19
|
+
},
|
|
20
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ShellModule } from "../../types/commands";
|
|
2
|
+
import { fetchResource, parseOutputPath, resolvePath } from "./helpers";
|
|
3
|
+
|
|
4
|
+
export const curlCommand: ShellModule = {
|
|
5
|
+
name: "curl",
|
|
6
|
+
params: ["[-o file] <url>"],
|
|
7
|
+
run: async ({ vfs, cwd, args }) => {
|
|
8
|
+
const { outputPath, inputArgs } = parseOutputPath(args);
|
|
9
|
+
const url = inputArgs[0];
|
|
10
|
+
|
|
11
|
+
if (!url) {
|
|
12
|
+
return { stderr: "curl: missing URL", exitCode: 1 };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const result = await fetchResource(url);
|
|
16
|
+
if (result.status >= 400) {
|
|
17
|
+
return { stderr: `curl: HTTP ${result.status}`, exitCode: 22 };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (outputPath) {
|
|
21
|
+
vfs.writeFile(resolvePath(cwd, outputPath), result.text);
|
|
22
|
+
return { exitCode: 0 };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return { stdout: result.text, exitCode: 0 };
|
|
26
|
+
},
|
|
27
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ShellModule } from "../../types/commands";
|
|
2
|
+
|
|
3
|
+
export const deluserCommand: ShellModule = {
|
|
4
|
+
name: "deluser",
|
|
5
|
+
params: ["<username>"],
|
|
6
|
+
run: async ({ authUser, users, args }) => {
|
|
7
|
+
if (authUser !== "root") {
|
|
8
|
+
return { stderr: "deluser: permission denied", exitCode: 1 };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const [username] = args;
|
|
12
|
+
if (!username) {
|
|
13
|
+
return { stderr: "deluser: usage: deluser <username>", exitCode: 1 };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
await users.deleteUser(username);
|
|
17
|
+
return { stdout: `deluser: user '${username}' deleted`, exitCode: 0 };
|
|
18
|
+
},
|
|
19
|
+
};
|