claude-code-termux 1.0.18 → 1.0.19

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.
@@ -1,113 +1,160 @@
1
- name: Auto-update and publish
1
+ name: Sync & Publish
2
2
 
3
3
  on:
4
4
  schedule:
5
- - cron: '0 6 * * *' # 6 AM UTC daily
6
- workflow_dispatch:
7
- inputs:
8
- force_publish:
9
- description: 'Force publish (skip upstream check)'
10
- required: false
11
- default: false
12
- type: boolean
13
- version_bump:
14
- description: 'Version bump type (only for force publish)'
15
- required: false
16
- default: 'patch'
17
- type: choice
18
- options:
19
- - patch
20
- - minor
21
- - major
5
+ - cron: '0 6 * * *' # Daily at 6 AM UTC
6
+ workflow_dispatch: # Manual trigger — no inputs needed
22
7
 
23
8
  permissions:
24
- contents: write # For pushing version bump commits
25
- id-token: write # Required for npm OIDC trusted publishing
9
+ contents: write
10
+ id-token: write
26
11
 
27
12
  jobs:
28
- check-and-publish:
13
+ sync-and-publish:
29
14
  runs-on: ubuntu-latest
30
15
  steps:
16
+
17
+ # ── Setup ──────────────────────────────────────────────
31
18
  - uses: actions/checkout@v4
19
+ with:
20
+ fetch-depth: 0
21
+ fetch-tags: true
32
22
 
33
23
  - uses: actions/setup-node@v4
34
24
  with:
35
- node-version: '24' # Node 24+ required for OIDC (npm >=11.5)
36
- registry-url: 'https://registry.npmjs.org'
25
+ node-version: 24
26
+ registry-url: https://registry.npmjs.org
37
27
 
38
- - name: Check for upstream updates
39
- id: check
40
- if: ${{ !inputs.force_publish }}
28
+ # ── Detect changes ────────────────────────────────────
29
+ - name: Check for upstream and source changes
30
+ id: detect
41
31
  run: |
42
- # Get current dependency version (strip ^ prefix)
43
- CURRENT=$(node -p "require('./package.json').dependencies['@anthropic-ai/claude-code'].replace('^', '')")
44
- echo "Current version: $CURRENT"
45
-
46
- # Get latest version from npm
47
- LATEST=$(npm view @anthropic-ai/claude-code version)
48
- echo "Latest version: $LATEST"
49
-
50
- if [ "$CURRENT" != "$LATEST" ]; then
51
- echo "Update needed: $CURRENT -> $LATEST"
52
- echo "update_needed=true" >> $GITHUB_OUTPUT
53
- echo "new_version=$LATEST" >> $GITHUB_OUTPUT
32
+ set -e
33
+
34
+ # --- Upstream check ---
35
+ CURRENT_DEP=$(node -p "require('./package.json').dependencies['@anthropic-ai/claude-code'].replace('^','')")
36
+ LATEST_DEP=$(npm view @anthropic-ai/claude-code version)
37
+ echo "current_dep=$CURRENT_DEP" >> "$GITHUB_OUTPUT"
38
+ echo "latest_dep=$LATEST_DEP" >> "$GITHUB_OUTPUT"
39
+
40
+ UPSTREAM_CHANGED=false
41
+ if [ "$CURRENT_DEP" != "$LATEST_DEP" ]; then
42
+ UPSTREAM_CHANGED=true
43
+ echo "⬆️ Upstream: $CURRENT_DEP $LATEST_DEP"
44
+ else
45
+ echo "✓ Upstream up to date ($CURRENT_DEP)"
46
+ fi
47
+ echo "upstream_changed=$UPSTREAM_CHANGED" >> "$GITHUB_OUTPUT"
48
+
49
+ # --- Source change check ---
50
+ REPO_VERSION=$(node -p "require('./package.json').version")
51
+ NPM_VERSION=$(npm view claude-code-termux version 2>/dev/null || echo "0.0.0")
52
+ echo "repo_version=$REPO_VERSION" >> "$GITHUB_OUTPUT"
53
+ echo "npm_version=$NPM_VERSION" >> "$GITHUB_OUTPUT"
54
+
55
+ SOURCE_CHANGED=false
56
+ if [ "$REPO_VERSION" != "$NPM_VERSION" ]; then
57
+ SOURCE_CHANGED=true
58
+ echo "📦 Source: repo v$REPO_VERSION != npm v$NPM_VERSION"
59
+ else
60
+ TAG="v$NPM_VERSION"
61
+ if git rev-parse "$TAG" >/dev/null 2>&1; then
62
+ COMMITS_SINCE=$(git log "$TAG"..HEAD --oneline -- . ':!package-lock.json' | wc -l | tr -d ' ')
63
+ if [ "$COMMITS_SINCE" -gt 0 ]; then
64
+ SOURCE_CHANGED=true
65
+ echo "📦 Source: $COMMITS_SINCE commit(s) since $TAG"
66
+ else
67
+ echo "✓ Source unchanged since $TAG"
68
+ fi
69
+ else
70
+ echo "✓ No tag $TAG found — using version comparison only"
71
+ fi
72
+ fi
73
+ echo "source_changed=$SOURCE_CHANGED" >> "$GITHUB_OUTPUT"
74
+
75
+ # --- Overall decision ---
76
+ if [ "$UPSTREAM_CHANGED" = "true" ] || [ "$SOURCE_CHANGED" = "true" ]; then
77
+ echo "needs_publish=true" >> "$GITHUB_OUTPUT"
54
78
  else
55
- echo "Already up to date"
56
- echo "update_needed=false" >> $GITHUB_OUTPUT
79
+ echo "needs_publish=false" >> "$GITHUB_OUTPUT"
80
+ echo " Everything is up to date. Nothing to publish."
57
81
  fi
58
82
 
59
- - name: Update dependency and bump version (auto)
60
- if: ${{ !inputs.force_publish && steps.check.outputs.update_needed == 'true' }}
83
+ # ── Update upstream dependency ────────────────────────
84
+ - name: Update upstream dependency
85
+ if: steps.detect.outputs.upstream_changed == 'true'
61
86
  run: |
62
- npm pkg set "dependencies.@anthropic-ai/claude-code=^${{ steps.check.outputs.new_version }}"
63
- npm version patch --no-git-tag-version
64
- echo "Updated package.json:"
65
- cat package.json | head -35
87
+ npm pkg set "dependencies.@anthropic-ai/claude-code=^${{ steps.detect.outputs.latest_dep }}"
66
88
 
67
- - name: Bump version (manual)
68
- if: ${{ inputs.force_publish }}
89
+ # ── Bump version if needed ────────────────────────────
90
+ - name: Bump version
91
+ if: steps.detect.outputs.needs_publish == 'true'
92
+ id: version
69
93
  run: |
70
- npm version ${{ inputs.version_bump }} --no-git-tag-version
94
+ REPO_VERSION=${{ steps.detect.outputs.repo_version }}
95
+ NPM_VERSION=${{ steps.detect.outputs.npm_version }}
96
+
97
+ if [ "$REPO_VERSION" = "$NPM_VERSION" ]; then
98
+ npm version patch --no-git-tag-version
99
+ echo "bumped=true" >> "$GITHUB_OUTPUT"
100
+ else
101
+ echo "bumped=false" >> "$GITHUB_OUTPUT"
102
+ fi
103
+
71
104
  NEW_VERSION=$(node -p "require('./package.json').version")
72
- echo "New version: $NEW_VERSION"
105
+ echo "new_version=$NEW_VERSION" >> "$GITHUB_OUTPUT"
106
+ echo "📌 Will publish v$NEW_VERSION"
73
107
 
74
- - name: Commit and push changes
75
- if: ${{ inputs.force_publish || steps.check.outputs.update_needed == 'true' }}
108
+ # ── Commit, tag, push ─────────────────────────────────
109
+ - name: Commit and push
110
+ if: steps.detect.outputs.needs_publish == 'true'
76
111
  run: |
77
112
  git config user.name "github-actions[bot]"
78
113
  git config user.email "github-actions[bot]@users.noreply.github.com"
79
- VERSION=$(node -p "require('./package.json').version")
80
- git add -A
81
- if [ "${{ inputs.force_publish }}" == "true" ]; then
82
- git commit -m "Release v${VERSION}"
83
- git tag "v${VERSION}"
84
- git push --tags
85
- else
86
- git commit -m "Bump @anthropic-ai/claude-code to ${{ steps.check.outputs.new_version }}"
114
+
115
+ VERSION=${{ steps.version.outputs.new_version }}
116
+ UPSTREAM=${{ steps.detect.outputs.upstream_changed }}
117
+ SOURCE=${{ steps.detect.outputs.source_changed }}
118
+
119
+ MSG="Release v${VERSION}"
120
+ DETAILS=""
121
+ if [ "$UPSTREAM" = "true" ]; then
122
+ DETAILS="${DETAILS}\n- Bump @anthropic-ai/claude-code to ${{ steps.detect.outputs.latest_dep }}"
123
+ fi
124
+ if [ "$SOURCE" = "true" ]; then
125
+ DETAILS="${DETAILS}\n- Include source changes"
87
126
  fi
88
- git push
89
127
 
128
+ git add -A
129
+ git commit -m "$(printf "${MSG}\n${DETAILS}")" || echo "Nothing to commit"
130
+ git tag "v${VERSION}"
131
+ git push --follow-tags
132
+
133
+ # ── Publish ───────────────────────────────────────────
90
134
  - name: Install dependencies
91
- if: ${{ inputs.force_publish || steps.check.outputs.update_needed == 'true' }}
135
+ if: steps.detect.outputs.needs_publish == 'true'
92
136
  run: npm install
93
137
 
94
- - name: Publish to npm (OIDC)
95
- if: ${{ inputs.force_publish || steps.check.outputs.update_needed == 'true' }}
138
+ - name: Publish to npm
139
+ if: steps.detect.outputs.needs_publish == 'true'
96
140
  run: npm publish --access public
97
141
 
142
+ # ── Summary ───────────────────────────────────────────
98
143
  - name: Summary
99
144
  run: |
100
- VERSION=$(node -p "require('./package.json').version")
101
- if [ "${{ inputs.force_publish }}" == "true" ]; then
102
- echo "## Published v${VERSION}" >> $GITHUB_STEP_SUMMARY
103
- echo "" >> $GITHUB_STEP_SUMMARY
104
- echo "Manual release published." >> $GITHUB_STEP_SUMMARY
105
- elif [ "${{ steps.check.outputs.update_needed }}" == "true" ]; then
106
- echo "## Update Published" >> $GITHUB_STEP_SUMMARY
107
- echo "" >> $GITHUB_STEP_SUMMARY
108
- echo "Updated @anthropic-ai/claude-code to version ${{ steps.check.outputs.new_version }}" >> $GITHUB_STEP_SUMMARY
145
+ NEEDS=${{ steps.detect.outputs.needs_publish }}
146
+ if [ "$NEEDS" = "true" ]; then
147
+ VERSION=${{ steps.version.outputs.new_version }}
148
+ echo "### ✅ Published v${VERSION}" >> "$GITHUB_STEP_SUMMARY"
149
+ echo "" >> "$GITHUB_STEP_SUMMARY"
150
+ if [ "${{ steps.detect.outputs.upstream_changed }}" = "true" ]; then
151
+ echo "- Upstream: \`@anthropic-ai/claude-code\` → ${{ steps.detect.outputs.latest_dep }}" >> "$GITHUB_STEP_SUMMARY"
152
+ fi
153
+ if [ "${{ steps.detect.outputs.source_changed }}" = "true" ]; then
154
+ echo "- Source changes included" >> "$GITHUB_STEP_SUMMARY"
155
+ fi
109
156
  else
110
- echo "## No Update Needed" >> $GITHUB_STEP_SUMMARY
111
- echo "" >> $GITHUB_STEP_SUMMARY
112
- echo "Already using the latest version of @anthropic-ai/claude-code" >> $GITHUB_STEP_SUMMARY
157
+ echo "### No Update Needed" >> "$GITHUB_STEP_SUMMARY"
158
+ echo "Upstream: \`${{ steps.detect.outputs.current_dep }}\` (latest)" >> "$GITHUB_STEP_SUMMARY"
159
+ echo "Package: \`v${{ steps.detect.outputs.repo_version }}\` (published)" >> "$GITHUB_STEP_SUMMARY"
113
160
  fi
@@ -15,21 +15,21 @@
15
15
 
16
16
  const path = require('path');
17
17
  const fs = require('fs');
18
+ const ui = require('../src/ui');
18
19
 
19
- // Detect if running on Termux/Android
20
- const isTermux = process.platform === 'android' ||
21
- process.env.PREFIX?.includes('com.termux') ||
22
- process.env.HOME?.includes('com.termux');
20
+ // Read version from package.json
21
+ const pkg = require('../package.json');
23
22
 
24
- // Apply runtime patches for Termux
25
- if (isTermux) {
26
- console.log('[claude-code-termux] Detected Termux environment, applying patches...');
23
+ // Show the banner (always — both Termux and desktop)
24
+ ui.printBanner(pkg.version);
27
25
 
26
+ // Apply runtime boosts for Termux
27
+ let boostResult = null;
28
+ if (ui.isTermux) {
28
29
  try {
29
- // Apply all patches
30
- require('../src/patches/apply-all');
30
+ boostResult = require('../src/patches/apply-all');
31
31
  } catch (err) {
32
- console.error('[claude-code-termux] Warning: Some patches could not be applied:', err.message);
32
+ ui.logError(`Boost loading failed: ${err.message}`);
33
33
  }
34
34
  }
35
35
 
@@ -66,18 +66,22 @@ try {
66
66
  }
67
67
 
68
68
  if (!claudeCodePath) {
69
- console.error('[claude-code-termux] Error: Could not find @anthropic-ai/claude-code');
69
+ ui.logError('Could not find @anthropic-ai/claude-code');
70
70
  console.error('');
71
- console.error('The Claude Code package was not found. Install it with:');
72
- console.error(' npm install -g @anthropic-ai/claude-code');
71
+ console.error(' Install it with:');
72
+ ui.logDim('npm install -g @anthropic-ai/claude-code');
73
73
  console.error('');
74
- console.error('Searched paths:');
74
+ console.error(' Searched paths:');
75
75
  possiblePaths.forEach(p => {
76
- if (p) console.error(` - ${p}`);
76
+ if (p) ui.logDim(p);
77
77
  });
78
78
  process.exit(1);
79
79
  }
80
80
 
81
+ // Show ready status
82
+ const hasWarnings = boostResult && boostResult.failedCount > 0;
83
+ ui.logReady(hasWarnings);
84
+
81
85
  // Auto-update check (runs before loading Claude Code)
82
86
  // This checks for updates and may restart the process if an update is found
83
87
  const { checkAndUpdate } = require('../src/auto-update');
@@ -92,17 +96,19 @@ checkAndUpdate().then(updated => {
92
96
  // Use dynamic import since cli.js is an ES module
93
97
  return import(claudeCodePath);
94
98
  }).catch(err => {
95
- console.error('[claude-code-termux] Error loading Claude Code:', err.message);
99
+ ui.logError(`Error loading Claude Code: ${err.message}`);
96
100
 
97
101
  if (err.message.includes('sharp')) {
98
- console.error('\nSharp module error detected. Try running:');
99
- console.error(' npm install @img/sharp-wasm32 --force');
100
- console.error(' npm install sharp --force');
102
+ console.error('');
103
+ ui.logWarn('Sharp module error detected. Try running:');
104
+ ui.logDim('npm install @img/sharp-wasm32 --force');
105
+ ui.logDim('npm install sharp --force');
101
106
  }
102
107
 
103
108
  if (err.message.includes('ENOENT') && err.message.includes('ripgrep')) {
104
- console.error('\nRipgrep binary not found. Try running:');
105
- console.error(' pkg install ripgrep');
109
+ console.error('');
110
+ ui.logWarn('Ripgrep binary not found. Try running:');
111
+ ui.logDim('pkg install ripgrep');
106
112
  }
107
113
 
108
114
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-termux",
3
- "version": "1.0.18",
3
+ "version": "1.0.19",
4
4
  "description": "Claude Code CLI with Termux/Android compatibility fixes - a wrapper that patches issues with Sharp, ripgrep, and path resolution on ARM64 Android",
5
5
  "author": "Jimoh Ovbiagele <findingjimoh@gmail.com>",
6
6
  "license": "MIT",
package/postinstall.js CHANGED
@@ -15,6 +15,10 @@
15
15
  const fs = require('fs');
16
16
  const path = require('path');
17
17
  const { execSync } = require('child_process');
18
+ const ui = require('./src/ui');
19
+
20
+ // Read version
21
+ const pkg = require('./package.json');
18
22
 
19
23
  // Detect Termux environment
20
24
  const isTermux = process.platform === 'android' ||
@@ -23,9 +27,8 @@ const isTermux = process.platform === 'android' ||
23
27
 
24
28
  const isARM64 = process.arch === 'arm64';
25
29
 
26
- console.log('[claude-code-termux] Post-install script running...');
27
- console.log(`[claude-code-termux] Platform: ${process.platform}, Arch: ${process.arch}`);
28
- console.log(`[claude-code-termux] Termux detected: ${isTermux}`);
30
+ ui.printBanner(pkg.version);
31
+ ui.logArrow('Running post-install setup...');
29
32
 
30
33
  /**
31
34
  * Find the Claude Code installation directory
@@ -50,12 +53,9 @@ function findClaudeCodePath() {
50
53
  */
51
54
  function setupRipgrep(claudeCodePath) {
52
55
  if (!isTermux || !isARM64) {
53
- console.log('[claude-code-termux] Skipping ripgrep setup (not Termux ARM64)');
54
56
  return;
55
57
  }
56
58
 
57
- console.log('[claude-code-termux] Setting up ripgrep for ARM64 Android...');
58
-
59
59
  const vendorDir = path.join(claudeCodePath, 'vendor', 'ripgrep', 'arm64-android');
60
60
 
61
61
  // Create directory if it doesn't exist
@@ -68,10 +68,8 @@ function setupRipgrep(claudeCodePath) {
68
68
  // Check if we have a bundled binary
69
69
  const bundledBinary = path.join(__dirname, 'src', 'binaries', 'rg');
70
70
  if (fs.existsSync(bundledBinary)) {
71
- console.log('[claude-code-termux] Using bundled ripgrep binary');
72
71
  fs.copyFileSync(bundledBinary, targetBinary);
73
72
  fs.chmodSync(targetBinary, 0o755);
74
- console.log('[claude-code-termux] Ripgrep binary installed successfully');
75
73
  return;
76
74
  }
77
75
 
@@ -79,13 +77,11 @@ function setupRipgrep(claudeCodePath) {
79
77
  try {
80
78
  const systemRg = execSync('which rg', { encoding: 'utf8' }).trim();
81
79
  if (systemRg && fs.existsSync(systemRg)) {
82
- console.log(`[claude-code-termux] Found system ripgrep at ${systemRg}`);
83
80
  // Create symlink to system ripgrep
84
81
  if (fs.existsSync(targetBinary)) {
85
82
  fs.unlinkSync(targetBinary);
86
83
  }
87
84
  fs.symlinkSync(systemRg, targetBinary);
88
- console.log('[claude-code-termux] Linked to system ripgrep');
89
85
  return;
90
86
  }
91
87
  } catch (err) {
@@ -93,7 +89,6 @@ function setupRipgrep(claudeCodePath) {
93
89
  }
94
90
 
95
91
  // Try to download ripgrep
96
- console.log('[claude-code-termux] Attempting to download ripgrep...');
97
92
  try {
98
93
  // Use curl to download (available in Termux)
99
94
  const rgUrl = 'https://github.com/BurntSushi/ripgrep/releases/download/14.1.1/ripgrep-14.1.1-aarch64-unknown-linux-gnu.tar.gz';
@@ -114,15 +109,14 @@ function setupRipgrep(claudeCodePath) {
114
109
  if (fs.existsSync(extractedBinary)) {
115
110
  fs.copyFileSync(extractedBinary, targetBinary);
116
111
  fs.chmodSync(targetBinary, 0o755);
117
- console.log('[claude-code-termux] Ripgrep downloaded and installed successfully');
118
112
  }
119
113
  }
120
114
 
121
115
  // Cleanup
122
116
  fs.rmSync(tmpDir, { recursive: true, force: true });
123
117
  } catch (err) {
124
- console.error('[claude-code-termux] Failed to download ripgrep:', err.message);
125
- console.log('[claude-code-termux] Please install ripgrep manually: pkg install ripgrep');
118
+ ui.logError(`Failed to download ripgrep: ${err.message}`);
119
+ ui.logDim('Install ripgrep manually: pkg install ripgrep');
126
120
  }
127
121
  }
128
122
 
@@ -147,11 +141,10 @@ function copyLinuxBinariesAsAndroidFallback(claudeCodePath) {
147
141
  const androidPath = path.join(vendorDir, tool, 'arm64-android');
148
142
 
149
143
  if (fs.existsSync(linuxPath) && !fs.existsSync(androidPath)) {
150
- console.log(`[claude-code-termux] Copying ${tool} linux-arm64 as android-arm64 fallback`);
151
144
  try {
152
145
  fs.cpSync(linuxPath, androidPath, { recursive: true });
153
146
  } catch (err) {
154
- console.error(`[claude-code-termux] Failed to copy ${tool}:`, err.message);
147
+ ui.logError(`Failed to copy ${tool}: ${err.message}`);
155
148
  }
156
149
  }
157
150
  }
@@ -166,7 +159,6 @@ function setupOAuthStorage() {
166
159
 
167
160
  if (!fs.existsSync(claudeDir)) {
168
161
  fs.mkdirSync(claudeDir, { recursive: true, mode: 0o700 });
169
- console.log('[claude-code-termux] Created .claude directory');
170
162
  }
171
163
  }
172
164
 
@@ -179,14 +171,11 @@ function installSharpWasm() {
179
171
  return;
180
172
  }
181
173
 
182
- console.log('[claude-code-termux] Installing Sharp WASM for image support...');
183
-
184
174
  const nodeModulesDir = path.join(__dirname, 'node_modules', '@img');
185
175
  const sharpWasmDir = path.join(nodeModulesDir, 'sharp-wasm32');
186
176
 
187
177
  // Check if already installed
188
178
  if (fs.existsSync(path.join(sharpWasmDir, 'package.json'))) {
189
- console.log('[claude-code-termux] Sharp WASM already installed');
190
179
  return;
191
180
  }
192
181
 
@@ -200,7 +189,6 @@ function installSharpWasm() {
200
189
  const tarballUrl = 'https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz';
201
190
  const tmpTar = path.join(__dirname, 'sharp-wasm32.tgz');
202
191
 
203
- console.log('[claude-code-termux] Downloading Sharp WASM from npm registry...');
204
192
  execSync(`curl -sL "${tarballUrl}" -o "${tmpTar}"`, { stdio: 'inherit' });
205
193
 
206
194
  // Extract to node_modules/@img/sharp-wasm32
@@ -218,12 +206,10 @@ function installSharpWasm() {
218
206
 
219
207
  // Cleanup
220
208
  fs.unlinkSync(tmpTar);
221
-
222
- console.log('[claude-code-termux] Sharp WASM installed successfully');
223
209
  } catch (err) {
224
- console.error('[claude-code-termux] Failed to install Sharp WASM:', err.message);
225
- console.log('[claude-code-termux] Image reading may not work. You can try manually:');
226
- console.log(' curl -sL https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz | tar -xz -C node_modules/@img/ && mv node_modules/@img/package node_modules/@img/sharp-wasm32');
210
+ ui.logError(`Failed to install Sharp WASM: ${err.message}`);
211
+ ui.logDim('Image reading may not work. You can try manually:');
212
+ ui.logDim('curl -sL https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz | tar -xz -C node_modules/@img/ && mv node_modules/@img/package node_modules/@img/sharp-wasm32');
227
213
  }
228
214
  }
229
215
 
@@ -234,12 +220,10 @@ function main() {
234
220
  const claudeCodePath = findClaudeCodePath();
235
221
 
236
222
  if (!claudeCodePath) {
237
- console.log('[claude-code-termux] Claude Code not found yet (will be installed next)');
223
+ ui.logDim('Claude Code not found yet (will be installed next)');
238
224
  return;
239
225
  }
240
226
 
241
- console.log(`[claude-code-termux] Found Claude Code at: ${claudeCodePath}`);
242
-
243
227
  // Run setup tasks
244
228
  if (isTermux) {
245
229
  setupRipgrep(claudeCodePath);
@@ -248,13 +232,15 @@ function main() {
248
232
  installSharpWasm();
249
233
  }
250
234
 
251
- console.log('[claude-code-termux] Post-install complete!');
235
+ ui.logStep('Setup complete!');
252
236
 
253
237
  if (isTermux) {
254
- console.log('\n[claude-code-termux] Termux setup tips:');
255
- console.log(' - If you encounter ripgrep errors, run: pkg install ripgrep');
256
- console.log(' - Use API key auth: export ANTHROPIC_API_KEY=your-key');
257
- console.log('\nRun "claude" to start Claude Code!');
238
+ console.log('');
239
+ ui.logDim('Termux tips:');
240
+ ui.logDim(' If you encounter ripgrep errors, run: pkg install ripgrep');
241
+ ui.logDim(' Use API key auth: export ANTHROPIC_API_KEY=your-key');
242
+ console.log('');
243
+ ui.logStep('Run "claude" to start Claude Code!');
258
244
  }
259
245
  }
260
246
 
@@ -12,19 +12,13 @@
12
12
  const fs = require('fs');
13
13
  const path = require('path');
14
14
  const { execSync } = require('child_process');
15
+ const { c, symbols, printBanner } = require('../src/ui');
15
16
 
16
- // ANSI colors
17
- const colors = {
18
- reset: '\x1b[0m',
19
- red: '\x1b[31m',
20
- green: '\x1b[32m',
21
- yellow: '\x1b[33m',
22
- blue: '\x1b[34m',
23
- };
17
+ const pkg = require('../package.json');
24
18
 
25
- const PASS = `${colors.green}✓${colors.reset}`;
26
- const FAIL = `${colors.red}✗${colors.reset}`;
27
- const WARN = `${colors.yellow}!${colors.reset}`;
19
+ const PASS = `${c.green}${symbols.check}${c.reset}`;
20
+ const FAIL = `${c.brightRed}${symbols.cross}${c.reset}`;
21
+ const WARN = `${c.yellow}${symbols.warn}${c.reset}`;
28
22
 
29
23
  let passCount = 0;
30
24
  let failCount = 0;
@@ -32,15 +26,15 @@ let warnCount = 0;
32
26
 
33
27
  function check(name, condition, warning = false) {
34
28
  if (condition) {
35
- console.log(`${PASS} ${name}`);
29
+ console.log(` ${PASS} ${name}`);
36
30
  passCount++;
37
31
  return true;
38
32
  } else if (warning) {
39
- console.log(`${WARN} ${name}`);
33
+ console.log(` ${WARN} ${name}`);
40
34
  warnCount++;
41
35
  return false;
42
36
  } else {
43
- console.log(`${FAIL} ${name}`);
37
+ console.log(` ${FAIL} ${name}`);
44
38
  failCount++;
45
39
  return false;
46
40
  }
@@ -54,14 +48,12 @@ function runCommand(cmd) {
54
48
  }
55
49
  }
56
50
 
57
- console.log('');
58
- console.log('='.repeat(50));
59
- console.log(' Claude Code Termux - Installation Verification');
60
- console.log('='.repeat(50));
51
+ printBanner(pkg.version);
52
+ console.log(` ${c.bold}${c.cyan}Installation Verification${c.reset}`);
61
53
  console.log('');
62
54
 
63
55
  // Environment checks
64
- console.log(`${colors.blue}Environment:${colors.reset}`);
56
+ console.log(` ${c.bold}${c.cyan}Environment:${c.reset}`);
65
57
 
66
58
  const isTermux = process.platform === 'android' ||
67
59
  process.env.PREFIX?.includes('com.termux') ||
@@ -69,37 +61,37 @@ const isTermux = process.platform === 'android' ||
69
61
 
70
62
  check('Termux environment detected', isTermux, true);
71
63
  check('HOME directory exists', fs.existsSync(process.env.HOME || ''));
72
- console.log(` Platform: ${process.platform}`);
73
- console.log(` Architecture: ${process.arch}`);
74
- console.log(` HOME: ${process.env.HOME}`);
64
+ console.log(` ${c.dim}Platform: ${process.platform}${c.reset}`);
65
+ console.log(` ${c.dim}Architecture: ${process.arch}${c.reset}`);
66
+ console.log(` ${c.dim}HOME: ${process.env.HOME}${c.reset}`);
75
67
  console.log('');
76
68
 
77
69
  // Node.js checks
78
- console.log(`${colors.blue}Node.js:${colors.reset}`);
70
+ console.log(` ${c.bold}${c.cyan}Node.js:${c.reset}`);
79
71
 
80
72
  const nodeVersion = process.version;
81
73
  const nodeMajor = parseInt(nodeVersion.slice(1).split('.')[0]);
82
74
 
83
75
  check('Node.js version >= 18', nodeMajor >= 18);
84
76
  check('Node.js version < 25', nodeMajor < 25, true);
85
- console.log(` Version: ${nodeVersion}`);
77
+ console.log(` ${c.dim}Version: ${nodeVersion}${c.reset}`);
86
78
  console.log('');
87
79
 
88
80
  // Ripgrep checks
89
- console.log(`${colors.blue}Ripgrep:${colors.reset}`);
81
+ console.log(` ${c.bold}${c.cyan}Ripgrep:${c.reset}`);
90
82
 
91
83
  const rgPath = runCommand('which rg');
92
84
  const rgVersion = runCommand('rg --version');
93
85
 
94
86
  check('Ripgrep installed', !!rgPath);
95
87
  if (rgPath) {
96
- console.log(` Path: ${rgPath}`);
97
- console.log(` Version: ${rgVersion?.split('\n')[0] || 'unknown'}`);
88
+ console.log(` ${c.dim}Path: ${rgPath}${c.reset}`);
89
+ console.log(` ${c.dim}Version: ${rgVersion?.split('\n')[0] || 'unknown'}${c.reset}`);
98
90
  }
99
91
  console.log('');
100
92
 
101
93
  // Claude Code checks
102
- console.log(`${colors.blue}Claude Code:${colors.reset}`);
94
+ console.log(` ${c.bold}${c.cyan}Claude Code:${c.reset}`);
103
95
 
104
96
  let claudeCodePath = null;
105
97
  try {
@@ -112,9 +104,9 @@ check('Claude Code installed', !!claudeCodePath);
112
104
 
113
105
  if (claudeCodePath) {
114
106
  const pkgPath = path.dirname(claudeCodePath);
115
- const pkg = require(claudeCodePath);
116
- console.log(` Version: ${pkg.version}`);
117
- console.log(` Path: ${pkgPath}`);
107
+ const ccPkg = require(claudeCodePath);
108
+ console.log(` ${c.dim}Version: ${ccPkg.version}${c.reset}`);
109
+ console.log(` ${c.dim}Path: ${pkgPath}${c.reset}`);
118
110
 
119
111
  // Check vendor binaries
120
112
  const vendorRg = path.join(pkgPath, 'vendor', 'ripgrep', 'arm64-android', 'rg');
@@ -126,7 +118,7 @@ if (claudeCodePath) {
126
118
  console.log('');
127
119
 
128
120
  // Sharp checks
129
- console.log(`${colors.blue}Sharp (Image Support):${colors.reset}`);
121
+ console.log(` ${c.bold}${c.cyan}Sharp (Image Support):${c.reset}`);
130
122
 
131
123
  let sharpWasm = false;
132
124
  let sharpNative = false;
@@ -150,7 +142,7 @@ check('Sharp native available', sharpNative, true);
150
142
  console.log('');
151
143
 
152
144
  // Configuration checks
153
- console.log(`${colors.blue}Configuration:${colors.reset}`);
145
+ console.log(` ${c.bold}${c.cyan}Configuration:${c.reset}`);
154
146
 
155
147
  const homeDir = process.env.HOME || '/data/data/com.termux/files/home';
156
148
  const claudeDir = path.join(homeDir, '.claude');
@@ -163,26 +155,26 @@ const hasApiKey = !!process.env.ANTHROPIC_API_KEY;
163
155
  check('ANTHROPIC_API_KEY set', hasApiKey, true);
164
156
 
165
157
  if (!hasApiKey) {
166
- console.log(` ${colors.yellow}Tip: export ANTHROPIC_API_KEY=your-key${colors.reset}`);
158
+ console.log(` ${c.dim}Tip: export ANTHROPIC_API_KEY=your-key${c.reset}`);
167
159
  }
168
160
  console.log('');
169
161
 
170
162
  // Summary
171
- console.log('='.repeat(50));
172
- console.log(' Summary');
173
- console.log('='.repeat(50));
163
+ console.log(` ${c.cyan}${symbols.boxH.repeat(40)}${c.reset}`);
164
+ console.log(` ${c.bold}Summary${c.reset}`);
165
+ console.log(` ${c.cyan}${symbols.boxH.repeat(40)}${c.reset}`);
174
166
  console.log(` ${PASS} Passed: ${passCount}`);
175
167
  console.log(` ${WARN} Warnings: ${warnCount}`);
176
168
  console.log(` ${FAIL} Failed: ${failCount}`);
177
169
  console.log('');
178
170
 
179
171
  if (failCount > 0) {
180
- console.log(`${colors.red}Some checks failed. Please review the errors above.${colors.reset}`);
172
+ console.log(` ${c.brightRed}Some checks failed. Please review the errors above.${c.reset}`);
181
173
  process.exit(1);
182
174
  } else if (warnCount > 0) {
183
- console.log(`${colors.yellow}Installation OK with some warnings.${colors.reset}`);
175
+ console.log(` ${c.yellow}Installation OK with some warnings.${c.reset}`);
184
176
  process.exit(0);
185
177
  } else {
186
- console.log(`${colors.green}All checks passed! Claude Code is ready to use.${colors.reset}`);
178
+ console.log(` ${c.green}All checks passed! Claude Code is ready to use.${c.reset}`);
187
179
  process.exit(0);
188
180
  }
@@ -4,10 +4,9 @@ const { execSync, spawn } = require('child_process');
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
6
  const https = require('https');
7
+ const { logArrow, logStep } = require('./ui');
7
8
 
8
9
  const PACKAGE_NAME = 'claude-code-termux';
9
- const CACHE_FILE = path.join(process.env.HOME || '', '.claude', '.auto-update-cache');
10
- const CACHE_DURATION_MS = 24 * 60 * 60 * 1000; // 24 hours
11
10
  const FETCH_TIMEOUT_MS = 10000; // 10 seconds
12
11
 
13
12
  /**
@@ -68,41 +67,6 @@ function fetchLatestVersion() {
68
67
  });
69
68
  }
70
69
 
71
- /**
72
- * Check if we should skip the update check based on cache
73
- */
74
- function shouldSkipCheck() {
75
- try {
76
- if (!fs.existsSync(CACHE_FILE)) {
77
- return false;
78
- }
79
- const cacheContent = fs.readFileSync(CACHE_FILE, 'utf8');
80
- const cache = JSON.parse(cacheContent);
81
- const lastCheck = new Date(cache.lastCheck).getTime();
82
- const now = Date.now();
83
- return (now - lastCheck) < CACHE_DURATION_MS;
84
- } catch (err) {
85
- return false;
86
- }
87
- }
88
-
89
- /**
90
- * Update the cache file with current timestamp
91
- */
92
- function updateCache() {
93
- try {
94
- const cacheDir = path.dirname(CACHE_FILE);
95
- if (!fs.existsSync(cacheDir)) {
96
- fs.mkdirSync(cacheDir, { recursive: true });
97
- }
98
- fs.writeFileSync(CACHE_FILE, JSON.stringify({
99
- lastCheck: new Date().toISOString()
100
- }));
101
- } catch (err) {
102
- // Silently ignore cache write errors
103
- }
104
- }
105
-
106
70
  /**
107
71
  * Compare semantic versions
108
72
  * Returns true if latestVersion > installedVersion
@@ -123,7 +87,7 @@ function isNewerVersion(installedVersion, latestVersion) {
123
87
  */
124
88
  function runUpdate(latestVersion) {
125
89
  return new Promise((resolve, reject) => {
126
- console.log(`[claude-code-termux] Updating to ${latestVersion}...`);
90
+ logArrow(`Updating to v${latestVersion}...`);
127
91
 
128
92
  try {
129
93
  // Use npm install with --force to bypass platform checks for sharp-wasm32
@@ -142,7 +106,7 @@ function runUpdate(latestVersion) {
142
106
  * Re-execute the current process with the same arguments
143
107
  */
144
108
  function relaunchProcess() {
145
- console.log('[claude-code-termux] Update complete, restarting...\n');
109
+ logStep('Update complete, restarting...');
146
110
 
147
111
  // Filter out any auto-update related args to prevent infinite loops
148
112
  const args = process.argv.slice(2).filter(arg => arg !== '--no-auto-update');
@@ -168,11 +132,6 @@ async function checkAndUpdate() {
168
132
  return false;
169
133
  }
170
134
 
171
- // Check cache
172
- if (shouldSkipCheck()) {
173
- return false;
174
- }
175
-
176
135
  try {
177
136
  // Get installed version
178
137
  const installedVersion = getInstalledVersion();
@@ -183,9 +142,6 @@ async function checkAndUpdate() {
183
142
  // Fetch latest version from npm
184
143
  const latestVersion = await fetchLatestVersion();
185
144
 
186
- // Update cache regardless of whether update is needed
187
- updateCache();
188
-
189
145
  // Check if update is needed
190
146
  if (!isNewerVersion(installedVersion, latestVersion)) {
191
147
  return false;
@@ -1,17 +1,21 @@
1
1
  /**
2
- * Apply All Termux Patches
2
+ * Apply All Termux Boosts
3
3
  *
4
- * This module applies all necessary runtime patches for Termux/Android compatibility.
4
+ * This module applies all necessary runtime boosts for Termux/Android compatibility.
5
5
  * It must be loaded before the main Claude Code CLI.
6
+ *
7
+ * Shows an animated progress bar during loading. Only surfaces failures.
6
8
  */
7
9
 
8
10
  'use strict';
9
11
 
10
12
  const path = require('path');
13
+ const { boostName, renderProgressBar, finishProgressBar, sleep, logWarn } = require('../ui');
11
14
 
12
- // Patch modules to load
13
- const patches = [
15
+ // Boost modules to load
16
+ const boosts = [
14
17
  './suppress-migration-ui', // Must be first - sets env vars before CLI loads
18
+ './tmpdir-config', // Must be early - /tmp is inaccessible on Termux
15
19
  './sharp-fallback',
16
20
  './ripgrep-fallback',
17
21
  './path-normalization',
@@ -19,28 +23,45 @@ const patches = [
19
23
  './hook-events',
20
24
  ];
21
25
 
22
- console.log('[claude-code-termux] Applying runtime patches...');
23
-
26
+ const total = boosts.length;
24
27
  let appliedCount = 0;
25
28
  let failedCount = 0;
29
+ const failures = [];
26
30
 
27
- for (const patchPath of patches) {
31
+ // Show initial empty progress bar
32
+ renderProgressBar(0, total);
33
+ sleep(60);
34
+
35
+ for (let i = 0; i < boosts.length; i++) {
36
+ const boostPath = boosts[i];
28
37
  try {
29
- const patch = require(patchPath);
30
- if (typeof patch.apply === 'function') {
31
- patch.apply();
38
+ const boost = require(boostPath);
39
+ if (typeof boost.apply === 'function') {
40
+ boost.apply();
32
41
  appliedCount++;
33
- console.log(`[claude-code-termux] Applied: ${path.basename(patchPath)}`);
34
42
  }
35
43
  } catch (err) {
36
44
  failedCount++;
37
- // Don't fail hard - some patches may not be needed
38
- if (process.env.DEBUG) {
39
- console.error(`[claude-code-termux] Failed to apply ${patchPath}:`, err.message);
40
- }
45
+ failures.push({
46
+ name: boostName(boostPath),
47
+ error: err.message,
48
+ });
41
49
  }
50
+
51
+ // Update progress bar
52
+ renderProgressBar(i + 1, total);
53
+ sleep(60);
42
54
  }
43
55
 
44
- console.log(`[claude-code-termux] Patches applied: ${appliedCount}, skipped: ${failedCount}`);
56
+ // Move past the progress bar line
57
+ finishProgressBar();
58
+
59
+ // Surface failures only
60
+ for (const f of failures) {
61
+ logWarn(`${f.name} failed`);
62
+ if (process.env.DEBUG) {
63
+ logWarn(` ${f.error}`);
64
+ }
65
+ }
45
66
 
46
- module.exports = { appliedCount, failedCount };
67
+ module.exports = { appliedCount, failedCount, failures };
@@ -55,10 +55,12 @@ function apply() {
55
55
  // Find system ripgrep
56
56
  systemRipgrepPath = findSystemRipgrep();
57
57
 
58
- if (systemRipgrepPath) {
59
- console.log(`[claude-code-termux] Found system ripgrep: ${systemRipgrepPath}`);
60
- } else {
61
- console.warn('[claude-code-termux] System ripgrep not found. Install with: pkg install ripgrep');
58
+ if (process.env.DEBUG) {
59
+ if (systemRipgrepPath) {
60
+ console.log(`[claude-code-termux] Found system ripgrep: ${systemRipgrepPath}`);
61
+ } else {
62
+ console.warn('[claude-code-termux] System ripgrep not found. Install with: pkg install ripgrep');
63
+ }
62
64
  }
63
65
 
64
66
  // Monkey-patch spawn to intercept ripgrep calls
@@ -70,7 +72,7 @@ function apply() {
70
72
  if (typeof command === 'string' && command.includes('ripgrep') && command.includes('arm64-android')) {
71
73
  // Replace with system ripgrep if available
72
74
  if (systemRipgrepPath) {
73
- console.log('[claude-code-termux] Redirecting ripgrep call to system binary');
75
+ if (process.env.DEBUG) console.log('[claude-code-termux] Redirecting ripgrep call to system binary');
74
76
  return originalSpawn(systemRipgrepPath, args, options);
75
77
  }
76
78
  }
@@ -78,7 +80,7 @@ function apply() {
78
80
  // Also intercept direct 'rg' calls with invalid paths
79
81
  if (typeof command === 'string' && command.endsWith('/rg') && !fs.existsSync(command)) {
80
82
  if (systemRipgrepPath) {
81
- console.log('[claude-code-termux] Redirecting invalid rg path to system binary');
83
+ if (process.env.DEBUG) console.log('[claude-code-termux] Redirecting invalid rg path to system binary');
82
84
  return originalSpawn(systemRipgrepPath, args, options);
83
85
  }
84
86
  }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * TMPDIR Configuration Patch
3
+ *
4
+ * Termux doesn't expose /tmp (permission denied). Claude Code's sandbox
5
+ * and MCP servers create temp directories under /tmp, which fails on Termux.
6
+ * This patch sets TMPDIR to the Termux-accessible temp directory so that
7
+ * os.tmpdir() and all child processes use the correct path.
8
+ */
9
+
10
+ 'use strict';
11
+
12
+ const fs = require('fs');
13
+
14
+ const isTermux = process.platform === 'android' ||
15
+ process.env.PREFIX?.includes('com.termux') ||
16
+ process.env.HOME?.includes('com.termux');
17
+
18
+ const TERMUX_TMP = '/data/data/com.termux/files/usr/tmp';
19
+
20
+ function apply() {
21
+ if (!isTermux) {
22
+ return;
23
+ }
24
+
25
+ // Don't override if the user has already set TMPDIR
26
+ if (process.env.TMPDIR) {
27
+ if (process.env.DEBUG) {
28
+ console.log('[claude-code-termux] TMPDIR already set:', process.env.TMPDIR);
29
+ }
30
+ return;
31
+ }
32
+
33
+ // Ensure the directory exists
34
+ if (!fs.existsSync(TERMUX_TMP)) {
35
+ fs.mkdirSync(TERMUX_TMP, { recursive: true });
36
+ }
37
+
38
+ process.env.TMPDIR = TERMUX_TMP;
39
+ process.env.TMP = TERMUX_TMP;
40
+ process.env.TEMP = TERMUX_TMP;
41
+
42
+ if (process.env.DEBUG) {
43
+ console.log('[claude-code-termux] TMPDIR set to:', TERMUX_TMP);
44
+ }
45
+ }
46
+
47
+ module.exports = { apply };
package/src/ui.js ADDED
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Claude Code Termux - UI Output Utilities
3
+ *
4
+ * Shared module for all user-facing terminal output.
5
+ * Provides colors, symbols, banner, progress bar, and logging helpers.
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ const path = require('path');
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Color support detection
14
+ // ---------------------------------------------------------------------------
15
+
16
+ function supportsColor() {
17
+ if (process.env.NO_COLOR !== undefined) return false;
18
+ if (process.env.FORCE_COLOR !== undefined) return true;
19
+ if (!process.stderr.isTTY) return false;
20
+ const term = process.env.TERM || '';
21
+ if (term === 'dumb') return false;
22
+ return true;
23
+ }
24
+
25
+ const useColor = supportsColor();
26
+
27
+ function esc(code) {
28
+ return useColor ? `\x1b[${code}m` : '';
29
+ }
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // Color palette
33
+ // ---------------------------------------------------------------------------
34
+
35
+ const c = {
36
+ reset: esc('0'),
37
+ bold: esc('1'),
38
+ dim: esc('2'),
39
+ red: esc('31'),
40
+ green: esc('32'),
41
+ yellow: esc('33'),
42
+ blue: esc('34'),
43
+ magenta: esc('35'),
44
+ cyan: esc('36'),
45
+ white: esc('37'),
46
+ brightRed: esc('91'),
47
+ brightCyan: esc('96'),
48
+ };
49
+
50
+ // ---------------------------------------------------------------------------
51
+ // Symbols
52
+ // ---------------------------------------------------------------------------
53
+
54
+ const symbols = {
55
+ check: '\u2714', // ✔
56
+ cross: '\u2716', // ✖
57
+ arrow: '\u2192', // →
58
+ bolt: '\u26A1', // ⚡
59
+ rocket: '\uD83D\uDE80', // 🚀
60
+ warn: '!',
61
+ boxTL: '\u256D', // ╭
62
+ boxTR: '\u256E', // ╮
63
+ boxBL: '\u2570', // ╰
64
+ boxBR: '\u256F', // ╯
65
+ boxH: '\u2500', // ─
66
+ boxV: '\u2502', // │
67
+ blockFull: '\u2588', // █
68
+ blockEmpty: '\u2591', // ░
69
+ };
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // Termux detection (shared helper)
73
+ // ---------------------------------------------------------------------------
74
+
75
+ const isTermux = process.platform === 'android' ||
76
+ (process.env.PREFIX || '').includes('com.termux') ||
77
+ (process.env.HOME || '').includes('com.termux');
78
+
79
+ // ---------------------------------------------------------------------------
80
+ // Banner
81
+ // ---------------------------------------------------------------------------
82
+
83
+ function printBanner(version) {
84
+ const tag = isTermux ? 'Termux Edition' : 'Desktop';
85
+ const inner = ` Claude Code ${c.dim}\u00B7${c.reset}${c.brightCyan} ${tag} ${c.dim}v${version}${c.reset}`;
86
+ const rawInner = ` Claude Code \u00B7 ${tag} v${version}`;
87
+ const width = rawInner.length + 2;
88
+
89
+ const top = ` ${c.cyan}${symbols.boxTL}${symbols.boxH.repeat(width)}${symbols.boxTR}${c.reset}`;
90
+ const mid = ` ${c.cyan}${symbols.boxV}${c.reset}${inner} ${c.cyan}${symbols.boxV}${c.reset}`;
91
+ const bot = ` ${c.cyan}${symbols.boxBL}${symbols.boxH.repeat(width)}${symbols.boxBR}${c.reset}`;
92
+
93
+ console.log('');
94
+ console.log(top);
95
+ console.log(mid);
96
+ console.log(bot);
97
+ }
98
+
99
+ // ---------------------------------------------------------------------------
100
+ // Boost (patch) name map
101
+ // ---------------------------------------------------------------------------
102
+
103
+ const BOOST_NAMES = {
104
+ 'suppress-migration-ui': 'Suppress migration prompts',
105
+ 'tmpdir-config': 'Temp directory (Termux)',
106
+ 'sharp-fallback': 'Image support (Sharp WASM)',
107
+ 'ripgrep-fallback': 'Code search (ripgrep)',
108
+ 'path-normalization': 'Termux path resolution',
109
+ 'oauth-storage': 'Token storage fallback',
110
+ 'hook-events': 'Event hook compatibility',
111
+ };
112
+
113
+ function boostName(patchPath) {
114
+ const base = path.basename(patchPath);
115
+ return BOOST_NAMES[base] || base;
116
+ }
117
+
118
+ // ---------------------------------------------------------------------------
119
+ // Progress bar
120
+ // ---------------------------------------------------------------------------
121
+
122
+ const BAR_WIDTH = 10;
123
+
124
+ function renderProgressBar(current, total) {
125
+ const filled = Math.round((current / total) * BAR_WIDTH);
126
+ const empty = BAR_WIDTH - filled;
127
+ const bar = symbols.blockFull.repeat(filled) + symbols.blockEmpty.repeat(empty);
128
+ const line = ` ${c.cyan}${symbols.bolt}${c.reset} Powering up... ${c.cyan}[${bar}]${c.reset} ${current}/${total} boosts`;
129
+ process.stdout.write(`\r${line}`);
130
+ }
131
+
132
+ function finishProgressBar() {
133
+ process.stdout.write('\n');
134
+ }
135
+
136
+ // ---------------------------------------------------------------------------
137
+ // Synchronous sleep (for animation timing)
138
+ // ---------------------------------------------------------------------------
139
+
140
+ function sleep(ms) {
141
+ const end = Date.now() + ms;
142
+ while (Date.now() < end) {
143
+ // busy-wait — keeps total under 500ms for 6 boosts
144
+ }
145
+ }
146
+
147
+ // ---------------------------------------------------------------------------
148
+ // Logging helpers
149
+ // ---------------------------------------------------------------------------
150
+
151
+ function logStep(message) {
152
+ console.log(` ${c.green}${symbols.check}${c.reset} ${message}`);
153
+ }
154
+
155
+ function logArrow(message) {
156
+ console.log(` ${c.cyan}${symbols.arrow}${c.reset} ${message}`);
157
+ }
158
+
159
+ function logWarn(message) {
160
+ console.log(` ${c.yellow}${symbols.warn}${c.reset} ${message}`);
161
+ }
162
+
163
+ function logError(message) {
164
+ console.error(` ${c.brightRed}${symbols.cross}${c.reset} ${message}`);
165
+ }
166
+
167
+ function logDim(message) {
168
+ console.log(` ${c.dim}${message}${c.reset}`);
169
+ }
170
+
171
+ function logReady(hasWarnings) {
172
+ if (hasWarnings) {
173
+ console.log(` ${symbols.rocket} ${c.yellow}Ready with warnings${c.reset}`);
174
+ } else {
175
+ console.log(` ${symbols.rocket} ${c.green}Ready to go!${c.reset}`);
176
+ }
177
+ console.log('');
178
+ }
179
+
180
+ // ---------------------------------------------------------------------------
181
+ // Exports
182
+ // ---------------------------------------------------------------------------
183
+
184
+ module.exports = {
185
+ c,
186
+ symbols,
187
+ useColor,
188
+ isTermux,
189
+ printBanner,
190
+ BOOST_NAMES,
191
+ boostName,
192
+ renderProgressBar,
193
+ finishProgressBar,
194
+ sleep,
195
+ logStep,
196
+ logArrow,
197
+ logWarn,
198
+ logError,
199
+ logDim,
200
+ logReady,
201
+ };