thebird 1.2.79 → 1.2.80
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/publish.yml +9 -1
- package/CHANGELOG.md +217 -0
- package/CLAUDE.md +16 -0
- package/docs/agent-chat.js +7 -4
- package/docs/app.js +14 -11
- package/docs/defaults.json +1 -1
- package/docs/index.html +23 -6
- package/docs/kilo-http-stream.js +24 -0
- package/docs/node-builtins.js +24 -0
- package/docs/preview/index.html +32 -0
- package/docs/preview-sw-client.js +37 -6
- package/docs/preview-sw.js +55 -51
- package/docs/shell-awk.js +113 -0
- package/docs/shell-builtins-extra.js +121 -0
- package/docs/shell-builtins-text.js +109 -0
- package/docs/shell-builtins-util.js +112 -0
- package/docs/shell-builtins.js +183 -0
- package/docs/shell-bun.js +45 -0
- package/docs/shell-control.js +132 -0
- package/docs/shell-deno.js +54 -0
- package/docs/shell-exec.js +85 -0
- package/docs/shell-expand.js +164 -0
- package/docs/shell-fd.js +86 -0
- package/docs/shell-jobs.js +86 -0
- package/docs/shell-node-advanced.js +86 -0
- package/docs/shell-node-brotli.js +22 -0
- package/docs/shell-node-busnet.js +90 -0
- package/docs/shell-node-cipher.js +61 -0
- package/docs/shell-node-cluster.js +33 -0
- package/docs/shell-node-coreutils.js +36 -0
- package/docs/shell-node-crypto.js +137 -0
- package/docs/shell-node-dns.js +41 -0
- package/docs/shell-node-extras.js +148 -0
- package/docs/shell-node-firefox.js +95 -0
- package/docs/shell-node-git.js +60 -0
- package/docs/shell-node-inspector.js +39 -0
- package/docs/shell-node-io.js +131 -0
- package/docs/shell-node-ipc.js +15 -0
- package/docs/shell-node-keyobject.js +60 -0
- package/docs/shell-node-modules.js +157 -0
- package/docs/shell-node-native.js +31 -0
- package/docs/shell-node-net.js +71 -0
- package/docs/shell-node-observe.js +80 -0
- package/docs/shell-node-opfs.js +54 -0
- package/docs/shell-node-procfs.js +42 -0
- package/docs/shell-node-profiler.js +50 -0
- package/docs/shell-node-registry.js +24 -0
- package/docs/shell-node-resolve.js +147 -0
- package/docs/shell-node-runtime.js +83 -0
- package/docs/shell-node-srcmap.js +52 -0
- package/docs/shell-node-stdlib.js +103 -0
- package/docs/shell-node-streams.js +66 -0
- package/docs/shell-node-tar.js +47 -0
- package/docs/shell-node-testrunner.js +35 -0
- package/docs/shell-node-util-extras.js +66 -0
- package/docs/shell-node.js +175 -169
- package/docs/shell-npm.js +173 -0
- package/docs/shell-parser.js +122 -0
- package/docs/shell-pm-layout.js +62 -0
- package/docs/shell-pm.js +39 -0
- package/docs/shell-posix.js +70 -0
- package/docs/shell-procsub.js +65 -0
- package/docs/shell-readline.js +59 -4
- package/docs/shell-runtime.js +37 -0
- package/docs/shell-sed.js +83 -0
- package/docs/shell-signals.js +54 -0
- package/docs/shell-sw-jobs.js +76 -0
- package/docs/shell-ts.js +30 -0
- package/docs/shell.js +161 -152
- package/docs/terminal.js +9 -11
- package/docs/todo.html +211 -0
- package/package.json +1 -1
- package/server.js +43 -4
- package/start-kilo.js +17 -0
- package/test.js +199 -0
- package/.codeinsight +0 -73
- package/docs/acp-stream.js +0 -102
- package/docs/coi-serviceworker.js +0 -2
|
@@ -0,0 +1,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
|
+
}
|
package/docs/shell-ts.js
ADDED
|
@@ -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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
112
|
-
window.__debug.
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
146
|
-
if (
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
158
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
if (
|
|
170
|
-
|
|
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
|
|
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')
|
|
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 {
|
|
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 = '
|
|
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
|
-
},
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
81
|
+
const shell = createShell({ term, onPreviewWrite: scheduleReload });
|
|
82
|
+
window.__debug.shell = shell;
|
|
83
|
+
|
|
84
|
+
registerPreviewSW().then(() => {
|
|
85
85
|
bootActor.send({ type: 'SW_READY' });
|
|
86
|
-
}
|
|
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
|
|