|
// Minimal Bun-API shim for running Bun-compiled claude under Node.js. |
|
// Loaded with `node -r ./bun-shim.js launch.cjs ...` (or required from a wrapper). |
|
// Only fills the calls cli.js actually makes. Add more as we hit ReferenceErrors. |
|
|
|
'use strict'; |
|
|
|
const child_process = require('child_process'); |
|
const fs_module = require('fs'); |
|
const fsp = require('fs/promises'); |
|
const path_module = require('path'); |
|
const os = require('os'); |
|
const net = require('net'); |
|
const { Readable } = require('stream'); |
|
|
|
if (typeof globalThis.Bun !== 'undefined') { |
|
module.exports = globalThis.Bun; |
|
} else (function installBunShim() { |
|
|
|
// --- Virtual module registry: claude's cli.js was bundled by bun and |
|
// require()s a few npm packages that don't have node-builtin equivalents. |
|
// We provide tiny stubs so the require() succeeds; full functionality only |
|
// matters for the specific features (MCP websocket, undici HTTP) that the |
|
// stubs cover. --- |
|
const Module = require('module'); |
|
const VIRTUAL = new Map(); |
|
|
|
function virtualModule(name, factory) { |
|
VIRTUAL.set(name, factory); |
|
} |
|
|
|
const origResolve = Module._resolveFilename; |
|
Module._resolveFilename = function (req, parent, ...rest) { |
|
if (VIRTUAL.has(req)) return req; |
|
return origResolve.call(this, req, parent, ...rest); |
|
}; |
|
const origLoad = Module._load; |
|
Module._load = function (req, parent, ...rest) { |
|
if (VIRTUAL.has(req)) { |
|
const cached = Module._cache[req]; |
|
if (cached) return cached.exports; |
|
const m = new Module(req); |
|
m.filename = req; |
|
m.loaded = true; |
|
Module._cache[req] = m; |
|
try { m.exports = VIRTUAL.get(req)(); } catch (e) { delete Module._cache[req]; throw e; } |
|
return m.exports; |
|
} |
|
return origLoad.call(this, req, parent, ...rest); |
|
}; |
|
|
|
// 'ws' — used for MCP websocket connections. Load the real package from a |
|
// vendored copy sitting next to this shim. Pure JS, ~130KB, no native deps. |
|
virtualModule('ws', () => { |
|
const path = require('path'); |
|
return require(path.join(__dirname, 'vendor', 'ws')); |
|
}); |
|
|
|
// 'undici' — claude uses for some HTTP calls; node has globalThis.fetch |
|
// natively. Map undici's most-used exports to node primitives. |
|
virtualModule('undici', () => { |
|
const u = {}; |
|
u.fetch = globalThis.fetch; |
|
u.Headers = globalThis.Headers; |
|
u.Request = globalThis.Request; |
|
u.Response = globalThis.Response; |
|
u.FormData = globalThis.FormData; |
|
u.Agent = class Agent { constructor(opts) { this.opts = opts; } }; |
|
u.ProxyAgent = class ProxyAgent { constructor(opts) { this.opts = opts; } }; |
|
u.setGlobalDispatcher = () => {}; |
|
u.getGlobalDispatcher = () => null; |
|
u.errors = { UndiciError: class UndiciError extends Error {} }; |
|
return u; |
|
}); |
|
|
|
|
|
// --- ANSI strip first (string width depends on it) --- |
|
const ANSI_RE = /[][[\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\d/#&.:=?%@~_]+)*|[a-zA-Z\d]+(?:;[-a-zA-Z\d/#&.:=?%@~_]*)*)?)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PRZcf-nq-uy=><~]))/g; |
|
function stripAnsi(s) { return String(s ?? '').replace(ANSI_RE, ''); } |
|
|
|
// --- string width (East Asian Width — coarse, no deps) --- |
|
function stringWidthImpl(s) { |
|
s = stripAnsi(String(s ?? '')); |
|
let w = 0; |
|
for (const ch of s) { |
|
const c = ch.codePointAt(0); |
|
if (c == null) continue; |
|
if (c < 32 || c === 127) continue; |
|
if ((c >= 0x1100 && c <= 0x115F) || |
|
(c >= 0x2E80 && c <= 0x303E) || |
|
(c >= 0x3041 && c <= 0x33FF) || |
|
(c >= 0x3400 && c <= 0x4DBF) || |
|
(c >= 0x4E00 && c <= 0x9FFF) || |
|
(c >= 0xA000 && c <= 0xA4CF) || |
|
(c >= 0xAC00 && c <= 0xD7A3) || |
|
(c >= 0xF900 && c <= 0xFAFF) || |
|
(c >= 0xFE30 && c <= 0xFE4F) || |
|
(c >= 0xFF00 && c <= 0xFF60) || |
|
(c >= 0xFFE0 && c <= 0xFFE6) || |
|
(c >= 0x20000 && c <= 0x2FFFD) || |
|
(c >= 0x30000 && c <= 0x3FFFD)) w += 2; |
|
else w += 1; |
|
} |
|
return w; |
|
} |
|
|
|
function wrapAnsi(s, columns = 80, opts = {}) { |
|
const text = String(s ?? ''); |
|
if (columns <= 0) return text; |
|
const out = []; |
|
for (const line of text.split('\n')) { |
|
if (stringWidthImpl(line) <= columns) { out.push(line); continue; } |
|
let buf = '', w = 0; |
|
for (const ch of line) { |
|
const cw = stringWidthImpl(ch); |
|
if (w + cw > columns) { out.push(buf); buf = ''; w = 0; } |
|
buf += ch; w += cw; |
|
} |
|
if (buf) out.push(buf); |
|
} |
|
return out.join('\n'); |
|
} |
|
|
|
// --- spawn (sync + async) --- |
|
function bunSpawn(arg1, arg2) { |
|
// bun.spawn supports two signatures: spawn({cmd, ...}) and spawn(cmdArr, opts) |
|
let cmd, opts; |
|
if (Array.isArray(arg1)) { cmd = arg1; opts = arg2 || {}; } |
|
else { cmd = arg1.cmd; opts = arg1; } |
|
const file = cmd[0]; |
|
const args = cmd.slice(1); |
|
const stdio = [ |
|
opts.stdin === 'inherit' ? 'inherit' : (opts.stdin === 'ignore' ? 'ignore' : 'pipe'), |
|
opts.stdout === 'inherit' ? 'inherit' : (opts.stdout === 'ignore' ? 'ignore' : 'pipe'), |
|
opts.stderr === 'inherit' ? 'inherit' : (opts.stderr === 'ignore' ? 'ignore' : 'pipe'), |
|
]; |
|
const child = child_process.spawn(file, args, { |
|
cwd: opts.cwd, |
|
env: opts.env || process.env, |
|
stdio, |
|
}); |
|
// Bun returns a Subprocess object with .exited (Promise), .pid, .stdin, .stdout, .stderr, .kill() |
|
const result = { |
|
pid: child.pid, |
|
stdin: child.stdin, |
|
stdout: child.stdout, |
|
stderr: child.stderr, |
|
kill: (sig) => child.kill(sig), |
|
exited: new Promise((res) => child.on('exit', (code) => res(code ?? 0))), |
|
exitCode: null, |
|
killed: false, |
|
}; |
|
child.on('exit', (code) => { result.exitCode = code ?? 0; }); |
|
return result; |
|
} |
|
|
|
function bunSpawnSync(arg1, arg2) { |
|
let cmd, opts; |
|
if (Array.isArray(arg1)) { cmd = arg1; opts = arg2 || {}; } |
|
else { cmd = arg1.cmd; opts = arg1; } |
|
const r = child_process.spawnSync(cmd[0], cmd.slice(1), { |
|
cwd: opts.cwd, |
|
env: opts.env || process.env, |
|
input: opts.stdin && opts.stdin !== 'inherit' && opts.stdin !== 'pipe' && opts.stdin !== 'ignore' ? opts.stdin : undefined, |
|
}); |
|
return { |
|
pid: r.pid, |
|
stdout: r.stdout ?? Buffer.alloc(0), |
|
stderr: r.stderr ?? Buffer.alloc(0), |
|
exitCode: r.status ?? 0, |
|
success: r.status === 0, |
|
signal: r.signal, |
|
}; |
|
} |
|
|
|
// --- which --- |
|
function which(cmd, opts = {}) { |
|
const PATH = (opts.PATH ?? process.env.PATH ?? '').split(path_module.delimiter); |
|
const exts = process.platform === 'win32' ? (process.env.PATHEXT || '').split(';') : ['']; |
|
for (const dir of PATH) { |
|
if (!dir) continue; |
|
for (const ext of exts) { |
|
const p = path_module.join(dir, cmd + ext); |
|
try { |
|
const st = fs_module.statSync(p); |
|
if (st.isFile()) return p; |
|
} catch {} |
|
} |
|
} |
|
return null; |
|
} |
|
|
|
// --- listen (TCP server, Bun.listen / Bun.connect style) --- |
|
function bunListen(opts) { |
|
const { hostname = '0.0.0.0', port, socket: handlers = {} } = opts; |
|
const server = net.createServer((sock) => { |
|
const ctx = sock; // pass the raw net.Socket as "socket" arg |
|
try { handlers.open && handlers.open(ctx); } catch (e) { handlers.error && handlers.error(ctx, e); } |
|
sock.on('data', (chunk) => { try { handlers.data && handlers.data(ctx, chunk); } catch (e) { handlers.error && handlers.error(ctx, e); } }); |
|
sock.on('close', () => { try { handlers.close && handlers.close(ctx); } catch {} }); |
|
sock.on('error', (e) => { try { handlers.error && handlers.error(ctx, e); } catch {} }); |
|
}); |
|
server.listen(port, hostname); |
|
return { |
|
hostname, port, |
|
stop: () => server.close(), |
|
[Symbol.dispose]: () => server.close(), |
|
}; |
|
} |
|
|
|
// --- hash (Bun.hash(input) -> bigint; algorithm-specific variants) --- |
|
const crypto = require('crypto'); |
|
function bunHash(input, seed) { |
|
// wyhash equivalent — we'll just use sha256 trimmed to u64 (claude only uses for caching keys) |
|
const h = crypto.createHash('sha256'); |
|
if (seed !== undefined) h.update(String(seed)); |
|
h.update(typeof input === 'string' ? input : Buffer.from(input)); |
|
const buf = h.digest(); |
|
return buf.readBigUInt64LE(0); |
|
} |
|
bunHash.wyhash = bunHash; |
|
|
|
// --- Transpiler (TS/JSX) — claude code internal eval of user .ts? Stub to identity for .js, error for .ts --- |
|
class BunTranspiler { |
|
constructor(opts = {}) { this.opts = opts; } |
|
transformSync(code) { return String(code); } |
|
transform(code) { return Promise.resolve(String(code)); } |
|
scan() { return { imports: [], exports: [] }; } |
|
scanImports() { return []; } |
|
} |
|
|
|
// --- YAML / JSONL — claude uses for streamed responses and config? minimal stubs --- |
|
// YAML stub: claude likely uses for tool descriptions / config; JSON fallback works for JSON-shaped input. |
|
const YAML = { |
|
parse: (s) => { throw new Error('Bun.YAML.parse: no yaml impl in shim (call site=' + (new Error().stack.split('\n')[2] || '?') + ')'); }, |
|
stringify: (o) => JSON.stringify(o, null, 2), |
|
}; |
|
|
|
const JSONL = { |
|
parse: (s) => String(s ?? '').split('\n').filter(Boolean).map((line) => JSON.parse(line)), |
|
stringify: (arr) => (Array.isArray(arr) ? arr : [arr]).map((o) => JSON.stringify(o)).join('\n') + '\n', |
|
}; |
|
|
|
// --- semver (minimal, no deps; sufficient for x.y.z >= a.b.c comparisons) --- |
|
function parseSemver(v) { |
|
const m = String(v).match(/^v?(\d+)\.(\d+)\.(\d+)/); |
|
return m ? [+m[1], +m[2], +m[3]] : [0, 0, 0]; |
|
} |
|
function cmpSemver(a, b) { |
|
const [a1,a2,a3] = parseSemver(a), [b1,b2,b3] = parseSemver(b); |
|
return a1!==b1 ? a1-b1 : a2!==b2 ? a2-b2 : a3-b3; |
|
} |
|
const semver = { |
|
satisfies: (v, r) => { |
|
// Very loose: support ">=x.y.z", "x.y.z", "*" |
|
if (r === '*' || !r) return true; |
|
const m = String(r).match(/^([<>=!~^]*)\s*(.+)$/); |
|
if (!m) return true; |
|
const c = cmpSemver(v, m[2]); |
|
const op = m[1] || '='; |
|
if (op.includes('=') && c === 0) return true; |
|
if (op.includes('>') && c > 0) return true; |
|
if (op.includes('<') && c < 0) return true; |
|
return false; |
|
}, |
|
order: cmpSemver, |
|
}; |
|
|
|
// --- Terminal — stdin/stdout helpers (used for raw mode etc) --- |
|
class BunTerminal { |
|
constructor() {} |
|
// Minimal: just expose process.stdin/stdout |
|
get input() { return process.stdin; } |
|
get output() { return process.stdout; } |
|
} |
|
|
|
// --- stdin (Bun.stdin is a Blob-like for the program's stdin) --- |
|
const bunStdin = { |
|
stream: () => Readable.toWeb ? Readable.toWeb(process.stdin) : process.stdin, |
|
text: async () => { |
|
const chunks = []; |
|
for await (const chunk of process.stdin) chunks.push(chunk); |
|
return Buffer.concat(chunks).toString('utf8'); |
|
}, |
|
arrayBuffer: async () => { |
|
const chunks = []; |
|
for await (const chunk of process.stdin) chunks.push(chunk); |
|
const buf = Buffer.concat(chunks); |
|
return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); |
|
}, |
|
json: async () => JSON.parse(await bunStdin.text()), |
|
}; |
|
|
|
// --- generateHeapSnapshot / gc --- |
|
function generateHeapSnapshot() { return { type: 'heap-snapshot', nodes: [], edges: [] }; } |
|
function bunGc(sync) { |
|
if (global.gc) global.gc(); |
|
return 0; |
|
} |
|
|
|
// --- embeddedFiles: claude uses this to detect "native bun standalone" vs |
|
// "npm install". Empty = looks like npm and prints a deprecation warning. |
|
// Provide a placeholder entry so claude treats us as the native build. --- |
|
const embeddedFiles = [{ name: 'cli.js', size: 0 }]; |
|
|
|
// --- Assemble Bun global --- |
|
const Bun = { |
|
version: '1.3.10', // pretend |
|
revision: 'bun-shim', |
|
argv: process.argv, |
|
env: process.env, |
|
main: require.main && require.main.filename, |
|
// strings |
|
stringWidth: stringWidthImpl, |
|
stripANSI: stripAnsi, |
|
wrapAnsi: wrapAnsi, |
|
// process |
|
spawn: bunSpawn, |
|
spawnSync: bunSpawnSync, |
|
which, |
|
// network |
|
listen: bunListen, |
|
// hashing |
|
hash: bunHash, |
|
// parsers |
|
Transpiler: BunTranspiler, |
|
YAML, |
|
JSONL, |
|
semver, |
|
// I/O |
|
Terminal: BunTerminal, |
|
stdin: bunStdin, |
|
embeddedFiles, |
|
// debug |
|
gc: bunGc, |
|
generateHeapSnapshot, |
|
// misc |
|
nanoseconds: () => Number(process.hrtime.bigint()), |
|
sleep: (ms) => new Promise((r) => setTimeout(r, ms)), |
|
sleepSync: (ms) => { const end = Date.now() + ms; while (Date.now() < end) {} }, |
|
inspect: (v) => require('util').inspect(v), |
|
fileURLToPath: (u) => new URL(u).pathname, |
|
pathToFileURL: (p) => 'file://' + p, |
|
}; |
|
|
|
globalThis.Bun = Bun; |
|
module.exports = Bun; |
|
})(); |