@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
|
@@ -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.
|
|
6
|
-
"previous_stable_version": "2.1.
|
|
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
|
-
|
|
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
|
-
|
|
699
|
-
|
|
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
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
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
|
-
|
|
717
|
-
|
|
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
|
-
|
|
723
|
-
|
|
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
|
-
|
|
729
|
-
|
|
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
|
-
|
|
735
|
-
|
|
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
|
});
|