diff --git a/README.md b/README.md index 63a0abb..712645a 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,43 @@ node dist/index.js **⚠️ Important:** Always use **Node** to run the built version (`npm start` or `node dist/index.js`), not `bun start`. The image export feature uses Playwright which has known compatibility issues with Bun's runtime, especially on Windows. Bun is great for development and building, but Node is required for running the final output. +### For Termux (Android) + +Running on Android via Termux requires special setup: + +**Prerequisites:** +```bash +# Install required packages +pkg install nodejs chromium git +``` + +**Setup:** +```bash +# Clone the repository +git clone https://github.com/d3varaja/gh-wrapped-cli.git +cd gh-wrapped-cli + +# Install dependencies +npm install --legacy-peer-deps + +# Set environment variables (add to ~/.bashrc for persistence) +export CHROMIUM_PATH=/data/data/com.termux/files/usr/bin/chromium-browser +export PLAYWRIGHT_BROWSERS_PATH=0 + +# Build for Termux +npm run build:termux + +# Run the app +npm start +``` + +**Available Termux-specific scripts:** +- `npm run dev:termux` - Run in development mode +- `npm run build:termux` - Build for Termux (uses esbuild) +- `npm run test:termux` - Run tests (uses vitest) + +**Note:** The app automatically detects Termux and uses system Chromium instead of downloading Playwright's bundled browser, saving ~200MB of storage. + ## How It Works 1. Fetches your public GitHub data via GitHub API diff --git a/package.json b/package.json index 4852d1f..85e2a17 100644 --- a/package.json +++ b/package.json @@ -18,12 +18,16 @@ }, "scripts": { "dev": "bun run src/index.tsx", + "dev:termux": "tsx src/index.tsx", "build": "bun build src/index.tsx --outdir dist --target node --minify --external playwright", + "build:termux": "esbuild src/index.tsx --bundle --platform=node --target=node18 --outdir=dist --minify --packages=external --format=esm", "postbuild": "node -e \"const fs = require('fs'); fs.cpSync('src/templates', 'dist/templates', {recursive: true}); fs.cpSync('node_modules/yoga-wasm-web/dist/yoga.wasm', 'dist/yoga.wasm');\"", "build:types": "tsc --emitDeclarationOnly", "start": "node dist/index.js", "prepublishOnly": "bun run build && bun run build:types", - "test": "bun test" + "prepublishOnly:termux": "npm run build:termux && npm run build:types", + "test": "bun test", + "test:termux": "vitest" }, "keywords": [ "github", @@ -50,14 +54,17 @@ "ink-spinner": "^5.0.0", "ink-text-input": "^5.0.1", "playwright": "^1.57.0", + "playwright-core": "^1.57.0", "react": "^18.2.0" }, "devDependencies": { "@types/node": "^20.10.5", "@types/react": "^18.2.45", + "esbuild": "^0.24.2", "react-devtools-core": "7.0.1", "tsx": "^4.7.0", - "typescript": "^5.3.3" + "typescript": "^5.3.3", + "vitest": "^2.1.8" }, "engines": { "node": ">=18.0.0", diff --git a/src/export-playwright.ts b/src/export-playwright.ts index cc33a91..6c92d44 100644 --- a/src/export-playwright.ts +++ b/src/export-playwright.ts @@ -1,4 +1,4 @@ -import { chromium, Browser } from 'playwright'; +import type { Browser, BrowserType } from 'playwright-core'; import { promises as fs, readFileSync } from 'fs'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; @@ -7,6 +7,22 @@ import { calculateScore, determineTier, getTierName, type Tier } from './tier-ca import { fetchAvatarAsBase64 } from './utils/avatar-fetcher.js'; import { injectDataIntoTemplate } from './utils/html-injector.js'; import { getBrowserInstaller } from './utils/browser-installer.js'; +import { detectPlatform } from './utils/platform-detector.js'; + +/** + * Dynamically load chromium from the appropriate package based on platform + */ +async function getChromium(): Promise { + const platform = detectPlatform(); + + if (platform.isTermux) { + const { chromium } = await import('playwright-core'); + return chromium; + } else { + const { chromium } = await import('playwright'); + return chromium; + } +} const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -75,10 +91,23 @@ export class PlaywrightExporter { await this.closeBrowser(); if (error instanceof Error && error.message?.includes('browserType.launch')) { + const platform = detectPlatform(); + + if (platform.isTermux) { + throw new Error( + 'Failed to launch Chromium on Termux!\n\n' + + 'Steps:\n' + + '1. Install: pkg install chromium\n' + + '2. Set: export CHROMIUM_PATH=/data/data/com.termux/files/usr/bin/chromium-browser\n' + + '3. Retry with [E]\n\n' + + 'Path checked: ' + (platform.chromiumPath || 'not set') + ); + } + throw new Error( 'Chromium browser not installed!\n\n' + - 'Install with: bunx playwright install chromium\n' + - 'Then retry export with [R]' + 'Install with: npx playwright install chromium\n' + + 'Then retry export with [E]' ); } @@ -119,7 +148,11 @@ export class PlaywrightExporter { */ private async renderHTMLToPNG(htmlContent: string, onProgress?: (status: string) => void): Promise { onProgress?.('Starting browser...'); - this.browser = await chromium.launch({ + + const chromium = await getChromium(); + const platform = detectPlatform(); + + const launchOptions: any = { headless: true, args: [ '--disable-dev-shm-usage', @@ -129,7 +162,14 @@ export class PlaywrightExporter { '--force-color-profile=srgb' ], timeout: 60000 - }); + }; + + // Add executablePath for Termux + if (platform.isTermux && platform.chromiumPath) { + launchOptions.executablePath = platform.chromiumPath; + } + + this.browser = await chromium.launch(launchOptions); const page = await this.browser.newPage(); await page.setViewportSize({ width: 440, height: 680 }); diff --git a/src/utils/browser-installer.ts b/src/utils/browser-installer.ts index 5915a10..8e545e9 100644 --- a/src/utils/browser-installer.ts +++ b/src/utils/browser-installer.ts @@ -1,6 +1,7 @@ import { exec } from 'child_process'; import { existsSync } from 'fs'; import { promisify } from 'util'; +import { detectPlatform } from './platform-detector.js'; const execAsync = promisify(exec); @@ -15,9 +16,19 @@ export class BackgroundBrowserInstaller { /** * Check if Chromium is installed */ - private isChromiumInstalled(): boolean { + private async isChromiumInstalled(): Promise { + const platform = detectPlatform(); + + if (platform.isTermux) { + // Check system chromium for Termux + return platform.chromiumPath + ? existsSync(platform.chromiumPath) + : false; + } + + // Check playwright chromium for regular environments try { - const { chromium } = require('playwright'); + const { chromium } = await import('playwright'); const executablePath = chromium.executablePath(); return existsSync(executablePath); } catch { @@ -40,13 +51,29 @@ export class BackgroundBrowserInstaller { private async checkAndInstall(): Promise { try { this.status = 'checking'; + const platform = detectPlatform(); - if (this.isChromiumInstalled()) { + if (await this.isChromiumInstalled()) { this.status = 'ready'; return; } - // Not installed - start installation + // Termux requires manual installation + if (platform.isTermux) { + this.status = 'error'; + this.error = new Error( + 'Termux requires system Chromium.\n\n' + + 'Install with:\n' + + ' pkg install chromium\n\n' + + 'Set environment:\n' + + ' export CHROMIUM_PATH=/data/data/com.termux/files/usr/bin/chromium-browser\n\n' + + 'Add to ~/.bashrc for persistence' + ); + this.progress = 'Manual installation required'; + return; + } + + // Regular environment - auto-install this.status = 'installing'; this.progress = 'Installing Chromium (one-time, ~200MB)...'; diff --git a/src/utils/platform-detector.ts b/src/utils/platform-detector.ts new file mode 100644 index 0000000..6887299 --- /dev/null +++ b/src/utils/platform-detector.ts @@ -0,0 +1,88 @@ +import { existsSync } from 'fs'; + +export interface PlatformConfig { + isTermux: boolean; + chromiumPath: string | null; + browserPackage: 'playwright' | 'playwright-core'; + requiresSystemChromium: boolean; +} + +/** + * Detect if running on Termux and return platform-specific configuration + */ +export function detectPlatform(): PlatformConfig { + // Allow manual override via environment variable + if (process.env.FORCE_PLAYWRIGHT_CORE === 'true') { + return { + isTermux: true, + chromiumPath: process.env.CHROMIUM_PATH || '/data/data/com.termux/files/usr/bin/chromium-browser', + browserPackage: 'playwright-core', + requiresSystemChromium: true + }; + } + + // Detection logic (in priority order): + // 1. Check for CHROMIUM_PATH environment variable (explicit Termux setup) + const chromiumPath = process.env.CHROMIUM_PATH; + + // 2. Check for PREFIX environment variable (Termux sets this) + const hasTermuxPrefix = process.env.PREFIX === '/data/data/com.termux/files/usr'; + + // 3. Check for Android platform + const isAndroid = process.platform === 'android'; + + // 4. Check if Termux chromium binary exists + const defaultTermuxChromiumPath = '/data/data/com.termux/files/usr/bin/chromium-browser'; + const hasTermuxChromium = existsSync(defaultTermuxChromiumPath); + + const isTermux = Boolean( + chromiumPath || + hasTermuxPrefix || + (isAndroid && hasTermuxChromium) + ); + + return { + isTermux, + chromiumPath: isTermux + ? (chromiumPath || defaultTermuxChromiumPath) + : null, + browserPackage: isTermux ? 'playwright-core' : 'playwright', + requiresSystemChromium: isTermux + }; +} + +/** + * Get the chromium executable path for the current platform + * Returns undefined for non-Termux platforms (uses playwright's bundled chromium) + */ +export function getChromiumExecutablePath(): string | undefined { + const config = detectPlatform(); + return config.chromiumPath || undefined; +} + +/** + * Validate Termux setup and return helpful error message if invalid + */ +export function validateTermuxSetup(): { valid: boolean; error?: string } { + const config = detectPlatform(); + + if (!config.isTermux) { + return { valid: true }; + } + + if (!config.chromiumPath) { + return { + valid: false, + error: 'CHROMIUM_PATH not set.\n\nRun: export CHROMIUM_PATH=/data/data/com.termux/files/usr/bin/chromium-browser' + }; + } + + if (!existsSync(config.chromiumPath)) { + return { + valid: false, + error: `Chromium not found at: ${config.chromiumPath}\n\nInstall with: pkg install chromium` + }; + } + + return { valid: true }; +}