@bash0816/claude-code 2.1.144 → 2.1.150-1

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
@@ -1,17 +1,19 @@
1
- # @bash0816/claude-code
1
+ # @bash0816/cluade-code
2
2
 
3
3
  Termux-native Claude Code wrapper package.
4
4
 
5
5
  Termux 向け Claude Code native wrapper package です。
6
6
 
7
- This package is the current Termux wrapper package line.
7
+ Legacy compatibility package:
8
8
 
9
- この package は現行の Termux wrapper package 系です。
9
+ 互換維持用の legacy package:
10
+
11
+ - canonical package: `@bash0816/claude-code`
10
12
 
11
13
  ## Install / インストール
12
14
 
13
15
  ```sh
14
- npm install -g @bash0816/claude-code@latest
16
+ npm install -g @bash0816/cluade-code@latest
15
17
  ```
16
18
 
17
19
  If an older audited launcher from this repository already exists at `$PREFIX/bin/claude`, migrate through the updated launcher.
@@ -27,38 +29,19 @@ If npm reports `EEXIST` before package scripts run, use a one-time forced migrat
27
29
  package script 実行前に npm が `EEXIST` を返す場合は、初回だけ forced migration を使います。
28
30
 
29
31
  ```sh
30
- npm install -g --force @bash0816/claude-code@latest
32
+ npm install -g --force @bash0816/cluade-code@latest
31
33
  ```
32
34
 
33
35
  After npm owns the `claude` bin link, normal `npm install -g` updates work.
34
36
 
35
37
  npm が `claude` bin link を管理する状態になれば、通常の `npm install -g` 更新が使えます。
36
38
 
37
- Specific audited versions:
39
+ Specific audited version:
38
40
 
39
41
  監査済みの固定 version:
40
42
 
41
- Current quick examples:
42
-
43
- 現在の quick example:
44
-
45
43
  ```sh
46
- npm install -g @bash0816/claude-code@2.1.118
47
- npm install -g @bash0816/claude-code@2.1.119
48
- npm install -g @bash0816/claude-code@2.1.121
49
- npm install -g @bash0816/claude-code@2.1.122
50
- npm install -g @bash0816/claude-code@2.1.123
51
- npm install -g @bash0816/claude-code@2.1.126
52
- npm install -g @bash0816/claude-code@2.1.128
53
- npm install -g @bash0816/claude-code@2.1.136
54
- npm install -g @bash0816/claude-code@2.1.137
55
- npm install -g @bash0816/claude-code@2.1.138
56
- npm install -g @bash0816/claude-code@2.1.139
57
- npm install -g @bash0816/claude-code@2.1.140
58
- npm install -g @bash0816/claude-code@2.1.141
59
- npm install -g @bash0816/claude-code@2.1.142
60
- npm install -g @bash0816/claude-code@2.1.143
61
- npm install -g @bash0816/claude-code@2.1.144
44
+ npm install -g @bash0816/cluade-code@2.1.123
62
45
  ```
63
46
 
64
47
  ## Update / 更新
@@ -67,9 +50,17 @@ npm install -g @bash0816/claude-code@2.1.144
67
50
  claude update
68
51
  ```
69
52
 
70
- `claude update` installs the latest audited package version.
53
+ `claude update` checks the GitHub release manifest and installs the latest audited package version.
54
+
55
+ `claude update` は GitHub release manifest を確認し、最新の監査済み package version を install します。
56
+
57
+ For the final legacy bridge release, `claude update` migrates this typo package to the canonical package.
58
+
59
+ 最終 legacy bridge release では、`claude update` がこの typo package から canonical package へ移行します。
60
+
61
+ This final bridge package is frozen at package version `2.1.123` and launches audited native `2.1.150` by default from the synced metadata set.
71
62
 
72
- `claude update` は最新の監査済み package version install します。
63
+ この最終 bridge package package version `2.1.123` で凍結し、同期済み metadata 集合から既定では監査済み native `2.1.150` を起動します。
73
64
 
74
65
  ```sh
75
66
  npm install -g @bash0816/claude-code@<latest_audited_version>
@@ -83,10 +74,10 @@ Normal launch also checks the same manifest with a short timeout and prints a no
83
74
 
84
75
  - Only audited versions in `config/claude-native-audited-versions.json` can run.
85
76
  - `config/claude-native-audited-versions.json` にある監査済み version だけを実行できます。
86
- - `2.1.118`, `2.1.119`, `2.1.121`, `2.1.122`, `2.1.123`, `2.1.126`, `2.1.128`, `2.1.136`, `2.1.137`, `2.1.138`, `2.1.139`, `2.1.140`, `2.1.141`, `2.1.142`, `2.1.143`, `2.1.144` are included in the package metadata.
87
- - `2.1.118`、`2.1.119`、`2.1.121`、`2.1.122`、`2.1.123`、`2.1.126`、`2.1.128`、`2.1.136`、`2.1.137`、`2.1.138`、`2.1.139`、`2.1.140`、`2.1.141`、`2.1.142`、`2.1.143`、`2.1.144` package metadata に含めています。
88
- - The metadata file is the source of truth for the currently audited set.
89
- - 現在の監査済み version 集合の source of truth metadata file です。
77
+ - This legacy package is frozen at `2.1.123` and exists only as a migration bridge to `@bash0816/claude-code`.
78
+ - この legacy package は `2.1.123` で凍結しており、`@bash0816/claude-code` への migration bridge としてのみ存在します。
79
+ - The metadata file remains the source of truth for the audited version set.
80
+ - 監査済み version 集合の source of truth は引き続き metadata file です。
90
81
  - Native artifacts are cached under `${HOME}/.claude-termux-native-package`.
91
82
  - native artifact は `${HOME}/.claude-termux-native-package` に cache します。
92
83
  - If native preparation fails, the command exits with an error.
package/bin/claude CHANGED
@@ -14,7 +14,8 @@ done
14
14
  PACKAGE_DIR=$(CDPATH= cd -- "$(dirname -- "${SELF}")/.." && pwd)
15
15
  CONFIG_FILE="${PACKAGE_DIR}/config/claude-native-audited-versions.json"
16
16
  PACKAGE_VERSION=$(node -e 'const fs=require("fs"); const path=require("path"); const p=JSON.parse(fs.readFileSync(path.join(process.argv[1],"package.json"),"utf8")); process.stdout.write(p.version)' "${PACKAGE_DIR}")
17
- CLAUDE_VERSION="${CLAUDE_TERMUX_CLAUDE_VERSION:-${PACKAGE_VERSION}}"
17
+ DEFAULT_NATIVE_VERSION=$(node -e 'const fs=require("fs"); const path=require("path"); const p=JSON.parse(fs.readFileSync(path.join(process.argv[1],"config","claude-termux-release-manifest.json"),"utf8")); process.stdout.write(String(p.default_native_version || p.latest_audited_version || ""))' "${PACKAGE_DIR}")
18
+ CLAUDE_VERSION="${CLAUDE_TERMUX_CLAUDE_VERSION:-${DEFAULT_NATIVE_VERSION:-${PACKAGE_VERSION}}}"
18
19
  UPDATE_HELPER="${PACKAGE_DIR}/lib/check-updates.js"
19
20
 
20
21
  if [ "${1:-}" = "update" ]; then
@@ -57,6 +58,5 @@ WORKDIR="${CACHE_DIR}/launcher-workdir" \
57
58
  ENTRY_JS_OFFSET="${ENTRY_JS_OFFSET}" \
58
59
  ENTRY_END_OFFSET="${ENTRY_END_OFFSET}" \
59
60
  CURRENT_CLAUDE_VERSION="${CLAUDE_VERSION}" \
60
- CLAUDE_TERMUX_PACKAGE_DIR="${PACKAGE_DIR}" \
61
61
  CLAUDE_NATIVE_UPDATE_CHECK=0 \
62
62
  exec sh "${PACKAGE_DIR}/lib/termux-run-claude-native.sh" "$@"
@@ -111,6 +111,20 @@
111
111
  "entry_js_offset": 216877940,
112
112
  "entry_end_offset": 231466269,
113
113
  "status": "termux_verified"
114
+ },
115
+ "2.1.150": {
116
+ "wrapper_spec": "@anthropic-ai/claude-code@2.1.150",
117
+ "native_spec": "@anthropic-ai/claude-code-linux-arm64@2.1.150",
118
+ "entry_js_offset": 221278436,
119
+ "entry_end_offset": 236586147,
120
+ "status": "termux_verified"
121
+ },
122
+ "2.1.150-1": {
123
+ "wrapper_spec": "@anthropic-ai/claude-code@2.1.150",
124
+ "native_spec": "@anthropic-ai/claude-code-linux-arm64@2.1.150",
125
+ "entry_js_offset": 221278436,
126
+ "entry_end_offset": 236586147,
127
+ "status": "termux_verified"
114
128
  }
115
129
  }
116
130
  }
@@ -1,8 +1,13 @@
1
1
  {
2
2
  "manifest_version": 1,
3
3
  "package_name": "@bash0816/claude-code",
4
- "latest_audited_version": "2.1.144",
5
- "latest_candidate_version": "2.1.144",
6
- "previous_stable_version": "2.1.143",
7
- "manifest_url": "https://raw.githubusercontent.com/bash0816/ClaudeCode-Termux/main/config/claude-termux-release-manifest.json"
4
+ "canonical_package_name": "@bash0816/claude-code",
5
+ "canonical_manifest_url": "https://raw.githubusercontent.com/bash0816/ClaudeCode-Termux/main/config/claude-termux-release-manifest.json",
6
+ "canonical_repository_url": "https://github.com/bash0816/ClaudeCode-Termux",
7
+ "bridge_package_version": "2.1.123",
8
+ "default_native_version": "2.1.150-1",
9
+ "latest_audited_version": "2.1.150-1",
10
+ "latest_candidate_version": "2.1.150-1",
11
+ "previous_stable_version": "2.1.150",
12
+ "manifest_url": "https://raw.githubusercontent.com/bash0816/CluadeCode-Termux/main/config/claude-termux-release-manifest.json"
8
13
  }
@@ -19,6 +19,9 @@ const timeoutMs = Number(process.env.CLAUDE_TERMUX_UPDATE_TIMEOUT_MS || '1200');
19
19
  const ttlMs = Number(process.env.CLAUDE_TERMUX_UPDATE_CACHE_TTL_MS || String(24 * 60 * 60 * 1000));
20
20
  const skipCheck = process.env.CLAUDE_TERMUX_SKIP_UPDATE_CHECK === '1';
21
21
  const manifestUrl = process.env.CLAUDE_TERMUX_MANIFEST_URL || localManifest.manifest_url;
22
+ const canonicalManifestUrl = process.env.CLAUDE_TERMUX_CANONICAL_MANIFEST_URL || localManifest.canonical_manifest_url || '';
23
+ const canonicalPackageName = process.env.CLAUDE_TERMUX_CANONICAL_PACKAGE_NAME || localManifest.canonical_package_name || '';
24
+ const canonicalRepositoryUrl = process.env.CLAUDE_TERMUX_CANONICAL_REPOSITORY_URL || localManifest.canonical_repository_url || '';
22
25
 
23
26
  function compareVersions(a, b) {
24
27
  const aParts = String(a).split('.').map(Number);
@@ -102,10 +105,64 @@ async function resolveManifest(forceRefresh) {
102
105
  }
103
106
  }
104
107
 
105
- function updateNoticeCache(cache, latestVersion) {
108
+ async function resolveCanonicalManifest() {
109
+ if (!canonicalManifestUrl || !canonicalPackageName || canonicalPackageName === pkg.name) {
110
+ return null;
111
+ }
112
+
113
+ if (skipCheck) {
114
+ return {
115
+ packageName: canonicalPackageName,
116
+ latestVersion: localManifest.latest_audited_version || pkg.version,
117
+ repositoryUrl: canonicalRepositoryUrl,
118
+ };
119
+ }
120
+
121
+ try {
122
+ const manifest = await fetchManifest(canonicalManifestUrl);
123
+ return {
124
+ packageName: manifest.package_name || canonicalPackageName,
125
+ latestVersion: manifest.latest_audited_version || localManifest.latest_audited_version || pkg.version,
126
+ repositoryUrl: canonicalRepositoryUrl,
127
+ };
128
+ } catch {
129
+ return {
130
+ packageName: canonicalPackageName,
131
+ latestVersion: localManifest.latest_audited_version || pkg.version,
132
+ repositoryUrl: canonicalRepositoryUrl,
133
+ };
134
+ }
135
+ }
136
+
137
+ async function resolveTarget(forceRefresh) {
138
+ const base = await resolveManifest(forceRefresh);
139
+ const canonical = await resolveCanonicalManifest();
140
+ if (!canonical) {
141
+ return {
142
+ manifest: base.manifest,
143
+ cache: base.cache,
144
+ packageName: base.manifest.package_name || pkg.name,
145
+ targetVersion: base.manifest.latest_audited_version || localManifest.latest_audited_version || pkg.version,
146
+ repositoryUrl: '',
147
+ migration: false,
148
+ };
149
+ }
150
+
151
+ return {
152
+ manifest: base.manifest,
153
+ cache: base.cache,
154
+ packageName: canonical.packageName,
155
+ targetVersion: canonical.latestVersion,
156
+ repositoryUrl: canonical.repositoryUrl,
157
+ migration: canonical.packageName !== pkg.name,
158
+ };
159
+ }
160
+
161
+ function updateNoticeCache(cache, packageName, latestVersion) {
106
162
  const nextCache = {
107
163
  ...cache,
108
164
  last_notice: {
165
+ package_name: packageName,
109
166
  current_version: currentVersion,
110
167
  latest_audited_version: latestVersion,
111
168
  notified_at: Date.now(),
@@ -114,16 +171,17 @@ function updateNoticeCache(cache, latestVersion) {
114
171
  writeCache(nextCache);
115
172
  }
116
173
 
117
- function shouldNotify(cache, latestVersion) {
174
+ function shouldNotify(cache, packageName, latestVersion) {
118
175
  const lastNotice = cache.last_notice;
119
176
  if (!lastNotice) return true;
177
+ if (lastNotice.package_name !== packageName) return true;
120
178
  if (lastNotice.current_version !== currentVersion) return true;
121
179
  if (lastNotice.latest_audited_version !== latestVersion) return true;
122
180
  return Date.now() - Number(lastNotice.notified_at || 0) >= ttlMs;
123
181
  }
124
182
 
125
- function installTarget(targetVersion) {
126
- const spec = `${pkg.name}@${targetVersion}`;
183
+ function installTarget(packageName, targetVersion) {
184
+ const spec = `${packageName}@${targetVersion}`;
127
185
  const command = `npm install -g ${spec}`;
128
186
 
129
187
  if (dryRun) {
@@ -140,25 +198,40 @@ function installTarget(targetVersion) {
140
198
  }
141
199
 
142
200
  async function runNotify() {
143
- const { manifest, cache } = await resolveManifest(false);
144
- const latest = manifest.latest_audited_version || localManifest.latest_audited_version || pkg.version;
145
- if (compareVersions(currentVersion, latest) >= 0) return 0;
146
- if (!shouldNotify(cache, latest)) return 0;
201
+ const target = await resolveTarget(false);
202
+ const latest = target.targetVersion;
203
+ const packageName = target.packageName;
204
+ const migration = target.migration;
205
+ if (!migration && compareVersions(currentVersion, latest) >= 0) return 0;
206
+ if (!shouldNotify(target.cache, packageName, latest)) return 0;
207
+
208
+ if (migration) {
209
+ console.error(`Legacy package detected: ${pkg.name}`);
210
+ console.error(`Canonical package available: ${packageName}@${latest}`);
211
+ if (target.repositoryUrl) {
212
+ console.error(`Canonical repository: ${target.repositoryUrl}`);
213
+ }
214
+ console.error('Run: claude update');
215
+ updateNoticeCache(target.cache, packageName, latest);
216
+ return 0;
217
+ }
147
218
 
148
219
  console.error(`Audited update available: ${currentVersion} -> ${latest}`);
149
220
  console.error('Run: claude update');
150
- updateNoticeCache(cache, latest);
221
+ updateNoticeCache(target.cache, packageName, latest);
151
222
  return 0;
152
223
  }
153
224
 
154
225
  async function runUpdate() {
155
- const { manifest } = await resolveManifest(true);
156
- const target = manifest.latest_audited_version || localManifest.latest_audited_version || pkg.version;
157
- if (compareVersions(currentVersion, target) >= 0) {
226
+ const target = await resolveTarget(true);
227
+ if (!target.migration && compareVersions(currentVersion, target.targetVersion) >= 0) {
158
228
  console.error(`Already on latest audited version: ${currentVersion}`);
159
229
  return 0;
160
230
  }
161
- return installTarget(target);
231
+ if (target.migration) {
232
+ console.error(`Migrating from legacy package ${pkg.name} to canonical package ${target.packageName}@${target.targetVersion}`);
233
+ }
234
+ return installTarget(target.packageName, target.targetVersion);
162
235
  }
163
236
 
164
237
  async function main() {
@@ -10,8 +10,9 @@ if (process.env.CLAUDE_TERMUX_SKIP_POSTINSTALL === '1') {
10
10
 
11
11
  const packageDir = path.resolve(__dirname, '..');
12
12
  const pkg = require(path.join(packageDir, 'package.json'));
13
+ const manifest = require(path.join(packageDir, 'config', 'claude-termux-release-manifest.json'));
13
14
  const script = path.join(__dirname, 'prepare-native.js');
14
- const version = process.env.CLAUDE_TERMUX_CLAUDE_VERSION || pkg.version;
15
+ const version = process.env.CLAUDE_TERMUX_CLAUDE_VERSION || manifest.default_native_version || manifest.latest_audited_version || pkg.version;
15
16
 
16
17
  const result = cp.spawnSync(process.execPath, [script, version], {
17
18
  stdio: 'inherit',
@@ -9,11 +9,9 @@ const path = require('path');
9
9
  const packageDir = path.resolve(__dirname, '..');
10
10
  const config = require(path.join(packageDir, 'config', 'claude-native-audited-versions.json'));
11
11
  const pkg = require(path.join(packageDir, 'package.json'));
12
- const version = process.argv[2] || pkg.version;
12
+ const manifest = require(path.join(packageDir, 'config', 'claude-termux-release-manifest.json'));
13
+ const version = process.argv[2] || manifest.default_native_version || manifest.latest_audited_version || pkg.version;
13
14
  const item = config.versions[version];
14
- const curlRetries = process.env.CLAUDE_TERMUX_FETCH_RETRIES || '4';
15
- const curlConnectTimeout = process.env.CLAUDE_TERMUX_FETCH_CONNECT_TIMEOUT || '20';
16
- const curlMaxTime = process.env.CLAUDE_TERMUX_FETCH_MAX_TIME || '300';
17
15
 
18
16
  if (!item) {
19
17
  console.error(`Unsupported audited Claude Code version: ${version}`);
@@ -31,14 +29,18 @@ if (fs.existsSync(sourceBin)) {
31
29
  }
32
30
 
33
31
  fs.mkdirSync(versionDir, { recursive: true });
34
- const packDir = fs.mkdtempSync(path.join(os.tmpdir(), `claude-code-${version}-`));
32
+ const packDir = fs.mkdtempSync(path.join(os.tmpdir(), `cluade-code-${version}-`));
35
33
 
36
34
  try {
37
- const tgzPath = fetchNativeTarball(item.native_spec, packDir);
35
+ run('npm', ['pack', item.native_spec, '--pack-destination', packDir]);
36
+ const tgz = fs.readdirSync(packDir).find(name => name.endsWith('.tgz'));
37
+ if (!tgz) {
38
+ throw new Error('npm pack did not produce a tgz');
39
+ }
38
40
 
39
41
  const extractDir = path.join(packDir, 'native');
40
42
  fs.mkdirSync(extractDir, { recursive: true });
41
- run('tar', ['-xzf', tgzPath, '-C', extractDir]);
43
+ run('tar', ['-xzf', path.join(packDir, tgz), '-C', extractDir]);
42
44
 
43
45
  fs.rmSync(nativeDest, { recursive: true, force: true });
44
46
  fs.mkdirSync(path.dirname(nativeDest), { recursive: true });
@@ -55,50 +57,6 @@ function run(command, args) {
55
57
  }
56
58
  }
57
59
 
58
- function runCapture(command, args) {
59
- const result = cp.spawnSync(command, args, {
60
- stdio: ['ignore', 'pipe', 'inherit'],
61
- encoding: 'utf8',
62
- });
63
- if (result.status !== 0) {
64
- throw new Error(`${command} ${args.join(' ')} failed`);
65
- }
66
- return result.stdout.trim();
67
- }
68
-
69
- function fetchNativeTarball(spec, packDir) {
70
- const directTarball = path.join(packDir, 'native-package.tgz');
71
-
72
- try {
73
- const tarballUrl = runCapture('npm', ['view', spec, 'dist.tarball', '--json']).replace(/^"|"$/g, '');
74
- if (!tarballUrl) {
75
- throw new Error(`npm view did not return dist.tarball for ${spec}`);
76
- }
77
- run('curl', [
78
- '--fail',
79
- '--location',
80
- '--retry', String(curlRetries),
81
- '--connect-timeout', String(curlConnectTimeout),
82
- '--max-time', String(curlMaxTime),
83
- '--output', directTarball,
84
- tarballUrl,
85
- ]);
86
- if (fs.existsSync(directTarball)) {
87
- return directTarball;
88
- }
89
- } catch (error) {
90
- console.error(`Direct tarball fetch failed for ${spec}; falling back to npm pack.`);
91
- console.error(error && error.message ? error.message : String(error));
92
- }
93
-
94
- run('npm', ['pack', spec, '--pack-destination', packDir]);
95
- const tgz = fs.readdirSync(packDir).find(name => name.endsWith('.tgz'));
96
- if (!tgz) {
97
- throw new Error('npm pack did not produce a tgz');
98
- }
99
- return path.join(packDir, tgz);
100
- }
101
-
102
60
  function validateOffsets(file, audited) {
103
61
  const buf = fs.readFileSync(file);
104
62
  const start = Number(audited.entry_js_offset);
@@ -6,7 +6,6 @@ WORKDIR="${WORKDIR:-${HOME}/.claude-termux-native-package/launcher-workdir}"
6
6
  ENTRY_JS_OFFSET="${ENTRY_JS_OFFSET:?ENTRY_JS_OFFSET is required}"
7
7
  ENTRY_END_OFFSET="${ENTRY_END_OFFSET:?ENTRY_END_OFFSET is required}"
8
8
  CURRENT_CLAUDE_VERSION="${CURRENT_CLAUDE_VERSION:?CURRENT_CLAUDE_VERSION is required}"
9
- CLAUDE_TERMUX_PACKAGE_DIR="${CLAUDE_TERMUX_PACKAGE_DIR:?CLAUDE_TERMUX_PACKAGE_DIR is required}"
10
9
  TERMUX_TMPDIR="${TMPDIR:-/data/data/com.termux/files/usr/tmp}"
11
10
  SSL_CERT_DIR="${SSL_CERT_DIR:-/data/data/com.termux/files/usr/etc/tls}"
12
11
  SSL_CERT_FILE="${SSL_CERT_FILE:-/data/data/com.termux/files/usr/etc/tls/cert.pem}"
@@ -43,10 +42,6 @@ export CURRENT_CLAUDE_VERSION
43
42
  node - "$@" <<'NODE'
44
43
  const fs = require('fs');
45
44
  const path = require('path');
46
- const {
47
- BLOCK_MESSAGE,
48
- createGuardedChildProcess,
49
- } = require(path.join(process.env.CLAUDE_TERMUX_PACKAGE_DIR, 'lib', 'native-update-guard.js'));
50
45
 
51
46
  const sourceBin = process.env.SOURCE_BIN;
52
47
  const workdir = process.env.WORKDIR;
@@ -78,20 +73,8 @@ function ensureEntryFile() {
78
73
  return extractedFile;
79
74
  }
80
75
 
81
- function stringWidth(value) {
82
- const text = String(value ?? '');
83
- if (typeof Intl !== 'undefined' && typeof Intl.Segmenter === 'function') {
84
- const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
85
- let width = 0;
86
- for (const _segment of segmenter.segment(text)) width += 1;
87
- return width;
88
- }
89
- return Array.from(text).length;
90
- }
91
-
92
76
  function createFakeRequire(realRequire) {
93
77
  const realChild = realRequire('child_process');
94
-
95
78
  function rewriteArgs(args) {
96
79
  if (
97
80
  Array.isArray(args) &&
@@ -104,18 +87,6 @@ function createFakeRequire(realRequire) {
104
87
  return args;
105
88
  }
106
89
 
107
- const guardedChild = createGuardedChildProcess(
108
- {
109
- spawn: (...args) => realChild.spawn(...rewriteArgs(args)),
110
- execFile: (...args) => realChild.execFile(...args),
111
- exec: (...args) => realChild.exec(...args),
112
- spawnSync: (...args) => realChild.spawnSync(...rewriteArgs(args)),
113
- execFileSync: (...args) => realChild.execFileSync(...args),
114
- execSync: (...args) => realChild.execSync(...args),
115
- },
116
- value => process.stderr.write(value),
117
- );
118
-
119
90
  return function fakeRequire(id) {
120
91
  if (id === 'ws') {
121
92
  class WS {
@@ -130,11 +101,14 @@ function createFakeRequire(realRequire) {
130
101
  }
131
102
 
132
103
  if (id === 'child_process') {
133
- return guardedChild;
134
- }
135
-
136
- if (id === 'node:child_process') {
137
- return guardedChild;
104
+ return {
105
+ spawn: (...args) => realChild.spawn(...rewriteArgs(args)),
106
+ execFile: (...args) => realChild.execFile(...args),
107
+ exec: (...args) => realChild.exec(...args),
108
+ spawnSync: (...args) => realChild.spawnSync(...rewriteArgs(args)),
109
+ execFileSync: (...args) => realChild.execFileSync(...args),
110
+ execSync: (...args) => realChild.execSync(...args),
111
+ };
138
112
  }
139
113
 
140
114
  if (id.startsWith('/$bunfs/root/')) {
@@ -145,6 +119,189 @@ function createFakeRequire(realRequire) {
145
119
  };
146
120
  }
147
121
 
122
+ function createBunShim() {
123
+ const childProcess = require('child_process');
124
+ const crypto = require('crypto');
125
+ const net = require('net');
126
+ const os = require('os');
127
+ const ansiPattern =
128
+ /[\u001B\u009B][[\]()#;?]*(?:(?:(?:;[-a-zA-Z\d\/#&.:=?%@~_]+)*|[a-zA-Z\d]+(?:;[-a-zA-Z\d\/#&.:=?%@~_]*)*)?(?:\u0007|\u001B\u005C|\u009C)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))/g;
129
+
130
+ function stripANSI(value) {
131
+ if (typeof value !== 'string' || value.length === 0) return '';
132
+ return value.replace(ansiPattern, '');
133
+ }
134
+
135
+ function isZeroWidth(codePoint) {
136
+ return (
137
+ codePoint <= 0x1f ||
138
+ (codePoint >= 0x7f && codePoint <= 0x9f) ||
139
+ (codePoint >= 0x300 && codePoint <= 0x36f) ||
140
+ (codePoint >= 0x200b && codePoint <= 0x200f) ||
141
+ codePoint === 0xfeff ||
142
+ (codePoint >= 0xfe00 && codePoint <= 0xfe0f)
143
+ );
144
+ }
145
+
146
+ function isWide(codePoint) {
147
+ return (
148
+ (codePoint >= 0x1100 && codePoint <= 0x115f) ||
149
+ (codePoint >= 0x2329 && codePoint <= 0x232a) ||
150
+ (codePoint >= 0x2e80 && codePoint <= 0xa4cf && codePoint !== 0x303f) ||
151
+ (codePoint >= 0xac00 && codePoint <= 0xd7a3) ||
152
+ (codePoint >= 0xf900 && codePoint <= 0xfaff) ||
153
+ (codePoint >= 0xfe10 && codePoint <= 0xfe19) ||
154
+ (codePoint >= 0xfe30 && codePoint <= 0xfe6f) ||
155
+ (codePoint >= 0xff00 && codePoint <= 0xff60) ||
156
+ (codePoint >= 0xffe0 && codePoint <= 0xffe6) ||
157
+ (codePoint >= 0x1f300 && codePoint <= 0x1faf8) ||
158
+ (codePoint >= 0x20000 && codePoint <= 0x3fffd)
159
+ );
160
+ }
161
+
162
+ function stringWidth(value) {
163
+ const text = stripANSI(typeof value === 'string' ? value : String(value ?? ''));
164
+ let width = 0;
165
+ for (const char of text) {
166
+ const codePoint = char.codePointAt(0);
167
+ if (isZeroWidth(codePoint)) continue;
168
+ width += isWide(codePoint) ? 2 : 1;
169
+ }
170
+ return width;
171
+ }
172
+
173
+ function normalizeInput(value) {
174
+ if (Buffer.isBuffer(value) || value instanceof Uint8Array) return Buffer.from(value);
175
+ if (typeof value === 'string') return Buffer.from(value);
176
+ return Buffer.from(String(value ?? ''));
177
+ }
178
+
179
+ function hash(value) {
180
+ const digest = crypto.createHash('sha256').update(normalizeInput(value)).digest();
181
+ return digest.readUInt32LE(0);
182
+ }
183
+
184
+ function which(command) {
185
+ if (typeof command !== 'string' || command.length === 0) return null;
186
+ const pathValue = process.env.PATH || '';
187
+ const separator = os.platform() === 'win32' ? ';' : ':';
188
+ for (const entry of pathValue.split(separator)) {
189
+ if (!entry) continue;
190
+ const candidate = path.join(entry, command);
191
+ try {
192
+ fs.accessSync(candidate, fs.constants.X_OK);
193
+ return candidate;
194
+ } catch {}
195
+ }
196
+ return null;
197
+ }
198
+
199
+ function spawn(argv, options = {}) {
200
+ if (!Array.isArray(argv) || argv.length === 0) {
201
+ throw new TypeError('Bun.spawn expects a non-empty argv array');
202
+ }
203
+ const [command, ...args] = argv;
204
+ const child = childProcess.spawn(command, args, {
205
+ cwd: options.cwd,
206
+ env: options.env,
207
+ detached: options.detached,
208
+ stdio: options.stdio || 'pipe',
209
+ windowsHide: options.windowsHide,
210
+ });
211
+ child.exited = new Promise(resolve => {
212
+ child.once('exit', (code, signal) => resolve({ code, signal }));
213
+ });
214
+ return child;
215
+ }
216
+
217
+ function listen(options) {
218
+ const sockets = new Set();
219
+ const server = net.createServer(socket => {
220
+ sockets.add(socket);
221
+ const connection = {
222
+ data: undefined,
223
+ write(chunk) {
224
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
225
+ const ok = socket.write(buffer);
226
+ return ok ? buffer.length : 0;
227
+ },
228
+ end(chunk) {
229
+ socket.end(chunk);
230
+ },
231
+ destroy(error) {
232
+ socket.destroy(error);
233
+ },
234
+ };
235
+ socket.on('data', chunk => {
236
+ if (options.socket && typeof options.socket.data === 'function') {
237
+ options.socket.data(connection, chunk);
238
+ }
239
+ });
240
+ socket.on('drain', () => {
241
+ if (options.socket && typeof options.socket.drain === 'function') {
242
+ options.socket.drain(connection);
243
+ }
244
+ });
245
+ socket.on('close', () => {
246
+ sockets.delete(socket);
247
+ if (options.socket && typeof options.socket.close === 'function') {
248
+ options.socket.close(connection);
249
+ }
250
+ });
251
+ socket.on('error', error => {
252
+ if (options.socket && typeof options.socket.error === 'function') {
253
+ options.socket.error(connection, error);
254
+ }
255
+ });
256
+ if (options.socket && typeof options.socket.open === 'function') {
257
+ options.socket.open(connection);
258
+ }
259
+ });
260
+
261
+ server.listen(options.port || 0, options.hostname || '127.0.0.1');
262
+
263
+ return {
264
+ get port() {
265
+ const address = server.address();
266
+ return address && typeof address === 'object' ? address.port : 0;
267
+ },
268
+ stop(closeConnections) {
269
+ server.close();
270
+ if (closeConnections) {
271
+ for (const socket of sockets) socket.destroy();
272
+ }
273
+ },
274
+ };
275
+ }
276
+
277
+ const semver = {
278
+ order(left, right) {
279
+ const leftParts = String(left).split('.').map(Number);
280
+ const rightParts = String(right).split('.').map(Number);
281
+ const size = Math.max(leftParts.length, rightParts.length);
282
+ for (let index = 0; index < size; index += 1) {
283
+ const diff = (leftParts[index] || 0) - (rightParts[index] || 0);
284
+ if (diff !== 0) return diff;
285
+ }
286
+ return 0;
287
+ },
288
+ };
289
+
290
+ return {
291
+ version: '1.1.8',
292
+ gc: typeof global.gc === 'function' ? () => global.gc() : () => {},
293
+ hash,
294
+ listen,
295
+ semver,
296
+ spawn,
297
+ stdin: process.stdin,
298
+ stripANSI,
299
+ stringWidth,
300
+ which,
301
+ wrapAnsi: value => String(value ?? ''),
302
+ };
303
+ }
304
+
148
305
  async function main() {
149
306
  const extractedFile = ensureEntryFile();
150
307
  const code = fs.readFileSync(extractedFile, 'utf8');
@@ -166,7 +323,7 @@ async function main() {
166
323
  process.once('uncaughtException', onAsyncError);
167
324
  process.once('unhandledRejection', onAsyncError);
168
325
  Object.defineProperty(process.versions, 'bun', { value: '1.1.8', configurable: true });
169
- globalThis.Bun = { version: '1.1.8', stringWidth };
326
+ globalThis.Bun = createBunShim();
170
327
  process.argv = ['node', extractedFile, ...argv];
171
328
  process.exit = code => {
172
329
  throw new RequestedExit(code);
@@ -201,10 +358,6 @@ async function main() {
201
358
  }
202
359
 
203
360
  main().catch(error => {
204
- if (error && error.code === 'CLAUDE_TERMUX_OFFICIAL_UPDATE_BLOCKED') {
205
- console.error(BLOCK_MESSAGE);
206
- process.exit(error.status || 1);
207
- }
208
361
  console.error(error && error.stack ? error.stack : String(error));
209
362
  process.exit(1);
210
363
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bash0816/claude-code",
3
- "version": "2.1.144",
3
+ "version": "2.1.150-1",
4
4
  "description": "Termux-native Claude Code wrapper with audited native replay",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -1,257 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- const EventEmitter = require('events');
5
-
6
- const OFFICIAL_PACKAGE = '@anthropic-ai/claude-code';
7
- const BLOCK_MESSAGE = [
8
- 'Official native self-update is disabled on Termux.',
9
- 'Run: claude update',
10
- ].join('\n');
11
-
12
- function lastPathSegment(value) {
13
- return String(value || '').replace(/\\/g, '/').split('/').pop();
14
- }
15
-
16
- function isNpmCommand(command) {
17
- const text = String(command || '');
18
- const leaf = lastPathSegment(text);
19
- return leaf === 'npm' || leaf === 'npm-cli.js';
20
- }
21
-
22
- function isPnpmCommand(command) {
23
- return lastPathSegment(command) === 'pnpm';
24
- }
25
-
26
- function isYarnCommand(command) {
27
- const leaf = lastPathSegment(command);
28
- return leaf === 'yarn' || leaf === 'yarnpkg';
29
- }
30
-
31
- function isNpxCommand(command) {
32
- const leaf = lastPathSegment(command);
33
- return leaf === 'npx' || leaf === 'pnpx';
34
- }
35
-
36
- function isEnvCommand(command) {
37
- return lastPathSegment(command) === 'env';
38
- }
39
-
40
- function isCorepackCommand(command) {
41
- return lastPathSegment(command) === 'corepack';
42
- }
43
-
44
- function isShellCommand(command) {
45
- const leaf = lastPathSegment(command);
46
- return leaf === 'sh' || leaf === 'bash';
47
- }
48
-
49
- function isSupportedOperation(value) {
50
- return ['install', 'i', 'add', 'update', 'up', 'upgrade'].includes(String(value || ''));
51
- }
52
-
53
- function isOfficialTarget(value) {
54
- return String(value || '') === OFFICIAL_PACKAGE || String(value || '').startsWith(`${OFFICIAL_PACKAGE}@`);
55
- }
56
-
57
- function normalizeCommand(command, args) {
58
- let normalizedCommand = command;
59
- let normalizedArgs = Array.isArray(args) ? args.slice() : [];
60
-
61
- if (isEnvCommand(normalizedCommand)) {
62
- while (normalizedArgs.length > 0 && /^[A-Za-z_][A-Za-z0-9_]*=.*/.test(String(normalizedArgs[0]))) {
63
- normalizedArgs.shift();
64
- }
65
- if (normalizedArgs.length === 0) return { command: normalizedCommand, args: normalizedArgs };
66
- normalizedCommand = normalizedArgs.shift();
67
- }
68
-
69
- if (isCorepackCommand(normalizedCommand)) {
70
- if (normalizedArgs.length === 0) return { command: normalizedCommand, args: normalizedArgs };
71
- normalizedCommand = normalizedArgs.shift();
72
- }
73
-
74
- return { command: normalizedCommand, args: normalizedArgs };
75
- }
76
-
77
- function hasOfficialTarget(args) {
78
- return args.some(isOfficialTarget);
79
- }
80
-
81
- function shouldBlockNpmLike(command, args) {
82
- if (!(isNpmCommand(command) || isPnpmCommand(command))) return false;
83
- if (!args.find(isSupportedOperation)) return false;
84
- return hasOfficialTarget(args);
85
- }
86
-
87
- function shouldBlockYarn(command, args) {
88
- if (!isYarnCommand(command)) return false;
89
- return String(args[0] || '') === 'global' && String(args[1] || '') === 'add' && hasOfficialTarget(args.slice(2));
90
- }
91
-
92
- function shouldBlockNpx(command, args) {
93
- if (!isNpxCommand(command)) return false;
94
- for (let index = 0; index < args.length; index += 1) {
95
- const token = String(args[index] || '');
96
- if (token === '-p' || token === '--package') {
97
- if (isOfficialTarget(args[index + 1])) return true;
98
- index += 1;
99
- continue;
100
- }
101
- if (token.startsWith('--package=')) {
102
- if (isOfficialTarget(token.slice('--package='.length))) return true;
103
- continue;
104
- }
105
- if (!token.startsWith('-')) {
106
- return isOfficialTarget(token);
107
- }
108
- }
109
- return false;
110
- }
111
-
112
- function shouldBlockCommand(command, args) {
113
- if (!Array.isArray(args)) return false;
114
- const normalized = normalizeCommand(command, args);
115
- return (
116
- shouldBlockNpmLike(normalized.command, normalized.args) ||
117
- shouldBlockYarn(normalized.command, normalized.args) ||
118
- shouldBlockNpx(normalized.command, normalized.args)
119
- );
120
- }
121
-
122
- function normalizeTokensForExec(tokens) {
123
- if (tokens.length === 0) return tokens;
124
- const normalized = normalizeCommand(tokens[0], tokens.slice(1));
125
- return [normalized.command, ...normalized.args].filter(value => value !== undefined);
126
- }
127
-
128
- function unwrapShellCommand(command, args) {
129
- if (!isShellCommand(command) || !Array.isArray(args) || args.length < 2) return null;
130
- const first = String(args[0] || '');
131
- const second = String(args[1] || '');
132
- if ((first === '-c' || first === '-lc') && second) return second;
133
- if (first === '-l' && String(args[1] || '') === '-c' && String(args[2] || '')) return String(args[2]);
134
- return null;
135
- }
136
-
137
- function tokenizeShellString(command) {
138
- const text = String(command || '').trim();
139
- if (!text) return [];
140
- return text.match(/(?:[^\s'"]+|'[^']*'|"[^"]*")+/g) || [];
141
- }
142
-
143
- function unquote(token) {
144
- if (
145
- (token.startsWith('"') && token.endsWith('"')) ||
146
- (token.startsWith("'") && token.endsWith("'"))
147
- ) {
148
- return token.slice(1, -1);
149
- }
150
- return token;
151
- }
152
-
153
- function shouldBlockExecString(command) {
154
- const tokens = normalizeTokensForExec(tokenizeShellString(command).map(unquote));
155
- if (tokens.length === 0) return false;
156
- const shellInner = unwrapShellCommand(tokens[0], tokens.slice(1));
157
- if (shellInner) return shouldBlockExecString(shellInner);
158
- return shouldBlockCommand(tokens[0], tokens.slice(1));
159
- }
160
-
161
- function createBlockedError(blockedStderr) {
162
- const error = new Error(BLOCK_MESSAGE);
163
- error.code = 'CLAUDE_TERMUX_OFFICIAL_UPDATE_BLOCKED';
164
- error.status = 1;
165
- error.stdout = Buffer.from('');
166
- error.stderr = Buffer.from(blockedStderr);
167
- return error;
168
- }
169
-
170
- function createBlockedSpawn(blockedStderr, stderrWriter, options) {
171
- const child = new EventEmitter();
172
- child.stdout = null;
173
- child.stderr = null;
174
- child.stdin = null;
175
- child.pid = 0;
176
- child.killed = false;
177
- child.kill = () => false;
178
- process.nextTick(() => {
179
- if (!(options && options.stdio === 'ignore')) {
180
- stderrWriter(blockedStderr);
181
- }
182
- child.emit('close', 1, null);
183
- child.emit('exit', 1, null);
184
- });
185
- return child;
186
- }
187
-
188
- function createBlockedSpawnSync(blockedStderr, stderrWriter) {
189
- stderrWriter(blockedStderr);
190
- return {
191
- pid: 0,
192
- output: [null, Buffer.from(''), Buffer.from(blockedStderr)],
193
- stdout: Buffer.from(''),
194
- stderr: Buffer.from(blockedStderr),
195
- status: 1,
196
- signal: null,
197
- error: createBlockedError(blockedStderr),
198
- };
199
- }
200
-
201
- function createBlockedExecResult(blockedStderr, stderrWriter, callback) {
202
- const child = createBlockedSpawn(blockedStderr, stderrWriter);
203
- const error = createBlockedError(blockedStderr);
204
- process.nextTick(() => {
205
- if (typeof callback === 'function') {
206
- callback(error, '', blockedStderr);
207
- }
208
- });
209
- return child;
210
- }
211
-
212
- function shouldBlockSpawnArgs(args) {
213
- if (!Array.isArray(args) || args.length === 0) return false;
214
- const command = args[0];
215
- const commandArgs = Array.isArray(args[1]) ? args[1] : [];
216
- const shellInner = unwrapShellCommand(command, commandArgs);
217
- if (shellInner) return shouldBlockExecString(shellInner);
218
- return shouldBlockCommand(command, commandArgs);
219
- }
220
-
221
- function createGuardedChildProcess(realChild, stderrWriter = value => process.stderr.write(value)) {
222
- const blockedStderr = `${BLOCK_MESSAGE}\n`;
223
- return {
224
- spawn: (...args) => {
225
- if (shouldBlockSpawnArgs(args)) return createBlockedSpawn(blockedStderr, stderrWriter, args[2]);
226
- return realChild.spawn(...args);
227
- },
228
- execFile: (...args) => {
229
- if (shouldBlockSpawnArgs(args)) return createBlockedExecResult(blockedStderr, stderrWriter, args[args.length - 1]);
230
- return realChild.execFile(...args);
231
- },
232
- exec: (...args) => {
233
- if (shouldBlockExecString(args[0])) return createBlockedExecResult(blockedStderr, stderrWriter, args[args.length - 1]);
234
- return realChild.exec(...args);
235
- },
236
- spawnSync: (...args) => {
237
- if (shouldBlockSpawnArgs(args)) return createBlockedSpawnSync(blockedStderr, stderrWriter);
238
- return realChild.spawnSync(...args);
239
- },
240
- execFileSync: (...args) => {
241
- if (shouldBlockSpawnArgs(args)) throw createBlockedError(blockedStderr);
242
- return realChild.execFileSync(...args);
243
- },
244
- execSync: (...args) => {
245
- if (shouldBlockExecString(args[0])) throw createBlockedError(blockedStderr);
246
- return realChild.execSync(...args);
247
- },
248
- };
249
- }
250
-
251
- module.exports = {
252
- BLOCK_MESSAGE,
253
- OFFICIAL_PACKAGE,
254
- createGuardedChildProcess,
255
- shouldBlockCommand,
256
- shouldBlockExecString,
257
- };
@@ -1,310 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- const test = require('node:test');
5
- const assert = require('node:assert/strict');
6
- const EventEmitter = require('events');
7
-
8
- const {
9
- BLOCK_MESSAGE,
10
- createGuardedChildProcess,
11
- shouldBlockCommand,
12
- shouldBlockExecString,
13
- } = require('./native-update-guard.js');
14
-
15
- test('blocks npm install of official package', () => {
16
- assert.equal(
17
- shouldBlockCommand('npm', ['install', '-g', '@anthropic-ai/claude-code@latest']),
18
- true,
19
- );
20
- });
21
-
22
- test('blocks npm update of official package via absolute npm path', () => {
23
- assert.equal(
24
- shouldBlockCommand('/usr/bin/npm', ['update', '-g', '@anthropic-ai/claude-code']),
25
- true,
26
- );
27
- });
28
-
29
- test('does not block canonical package install', () => {
30
- assert.equal(
31
- shouldBlockCommand('npm', ['install', '-g', '@bash0816/claude-code@2.1.137']),
32
- false,
33
- );
34
- });
35
-
36
- test('blocks exec string that installs official package', () => {
37
- assert.equal(
38
- shouldBlockExecString('npm install -g @anthropic-ai/claude-code@latest'),
39
- true,
40
- );
41
- });
42
-
43
- test('does not block unrelated exec string', () => {
44
- assert.equal(
45
- shouldBlockExecString('npm install -g @bash0816/claude-code@2.1.137'),
46
- false,
47
- );
48
- });
49
-
50
- test('blocks env npm install of official package', () => {
51
- assert.equal(
52
- shouldBlockCommand('env', ['FOO=bar', 'npm', 'install', '-g', '@anthropic-ai/claude-code@latest']),
53
- true,
54
- );
55
- });
56
-
57
- test('blocks npx official package invocation', () => {
58
- assert.equal(shouldBlockCommand('npx', ['@anthropic-ai/claude-code']), true);
59
- });
60
-
61
- test('blocks npx package option official package invocation', () => {
62
- assert.equal(shouldBlockCommand('npx', ['-p', '@anthropic-ai/claude-code', 'claude']), true);
63
- assert.equal(shouldBlockCommand('npx', ['--package=@anthropic-ai/claude-code', 'claude']), true);
64
- });
65
-
66
- test('blocks pnpm add of official package', () => {
67
- assert.equal(
68
- shouldBlockCommand('pnpm', ['add', '-g', '@anthropic-ai/claude-code']),
69
- true,
70
- );
71
- });
72
-
73
- test('blocks yarn global add of official package', () => {
74
- assert.equal(
75
- shouldBlockCommand('yarn', ['global', 'add', '@anthropic-ai/claude-code']),
76
- true,
77
- );
78
- });
79
-
80
- test('blocks corepack pnpm add of official package', () => {
81
- assert.equal(
82
- shouldBlockCommand('corepack', ['pnpm', 'add', '-g', '@anthropic-ai/claude-code']),
83
- true,
84
- );
85
- });
86
-
87
- test('blocks corepack yarn global add of official package', () => {
88
- assert.equal(
89
- shouldBlockCommand('corepack', ['yarn', 'global', 'add', '@anthropic-ai/claude-code']),
90
- true,
91
- );
92
- });
93
-
94
- test('does not block corepack pnpm add of canonical package', () => {
95
- assert.equal(
96
- shouldBlockCommand('corepack', ['pnpm', 'add', '-g', '@bash0816/claude-code@2.1.137']),
97
- false,
98
- );
99
- });
100
-
101
- test('blocks exec string through env npm', () => {
102
- assert.equal(
103
- shouldBlockExecString('env FOO=bar npm install -g @anthropic-ai/claude-code@latest'),
104
- true,
105
- );
106
- });
107
-
108
- test('blocks exec string through corepack yarn', () => {
109
- assert.equal(
110
- shouldBlockExecString('corepack yarn global add @anthropic-ai/claude-code'),
111
- true,
112
- );
113
- });
114
-
115
- test('blocks exec string through sh -c npm install', () => {
116
- assert.equal(
117
- shouldBlockExecString('sh -c "npm install -g @anthropic-ai/claude-code"'),
118
- true,
119
- );
120
- });
121
-
122
- test('blocks exec string through bash -lc pnpm add', () => {
123
- assert.equal(
124
- shouldBlockExecString('bash -lc "pnpm add -g @anthropic-ai/claude-code"'),
125
- true,
126
- );
127
- });
128
-
129
- test('does not block canonical exec string through bash -lc', () => {
130
- assert.equal(
131
- shouldBlockExecString('bash -lc "npm install -g @bash0816/claude-code@2.1.137"'),
132
- false,
133
- );
134
- });
135
-
136
- test('does not block unrelated shell exec string', () => {
137
- assert.equal(shouldBlockExecString('sh -c "echo ok"'), false);
138
- });
139
-
140
- function createRealChildStub() {
141
- const calls = [];
142
- return {
143
- calls,
144
- spawn(...args) {
145
- calls.push(['spawn', args]);
146
- return { kind: 'spawn', args };
147
- },
148
- execFile(...args) {
149
- calls.push(['execFile', args]);
150
- return { kind: 'execFile', args };
151
- },
152
- exec(...args) {
153
- calls.push(['exec', args]);
154
- return { kind: 'exec', args };
155
- },
156
- spawnSync(...args) {
157
- calls.push(['spawnSync', args]);
158
- return { kind: 'spawnSync', args, status: 0 };
159
- },
160
- execFileSync(...args) {
161
- calls.push(['execFileSync', args]);
162
- return Buffer.from('ok');
163
- },
164
- execSync(...args) {
165
- calls.push(['execSync', args]);
166
- return Buffer.from('ok');
167
- },
168
- };
169
- }
170
-
171
- test('guarded spawn blocks official npm update without delegating', async () => {
172
- const writes = [];
173
- const realChild = createRealChildStub();
174
- const guarded = createGuardedChildProcess(realChild, value => writes.push(value));
175
-
176
- const child = guarded.spawn('npm', ['install', '-g', '@anthropic-ai/claude-code@latest']);
177
- assert.equal(child instanceof EventEmitter, true);
178
-
179
- const result = await new Promise(resolve => {
180
- child.once('close', (code, signal) => resolve({ code, signal }));
181
- });
182
-
183
- assert.deepEqual(result, { code: 1, signal: null });
184
- assert.deepEqual(realChild.calls, []);
185
- assert.equal(writes[0], `${BLOCK_MESSAGE}\n`);
186
- });
187
-
188
- test('guarded spawn delegates canonical install', () => {
189
- const realChild = createRealChildStub();
190
- const guarded = createGuardedChildProcess(realChild, () => {});
191
- const result = guarded.spawn('npm', ['install', '-g', '@bash0816/claude-code@2.1.137']);
192
-
193
- assert.equal(result.kind, 'spawn');
194
- assert.equal(realChild.calls.length, 1);
195
- });
196
-
197
- test('guarded execFile blocks official npm update callback path', async () => {
198
- const writes = [];
199
- const realChild = createRealChildStub();
200
- const guarded = createGuardedChildProcess(realChild, value => writes.push(value));
201
-
202
- const callbackResult = await new Promise(resolve => {
203
- guarded.execFile(
204
- 'npm',
205
- ['update', '-g', '@anthropic-ai/claude-code'],
206
- (error, stdout, stderr) => resolve({ error, stdout, stderr }),
207
- );
208
- });
209
-
210
- assert.equal(callbackResult.error.code, 'CLAUDE_TERMUX_OFFICIAL_UPDATE_BLOCKED');
211
- assert.equal(callbackResult.stdout, '');
212
- assert.equal(callbackResult.stderr, `${BLOCK_MESSAGE}\n`);
213
- assert.deepEqual(realChild.calls, []);
214
- assert.equal(writes[0], `${BLOCK_MESSAGE}\n`);
215
- });
216
-
217
- test('guarded exec blocks official npm update shell string', async () => {
218
- const realChild = createRealChildStub();
219
- const guarded = createGuardedChildProcess(realChild, () => {});
220
-
221
- const callbackResult = await new Promise(resolve => {
222
- guarded.exec(
223
- 'npm install -g @anthropic-ai/claude-code@latest',
224
- (error, stdout, stderr) => resolve({ error, stdout, stderr }),
225
- );
226
- });
227
-
228
- assert.equal(callbackResult.error.code, 'CLAUDE_TERMUX_OFFICIAL_UPDATE_BLOCKED');
229
- assert.equal(callbackResult.stdout, '');
230
- assert.equal(callbackResult.stderr, `${BLOCK_MESSAGE}\n`);
231
- assert.deepEqual(realChild.calls, []);
232
- });
233
-
234
- test('guarded spawnSync blocks official npm update with status 1', () => {
235
- const writes = [];
236
- const realChild = createRealChildStub();
237
- const guarded = createGuardedChildProcess(realChild, value => writes.push(value));
238
- const result = guarded.spawnSync('npm', ['update', '-g', '@anthropic-ai/claude-code']);
239
-
240
- assert.equal(result.status, 1);
241
- assert.equal(Buffer.isBuffer(result.stderr), true);
242
- assert.equal(String(result.stderr), `${BLOCK_MESSAGE}\n`);
243
- assert.equal(result.error.code, 'CLAUDE_TERMUX_OFFICIAL_UPDATE_BLOCKED');
244
- assert.deepEqual(realChild.calls, []);
245
- assert.equal(writes[0], `${BLOCK_MESSAGE}\n`);
246
- });
247
-
248
- test('guarded execFileSync throws on official npm update', () => {
249
- const realChild = createRealChildStub();
250
- const guarded = createGuardedChildProcess(realChild, () => {});
251
-
252
- assert.throws(
253
- () => guarded.execFileSync('npm', ['install', '-g', '@anthropic-ai/claude-code@latest']),
254
- error => error && error.code === 'CLAUDE_TERMUX_OFFICIAL_UPDATE_BLOCKED',
255
- );
256
- assert.deepEqual(realChild.calls, []);
257
- });
258
-
259
- test('guarded execSync throws on official npm update shell string', () => {
260
- const realChild = createRealChildStub();
261
- const guarded = createGuardedChildProcess(realChild, () => {});
262
-
263
- assert.throws(
264
- () => guarded.execSync('npm update -g @anthropic-ai/claude-code'),
265
- error => error && error.code === 'CLAUDE_TERMUX_OFFICIAL_UPDATE_BLOCKED',
266
- );
267
- assert.deepEqual(realChild.calls, []);
268
- });
269
-
270
- test('guarded spawn blocks official npm update through sh -c', async () => {
271
- const writes = [];
272
- const realChild = createRealChildStub();
273
- const guarded = createGuardedChildProcess(realChild, value => writes.push(value));
274
-
275
- const child = guarded.spawn('/bin/sh', ['-c', 'npm install -g @anthropic-ai/claude-code']);
276
- const result = await new Promise(resolve => {
277
- child.once('close', (code, signal) => resolve({ code, signal }));
278
- });
279
-
280
- assert.deepEqual(result, { code: 1, signal: null });
281
- assert.deepEqual(realChild.calls, []);
282
- assert.equal(writes[0], `${BLOCK_MESSAGE}\n`);
283
- });
284
-
285
- test('guarded execFile blocks official update through bash -lc', async () => {
286
- const realChild = createRealChildStub();
287
- const guarded = createGuardedChildProcess(realChild, () => {});
288
-
289
- const callbackResult = await new Promise(resolve => {
290
- guarded.execFile(
291
- '/usr/bin/bash',
292
- ['-lc', 'pnpm add -g @anthropic-ai/claude-code'],
293
- (error, stdout, stderr) => resolve({ error, stdout, stderr }),
294
- );
295
- });
296
-
297
- assert.equal(callbackResult.error.code, 'CLAUDE_TERMUX_OFFICIAL_UPDATE_BLOCKED');
298
- assert.equal(callbackResult.stdout, '');
299
- assert.equal(callbackResult.stderr, `${BLOCK_MESSAGE}\n`);
300
- assert.deepEqual(realChild.calls, []);
301
- });
302
-
303
- test('guarded spawn delegates canonical shell install', () => {
304
- const realChild = createRealChildStub();
305
- const guarded = createGuardedChildProcess(realChild, () => {});
306
- const result = guarded.spawn('/bin/sh', ['-c', 'npm install -g @bash0816/claude-code@2.1.137']);
307
-
308
- assert.equal(result.kind, 'spawn');
309
- assert.equal(realChild.calls.length, 1);
310
- });