thebird 1.2.79 → 1.2.80
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/workflows/publish.yml +9 -1
- package/CHANGELOG.md +217 -0
- package/CLAUDE.md +16 -0
- package/docs/agent-chat.js +7 -4
- package/docs/app.js +14 -11
- package/docs/defaults.json +1 -1
- package/docs/index.html +23 -6
- package/docs/kilo-http-stream.js +24 -0
- package/docs/node-builtins.js +24 -0
- package/docs/preview/index.html +32 -0
- package/docs/preview-sw-client.js +37 -6
- package/docs/preview-sw.js +55 -51
- package/docs/shell-awk.js +113 -0
- package/docs/shell-builtins-extra.js +121 -0
- package/docs/shell-builtins-text.js +109 -0
- package/docs/shell-builtins-util.js +112 -0
- package/docs/shell-builtins.js +183 -0
- package/docs/shell-bun.js +45 -0
- package/docs/shell-control.js +132 -0
- package/docs/shell-deno.js +54 -0
- package/docs/shell-exec.js +85 -0
- package/docs/shell-expand.js +164 -0
- package/docs/shell-fd.js +86 -0
- package/docs/shell-jobs.js +86 -0
- package/docs/shell-node-advanced.js +86 -0
- package/docs/shell-node-brotli.js +22 -0
- package/docs/shell-node-busnet.js +90 -0
- package/docs/shell-node-cipher.js +61 -0
- package/docs/shell-node-cluster.js +33 -0
- package/docs/shell-node-coreutils.js +36 -0
- package/docs/shell-node-crypto.js +137 -0
- package/docs/shell-node-dns.js +41 -0
- package/docs/shell-node-extras.js +148 -0
- package/docs/shell-node-firefox.js +95 -0
- package/docs/shell-node-git.js +60 -0
- package/docs/shell-node-inspector.js +39 -0
- package/docs/shell-node-io.js +131 -0
- package/docs/shell-node-ipc.js +15 -0
- package/docs/shell-node-keyobject.js +60 -0
- package/docs/shell-node-modules.js +157 -0
- package/docs/shell-node-native.js +31 -0
- package/docs/shell-node-net.js +71 -0
- package/docs/shell-node-observe.js +80 -0
- package/docs/shell-node-opfs.js +54 -0
- package/docs/shell-node-procfs.js +42 -0
- package/docs/shell-node-profiler.js +50 -0
- package/docs/shell-node-registry.js +24 -0
- package/docs/shell-node-resolve.js +147 -0
- package/docs/shell-node-runtime.js +83 -0
- package/docs/shell-node-srcmap.js +52 -0
- package/docs/shell-node-stdlib.js +103 -0
- package/docs/shell-node-streams.js +66 -0
- package/docs/shell-node-tar.js +47 -0
- package/docs/shell-node-testrunner.js +35 -0
- package/docs/shell-node-util-extras.js +66 -0
- package/docs/shell-node.js +175 -169
- package/docs/shell-npm.js +173 -0
- package/docs/shell-parser.js +122 -0
- package/docs/shell-pm-layout.js +62 -0
- package/docs/shell-pm.js +39 -0
- package/docs/shell-posix.js +70 -0
- package/docs/shell-procsub.js +65 -0
- package/docs/shell-readline.js +59 -4
- package/docs/shell-runtime.js +37 -0
- package/docs/shell-sed.js +83 -0
- package/docs/shell-signals.js +54 -0
- package/docs/shell-sw-jobs.js +76 -0
- package/docs/shell-ts.js +30 -0
- package/docs/shell.js +161 -152
- package/docs/terminal.js +9 -11
- package/docs/todo.html +211 -0
- package/package.json +1 -1
- package/server.js +43 -4
- package/start-kilo.js +17 -0
- package/test.js +199 -0
- package/.codeinsight +0 -73
- package/docs/acp-stream.js +0 -102
- package/docs/coi-serviceworker.js +0 -2
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { registerStream, readStream } from './shell-procsub.js';
|
|
2
|
+
import { tokenize, globToRe } from './shell-parser.js';
|
|
3
|
+
import { fullExpand, expandBraces, expandTilde } from './shell-expand.js';
|
|
4
|
+
import { resolvePath } from './shell-builtins.js';
|
|
5
|
+
import { NODE_VERSION } from './shell-node-modules.js';
|
|
6
|
+
|
|
7
|
+
export function makeExpander(ctx, captureRun, parseRedirect) {
|
|
8
|
+
const toKey = p => p.replace(/^\//, '');
|
|
9
|
+
const snap = () => window.__debug.idbSnapshot || {};
|
|
10
|
+
|
|
11
|
+
function replaceProcSub(token) {
|
|
12
|
+
let out = ''; let i = 0;
|
|
13
|
+
while (i < token.length) {
|
|
14
|
+
if ((token[i] === '<' || token[i] === '>') && token[i + 1] === '(') {
|
|
15
|
+
let depth = 1; let j = i + 2;
|
|
16
|
+
while (j < token.length && depth > 0) { if (token[j] === '(') depth++; else if (token[j] === ')') depth--; if (depth) j++; }
|
|
17
|
+
if (depth === 0) { out += registerStream(captureRun(token.slice(i + 2, j))); i = j + 1; continue; }
|
|
18
|
+
}
|
|
19
|
+
out += token[i++];
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function expandGlob(token) {
|
|
25
|
+
if (!token.includes('*') && !token.includes('?') && !token.includes('[')) return [token];
|
|
26
|
+
const prefix = toKey(resolvePath(ctx.cwd, ''));
|
|
27
|
+
const keys = Object.keys(snap()).map(k => prefix && k.startsWith(prefix + '/') ? k.slice(prefix.length + 1) : k);
|
|
28
|
+
const re = globToRe(token);
|
|
29
|
+
const matches = keys.filter(k => re.test(k));
|
|
30
|
+
return matches.length ? matches.sort() : [token];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function expandTokens(tokens) {
|
|
34
|
+
return tokens.flatMap(t => {
|
|
35
|
+
const procsub = t.includes('<(') || t.includes('>(') ? replaceProcSub(t) : t;
|
|
36
|
+
const tilde = expandTilde(procsub, ctx.env);
|
|
37
|
+
const braces = expandBraces(tilde);
|
|
38
|
+
return braces.flatMap(b => expandGlob(fullExpand(b, ctx.env, ctx.lastExitCode, ctx.argv, captureRun, ctx.arrays)));
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return { expandTokens, expandGlob, replaceProcSub };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function makeCaptureRun(ctx, BUILTINS, actor, parseRedirect, expandTokens) {
|
|
45
|
+
return function captureRun(line) {
|
|
46
|
+
const raw = tokenize(line); if (!raw.length) return '';
|
|
47
|
+
let out = ''; const orig = ctx.term.write.bind(ctx.term); ctx.term.write = s => { out += s; };
|
|
48
|
+
try { const [cmd, ...args] = parseRedirect(expandTokens(raw)).args; BUILTINS[cmd]?.(args, actor); } finally { ctx.term.write = orig; }
|
|
49
|
+
return out.replace(/\r\n/g, '\n').replace(/\r/g, '\n').trim();
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const NODE_HELP = 'Usage: node [options] [script.js] [arguments]\r\n -v, --version print Node.js version\r\n -e, --eval evaluate script\r\n -p, --print evaluate and print result\r\n -h, --help print this help\r\n';
|
|
54
|
+
|
|
55
|
+
export function makeNodeRunner(ctx, actor) {
|
|
56
|
+
const toKey = p => p.replace(/^\//, '');
|
|
57
|
+
const snap = () => window.__debug.idbSnapshot || {};
|
|
58
|
+
return async function runNode(args, stdinBuf) {
|
|
59
|
+
const term = ctx.term;
|
|
60
|
+
if (!args.length) { actor.send({ type: 'ENTER_REPL' }); term.write('Welcome to Node.js ' + NODE_VERSION + '.\r\nType ".help" for more information.\r\n> '); return; }
|
|
61
|
+
const a0 = args[0];
|
|
62
|
+
if (a0 === '-v' || a0 === '--version') { term.write(NODE_VERSION + '\r\n'); return; }
|
|
63
|
+
if (a0 === '-h' || a0 === '--help') { term.write(NODE_HELP); return; }
|
|
64
|
+
if (a0 === '-e' || a0 === '--eval') { await ctx.nodeEval(args.slice(1).join(' '), null, [], stdinBuf); return; }
|
|
65
|
+
if (a0 === '-p' || a0 === '--print') { await ctx.nodeEval('process.stdout.write(String(' + args.slice(1).join(' ') + ') + "\\n")', null, [], stdinBuf); return; }
|
|
66
|
+
const code = snap()[toKey(resolvePath(ctx.cwd, a0))];
|
|
67
|
+
if (code == null) { term.write('\x1b[31mnode: ' + a0 + ': No such file or directory\x1b[0m\r\n'); ctx.lastExitCode = 1; return; }
|
|
68
|
+
actor.send({ type: 'NODE_START' }); ctx.argv = args;
|
|
69
|
+
try { await ctx.nodeEval(code, a0, args.slice(1), stdinBuf); } finally { ctx.argv = []; }
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function makeNpmResultRunner(ctx, run) {
|
|
74
|
+
return async function runNpmResult(r) {
|
|
75
|
+
if (!r) return;
|
|
76
|
+
if (r.runInShell) { await run(r.runInShell); return; }
|
|
77
|
+
if (!r.npmChain) return;
|
|
78
|
+
for (const step of r.npmChain) {
|
|
79
|
+
ctx.term.write('\r\n> ' + r.pkgName + '@' + r.pkgVersion + ' ' + step.name + '\r\n> ' + step.cmd + '\r\n\r\n');
|
|
80
|
+
ctx.env.npm_lifecycle_event = step.name;
|
|
81
|
+
await run(step.cmd);
|
|
82
|
+
if (ctx.lastExitCode !== 0) return;
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
export function expandParam(name, env, argv, lastExit, arrays) {
|
|
2
|
+
if (name === '?') return String(lastExit ?? 0);
|
|
3
|
+
if (name === '!') return env['!'] || '';
|
|
4
|
+
if (name === '$') return env.$ || '0';
|
|
5
|
+
if (name === '#') return String((argv || []).length > 0 ? (argv || []).length - 1 : 0);
|
|
6
|
+
if (name === '@' || name === '*') return (argv || []).slice(1).join(' ');
|
|
7
|
+
if (name === '0') return (argv || [])[0] || '';
|
|
8
|
+
if (/^[1-9]$/.test(name)) return (argv || [])[parseInt(name)] || '';
|
|
9
|
+
const arrM = name.match(/^([A-Za-z_][A-Za-z0-9_]*)\[(.+?)\]$/);
|
|
10
|
+
if (arrM && arrays) {
|
|
11
|
+
const a = arrays[arrM[1]];
|
|
12
|
+
if (a == null) return '';
|
|
13
|
+
if (arrM[2] === '@' || arrM[2] === '*') return Array.isArray(a) ? a.join(' ') : Object.values(a).join(' ');
|
|
14
|
+
if (Array.isArray(a)) return a[parseInt(arrM[2], 10)] || '';
|
|
15
|
+
return a[arrM[2]] || '';
|
|
16
|
+
}
|
|
17
|
+
const lenArrM = name.match(/^#([A-Za-z_][A-Za-z0-9_]*)\[@\]$/);
|
|
18
|
+
if (lenArrM && arrays) { const a = arrays[lenArrM[1]] || []; return String(Array.isArray(a) ? a.length : Object.keys(a).length); }
|
|
19
|
+
return env[name] ?? '';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function expandParamOp(expr, env, argv, lastExit, arrays) {
|
|
23
|
+
if (expr.startsWith('!')) {
|
|
24
|
+
const prefM = expr.match(/^!([A-Za-z_]\w*)([@*])$/);
|
|
25
|
+
if (prefM) return Object.keys(env).filter(k => k.startsWith(prefM[1])).join(' ');
|
|
26
|
+
const keysM = expr.match(/^!([A-Za-z_]\w*)\[[@*]\]$/);
|
|
27
|
+
if (keysM && arrays) { const a = arrays[keysM[1]] || []; return Array.isArray(a) ? a.map((_, i) => i).join(' ') : Object.keys(a).join(' '); }
|
|
28
|
+
const indM = expr.match(/^!([A-Za-z_]\w*)$/);
|
|
29
|
+
if (indM) { const t = env[indM[1]]; return t ? expandParam(t, env, argv, lastExit, arrays) : ''; }
|
|
30
|
+
}
|
|
31
|
+
const caseM = expr.match(/^([A-Za-z_][A-Za-z0-9_]*|@)(\^\^|,,|\^|,)(.*)$/s);
|
|
32
|
+
if (caseM) {
|
|
33
|
+
const v = expandParam(caseM[1], env, argv, lastExit, arrays);
|
|
34
|
+
const op = caseM[2];
|
|
35
|
+
if (op === '^^') return v.toUpperCase();
|
|
36
|
+
if (op === ',,') return v.toLowerCase();
|
|
37
|
+
if (op === '^') return v.charAt(0).toUpperCase() + v.slice(1);
|
|
38
|
+
if (op === ',') return v.charAt(0).toLowerCase() + v.slice(1);
|
|
39
|
+
}
|
|
40
|
+
const qM = expr.match(/^([A-Za-z_][A-Za-z0-9_]*|@)@([QEP])$/);
|
|
41
|
+
if (qM) {
|
|
42
|
+
const v = expandParam(qM[1], env, argv, lastExit, arrays);
|
|
43
|
+
if (qM[2] === 'Q') return "'" + v.replace(/'/g, "'\\''") + "'";
|
|
44
|
+
if (qM[2] === 'E') return v.replace(/\\n/g, '\n').replace(/\\t/g, '\t');
|
|
45
|
+
if (qM[2] === 'P') return v;
|
|
46
|
+
}
|
|
47
|
+
const lenArrM = expr.match(/^#([A-Za-z_]\w*)\[[@*]\]$/);
|
|
48
|
+
if (lenArrM) { const a = (arrays || {})[lenArrM[1]] || []; return String(Array.isArray(a) ? a.length : Object.keys(a).length); }
|
|
49
|
+
const lenM = expr.match(/^#(.+)$/);
|
|
50
|
+
if (lenM) return String(expandParam(lenM[1], env, argv, lastExit, arrays).length);
|
|
51
|
+
const sliceM = expr.match(/^([^:]+):(\d+)(?::(\d+))?$/);
|
|
52
|
+
if (sliceM) { const v = expandParam(sliceM[1], env, argv, lastExit, arrays); const s = +sliceM[2]; return sliceM[3] !== undefined ? v.slice(s, s + (+sliceM[3])) : v.slice(s); }
|
|
53
|
+
const defM = expr.match(/^([A-Za-z_][A-Za-z0-9_]*|\?|#|@|[0-9])(:-|:=|:\?|:\+|-|=|\+)(.*)$/s);
|
|
54
|
+
if (defM) {
|
|
55
|
+
const [, name, op, def] = defM;
|
|
56
|
+
const v = expandParam(name, env, argv, lastExit, arrays);
|
|
57
|
+
const defined = v !== '' && v != null;
|
|
58
|
+
if (op === ':-' || op === '-') return defined ? v : def;
|
|
59
|
+
if (op === ':=' || op === '=') { if (!defined) env[name] = def; return defined ? v : def; }
|
|
60
|
+
if (op === ':?' || op === '?') { if (!defined) throw new Error(name + ': ' + (def || 'parameter null')); return v; }
|
|
61
|
+
if (op === ':+' || op === '+') return defined ? def : '';
|
|
62
|
+
}
|
|
63
|
+
const sufM = expr.match(/^([A-Za-z_][A-Za-z0-9_]*|@|#)(%%?|##?)(.+)$/s);
|
|
64
|
+
if (sufM) {
|
|
65
|
+
const [, name, op, pat] = sufM;
|
|
66
|
+
const v = expandParam(name, env, argv, lastExit, arrays);
|
|
67
|
+
const bare = globReLine(pat).replace(/^\^|\$$/g, '');
|
|
68
|
+
if (op === '#') { const m = v.match(new RegExp('^' + bare)); return m ? v.slice(m[0].length) : v; }
|
|
69
|
+
if (op === '##') { const m = v.match(new RegExp('^' + bare.replace(/\.\*/g, '.*?') + '.*')); return m ? '' : v; }
|
|
70
|
+
if (op === '%') { const m = v.match(new RegExp(bare + '$')); return m ? v.slice(0, -m[0].length) : v; }
|
|
71
|
+
if (op === '%%') { const m = v.match(new RegExp('^.*' + bare + '$')); return m ? '' : v; }
|
|
72
|
+
}
|
|
73
|
+
const subM = expr.match(/^([A-Za-z_][A-Za-z0-9_]*|@)\/(\/?)(.+?)\/(.*)$/s);
|
|
74
|
+
if (subM) {
|
|
75
|
+
const [, name, all, pat, rep] = subM;
|
|
76
|
+
const v = expandParam(name, env, argv, lastExit, arrays);
|
|
77
|
+
return v.replace(new RegExp(globReLine(pat).replace(/^\^|\$$/g, ''), all ? 'g' : ''), rep);
|
|
78
|
+
}
|
|
79
|
+
return expandParam(expr, env, argv, lastExit, arrays);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function globReLine(pat) { return '^' + pat.replace(/[-[\]{}()+.,\\^$|#]/g, (c) => (c === '*' || c === '?') ? c : '\\' + c).replace(/\*/g, '.*').replace(/\?/g, '.') + '$'; }
|
|
83
|
+
|
|
84
|
+
export function evalArith(expr, env) {
|
|
85
|
+
const src = expr.replace(/\b([A-Za-z_][A-Za-z0-9_]*)\b/g, (_, n) => String(parseInt(env[n], 10) || 0));
|
|
86
|
+
if (!/^[-+*/%()<>=!&|\s\d?:]+$/.test(src)) return 0;
|
|
87
|
+
try { return Function('"use strict"; return (' + src + ')')() | 0; } catch { return 0; }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function expandBraces(token) {
|
|
91
|
+
const listM = token.match(/^(.*?)\{([^{}]*,[^{}]*)\}(.*)$/s);
|
|
92
|
+
if (listM) {
|
|
93
|
+
const [, pre, list, post] = listM;
|
|
94
|
+
return list.split(',').flatMap(p => expandBraces(pre + p + post));
|
|
95
|
+
}
|
|
96
|
+
const rangeM = token.match(/^(.*?)\{(-?\d+)\.\.(-?\d+)(?:\.\.(-?\d+))?\}(.*)$/s);
|
|
97
|
+
if (rangeM) {
|
|
98
|
+
const [, pre, a, b, step, post] = rangeM;
|
|
99
|
+
const s = step ? +step : ((+a) <= (+b) ? 1 : -1);
|
|
100
|
+
const out = [];
|
|
101
|
+
for (let i = +a; s > 0 ? i <= +b : i >= +b; i += s) out.push(i);
|
|
102
|
+
return out.flatMap(i => expandBraces(pre + i + post));
|
|
103
|
+
}
|
|
104
|
+
return [token];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function expandTilde(token, env) {
|
|
108
|
+
if (token === '~') return env.HOME || '/';
|
|
109
|
+
if (token.startsWith('~/')) return (env.HOME || '') + token.slice(1);
|
|
110
|
+
const m = token.match(/^~([A-Za-z_][A-Za-z0-9_]*)(\/.*)?$/);
|
|
111
|
+
if (m) return '/home/' + m[1] + (m[2] || '');
|
|
112
|
+
return token;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function fullExpand(token, env, lastExit, argv, runCap, arrays) {
|
|
116
|
+
let out = '';
|
|
117
|
+
let i = 0;
|
|
118
|
+
while (i < token.length) {
|
|
119
|
+
if (token[i] === '`') {
|
|
120
|
+
const end = token.indexOf('`', i + 1);
|
|
121
|
+
if (end < 0) { out += token.slice(i); break; }
|
|
122
|
+
out += runCap ? runCap(token.slice(i + 1, end)) : '';
|
|
123
|
+
i = end + 1; continue;
|
|
124
|
+
}
|
|
125
|
+
if (token[i] === '$' && token[i + 1] === '(' && token[i + 2] === '(') {
|
|
126
|
+
const close = token.indexOf('))', i + 3);
|
|
127
|
+
if (close < 0) { out += token[i++]; continue; }
|
|
128
|
+
out += String(evalArith(token.slice(i + 3, close), env));
|
|
129
|
+
i = close + 2; continue;
|
|
130
|
+
}
|
|
131
|
+
if (token[i] === '$' && token[i + 1] === '(') {
|
|
132
|
+
const end = findMatch(token, i + 1, '(', ')');
|
|
133
|
+
if (end < 0) { out += token[i++]; continue; }
|
|
134
|
+
out += runCap ? runCap(token.slice(i + 2, end)) : '';
|
|
135
|
+
i = end + 1; continue;
|
|
136
|
+
}
|
|
137
|
+
if (token[i] === '$' && token[i + 1] === '{') {
|
|
138
|
+
const end = token.indexOf('}', i + 2);
|
|
139
|
+
if (end < 0) { out += token[i++]; continue; }
|
|
140
|
+
out += expandParamOp(token.slice(i + 2, end), env, argv, lastExit, arrays);
|
|
141
|
+
i = end + 1; continue;
|
|
142
|
+
}
|
|
143
|
+
if (token[i] === '$') {
|
|
144
|
+
const m = token.slice(i + 1).match(/^(\?|!|#|@|\*|[0-9]|[A-Za-z_][A-Za-z0-9_]*)/);
|
|
145
|
+
if (m) { out += expandParam(m[1], env, argv, lastExit, arrays); i += 1 + m[1].length; continue; }
|
|
146
|
+
}
|
|
147
|
+
out += token[i++];
|
|
148
|
+
}
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function findMatch(s, start, open, close) {
|
|
153
|
+
let depth = 0; let inSingle = false, inDouble = false;
|
|
154
|
+
for (let i = start; i < s.length; i++) {
|
|
155
|
+
const c = s[i];
|
|
156
|
+
if (c === "'" && !inDouble) inSingle = !inSingle;
|
|
157
|
+
else if (c === '"' && !inSingle) inDouble = !inDouble;
|
|
158
|
+
else if (!inSingle && !inDouble) {
|
|
159
|
+
if (c === open) depth++;
|
|
160
|
+
else if (c === close) { depth--; if (depth === 0) return i; }
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return -1;
|
|
164
|
+
}
|
package/docs/shell-fd.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
export function createFdTable(ctx) {
|
|
2
|
+
const table = { 0: { kind: 'stdin', data: '' }, 1: { kind: 'stdout' }, 2: { kind: 'stderr' } };
|
|
3
|
+
ctx.fds = table;
|
|
4
|
+
|
|
5
|
+
function open(fd, source, mode) {
|
|
6
|
+
const n = parseInt(fd, 10);
|
|
7
|
+
if (isNaN(n)) throw new Error('fd: invalid: ' + fd);
|
|
8
|
+
table[n] = { kind: 'file', path: source, mode: mode || 'r', buf: '' };
|
|
9
|
+
return n;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function close(fd) {
|
|
13
|
+
const n = parseInt(fd, 10);
|
|
14
|
+
delete table[n];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function dup2(src, dst) {
|
|
18
|
+
const s = parseInt(src, 10);
|
|
19
|
+
const d = parseInt(dst, 10);
|
|
20
|
+
if (!table[s]) throw new Error('fd: ' + src + ': bad descriptor');
|
|
21
|
+
table[d] = { ...table[s], duped: s };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function readFd(fd) {
|
|
25
|
+
const n = parseInt(fd, 10);
|
|
26
|
+
const slot = table[n];
|
|
27
|
+
if (!slot) throw new Error('fd ' + fd + ' not open');
|
|
28
|
+
if (slot.kind === 'stdin') return slot.data || '';
|
|
29
|
+
if (slot.kind === 'file') {
|
|
30
|
+
const snap = window.__debug?.idbSnapshot || {};
|
|
31
|
+
return snap[slot.path.replace(/^\//, '')] || '';
|
|
32
|
+
}
|
|
33
|
+
return slot.buf || '';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function writeFd(fd, data) {
|
|
37
|
+
const n = parseInt(fd, 10);
|
|
38
|
+
const slot = table[n];
|
|
39
|
+
if (!slot) throw new Error('fd ' + fd + ' not open');
|
|
40
|
+
if (slot.kind === 'stdout' || n === 1) ctx.term.write(data.replace(/\n/g, '\r\n'));
|
|
41
|
+
else if (slot.kind === 'stderr' || n === 2) ctx.term.write('\x1b[31m' + data.replace(/\n/g, '\r\n') + '\x1b[0m');
|
|
42
|
+
else if (slot.kind === 'file') {
|
|
43
|
+
const snap = window.__debug?.idbSnapshot || (window.__debug.idbSnapshot = {});
|
|
44
|
+
const k = slot.path.replace(/^\//, '');
|
|
45
|
+
snap[k] = slot.mode === 'a' ? (snap[k] || '') + data : data;
|
|
46
|
+
window.__debug?.idbPersist?.();
|
|
47
|
+
} else { slot.buf = (slot.buf || '') + data; }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return { table, open, close, dup2, readFd, writeFd };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function parseFdRedirects(tokens) {
|
|
54
|
+
const out = { args: [], redirs: [] };
|
|
55
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
56
|
+
const t = tokens[i];
|
|
57
|
+
const m = t.match(/^(\d+)?(>>|>|<|>&|<&)(\d+)?$/);
|
|
58
|
+
if (m) {
|
|
59
|
+
const from = m[1] != null ? +m[1] : (m[2].includes('<') ? 0 : 1);
|
|
60
|
+
const op = m[2];
|
|
61
|
+
const toNum = m[3] != null ? +m[3] : null;
|
|
62
|
+
if (op === '>&' || op === '<&') { out.redirs.push({ kind: 'dup', fd: from, target: toNum }); continue; }
|
|
63
|
+
const target = tokens[++i];
|
|
64
|
+
out.redirs.push({ kind: op === '<' ? 'read' : 'write', fd: from, path: target, append: op === '>>' });
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
out.args.push(t);
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function makeExecBuiltin(ctx, fdTable) {
|
|
73
|
+
return args => {
|
|
74
|
+
if (!args.length) return;
|
|
75
|
+
for (const a of args) {
|
|
76
|
+
const m = a.match(/^(\d+)>(>?)(.+)$/);
|
|
77
|
+
if (m) { fdTable.open(m[1], m[3], m[2] === '>' ? 'a' : 'w'); continue; }
|
|
78
|
+
const r = a.match(/^(\d+)<(.+)$/);
|
|
79
|
+
if (r) { fdTable.open(r[1], r[2], 'r'); continue; }
|
|
80
|
+
const d = a.match(/^(\d+)>&(\d+)$/);
|
|
81
|
+
if (d) { fdTable.dup2(d[2], d[1]); continue; }
|
|
82
|
+
const c = a.match(/^(\d+)>&-$/);
|
|
83
|
+
if (c) { fdTable.close(c[1]); continue; }
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { createMachine, createActor } from './vendor/xstate.js';
|
|
2
|
+
|
|
3
|
+
const jobMachine = createMachine({
|
|
4
|
+
id: 'job', initial: 'running',
|
|
5
|
+
states: {
|
|
6
|
+
running: { on: { STOP: 'stopped', DONE: 'done', FAIL: 'failed', SIGNAL: { actions: 'deliverSignal' } } },
|
|
7
|
+
stopped: { on: { CONT: 'running', DONE: 'done', SIGNAL: { actions: 'deliverSignal' } } },
|
|
8
|
+
done: { type: 'final' },
|
|
9
|
+
failed: { type: 'final' },
|
|
10
|
+
},
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export function createJobRegistry(ctx) {
|
|
14
|
+
ctx.bgJobs = ctx.bgJobs || {};
|
|
15
|
+
let nextId = 1;
|
|
16
|
+
|
|
17
|
+
function spawnJob(cmd, runPipeline) {
|
|
18
|
+
const id = String(nextId++);
|
|
19
|
+
const actor = createActor(jobMachine.provide({
|
|
20
|
+
actions: {
|
|
21
|
+
deliverSignal: (_, ev) => {
|
|
22
|
+
if (ev?.sig && ctx.signals) ctx.signals.raise(ev.sig);
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
}));
|
|
26
|
+
actor.start();
|
|
27
|
+
const job = { id, cmd, actor, done: false, stopped: false, killed: false, startedAt: Date.now() };
|
|
28
|
+
const p = (async () => {
|
|
29
|
+
try { await runPipeline(cmd); job.exit = ctx.lastExitCode; actor.send({ type: 'DONE' }); }
|
|
30
|
+
catch (e) { job.error = e.message; actor.send({ type: 'FAIL' }); }
|
|
31
|
+
finally { job.done = true; }
|
|
32
|
+
})();
|
|
33
|
+
job.promise = p;
|
|
34
|
+
ctx.bgJobs[id] = job;
|
|
35
|
+
if (ctx.swJobs) ctx.swJobs.register(id, cmd).catch(() => {});
|
|
36
|
+
return id;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function list() {
|
|
40
|
+
return Object.values(ctx.bgJobs).map(j => ({ id: j.id, cmd: j.cmd, state: j.actor?.getSnapshot().value || 'unknown', done: j.done, stopped: j.stopped }));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function resolve(ref) {
|
|
44
|
+
const id = ref.startsWith('%') ? ref.slice(1) : ref;
|
|
45
|
+
if (id === '+' || !id) { const keys = Object.keys(ctx.bgJobs); return ctx.bgJobs[keys[keys.length - 1]]; }
|
|
46
|
+
return ctx.bgJobs[id];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return { spawnJob, list, resolve };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function makeJobsBuiltin(ctx, registry) {
|
|
53
|
+
return args => {
|
|
54
|
+
const long = args.includes('-l');
|
|
55
|
+
for (const j of registry.list()) ctx.term.write('[' + j.id + '] ' + (j.stopped ? 'Stopped' : j.done ? 'Done' : 'Running') + (long ? ' ' + j.id : '') + ' ' + j.cmd + '\r\n');
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function makeFgBuiltin(ctx, registry) {
|
|
60
|
+
return async args => {
|
|
61
|
+
const job = registry.resolve(args[0] || '+');
|
|
62
|
+
if (!job) { ctx.term.write('fg: no such job\r\n'); ctx.lastExitCode = 1; return; }
|
|
63
|
+
if (job.stopped) { job.actor.send({ type: 'CONT' }); job.stopped = false; }
|
|
64
|
+
ctx.currentJob = job;
|
|
65
|
+
try { await job.promise; } finally { ctx.currentJob = null; }
|
|
66
|
+
ctx.lastExitCode = job.exit ?? (job.error ? 1 : 0);
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function makeBgBuiltin(ctx, registry) {
|
|
71
|
+
return args => {
|
|
72
|
+
const job = registry.resolve(args[0] || '+');
|
|
73
|
+
if (!job) { ctx.term.write('bg: no such job\r\n'); ctx.lastExitCode = 1; return; }
|
|
74
|
+
if (job.stopped) { job.actor.send({ type: 'CONT' }); job.stopped = false; }
|
|
75
|
+
ctx.term.write('[' + job.id + ']+ ' + job.cmd + ' &\r\n');
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function makeDisownBuiltin(ctx) {
|
|
80
|
+
return args => {
|
|
81
|
+
for (const a of args) {
|
|
82
|
+
const id = a.startsWith('%') ? a.slice(1) : a;
|
|
83
|
+
if (ctx.bgJobs[id]) { ctx.bgJobs[id].disowned = true; delete ctx.bgJobs[id]; }
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
export function makeStreamingZlib(streamMod,Buf,fflate){
|
|
2
|
+
const Transform=streamMod.Transform;
|
|
3
|
+
const mkT=(Klass,errMsg)=>()=>{let inst=null;const chunks=[];const out=new Transform({transform(c,e,cb){try{if(!inst)inst=new fflate[Klass]((chunk,fin)=>{out.push(Buf.from(chunk));});inst.push(c instanceof Uint8Array?c:new TextEncoder().encode(String(c)),false);cb();}catch(e){cb(e);}},flush(cb){try{if(inst)inst.push(new Uint8Array(0),true);cb();}catch(e){cb(e);}}});return out;};
|
|
4
|
+
return{
|
|
5
|
+
createGzip:mkT('Gzip'),
|
|
6
|
+
createGunzip:mkT('Gunzip'),
|
|
7
|
+
createDeflate:mkT('Deflate'),
|
|
8
|
+
createInflate:mkT('Inflate'),
|
|
9
|
+
createDeflateRaw:mkT('Deflate'),
|
|
10
|
+
createInflateRaw:mkT('Inflate'),
|
|
11
|
+
createBrotliCompress:()=>{throw new Error('brotli: not in webcrypto/CompressionStream — use gzip instead');},
|
|
12
|
+
createBrotliDecompress:()=>{throw new Error('brotli: not supported in browser');},
|
|
13
|
+
brotliCompressSync:()=>{throw new Error('brotli: not supported — use gzipSync');},
|
|
14
|
+
brotliDecompressSync:()=>{throw new Error('brotli: not supported');},
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function makeVmModule(){
|
|
19
|
+
const contexts=new WeakMap();
|
|
20
|
+
const registry=typeof FinalizationRegistry!=='undefined'?new FinalizationRegistry(iframe=>{try{iframe.remove();}catch{}}):{register(){}};
|
|
21
|
+
const hasDom=typeof document!=='undefined';
|
|
22
|
+
const cloneAcross=v=>{if(v==null||typeof v!=='object'&&typeof v!=='function')return v;if(typeof v==='function')return v;try{return structuredClone(v);}catch{return v;}};
|
|
23
|
+
const mkIframe=ctx=>{if(!hasDom)return{iframe:null,win:globalThis};const f=document.createElement('iframe');f.style.display='none';f.setAttribute('sandbox','allow-scripts allow-same-origin');document.body.appendChild(f);const win=f.contentWindow;if(ctx)for(const k of Object.keys(ctx))win[k]=cloneAcross(ctx[k]);registry.register(ctx||{},f);return{iframe:f,win};};
|
|
24
|
+
const syncBack=(ctx,win)=>{for(const k of Object.keys(ctx))if(k in win)ctx[k]=cloneAcross(win[k]);for(const k of Object.keys(win))if(!(k in ctx)&&!['window','self','document','location','navigator','parent','top','frames','opener','localStorage','sessionStorage'].includes(k))ctx[k]=cloneAcross(win[k]);};
|
|
25
|
+
return{
|
|
26
|
+
runInThisContext:code=>(0,eval)(code),
|
|
27
|
+
runInNewContext:(code,ctx={})=>{const{win}=mkIframe(ctx);const r=win.eval?win.eval(code):(0,eval)(code);if(win.eval)syncBack(ctx,win);return cloneAcross(r);},
|
|
28
|
+
runInContext:(code,ctxObj)=>{const ref=contexts.get(ctxObj);if(!ref)throw new Error('vm.runInContext: context not created via createContext');const r=ref.win.eval(code);syncBack(ctxObj,ref.win);return cloneAcross(r);},
|
|
29
|
+
createContext:(ctx={})=>{const ref=mkIframe(ctx);contexts.set(ctx,ref);return ctx;},
|
|
30
|
+
isContext:ctx=>contexts.has(ctx),
|
|
31
|
+
Script:class Script{constructor(code){this.code=code;}runInThisContext(){return(0,eval)(this.code);}runInNewContext(ctx={}){const{win}=mkIframe(ctx);const r=win.eval?win.eval(this.code):(0,eval)(this.code);if(win.eval)syncBack(ctx,win);return cloneAcross(r);}runInContext(ctx){const ref=contexts.get(ctx);if(!ref)throw new Error('Script.runInContext: createContext first');const r=ref.win.eval(this.code);syncBack(ctx,ref.win);return cloneAcross(r);}},
|
|
32
|
+
compileFunction:(code,params=[])=>new Function(...params,code),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function makeModuleRegister(MODULES,snap,pathmod){
|
|
37
|
+
const hooks=[];
|
|
38
|
+
return{
|
|
39
|
+
register(specifier,parentURL){hooks.push({specifier,parentURL:parentURL||'file:///'});return{addEventListener(){}};},
|
|
40
|
+
_runResolve:async(specifier,context)=>{let result={url:specifier,shortCircuit:false};for(const h of hooks){try{const m=await import(h.specifier);if(m.resolve){const nextResolve=async(s,c)=>({url:s,shortCircuit:true});const r=await m.resolve(specifier,context,nextResolve);if(r&&r.shortCircuit){result=r;break;}if(r)result=r;}}catch{}}return result;},
|
|
41
|
+
_runLoad:async(url,context)=>{let result={format:'module',source:null,shortCircuit:false};for(const h of hooks){try{const m=await import(h.specifier);if(m.load){const nextLoad=async(u,c)=>({format:'module',source:null,shortCircuit:true});const r=await m.load(url,context,nextLoad);if(r&&r.shortCircuit){result=r;break;}if(r)result=r;}}catch{}}return result;},
|
|
42
|
+
_hooks:hooks,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function makeHttp2(){
|
|
47
|
+
const mkStream=()=>{const handlers={};const on=(e,f)=>{(handlers[e]=handlers[e]||[]).push(f);return stream;};const emit=(e,...a)=>{for(const f of handlers[e]||[])f(...a);};const stream={on,once:on,emit,close(){emit('close');},end(){emit('end');},write(){},pipe:d=>d,setEncoding(){return stream;}};return{stream,emit};};
|
|
48
|
+
return{
|
|
49
|
+
connect(authority){
|
|
50
|
+
const h={};let closed=false;
|
|
51
|
+
const session={
|
|
52
|
+
on:(e,f)=>{(h[e]=h[e]||[]).push(f);return session;},
|
|
53
|
+
once:(e,f)=>session.on(e,f),
|
|
54
|
+
emit:(e,...a)=>{for(const f of h[e]||[])f(...a);},
|
|
55
|
+
close(){closed=true;session.emit('close');},
|
|
56
|
+
destroy(){closed=true;session.emit('close');},
|
|
57
|
+
request(headers){
|
|
58
|
+
const {stream,emit}=mkStream();
|
|
59
|
+
const method=headers[':method']||'GET';
|
|
60
|
+
const path=headers[':path']||'/';
|
|
61
|
+
const url=authority.toString().replace(/\/$/,'')+path;
|
|
62
|
+
const fetchHeaders={};for(const[k,v]of Object.entries(headers))if(!k.startsWith(':'))fetchHeaders[k]=v;
|
|
63
|
+
fetch(url,{method,headers:fetchHeaders}).then(async r=>{const respHeaders={':status':r.status};r.headers.forEach((v,k)=>{respHeaders[k]=v;});emit('response',respHeaders,0);const reader=r.body?.getReader();if(reader)for(;;){const{value,done}=await reader.read();if(done)break;emit('data',new Uint8Array(value));}emit('end');emit('close');}).catch(e=>emit('error',e));
|
|
64
|
+
return stream;
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
queueMicrotask(()=>session.emit('connect',session));
|
|
68
|
+
return session;
|
|
69
|
+
},
|
|
70
|
+
constants:{NGHTTP2_REFUSED_STREAM:0xb,HTTP2_HEADER_METHOD:':method',HTTP2_HEADER_PATH:':path',HTTP2_HEADER_STATUS:':status'},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let wasiPromise=null;
|
|
75
|
+
async function getWasi(){if(!wasiPromise)wasiPromise=import('https://esm.sh/@bjorn3/browser_wasi_shim@0.3.0/es2022/browser_wasi_shim.mjs').then(m=>m.default||m);return wasiPromise;}
|
|
76
|
+
|
|
77
|
+
export function makeWasi(){
|
|
78
|
+
return{
|
|
79
|
+
WASI:class WASI{
|
|
80
|
+
constructor(opts={}){this._opts=opts;this._lib=null;this.wasiImport={};}
|
|
81
|
+
async _load(){if(!this._lib){this._lib=await getWasi();const args=this._opts.args||[];const env=Object.entries(this._opts.env||{}).map(([k,v])=>`${k}=${v}`);const fds=[new this._lib.OpenFile(new this._lib.File([])),new this._lib.OpenFile(new this._lib.File([])),new this._lib.OpenFile(new this._lib.File([]))];this._wasi=new this._lib.WASI(args,env,fds);this.wasiImport=this._wasi.wasiImport;}return this._wasi;}
|
|
82
|
+
async start(instance){await this._load();return this._wasi.start(instance);}
|
|
83
|
+
async initialize(instance){await this._load();return this._wasi.initialize(instance);}
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
let brotliMod=null;let brotliPromise=null;
|
|
2
|
+
|
|
3
|
+
async function loadBrotli(){
|
|
4
|
+
if(!brotliPromise)brotliPromise=import('https://esm.sh/brotli-wasm@3.0.1/es2022/brotli-wasm.mjs').then(async m=>{const lib=m.default||m;if(lib.then)return await lib;if(lib.compress&&lib.decompress)return lib;return lib;});
|
|
5
|
+
return brotliPromise;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function preloadBrotli(){brotliMod=await loadBrotli();return brotliMod;}
|
|
9
|
+
|
|
10
|
+
export function makeBrotli(streamMod,Buf){
|
|
11
|
+
const need=()=>{if(!brotliMod)throw new Error('brotli: call preloadBrotli() once before sync brotli calls (auto-preloaded on node entry)');return brotliMod;};
|
|
12
|
+
const toBytes=d=>d instanceof Uint8Array?d:new TextEncoder().encode(String(d));
|
|
13
|
+
const encodeErr=fn=>{try{return fn();}catch(e){throw new Error('brotli: '+e.message);}};
|
|
14
|
+
return{
|
|
15
|
+
brotliCompressSync:b=>Buf.from(encodeErr(()=>need().compress(toBytes(b)))),
|
|
16
|
+
brotliDecompressSync:b=>Buf.from(encodeErr(()=>need().decompress(toBytes(b)))),
|
|
17
|
+
brotliCompress:async(b,cb)=>{try{await loadBrotli();const out=Buf.from(need().compress(toBytes(b)));if(cb)cb(null,out);return out;}catch(e){if(cb)cb(e);else throw e;}},
|
|
18
|
+
brotliDecompress:async(b,cb)=>{try{await loadBrotli();const out=Buf.from(need().decompress(toBytes(b)));if(cb)cb(null,out);return out;}catch(e){if(cb)cb(e);else throw e;}},
|
|
19
|
+
createBrotliCompress:()=>{const chunks=[];return new streamMod.Transform({transform(c,e,cb){chunks.push(toBytes(c));cb();},flush(cb){try{const all=new Uint8Array(chunks.reduce((s,c)=>s+c.length,0));let off=0;for(const c of chunks){all.set(c,off);off+=c.length;}this.push(Buf.from(need().compress(all)));cb();}catch(e){cb(e);}}});},
|
|
20
|
+
createBrotliDecompress:()=>{const chunks=[];return new streamMod.Transform({transform(c,e,cb){chunks.push(toBytes(c));cb();},flush(cb){try{const all=new Uint8Array(chunks.reduce((s,c)=>s+c.length,0));let off=0;for(const c of chunks){all.set(c,off);off+=c.length;}this.push(Buf.from(need().decompress(all)));cb();}catch(e){cb(e);}}});},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
const BUS_CHAN='plugkit-busnet';
|
|
2
|
+
const bus=typeof BroadcastChannel!=='undefined'?new BroadcastChannel(BUS_CHAN):null;
|
|
3
|
+
const listeners=new Map();
|
|
4
|
+
const connections=new Map();let connSeq=1;
|
|
5
|
+
const serviceRegistry=new Map();
|
|
6
|
+
let _origin=null;
|
|
7
|
+
function getOrigin(){if(_origin)return _origin;_origin=(globalThis.crypto?.randomUUID?.()||String(Math.random())).slice(0,8);return _origin;}
|
|
8
|
+
|
|
9
|
+
function handleMsg(m){
|
|
10
|
+
if(!m)return;
|
|
11
|
+
if(m.type==='discover'&&m.origin!==getOrigin()){for(const[port,l] of listeners)post({type:'announce',port,service:l.service,origin:getOrigin()});return;}
|
|
12
|
+
if(m.type==='connect'&&listeners.has(m.port)){const l=listeners.get(m.port);const conn=createConnection(m.id,m.from,true);l.onConnection?.(conn);connections.set(m.id,conn);post({type:'connected',id:m.id,from:getOrigin(),to:m.from});return;}
|
|
13
|
+
if(m.type==='connected'&&connections.has(m.id)){queueMicrotask(()=>connections.get(m.id)?._onOpen());return;}
|
|
14
|
+
if(m.type==='data'&&connections.has(m.id)&&m.from!==null&&m.to===getOrigin()){connections.get(m.id)._onData(m.data);return;}
|
|
15
|
+
if(m.type==='data-local'&&connections.has(m.id)){connections.get(m.id)._onData(m.data);return;}
|
|
16
|
+
if(m.type==='close'&&connections.has(m.id)){connections.get(m.id)._onClose();connections.delete(m.id);return;}
|
|
17
|
+
if(m.type==='announce'&&m.origin!==getOrigin()){serviceRegistry.set(m.port+'@'+m.origin,{port:m.port,service:m.service,origin:m.origin,seen:Date.now()});return;}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function post(msg){if(bus)bus.postMessage(msg);}
|
|
21
|
+
|
|
22
|
+
if(bus)bus.addEventListener('message',e=>{if(e.data?.origin===getOrigin())return;handleMsg(e.data);});
|
|
23
|
+
|
|
24
|
+
function createConnection(id,remote,fromServer=false){
|
|
25
|
+
const handlers={data:[],end:[],close:[],open:[],error:[]};
|
|
26
|
+
const conn={
|
|
27
|
+
id,remote,fromServer,readable:true,writable:true,
|
|
28
|
+
on(ev,fn){(handlers[ev]=handlers[ev]||[]).push(fn);return this;},
|
|
29
|
+
once(ev,fn){const w=(...a)=>{this.off(ev,w);fn(...a);};return this.on(ev,w);},
|
|
30
|
+
off(ev,fn){handlers[ev]=(handlers[ev]||[]).filter(x=>x!==fn);return this;},
|
|
31
|
+
write(data){const peer=connections.get(conn._peerId);if(peer){queueMicrotask(()=>peer._onData(data));return true;}post({type:'data',id,data,from:getOrigin(),to:remote});return true;},
|
|
32
|
+
end(data){if(data)conn.write(data);for(const h of handlers.close)h();connections.delete(id);},
|
|
33
|
+
destroy(){for(const h of handlers.close)h();connections.delete(id);},
|
|
34
|
+
_onOpen(){for(const h of handlers.open)h();},
|
|
35
|
+
_onData(d){for(const h of handlers.data)h(d);},
|
|
36
|
+
_onClose(){for(const h of handlers.close)h();},
|
|
37
|
+
_onError(e){for(const h of handlers.error)h(e);},
|
|
38
|
+
_peerId:null,
|
|
39
|
+
};
|
|
40
|
+
return conn;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function makeBusnet(){
|
|
44
|
+
return{
|
|
45
|
+
listen(port,service,onConnection){
|
|
46
|
+
if(listeners.has(port))throw Object.assign(new Error('EADDRINUSE'),{code:'EADDRINUSE',port});
|
|
47
|
+
listeners.set(port,{port,service:service||'generic',onConnection,origin:getOrigin()});
|
|
48
|
+
if(bus)bus.postMessage({type:'announce',port,service:service||'generic',origin:getOrigin()});
|
|
49
|
+
return{port,close:()=>{listeners.delete(port);}};
|
|
50
|
+
},
|
|
51
|
+
connect(port,targetOrigin,cb){
|
|
52
|
+
const id=getOrigin()+'-'+(connSeq++);
|
|
53
|
+
const clientConn=createConnection(id,targetOrigin);
|
|
54
|
+
connections.set(id,clientConn);
|
|
55
|
+
if(cb)clientConn.on('open',cb);
|
|
56
|
+
if(listeners.has(port)&&(!targetOrigin||targetOrigin===getOrigin())){
|
|
57
|
+
const srvId=id+'-srv';
|
|
58
|
+
const serverConn=createConnection(srvId,getOrigin(),true);
|
|
59
|
+
connections.set(srvId,serverConn);
|
|
60
|
+
clientConn._peerId=srvId; serverConn._peerId=id;
|
|
61
|
+
const l=listeners.get(port);
|
|
62
|
+
queueMicrotask(()=>{l.onConnection?.(serverConn);serverConn._onOpen();clientConn._onOpen();});
|
|
63
|
+
}else{
|
|
64
|
+
post({type:'connect',id,port,from:getOrigin(),to:targetOrigin});
|
|
65
|
+
}
|
|
66
|
+
return clientConn;
|
|
67
|
+
},
|
|
68
|
+
discover(filter){
|
|
69
|
+
if(bus)bus.postMessage({type:'discover',origin:getOrigin()});
|
|
70
|
+
return new Promise(r=>setTimeout(()=>{const out=[...serviceRegistry.values()];r(filter?.service?out.filter(s=>s.service===filter.service):out);},200));
|
|
71
|
+
},
|
|
72
|
+
getListeners(){return[...listeners.keys()];},
|
|
73
|
+
getServices(){return[...serviceRegistry.values()];},
|
|
74
|
+
origin:getOrigin(),
|
|
75
|
+
_bus:bus,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function makeBusHttp(busnet){
|
|
80
|
+
const respond=(conn,status,headers,body)=>{const res=`HTTP/1.1 ${status} ${status===200?'OK':'ERR'}\r\n`+Object.entries(headers||{}).map(([k,v])=>`${k}: ${v}`).join('\r\n')+`\r\nContent-Length: ${body.length}\r\n\r\n${body}`;conn.write(res);};
|
|
81
|
+
return{
|
|
82
|
+
createServer(handler){
|
|
83
|
+
return{
|
|
84
|
+
listen(port,host,cb){if(typeof host==='function'){cb=host;host=null;}busnet.listen(port,'http',conn=>{conn.on('data',raw=>{const[reqLine,...rest]=String(raw).split('\r\n');const[method,url]=reqLine.split(' ');const bodyIdx=rest.indexOf('');const headers={};for(const line of rest.slice(0,bodyIdx)){const [k,v]=line.split(':');if(k)headers[k.trim().toLowerCase()]=v?.trim();}const req={method,url,headers,body:rest.slice(bodyIdx+1).join('\r\n')};const res={statusCode:200,_headers:{},setHeader(k,v){this._headers[k]=v;},end(body){respond(conn,this.statusCode,this._headers,body||'');conn.end();}};handler(req,res);});});cb?.();return this;},
|
|
85
|
+
close(cb){cb?.();},
|
|
86
|
+
};
|
|
87
|
+
},
|
|
88
|
+
request(opts,cb){const port=opts.port,targetOrigin=opts.origin||null;const conn=busnet.connect(port,targetOrigin,()=>{const path=opts.path||'/';const method=opts.method||'GET';const hdrs=Object.entries(opts.headers||{}).map(([k,v])=>`${k}: ${v}`).join('\r\n');conn.write(`${method} ${path} HTTP/1.1\r\n${hdrs}\r\n\r\n`);});const handlers={response:[]};conn.on('data',raw=>{const[status,...rest]=String(raw).split('\r\n');const s=status.match(/HTTP\/\S+\s+(\d+)/);const bodyIdx=rest.indexOf('');const headers={};for(const line of rest.slice(0,bodyIdx)){const [k,v]=line.split(':');if(k)headers[k.trim().toLowerCase()]=v?.trim();}const body=rest.slice(bodyIdx+1).join('\r\n');const res={statusCode:s?+s[1]:200,headers,body,on(ev,fn){if(ev==='data')queueMicrotask(()=>fn(body));if(ev==='end')queueMicrotask(()=>fn());return res;}};for(const h of handlers.response)h(res);cb?.(res);});const req={on(ev,fn){if(ev==='response')handlers.response.push(fn);return req;},end(){},write(){}};return req;},
|
|
89
|
+
};
|
|
90
|
+
}
|