@bash0816/claude-code 2.1.161 โ 2.1.176
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/LICENSE +684 -198
- package/README.md +9 -1
- package/bin/claude +9 -5
- package/config/claude-native-audited-versions.json +101 -0
- package/config/claude-termux-release-manifest.json +3 -3
- package/lib/termux-run-claude-native.sh +605 -68
- package/lib/termux-run-claude-native.test.js +323 -0
- package/package.json +2 -2
|
@@ -0,0 +1,323 @@
|
|
|
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}; npmInstallDeprecated:!0 }`;
|
|
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
|
+
assert.match(patched, /npmInstallDeprecated:!1/);
|
|
297
|
+
assert.doesNotMatch(patched, /npmInstallDeprecated:!0/);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test('rewriteNativeChunkSource rejects unexpected replacement counts', () => {
|
|
301
|
+
const { rewriteNativeChunkSource } = loadHelperApi();
|
|
302
|
+
const source = buildSyntheticBundleSource().replace('Bun.p30;', '');
|
|
303
|
+
|
|
304
|
+
assert.throws(
|
|
305
|
+
() => rewriteNativeChunkSource(source),
|
|
306
|
+
/unexpected Bun property access count 32/,
|
|
307
|
+
);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
const tarballPath = path.join(__dirname, '..', 'bash0816-claude-code-2.1.177.tgz');
|
|
311
|
+
|
|
312
|
+
test('tarball contents match the workspace runner and test file', { skip: !fs.existsSync(tarballPath) }, () => {
|
|
313
|
+
const tarRunner = child_process.execFileSync('tar', ['-xOf', tarballPath, 'package/lib/termux-run-claude-native.sh']);
|
|
314
|
+
const tarTest = child_process.execFileSync('tar', ['-xOf', tarballPath, 'package/lib/termux-run-claude-native.test.js']);
|
|
315
|
+
|
|
316
|
+
const worktreeRunner = fs.readFileSync(path.join(__dirname, 'termux-run-claude-native.sh'));
|
|
317
|
+
const worktreeTest = fs.readFileSync(path.join(__dirname, 'termux-run-claude-native.test.js'));
|
|
318
|
+
|
|
319
|
+
assert.equal(crypto.createHash('sha256').update(tarRunner).digest('hex'), crypto.createHash('sha256').update(worktreeRunner).digest('hex'));
|
|
320
|
+
assert.equal(crypto.createHash('sha256').update(tarTest).digest('hex'), crypto.createHash('sha256').update(worktreeTest).digest('hex'));
|
|
321
|
+
assert.equal(tarRunner.length, worktreeRunner.length);
|
|
322
|
+
assert.equal(tarTest.length, worktreeTest.length);
|
|
323
|
+
});
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bash0816/claude-code",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.176",
|
|
4
4
|
"description": "Unofficial Termux-native Claude Code wrapper with audited native replay",
|
|
5
|
-
"license": "
|
|
5
|
+
"license": "GPL-3.0-only",
|
|
6
6
|
"bin": {
|
|
7
7
|
"claude": "bin/claude"
|
|
8
8
|
},
|