thebird 1.2.78 → 1.2.80
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/publish.yml +9 -1
- package/CHANGELOG.md +226 -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 +194 -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 +188 -97
- 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 +159 -167
- 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
package/docs/shell.js
CHANGED
|
@@ -1,205 +1,197 @@
|
|
|
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 keys = prefix === '/' ? Object.keys(snap()) : Object.keys(snap()).filter(k => k.startsWith(prefix));
|
|
26
|
-
wl(keys.join('\r\n') || '(empty)');
|
|
27
|
-
},
|
|
28
|
-
cat: ([f]) => {
|
|
29
|
-
const c = snap()[toKey(resolvePath(ctx.cwd, f))];
|
|
30
|
-
if (c == null) throw new Error('no such file: ' + f);
|
|
31
|
-
wl(c);
|
|
32
|
-
},
|
|
33
|
-
echo: args => wl(args.join(' ')),
|
|
34
|
-
pwd: () => wl(ctx.cwd),
|
|
35
|
-
cd: ([p]) => { ctx.cwd = resolvePath(ctx.cwd, p || '~'); },
|
|
36
|
-
mkdir: ([p]) => {
|
|
37
|
-
window.__debug.idbSnapshot[toKey(resolvePath(ctx.cwd, p)) + '/.keep'] = '';
|
|
38
|
-
window.__debug.idbPersist?.();
|
|
39
|
-
},
|
|
40
|
-
rm: ([f]) => {
|
|
41
|
-
delete window.__debug.idbSnapshot[toKey(resolvePath(ctx.cwd, f))];
|
|
42
|
-
window.__debug.idbPersist?.();
|
|
43
|
-
},
|
|
44
|
-
cp: ([s, d]) => {
|
|
45
|
-
window.__debug.idbSnapshot[toKey(resolvePath(ctx.cwd, d))] = snap()[toKey(resolvePath(ctx.cwd, s))];
|
|
46
|
-
window.__debug.idbPersist?.();
|
|
47
|
-
},
|
|
48
|
-
mv: ([s, d]) => {
|
|
49
|
-
const src = toKey(resolvePath(ctx.cwd, s)), dst = toKey(resolvePath(ctx.cwd, d));
|
|
50
|
-
window.__debug.idbSnapshot[dst] = snap()[src];
|
|
51
|
-
delete window.__debug.idbSnapshot[src];
|
|
52
|
-
window.__debug.idbPersist?.();
|
|
53
|
-
},
|
|
54
|
-
env: () => wl(Object.entries(ctx.env).map(([k, v]) => k + '=' + v).join('\r\n')),
|
|
55
|
-
export: ([kv]) => { const [k, ...v] = (kv || '').split('='); ctx.env[k] = v.join('='); },
|
|
56
|
-
clear: () => ctx.term.clear(),
|
|
57
|
-
help: () => wl(Object.keys(makeBuiltins(ctx)).join(' ')),
|
|
58
|
-
exit: (actor) => {
|
|
59
|
-
const st = actor.getSnapshot().value;
|
|
60
|
-
if (st === 'node-repl') { actor.send({ type: 'EXIT_REPL' }); wl('[shell]'); }
|
|
61
|
-
},
|
|
62
|
-
node: async ([file], actor) => {
|
|
63
|
-
if (!file) { actor.send({ type: 'ENTER_REPL' }); wl('[node repl — type exit to return]'); return; }
|
|
64
|
-
const path = resolvePath(ctx.cwd, file);
|
|
65
|
-
const code = snap()[toKey(path)];
|
|
66
|
-
if (code == null) throw new Error('no such file: ' + path);
|
|
67
|
-
actor.send({ type: 'NODE_START' });
|
|
68
|
-
await ctx.nodeEval(code, path);
|
|
69
|
-
},
|
|
70
|
-
npm: async (args, actor) => {
|
|
71
|
-
if (args[0] !== 'install') throw new Error('only npm install supported');
|
|
72
|
-
const pkg = args[1];
|
|
73
|
-
if (!pkg) throw new Error('npm install <pkg>');
|
|
74
|
-
actor.send({ type: 'NPM_START' });
|
|
75
|
-
w('fetching ' + pkg + '...\r\n');
|
|
76
|
-
const r = await fetch('https://esm.sh/' + pkg);
|
|
77
|
-
if (!r.ok) throw new Error('fetch failed: ' + r.status);
|
|
78
|
-
const key = 'node_modules/' + pkg + '/index.js';
|
|
79
|
-
window.__debug.idbSnapshot[key] = await r.text();
|
|
80
|
-
window.__debug.idbPersist?.();
|
|
81
|
-
wl('installed ' + pkg);
|
|
82
|
-
},
|
|
83
|
-
};
|
|
84
|
-
}
|
|
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';
|
|
85
17
|
|
|
86
18
|
const machine = createMachine({ id: 'shell', initial: 'idle', states: {
|
|
87
|
-
idle: { on: { RUN: 'executing', ENTER_REPL: 'node-repl',
|
|
19
|
+
idle: { on: { RUN: 'executing', ENTER_REPL: 'node-repl', NODE_START: 'node-running' } },
|
|
88
20
|
executing: { on: { DONE: 'idle', ERROR: 'idle' } },
|
|
89
|
-
'npm-installing': { on: { DONE: 'idle', ERROR: 'idle' } },
|
|
90
21
|
'node-running': { on: { DONE: 'idle', ERROR: 'idle' } },
|
|
91
22
|
'node-repl': { on: { EXIT_REPL: 'idle', RUN: 'node-repl' } },
|
|
92
23
|
}});
|
|
93
24
|
|
|
94
25
|
export function createShell({ term, onPreviewWrite }) {
|
|
95
|
-
const ctx = { term, cwd: '/', env: {}, history: [],
|
|
96
|
-
const BUILTINS = makeBuiltins(ctx);
|
|
97
|
-
ctx.nodeEval = createNodeEnv({ ctx, term });
|
|
98
|
-
|
|
26
|
+
const ctx = { term, cwd: '/', prevCwd: '/', env: {}, history: [], lastExitCode: 0, argv: [], functions: {}, opts: {}, localStack: [], loopFlag: null, arrays: {}, bgJobs: {}, traps: {} };
|
|
99
27
|
const actor = createActor(machine);
|
|
100
28
|
actor.start();
|
|
29
|
+
const httpHandlers = {};
|
|
30
|
+
window.__debug = window.__debug || {};
|
|
101
31
|
|
|
102
32
|
let inputQueue = [];
|
|
33
|
+
function drainQueue(onData) { const items = inputQueue.slice(); inputQueue = []; for (const d of items) onData(d); }
|
|
103
34
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
35
|
+
const toKey = p => p.replace(/^\//, '');
|
|
36
|
+
const snap = () => window.__debug.idbSnapshot || {};
|
|
37
|
+
|
|
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; }
|
|
63
|
+
return out;
|
|
108
64
|
}
|
|
109
65
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
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
|
+
}
|
|
119
75
|
|
|
120
|
-
async function
|
|
121
|
-
if (
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if (!capture) {
|
|
125
|
-
if (fn) await fn(args, actor); else term.write('command not found: ' + cmd + '\r\n');
|
|
126
|
-
return '';
|
|
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));
|
|
127
80
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
finally { term.write = orig; }
|
|
133
|
-
return out;
|
|
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));
|
|
134
85
|
}
|
|
135
86
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
if (
|
|
141
|
-
const
|
|
142
|
-
if (
|
|
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; }
|
|
143
102
|
try {
|
|
144
|
-
|
|
145
|
-
if (
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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;
|
|
155
136
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
} catch (e) {
|
|
159
|
-
term.write('\x1b[31m' + e.message + '\x1b[0m\r\n');
|
|
160
|
-
actor.send({ type: 'ERROR' });
|
|
161
|
-
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; }
|
|
162
139
|
}
|
|
163
140
|
}
|
|
164
141
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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));
|
|
172
169
|
}
|
|
173
|
-
|
|
170
|
+
if (onData) drainQueue(onData);
|
|
174
171
|
}
|
|
175
172
|
|
|
176
|
-
const
|
|
177
|
-
term,
|
|
178
|
-
getCompletions,
|
|
179
|
-
getPrompt: () => ctx.cwd,
|
|
180
|
-
onLine: line => run(line, onData).then(() => rl.showPrompt()),
|
|
181
|
-
});
|
|
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));
|
|
182
174
|
|
|
183
|
-
|
|
184
|
-
if (
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
term.write('
|
|
188
|
-
rl.showPrompt();
|
|
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(); });
|
|
189
180
|
return;
|
|
190
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 });
|
|
185
|
+
function onData(data) {
|
|
186
|
+
if (data === '\x03') { actor.send({ type: 'ERROR' }); inputQueue = []; blockLines = []; term.write('^C'); rl.showPrompt(); return; }
|
|
191
187
|
const st = actor.getSnapshot().value;
|
|
192
|
-
if (st !== 'idle' && st !== 'node-repl')
|
|
193
|
-
inputQueue.push(data);
|
|
194
|
-
return;
|
|
195
|
-
}
|
|
196
|
-
rl.onData(data);
|
|
188
|
+
if (st !== 'idle' && st !== 'node-repl') inputQueue.push(data); else rl.onData(data);
|
|
197
189
|
}
|
|
198
|
-
|
|
199
190
|
term.onData(onData);
|
|
200
|
-
onPreviewWrite && (window.__debug.shell.onPreviewWrite = onPreviewWrite);
|
|
201
|
-
const runPublic = line => run(line, onData);
|
|
202
|
-
window.__debug.shell.run = runPublic;
|
|
203
191
|
rl.showPrompt();
|
|
204
|
-
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
|
+
};
|
|
205
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
|
|
package/docs/todo.html
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Todo App</title>
|
|
7
|
+
<style>
|
|
8
|
+
* {
|
|
9
|
+
margin: 0;
|
|
10
|
+
padding: 0;
|
|
11
|
+
box-sizing: border-box;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
body {
|
|
15
|
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
16
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
17
|
+
min-height: 100vh;
|
|
18
|
+
display: flex;
|
|
19
|
+
justify-content: center;
|
|
20
|
+
align-items: center;
|
|
21
|
+
padding: 20px;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.container {
|
|
25
|
+
background: white;
|
|
26
|
+
border-radius: 10px;
|
|
27
|
+
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
|
|
28
|
+
width: 100%;
|
|
29
|
+
max-width: 500px;
|
|
30
|
+
padding: 30px;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
h1 {
|
|
34
|
+
color: #333;
|
|
35
|
+
margin-bottom: 20px;
|
|
36
|
+
text-align: center;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.input-group {
|
|
40
|
+
display: flex;
|
|
41
|
+
gap: 10px;
|
|
42
|
+
margin-bottom: 20px;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
input[type="text"] {
|
|
46
|
+
flex: 1;
|
|
47
|
+
padding: 12px;
|
|
48
|
+
border: 2px solid #e0e0e0;
|
|
49
|
+
border-radius: 5px;
|
|
50
|
+
font-size: 16px;
|
|
51
|
+
transition: border-color 0.3s;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
input[type="text"]:focus {
|
|
55
|
+
outline: none;
|
|
56
|
+
border-color: #667eea;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
button {
|
|
60
|
+
background: #667eea;
|
|
61
|
+
color: white;
|
|
62
|
+
border: none;
|
|
63
|
+
padding: 12px 25px;
|
|
64
|
+
border-radius: 5px;
|
|
65
|
+
font-size: 16px;
|
|
66
|
+
cursor: pointer;
|
|
67
|
+
transition: background 0.3s;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
button:hover {
|
|
71
|
+
background: #764ba2;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.todo-list {
|
|
75
|
+
list-style: none;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.todo-item {
|
|
79
|
+
display: flex;
|
|
80
|
+
align-items: center;
|
|
81
|
+
gap: 12px;
|
|
82
|
+
padding: 15px;
|
|
83
|
+
background: #f5f5f5;
|
|
84
|
+
border-radius: 5px;
|
|
85
|
+
margin-bottom: 10px;
|
|
86
|
+
transition: all 0.3s;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.todo-item:hover {
|
|
90
|
+
background: #efefef;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.todo-item.completed {
|
|
94
|
+
opacity: 0.6;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.todo-item.completed .todo-text {
|
|
98
|
+
text-decoration: line-through;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.todo-checkbox {
|
|
102
|
+
width: 20px;
|
|
103
|
+
height: 20px;
|
|
104
|
+
cursor: pointer;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.todo-text {
|
|
108
|
+
flex: 1;
|
|
109
|
+
color: #333;
|
|
110
|
+
word-break: break-word;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.delete-btn {
|
|
114
|
+
background: #ff6b6b;
|
|
115
|
+
padding: 8px 12px;
|
|
116
|
+
font-size: 14px;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.delete-btn:hover {
|
|
120
|
+
background: #ff5252;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.empty-state {
|
|
124
|
+
text-align: center;
|
|
125
|
+
color: #999;
|
|
126
|
+
padding: 40px 20px;
|
|
127
|
+
}
|
|
128
|
+
</style>
|
|
129
|
+
</head>
|
|
130
|
+
<body>
|
|
131
|
+
<div class="container">
|
|
132
|
+
<h1>📝 Todo App</h1>
|
|
133
|
+
|
|
134
|
+
<div class="input-group">
|
|
135
|
+
<input type="text" id="todoInput" placeholder="Add a new todo...">
|
|
136
|
+
<button onclick="addTodo()">Add</button>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<ul class="todo-list" id="todoList">
|
|
140
|
+
<li class="empty-state">No todos yet. Add one to get started!</li>
|
|
141
|
+
</ul>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
<script>
|
|
145
|
+
let todos = JSON.parse(localStorage.getItem('todos')) || [];
|
|
146
|
+
|
|
147
|
+
function renderTodos() {
|
|
148
|
+
const todoList = document.getElementById('todoList');
|
|
149
|
+
|
|
150
|
+
if (todos.length === 0) {
|
|
151
|
+
todoList.innerHTML = '<li class="empty-state">No todos yet. Add one to get started!</li>';
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
todoList.innerHTML = todos.map((todo, index) => `
|
|
156
|
+
<li class="todo-item ${todo.completed ? 'completed' : ''}">
|
|
157
|
+
<input type="checkbox" class="todo-checkbox"
|
|
158
|
+
${todo.completed ? 'checked' : ''}
|
|
159
|
+
onchange="toggleTodo(${index})">
|
|
160
|
+
<span class="todo-text">${escapeHtml(todo.text)}</span>
|
|
161
|
+
<button class="delete-btn" onclick="deleteTodo(${index})">Delete</button>
|
|
162
|
+
</li>
|
|
163
|
+
`).join('');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function addTodo() {
|
|
167
|
+
const input = document.getElementById('todoInput');
|
|
168
|
+
const text = input.value.trim();
|
|
169
|
+
|
|
170
|
+
if (text === '') {
|
|
171
|
+
alert('Please enter a todo!');
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
todos.push({ text, completed: false });
|
|
176
|
+
localStorage.setItem('todos', JSON.stringify(todos));
|
|
177
|
+
input.value = '';
|
|
178
|
+
renderTodos();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function toggleTodo(index) {
|
|
182
|
+
todos[index].completed = !todos[index].completed;
|
|
183
|
+
localStorage.setItem('todos', JSON.stringify(todos));
|
|
184
|
+
renderTodos();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function deleteTodo(index) {
|
|
188
|
+
todos.splice(index, 1);
|
|
189
|
+
localStorage.setItem('todos', JSON.stringify(todos));
|
|
190
|
+
renderTodos();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function escapeHtml(text) {
|
|
194
|
+
const map = {
|
|
195
|
+
'&': '&',
|
|
196
|
+
'<': '<',
|
|
197
|
+
'>': '>',
|
|
198
|
+
'"': '"',
|
|
199
|
+
"'": '''
|
|
200
|
+
};
|
|
201
|
+
return text.replace(/[&<>"']/g, m => map[m]);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
document.getElementById('todoInput').addEventListener('keypress', (e) => {
|
|
205
|
+
if (e.key === 'Enter') addTodo();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
renderTodos();
|
|
209
|
+
</script>
|
|
210
|
+
</body>
|
|
211
|
+
</html>
|
package/package.json
CHANGED