thebird 1.2.77 → 1.2.79
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/auto-declaudeify.yml +20 -16
- package/CHANGELOG.md +9 -0
- package/docs/node-builtins.js +170 -0
- package/docs/shell-node.js +163 -78
- package/docs/shell.js +51 -68
- package/package.json +1 -1
|
@@ -4,41 +4,45 @@ on:
|
|
|
4
4
|
push:
|
|
5
5
|
branches: ['**']
|
|
6
6
|
|
|
7
|
+
permissions:
|
|
8
|
+
contents: write
|
|
9
|
+
|
|
7
10
|
jobs:
|
|
8
11
|
declaudeify:
|
|
9
|
-
if: contains(github.event.head_commit.author.name, 'Claude') || contains(github.event.head_commit.author.email, 'claude')
|
|
10
12
|
runs-on: ubuntu-latest
|
|
11
13
|
steps:
|
|
12
14
|
- name: Checkout
|
|
13
15
|
uses: actions/checkout@v4
|
|
14
16
|
with:
|
|
15
17
|
fetch-depth: 0
|
|
18
|
+
token: ${{ secrets.GITHUB_TOKEN }}
|
|
16
19
|
|
|
17
20
|
- name: Check for Claude commits
|
|
18
21
|
id: check
|
|
19
22
|
run: |
|
|
20
23
|
CLAUDE_COMMITS=$(git log --all --author="Claude" --oneline | wc -l)
|
|
21
24
|
echo "count=$CLAUDE_COMMITS" >> $GITHUB_OUTPUT
|
|
22
|
-
if [ $CLAUDE_COMMITS -gt 0 ]; then
|
|
25
|
+
if [ "$CLAUDE_COMMITS" -gt 0 ]; then
|
|
23
26
|
echo "Found $CLAUDE_COMMITS Claude commits, will filter"
|
|
27
|
+
else
|
|
28
|
+
echo "No Claude commits found"
|
|
24
29
|
fi
|
|
25
30
|
|
|
26
31
|
- name: Filter Claude from history
|
|
27
|
-
if: steps.check.outputs.count
|
|
32
|
+
if: steps.check.outputs.count != '0'
|
|
28
33
|
run: |
|
|
29
|
-
git
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
git config user.name "lanmower"
|
|
35
|
+
git config user.email "lanmower@lanmower.com"
|
|
36
|
+
git filter-branch --force --env-filter '
|
|
37
|
+
if [ "$GIT_AUTHOR_NAME" = "Claude" ]; then
|
|
38
|
+
export GIT_AUTHOR_NAME="lanmower"
|
|
39
|
+
export GIT_AUTHOR_EMAIL="lanmower@lanmower.com"
|
|
40
|
+
fi
|
|
41
|
+
if [ "$GIT_COMMITTER_NAME" = "Claude" ]; then
|
|
42
|
+
export GIT_COMMITTER_NAME="lanmower"
|
|
43
|
+
export GIT_COMMITTER_EMAIL="lanmower@lanmower.com"
|
|
44
|
+
fi' --tag-name-filter cat -- --all
|
|
39
45
|
|
|
40
46
|
- name: Force push filtered history
|
|
41
|
-
if: steps.check.outputs.count
|
|
47
|
+
if: steps.check.outputs.count != '0'
|
|
42
48
|
run: git push --all --force
|
|
43
|
-
env:
|
|
44
|
-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
### Added
|
|
4
|
+
- `docs/node-builtins.js`: Full Node.js module polyfills — path, fs (IDB-backed), events (EventEmitter), url, querystring, Buffer class with encoding support
|
|
5
|
+
- `docs/shell-node.js`: Enhanced Node env — relative require, JSON require, per-file __dirname, process.stdout/stderr/nextTick/argv/hrtime, console.dir/table/time/assert/count, express with route params/middleware/static/json, os/util/crypto/stream modules
|
|
6
|
+
- `docs/shell.js`: Added node -e/-v flags, touch/head/tail/wc/grep/which commands, npm i alias
|
|
7
|
+
|
|
8
|
+
### Changed
|
|
9
|
+
- `docs/shell-node.js`: Rewritten to import from node-builtins.js; require() resolves relative paths, .json, directory/index.js
|
|
10
|
+
- `docs/shell.js`: Trimmed from 206L to 189L; ls shows directory entries properly
|
|
11
|
+
|
|
3
12
|
### Added
|
|
4
13
|
- `lib/errors.js`: Typed error hierarchy — BridgeError, AuthError, RateLimitError, TimeoutError, ContextWindowError, ContentPolicyError, ProviderError with classifyError factory. GeminiError kept as alias.
|
|
5
14
|
- `lib/errors.js`: `redactKeys()` — auto-redacts API keys (AIza, sk-, key- patterns) in error messages to `...XXXX`
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
const snap = () => window.__debug?.idbSnapshot || {};
|
|
2
|
+
const toKey = p => p.replace(/^\//, '');
|
|
3
|
+
const persist = () => window.__debug?.idbPersist?.();
|
|
4
|
+
const previewWrite = () => window.__debug?.shell?.onPreviewWrite?.();
|
|
5
|
+
|
|
6
|
+
export function createPath() {
|
|
7
|
+
const sep = '/';
|
|
8
|
+
const normalize = p => {
|
|
9
|
+
const parts = [];
|
|
10
|
+
for (const s of p.split('/')) {
|
|
11
|
+
if (s === '..') parts.pop();
|
|
12
|
+
else if (s && s !== '.') parts.push(s);
|
|
13
|
+
}
|
|
14
|
+
return (p.startsWith('/') ? '/' : '') + parts.join('/');
|
|
15
|
+
};
|
|
16
|
+
return {
|
|
17
|
+
sep,
|
|
18
|
+
normalize,
|
|
19
|
+
join: (...a) => normalize(a.join('/')),
|
|
20
|
+
resolve: (...a) => {
|
|
21
|
+
let r = '';
|
|
22
|
+
for (const p of a) r = p.startsWith('/') ? p : r + '/' + p;
|
|
23
|
+
return normalize(r);
|
|
24
|
+
},
|
|
25
|
+
dirname: p => { const i = p.lastIndexOf('/'); return i <= 0 ? '/' : p.slice(0, i); },
|
|
26
|
+
basename: (p, ext) => { const b = p.split('/').pop() || ''; return ext && b.endsWith(ext) ? b.slice(0, -ext.length) : b; },
|
|
27
|
+
extname: p => { const b = p.split('/').pop() || ''; const i = b.lastIndexOf('.'); return i > 0 ? b.slice(i) : ''; },
|
|
28
|
+
isAbsolute: p => p.startsWith('/'),
|
|
29
|
+
relative: (from, to) => to.replace(from.replace(/\/$/, '') + '/', ''),
|
|
30
|
+
parse: p => {
|
|
31
|
+
const dir = p.slice(0, p.lastIndexOf('/')) || '/';
|
|
32
|
+
const base = p.split('/').pop() || '';
|
|
33
|
+
const ext = base.lastIndexOf('.') > 0 ? base.slice(base.lastIndexOf('.')) : '';
|
|
34
|
+
return { root: '/', dir, base, ext, name: ext ? base.slice(0, -ext.length) : base };
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function createFs() {
|
|
40
|
+
const resolveP = p => typeof p === 'string' ? p : p.toString();
|
|
41
|
+
return {
|
|
42
|
+
readFileSync: (p, enc) => {
|
|
43
|
+
const key = toKey(resolveP(p));
|
|
44
|
+
const data = snap()[key];
|
|
45
|
+
if (data == null) throw Object.assign(new Error('ENOENT: ' + p), { code: 'ENOENT' });
|
|
46
|
+
return enc ? data : data;
|
|
47
|
+
},
|
|
48
|
+
writeFileSync: (p, data) => {
|
|
49
|
+
const s = snap();
|
|
50
|
+
s[toKey(resolveP(p))] = typeof data === 'string' ? data : String(data);
|
|
51
|
+
persist();
|
|
52
|
+
previewWrite();
|
|
53
|
+
},
|
|
54
|
+
appendFileSync: (p, data) => {
|
|
55
|
+
const key = toKey(resolveP(p));
|
|
56
|
+
const s = snap();
|
|
57
|
+
s[key] = (s[key] || '') + (typeof data === 'string' ? data : String(data));
|
|
58
|
+
persist();
|
|
59
|
+
},
|
|
60
|
+
existsSync: p => toKey(resolveP(p)) in snap(),
|
|
61
|
+
unlinkSync: p => {
|
|
62
|
+
const key = toKey(resolveP(p));
|
|
63
|
+
if (!(key in snap())) throw Object.assign(new Error('ENOENT: ' + p), { code: 'ENOENT' });
|
|
64
|
+
delete snap()[key];
|
|
65
|
+
persist();
|
|
66
|
+
},
|
|
67
|
+
mkdirSync: (p, opts) => {
|
|
68
|
+
const key = toKey(resolveP(p));
|
|
69
|
+
if (!snap()[key + '/.keep']) { snap()[key + '/.keep'] = ''; persist(); }
|
|
70
|
+
},
|
|
71
|
+
readdirSync: p => {
|
|
72
|
+
const prefix = toKey(resolveP(p));
|
|
73
|
+
const pLen = prefix ? prefix.length + 1 : 0;
|
|
74
|
+
const seen = new Set();
|
|
75
|
+
for (const k of Object.keys(snap())) {
|
|
76
|
+
if (prefix && !k.startsWith(prefix + '/') && k !== prefix) continue;
|
|
77
|
+
if (!prefix && !k.includes('/')) { seen.add(k); continue; }
|
|
78
|
+
const rest = k.slice(pLen);
|
|
79
|
+
const first = rest.split('/')[0];
|
|
80
|
+
if (first && first !== '.keep') seen.add(first);
|
|
81
|
+
}
|
|
82
|
+
return [...seen];
|
|
83
|
+
},
|
|
84
|
+
statSync: p => {
|
|
85
|
+
const key = toKey(resolveP(p));
|
|
86
|
+
const s = snap();
|
|
87
|
+
const isFile = key in s;
|
|
88
|
+
const isDir = !isFile && Object.keys(s).some(k => k.startsWith(key + '/'));
|
|
89
|
+
if (!isFile && !isDir) throw Object.assign(new Error('ENOENT: ' + p), { code: 'ENOENT' });
|
|
90
|
+
return { isFile: () => isFile, isDirectory: () => isDir, size: isFile ? (s[key]?.length || 0) : 0 };
|
|
91
|
+
},
|
|
92
|
+
renameSync: (o, n) => {
|
|
93
|
+
const s = snap();
|
|
94
|
+
const ok = toKey(resolveP(o)), nk = toKey(resolveP(n));
|
|
95
|
+
if (!(ok in s)) throw Object.assign(new Error('ENOENT: ' + o), { code: 'ENOENT' });
|
|
96
|
+
s[nk] = s[ok];
|
|
97
|
+
delete s[ok];
|
|
98
|
+
persist();
|
|
99
|
+
},
|
|
100
|
+
copyFileSync: (s, d) => {
|
|
101
|
+
const src = snap()[toKey(resolveP(s))];
|
|
102
|
+
if (src == null) throw Object.assign(new Error('ENOENT: ' + s), { code: 'ENOENT' });
|
|
103
|
+
snap()[toKey(resolveP(d))] = src;
|
|
104
|
+
persist();
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function createEvents() {
|
|
110
|
+
return class EventEmitter {
|
|
111
|
+
constructor() { this._e = {}; }
|
|
112
|
+
on(ev, fn) { (this._e[ev] = this._e[ev] || []).push(fn); return this; }
|
|
113
|
+
once(ev, fn) { const w = (...a) => { this.off(ev, w); fn(...a); }; return this.on(ev, w); }
|
|
114
|
+
off(ev, fn) { this._e[ev] = (this._e[ev] || []).filter(f => f !== fn); return this; }
|
|
115
|
+
removeListener(ev, fn) { return this.off(ev, fn); }
|
|
116
|
+
removeAllListeners(ev) { if (ev) delete this._e[ev]; else this._e = {}; return this; }
|
|
117
|
+
emit(ev, ...a) { for (const fn of (this._e[ev] || [])) fn(...a); return (this._e[ev] || []).length > 0; }
|
|
118
|
+
listeners(ev) { return (this._e[ev] || []).slice(); }
|
|
119
|
+
listenerCount(ev) { return (this._e[ev] || []).length; }
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function createUrl() {
|
|
124
|
+
return {
|
|
125
|
+
parse: s => {
|
|
126
|
+
const u = new URL(s);
|
|
127
|
+
return { protocol: u.protocol, host: u.host, hostname: u.hostname, port: u.port, pathname: u.pathname, search: u.search, query: u.search.slice(1), hash: u.hash, href: u.href };
|
|
128
|
+
},
|
|
129
|
+
format: o => {
|
|
130
|
+
const u = new URL('http://x');
|
|
131
|
+
for (const [k, v] of Object.entries(o)) { try { u[k] = v; } catch {} }
|
|
132
|
+
return u.href;
|
|
133
|
+
},
|
|
134
|
+
resolve: (from, to) => new URL(to, from).href,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function createQuerystring() {
|
|
139
|
+
return {
|
|
140
|
+
parse: s => Object.fromEntries(new URLSearchParams(s)),
|
|
141
|
+
stringify: o => new URLSearchParams(o).toString(),
|
|
142
|
+
escape: s => encodeURIComponent(s),
|
|
143
|
+
unescape: s => decodeURIComponent(s),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function createBuffer() {
|
|
148
|
+
class Buf extends Uint8Array {
|
|
149
|
+
toString(enc) {
|
|
150
|
+
if (enc === 'base64') return btoa(String.fromCharCode(...this));
|
|
151
|
+
if (enc === 'hex') return [...this].map(b => b.toString(16).padStart(2, '0')).join('');
|
|
152
|
+
return new TextDecoder().decode(this);
|
|
153
|
+
}
|
|
154
|
+
toJSON() { return { type: 'Buffer', data: [...this] }; }
|
|
155
|
+
slice(s, e) { return Buf.from(super.slice(s, e)); }
|
|
156
|
+
}
|
|
157
|
+
Buf.from = (d, enc) => {
|
|
158
|
+
if (d instanceof Uint8Array) return new Buf(d);
|
|
159
|
+
if (Array.isArray(d)) return new Buf(d);
|
|
160
|
+
if (typeof d !== 'string') return new Buf(0);
|
|
161
|
+
if (enc === 'base64') return new Buf(Uint8Array.from(atob(d), c => c.charCodeAt(0)));
|
|
162
|
+
if (enc === 'hex') return new Buf(d.match(/.{2}/g).map(h => parseInt(h, 16)));
|
|
163
|
+
return new Buf(new TextEncoder().encode(d));
|
|
164
|
+
};
|
|
165
|
+
Buf.alloc = (n, fill) => { const b = new Buf(n); if (fill) b.fill(typeof fill === 'number' ? fill : fill.charCodeAt(0)); return b; };
|
|
166
|
+
Buf.concat = list => { const t = list.reduce((s, b) => s + b.length, 0); const r = new Buf(t); let o = 0; for (const b of list) { r.set(b, o); o += b.length; } return r; };
|
|
167
|
+
Buf.isBuffer = o => o instanceof Buf;
|
|
168
|
+
Buf.byteLength = (s, enc) => Buf.from(s, enc).length;
|
|
169
|
+
return Buf;
|
|
170
|
+
}
|
package/docs/shell-node.js
CHANGED
|
@@ -1,106 +1,191 @@
|
|
|
1
|
+
import { createPath, createFs, createEvents, createUrl, createQuerystring, createBuffer } from './node-builtins.js';
|
|
2
|
+
|
|
1
3
|
function serializeRoutes(routes) {
|
|
2
4
|
const out = {};
|
|
3
|
-
for (const [method, arr] of Object.entries(routes)) {
|
|
4
|
-
out[method] = arr.map(r => ({ path: r.path }));
|
|
5
|
-
}
|
|
5
|
+
for (const [method, arr] of Object.entries(routes)) out[method] = arr.map(r => ({ path: r.path }));
|
|
6
6
|
return out;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
9
|
+
function matchRoute(pattern, path) {
|
|
10
|
+
if (pattern === '*') return {};
|
|
11
|
+
const pp = pattern.split('/'), rp = path.split('/');
|
|
12
|
+
if (pp.length !== rp.length) return null;
|
|
13
|
+
const params = {};
|
|
14
|
+
for (let i = 0; i < pp.length; i++) {
|
|
15
|
+
if (pp[i].startsWith(':')) params[pp[i].slice(1)] = rp[i];
|
|
16
|
+
else if (pp[i] !== rp[i]) return null;
|
|
17
|
+
}
|
|
18
|
+
return params;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function createExpress(term, fsmod) {
|
|
22
|
+
return () => {
|
|
23
|
+
const routes = { GET: [], POST: [], PUT: [], DELETE: [], USE: [] };
|
|
24
|
+
const middlewares = [];
|
|
25
|
+
const app = fn => middlewares.push(fn);
|
|
26
|
+
app.get = (p, ...fns) => routes.GET.push({ path: p, fns });
|
|
27
|
+
app.post = (p, ...fns) => routes.POST.push({ path: p, fns });
|
|
28
|
+
app.put = (p, ...fns) => routes.PUT.push({ path: p, fns });
|
|
29
|
+
app.delete = (p, ...fns) => routes.DELETE.push({ path: p, fns });
|
|
30
|
+
app.use = (...args) => {
|
|
31
|
+
if (typeof args[0] === 'function') middlewares.push(args[0]);
|
|
32
|
+
else routes.USE.push({ path: args[0], fn: args[1] });
|
|
33
|
+
};
|
|
34
|
+
app.listen = (port, cb) => {
|
|
35
|
+
window.__debug.shell.httpHandlers[port] = { routes, middlewares };
|
|
36
|
+
navigator.serviceWorker?.controller?.postMessage({ type: 'REGISTER_ROUTES', port, routes: serializeRoutes(routes) });
|
|
37
|
+
term.write('Express listening on :' + port + '\r\n');
|
|
38
|
+
cb?.();
|
|
39
|
+
};
|
|
40
|
+
app.json = () => (req, res, next) => {
|
|
41
|
+
if (typeof req.body === 'string') try { req.body = JSON.parse(req.body); } catch {}
|
|
42
|
+
next?.();
|
|
43
|
+
};
|
|
44
|
+
app.static = dir => (req, res) => {
|
|
45
|
+
const fp = dir.replace(/\/$/, '') + req.path;
|
|
46
|
+
try { res.send(fsmod.readFileSync(fp)); } catch { res.status(404).send('Not Found'); }
|
|
22
47
|
};
|
|
23
48
|
return app;
|
|
24
|
-
}
|
|
25
|
-
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function createSqlite() {
|
|
53
|
+
return class Database {
|
|
26
54
|
constructor(name) {
|
|
27
55
|
this._name = name;
|
|
28
|
-
if (!window.__sqlJs) throw new Error('sql.js not loaded
|
|
56
|
+
if (!window.__sqlJs) throw new Error('sql.js not loaded');
|
|
29
57
|
this._db = new window.__sqlJs.Database();
|
|
30
58
|
}
|
|
31
59
|
prepare(sql) {
|
|
32
60
|
const db = this._db;
|
|
33
61
|
return {
|
|
34
|
-
run: (...
|
|
35
|
-
get: (...
|
|
36
|
-
all: (...
|
|
62
|
+
run: (...p) => { db.run(sql, p); return { changes: 1 }; },
|
|
63
|
+
get: (...p) => { const r = db.exec(sql, p); return r[0]?.values[0] ? Object.fromEntries(r[0].columns.map((c, i) => [c, r[0].values[0][i]])) : undefined; },
|
|
64
|
+
all: (...p) => { const r = db.exec(sql, p); if (!r[0]) return []; return r[0].values.map(row => Object.fromEntries(r[0].columns.map((c, i) => [c, row[i]]))); },
|
|
37
65
|
};
|
|
38
66
|
}
|
|
39
|
-
|
|
40
|
-
}
|
|
67
|
+
close() {}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
41
70
|
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (BUILTIN_MODULES[id]) return BUILTIN_MODULES[id]();
|
|
58
|
-
const key = 'node_modules/' + id + '/index.js';
|
|
59
|
-
const src = (window.__debug.idbSnapshot || {})[key];
|
|
60
|
-
if (src == null) throw new Error('module not found: ' + id);
|
|
61
|
-
const mod = { exports: {} };
|
|
62
|
-
new Function('module', 'exports', 'require', src)(mod, mod.exports, scope.require);
|
|
63
|
-
return mod.exports;
|
|
71
|
+
function createConsole(term) {
|
|
72
|
+
const w = s => term.write(s + '\r\n');
|
|
73
|
+
const timers = {};
|
|
74
|
+
return {
|
|
75
|
+
log: (...a) => w(a.map(v => typeof v === 'object' ? JSON.stringify(v, null, 2) : String(v)).join(' ')),
|
|
76
|
+
error: (...a) => term.write('\x1b[31m' + a.map(String).join(' ') + '\x1b[0m\r\n'),
|
|
77
|
+
warn: (...a) => term.write('\x1b[33m' + a.map(String).join(' ') + '\x1b[0m\r\n'),
|
|
78
|
+
info: (...a) => w(a.map(String).join(' ')),
|
|
79
|
+
dir: (o, opts) => w(JSON.stringify(o, null, 2)),
|
|
80
|
+
table: data => {
|
|
81
|
+
if (!Array.isArray(data)) { w(JSON.stringify(data, null, 2)); return; }
|
|
82
|
+
if (!data.length) { w('(empty)'); return; }
|
|
83
|
+
const cols = Object.keys(data[0]);
|
|
84
|
+
w(cols.join('\t'));
|
|
85
|
+
for (const row of data) w(cols.map(c => String(row[c] ?? '')).join('\t'));
|
|
64
86
|
},
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
document.head.appendChild(s);
|
|
72
|
-
});
|
|
73
|
-
window.__sqlJs = await initSqlJs({ locateFile: f => './vendor/' + f });
|
|
74
|
-
return window.__sqlJs;
|
|
75
|
-
},
|
|
76
|
-
setTimeout, setInterval, clearTimeout, clearInterval, fetch,
|
|
77
|
-
Buffer: {
|
|
78
|
-
from: (s, enc) => enc === 'base64'
|
|
79
|
-
? new Uint8Array(atob(s).split('').map(c => c.charCodeAt(0)))
|
|
80
|
-
: new TextEncoder().encode(s),
|
|
81
|
-
toString: (buf, enc) => enc === 'base64'
|
|
82
|
-
? btoa(String.fromCharCode(...buf))
|
|
83
|
-
: new TextDecoder().decode(buf),
|
|
84
|
-
},
|
|
85
|
-
get __filename() { return ctx.cwd + '/repl'; },
|
|
86
|
-
get __dirname() { return ctx.cwd; },
|
|
87
|
-
http: {
|
|
88
|
-
createServer: handler => ({
|
|
89
|
-
listen: (port, cb) => {
|
|
90
|
-
window.__debug.shell.httpHandlers[port] = handler;
|
|
91
|
-
term.write('listening on :' + port + '\r\n');
|
|
92
|
-
cb?.();
|
|
93
|
-
},
|
|
94
|
-
}),
|
|
87
|
+
time: label => { timers[label || 'default'] = performance.now(); },
|
|
88
|
+
timeEnd: label => {
|
|
89
|
+
const k = label || 'default';
|
|
90
|
+
const ms = timers[k] ? (performance.now() - timers[k]).toFixed(3) : 0;
|
|
91
|
+
delete timers[k];
|
|
92
|
+
w(k + ': ' + ms + 'ms');
|
|
95
93
|
},
|
|
94
|
+
assert: (cond, ...a) => { if (!cond) term.write('\x1b[31mAssertion failed: ' + a.join(' ') + '\x1b[0m\r\n'); },
|
|
95
|
+
count: (() => { const c = {}; return label => { const k = label || 'default'; c[k] = (c[k] || 0) + 1; w(k + ': ' + c[k]); }; })(),
|
|
96
|
+
clear: () => term.clear(),
|
|
97
|
+
trace: (...a) => w('Trace: ' + a.map(String).join(' ')),
|
|
98
|
+
group: () => {},
|
|
99
|
+
groupEnd: () => {},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function createProcess(term, ctx) {
|
|
104
|
+
return {
|
|
105
|
+
argv: ['node'],
|
|
106
|
+
env: ctx.env,
|
|
107
|
+
cwd: () => ctx.cwd,
|
|
108
|
+
chdir: d => { ctx.cwd = d; },
|
|
109
|
+
exit: code => term.write('[exit ' + (code || 0) + ']\r\n'),
|
|
110
|
+
platform: 'browser',
|
|
111
|
+
version: 'v20.0.0',
|
|
112
|
+
versions: { node: '20.0.0' },
|
|
113
|
+
pid: 1,
|
|
114
|
+
nextTick: fn => Promise.resolve().then(fn),
|
|
115
|
+
stdout: { write: s => term.write(String(s)) },
|
|
116
|
+
stderr: { write: s => term.write('\x1b[31m' + String(s) + '\x1b[0m') },
|
|
117
|
+
stdin: { on: () => {} },
|
|
118
|
+
on: () => {},
|
|
119
|
+
off: () => {},
|
|
120
|
+
hrtime: { bigint: () => BigInt(Math.round(performance.now() * 1e6)) },
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function createNodeEnv({ ctx, term }) {
|
|
125
|
+
const pathmod = createPath();
|
|
126
|
+
const fsmod = createFs();
|
|
127
|
+
const Buf = createBuffer();
|
|
128
|
+
const MODULES = {
|
|
129
|
+
path: () => pathmod,
|
|
130
|
+
fs: () => fsmod,
|
|
131
|
+
events: () => createEvents(),
|
|
132
|
+
url: () => createUrl(),
|
|
133
|
+
querystring: () => createQuerystring(),
|
|
134
|
+
os: () => ({ platform: () => 'browser', homedir: () => '/', tmpdir: () => '/tmp', cpus: () => [{}], totalmem: () => 1073741824, freemem: () => 536870912, hostname: () => 'thebird', EOL: '\n' }),
|
|
135
|
+
util: () => ({ format: (...a) => a.join(' '), inspect: o => JSON.stringify(o, null, 2), promisify: fn => (...a) => new Promise((r, j) => fn(...a, (e, v) => e ? j(e) : r(v))), types: { isPromise: p => p instanceof Promise } }),
|
|
136
|
+
crypto: () => ({ randomBytes: n => Buf.from(Array.from({ length: n }, () => Math.random() * 256 | 0)), randomUUID: () => crypto.randomUUID(), createHash: () => ({ update: () => ({ digest: () => 'stub' }) }) }),
|
|
137
|
+
stream: () => ({ Readable: createEvents(), Writable: createEvents(), Transform: createEvents(), pipeline: (...a) => a.pop()(null) }),
|
|
138
|
+
express: createExpress(term, fsmod),
|
|
139
|
+
'better-sqlite3': createSqlite,
|
|
96
140
|
};
|
|
97
141
|
|
|
98
|
-
|
|
142
|
+
const cons = createConsole(term);
|
|
143
|
+
const proc = createProcess(term, ctx);
|
|
144
|
+
|
|
145
|
+
function makeRequire(dir) {
|
|
146
|
+
return function require(id) {
|
|
147
|
+
if (MODULES[id]) return MODULES[id]();
|
|
148
|
+
const candidates = id.startsWith('.') ? [
|
|
149
|
+
pathmod.resolve(dir, id) + '.js',
|
|
150
|
+
pathmod.resolve(dir, id),
|
|
151
|
+
pathmod.resolve(dir, id) + '/index.js',
|
|
152
|
+
pathmod.resolve(dir, id, 'index.js'),
|
|
153
|
+
] : ['node_modules/' + id + '/index.js'];
|
|
154
|
+
const s = snap();
|
|
155
|
+
for (const c of candidates) {
|
|
156
|
+
const key = c.replace(/^\//, '');
|
|
157
|
+
if (key in s) {
|
|
158
|
+
if (key.endsWith('.json')) return JSON.parse(s[key]);
|
|
159
|
+
const mod = { exports: {} };
|
|
160
|
+
const modDir = pathmod.dirname('/' + key);
|
|
161
|
+
new Function('module', 'exports', 'require', '__filename', '__dirname', 'process', 'console', 'Buffer', 'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval', 'fetch', s[key])(mod, mod.exports, makeRequire(modDir), '/' + key, modDir, proc, cons, Buf, setTimeout, setInterval, clearTimeout, clearInterval, fetch);
|
|
162
|
+
return mod.exports;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
throw new Error('Cannot find module: ' + id);
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const snap = () => window.__debug?.idbSnapshot || {};
|
|
170
|
+
|
|
171
|
+
async function loadSql() {
|
|
172
|
+
if (window.__sqlJs) return window.__sqlJs;
|
|
173
|
+
await new Promise((res, rej) => { const s = document.createElement('script'); s.src = './vendor/sql-wasm.js'; s.onload = res; s.onerror = rej; document.head.appendChild(s); });
|
|
174
|
+
window.__sqlJs = await initSqlJs({ locateFile: f => './vendor/' + f });
|
|
175
|
+
return window.__sqlJs;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return async function nodeEval(code, filename, argv) {
|
|
179
|
+
const dir = filename ? pathmod.dirname(filename) : ctx.cwd;
|
|
180
|
+
const fpath = filename || ctx.cwd + '/repl';
|
|
181
|
+
proc.argv = ['node', fpath, ...(argv || [])];
|
|
182
|
+
const scope = { process: proc, console: cons, require: makeRequire(dir), Buffer: Buf, __filename: fpath, __dirname: dir, setTimeout, setInterval, clearTimeout, clearInterval, fetch, loadSql, module: { exports: {} }, exports: {} };
|
|
99
183
|
try {
|
|
100
184
|
const keys = Object.keys(scope);
|
|
101
185
|
const vals = Object.values(scope);
|
|
102
186
|
const fn = new Function(...keys, 'return (async () => {\n' + code + '\n})()');
|
|
103
|
-
await fn(...vals);
|
|
187
|
+
const result = await fn(...vals);
|
|
188
|
+
if (result !== undefined && !filename) cons.log(result);
|
|
104
189
|
} catch (e) {
|
|
105
190
|
term.write('\x1b[31m' + (filename ? filename + ': ' : '') + e.message + '\x1b[0m\r\n');
|
|
106
191
|
}
|
package/docs/shell.js
CHANGED
|
@@ -21,9 +21,17 @@ function makeBuiltins(ctx) {
|
|
|
21
21
|
const wl = s => w(s + '\r\n');
|
|
22
22
|
return {
|
|
23
23
|
ls: ([p]) => {
|
|
24
|
-
const prefix = toKey(resolvePath(ctx.cwd, p || ''))
|
|
25
|
-
const
|
|
26
|
-
|
|
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)');
|
|
27
35
|
},
|
|
28
36
|
cat: ([f]) => {
|
|
29
37
|
const c = snap()[toKey(resolvePath(ctx.cwd, f))];
|
|
@@ -33,50 +41,49 @@ function makeBuiltins(ctx) {
|
|
|
33
41
|
echo: args => wl(args.join(' ')),
|
|
34
42
|
pwd: () => wl(ctx.cwd),
|
|
35
43
|
cd: ([p]) => { ctx.cwd = resolvePath(ctx.cwd, p || '~'); },
|
|
36
|
-
mkdir: ([p]) => {
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
},
|
|
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?.(); },
|
|
48
47
|
mv: ([s, d]) => {
|
|
49
48
|
const src = toKey(resolvePath(ctx.cwd, s)), dst = toKey(resolvePath(ctx.cwd, d));
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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)');
|
|
53
60
|
},
|
|
54
61
|
env: () => wl(Object.entries(ctx.env).map(([k, v]) => k + '=' + v).join('\r\n')),
|
|
55
62
|
export: ([kv]) => { const [k, ...v] = (kv || '').split('='); ctx.env[k] = v.join('='); },
|
|
56
63
|
clear: () => ctx.term.clear(),
|
|
57
64
|
help: () => wl(Object.keys(makeBuiltins(ctx)).join(' ')),
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (
|
|
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]'); }
|
|
61
68
|
},
|
|
62
|
-
node: async (
|
|
63
|
-
if (!
|
|
64
|
-
|
|
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]);
|
|
65
74
|
const code = snap()[toKey(path)];
|
|
66
75
|
if (code == null) throw new Error('no such file: ' + path);
|
|
67
76
|
actor.send({ type: 'NODE_START' });
|
|
68
|
-
await ctx.nodeEval(code, path);
|
|
77
|
+
await ctx.nodeEval(code, path, args.slice(1));
|
|
69
78
|
},
|
|
70
|
-
npm: async (args
|
|
71
|
-
if (args[0] !== 'install') throw new Error('only npm install supported');
|
|
79
|
+
npm: async (args) => {
|
|
80
|
+
if (args[0] !== 'install' && args[0] !== 'i') throw new Error('only npm install supported');
|
|
72
81
|
const pkg = args[1];
|
|
73
82
|
if (!pkg) throw new Error('npm install <pkg>');
|
|
74
|
-
actor.send({ type: 'NPM_START' });
|
|
75
83
|
w('fetching ' + pkg + '...\r\n');
|
|
76
84
|
const r = await fetch('https://esm.sh/' + pkg);
|
|
77
85
|
if (!r.ok) throw new Error('fetch failed: ' + r.status);
|
|
78
|
-
|
|
79
|
-
window.__debug.idbSnapshot[key] = await r.text();
|
|
86
|
+
snap()['node_modules/' + pkg + '/index.js'] = await r.text();
|
|
80
87
|
window.__debug.idbPersist?.();
|
|
81
88
|
wl('installed ' + pkg);
|
|
82
89
|
},
|
|
@@ -84,15 +91,14 @@ function makeBuiltins(ctx) {
|
|
|
84
91
|
}
|
|
85
92
|
|
|
86
93
|
const machine = createMachine({ id: 'shell', initial: 'idle', states: {
|
|
87
|
-
idle: { on: { RUN: 'executing', ENTER_REPL: 'node-repl',
|
|
94
|
+
idle: { on: { RUN: 'executing', ENTER_REPL: 'node-repl', NODE_START: 'node-running' } },
|
|
88
95
|
executing: { on: { DONE: 'idle', ERROR: 'idle' } },
|
|
89
|
-
'npm-installing': { on: { DONE: 'idle', ERROR: 'idle' } },
|
|
90
96
|
'node-running': { on: { DONE: 'idle', ERROR: 'idle' } },
|
|
91
97
|
'node-repl': { on: { EXIT_REPL: 'idle', RUN: 'node-repl' } },
|
|
92
98
|
}});
|
|
93
99
|
|
|
94
100
|
export function createShell({ term, onPreviewWrite }) {
|
|
95
|
-
const ctx = { term, cwd: '/', env: {}, history: []
|
|
101
|
+
const ctx = { term, cwd: '/', env: {}, history: [] };
|
|
96
102
|
const BUILTINS = makeBuiltins(ctx);
|
|
97
103
|
ctx.nodeEval = createNodeEnv({ ctx, term });
|
|
98
104
|
|
|
@@ -100,12 +106,7 @@ export function createShell({ term, onPreviewWrite }) {
|
|
|
100
106
|
actor.start();
|
|
101
107
|
|
|
102
108
|
let inputQueue = [];
|
|
103
|
-
|
|
104
|
-
function drainQueue(onData) {
|
|
105
|
-
const items = inputQueue.slice();
|
|
106
|
-
inputQueue = [];
|
|
107
|
-
for (const d of items) onData(d);
|
|
108
|
-
}
|
|
109
|
+
function drainQueue(onData) { const items = inputQueue.slice(); inputQueue = []; for (const d of items) onData(d); }
|
|
109
110
|
|
|
110
111
|
window.__debug = window.__debug || {};
|
|
111
112
|
window.__debug.shell = {
|
|
@@ -113,11 +114,11 @@ export function createShell({ term, onPreviewWrite }) {
|
|
|
113
114
|
get cwd() { return ctx.cwd; },
|
|
114
115
|
get env() { return ctx.env; },
|
|
115
116
|
get history() { return ctx.history; },
|
|
116
|
-
httpHandlers:
|
|
117
|
+
httpHandlers: {},
|
|
117
118
|
get inputQueue() { return inputQueue.slice(); },
|
|
118
119
|
};
|
|
119
120
|
|
|
120
|
-
async function runCmd(line, capture
|
|
121
|
+
async function runCmd(line, capture) {
|
|
121
122
|
if (!line.trim()) return '';
|
|
122
123
|
const [cmd, ...args] = line.trim().split(/\s+/);
|
|
123
124
|
const fn = BUILTINS[cmd];
|
|
@@ -128,7 +129,7 @@ export function createShell({ term, onPreviewWrite }) {
|
|
|
128
129
|
let out = '';
|
|
129
130
|
const orig = term.write.bind(term);
|
|
130
131
|
term.write = s => { out += s; };
|
|
131
|
-
try { if (fn) await fn(args, actor); else out += 'command not found: ' + cmd
|
|
132
|
+
try { if (fn) await fn(args, actor); else out += 'command not found: ' + cmd; }
|
|
132
133
|
finally { term.write = orig; }
|
|
133
134
|
return out;
|
|
134
135
|
}
|
|
@@ -137,13 +138,13 @@ export function createShell({ term, onPreviewWrite }) {
|
|
|
137
138
|
if (!line.trim()) return;
|
|
138
139
|
const st = actor.getSnapshot().value;
|
|
139
140
|
if (st === 'node-repl' && line.trim() !== 'exit') { await ctx.nodeEval(line); return; }
|
|
140
|
-
if (line.trim() === 'exit') { BUILTINS.exit(actor); return; }
|
|
141
|
+
if (line.trim() === 'exit') { BUILTINS.exit([], actor); return; }
|
|
141
142
|
const [cmd] = line.trim().split(/\s+/);
|
|
142
143
|
if (cmd !== 'npm' && cmd !== 'node') actor.send({ type: 'RUN' });
|
|
143
144
|
try {
|
|
144
145
|
const parts = line.split(' | ');
|
|
145
146
|
if (parts.length > 1) {
|
|
146
|
-
let buf = await runCmd(parts[0], true
|
|
147
|
+
let buf = await runCmd(parts[0], true);
|
|
147
148
|
for (const p of parts.slice(1)) {
|
|
148
149
|
const [c, ...a] = p.trim().split(/\s+/);
|
|
149
150
|
const fn = BUILTINS[c];
|
|
@@ -151,7 +152,7 @@ export function createShell({ term, onPreviewWrite }) {
|
|
|
151
152
|
buf = '';
|
|
152
153
|
}
|
|
153
154
|
} else {
|
|
154
|
-
await runCmd(line, false
|
|
155
|
+
await runCmd(line, false);
|
|
155
156
|
}
|
|
156
157
|
actor.send({ type: 'DONE' });
|
|
157
158
|
drainQueue(onData);
|
|
@@ -163,36 +164,18 @@ export function createShell({ term, onPreviewWrite }) {
|
|
|
163
164
|
}
|
|
164
165
|
|
|
165
166
|
function getCompletions(line, word) {
|
|
166
|
-
const
|
|
167
|
-
const files = Object.keys(snap);
|
|
167
|
+
const files = Object.keys(window.__debug.idbSnapshot || {});
|
|
168
168
|
const tokens = line.trim().split(/\s+/);
|
|
169
|
-
if (tokens.length <= 1 && !line.includes(' '))
|
|
170
|
-
const cmds = Object.keys(BUILTINS);
|
|
171
|
-
return cmds.filter(c => c.startsWith(word));
|
|
172
|
-
}
|
|
169
|
+
if (tokens.length <= 1 && !line.includes(' ')) return Object.keys(BUILTINS).filter(c => c.startsWith(word));
|
|
173
170
|
return files.filter(f => f.startsWith(word));
|
|
174
171
|
}
|
|
175
172
|
|
|
176
|
-
const rl = createReadline({
|
|
177
|
-
term,
|
|
178
|
-
getCompletions,
|
|
179
|
-
getPrompt: () => ctx.cwd,
|
|
180
|
-
onLine: line => run(line, onData).then(() => rl.showPrompt()),
|
|
181
|
-
});
|
|
173
|
+
const rl = createReadline({ term, getCompletions, getPrompt: () => ctx.cwd, onLine: line => run(line, onData).then(() => rl.showPrompt()) });
|
|
182
174
|
|
|
183
175
|
function onData(data) {
|
|
184
|
-
if (data === '\x03') {
|
|
185
|
-
actor.send({ type: 'ERROR' });
|
|
186
|
-
inputQueue = [];
|
|
187
|
-
term.write('^C');
|
|
188
|
-
rl.showPrompt();
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
176
|
+
if (data === '\x03') { actor.send({ type: 'ERROR' }); inputQueue = []; term.write('^C'); rl.showPrompt(); return; }
|
|
191
177
|
const st = actor.getSnapshot().value;
|
|
192
|
-
if (st !== 'idle' && st !== 'node-repl') {
|
|
193
|
-
inputQueue.push(data);
|
|
194
|
-
return;
|
|
195
|
-
}
|
|
178
|
+
if (st !== 'idle' && st !== 'node-repl') { inputQueue.push(data); return; }
|
|
196
179
|
rl.onData(data);
|
|
197
180
|
}
|
|
198
181
|
|
package/package.json
CHANGED