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.
Files changed (79) hide show
  1. package/.github/workflows/publish.yml +9 -1
  2. package/CHANGELOG.md +217 -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-fs-mirror.js +15 -0
  9. package/docs/kilo-http-stream.js +47 -0
  10. package/docs/node-builtins.js +24 -0
  11. package/docs/preview/index.html +32 -0
  12. package/docs/preview-sw-client.js +37 -6
  13. package/docs/preview-sw.js +55 -51
  14. package/docs/shell-awk.js +113 -0
  15. package/docs/shell-builtins-extra.js +121 -0
  16. package/docs/shell-builtins-text.js +109 -0
  17. package/docs/shell-builtins-util.js +112 -0
  18. package/docs/shell-builtins.js +183 -0
  19. package/docs/shell-bun.js +45 -0
  20. package/docs/shell-control.js +132 -0
  21. package/docs/shell-deno.js +54 -0
  22. package/docs/shell-exec.js +85 -0
  23. package/docs/shell-expand.js +164 -0
  24. package/docs/shell-fd.js +86 -0
  25. package/docs/shell-jobs.js +86 -0
  26. package/docs/shell-node-advanced.js +86 -0
  27. package/docs/shell-node-brotli.js +22 -0
  28. package/docs/shell-node-busnet.js +90 -0
  29. package/docs/shell-node-cipher.js +61 -0
  30. package/docs/shell-node-cluster.js +33 -0
  31. package/docs/shell-node-coreutils.js +36 -0
  32. package/docs/shell-node-crypto.js +137 -0
  33. package/docs/shell-node-dns.js +41 -0
  34. package/docs/shell-node-extras.js +148 -0
  35. package/docs/shell-node-firefox.js +95 -0
  36. package/docs/shell-node-git.js +60 -0
  37. package/docs/shell-node-inspector.js +39 -0
  38. package/docs/shell-node-io.js +131 -0
  39. package/docs/shell-node-ipc.js +15 -0
  40. package/docs/shell-node-keyobject.js +60 -0
  41. package/docs/shell-node-modules.js +157 -0
  42. package/docs/shell-node-native.js +31 -0
  43. package/docs/shell-node-net.js +71 -0
  44. package/docs/shell-node-observe.js +80 -0
  45. package/docs/shell-node-opfs.js +54 -0
  46. package/docs/shell-node-procfs.js +42 -0
  47. package/docs/shell-node-profiler.js +50 -0
  48. package/docs/shell-node-registry.js +24 -0
  49. package/docs/shell-node-resolve.js +147 -0
  50. package/docs/shell-node-runtime.js +83 -0
  51. package/docs/shell-node-srcmap.js +52 -0
  52. package/docs/shell-node-stdlib.js +103 -0
  53. package/docs/shell-node-streams.js +66 -0
  54. package/docs/shell-node-tar.js +47 -0
  55. package/docs/shell-node-testrunner.js +35 -0
  56. package/docs/shell-node-util-extras.js +66 -0
  57. package/docs/shell-node.js +175 -169
  58. package/docs/shell-npm.js +173 -0
  59. package/docs/shell-parser.js +122 -0
  60. package/docs/shell-pm-layout.js +62 -0
  61. package/docs/shell-pm.js +39 -0
  62. package/docs/shell-posix.js +70 -0
  63. package/docs/shell-procsub.js +65 -0
  64. package/docs/shell-readline.js +59 -4
  65. package/docs/shell-runtime.js +37 -0
  66. package/docs/shell-sed.js +83 -0
  67. package/docs/shell-signals.js +54 -0
  68. package/docs/shell-sw-jobs.js +76 -0
  69. package/docs/shell-ts.js +30 -0
  70. package/docs/shell.js +161 -152
  71. package/docs/terminal.js +9 -11
  72. package/docs/todo.html +211 -0
  73. package/package.json +1 -1
  74. package/server.js +43 -4
  75. package/start-kilo.js +45 -0
  76. package/test.js +199 -0
  77. package/.codeinsight +0 -73
  78. package/docs/acp-stream.js +0 -102
  79. 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,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
+ }
@@ -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: 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
+ }
@@ -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
+ }