typescript-virtual-container 1.3.1 → 1.3.2
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 +61 -1
- package/builds/self-standalone.js +481 -0
- package/builds/self-standalone.js.map +7 -0
- package/{standalone-wo-sftp.js → builds/standalone-wo-sftp.js} +137 -137
- package/{standalone-wo-sftp.js.map → builds/standalone-wo-sftp.js.map} +4 -4
- package/{standalone.js → builds/standalone.js} +176 -176
- package/{standalone.js.map → builds/standalone.js.map} +4 -4
- package/builds/web-full-api.min.js +13 -0
- package/builds/web-full-api.min.js.map +7 -0
- package/builds/web-iife.min.js +13 -0
- package/builds/web-iife.min.js.map +7 -0
- package/builds/web.min.js +13 -0
- package/builds/web.min.js.map +7 -0
- package/dist/SSHMimic/loginBanner.d.ts +7 -0
- package/dist/SSHMimic/loginBanner.d.ts.map +1 -0
- package/dist/SSHMimic/loginBanner.js +22 -0
- package/dist/VirtualShell/index.d.ts +21 -1
- package/dist/VirtualShell/index.d.ts.map +1 -1
- package/dist/VirtualShell/index.js +34 -2
- package/dist/VirtualShell/shell.d.ts.map +1 -1
- package/dist/VirtualShell/shell.js +2 -17
- package/dist/self-standalone.d.ts +2 -0
- package/dist/self-standalone.d.ts.map +1 -0
- package/dist/self-standalone.js +147 -0
- package/dist/web-api.d.ts +26 -0
- package/dist/web-api.d.ts.map +1 -0
- package/dist/web-api.js +46 -0
- package/dist/web-full.d.ts +4 -0
- package/dist/web-full.d.ts.map +1 -0
- package/dist/web-full.js +8 -0
- package/dist/web.d.ts +108 -0
- package/dist/web.d.ts.map +1 -0
- package/dist/web.js +773 -0
- package/examples/README.md +81 -3
- package/examples/app-iife.js +58 -0
- package/examples/app.js +28 -0
- package/examples/index-cf.html +27 -0
- package/examples/index.html +27 -0
- package/examples/server.js +55 -0
- package/examples/web-iife.min.js +13 -0
- package/examples/web.min.js +13 -0
- package/package.json +10 -3
- package/polyfills/node:child_process/index.js +2 -0
- package/polyfills/node:crypto/index.js +7 -0
- package/polyfills/node:events/index.js +9 -0
- package/polyfills/node:fs/index.js +8 -0
- package/polyfills/node:fs/promises.js +4 -0
- package/polyfills/node:os/index.js +9 -0
- package/polyfills/node:path/index.js +14 -0
- package/polyfills/node:vm/index.js +7 -0
- package/polyfills/node:zlib/index.js +3 -0
- package/src/SSHMimic/loginBanner.ts +36 -0
- package/src/VirtualShell/index.ts +60 -2
- package/src/VirtualShell/shell.ts +3 -31
- package/src/self-standalone.ts +183 -0
- package/src/web-api.ts +62 -0
- package/src/web-full.ts +11 -0
- package/src/web.ts +930 -0
- package/tests/web.test.ts +182 -0
package/examples/README.md
CHANGED
|
@@ -1,10 +1,88 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Examples
|
|
2
|
+
|
|
3
|
+
## Web Shell Demo
|
|
4
|
+
|
|
5
|
+
### Browser-based Virtual Shell
|
|
6
|
+
|
|
7
|
+
A fully functional virtual shell that runs in the browser with IndexedDB persistence.
|
|
8
|
+
|
|
9
|
+
**Two versions available:**
|
|
10
|
+
|
|
11
|
+
#### 1. ESM Version (Recommended for local development)
|
|
12
|
+
**Files:** `index.html` + `app.js` + `web.min.js`
|
|
13
|
+
|
|
14
|
+
Standard ES6 module format. Works perfectly locally over HTTP.
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
bun run example-serve
|
|
18
|
+
# or manually: cd examples && node server.js
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Then open: **`http://localhost:8787/index.html`**
|
|
22
|
+
|
|
23
|
+
**Note:** Requires HTTP server (browsers block ES6 imports via `file://` protocol)
|
|
24
|
+
|
|
25
|
+
#### 2. IIFE Version (Cloudflare & third-party proxy compatible)
|
|
26
|
+
**Files:** `index-cf.html` + `app-iife.js` + `web-iife.min.js`
|
|
27
|
+
|
|
28
|
+
Self-contained IIFE bundle that bypasses Cloudflare challenges and works through reverse proxies.
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
bun run example-serve
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Then open: **`http://localhost:8787/index-cf.html`**
|
|
35
|
+
|
|
36
|
+
Or via Cloudflare proxy: **`https://dev.fox3000foxy.com/index-cf.html`**
|
|
37
|
+
|
|
38
|
+
### Supported Shell Commands
|
|
39
|
+
|
|
40
|
+
The virtual shell includes ~20 built-in commands:
|
|
41
|
+
|
|
42
|
+
- **Navigation**: `pwd`, `cd`
|
|
43
|
+
- **File Operations**: `ls`, `cat`, `mkdir`, `touch`, `cp`, `mv`, `rm`
|
|
44
|
+
- **Text**: `echo`, `tee`
|
|
45
|
+
- **Environment**: `env`, `export`, `unset`
|
|
46
|
+
- **Network**: `curl`, `wget`
|
|
47
|
+
- **System**: `true`, `false`, `help`
|
|
48
|
+
|
|
49
|
+
All file operations are persisted to IndexedDB (survives page reload).
|
|
50
|
+
|
|
51
|
+
### How It Works
|
|
52
|
+
|
|
53
|
+
**Architecture:**
|
|
54
|
+
- `src/web.ts` → WebShell class (browser-compatible shell runtime)
|
|
55
|
+
- `src/VirtualFileSystem/` → IndexedDB-backed virtual filesystem
|
|
56
|
+
- esbuild bundles with NodeJS polyfills for browser compatibility
|
|
57
|
+
|
|
58
|
+
**Build Commands:**
|
|
59
|
+
```bash
|
|
60
|
+
# ESM version for HTTP servers
|
|
61
|
+
bun run web-build
|
|
62
|
+
|
|
63
|
+
# IIFE version for Cloudflare/proxies
|
|
64
|
+
bun run web-build-iife
|
|
65
|
+
|
|
66
|
+
# Both + copy to examples
|
|
67
|
+
bun run example-build
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**Cloudflare Issues & Solutions:**
|
|
71
|
+
|
|
72
|
+
Cloudflare's security challenge interferes with ES6 module loading. The IIFE version solves this by:
|
|
73
|
+
- Using IIFE format (no module imports needed)
|
|
74
|
+
- Dynamically loading the bundle via script tag
|
|
75
|
+
- Fully self-contained, no external dependencies
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## HoneyPot Examples
|
|
2
80
|
|
|
3
81
|
This directory contains practical examples demonstrating how to use the `HoneyPot` auditing and event tracking utility.
|
|
4
82
|
|
|
5
|
-
|
|
83
|
+
### Quick Start with HoneyPot
|
|
6
84
|
|
|
7
|
-
|
|
85
|
+
#### 1. Basic Introduction (Recommended First)
|
|
8
86
|
|
|
9
87
|
**File:** `honeypot-quickstart.ts`
|
|
10
88
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Fallback app for Cloudflare environments
|
|
2
|
+
// Uses IIFE bundle instead of ESM to avoid module loading issues with CF challenges
|
|
3
|
+
|
|
4
|
+
const out = document.getElementById('output');
|
|
5
|
+
const cmd = document.getElementById('cmd');
|
|
6
|
+
|
|
7
|
+
function print(s) {
|
|
8
|
+
out.textContent += s;
|
|
9
|
+
out.scrollTop = out.scrollHeight;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function main() {
|
|
13
|
+
// Wait for WebShellLib to be loaded from web-iife.min.js
|
|
14
|
+
if (typeof WebShellLib === 'undefined') {
|
|
15
|
+
print('Error: WebShellLib not loaded\n');
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const { createWebShell } = WebShellLib;
|
|
20
|
+
const shell = createWebShell('web-shell');
|
|
21
|
+
|
|
22
|
+
await shell.ensureInitialized();
|
|
23
|
+
print(`$ `);
|
|
24
|
+
|
|
25
|
+
cmd.addEventListener('keydown', async (ev) => {
|
|
26
|
+
if (ev.key !== 'Enter') return;
|
|
27
|
+
const input = cmd.value;
|
|
28
|
+
cmd.value = '';
|
|
29
|
+
|
|
30
|
+
if (!input.trim()) {
|
|
31
|
+
print('\n$ ');
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
print(`${input}\n`);
|
|
36
|
+
try {
|
|
37
|
+
const result = await shell.executeCommandLine(input);
|
|
38
|
+
if (result.stdout) print(result.stdout);
|
|
39
|
+
if (result.stderr) print(result.stderr);
|
|
40
|
+
if ((result.exitCode ?? 0) !== 0) {
|
|
41
|
+
print(`exit code: ${result.exitCode}\n`);
|
|
42
|
+
}
|
|
43
|
+
} catch (e) {
|
|
44
|
+
print(`Error: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
print(`$ `);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Load IIFE bundle
|
|
52
|
+
const script = document.createElement('script');
|
|
53
|
+
script.src = './web-iife.min.js';
|
|
54
|
+
script.onload = main;
|
|
55
|
+
script.onerror = () => {
|
|
56
|
+
print('Error: Failed to load web-iife.min.js\n');
|
|
57
|
+
};
|
|
58
|
+
document.head.appendChild(script);
|
package/examples/app.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { createWebShell } from './web.min.js';
|
|
2
|
+
|
|
3
|
+
const out = document.getElementById('output');
|
|
4
|
+
const cmd = document.getElementById('cmd');
|
|
5
|
+
|
|
6
|
+
function print(s){
|
|
7
|
+
out.textContent += s;
|
|
8
|
+
out.scrollTop = out.scrollHeight;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const shell = createWebShell('web-vm');
|
|
12
|
+
|
|
13
|
+
cmd.addEventListener('keydown', async (e) => {
|
|
14
|
+
if (e.key === 'Enter') {
|
|
15
|
+
const value = cmd.value.trim();
|
|
16
|
+
print(`$ ${value}\n`);
|
|
17
|
+
if (value) {
|
|
18
|
+
try {
|
|
19
|
+
const res = await shell.executeCommandLine(value);
|
|
20
|
+
if (res.stdout) print(res.stdout);
|
|
21
|
+
if (res.stderr) print(res.stderr);
|
|
22
|
+
} catch (err) {
|
|
23
|
+
print(String(err) + '\n');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
cmd.value = '';
|
|
27
|
+
}
|
|
28
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
6
|
+
<title>Virtual Shell — Web Demo (CF)</title>
|
|
7
|
+
<style>
|
|
8
|
+
html,body{height:100%;margin:0}
|
|
9
|
+
body{display:flex;flex-direction:column;font-family:monospace;background:#0b0f14;color:#e6eef6}
|
|
10
|
+
#terminal{flex:1;display:flex;flex-direction:column;padding:12px;box-sizing:border-box}
|
|
11
|
+
#output{flex:1;overflow:auto;white-space:pre-wrap}
|
|
12
|
+
#inputRow{display:flex;border-top:1px solid rgba(255,255,255,0.04);padding-top:8px}
|
|
13
|
+
#prompt{padding-right:8px}
|
|
14
|
+
#cmd{flex:1;background:transparent;border:0;color:inherit;outline:none;font-family:inherit}
|
|
15
|
+
</style>
|
|
16
|
+
</head>
|
|
17
|
+
<body>
|
|
18
|
+
<div id="terminal">
|
|
19
|
+
<div id="output">Welcome to Virtual Shell (web build - Cloudflare compatible)\n</div>
|
|
20
|
+
<div id="inputRow">
|
|
21
|
+
<div id="prompt">$</div>
|
|
22
|
+
<input id="cmd" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" />
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
<script src="./app-iife.js"></script>
|
|
26
|
+
</body>
|
|
27
|
+
</html>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
6
|
+
<title>Virtual Shell — Web Demo</title>
|
|
7
|
+
<style>
|
|
8
|
+
html,body{height:100%;margin:0}
|
|
9
|
+
body{display:flex;flex-direction:column;font-family:monospace;background:#0b0f14;color:#e6eef6}
|
|
10
|
+
#terminal{flex:1;display:flex;flex-direction:column;padding:12px;box-sizing:border-box}
|
|
11
|
+
#output{flex:1;overflow:auto;white-space:pre-wrap}
|
|
12
|
+
#inputRow{display:flex;border-top:1px solid rgba(255,255,255,0.04);padding-top:8px}
|
|
13
|
+
#prompt{padding-right:8px}
|
|
14
|
+
#cmd{flex:1;background:transparent;border:0;color:inherit;outline:none;font-family:inherit}
|
|
15
|
+
</style>
|
|
16
|
+
</head>
|
|
17
|
+
<body>
|
|
18
|
+
<div id="terminal">
|
|
19
|
+
<div id="output">Welcome to Virtual Shell (web build)\n</div>
|
|
20
|
+
<div id="inputRow">
|
|
21
|
+
<div id="prompt">$</div>
|
|
22
|
+
<input id="cmd" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" />
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
<script type="module" src="./app.js"></script>
|
|
26
|
+
</body>
|
|
27
|
+
</html>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { createServer } from 'node:http';
|
|
3
|
+
import { extname, join, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
const __dirname = resolve(fileURLToPath(import.meta.url), '..');
|
|
7
|
+
const examplesDir = __dirname;
|
|
8
|
+
|
|
9
|
+
const mimeTypes = {
|
|
10
|
+
'.html': 'text/html',
|
|
11
|
+
'.js': 'application/javascript',
|
|
12
|
+
'.json': 'application/json',
|
|
13
|
+
'.css': 'text/css',
|
|
14
|
+
'.map': 'application/json',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const server = createServer(async (req, res) => {
|
|
18
|
+
const reqPath = req.url === '/' ? '/index.html' : req.url;
|
|
19
|
+
const filePath = join(examplesDir, reqPath);
|
|
20
|
+
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url} -> ${filePath}`);
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const content = await readFile(filePath);
|
|
24
|
+
const ext = extname(filePath);
|
|
25
|
+
const mimeType = mimeTypes[ext] || 'application/octet-stream';
|
|
26
|
+
|
|
27
|
+
// Headers to bypass Cloudflare challenges and allow ES6 modules
|
|
28
|
+
const headers = {
|
|
29
|
+
'Content-Type': mimeType,
|
|
30
|
+
'Cache-Control': 'public, max-age=3600',
|
|
31
|
+
'X-Content-Type-Options': 'nosniff',
|
|
32
|
+
'Access-Control-Allow-Origin': '*',
|
|
33
|
+
'Access-Control-Allow-Methods': 'GET, HEAD, OPTIONS',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Cloudflare-specific header to bypass challenge
|
|
37
|
+
if (reqPath === '/index.html' || reqPath === '/app.js' || reqPath === '/web.min.js') {
|
|
38
|
+
headers['CF-Mitigate-Challenge'] = 'bypass';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
res.writeHead(200, headers);
|
|
42
|
+
res.end(content);
|
|
43
|
+
console.log(` ✓ 200 ${mimeType}`);
|
|
44
|
+
} catch (err) {
|
|
45
|
+
console.log(` ✗ 404 ${err.code}`);
|
|
46
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
47
|
+
res.end('404 Not Found');
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const PORT = 8787;
|
|
52
|
+
server.listen(PORT, () => {
|
|
53
|
+
console.log(`Server running at http://localhost:${PORT}`);
|
|
54
|
+
console.log(`Serving files from: ${examplesDir}`);
|
|
55
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";var WebShellLib=(()=>{var p=Object.defineProperty;var A=Object.getOwnPropertyDescriptor;var z=Object.getOwnPropertyNames;var O=Object.prototype.hasOwnProperty;var R=(o,e,t)=>e in o?p(o,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):o[e]=t;var F=(o,e)=>{for(var t in e)p(o,t,{get:e[t],enumerable:!0})},L=(o,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of z(e))!O.call(o,n)&&n!==t&&p(o,n,{get:()=>e[n],enumerable:!(r=A(e,n))||r.enumerable});return o};var _=o=>L(p({},"__esModule",{value:!0}),o);var u=(o,e,t)=>R(o,typeof e!="symbol"?e+"":e,t);var K={};F(K,{IndexedDbMirrorVfs:()=>b,WebShell:()=>y,createWebShell:()=>J});function w(o){let e=o.trim();if(!e)return{statements:[],isValid:!0};try{return{statements:I(e),isValid:!0}}catch(t){return{statements:[],isValid:!1,error:t.message}}}function I(o){let e=k(o),t=[];for(let r of e){let s={pipeline:{commands:M(r.text.trim()),isValid:!0}};r.op&&(s.op=r.op),t.push(s)}return t}function k(o){let e=[],t="",r=0,n=!1,s="",i=0,a=c=>{t.trim()&&e.push({text:t,op:c}),t=""};for(;i<o.length;){let c=o[i],d=o.slice(i,i+2);if((c==='"'||c==="'")&&!n){n=!0,s=c,t+=c,i++;continue}if(n&&c===s){n=!1,t+=c,i++;continue}if(n){t+=c,i++;continue}if(c==="("){r++,t+=c,i++;continue}if(c===")"){r--,t+=c,i++;continue}if(r>0){t+=c,i++;continue}if(d==="&&"){a("&&"),i+=2;continue}if(d==="||"){a("||"),i+=2;continue}if(c===";"){a(";"),i++;continue}t+=c,i++}return a(),e}function M(o){return B(o).map(T)}function B(o){let e=[],t="",r=!1,n="";for(let i=0;i<o.length;i++){let a=o[i];if((a==='"'||a==="'")&&!r){r=!0,n=a,t+=a;continue}if(r&&a===n){r=!1,t+=a;continue}if(r){t+=a;continue}if(a==="|"&&o[i+1]!=="|"){if(!t.trim())throw new Error("Syntax error near unexpected token '|'");e.push(t.trim()),t=""}else t+=a}let s=t.trim();if(!s&&e.length>0)throw new Error("Syntax error near unexpected token '|'");return s&&e.push(s),e}function T(o){let e=j(o);if(e.length===0)return{name:"",args:[]};let t=[],r,n,s=!1,i=0;for(;i<e.length;){let c=e[i];if(c==="<"){if(i++,i>=e.length)throw new Error("Syntax error: expected filename after <");r=e[i],i++}else if(c===">>"){if(i++,i>=e.length)throw new Error("Syntax error: expected filename after >>");n=e[i],s=!0,i++}else if(c===">"){if(i++,i>=e.length)throw new Error("Syntax error: expected filename after >");n=e[i],s=!1,i++}else t.push(c),i++}return{name:(t[0]??"").toLowerCase(),args:t.slice(1),inputFile:r,outputFile:n,appendOutput:s}}function j(o){let e=[],t="",r=!1,n="",s=0;for(;s<o.length;){let i=o[s],a=o[s+1];if((i==='"'||i==="'")&&!r){r=!0,n=i,s++;continue}if(r&&i===n){r=!1,n="",s++;continue}if(r){t+=i,s++;continue}if(i===" "){t&&(e.push(t),t=""),s++;continue}if((i===">"||i==="<")&&!r){t&&(e.push(t),t=""),i===">"&&a===">"?(e.push(">>"),s+=2):(e.push(i),s++);continue}t+=i,s++}return t&&e.push(t),e}function V(o,e){let t=o.replace(/\b([A-Za-z_][A-Za-z0-9_]*)\b/g,(r,n)=>{let s=e[n];return s!==void 0&&s!==""?s:"0"});if(!/^[\d\s+\-*/%()^!&|<>=,. ]+$/.test(t))return NaN;try{let r=Function(`"use strict"; return (${t.replace(/\*\*/g,"**")});`)();return typeof r=="number"?Math.trunc(r):NaN}catch{return NaN}}function U(o,e){let t=[],r=0;for(;r<o.length;){let n=o.indexOf("'",r);if(n===-1){t.push(e(o.slice(r)));break}t.push(e(o.slice(r,n)));let s=o.indexOf("'",n+1);if(s===-1){t.push(o.slice(n));break}t.push(o.slice(n,s+1)),r=s+1}return t.join("")}function m(o,e,t=0,r){let n=r??e.HOME??"/home/user";return U(o,s=>{let i=s;return i=i.replace(/(^|[\s:])~(\/|$)/g,(a,c,d)=>`${c}${n}${d}`),i=i.replace(/\$\?/g,String(t)),i=i.replace(/\$\$/g,"1"),i=i.replace(/\$#/g,"0"),i=i.replace(/\$\(\(([^)]+(?:\([^)]*\)[^)]*)*)\)\)/g,(a,c)=>{let d=V(c,e);return Number.isNaN(d)?"0":String(d)}),i=i.replace(/\$\{#([A-Za-z_][A-Za-z0-9_]*)\}/g,(a,c)=>String((e[c]??"").length)),i=i.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*):-([^}]*)\}/g,(a,c,d)=>e[c]!==void 0&&e[c]!==""?e[c]:d),i=i.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*):=([^}]*)\}/g,(a,c,d)=>((e[c]===void 0||e[c]==="")&&(e[c]=d),e[c])),i=i.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*):\+([^}]*)\}/g,(a,c,d)=>e[c]!==void 0&&e[c]!==""?d:""),i=i.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g,(a,c)=>e[c]??""),i=i.replace(/\$([A-Za-z_][A-Za-z0-9_]*)/g,(a,c)=>e[c]??""),i})}async function C(o,e,t,r){if(o.includes("$(")){let n="",s=!1,i=0;for(;i<o.length;){let a=o[i];if(a==="'"&&!s){s=!0,n+=a,i++;continue}if(a==="'"&&s){s=!1,n+=a,i++;continue}if(!s&&a==="$"&&o[i+1]==="("){if(o[i+2]==="("){n+=a,i++;continue}let c=0,d=i+1;for(;d<o.length;){if(o[d]==="(")c++;else if(o[d]===")"&&(c--,c===0))break;d++}let x=o.slice(i+2,d).trim(),P=(await r(x)).replace(/\n$/,"");n+=P,i=d+1;continue}n+=a,i++}o=n}return m(o,e,t)}var Z=new TextEncoder,q=new TextDecoder;function H(o){let e="";for(let t of o)e+=String.fromCharCode(t);return btoa(e)}function N(o){let e=atob(o),t=new Uint8Array(e.length);for(let r=0;r<e.length;r+=1)t[r]=e.charCodeAt(r);return t}function l(o,e="/"){let r=(o.startsWith("/")?o:`${e}/${o}`).split("/"),n=[];for(let s of r)if(!(!s||s===".")){if(s===".."){n.pop();continue}n.push(s)}return`/${n.join("/")}`||"/"}function h(o){let e=l(o);if(e==="/")return"/";let t=e.split("/").filter(Boolean);return t.pop(),t.length>0?`/${t.join("/")}`:"/"}function f(o){let e=l(o);return e==="/"?"/":e.split("/").filter(Boolean).at(-1)??"/"}function D(o){return o.type==="file"?{...o,contentBase64:o.contentBase64}:{...o,children:o.children.map(e=>D(e))}}function W(o,e){let t=new Date().toISOString();return{type:"directory",name:o,mode:e,createdAt:t,updatedAt:t,children:[]}}function Q(o,e,t){let r=new Date().toISOString();return{type:"file",name:o,mode:t,createdAt:r,updatedAt:r,contentBase64:H(e)}}function v(o,e){return o.children.find(t=>t.name===e)}function g(o,e){let t=o.children.findIndex(r=>r.name===e.name);if(t===-1){o.children.push(e);return}o.children[t]=e}function S(o,e){o.children=o.children.filter(t=>t.name!==e)}function $(o){return l(o).split("/").filter(Boolean)}var G=globalThis;function E(o){return new Promise((e,t)=>{o.addEventListener("success",()=>e(o.result)),o.addEventListener("error",()=>t(o.error))})}var b=class{constructor(e={}){u(this,"databaseName");u(this,"storeName");u(this,"key");u(this,"root");this.databaseName=e.databaseName??"typescript-virtual-container-web",this.storeName=e.storeName??"snapshots",this.key=e.key??"current",this.root=W("",493)}async openDatabase(){return new Promise((e,t)=>{let r=G.indexedDB;if(!r){t(new Error("IndexedDB is not available in this environment"));return}let n=r.open(this.databaseName,1);n.addEventListener("upgradeneeded",()=>{let s=n.result;s.objectStoreNames.contains(this.storeName)||s.createObjectStore(this.storeName)}),n.addEventListener("success",()=>e(n.result)),n.addEventListener("error",()=>t(n.error))})}async readSnapshot(){let e=await this.openDatabase();try{let n=e.transaction(this.storeName,"readonly").objectStore(this.storeName).get(this.key),s=await E(n);return s?JSON.parse(s):null}finally{e.close()}}async writeSnapshot(e){let t=await this.openDatabase();try{let r=t.transaction(this.storeName,"readwrite"),n=r.objectStore(this.storeName);await E(n.put(JSON.stringify(e),this.key)),await new Promise((s,i)=>{r.addEventListener("complete",()=>s()),r.addEventListener("error",()=>i(r.error)),r.addEventListener("abort",()=>i(r.error))})}finally{t.close()}}serializeNode(e){return e.type==="file"?{...e}:{...e,children:e.children.map(t=>this.serializeNode(t))}}deserializeNode(e){return e.type==="file"?{...e}:{...e,children:e.children.map(t=>this.deserializeNode(t))}}getNode(e){let t=l(e);if(t==="/")return this.root;let r=$(t),n=this.root;for(let s of r){if(n.type!=="directory")throw new Error(`Not a directory: ${t}`);let i=v(n,s);if(!i)throw new Error(`No such file or directory: ${t}`);n=i}return n}ensureDirectory(e,t){let r=l(e);if(r==="/")return this.root;let n=$(r),s=this.root;for(let i of n){let a=v(s,i);if(!a){let c=W(i,t);g(s,c),s=c;continue}if(a.type!=="directory")throw new Error(`Cannot create directory '${r}': path is a file.`);s=a}return s}removeNode(e,t){let r=l(e);if(r==="/")throw new Error("Cannot remove root directory");let n=this.getNode(h(r));if(n.type!=="directory")throw new Error(`Not a directory: ${h(r)}`);let s=f(r),i=v(n,s);if(!i)throw new Error(`No such file or directory: ${r}`);if(i.type==="directory"&&i.children.length>0&&!t)throw new Error(`Cannot remove '${r}': directory not empty.`);S(n,s)}copyNode(e){return e.type==="file"?{...e,contentBase64:e.contentBase64}:{...e,children:e.children.map(t=>this.copyNode(t))}}async restoreMirror(){let e=await this.readSnapshot();e&&(this.root=this.deserializeNode(e.root))}async flushMirror(){await this.writeSnapshot({root:this.serializeNode(this.root)})}exists(e){try{return this.getNode(e),!0}catch{return!1}}list(e){let t=this.getNode(e);if(t.type!=="directory")throw new Error(`Not a directory: ${e}`);return t.children.map(r=>r.name).sort((r,n)=>r.localeCompare(n))}stat(e){let t=this.getNode(e);return t.type==="file"?{type:"file",mode:t.mode,size:N(t.contentBase64).byteLength,name:t.name}:{type:"directory",mode:t.mode,size:0,name:t.name}}readFile(e){let t=this.getNode(e);if(t.type!=="file")throw new Error(`Is a directory: ${e}`);return q.decode(N(t.contentBase64))}writeFile(e,t,r=420){let n=l(e),s=this.ensureDirectory(h(n),493),i=typeof t=="string"?Z.encode(t):t,a=Q(f(n),i,r);g(s,a)}mkdir(e,t=493){this.ensureDirectory(e,t)}touch(e){this.exists(e)||this.writeFile(e,"")}move(e,t){let r=this.getNode(e),n=this.getNode(h(e)),s=this.ensureDirectory(h(t),493);if(n.type!=="directory")throw new Error(`Not a directory: ${h(e)}`);S(n,f(e));let i=D(r);i.name=f(t),g(s,i)}copy(e,t){let r=this.getNode(e),n=this.ensureDirectory(h(t),493),s=this.copyNode(r);s.name=f(t),g(n,s)}remove(e,t={}){this.removeNode(e,t.recursive??!1)}exportSnapshot(){return{root:this.serializeNode(this.root)}}importSnapshot(e){this.root=this.deserializeNode(e.root)}},y=class{constructor(e,t={}){u(this,"hostname");u(this,"vfs");u(this,"env");u(this,"cwd");u(this,"commands",new Map);u(this,"initialized",!1);this.hostname=e,this.cwd=t.cwd??"/home/root",this.env={vars:{PATH:"/usr/bin:/bin",HOME:"/home/root",USER:"root",LOGNAME:"root",SHELL:"/bin/sh",HOSTNAME:e,PWD:this.cwd},lastExitCode:0},this.vfs=new b(t.vfs),this.registerBuiltins()}register(e){this.commands.set(e.name,e);for(let t of e.aliases??[])this.commands.set(t,e)}registerBuiltins(){this.register({name:"help",description:"List available web commands",params:[],run:()=>({stdout:`${this.listCommands().join(`
|
|
2
|
+
`)}
|
|
3
|
+
`,exitCode:0})}),this.register({name:"pwd",description:"Print current directory",params:[],run:()=>({stdout:`${this.cwd}
|
|
4
|
+
`,exitCode:0})}),this.register({name:"cd",description:"Change current directory",params:["[dir]"],run:({args:e})=>{let t=e[0]?l(e[0],this.cwd):"/home/root";return!this.vfs.exists(t)||this.vfs.stat(t).type!=="directory"?{stderr:`cd: no such file or directory: ${t}`,exitCode:1}:(this.cwd=t,this.env.vars.PWD=t,{exitCode:0,nextCwd:t})}}),this.register({name:"echo",description:"Display text",params:["[-n] [-e] [text...]"],run:({args:e,stdin:t})=>{let r=e.includes("-n"),n=e.filter(a=>a!=="-n"&&a!=="-e"&&a!=="-E"),s=n.length>0?n.join(" "):t??"",i=m(s,this.env.vars,this.env.lastExitCode,this.env.vars.HOME);return{stdout:r?i:`${i}
|
|
5
|
+
`,exitCode:0}}}),this.register({name:"env",description:"Print environment variables",params:[],run:()=>({stdout:`${Object.entries(this.env.vars).map(([e,t])=>`${e}=${t}`).join(`
|
|
6
|
+
`)}
|
|
7
|
+
`,exitCode:0})}),this.register({name:"export",description:"Set environment variables",params:["KEY=VALUE..."],run:({args:e})=>{for(let t of e){let r=t.indexOf("=");if(r===-1)continue;let n=t.slice(0,r).trim(),s=t.slice(r+1);n&&(this.env.vars[n]=s)}return{exitCode:0}}}),this.register({name:"unset",description:"Unset environment variables",params:["NAME..."],run:({args:e})=>{for(let t of e)delete this.env.vars[t];return{exitCode:0}}}),this.register({name:"mkdir",description:"Create directories",params:["[-p] dir..."],run:async({args:e})=>{let t=e.filter(r=>r!=="-p");for(let r of t)this.vfs.mkdir(l(r,this.cwd));return await this.vfs.flushMirror(),{exitCode:0}}}),this.register({name:"touch",description:"Create files",params:["file..."],run:async({args:e})=>{for(let t of e)this.vfs.touch(l(t,this.cwd));return await this.vfs.flushMirror(),{exitCode:0}}}),this.register({name:"rm",description:"Remove files or directories",params:["[-r] [-f] path..."],run:async({args:e})=>{let t=e.includes("-r"),r=e.filter(n=>n!=="-r"&&n!=="-f");for(let n of r)this.vfs.remove(l(n,this.cwd),{recursive:t});return await this.vfs.flushMirror(),{exitCode:0}}}),this.register({name:"cp",description:"Copy files or directories",params:["[-r] source destination"],run:async({args:e})=>{let t=e.includes("-r"),r=e.filter(s=>s!=="-r");if(r.length<2)return{stderr:"cp: missing destination file operand",exitCode:1};let n=l(r.at(-1),this.cwd);for(let s of r.slice(0,-1)){let i=l(s,this.cwd);if(!t&&this.vfs.stat(i).type==="directory")return{stderr:`cp: -r not specified; omitting directory '${i}'`,exitCode:1};this.vfs.copy(i,n)}return await this.vfs.flushMirror(),{exitCode:0}}}),this.register({name:"mv",description:"Move or rename files",params:["source destination"],run:async({args:e})=>{if(e.length<2)return{stderr:"mv: missing destination file operand",exitCode:1};let t=l(e[0],this.cwd),r=l(e[1],this.cwd);return this.vfs.move(t,r),await this.vfs.flushMirror(),{exitCode:0}}}),this.register({name:"cat",description:"Concatenate files",params:["[file...]"],run:({args:e,stdin:t})=>{if(e.length===0)return{stdout:t??"",exitCode:0};let r="";for(let n of e)r+=this.vfs.readFile(l(n,this.cwd));return{stdout:r,exitCode:0}}}),this.register({name:"ls",description:"List files",params:["[path]"],run:({args:e})=>{let t=l(e[0]??".",this.cwd);return{stdout:`${this.vfs.list(t).join(" ")}
|
|
8
|
+
`,exitCode:0}}}),this.register({name:"tee",description:"Read from stdin and write to files",params:["[-a] file..."],run:async({args:e,stdin:t})=>{let r=e.includes("-a"),n=e.filter(i=>i!=="-a"),s=t??"";for(let i of n){let a=l(i,this.cwd);if(r&&this.vfs.exists(a)){let c=this.vfs.readFile(a);this.vfs.writeFile(a,`${c}${s}`)}else this.vfs.writeFile(a,s)}return await this.vfs.flushMirror(),{stdout:s,exitCode:0}}}),this.register({name:"curl",description:"Fetch a URL and optionally write to a file",params:["[-o file] URL"],run:async({args:e})=>{let t=e.indexOf("-o"),r=t!==-1?e[t+1]:void 0,s=e.filter((c,d)=>c!=="-o"&&d!==t+1).at(-1);if(!s)return{stderr:"curl: missing URL",exitCode:2};let i=await fetch(s),a=await i.text();return r?(this.vfs.writeFile(l(r,this.cwd),a),await this.vfs.flushMirror(),{exitCode:i.ok?0:1}):{stdout:a,exitCode:i.ok?0:1}}}),this.register({name:"wget",description:"Fetch a URL and optionally write to a file",params:["[-O file] URL"],run:async({args:e})=>{let t=e.indexOf("-O"),r=t!==-1?e[t+1]:void 0,s=e.filter((d,x)=>d!=="-O"&&x!==t+1).at(-1);if(!s)return{stderr:"wget: missing URL",exitCode:2};let i=await fetch(s),a=await i.text(),c=r??f(new URL(s).pathname||"index.html");return this.vfs.writeFile(l(c,this.cwd),a),await this.vfs.flushMirror(),{exitCode:i.ok?0:1}}}),this.register({name:"true",description:"Return success",params:[],run:()=>({exitCode:0})}),this.register({name:"false",description:"Return failure",params:[],run:()=>({exitCode:1})})}listCommands(){let e=new Map;for(let t of this.commands.values())e.set(t.name,t);return Array.from(e.values()).sort((t,r)=>t.name.localeCompare(r.name)).map(t=>`${t.name}${t.params.length>0?` ${t.params.join(" ")}`:""}`)}resolveCommand(e){return this.commands.get(e.toLowerCase())}async ensureInitialized(){this.initialized||(await this.vfs.restoreMirror(),this.vfs.exists("/home")||this.vfs.mkdir("/home"),this.vfs.exists("/home/root")||(this.vfs.mkdir("/home/root"),this.vfs.writeFile("/home/root/README.txt",`Welcome to ${this.hostname}
|
|
9
|
+
`)),this.vfs.exists("/tmp")||this.vfs.mkdir("/tmp"),this.vfs.exists("/etc")||this.vfs.mkdir("/etc"),this.vfs.exists("/etc/hostname")||this.vfs.writeFile("/etc/hostname",`${this.hostname}
|
|
10
|
+
`),this.vfs.exists("/etc/hosts")||this.vfs.writeFile("/etc/hosts",`127.0.0.1 localhost
|
|
11
|
+
::1 localhost
|
|
12
|
+
`),this.initialized=!0)}getCurrentWorkingDirectory(){return this.cwd}async executeCommandLine(e,t=!0){await this.ensureInitialized();let r=e.trim();if(!r)return{exitCode:0};let n=await C(r,this.env.vars,this.env.lastExitCode,a=>this.executeCommandLine(a,!1).then(c=>c.stdout??"")),s=w(n),i=await this.executeStatements(s.statements);return this.env.lastExitCode=i.exitCode??0,t&&await this.vfs.flushMirror(),i}async executeStatements(e){let t={exitCode:0},r=0;for(;r<e.length;){let n=e[r];if(t=await this.executePipeline(n.pipeline.commands),this.env.lastExitCode=t.exitCode??0,t.closeSession||t.switchUser)return t;let s=n.op;if(!(!s||s===";")){if(s==="&&"){if((t.exitCode??0)!==0)for(;r<e.length&&e[r]?.op==="&&";)r+=1}else if(s==="||"&&(t.exitCode??0)===0)for(;r<e.length&&e[r]?.op==="||";)r+=1}r+=1}return t}async executePipeline(e){return e.length===0?{exitCode:0}:e.length===1?this.executeSingleCommandWithRedirections(e[0]):this.executePipelineChain(e)}async executeSingleCommandWithRedirections(e){let t;if(e.inputFile){let n=l(e.inputFile,this.cwd);try{t=this.vfs.readFile(n)}catch{return{stderr:`${e.inputFile}: No such file or directory`,exitCode:1}}}let r=await this.executeCommand(e.name,e.args,t);if(e.outputFile){let n=l(e.outputFile,this.cwd),s=r.stdout??"";if(e.appendOutput&&this.vfs.exists(n)){let i=this.vfs.readFile(n);this.vfs.writeFile(n,`${i}${s}`)}else this.vfs.writeFile(n,s);return{...r,stdout:""}}return r}async executePipelineChain(e){let t="",r=0;for(let n=0;n<e.length;n+=1){let s=e[n];if(n===0&&s.inputFile){let a=l(s.inputFile,this.cwd);try{t=this.vfs.readFile(a)}catch{return{stderr:`${s.inputFile}: No such file or directory`,exitCode:1}}}let i=await this.executeCommand(s.name,s.args,t);t=i.stdout??"",r=i.exitCode??0}return{stdout:t,exitCode:r}}async executeCommand(e,t,r){let n=this.resolveCommand(e);if(!n)return{stderr:`${e}: command not found`,exitCode:127};let i={args:t.map(c=>m(c,this.env.vars,this.env.lastExitCode,this.env.vars.HOME)),stdin:r,cwd:this.cwd,env:this.env,rawInput:`${e} ${t.join(" ")}`.trim(),shell:this},a=await n.run(i);return a.nextCwd&&(this.cwd=a.nextCwd,this.env.vars.PWD=a.nextCwd),a}};function J(o="typescript-vm",e={}){return new y(o,e)}return _(K);})();
|
|
13
|
+
//# sourceMappingURL=web-iife.min.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
var P=Object.defineProperty;var A=(o,e,t)=>e in o?P(o,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):o[e]=t;var u=(o,e,t)=>A(o,typeof e!="symbol"?e+"":e,t);function v(o){let e=o.trim();if(!e)return{statements:[],isValid:!0};try{return{statements:z(e),isValid:!0}}catch(t){return{statements:[],isValid:!1,error:t.message}}}function z(o){let e=O(o),t=[];for(let r of e){let s={pipeline:{commands:R(r.text.trim()),isValid:!0}};r.op&&(s.op=r.op),t.push(s)}return t}function O(o){let e=[],t="",r=0,n=!1,s="",i=0,a=c=>{t.trim()&&e.push({text:t,op:c}),t=""};for(;i<o.length;){let c=o[i],d=o.slice(i,i+2);if((c==='"'||c==="'")&&!n){n=!0,s=c,t+=c,i++;continue}if(n&&c===s){n=!1,t+=c,i++;continue}if(n){t+=c,i++;continue}if(c==="("){r++,t+=c,i++;continue}if(c===")"){r--,t+=c,i++;continue}if(r>0){t+=c,i++;continue}if(d==="&&"){a("&&"),i+=2;continue}if(d==="||"){a("||"),i+=2;continue}if(c===";"){a(";"),i++;continue}t+=c,i++}return a(),e}function R(o){return F(o).map(L)}function F(o){let e=[],t="",r=!1,n="";for(let i=0;i<o.length;i++){let a=o[i];if((a==='"'||a==="'")&&!r){r=!0,n=a,t+=a;continue}if(r&&a===n){r=!1,t+=a;continue}if(r){t+=a;continue}if(a==="|"&&o[i+1]!=="|"){if(!t.trim())throw new Error("Syntax error near unexpected token '|'");e.push(t.trim()),t=""}else t+=a}let s=t.trim();if(!s&&e.length>0)throw new Error("Syntax error near unexpected token '|'");return s&&e.push(s),e}function L(o){let e=_(o);if(e.length===0)return{name:"",args:[]};let t=[],r,n,s=!1,i=0;for(;i<e.length;){let c=e[i];if(c==="<"){if(i++,i>=e.length)throw new Error("Syntax error: expected filename after <");r=e[i],i++}else if(c===">>"){if(i++,i>=e.length)throw new Error("Syntax error: expected filename after >>");n=e[i],s=!0,i++}else if(c===">"){if(i++,i>=e.length)throw new Error("Syntax error: expected filename after >");n=e[i],s=!1,i++}else t.push(c),i++}return{name:(t[0]??"").toLowerCase(),args:t.slice(1),inputFile:r,outputFile:n,appendOutput:s}}function _(o){let e=[],t="",r=!1,n="",s=0;for(;s<o.length;){let i=o[s],a=o[s+1];if((i==='"'||i==="'")&&!r){r=!0,n=i,s++;continue}if(r&&i===n){r=!1,n="",s++;continue}if(r){t+=i,s++;continue}if(i===" "){t&&(e.push(t),t=""),s++;continue}if((i===">"||i==="<")&&!r){t&&(e.push(t),t=""),i===">"&&a===">"?(e.push(">>"),s+=2):(e.push(i),s++);continue}t+=i,s++}return t&&e.push(t),e}function I(o,e){let t=o.replace(/\b([A-Za-z_][A-Za-z0-9_]*)\b/g,(r,n)=>{let s=e[n];return s!==void 0&&s!==""?s:"0"});if(!/^[\d\s+\-*/%()^!&|<>=,. ]+$/.test(t))return NaN;try{let r=Function(`"use strict"; return (${t.replace(/\*\*/g,"**")});`)();return typeof r=="number"?Math.trunc(r):NaN}catch{return NaN}}function k(o,e){let t=[],r=0;for(;r<o.length;){let n=o.indexOf("'",r);if(n===-1){t.push(e(o.slice(r)));break}t.push(e(o.slice(r,n)));let s=o.indexOf("'",n+1);if(s===-1){t.push(o.slice(n));break}t.push(o.slice(n,s+1)),r=s+1}return t.join("")}function p(o,e,t=0,r){let n=r??e.HOME??"/home/user";return k(o,s=>{let i=s;return i=i.replace(/(^|[\s:])~(\/|$)/g,(a,c,d)=>`${c}${n}${d}`),i=i.replace(/\$\?/g,String(t)),i=i.replace(/\$\$/g,"1"),i=i.replace(/\$#/g,"0"),i=i.replace(/\$\(\(([^)]+(?:\([^)]*\)[^)]*)*)\)\)/g,(a,c)=>{let d=I(c,e);return Number.isNaN(d)?"0":String(d)}),i=i.replace(/\$\{#([A-Za-z_][A-Za-z0-9_]*)\}/g,(a,c)=>String((e[c]??"").length)),i=i.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*):-([^}]*)\}/g,(a,c,d)=>e[c]!==void 0&&e[c]!==""?e[c]:d),i=i.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*):=([^}]*)\}/g,(a,c,d)=>((e[c]===void 0||e[c]==="")&&(e[c]=d),e[c])),i=i.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*):\+([^}]*)\}/g,(a,c,d)=>e[c]!==void 0&&e[c]!==""?d:""),i=i.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g,(a,c)=>e[c]??""),i=i.replace(/\$([A-Za-z_][A-Za-z0-9_]*)/g,(a,c)=>e[c]??""),i})}async function w(o,e,t,r){if(o.includes("$(")){let n="",s=!1,i=0;for(;i<o.length;){let a=o[i];if(a==="'"&&!s){s=!0,n+=a,i++;continue}if(a==="'"&&s){s=!1,n+=a,i++;continue}if(!s&&a==="$"&&o[i+1]==="("){if(o[i+2]==="("){n+=a,i++;continue}let c=0,d=i+1;for(;d<o.length;){if(o[d]==="(")c++;else if(o[d]===")"&&(c--,c===0))break;d++}let g=o.slice(i+2,d).trim(),D=(await r(g)).replace(/\n$/,"");n+=D,i=d+1;continue}n+=a,i++}o=n}return p(o,e,t)}var M=new TextEncoder,B=new TextDecoder;function T(o){let e="";for(let t of o)e+=String.fromCharCode(t);return btoa(e)}function C(o){let e=atob(o),t=new Uint8Array(e.length);for(let r=0;r<e.length;r+=1)t[r]=e.charCodeAt(r);return t}function l(o,e="/"){let r=(o.startsWith("/")?o:`${e}/${o}`).split("/"),n=[];for(let s of r)if(!(!s||s===".")){if(s===".."){n.pop();continue}n.push(s)}return`/${n.join("/")}`||"/"}function h(o){let e=l(o);if(e==="/")return"/";let t=e.split("/").filter(Boolean);return t.pop(),t.length>0?`/${t.join("/")}`:"/"}function f(o){let e=l(o);return e==="/"?"/":e.split("/").filter(Boolean).at(-1)??"/"}function E(o){return o.type==="file"?{...o,contentBase64:o.contentBase64}:{...o,children:o.children.map(e=>E(e))}}function N(o,e){let t=new Date().toISOString();return{type:"directory",name:o,mode:e,createdAt:t,updatedAt:t,children:[]}}function j(o,e,t){let r=new Date().toISOString();return{type:"file",name:o,mode:t,createdAt:r,updatedAt:r,contentBase64:T(e)}}function b(o,e){return o.children.find(t=>t.name===e)}function m(o,e){let t=o.children.findIndex(r=>r.name===e.name);if(t===-1){o.children.push(e);return}o.children[t]=e}function W(o,e){o.children=o.children.filter(t=>t.name!==e)}function S(o){return l(o).split("/").filter(Boolean)}var V=globalThis;function $(o){return new Promise((e,t)=>{o.addEventListener("success",()=>e(o.result)),o.addEventListener("error",()=>t(o.error))})}var y=class{constructor(e={}){u(this,"databaseName");u(this,"storeName");u(this,"key");u(this,"root");this.databaseName=e.databaseName??"typescript-virtual-container-web",this.storeName=e.storeName??"snapshots",this.key=e.key??"current",this.root=N("",493)}async openDatabase(){return new Promise((e,t)=>{let r=V.indexedDB;if(!r){t(new Error("IndexedDB is not available in this environment"));return}let n=r.open(this.databaseName,1);n.addEventListener("upgradeneeded",()=>{let s=n.result;s.objectStoreNames.contains(this.storeName)||s.createObjectStore(this.storeName)}),n.addEventListener("success",()=>e(n.result)),n.addEventListener("error",()=>t(n.error))})}async readSnapshot(){let e=await this.openDatabase();try{let n=e.transaction(this.storeName,"readonly").objectStore(this.storeName).get(this.key),s=await $(n);return s?JSON.parse(s):null}finally{e.close()}}async writeSnapshot(e){let t=await this.openDatabase();try{let r=t.transaction(this.storeName,"readwrite"),n=r.objectStore(this.storeName);await $(n.put(JSON.stringify(e),this.key)),await new Promise((s,i)=>{r.addEventListener("complete",()=>s()),r.addEventListener("error",()=>i(r.error)),r.addEventListener("abort",()=>i(r.error))})}finally{t.close()}}serializeNode(e){return e.type==="file"?{...e}:{...e,children:e.children.map(t=>this.serializeNode(t))}}deserializeNode(e){return e.type==="file"?{...e}:{...e,children:e.children.map(t=>this.deserializeNode(t))}}getNode(e){let t=l(e);if(t==="/")return this.root;let r=S(t),n=this.root;for(let s of r){if(n.type!=="directory")throw new Error(`Not a directory: ${t}`);let i=b(n,s);if(!i)throw new Error(`No such file or directory: ${t}`);n=i}return n}ensureDirectory(e,t){let r=l(e);if(r==="/")return this.root;let n=S(r),s=this.root;for(let i of n){let a=b(s,i);if(!a){let c=N(i,t);m(s,c),s=c;continue}if(a.type!=="directory")throw new Error(`Cannot create directory '${r}': path is a file.`);s=a}return s}removeNode(e,t){let r=l(e);if(r==="/")throw new Error("Cannot remove root directory");let n=this.getNode(h(r));if(n.type!=="directory")throw new Error(`Not a directory: ${h(r)}`);let s=f(r),i=b(n,s);if(!i)throw new Error(`No such file or directory: ${r}`);if(i.type==="directory"&&i.children.length>0&&!t)throw new Error(`Cannot remove '${r}': directory not empty.`);W(n,s)}copyNode(e){return e.type==="file"?{...e,contentBase64:e.contentBase64}:{...e,children:e.children.map(t=>this.copyNode(t))}}async restoreMirror(){let e=await this.readSnapshot();e&&(this.root=this.deserializeNode(e.root))}async flushMirror(){await this.writeSnapshot({root:this.serializeNode(this.root)})}exists(e){try{return this.getNode(e),!0}catch{return!1}}list(e){let t=this.getNode(e);if(t.type!=="directory")throw new Error(`Not a directory: ${e}`);return t.children.map(r=>r.name).sort((r,n)=>r.localeCompare(n))}stat(e){let t=this.getNode(e);return t.type==="file"?{type:"file",mode:t.mode,size:C(t.contentBase64).byteLength,name:t.name}:{type:"directory",mode:t.mode,size:0,name:t.name}}readFile(e){let t=this.getNode(e);if(t.type!=="file")throw new Error(`Is a directory: ${e}`);return B.decode(C(t.contentBase64))}writeFile(e,t,r=420){let n=l(e),s=this.ensureDirectory(h(n),493),i=typeof t=="string"?M.encode(t):t,a=j(f(n),i,r);m(s,a)}mkdir(e,t=493){this.ensureDirectory(e,t)}touch(e){this.exists(e)||this.writeFile(e,"")}move(e,t){let r=this.getNode(e),n=this.getNode(h(e)),s=this.ensureDirectory(h(t),493);if(n.type!=="directory")throw new Error(`Not a directory: ${h(e)}`);W(n,f(e));let i=E(r);i.name=f(t),m(s,i)}copy(e,t){let r=this.getNode(e),n=this.ensureDirectory(h(t),493),s=this.copyNode(r);s.name=f(t),m(n,s)}remove(e,t={}){this.removeNode(e,t.recursive??!1)}exportSnapshot(){return{root:this.serializeNode(this.root)}}importSnapshot(e){this.root=this.deserializeNode(e.root)}},x=class{constructor(e,t={}){u(this,"hostname");u(this,"vfs");u(this,"env");u(this,"cwd");u(this,"commands",new Map);u(this,"initialized",!1);this.hostname=e,this.cwd=t.cwd??"/home/root",this.env={vars:{PATH:"/usr/bin:/bin",HOME:"/home/root",USER:"root",LOGNAME:"root",SHELL:"/bin/sh",HOSTNAME:e,PWD:this.cwd},lastExitCode:0},this.vfs=new y(t.vfs),this.registerBuiltins()}register(e){this.commands.set(e.name,e);for(let t of e.aliases??[])this.commands.set(t,e)}registerBuiltins(){this.register({name:"help",description:"List available web commands",params:[],run:()=>({stdout:`${this.listCommands().join(`
|
|
2
|
+
`)}
|
|
3
|
+
`,exitCode:0})}),this.register({name:"pwd",description:"Print current directory",params:[],run:()=>({stdout:`${this.cwd}
|
|
4
|
+
`,exitCode:0})}),this.register({name:"cd",description:"Change current directory",params:["[dir]"],run:({args:e})=>{let t=e[0]?l(e[0],this.cwd):"/home/root";return!this.vfs.exists(t)||this.vfs.stat(t).type!=="directory"?{stderr:`cd: no such file or directory: ${t}`,exitCode:1}:(this.cwd=t,this.env.vars.PWD=t,{exitCode:0,nextCwd:t})}}),this.register({name:"echo",description:"Display text",params:["[-n] [-e] [text...]"],run:({args:e,stdin:t})=>{let r=e.includes("-n"),n=e.filter(a=>a!=="-n"&&a!=="-e"&&a!=="-E"),s=n.length>0?n.join(" "):t??"",i=p(s,this.env.vars,this.env.lastExitCode,this.env.vars.HOME);return{stdout:r?i:`${i}
|
|
5
|
+
`,exitCode:0}}}),this.register({name:"env",description:"Print environment variables",params:[],run:()=>({stdout:`${Object.entries(this.env.vars).map(([e,t])=>`${e}=${t}`).join(`
|
|
6
|
+
`)}
|
|
7
|
+
`,exitCode:0})}),this.register({name:"export",description:"Set environment variables",params:["KEY=VALUE..."],run:({args:e})=>{for(let t of e){let r=t.indexOf("=");if(r===-1)continue;let n=t.slice(0,r).trim(),s=t.slice(r+1);n&&(this.env.vars[n]=s)}return{exitCode:0}}}),this.register({name:"unset",description:"Unset environment variables",params:["NAME..."],run:({args:e})=>{for(let t of e)delete this.env.vars[t];return{exitCode:0}}}),this.register({name:"mkdir",description:"Create directories",params:["[-p] dir..."],run:async({args:e})=>{let t=e.filter(r=>r!=="-p");for(let r of t)this.vfs.mkdir(l(r,this.cwd));return await this.vfs.flushMirror(),{exitCode:0}}}),this.register({name:"touch",description:"Create files",params:["file..."],run:async({args:e})=>{for(let t of e)this.vfs.touch(l(t,this.cwd));return await this.vfs.flushMirror(),{exitCode:0}}}),this.register({name:"rm",description:"Remove files or directories",params:["[-r] [-f] path..."],run:async({args:e})=>{let t=e.includes("-r"),r=e.filter(n=>n!=="-r"&&n!=="-f");for(let n of r)this.vfs.remove(l(n,this.cwd),{recursive:t});return await this.vfs.flushMirror(),{exitCode:0}}}),this.register({name:"cp",description:"Copy files or directories",params:["[-r] source destination"],run:async({args:e})=>{let t=e.includes("-r"),r=e.filter(s=>s!=="-r");if(r.length<2)return{stderr:"cp: missing destination file operand",exitCode:1};let n=l(r.at(-1),this.cwd);for(let s of r.slice(0,-1)){let i=l(s,this.cwd);if(!t&&this.vfs.stat(i).type==="directory")return{stderr:`cp: -r not specified; omitting directory '${i}'`,exitCode:1};this.vfs.copy(i,n)}return await this.vfs.flushMirror(),{exitCode:0}}}),this.register({name:"mv",description:"Move or rename files",params:["source destination"],run:async({args:e})=>{if(e.length<2)return{stderr:"mv: missing destination file operand",exitCode:1};let t=l(e[0],this.cwd),r=l(e[1],this.cwd);return this.vfs.move(t,r),await this.vfs.flushMirror(),{exitCode:0}}}),this.register({name:"cat",description:"Concatenate files",params:["[file...]"],run:({args:e,stdin:t})=>{if(e.length===0)return{stdout:t??"",exitCode:0};let r="";for(let n of e)r+=this.vfs.readFile(l(n,this.cwd));return{stdout:r,exitCode:0}}}),this.register({name:"ls",description:"List files",params:["[path]"],run:({args:e})=>{let t=l(e[0]??".",this.cwd);return{stdout:`${this.vfs.list(t).join(" ")}
|
|
8
|
+
`,exitCode:0}}}),this.register({name:"tee",description:"Read from stdin and write to files",params:["[-a] file..."],run:async({args:e,stdin:t})=>{let r=e.includes("-a"),n=e.filter(i=>i!=="-a"),s=t??"";for(let i of n){let a=l(i,this.cwd);if(r&&this.vfs.exists(a)){let c=this.vfs.readFile(a);this.vfs.writeFile(a,`${c}${s}`)}else this.vfs.writeFile(a,s)}return await this.vfs.flushMirror(),{stdout:s,exitCode:0}}}),this.register({name:"curl",description:"Fetch a URL and optionally write to a file",params:["[-o file] URL"],run:async({args:e})=>{let t=e.indexOf("-o"),r=t!==-1?e[t+1]:void 0,s=e.filter((c,d)=>c!=="-o"&&d!==t+1).at(-1);if(!s)return{stderr:"curl: missing URL",exitCode:2};let i=await fetch(s),a=await i.text();return r?(this.vfs.writeFile(l(r,this.cwd),a),await this.vfs.flushMirror(),{exitCode:i.ok?0:1}):{stdout:a,exitCode:i.ok?0:1}}}),this.register({name:"wget",description:"Fetch a URL and optionally write to a file",params:["[-O file] URL"],run:async({args:e})=>{let t=e.indexOf("-O"),r=t!==-1?e[t+1]:void 0,s=e.filter((d,g)=>d!=="-O"&&g!==t+1).at(-1);if(!s)return{stderr:"wget: missing URL",exitCode:2};let i=await fetch(s),a=await i.text(),c=r??f(new URL(s).pathname||"index.html");return this.vfs.writeFile(l(c,this.cwd),a),await this.vfs.flushMirror(),{exitCode:i.ok?0:1}}}),this.register({name:"true",description:"Return success",params:[],run:()=>({exitCode:0})}),this.register({name:"false",description:"Return failure",params:[],run:()=>({exitCode:1})})}listCommands(){let e=new Map;for(let t of this.commands.values())e.set(t.name,t);return Array.from(e.values()).sort((t,r)=>t.name.localeCompare(r.name)).map(t=>`${t.name}${t.params.length>0?` ${t.params.join(" ")}`:""}`)}resolveCommand(e){return this.commands.get(e.toLowerCase())}async ensureInitialized(){this.initialized||(await this.vfs.restoreMirror(),this.vfs.exists("/home")||this.vfs.mkdir("/home"),this.vfs.exists("/home/root")||(this.vfs.mkdir("/home/root"),this.vfs.writeFile("/home/root/README.txt",`Welcome to ${this.hostname}
|
|
9
|
+
`)),this.vfs.exists("/tmp")||this.vfs.mkdir("/tmp"),this.vfs.exists("/etc")||this.vfs.mkdir("/etc"),this.vfs.exists("/etc/hostname")||this.vfs.writeFile("/etc/hostname",`${this.hostname}
|
|
10
|
+
`),this.vfs.exists("/etc/hosts")||this.vfs.writeFile("/etc/hosts",`127.0.0.1 localhost
|
|
11
|
+
::1 localhost
|
|
12
|
+
`),this.initialized=!0)}getCurrentWorkingDirectory(){return this.cwd}async executeCommandLine(e,t=!0){await this.ensureInitialized();let r=e.trim();if(!r)return{exitCode:0};let n=await w(r,this.env.vars,this.env.lastExitCode,a=>this.executeCommandLine(a,!1).then(c=>c.stdout??"")),s=v(n),i=await this.executeStatements(s.statements);return this.env.lastExitCode=i.exitCode??0,t&&await this.vfs.flushMirror(),i}async executeStatements(e){let t={exitCode:0},r=0;for(;r<e.length;){let n=e[r];if(t=await this.executePipeline(n.pipeline.commands),this.env.lastExitCode=t.exitCode??0,t.closeSession||t.switchUser)return t;let s=n.op;if(!(!s||s===";")){if(s==="&&"){if((t.exitCode??0)!==0)for(;r<e.length&&e[r]?.op==="&&";)r+=1}else if(s==="||"&&(t.exitCode??0)===0)for(;r<e.length&&e[r]?.op==="||";)r+=1}r+=1}return t}async executePipeline(e){return e.length===0?{exitCode:0}:e.length===1?this.executeSingleCommandWithRedirections(e[0]):this.executePipelineChain(e)}async executeSingleCommandWithRedirections(e){let t;if(e.inputFile){let n=l(e.inputFile,this.cwd);try{t=this.vfs.readFile(n)}catch{return{stderr:`${e.inputFile}: No such file or directory`,exitCode:1}}}let r=await this.executeCommand(e.name,e.args,t);if(e.outputFile){let n=l(e.outputFile,this.cwd),s=r.stdout??"";if(e.appendOutput&&this.vfs.exists(n)){let i=this.vfs.readFile(n);this.vfs.writeFile(n,`${i}${s}`)}else this.vfs.writeFile(n,s);return{...r,stdout:""}}return r}async executePipelineChain(e){let t="",r=0;for(let n=0;n<e.length;n+=1){let s=e[n];if(n===0&&s.inputFile){let a=l(s.inputFile,this.cwd);try{t=this.vfs.readFile(a)}catch{return{stderr:`${s.inputFile}: No such file or directory`,exitCode:1}}}let i=await this.executeCommand(s.name,s.args,t);t=i.stdout??"",r=i.exitCode??0}return{stdout:t,exitCode:r}}async executeCommand(e,t,r){let n=this.resolveCommand(e);if(!n)return{stderr:`${e}: command not found`,exitCode:127};let i={args:t.map(c=>p(c,this.env.vars,this.env.lastExitCode,this.env.vars.HOME)),stdin:r,cwd:this.cwd,env:this.env,rawInput:`${e} ${t.join(" ")}`.trim(),shell:this},a=await n.run(i);return a.nextCwd&&(this.cwd=a.nextCwd,this.env.vars.PWD=a.nextCwd),a}};function G(o="typescript-vm",e={}){return new x(o,e)}export{y as IndexedDbMirrorVfs,x as WebShell,G as createWebShell};
|
|
13
|
+
//# sourceMappingURL=web.min.js.map
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"type": "module",
|
|
7
|
-
"version": "1.3.
|
|
7
|
+
"version": "1.3.2",
|
|
8
8
|
"license": "MIT",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
@@ -29,9 +29,16 @@
|
|
|
29
29
|
"build": "tsc --project tsconfig.json",
|
|
30
30
|
"deploy:npm": "npm publish --access public",
|
|
31
31
|
"bench": "rm -rf .benchmark-shells/ && bun benchmark-virtualshell.ts",
|
|
32
|
-
"standalone-build:wo-sftp": "bunx esbuild src/standalone-wo-sftp.ts --bundle --platform=node --target=node18 --outfile=standalone-wo-sftp.js --tree-shaking=true --minify --sourcemap",
|
|
32
|
+
"standalone-build:wo-sftp": "bunx esbuild src/standalone-wo-sftp.ts --bundle --platform=node --target=node18 --outfile=builds/standalone-wo-sftp.js --tree-shaking=true --minify --sourcemap",
|
|
33
|
+
"web-build": "bunx esbuild src/web.ts --bundle --platform=browser --format=esm --target=es2020 --outfile=builds/web.min.js --tree-shaking=true --minify --sourcemap",
|
|
34
|
+
"web-build-iife": "bunx esbuild src/web.ts --bundle --platform=browser --format=iife --target=es2020 --outfile=builds/web-iife.min.js --tree-shaking=true --minify --sourcemap --global-name=WebShellLib",
|
|
35
|
+
"example-build": "bun run web-build && cp builds/web.min.js examples/web.min.js",
|
|
36
|
+
"example-serve": "cd examples && node server.js",
|
|
37
|
+
"web-full-build": "bunx esbuild src/web-api.ts --bundle --platform=browser --format=esm --target=es2020 --outfile=builds/web-full-api.min.js --tree-shaking=true --minify --sourcemap --alias:node:events=./polyfills/node:events/index.js --alias:node:path=./polyfills/node:path/index.js --alias:node:os=./polyfills/node:os/index.js --alias:node:fs=./polyfills/node:fs/index.js --alias:node:fs/promises=./polyfills/node:fs/promises.js --alias:node:crypto=./polyfills/node:crypto/index.js --alias:node:child_process=./polyfills/node:child_process/index.js --alias:node:zlib=./polyfills/node:zlib/index.js --alias:node:vm=./polyfills/node:vm/index.js",
|
|
33
38
|
"publish-package": "bash ./scripts/publish-package.sh",
|
|
34
|
-
"standalone-build": "bunx esbuild src/standalone.ts --bundle --platform=node --target=node18 --outfile=standalone.js --tree-shaking=true --minify --sourcemap"
|
|
39
|
+
"self-standalone-build": "bunx esbuild src/self-standalone.ts --bundle --platform=node --format=esm --target=node18 --outfile=builds/self-standalone.js --tree-shaking=true --minify --sourcemap",
|
|
40
|
+
"standalone-build": "bunx esbuild src/standalone.ts --bundle --platform=node --target=node18 --outfile=builds/standalone.js --tree-shaking=true --minify --sourcemap",
|
|
41
|
+
"build-all": "bun run self-standalone-build && bun run standalone-build && bun run standalone-build:wo-sftp && bun run web-build && bun run web-full-build && bun run example-build"
|
|
35
42
|
},
|
|
36
43
|
"devDependencies": {
|
|
37
44
|
"@biomejs/biome": "^2.4.13",
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Minimal crypto shim using Web Crypto where possible; fallback simple hashes.
|
|
2
|
+
export function randomBytes(n){ const a=new Uint8Array(n); crypto.getRandomValues(a); return a; }
|
|
3
|
+
export function randomUUID(){ return crypto.randomUUID ? crypto.randomUUID() : Math.floor(Math.random()*1e9).toString(16); }
|
|
4
|
+
export function createHash(alg){ let data=''; return { update(d){ data += String(d); return this; }, digest(enc='hex'){ // simple hash fallback
|
|
5
|
+
let h=0; for(let i=0;i<data.length;i++) h=(h*31+data.charCodeAt(i))|0; const s=(h>>>0).toString(16); return enc==='hex'?s:s; } }; }
|
|
6
|
+
export function scryptSync(){ throw new Error('scryptSync not implemented in browser shim'); }
|
|
7
|
+
export default { randomBytes, randomUUID, createHash, scryptSync };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export class EventEmitter {
|
|
2
|
+
constructor(){ this._events = Object.create(null); }
|
|
3
|
+
on(ev, fn){ (this._events[ev] ||= []).push(fn); return this; }
|
|
4
|
+
addListener(ev, fn){ return this.on(ev, fn); }
|
|
5
|
+
emit(ev, ...args){ const fns = this._events[ev] || []; for (const f of fns) try{ f(...args);}catch(e){} return fns.length>0; }
|
|
6
|
+
removeListener(ev, fn){ if(!this._events[ev]) return; this._events[ev]=this._events[ev].filter(x=>x!==fn); }
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default EventEmitter;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Minimal fs shim: synchronous operations not supported in browser. Provide stubs.
|
|
2
|
+
export function existsSync(){ return false; }
|
|
3
|
+
export function readFileSync(){ throw new Error('node:fs.readFileSync is not supported in browser'); }
|
|
4
|
+
export function writeFileSync(){ throw new Error('node:fs.writeFileSync is not supported in browser'); }
|
|
5
|
+
export function readdirSync(){ return []; }
|
|
6
|
+
export function mkdirSync(){ throw new Error('node:fs.mkdirSync not supported in browser'); }
|
|
7
|
+
export function statSync(){ throw new Error('node:fs.statSync not supported in browser'); }
|
|
8
|
+
export default { existsSync, readFileSync, writeFileSync };
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export async function readFile(){ throw new Error('node:fs/promises.readFile is not supported in browser'); }
|
|
2
|
+
export async function writeFile(){ throw new Error('node:fs/promises.writeFile is not supported in browser'); }
|
|
3
|
+
export async function unlink(){ throw new Error('node:fs/promises.unlink is not supported in browser'); }
|
|
4
|
+
export default { readFile, writeFile, unlink };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function totalmem(){ try{ return navigator?.deviceMemory? navigator.deviceMemory*1024*1024 : 1024*1024*1024; }catch(e){return 1024*1024*1024;} }
|
|
2
|
+
export function freemem(){ return Math.floor(totalmem()*0.5); }
|
|
3
|
+
export function cpus(){ return [{ model: 'web-cpu', speed: 1000 }]; }
|
|
4
|
+
export function platform(){ return 'browser'; }
|
|
5
|
+
export function type(){ return 'web'; }
|
|
6
|
+
export function arch(){ return 'x86_64'; }
|
|
7
|
+
export function release(){ return 'web-release'; }
|
|
8
|
+
export function uptime(){ return Math.floor(performance.now()/1000); }
|
|
9
|
+
export default { totalmem, freemem, cpus, platform, type, arch, release, uptime };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const posix = {
|
|
2
|
+
basename(p){ const parts=p.split('/').filter(Boolean); return parts.length?parts[parts.length-1]:''; },
|
|
3
|
+
dirname(p){ if(!p) return '.'; const parts=p.split('/').filter(Boolean); parts.pop(); return parts.length?'/'+parts.join('/'):'/'; },
|
|
4
|
+
join(...parts){ return parts.join('/').replace(/\/+/g,'/'); },
|
|
5
|
+
resolve(...parts){ // naive resolve
|
|
6
|
+
const joined = parts.join('/');
|
|
7
|
+
if (joined.startsWith('/')) return joined;
|
|
8
|
+
return '/'+joined;
|
|
9
|
+
},
|
|
10
|
+
};
|
|
11
|
+
export function basename(p){ return posix.basename(p); }
|
|
12
|
+
export function dirname(p){ return posix.dirname(p); }
|
|
13
|
+
export function resolve(...parts){ return posix.resolve(...parts); }
|
|
14
|
+
export default { posix, basename, dirname, resolve };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Minimal vm shim: execute code in Function scope (no sandbox guarantees)
|
|
2
|
+
export default function runInVm(code, options){
|
|
3
|
+
const fn = new Function('exports','require','module','__filename','__dirname', code);
|
|
4
|
+
const module = { exports: {} };
|
|
5
|
+
fn(module.exports, ()=>{throw new Error('require not supported in vm shim')}, module, '', '');
|
|
6
|
+
return module.exports;
|
|
7
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ShellProperties } from "../VirtualShell";
|
|
2
|
+
import { formatLoginDate } from "./loginFormat";
|
|
3
|
+
|
|
4
|
+
export interface LoginBannerState {
|
|
5
|
+
at: string;
|
|
6
|
+
from: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function buildLoginBanner(
|
|
10
|
+
hostname: string,
|
|
11
|
+
properties: ShellProperties,
|
|
12
|
+
lastLogin: LoginBannerState | null,
|
|
13
|
+
): string {
|
|
14
|
+
const lines = [
|
|
15
|
+
`Linux ${hostname} ${properties.kernel} ${properties.arch}`,
|
|
16
|
+
"",
|
|
17
|
+
"The programs included with the Fortune GNU/Linux system are free software;",
|
|
18
|
+
"the exact distribution terms for each program are described in the",
|
|
19
|
+
"individual files in /usr/share/doc/*/copyright.",
|
|
20
|
+
"",
|
|
21
|
+
"Fortune GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent",
|
|
22
|
+
"permitted by applicable law.",
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
if (lastLogin) {
|
|
26
|
+
const when = new Date(lastLogin.at);
|
|
27
|
+
const displayed = Number.isNaN(when.getTime())
|
|
28
|
+
? lastLogin.at
|
|
29
|
+
: formatLoginDate(when);
|
|
30
|
+
lines.push(`Last login: ${displayed} from ${lastLogin.from || "unknown"}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
lines.push("");
|
|
34
|
+
|
|
35
|
+
return `${lines.map((line) => `${line}\r\n`).join("")}`;
|
|
36
|
+
}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
} from "../modules/linuxRootfs";
|
|
9
9
|
import type { CommandContext, CommandResult } from "../types/commands";
|
|
10
10
|
import type { ShellStream } from "../types/streams";
|
|
11
|
+
import type { VfsNodeStats } from "../types/vfs";
|
|
11
12
|
import type { PerfLogger } from "../utils/perfLogger";
|
|
12
13
|
import { createPerfLogger } from "../utils/perfLogger";
|
|
13
14
|
import VirtualFileSystem, { type VfsOptions } from "../VirtualFileSystem";
|
|
@@ -40,6 +41,56 @@ export interface ShellProperties {
|
|
|
40
41
|
arch: string;
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
export interface VirtualShellVfsLike {
|
|
45
|
+
restoreMirror(): Promise<void>;
|
|
46
|
+
flushMirror(): Promise<void>;
|
|
47
|
+
writeFile(targetPath: string, content: string | Uint8Array): void;
|
|
48
|
+
readFile(targetPath: string): string;
|
|
49
|
+
mkdir(targetPath: string, mode?: number): void;
|
|
50
|
+
exists(targetPath: string): boolean;
|
|
51
|
+
stat(targetPath: string): VfsNodeStats;
|
|
52
|
+
list(targetPath: string): string[];
|
|
53
|
+
remove(targetPath: string, options?: { recursive?: boolean }): void;
|
|
54
|
+
chmod?(targetPath: string, mode: number): void;
|
|
55
|
+
symlink?(targetPath: string, linkPath: string): void;
|
|
56
|
+
getUsageBytes?(targetPath?: string): number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface VirtualShellVfsOptions {
|
|
60
|
+
vfsInstance?: VirtualShellVfsLike;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function hasVfsInstance(obj: unknown): obj is { vfsInstance: VirtualShellVfsLike } {
|
|
64
|
+
return (
|
|
65
|
+
typeof obj === "object" &&
|
|
66
|
+
obj !== null &&
|
|
67
|
+
"vfsInstance" in obj &&
|
|
68
|
+
isVirtualShellVfsLike((obj as Record<string, unknown>).vfsInstance)
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function isVirtualShellVfsLike(value: unknown): value is VirtualShellVfsLike {
|
|
73
|
+
if (typeof value !== "object" || value === null) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const candidate = value as Record<string, unknown>;
|
|
78
|
+
return (
|
|
79
|
+
typeof candidate.restoreMirror === "function" &&
|
|
80
|
+
typeof candidate.flushMirror === "function" &&
|
|
81
|
+
typeof candidate.writeFile === "function" &&
|
|
82
|
+
typeof candidate.readFile === "function" &&
|
|
83
|
+
typeof candidate.mkdir === "function" &&
|
|
84
|
+
typeof candidate.exists === "function" &&
|
|
85
|
+
typeof candidate.stat === "function" &&
|
|
86
|
+
typeof candidate.list === "function" &&
|
|
87
|
+
typeof candidate.remove === "function" &&
|
|
88
|
+
typeof candidate.copy === "function" &&
|
|
89
|
+
typeof candidate.move === "function" &&
|
|
90
|
+
typeof candidate.touch === "function"
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
43
94
|
const defaultShellProperties: ShellProperties = {
|
|
44
95
|
kernel: "1.0.0+itsrealfortune+1-amd64",
|
|
45
96
|
os: "Fortune GNU/Linux x64",
|
|
@@ -104,14 +155,21 @@ class VirtualShell extends EventEmitter {
|
|
|
104
155
|
constructor(
|
|
105
156
|
hostname: string,
|
|
106
157
|
properties?: ShellProperties,
|
|
107
|
-
|
|
158
|
+
vfsOptionsOrInstance?: VfsOptions | VirtualShellVfsLike | VirtualShellVfsOptions,
|
|
108
159
|
) {
|
|
109
160
|
super();
|
|
110
161
|
perf.mark("constructor");
|
|
111
162
|
this.hostname = hostname;
|
|
112
163
|
this.properties = properties || defaultShellProperties;
|
|
113
164
|
this.startTime = Date.now();
|
|
114
|
-
|
|
165
|
+
|
|
166
|
+
if (isVirtualShellVfsLike(vfsOptionsOrInstance)) {
|
|
167
|
+
this.vfs = vfsOptionsOrInstance as unknown as VirtualFileSystem;
|
|
168
|
+
} else if (hasVfsInstance(vfsOptionsOrInstance)) {
|
|
169
|
+
this.vfs = vfsOptionsOrInstance.vfsInstance as unknown as VirtualFileSystem;
|
|
170
|
+
} else {
|
|
171
|
+
this.vfs = new VirtualFileSystem((vfsOptionsOrInstance as VfsOptions) ?? {});
|
|
172
|
+
}
|
|
115
173
|
this.users = new VirtualUserManager(this.vfs, resolveAutoSudoForNewUsers());
|
|
116
174
|
this.packageManager = new VirtualPackageManager(this.vfs, this.users);
|
|
117
175
|
|