@bash0816/claude-code 2.1.161 → 2.1.177

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.161
40
+ npm install -g @bash0816/claude-code@2.1.161-2
41
41
  ```
42
42
 
43
43
  ## Update / 更新
@@ -54,6 +54,14 @@ claude update
54
54
  npm install -g @bash0816/claude-code@<latest_audited_version>
55
55
  ```
56
56
 
57
+ If you already have the repo's `2.1.161` release or the official upstream
58
+ `2.1.165` installed, install `2.1.159-13` explicitly. `claude update` will not
59
+ roll a newer installed terminal back to this repo's current audited release.
60
+
61
+ この repo の `2.1.161` release または official upstream の `2.1.165` を
62
+ すでに入れている端末では、`2.1.159-13` を明示 install してください。
63
+ `claude update` だけでは、より新しい端末をこの repo の現在の audited release に戻しません。
64
+
57
65
  Normal launch also checks the same manifest with a short timeout and prints a notice when a newer audited version is available.
58
66
 
59
67
  通常起動時も、同じ manifest を短い timeout で確認し、新しい監査済み version があれば通知します。
@@ -229,6 +229,26 @@
229
229
  "status": "termux_verified"
230
230
  },
231
231
  "2.1.161": {
232
+ "wrapper_spec": "@anthropic-ai/claude-code@2.1.161",
233
+ "native_spec": "@anthropic-ai/claude-code-linux-arm64@2.1.161",
234
+ "entry_js_offset": 225551924,
235
+ "entry_end_offset": 241503454,
236
+ "tarball_integrity": "sha512-rLEzd8/q4UnTLDSAd3eYgog+G0iZ8y6K8yh0gSxf82d7uOkypYaBUkbmFENuHuzIzCIf9Yy/utCJfp6kQFjUow==",
237
+ "tarball_sha256": "e87d973bbe58593e06c101196f6417f7d950bf777cb9f1a8ff002ebf987ea066",
238
+ "tarball_size": 74314760,
239
+ "status": "offset_discovered"
240
+ },
241
+ "2.1.161-1": {
242
+ "wrapper_spec": "@anthropic-ai/claude-code@2.1.161",
243
+ "native_spec": "@anthropic-ai/claude-code-linux-arm64@2.1.161",
244
+ "entry_js_offset": 225551924,
245
+ "entry_end_offset": 241503454,
246
+ "tarball_integrity": "sha512-rLEzd8/q4UnTLDSAd3eYgog+G0iZ8y6K8yh0gSxf82d7uOkypYaBUkbmFENuHuzIzCIf9Yy/utCJfp6kQFjUow==",
247
+ "tarball_sha256": "e87d973bbe58593e06c101196f6417f7d950bf777cb9f1a8ff002ebf987ea066",
248
+ "tarball_size": 74314760,
249
+ "status": "reverted"
250
+ },
251
+ "2.1.161-2": {
232
252
  "wrapper_spec": "@anthropic-ai/claude-code@2.1.161",
233
253
  "native_spec": "@anthropic-ai/claude-code-linux-arm64@2.1.161",
234
254
  "entry_js_offset": 225551924,
@@ -237,6 +257,24 @@
237
257
  "tarball_sha256": "e87d973bbe58593e06c101196f6417f7d950bf777cb9f1a8ff002ebf987ea066",
238
258
  "tarball_size": 74314760,
239
259
  "status": "termux_verified"
260
+ },
261
+ "2.1.165": {
262
+ "wrapper_spec": "@anthropic-ai/claude-code@2.1.165",
263
+ "native_spec": "@anthropic-ai/claude-code-linux-arm64@2.1.165",
264
+ "entry_js_offset": 226798932,
265
+ "entry_end_offset": 242980856,
266
+ "tarball_integrity": "sha512-uGwKxRQGTxh3BZGnX096CLf/2us5lUFnk4KerWhG4WCpRiEUSu4TUDojp/S3SUzA//FpM57fViUW07XL3T19lw==",
267
+ "tarball_sha256": "6dc6d84624bedaf5ff0948b178fdfc9c2724d8051cb00e70559e292055f7155e",
268
+ "status": "reverted"
269
+ },
270
+ "2.1.177": {
271
+ "wrapper_spec": "@anthropic-ai/claude-code@2.1.177",
272
+ "native_spec": "@anthropic-ai/claude-code-linux-arm64@2.1.177",
273
+ "entry_js_offset": 231509988,
274
+ "entry_end_offset": 248542413,
275
+ "tarball_integrity": "sha512-qb0K+/xwaZFz9spBUcDX3vNgEFTSNdbDze8AOpU4R9weVrwBY9zAO/OPqxC9qNhb5ZIRnJAZLKiICEeEUHowzA==",
276
+ "tarball_sha256": "dc6f28ab95d8748528a92e207d0f2d1dcd173855d18f11b8f82c5827bbdcb234",
277
+ "status": "offset_discovered"
240
278
  }
241
279
  }
242
280
  }
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "manifest_version": 1,
3
3
  "package_name": "@bash0816/claude-code",
4
- "latest_audited_version": "2.1.161",
5
- "latest_candidate_version": "2.1.161",
4
+ "latest_audited_version": "2.1.161-2",
5
+ "latest_candidate_version": "2.1.177",
6
6
  "previous_stable_version": "2.1.159-13",
7
7
  "manifest_url": "https://raw.githubusercontent.com/bash0816/ClaudeCode-Termux/main/config/claude-termux-release-manifest.json"
8
8
  }
@@ -49,7 +49,7 @@ for _a in "$@"; do
49
49
  done
50
50
 
51
51
  if [ "$_pf" = "1" ] && [ "${CLAUDE_TERMUX_STDIN:-}" != "inherit" ]; then
52
- _helper=$(mktemp "${TMPDIR:-/tmp}/claude-helper.XXXXXX.js")
52
+ _helper=$(mktemp "${TERMUX_TMPDIR}/claude-helper.XXXXXX.js")
53
53
  trap 'rm -f "$_helper"' EXIT HUP INT TERM
54
54
  cat <<'NODE' > "$_helper"
55
55
  const fs = require('fs');
@@ -73,8 +73,39 @@ class RequestedExit extends Error {
73
73
  }
74
74
  }
75
75
 
76
+ function cleanupStaleEntryFiles(currentWorkdir = workdir, currentEntryJsOffset = entryJsOffset, currentEntryEndOffset = entryEndOffset, now = Date.now()) {
77
+ const prefix = `cli.${currentEntryJsOffset}.${currentEntryEndOffset}.`;
78
+ const suffix = '.bare-path.js';
79
+ const maxAgeMs = 24 * 60 * 60 * 1000;
80
+ let entries;
81
+ try {
82
+ entries = fs.readdirSync(currentWorkdir, { withFileTypes: true });
83
+ } catch {
84
+ return;
85
+ }
86
+ for (const entry of entries) {
87
+ if (!entry.isFile() && !entry.isSymbolicLink()) continue;
88
+ if (!entry.name.startsWith(prefix) || !entry.name.endsWith(suffix)) continue;
89
+ const filePath = path.join(currentWorkdir, entry.name);
90
+ let stats;
91
+ try {
92
+ stats = fs.statSync(filePath);
93
+ } catch {
94
+ continue;
95
+ }
96
+ if (Number.isFinite(stats.mtimeMs) && now - stats.mtimeMs < maxAgeMs) continue;
97
+ try {
98
+ fs.rmSync(filePath, { force: true });
99
+ } catch {}
100
+ }
101
+ }
102
+
76
103
  function ensureEntryFile() {
77
- const extractedFile = path.join(workdir, `cli.${entryJsOffset}.${entryEndOffset}.bare-path.js`);
104
+ cleanupStaleEntryFiles();
105
+ const extractedFile = path.join(
106
+ workdir,
107
+ `cli.${entryJsOffset}.${entryEndOffset}.${process.pid}.${Date.now().toString(36)}.${Math.random().toString(36).slice(2)}.bare-path.js`,
108
+ );
78
109
  const len = entryEndOffset - entryJsOffset;
79
110
  if (!(len > 0)) throw new Error('invalid replay offsets');
80
111
 
@@ -87,15 +118,165 @@ function ensureEntryFile() {
87
118
  return extractedFile;
88
119
  }
89
120
 
121
+ function isFullWidthCodePoint(codePoint) {
122
+ return Number.isFinite(codePoint) && (
123
+ codePoint >= 0x1100 && (
124
+ codePoint <= 0x115f ||
125
+ codePoint === 0x2329 ||
126
+ codePoint === 0x232a ||
127
+ (codePoint >= 0x2e80 && codePoint <= 0xa4cf && codePoint !== 0x303f) ||
128
+ (codePoint >= 0xac00 && codePoint <= 0xd7a3) ||
129
+ (codePoint >= 0xf900 && codePoint <= 0xfaff) ||
130
+ (codePoint >= 0xfe10 && codePoint <= 0xfe19) ||
131
+ (codePoint >= 0xfe30 && codePoint <= 0xfe6f) ||
132
+ (codePoint >= 0xff00 && codePoint <= 0xff60) ||
133
+ (codePoint >= 0xffe0 && codePoint <= 0xffe6) ||
134
+ (codePoint >= 0x1f300 && codePoint <= 0x1f6ff) ||
135
+ (codePoint >= 0x1fa70 && codePoint <= 0x1faff) ||
136
+ (codePoint >= 0x1f900 && codePoint <= 0x1f9ff) ||
137
+ (codePoint >= 0x20000 && codePoint <= 0x3fffd)
138
+ )
139
+ );
140
+ }
141
+
142
+ function graphemeWidth(grapheme) {
143
+ let width = 0;
144
+ const symbols = Array.from(String(grapheme ?? ''));
145
+ const codePoints = symbols.map(symbol => symbol.codePointAt(0)).filter(codePoint => Number.isFinite(codePoint));
146
+ if (codePoints.length === 0) return 0;
147
+ if (codePoints.length > 1 && codePoints.every(codePoint => codePoint >= 0x1f1e6 && codePoint <= 0x1f1ff)) {
148
+ return 2;
149
+ }
150
+ if (codePoints.includes(0x20e3) || codePoints.includes(0x200d)) {
151
+ return 2;
152
+ }
153
+ if (codePoints.includes(0xfe0f) || codePoints.some(codePoint => codePoint >= 0x2600 && codePoint <= 0x27bf)) {
154
+ return 2;
155
+ }
156
+ for (const symbol of symbols) {
157
+ const codePoint = symbol.codePointAt(0);
158
+ if (codePoint === undefined || codePoint === 0) continue;
159
+ if (codePoint < 32 || (codePoint >= 0x7f && codePoint < 0xa0)) continue;
160
+ if (codePoint === 0x200d || codePoint === 0xfe0f) continue;
161
+ if (/\p{M}/u.test(symbol)) continue;
162
+ if (isFullWidthCodePoint(codePoint)) return 2;
163
+ width = 1;
164
+ }
165
+ return width;
166
+ }
167
+
90
168
  function stringWidth(value) {
91
- const text = String(value ?? '');
169
+ const text = stripANSI(value);
92
170
  if (typeof Intl !== 'undefined' && typeof Intl.Segmenter === 'function') {
93
171
  const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
94
172
  let width = 0;
95
- for (const _segment of segmenter.segment(text)) width += 1;
173
+ for (const segment of segmenter.segment(text)) width += graphemeWidth(segment.segment);
96
174
  return width;
97
175
  }
98
- return Array.from(text).length;
176
+ return Array.from(text).reduce((width, symbol) => width + graphemeWidth(symbol), 0);
177
+ }
178
+
179
+ function stripANSI(value) {
180
+ return String(value ?? '')
181
+ .replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, '')
182
+ .replace(/\x1B\][^\x07]*(?:\x07|\x1B\\)/g, '');
183
+ }
184
+
185
+ function wrapAnsi(value, columns, options = {}) {
186
+ const text = String(value ?? '');
187
+ const width = Number(columns);
188
+ const hard = options.hard !== false;
189
+ const trim = options.trim === true;
190
+ const wordWrap = options.wordWrap !== false;
191
+ if (!Number.isFinite(width) || width <= 0) return trim ? text.trimEnd() : text;
192
+
193
+ const ansiPattern = /\x1B\[[0-?]*[ -/]*[@-~]|\x1B\][^\x07]*(?:\x07|\x1B\\)/g;
194
+ const segmenter = typeof Intl !== 'undefined' && typeof Intl.Segmenter === 'function'
195
+ ? new Intl.Segmenter('en', { granularity: 'grapheme' })
196
+ : null;
197
+ const splitVisible = (chunk) => {
198
+ if (chunk === '') return [];
199
+ if (!segmenter) return Array.from(chunk);
200
+ return Array.from(segmenter.segment(chunk), part => part.segment);
201
+ };
202
+ const splitByGrapheme = hard || !wordWrap;
203
+ let result = '';
204
+ let currentWidth = 0;
205
+ let lastIndex = 0;
206
+ const appendVisible = (chunk) => {
207
+ const tokens = splitByGrapheme ? splitVisible(chunk) : (chunk.match(/\s+|[^\s]+/gu) || []);
208
+ for (const token of tokens) {
209
+ if (token === '\n') {
210
+ if (trim) result = result.replace(/[ \t]+$/g, '');
211
+ result += token;
212
+ currentWidth = 0;
213
+ continue;
214
+ }
215
+ const tokenWidth = stringWidth(token);
216
+ const isWhitespace = /^\s+$/u.test(token);
217
+ if (trim && isWhitespace && currentWidth === 0) continue;
218
+ if (currentWidth > 0 && currentWidth + tokenWidth > width) {
219
+ if (!splitByGrapheme && !isWhitespace) {
220
+ result = result.replace(/[ \t]+$/g, '');
221
+ result += '\n';
222
+ currentWidth = 0;
223
+ } else {
224
+ for (const piece of splitVisible(token)) {
225
+ const pieceWidth = stringWidth(piece);
226
+ if (currentWidth > 0 && currentWidth + pieceWidth > width) {
227
+ if (trim) result = result.replace(/[ \t]+$/g, '');
228
+ result += '\n';
229
+ currentWidth = 0;
230
+ }
231
+ if (trim && /^\s+$/u.test(piece) && currentWidth === 0) continue;
232
+ result += piece;
233
+ currentWidth += pieceWidth;
234
+ }
235
+ continue;
236
+ }
237
+ }
238
+ result += token;
239
+ currentWidth += tokenWidth;
240
+ }
241
+ };
242
+ for (const match of text.matchAll(ansiPattern)) {
243
+ appendVisible(text.slice(lastIndex, match.index ?? 0));
244
+ result += match[0];
245
+ lastIndex = (match.index ?? 0) + match[0].length;
246
+ }
247
+ appendVisible(text.slice(lastIndex));
248
+ return trim ? result.replace(/[ \t]+$/gm, '') : result;
249
+ }
250
+
251
+ function stableHash(value, seed) {
252
+ const text = String(value ?? '');
253
+ let hash = 2166136261;
254
+ if (seed !== undefined) {
255
+ const seedText = String(seed ?? '');
256
+ for (let i = 0; i < seedText.length; i += 1) {
257
+ hash ^= seedText.charCodeAt(i);
258
+ hash = Math.imul(hash, 16777619);
259
+ }
260
+ hash ^= 0x9e3779b9;
261
+ hash = Math.imul(hash, 16777619);
262
+ }
263
+ for (let i = 0; i < text.length; i += 1) {
264
+ hash ^= text.charCodeAt(i);
265
+ hash = Math.imul(hash, 16777619);
266
+ }
267
+ return hash >>> 0;
268
+ }
269
+
270
+ function replaceRequired(source, pattern, replacement, label, expectedCount) {
271
+ const text = String(source);
272
+ const matches = text.match(pattern);
273
+ if (!matches || matches.length === 0) {
274
+ throw new Error(`rewriteNativeChunkSource: missing ${label}`);
275
+ }
276
+ if (expectedCount !== undefined && matches.length !== expectedCount) {
277
+ throw new Error(`rewriteNativeChunkSource: unexpected ${label} count ${matches.length}`);
278
+ }
279
+ return text.replace(pattern, replacement);
99
280
  }
100
281
 
101
282
  function parseScalar(value) {
@@ -244,6 +425,9 @@ function createFakeRequire(realRequire) {
244
425
  writable: true,
245
426
  });
246
427
  }
428
+ if (context.Bun && globalThis.__claudeYaml) {
429
+ context.Bun.YAML = globalThis.__claudeYaml;
430
+ }
247
431
  } catch {}
248
432
  return context;
249
433
  }
@@ -344,25 +528,53 @@ function createFakeRequire(realRequire) {
344
528
  };
345
529
  }
346
530
 
347
- async function main() {
348
- const extractedFile = ensureEntryFile();
349
- const code = fs.readFileSync(extractedFile, 'utf8');
350
- const patchedCode = code.replace(
351
- /^function\(exports, require, module, __filename, __dirname\) \{/,
352
- 'function(exports, require, module, __filename, __dirname) {var __claudeBun = globalThis.__claudeBunShim;',
353
- ).replace(
531
+ function rewriteNativeChunkSource(source) {
532
+ const rawPrefix = 'function(exports, require, module, __filename, __dirname) {';
533
+ const injectedPrefix = rawPrefix + 'var __claudeBun = globalThis.__claudeBunShim;';
534
+ let patched = String(source);
535
+ patched = replaceRequired(
536
+ patched,
537
+ /^function\(exports, require, module, __filename, __dirname\) \{(?:var __claudeBun = globalThis\.__claudeBunShim;)?/,
538
+ injectedPrefix,
539
+ 'module wrapper prefix',
540
+ 1,
541
+ );
542
+ patched = replaceRequired(
543
+ patched,
354
544
  /\btypeof Bun\b/g,
355
545
  'typeof __claudeBun',
356
- ).replace(
546
+ 'typeof Bun',
547
+ 3,
548
+ );
549
+ patched = replaceRequired(
550
+ patched,
551
+ /\btypeof globalThis\.Bun\b/g,
552
+ 'typeof globalThis.__claudeBun',
553
+ 'typeof globalThis.Bun',
554
+ 1,
555
+ );
556
+ patched = replaceRequired(
557
+ patched,
558
+ /\bglobalThis\.Bun\b/g,
559
+ 'globalThis.__claudeBun',
560
+ 'globalThis.Bun',
561
+ 1,
562
+ );
563
+ patched = replaceRequired(
564
+ patched,
357
565
  /\bBun\./g,
358
566
  '__claudeBun.',
359
- ).replace(
360
- /function t5q\(q\)\{return Bun\.YAML\.parse\(q\)\}/g,
361
- 'function t5q(q){return globalThis.__claudeYaml.parse(q)}',
362
- ).replace(
363
- /function VK6\(q\)\{return Bun\.YAML\.stringify\(q,null,2\)\+`/g,
364
- 'function VK6(q){return globalThis.__claudeYaml.stringify(q,null,2)+`',
567
+ 'Bun property access',
568
+ 33,
365
569
  );
570
+ return patched;
571
+ }
572
+
573
+ async function main() {
574
+ let extractedFile;
575
+ extractedFile = ensureEntryFile();
576
+ const code = fs.readFileSync(extractedFile, 'utf8');
577
+ const patchedCode = rewriteNativeChunkSource(code);
366
578
  const fn = eval('(' + patchedCode.replace(/\)\s*$/, '') + ')');
367
579
 
368
580
  const originalArgv = process.argv.slice();
@@ -389,6 +601,9 @@ async function main() {
389
601
  globalThis.Bun = {
390
602
  version: '1.1.8',
391
603
  stringWidth,
604
+ wrapAnsi,
605
+ stripANSI,
606
+ hash: stableHash,
392
607
  which: (cmd) => {
393
608
  try {
394
609
  return _realChild.execFileSync('which', [String(cmd)], { encoding: 'utf8' }).trim() || null;
@@ -424,9 +639,9 @@ async function main() {
424
639
  gte: (a, b) => _cmp(a, b) >= 0,
425
640
  lt: (a, b) => _cmp(a, b) < 0,
426
641
  lte: (a, b) => _cmp(a, b) <= 0,
427
- };
428
- })(),
429
- YAML: globalThis.__claudeYaml,
642
+ };
643
+ })(),
644
+ YAML: globalThis.__claudeYaml,
430
645
  };
431
646
  Object.assign(globalThis.__claudeBunShim, globalThis.Bun);
432
647
  if (typeof globalThis.__claudeBunShim.gc !== 'function') {
@@ -439,10 +654,13 @@ async function main() {
439
654
  throw new RequestedExit(code);
440
655
  };
441
656
 
657
+ const printWaitMs = Number(process.env.CLAUDE_TERMUX_PRINT_WAIT_MS || 5000);
442
658
  const moduleLike = { exports: {} };
443
659
  const maybePromise = fn(moduleLike.exports, fakeRequire, moduleLike, extractedFile, workdir);
444
660
  if (maybePromise && typeof maybePromise.then === 'function') await maybePromise;
445
- await new Promise(resolve => setTimeout(resolve, 5000));
661
+ if (Number.isFinite(printWaitMs) && printWaitMs > 0) {
662
+ await new Promise(resolve => setTimeout(resolve, printWaitMs));
663
+ }
446
664
  if (asyncErrors.length > 0) throw asyncErrors[0];
447
665
  } catch (error) {
448
666
  if (error instanceof RequestedExit) {
@@ -453,6 +671,11 @@ async function main() {
453
671
  } finally {
454
672
  process.removeListener('uncaughtException', onAsyncError);
455
673
  process.removeListener('unhandledRejection', onAsyncError);
674
+ if (extractedFile) {
675
+ try {
676
+ fs.rmSync(extractedFile, { force: true });
677
+ } catch {}
678
+ }
456
679
  process.argv = originalArgv;
457
680
  process.exit = originalExit;
458
681
  try {
@@ -488,7 +711,7 @@ NODE
488
711
  trap - EXIT HUP INT TERM
489
712
  exit "$_status"
490
713
  else
491
- _bootstrap=$(mktemp "${TMPDIR:-/tmp}/claude-bootstrap.XXXXXX.js")
714
+ _bootstrap=$(mktemp "${TERMUX_TMPDIR}/claude-bootstrap.XXXXXX.js")
492
715
  trap 'rm -f "$_bootstrap"' EXIT HUP INT TERM
493
716
  cat <<'NODE' > "$_bootstrap"
494
717
  const fs = require('fs');
@@ -512,8 +735,39 @@ class RequestedExit extends Error {
512
735
  }
513
736
  }
514
737
 
738
+ function cleanupStaleEntryFiles(currentWorkdir = workdir, currentEntryJsOffset = entryJsOffset, currentEntryEndOffset = entryEndOffset, now = Date.now()) {
739
+ const prefix = `cli.${currentEntryJsOffset}.${currentEntryEndOffset}.`;
740
+ const suffix = '.bare-path.js';
741
+ const maxAgeMs = 24 * 60 * 60 * 1000;
742
+ let entries;
743
+ try {
744
+ entries = fs.readdirSync(currentWorkdir, { withFileTypes: true });
745
+ } catch {
746
+ return;
747
+ }
748
+ for (const entry of entries) {
749
+ if (!entry.isFile() && !entry.isSymbolicLink()) continue;
750
+ if (!entry.name.startsWith(prefix) || !entry.name.endsWith(suffix)) continue;
751
+ const filePath = path.join(currentWorkdir, entry.name);
752
+ let stats;
753
+ try {
754
+ stats = fs.statSync(filePath);
755
+ } catch {
756
+ continue;
757
+ }
758
+ if (Number.isFinite(stats.mtimeMs) && now - stats.mtimeMs < maxAgeMs) continue;
759
+ try {
760
+ fs.rmSync(filePath, { force: true });
761
+ } catch {}
762
+ }
763
+ }
764
+
515
765
  function ensureEntryFile() {
516
- const extractedFile = path.join(workdir, `cli.${entryJsOffset}.${entryEndOffset}.bare-path.js`);
766
+ cleanupStaleEntryFiles();
767
+ const extractedFile = path.join(
768
+ workdir,
769
+ `cli.${entryJsOffset}.${entryEndOffset}.${process.pid}.${Date.now().toString(36)}.${Math.random().toString(36).slice(2)}.bare-path.js`,
770
+ );
517
771
  const len = entryEndOffset - entryJsOffset;
518
772
  if (!(len > 0)) throw new Error('invalid replay offsets');
519
773
 
@@ -526,15 +780,165 @@ function ensureEntryFile() {
526
780
  return extractedFile;
527
781
  }
528
782
 
783
+ function isFullWidthCodePoint(codePoint) {
784
+ return Number.isFinite(codePoint) && (
785
+ codePoint >= 0x1100 && (
786
+ codePoint <= 0x115f ||
787
+ codePoint === 0x2329 ||
788
+ codePoint === 0x232a ||
789
+ (codePoint >= 0x2e80 && codePoint <= 0xa4cf && codePoint !== 0x303f) ||
790
+ (codePoint >= 0xac00 && codePoint <= 0xd7a3) ||
791
+ (codePoint >= 0xf900 && codePoint <= 0xfaff) ||
792
+ (codePoint >= 0xfe10 && codePoint <= 0xfe19) ||
793
+ (codePoint >= 0xfe30 && codePoint <= 0xfe6f) ||
794
+ (codePoint >= 0xff00 && codePoint <= 0xff60) ||
795
+ (codePoint >= 0xffe0 && codePoint <= 0xffe6) ||
796
+ (codePoint >= 0x1f300 && codePoint <= 0x1f6ff) ||
797
+ (codePoint >= 0x1fa70 && codePoint <= 0x1faff) ||
798
+ (codePoint >= 0x1f900 && codePoint <= 0x1f9ff) ||
799
+ (codePoint >= 0x20000 && codePoint <= 0x3fffd)
800
+ )
801
+ );
802
+ }
803
+
804
+ function graphemeWidth(grapheme) {
805
+ let width = 0;
806
+ const symbols = Array.from(String(grapheme ?? ''));
807
+ const codePoints = symbols.map(symbol => symbol.codePointAt(0)).filter(codePoint => Number.isFinite(codePoint));
808
+ if (codePoints.length === 0) return 0;
809
+ if (codePoints.length > 1 && codePoints.every(codePoint => codePoint >= 0x1f1e6 && codePoint <= 0x1f1ff)) {
810
+ return 2;
811
+ }
812
+ if (codePoints.includes(0x20e3) || codePoints.includes(0x200d)) {
813
+ return 2;
814
+ }
815
+ if (codePoints.includes(0xfe0f) || codePoints.some(codePoint => codePoint >= 0x2600 && codePoint <= 0x27bf)) {
816
+ return 2;
817
+ }
818
+ for (const symbol of symbols) {
819
+ const codePoint = symbol.codePointAt(0);
820
+ if (codePoint === undefined || codePoint === 0) continue;
821
+ if (codePoint < 32 || (codePoint >= 0x7f && codePoint < 0xa0)) continue;
822
+ if (codePoint === 0x200d || codePoint === 0xfe0f) continue;
823
+ if (/\p{M}/u.test(symbol)) continue;
824
+ if (isFullWidthCodePoint(codePoint)) return 2;
825
+ width = 1;
826
+ }
827
+ return width;
828
+ }
829
+
529
830
  function stringWidth(value) {
530
- const text = String(value ?? '');
831
+ const text = stripANSI(value);
531
832
  if (typeof Intl !== 'undefined' && typeof Intl.Segmenter === 'function') {
532
833
  const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
533
834
  let width = 0;
534
- for (const _segment of segmenter.segment(text)) width += 1;
835
+ for (const segment of segmenter.segment(text)) width += graphemeWidth(segment.segment);
535
836
  return width;
536
837
  }
537
- return Array.from(text).length;
838
+ return Array.from(text).reduce((width, symbol) => width + graphemeWidth(symbol), 0);
839
+ }
840
+
841
+ function stripANSI(value) {
842
+ return String(value ?? '')
843
+ .replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, '')
844
+ .replace(/\x1B\][^\x07]*(?:\x07|\x1B\\)/g, '');
845
+ }
846
+
847
+ function wrapAnsi(value, columns, options = {}) {
848
+ const text = String(value ?? '');
849
+ const width = Number(columns);
850
+ const hard = options.hard !== false;
851
+ const trim = options.trim === true;
852
+ const wordWrap = options.wordWrap !== false;
853
+ if (!Number.isFinite(width) || width <= 0) return trim ? text.trimEnd() : text;
854
+
855
+ const ansiPattern = /\x1B\[[0-?]*[ -/]*[@-~]|\x1B\][^\x07]*(?:\x07|\x1B\\)/g;
856
+ const segmenter = typeof Intl !== 'undefined' && typeof Intl.Segmenter === 'function'
857
+ ? new Intl.Segmenter('en', { granularity: 'grapheme' })
858
+ : null;
859
+ const splitVisible = (chunk) => {
860
+ if (chunk === '') return [];
861
+ if (!segmenter) return Array.from(chunk);
862
+ return Array.from(segmenter.segment(chunk), part => part.segment);
863
+ };
864
+ const splitByGrapheme = hard || !wordWrap;
865
+ let result = '';
866
+ let currentWidth = 0;
867
+ let lastIndex = 0;
868
+ const appendVisible = (chunk) => {
869
+ const tokens = splitByGrapheme ? splitVisible(chunk) : (chunk.match(/\s+|[^\s]+/gu) || []);
870
+ for (const token of tokens) {
871
+ if (token === '\n') {
872
+ if (trim) result = result.replace(/[ \t]+$/g, '');
873
+ result += token;
874
+ currentWidth = 0;
875
+ continue;
876
+ }
877
+ const tokenWidth = stringWidth(token);
878
+ const isWhitespace = /^\s+$/u.test(token);
879
+ if (trim && isWhitespace && currentWidth === 0) continue;
880
+ if (currentWidth > 0 && currentWidth + tokenWidth > width) {
881
+ if (!splitByGrapheme && !isWhitespace) {
882
+ result = result.replace(/[ \t]+$/g, '');
883
+ result += '\n';
884
+ currentWidth = 0;
885
+ } else {
886
+ for (const piece of splitVisible(token)) {
887
+ const pieceWidth = stringWidth(piece);
888
+ if (currentWidth > 0 && currentWidth + pieceWidth > width) {
889
+ if (trim) result = result.replace(/[ \t]+$/g, '');
890
+ result += '\n';
891
+ currentWidth = 0;
892
+ }
893
+ if (trim && /^\s+$/u.test(piece) && currentWidth === 0) continue;
894
+ result += piece;
895
+ currentWidth += pieceWidth;
896
+ }
897
+ continue;
898
+ }
899
+ }
900
+ result += token;
901
+ currentWidth += tokenWidth;
902
+ }
903
+ };
904
+ for (const match of text.matchAll(ansiPattern)) {
905
+ appendVisible(text.slice(lastIndex, match.index ?? 0));
906
+ result += match[0];
907
+ lastIndex = (match.index ?? 0) + match[0].length;
908
+ }
909
+ appendVisible(text.slice(lastIndex));
910
+ return trim ? result.replace(/[ \t]+$/gm, '') : result;
911
+ }
912
+
913
+ function stableHash(value, seed) {
914
+ const text = String(value ?? '');
915
+ let hash = 2166136261;
916
+ if (seed !== undefined) {
917
+ const seedText = String(seed ?? '');
918
+ for (let i = 0; i < seedText.length; i += 1) {
919
+ hash ^= seedText.charCodeAt(i);
920
+ hash = Math.imul(hash, 16777619);
921
+ }
922
+ hash ^= 0x9e3779b9;
923
+ hash = Math.imul(hash, 16777619);
924
+ }
925
+ for (let i = 0; i < text.length; i += 1) {
926
+ hash ^= text.charCodeAt(i);
927
+ hash = Math.imul(hash, 16777619);
928
+ }
929
+ return hash >>> 0;
930
+ }
931
+
932
+ function replaceRequired(source, pattern, replacement, label, expectedCount) {
933
+ const text = String(source);
934
+ const matches = text.match(pattern);
935
+ if (!matches || matches.length === 0) {
936
+ throw new Error(`rewriteNativeChunkSource: missing ${label}`);
937
+ }
938
+ if (expectedCount !== undefined && matches.length !== expectedCount) {
939
+ throw new Error(`rewriteNativeChunkSource: unexpected ${label} count ${matches.length}`);
940
+ }
941
+ return text.replace(pattern, replacement);
538
942
  }
539
943
 
540
944
  function parseScalar(value) {
@@ -633,6 +1037,63 @@ function createFakeRequire(realRequire) {
633
1037
  const realChild = realRequire('child_process');
634
1038
  const realVm = realRequire('vm');
635
1039
 
1040
+ function injectBunIntoContext(context) {
1041
+ if (!context || typeof context !== 'object') return context;
1042
+ try {
1043
+ if (!Object.prototype.hasOwnProperty.call(context, '__claudeYaml')) {
1044
+ Object.defineProperty(context, '__claudeYaml', {
1045
+ value: globalThis.__claudeYaml,
1046
+ configurable: true,
1047
+ writable: true,
1048
+ });
1049
+ }
1050
+ if (!Object.prototype.hasOwnProperty.call(context, '__claudeBunShim')) {
1051
+ Object.defineProperty(context, '__claudeBunShim', {
1052
+ value: globalThis.__claudeBunShim,
1053
+ configurable: true,
1054
+ writable: true,
1055
+ });
1056
+ }
1057
+ if (!Object.prototype.hasOwnProperty.call(context, '__claudeBun')) {
1058
+ Object.defineProperty(context, '__claudeBun', {
1059
+ value: globalThis.__claudeBunShim,
1060
+ configurable: true,
1061
+ writable: true,
1062
+ });
1063
+ }
1064
+ if (Object.prototype.hasOwnProperty.call(context, 'Bun')) {
1065
+ if (context.Bun && typeof context.Bun === 'object' && context.Bun !== globalThis.Bun) {
1066
+ try {
1067
+ context.Bun = globalThis.Bun;
1068
+ } catch {
1069
+ Object.defineProperty(context, 'Bun', {
1070
+ value: globalThis.Bun,
1071
+ configurable: true,
1072
+ writable: true,
1073
+ });
1074
+ }
1075
+ }
1076
+ if (!context.Bun || typeof context.Bun !== 'object') {
1077
+ Object.defineProperty(context, 'Bun', {
1078
+ value: globalThis.Bun,
1079
+ configurable: true,
1080
+ writable: true,
1081
+ });
1082
+ }
1083
+ } else {
1084
+ Object.defineProperty(context, 'Bun', {
1085
+ value: globalThis.Bun,
1086
+ configurable: true,
1087
+ writable: true,
1088
+ });
1089
+ }
1090
+ if (context.Bun && globalThis.__claudeYaml) {
1091
+ context.Bun.YAML = globalThis.__claudeYaml;
1092
+ }
1093
+ } catch {}
1094
+ return context;
1095
+ }
1096
+
636
1097
  function rewriteArgs(args) {
637
1098
  if (
638
1099
  Array.isArray(args) &&
@@ -729,25 +1190,53 @@ function createFakeRequire(realRequire) {
729
1190
  };
730
1191
  }
731
1192
 
732
- async function main() {
733
- const extractedFile = ensureEntryFile();
734
- const code = fs.readFileSync(extractedFile, 'utf8');
735
- const patchedCode = code.replace(
736
- /^function\(exports, require, module, __filename, __dirname\) \{/,
737
- 'function(exports, require, module, __filename, __dirname) {var __claudeBun = globalThis.__claudeBunShim;',
738
- ).replace(
1193
+ function rewriteNativeChunkSource(source) {
1194
+ const rawPrefix = 'function(exports, require, module, __filename, __dirname) {';
1195
+ const injectedPrefix = rawPrefix + 'var __claudeBun = globalThis.__claudeBunShim;';
1196
+ let patched = String(source);
1197
+ patched = replaceRequired(
1198
+ patched,
1199
+ /^function\(exports, require, module, __filename, __dirname\) \{(?:var __claudeBun = globalThis\.__claudeBunShim;)?/,
1200
+ injectedPrefix,
1201
+ 'module wrapper prefix',
1202
+ 1,
1203
+ );
1204
+ patched = replaceRequired(
1205
+ patched,
739
1206
  /\btypeof Bun\b/g,
740
1207
  'typeof __claudeBun',
741
- ).replace(
1208
+ 'typeof Bun',
1209
+ 3,
1210
+ );
1211
+ patched = replaceRequired(
1212
+ patched,
1213
+ /\btypeof globalThis\.Bun\b/g,
1214
+ 'typeof globalThis.__claudeBun',
1215
+ 'typeof globalThis.Bun',
1216
+ 1,
1217
+ );
1218
+ patched = replaceRequired(
1219
+ patched,
1220
+ /\bglobalThis\.Bun\b/g,
1221
+ 'globalThis.__claudeBun',
1222
+ 'globalThis.Bun',
1223
+ 1,
1224
+ );
1225
+ patched = replaceRequired(
1226
+ patched,
742
1227
  /\bBun\./g,
743
1228
  '__claudeBun.',
744
- ).replace(
745
- /function t5q\(q\)\{return Bun\.YAML\.parse\(q\)\}/g,
746
- 'function t5q(q){return globalThis.__claudeYaml.parse(q)}',
747
- ).replace(
748
- /function VK6\(q\)\{return Bun\.YAML\.stringify\(q,null,2\)\+`/g,
749
- 'function VK6(q){return globalThis.__claudeYaml.stringify(q,null,2)+`',
1229
+ 'Bun property access',
1230
+ 33,
750
1231
  );
1232
+ return patched;
1233
+ }
1234
+
1235
+ async function main() {
1236
+ let extractedFile;
1237
+ extractedFile = ensureEntryFile();
1238
+ const code = fs.readFileSync(extractedFile, 'utf8');
1239
+ const patchedCode = rewriteNativeChunkSource(code);
751
1240
  const fn = eval('(' + patchedCode.replace(/\)\s*$/, '') + ')');
752
1241
 
753
1242
  const originalArgv = process.argv.slice();
@@ -774,6 +1263,9 @@ async function main() {
774
1263
  globalThis.Bun = {
775
1264
  version: '1.1.8',
776
1265
  stringWidth,
1266
+ wrapAnsi,
1267
+ stripANSI,
1268
+ hash: stableHash,
777
1269
  which: (cmd) => {
778
1270
  try {
779
1271
  return _realChild.execFileSync('which', [String(cmd)], { encoding: 'utf8' }).trim() || null;
@@ -810,8 +1302,8 @@ async function main() {
810
1302
  lt: (a, b) => _cmp(a, b) < 0,
811
1303
  lte: (a, b) => _cmp(a, b) <= 0,
812
1304
  };
813
- })(),
814
- YAML: globalThis.__claudeYaml,
1305
+ })(),
1306
+ YAML: globalThis.__claudeYaml,
815
1307
  };
816
1308
  Object.assign(globalThis.__claudeBunShim, globalThis.Bun);
817
1309
  if (typeof globalThis.__claudeBunShim.gc !== 'function') {
@@ -827,8 +1319,6 @@ async function main() {
827
1319
  const moduleLike = { exports: {} };
828
1320
  const maybePromise = fn(moduleLike.exports, fakeRequire, moduleLike, extractedFile, workdir);
829
1321
  if (maybePromise && typeof maybePromise.then === 'function') await maybePromise;
830
- const _waitMs = Number(process.env.CLAUDE_TERMUX_PRINT_WAIT_MS || 200);
831
- await new Promise(resolve => setTimeout(resolve, _waitMs));
832
1322
  if (asyncErrors.length > 0) throw asyncErrors[0];
833
1323
  } catch (error) {
834
1324
  if (error instanceof RequestedExit) {
@@ -841,17 +1331,25 @@ async function main() {
841
1331
  process.removeListener('unhandledRejection', onAsyncError);
842
1332
  process.argv = originalArgv;
843
1333
  process.exit = originalExit;
844
- try {
845
- if (originalBun === undefined) {
846
- delete process.versions.bun;
847
- } else {
848
- Object.defineProperty(process.versions, 'bun', { value: originalBun, configurable: true });
1334
+ process.once('exit', () => {
1335
+ if (extractedFile) {
1336
+ try {
1337
+ fs.rmSync(extractedFile, { force: true });
1338
+ } catch {}
849
1339
  }
850
- } catch {}
851
- delete globalThis.__claudeYaml;
852
- delete globalThis.__claudeBunShim;
853
- delete globalThis.__claudeBun;
854
- if (hadGlobalBun) globalThis.Bun = originalGlobalBun;
1340
+ try {
1341
+ if (originalBun === undefined) {
1342
+ delete process.versions.bun;
1343
+ } else {
1344
+ Object.defineProperty(process.versions, 'bun', { value: originalBun, configurable: true });
1345
+ }
1346
+ } catch {}
1347
+ delete globalThis.__claudeYaml;
1348
+ delete globalThis.__claudeBunShim;
1349
+ delete globalThis.__claudeBun;
1350
+ if (hadGlobalBun) globalThis.Bun = originalGlobalBun;
1351
+ else delete globalThis.Bun;
1352
+ });
855
1353
  }
856
1354
  }
857
1355
 
@@ -0,0 +1,321 @@
1
+ 'use strict';
2
+
3
+ const test = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+ const crypto = require('crypto');
6
+ const child_process = require('child_process');
7
+ const fs = require('fs');
8
+ const os = require('os');
9
+ const path = require('path');
10
+ const vm = require('vm');
11
+
12
+ const scriptPath = path.join(__dirname, 'termux-run-claude-native.sh');
13
+ const script = fs.readFileSync(scriptPath, 'utf8');
14
+
15
+ function extractBlock(marker, trailer) {
16
+ const start = script.indexOf(marker);
17
+ assert.notEqual(start, -1, `missing marker: ${marker}`);
18
+ const bodyStart = script.indexOf('\n', start);
19
+ assert.notEqual(bodyStart, -1, `missing body start: ${marker}`);
20
+ const end = script.indexOf(trailer, bodyStart + 1);
21
+ assert.notEqual(end, -1, `missing trailer: ${trailer}`);
22
+ return script.slice(bodyStart + 1, end);
23
+ }
24
+
25
+ function extractFunction(block, startName, endName) {
26
+ const start = block.indexOf(startName);
27
+ assert.notEqual(start, -1, `missing function: ${startName}`);
28
+ const end = block.indexOf(endName, start);
29
+ assert.notEqual(end, -1, `missing end marker: ${endName}`);
30
+ return block.slice(start, end).trimEnd();
31
+ }
32
+
33
+ function loadHelperApi() {
34
+ const helperBlock = extractBlock('cat <<\'NODE\' > "$_helper"', '\n export CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=');
35
+ const replaceSource = extractFunction(
36
+ helperBlock,
37
+ 'function replaceRequired(source, pattern, replacement, label, expectedCount) {',
38
+ '\n\nfunction parseScalar(value) {',
39
+ );
40
+ const fullWidthSource = extractFunction(
41
+ helperBlock,
42
+ 'function isFullWidthCodePoint(codePoint) {',
43
+ '\n\nfunction graphemeWidth(grapheme) {',
44
+ );
45
+ const graphemeWidthSource = extractFunction(
46
+ helperBlock,
47
+ 'function graphemeWidth(grapheme) {',
48
+ '\n\nfunction stringWidth(value) {',
49
+ );
50
+ const stringWidthSource = extractFunction(
51
+ helperBlock,
52
+ 'function stringWidth(value) {',
53
+ '\n\nfunction stripANSI(value) {',
54
+ );
55
+ const stripAnsiSource = extractFunction(
56
+ helperBlock,
57
+ 'function stripANSI(value) {',
58
+ '\n\nfunction wrapAnsi(value, columns, options = {}) {',
59
+ );
60
+ const cleanupSource = extractFunction(
61
+ helperBlock,
62
+ 'function cleanupStaleEntryFiles(currentWorkdir = workdir, currentEntryJsOffset = entryJsOffset, currentEntryEndOffset = entryEndOffset, now = Date.now()) {',
63
+ '\n\nfunction ensureEntryFile() {',
64
+ );
65
+ const wrapAnsiSource = extractFunction(
66
+ helperBlock,
67
+ 'function wrapAnsi(value, columns, options = {}) {',
68
+ '\n\nfunction stableHash(value, seed) {',
69
+ );
70
+ const stableHashSource = extractFunction(
71
+ helperBlock,
72
+ 'function stableHash(value, seed) {',
73
+ '\n\nfunction replaceRequired(source, pattern, replacement, label, expectedCount) {',
74
+ );
75
+ const rewriteSource = extractFunction(
76
+ helperBlock,
77
+ 'function rewriteNativeChunkSource(source) {',
78
+ '\n\nasync function main() {',
79
+ );
80
+
81
+ const context = vm.createContext({ module: { exports: {} }, exports: {}, fs, path });
82
+ vm.runInContext(
83
+ `${replaceSource}\n${cleanupSource}\n${fullWidthSource}\n${graphemeWidthSource}\n${stripAnsiSource}\n${stringWidthSource}\n${wrapAnsiSource}\n${stableHashSource}\n${rewriteSource}\nmodule.exports = { replaceRequired, cleanupStaleEntryFiles, isFullWidthCodePoint, graphemeWidth, stripANSI, stringWidth, wrapAnsi, stableHash, rewriteNativeChunkSource };`,
84
+ context,
85
+ );
86
+ return context.module.exports;
87
+ }
88
+
89
+ function sha256(filePath) {
90
+ return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex');
91
+ }
92
+
93
+ test('helper and bootstrap rewrite helpers stay identical', () => {
94
+ const helperBlock = extractBlock('cat <<\'NODE\' > "$_helper"', '\n export CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=');
95
+ const bootstrapBlock = extractBlock('cat <<\'NODE\' > "$_bootstrap"', '\n export CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=');
96
+
97
+ const helperReplace = extractFunction(
98
+ helperBlock,
99
+ 'function replaceRequired(source, pattern, replacement, label, expectedCount) {',
100
+ '\n\nfunction parseScalar(value) {',
101
+ );
102
+ const bootstrapReplace = extractFunction(
103
+ bootstrapBlock,
104
+ 'function replaceRequired(source, pattern, replacement, label, expectedCount) {',
105
+ '\n\nfunction parseScalar(value) {',
106
+ );
107
+ const helperRewrite = extractFunction(
108
+ helperBlock,
109
+ 'function rewriteNativeChunkSource(source) {',
110
+ '\n\nasync function main() {',
111
+ );
112
+ const bootstrapRewrite = extractFunction(
113
+ bootstrapBlock,
114
+ 'function rewriteNativeChunkSource(source) {',
115
+ '\n\nasync function main() {',
116
+ );
117
+ const helperWrapAnsi = extractFunction(
118
+ helperBlock,
119
+ 'function wrapAnsi(value, columns, options = {}) {',
120
+ '\n\nfunction stableHash(value, seed) {',
121
+ );
122
+ const helperCleanup = extractFunction(
123
+ helperBlock,
124
+ 'function cleanupStaleEntryFiles(currentWorkdir = workdir, currentEntryJsOffset = entryJsOffset, currentEntryEndOffset = entryEndOffset, now = Date.now()) {',
125
+ '\n\nfunction ensureEntryFile() {',
126
+ );
127
+ const bootstrapWrapAnsi = extractFunction(
128
+ bootstrapBlock,
129
+ 'function wrapAnsi(value, columns, options = {}) {',
130
+ '\n\nfunction stableHash(value, seed) {',
131
+ );
132
+ const bootstrapCleanup = extractFunction(
133
+ bootstrapBlock,
134
+ 'function cleanupStaleEntryFiles(currentWorkdir = workdir, currentEntryJsOffset = entryJsOffset, currentEntryEndOffset = entryEndOffset, now = Date.now()) {',
135
+ '\n\nfunction ensureEntryFile() {',
136
+ );
137
+ const helperStableHash = extractFunction(
138
+ helperBlock,
139
+ 'function stableHash(value, seed) {',
140
+ '\n\nfunction replaceRequired(source, pattern, replacement, label, expectedCount) {',
141
+ );
142
+ const bootstrapStableHash = extractFunction(
143
+ bootstrapBlock,
144
+ 'function stableHash(value, seed) {',
145
+ '\n\nfunction replaceRequired(source, pattern, replacement, label, expectedCount) {',
146
+ );
147
+ const helperStringWidth = extractFunction(
148
+ helperBlock,
149
+ 'function isFullWidthCodePoint(codePoint) {',
150
+ '\n\nfunction graphemeWidth(grapheme) {',
151
+ ) + '\n\n' + extractFunction(
152
+ helperBlock,
153
+ 'function graphemeWidth(grapheme) {',
154
+ '\n\nfunction stringWidth(value) {',
155
+ ) + '\n\n' + extractFunction(
156
+ helperBlock,
157
+ 'function stringWidth(value) {',
158
+ '\n\nfunction stripANSI(value) {',
159
+ );
160
+ const bootstrapStringWidth = extractFunction(
161
+ bootstrapBlock,
162
+ 'function isFullWidthCodePoint(codePoint) {',
163
+ '\n\nfunction graphemeWidth(grapheme) {',
164
+ ) + '\n\n' + extractFunction(
165
+ bootstrapBlock,
166
+ 'function graphemeWidth(grapheme) {',
167
+ '\n\nfunction stringWidth(value) {',
168
+ ) + '\n\n' + extractFunction(
169
+ bootstrapBlock,
170
+ 'function stringWidth(value) {',
171
+ '\n\nfunction stripANSI(value) {',
172
+ );
173
+
174
+ assert.equal(helperReplace, bootstrapReplace);
175
+ assert.equal(helperRewrite, bootstrapRewrite);
176
+ assert.equal(helperWrapAnsi, bootstrapWrapAnsi);
177
+ assert.equal(helperCleanup, bootstrapCleanup);
178
+ assert.equal(helperStableHash, bootstrapStableHash);
179
+ assert.equal(helperStringWidth, bootstrapStringWidth);
180
+ });
181
+
182
+ test('print path does not defer cleanup to exit', () => {
183
+ const helperBlock = extractBlock('cat <<\'NODE\' > "$_helper"', '\n export CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=');
184
+
185
+ assert.equal(helperBlock.includes('CLAUDE_TERMUX_PRINT_WAIT_MS'), true);
186
+ assert.equal(helperBlock.includes('setTimeout(resolve, printWaitMs)'), true);
187
+ assert.equal(helperBlock.includes('process.once(\'exit\''), false);
188
+ assert.equal(helperBlock.includes('process.removeListener(\'uncaughtException\''), true);
189
+ });
190
+
191
+ test('bootstrap path defers cleanup to exit', () => {
192
+ const bootstrapBlock = extractBlock('cat <<\'NODE\' > "$_bootstrap"', '\n export CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=');
193
+
194
+ assert.equal(bootstrapBlock.includes('CLAUDE_TERMUX_PRINT_WAIT_MS'), false);
195
+ assert.equal(bootstrapBlock.includes('setTimeout(resolve, printWaitMs)'), false);
196
+ assert.equal(bootstrapBlock.includes('process.once(\'exit\''), true);
197
+ assert.equal(bootstrapBlock.includes('process.removeListener(\'uncaughtException\''), true);
198
+ });
199
+
200
+ test('entry extraction uses a process-unique filename', () => {
201
+ const helperBlock = extractBlock('cat <<\'NODE\' > "$_helper"', '\n export CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=');
202
+ const bootstrapBlock = extractBlock('cat <<\'NODE\' > "$_bootstrap"', '\n export CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=');
203
+
204
+ assert.equal(helperBlock.includes('process.pid'), true);
205
+ assert.equal(helperBlock.includes('Math.random'), true);
206
+ assert.equal(bootstrapBlock.includes('process.pid'), true);
207
+ assert.equal(bootstrapBlock.includes('Math.random'), true);
208
+ });
209
+
210
+ test('replaceRequired enforces the expected replacement count', () => {
211
+ const { replaceRequired } = loadHelperApi();
212
+
213
+ assert.equal(replaceRequired('abc abc', /abc/g, 'x', 'abc', 2), 'x x');
214
+ assert.throws(
215
+ () => replaceRequired('abc abc', /abc/g, 'x', 'abc', 1),
216
+ /unexpected abc count 2/,
217
+ );
218
+ });
219
+
220
+ test('stringWidth treats full-width and combining text as expected', () => {
221
+ const { stringWidth } = loadHelperApi();
222
+
223
+ assert.equal(stringWidth('abc'), 3);
224
+ assert.equal(stringWidth('あ'), 2);
225
+ assert.equal(stringWidth('a\u0301'), 1);
226
+ assert.equal(stringWidth('🙂'), 2);
227
+ assert.equal(stringWidth('🚀'), 2);
228
+ assert.equal(stringWidth('🚗'), 2);
229
+ assert.equal(stringWidth('❤️'), 2);
230
+ assert.equal(stringWidth('☕️'), 2);
231
+ assert.equal(stringWidth('🇯🇵'), 2);
232
+ assert.equal(stringWidth('#️⃣'), 2);
233
+ assert.equal(stringWidth('👨‍👩‍👧‍👦'), 2);
234
+ assert.equal(stringWidth('\u001b[31mあ\u001b[0m'), 2);
235
+ assert.equal(stringWidth('\u001b[36m🇯🇵\u001b[0m'), 2);
236
+ });
237
+
238
+ test('stableHash incorporates the optional seed', () => {
239
+ const { stableHash } = loadHelperApi();
240
+
241
+ assert.equal(stableHash('abc', 123), stableHash('abc', 123));
242
+ assert.notEqual(stableHash('abc', 123), stableHash('abc', 456));
243
+ });
244
+
245
+ test('wrapAnsi respects soft wrap, trim, and no-wrap options', () => {
246
+ const { wrapAnsi } = loadHelperApi();
247
+
248
+ assert.equal(wrapAnsi('abc def', 4, { hard: false }), 'abc\ndef');
249
+ assert.equal(wrapAnsi(' abc', 10, { trim: true }), 'abc');
250
+ assert.equal(wrapAnsi('abc ', 10, { trim: true }), 'abc');
251
+ assert.equal(wrapAnsi('abc def', 3, { wordWrap: false }).includes('\n'), true);
252
+ assert.equal(wrapAnsi('ab\u001b[31mcd', 3).includes('\n'), true);
253
+ });
254
+
255
+ test('cleanupStaleEntryFiles removes only stale extracted files for the same offsets', () => {
256
+ const { cleanupStaleEntryFiles } = loadHelperApi();
257
+ const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-termux-'));
258
+ const tmpdir = fs.mkdtempSync(path.join(tmpRoot, 'work-'));
259
+ const stale = path.join(tmpdir, 'cli.11.22.123.old.bare-path.js');
260
+ const fresh = path.join(tmpdir, 'cli.11.22.456.new.bare-path.js');
261
+ const otherOffsets = path.join(tmpdir, 'cli.33.44.999.old.bare-path.js');
262
+ try {
263
+ fs.writeFileSync(stale, 'stale');
264
+ fs.writeFileSync(fresh, 'fresh');
265
+ fs.writeFileSync(otherOffsets, 'other');
266
+ const oldTime = new Date(Date.now() - (2 * 24 * 60 * 60 * 1000));
267
+ fs.utimesSync(stale, oldTime, oldTime);
268
+ fs.utimesSync(otherOffsets, oldTime, oldTime);
269
+
270
+ cleanupStaleEntryFiles(tmpdir, 11, 22, Date.now());
271
+
272
+ assert.equal(fs.existsSync(stale), false);
273
+ assert.equal(fs.existsSync(fresh), true);
274
+ assert.equal(fs.existsSync(otherOffsets), true);
275
+ } finally {
276
+ fs.rmSync(tmpRoot, { recursive: true, force: true });
277
+ }
278
+ });
279
+
280
+ function buildSyntheticBundleSource() {
281
+ const typeofBun = Array.from({ length: 3 }, () => 'typeof Bun').join('; ');
282
+ const bunProps = Array.from({ length: 33 }, (_, index) => `Bun.p${index}`).join('; ');
283
+ return `function(exports, require, module, __filename, __dirname) { ${typeofBun}; typeof globalThis.Bun; globalThis.Bun; ${bunProps}; }`;
284
+ }
285
+
286
+ test('rewriteNativeChunkSource rewrites the synthetic bundle slice', () => {
287
+ const { rewriteNativeChunkSource } = loadHelperApi();
288
+ const source = buildSyntheticBundleSource();
289
+ const patched = rewriteNativeChunkSource(source);
290
+
291
+ assert.match(patched, /var __claudeBun = globalThis\.__claudeBunShim;/);
292
+ assert.match(patched, /typeof __claudeBun/);
293
+ assert.match(patched, /typeof globalThis\.__claudeBun/);
294
+ assert.match(patched, /globalThis\.__claudeBun/);
295
+ assert.match(patched, /__claudeBun\./);
296
+ });
297
+
298
+ test('rewriteNativeChunkSource rejects unexpected replacement counts', () => {
299
+ const { rewriteNativeChunkSource } = loadHelperApi();
300
+ const source = buildSyntheticBundleSource().replace('Bun.p30;', '');
301
+
302
+ assert.throws(
303
+ () => rewriteNativeChunkSource(source),
304
+ /unexpected Bun property access count 32/,
305
+ );
306
+ });
307
+
308
+ const tarballPath = path.join(__dirname, '..', 'bash0816-claude-code-2.1.177.tgz');
309
+
310
+ test('tarball contents match the workspace runner and test file', { skip: !fs.existsSync(tarballPath) }, () => {
311
+ const tarRunner = child_process.execFileSync('tar', ['-xOf', tarballPath, 'package/lib/termux-run-claude-native.sh']);
312
+ const tarTest = child_process.execFileSync('tar', ['-xOf', tarballPath, 'package/lib/termux-run-claude-native.test.js']);
313
+
314
+ const worktreeRunner = fs.readFileSync(path.join(__dirname, 'termux-run-claude-native.sh'));
315
+ const worktreeTest = fs.readFileSync(path.join(__dirname, 'termux-run-claude-native.test.js'));
316
+
317
+ assert.equal(crypto.createHash('sha256').update(tarRunner).digest('hex'), crypto.createHash('sha256').update(worktreeRunner).digest('hex'));
318
+ assert.equal(crypto.createHash('sha256').update(tarTest).digest('hex'), crypto.createHash('sha256').update(worktreeTest).digest('hex'));
319
+ assert.equal(tarRunner.length, worktreeRunner.length);
320
+ assert.equal(tarTest.length, worktreeTest.length);
321
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bash0816/claude-code",
3
- "version": "2.1.161",
3
+ "version": "2.1.177",
4
4
  "description": "Unofficial Termux-native Claude Code wrapper with audited native replay",
5
5
  "license": "Apache-2.0",
6
6
  "bin": {