@bash0816/claude-code 2.1.220-2 → 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.220
40
+ npm install -g @bash0816/claude-code@2.1.220-2
41
41
  ```
42
42
 
43
43
  ## Update / 更新
@@ -598,6 +598,24 @@
598
598
  "entry_end_offset": 265457701,
599
599
  "tarball_integrity": "sha512-VHFI8mKruIntKn7eq81sbyS19/KWmQcmJQsS/C+j9M/E+w0s4UytgsL7DADPjBE/GByNiKoRtLYDMntCjRlOdA==",
600
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",
601
619
  "status": "offset_discovered"
602
620
  }
603
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.220",
5
- "latest_candidate_version": "2.1.220-2",
6
- "previous_stable_version": "2.1.219",
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}`);
@@ -628,6 +644,7 @@ async function main() {
628
644
  const hadGlobalBun = Object.prototype.hasOwnProperty.call(globalThis, 'Bun');
629
645
  const originalGlobalBun = globalThis.Bun;
630
646
  const asyncErrors = [];
647
+ let streamJsonWatcher = null;
631
648
 
632
649
  globalThis.__claudeYaml = createYamlShim();
633
650
  if (!globalThis.__claudeBunShim || typeof globalThis.__claudeBunShim !== 'object') {
@@ -638,7 +655,95 @@ async function main() {
638
655
  asyncErrors.push(error);
639
656
  }
640
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
+
641
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
+ }
642
747
  if (Number.isFinite(printWaitMs) && printWaitMs > 0) {
643
748
  await new Promise(resolve => setTimeout(resolve, printWaitMs));
644
749
  }
@@ -712,6 +817,11 @@ async function main() {
712
817
  }
713
818
  return originalKill.call(process, pid, signal);
714
819
  };
820
+
821
+ if (isStreamJsonPrintMode(argv)) {
822
+ streamJsonWatcher = installStreamJsonTerminalWatcher();
823
+ }
824
+
715
825
  const moduleLike = { exports: {} };
716
826
  const maybePromise = fn(moduleLike.exports, fakeRequire, moduleLike, extractedFile, workdir);
717
827
  if (maybePromise && typeof maybePromise.then === 'function') await maybePromise;
@@ -735,6 +845,9 @@ async function main() {
735
845
  process.argv = originalArgv;
736
846
  process.exit = originalExit;
737
847
  process.kill = originalKill;
848
+ if (streamJsonWatcher) {
849
+ try { streamJsonWatcher.restore(); } catch {}
850
+ }
738
851
  try {
739
852
  if (originalBun === undefined) {
740
853
  delete process.versions.bun;
@@ -786,6 +899,22 @@ const entryJsOffset = Number(process.env.ENTRY_JS_OFFSET);
786
899
  const entryEndOffset = Number(process.env.ENTRY_END_OFFSET);
787
900
  const argv = process.argv.slice(2);
788
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
+
789
918
  class RequestedExit extends Error {
790
919
  constructor(code) {
791
920
  super(`process.exit ${code}`);
@@ -1335,6 +1464,7 @@ async function main() {
1335
1464
  const hadGlobalBun = Object.prototype.hasOwnProperty.call(globalThis, 'Bun');
1336
1465
  const originalGlobalBun = globalThis.Bun;
1337
1466
  const asyncErrors = [];
1467
+ let streamJsonWatcher = null;
1338
1468
 
1339
1469
  globalThis.__claudeYaml = createYamlShim();
1340
1470
  if (!globalThis.__claudeBunShim || typeof globalThis.__claudeBunShim !== 'object') {
@@ -1344,8 +1474,96 @@ async function main() {
1344
1474
  function onAsyncError(error) {
1345
1475
  asyncErrors.push(error);
1346
1476
  }
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
+
1347
1546
  async function waitForPrintFlushIfNeeded() {
1348
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
+ }
1349
1567
  const printWaitMs = Number(process.env.CLAUDE_TERMUX_PRINT_WAIT_MS || 5000);
1350
1568
  if (Number.isFinite(printWaitMs) && printWaitMs > 0) {
1351
1569
  await new Promise(resolve => setTimeout(resolve, printWaitMs));
@@ -1421,6 +1639,10 @@ async function main() {
1421
1639
  return originalKill.call(process, pid, signal);
1422
1640
  };
1423
1641
 
1642
+ if (isStreamJsonPrintMode(argv)) {
1643
+ streamJsonWatcher = installStreamJsonTerminalWatcher();
1644
+ }
1645
+
1424
1646
  const moduleLike = { exports: {} };
1425
1647
  const maybePromise = fn(moduleLike.exports, fakeRequire, moduleLike, extractedFile, workdir);
1426
1648
  if (maybePromise && typeof maybePromise.then === 'function') await maybePromise;
@@ -1439,6 +1661,9 @@ async function main() {
1439
1661
  process.argv = originalArgv;
1440
1662
  process.exit = originalExit;
1441
1663
  process.kill = originalKill;
1664
+ if (streamJsonWatcher) {
1665
+ try { streamJsonWatcher.restore(); } catch {}
1666
+ }
1442
1667
  process.once('exit', () => {
1443
1668
  if (extractedFile) {
1444
1669
  try {
@@ -610,11 +610,39 @@ function buildScenarioFixtureSource() {
610
610
  process.stdout.write('ok');
611
611
  return;
612
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
+ }
613
641
  process.stdout.write('ok');
614
642
  }`;
615
643
  }
616
644
 
617
- function runScenario({ printMode, stdinInherit, scenario }) {
645
+ function runScenario({ printMode, stdinInherit, scenario, extraArgs }) {
618
646
  const tmpBase = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-stdin-test-'));
619
647
  const sourceBin = path.join(tmpBase, 'fake-source.js');
620
648
  const fixtureSource = buildScenarioFixtureSource();
@@ -640,7 +668,7 @@ function runScenario({ printMode, stdinInherit, scenario }) {
640
668
  if (stdinInherit) env.CLAUDE_TERMUX_STDIN = 'inherit';
641
669
  else delete env.CLAUDE_TERMUX_STDIN;
642
670
 
643
- const args = printMode ? ['-p', 'x'] : [];
671
+ const args = printMode ? ['-p', 'x', ...(extraArgs || [])] : [];
644
672
  const start = Date.now();
645
673
  const result = child_process.spawnSync('sh', [scriptPath, ...args], {
646
674
  env,
@@ -649,8 +677,7 @@ function runScenario({ printMode, stdinInherit, scenario }) {
649
677
  timeout: 10000,
650
678
  });
651
679
  const elapsedMs = Date.now() - start;
652
- fs.rmSync(tmpBase, { recursive: true, force: true });
653
- return { ...result, elapsedMs };
680
+ return { ...result, elapsedMs, tmpBase };
654
681
  }
655
682
 
656
683
  test('helper and bootstrap intercept process.kill(self, SIGKILL) statically', () => {
@@ -695,42 +722,619 @@ test('helper and bootstrap intercept process.kill(self, SIGKILL) statically', ()
695
722
  test('helper branch (CLI -p, no stdin inherit): normal/sync-exit/async-exit all produce output', () => {
696
723
  for (const scenario of ['normal', 'sync-exit', 'async-exit']) {
697
724
  const r = runScenario({ printMode: true, stdinInherit: false, scenario });
698
- assert.ok((r.stdout || '').includes('ok'), `scenario=${scenario} stdout=${r.stdout} stderr=${r.stderr}`);
699
- assert.equal(r.status, 0, `scenario=${scenario} status=${r.status} stderr=${r.stderr}`);
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
+ }
700
731
  }
701
732
  });
702
733
 
703
734
  test('bootstrap branch (-p + CLAUDE_TERMUX_STDIN=inherit): normal/sync-exit/async-exit all produce output', () => {
704
735
  for (const scenario of ['normal', 'sync-exit', 'async-exit']) {
705
736
  const r = runScenario({ printMode: true, stdinInherit: true, scenario });
706
- assert.ok((r.stdout || '').includes('ok'), `scenario=${scenario} stdout=${r.stdout} stderr=${r.stderr}`);
707
- assert.equal(r.status, 0, `scenario=${scenario} status=${r.status} stderr=${r.stderr}`);
708
- if (scenario === 'async-exit') {
709
- assert.ok(r.elapsedMs >= 250, `expected wait >= 250ms, got ${r.elapsedMs}ms`);
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 });
710
745
  }
711
746
  }
712
747
  });
713
748
 
714
749
  test('helper branch intercepts self-directed SIGKILL (string signal) and exits with proper code', () => {
715
750
  const r = runScenario({ printMode: true, stdinInherit: false, scenario: 'self-sigkill-fallback-string' });
716
- assert.equal(r.status, 17, `expected status 17, got ${r.status}; stderr=${r.stderr}`);
717
- assert.ok(!r.signal, `expected clean exit (no signal), but got signal: ${r.signal}`);
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
+ }
718
757
  });
719
758
 
720
759
  test('helper branch intercepts self-directed SIGKILL (numeric signal 9) and exits with proper code', () => {
721
760
  const r = runScenario({ printMode: true, stdinInherit: false, scenario: 'self-sigkill-fallback-numeric' });
722
- assert.equal(r.status, 17, `expected status 17, got ${r.status}; stderr=${r.stderr}`);
723
- assert.ok(!r.signal, `expected clean exit (no signal), but got signal: ${r.signal}`);
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
+ }
724
767
  });
725
768
 
726
769
  test('helper branch intercepts self-directed SIGKILL with process.exit() (no code) and defaults to 0', () => {
727
770
  const r = runScenario({ printMode: true, stdinInherit: false, scenario: 'self-sigkill-fallback-no-code' });
728
- assert.equal(r.status, 0, `expected status 0, got ${r.status}; stderr=${r.stderr}`);
729
- assert.ok(!r.signal, `expected clean exit (no signal), but got signal: ${r.signal}`);
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
+ }
730
777
  });
731
778
 
732
779
  test('helper branch allows process.kill to other processes (signal 0, passthrough)', () => {
733
780
  const r = runScenario({ printMode: true, stdinInherit: false, scenario: 'other-process-kill' });
734
- assert.ok((r.stdout || '').includes('ok'), `expected ok output, got stdout=${r.stdout} stderr=${r.stderr}`);
735
- assert.equal(r.status, 0, `expected status 0, got ${r.status}; stderr=${r.stderr}`);
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();
736
1340
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bash0816/claude-code",
3
- "version": "2.1.220-2",
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": {