thebird 1.2.78 → 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.
Files changed (78) hide show
  1. package/.github/workflows/publish.yml +9 -1
  2. package/CHANGELOG.md +226 -0
  3. package/CLAUDE.md +16 -0
  4. package/docs/agent-chat.js +7 -4
  5. package/docs/app.js +14 -11
  6. package/docs/defaults.json +1 -1
  7. package/docs/index.html +23 -6
  8. package/docs/kilo-http-stream.js +24 -0
  9. package/docs/node-builtins.js +194 -0
  10. package/docs/preview/index.html +32 -0
  11. package/docs/preview-sw-client.js +37 -6
  12. package/docs/preview-sw.js +55 -51
  13. package/docs/shell-awk.js +113 -0
  14. package/docs/shell-builtins-extra.js +121 -0
  15. package/docs/shell-builtins-text.js +109 -0
  16. package/docs/shell-builtins-util.js +112 -0
  17. package/docs/shell-builtins.js +183 -0
  18. package/docs/shell-bun.js +45 -0
  19. package/docs/shell-control.js +132 -0
  20. package/docs/shell-deno.js +54 -0
  21. package/docs/shell-exec.js +85 -0
  22. package/docs/shell-expand.js +164 -0
  23. package/docs/shell-fd.js +86 -0
  24. package/docs/shell-jobs.js +86 -0
  25. package/docs/shell-node-advanced.js +86 -0
  26. package/docs/shell-node-brotli.js +22 -0
  27. package/docs/shell-node-busnet.js +90 -0
  28. package/docs/shell-node-cipher.js +61 -0
  29. package/docs/shell-node-cluster.js +33 -0
  30. package/docs/shell-node-coreutils.js +36 -0
  31. package/docs/shell-node-crypto.js +137 -0
  32. package/docs/shell-node-dns.js +41 -0
  33. package/docs/shell-node-extras.js +148 -0
  34. package/docs/shell-node-firefox.js +95 -0
  35. package/docs/shell-node-git.js +60 -0
  36. package/docs/shell-node-inspector.js +39 -0
  37. package/docs/shell-node-io.js +131 -0
  38. package/docs/shell-node-ipc.js +15 -0
  39. package/docs/shell-node-keyobject.js +60 -0
  40. package/docs/shell-node-modules.js +157 -0
  41. package/docs/shell-node-native.js +31 -0
  42. package/docs/shell-node-net.js +71 -0
  43. package/docs/shell-node-observe.js +80 -0
  44. package/docs/shell-node-opfs.js +54 -0
  45. package/docs/shell-node-procfs.js +42 -0
  46. package/docs/shell-node-profiler.js +50 -0
  47. package/docs/shell-node-registry.js +24 -0
  48. package/docs/shell-node-resolve.js +147 -0
  49. package/docs/shell-node-runtime.js +83 -0
  50. package/docs/shell-node-srcmap.js +52 -0
  51. package/docs/shell-node-stdlib.js +103 -0
  52. package/docs/shell-node-streams.js +66 -0
  53. package/docs/shell-node-tar.js +47 -0
  54. package/docs/shell-node-testrunner.js +35 -0
  55. package/docs/shell-node-util-extras.js +66 -0
  56. package/docs/shell-node.js +188 -97
  57. package/docs/shell-npm.js +173 -0
  58. package/docs/shell-parser.js +122 -0
  59. package/docs/shell-pm-layout.js +62 -0
  60. package/docs/shell-pm.js +39 -0
  61. package/docs/shell-posix.js +70 -0
  62. package/docs/shell-procsub.js +65 -0
  63. package/docs/shell-readline.js +59 -4
  64. package/docs/shell-runtime.js +37 -0
  65. package/docs/shell-sed.js +83 -0
  66. package/docs/shell-signals.js +54 -0
  67. package/docs/shell-sw-jobs.js +76 -0
  68. package/docs/shell-ts.js +30 -0
  69. package/docs/shell.js +159 -167
  70. package/docs/terminal.js +9 -11
  71. package/docs/todo.html +211 -0
  72. package/package.json +1 -1
  73. package/server.js +43 -4
  74. package/start-kilo.js +17 -0
  75. package/test.js +199 -0
  76. package/.codeinsight +0 -73
  77. package/docs/acp-stream.js +0 -102
  78. 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
- send(b) { this._body = b; resolve({ status: this._status, body: this._body, ct: this._ct }); },
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
- try { match.fn({ method: method || 'GET', path, query: {}, headers: {} }, res); }
52
- catch(e) { resolve({ status: 500, body: e.message, ct: 'text/plain' }); }
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 app.js in terminal.</pre>';
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,24 @@
1
+ export async function* streamKiloHTTP({ url, model, messages }) {
2
+ yield { type: 'start-step' };
3
+ const base = (url || 'http://localhost:4780').replace(/\/$/, '');
4
+ let sessRes;
5
+ try { sessRes = await fetch(base + '/session', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' }); }
6
+ catch (e) { throw new Error('kilo serve not reachable at ' + base + ' — start it with: kilo serve --port ' + (new URL(base).port || 4780) + ' --cors ' + location.origin); }
7
+ if (!sessRes.ok) throw new Error('kilo /session ' + sessRes.status + ': ' + await sessRes.text());
8
+ const { id: sessionId } = await sessRes.json();
9
+ Object.assign(window.__debug = window.__debug || {}, { kilo: { sessionId, url: base, lastStatus: null } });
10
+
11
+ const userText = messages.filter(m => m.role === 'user').map(m =>
12
+ typeof m.content === 'string' ? m.content : (m.content || []).filter(b => b.type === 'text').map(b => b.text).join('')
13
+ ).join('\n');
14
+
15
+ const body = { parts: [{ type: 'text', text: userText }], providerID: 'kilo', modelID: model || 'x-ai/grok-code-fast-1:optimized:free', agent: 'hermes-llm' };
16
+ const msgRes = await fetch(base + '/session/' + sessionId + '/message', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
17
+ window.__debug.kilo.lastStatus = msgRes.status;
18
+ if (!msgRes.ok) throw new Error('kilo message ' + msgRes.status + ': ' + await msgRes.text());
19
+ const result = await msgRes.json();
20
+ window.__debug.kilo.lastResult = result;
21
+ const textParts = (result.parts || []).filter(p => p.type === 'text');
22
+ for (const tp of textParts) yield { type: 'text-delta', textDelta: tp.text };
23
+ yield { type: 'finish-step', finishReason: result.info?.finish || 'stop' };
24
+ }
@@ -0,0 +1,194 @@
1
+ const snap = () => window.__debug?.idbSnapshot || {};
2
+ const toKey = p => p.replace(/^\//, '');
3
+ const persist = () => window.__debug?.idbPersist?.();
4
+ const previewWrite = () => window.__debug?.shell?.onPreviewWrite?.();
5
+
6
+ export function createPath() {
7
+ const sep = '/';
8
+ const normalize = p => {
9
+ const parts = [];
10
+ for (const s of p.split('/')) {
11
+ if (s === '..') parts.pop();
12
+ else if (s && s !== '.') parts.push(s);
13
+ }
14
+ return (p.startsWith('/') ? '/' : '') + parts.join('/');
15
+ };
16
+ return {
17
+ sep,
18
+ normalize,
19
+ join: (...a) => normalize(a.join('/')),
20
+ resolve: (...a) => {
21
+ let r = '';
22
+ for (const p of a) r = p.startsWith('/') ? p : r + '/' + p;
23
+ return normalize(r);
24
+ },
25
+ dirname: p => { const i = p.lastIndexOf('/'); return i <= 0 ? '/' : p.slice(0, i); },
26
+ basename: (p, ext) => { const b = p.split('/').pop() || ''; return ext && b.endsWith(ext) ? b.slice(0, -ext.length) : b; },
27
+ extname: p => { const b = p.split('/').pop() || ''; const i = b.lastIndexOf('.'); return i > 0 ? b.slice(i) : ''; },
28
+ isAbsolute: p => p.startsWith('/'),
29
+ relative: (from, to) => to.replace(from.replace(/\/$/, '') + '/', ''),
30
+ parse: p => {
31
+ const dir = p.slice(0, p.lastIndexOf('/')) || '/';
32
+ const base = p.split('/').pop() || '';
33
+ const ext = base.lastIndexOf('.') > 0 ? base.slice(base.lastIndexOf('.')) : '';
34
+ return { root: '/', dir, base, ext, name: ext ? base.slice(0, -ext.length) : base };
35
+ },
36
+ };
37
+ }
38
+
39
+ export function createFs() {
40
+ const resolveP = p => typeof p === 'string' ? p : p.toString();
41
+ return {
42
+ readFileSync: (p, enc) => {
43
+ const key = toKey(resolveP(p));
44
+ const data = snap()[key];
45
+ if (data == null) throw Object.assign(new Error('ENOENT: ' + p), { code: 'ENOENT' });
46
+ return enc ? data : data;
47
+ },
48
+ writeFileSync: (p, data) => {
49
+ const s = snap();
50
+ s[toKey(resolveP(p))] = typeof data === 'string' ? data : String(data);
51
+ persist();
52
+ previewWrite();
53
+ },
54
+ appendFileSync: (p, data) => {
55
+ const key = toKey(resolveP(p));
56
+ const s = snap();
57
+ s[key] = (s[key] || '') + (typeof data === 'string' ? data : String(data));
58
+ persist();
59
+ },
60
+ existsSync: p => toKey(resolveP(p)) in snap(),
61
+ unlinkSync: p => {
62
+ const key = toKey(resolveP(p));
63
+ if (!(key in snap())) throw Object.assign(new Error('ENOENT: ' + p), { code: 'ENOENT' });
64
+ delete snap()[key];
65
+ persist();
66
+ },
67
+ mkdirSync: (p, opts) => {
68
+ const key = toKey(resolveP(p));
69
+ if (!snap()[key + '/.keep']) { snap()[key + '/.keep'] = ''; persist(); }
70
+ },
71
+ readdirSync: p => {
72
+ const prefix = toKey(resolveP(p));
73
+ const pLen = prefix ? prefix.length + 1 : 0;
74
+ const seen = new Set();
75
+ for (const k of Object.keys(snap())) {
76
+ if (prefix && !k.startsWith(prefix + '/') && k !== prefix) continue;
77
+ if (!prefix && !k.includes('/')) { seen.add(k); continue; }
78
+ const rest = k.slice(pLen);
79
+ const first = rest.split('/')[0];
80
+ if (first && first !== '.keep') seen.add(first);
81
+ }
82
+ return [...seen];
83
+ },
84
+ statSync: p => {
85
+ const key = toKey(resolveP(p));
86
+ const s = snap();
87
+ const isFile = key in s;
88
+ const isDir = !isFile && Object.keys(s).some(k => k.startsWith(key + '/'));
89
+ if (!isFile && !isDir) throw Object.assign(new Error('ENOENT: ' + p), { code: 'ENOENT' });
90
+ return { isFile: () => isFile, isDirectory: () => isDir, size: isFile ? (s[key]?.length || 0) : 0 };
91
+ },
92
+ renameSync: (o, n) => {
93
+ const s = snap();
94
+ const ok = toKey(resolveP(o)), nk = toKey(resolveP(n));
95
+ if (!(ok in s)) throw Object.assign(new Error('ENOENT: ' + o), { code: 'ENOENT' });
96
+ s[nk] = s[ok];
97
+ delete s[ok];
98
+ persist();
99
+ },
100
+ copyFileSync: (s, d) => {
101
+ const src = snap()[toKey(resolveP(s))];
102
+ if (src == null) throw Object.assign(new Error('ENOENT: ' + s), { code: 'ENOENT' });
103
+ snap()[toKey(resolveP(d))] = src;
104
+ persist();
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,
116
+ };
117
+ }
118
+
119
+ export function createEvents() {
120
+ return class EventEmitter {
121
+ constructor() { this._e = {}; }
122
+ on(ev, fn) { (this._e[ev] = this._e[ev] || []).push(fn); return this; }
123
+ once(ev, fn) { const w = (...a) => { this.off(ev, w); fn(...a); }; return this.on(ev, w); }
124
+ off(ev, fn) { this._e[ev] = (this._e[ev] || []).filter(f => f !== fn); return this; }
125
+ removeListener(ev, fn) { return this.off(ev, fn); }
126
+ removeAllListeners(ev) { if (ev) delete this._e[ev]; else this._e = {}; return this; }
127
+ emit(ev, ...a) { for (const fn of (this._e[ev] || [])) fn(...a); return (this._e[ev] || []).length > 0; }
128
+ listeners(ev) { return (this._e[ev] || []).slice(); }
129
+ listenerCount(ev) { return (this._e[ev] || []).length; }
130
+ };
131
+ }
132
+
133
+ export function createUrl() {
134
+ return {
135
+ parse: s => {
136
+ const u = new URL(s);
137
+ return { protocol: u.protocol, host: u.host, hostname: u.hostname, port: u.port, pathname: u.pathname, search: u.search, query: u.search.slice(1), hash: u.hash, href: u.href };
138
+ },
139
+ format: o => {
140
+ const u = new URL('http://x');
141
+ for (const [k, v] of Object.entries(o)) { try { u[k] = v; } catch {} }
142
+ return u.href;
143
+ },
144
+ resolve: (from, to) => new URL(to, from).href,
145
+ };
146
+ }
147
+
148
+ export function createQuerystring() {
149
+ return {
150
+ parse: s => Object.fromEntries(new URLSearchParams(s)),
151
+ stringify: o => new URLSearchParams(o).toString(),
152
+ escape: s => encodeURIComponent(s),
153
+ unescape: s => decodeURIComponent(s),
154
+ };
155
+ }
156
+
157
+ export function createBuffer() {
158
+ class Buf extends Uint8Array {
159
+ toString(enc) {
160
+ if (enc === 'base64') return btoa(String.fromCharCode(...this));
161
+ if (enc === 'hex') return [...this].map(b => b.toString(16).padStart(2, '0')).join('');
162
+ return new TextDecoder().decode(this);
163
+ }
164
+ toJSON() { return { type: 'Buffer', data: [...this] }; }
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; }
177
+ }
178
+ Buf.from = (d, enc) => {
179
+ if (d instanceof Uint8Array) return new Buf(d);
180
+ if (Array.isArray(d)) return new Buf(d);
181
+ if (typeof d !== 'string') return new Buf(0);
182
+ if (enc === 'base64') return new Buf(Uint8Array.from(atob(d), c => c.charCodeAt(0)));
183
+ if (enc === 'hex') return new Buf(d.match(/.{2}/g).map(h => parseInt(h, 16)));
184
+ return new Buf(new TextEncoder().encode(d));
185
+ };
186
+ Buf.alloc = (n, fill) => { const b = new Buf(n); if (fill) b.fill(typeof fill === 'number' ? fill : fill.charCodeAt(0)); return b; };
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; };
188
+ Buf.isBuffer = o => o instanceof Buf;
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;
193
+ return Buf;
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: 404, body: 'no express app' }); return; }
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
- send(b) { this._body = b; replyPort.postMessage({ status: this._status, body: this._body, contentType: this._ct }); },
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
- const req = { method, path, query: {}, headers: {} };
41
- try { match.fn(req, res); } catch (err) { replyPort.postMessage({ status: 500, body: err.message }); }
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, '&lt;') + '</pre>', 'text/html'));
71
+ } catch (err) { finish(500, '<h1>500</h1><pre>' + String(err.message).replace(/</g, '&lt;') + '</pre>', 'text/html'); }
72
+ setTimeout(() => finish(res._status, res._body, res._ct), 10000);
42
73
  });
@@ -1,61 +1,65 @@
1
- const MIME = {
2
- '.html': 'text/html',
3
- '.js': 'application/javascript',
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
- function getMime(key) {
10
- const ext = key.slice(key.lastIndexOf('.'));
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('install', () => self.skipWaiting());
33
- self.addEventListener('activate', e => e.waitUntil(self.clients.claim()));
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 idx = url.pathname.indexOf('/preview/');
38
- if (idx === -1) return;
39
- const key = url.pathname.slice(idx + '/preview/'.length) || 'index.html';
40
- e.respondWith(handlePreview(key, e.request));
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
- const CORS_HEADERS = {
44
- 'Cross-Origin-Resource-Policy': 'cross-origin',
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 handlePreview(key, request) {
48
- const db = await openIDB();
49
- const fs = await getFS(db);
50
- if (key in fs) return new Response(fs[key], { status: 200, headers: { ...CORS_HEADERS, 'Content-Type': getMime(key) } });
51
- const clients = await self.clients.matchAll({ type: 'window', includeUncontrolled: true });
52
- if (!clients.length) return new Response('not found: ' + key, { status: 404, headers: CORS_HEADERS });
53
- const { port1, port2 } = new MessageChannel();
54
- const result = await new Promise((res, rej) => {
55
- const t = setTimeout(() => rej(new Error('express timeout')), 5000);
56
- port1.onmessage = e => { clearTimeout(t); res(e.data); };
57
- clients[0].postMessage({ type: 'EXPRESS_REQUEST', path: '/' + key, method: request.method }, [port2]);
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
+ }