Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 18 additions & 5 deletions packages/claude-code/lib/termux-run-claude-native.sh
Original file line number Diff line number Diff line change
Expand Up @@ -636,6 +636,12 @@ async function main() {
function onAsyncError(error) {
asyncErrors.push(error);
}
const printWaitMs = Number(process.env.CLAUDE_TERMUX_PRINT_WAIT_MS || 5000);
async function waitForPrintFlush() {
if (Number.isFinite(printWaitMs) && printWaitMs > 0) {
await new Promise(resolve => setTimeout(resolve, printWaitMs));
}
}

try {
process.once('uncaughtException', onAsyncError);
Expand Down Expand Up @@ -697,18 +703,15 @@ async function main() {
process.exit = code => {
throw new RequestedExit(code);
};

const printWaitMs = Number(process.env.CLAUDE_TERMUX_PRINT_WAIT_MS || 5000);
const moduleLike = { exports: {} };
const maybePromise = fn(moduleLike.exports, fakeRequire, moduleLike, extractedFile, workdir);
if (maybePromise && typeof maybePromise.then === 'function') await maybePromise;
if (Number.isFinite(printWaitMs) && printWaitMs > 0) {
await new Promise(resolve => setTimeout(resolve, printWaitMs));
}
await waitForPrintFlush();
if (asyncErrors.length > 0) throw asyncErrors[0];
} catch (error) {
if (error instanceof RequestedExit) {
process.exitCode = error.code;
await waitForPrintFlush();
return;
}
throw error;
Expand Down Expand Up @@ -758,6 +761,7 @@ else
_bootstrap=$(mktemp "${TERMUX_TMPDIR}/claude-bootstrap.XXXXXX.js")
trap 'rm -f "$_bootstrap"' EXIT HUP INT TERM
export CLAUDE_TERMUX_TUI="${_tui}"
export CLAUDE_TERMUX_PRINT_MODE="${_pf}"
cat <<'NODE' > "$_bootstrap"
const fs = require('fs');
const path = require('path');
Expand Down Expand Up @@ -1329,6 +1333,13 @@ async function main() {
function onAsyncError(error) {
asyncErrors.push(error);
}
async function waitForPrintFlushIfNeeded() {
if (process.env.CLAUDE_TERMUX_PRINT_MODE !== '1') return;
const printWaitMs = Number(process.env.CLAUDE_TERMUX_PRINT_WAIT_MS || 5000);
if (Number.isFinite(printWaitMs) && printWaitMs > 0) {
await new Promise(resolve => setTimeout(resolve, printWaitMs));
}
}

try {
process.once('uncaughtException', onAsyncError);
Expand Down Expand Up @@ -1394,10 +1405,12 @@ async function main() {
const moduleLike = { exports: {} };
const maybePromise = fn(moduleLike.exports, fakeRequire, moduleLike, extractedFile, workdir);
if (maybePromise && typeof maybePromise.then === 'function') await maybePromise;
await waitForPrintFlushIfNeeded();
if (asyncErrors.length > 0) throw asyncErrors[0];
} catch (error) {
if (error instanceof RequestedExit) {
process.exitCode = error.code;
await waitForPrintFlushIfNeeded();
return;
}
throw error;
Expand Down
98 changes: 96 additions & 2 deletions packages/claude-code/lib/termux-run-claude-native.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -191,12 +191,32 @@ test('print path does not defer cleanup to exit', () => {
test('bootstrap path defers cleanup to exit', () => {
const bootstrapBlock = extractBlock('cat <<\'NODE\' > "$_bootstrap"', '\n export ENABLE_CLAUDEAI_MCP_SERVERS=');

assert.equal(bootstrapBlock.includes('CLAUDE_TERMUX_PRINT_WAIT_MS'), false);
assert.equal(bootstrapBlock.includes('setTimeout(resolve, printWaitMs)'), false);
assert.equal(bootstrapBlock.includes('process.once(\'exit\''), true);
assert.equal(bootstrapBlock.includes('process.removeListener(\'uncaughtException\''), true);
});

test('bootstrap branch exports CLAUDE_TERMUX_PRINT_MODE before invoking node', () => {
const bootstrapShellRegion = extractFunction(
script,
'export CLAUDE_TERMUX_TUI="${_tui}"',
'cat <<\'NODE\' > "$_bootstrap"',
);
assert.equal(bootstrapShellRegion.includes('export CLAUDE_TERMUX_PRINT_MODE="${_pf}"'), true);
});

test('helper and bootstrap wait for print flush, including the RequestedExit path', () => {
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(helperBlock.includes('async function waitForPrintFlush()'), true);
assert.equal(helperBlock.includes('await waitForPrintFlush();\n if (asyncErrors.length > 0) throw asyncErrors[0];'), true);
assert.equal(helperBlock.includes('process.exitCode = error.code;\n await waitForPrintFlush();\n return;'), true);

assert.equal(bootstrapBlock.includes('async function waitForPrintFlushIfNeeded()'), true);
assert.equal(bootstrapBlock.includes('await waitForPrintFlushIfNeeded();\n if (asyncErrors.length > 0) throw asyncErrors[0];'), true);
assert.equal(bootstrapBlock.includes('process.exitCode = error.code;\n await waitForPrintFlushIfNeeded();\n return;'), true);
});

test('entry extraction uses a process-unique filename', () => {
const helperBlock = extractBlock('cat <<\'NODE\' > "$_helper"', '\n export ENABLE_CLAUDEAI_MCP_SERVERS=');
const bootstrapBlock = extractBlock('cat <<\'NODE\' > "$_bootstrap"', '\n export ENABLE_CLAUDEAI_MCP_SERVERS=');
Expand Down Expand Up @@ -544,3 +564,77 @@ test('tarball contents match the workspace runner and test file', { skip: !fs.ex
test('CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC is never forced (regression guard)', () => {
assert.equal(script.includes('CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC'), false);
});

function buildScenarioFixtureSource() {
const typeofBun = Array.from({ length: 7 }, () => 'typeof Bun').join('; ');
const bunProps = Array.from({ length: 42 }, (_, i) => `Bun.p${i}`).join('; ');
return `function(exports, require, module, __filename, __dirname) {
${typeofBun}; typeof globalThis.Bun; globalThis.Bun; ${bunProps};
npmInstallDeprecated:!0; npmInstallDeprecated:!0;
const scenario = process.env.TEST_SCENARIO;
if (scenario === 'sync-exit') { process.stdout.write('ok'); process.exit(0); return; }
if (scenario === 'async-exit') {
setTimeout(() => { process.stdout.write('ok'); }, 100).unref();
return;
}
process.stdout.write('ok');
}`;
}

function runScenario({ printMode, stdinInherit, scenario }) {
const tmpBase = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-stdin-test-'));
const sourceBin = path.join(tmpBase, 'fake-source.js');
const fixtureSource = buildScenarioFixtureSource();
fs.writeFileSync(sourceBin, fixtureSource, 'utf8');
const entryJsOffset = 0;
const entryEndOffset = Buffer.byteLength(fixtureSource, 'utf8');
const workdir = path.join(tmpBase, 'workdir');
fs.mkdirSync(workdir, { recursive: true });

const env = {
...process.env,
SOURCE_BIN: sourceBin,
WORKDIR: workdir,
ENTRY_JS_OFFSET: String(entryJsOffset),
ENTRY_END_OFFSET: String(entryEndOffset),
CURRENT_CLAUDE_VERSION: '2.1.220',
CLAUDE_TERMUX_PACKAGE_DIR: path.join(__dirname, '..'),
MAGI_ENV: '1',
CLAUDE_TERMUX_PRINT_WAIT_MS: '300',
TMPDIR: tmpBase,
TEST_SCENARIO: scenario,
};
if (stdinInherit) env.CLAUDE_TERMUX_STDIN = 'inherit';
else delete env.CLAUDE_TERMUX_STDIN;

const args = printMode ? ['-p', 'x'] : [];
const start = Date.now();
const result = child_process.spawnSync('sh', [scriptPath, ...args], {
env,
input: 'test input\n',
encoding: 'utf8',
timeout: 10000,
});
const elapsedMs = Date.now() - start;
fs.rmSync(tmpBase, { recursive: true, force: true });
return { ...result, elapsedMs };
}

test('helper branch (CLI -p, no stdin inherit): normal/sync-exit/async-exit all produce output', () => {
for (const scenario of ['normal', 'sync-exit', 'async-exit']) {
const r = runScenario({ printMode: true, stdinInherit: false, scenario });
assert.ok((r.stdout || '').includes('ok'), `scenario=${scenario} stdout=${r.stdout} stderr=${r.stderr}`);
assert.equal(r.status, 0, `scenario=${scenario} status=${r.status} stderr=${r.stderr}`);
}
});

test('bootstrap branch (-p + CLAUDE_TERMUX_STDIN=inherit): normal/sync-exit/async-exit all produce output', () => {
for (const scenario of ['normal', 'sync-exit', 'async-exit']) {
const r = runScenario({ printMode: true, stdinInherit: true, scenario });
assert.ok((r.stdout || '').includes('ok'), `scenario=${scenario} stdout=${r.stdout} stderr=${r.stderr}`);
assert.equal(r.status, 0, `scenario=${scenario} status=${r.status} stderr=${r.stderr}`);
if (scenario === 'async-exit') {
assert.ok(r.elapsedMs >= 250, `expected wait >= 250ms, got ${r.elapsedMs}ms`);
}
}
});