Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 9 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
50 changes: 45 additions & 5 deletions src/export-playwright.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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<BrowserType> {
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);
Expand Down Expand Up @@ -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]'
);
}

Expand Down Expand Up @@ -119,7 +148,11 @@ export class PlaywrightExporter {
*/
private async renderHTMLToPNG(htmlContent: string, onProgress?: (status: string) => void): Promise<Buffer> {
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',
Expand All @@ -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 });
Expand Down
35 changes: 31 additions & 4 deletions src/utils/browser-installer.ts
Original file line number Diff line number Diff line change
@@ -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);

Expand All @@ -15,9 +16,19 @@ export class BackgroundBrowserInstaller {
/**
* Check if Chromium is installed
*/
private isChromiumInstalled(): boolean {
private async isChromiumInstalled(): Promise<boolean> {
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 {
Expand All @@ -40,13 +51,29 @@ export class BackgroundBrowserInstaller {
private async checkAndInstall(): Promise<void> {
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)...';

Expand Down
88 changes: 88 additions & 0 deletions src/utils/platform-detector.ts
Original file line number Diff line number Diff line change
@@ -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 };
}