thebird 1.2.79 → 1.2.81
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-fs-mirror.js +15 -0
- package/docs/kilo-http-stream.js +47 -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 +45 -0
- package/test.js +199 -0
- package/.codeinsight +0 -73
- package/docs/acp-stream.js +0 -102
- package/docs/coi-serviceworker.js +0 -2
package/docs/index.html
CHANGED
|
@@ -5,8 +5,6 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<meta name="version" content="1.2.23">
|
|
7
7
|
<title>thebird — TUI</title>
|
|
8
|
-
<script>window.coi = { coepDegrade: () => false };</script>
|
|
9
|
-
<script src="coi-serviceworker.js"></script>
|
|
10
8
|
<link rel="stylesheet" href="vendor/xterm.css" />
|
|
11
9
|
<link rel="stylesheet" href="tui.css" />
|
|
12
10
|
</head>
|
|
@@ -42,24 +40,43 @@ function callExpressRoute(path, method) {
|
|
|
42
40
|
const match = routes.find(r => r.path === '*' || r.path === path || path.startsWith(r.path));
|
|
43
41
|
if (!match) return null;
|
|
44
42
|
return new Promise(resolve => {
|
|
43
|
+
let done = false;
|
|
44
|
+
const finish = (status, body, ct) => { if (done) return; done = true; resolve({ status, body, ct }); };
|
|
45
45
|
const res = {
|
|
46
46
|
_body: '', _status: 200, _ct: 'text/html',
|
|
47
|
-
|
|
47
|
+
writeHead(code, headers) { this._status = code; if (headers?.['Content-Type']) this._ct = headers['Content-Type']; return this; },
|
|
48
|
+
setHeader(k, v) { if (k.toLowerCase() === 'content-type') this._ct = v; return this; },
|
|
49
|
+
write(chunk) { this._body += String(chunk); return true; },
|
|
50
|
+
end(chunk) { if (chunk != null) this._body += String(chunk); finish(this._status, this._body, this._ct); },
|
|
51
|
+
send(b) { this._body = typeof b === 'string' ? b : JSON.stringify(b); finish(this._status, this._body, this._ct); },
|
|
48
52
|
json(o) { this._ct = 'application/json'; this.send(JSON.stringify(o)); },
|
|
49
53
|
status(n) { this._status = n; return this; },
|
|
50
54
|
};
|
|
51
|
-
|
|
52
|
-
|
|
55
|
+
const req = { method: method || 'GET', url: path, path, query: {}, headers: {}, [Symbol.asyncIterator]: async function* () {} };
|
|
56
|
+
try {
|
|
57
|
+
const r = match.fn(req, res);
|
|
58
|
+
if (r && typeof r.then === 'function') r.catch(e => finish(500, e.message, 'text/plain'));
|
|
59
|
+
} catch(e) { finish(500, e.message, 'text/plain'); }
|
|
60
|
+
setTimeout(() => finish(res._status, res._body, res._ct), 5000);
|
|
53
61
|
});
|
|
54
62
|
}
|
|
55
63
|
async function refreshPreview() {
|
|
56
64
|
const iframe = document.getElementById('preview-frame');
|
|
57
65
|
if (!iframe) return;
|
|
66
|
+
const swReady = window.__debug?.sw?.registered;
|
|
67
|
+
const handlers = window.__debug?.shell?.httpHandlers || {};
|
|
68
|
+
const hasServer = Object.keys(handlers).length > 0;
|
|
69
|
+
if (swReady && hasServer) {
|
|
70
|
+
const target = './preview/';
|
|
71
|
+
if (!iframe.src || !iframe.src.includes('/preview/')) iframe.src = target;
|
|
72
|
+
else iframe.contentWindow?.location.reload();
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
58
75
|
const result = await callExpressRoute('/');
|
|
59
76
|
if (result) { iframe.srcdoc = result.body; return; }
|
|
60
77
|
const snap = window.__debug?.idbSnapshot || {};
|
|
61
78
|
if (snap['index.html']) { iframe.srcdoc = snap['index.html']; return; }
|
|
62
|
-
iframe.srcdoc = '<pre style="color:#33ff33;padding:1ch;font-family:monospace">No express app running.\nRun node
|
|
79
|
+
iframe.srcdoc = '<pre style="color:#33ff33;padding:1ch;font-family:monospace">No express app running.\nRun: node server.js</pre>';
|
|
63
80
|
}
|
|
64
81
|
function switchTab(t) {
|
|
65
82
|
['chat', 'term', 'preview'].forEach(id => {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export async function mirrorFromSandbox(fsBase, _touchedPaths) {
|
|
2
|
+
const listRes = await fetch(fsBase + '/__list');
|
|
3
|
+
if (!listRes.ok) return [];
|
|
4
|
+
const relFiles = await listRes.json();
|
|
5
|
+
const snap = window.__debug.idbSnapshot || (window.__debug.idbSnapshot = {});
|
|
6
|
+
const mirrored = [];
|
|
7
|
+
for (const rel of relFiles) {
|
|
8
|
+
const r = await fetch(fsBase + '/' + rel);
|
|
9
|
+
if (!r.ok) continue;
|
|
10
|
+
const content = await r.text();
|
|
11
|
+
if (snap[rel] !== content) { snap[rel] = content; mirrored.push(rel); }
|
|
12
|
+
}
|
|
13
|
+
if (mirrored.length) { window.__debug.idbPersist?.(); window.__debug.shell?.onPreviewWrite?.(); }
|
|
14
|
+
return mirrored;
|
|
15
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { mirrorFromSandbox } from './kilo-fs-mirror.js';
|
|
2
|
+
|
|
3
|
+
export async function* streamKiloHTTP({ url, model, messages }) {
|
|
4
|
+
yield { type: 'start-step' };
|
|
5
|
+
const base = (url || 'http://localhost:4780').replace(/\/$/, '');
|
|
6
|
+
const fsBase = base.replace(/:\d+$/, ':4781');
|
|
7
|
+
let sessRes;
|
|
8
|
+
try { sessRes = await fetch(base + '/session', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' }); }
|
|
9
|
+
catch (e) { throw new Error('kilo serve not reachable at ' + base + ' — start it with: node start-kilo.js --origin ' + location.origin); }
|
|
10
|
+
if (!sessRes.ok) throw new Error('kilo /session ' + sessRes.status + ': ' + await sessRes.text());
|
|
11
|
+
const { id: sessionId } = await sessRes.json();
|
|
12
|
+
Object.assign(window.__debug = window.__debug || {}, { kilo: { sessionId, url: base, fsBase, writes: [], lastStatus: null } });
|
|
13
|
+
|
|
14
|
+
const es = new EventSource(base + '/event');
|
|
15
|
+
const pendingWrites = new Set();
|
|
16
|
+
es.onmessage = (ev) => {
|
|
17
|
+
try {
|
|
18
|
+
const msg = JSON.parse(ev.data);
|
|
19
|
+
if (msg.type === 'message.part.updated') {
|
|
20
|
+
const part = msg.properties?.part;
|
|
21
|
+
if (part?.type === 'tool' && part.state?.status === 'completed' && (part.tool === 'write' || part.tool === 'edit')) {
|
|
22
|
+
const abs = part.state.input?.filePath;
|
|
23
|
+
if (abs) pendingWrites.add(abs);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
} catch (_) {}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const userText = messages.filter(m => m.role === 'user').map(m =>
|
|
30
|
+
typeof m.content === 'string' ? m.content : (m.content || []).filter(b => b.type === 'text').map(b => b.text).join('')
|
|
31
|
+
).join('\n');
|
|
32
|
+
|
|
33
|
+
const body = { parts: [{ type: 'text', text: userText }], providerID: 'kilo', modelID: model || 'x-ai/grok-code-fast-1:optimized:free' };
|
|
34
|
+
const msgRes = await fetch(base + '/session/' + sessionId + '/message', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
|
35
|
+
window.__debug.kilo.lastStatus = msgRes.status;
|
|
36
|
+
if (!msgRes.ok) { es.close(); throw new Error('kilo message ' + msgRes.status + ': ' + await msgRes.text()); }
|
|
37
|
+
const result = await msgRes.json();
|
|
38
|
+
window.__debug.kilo.lastResult = result;
|
|
39
|
+
es.close();
|
|
40
|
+
const mirrored = await mirrorFromSandbox(fsBase, [...pendingWrites]);
|
|
41
|
+
window.__debug.kilo.writes = mirrored;
|
|
42
|
+
if (mirrored.length) window.refreshPreview?.();
|
|
43
|
+
const textParts = (result.parts || []).filter(p => p.type === 'text');
|
|
44
|
+
for (const tp of textParts) yield { type: 'text-delta', textDelta: tp.text };
|
|
45
|
+
if (mirrored.length) yield { type: 'text-delta', textDelta: '\n\n[mirrored to preview: ' + mirrored.join(', ') + ']' };
|
|
46
|
+
yield { type: 'finish-step', finishReason: result.info?.finish || 'stop' };
|
|
47
|
+
}
|
package/docs/node-builtins.js
CHANGED
|
@@ -103,6 +103,16 @@ export function createFs() {
|
|
|
103
103
|
snap()[toKey(resolveP(d))] = src;
|
|
104
104
|
persist();
|
|
105
105
|
},
|
|
106
|
+
rmSync: (p, opts = {}) => {
|
|
107
|
+
const key = toKey(resolveP(p)); const s = snap();
|
|
108
|
+
if (key in s) { delete s[key]; persist(); return; }
|
|
109
|
+
if (opts.recursive) { for (const k of Object.keys(s)) if (k === key || k.startsWith(key + '/')) delete s[k]; persist(); return; }
|
|
110
|
+
if (!opts.force) throw Object.assign(new Error('ENOENT: ' + p), { code: 'ENOENT' });
|
|
111
|
+
},
|
|
112
|
+
rmdirSync: (p, opts = {}) => { const key = toKey(resolveP(p)); for (const k of Object.keys(snap())) if (k.startsWith(key + '/')) delete snap()[k]; persist(); },
|
|
113
|
+
accessSync: p => { if (!(toKey(resolveP(p)) in snap())) throw Object.assign(new Error('ENOENT: ' + p), { code: 'ENOENT' }); },
|
|
114
|
+
realpathSync: p => resolveP(p),
|
|
115
|
+
promises: null,
|
|
106
116
|
};
|
|
107
117
|
}
|
|
108
118
|
|
|
@@ -153,6 +163,17 @@ export function createBuffer() {
|
|
|
153
163
|
}
|
|
154
164
|
toJSON() { return { type: 'Buffer', data: [...this] }; }
|
|
155
165
|
slice(s, e) { return Buf.from(super.slice(s, e)); }
|
|
166
|
+
subarray(s, e) { return Buf.from(super.subarray(s, e)); }
|
|
167
|
+
equals(o) { if (!(o instanceof Uint8Array) || o.length !== this.length) return false; for (let i = 0; i < this.length; i++) if (this[i] !== o[i]) return false; return true; }
|
|
168
|
+
compare(o) { const l = Math.min(this.length, o.length); for (let i = 0; i < l; i++) { if (this[i] < o[i]) return -1; if (this[i] > o[i]) return 1; } return this.length === o.length ? 0 : this.length < o.length ? -1 : 1; }
|
|
169
|
+
indexOf(v, fromIdx = 0) { const bytes = typeof v === 'string' ? new TextEncoder().encode(v) : v instanceof Uint8Array ? v : new Uint8Array([v]); outer: for (let i = fromIdx; i <= this.length - bytes.length; i++) { for (let j = 0; j < bytes.length; j++) if (this[i + j] !== bytes[j]) continue outer; return i; } return -1; }
|
|
170
|
+
includes(v) { return this.indexOf(v) !== -1; }
|
|
171
|
+
write(str, offset = 0, length, encoding) { if (typeof offset === 'string') { encoding = offset; offset = 0; } const bytes = new TextEncoder().encode(str); const n = Math.min(bytes.length, this.length - offset, length ?? bytes.length); this.set(bytes.subarray(0, n), offset); return n; }
|
|
172
|
+
readUInt8(o = 0) { return this[o]; }
|
|
173
|
+
readUInt16BE(o = 0) { return (this[o] << 8) | this[o + 1]; }
|
|
174
|
+
readUInt16LE(o = 0) { return this[o] | (this[o + 1] << 8); }
|
|
175
|
+
readUInt32BE(o = 0) { return ((this[o] * 0x1000000) + (this[o + 1] << 16) | (this[o + 2] << 8) | this[o + 3]) >>> 0; }
|
|
176
|
+
writeUInt8(v, o = 0) { this[o] = v & 0xff; return o + 1; }
|
|
156
177
|
}
|
|
157
178
|
Buf.from = (d, enc) => {
|
|
158
179
|
if (d instanceof Uint8Array) return new Buf(d);
|
|
@@ -166,5 +187,8 @@ export function createBuffer() {
|
|
|
166
187
|
Buf.concat = list => { const t = list.reduce((s, b) => s + b.length, 0); const r = new Buf(t); let o = 0; for (const b of list) { r.set(b, o); o += b.length; } return r; };
|
|
167
188
|
Buf.isBuffer = o => o instanceof Buf;
|
|
168
189
|
Buf.byteLength = (s, enc) => Buf.from(s, enc).length;
|
|
190
|
+
Buf.compare = (a, b) => a.compare(b);
|
|
191
|
+
Buf.allocUnsafe = n => new Buf(n);
|
|
192
|
+
Buf.poolSize = 8192;
|
|
169
193
|
return Buf;
|
|
170
194
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html><head><meta charset="UTF-8"><title>preview — bootstrap</title>
|
|
3
|
+
<style>body{font-family:monospace;background:#000;color:#33ff33;padding:2ch;margin:0}</style></head>
|
|
4
|
+
<body>
|
|
5
|
+
<p id="msg">bootstrapping preview...</p>
|
|
6
|
+
<script>
|
|
7
|
+
(async () => {
|
|
8
|
+
const msg = document.getElementById('msg');
|
|
9
|
+
if (navigator.serviceWorker.controller) {
|
|
10
|
+
msg.textContent = 'SW already controls — reloading to fetch content...';
|
|
11
|
+
location.reload();
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
msg.textContent = 'waiting for SW to activate...';
|
|
15
|
+
try {
|
|
16
|
+
await navigator.serviceWorker.ready;
|
|
17
|
+
let tries = 0;
|
|
18
|
+
while (!navigator.serviceWorker.controller && tries++ < 20) {
|
|
19
|
+
await new Promise(r => setTimeout(r, 100));
|
|
20
|
+
}
|
|
21
|
+
if (navigator.serviceWorker.controller) {
|
|
22
|
+
msg.textContent = 'SW ready — fetching real content...';
|
|
23
|
+
location.reload();
|
|
24
|
+
} else {
|
|
25
|
+
msg.textContent = 'SW registered but not controlling. Ensure preview-sw.js is at /thebird/preview-sw.js.';
|
|
26
|
+
}
|
|
27
|
+
} catch (e) {
|
|
28
|
+
msg.textContent = 'SW error: ' + e.message;
|
|
29
|
+
}
|
|
30
|
+
})();
|
|
31
|
+
</script>
|
|
32
|
+
</body></html>
|
|
@@ -22,21 +22,52 @@ export async function registerPreviewSW() {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
navigator.serviceWorker?.addEventListener('message', e => {
|
|
25
|
+
if (e.data?.type === 'SW_STREAM_READ') {
|
|
26
|
+
const path = e.data.path;
|
|
27
|
+
const procsub = path.match(/^\/procsub\/(\d+)$/);
|
|
28
|
+
if (procsub && window.__debug?.shell?.procsubRead) {
|
|
29
|
+
const data = window.__debug.shell.procsubRead(procsub[1]);
|
|
30
|
+
e.ports[0]?.postMessage({ data: data || '', found: data != null });
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const fdM = path.match(/^\/dev\/fd\/(\d+)$/);
|
|
34
|
+
if (fdM && window.__debug?.shell?.fdRead) {
|
|
35
|
+
try { const data = window.__debug.shell.fdRead(fdM[1]); e.ports[0]?.postMessage({ data: data || '', found: true }); }
|
|
36
|
+
catch { e.ports[0]?.postMessage({ data: '', found: false }); }
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
e.ports[0]?.postMessage({ found: false });
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
25
42
|
if (e.data?.type !== 'EXPRESS_REQUEST') return;
|
|
26
|
-
const { path, method } = e.data;
|
|
43
|
+
const { path, method, body: reqBody, headers: reqHeaders } = e.data;
|
|
27
44
|
const replyPort = e.ports[0];
|
|
28
45
|
const handlers = window.__debug?.shell?.httpHandlers || {};
|
|
29
46
|
const app = Object.values(handlers)[0];
|
|
30
|
-
if (!app?.routes) { replyPort.postMessage({ status:
|
|
47
|
+
if (!app?.routes) { replyPort.postMessage({ status: 503, body: '<h1>503</h1><p>no server running — run <code>node server.js</code> in terminal</p>', contentType: 'text/html' }); return; }
|
|
31
48
|
const routes = app.routes[method] || [];
|
|
32
49
|
const match = routes.find(r => r.path === '*' || r.path === path || path.startsWith(r.path));
|
|
33
|
-
if (!match) { replyPort.postMessage({ status: 404, body: 'no route for ' + path }); return; }
|
|
50
|
+
if (!match) { replyPort.postMessage({ status: 404, body: 'no route for ' + method + ' ' + path, contentType: 'text/plain' }); return; }
|
|
51
|
+
let done = false;
|
|
52
|
+
const finish = (status, body, ct) => { if (done) return; done = true; replyPort.postMessage({ status, body, contentType: ct }); };
|
|
34
53
|
const res = {
|
|
35
54
|
_body: '', _status: 200, _ct: 'text/html',
|
|
36
|
-
|
|
55
|
+
writeHead(code, headers) { this._status = code; if (headers?.['Content-Type']) this._ct = headers['Content-Type']; return this; },
|
|
56
|
+
setHeader(k, v) { if (k.toLowerCase() === 'content-type') this._ct = v; return this; },
|
|
57
|
+
write(chunk) { this._body += String(chunk); return true; },
|
|
58
|
+
end(chunk) { if (chunk != null) this._body += String(chunk); finish(this._status, this._body, this._ct); },
|
|
59
|
+
send(b) { this._body = typeof b === 'string' ? b : JSON.stringify(b); finish(this._status, this._body, this._ct); },
|
|
37
60
|
json(o) { this._ct = 'application/json'; this.send(JSON.stringify(o)); },
|
|
38
61
|
status(n) { this._status = n; return this; },
|
|
39
62
|
};
|
|
40
|
-
|
|
41
|
-
|
|
63
|
+
let bodyConsumed = false;
|
|
64
|
+
const req = {
|
|
65
|
+
method, url: path, path, query: {}, headers: reqHeaders || {},
|
|
66
|
+
[Symbol.asyncIterator]: async function* () { if (!bodyConsumed && reqBody) { bodyConsumed = true; yield reqBody; } },
|
|
67
|
+
};
|
|
68
|
+
try {
|
|
69
|
+
const r = match.fn(req, res);
|
|
70
|
+
if (r && typeof r.then === 'function') r.catch(err => finish(500, '<h1>500</h1><pre>' + String(err.message).replace(/</g, '<') + '</pre>', 'text/html'));
|
|
71
|
+
} catch (err) { finish(500, '<h1>500</h1><pre>' + String(err.message).replace(/</g, '<') + '</pre>', 'text/html'); }
|
|
72
|
+
setTimeout(() => finish(res._status, res._body, res._ct), 10000);
|
|
42
73
|
});
|
package/docs/preview-sw.js
CHANGED
|
@@ -1,61 +1,65 @@
|
|
|
1
|
-
const
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
'.css': 'text/css',
|
|
5
|
-
'.json': 'application/json',
|
|
6
|
-
'.txt': 'text/plain',
|
|
7
|
-
};
|
|
1
|
+
const swJobs = new Map();
|
|
2
|
+
const swFds = new Map();
|
|
3
|
+
const swProcsubs = new Map();
|
|
8
4
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
return MIME[ext] || 'application/octet-stream';
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function openIDB() {
|
|
15
|
-
return new Promise((resolve, reject) => {
|
|
16
|
-
const req = indexedDB.open('thebird', 1);
|
|
17
|
-
req.onsuccess = () => resolve(req.result);
|
|
18
|
-
req.onerror = () => reject(req.error);
|
|
19
|
-
});
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function getFS(db) {
|
|
23
|
-
return new Promise((resolve, reject) => {
|
|
24
|
-
const tx = db.transaction('fs', 'readonly');
|
|
25
|
-
const store = tx.objectStore('fs');
|
|
26
|
-
const req = store.get('thebird_fs_v2');
|
|
27
|
-
req.onsuccess = () => resolve(req.result ? JSON.parse(req.result) : {});
|
|
28
|
-
req.onerror = () => reject(req.error);
|
|
29
|
-
});
|
|
30
|
-
}
|
|
5
|
+
self.addEventListener('install', e => self.skipWaiting());
|
|
6
|
+
self.addEventListener('activate', e => e.waitUntil(clients.claim()));
|
|
31
7
|
|
|
32
|
-
self.addEventListener('
|
|
33
|
-
|
|
8
|
+
self.addEventListener('message', e => {
|
|
9
|
+
const d = e.data;
|
|
10
|
+
if (!d) return;
|
|
11
|
+
if (d.type === 'JOB_REGISTER') swJobs.set(d.id + '@' + d.tabId, { id: d.id, cmd: d.cmd, tabId: d.tabId, startedAt: Date.now() });
|
|
12
|
+
else if (d.type === 'JOB_UNREGISTER') swJobs.delete(d.id + '@' + d.tabId);
|
|
13
|
+
else if (d.type === 'JOB_LIST') e.ports?.[0]?.postMessage({ jobs: [...swJobs.values()] });
|
|
14
|
+
else if (d.type === 'PROCSUB_PUT') swProcsubs.set(d.id, d.data);
|
|
15
|
+
else if (d.type === 'FD_PUT') swFds.set(d.id, d.data);
|
|
16
|
+
});
|
|
34
17
|
|
|
35
18
|
self.addEventListener('fetch', e => {
|
|
36
19
|
const url = new URL(e.request.url);
|
|
37
|
-
const
|
|
38
|
-
if (
|
|
39
|
-
const
|
|
40
|
-
|
|
20
|
+
const swScope = new URL(self.registration.scope);
|
|
21
|
+
if (!url.pathname.startsWith(swScope.pathname)) return;
|
|
22
|
+
const path = '/' + url.pathname.slice(swScope.pathname.length).replace(/^\//, '') || '/';
|
|
23
|
+
|
|
24
|
+
const procsubM = path.match(/^\/procsub\/(\d+)$/);
|
|
25
|
+
if (procsubM) { e.respondWith(serveFromClient(path, e.request)); return; }
|
|
26
|
+
if (path.startsWith('/dev/fd/')) { e.respondWith(serveFromClient(path, e.request)); return; }
|
|
27
|
+
const tcpM = path.match(/^\/dev\/tcp\/([^/]+)\/(\d+)(\/.*)?$/);
|
|
28
|
+
if (tcpM) { e.respondWith(fetch('http://' + tcpM[1] + ':' + tcpM[2] + (tcpM[3] || '/'), { method: e.request.method, body: e.request.method !== 'GET' ? e.request.body : undefined })); return; }
|
|
29
|
+
|
|
30
|
+
e.respondWith(forwardExpress(e.request, path));
|
|
41
31
|
});
|
|
42
32
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
33
|
+
async function serveFromClient(path, request) {
|
|
34
|
+
const clients_ = await clients.matchAll({ includeUncontrolled: true });
|
|
35
|
+
const target = clients_.find(c => c.frameType !== 'nested') || clients_[0];
|
|
36
|
+
if (!target) return new Response('no client', { status: 503 });
|
|
37
|
+
const chan = new MessageChannel();
|
|
38
|
+
target.postMessage({ type: 'SW_STREAM_READ', path }, [chan.port2]);
|
|
39
|
+
return new Promise(res => {
|
|
40
|
+
const timeout = setTimeout(() => res(new Response('timeout', { status: 504 })), 5000);
|
|
41
|
+
chan.port1.onmessage = msg => {
|
|
42
|
+
clearTimeout(timeout);
|
|
43
|
+
if (!msg.data?.found) res(new Response('not found', { status: 404 }));
|
|
44
|
+
else res(new Response(msg.data.data, { status: 200, headers: { 'Content-Type': 'text/plain' } }));
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
}
|
|
46
48
|
|
|
47
|
-
async function
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
49
|
+
async function forwardExpress(request, path) {
|
|
50
|
+
const body = ['POST', 'PUT', 'PATCH'].includes(request.method) ? await request.text() : null;
|
|
51
|
+
const headers = {};
|
|
52
|
+
request.headers.forEach((v, k) => { headers[k] = v; });
|
|
53
|
+
const chan = new MessageChannel();
|
|
54
|
+
const clients_ = await clients.matchAll({ includeUncontrolled: true });
|
|
55
|
+
const target = clients_.find(c => c.frameType !== 'nested') || clients_[0];
|
|
56
|
+
if (!target) return new Response('no client', { status: 503 });
|
|
57
|
+
target.postMessage({ type: 'EXPRESS_REQUEST', path, method: request.method, body, headers }, [chan.port2]);
|
|
58
|
+
return new Promise(res => {
|
|
59
|
+
const timeout = setTimeout(() => res(new Response('timeout', { status: 504 })), 10000);
|
|
60
|
+
chan.port1.onmessage = msg => {
|
|
61
|
+
clearTimeout(timeout);
|
|
62
|
+
res(new Response(msg.data.body, { status: msg.data.status || 200, headers: { 'Content-Type': msg.data.contentType || 'text/html' } }));
|
|
63
|
+
};
|
|
58
64
|
});
|
|
59
|
-
if (!result || result.status === 404) return new Response('not found: ' + key, { status: 404, headers: CORS_HEADERS });
|
|
60
|
-
return new Response(result.body, { status: result.status || 200, headers: { ...CORS_HEADERS, 'Content-Type': result.contentType || 'text/html' } });
|
|
61
65
|
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
export function runAwk(program, stdin, fs_sep) {
|
|
2
|
+
const fs = fs_sep || /\s+/;
|
|
3
|
+
const blocks = parseAwk(program);
|
|
4
|
+
const out = [];
|
|
5
|
+
const ctx = { NR: 0, vars: {} };
|
|
6
|
+
const emit = s => out.push(s);
|
|
7
|
+
for (const b of blocks.filter(b => b.when === 'BEGIN')) execAction(b.action, { $: [], NR: 0, NF: 0 }, emit, ctx);
|
|
8
|
+
const lines = (stdin || '').split('\n');
|
|
9
|
+
const effective = lines.length > 0 && lines[lines.length - 1] === '' ? lines.slice(0, -1) : lines;
|
|
10
|
+
for (const line of effective) {
|
|
11
|
+
ctx.NR++;
|
|
12
|
+
const fields = line.split(fs).filter((_, i, a) => i > 0 || a.length === 1 || _ !== '');
|
|
13
|
+
const rec = { $: [line, ...fields], NR: ctx.NR, NF: fields.length };
|
|
14
|
+
for (const b of blocks.filter(b => b.when !== 'BEGIN' && b.when !== 'END')) {
|
|
15
|
+
if (!b.when || matchCond(b.when, rec, ctx)) execAction(b.action, rec, emit, ctx);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
for (const b of blocks.filter(b => b.when === 'END')) execAction(b.action, { $: [], NR: ctx.NR, NF: 0 }, emit, ctx);
|
|
19
|
+
return out.join('\n');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function parseAwk(prog) {
|
|
23
|
+
const blocks = [];
|
|
24
|
+
let i = 0;
|
|
25
|
+
while (i < prog.length) {
|
|
26
|
+
while (i < prog.length && /\s/.test(prog[i])) i++;
|
|
27
|
+
if (i >= prog.length) break;
|
|
28
|
+
let when = '';
|
|
29
|
+
while (i < prog.length && prog[i] !== '{') { when += prog[i++]; }
|
|
30
|
+
when = when.trim();
|
|
31
|
+
if (prog[i] !== '{') { if (when) blocks.push({ when, action: 'print' }); break; }
|
|
32
|
+
let depth = 1; i++;
|
|
33
|
+
let action = '';
|
|
34
|
+
while (i < prog.length && depth > 0) {
|
|
35
|
+
if (prog[i] === '{') depth++;
|
|
36
|
+
else if (prog[i] === '}') { depth--; if (!depth) break; }
|
|
37
|
+
action += prog[i++];
|
|
38
|
+
}
|
|
39
|
+
i++;
|
|
40
|
+
blocks.push({ when: when || null, action: action.trim() || 'print' });
|
|
41
|
+
}
|
|
42
|
+
return blocks;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function matchCond(cond, rec, ctx) {
|
|
46
|
+
const re = cond.match(/^\/(.+)\/$/);
|
|
47
|
+
if (re) return new RegExp(re[1]).test(rec.$[0]);
|
|
48
|
+
const cmp = cond.match(/^\$(\d+)\s*(==|!=|<|>|~)\s*"(.*)"$/);
|
|
49
|
+
if (cmp) { const v = rec.$[+cmp[1]] || ''; const OPS = { '==': v === cmp[3], '!=': v !== cmp[3], '<': v < cmp[3], '>': v > cmp[3], '~': new RegExp(cmp[3]).test(v) }; return OPS[cmp[2]]; }
|
|
50
|
+
if (cond === 'NR==1') return rec.NR === 1;
|
|
51
|
+
try { return !!Function('$', 'NR', 'NF', 'return (' + cond.replace(/\$(\d+)/g, (_, n) => '$[' + n + ']') + ')')(rec.$, rec.NR, rec.NF); } catch { return false; }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function execAction(action, rec, emit, ctx) {
|
|
55
|
+
for (const stmt of action.split(';').map(s => s.trim()).filter(Boolean)) {
|
|
56
|
+
const pr = stmt.match(/^print\s*(.*)$/);
|
|
57
|
+
if (pr) { emit(evalPrint(pr[1] || '$0', rec, ctx)); continue; }
|
|
58
|
+
const printf = stmt.match(/^printf\s+(.+)$/);
|
|
59
|
+
if (printf) { const parts = splitTop(printf[1], ','); const vals = parts.map(p => evalExpr(p.trim(), rec, ctx)); emit(AWK_FNS.sprintf(vals[0], ...vals.slice(1))); continue; }
|
|
60
|
+
const assign = stmt.match(/^([A-Za-z_]\w*)\s*=\s*(.+)$/);
|
|
61
|
+
if (assign) { ctx.vars[assign[1]] = evalExpr(assign[2], rec, ctx); continue; }
|
|
62
|
+
if (stmt === 'next') { ctx.skipRest = true; continue; }
|
|
63
|
+
try { evalExpr(stmt, rec, ctx); } catch {}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function evalPrint(args, rec, ctx) {
|
|
68
|
+
if (!args) return rec.$[0] || '';
|
|
69
|
+
const parts = splitTop(args, ',');
|
|
70
|
+
return parts.map(p => String(evalExpr(p.trim(), rec, ctx))).join(' ');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const AWK_FNS = {
|
|
74
|
+
length: s => String(s ?? '').length,
|
|
75
|
+
substr: (s, i, n) => String(s).substr((i|0) - 1, n == null ? undefined : n|0),
|
|
76
|
+
tolower: s => String(s).toLowerCase(),
|
|
77
|
+
toupper: s => String(s).toUpperCase(),
|
|
78
|
+
index: (s, t) => String(s).indexOf(String(t)) + 1,
|
|
79
|
+
match: (s, re) => { const m = String(s).match(new RegExp(re)); return m ? m.index + 1 : 0; },
|
|
80
|
+
split: (s, arr, re) => { const parts = String(s).split(new RegExp(re || /\s+/)); for (let i = 0; i < parts.length; i++) arr[i + 1] = parts[i]; return parts.length; },
|
|
81
|
+
sub: (re, rep, s) => { const r = new RegExp(re); const v = String(s ?? ''); return v.replace(r, rep); },
|
|
82
|
+
gsub: (re, rep, s) => { const r = new RegExp(re, 'g'); const v = String(s ?? ''); return v.replace(r, rep); },
|
|
83
|
+
sprintf: (fmt, ...a) => { let i = 0; return String(fmt).replace(/%([sdfox])/g, (_, k) => { const v = a[i++]; if (k === 'd') return String(parseInt(v, 10) || 0); if (k === 's') return String(v ?? ''); if (k === 'f') return String(parseFloat(v) || 0); if (k === 'x') return (parseInt(v, 10) || 0).toString(16); if (k === 'o') return (parseInt(v, 10) || 0).toString(8); return ''; }); },
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
function evalExpr(expr, rec, ctx) {
|
|
87
|
+
const s = expr.trim();
|
|
88
|
+
const strM = s.match(/^"(.*)"$/);
|
|
89
|
+
if (strM) return strM[1];
|
|
90
|
+
const fldM = s.match(/^\$(\d+|NF)$/);
|
|
91
|
+
if (fldM) { const n = fldM[1] === 'NF' ? rec.NF : +fldM[1]; return rec.$[n] || ''; }
|
|
92
|
+
if (s === 'NR') return rec.NR;
|
|
93
|
+
if (s === 'NF') return rec.NF;
|
|
94
|
+
if (ctx.vars[s] !== undefined) return ctx.vars[s];
|
|
95
|
+
const num = +s;
|
|
96
|
+
if (!isNaN(num)) return num;
|
|
97
|
+
try {
|
|
98
|
+
const body = 'return (' + s.replace(/\$(\d+|NF)/g, (_, n) => n === 'NF' ? 'NF' : '$[' + n + ']').replace(/\b([A-Za-z_]\w*)\b/g, (_, n) => AWK_FNS[n] ? 'F.' + n : 'v.' + n) + ')';
|
|
99
|
+
return Function('$', 'NR', 'NF', 'v', 'F', body)(rec.$, rec.NR, rec.NF, ctx.vars, AWK_FNS) ?? '';
|
|
100
|
+
} catch { return s; }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function splitTop(s, sep) {
|
|
104
|
+
const out = []; let cur = ''; let depth = 0; let inStr = false;
|
|
105
|
+
for (const c of s) {
|
|
106
|
+
if (c === '"') inStr = !inStr;
|
|
107
|
+
else if (!inStr) { if (c === '(' || c === '[') depth++; else if (c === ')' || c === ']') depth--; }
|
|
108
|
+
if (c === sep && !inStr && !depth) { out.push(cur); cur = ''; continue; }
|
|
109
|
+
cur += c;
|
|
110
|
+
}
|
|
111
|
+
if (cur) out.push(cur);
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { resolvePath } from './shell-builtins.js';
|
|
2
|
+
|
|
3
|
+
const toKey = p => p.replace(/^\//, '');
|
|
4
|
+
const snap = () => window.__debug.idbSnapshot || {};
|
|
5
|
+
|
|
6
|
+
export function makeExtraBuiltins(ctx, readFile, writeFile) {
|
|
7
|
+
const w = s => ctx.term.write(s);
|
|
8
|
+
const wl = s => w(s + '\r\n');
|
|
9
|
+
return {
|
|
10
|
+
test: args => { ctx.lastExitCode = evalTest(args) ? 0 : 1; },
|
|
11
|
+
'[': args => { const inner = args[args.length - 1] === ']' ? args.slice(0, -1) : args; ctx.lastExitCode = evalTest(inner) ? 0 : 1; },
|
|
12
|
+
tee: (args, _a, stdin) => {
|
|
13
|
+
const files = args.filter(a => !a.startsWith('-'));
|
|
14
|
+
const append = args.some(a => a === '-a');
|
|
15
|
+
const buf = stdin || '';
|
|
16
|
+
for (const f of files) writeFile(f, append ? (snap()[toKey(resolvePath(ctx.cwd, f))] || '') + buf : buf);
|
|
17
|
+
w(buf.replace(/\n/g, '\r\n'));
|
|
18
|
+
},
|
|
19
|
+
xargs: async (args, _a, stdin, invokeBuiltin) => {
|
|
20
|
+
const parts = (stdin || '').trim().split(/\s+/).filter(Boolean);
|
|
21
|
+
if (!args.length || !parts.length) return;
|
|
22
|
+
await invokeBuiltin(args[0], [...args.slice(1), ...parts], false);
|
|
23
|
+
},
|
|
24
|
+
read: (args, _a, stdin) => {
|
|
25
|
+
const flags = args.filter(a => a.startsWith('-')).join('');
|
|
26
|
+
const names = args.filter(a => !a.startsWith('-'));
|
|
27
|
+
if (!names.length) names.push('REPLY');
|
|
28
|
+
let line = (stdin || '').split('\n')[0];
|
|
29
|
+
if (!flags.includes('r')) line = line.replace(/\\(.)/g, '$1');
|
|
30
|
+
line = line.replace(/\r$/, '');
|
|
31
|
+
const nIdx = flags.indexOf('n');
|
|
32
|
+
if (nIdx >= 0) { const n = parseInt(flags.slice(nIdx + 1), 10); if (!isNaN(n)) line = line.slice(0, n); }
|
|
33
|
+
const parts = line.split(/\s+/);
|
|
34
|
+
for (let i = 0; i < names.length; i++) ctx.env[names[i]] = i === names.length - 1 ? parts.slice(i).join(' ') : parts[i] || '';
|
|
35
|
+
},
|
|
36
|
+
printf: args => {
|
|
37
|
+
if (!args.length) return;
|
|
38
|
+
let dest = null;
|
|
39
|
+
if (args[0] === '-v') { dest = args[1]; args = args.slice(2); }
|
|
40
|
+
if (!args.length) return;
|
|
41
|
+
const fmt = args[0].replace(/\\n/g, '\n').replace(/\\t/g, '\t').replace(/\\r/g, '\r');
|
|
42
|
+
let idx = 1;
|
|
43
|
+
const out = fmt.replace(/%([sdxof])/g, (_, spec) => {
|
|
44
|
+
const v = args[idx++] ?? '';
|
|
45
|
+
if (spec === 'd') return String(parseInt(v, 10) || 0);
|
|
46
|
+
if (spec === 'x') return (parseInt(v, 10) || 0).toString(16);
|
|
47
|
+
if (spec === 'o') return (parseInt(v, 10) || 0).toString(8);
|
|
48
|
+
if (spec === 'f') return String(parseFloat(v) || 0);
|
|
49
|
+
return String(v);
|
|
50
|
+
});
|
|
51
|
+
if (dest) ctx.env[dest] = out; else w(out.replace(/\n/g, '\r\n'));
|
|
52
|
+
},
|
|
53
|
+
declare: args => {
|
|
54
|
+
const assoc = args.includes('-A');
|
|
55
|
+
const arr = args.includes('-a');
|
|
56
|
+
const names = args.filter(a => !a.startsWith('-'));
|
|
57
|
+
for (const n of names) { const eq = n.indexOf('='); const k = eq >= 0 ? n.slice(0, eq) : n; if (assoc) { ctx.arrays = ctx.arrays || {}; ctx.arrays[k] = {}; } else if (arr) { ctx.arrays = ctx.arrays || {}; ctx.arrays[k] = []; } else if (eq >= 0) ctx.env[k] = n.slice(eq + 1); }
|
|
58
|
+
},
|
|
59
|
+
shift: args => {
|
|
60
|
+
const n = parseInt(args[0], 10) || 1;
|
|
61
|
+
ctx.argv = (ctx.argv || []).slice(n);
|
|
62
|
+
},
|
|
63
|
+
local: args => {
|
|
64
|
+
for (const kv of args) {
|
|
65
|
+
const eq = kv.indexOf('=');
|
|
66
|
+
const k = eq >= 0 ? kv.slice(0, eq) : kv;
|
|
67
|
+
const v = eq >= 0 ? kv.slice(eq + 1) : '';
|
|
68
|
+
(ctx.localStack && ctx.localStack[ctx.localStack.length - 1] || {})[k] = ctx.env[k];
|
|
69
|
+
ctx.env[k] = v;
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
set: args => {
|
|
73
|
+
for (const a of args) {
|
|
74
|
+
if (a === '-e') ctx.opts = { ...ctx.opts, errexit: true };
|
|
75
|
+
else if (a === '+e') ctx.opts = { ...ctx.opts, errexit: false };
|
|
76
|
+
else if (a === '-x') ctx.opts = { ...ctx.opts, xtrace: true };
|
|
77
|
+
else if (a === '+x') ctx.opts = { ...ctx.opts, xtrace: false };
|
|
78
|
+
else if (a === '-u') ctx.opts = { ...ctx.opts, nounset: true };
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
break: args => { ctx.loopFlag = 'break'; ctx.loopDepth = parseInt(args[0], 10) || 1; },
|
|
82
|
+
continue: args => { ctx.loopFlag = 'continue'; ctx.loopDepth = parseInt(args[0], 10) || 1; },
|
|
83
|
+
source: async (args, _a, _s, invokeBuiltin, runLine) => {
|
|
84
|
+
if (!args[0]) throw new Error('source: missing file');
|
|
85
|
+
const content = snap()[toKey(resolvePath(ctx.cwd, args[0]))];
|
|
86
|
+
if (content == null) throw new Error('source: ' + args[0] + ': No such file');
|
|
87
|
+
const savedArgv = ctx.argv;
|
|
88
|
+
ctx.argv = [args[0], ...args.slice(1)];
|
|
89
|
+
try { if (ctx.runScript) await ctx.runScript(content); else for (const ln of content.split('\n')) if (ln.trim()) await runLine(ln); }
|
|
90
|
+
finally { ctx.argv = savedArgv; }
|
|
91
|
+
},
|
|
92
|
+
'.': async (args, actor, stdin, invokeBuiltin, runLine) => {
|
|
93
|
+
const src = (ctx.builtinsRef || {}).source;
|
|
94
|
+
if (src) await src(args, actor, stdin, invokeBuiltin, runLine);
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function evalTest(args) {
|
|
100
|
+
if (args.length === 1) return !!args[0];
|
|
101
|
+
if (args.length === 2) {
|
|
102
|
+
const [flag, val] = args;
|
|
103
|
+
const s = () => window.__debug.idbSnapshot || {};
|
|
104
|
+
const OPS = {
|
|
105
|
+
'-z': v => v === '', '-n': v => v !== '',
|
|
106
|
+
'-f': v => v in s(),
|
|
107
|
+
'-d': v => Object.keys(s()).some(k => k.startsWith(v + '/')),
|
|
108
|
+
'-e': v => v in s() || Object.keys(s()).some(k => k.startsWith(v + '/')),
|
|
109
|
+
'!': v => !v,
|
|
110
|
+
};
|
|
111
|
+
return OPS[flag]?.(val) ?? false;
|
|
112
|
+
}
|
|
113
|
+
if (args.length === 3) {
|
|
114
|
+
const [a, op, b] = args;
|
|
115
|
+
const CMP = { '=':(x,y)=>x===y,'==':(x,y)=>x===y,'!=':(x,y)=>x!==y,
|
|
116
|
+
'-eq':(x,y)=>+x===+y,'-ne':(x,y)=>+x!==+y,
|
|
117
|
+
'-lt':(x,y)=>+x<+y,'-gt':(x,y)=>+x>+y,'-le':(x,y)=>+x<=+y,'-ge':(x,y)=>+x>=+y };
|
|
118
|
+
return CMP[op]?.(a, b) ?? false;
|
|
119
|
+
}
|
|
120
|
+
return false;
|
|
121
|
+
}
|