diff --git a/config/claude-native-audited-versions.json b/config/claude-native-audited-versions.json index 30d7b00..7000e01 100644 --- a/config/claude-native-audited-versions.json +++ b/config/claude-native-audited-versions.json @@ -617,6 +617,15 @@ "tarball_integrity": "sha512-EXSediF1ujcqQeElbXdCEe+uW3NUQAOjn1xHHAgvwzY08nzJeePjagn9+YS6bmhHxBgDKwlGu7pL8Kv7EpiFtg==", "tarball_sha256": "1bb3c8364d652dd08a15856ee27c6600ecd1e45360243154fa846f240ce7a1df", "status": "termux_verified" + }, + "2.1.222-1": { + "wrapper_spec": "@anthropic-ai/claude-code@2.1.222", + "native_spec": "@anthropic-ai/claude-code-linux-arm64@2.1.222", + "entry_js_offset": 256895476, + "entry_end_offset": 279849039, + "tarball_integrity": "sha512-EXSediF1ujcqQeElbXdCEe+uW3NUQAOjn1xHHAgvwzY08nzJeePjagn9+YS6bmhHxBgDKwlGu7pL8Kv7EpiFtg==", + "tarball_sha256": "1bb3c8364d652dd08a15856ee27c6600ecd1e45360243154fa846f240ce7a1df", + "status": "offset_discovered" } } } diff --git a/config/claude-termux-release-manifest.json b/config/claude-termux-release-manifest.json index af43c35..1d8f045 100644 --- a/config/claude-termux-release-manifest.json +++ b/config/claude-termux-release-manifest.json @@ -2,7 +2,7 @@ "manifest_version": 1, "package_name": "@bash0816/claude-code", "latest_audited_version": "2.1.222", - "latest_candidate_version": "2.1.222", + "latest_candidate_version": "2.1.222-1", "previous_stable_version": "2.1.220-2", "stable_pinned_version": "2.1.193", "manifest_url": "https://raw.githubusercontent.com/bash0816/ClaudeCode-Termux/main/config/claude-termux-release-manifest.json" diff --git a/packages/claude-code/config/claude-native-audited-versions.json b/packages/claude-code/config/claude-native-audited-versions.json index 30d7b00..7000e01 100644 --- a/packages/claude-code/config/claude-native-audited-versions.json +++ b/packages/claude-code/config/claude-native-audited-versions.json @@ -617,6 +617,15 @@ "tarball_integrity": "sha512-EXSediF1ujcqQeElbXdCEe+uW3NUQAOjn1xHHAgvwzY08nzJeePjagn9+YS6bmhHxBgDKwlGu7pL8Kv7EpiFtg==", "tarball_sha256": "1bb3c8364d652dd08a15856ee27c6600ecd1e45360243154fa846f240ce7a1df", "status": "termux_verified" + }, + "2.1.222-1": { + "wrapper_spec": "@anthropic-ai/claude-code@2.1.222", + "native_spec": "@anthropic-ai/claude-code-linux-arm64@2.1.222", + "entry_js_offset": 256895476, + "entry_end_offset": 279849039, + "tarball_integrity": "sha512-EXSediF1ujcqQeElbXdCEe+uW3NUQAOjn1xHHAgvwzY08nzJeePjagn9+YS6bmhHxBgDKwlGu7pL8Kv7EpiFtg==", + "tarball_sha256": "1bb3c8364d652dd08a15856ee27c6600ecd1e45360243154fa846f240ce7a1df", + "status": "offset_discovered" } } } diff --git a/packages/claude-code/config/claude-termux-release-manifest.json b/packages/claude-code/config/claude-termux-release-manifest.json index af43c35..1d8f045 100644 --- a/packages/claude-code/config/claude-termux-release-manifest.json +++ b/packages/claude-code/config/claude-termux-release-manifest.json @@ -2,7 +2,7 @@ "manifest_version": 1, "package_name": "@bash0816/claude-code", "latest_audited_version": "2.1.222", - "latest_candidate_version": "2.1.222", + "latest_candidate_version": "2.1.222-1", "previous_stable_version": "2.1.220-2", "stable_pinned_version": "2.1.193", "manifest_url": "https://raw.githubusercontent.com/bash0816/ClaudeCode-Termux/main/config/claude-termux-release-manifest.json" diff --git a/packages/claude-code/lib/termux-run-claude-native.sh b/packages/claude-code/lib/termux-run-claude-native.sh index 5a7996a..60af4cf 100755 --- a/packages/claude-code/lib/termux-run-claude-native.sh +++ b/packages/claude-code/lib/termux-run-claude-native.sh @@ -627,6 +627,13 @@ function rewriteNativeChunkSource(source) { 'npmInstallDeprecated flag', _npmInstallDeprecatedExpected, ); + patched = replaceRequired( + patched, + /function ([A-Za-z_$][\w$]*)\(\)\{(return\([A-Za-z_$][\w$]*,[A-Za-z_$][\w$]*,[A-Za-z_$][\w$]*\)=>\{let\{cmd:([A-Za-z_$][\w$]*),prefixArgs:([A-Za-z_$][\w$]*)\}=[A-Za-z_$][\w$]*\(\{pinToCurrentBinary:!0\}\),[A-Za-z_$][\w$]*=\[\3,\.\.\.\4,"--bg-pty-host")/g, + 'function $1(){return undefined;$2', + 'bg-pty-host factory disable', + 1, + ); return patched; } @@ -795,9 +802,47 @@ async function main() { gte: (a, b) => _cmp(a, b) >= 0, lt: (a, b) => _cmp(a, b) < 0, lte: (a, b) => _cmp(a, b) <= 0, - }; + }; })(), YAML: globalThis.__claudeYaml, + spawn: (cmd, options) => { + const opts = options || {}; + const stdioArrayRaw = opts.stdio; + const cmdArray = Array.isArray(cmd) ? cmd : [cmd]; + const normalizeStdio = (v) => { + if (typeof v === 'number' && Number.isInteger(v) && v >= 0) return v; + return (v === 'ignore' || v === 'pipe' || v === 'inherit') ? v : 'pipe'; + }; + const stdioMapped = Array.isArray(stdioArrayRaw) + ? stdioArrayRaw.map(normalizeStdio) + : [normalizeStdio(opts.stdin), normalizeStdio(opts.stdout), normalizeStdio(opts.stderr)]; + const child = _realChild.spawn(cmdArray[0], cmdArray.slice(1), { + stdio: stdioMapped, + cwd: opts.cwd, + env: opts.env, + detached: !!opts.detached, + argv0: opts.argv0, + }); + const stdoutChunks = []; + if (child.stdout) child.stdout.on('data', d => stdoutChunks.push(d)); + let resolveExited; + const exited = new Promise(resolve => { resolveExited = resolve; }); + child.on('exit', (code, signal) => resolveExited(code !== null ? code : (signal ? 128 : 0))); + child.on('error', () => resolveExited(1)); + return { + pid: child.pid, + exited, + stdout: { text: async () => { await exited; return Buffer.concat(stdoutChunks).toString('utf8'); } }, + unref: () => { try { child.unref(); } catch {} }, + kill: (signal) => { try { child.kill(signal); } catch {} }, + }; + }, + file: (path) => { + const err = new Error(`ENOENT: Bun.file(${String(path)}) is not supported by the Termux compatibility shim`); + err.code = 'ENOENT'; + err.errno = -2; + throw err; + }, }; Object.assign(globalThis.__claudeBunShim, globalThis.Bun); if (typeof globalThis.__claudeBunShim.gc !== 'function') { @@ -1447,6 +1492,13 @@ function rewriteNativeChunkSource(source) { 'npmInstallDeprecated flag', _npmInstallDeprecatedExpected, ); + patched = replaceRequired( + patched, + /function ([A-Za-z_$][\w$]*)\(\)\{(return\([A-Za-z_$][\w$]*,[A-Za-z_$][\w$]*,[A-Za-z_$][\w$]*\)=>\{let\{cmd:([A-Za-z_$][\w$]*),prefixArgs:([A-Za-z_$][\w$]*)\}=[A-Za-z_$][\w$]*\(\{pinToCurrentBinary:!0\}\),[A-Za-z_$][\w$]*=\[\3,\.\.\.\4,"--bg-pty-host")/g, + 'function $1(){return undefined;$2', + 'bg-pty-host factory disable', + 1, + ); return patched; } @@ -1619,6 +1671,44 @@ async function main() { }; })(), YAML: globalThis.__claudeYaml, + spawn: (cmd, options) => { + const opts = options || {}; + const stdioArrayRaw = opts.stdio; + const cmdArray = Array.isArray(cmd) ? cmd : [cmd]; + const normalizeStdio = (v) => { + if (typeof v === 'number' && Number.isInteger(v) && v >= 0) return v; + return (v === 'ignore' || v === 'pipe' || v === 'inherit') ? v : 'pipe'; + }; + const stdioMapped = Array.isArray(stdioArrayRaw) + ? stdioArrayRaw.map(normalizeStdio) + : [normalizeStdio(opts.stdin), normalizeStdio(opts.stdout), normalizeStdio(opts.stderr)]; + const child = _realChild.spawn(cmdArray[0], cmdArray.slice(1), { + stdio: stdioMapped, + cwd: opts.cwd, + env: opts.env, + detached: !!opts.detached, + argv0: opts.argv0, + }); + const stdoutChunks = []; + if (child.stdout) child.stdout.on('data', d => stdoutChunks.push(d)); + let resolveExited; + const exited = new Promise(resolve => { resolveExited = resolve; }); + child.on('exit', (code, signal) => resolveExited(code !== null ? code : (signal ? 128 : 0))); + child.on('error', () => resolveExited(1)); + return { + pid: child.pid, + exited, + stdout: { text: async () => { await exited; return Buffer.concat(stdoutChunks).toString('utf8'); } }, + unref: () => { try { child.unref(); } catch {} }, + kill: (signal) => { try { child.kill(signal); } catch {} }, + }; + }, + file: (path) => { + const err = new Error(`ENOENT: Bun.file(${String(path)}) is not supported by the Termux compatibility shim`); + err.code = 'ENOENT'; + err.errno = -2; + throw err; + }, }; Object.assign(globalThis.__claudeBunShim, globalThis.Bun); if (typeof globalThis.__claudeBunShim.gc !== 'function') { diff --git a/packages/claude-code/lib/termux-run-claude-native.test.js b/packages/claude-code/lib/termux-run-claude-native.test.js index fbba7ce..960187c 100644 --- a/packages/claude-code/lib/termux-run-claude-native.test.js +++ b/packages/claude-code/lib/termux-run-claude-native.test.js @@ -1,6 +1,7 @@ 'use strict'; const test = require('node:test'); +const { mock } = require('node:test'); const assert = require('node:assert/strict'); const crypto = require('crypto'); const child_process = require('child_process'); @@ -300,9 +301,36 @@ test('cleanupStaleEntryFiles removes only stale extracted files for the same off function buildSyntheticBundleSource() { const typeofBun = Array.from({ length: 6 }, () => 'typeof Bun').join('; '); const bunProps = Array.from({ length: 37 }, (_, index) => `Bun.p${index}`).join('; '); - return `function(exports, require, module, __filename, __dirname) { ${typeofBun}; typeof globalThis.Bun; globalThis.Bun; ${bunProps}; npmInstallDeprecated:!0 }`; + return `function(exports, require, module, __filename, __dirname) { ${typeofBun}; typeof globalThis.Bun; globalThis.Bun; ${bunProps}; npmInstallDeprecated:!0; function dYs(){return(e,t,r)=>{let{cmd:n,prefixArgs:o}=ox({pinToCurrentBinary:!0}),i=[n,...o,"--bg-pty-host",r.ptySock];return i}} }`; } +test('rewriteNativeChunkSource disables the bg-pty-host factory', () => { + process.env.CURRENT_CLAUDE_VERSION = '2.1.198'; + try { + const { rewriteNativeChunkSource } = loadHelperApi(); + const patched = rewriteNativeChunkSource(buildSyntheticBundleSource()); + const start = patched.indexOf('function dYs(){'); + assert.notEqual(start, -1); + let depth = 0; + let end = -1; + for (let index = start; index < patched.length; index += 1) { + if (patched[index] === '{') depth += 1; + if (patched[index] === '}') { + depth -= 1; + if (depth === 0) { + end = index + 1; + break; + } + } + } + assert.notEqual(end, -1); + const rewrittenFactory = eval('(' + patched.slice(start, end) + ')'); + assert.equal(rewrittenFactory(), undefined); + } finally { + delete process.env.CURRENT_CLAUDE_VERSION; + } +}); + test('rewriteNativeChunkSource rewrites the synthetic bundle slice (version 2.1.198)', () => { process.env.CURRENT_CLAUDE_VERSION = '2.1.198'; try { @@ -328,7 +356,7 @@ test('rewriteNativeChunkSource rewrites the synthetic bundle slice (version 2.1. const { rewriteNativeChunkSource } = loadHelperApi(); const typeofBun = Array.from({ length: 7 }, () => 'typeof Bun').join('; '); const bunProps = Array.from({ length: 40 }, (_, index) => `Bun.p${index}`).join('; '); - const source = `function(exports, require, module, __filename, __dirname) { ${typeofBun}; typeof globalThis.Bun; globalThis.Bun; ${bunProps}; npmInstallDeprecated:!0 }`; + const source = `function(exports, require, module, __filename, __dirname) { ${typeofBun}; typeof globalThis.Bun; globalThis.Bun; ${bunProps}; npmInstallDeprecated:!0; function dYs(){return(e,t,r)=>{let{cmd:n,prefixArgs:o}=ox({pinToCurrentBinary:!0}),i=[n,...o,"--bg-pty-host",r.ptySock];return i}} }`; const patched = rewriteNativeChunkSource(source); assert.match(patched, /var __claudeBun = globalThis\.__claudeBunShim;/); @@ -349,7 +377,7 @@ test('rewriteNativeChunkSource rewrites the synthetic bundle slice (version 2.1. const { rewriteNativeChunkSource } = loadHelperApi(); const typeofBun = Array.from({ length: 7 }, () => 'typeof Bun').join('; '); const bunProps = Array.from({ length: 41 }, (_, index) => `Bun.p${index}`).join('; '); - const source = `function(exports, require, module, __filename, __dirname) { ${typeofBun}; typeof globalThis.Bun; globalThis.Bun; ${bunProps}; npmInstallDeprecated:!0 }`; + const source = `function(exports, require, module, __filename, __dirname) { ${typeofBun}; typeof globalThis.Bun; globalThis.Bun; ${bunProps}; npmInstallDeprecated:!0; function dYs(){return(e,t,r)=>{let{cmd:n,prefixArgs:o}=ox({pinToCurrentBinary:!0}),i=[n,...o,"--bg-pty-host",r.ptySock];return i}} }`; const patched = rewriteNativeChunkSource(source); assert.match(patched, /var __claudeBun = globalThis\.__claudeBunShim;/); @@ -370,7 +398,7 @@ test('rewriteNativeChunkSource rewrites the synthetic bundle slice (version 2.1. const { rewriteNativeChunkSource } = loadHelperApi(); const typeofBun = Array.from({ length: 7 }, () => 'typeof Bun').join('; '); const bunProps = Array.from({ length: 41 }, (_, index) => `Bun.p${index}`).join('; '); - const source = `function(exports, require, module, __filename, __dirname) { ${typeofBun}; typeof globalThis.Bun; globalThis.Bun; ${bunProps}; npmInstallDeprecated:!0; npmInstallDeprecated:!0 }`; + const source = `function(exports, require, module, __filename, __dirname) { ${typeofBun}; typeof globalThis.Bun; globalThis.Bun; ${bunProps}; npmInstallDeprecated:!0; npmInstallDeprecated:!0; function dYs(){return(e,t,r)=>{let{cmd:n,prefixArgs:o}=ox({pinToCurrentBinary:!0}),i=[n,...o,"--bg-pty-host",r.ptySock];return i}} }`; const patched = rewriteNativeChunkSource(source); assert.match(patched, /var __claudeBun = globalThis\.__claudeBunShim;/); @@ -391,7 +419,7 @@ test('rewriteNativeChunkSource rewrites the synthetic bundle slice (version 2.1. const { rewriteNativeChunkSource } = loadHelperApi(); const typeofBun = Array.from({ length: 7 }, () => 'typeof Bun').join('; '); const bunProps = Array.from({ length: 38 }, (_, index) => `Bun.p${index}`).join('; '); - const source = `function(exports, require, module, __filename, __dirname) { ${typeofBun}; typeof globalThis.Bun; globalThis.Bun; ${bunProps}; npmInstallDeprecated:!0; npmInstallDeprecated:!0 }`; + const source = `function(exports, require, module, __filename, __dirname) { ${typeofBun}; typeof globalThis.Bun; globalThis.Bun; ${bunProps}; npmInstallDeprecated:!0; npmInstallDeprecated:!0; function dYs(){return(e,t,r)=>{let{cmd:n,prefixArgs:o}=ox({pinToCurrentBinary:!0}),i=[n,...o,"--bg-pty-host",r.ptySock];return i}} }`; const patched = rewriteNativeChunkSource(source); assert.match(patched, /var __claudeBun = globalThis\.__claudeBunShim;/); @@ -412,7 +440,7 @@ test('rewriteNativeChunkSource rejects stale Bun property access count for versi const { rewriteNativeChunkSource } = loadHelperApi(); const typeofBun = Array.from({ length: 7 }, () => 'typeof Bun').join('; '); const bunProps = Array.from({ length: 41 }, (_, index) => `Bun.p${index}`).join('; '); - const source = `function(exports, require, module, __filename, __dirname) { ${typeofBun}; typeof globalThis.Bun; globalThis.Bun; ${bunProps}; npmInstallDeprecated:!0; npmInstallDeprecated:!0 }`; + const source = `function(exports, require, module, __filename, __dirname) { ${typeofBun}; typeof globalThis.Bun; globalThis.Bun; ${bunProps}; npmInstallDeprecated:!0; npmInstallDeprecated:!0; function dYs(){return(e,t,r)=>{let{cmd:n,prefixArgs:o}=ox({pinToCurrentBinary:!0}),i=[n,...o,"--bg-pty-host",r.ptySock];return i}} }`; assert.throws( () => rewriteNativeChunkSource(source), @@ -429,7 +457,7 @@ test('rewriteNativeChunkSource rewrites the synthetic bundle slice (version 2.1. const { rewriteNativeChunkSource } = loadHelperApi(); const typeofBun = Array.from({ length: 7 }, () => 'typeof Bun').join('; '); const bunProps = Array.from({ length: 39 }, (_, index) => `Bun.p${index}`).join('; '); - const source = `function(exports, require, module, __filename, __dirname) { ${typeofBun}; typeof globalThis.Bun; globalThis.Bun; ${bunProps}; npmInstallDeprecated:!0; npmInstallDeprecated:!0 }`; + const source = `function(exports, require, module, __filename, __dirname) { ${typeofBun}; typeof globalThis.Bun; globalThis.Bun; ${bunProps}; npmInstallDeprecated:!0; npmInstallDeprecated:!0; function dYs(){return(e,t,r)=>{let{cmd:n,prefixArgs:o}=ox({pinToCurrentBinary:!0}),i=[n,...o,"--bg-pty-host",r.ptySock];return i}} }`; const patched = rewriteNativeChunkSource(source); assert.match(patched, /var __claudeBun = globalThis\.__claudeBunShim;/); @@ -450,7 +478,7 @@ test('rewriteNativeChunkSource rejects stale Bun property access count for versi const { rewriteNativeChunkSource } = loadHelperApi(); const typeofBun = Array.from({ length: 7 }, () => 'typeof Bun').join('; '); const bunProps = Array.from({ length: 38 }, (_, index) => `Bun.p${index}`).join('; '); - const source = `function(exports, require, module, __filename, __dirname) { ${typeofBun}; typeof globalThis.Bun; globalThis.Bun; ${bunProps}; npmInstallDeprecated:!0; npmInstallDeprecated:!0 }`; + const source = `function(exports, require, module, __filename, __dirname) { ${typeofBun}; typeof globalThis.Bun; globalThis.Bun; ${bunProps}; npmInstallDeprecated:!0; npmInstallDeprecated:!0; function dYs(){return(e,t,r)=>{let{cmd:n,prefixArgs:o}=ox({pinToCurrentBinary:!0}),i=[n,...o,"--bg-pty-host",r.ptySock];return i}} }`; assert.throws( () => rewriteNativeChunkSource(source), @@ -467,7 +495,7 @@ test('rewriteNativeChunkSource rewrites the synthetic bundle slice (version 2.1. const { rewriteNativeChunkSource } = loadHelperApi(); const typeofBun = Array.from({ length: 7 }, () => 'typeof Bun').join('; '); const bunProps = Array.from({ length: 40 }, (_, index) => `Bun.p${index}`).join('; '); - const source = `function(exports, require, module, __filename, __dirname) { ${typeofBun}; typeof globalThis.Bun; globalThis.Bun; ${bunProps}; npmInstallDeprecated:!0; npmInstallDeprecated:!0 }`; + const source = `function(exports, require, module, __filename, __dirname) { ${typeofBun}; typeof globalThis.Bun; globalThis.Bun; ${bunProps}; npmInstallDeprecated:!0; npmInstallDeprecated:!0; function dYs(){return(e,t,r)=>{let{cmd:n,prefixArgs:o}=ox({pinToCurrentBinary:!0}),i=[n,...o,"--bg-pty-host",r.ptySock];return i}} }`; const patched = rewriteNativeChunkSource(source); assert.match(patched, /var __claudeBun = globalThis\.__claudeBunShim;/); @@ -487,7 +515,7 @@ test('rewriteNativeChunkSource rejects stale Bun property access count for versi const { rewriteNativeChunkSource } = loadHelperApi(); const typeofBun = Array.from({ length: 7 }, () => 'typeof Bun').join('; '); const bunProps = Array.from({ length: 39 }, (_, index) => `Bun.p${index}`).join('; '); - const source = `function(exports, require, module, __filename, __dirname) { ${typeofBun}; typeof globalThis.Bun; globalThis.Bun; ${bunProps}; npmInstallDeprecated:!0; npmInstallDeprecated:!0 }`; + const source = `function(exports, require, module, __filename, __dirname) { ${typeofBun}; typeof globalThis.Bun; globalThis.Bun; ${bunProps}; npmInstallDeprecated:!0; npmInstallDeprecated:!0; function dYs(){return(e,t,r)=>{let{cmd:n,prefixArgs:o}=ox({pinToCurrentBinary:!0}),i=[n,...o,"--bg-pty-host",r.ptySock];return i}} }`; assert.throws( () => rewriteNativeChunkSource(source), @@ -504,7 +532,7 @@ test('rewriteNativeChunkSource rewrites the synthetic bundle slice (version 2.1. const { rewriteNativeChunkSource } = loadHelperApi(); const typeofBun = Array.from({ length: 7 }, () => 'typeof Bun').join('; '); const bunProps = Array.from({ length: 42 }, (_, index) => `Bun.p${index}`).join('; '); - const source = `function(exports, require, module, __filename, __dirname) { ${typeofBun}; typeof globalThis.Bun; globalThis.Bun; ${bunProps}; npmInstallDeprecated:!0; npmInstallDeprecated:!0 }`; + const source = `function(exports, require, module, __filename, __dirname) { ${typeofBun}; typeof globalThis.Bun; globalThis.Bun; ${bunProps}; npmInstallDeprecated:!0; npmInstallDeprecated:!0; function dYs(){return(e,t,r)=>{let{cmd:n,prefixArgs:o}=ox({pinToCurrentBinary:!0}),i=[n,...o,"--bg-pty-host",r.ptySock];return i}} }`; const patched = rewriteNativeChunkSource(source); assert.match(patched, /var __claudeBun = globalThis\.__claudeBunShim;/); @@ -524,7 +552,7 @@ test('rewriteNativeChunkSource rejects stale Bun property access count for versi const { rewriteNativeChunkSource } = loadHelperApi(); const typeofBun = Array.from({ length: 7 }, () => 'typeof Bun').join('; '); const bunProps = Array.from({ length: 41 }, (_, index) => `Bun.p${index}`).join('; '); - const source = `function(exports, require, module, __filename, __dirname) { ${typeofBun}; typeof globalThis.Bun; globalThis.Bun; ${bunProps}; npmInstallDeprecated:!0; npmInstallDeprecated:!0 }`; + const source = `function(exports, require, module, __filename, __dirname) { ${typeofBun}; typeof globalThis.Bun; globalThis.Bun; ${bunProps}; npmInstallDeprecated:!0; npmInstallDeprecated:!0; function dYs(){return(e,t,r)=>{let{cmd:n,prefixArgs:o}=ox({pinToCurrentBinary:!0}),i=[n,...o,"--bg-pty-host",r.ptySock];return i}} }`; assert.throws( () => rewriteNativeChunkSource(source), @@ -571,6 +599,7 @@ function buildScenarioFixtureSource() { return `function(exports, require, module, __filename, __dirname) { ${typeofBun}; typeof globalThis.Bun; globalThis.Bun; ${bunProps}; npmInstallDeprecated:!0; npmInstallDeprecated:!0; + function dYs(){return(e,t,r)=>{let{cmd:n,prefixArgs:o}=ox({pinToCurrentBinary:!0}),i=[n,...o,"--bg-pty-host",r.ptySock];return i}} const scenario = process.env.TEST_SCENARIO; if (scenario === 'sync-exit') { process.stdout.write('ok'); process.exit(0); return; } if (scenario === 'async-exit') { @@ -1338,3 +1367,145 @@ test('installStreamJsonTerminalWatcher waits for write callback before completin watcher.restore(); }); + +function extractShimSource(block) { + const startMarker = 'const _realChild = require(\'child_process\');'; + const endMarker = 'Object.assign(globalThis.__claudeBunShim, globalThis.Bun);'; + const start = block.indexOf(startMarker); + assert.notEqual(start, -1, 'missing Bun shim start'); + const end = block.indexOf(endMarker, start); + assert.notEqual(end, -1, 'missing Bun shim end'); + return block.slice(start, end + endMarker.length); +} + +function loadBunShim(source) { + const context = vm.createContext({ + stringWidth: () => 0, + wrapAnsi: value => value, + stripANSI: value => value, + stableHash: () => 0, + __claudeYaml: {}, + Buffer, + require, + }); + context.__claudeBunShim = {}; + context.__claudeYaml = {}; + context.Bun = {}; + context.module = { exports: {} }; + vm.runInContext(`${source}\nmodule.exports = globalThis.__claudeBunShim;`, context); + return context.module.exports; +} + +test('helper and bootstrap Bun shim source is identical', () => { + const helperBlock = extractBlock('cat <<\'NODE\' > "$_helper"', '\n export ENABLE_CLAUDEAI_MCP_SERVERS='); + const bootstrapBlock = extractBlock('cat <<\'NODE\' > "$_bootstrap"', '\n export ENABLE_CLAUDEAI_MCP_SERVERS='); + assert.equal(extractShimSource(helperBlock), extractShimSource(bootstrapBlock)); +}); + +test('Bun.file always throws ENOENT', () => { + for (const blockMarker of ['cat <<\'NODE\' > "$_helper"', 'cat <<\'NODE\' > "$_bootstrap"']) { + const block = extractBlock(blockMarker, '\n export ENABLE_CLAUDEAI_MCP_SERVERS='); + const Bun = loadBunShim(extractShimSource(block)); + assert.throws(() => Bun.file('/some/path'), error => + error.code === 'ENOENT' && error.errno === -2); + } +}); + +test('Bun.spawn supports top-level and array stdio forms', async () => { + const block = extractBlock('cat <<\'NODE\' > "$_helper"', '\n export ENABLE_CLAUDEAI_MCP_SERVERS='); + const Bun = loadBunShim(extractShimSource(block)); + const topLevel = Bun.spawn(['echo', 'hello'], { stdout: 'pipe', stderr: 'ignore' }); + assert.equal(await topLevel.stdout.text(), 'hello\n'); + assert.equal(await topLevel.exited, 0); + const arrayForm = Bun.spawn(['echo', 'hello'], { stdio: ['ignore', 'pipe', 'ignore'] }); + assert.equal(await arrayForm.stdout.text(), 'hello\n'); + assert.equal(await arrayForm.exited, 0); +}); + +test('Bun.spawn forwards options, preserves numeric fds, and delegates child controls', () => { + const block = extractBlock('cat <<\'NODE\' > "$_helper"', '\n export ENABLE_CLAUDEAI_MCP_SERVERS='); + const calls = []; + const handlers = {}; + const child = { + pid: 123, + stdout: null, + on(event, handler) { handlers[event] = handler; return this; }, + unref() { calls.push(['unref']); }, + kill(signal) { calls.push(['kill', signal]); }, + }; + mock.method(child_process, 'spawn', (...args) => { + calls.push(args); + return child; + }); + try { + const Bun = loadBunShim(extractShimSource(block)); + const result = Bun.spawn(['cmd', 'arg'], { + detached: true, + argv0: 'argv0-value', + cwd: '/tmp/work', + env: { TEST: 'yes' }, + stdio: [0, 1, 2], + }); + assert.deepEqual(JSON.parse(JSON.stringify(calls[0])), [ + 'cmd', + ['arg'], + { + stdio: [0, 1, 2], + cwd: '/tmp/work', + env: { TEST: 'yes' }, + detached: true, + argv0: 'argv0-value', + }, + ]); + result.unref(); + result.kill('SIGTERM'); + assert.deepEqual(JSON.parse(JSON.stringify(calls.slice(1))), [['unref'], ['kill', 'SIGTERM']]); + } finally { + mock.restoreAll(); + } +}); + +test('Bun.spawn prefers stdio array over top-level stdio options', () => { + for (const blockMarker of ['cat <<\'NODE\' > "$_helper"', 'cat <<\'NODE\' > "$_bootstrap"']) { + const block = extractBlock(blockMarker, '\n export ENABLE_CLAUDEAI_MCP_SERVERS='); + const calls = []; + mock.method(child_process, 'spawn', (...args) => { + calls.push(args); + return { pid: 123, stdout: null, on() { return this; } }; + }); + try { + const Bun = loadBunShim(extractShimSource(block)); + Bun.spawn(['cmd'], { + stdio: [0, 1, 2], + stdin: 'pipe', + stdout: 'pipe', + stderr: 'pipe', + }); + assert.deepEqual(JSON.parse(JSON.stringify(calls[0][2])), { + stdio: [0, 1, 2], + detached: false, + }); + } finally { + mock.restoreAll(); + } + } +}); + +test('Bun.spawn forwards top-level stdin in the first stdio position', () => { + for (const blockMarker of ['cat <<\'NODE\' > "$_helper"', 'cat <<\'NODE\' > "$_bootstrap"']) { + const block = extractBlock(blockMarker, '\n export ENABLE_CLAUDEAI_MCP_SERVERS='); + const calls = []; + mock.method(child_process, 'spawn', (...args) => { + calls.push(args); + return { pid: 123, stdout: null, on() { return this; } }; + }); + try { + const Bun = loadBunShim(extractShimSource(block)); + Bun.spawn(['cmd'], { stdin: 'inherit', stdout: 'pipe', stderr: 'ignore' }); + assert.equal(calls[0][2].stdio[0], 'inherit'); + assert.deepEqual(JSON.parse(JSON.stringify(calls[0][2].stdio)), ['inherit', 'pipe', 'ignore']); + } finally { + mock.restoreAll(); + } + } +}); diff --git a/packages/claude-code/package.json b/packages/claude-code/package.json index f68745f..4da1f85 100644 --- a/packages/claude-code/package.json +++ b/packages/claude-code/package.json @@ -1,6 +1,6 @@ { "name": "@bash0816/claude-code", - "version": "2.1.222", + "version": "2.1.222-1", "description": "Unofficial Termux-native Claude Code wrapper with audited native replay", "license": "GPL-3.0-only", "bin": {