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
@@ -0,0 +1,76 @@
1
+ export function createSwJobs() {
2
+ const registry = new Map();
3
+
4
+ async function postSw(msg) {
5
+ if (!navigator.serviceWorker?.controller) return null;
6
+ const chan = new MessageChannel();
7
+ const p = new Promise(res => { chan.port1.onmessage = e => res(e.data); setTimeout(() => res(null), 2000); });
8
+ navigator.serviceWorker.controller.postMessage(msg, [chan.port2]);
9
+ return p;
10
+ }
11
+
12
+ return {
13
+ async register(id, cmd) {
14
+ registry.set(id, { cmd, startedAt: Date.now() });
15
+ await postSw({ type: 'JOB_REGISTER', id, cmd, tabId: getTabId() });
16
+ },
17
+ async unregister(id) {
18
+ registry.delete(id);
19
+ await postSw({ type: 'JOB_UNREGISTER', id, tabId: getTabId() });
20
+ },
21
+ async list() {
22
+ const r = await postSw({ type: 'JOB_LIST' });
23
+ return r?.jobs || [...registry.entries()].map(([id, j]) => ({ id, ...j, tabId: getTabId() }));
24
+ },
25
+ local: () => [...registry.entries()].map(([id, j]) => ({ id, ...j })),
26
+ };
27
+ }
28
+
29
+ let _tabId = null;
30
+ function getTabId() {
31
+ if (_tabId) return _tabId;
32
+ try { _tabId = sessionStorage.getItem('thebird_tab') || String(Date.now()) + Math.random().toString(36).slice(2, 6); sessionStorage.setItem('thebird_tab', _tabId); } catch { _tabId = 'main'; }
33
+ return _tabId;
34
+ }
35
+
36
+ export function makeNohupBuiltin(ctx) {
37
+ return async args => {
38
+ if (!args.length) return;
39
+ ctx.term.write('nohup: ignoring HUP\r\n');
40
+ const cmd = args.join(' ');
41
+ if (ctx.jobRegistry) ctx.jobRegistry.spawnJob(cmd, ctx.runPipeline);
42
+ };
43
+ }
44
+
45
+ export function makeNetcatStub(ctx) {
46
+ return async (args, _a, stdin) => {
47
+ const host = args.find(a => !a.startsWith('-'));
48
+ const portArg = args[args.indexOf(host) + 1];
49
+ if (!host || !portArg) throw new Error('nc: usage: nc HOST PORT');
50
+ const url = 'http://' + host + ':' + portArg;
51
+ try {
52
+ const r = await fetch(url, { method: stdin ? 'POST' : 'GET', body: stdin || undefined });
53
+ const text = await r.text();
54
+ ctx.term.write(text.replace(/\n/g, '\r\n') + '\r\n');
55
+ } catch (e) {
56
+ ctx.term.write('\x1b[31mnc: ' + e.message + '\x1b[0m\r\n');
57
+ ctx.lastExitCode = 1;
58
+ }
59
+ };
60
+ }
61
+
62
+ export function makeCurlBuiltin(ctx) {
63
+ return async (args, _a, stdin) => {
64
+ const url = args.find(a => !a.startsWith('-') && (a.includes('://') || a.startsWith('/dev/tcp/')));
65
+ if (!url) throw new Error('curl: missing url');
66
+ let fetchUrl = url;
67
+ const tcpM = url.match(/^\/dev\/tcp\/([^/]+)\/(\d+)(\/.*)?$/);
68
+ if (tcpM) fetchUrl = 'http://' + tcpM[1] + ':' + tcpM[2] + (tcpM[3] || '/');
69
+ const method = args.includes('-X') ? args[args.indexOf('-X') + 1] : (args.includes('-d') || stdin ? 'POST' : 'GET');
70
+ const body = args.includes('-d') ? args[args.indexOf('-d') + 1] : stdin;
71
+ try {
72
+ const r = await fetch(fetchUrl, { method, body });
73
+ ctx.term.write((await r.text()).replace(/\n/g, '\r\n'));
74
+ } catch (e) { ctx.term.write('\x1b[31mcurl: ' + e.message + '\x1b[0m\r\n'); ctx.lastExitCode = 1; }
75
+ };
76
+ }
@@ -0,0 +1,30 @@
1
+ let stripPromise=null;
2
+ async function getStripper(){
3
+ if(!stripPromise)stripPromise=import('https://esm.sh/sucrase@3.35.0/es2022/sucrase.mjs').then(m=>m.default||m).catch(()=>null);
4
+ return stripPromise;
5
+ }
6
+
7
+ const typeDeclRE=/(^|\n)\s*(type|interface)\s+\w+[^\n]*(?:\n\s+[^\n]+)*\n?/g;
8
+ const asAnyRE=/\s+as\s+[\w.<>|&[\]]+/g;
9
+ const genericRE=/<[A-Z]\w*(?:\s*(?:extends|,)[^>]*)?>/g;
10
+ const varAnnotRE=/(\b(?:const|let|var)\s+\w+)\s*:\s*[^=,;)\n]+/g;
11
+ const paramAnnotRE=/(\b\w+)\s*:\s*[\w.<>|&[\]{} ]+?(?=\s*[,)=])/g;
12
+ const retTypeRE=/\)\s*:\s*[\w.<>|&[\]{} ]+?(?=\s*[{=]|$)/gm;
13
+
14
+ export function stripTypesSync(src){
15
+ let out=src.replace(typeDeclRE,'\n').replace(asAnyRE,'').replace(genericRE,'');
16
+ out=out.replace(varAnnotRE,'$1').replace(retTypeRE,')');
17
+ return out;
18
+ }
19
+
20
+ export async function stripTypes(src,opts={}){
21
+ try{const s=await getStripper();if(s?.transform){const r=s.transform(src,{transforms:['typescript',...(opts.jsx?['jsx']:[])],jsxRuntime:'automatic'});return r.code;}}catch{}
22
+ return stripTypesSync(src);
23
+ }
24
+
25
+ export function isTsFile(filename){return/\.(ts|tsx|mts|cts)$/i.test(filename);}
26
+
27
+ export function preprocessSource(filename,src){
28
+ if(!isTsFile(filename))return Promise.resolve(src);
29
+ return stripTypes(src,{jsx:/\.tsx$/.test(filename)});
30
+ }
package/docs/shell.js CHANGED
@@ -1,94 +1,19 @@
1
1
  import { createMachine, createActor } from './vendor/xstate.js';
2
2
  import { createNodeEnv } from './shell-node.js';
3
3
  import { createReadline } from './shell-readline.js';
4
-
5
- function resolvePath(cwd, p) {
6
- if (!p || p === '~') return '/';
7
- if (p.startsWith('~/')) p = '/' + p.slice(2);
8
- if (!p.startsWith('/')) p = cwd.replace(/\/$/, '') + '/' + 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 '/' + parts.join('/');
15
- }
16
-
17
- function makeBuiltins(ctx) {
18
- const snap = () => window.__debug.idbSnapshot || {};
19
- const toKey = p => p.replace(/^\//, '');
20
- const w = s => ctx.term.write(s);
21
- const wl = s => w(s + '\r\n');
22
- return {
23
- ls: ([p]) => {
24
- const prefix = toKey(resolvePath(ctx.cwd, p || ''));
25
- const pLen = prefix ? prefix.length + 1 : 0;
26
- const seen = new Set();
27
- for (const k of Object.keys(snap())) {
28
- if (prefix && !k.startsWith(prefix + '/') && k !== prefix) continue;
29
- if (!prefix && !k.includes('/')) { seen.add(k); continue; }
30
- const rest = k.slice(pLen);
31
- const first = rest.split('/')[0];
32
- if (first && first !== '.keep') seen.add(first);
33
- }
34
- wl([...seen].join('\r\n') || '(empty)');
35
- },
36
- cat: ([f]) => {
37
- const c = snap()[toKey(resolvePath(ctx.cwd, f))];
38
- if (c == null) throw new Error('no such file: ' + f);
39
- wl(c);
40
- },
41
- echo: args => wl(args.join(' ')),
42
- pwd: () => wl(ctx.cwd),
43
- cd: ([p]) => { ctx.cwd = resolvePath(ctx.cwd, p || '~'); },
44
- mkdir: ([p]) => { snap()[toKey(resolvePath(ctx.cwd, p)) + '/.keep'] = ''; window.__debug.idbPersist?.(); },
45
- rm: ([f]) => { delete snap()[toKey(resolvePath(ctx.cwd, f))]; window.__debug.idbPersist?.(); },
46
- cp: ([s, d]) => { snap()[toKey(resolvePath(ctx.cwd, d))] = snap()[toKey(resolvePath(ctx.cwd, s))]; window.__debug.idbPersist?.(); },
47
- mv: ([s, d]) => {
48
- const src = toKey(resolvePath(ctx.cwd, s)), dst = toKey(resolvePath(ctx.cwd, d));
49
- snap()[dst] = snap()[src]; delete snap()[src]; window.__debug.idbPersist?.();
50
- },
51
- touch: ([f]) => { const k = toKey(resolvePath(ctx.cwd, f)); if (!(k in snap())) { snap()[k] = ''; window.__debug.idbPersist?.(); } },
52
- head: ([f]) => { const c = snap()[toKey(resolvePath(ctx.cwd, f))]; if (!c) throw new Error('no such file: ' + f); wl(c.split('\n').slice(0, 10).join('\r\n')); },
53
- tail: ([f]) => { const c = snap()[toKey(resolvePath(ctx.cwd, f))]; if (!c) throw new Error('no such file: ' + f); wl(c.split('\n').slice(-10).join('\r\n')); },
54
- wc: ([f]) => { const c = snap()[toKey(resolvePath(ctx.cwd, f))]; if (!c) throw new Error('no such file: ' + f); const lines = c.split('\n').length; wl(lines + ' ' + c.length + ' ' + f); },
55
- grep: ([pat, f]) => {
56
- const c = snap()[toKey(resolvePath(ctx.cwd, f))];
57
- if (!c) throw new Error('no such file: ' + f);
58
- const re = new RegExp(pat, 'g');
59
- wl(c.split('\n').filter(l => re.test(l)).join('\r\n') || '(no matches)');
60
- },
61
- env: () => wl(Object.entries(ctx.env).map(([k, v]) => k + '=' + v).join('\r\n')),
62
- export: ([kv]) => { const [k, ...v] = (kv || '').split('='); ctx.env[k] = v.join('='); },
63
- clear: () => ctx.term.clear(),
64
- help: () => wl(Object.keys(makeBuiltins(ctx)).join(' ')),
65
- which: ([cmd]) => wl(makeBuiltins(ctx)[cmd] ? '(builtin) ' + cmd : 'not found: ' + cmd),
66
- exit: (_, actor) => {
67
- if (actor.getSnapshot().value === 'node-repl') { actor.send({ type: 'EXIT_REPL' }); wl('[shell]'); }
68
- },
69
- node: async (args, actor) => {
70
- if (!args.length) { actor.send({ type: 'ENTER_REPL' }); wl('[node repl — type exit to return]'); return; }
71
- if (args[0] === '-v' || args[0] === '--version') { wl('v20.0.0'); return; }
72
- if (args[0] === '-e' || args[0] === '--eval') { await ctx.nodeEval(args.slice(1).join(' ')); return; }
73
- const path = resolvePath(ctx.cwd, args[0]);
74
- const code = snap()[toKey(path)];
75
- if (code == null) throw new Error('no such file: ' + path);
76
- actor.send({ type: 'NODE_START' });
77
- await ctx.nodeEval(code, path, args.slice(1));
78
- },
79
- npm: async (args) => {
80
- if (args[0] !== 'install' && args[0] !== 'i') throw new Error('only npm install supported');
81
- const pkg = args[1];
82
- if (!pkg) throw new Error('npm install <pkg>');
83
- w('fetching ' + pkg + '...\r\n');
84
- const r = await fetch('https://esm.sh/' + pkg);
85
- if (!r.ok) throw new Error('fetch failed: ' + r.status);
86
- snap()['node_modules/' + pkg + '/index.js'] = await r.text();
87
- window.__debug.idbPersist?.();
88
- wl('installed ' + pkg);
89
- },
90
- };
91
- }
4
+ import { makeBuiltins, resolvePath } from './shell-builtins.js';
5
+ import { makeNpm, makeNpx } from './shell-npm.js';
6
+ import { makePmDispatcher, makeCorepackStub, detectPm } from './shell-pm.js';
7
+ import { makeDlx } from './shell-pm-layout.js';
8
+ import { tokenize, splitTopLevel, parsePipes } from './shell-parser.js';
9
+ import { fullExpand } from './shell-expand.js';
10
+ import { isControlStart, isBlockOpen, runControl, runScript } from './shell-control.js';
11
+ import { createSignals, makeKillBuiltin, makeTrapBuiltin } from './shell-signals.js';
12
+ import { createJobRegistry, makeJobsBuiltin, makeFgBuiltin, makeBgBuiltin, makeDisownBuiltin } from './shell-jobs.js';
13
+ import { createFdTable, makeExecBuiltin } from './shell-fd.js';
14
+ import { readStream } from './shell-procsub.js';
15
+ import { makeExpander, makeCaptureRun, makeNodeRunner, makeNpmResultRunner } from './shell-exec.js';
16
+ import { createSwJobs, makeNohupBuiltin, makeNetcatStub, makeCurlBuiltin } from './shell-sw-jobs.js';
92
17
 
93
18
  const machine = createMachine({ id: 'shell', initial: 'idle', states: {
94
19
  idle: { on: { RUN: 'executing', ENTER_REPL: 'node-repl', NODE_START: 'node-running' } },
@@ -98,91 +23,175 @@ const machine = createMachine({ id: 'shell', initial: 'idle', states: {
98
23
  }});
99
24
 
100
25
  export function createShell({ term, onPreviewWrite }) {
101
- const ctx = { term, cwd: '/', env: {}, history: [] };
102
- const BUILTINS = makeBuiltins(ctx);
103
- ctx.nodeEval = createNodeEnv({ ctx, term });
104
-
26
+ const ctx = { term, cwd: '/', prevCwd: '/', env: {}, history: [], lastExitCode: 0, argv: [], functions: {}, opts: {}, localStack: [], loopFlag: null, arrays: {}, bgJobs: {}, traps: {} };
105
27
  const actor = createActor(machine);
106
28
  actor.start();
29
+ const httpHandlers = {};
30
+ window.__debug = window.__debug || {};
107
31
 
108
32
  let inputQueue = [];
109
33
  function drainQueue(onData) { const items = inputQueue.slice(); inputQueue = []; for (const d of items) onData(d); }
110
34
 
111
- window.__debug = window.__debug || {};
112
- window.__debug.shell = {
113
- get state() { return actor.getSnapshot().value; },
114
- get cwd() { return ctx.cwd; },
115
- get env() { return ctx.env; },
116
- get history() { return ctx.history; },
117
- httpHandlers: {},
118
- get inputQueue() { return inputQueue.slice(); },
119
- };
35
+ const toKey = p => p.replace(/^\//, '');
36
+ const snap = () => window.__debug.idbSnapshot || {};
120
37
 
121
- async function runCmd(line, capture) {
122
- if (!line.trim()) return '';
123
- const [cmd, ...args] = line.trim().split(/\s+/);
124
- const fn = BUILTINS[cmd];
125
- if (!capture) {
126
- if (fn) await fn(args, actor); else term.write('command not found: ' + cmd + '\r\n');
127
- return '';
128
- }
129
- let out = '';
130
- const orig = term.write.bind(term);
131
- term.write = s => { out += s; };
132
- try { if (fn) await fn(args, actor); else out += 'command not found: ' + cmd; }
133
- finally { term.write = orig; }
38
+ const BUILTINS = makeBuiltins(ctx, actor, invokeBuiltin);
39
+ ctx.builtinsRef = BUILTINS;
40
+ const _exp = makeExpander(ctx, l => captureRun(l), t => parseRedirect(t));
41
+ expandTokens = _exp.expandTokens;
42
+ captureRun = makeCaptureRun(ctx, BUILTINS, actor, t => parseRedirect(t), t => expandTokens(t));
43
+ ctx.signals = createSignals(ctx);
44
+ ctx.fdTable = createFdTable(ctx);
45
+ ctx.swJobs = createSwJobs();
46
+ const jobRegistry = createJobRegistry(ctx);
47
+ ctx.jobRegistry = jobRegistry;
48
+ ctx.runPipeline = line => runPipeline(line);
49
+ Object.assign(BUILTINS, { kill: makeKillBuiltin(ctx), trap: makeTrapBuiltin(ctx), jobs: makeJobsBuiltin(ctx, jobRegistry), fg: makeFgBuiltin(ctx, jobRegistry), bg: makeBgBuiltin(ctx, jobRegistry), disown: makeDisownBuiltin(ctx), exec: makeExecBuiltin(ctx, ctx.fdTable), nohup: makeNohupBuiltin(ctx), nc: makeNetcatStub(ctx), curl: makeCurlBuiltin(ctx) });
50
+ ctx.runScript = text => runScript(text, run, ctx);
51
+ ctx.expand = token => fullExpand(token, ctx.env, ctx.lastExitCode, ctx.argv, captureRun, ctx.arrays);
52
+ const npmCmd = makeNpm(ctx); const npxCmd = makeNpx(npmCmd); ctx.exec = line => run(line);
53
+ const pmDispatch = makePmDispatcher(term, null, () => window.__debug.idbPersist?.(), ctx); const corepackCmd = makeCorepackStub(term); const dlxCmd = makeDlx(term, null, ctx, run);
54
+ ctx.nodeEval = createNodeEnv({ ctx, term });
55
+ const runNode = makeNodeRunner(ctx, actor);
56
+ const runNpmResult = makeNpmResultRunner(ctx, line => run(line));
57
+
58
+ let expandTokens, captureRun;
59
+
60
+ async function captureFn(fn) {
61
+ let out = ''; const orig = term.write.bind(term); term.write = s => { out += s; };
62
+ try { await fn(); } finally { term.write = orig; }
134
63
  return out;
135
64
  }
136
65
 
137
- async function run(line, onData) {
138
- if (!line.trim()) return;
139
- const st = actor.getSnapshot().value;
140
- if (st === 'node-repl' && line.trim() !== 'exit') { await ctx.nodeEval(line); return; }
141
- if (line.trim() === 'exit') { BUILTINS.exit([], actor); return; }
142
- const [cmd] = line.trim().split(/\s+/);
143
- if (cmd !== 'npm' && cmd !== 'node') actor.send({ type: 'RUN' });
66
+ async function runFunction(name, args) {
67
+ const savedArgv = ctx.argv; ctx.argv = [name, ...args]; ctx.localStack.push({});
68
+ try { await runScript(ctx.functions[name], run, ctx); }
69
+ finally {
70
+ const locals = ctx.localStack.pop();
71
+ for (const k of Object.keys(locals)) { if (locals[k] === undefined) delete ctx.env[k]; else ctx.env[k] = locals[k]; }
72
+ ctx.argv = savedArgv;
73
+ }
74
+ }
75
+
76
+ async function invokeBuiltin(name, args, withCaptureInto, stdinBuf) {
77
+ if (ctx.functions[name]) {
78
+ if (!withCaptureInto) { await runFunction(name, args); return ''; }
79
+ return captureFn(() => runFunction(name, args));
80
+ }
81
+ const fn = BUILTINS[name];
82
+ if (!fn) throw new Error('command not found: ' + name);
83
+ if (!withCaptureInto) { await fn(args, actor, stdinBuf, invokeBuiltin, run); return ''; }
84
+ return captureFn(() => fn(args, actor, stdinBuf, invokeBuiltin, run));
85
+ }
86
+
87
+ function evalKV(kv) { const eq = kv.indexOf('='); return [kv.slice(0, eq), fullExpand(kv.slice(eq + 1), ctx.env, ctx.lastExitCode, ctx.argv, captureRun)]; }
88
+
89
+ async function runSingleCommand(line) {
90
+ const arrM = line.trim().match(/^([A-Za-z_][A-Za-z0-9_]*)=\((.*)\)\s*$/);
91
+ if (arrM) { (ctx.arrays = ctx.arrays || {})[arrM[1]] = tokenize(arrM[2]).map(t => fullExpand(t, ctx.env, ctx.lastExitCode, ctx.argv, captureRun, ctx.arrays)); return; }
92
+ const idxM = line.trim().match(/^([A-Za-z_][A-Za-z0-9_]*)\[([^\]]+)\]=(.*)$/);
93
+ if (idxM) { ctx.arrays = ctx.arrays || {}; if (!ctx.arrays[idxM[1]]) ctx.arrays[idxM[1]] = {}; const a = ctx.arrays[idxM[1]], ex = t => fullExpand(t, ctx.env, ctx.lastExitCode, ctx.argv, captureRun, ctx.arrays), k = ex(idxM[2]), v = ex(idxM[3]); if (Array.isArray(a)) a[parseInt(k, 10)] = v; else a[k] = v; return; }
94
+ const raw = tokenize(line); if (!raw.length) return;
95
+ let i = 0; const varAssigns = [];
96
+ while (i < raw.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(raw[i])) varAssigns.push(raw[i++]);
97
+ const rest = raw.slice(i);
98
+ if (!rest.length) { for (const kv of varAssigns) { const [k, v] = evalKV(kv); ctx.env[k] = v; } return; }
99
+ const { args: [cmd, ...args], stdout: rout, append } = parseRedirect(expandTokens(rest));
100
+ const writeOut = rout ? buf => { const k = toKey(resolvePath(ctx.cwd, rout)); snap()[k] = append ? (snap()[k] || '') + buf : buf; window.__debug.idbPersist?.(); } : null;
101
+ const prevEnv = {}; for (const kv of varAssigns) { const [k, v] = evalKV(kv); prevEnv[k] = ctx.env[k]; ctx.env[k] = v; }
144
102
  try {
145
- const parts = line.split(' | ');
146
- if (parts.length > 1) {
147
- let buf = await runCmd(parts[0], true);
148
- for (const p of parts.slice(1)) {
149
- const [c, ...a] = p.trim().split(/\s+/);
150
- const fn = BUILTINS[c];
151
- if (fn) await fn([buf, ...a], actor); else term.write('command not found: ' + c + '\r\n');
152
- buf = '';
153
- }
154
- } else {
155
- await runCmd(line, false);
103
+ if (cmd === 'npm') { if (writeOut) { writeOut(await captureFn(async () => { await runNpmResult(await npmCmd(args)); })); return; } await runNpmResult(await npmCmd(args)); return; }
104
+ if (cmd === 'npx') { await runNpmResult(await npxCmd(args)); return; }
105
+ if (cmd === 'pnpm' || cmd === 'yarn' || cmd === 'bun') { ctx.lastExitCode = args[0] === 'dlx' || args[0] === 'x' ? await dlxCmd(args.slice(1)) : await pmDispatch(cmd, args[0] || 'install', args.slice(1)); return; }
106
+ if (cmd === 'deno') { if (args[0] === 'run') { await runNode(args.slice(1)); return; } ctx.lastExitCode = await pmDispatch('deno', args[0] || 'task', args.slice(1)); return; }
107
+ if (cmd === 'corepack') { ctx.lastExitCode = await corepackCmd(args); return; }
108
+ if (cmd === 'node') { await runNode(args); return; }
109
+ if (cmd === 'exit') { BUILTINS.exit([], actor); return; }
110
+ if (writeOut) { writeOut(await invokeBuiltin(cmd, args, true)); return; }
111
+ await invokeBuiltin(cmd, args, false);
112
+ } finally { for (const k of Object.keys(prevEnv)) { if (prevEnv[k] === undefined) delete ctx.env[k]; else ctx.env[k] = prevEnv[k]; } }
113
+ }
114
+
115
+ function parseRedirect(tokens) {
116
+ const out = { args: [], stdout: null, append: false };
117
+ for (let i = 0; i < tokens.length; i++) {
118
+ const t = tokens[i];
119
+ if (t === '>' || t === '>>') { out.stdout = tokens[++i]; out.append = t === '>>'; } else out.args.push(t);
120
+ }
121
+ return out;
122
+ }
123
+
124
+ async function runPipeline(line) {
125
+ const pipes = parsePipes(line);
126
+ if (pipes.length === 1) { await runSingleCommand(pipes[0]); return; }
127
+ let buf = '';
128
+ for (let i = 0; i < pipes.length; i++) {
129
+ const isLast = i === pipes.length - 1;
130
+ const { args: [cmd, ...args], stdout: rout, append } = parseRedirect(expandTokens(tokenize(pipes[i])));
131
+ const sArgs = i === 0 ? args : (buf && cmd !== 'node' ? [buf, ...args] : args);
132
+ const stdinForStage = cmd === 'node' ? buf : buf;
133
+ if (isLast && !rout) {
134
+ if (cmd === 'node') { await runNode(args, stdinForStage); buf = ''; continue; }
135
+ await invokeBuiltin(cmd, sArgs, false, stdinForStage); buf = ''; continue;
156
136
  }
157
- actor.send({ type: 'DONE' });
158
- drainQueue(onData);
159
- } catch (e) {
160
- term.write('\x1b[31m' + e.message + '\x1b[0m\r\n');
161
- actor.send({ type: 'ERROR' });
162
- drainQueue(onData);
137
+ const out = cmd === 'node' ? await captureFn(() => runNode(args, stdinForStage)) : await invokeBuiltin(cmd, sArgs, true, stdinForStage);
138
+ if (rout) { const k = toKey(resolvePath(ctx.cwd, rout)); snap()[k] = append ? (snap()[k] || '') + out : out; window.__debug.idbPersist?.(); buf = ''; } else { buf = out; }
163
139
  }
164
140
  }
165
141
 
166
- function getCompletions(line, word) {
167
- const files = Object.keys(window.__debug.idbSnapshot || {});
168
- const tokens = line.trim().split(/\s+/);
169
- if (tokens.length <= 1 && !line.includes(' ')) return Object.keys(BUILTINS).filter(c => c.startsWith(word));
170
- return files.filter(f => f.startsWith(word));
142
+ let blockLines = [];
143
+
144
+ async function run(line, onData) {
145
+ if (!line.trim()) return;
146
+ ctx.history.push(line);
147
+ const st = actor.getSnapshot().value;
148
+ if (st === 'node-repl') {
149
+ const t = line.trim();
150
+ if (t === 'exit' || t === '.exit' || t === '.quit') { actor.send({ type: 'EXIT_REPL' }); return; }
151
+ if (t === '.help') { term.write('.exit Exit the REPL\r\n.help Show this list\r\n.clear Break out of current expression\r\n'); return; }
152
+ if (t === '.clear') return;
153
+ const exprCode = 'try { const __r = (' + line + '); if (__r !== undefined) console.log(require("util").inspect(__r)); } catch (__e1) { try {\n' + line + '\n} catch (__e2) { console.error(__e2.message); } }';
154
+ await ctx.nodeEval(exprCode); return;
155
+ }
156
+ if (ctx.opts.xtrace) term.write('\x1b[90m+ ' + line + '\x1b[0m\r\n');
157
+ const chain = splitTopLevel(line, ['&&', '||', ';', '&']);
158
+ let lastOk = true;
159
+ for (const { cmd, sep } of chain) {
160
+ if (ctx.loopFlag) break;
161
+ if (sep === '&&' && !lastOk) continue;
162
+ if (sep === '||' && lastOk) { lastOk = true; continue; }
163
+ if (sep === '&') { const id = jobRegistry.spawnJob(cmd, runPipeline); ctx.env['!'] = id; term.write('[' + id + '] spawned\r\n'); continue; }
164
+ actor.send({ type: 'RUN' });
165
+ try { ctx.lastExitCode = 0; await runPipeline(cmd); lastOk = ctx.lastExitCode === 0; actor.send({ type: 'DONE' }); }
166
+ catch (e) { term.write('\x1b[31m' + e.message + '\x1b[0m\r\n'); ctx.lastExitCode = 1; lastOk = false; actor.send({ type: 'ERROR' }); }
167
+ if (ctx.opts.errexit && !lastOk) break;
168
+ if (ctx.signals) await ctx.signals.check(l => run(l));
169
+ }
170
+ if (onData) drainQueue(onData);
171
171
  }
172
172
 
173
- const rl = createReadline({ term, getCompletions, getPrompt: () => ctx.cwd, onLine: line => run(line, onData).then(() => rl.showPrompt()) });
173
+ const getCompletions = (line, word) => (line.trim().split(/\s+/).length <= 1 && !line.includes(' ')) ? Object.keys(BUILTINS).concat(['npm', 'node', 'pnpm', 'yarn', 'bun', 'deno', 'npx', 'corepack']).filter(c => c.startsWith(word)) : Object.keys(snap()).filter(f => f.startsWith(word));
174
174
 
175
+ const handleLine = line => {
176
+ if (blockLines.length > 0 || isControlStart(line)) {
177
+ blockLines.push(line); if (isBlockOpen(blockLines)) { rl.showContinuation(); return; }
178
+ const block = blockLines.slice(); blockLines = [];
179
+ runControl(block, run, ctx).then(() => rl.showPrompt()).catch(e => { term.write('\x1b[31m' + e.message + '\x1b[0m\r\n'); rl.showPrompt(); });
180
+ return;
181
+ }
182
+ run(line, onData).then(() => rl.showPrompt());
183
+ };
184
+ const rl = createReadline({ term, getCompletions, getPrompt: () => actor.getSnapshot().value === 'node-repl' ? '> ' : ctx.cwd, isBlockOpen: () => blockLines.length > 0, onLine: handleLine });
175
185
  function onData(data) {
176
- if (data === '\x03') { actor.send({ type: 'ERROR' }); inputQueue = []; term.write('^C'); rl.showPrompt(); return; }
186
+ if (data === '\x03') { actor.send({ type: 'ERROR' }); inputQueue = []; blockLines = []; term.write('^C'); rl.showPrompt(); return; }
177
187
  const st = actor.getSnapshot().value;
178
- if (st !== 'idle' && st !== 'node-repl') { inputQueue.push(data); return; }
179
- rl.onData(data);
188
+ if (st !== 'idle' && st !== 'node-repl') inputQueue.push(data); else rl.onData(data);
180
189
  }
181
-
182
190
  term.onData(onData);
183
- onPreviewWrite && (window.__debug.shell.onPreviewWrite = onPreviewWrite);
184
- const runPublic = line => run(line, onData);
185
- window.__debug.shell.run = runPublic;
186
191
  rl.showPrompt();
187
- return { run: runPublic };
192
+ return {
193
+ run: line => run(line, onData), onPreviewWrite, httpHandlers, procsubRead: id => readStream(id), fdRead: fd => ctx.fdTable.readFd(fd),
194
+ get state() { return actor.getSnapshot().value; }, get cwd() { return ctx.cwd; }, get env() { return ctx.env; }, get history() { return ctx.history; },
195
+ get lastExitCode() { return ctx.lastExitCode; }, get inputQueue() { return inputQueue.slice(); },
196
+ };
188
197
  }
package/docs/terminal.js CHANGED
@@ -3,7 +3,7 @@ import { createMachine, createActor } from './vendor/xstate.js';
3
3
  import { createShell } from './shell.js';
4
4
  import { registerPreviewSW } from './preview-sw-client.js';
5
5
 
6
- const IDB_KEY = 'thebird_fs_v2';
6
+ const IDB_KEY = 'thebird_fs_v4';
7
7
 
8
8
  async function idbLoad() {
9
9
  return new Promise((res, rej) => {
@@ -44,7 +44,7 @@ function scheduleReload() {
44
44
  clearTimeout(reloadTimer);
45
45
  reloadTimer = setTimeout(() => {
46
46
  if (typeof window.refreshPreview === 'function') window.refreshPreview();
47
- }, 5000);
47
+ }, 1000);
48
48
  }
49
49
 
50
50
  async function boot() {
@@ -57,7 +57,7 @@ async function boot() {
57
57
  window.__debug = window.__debug || {};
58
58
  window.__debug.terminal = { get state() { return bootActor.getSnapshot().value; } };
59
59
 
60
- const term = new Terminal({ theme: { background: '#000000' }, convertEol: true });
60
+ const term = new Terminal({ theme: { background: '#000000', foreground: '#33ff33' }, convertEol: true });
61
61
  const fit = new FitAddon();
62
62
  term.loadAddon(fit);
63
63
  term.open(el);
@@ -78,19 +78,17 @@ async function boot() {
78
78
  window.__debug.term = term;
79
79
  bootActor.send({ type: 'IDB_READY' });
80
80
 
81
- try {
82
- const swPromise = registerPreviewSW();
83
- const swTimeout = new Promise((_, rej) => setTimeout(() => rej(new Error('SW registration timeout')), 3000));
84
- await Promise.race([swPromise, swTimeout]);
81
+ const shell = createShell({ term, onPreviewWrite: scheduleReload });
82
+ window.__debug.shell = shell;
83
+
84
+ registerPreviewSW().then(() => {
85
85
  bootActor.send({ type: 'SW_READY' });
86
- } catch (e) {
86
+ }).catch(e => {
87
87
  console.log('[terminal] SW error:', e.message);
88
88
  window.__debug.sw = window.__debug.sw || {};
89
89
  window.__debug.sw.bootError = e.message;
90
90
  bootActor.send({ type: 'SW_ERROR' });
91
- }
92
-
93
- const shell = createShell({ term, onPreviewWrite: scheduleReload });
91
+ });
94
92
  window.__debug.shellWriter = { write: line => shell.run(line.replace(/\n$/, '')) };
95
93
  }
96
94