@bash0816/claude-code 2.1.220 → 2.1.222

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/README.md CHANGED
@@ -37,7 +37,7 @@ npm が `claude` bin link を管理する状態になれば、通常の `npm ins
37
37
  Latest audited version / 最新監査済み版:
38
38
 
39
39
  ```sh
40
- npm install -g @bash0816/claude-code@2.1.219
40
+ npm install -g @bash0816/claude-code@2.1.220-2
41
41
  ```
42
42
 
43
43
  ## Update / 更新
@@ -580,6 +580,42 @@
580
580
  "entry_end_offset": 265457701,
581
581
  "tarball_integrity": "sha512-VHFI8mKruIntKn7eq81sbyS19/KWmQcmJQsS/C+j9M/E+w0s4UytgsL7DADPjBE/GByNiKoRtLYDMntCjRlOdA==",
582
582
  "tarball_sha256": "e38454d73576a08a2e707f26539d73fc9ef33e890228ca5c58a2bbe810ac884d",
583
+ "status": "termux_verified"
584
+ },
585
+ "2.1.220-1": {
586
+ "wrapper_spec": "@anthropic-ai/claude-code@2.1.220",
587
+ "native_spec": "@anthropic-ai/claude-code-linux-arm64@2.1.220",
588
+ "entry_js_offset": 243831156,
589
+ "entry_end_offset": 265457701,
590
+ "tarball_integrity": "sha512-VHFI8mKruIntKn7eq81sbyS19/KWmQcmJQsS/C+j9M/E+w0s4UytgsL7DADPjBE/GByNiKoRtLYDMntCjRlOdA==",
591
+ "tarball_sha256": "e38454d73576a08a2e707f26539d73fc9ef33e890228ca5c58a2bbe810ac884d",
592
+ "status": "offset_discovered"
593
+ },
594
+ "2.1.220-2": {
595
+ "wrapper_spec": "@anthropic-ai/claude-code@2.1.220",
596
+ "native_spec": "@anthropic-ai/claude-code-linux-arm64@2.1.220",
597
+ "entry_js_offset": 243831156,
598
+ "entry_end_offset": 265457701,
599
+ "tarball_integrity": "sha512-VHFI8mKruIntKn7eq81sbyS19/KWmQcmJQsS/C+j9M/E+w0s4UytgsL7DADPjBE/GByNiKoRtLYDMntCjRlOdA==",
600
+ "tarball_sha256": "e38454d73576a08a2e707f26539d73fc9ef33e890228ca5c58a2bbe810ac884d",
601
+ "status": "termux_verified"
602
+ },
603
+ "2.1.220-3": {
604
+ "wrapper_spec": "@anthropic-ai/claude-code@2.1.220",
605
+ "native_spec": "@anthropic-ai/claude-code-linux-arm64@2.1.220",
606
+ "entry_js_offset": 243831156,
607
+ "entry_end_offset": 265457701,
608
+ "tarball_integrity": "sha512-VHFI8mKruIntKn7eq81sbyS19/KWmQcmJQsS/C+j9M/E+w0s4UytgsL7DADPjBE/GByNiKoRtLYDMntCjRlOdA==",
609
+ "tarball_sha256": "e38454d73576a08a2e707f26539d73fc9ef33e890228ca5c58a2bbe810ac884d",
610
+ "status": "offset_discovered"
611
+ },
612
+ "2.1.222": {
613
+ "wrapper_spec": "@anthropic-ai/claude-code@2.1.222",
614
+ "native_spec": "@anthropic-ai/claude-code-linux-arm64@2.1.222",
615
+ "entry_js_offset": 256895476,
616
+ "entry_end_offset": 279849039,
617
+ "tarball_integrity": "sha512-EXSediF1ujcqQeElbXdCEe+uW3NUQAOjn1xHHAgvwzY08nzJeePjagn9+YS6bmhHxBgDKwlGu7pL8Kv7EpiFtg==",
618
+ "tarball_sha256": "1bb3c8364d652dd08a15856ee27c6600ecd1e45360243154fa846f240ce7a1df",
583
619
  "status": "offset_discovered"
584
620
  }
585
621
  }
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "manifest_version": 1,
3
3
  "package_name": "@bash0816/claude-code",
4
- "latest_audited_version": "2.1.219",
5
- "latest_candidate_version": "2.1.220",
6
- "previous_stable_version": "2.1.218",
4
+ "latest_audited_version": "2.1.220-2",
5
+ "latest_candidate_version": "2.1.222",
6
+ "previous_stable_version": "2.1.220",
7
7
  "stable_pinned_version": "2.1.193",
8
8
  "manifest_url": "https://raw.githubusercontent.com/bash0816/ClaudeCode-Termux/main/config/claude-termux-release-manifest.json"
9
9
  }
@@ -79,6 +79,22 @@ const entryJsOffset = Number(process.env.ENTRY_JS_OFFSET);
79
79
  const entryEndOffset = Number(process.env.ENTRY_END_OFFSET);
80
80
  const argv = process.argv.slice(2);
81
81
 
82
+ function isStreamJsonPrintMode(argv) {
83
+ const dashDashIndex = argv.indexOf('--');
84
+ const ownArgs = dashDashIndex === -1 ? argv : argv.slice(0, dashDashIndex);
85
+ const hasPrintFlag = ownArgs.includes('-p') || ownArgs.includes('--print');
86
+ let hasStreamJsonFormat = false;
87
+ for (let i = 0; i < ownArgs.length; i++) {
88
+ const tok = ownArgs[i];
89
+ if (tok === '--output-format=stream-json') { hasStreamJsonFormat = true; break; }
90
+ if (tok === '--output-format' && ownArgs[i + 1] === 'stream-json') {
91
+ hasStreamJsonFormat = true;
92
+ break;
93
+ }
94
+ }
95
+ return hasPrintFlag && hasStreamJsonFormat;
96
+ }
97
+
82
98
  class RequestedExit extends Error {
83
99
  constructor(code) {
84
100
  super(`process.exit ${code}`);
@@ -623,10 +639,12 @@ async function main() {
623
639
 
624
640
  const originalArgv = process.argv.slice();
625
641
  const originalExit = process.exit;
642
+ const originalKill = process.kill;
626
643
  const originalBun = process.versions.bun;
627
644
  const hadGlobalBun = Object.prototype.hasOwnProperty.call(globalThis, 'Bun');
628
645
  const originalGlobalBun = globalThis.Bun;
629
646
  const asyncErrors = [];
647
+ let streamJsonWatcher = null;
630
648
 
631
649
  globalThis.__claudeYaml = createYamlShim();
632
650
  if (!globalThis.__claudeBunShim || typeof globalThis.__claudeBunShim !== 'object') {
@@ -636,6 +654,100 @@ async function main() {
636
654
  function onAsyncError(error) {
637
655
  asyncErrors.push(error);
638
656
  }
657
+ const printWaitMs = Number(process.env.CLAUDE_TERMUX_PRINT_WAIT_MS || 5000);
658
+
659
+ function installStreamJsonTerminalWatcher() {
660
+ const hadOwnWrite = Object.prototype.hasOwnProperty.call(process.stdout, 'write');
661
+ const originalWriteDescriptor = hadOwnWrite ? Object.getOwnPropertyDescriptor(process.stdout, 'write') : undefined;
662
+ const originalWrite = process.stdout.write.bind(process.stdout);
663
+ const { StringDecoder } = require('string_decoder');
664
+ const decoder = new StringDecoder('utf8');
665
+ let lineBuffer = '';
666
+ let resultPromiseResolve = null;
667
+ let foundResult = false;
668
+ let restored = false;
669
+
670
+ process.stdout.write = function wrappedWrite(chunk, encoding, callback) {
671
+ let cb = callback;
672
+ let enc = encoding;
673
+ if (typeof encoding === 'function') {
674
+ cb = encoding;
675
+ enc = undefined;
676
+ }
677
+ const combinedCallback = (err) => {
678
+ if (!err && !foundResult) {
679
+ const chunkStr = typeof chunk === 'string'
680
+ ? decoder.write(Buffer.from(chunk, typeof enc === 'string' ? enc : 'utf8'))
681
+ : decoder.write(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
682
+ lineBuffer += chunkStr;
683
+ const lines = lineBuffer.split('\n');
684
+ lineBuffer = lines[lines.length - 1];
685
+ for (let i = 0; i < lines.length - 1; i++) {
686
+ try {
687
+ const parsed = JSON.parse(lines[i]);
688
+ if (parsed && parsed.type === 'result') {
689
+ foundResult = true;
690
+ if (typeof resultPromiseResolve === 'function') resultPromiseResolve();
691
+ }
692
+ } catch {}
693
+ }
694
+ }
695
+ if (typeof cb === 'function') cb(err);
696
+ };
697
+ return originalWrite(chunk, enc, combinedCallback);
698
+ };
699
+
700
+ return {
701
+ waitForResult() {
702
+ if (foundResult) return Promise.resolve();
703
+ return new Promise((resolve, reject) => {
704
+ resultPromiseResolve = resolve;
705
+ });
706
+ },
707
+ restore() {
708
+ if (restored) return;
709
+ restored = true;
710
+ if (hadOwnWrite) {
711
+ Object.defineProperty(process.stdout, 'write', originalWriteDescriptor);
712
+ } else {
713
+ delete process.stdout.write;
714
+ }
715
+ },
716
+ };
717
+ }
718
+
719
+ function forceTimeoutExit(exitCode) {
720
+ try { if (streamJsonWatcher) streamJsonWatcher.restore(); } catch {}
721
+ if (extractedFile) {
722
+ try { fs.rmSync(extractedFile, { force: true }); } catch {}
723
+ }
724
+ originalExit(exitCode);
725
+ }
726
+
727
+ async function waitForPrintFlush() {
728
+ if (isStreamJsonPrintMode(argv) && streamJsonWatcher) {
729
+ let timedOut = false;
730
+ const rawResultTimeoutMs = Number(process.env.CLAUDE_TERMUX_PRINT_RESULT_TIMEOUT_MS);
731
+ const resultTimeoutMs = (Number.isFinite(rawResultTimeoutMs) && rawResultTimeoutMs > 0) ? rawResultTimeoutMs : 300000;
732
+ let timeoutHandle;
733
+ const timeoutPromise = new Promise(resolve => {
734
+ timeoutHandle = setTimeout(() => { timedOut = true; resolve(); }, resultTimeoutMs);
735
+ });
736
+ try {
737
+ await Promise.race([streamJsonWatcher.waitForResult(), timeoutPromise]);
738
+ } finally {
739
+ clearTimeout(timeoutHandle);
740
+ }
741
+ if (timedOut) {
742
+ forceTimeoutExit(1);
743
+ return;
744
+ }
745
+ return;
746
+ }
747
+ if (Number.isFinite(printWaitMs) && printWaitMs > 0) {
748
+ await new Promise(resolve => setTimeout(resolve, printWaitMs));
749
+ }
750
+ }
639
751
 
640
752
  try {
641
753
  process.once('uncaughtException', onAsyncError);
@@ -694,21 +806,31 @@ async function main() {
694
806
  globalThis.__claudeBun = globalThis.__claudeBunShim;
695
807
  globalThis.Bun = globalThis.__claudeBunShim;
696
808
  process.argv = ['node', extractedFile, ...argv];
809
+ let lastExitAttemptCode = 0;
697
810
  process.exit = code => {
698
- throw new RequestedExit(code);
811
+ lastExitAttemptCode = code ?? 0;
812
+ throw new RequestedExit(lastExitAttemptCode);
813
+ };
814
+ process.kill = (pid, signal) => {
815
+ if (pid === process.pid && (signal === 'SIGKILL' || signal === 9)) {
816
+ throw new RequestedExit(lastExitAttemptCode);
817
+ }
818
+ return originalKill.call(process, pid, signal);
699
819
  };
700
820
 
701
- const printWaitMs = Number(process.env.CLAUDE_TERMUX_PRINT_WAIT_MS || 5000);
821
+ if (isStreamJsonPrintMode(argv)) {
822
+ streamJsonWatcher = installStreamJsonTerminalWatcher();
823
+ }
824
+
702
825
  const moduleLike = { exports: {} };
703
826
  const maybePromise = fn(moduleLike.exports, fakeRequire, moduleLike, extractedFile, workdir);
704
827
  if (maybePromise && typeof maybePromise.then === 'function') await maybePromise;
705
- if (Number.isFinite(printWaitMs) && printWaitMs > 0) {
706
- await new Promise(resolve => setTimeout(resolve, printWaitMs));
707
- }
828
+ await waitForPrintFlush();
708
829
  if (asyncErrors.length > 0) throw asyncErrors[0];
709
830
  } catch (error) {
710
831
  if (error instanceof RequestedExit) {
711
832
  process.exitCode = error.code;
833
+ await waitForPrintFlush();
712
834
  return;
713
835
  }
714
836
  throw error;
@@ -722,6 +844,10 @@ async function main() {
722
844
  }
723
845
  process.argv = originalArgv;
724
846
  process.exit = originalExit;
847
+ process.kill = originalKill;
848
+ if (streamJsonWatcher) {
849
+ try { streamJsonWatcher.restore(); } catch {}
850
+ }
725
851
  try {
726
852
  if (originalBun === undefined) {
727
853
  delete process.versions.bun;
@@ -758,6 +884,7 @@ else
758
884
  _bootstrap=$(mktemp "${TERMUX_TMPDIR}/claude-bootstrap.XXXXXX.js")
759
885
  trap 'rm -f "$_bootstrap"' EXIT HUP INT TERM
760
886
  export CLAUDE_TERMUX_TUI="${_tui}"
887
+ export CLAUDE_TERMUX_PRINT_MODE="${_pf}"
761
888
  cat <<'NODE' > "$_bootstrap"
762
889
  const fs = require('fs');
763
890
  const path = require('path');
@@ -772,6 +899,22 @@ const entryJsOffset = Number(process.env.ENTRY_JS_OFFSET);
772
899
  const entryEndOffset = Number(process.env.ENTRY_END_OFFSET);
773
900
  const argv = process.argv.slice(2);
774
901
 
902
+ function isStreamJsonPrintMode(argv) {
903
+ const dashDashIndex = argv.indexOf('--');
904
+ const ownArgs = dashDashIndex === -1 ? argv : argv.slice(0, dashDashIndex);
905
+ const hasPrintFlag = ownArgs.includes('-p') || ownArgs.includes('--print');
906
+ let hasStreamJsonFormat = false;
907
+ for (let i = 0; i < ownArgs.length; i++) {
908
+ const tok = ownArgs[i];
909
+ if (tok === '--output-format=stream-json') { hasStreamJsonFormat = true; break; }
910
+ if (tok === '--output-format' && ownArgs[i + 1] === 'stream-json') {
911
+ hasStreamJsonFormat = true;
912
+ break;
913
+ }
914
+ }
915
+ return hasPrintFlag && hasStreamJsonFormat;
916
+ }
917
+
775
918
  class RequestedExit extends Error {
776
919
  constructor(code) {
777
920
  super(`process.exit ${code}`);
@@ -1316,10 +1459,12 @@ async function main() {
1316
1459
 
1317
1460
  const originalArgv = process.argv.slice();
1318
1461
  const originalExit = process.exit;
1462
+ const originalKill = process.kill;
1319
1463
  const originalBun = process.versions.bun;
1320
1464
  const hadGlobalBun = Object.prototype.hasOwnProperty.call(globalThis, 'Bun');
1321
1465
  const originalGlobalBun = globalThis.Bun;
1322
1466
  const asyncErrors = [];
1467
+ let streamJsonWatcher = null;
1323
1468
 
1324
1469
  globalThis.__claudeYaml = createYamlShim();
1325
1470
  if (!globalThis.__claudeBunShim || typeof globalThis.__claudeBunShim !== 'object') {
@@ -1330,6 +1475,101 @@ async function main() {
1330
1475
  asyncErrors.push(error);
1331
1476
  }
1332
1477
 
1478
+ function installStreamJsonTerminalWatcher() {
1479
+ const hadOwnWrite = Object.prototype.hasOwnProperty.call(process.stdout, 'write');
1480
+ const originalWriteDescriptor = hadOwnWrite ? Object.getOwnPropertyDescriptor(process.stdout, 'write') : undefined;
1481
+ const originalWrite = process.stdout.write.bind(process.stdout);
1482
+ const { StringDecoder } = require('string_decoder');
1483
+ const decoder = new StringDecoder('utf8');
1484
+ let lineBuffer = '';
1485
+ let resultPromiseResolve = null;
1486
+ let foundResult = false;
1487
+ let restored = false;
1488
+
1489
+ process.stdout.write = function wrappedWrite(chunk, encoding, callback) {
1490
+ let cb = callback;
1491
+ let enc = encoding;
1492
+ if (typeof encoding === 'function') {
1493
+ cb = encoding;
1494
+ enc = undefined;
1495
+ }
1496
+ const combinedCallback = (err) => {
1497
+ if (!err && !foundResult) {
1498
+ const chunkStr = typeof chunk === 'string'
1499
+ ? decoder.write(Buffer.from(chunk, typeof enc === 'string' ? enc : 'utf8'))
1500
+ : decoder.write(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
1501
+ lineBuffer += chunkStr;
1502
+ const lines = lineBuffer.split('\n');
1503
+ lineBuffer = lines[lines.length - 1];
1504
+ for (let i = 0; i < lines.length - 1; i++) {
1505
+ try {
1506
+ const parsed = JSON.parse(lines[i]);
1507
+ if (parsed && parsed.type === 'result') {
1508
+ foundResult = true;
1509
+ if (typeof resultPromiseResolve === 'function') resultPromiseResolve();
1510
+ }
1511
+ } catch {}
1512
+ }
1513
+ }
1514
+ if (typeof cb === 'function') cb(err);
1515
+ };
1516
+ return originalWrite(chunk, enc, combinedCallback);
1517
+ };
1518
+
1519
+ return {
1520
+ waitForResult() {
1521
+ if (foundResult) return Promise.resolve();
1522
+ return new Promise((resolve, reject) => {
1523
+ resultPromiseResolve = resolve;
1524
+ });
1525
+ },
1526
+ restore() {
1527
+ if (restored) return;
1528
+ restored = true;
1529
+ if (hadOwnWrite) {
1530
+ Object.defineProperty(process.stdout, 'write', originalWriteDescriptor);
1531
+ } else {
1532
+ delete process.stdout.write;
1533
+ }
1534
+ },
1535
+ };
1536
+ }
1537
+
1538
+ function forceTimeoutExit(exitCode) {
1539
+ try { if (streamJsonWatcher) streamJsonWatcher.restore(); } catch {}
1540
+ if (extractedFile) {
1541
+ try { fs.rmSync(extractedFile, { force: true }); } catch {}
1542
+ }
1543
+ originalExit(exitCode);
1544
+ }
1545
+
1546
+ async function waitForPrintFlushIfNeeded() {
1547
+ if (process.env.CLAUDE_TERMUX_PRINT_MODE !== '1') return;
1548
+ if (isStreamJsonPrintMode(argv) && streamJsonWatcher) {
1549
+ let timedOut = false;
1550
+ const rawResultTimeoutMs = Number(process.env.CLAUDE_TERMUX_PRINT_RESULT_TIMEOUT_MS);
1551
+ const resultTimeoutMs = (Number.isFinite(rawResultTimeoutMs) && rawResultTimeoutMs > 0) ? rawResultTimeoutMs : 300000;
1552
+ let timeoutHandle;
1553
+ const timeoutPromise = new Promise(resolve => {
1554
+ timeoutHandle = setTimeout(() => { timedOut = true; resolve(); }, resultTimeoutMs);
1555
+ });
1556
+ try {
1557
+ await Promise.race([streamJsonWatcher.waitForResult(), timeoutPromise]);
1558
+ } finally {
1559
+ clearTimeout(timeoutHandle);
1560
+ }
1561
+ if (timedOut) {
1562
+ forceTimeoutExit(1);
1563
+ return;
1564
+ }
1565
+ return;
1566
+ }
1567
+ const printWaitMs = Number(process.env.CLAUDE_TERMUX_PRINT_WAIT_MS || 5000);
1568
+ if (Number.isFinite(printWaitMs) && printWaitMs > 0) {
1569
+ await new Promise(resolve => setTimeout(resolve, printWaitMs));
1570
+ }
1571
+ }
1572
+
1333
1573
  try {
1334
1574
  process.once('uncaughtException', onAsyncError);
1335
1575
  process.once('unhandledRejection', onAsyncError);
@@ -1387,17 +1627,31 @@ async function main() {
1387
1627
  globalThis.__claudeBun = globalThis.__claudeBunShim;
1388
1628
  globalThis.Bun = globalThis.__claudeBunShim;
1389
1629
  process.argv = ['node', extractedFile, ...argv];
1630
+ let lastExitAttemptCode = 0;
1390
1631
  process.exit = code => {
1391
- throw new RequestedExit(code);
1632
+ lastExitAttemptCode = code ?? 0;
1633
+ throw new RequestedExit(lastExitAttemptCode);
1392
1634
  };
1635
+ process.kill = (pid, signal) => {
1636
+ if (pid === process.pid && (signal === 'SIGKILL' || signal === 9)) {
1637
+ throw new RequestedExit(lastExitAttemptCode);
1638
+ }
1639
+ return originalKill.call(process, pid, signal);
1640
+ };
1641
+
1642
+ if (isStreamJsonPrintMode(argv)) {
1643
+ streamJsonWatcher = installStreamJsonTerminalWatcher();
1644
+ }
1393
1645
 
1394
1646
  const moduleLike = { exports: {} };
1395
1647
  const maybePromise = fn(moduleLike.exports, fakeRequire, moduleLike, extractedFile, workdir);
1396
1648
  if (maybePromise && typeof maybePromise.then === 'function') await maybePromise;
1649
+ await waitForPrintFlushIfNeeded();
1397
1650
  if (asyncErrors.length > 0) throw asyncErrors[0];
1398
1651
  } catch (error) {
1399
1652
  if (error instanceof RequestedExit) {
1400
1653
  process.exitCode = error.code;
1654
+ await waitForPrintFlushIfNeeded();
1401
1655
  return;
1402
1656
  }
1403
1657
  throw error;
@@ -1406,6 +1660,10 @@ async function main() {
1406
1660
  process.removeListener('unhandledRejection', onAsyncError);
1407
1661
  process.argv = originalArgv;
1408
1662
  process.exit = originalExit;
1663
+ process.kill = originalKill;
1664
+ if (streamJsonWatcher) {
1665
+ try { streamJsonWatcher.restore(); } catch {}
1666
+ }
1409
1667
  process.once('exit', () => {
1410
1668
  if (extractedFile) {
1411
1669
  try {
@@ -191,12 +191,32 @@ test('print path does not defer cleanup to exit', () => {
191
191
  test('bootstrap path defers cleanup to exit', () => {
192
192
  const bootstrapBlock = extractBlock('cat <<\'NODE\' > "$_bootstrap"', '\n export ENABLE_CLAUDEAI_MCP_SERVERS=');
193
193
 
194
- assert.equal(bootstrapBlock.includes('CLAUDE_TERMUX_PRINT_WAIT_MS'), false);
195
- assert.equal(bootstrapBlock.includes('setTimeout(resolve, printWaitMs)'), false);
196
194
  assert.equal(bootstrapBlock.includes('process.once(\'exit\''), true);
197
195
  assert.equal(bootstrapBlock.includes('process.removeListener(\'uncaughtException\''), true);
198
196
  });
199
197
 
198
+ test('bootstrap branch exports CLAUDE_TERMUX_PRINT_MODE before invoking node', () => {
199
+ const bootstrapShellRegion = extractFunction(
200
+ script,
201
+ 'export CLAUDE_TERMUX_TUI="${_tui}"',
202
+ 'cat <<\'NODE\' > "$_bootstrap"',
203
+ );
204
+ assert.equal(bootstrapShellRegion.includes('export CLAUDE_TERMUX_PRINT_MODE="${_pf}"'), true);
205
+ });
206
+
207
+ test('helper and bootstrap wait for print flush, including the RequestedExit path', () => {
208
+ const helperBlock = extractBlock('cat <<\'NODE\' > "$_helper"', '\n export ENABLE_CLAUDEAI_MCP_SERVERS=');
209
+ const bootstrapBlock = extractBlock('cat <<\'NODE\' > "$_bootstrap"', '\n export ENABLE_CLAUDEAI_MCP_SERVERS=');
210
+
211
+ assert.equal(helperBlock.includes('async function waitForPrintFlush()'), true);
212
+ assert.equal(helperBlock.includes('await waitForPrintFlush();\n if (asyncErrors.length > 0) throw asyncErrors[0];'), true);
213
+ assert.equal(helperBlock.includes('process.exitCode = error.code;\n await waitForPrintFlush();\n return;'), true);
214
+
215
+ assert.equal(bootstrapBlock.includes('async function waitForPrintFlushIfNeeded()'), true);
216
+ assert.equal(bootstrapBlock.includes('await waitForPrintFlushIfNeeded();\n if (asyncErrors.length > 0) throw asyncErrors[0];'), true);
217
+ assert.equal(bootstrapBlock.includes('process.exitCode = error.code;\n await waitForPrintFlushIfNeeded();\n return;'), true);
218
+ });
219
+
200
220
  test('entry extraction uses a process-unique filename', () => {
201
221
  const helperBlock = extractBlock('cat <<\'NODE\' > "$_helper"', '\n export ENABLE_CLAUDEAI_MCP_SERVERS=');
202
222
  const bootstrapBlock = extractBlock('cat <<\'NODE\' > "$_bootstrap"', '\n export ENABLE_CLAUDEAI_MCP_SERVERS=');
@@ -544,3 +564,777 @@ test('tarball contents match the workspace runner and test file', { skip: !fs.ex
544
564
  test('CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC is never forced (regression guard)', () => {
545
565
  assert.equal(script.includes('CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC'), false);
546
566
  });
567
+
568
+ function buildScenarioFixtureSource() {
569
+ const typeofBun = Array.from({ length: 7 }, () => 'typeof Bun').join('; ');
570
+ const bunProps = Array.from({ length: 42 }, (_, i) => `Bun.p${i}`).join('; ');
571
+ return `function(exports, require, module, __filename, __dirname) {
572
+ ${typeofBun}; typeof globalThis.Bun; globalThis.Bun; ${bunProps};
573
+ npmInstallDeprecated:!0; npmInstallDeprecated:!0;
574
+ const scenario = process.env.TEST_SCENARIO;
575
+ if (scenario === 'sync-exit') { process.stdout.write('ok'); process.exit(0); return; }
576
+ if (scenario === 'async-exit') {
577
+ setTimeout(() => { process.stdout.write('ok'); }, 100).unref();
578
+ return;
579
+ }
580
+ if (scenario === 'self-sigkill-fallback-string') {
581
+ try {
582
+ process.exit(17);
583
+ } catch (e) {
584
+ process.kill(process.pid, 'SIGKILL');
585
+ }
586
+ return;
587
+ }
588
+ if (scenario === 'self-sigkill-fallback-numeric') {
589
+ try {
590
+ process.exit(17);
591
+ } catch (e) {
592
+ process.kill(process.pid, 9);
593
+ }
594
+ return;
595
+ }
596
+ if (scenario === 'self-sigkill-fallback-no-code') {
597
+ try {
598
+ process.exit();
599
+ } catch (e) {
600
+ process.kill(process.pid, 'SIGKILL');
601
+ }
602
+ return;
603
+ }
604
+ if (scenario === 'other-process-kill') {
605
+ const cp = require('child_process');
606
+ const child = cp.spawn('node', ['-e', 'setTimeout(() => {}, 5000)']);
607
+ const childPid = child.pid;
608
+ process.kill(childPid, 0);
609
+ child.kill();
610
+ process.stdout.write('ok');
611
+ return;
612
+ }
613
+ if (scenario === 'stream-json-result') {
614
+ process.stdout.write('{"type":"init"}\\n');
615
+ const msg = '{"type":"result","data":"test"}\\n';
616
+ process.stdout.write(msg, undefined, () => {});
617
+ return;
618
+ }
619
+ if (scenario === 'stream-json-multibyte-split') {
620
+ // Split a UTF-8 multi-byte character across write calls
621
+ // 'あ' is 3 bytes in UTF-8: e3 81 82
622
+ const buf = Buffer.from('あ', 'utf8');
623
+ // Split the 3-byte character: first byte in one write, remaining in another
624
+ const jsonLine = '{"type":"result"}\\n';
625
+ const part1 = Buffer.concat([Buffer.from(jsonLine), buf.slice(0, 1)]);
626
+ const part2 = Buffer.concat([buf.slice(1)]);
627
+ process.stdout.write(part1);
628
+ process.stdout.write(part2, undefined, () => {});
629
+ return;
630
+ }
631
+ if (scenario === 'stream-json-timeout') {
632
+ process.stdout.write('{"type":"init"}\\n');
633
+ setTimeout(() => {}, 5000);
634
+ return;
635
+ }
636
+ if (scenario === 'stream-json-requested-exit-then-result') {
637
+ process.stdout.write('{"type":"result"}\\n');
638
+ process.exit(0);
639
+ return;
640
+ }
641
+ process.stdout.write('ok');
642
+ }`;
643
+ }
644
+
645
+ function runScenario({ printMode, stdinInherit, scenario, extraArgs }) {
646
+ const tmpBase = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-stdin-test-'));
647
+ const sourceBin = path.join(tmpBase, 'fake-source.js');
648
+ const fixtureSource = buildScenarioFixtureSource();
649
+ fs.writeFileSync(sourceBin, fixtureSource, 'utf8');
650
+ const entryJsOffset = 0;
651
+ const entryEndOffset = Buffer.byteLength(fixtureSource, 'utf8');
652
+ const workdir = path.join(tmpBase, 'workdir');
653
+ fs.mkdirSync(workdir, { recursive: true });
654
+
655
+ const env = {
656
+ ...process.env,
657
+ SOURCE_BIN: sourceBin,
658
+ WORKDIR: workdir,
659
+ ENTRY_JS_OFFSET: String(entryJsOffset),
660
+ ENTRY_END_OFFSET: String(entryEndOffset),
661
+ CURRENT_CLAUDE_VERSION: '2.1.220',
662
+ CLAUDE_TERMUX_PACKAGE_DIR: path.join(__dirname, '..'),
663
+ MAGI_ENV: '1',
664
+ CLAUDE_TERMUX_PRINT_WAIT_MS: '300',
665
+ TMPDIR: tmpBase,
666
+ TEST_SCENARIO: scenario,
667
+ };
668
+ if (stdinInherit) env.CLAUDE_TERMUX_STDIN = 'inherit';
669
+ else delete env.CLAUDE_TERMUX_STDIN;
670
+
671
+ const args = printMode ? ['-p', 'x', ...(extraArgs || [])] : [];
672
+ const start = Date.now();
673
+ const result = child_process.spawnSync('sh', [scriptPath, ...args], {
674
+ env,
675
+ input: 'test input\n',
676
+ encoding: 'utf8',
677
+ timeout: 10000,
678
+ });
679
+ const elapsedMs = Date.now() - start;
680
+ return { ...result, elapsedMs, tmpBase };
681
+ }
682
+
683
+ test('helper and bootstrap intercept process.kill(self, SIGKILL) statically', () => {
684
+ const helperBlock = extractBlock('cat <<\'NODE\' > "$_helper"', '\n export ENABLE_CLAUDEAI_MCP_SERVERS=');
685
+ const bootstrapBlock = extractBlock('cat <<\'NODE\' > "$_bootstrap"', '\n export ENABLE_CLAUDEAI_MCP_SERVERS=');
686
+
687
+ // Check helper branch
688
+ const helperOriginalKillIdx = helperBlock.indexOf('const originalKill = process.kill;');
689
+ assert.ok(helperOriginalKillIdx > -1, 'helper: missing originalKill declaration');
690
+
691
+ const helperProcessKillIdx = helperBlock.indexOf('process.kill = (pid, signal) => {', helperOriginalKillIdx);
692
+ assert.ok(helperProcessKillIdx > helperOriginalKillIdx, 'helper: process.kill override must come after originalKill declaration');
693
+
694
+ const helperRestoreIdx = helperBlock.indexOf('process.kill = originalKill;', helperProcessKillIdx);
695
+ assert.ok(helperRestoreIdx > helperProcessKillIdx, 'helper: process.kill restoration must come after override');
696
+
697
+ // Verify SIGKILL check within the override
698
+ const helperKillOverrideEnd = helperBlock.indexOf('};', helperProcessKillIdx);
699
+ const helperKillOverride = helperBlock.slice(helperProcessKillIdx, helperKillOverrideEnd);
700
+ assert.ok(helperKillOverride.includes('signal === \'SIGKILL\''), 'helper: process.kill must check for SIGKILL string');
701
+ assert.ok(helperKillOverride.includes('signal === 9'), 'helper: process.kill must check for SIGKILL numeric value');
702
+ assert.ok(helperKillOverride.includes('throw new RequestedExit'), 'helper: process.kill must throw RequestedExit for self SIGKILL');
703
+
704
+ // Check bootstrap branch
705
+ const bootstrapOriginalKillIdx = bootstrapBlock.indexOf('const originalKill = process.kill;');
706
+ assert.ok(bootstrapOriginalKillIdx > -1, 'bootstrap: missing originalKill declaration');
707
+
708
+ const bootstrapProcessKillIdx = bootstrapBlock.indexOf('process.kill = (pid, signal) => {', bootstrapOriginalKillIdx);
709
+ assert.ok(bootstrapProcessKillIdx > bootstrapOriginalKillIdx, 'bootstrap: process.kill override must come after originalKill declaration');
710
+
711
+ const bootstrapRestoreIdx = bootstrapBlock.indexOf('process.kill = originalKill;', bootstrapProcessKillIdx);
712
+ assert.ok(bootstrapRestoreIdx > bootstrapProcessKillIdx, 'bootstrap: process.kill restoration must come after override');
713
+
714
+ // Verify SIGKILL check within the override
715
+ const bootstrapKillOverrideEnd = bootstrapBlock.indexOf('};', bootstrapProcessKillIdx);
716
+ const bootstrapKillOverride = bootstrapBlock.slice(bootstrapProcessKillIdx, bootstrapKillOverrideEnd);
717
+ assert.ok(bootstrapKillOverride.includes('signal === \'SIGKILL\''), 'bootstrap: process.kill must check for SIGKILL string');
718
+ assert.ok(bootstrapKillOverride.includes('signal === 9'), 'bootstrap: process.kill must check for SIGKILL numeric value');
719
+ assert.ok(bootstrapKillOverride.includes('throw new RequestedExit'), 'bootstrap: process.kill must throw RequestedExit for self SIGKILL');
720
+ });
721
+
722
+ test('helper branch (CLI -p, no stdin inherit): normal/sync-exit/async-exit all produce output', () => {
723
+ for (const scenario of ['normal', 'sync-exit', 'async-exit']) {
724
+ const r = runScenario({ printMode: true, stdinInherit: false, scenario });
725
+ try {
726
+ assert.ok((r.stdout || '').includes('ok'), `scenario=${scenario} stdout=${r.stdout} stderr=${r.stderr}`);
727
+ assert.equal(r.status, 0, `scenario=${scenario} status=${r.status} stderr=${r.stderr}`);
728
+ } finally {
729
+ fs.rmSync(r.tmpBase, { recursive: true, force: true });
730
+ }
731
+ }
732
+ });
733
+
734
+ test('bootstrap branch (-p + CLAUDE_TERMUX_STDIN=inherit): normal/sync-exit/async-exit all produce output', () => {
735
+ for (const scenario of ['normal', 'sync-exit', 'async-exit']) {
736
+ const r = runScenario({ printMode: true, stdinInherit: true, scenario });
737
+ try {
738
+ assert.ok((r.stdout || '').includes('ok'), `scenario=${scenario} stdout=${r.stdout} stderr=${r.stderr}`);
739
+ assert.equal(r.status, 0, `scenario=${scenario} status=${r.status} stderr=${r.stderr}`);
740
+ if (scenario === 'async-exit') {
741
+ assert.ok(r.elapsedMs >= 250, `expected wait >= 250ms, got ${r.elapsedMs}ms`);
742
+ }
743
+ } finally {
744
+ fs.rmSync(r.tmpBase, { recursive: true, force: true });
745
+ }
746
+ }
747
+ });
748
+
749
+ test('helper branch intercepts self-directed SIGKILL (string signal) and exits with proper code', () => {
750
+ const r = runScenario({ printMode: true, stdinInherit: false, scenario: 'self-sigkill-fallback-string' });
751
+ try {
752
+ assert.equal(r.status, 17, `expected status 17, got ${r.status}; stderr=${r.stderr}`);
753
+ assert.ok(!r.signal, `expected clean exit (no signal), but got signal: ${r.signal}`);
754
+ } finally {
755
+ fs.rmSync(r.tmpBase, { recursive: true, force: true });
756
+ }
757
+ });
758
+
759
+ test('helper branch intercepts self-directed SIGKILL (numeric signal 9) and exits with proper code', () => {
760
+ const r = runScenario({ printMode: true, stdinInherit: false, scenario: 'self-sigkill-fallback-numeric' });
761
+ try {
762
+ assert.equal(r.status, 17, `expected status 17, got ${r.status}; stderr=${r.stderr}`);
763
+ assert.ok(!r.signal, `expected clean exit (no signal), but got signal: ${r.signal}`);
764
+ } finally {
765
+ fs.rmSync(r.tmpBase, { recursive: true, force: true });
766
+ }
767
+ });
768
+
769
+ test('helper branch intercepts self-directed SIGKILL with process.exit() (no code) and defaults to 0', () => {
770
+ const r = runScenario({ printMode: true, stdinInherit: false, scenario: 'self-sigkill-fallback-no-code' });
771
+ try {
772
+ assert.equal(r.status, 0, `expected status 0, got ${r.status}; stderr=${r.stderr}`);
773
+ assert.ok(!r.signal, `expected clean exit (no signal), but got signal: ${r.signal}`);
774
+ } finally {
775
+ fs.rmSync(r.tmpBase, { recursive: true, force: true });
776
+ }
777
+ });
778
+
779
+ test('helper branch allows process.kill to other processes (signal 0, passthrough)', () => {
780
+ const r = runScenario({ printMode: true, stdinInherit: false, scenario: 'other-process-kill' });
781
+ try {
782
+ assert.ok((r.stdout || '').includes('ok'), `expected ok output, got stdout=${r.stdout} stderr=${r.stderr}`);
783
+ assert.equal(r.status, 0, `expected status 0, got ${r.status}; stderr=${r.stderr}`);
784
+ } finally {
785
+ fs.rmSync(r.tmpBase, { recursive: true, force: true });
786
+ }
787
+ });
788
+
789
+ test('isStreamJsonPrintMode detects print flag and stream-json format (case 1: -p + format + value)', () => {
790
+ const helperBlock = extractBlock('cat <<\'NODE\' > "$_helper"', '\n export ENABLE_CLAUDEAI_MCP_SERVERS=');
791
+ const fnSource = extractFunction(helperBlock, 'function isStreamJsonPrintMode(argv) {', '\n\nclass RequestedExit');
792
+ const context = vm.createContext({ module: { exports: {} } });
793
+ vm.runInContext(`${fnSource}\nmodule.exports = isStreamJsonPrintMode;`, context);
794
+ const isStreamJsonPrintMode = context.module.exports;
795
+
796
+ assert.equal(isStreamJsonPrintMode(['-p', 'hello', '--output-format', 'stream-json']), true, 'case 1');
797
+ assert.equal(isStreamJsonPrintMode(['-p', 'hello', '--output-format=stream-json']), true, 'case 2');
798
+ assert.equal(isStreamJsonPrintMode(['--print', '--output-format=stream-json']), true, 'case 3');
799
+ assert.equal(isStreamJsonPrintMode(['-p', 'hello', '--output-format', 'json']), false, 'case 4');
800
+ assert.equal(isStreamJsonPrintMode(['-p']), false, 'case 5');
801
+ assert.equal(isStreamJsonPrintMode(['--output-format=stream-json']), false, 'case 6');
802
+ assert.equal(isStreamJsonPrintMode(['-p', '--', '--output-format=stream-json']), false, 'case 7');
803
+ assert.equal(isStreamJsonPrintMode(['-p', '--output-format=stream-json', '--', 'extra']), true, 'case 8');
804
+ assert.equal(isStreamJsonPrintMode(['-p', '--output-format', '--', 'stream-json']), false, 'case 9');
805
+ assert.equal(isStreamJsonPrintMode(['-p', '--output-format']), false, 'case 10');
806
+ assert.equal(isStreamJsonPrintMode([]), false, 'case 11');
807
+ });
808
+
809
+ test('isStreamJsonPrintMode is identical in helper and bootstrap', () => {
810
+ const helperBlock = extractBlock('cat <<\'NODE\' > "$_helper"', '\n export ENABLE_CLAUDEAI_MCP_SERVERS=');
811
+ const bootstrapBlock = extractBlock('cat <<\'NODE\' > "$_bootstrap"', '\n export ENABLE_CLAUDEAI_MCP_SERVERS=');
812
+
813
+ const helperFn = extractFunction(helperBlock, 'function isStreamJsonPrintMode(argv) {', '\n\nclass RequestedExit');
814
+ const bootstrapFn = extractFunction(bootstrapBlock, 'function isStreamJsonPrintMode(argv) {', '\n\nclass RequestedExit');
815
+
816
+ assert.equal(helperFn, bootstrapFn, 'isStreamJsonPrintMode must be identical in both heredocs');
817
+ });
818
+
819
+ test('CLAUDE_TERMUX_PRINT_RESULT_TIMEOUT_MS fallback to 300000 on NaN', () => {
820
+ const tmpBase = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-timeout-test-'));
821
+ try {
822
+ const sourceBin = path.join(tmpBase, 'fake-source.js');
823
+ const fixtureSource = buildScenarioFixtureSource();
824
+ fs.writeFileSync(sourceBin, fixtureSource, 'utf8');
825
+ const entryJsOffset = 0;
826
+ const entryEndOffset = Buffer.byteLength(fixtureSource, 'utf8');
827
+ const workdir = path.join(tmpBase, 'workdir');
828
+ fs.mkdirSync(workdir, { recursive: true });
829
+
830
+ const env = {
831
+ ...process.env,
832
+ SOURCE_BIN: sourceBin,
833
+ WORKDIR: workdir,
834
+ ENTRY_JS_OFFSET: String(entryJsOffset),
835
+ ENTRY_END_OFFSET: String(entryEndOffset),
836
+ CURRENT_CLAUDE_VERSION: '2.1.220',
837
+ CLAUDE_TERMUX_PACKAGE_DIR: path.join(__dirname, '..'),
838
+ MAGI_ENV: '1',
839
+ CLAUDE_TERMUX_PRINT_WAIT_MS: '300',
840
+ CLAUDE_TERMUX_PRINT_RESULT_TIMEOUT_MS: 'invalid',
841
+ TMPDIR: tmpBase,
842
+ TEST_SCENARIO: 'stream-json-result',
843
+ };
844
+ delete env.CLAUDE_TERMUX_STDIN;
845
+
846
+ const result = child_process.spawnSync('sh', [scriptPath, '-p', 'x', '--output-format=stream-json'], {
847
+ env,
848
+ input: 'test input\n',
849
+ encoding: 'utf8',
850
+ timeout: 10000,
851
+ });
852
+ assert.equal(result.status, 0, `expected successful exit with invalid timeout fallback, got status=${result.status} stderr=${result.stderr}`);
853
+ } finally {
854
+ fs.rmSync(tmpBase, { recursive: true, force: true });
855
+ }
856
+ });
857
+
858
+ test('CLAUDE_TERMUX_PRINT_RESULT_TIMEOUT_MS fallback to 300000 on zero', () => {
859
+ const tmpBase = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-timeout-test-'));
860
+ try {
861
+ const sourceBin = path.join(tmpBase, 'fake-source.js');
862
+ const fixtureSource = buildScenarioFixtureSource();
863
+ fs.writeFileSync(sourceBin, fixtureSource, 'utf8');
864
+ const entryJsOffset = 0;
865
+ const entryEndOffset = Buffer.byteLength(fixtureSource, 'utf8');
866
+ const workdir = path.join(tmpBase, 'workdir');
867
+ fs.mkdirSync(workdir, { recursive: true });
868
+
869
+ const env = {
870
+ ...process.env,
871
+ SOURCE_BIN: sourceBin,
872
+ WORKDIR: workdir,
873
+ ENTRY_JS_OFFSET: String(entryJsOffset),
874
+ ENTRY_END_OFFSET: String(entryEndOffset),
875
+ CURRENT_CLAUDE_VERSION: '2.1.220',
876
+ CLAUDE_TERMUX_PACKAGE_DIR: path.join(__dirname, '..'),
877
+ MAGI_ENV: '1',
878
+ CLAUDE_TERMUX_PRINT_WAIT_MS: '300',
879
+ CLAUDE_TERMUX_PRINT_RESULT_TIMEOUT_MS: '0',
880
+ TMPDIR: tmpBase,
881
+ TEST_SCENARIO: 'stream-json-result',
882
+ };
883
+ delete env.CLAUDE_TERMUX_STDIN;
884
+
885
+ const result = child_process.spawnSync('sh', [scriptPath, '-p', 'x', '--output-format=stream-json'], {
886
+ env,
887
+ input: 'test input\n',
888
+ encoding: 'utf8',
889
+ timeout: 10000,
890
+ });
891
+ assert.equal(result.status, 0, `expected successful exit with zero timeout fallback, got status=${result.status} stderr=${result.stderr}`);
892
+ } finally {
893
+ fs.rmSync(tmpBase, { recursive: true, force: true });
894
+ }
895
+ });
896
+
897
+ test('helper branch (CLI -p, no stdin inherit) stream-json: result detected -> exits immediately without waiting for timeout', () => {
898
+ const tmpBase = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-stream-json-clear-timeout-'));
899
+ try {
900
+ const sourceBin = path.join(tmpBase, 'fake-source.js');
901
+ const fixtureSource = buildScenarioFixtureSource();
902
+ fs.writeFileSync(sourceBin, fixtureSource, 'utf8');
903
+ const entryJsOffset = 0;
904
+ const entryEndOffset = Buffer.byteLength(fixtureSource, 'utf8');
905
+ const workdir = path.join(tmpBase, 'workdir');
906
+ fs.mkdirSync(workdir, { recursive: true });
907
+
908
+ const env = {
909
+ ...process.env,
910
+ SOURCE_BIN: sourceBin,
911
+ WORKDIR: workdir,
912
+ ENTRY_JS_OFFSET: String(entryJsOffset),
913
+ ENTRY_END_OFFSET: String(entryEndOffset),
914
+ CURRENT_CLAUDE_VERSION: '2.1.220',
915
+ CLAUDE_TERMUX_PACKAGE_DIR: path.join(__dirname, '..'),
916
+ MAGI_ENV: '1',
917
+ CLAUDE_TERMUX_PRINT_WAIT_MS: '300',
918
+ CLAUDE_TERMUX_PRINT_RESULT_TIMEOUT_MS: '10000',
919
+ TMPDIR: tmpBase,
920
+ TEST_SCENARIO: 'stream-json-result',
921
+ };
922
+ delete env.CLAUDE_TERMUX_STDIN;
923
+
924
+ const start = Date.now();
925
+ const result = child_process.spawnSync('sh', [scriptPath, '-p', 'x', '--output-format=stream-json'], {
926
+ env,
927
+ input: 'test input\n',
928
+ encoding: 'utf8',
929
+ timeout: 20000,
930
+ });
931
+ const elapsedMs = Date.now() - start;
932
+
933
+ assert.equal(result.status, 0, `expected successful exit, got status=${result.status} stderr=${result.stderr}`);
934
+ assert.ok(
935
+ elapsedMs < 3000,
936
+ `expected process to exit quickly after result detected, but elapsed=${elapsedMs}ms (should be < 3000ms with 10000ms timeout). This indicates the timeout timer was not cleared.`
937
+ );
938
+ } finally {
939
+ fs.rmSync(tmpBase, { recursive: true, force: true });
940
+ }
941
+ });
942
+
943
+ test('bootstrap branch (-p + CLAUDE_TERMUX_STDIN=inherit) stream-json: result detected -> exits immediately without waiting for timeout', () => {
944
+ const tmpBase = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-stream-json-clear-timeout-bootstrap-'));
945
+ try {
946
+ const sourceBin = path.join(tmpBase, 'fake-source.js');
947
+ const fixtureSource = buildScenarioFixtureSource();
948
+ fs.writeFileSync(sourceBin, fixtureSource, 'utf8');
949
+ const entryJsOffset = 0;
950
+ const entryEndOffset = Buffer.byteLength(fixtureSource, 'utf8');
951
+ const workdir = path.join(tmpBase, 'workdir');
952
+ fs.mkdirSync(workdir, { recursive: true });
953
+
954
+ const env = {
955
+ ...process.env,
956
+ SOURCE_BIN: sourceBin,
957
+ WORKDIR: workdir,
958
+ ENTRY_JS_OFFSET: String(entryJsOffset),
959
+ ENTRY_END_OFFSET: String(entryEndOffset),
960
+ CURRENT_CLAUDE_VERSION: '2.1.220',
961
+ CLAUDE_TERMUX_PACKAGE_DIR: path.join(__dirname, '..'),
962
+ MAGI_ENV: '1',
963
+ CLAUDE_TERMUX_STDIN: 'inherit',
964
+ CLAUDE_TERMUX_PRINT_WAIT_MS: '300',
965
+ CLAUDE_TERMUX_PRINT_RESULT_TIMEOUT_MS: '10000',
966
+ TMPDIR: tmpBase,
967
+ TEST_SCENARIO: 'stream-json-result',
968
+ };
969
+
970
+ const start = Date.now();
971
+ const result = child_process.spawnSync('sh', [scriptPath, '-p', 'x', '--output-format=stream-json'], {
972
+ env,
973
+ input: 'test input\n',
974
+ encoding: 'utf8',
975
+ timeout: 20000,
976
+ });
977
+ const elapsedMs = Date.now() - start;
978
+
979
+ assert.equal(result.status, 0, `expected successful exit, got status=${result.status} stderr=${result.stderr}`);
980
+ assert.ok(
981
+ elapsedMs < 3000,
982
+ `expected process to exit quickly after result detected, but elapsed=${elapsedMs}ms (should be < 3000ms with 10000ms timeout). This indicates the timeout timer was not cleared.`
983
+ );
984
+ } finally {
985
+ fs.rmSync(tmpBase, { recursive: true, force: true });
986
+ }
987
+ });
988
+
989
+ test('helper and bootstrap installStreamJsonTerminalWatcher helpers stay identical', () => {
990
+ const helperBlock = extractBlock('cat <<\'NODE\' > "$_helper"', '\n export ENABLE_CLAUDEAI_MCP_SERVERS=');
991
+ const bootstrapBlock = extractBlock('cat <<\'NODE\' > "$_bootstrap"', '\n export ENABLE_CLAUDEAI_MCP_SERVERS=');
992
+
993
+ const helperWatcher = extractFunction(
994
+ helperBlock,
995
+ 'function installStreamJsonTerminalWatcher() {',
996
+ '\n function forceTimeoutExit',
997
+ );
998
+ const bootstrapWatcher = extractFunction(
999
+ bootstrapBlock,
1000
+ 'function installStreamJsonTerminalWatcher() {',
1001
+ '\n function forceTimeoutExit',
1002
+ );
1003
+
1004
+ assert.equal(helperWatcher, bootstrapWatcher, 'installStreamJsonTerminalWatcher must be identical in both heredocs');
1005
+ });
1006
+
1007
+ test('helper and bootstrap forceTimeoutExit helpers stay identical', () => {
1008
+ const helperBlock = extractBlock('cat <<\'NODE\' > "$_helper"', '\n export ENABLE_CLAUDEAI_MCP_SERVERS=');
1009
+ const bootstrapBlock = extractBlock('cat <<\'NODE\' > "$_bootstrap"', '\n export ENABLE_CLAUDEAI_MCP_SERVERS=');
1010
+
1011
+ const helperForceExit = extractFunction(
1012
+ helperBlock,
1013
+ 'function forceTimeoutExit(exitCode) {',
1014
+ '\n async function waitForPrintFlush',
1015
+ );
1016
+ const bootstrapForceExit = extractFunction(
1017
+ bootstrapBlock,
1018
+ 'function forceTimeoutExit(exitCode) {',
1019
+ '\n async function waitForPrintFlushIfNeeded',
1020
+ );
1021
+
1022
+ assert.equal(helperForceExit, bootstrapForceExit, 'forceTimeoutExit must be identical in both heredocs');
1023
+ });
1024
+
1025
+ test('installStreamJsonTerminalWatcher restores process.stdout.write own property state', () => {
1026
+ // Test with the real process.stdout to verify own property handling
1027
+ const hadOwnPropertyBefore = Object.prototype.hasOwnProperty.call(process.stdout, 'write');
1028
+ const descriptorBefore = hadOwnPropertyBefore ? Object.getOwnPropertyDescriptor(process.stdout, 'write') : undefined;
1029
+
1030
+ // Get the watcher function from helper
1031
+ const helperBlock = extractBlock('cat <<\'NODE\' > "$_helper"', '\n export ENABLE_CLAUDEAI_MCP_SERVERS=');
1032
+ const watcherSource = extractFunction(helperBlock, 'function installStreamJsonTerminalWatcher() {', '\n function forceTimeoutExit');
1033
+
1034
+ const context = vm.createContext({
1035
+ module: { exports: {} },
1036
+ process,
1037
+ Object,
1038
+ Buffer,
1039
+ require: (id) => {
1040
+ if (id === 'string_decoder') return require('string_decoder');
1041
+ throw new Error('require not available');
1042
+ },
1043
+ });
1044
+
1045
+ vm.runInContext(`
1046
+ ${watcherSource}
1047
+ module.exports = installStreamJsonTerminalWatcher;
1048
+ `, context);
1049
+
1050
+ const installStreamJsonTerminalWatcher = context.module.exports;
1051
+
1052
+ try {
1053
+ // Test case 1: Normal case where write is not an own property
1054
+ {
1055
+ const watcher = installStreamJsonTerminalWatcher();
1056
+ const hadOwnPropertyAfterInstall = Object.prototype.hasOwnProperty.call(process.stdout, 'write');
1057
+ assert.equal(hadOwnPropertyAfterInstall, true, 'after install: process.stdout.write should be own property');
1058
+
1059
+ // Restore watcher
1060
+ watcher.restore();
1061
+ const hadOwnPropertyAfterRestore = Object.prototype.hasOwnProperty.call(process.stdout, 'write');
1062
+ assert.equal(hadOwnPropertyAfterRestore, hadOwnPropertyBefore, 'after restore: own property state should match initial');
1063
+
1064
+ // Verify restore is idempotent
1065
+ watcher.restore();
1066
+ const hadOwnPropertyAfterSecondRestore = Object.prototype.hasOwnProperty.call(process.stdout, 'write');
1067
+ assert.equal(hadOwnPropertyAfterSecondRestore, hadOwnPropertyBefore, 'second restore should also maintain initial state');
1068
+ }
1069
+
1070
+ // Test case 2: When write is an own property before installation
1071
+ {
1072
+ const testDescriptor = {
1073
+ value: function testWrite() { return true; },
1074
+ writable: true,
1075
+ configurable: true,
1076
+ enumerable: false,
1077
+ };
1078
+ Object.defineProperty(process.stdout, 'write', testDescriptor);
1079
+
1080
+ const watcher = installStreamJsonTerminalWatcher();
1081
+ const hadOwnAfterInstall = Object.prototype.hasOwnProperty.call(process.stdout, 'write');
1082
+ assert.equal(hadOwnAfterInstall, true, 'test case 2: after install should have own property');
1083
+
1084
+ watcher.restore();
1085
+ const hadOwnAfterRestore = Object.prototype.hasOwnProperty.call(process.stdout, 'write');
1086
+ assert.equal(hadOwnAfterRestore, true, 'test case 2: after restore should still have own property');
1087
+
1088
+ const restoredDescriptor = Object.getOwnPropertyDescriptor(process.stdout, 'write');
1089
+ assert.equal(typeof restoredDescriptor.value, 'function', 'test case 2: restored value should be a function');
1090
+ assert.equal(restoredDescriptor.configurable, true, 'test case 2: restored configurable should match');
1091
+ }
1092
+ } finally {
1093
+ // Ensure stdout.write is fully restored to original state
1094
+ if (hadOwnPropertyBefore && descriptorBefore) {
1095
+ Object.defineProperty(process.stdout, 'write', descriptorBefore);
1096
+ } else if (Object.prototype.hasOwnProperty.call(process.stdout, 'write')) {
1097
+ delete process.stdout.write;
1098
+ }
1099
+ }
1100
+ });
1101
+
1102
+ test('helper branch stream-json result detection (single write)', () => {
1103
+ const r = runScenario({
1104
+ printMode: true,
1105
+ stdinInherit: false,
1106
+ scenario: 'stream-json-result',
1107
+ extraArgs: ['--output-format=stream-json'],
1108
+ });
1109
+ try {
1110
+ assert.equal(r.status, 0, `expected status 0, got ${r.status}; stderr=${r.stderr}`);
1111
+ // Result detection should be significantly faster than traditional PRINT_WAIT_MS (300ms in tests)
1112
+ assert.ok(r.elapsedMs < 1500, `expected completion < 1500ms (much faster than 300ms PRINT_WAIT_MS), got ${r.elapsedMs}ms`);
1113
+ } finally {
1114
+ fs.rmSync(r.tmpBase, { recursive: true, force: true });
1115
+ }
1116
+ });
1117
+
1118
+ test('bootstrap branch stream-json result detection (single write)', () => {
1119
+ const r = runScenario({
1120
+ printMode: true,
1121
+ stdinInherit: true,
1122
+ scenario: 'stream-json-result',
1123
+ extraArgs: ['--output-format=stream-json'],
1124
+ });
1125
+ try {
1126
+ assert.equal(r.status, 0, `expected status 0, got ${r.status}; stderr=${r.stderr}`);
1127
+ // Result detection should be significantly faster than traditional PRINT_WAIT_MS (300ms in tests)
1128
+ assert.ok(r.elapsedMs < 1500, `expected completion < 1500ms (much faster than 300ms PRINT_WAIT_MS), got ${r.elapsedMs}ms`);
1129
+ } finally {
1130
+ fs.rmSync(r.tmpBase, { recursive: true, force: true });
1131
+ }
1132
+ });
1133
+
1134
+ test('helper branch stream-json multibyte character split handling', () => {
1135
+ const r = runScenario({
1136
+ printMode: true,
1137
+ stdinInherit: false,
1138
+ scenario: 'stream-json-multibyte-split',
1139
+ extraArgs: ['--output-format=stream-json'],
1140
+ });
1141
+ try {
1142
+ assert.equal(r.status, 0, `expected status 0, got ${r.status}; stderr=${r.stderr}`);
1143
+ assert.ok(r.elapsedMs < 6000, `expected completion < 6000ms, got ${r.elapsedMs}ms`);
1144
+ } finally {
1145
+ fs.rmSync(r.tmpBase, { recursive: true, force: true });
1146
+ }
1147
+ });
1148
+
1149
+ test('bootstrap branch stream-json multibyte character split handling', () => {
1150
+ const r = runScenario({
1151
+ printMode: true,
1152
+ stdinInherit: true,
1153
+ scenario: 'stream-json-multibyte-split',
1154
+ extraArgs: ['--output-format=stream-json'],
1155
+ });
1156
+ try {
1157
+ assert.equal(r.status, 0, `expected status 0, got ${r.status}; stderr=${r.stderr}`);
1158
+ assert.ok(r.elapsedMs < 6000, `expected completion < 6000ms, got ${r.elapsedMs}ms`);
1159
+ } finally {
1160
+ fs.rmSync(r.tmpBase, { recursive: true, force: true });
1161
+ }
1162
+ });
1163
+
1164
+ test('helper branch stream-json timeout triggers exit with status 1', () => {
1165
+ const tmpBase = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-stream-timeout-'));
1166
+ try {
1167
+ const sourceBin = path.join(tmpBase, 'fake-source.js');
1168
+ const fixtureSource = buildScenarioFixtureSource();
1169
+ fs.writeFileSync(sourceBin, fixtureSource, 'utf8');
1170
+ const entryJsOffset = 0;
1171
+ const entryEndOffset = Buffer.byteLength(fixtureSource, 'utf8');
1172
+ const workdir = path.join(tmpBase, 'workdir');
1173
+ fs.mkdirSync(workdir, { recursive: true });
1174
+
1175
+ const env = {
1176
+ ...process.env,
1177
+ SOURCE_BIN: sourceBin,
1178
+ WORKDIR: workdir,
1179
+ ENTRY_JS_OFFSET: String(entryJsOffset),
1180
+ ENTRY_END_OFFSET: String(entryEndOffset),
1181
+ CURRENT_CLAUDE_VERSION: '2.1.220',
1182
+ CLAUDE_TERMUX_PACKAGE_DIR: path.join(__dirname, '..'),
1183
+ MAGI_ENV: '1',
1184
+ CLAUDE_TERMUX_PRINT_WAIT_MS: '300',
1185
+ CLAUDE_TERMUX_PRINT_RESULT_TIMEOUT_MS: '300',
1186
+ TMPDIR: tmpBase,
1187
+ TEST_SCENARIO: 'stream-json-timeout',
1188
+ };
1189
+ delete env.CLAUDE_TERMUX_STDIN;
1190
+
1191
+ const result = child_process.spawnSync('sh', [scriptPath, '-p', 'x', '--output-format=stream-json'], {
1192
+ env,
1193
+ input: 'test input\n',
1194
+ encoding: 'utf8',
1195
+ timeout: 5000,
1196
+ });
1197
+
1198
+ assert.equal(result.status, 1, `expected status 1 on timeout, got ${result.status}; stderr=${result.stderr}`);
1199
+ const entries = fs.readdirSync(workdir, { withFileTypes: true });
1200
+ const entryFiles = entries.filter(e => e.name.includes('cli.') && e.name.endsWith('.bare-path.js'));
1201
+ assert.equal(entryFiles.length, 0, `expected no extracted entry files after timeout, found ${entryFiles.length}`);
1202
+ } finally {
1203
+ fs.rmSync(tmpBase, { recursive: true, force: true });
1204
+ }
1205
+ });
1206
+
1207
+ test('bootstrap branch stream-json timeout triggers exit with status 1', () => {
1208
+ const tmpBase = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-stream-timeout-'));
1209
+ try {
1210
+ const sourceBin = path.join(tmpBase, 'fake-source.js');
1211
+ const fixtureSource = buildScenarioFixtureSource();
1212
+ fs.writeFileSync(sourceBin, fixtureSource, 'utf8');
1213
+ const entryJsOffset = 0;
1214
+ const entryEndOffset = Buffer.byteLength(fixtureSource, 'utf8');
1215
+ const workdir = path.join(tmpBase, 'workdir');
1216
+ fs.mkdirSync(workdir, { recursive: true });
1217
+
1218
+ const env = {
1219
+ ...process.env,
1220
+ SOURCE_BIN: sourceBin,
1221
+ WORKDIR: workdir,
1222
+ ENTRY_JS_OFFSET: String(entryJsOffset),
1223
+ ENTRY_END_OFFSET: String(entryEndOffset),
1224
+ CURRENT_CLAUDE_VERSION: '2.1.220',
1225
+ CLAUDE_TERMUX_PACKAGE_DIR: path.join(__dirname, '..'),
1226
+ MAGI_ENV: '1',
1227
+ CLAUDE_TERMUX_PRINT_WAIT_MS: '300',
1228
+ CLAUDE_TERMUX_PRINT_RESULT_TIMEOUT_MS: '300',
1229
+ CLAUDE_TERMUX_STDIN: 'inherit',
1230
+ TMPDIR: tmpBase,
1231
+ TEST_SCENARIO: 'stream-json-timeout',
1232
+ };
1233
+
1234
+ const result = child_process.spawnSync('sh', [scriptPath, '-p', 'x', '--output-format=stream-json'], {
1235
+ env,
1236
+ input: 'test input\n',
1237
+ encoding: 'utf8',
1238
+ timeout: 5000,
1239
+ });
1240
+
1241
+ assert.equal(result.status, 1, `expected status 1 on timeout, got ${result.status}; stderr=${result.stderr}`);
1242
+ const entries = fs.readdirSync(workdir, { withFileTypes: true });
1243
+ const entryFiles = entries.filter(e => e.name.includes('cli.') && e.name.endsWith('.bare-path.js'));
1244
+ assert.equal(entryFiles.length, 0, `expected no extracted entry files after timeout, found ${entryFiles.length}`);
1245
+ } finally {
1246
+ fs.rmSync(tmpBase, { recursive: true, force: true });
1247
+ }
1248
+ });
1249
+
1250
+ test('helper branch stream-json requested exit after result', () => {
1251
+ const r = runScenario({
1252
+ printMode: true,
1253
+ stdinInherit: false,
1254
+ scenario: 'stream-json-requested-exit-then-result',
1255
+ extraArgs: ['--output-format=stream-json'],
1256
+ });
1257
+ try {
1258
+ assert.equal(r.status, 0, `expected status 0, got ${r.status}; stderr=${r.stderr}`);
1259
+ } finally {
1260
+ fs.rmSync(r.tmpBase, { recursive: true, force: true });
1261
+ }
1262
+ });
1263
+
1264
+ test('bootstrap branch stream-json requested exit after result', () => {
1265
+ const r = runScenario({
1266
+ printMode: true,
1267
+ stdinInherit: true,
1268
+ scenario: 'stream-json-requested-exit-then-result',
1269
+ extraArgs: ['--output-format=stream-json'],
1270
+ });
1271
+ try {
1272
+ assert.equal(r.status, 0, `expected status 0, got ${r.status}; stderr=${r.stderr}`);
1273
+ } finally {
1274
+ fs.rmSync(r.tmpBase, { recursive: true, force: true });
1275
+ }
1276
+ });
1277
+
1278
+ test('installStreamJsonTerminalWatcher waits for write callback before completing result', async () => {
1279
+ const helperBlock = extractBlock('cat <<\'NODE\' > "$_helper"', '\n export ENABLE_CLAUDEAI_MCP_SERVERS=');
1280
+ const watcherSource = extractFunction(helperBlock, 'function installStreamJsonTerminalWatcher() {', '\n function forceTimeoutExit');
1281
+
1282
+ // Create a dedicated mock stdout object instead of modifying the real one
1283
+ let callbackFired = false;
1284
+ const mockStdout = Object.create(Object.getPrototypeOf(process.stdout));
1285
+
1286
+ // Copy necessary properties
1287
+ Object.defineProperty(mockStdout, 'write', {
1288
+ value: function(chunk, encoding, callback) {
1289
+ if (typeof encoding === 'function') {
1290
+ callback = encoding;
1291
+ encoding = undefined;
1292
+ }
1293
+ if (callback) {
1294
+ // Defer callback to next microtask
1295
+ setImmediate(() => {
1296
+ callbackFired = true;
1297
+ callback();
1298
+ });
1299
+ }
1300
+ return true;
1301
+ },
1302
+ writable: true,
1303
+ configurable: true,
1304
+ });
1305
+
1306
+ const context = vm.createContext({
1307
+ module: { exports: {} },
1308
+ process: { stdout: mockStdout },
1309
+ Object,
1310
+ Buffer,
1311
+ require: (id) => {
1312
+ if (id === 'string_decoder') return require('string_decoder');
1313
+ throw new Error('require not available');
1314
+ },
1315
+ });
1316
+
1317
+ vm.runInContext(`
1318
+ ${watcherSource}
1319
+ module.exports = installStreamJsonTerminalWatcher;
1320
+ `, context);
1321
+
1322
+ const installStreamJsonTerminalWatcher = context.module.exports;
1323
+ const watcher = installStreamJsonTerminalWatcher();
1324
+
1325
+ // Simulate a write with result JSON
1326
+ const resultJson = '{"type":"result","data":"test"}\n';
1327
+ mockStdout.write(resultJson, 'utf8');
1328
+
1329
+ // Get the promise before callback fires
1330
+ const resultPromise = watcher.waitForResult();
1331
+
1332
+ // Give time for callback to fire
1333
+ await new Promise(resolve => setTimeout(resolve, 50));
1334
+
1335
+ // Promise should now be resolved
1336
+ await resultPromise;
1337
+ assert.ok(callbackFired, 'callback should have been fired');
1338
+
1339
+ watcher.restore();
1340
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bash0816/claude-code",
3
- "version": "2.1.220",
3
+ "version": "2.1.222",
4
4
  "description": "Unofficial Termux-native Claude Code wrapper with audited native replay",
5
5
  "license": "GPL-3.0-only",
6
6
  "bin": {