thebird 1.2.78 → 1.2.80

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/.github/workflows/publish.yml +9 -1
  2. package/CHANGELOG.md +226 -0
  3. package/CLAUDE.md +16 -0
  4. package/docs/agent-chat.js +7 -4
  5. package/docs/app.js +14 -11
  6. package/docs/defaults.json +1 -1
  7. package/docs/index.html +23 -6
  8. package/docs/kilo-http-stream.js +24 -0
  9. package/docs/node-builtins.js +194 -0
  10. package/docs/preview/index.html +32 -0
  11. package/docs/preview-sw-client.js +37 -6
  12. package/docs/preview-sw.js +55 -51
  13. package/docs/shell-awk.js +113 -0
  14. package/docs/shell-builtins-extra.js +121 -0
  15. package/docs/shell-builtins-text.js +109 -0
  16. package/docs/shell-builtins-util.js +112 -0
  17. package/docs/shell-builtins.js +183 -0
  18. package/docs/shell-bun.js +45 -0
  19. package/docs/shell-control.js +132 -0
  20. package/docs/shell-deno.js +54 -0
  21. package/docs/shell-exec.js +85 -0
  22. package/docs/shell-expand.js +164 -0
  23. package/docs/shell-fd.js +86 -0
  24. package/docs/shell-jobs.js +86 -0
  25. package/docs/shell-node-advanced.js +86 -0
  26. package/docs/shell-node-brotli.js +22 -0
  27. package/docs/shell-node-busnet.js +90 -0
  28. package/docs/shell-node-cipher.js +61 -0
  29. package/docs/shell-node-cluster.js +33 -0
  30. package/docs/shell-node-coreutils.js +36 -0
  31. package/docs/shell-node-crypto.js +137 -0
  32. package/docs/shell-node-dns.js +41 -0
  33. package/docs/shell-node-extras.js +148 -0
  34. package/docs/shell-node-firefox.js +95 -0
  35. package/docs/shell-node-git.js +60 -0
  36. package/docs/shell-node-inspector.js +39 -0
  37. package/docs/shell-node-io.js +131 -0
  38. package/docs/shell-node-ipc.js +15 -0
  39. package/docs/shell-node-keyobject.js +60 -0
  40. package/docs/shell-node-modules.js +157 -0
  41. package/docs/shell-node-native.js +31 -0
  42. package/docs/shell-node-net.js +71 -0
  43. package/docs/shell-node-observe.js +80 -0
  44. package/docs/shell-node-opfs.js +54 -0
  45. package/docs/shell-node-procfs.js +42 -0
  46. package/docs/shell-node-profiler.js +50 -0
  47. package/docs/shell-node-registry.js +24 -0
  48. package/docs/shell-node-resolve.js +147 -0
  49. package/docs/shell-node-runtime.js +83 -0
  50. package/docs/shell-node-srcmap.js +52 -0
  51. package/docs/shell-node-stdlib.js +103 -0
  52. package/docs/shell-node-streams.js +66 -0
  53. package/docs/shell-node-tar.js +47 -0
  54. package/docs/shell-node-testrunner.js +35 -0
  55. package/docs/shell-node-util-extras.js +66 -0
  56. package/docs/shell-node.js +188 -97
  57. package/docs/shell-npm.js +173 -0
  58. package/docs/shell-parser.js +122 -0
  59. package/docs/shell-pm-layout.js +62 -0
  60. package/docs/shell-pm.js +39 -0
  61. package/docs/shell-posix.js +70 -0
  62. package/docs/shell-procsub.js +65 -0
  63. package/docs/shell-readline.js +59 -4
  64. package/docs/shell-runtime.js +37 -0
  65. package/docs/shell-sed.js +83 -0
  66. package/docs/shell-signals.js +54 -0
  67. package/docs/shell-sw-jobs.js +76 -0
  68. package/docs/shell-ts.js +30 -0
  69. package/docs/shell.js +159 -167
  70. package/docs/terminal.js +9 -11
  71. package/docs/todo.html +211 -0
  72. package/package.json +1 -1
  73. package/server.js +43 -4
  74. package/start-kilo.js +17 -0
  75. package/test.js +199 -0
  76. package/.codeinsight +0 -73
  77. package/docs/acp-stream.js +0 -102
  78. package/docs/coi-serviceworker.js +0 -2
@@ -0,0 +1,132 @@
1
+ export async function runScript(text, run, ctx) {
2
+ let block = [];
3
+ for (const s of text.split('\n').flatMap(l => l.split(';')).map(x => x.trim()).filter(Boolean)) {
4
+ if (block.length || isControlStart(s)) { block.push(s); if (!isBlockOpen(block)) { await runControl(block.slice(), run, ctx); block = []; } continue; }
5
+ await run(s);
6
+ }
7
+ }
8
+
9
+ export function isControlStart(cmd) {
10
+ const t = cmd.trim();
11
+ const first = t.split(/\s+/)[0];
12
+ if (first === 'if' || first === 'while' || first === 'for' || first === 'case' || first === 'until' || first === 'select') return true;
13
+ if (/^[A-Za-z_][A-Za-z0-9_]*\s*\(\s*\)/.test(t)) return true;
14
+ return false;
15
+ }
16
+
17
+ export function isBlockOpen(lines) {
18
+ const joined = lines.join(' ').trim();
19
+ let depth = 0;
20
+ const tokens = joined.split(/\s+/);
21
+ for (const t of tokens) {
22
+ if (t === 'if' || t === 'while' || t === 'for' || t === 'case' || t === 'until' || t === 'select') depth++;
23
+ if (t === 'fi' || t === 'done' || t === 'esac') depth--;
24
+ }
25
+ const fnOpen = /\{\s*$/.test(joined) || /\(\s*\)\s*$/.test(joined);
26
+ const fnClose = /\}\s*$/.test(joined);
27
+ let braceDepth = 0;
28
+ let inSingle = false, inDouble = false;
29
+ for (const ch of joined) {
30
+ if (ch === "'" && !inDouble) inSingle = !inSingle;
31
+ else if (ch === '"' && !inSingle) inDouble = !inDouble;
32
+ else if (!inSingle && !inDouble) {
33
+ if (ch === '{') braceDepth++;
34
+ if (ch === '}') braceDepth--;
35
+ }
36
+ }
37
+ return depth > 0 || braceDepth > 0 || (fnOpen && !fnClose);
38
+ }
39
+
40
+ export async function runControl(block, run, ctx) {
41
+ const joined = block.join(' ').trim();
42
+ if (/^[A-Za-z_][A-Za-z0-9_]*\s*\(\s*\)/.test(joined)) return defineFn(joined, ctx);
43
+ if (joined.startsWith('if ')) return runIf(joined, run, ctx);
44
+ if (joined.startsWith('while ')) return runWhile(joined, run, ctx, false);
45
+ if (joined.startsWith('until ')) return runWhile(joined.replace(/^until /, 'while '), run, ctx, true);
46
+ if (joined.startsWith('for ')) return runFor(joined, run, ctx);
47
+ if (joined.startsWith('case ')) return runCase(joined, run, ctx);
48
+ if (joined.startsWith('select ')) return runSelect(joined, run, ctx);
49
+ }
50
+
51
+ async function runSelect(text, run, ctx) {
52
+ const m = text.match(/^select\s+(\w+)\s+in\s+(.+?)\s*;\s*do\s+(.+?)\s*;\s*done$/s);
53
+ if (!m) throw new Error('select: parse error: ' + text);
54
+ const [, varName, listExpr, body] = m;
55
+ const items = listExpr.split(/\s+/).filter(Boolean);
56
+ for (let i = 0; i < items.length; i++) ctx.term.write((i + 1) + ') ' + items[i] + '\r\n');
57
+ for (const it of items) { ctx.env[varName] = it; await run(body); if (ctx.loopFlag === 'break') { ctx.loopFlag = null; break; } }
58
+ }
59
+
60
+ function defineFn(text, ctx) {
61
+ const m = text.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*\(\s*\)\s*\{?\s*(.+?)\s*\}?\s*$/s);
62
+ if (!m) throw new Error('function: parse error: ' + text);
63
+ const [, name, body] = m;
64
+ ctx.functions = ctx.functions || {};
65
+ ctx.functions[name] = body.replace(/^\{\s*/, '').replace(/\s*\}$/, '').trim();
66
+ }
67
+
68
+ async function runIf(text, run, ctx) {
69
+ const body = text.replace(/^if\s+/, '').replace(/\s*;\s*fi$/, '');
70
+ const parts = body.split(/\s*;\s*(?=then|elif|else)\s*/);
71
+ const branches = [];
72
+ let i = 0;
73
+ while (i < parts.length) {
74
+ if (parts[i] === 'then' || parts[i] === 'elif' || parts[i] === 'else') { i++; continue; }
75
+ if (parts[i - 1] === 'else') { branches.push({ cond: null, body: parts[i] }); i++; continue; }
76
+ const cond = parts[i]; const bodyPart = parts[i + 2] || parts[i + 1];
77
+ branches.push({ cond, body: bodyPart });
78
+ i += (parts[i + 1] === 'then' ? 3 : 2);
79
+ }
80
+ for (const br of branches) {
81
+ if (br.cond === null) { await run(br.body); return; }
82
+ await run(br.cond);
83
+ if (ctx.lastExitCode === 0) { await run(br.body); return; }
84
+ }
85
+ }
86
+
87
+ async function runWhile(text, run, ctx, invert) {
88
+ const m = text.match(/^while\s+(.+?)\s*;\s*do\s+(.+?)\s*;\s*done$/s);
89
+ if (!m) throw new Error('while: parse error: ' + text);
90
+ const [, cond, body] = m;
91
+ let guard = 0;
92
+ ctx.loopFlag = null;
93
+ while (guard++ < 10000) {
94
+ await run(cond);
95
+ const ok = ctx.lastExitCode === 0;
96
+ if ((invert ? ok : !ok)) break;
97
+ await run(body);
98
+ if (ctx.loopFlag === 'break') { ctx.loopFlag = null; break; }
99
+ if (ctx.loopFlag === 'continue') ctx.loopFlag = null;
100
+ }
101
+ }
102
+
103
+ async function runFor(text, run, ctx) {
104
+ const m = text.match(/^for\s+(\w+)\s+in\s+(.+?)\s*;\s*do\s+(.+?)\s*;\s*done$/s);
105
+ if (!m) throw new Error('for: parse error: ' + text);
106
+ const [, varName, listExpr, body] = m;
107
+ const items = listExpr.split(/\s+/).filter(Boolean);
108
+ ctx.loopFlag = null;
109
+ for (const item of items) {
110
+ ctx.env[varName] = item;
111
+ await run(body);
112
+ if (ctx.loopFlag === 'break') { ctx.loopFlag = null; break; }
113
+ if (ctx.loopFlag === 'continue') { ctx.loopFlag = null; continue; }
114
+ }
115
+ }
116
+
117
+ async function runCase(text, run, ctx) {
118
+ const m = text.match(/^case\s+(.+?)\s+in\s+(.+?)\s*;\s*esac$/s);
119
+ if (!m) throw new Error('case: parse error: ' + text);
120
+ const [, subject, body] = m;
121
+ const sub = (ctx.expand ? ctx.expand(subject) : subject).trim();
122
+ const clauses = body.split(/\s*;;\s*/).filter(Boolean);
123
+ for (const clause of clauses) {
124
+ const cm = clause.match(/^(.+?)\)\s*(.+)$/s);
125
+ if (!cm) continue;
126
+ const [, patterns, cmds] = cm;
127
+ for (const pat of patterns.split('|').map(s => s.trim())) {
128
+ const re = new RegExp('^' + pat.replace(/[-[\]{}()+.,\\^$|#]/g, '\\$&').replace(/\*/g, '.*').replace(/\?/g, '.') + '$');
129
+ if (re.test(sub)) { await run(cmds); return; }
130
+ }
131
+ }
132
+ }
@@ -0,0 +1,54 @@
1
+ export function makeDenoGlobal(fs,proc,cpMod,httpHandlers,Buf){
2
+ const enc=new TextEncoder(),dec=new TextDecoder();
3
+ const readFile=async p=>fs.existsSync(p)?(typeof fs.readFileSync(p)==='string'?enc.encode(fs.readFileSync(p)):fs.readFileSync(p)):(()=>{const e=new Error('NotFound: '+p);e.code='ENOENT';throw e;})();
4
+ return{
5
+ version:{deno:'2.0.0',v8:'12.9.202',typescript:'5.6.0'},
6
+ build:{target:'x86_64-unknown-linux-gnu',arch:'x86_64',os:'linux',vendor:'unknown'},
7
+ pid:proc.pid||1,ppid:0,hostname:()=>'thebird',
8
+ cwd:()=>proc.cwd?.()||'/',
9
+ chdir:p=>proc.chdir?.(p),
10
+ exit:code=>proc.exit(code||0),
11
+ env:{get:k=>proc.env[k],set:(k,v)=>{proc.env[k]=v;},delete:k=>{delete proc.env[k];},has:k=>k in proc.env,toObject:()=>({...proc.env})},
12
+ args:(proc.argv||[]).slice(2),
13
+ execPath:()=>'/usr/local/bin/deno',
14
+ readTextFile:async p=>{const d=await readFile(p);return typeof d==='string'?d:dec.decode(d);},
15
+ readTextFileSync:p=>fs.readFileSync(p,'utf8'),
16
+ readFile,
17
+ readFileSync:p=>{const d=fs.readFileSync(p);return typeof d==='string'?enc.encode(d):d;},
18
+ writeTextFile:async(p,d)=>fs.writeFileSync(p,d),
19
+ writeTextFileSync:(p,d)=>fs.writeFileSync(p,d),
20
+ writeFile:async(p,d)=>fs.writeFileSync(p,d),
21
+ writeFileSync:(p,d)=>fs.writeFileSync(p,d),
22
+ mkdir:async(p,opts)=>fs.mkdirSync(p,opts),
23
+ mkdirSync:(p,opts)=>fs.mkdirSync(p,opts),
24
+ remove:async(p,opts)=>{if(opts?.recursive)fs.rmSync(p,{recursive:true});else fs.unlinkSync(p);},
25
+ removeSync:(p,opts)=>{if(opts?.recursive)fs.rmSync(p,{recursive:true});else fs.unlinkSync(p);},
26
+ rename:async(o,n)=>fs.renameSync(o,n),
27
+ renameSync:(o,n)=>fs.renameSync(o,n),
28
+ stat:async p=>fs.statSync(p),
29
+ statSync:p=>fs.statSync(p),
30
+ lstat:async p=>fs.lstatSync?.(p)||fs.statSync(p),
31
+ lstatSync:p=>fs.lstatSync?.(p)||fs.statSync(p),
32
+ symlink:async(t,l)=>fs.symlinkSync?.(t,l),
33
+ symlinkSync:(t,l)=>fs.symlinkSync?.(t,l),
34
+ realPath:async p=>fs.realpathSync?.(p)||p,
35
+ realPathSync:p=>fs.realpathSync?.(p)||p,
36
+ readDir:async function*(p){for(const name of fs.readdirSync(p))yield{name,isFile:true,isDirectory:false,isSymlink:false};},
37
+ readDirSync:function*(p){for(const name of fs.readdirSync(p))yield{name,isFile:true,isDirectory:false,isSymlink:false};},
38
+ makeTempDir:async opts=>fs.mkdtempSync?.((opts?.prefix||'/tmp/deno-'))||'/tmp/deno-'+Math.random().toString(36).slice(2,8),
39
+ makeTempFile:async opts=>{const p=(opts?.prefix||'/tmp/')+'deno-'+Math.random().toString(36).slice(2,8);fs.writeFileSync(p,'');return p;},
40
+ serve(opts,handler){const h=typeof opts==='function'?opts:handler||opts.fetch;const port=opts.port||8000;httpHandlers[port]={routes:{GET:[{path:'/',fn:async(req,res)=>{const r=await h(new Request('http://localhost:'+port+req.url,{method:req.method,headers:req.headers}));res.statusCode=r.status;r.headers.forEach((v,k)=>res.setHeader(k,v));res.end(await r.text());}}]}};return{shutdown:async()=>{delete httpHandlers[port];},finished:Promise.resolve()};},
41
+ Command:class{constructor(cmd,opts={}){this.cmd=cmd;this.opts=opts;}async output(){return new Promise((resolve,reject)=>{cpMod.exec([this.cmd,...(this.opts.args||[])].join(' '),{cwd:this.opts.cwd,env:this.opts.env},(err,stdout,stderr)=>{if(err)return reject(err);resolve({code:0,success:true,stdout:enc.encode(stdout),stderr:enc.encode(stderr)});});});}spawn(){return this;}},
42
+ permissions:{query:async d=>({state:'granted',onchange:null,partial:false}),request:async d=>({state:'granted'}),revoke:async d=>({state:'prompt'})},
43
+ errors:{NotFound:class extends Error{constructor(m){super(m);this.name='NotFound';}},PermissionDenied:class extends Error{constructor(m){super(m);this.name='PermissionDenied';}},AlreadyExists:class extends Error{constructor(m){super(m);this.name='AlreadyExists';}}},
44
+ inspect:v=>JSON.stringify(v,null,2),
45
+ noColor:false,isatty:()=>true,
46
+ addSignalListener(sig,fn){proc.on(sig,fn);},removeSignalListener(sig,fn){proc.off?.(sig,fn);},
47
+ stdin:{readable:new ReadableStream({start(c){proc.stdin?.on?.('data',d=>c.enqueue(typeof d==='string'?new TextEncoder().encode(d):d));proc.stdin?.on?.('end',()=>c.close());}}),readSync(){return 0;},read:async buf=>0,rid:0,isTerminal:()=>!!proc.stdin?.isTTY},
48
+ stdout:{writable:new WritableStream({write(c){proc.stdout.write(typeof c==='string'?c:new TextDecoder().decode(c));}}),writeSync:d=>{proc.stdout.write(typeof d==='string'?d:new TextDecoder().decode(d));return d.length;},write:async d=>d.length,rid:1,isTerminal:()=>!!proc.stdout?.isTTY},
49
+ stderr:{writable:new WritableStream({write(c){proc.stderr.write(typeof c==='string'?c:new TextDecoder().decode(c));}}),writeSync:d=>{proc.stderr.write(typeof d==='string'?d:new TextDecoder().decode(d));return d.length;},write:async d=>d.length,rid:2,isTerminal:()=>!!proc.stderr?.isTTY},
50
+ memoryUsage:()=>proc.memoryUsage(),
51
+ resources:()=>({}),close:rid=>{},
52
+ refTimer:t=>t?.ref?.(),unrefTimer:t=>t?.unref?.(),
53
+ };
54
+ }
@@ -0,0 +1,85 @@
1
+ import { registerStream, readStream } from './shell-procsub.js';
2
+ import { tokenize, globToRe } from './shell-parser.js';
3
+ import { fullExpand, expandBraces, expandTilde } from './shell-expand.js';
4
+ import { resolvePath } from './shell-builtins.js';
5
+ import { NODE_VERSION } from './shell-node-modules.js';
6
+
7
+ export function makeExpander(ctx, captureRun, parseRedirect) {
8
+ const toKey = p => p.replace(/^\//, '');
9
+ const snap = () => window.__debug.idbSnapshot || {};
10
+
11
+ function replaceProcSub(token) {
12
+ let out = ''; let i = 0;
13
+ while (i < token.length) {
14
+ if ((token[i] === '<' || token[i] === '>') && token[i + 1] === '(') {
15
+ let depth = 1; let j = i + 2;
16
+ while (j < token.length && depth > 0) { if (token[j] === '(') depth++; else if (token[j] === ')') depth--; if (depth) j++; }
17
+ if (depth === 0) { out += registerStream(captureRun(token.slice(i + 2, j))); i = j + 1; continue; }
18
+ }
19
+ out += token[i++];
20
+ }
21
+ return out;
22
+ }
23
+
24
+ function expandGlob(token) {
25
+ if (!token.includes('*') && !token.includes('?') && !token.includes('[')) return [token];
26
+ const prefix = toKey(resolvePath(ctx.cwd, ''));
27
+ const keys = Object.keys(snap()).map(k => prefix && k.startsWith(prefix + '/') ? k.slice(prefix.length + 1) : k);
28
+ const re = globToRe(token);
29
+ const matches = keys.filter(k => re.test(k));
30
+ return matches.length ? matches.sort() : [token];
31
+ }
32
+
33
+ function expandTokens(tokens) {
34
+ return tokens.flatMap(t => {
35
+ const procsub = t.includes('<(') || t.includes('>(') ? replaceProcSub(t) : t;
36
+ const tilde = expandTilde(procsub, ctx.env);
37
+ const braces = expandBraces(tilde);
38
+ return braces.flatMap(b => expandGlob(fullExpand(b, ctx.env, ctx.lastExitCode, ctx.argv, captureRun, ctx.arrays)));
39
+ });
40
+ }
41
+ return { expandTokens, expandGlob, replaceProcSub };
42
+ }
43
+
44
+ export function makeCaptureRun(ctx, BUILTINS, actor, parseRedirect, expandTokens) {
45
+ return function captureRun(line) {
46
+ const raw = tokenize(line); if (!raw.length) return '';
47
+ let out = ''; const orig = ctx.term.write.bind(ctx.term); ctx.term.write = s => { out += s; };
48
+ try { const [cmd, ...args] = parseRedirect(expandTokens(raw)).args; BUILTINS[cmd]?.(args, actor); } finally { ctx.term.write = orig; }
49
+ return out.replace(/\r\n/g, '\n').replace(/\r/g, '\n').trim();
50
+ };
51
+ }
52
+
53
+ const NODE_HELP = 'Usage: node [options] [script.js] [arguments]\r\n -v, --version print Node.js version\r\n -e, --eval evaluate script\r\n -p, --print evaluate and print result\r\n -h, --help print this help\r\n';
54
+
55
+ export function makeNodeRunner(ctx, actor) {
56
+ const toKey = p => p.replace(/^\//, '');
57
+ const snap = () => window.__debug.idbSnapshot || {};
58
+ return async function runNode(args, stdinBuf) {
59
+ const term = ctx.term;
60
+ if (!args.length) { actor.send({ type: 'ENTER_REPL' }); term.write('Welcome to Node.js ' + NODE_VERSION + '.\r\nType ".help" for more information.\r\n> '); return; }
61
+ const a0 = args[0];
62
+ if (a0 === '-v' || a0 === '--version') { term.write(NODE_VERSION + '\r\n'); return; }
63
+ if (a0 === '-h' || a0 === '--help') { term.write(NODE_HELP); return; }
64
+ if (a0 === '-e' || a0 === '--eval') { await ctx.nodeEval(args.slice(1).join(' '), null, [], stdinBuf); return; }
65
+ if (a0 === '-p' || a0 === '--print') { await ctx.nodeEval('process.stdout.write(String(' + args.slice(1).join(' ') + ') + "\\n")', null, [], stdinBuf); return; }
66
+ const code = snap()[toKey(resolvePath(ctx.cwd, a0))];
67
+ if (code == null) { term.write('\x1b[31mnode: ' + a0 + ': No such file or directory\x1b[0m\r\n'); ctx.lastExitCode = 1; return; }
68
+ actor.send({ type: 'NODE_START' }); ctx.argv = args;
69
+ try { await ctx.nodeEval(code, a0, args.slice(1), stdinBuf); } finally { ctx.argv = []; }
70
+ };
71
+ }
72
+
73
+ export function makeNpmResultRunner(ctx, run) {
74
+ return async function runNpmResult(r) {
75
+ if (!r) return;
76
+ if (r.runInShell) { await run(r.runInShell); return; }
77
+ if (!r.npmChain) return;
78
+ for (const step of r.npmChain) {
79
+ ctx.term.write('\r\n> ' + r.pkgName + '@' + r.pkgVersion + ' ' + step.name + '\r\n> ' + step.cmd + '\r\n\r\n');
80
+ ctx.env.npm_lifecycle_event = step.name;
81
+ await run(step.cmd);
82
+ if (ctx.lastExitCode !== 0) return;
83
+ }
84
+ };
85
+ }
@@ -0,0 +1,164 @@
1
+ export function expandParam(name, env, argv, lastExit, arrays) {
2
+ if (name === '?') return String(lastExit ?? 0);
3
+ if (name === '!') return env['!'] || '';
4
+ if (name === '$') return env.$ || '0';
5
+ if (name === '#') return String((argv || []).length > 0 ? (argv || []).length - 1 : 0);
6
+ if (name === '@' || name === '*') return (argv || []).slice(1).join(' ');
7
+ if (name === '0') return (argv || [])[0] || '';
8
+ if (/^[1-9]$/.test(name)) return (argv || [])[parseInt(name)] || '';
9
+ const arrM = name.match(/^([A-Za-z_][A-Za-z0-9_]*)\[(.+?)\]$/);
10
+ if (arrM && arrays) {
11
+ const a = arrays[arrM[1]];
12
+ if (a == null) return '';
13
+ if (arrM[2] === '@' || arrM[2] === '*') return Array.isArray(a) ? a.join(' ') : Object.values(a).join(' ');
14
+ if (Array.isArray(a)) return a[parseInt(arrM[2], 10)] || '';
15
+ return a[arrM[2]] || '';
16
+ }
17
+ const lenArrM = name.match(/^#([A-Za-z_][A-Za-z0-9_]*)\[@\]$/);
18
+ if (lenArrM && arrays) { const a = arrays[lenArrM[1]] || []; return String(Array.isArray(a) ? a.length : Object.keys(a).length); }
19
+ return env[name] ?? '';
20
+ }
21
+
22
+ export function expandParamOp(expr, env, argv, lastExit, arrays) {
23
+ if (expr.startsWith('!')) {
24
+ const prefM = expr.match(/^!([A-Za-z_]\w*)([@*])$/);
25
+ if (prefM) return Object.keys(env).filter(k => k.startsWith(prefM[1])).join(' ');
26
+ const keysM = expr.match(/^!([A-Za-z_]\w*)\[[@*]\]$/);
27
+ if (keysM && arrays) { const a = arrays[keysM[1]] || []; return Array.isArray(a) ? a.map((_, i) => i).join(' ') : Object.keys(a).join(' '); }
28
+ const indM = expr.match(/^!([A-Za-z_]\w*)$/);
29
+ if (indM) { const t = env[indM[1]]; return t ? expandParam(t, env, argv, lastExit, arrays) : ''; }
30
+ }
31
+ const caseM = expr.match(/^([A-Za-z_][A-Za-z0-9_]*|@)(\^\^|,,|\^|,)(.*)$/s);
32
+ if (caseM) {
33
+ const v = expandParam(caseM[1], env, argv, lastExit, arrays);
34
+ const op = caseM[2];
35
+ if (op === '^^') return v.toUpperCase();
36
+ if (op === ',,') return v.toLowerCase();
37
+ if (op === '^') return v.charAt(0).toUpperCase() + v.slice(1);
38
+ if (op === ',') return v.charAt(0).toLowerCase() + v.slice(1);
39
+ }
40
+ const qM = expr.match(/^([A-Za-z_][A-Za-z0-9_]*|@)@([QEP])$/);
41
+ if (qM) {
42
+ const v = expandParam(qM[1], env, argv, lastExit, arrays);
43
+ if (qM[2] === 'Q') return "'" + v.replace(/'/g, "'\\''") + "'";
44
+ if (qM[2] === 'E') return v.replace(/\\n/g, '\n').replace(/\\t/g, '\t');
45
+ if (qM[2] === 'P') return v;
46
+ }
47
+ const lenArrM = expr.match(/^#([A-Za-z_]\w*)\[[@*]\]$/);
48
+ if (lenArrM) { const a = (arrays || {})[lenArrM[1]] || []; return String(Array.isArray(a) ? a.length : Object.keys(a).length); }
49
+ const lenM = expr.match(/^#(.+)$/);
50
+ if (lenM) return String(expandParam(lenM[1], env, argv, lastExit, arrays).length);
51
+ const sliceM = expr.match(/^([^:]+):(\d+)(?::(\d+))?$/);
52
+ if (sliceM) { const v = expandParam(sliceM[1], env, argv, lastExit, arrays); const s = +sliceM[2]; return sliceM[3] !== undefined ? v.slice(s, s + (+sliceM[3])) : v.slice(s); }
53
+ const defM = expr.match(/^([A-Za-z_][A-Za-z0-9_]*|\?|#|@|[0-9])(:-|:=|:\?|:\+|-|=|\+)(.*)$/s);
54
+ if (defM) {
55
+ const [, name, op, def] = defM;
56
+ const v = expandParam(name, env, argv, lastExit, arrays);
57
+ const defined = v !== '' && v != null;
58
+ if (op === ':-' || op === '-') return defined ? v : def;
59
+ if (op === ':=' || op === '=') { if (!defined) env[name] = def; return defined ? v : def; }
60
+ if (op === ':?' || op === '?') { if (!defined) throw new Error(name + ': ' + (def || 'parameter null')); return v; }
61
+ if (op === ':+' || op === '+') return defined ? def : '';
62
+ }
63
+ const sufM = expr.match(/^([A-Za-z_][A-Za-z0-9_]*|@|#)(%%?|##?)(.+)$/s);
64
+ if (sufM) {
65
+ const [, name, op, pat] = sufM;
66
+ const v = expandParam(name, env, argv, lastExit, arrays);
67
+ const bare = globReLine(pat).replace(/^\^|\$$/g, '');
68
+ if (op === '#') { const m = v.match(new RegExp('^' + bare)); return m ? v.slice(m[0].length) : v; }
69
+ if (op === '##') { const m = v.match(new RegExp('^' + bare.replace(/\.\*/g, '.*?') + '.*')); return m ? '' : v; }
70
+ if (op === '%') { const m = v.match(new RegExp(bare + '$')); return m ? v.slice(0, -m[0].length) : v; }
71
+ if (op === '%%') { const m = v.match(new RegExp('^.*' + bare + '$')); return m ? '' : v; }
72
+ }
73
+ const subM = expr.match(/^([A-Za-z_][A-Za-z0-9_]*|@)\/(\/?)(.+?)\/(.*)$/s);
74
+ if (subM) {
75
+ const [, name, all, pat, rep] = subM;
76
+ const v = expandParam(name, env, argv, lastExit, arrays);
77
+ return v.replace(new RegExp(globReLine(pat).replace(/^\^|\$$/g, ''), all ? 'g' : ''), rep);
78
+ }
79
+ return expandParam(expr, env, argv, lastExit, arrays);
80
+ }
81
+
82
+ function globReLine(pat) { return '^' + pat.replace(/[-[\]{}()+.,\\^$|#]/g, (c) => (c === '*' || c === '?') ? c : '\\' + c).replace(/\*/g, '.*').replace(/\?/g, '.') + '$'; }
83
+
84
+ export function evalArith(expr, env) {
85
+ const src = expr.replace(/\b([A-Za-z_][A-Za-z0-9_]*)\b/g, (_, n) => String(parseInt(env[n], 10) || 0));
86
+ if (!/^[-+*/%()<>=!&|\s\d?:]+$/.test(src)) return 0;
87
+ try { return Function('"use strict"; return (' + src + ')')() | 0; } catch { return 0; }
88
+ }
89
+
90
+ export function expandBraces(token) {
91
+ const listM = token.match(/^(.*?)\{([^{}]*,[^{}]*)\}(.*)$/s);
92
+ if (listM) {
93
+ const [, pre, list, post] = listM;
94
+ return list.split(',').flatMap(p => expandBraces(pre + p + post));
95
+ }
96
+ const rangeM = token.match(/^(.*?)\{(-?\d+)\.\.(-?\d+)(?:\.\.(-?\d+))?\}(.*)$/s);
97
+ if (rangeM) {
98
+ const [, pre, a, b, step, post] = rangeM;
99
+ const s = step ? +step : ((+a) <= (+b) ? 1 : -1);
100
+ const out = [];
101
+ for (let i = +a; s > 0 ? i <= +b : i >= +b; i += s) out.push(i);
102
+ return out.flatMap(i => expandBraces(pre + i + post));
103
+ }
104
+ return [token];
105
+ }
106
+
107
+ export function expandTilde(token, env) {
108
+ if (token === '~') return env.HOME || '/';
109
+ if (token.startsWith('~/')) return (env.HOME || '') + token.slice(1);
110
+ const m = token.match(/^~([A-Za-z_][A-Za-z0-9_]*)(\/.*)?$/);
111
+ if (m) return '/home/' + m[1] + (m[2] || '');
112
+ return token;
113
+ }
114
+
115
+ export function fullExpand(token, env, lastExit, argv, runCap, arrays) {
116
+ let out = '';
117
+ let i = 0;
118
+ while (i < token.length) {
119
+ if (token[i] === '`') {
120
+ const end = token.indexOf('`', i + 1);
121
+ if (end < 0) { out += token.slice(i); break; }
122
+ out += runCap ? runCap(token.slice(i + 1, end)) : '';
123
+ i = end + 1; continue;
124
+ }
125
+ if (token[i] === '$' && token[i + 1] === '(' && token[i + 2] === '(') {
126
+ const close = token.indexOf('))', i + 3);
127
+ if (close < 0) { out += token[i++]; continue; }
128
+ out += String(evalArith(token.slice(i + 3, close), env));
129
+ i = close + 2; continue;
130
+ }
131
+ if (token[i] === '$' && token[i + 1] === '(') {
132
+ const end = findMatch(token, i + 1, '(', ')');
133
+ if (end < 0) { out += token[i++]; continue; }
134
+ out += runCap ? runCap(token.slice(i + 2, end)) : '';
135
+ i = end + 1; continue;
136
+ }
137
+ if (token[i] === '$' && token[i + 1] === '{') {
138
+ const end = token.indexOf('}', i + 2);
139
+ if (end < 0) { out += token[i++]; continue; }
140
+ out += expandParamOp(token.slice(i + 2, end), env, argv, lastExit, arrays);
141
+ i = end + 1; continue;
142
+ }
143
+ if (token[i] === '$') {
144
+ const m = token.slice(i + 1).match(/^(\?|!|#|@|\*|[0-9]|[A-Za-z_][A-Za-z0-9_]*)/);
145
+ if (m) { out += expandParam(m[1], env, argv, lastExit, arrays); i += 1 + m[1].length; continue; }
146
+ }
147
+ out += token[i++];
148
+ }
149
+ return out;
150
+ }
151
+
152
+ function findMatch(s, start, open, close) {
153
+ let depth = 0; let inSingle = false, inDouble = false;
154
+ for (let i = start; i < s.length; i++) {
155
+ const c = s[i];
156
+ if (c === "'" && !inDouble) inSingle = !inSingle;
157
+ else if (c === '"' && !inSingle) inDouble = !inDouble;
158
+ else if (!inSingle && !inDouble) {
159
+ if (c === open) depth++;
160
+ else if (c === close) { depth--; if (depth === 0) return i; }
161
+ }
162
+ }
163
+ return -1;
164
+ }
@@ -0,0 +1,86 @@
1
+ export function createFdTable(ctx) {
2
+ const table = { 0: { kind: 'stdin', data: '' }, 1: { kind: 'stdout' }, 2: { kind: 'stderr' } };
3
+ ctx.fds = table;
4
+
5
+ function open(fd, source, mode) {
6
+ const n = parseInt(fd, 10);
7
+ if (isNaN(n)) throw new Error('fd: invalid: ' + fd);
8
+ table[n] = { kind: 'file', path: source, mode: mode || 'r', buf: '' };
9
+ return n;
10
+ }
11
+
12
+ function close(fd) {
13
+ const n = parseInt(fd, 10);
14
+ delete table[n];
15
+ }
16
+
17
+ function dup2(src, dst) {
18
+ const s = parseInt(src, 10);
19
+ const d = parseInt(dst, 10);
20
+ if (!table[s]) throw new Error('fd: ' + src + ': bad descriptor');
21
+ table[d] = { ...table[s], duped: s };
22
+ }
23
+
24
+ function readFd(fd) {
25
+ const n = parseInt(fd, 10);
26
+ const slot = table[n];
27
+ if (!slot) throw new Error('fd ' + fd + ' not open');
28
+ if (slot.kind === 'stdin') return slot.data || '';
29
+ if (slot.kind === 'file') {
30
+ const snap = window.__debug?.idbSnapshot || {};
31
+ return snap[slot.path.replace(/^\//, '')] || '';
32
+ }
33
+ return slot.buf || '';
34
+ }
35
+
36
+ function writeFd(fd, data) {
37
+ const n = parseInt(fd, 10);
38
+ const slot = table[n];
39
+ if (!slot) throw new Error('fd ' + fd + ' not open');
40
+ if (slot.kind === 'stdout' || n === 1) ctx.term.write(data.replace(/\n/g, '\r\n'));
41
+ else if (slot.kind === 'stderr' || n === 2) ctx.term.write('\x1b[31m' + data.replace(/\n/g, '\r\n') + '\x1b[0m');
42
+ else if (slot.kind === 'file') {
43
+ const snap = window.__debug?.idbSnapshot || (window.__debug.idbSnapshot = {});
44
+ const k = slot.path.replace(/^\//, '');
45
+ snap[k] = slot.mode === 'a' ? (snap[k] || '') + data : data;
46
+ window.__debug?.idbPersist?.();
47
+ } else { slot.buf = (slot.buf || '') + data; }
48
+ }
49
+
50
+ return { table, open, close, dup2, readFd, writeFd };
51
+ }
52
+
53
+ export function parseFdRedirects(tokens) {
54
+ const out = { args: [], redirs: [] };
55
+ for (let i = 0; i < tokens.length; i++) {
56
+ const t = tokens[i];
57
+ const m = t.match(/^(\d+)?(>>|>|<|>&|<&)(\d+)?$/);
58
+ if (m) {
59
+ const from = m[1] != null ? +m[1] : (m[2].includes('<') ? 0 : 1);
60
+ const op = m[2];
61
+ const toNum = m[3] != null ? +m[3] : null;
62
+ if (op === '>&' || op === '<&') { out.redirs.push({ kind: 'dup', fd: from, target: toNum }); continue; }
63
+ const target = tokens[++i];
64
+ out.redirs.push({ kind: op === '<' ? 'read' : 'write', fd: from, path: target, append: op === '>>' });
65
+ continue;
66
+ }
67
+ out.args.push(t);
68
+ }
69
+ return out;
70
+ }
71
+
72
+ export function makeExecBuiltin(ctx, fdTable) {
73
+ return args => {
74
+ if (!args.length) return;
75
+ for (const a of args) {
76
+ const m = a.match(/^(\d+)>(>?)(.+)$/);
77
+ if (m) { fdTable.open(m[1], m[3], m[2] === '>' ? 'a' : 'w'); continue; }
78
+ const r = a.match(/^(\d+)<(.+)$/);
79
+ if (r) { fdTable.open(r[1], r[2], 'r'); continue; }
80
+ const d = a.match(/^(\d+)>&(\d+)$/);
81
+ if (d) { fdTable.dup2(d[2], d[1]); continue; }
82
+ const c = a.match(/^(\d+)>&-$/);
83
+ if (c) { fdTable.close(c[1]); continue; }
84
+ }
85
+ };
86
+ }
@@ -0,0 +1,86 @@
1
+ import { createMachine, createActor } from './vendor/xstate.js';
2
+
3
+ const jobMachine = createMachine({
4
+ id: 'job', initial: 'running',
5
+ states: {
6
+ running: { on: { STOP: 'stopped', DONE: 'done', FAIL: 'failed', SIGNAL: { actions: 'deliverSignal' } } },
7
+ stopped: { on: { CONT: 'running', DONE: 'done', SIGNAL: { actions: 'deliverSignal' } } },
8
+ done: { type: 'final' },
9
+ failed: { type: 'final' },
10
+ },
11
+ });
12
+
13
+ export function createJobRegistry(ctx) {
14
+ ctx.bgJobs = ctx.bgJobs || {};
15
+ let nextId = 1;
16
+
17
+ function spawnJob(cmd, runPipeline) {
18
+ const id = String(nextId++);
19
+ const actor = createActor(jobMachine.provide({
20
+ actions: {
21
+ deliverSignal: (_, ev) => {
22
+ if (ev?.sig && ctx.signals) ctx.signals.raise(ev.sig);
23
+ },
24
+ },
25
+ }));
26
+ actor.start();
27
+ const job = { id, cmd, actor, done: false, stopped: false, killed: false, startedAt: Date.now() };
28
+ const p = (async () => {
29
+ try { await runPipeline(cmd); job.exit = ctx.lastExitCode; actor.send({ type: 'DONE' }); }
30
+ catch (e) { job.error = e.message; actor.send({ type: 'FAIL' }); }
31
+ finally { job.done = true; }
32
+ })();
33
+ job.promise = p;
34
+ ctx.bgJobs[id] = job;
35
+ if (ctx.swJobs) ctx.swJobs.register(id, cmd).catch(() => {});
36
+ return id;
37
+ }
38
+
39
+ function list() {
40
+ return Object.values(ctx.bgJobs).map(j => ({ id: j.id, cmd: j.cmd, state: j.actor?.getSnapshot().value || 'unknown', done: j.done, stopped: j.stopped }));
41
+ }
42
+
43
+ function resolve(ref) {
44
+ const id = ref.startsWith('%') ? ref.slice(1) : ref;
45
+ if (id === '+' || !id) { const keys = Object.keys(ctx.bgJobs); return ctx.bgJobs[keys[keys.length - 1]]; }
46
+ return ctx.bgJobs[id];
47
+ }
48
+
49
+ return { spawnJob, list, resolve };
50
+ }
51
+
52
+ export function makeJobsBuiltin(ctx, registry) {
53
+ return args => {
54
+ const long = args.includes('-l');
55
+ for (const j of registry.list()) ctx.term.write('[' + j.id + '] ' + (j.stopped ? 'Stopped' : j.done ? 'Done' : 'Running') + (long ? ' ' + j.id : '') + ' ' + j.cmd + '\r\n');
56
+ };
57
+ }
58
+
59
+ export function makeFgBuiltin(ctx, registry) {
60
+ return async args => {
61
+ const job = registry.resolve(args[0] || '+');
62
+ if (!job) { ctx.term.write('fg: no such job\r\n'); ctx.lastExitCode = 1; return; }
63
+ if (job.stopped) { job.actor.send({ type: 'CONT' }); job.stopped = false; }
64
+ ctx.currentJob = job;
65
+ try { await job.promise; } finally { ctx.currentJob = null; }
66
+ ctx.lastExitCode = job.exit ?? (job.error ? 1 : 0);
67
+ };
68
+ }
69
+
70
+ export function makeBgBuiltin(ctx, registry) {
71
+ return args => {
72
+ const job = registry.resolve(args[0] || '+');
73
+ if (!job) { ctx.term.write('bg: no such job\r\n'); ctx.lastExitCode = 1; return; }
74
+ if (job.stopped) { job.actor.send({ type: 'CONT' }); job.stopped = false; }
75
+ ctx.term.write('[' + job.id + ']+ ' + job.cmd + ' &\r\n');
76
+ };
77
+ }
78
+
79
+ export function makeDisownBuiltin(ctx) {
80
+ return args => {
81
+ for (const a of args) {
82
+ const id = a.startsWith('%') ? a.slice(1) : a;
83
+ if (ctx.bgJobs[id]) { ctx.bgJobs[id].disowned = true; delete ctx.bgJobs[id]; }
84
+ }
85
+ };
86
+ }