Migrate to Microsoft node-pty v1.1.0-beta34 (#87)

This commit is contained in:
Peter Steinberger 2025-06-26 23:10:05 +02:00 committed by GitHub
parent 5a4b939564
commit c70330bcfd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 1033 additions and 866 deletions

148
.github/workflows/README.md vendored Normal file
View file

@ -0,0 +1,148 @@
# VibeTunnel CI/CD Workflows
This directory contains GitHub Actions workflows for continuous integration and testing.
## Workflows
### 1. Web CI (`web-ci.yml`)
Basic CI workflow that runs on every push and PR affecting the web directory.
**Jobs:**
- **Lint and Type Check**: Runs biome linting and TypeScript type checking
- **Build**: Builds the project and uploads artifacts
- **Test**: Runs the test suite
**Triggers:**
- Push to `main` or `ms-pty` branches
- Pull requests to `main`
- Only when files in `web/` directory change
### 2. SEA Build Test (`sea-build-test.yml`)
Advanced workflow for testing Single Executable Application (SEA) builds with custom Node.js.
**Features:**
- Builds custom Node.js from source with optimizations
- Uses Blacksmith runners for significantly faster builds
- Caches custom Node.js builds for faster subsequent runs
- Tests SEA builds with both system and custom Node.js
- Supports manual triggers with custom Node.js versions
**Jobs:**
1. **build-custom-node**:
- Runs on `blacksmith-32vcpu-ubuntu-2404-arm` for fast compilation
- Builds minimal Node.js without npm, intl, inspector, etc.
- Uses Blacksmith cache for persistence
- Outputs the custom Node.js path for downstream jobs
2. **test-sea-build**:
- Runs on `blacksmith-8vcpu-ubuntu-2404-arm`
- Matrix build testing both system and custom Node.js
- Builds SEA executable with node-pty patches
- Performs smoke tests on the generated executable
- Uploads artifacts for inspection
3. **test-github-runners**:
- Uses standard `ubuntu-latest` runners for comparison
- Helps identify any Blacksmith-specific issues
- Runs only on push events
### 3. Xcode SEA Test (`xcode-sea-test.yml`)
Tests the macOS Xcode build with custom Node.js to ensure the VibeTunnel.app works correctly with SEA executables.
**Features:**
- Builds custom Node.js on macOS using self-hosted runners
- Tests integration of SEA executable into macOS app bundle
- Verifies the app launches and contains the correct binaries
- Supports manual triggers with custom Node.js versions
**Jobs:**
1. **build-custom-node-mac**:
- Runs on self-hosted macOS runner
- Builds custom Node.js for macOS
- Uses GitHub Actions cache (appropriate for self-hosted)
- Outputs node path and size information
2. **test-xcode-build**:
- Builds SEA executable with custom Node.js
- Copies SEA and native modules to app resources
- Builds VibeTunnel.app using Xcode
- Verifies SEA executable is correctly bundled
- Tests basic app functionality
- Uploads built app as artifact
## Runner Strategy
### Blacksmith Runners (Linux)
- **Custom Node.js Build**: `blacksmith-32vcpu-ubuntu-2404-arm` (high CPU for compilation)
- **Other CI Jobs**: `blacksmith-8vcpu-ubuntu-2404-arm` (standard workloads)
- Benefits: Significantly faster builds, better caching, ARM64 architecture
### Self-Hosted Runners (macOS)
- Used for Xcode builds and macOS-specific testing
- Access to Xcode and macOS-specific tools
- Can test code signing and notarization
### GitHub Runners (Comparison)
- `ubuntu-latest` used in test job for baseline comparison
- Helps identify Blacksmith-specific issues
## Caching Strategy
### Blacksmith Cache
**IMPORTANT**: When using Blacksmith runners, you MUST use `useblacksmith/cache@v1`
- Used for all jobs running on Blacksmith runners
- Provides faster cache operations
- Better persistence than GitHub Actions cache
- Cache key: `custom-node-linux-x64-v{version}-{hash}`
### GitHub Actions Cache
**Only used for self-hosted runners and standard GitHub runners**
- Self-hosted macOS runners use `actions/cache@v4`
- Standard GitHub runners use `actions/cache@v4`
- Cache key format same as Blacksmith
### Cache Performance
- Initial custom Node.js build: ~10-15 minutes on 32vCPU
- Cached builds: ~1 minute
- Blacksmith cache restoration: 2-3x faster than GitHub Actions cache
## Manual Triggers
The SEA build workflow supports manual triggers via GitHub UI:
```yaml
workflow_dispatch:
inputs:
node_version:
description: 'Node.js version to build'
default: '24.2.0'
```
## Local Testing
To test the SEA build locally:
```bash
# Build custom Node.js
cd web
node build-custom-node.js
# Build SEA with custom Node.js
node build-native.js --custom-node=".node-builds/node-v24.2.0-minimal/out/Release/node"
```
## Optimization Details
The custom Node.js build removes:
- International support (`--without-intl`)
- npm and corepack (`--without-npm --without-corepack`)
- Inspector/debugging (`--without-inspector`)
- Code cache and snapshots
- Uses `-Os` optimization for size
This reduces the Node.js binary from ~120MB to ~50-60MB.
## Future Improvements
- [ ] Add Windows and macOS to the build matrix
- [ ] Implement release workflow for automated releases
- [ ] Add performance benchmarks
- [ ] Integrate with release signing process

View file

@ -148,38 +148,4 @@ jobs:
HEAD_BRANCH: ${{ github.event.pull_request.head.ref }}
CHANGED_FILES: ${{ github.event.pull_request.changed_files }}
ADDITIONS: ${{ github.event.pull_request.additions }}
DELETIONS: ${{ github.event.pull_request.deletions }}
# Optional: Post a summary comment if Claude's review is very long
- name: Create summary if needed
if: steps.check-review.outputs.skip != 'true' && always()
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
// Wait a bit for Claude's comment to appear
await new Promise(resolve => setTimeout(resolve, 5000));
// Find Claude's latest comment
const comments = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
per_page: 10,
sort: 'created',
direction: 'desc'
});
const claudeComment = comments.data.find(c => c.user.login === 'claude[bot]');
if (claudeComment && claudeComment.body.length > 10000) {
// If the review is very long, add a summary at the top
const summary = `## 📊 Review Summary\n\n**Review length**: ${claudeComment.body.length} characters\n**Commit**: ${context.payload.pull_request.head.sha.substring(0, 7)}\n\n> 💡 Tip: Use the table of contents below to navigate this review.\n\n---\n\n`;
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: claudeComment.id,
body: summary + claudeComment.body
});
}
DELETIONS: ${{ github.event.pull_request.deletions }}

234
.github/workflows/sea-build-test.yml vendored Normal file
View file

@ -0,0 +1,234 @@
name: SEA Build Test
on:
push:
branches: [ main ]
paths:
- 'web/**'
- '.github/workflows/sea-build-test.yml'
pull_request:
branches: [ main ]
paths:
- 'web/**'
- '.github/workflows/sea-build-test.yml'
workflow_dispatch:
inputs:
node_version:
description: 'Node.js version to build'
required: false
default: '24.2.0'
type: string
env:
NODE_VERSION: ${{ github.event.inputs.node_version || '24.2.0' }}
CUSTOM_NODE_CACHE_KEY: custom-node-linux-x64
jobs:
build-custom-node:
name: Build Custom Node.js
# DISABLED: Custom Node.js compilation temporarily disabled
if: false
runs-on: blacksmith-32vcpu-ubuntu-2404-arm
outputs:
cache-hit: ${{ steps.cache-custom-node.outputs.cache-hit }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup build dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
build-essential \
python3 \
ninja-build \
ccache \
libpam0g-dev
- name: Cache custom Node.js build (Blacksmith)
id: cache-custom-node
uses: useblacksmith/cache@v1
with:
path: |
web/.node-builds/node-v${{ env.NODE_VERSION }}-minimal
key: ${{ env.CUSTOM_NODE_CACHE_KEY }}-v${{ env.NODE_VERSION }}-${{ hashFiles('web/build-custom-node.js') }}
restore-keys: |
${{ env.CUSTOM_NODE_CACHE_KEY }}-v${{ env.NODE_VERSION }}-
- name: Build custom Node.js
if: steps.cache-custom-node.outputs.cache-hit != 'true'
working-directory: web
run: |
node build-custom-node.js --version=${{ env.NODE_VERSION }}
test-sea-build:
name: Test SEA Build
# DISABLED: Removed dependency on build-custom-node since it's disabled
# needs: build-custom-node
runs-on: blacksmith-8vcpu-ubuntu-2404-arm
strategy:
matrix:
# DISABLED: Only testing with system Node.js, custom disabled
node-type: [system] # was: [system, custom]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10.12.1
- name: Setup Node.js (system)
if: matrix.node-type == 'system'
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
cache-dependency-path: 'web/pnpm-lock.yaml'
- name: Restore custom Node.js from cache (Blacksmith)
if: matrix.node-type == 'custom'
id: restore-custom-node
uses: useblacksmith/cache@v1
with:
path: |
web/.node-builds/node-v${{ env.NODE_VERSION }}-minimal
key: ${{ env.CUSTOM_NODE_CACHE_KEY }}-v${{ env.NODE_VERSION }}-${{ hashFiles('web/build-custom-node.js') }}
restore-keys: |
${{ env.CUSTOM_NODE_CACHE_KEY }}-v${{ env.NODE_VERSION }}-
- name: Build custom Node.js if not cached
# DISABLED: Custom Node.js compilation temporarily disabled
if: false && matrix.node-type == 'custom' && steps.restore-custom-node.outputs.cache-hit != 'true'
working-directory: web
run: |
echo "Custom Node.js not found in cache, building..."
node build-custom-node.js --version=${{ env.NODE_VERSION }}
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y libpam0g-dev
- name: Install dependencies
working-directory: web
run: |
pnpm install --frozen-lockfile
- name: Build SEA executable (system Node.js)
if: matrix.node-type == 'system'
working-directory: web
run: |
echo "Building SEA with system Node.js..."
node --version
node build-native.js
- name: Build SEA executable (custom Node.js)
# DISABLED: Custom Node.js test temporarily disabled
if: false && matrix.node-type == 'custom'
working-directory: web
run: |
# Use auto-discovery since we know the custom Node.js is in .node-builds
echo "Building SEA with custom Node.js (auto-discovery)..."
node build-native.js --custom-node
- name: Test SEA executable
working-directory: web
run: |
echo "Testing SEA executable..."
./native/vibetunnel --version || true
# Basic smoke test - check if it starts
timeout 5s ./native/vibetunnel --help || true
# Check binary size
ls -lh native/
size_mb=$(du -m native/vibetunnel | cut -f1)
echo "SEA executable size: ${size_mb} MB"
# Ensure native modules are present
test -f native/pty.node || (echo "ERROR: pty.node not found" && exit 1)
test -f native/authenticate_pam.node || (echo "ERROR: authenticate_pam.node not found" && exit 1)
# spawn-helper is only needed on macOS
if [[ "$RUNNER_OS" == "macOS" ]]; then
test -f native/spawn-helper || (echo "ERROR: spawn-helper not found" && exit 1)
fi
- name: Upload SEA artifacts
uses: actions/upload-artifact@v4
with:
name: sea-build-${{ matrix.node-type }}-linux
path: |
web/native/
retention-days: 7
# Test on standard GitHub runners for comparison
test-github-runners:
name: Test on GitHub Runners
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup build dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
build-essential \
python3 \
ninja-build \
ccache \
libpam0g-dev
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10.12.1
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
cache-dependency-path: 'web/pnpm-lock.yaml'
- name: Cache custom Node.js build
uses: actions/cache@v4
with:
path: |
web/.node-builds/node-v${{ env.NODE_VERSION }}-minimal
key: blacksmith-${{ env.CUSTOM_NODE_CACHE_KEY }}-v${{ env.NODE_VERSION }}-${{ hashFiles('web/build-custom-node.js') }}
restore-keys: |
blacksmith-${{ env.CUSTOM_NODE_CACHE_KEY }}-v${{ env.NODE_VERSION }}-
- name: Build and test everything
working-directory: web
run: |
# Install dependencies
pnpm install --frozen-lockfile
# Build custom Node.js if not cached
# DISABLED: Custom Node.js compilation temporarily disabled
# if [ ! -f ".node-builds/node-v${NODE_VERSION}-minimal/out/Release/node" ]; then
# node build-custom-node.js --version=${NODE_VERSION}
# fi
# Test both builds
echo "=== Testing with system Node.js ==="
node build-native.js
./native/vibetunnel --version || true
# DISABLED: Custom Node.js test temporarily disabled
# echo "=== Testing with custom Node.js ==="
# CUSTOM_NODE=".node-builds/node-v${NODE_VERSION}-minimal/out/Release/node"
# node build-native.js --custom-node="${CUSTOM_NODE}"
# ./native/vibetunnel --version || true
- name: Compare sizes
working-directory: web
run: |
echo "Binary sizes comparison:"
ls -lh native/vibetunnel
echo "System Node.js based: $(du -h native/vibetunnel | cut -f1)"

131
.github/workflows/web-ci.yml vendored Normal file
View file

@ -0,0 +1,131 @@
name: Web CI
on:
push:
branches: [ main, ms-pty ]
paths:
- 'web/**'
- '.github/workflows/web-ci.yml'
pull_request:
branches: [ main ]
paths:
- 'web/**'
- '.github/workflows/web-ci.yml'
defaults:
run:
working-directory: web
jobs:
lint-and-type-check:
name: Lint and Type Check
runs-on: blacksmith-8vcpu-ubuntu-2404-arm
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10.12.1
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
cache-dependency-path: 'web/pnpm-lock.yaml'
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y libpam0g-dev
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run linting
run: pnpm run lint
- name: Run type checking
run: pnpm run typecheck
- name: Check formatting
run: pnpm run format:check
build:
name: Build
runs-on: blacksmith-8vcpu-ubuntu-2404-arm
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10.12.1
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
cache-dependency-path: 'web/pnpm-lock.yaml'
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y libpam0g-dev
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build project
run: pnpm run build
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: web-build
path: |
web/dist/
web/public/
retention-days: 7
test:
name: Test
runs-on: blacksmith-8vcpu-ubuntu-2404-arm
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10.12.1
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
cache-dependency-path: 'web/pnpm-lock.yaml'
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y libpam0g-dev
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run tests
run: pnpm run test:ci
- name: Upload coverage
uses: actions/upload-artifact@v4
if: always()
with:
name: coverage-report
path: web/coverage/
retention-days: 7

View file

@ -31,6 +31,19 @@ By building a custom Node.js without these features, we achieve a significantly
- Uses the custom Node.js to create a smaller executable
- Build output shows version and size comparison
## Prerequisites
### Required Build Tools
For optimal build performance, the following tools are required:
- **Ninja**: Build system for faster compilation (significantly faster than Make)
- **ccache**: Compiler cache to speed up rebuilds
#### Installation
- **macOS**: `brew install ninja ccache`
- **Linux**: `apt-get install ninja-build ccache` (or equivalent for your distribution)
The build script will automatically use these tools if available, falling back to Make if Ninja is not found.
## Build Automation
### Release Builds

View file

@ -13,24 +13,7 @@ struct DockIconManagerTests {
let instance2 = DockIconManager.shared
#expect(instance1 === instance2)
}
@Test("User preference for dock icon")
func userPreferenceForDockIcon() {
// Store current value to restore later
let currentValue = UserDefaults.standard.bool(forKey: "showInDock")
// Test with dock icon enabled
UserDefaults.standard.set(true, forKey: "showInDock")
#expect(UserDefaults.standard.bool(forKey: "showInDock") == true)
// Test with dock icon disabled
UserDefaults.standard.set(false, forKey: "showInDock")
#expect(UserDefaults.standard.bool(forKey: "showInDock") == false)
// Restore original value
UserDefaults.standard.set(currentValue, forKey: "showInDock")
}
@Test("Update dock visibility based on windows")
@MainActor
func updateDockVisibilityBasedOnWindows() {
@ -108,4 +91,4 @@ struct DockIconManagerTests {
// Restore
UserDefaults.standard.set(originalPref, forKey: "showInDock")
}
}
}

View file

@ -3,12 +3,18 @@
/**
* Build a custom Node.js binary with reduced size by excluding features we don't need.
*
* See custom-node-build-flags.md for detailed documentation and size optimization results.
* This script automatically adapts to CI and local environments.
*
* Quick usage:
* node build-custom-node.js # Builds Node.js 24.2.0 (recommended)
* node build-custom-node.js --latest # Latest version
* node build-custom-node.js --version=24.2.0 # Specific version
* Usage:
* node build-custom-node.js # Builds Node.js 24.2.0 (recommended)
* node build-custom-node.js --latest # Latest version
* node build-custom-node.js --version=24.2.0 # Specific version
* NODE_VERSION=24.2.0 node build-custom-node.js # Via environment variable (CI)
*
* In CI environments:
* - Outputs GitHub Actions variables
* - Uses ccache if available
* - Creates build summary files
*/
const { execSync } = require('child_process');
@ -16,6 +22,9 @@ const fs = require('fs');
const path = require('path');
const https = require('https');
// Detect if running in CI
const isCI = process.env.CI === 'true' || process.env.GITHUB_ACTIONS === 'true';
// Parse command line arguments
const args = process.argv.slice(2);
let targetVersion = null;
@ -29,6 +38,13 @@ for (const arg of args) {
}
}
// Helper for GitHub Actions output
function setOutput(name, value) {
if (process.env.GITHUB_OUTPUT) {
fs.appendFileSync(process.env.GITHUB_OUTPUT, `${name}=${value}\n`);
}
}
// Helper to download files
function downloadFile(url, destPath) {
return new Promise((resolve, reject) => {
@ -70,7 +86,7 @@ async function getLatestNodeVersion() {
}
async function buildCustomNode() {
// Determine version to build
// Determine version to build (CLI args take precedence over env vars)
let nodeSourceVersion;
if (useLatest) {
console.log('Fetching latest Node.js version...');
@ -78,18 +94,25 @@ async function buildCustomNode() {
console.log(`Latest Node.js version: ${nodeSourceVersion}`);
} else if (targetVersion) {
nodeSourceVersion = targetVersion;
} else if (process.env.NODE_VERSION) {
// Support CI environment variable
nodeSourceVersion = process.env.NODE_VERSION;
} else {
// Default to Node.js 24.2.0 (recommended version)
nodeSourceVersion = '24.2.0';
}
console.log(`Building custom Node.js ${nodeSourceVersion} with all feature removals (-Os)...`);
const platform = process.platform;
const arch = process.arch;
console.log(`Building custom Node.js ${nodeSourceVersion} for ${platform}-${arch}...`);
console.log('This will take 10-20 minutes on first run, but will be cached for future builds.');
const nodeSourceUrl = `https://nodejs.org/dist/v${nodeSourceVersion}/node-v${nodeSourceVersion}.tar.gz`;
const majorVersion = nodeSourceVersion.split('.')[0];
const buildDir = path.join(__dirname, '.node-builds');
// In CI scripts directory, go up one level to find web root
const buildDir = path.join(__dirname, __dirname.endsWith('scripts') ? '..' : '.', '.node-builds');
const versionDir = path.join(buildDir, `node-v${nodeSourceVersion}-minimal`);
const markerFile = path.join(versionDir, '.build-complete');
const customNodePath = path.join(versionDir, 'out', 'Release', 'node');
@ -99,8 +122,16 @@ async function buildCustomNode() {
console.log(`Using cached custom Node.js build from ${customNodePath}`);
const stats = fs.statSync(customNodePath);
console.log(`Cached custom Node.js size: ${(stats.size / 1024 / 1024).toFixed(2)} MB`);
console.log(`\nTo use this custom Node.js with build-native.js:`);
console.log(`node build-native.js --custom-node="${customNodePath}"`);
if (isCI) {
// Set outputs for GitHub Actions
setOutput('node-path', customNodePath);
setOutput('node-size', stats.size);
setOutput('cache-hit', 'true');
} else {
console.log(`\nTo use this custom Node.js with build-native.js:`);
console.log(`node build-native.js --custom-node="${customNodePath}"`);
}
return customNodePath;
}
@ -109,8 +140,8 @@ async function buildCustomNode() {
fs.mkdirSync(buildDir, { recursive: true });
}
// Clean up old version directory if exists
if (fs.existsSync(versionDir)) {
// Clean up incomplete builds (check for marker file)
if (fs.existsSync(versionDir) && !fs.existsSync(markerFile)) {
console.log('Cleaning up incomplete build...');
fs.rmSync(versionDir, { recursive: true, force: true });
}
@ -131,12 +162,14 @@ async function buildCustomNode() {
// Rename to version-specific directory
const extractedDir = path.join(buildDir, `node-v${nodeSourceVersion}`);
fs.renameSync(extractedDir, versionDir);
if (fs.existsSync(extractedDir)) {
fs.renameSync(extractedDir, versionDir);
}
// Configure and build
process.chdir(versionDir);
console.log('Configuring Node.js build (all feature removals, -Os only)...');
console.log('Configuring Node.js build...');
const configureArgs = [
'--without-intl', // Remove internationalization support
'--without-npm', // Don't include npm
@ -144,42 +177,49 @@ async function buildCustomNode() {
'--without-inspector', // Remove debugging/profiling features
'--without-node-code-cache', // Disable code cache
'--without-node-snapshot', // Don't create/use startup snapshot
'--ninja', // Use ninja if available for faster builds
];
// Check if ninja is available
try {
execSync('which ninja', { stdio: 'ignore' });
configureArgs.push('--ninja');
console.log('Using Ninja for faster builds...');
} catch {
console.log('Ninja not found, using Make...');
}
// Enable ccache if available
try {
execSync('which ccache', { stdio: 'ignore' });
process.env.CC = 'ccache gcc';
process.env.CXX = 'ccache g++';
console.log('Using ccache for faster rebuilds...');
} catch {
console.log('ccache not found, proceeding without it...');
}
// Use -Os optimization which is proven to be safe
process.env.CFLAGS = '-Os';
process.env.CXXFLAGS = '-Os';
// Clear LDFLAGS to avoid any issues
delete process.env.LDFLAGS;
// Check if ninja is available, install if not
try {
execSync('which ninja', { stdio: 'ignore' });
console.log('Using Ninja for faster builds...');
} catch {
console.log('Ninja not found, installing via Homebrew...');
try {
execSync('brew install ninja', { stdio: 'inherit' });
console.log('Ninja installed successfully');
} catch (brewError) {
console.log('Failed to install ninja, falling back to Make...');
// Remove --ninja if not available
configureArgs.pop();
}
}
execSync(`./configure ${configureArgs.join(' ')}`, { stdio: 'inherit' });
console.log('Building Node.js (this will take a while)...');
const cores = require('os').cpus().length;
const startTime = Date.now();
// Check if we're using ninja or make
const buildSystem = configureArgs.includes('--ninja') ? 'ninja' : 'make';
if (buildSystem === 'ninja') {
execSync(`ninja -C out/Release -j ${cores}`, { stdio: 'inherit' });
} else {
execSync(`make -j${cores}`, { stdio: 'inherit' });
const buildCmd = configureArgs.includes('--ninja')
? `ninja -C out/Release -j ${cores}`
: `make -j${cores}`;
execSync(buildCmd, { stdio: 'inherit' });
const buildTime = Math.round((Date.now() - startTime) / 1000);
if (isCI) {
console.log(`Build completed in ${buildTime} seconds`);
}
// Verify the build
@ -187,9 +227,14 @@ async function buildCustomNode() {
throw new Error('Node.js build failed - binary not found');
}
// Strip the binary
// Test the binary
const version = execSync(`"${customNodePath}" --version`, { encoding: 'utf8' }).trim();
console.log(`Built Node.js version: ${version}`);
// Strip the binary (different command for Linux vs macOS)
console.log('Stripping Node.js binary...');
execSync(`strip -S "${customNodePath}"`, { stdio: 'inherit' });
const stripCmd = platform === 'darwin' ? 'strip -S' : 'strip -s';
execSync(`${stripCmd} "${customNodePath}"`, { stdio: 'inherit' });
// Check final size
const stats = fs.statSync(customNodePath);
@ -206,31 +251,71 @@ async function buildCustomNode() {
}
// Mark build as complete
fs.writeFileSync(markerFile, JSON.stringify({
const buildInfo = {
version: nodeSourceVersion,
buildDate: new Date().toISOString(),
size: stats.size,
configureArgs: configureArgs
}, null, 2));
platform: platform,
arch: arch,
configureArgs: configureArgs,
buildTime: buildTime
};
fs.writeFileSync(markerFile, JSON.stringify(buildInfo, null, 2));
// Create a summary file
const summaryPath = path.join(versionDir, 'build-summary.txt');
const summary = `
Custom Node.js Build Summary
============================
Version: ${nodeSourceVersion}
Platform: ${platform}-${arch}
Size: ${(stats.size / 1024 / 1024).toFixed(2)} MB
Build Time: ${buildTime} seconds
Configure Args: ${configureArgs.join(' ')}
Path: ${customNodePath}
`;
fs.writeFileSync(summaryPath, summary);
// Change back to original directory
process.chdir(originalCwd);
if (isCI) {
// Set outputs for GitHub Actions
setOutput('node-path', customNodePath);
setOutput('node-size', stats.size);
setOutput('node-version', version);
setOutput('build-time', buildTime);
setOutput('cache-hit', 'false');
}
// Output for both CI and local use
console.log(`\nCustom Node.js location: ${customNodePath}`);
console.log(`\nTo use this custom Node.js with build-native.js:`);
console.log(`To use this custom Node.js with build-native.js:`);
console.log(`node build-native.js --custom-node="${customNodePath}"`);
return customNodePath;
} catch (error) {
process.chdir(originalCwd);
console.error('Failed to build custom Node.js:', error.message);
console.error('Failed to build custom Node.js:', error.message || error);
// Set error output for CI
if (isCI) {
setOutput('build-error', error.message || 'Unknown error');
}
process.exit(1);
}
}
// Run the build
buildCustomNode().catch(err => {
console.error('Build failed:', err);
process.exit(1);
});
// Run the build if called directly
if (require.main === module) {
buildCustomNode().catch(err => {
console.error('Build failed:', err);
process.exit(1);
});
}
// Export for use as a module
module.exports = { buildCustomNode };

View file

@ -13,51 +13,14 @@
* - `pty.node` - Native binding for terminal emulation
* - `spawn-helper` - Helper binary for spawning processes (Unix only)
*
* ## How it works
*
* 1. **Patches node-pty** to work with SEA's limitations:
* - SEA's require() can only load built-in Node.js modules, not external files
* - We patch node-pty to use `process.dlopen()` instead of `require()` for native modules
* - All file lookups are changed to look next to the executable, not in node_modules
*
* 2. **Bundles TypeScript** using esbuild:
* - Compiles and bundles all TypeScript/JavaScript into a single file
* - Includes inline sourcemaps for better debugging
* - Source map support can be enabled with --sourcemap flag
*
* 3. **Creates SEA blob**:
* - Uses Node.js's experimental SEA config to generate a blob from the bundle
* - The blob contains all the JavaScript code and can be injected into a Node binary
*
* 4. **Injects into Node.js binary**:
* - Copies the Node.js executable and injects the SEA blob using postject
* - Signs the binary on macOS to avoid security warnings
*
* ## Portability
* The resulting executable is fully portable:
* - No absolute paths are embedded
* - Native modules are loaded relative to the executable location
* - Can be moved to any directory or machine with the same OS/architecture
*
* ## Usage
* ```bash
* node build-native.js # Build with system Node.js
* node build-native.js --sourcemap # Build with inline sourcemaps
* node build-native.js --custom-node=/path/to/node # Use custom Node.js binary
*
* # Build custom Node.js first:
* node build-custom-node.js # Build minimal Node.js for current version
* node build-custom-node.js --version=24.2.0 # Build specific version
* node build-native.js --custom-node # Auto-discover custom Node.js (uses most recent)
* node build-native.js --custom-node=/path/to/node # Use specific custom Node.js binary
* node build-native.js --custom-node /path/to/node # Alternative syntax
* ```
*
* ## Requirements
* - Node.js 20+ (for SEA support)
* - postject (installed automatically if needed)
*
* ## Known Limitations
* - The SEA warning about require() limitations is expected and harmless
* - Native modules must be distributed alongside the executable
* - Cross-platform builds are not supported (build on the target platform)
*/
const { execSync } = require('child_process');
@ -74,31 +37,12 @@ for (let i = 0; i < process.argv.length; i++) {
if (arg.startsWith('--custom-node=')) {
customNodePath = arg.split('=')[1];
} else if (arg === '--custom-node') {
// Check if next argument is a path
if (i + 1 < process.argv.length && !process.argv[i + 1].startsWith('--')) {
// Next argument is the path
customNodePath = process.argv[i + 1];
} else {
// No path provided, search for custom Node.js build
console.log('Searching for custom Node.js build...');
const customBuildsDir = path.join(__dirname, '.node-builds');
if (fs.existsSync(customBuildsDir)) {
const dirs = fs.readdirSync(customBuildsDir)
.filter(dir => dir.startsWith('node-v') && dir.endsWith('-minimal'))
.map(dir => ({
name: dir,
path: path.join(customBuildsDir, dir, 'out/Release/node'),
mtime: fs.statSync(path.join(customBuildsDir, dir)).mtime
}))
.filter(item => fs.existsSync(item.path))
.sort((a, b) => b.mtime - a.mtime); // Sort by modification time, newest first
if (dirs.length > 0) {
customNodePath = dirs[0].path;
console.log(`Found custom Node.js at: ${customNodePath}`);
} else {
console.log('No custom Node.js builds found in .node-builds/');
}
}
// No path provided, use auto-discovery
customNodePath = 'auto';
}
}
}
@ -116,340 +60,6 @@ if (nodeVersion < 20) {
process.exit(1);
}
function patchNodePty() {
console.log('Preparing node-pty for SEA build...');
// Always reinstall to ensure clean state
console.log('Reinstalling node-pty to ensure clean state...');
execSync('rm -rf node_modules/@homebridge/node-pty-prebuilt-multiarch', { stdio: 'inherit' });
// Suppress npm warnings during installation
execSync('NODE_NO_WARNINGS=1 pnpm install @homebridge/node-pty-prebuilt-multiarch --silent', {
stdio: 'inherit',
env: {
...process.env,
NODE_NO_WARNINGS: '1',
npm_config_loglevel: 'error'
}
});
// Also ensure authenticate-pam is installed
console.log('Ensuring authenticate-pam is installed...');
execSync('pnpm install authenticate-pam --silent 2>/dev/null || pnpm install authenticate-pam --silent', {
stdio: ['inherit', 'inherit', 'pipe'],
shell: true
});
// If using custom Node.js, rebuild native modules
if (customNodePath) {
console.log('Custom Node.js detected - rebuilding native modules...');
// Get versions
const customVersion = execSync(`"${customNodePath}" --version`, { encoding: 'utf8' }).trim();
const systemVersion = process.version;
console.log(`Custom Node.js: ${customVersion}`);
console.log(`System Node.js: ${systemVersion}`);
// Rebuild node-pty with the custom Node using pnpm rebuild
console.log('Rebuilding @homebridge/node-pty-prebuilt-multiarch with custom Node.js...');
try {
// Use system Node to run pnpm, but rebuild for custom Node version
// The key is to use system Node.js to run pnpm (which needs regex support),
// but tell node-gyp to build against the custom Node.js headers
console.log('Using system Node.js to run pnpm for compatibility...');
// First rebuild node-pty which is critical
execSync(`pnpm rebuild @homebridge/node-pty-prebuilt-multiarch`, {
stdio: 'inherit',
env: {
...process.env,
npm_config_runtime: 'node',
npm_config_target: customVersion.substring(1), // Remove 'v' prefix
npm_config_arch: process.arch,
npm_config_target_arch: process.arch,
npm_config_disturl: 'https://nodejs.org/dist',
npm_config_build_from_source: 'true',
CXXFLAGS: '-std=c++20 -stdlib=libc++ -mmacosx-version-min=14.0',
MACOSX_DEPLOYMENT_TARGET: '14.0'
}
});
console.log('node-pty rebuilt successfully');
// Rebuild authenticate-pam (required for authentication)
console.log('Rebuilding authenticate-pam...');
// Create a wrapper script to filter warnings
const wrapperScript = `#!/bin/bash
# Filter out specific warnings while preserving errors
pnpm rebuild authenticate-pam "$@" 2>&1 | grep -v "cast from 'typename" | grep -v "converts to incompatible function type" | grep -v "expanded from macro" | grep -v "~~~" | grep -v "In file included from" | grep -v "warnings generated" | grep -v "In instantiation of" | grep -v "requested here" || true
`;
fs.writeFileSync('build-wrapper.sh', wrapperScript, { mode: 0o755 });
try {
execSync(`./build-wrapper.sh`, {
stdio: 'inherit',
env: {
...process.env,
npm_config_runtime: 'node',
npm_config_target: customVersion.substring(1),
npm_config_arch: process.arch,
npm_config_target_arch: process.arch,
npm_config_disturl: 'https://nodejs.org/dist',
npm_config_build_from_source: 'true',
CXXFLAGS: '-std=c++20 -stdlib=libc++ -mmacosx-version-min=14.0 -Wno-cast-function-type -Wno-incompatible-function-pointer-types',
MACOSX_DEPLOYMENT_TARGET: '14.0'
}
});
console.log('authenticate-pam rebuilt successfully');
} finally {
// Clean up wrapper script
if (fs.existsSync('build-wrapper.sh')) {
fs.unlinkSync('build-wrapper.sh');
}
}
console.log('Native modules rebuilt successfully with custom Node.js');
} catch (error) {
console.error('Failed to rebuild native module:', error.message);
console.error('Trying alternative rebuild method...');
// Alternative: Force reinstall and rebuild
try {
console.log('Forcing reinstall and rebuild...');
execSync(`rm -rf node_modules/@homebridge/node-pty-prebuilt-multiarch`, { stdio: 'inherit' });
execSync(`rm -rf node_modules/authenticate-pam`, { stdio: 'inherit' });
// First install the packages
execSync(`pnpm install @homebridge/node-pty-prebuilt-multiarch authenticate-pam --force`, { stdio: 'inherit' });
// Then rebuild them with custom Node settings
// Create a wrapper script to filter warnings
const rebuildWrapperScript = `#!/bin/bash
# Filter out specific warnings while preserving errors
pnpm rebuild @homebridge/node-pty-prebuilt-multiarch authenticate-pam "$@" 2>&1 | grep -v "cast from 'typename" | grep -v "converts to incompatible function type" | grep -v "expanded from macro" | grep -v "~~~" | grep -v "In file included from" | grep -v "warnings generated" | grep -v "In instantiation of" | grep -v "requested here" || true
`;
fs.writeFileSync('rebuild-wrapper.sh', rebuildWrapperScript, { mode: 0o755 });
try {
execSync(`./rebuild-wrapper.sh`, {
stdio: 'inherit',
env: {
...process.env,
npm_config_runtime: 'node',
npm_config_target: customVersion.substring(1),
npm_config_arch: process.arch,
npm_config_target_arch: process.arch,
npm_config_disturl: 'https://nodejs.org/dist',
CXXFLAGS: '-std=c++20 -stdlib=libc++ -mmacosx-version-min=14.0 -Wno-cast-function-type -Wno-incompatible-function-pointer-types',
MACOSX_DEPLOYMENT_TARGET: '14.0'
}
});
} finally {
// Clean up wrapper script
if (fs.existsSync('rebuild-wrapper.sh')) {
fs.unlinkSync('rebuild-wrapper.sh');
}
}
console.log('Native module rebuilt from source successfully');
} catch (error2) {
console.error('Alternative rebuild also failed:', error2.message);
process.exit(1);
}
}
}
console.log('Patching node-pty for SEA build...');
// Marker to detect if files have been patched
const PATCH_MARKER = '/* VIBETUNNEL_SEA_PATCHED */';
// Helper function to check if file is already patched
function isFilePatched(filePath) {
if (!fs.existsSync(filePath)) return false;
const content = fs.readFileSync(filePath, 'utf8');
return content.includes(PATCH_MARKER);
}
// Patch prebuild-loader.js to use process.dlopen instead of require
const prebuildLoaderFile = path.join(__dirname, 'node_modules/@homebridge/node-pty-prebuilt-multiarch/lib/prebuild-loader.js');
const prebuildLoaderContent = `"use strict";
${PATCH_MARKER}
Object.defineProperty(exports, "__esModule", { value: true });
var path = require("path");
var fs = require("fs");
// Custom loader for SEA that uses process.dlopen
var pty;
// Helper function to load native module using dlopen
function loadNativeModule(modulePath) {
const module = { exports: {} };
process.dlopen(module, modulePath);
return module.exports;
}
// Determine the path to pty.node
function getPtyPath() {
const execDir = path.dirname(process.execPath);
// Look for pty.node next to the executable first
const ptyPath = path.join(execDir, 'pty.node');
if (fs.existsSync(ptyPath)) {
return ptyPath;
}
// If not found, throw error with helpful message
throw new Error('Could not find pty.node next to executable at: ' + ptyPath);
}
try {
const ptyPath = getPtyPath();
// Set spawn-helper path for Unix systems
if (process.platform !== 'win32') {
const execDir = path.dirname(process.execPath);
const spawnHelperPath = path.join(execDir, 'spawn-helper');
if (fs.existsSync(spawnHelperPath)) {
process.env.NODE_PTY_SPAWN_HELPER_PATH = spawnHelperPath;
}
}
pty = loadNativeModule(ptyPath);
} catch (error) {
console.error('Failed to load pty.node:', error);
throw error;
}
exports.default = pty;
//# sourceMappingURL=prebuild-loader.js.map`;
if (isFilePatched(prebuildLoaderFile)) {
console.log('prebuild-loader.js is already patched, skipping...');
} else {
fs.writeFileSync(prebuildLoaderFile, prebuildLoaderContent.trimEnd() + '\n');
console.log('Patched prebuild-loader.js');
}
// Also patch windowsPtyAgent.js if it exists
const windowsPtyAgentFile = path.join(__dirname, 'node_modules/@homebridge/node-pty-prebuilt-multiarch/lib/windowsPtyAgent.js');
if (fs.existsSync(windowsPtyAgentFile)) {
if (isFilePatched(windowsPtyAgentFile)) {
console.log('windowsPtyAgent.js is already patched, skipping...');
} else {
let content = fs.readFileSync(windowsPtyAgentFile, 'utf8');
// Add patch marker at the beginning
content = `${PATCH_MARKER}\n` + content;
// Replace direct require of .node files with our loader
content = content.replace(
/require\(['"]\.\.\/build\/Release\/pty\.node['"]\)/g,
"require('./prebuild-loader').default"
);
fs.writeFileSync(windowsPtyAgentFile, content.trimEnd() + '\n');
console.log('Patched windowsPtyAgent.js');
}
}
// Patch index.js exports.native line
const indexFile = path.join(__dirname, 'node_modules/@homebridge/node-pty-prebuilt-multiarch/lib/index.js');
if (fs.existsSync(indexFile)) {
if (isFilePatched(indexFile)) {
console.log('index.js is already patched, skipping...');
} else {
let content = fs.readFileSync(indexFile, 'utf8');
// Add patch marker at the beginning
content = `${PATCH_MARKER}\n` + content;
// Replace the exports.native line that directly requires .node
content = content.replace(
/exports\.native = \(process\.platform !== 'win32' \? require\(prebuild_file_path_1\.ptyPath \|\| '\.\.\/build\/Release\/pty\.node'\) : null\);/,
"exports.native = (process.platform !== 'win32' ? require('./prebuild-loader').default : null);"
);
fs.writeFileSync(indexFile, content.trimEnd() + '\n');
console.log('Patched index.js');
}
}
// Patch unixTerminal.js to fix spawn-helper path resolution
const unixTerminalFile = path.join(__dirname, 'node_modules/@homebridge/node-pty-prebuilt-multiarch/lib/unixTerminal.js');
if (fs.existsSync(unixTerminalFile)) {
if (isFilePatched(unixTerminalFile)) {
console.log('unixTerminal.js is already patched, skipping...');
} else {
let content = fs.readFileSync(unixTerminalFile, 'utf8');
// Add patch marker at the beginning
content = `${PATCH_MARKER}\n` + content;
// Replace the helperPath resolution logic
const helperPathPatch = `var helperPath;
// For SEA, use spawn-helper from environment or next to executable
if (process.env.NODE_PTY_SPAWN_HELPER_PATH) {
helperPath = process.env.NODE_PTY_SPAWN_HELPER_PATH;
} else {
// In SEA context, look next to the executable
const execDir = path.dirname(process.execPath);
const spawnHelperPath = path.join(execDir, 'spawn-helper');
if (require('fs').existsSync(spawnHelperPath)) {
helperPath = spawnHelperPath;
} else {
// Fallback to original logic
helperPath = '../build/Release/spawn-helper';
helperPath = path.resolve(__dirname, helperPath);
helperPath = helperPath.replace('app.asar', 'app.asar.unpacked');
helperPath = helperPath.replace('node_modules.asar', 'node_modules.asar.unpacked');
}
}`;
// Find and replace the helperPath section
content = content.replace(
/var helperPath;[\s\S]*?helperPath = helperPath\.replace\('node_modules\.asar', 'node_modules\.asar\.unpacked'\);/m,
helperPathPatch
);
fs.writeFileSync(unixTerminalFile, content.trimEnd() + '\n');
console.log('Patched unixTerminal.js');
}
}
console.log('node-pty patching complete.');
}
// Function to clean patches from node-pty
function cleanPatches() {
console.log('Cleaning patches from node-pty...');
const filesToClean = [
'node_modules/@homebridge/node-pty-prebuilt-multiarch/lib/prebuild-loader.js',
'node_modules/@homebridge/node-pty-prebuilt-multiarch/lib/windowsPtyAgent.js',
'node_modules/@homebridge/node-pty-prebuilt-multiarch/lib/index.js',
'node_modules/@homebridge/node-pty-prebuilt-multiarch/lib/unixTerminal.js'
];
filesToClean.forEach(file => {
const filePath = path.join(__dirname, file);
if (fs.existsSync(filePath)) {
try {
fs.unlinkSync(filePath);
console.log(`Removed patched file: ${file}`);
} catch (err) {
console.error(`Failed to remove ${file}:`, err.message);
}
}
});
// Run pnpm install to restore original files
console.log('Running pnpm install to restore original files...');
try {
execSync('pnpm install @homebridge/node-pty-prebuilt-multiarch --force', {
cwd: __dirname,
stdio: 'inherit'
});
console.log('Original files restored.');
} catch (err) {
console.error('Failed to restore original files:', err.message);
}
}
// Cleanup function
function cleanup() {
if (fs.existsSync('build') && !process.argv.includes('--keep-build')) {
@ -469,34 +79,173 @@ process.on('SIGTERM', () => {
process.exit(1);
});
function applyMinimalPatches() {
console.log('Applying minimal SEA patches to node-pty...');
// Create sea-loader.js
const seaLoaderPath = path.join(__dirname, 'node_modules/node-pty/lib/sea-loader.js');
if (!fs.existsSync(seaLoaderPath)) {
const seaLoaderContent = `"use strict";
/* VIBETUNNEL_SEA_LOADER */
Object.defineProperty(exports, "__esModule", { value: true });
var path = require("path");
var fs = require("fs");
// Custom loader for SEA that uses process.dlopen
var pty;
// Helper function to load native module using dlopen
function loadNativeModule(modulePath) {
const module = { exports: {} };
process.dlopen(module, modulePath);
return module.exports;
}
// Determine the path to pty.node
function getPtyPath() {
const execDir = path.dirname(process.execPath);
// Look for pty.node next to the executable first
const ptyPath = path.join(execDir, 'pty.node');
if (fs.existsSync(ptyPath)) {
// Add path validation for security
const resolvedPath = path.resolve(ptyPath);
const resolvedExecDir = path.resolve(execDir);
if (!resolvedPath.startsWith(resolvedExecDir)) {
throw new Error('Invalid pty.node path detected');
}
return ptyPath;
}
// If not found, throw error with helpful message
throw new Error('Could not find pty.node next to executable at: ' + ptyPath);
}
try {
const ptyPath = getPtyPath();
// Set spawn-helper path for macOS only
// Linux uses forkpty() directly and doesn't need spawn-helper
if (process.platform === 'darwin') {
const execDir = path.dirname(process.execPath);
const spawnHelperPath = path.join(execDir, 'spawn-helper');
if (fs.existsSync(spawnHelperPath)) {
process.env.NODE_PTY_SPAWN_HELPER_PATH = spawnHelperPath;
}
}
pty = loadNativeModule(ptyPath);
} catch (error) {
console.error('Failed to load pty.node:', error);
throw error;
}
exports.default = pty;
`;
fs.writeFileSync(seaLoaderPath, seaLoaderContent);
}
// Patch index.js
const indexPath = path.join(__dirname, 'node_modules/node-pty/lib/index.js');
if (fs.existsSync(indexPath)) {
let content = fs.readFileSync(indexPath, 'utf8');
if (!content.includes('VIBETUNNEL_SEA')) {
content = content.replace(
"exports.native = (process.platform !== 'win32' ? require('../build/Release/pty.node') : null);",
"exports.native = (process.platform !== 'win32' ? (process.env.VIBETUNNEL_SEA ? require('./sea-loader').default : require('../build/Release/pty.node')) : null);"
);
fs.writeFileSync(indexPath, content);
}
}
// Patch unixTerminal.js
const unixPath = path.join(__dirname, 'node_modules/node-pty/lib/unixTerminal.js');
if (fs.existsSync(unixPath)) {
let content = fs.readFileSync(unixPath, 'utf8');
if (!content.includes('VIBETUNNEL_SEA')) {
// Find and replace the pty loading section
const startMarker = 'var pty;\nvar helperPath;';
const endMarker = 'var DEFAULT_FILE = \'sh\';';
const startIdx = content.indexOf(startMarker);
const endIdx = content.indexOf(endMarker);
if (startIdx !== -1 && endIdx !== -1) {
const newSection = `var pty;
var helperPath;
// For SEA, check environment variables
if (process.env.VIBETUNNEL_SEA) {
pty = require('./sea-loader').default;
// In SEA context, look for spawn-helper on macOS only (Linux doesn't use it)
if (process.platform === 'darwin') {
const execDir = path.dirname(process.execPath);
const spawnHelperPath = path.join(execDir, 'spawn-helper');
if (require('fs').existsSync(spawnHelperPath)) {
helperPath = spawnHelperPath;
} else if (process.env.NODE_PTY_SPAWN_HELPER_PATH) {
helperPath = process.env.NODE_PTY_SPAWN_HELPER_PATH;
}
}
// On Linux, helperPath remains undefined which is fine
} else {
// Original loading logic
try {
pty = require('../build/Release/pty.node');
helperPath = '../build/Release/spawn-helper';
}
catch (outerError) {
try {
pty = require('../build/Debug/pty.node');
helperPath = '../build/Debug/spawn-helper';
}
catch (innerError) {
console.error('innerError', innerError);
// Re-throw the exception from the Release require if the Debug require fails as well
throw outerError;
}
}
helperPath = path.resolve(__dirname, helperPath);
helperPath = helperPath.replace('app.asar', 'app.asar.unpacked');
helperPath = helperPath.replace('node_modules.asar', 'node_modules.asar.unpacked');
}
`;
content = content.substring(0, startIdx) + newSection + content.substring(endIdx);
fs.writeFileSync(unixPath, content);
}
}
}
console.log('SEA patches applied successfully');
}
async function main() {
try {
// Set up environment to suppress warnings
process.env.NODE_NO_WARNINGS = '1';
process.env.npm_config_loglevel = 'error';
// Apply minimal patches to node-pty
applyMinimalPatches();
// Handle command line arguments
if (process.argv.includes('--help')) {
console.log('VibeTunnel Native Build Script\n');
console.log('Usage: node build-native.js [options]\n');
console.log('Options:');
console.log(' --help Show this help message');
console.log(' --clean-patches Remove all patches from node-pty and restore original files');
console.log(' --force-patch Force re-patching even if files are already patched');
console.log(' --keep-build Keep the build directory after completion');
console.log(' --node <path> Use a custom Node.js binary for SEA\n');
process.exit(0);
// Ensure native modules are built (in case postinstall didn't run)
const nativePtyDir = 'node_modules/node-pty/build/Release';
const nativeAuthDir = 'node_modules/authenticate-pam/build/Release';
if (!fs.existsSync(nativePtyDir)) {
console.log('Building node-pty native module...');
// Find the actual node-pty path (could be in .pnpm directory)
const nodePtyPath = require.resolve('node-pty/package.json');
const nodePtyDir = path.dirname(nodePtyPath);
console.log(`Found node-pty at: ${nodePtyDir}`);
// Build node-pty using node-gyp directly to avoid TypeScript compilation
execSync(`cd "${nodePtyDir}" && npx node-gyp rebuild`, {
stdio: 'inherit',
shell: true
});
}
if (process.argv.includes('--clean-patches')) {
cleanPatches();
process.exit(0);
}
const forcePatching = process.argv.includes('--force-patch');
if (forcePatching) {
console.log('Force patching enabled - will re-patch even if already patched');
cleanPatches();
if (!fs.existsSync(nativeAuthDir)) {
console.log('Building authenticate-pam native module...');
execSync('npm rebuild authenticate-pam', {
stdio: 'inherit',
cwd: __dirname
});
}
// Create build directory
@ -512,11 +261,52 @@ async function main() {
// 0. Determine which Node.js to use
let nodeExe = process.execPath;
if (customNodePath) {
// Validate custom node exists
if (!fs.existsSync(customNodePath)) {
console.error(`Error: Custom Node.js not found at ${customNodePath}`);
console.error('Build one using: node build-custom-node.js');
process.exit(1);
if (customNodePath === 'auto') {
// Auto-discover custom Node.js build
const buildDir = path.join(__dirname, '.node-builds');
if (fs.existsSync(buildDir)) {
// Find the most recent custom Node.js build
const builds = fs.readdirSync(buildDir)
.filter(name => name.startsWith('node-v') && name.endsWith('-minimal'))
.map(name => {
const nodePath = path.join(buildDir, name, 'out', 'Release', 'node');
if (fs.existsSync(nodePath)) {
const match = name.match(/node-v(.+)-minimal/);
if (!match || !match[1]) {
console.warn(`Warning: Skipping directory with invalid name format: ${name}`);
return null;
}
return {
path: nodePath,
version: match[1],
mtime: fs.statSync(nodePath).mtime
};
}
return null;
})
.filter(Boolean)
.sort((a, b) => b.mtime - a.mtime);
if (builds.length > 0) {
customNodePath = builds[0].path;
console.log(`Auto-discovered custom Node.js v${builds[0].version} at ${customNodePath}`);
} else {
console.error('Error: No custom Node.js builds found in .node-builds/');
console.error('Build one using: node build-custom-node.js');
process.exit(1);
}
} else {
console.error('Error: No .node-builds directory found');
console.error('Build a custom Node.js using: node build-custom-node.js');
process.exit(1);
}
} else {
// Validate custom node exists at specified path
if (!fs.existsSync(customNodePath)) {
console.error(`Error: Custom Node.js not found at ${customNodePath}`);
console.error('Build one using: node build-custom-node.js');
process.exit(1);
}
}
nodeExe = customNodePath;
}
@ -525,26 +315,35 @@ async function main() {
const nodeStats = fs.statSync(nodeExe);
console.log(`Node.js binary size: ${(nodeStats.size / 1024 / 1024).toFixed(2)} MB`);
// Get version of the Node.js we're using
// 1. Rebuild native modules if using custom Node.js
if (customNodePath) {
try {
const customVersion = execSync(`"${nodeExe}" --version`, { encoding: 'utf8' }).trim();
console.log(`Custom Node.js version: ${customVersion}`);
console.log('This minimal build excludes intl, npm, inspector, and other unused features.');
} catch (e) {
console.log('Could not determine custom Node.js version');
}
console.log('\nCustom Node.js detected - rebuilding native modules...');
const customVersion = execSync(`"${nodeExe}" --version`, { encoding: 'utf8' }).trim();
console.log(`Custom Node.js version: ${customVersion}`);
execSync(`pnpm rebuild node-pty authenticate-pam`, {
stdio: 'inherit',
env: {
...process.env,
npm_config_runtime: 'node',
npm_config_target: customVersion.substring(1), // Remove 'v' prefix
npm_config_arch: process.arch,
npm_config_target_arch: process.arch,
npm_config_disturl: 'https://nodejs.org/dist',
npm_config_build_from_source: 'true',
// Node.js 24 requires C++20
CXXFLAGS: '-std=c++20',
npm_config_cxxflags: '-std=c++20'
}
});
}
// 1. Patch node-pty
patchNodePty();
// 2. Bundle TypeScript with esbuild using custom loader
// 2. Bundle TypeScript with esbuild
console.log('\nBundling TypeScript with esbuild...');
// Use deterministic timestamps based on git commit or source
let buildDate;
let buildTimestamp;
let buildDate = new Date().toISOString();
let buildTimestamp = Date.now();
try {
// Try to use the last commit date for reproducible builds
@ -553,20 +352,10 @@ async function main() {
buildTimestamp = new Date(gitDate).getTime();
console.log(`Using git commit date for reproducible build: ${buildDate}`);
} catch (e) {
// Fallback to SOURCE_DATE_EPOCH if set (for reproducible builds)
if (process.env.SOURCE_DATE_EPOCH) {
buildTimestamp = parseInt(process.env.SOURCE_DATE_EPOCH) * 1000;
buildDate = new Date(buildTimestamp).toISOString();
console.log(`Using SOURCE_DATE_EPOCH for reproducible build: ${buildDate}`);
} else {
// Only use current time as last resort
buildDate = new Date().toISOString();
buildTimestamp = Date.now();
console.warn('Warning: Using current time for build - output will not be reproducible');
}
// Fallback to current time
console.warn('Warning: Using current time for build - output will not be reproducible');
}
// Use esbuild directly without custom loader since we're patching node-pty
let esbuildCmd = `NODE_NO_WARNINGS=1 npx esbuild src/cli.ts \\
--bundle \\
--platform=node \\
@ -575,15 +364,17 @@ async function main() {
--format=cjs \\
--keep-names \\
--external:authenticate-pam \\
--external:../build/Release/pty.node \\
--external:./build/Release/pty.node \\
--define:process.env.BUILD_DATE='"${buildDate}"' \\
--define:process.env.BUILD_TIMESTAMP='"${buildTimestamp}"'`;
--define:process.env.BUILD_TIMESTAMP='"${buildTimestamp}"' \\
--define:process.env.VIBETUNNEL_SEA='"true"'`;
// Also inject git commit hash for version tracking
try {
const gitCommit = execSync('git rev-parse --short HEAD', { encoding: 'utf8' }).trim();
esbuildCmd += ` \\\n --define:process.env.GIT_COMMIT='"${gitCommit}"'`;
} catch (e) {
// Not in a git repo or git not available
esbuildCmd += ` \\\n --define:process.env.GIT_COMMIT='"unknown"'`;
}
@ -600,7 +391,30 @@ async function main() {
}
});
// 2. Create SEA configuration
// 2b. Post-process bundle to ensure VIBETUNNEL_SEA is properly set
console.log('\nPost-processing bundle for SEA compatibility...');
let bundleContent = fs.readFileSync('build/bundle.js', 'utf8');
// Remove shebang line if present (not valid in SEA bundles)
if (bundleContent.startsWith('#!')) {
bundleContent = bundleContent.substring(bundleContent.indexOf('\n') + 1);
}
// Add VIBETUNNEL_SEA environment variable at the top of the bundle
// This ensures the patched node-pty knows it's running in SEA mode
const seaEnvSetup = `// Set VIBETUNNEL_SEA environment variable for SEA mode
if (typeof process !== 'undefined' && process.versions && process.versions.node) {
process.env.VIBETUNNEL_SEA = 'true';
}
`;
bundleContent = seaEnvSetup + bundleContent;
fs.writeFileSync('build/bundle.js', bundleContent);
console.log('Bundle post-processing complete');
// 3. Create SEA configuration
console.log('\nCreating SEA configuration...');
const seaConfig = {
main: 'build/bundle.js',
@ -612,11 +426,11 @@ async function main() {
fs.writeFileSync('build/sea-config.json', JSON.stringify(seaConfig, null, 2));
// 3. Generate SEA blob
// 4. Generate SEA blob
console.log('Generating SEA blob...');
execSync('node --experimental-sea-config build/sea-config.json', { stdio: 'inherit' });
// 4. Create executable
// 5. Create executable
console.log('\nCreating executable...');
const targetExe = process.platform === 'win32' ? 'native/vibetunnel.exe' : 'native/vibetunnel';
@ -626,7 +440,7 @@ async function main() {
fs.chmodSync(targetExe, 0o755);
}
// 5. Inject the blob
// 6. Inject the blob
console.log('Injecting SEA blob...');
let postjectCmd = `npx postject ${targetExe} NODE_SEA_BLOB build/sea-prep.blob \\
--sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2`;
@ -637,16 +451,14 @@ async function main() {
execSync(postjectCmd, { stdio: 'inherit' });
// 6. Strip the executable first (before signing)
// 7. Strip the executable first (before signing)
console.log('Stripping final executable...');
// Note: This will show a warning about invalidating code signature, which is expected
// since we're modifying a signed Node.js binary. We'll re-sign it in the next step.
execSync(`strip -S ${targetExe} 2>&1 | grep -v "warning: changes being made" || true`, {
stdio: 'inherit',
shell: true
});
// 7. Sign on macOS (after stripping)
// 8. Sign on macOS (after stripping)
if (process.platform === 'darwin') {
console.log('Signing executable...');
execSync(`codesign --sign - ${targetExe}`, { stdio: 'inherit' });
@ -657,9 +469,13 @@ async function main() {
console.log(`Final executable size: ${(finalStats.size / 1024 / 1024).toFixed(2)} MB`);
console.log(`Size reduction: ${((nodeStats.size - finalStats.size) / 1024 / 1024).toFixed(2)} MB`);
// 8. Copy native modules BEFORE restoring (to preserve custom-built versions)
console.log('Copying native modules...');
const nativeModulesDir = 'node_modules/@homebridge/node-pty-prebuilt-multiarch/build/Release';
// 9. Copy native modules
console.log('\nCopying native modules...');
// Find the actual node-pty build directory (could be in .pnpm directory)
const nodePtyPath = require.resolve('node-pty/package.json');
const nodePtyBaseDir = path.dirname(nodePtyPath);
const nativeModulesDir = path.join(nodePtyBaseDir, 'build/Release');
// Check if native modules exist
if (!fs.existsSync(nativeModulesDir)) {
@ -677,8 +493,10 @@ async function main() {
fs.copyFileSync(ptyNodePath, 'native/pty.node');
console.log(' - Copied pty.node');
// Copy spawn-helper (Unix only)
if (process.platform !== 'win32') {
// Copy spawn-helper (macOS only)
// Note: spawn-helper is only built and required on macOS where it's used for pty_posix_spawn()
// On Linux, node-pty uses forkpty() directly and doesn't need spawn-helper
if (process.platform === 'darwin') {
const spawnHelperPath = path.join(nativeModulesDir, 'spawn-helper');
if (!fs.existsSync(spawnHelperPath)) {
console.error('Error: spawn-helper not found. Native module build may have failed.');
@ -699,18 +517,14 @@ async function main() {
process.exit(1);
}
// 9. Restore original node-pty (AFTER copying the custom-built version)
console.log('\nRestoring original node-pty for development...');
execSync('rm -rf node_modules/@homebridge/node-pty-prebuilt-multiarch', { stdio: 'inherit' });
execSync('pnpm install @homebridge/node-pty-prebuilt-multiarch --silent', { stdio: 'inherit' });
console.log('\n✅ Build complete!');
console.log(`\nPortable executable created in native/ directory:`);
console.log(` - vibetunnel (executable)`);
console.log(` - pty.node`);
if (process.platform !== 'win32') {
if (process.platform === 'darwin') {
console.log(` - spawn-helper`);
}
console.log(` - authenticate_pam.node`);
console.log('\nAll files must be kept together in the same directory.');
console.log('This bundle will work on any machine with the same OS/architecture.');

View file

@ -5,8 +5,8 @@
* Tests PTY spawning, terminal raw mode, stdin/stdout forwarding
*/
import * as pty from '@homebridge/node-pty-prebuilt-multiarch';
import { which } from '@homebridge/node-pty-prebuilt-multiarch/lib/utils';
import * as pty from 'node-pty';
import { which } from 'node-pty/lib/utils';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';

View file

@ -16,8 +16,9 @@
"lint:fix": "biome check src --write",
"lint:biome": "biome check src",
"typecheck": "concurrently -n server,client,sw \"tsc --noEmit --project tsconfig.server.json\" \"tsc --noEmit --project tsconfig.client.json\" \"tsc --noEmit --project tsconfig.sw.json\"",
"pretest": "node scripts/ensure-native-modules.js",
"test": "vitest",
"test:ci": "vitest run --reporter=verbose",
"test:ci": "npm run pretest && vitest run --reporter=verbose",
"test:coverage": "vitest run --coverage",
"test:client": "vitest run --mode=client",
"test:server": "vitest run --mode=server",
@ -31,7 +32,6 @@
},
"pnpm": {
"onlyBuiltDependencies": [
"@homebridge/node-pty-prebuilt-multiarch",
"authenticate-pam",
"esbuild",
"puppeteer"
@ -48,7 +48,6 @@
"@codemirror/state": "^6.4.1",
"@codemirror/theme-one-dark": "^6.1.2",
"@codemirror/view": "^6.28.0",
"@homebridge/node-pty-prebuilt-multiarch": "^0.12.0",
"@xterm/headless": "^5.5.0",
"authenticate-pam": "^1.0.5",
"chalk": "^4.1.2",
@ -57,6 +56,7 @@
"lit": "^3.3.0",
"mime-types": "^3.0.1",
"monaco-editor": "^0.52.2",
"node-pty": "github:microsoft/node-pty#v1.1.0-beta34",
"postject": "^1.0.0-alpha.6",
"signal-exit": "^4.1.0",
"web-push": "^3.6.7",

View file

@ -38,9 +38,6 @@ importers:
'@codemirror/view':
specifier: ^6.28.0
version: 6.37.2
'@homebridge/node-pty-prebuilt-multiarch':
specifier: ^0.12.0
version: 0.12.0
'@xterm/headless':
specifier: ^5.5.0
version: 5.5.0
@ -65,6 +62,9 @@ importers:
monaco-editor:
specifier: ^0.52.2
version: 0.52.2
node-pty:
specifier: github:microsoft/node-pty#v1.1.0-beta34
version: https://codeload.github.com/microsoft/node-pty/tar.gz/d738123f1faf7287513b0df8b9e327be54702e94
postject:
specifier: ^1.0.0-alpha.6
version: 1.0.0-alpha.6
@ -468,9 +468,6 @@ packages:
'@hapi/bourne@3.0.0':
resolution: {integrity: sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w==}
'@homebridge/node-pty-prebuilt-multiarch@0.12.0':
resolution: {integrity: sha512-hJCGcfOnMeRh2KUdWPlVN/1egnfqI4yxgpDhqHSkF2DLn5fiJNdjEHHlcM1K2w9+QBmRE2D/wfmM4zUOb8aMyQ==}
'@isaacs/cliui@8.0.2':
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
engines: {node: '>=12'}
@ -1141,9 +1138,6 @@ packages:
bare-events:
optional: true
base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
basic-ftp@5.0.5:
resolution: {integrity: sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==}
engines: {node: '>=10.0.0'}
@ -1152,9 +1146,6 @@ packages:
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
engines: {node: '>=8'}
bl@4.1.0:
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
bn.js@4.12.2:
resolution: {integrity: sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==}
@ -1180,9 +1171,6 @@ packages:
buffer-equal-constant-time@1.0.1:
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
buffer@5.7.1:
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
bytes@3.1.2:
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
engines: {node: '>= 0.8'}
@ -1246,9 +1234,6 @@ packages:
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
engines: {node: '>= 14.16.0'}
chownr@1.1.4:
resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
chromium-bidi@5.1.0:
resolution: {integrity: sha512-9MSRhWRVoRPDG0TgzkHrshFSJJNZzfY5UFqUMuksg7zL1yoZIZ3jLB0YAgHclbiAxPI86pBnwDX1tbzoiV8aFw==}
peerDependencies:
@ -1396,10 +1381,6 @@ packages:
resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
engines: {node: '>=0.10.0'}
decompress-response@6.0.0:
resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
engines: {node: '>=10'}
deep-eql@5.0.2:
resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
engines: {node: '>=6'}
@ -1407,10 +1388,6 @@ packages:
deep-equal@1.0.1:
resolution: {integrity: sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==}
deep-extend@0.6.0:
resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==}
engines: {node: '>=4.0.0'}
default-gateway@6.0.3:
resolution: {integrity: sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==}
engines: {node: '>= 10'}
@ -1450,10 +1427,6 @@ packages:
resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==}
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
detect-libc@2.0.4:
resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==}
engines: {node: '>=8'}
devtools-protocol@0.0.1452169:
resolution: {integrity: sha512-FOFDVMGrAUNp0dDKsAU1TorWJUx2JOU1k9xdgBKKJF3IBh/Uhl2yswG5r3TEAOrCiGY2QRp1e6LVDQrCsTKO4g==}
@ -1579,10 +1552,6 @@ packages:
resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
engines: {node: '>=10'}
expand-template@2.0.3:
resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==}
engines: {node: '>=6'}
expect-type@1.2.1:
resolution: {integrity: sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==}
engines: {node: '>=12.0.0'}
@ -1669,9 +1638,6 @@ packages:
resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
engines: {node: '>= 0.6'}
fs-constants@1.0.0:
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@ -1707,9 +1673,6 @@ packages:
resolution: {integrity: sha512-E1b1lFFLvLgak2whF2xDBcOy6NLVGZBqqjJjsIhvopKfWWEi64pLVTWWehV8KlLerZkfNTA95sTe2OdJKm1OzQ==}
engines: {node: '>= 14'}
github-from-package@0.0.0:
resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==}
glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'}
@ -1789,9 +1752,6 @@ packages:
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
engines: {node: '>=0.10.0'}
ieee754@1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
ignore@5.3.2:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'}
@ -1810,9 +1770,6 @@ packages:
inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
ini@1.3.8:
resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
internal-ip@6.2.0:
resolution: {integrity: sha512-D8WGsR6yDt8uq7vDMu7mjcR+yRMm3dW8yufyChmszWRjcSHuxLBkR3GdS2HZAjodsaGuCvXeEJpueisXJULghg==}
engines: {node: '>=10'}
@ -2111,10 +2068,6 @@ packages:
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
engines: {node: '>=6'}
mimic-response@3.1.0:
resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==}
engines: {node: '>=10'}
minimalistic-assert@1.0.1:
resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==}
@ -2132,9 +2085,6 @@ packages:
mitt@3.0.1:
resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
mkdirp-classic@0.5.3:
resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==}
mkdirp@1.0.4:
resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==}
engines: {node: '>=10'}
@ -2167,9 +2117,6 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
napi-build-utils@2.0.0:
resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==}
negotiator@0.6.3:
resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
engines: {node: '>= 0.6'}
@ -2178,10 +2125,6 @@ packages:
resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==}
engines: {node: '>= 0.4.0'}
node-abi@3.75.0:
resolution: {integrity: sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg==}
engines: {node: '>=10'}
node-addon-api@7.1.1:
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
@ -2194,6 +2137,10 @@ packages:
resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
node-pty@https://codeload.github.com/microsoft/node-pty/tar.gz/d738123f1faf7287513b0df8b9e327be54702e94:
resolution: {tarball: https://codeload.github.com/microsoft/node-pty/tar.gz/d738123f1faf7287513b0df8b9e327be54702e94}
version: 1.0.0
node-releases@2.0.19:
resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
@ -2394,11 +2341,6 @@ packages:
engines: {node: '>=14.0.0'}
hasBin: true
prebuild-install@7.1.3:
resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==}
engines: {node: '>=10'}
hasBin: true
prettier@3.6.1:
resolution: {integrity: sha512-5xGWRa90Sp2+x1dQtNpIpeOQpTDBs9cZDmA/qs2vDNN2i18PdapqY7CmBeyLlMuGqXJRIOPaCaVZTLNQRWUH/A==}
engines: {node: '>=14'}
@ -2454,20 +2396,12 @@ packages:
resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==}
engines: {node: '>= 0.8'}
rc@1.2.8:
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
hasBin: true
react-is@17.0.2:
resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
read-cache@1.0.0:
resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==}
readable-stream@3.6.2:
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
engines: {node: '>= 6'}
readdirp@3.6.0:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
engines: {node: '>=8.10.0'}
@ -2588,12 +2522,6 @@ packages:
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
engines: {node: '>=14'}
simple-concat@1.0.1:
resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==}
simple-get@4.0.1:
resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==}
sirv@3.0.1:
resolution: {integrity: sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==}
engines: {node: '>=18'}
@ -2662,9 +2590,6 @@ packages:
resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
engines: {node: '>=12'}
string_decoder@1.3.0:
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
strip-ansi@5.2.0:
resolution: {integrity: sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==}
engines: {node: '>=6'}
@ -2681,10 +2606,6 @@ packages:
resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==}
engines: {node: '>=6'}
strip-json-comments@2.0.1:
resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==}
engines: {node: '>=0.10.0'}
strip-literal@3.0.0:
resolution: {integrity: sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==}
@ -2721,16 +2642,9 @@ packages:
engines: {node: '>=14.0.0'}
hasBin: true
tar-fs@2.1.3:
resolution: {integrity: sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==}
tar-fs@3.0.10:
resolution: {integrity: sha512-C1SwlQGNLe/jPNqapK8epDsXME7CAJR5RL3GcE6KWx1d9OUByzoHVcbu1VPI8tevg9H8Alae0AApHHFGzrD5zA==}
tar-stream@2.2.0:
resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
engines: {node: '>=6'}
tar-stream@3.1.7:
resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==}
@ -2801,9 +2715,6 @@ packages:
engines: {node: '>=18.0.0'}
hasBin: true
tunnel-agent@0.6.0:
resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
type-fest@0.21.3:
resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==}
engines: {node: '>=10'}
@ -3299,11 +3210,6 @@ snapshots:
'@hapi/bourne@3.0.0': {}
'@homebridge/node-pty-prebuilt-multiarch@0.12.0':
dependencies:
node-addon-api: 7.1.1
prebuild-install: 7.1.3
'@isaacs/cliui@8.0.2':
dependencies:
string-width: 5.1.2
@ -4042,18 +3948,10 @@ snapshots:
bare-events: 2.5.4
optional: true
base64-js@1.5.1: {}
basic-ftp@5.0.5: {}
binary-extensions@2.3.0: {}
bl@4.1.0:
dependencies:
buffer: 5.7.1
inherits: 2.0.4
readable-stream: 3.6.2
bn.js@4.12.2: {}
body-parser@1.20.3:
@ -4092,11 +3990,6 @@ snapshots:
buffer-equal-constant-time@1.0.1: {}
buffer@5.7.1:
dependencies:
base64-js: 1.5.1
ieee754: 1.2.1
bytes@3.1.2: {}
cac@6.7.14: {}
@ -4166,8 +4059,6 @@ snapshots:
dependencies:
readdirp: 4.1.2
chownr@1.1.4: {}
chromium-bidi@5.1.0(devtools-protocol@0.0.1452169):
dependencies:
devtools-protocol: 0.0.1452169
@ -4292,16 +4183,10 @@ snapshots:
decamelize@1.2.0: {}
decompress-response@6.0.0:
dependencies:
mimic-response: 3.1.0
deep-eql@5.0.2: {}
deep-equal@1.0.1: {}
deep-extend@0.6.0: {}
default-gateway@6.0.3:
dependencies:
execa: 5.1.1
@ -4328,8 +4213,6 @@ snapshots:
destroy@1.2.0: {}
detect-libc@2.0.4: {}
devtools-protocol@0.0.1452169: {}
dezalgo@1.0.4:
@ -4466,8 +4349,6 @@ snapshots:
signal-exit: 3.0.7
strip-final-newline: 2.0.0
expand-template@2.0.3: {}
expect-type@1.2.1: {}
express@4.21.2:
@ -4598,8 +4479,6 @@ snapshots:
fresh@0.5.2: {}
fs-constants@1.0.0: {}
fsevents@2.3.3:
optional: true
@ -4643,8 +4522,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
github-from-package@0.0.0: {}
glob-parent@5.1.2:
dependencies:
is-glob: 4.0.3
@ -4743,8 +4620,6 @@ snapshots:
dependencies:
safer-buffer: 2.1.2
ieee754@1.2.1: {}
ignore@5.3.2: {}
import-fresh@3.3.1:
@ -4758,8 +4633,6 @@ snapshots:
inherits@2.0.4: {}
ini@1.3.8: {}
internal-ip@6.2.0:
dependencies:
default-gateway: 6.0.3
@ -5073,8 +4946,6 @@ snapshots:
mimic-fn@2.1.0: {}
mimic-response@3.1.0: {}
minimalistic-assert@1.0.1: {}
minimatch@9.0.5:
@ -5087,8 +4958,6 @@ snapshots:
mitt@3.0.1: {}
mkdirp-classic@0.5.3: {}
mkdirp@1.0.4: {}
monaco-editor@0.52.2: {}
@ -5111,16 +4980,10 @@ snapshots:
nanoid@3.3.11: {}
napi-build-utils@2.0.0: {}
negotiator@0.6.3: {}
netmask@2.0.2: {}
node-abi@3.75.0:
dependencies:
semver: 7.7.2
node-addon-api@7.1.1: {}
node-domexception@1.0.0: {}
@ -5131,6 +4994,10 @@ snapshots:
fetch-blob: 3.2.0
formdata-polyfill: 4.0.10
node-pty@https://codeload.github.com/microsoft/node-pty/tar.gz/d738123f1faf7287513b0df8b9e327be54702e94:
dependencies:
node-addon-api: 7.1.1
node-releases@2.0.19: {}
normalize-path@3.0.0: {}
@ -5316,21 +5183,6 @@ snapshots:
dependencies:
commander: 9.5.0
prebuild-install@7.1.3:
dependencies:
detect-libc: 2.0.4
expand-template: 2.0.3
github-from-package: 0.0.0
minimist: 1.2.8
mkdirp-classic: 0.5.3
napi-build-utils: 2.0.0
node-abi: 3.75.0
pump: 3.0.3
rc: 1.2.8
simple-get: 4.0.1
tar-fs: 2.1.3
tunnel-agent: 0.6.0
prettier@3.6.1: {}
pretty-format@27.5.1:
@ -5414,25 +5266,12 @@ snapshots:
iconv-lite: 0.4.24
unpipe: 1.0.0
rc@1.2.8:
dependencies:
deep-extend: 0.6.0
ini: 1.3.8
minimist: 1.2.8
strip-json-comments: 2.0.1
react-is@17.0.2: {}
read-cache@1.0.0:
dependencies:
pify: 2.3.0
readable-stream@3.6.2:
dependencies:
inherits: 2.0.4
string_decoder: 1.3.0
util-deprecate: 1.0.2
readdirp@3.6.0:
dependencies:
picomatch: 2.3.1
@ -5586,14 +5425,6 @@ snapshots:
signal-exit@4.1.0: {}
simple-concat@1.0.1: {}
simple-get@4.0.1:
dependencies:
decompress-response: 6.0.0
once: 1.4.0
simple-concat: 1.0.1
sirv@3.0.1:
dependencies:
'@polka/url': 1.0.0-next.29
@ -5665,10 +5496,6 @@ snapshots:
emoji-regex: 9.2.2
strip-ansi: 7.1.0
string_decoder@1.3.0:
dependencies:
safe-buffer: 5.2.1
strip-ansi@5.2.0:
dependencies:
ansi-regex: 4.1.1
@ -5683,8 +5510,6 @@ snapshots:
strip-final-newline@2.0.0: {}
strip-json-comments@2.0.1: {}
strip-literal@3.0.0:
dependencies:
js-tokens: 9.0.1
@ -5759,13 +5584,6 @@ snapshots:
transitivePeerDependencies:
- ts-node
tar-fs@2.1.3:
dependencies:
chownr: 1.1.4
mkdirp-classic: 0.5.3
pump: 3.0.3
tar-stream: 2.2.0
tar-fs@3.0.10:
dependencies:
pump: 3.0.3
@ -5776,14 +5594,6 @@ snapshots:
transitivePeerDependencies:
- bare-buffer
tar-stream@2.2.0:
dependencies:
bl: 4.1.0
end-of-stream: 1.4.5
fs-constants: 1.0.0
inherits: 2.0.4
readable-stream: 3.6.2
tar-stream@3.1.7:
dependencies:
b4a: 1.6.7
@ -5846,10 +5656,6 @@ snapshots:
optionalDependencies:
fsevents: 2.3.3
tunnel-agent@0.6.0:
dependencies:
safe-buffer: 5.2.1
type-fest@0.21.3: {}
type-is@1.6.18:

View file

@ -66,7 +66,7 @@ async function build() {
format: 'cjs',
outfile: 'dist/vibetunnel-cli',
external: [
'@homebridge/node-pty-prebuilt-multiarch',
'node-pty',
'authenticate-pam',
],
minify: true,

View file

@ -0,0 +1,66 @@
#!/usr/bin/env node
/**
* Ensures native modules are built and available for tests
* This script handles pnpm's symlink structure where node-pty might be in .pnpm directory
*/
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
console.log('Ensuring native modules are built for tests...');
// Find the actual node-pty location (could be in .pnpm directory)
let nodePtyPath;
try {
nodePtyPath = require.resolve('node-pty/package.json');
} catch (e) {
console.error('Could not find node-pty module');
process.exit(1);
}
const nodePtyDir = path.dirname(nodePtyPath);
const buildDir = path.join(nodePtyDir, 'build');
const releaseDir = path.join(buildDir, 'Release');
console.log(`Found node-pty at: ${nodePtyDir}`);
// Check if native modules are built
if (!fs.existsSync(releaseDir) || !fs.existsSync(path.join(releaseDir, 'pty.node'))) {
console.log('Native modules not found, building...');
try {
// Build using node-gyp directly to avoid TypeScript issues
execSync(`cd "${nodePtyDir}" && npx node-gyp rebuild`, {
stdio: 'inherit',
shell: true
});
} catch (e) {
console.error('Failed to build native modules:', e.message);
process.exit(1);
}
}
// For pnpm, ensure the symlinked node_modules/node-pty has the build directory
const symlinkNodePty = path.join(__dirname, '../node_modules/node-pty');
if (fs.existsSync(symlinkNodePty) && fs.lstatSync(symlinkNodePty).isSymbolicLink()) {
const symlinkBuildDir = path.join(symlinkNodePty, 'build');
// If the symlinked location doesn't have a build directory, create a symlink to the real one
if (!fs.existsSync(symlinkBuildDir) && fs.existsSync(buildDir)) {
console.log('Creating symlink for build directory in node_modules/node-pty...');
try {
const symlinkType = process.platform === 'win32' ? 'junction' : 'dir';
fs.symlinkSync(buildDir, symlinkBuildDir, symlinkType);
console.log('Created build directory symlink');
} catch (e) {
// If symlink fails, try copying instead
console.log('Symlink failed, trying to copy build directory...');
fs.cpSync(buildDir, symlinkBuildDir, { recursive: true });
console.log('Copied build directory');
}
}
}
console.log('Native modules are ready for tests');

View file

@ -6,7 +6,6 @@
import { html } from 'lit';
import { customElement } from 'lit/decorators.js';
import { HeaderBase } from './header-base.js';
import type { Session } from './session-list.js';
import './terminal-icon.js';
@customElement('sidebar-header')
@ -58,83 +57,6 @@ export class SidebarHeader extends HeaderBase {
`;
}
private renderExitedToggleButton(exitedSessions: Session[], compact: boolean) {
if (exitedSessions.length === 0) return '';
const buttonClass = compact
? 'relative font-mono text-xs px-3 py-1.5 w-full rounded-lg border transition-all duration-200'
: 'relative font-mono text-xs px-4 py-2 rounded-lg border transition-all duration-200';
const stateClass = this.hideExited
? 'border-dark-border bg-dark-bg-tertiary text-dark-text hover:border-accent-green-darker'
: 'border-accent-green bg-accent-green text-dark-bg hover:bg-accent-green-darker';
return html`
<button
class="${buttonClass} ${stateClass}"
@click=${this.handleHideExitedToggle}
title="${
this.hideExited
? `Show ${exitedSessions.length} exited sessions`
: `Hide ${exitedSessions.length} exited sessions`
}"
>
<div class="flex items-center justify-between">
<span>${this.hideExited ? 'Show' : 'Hide'} Exited</span>
<div class="flex items-center gap-2">
<span class="text-xs opacity-75">(${exitedSessions.length})</span>
<div
class="w-8 h-4 rounded-full transition-colors duration-200 ${
this.hideExited ? 'bg-dark-border' : 'bg-dark-bg'
}"
>
<div
class="w-3 h-3 rounded-full transition-transform duration-200 mt-0.5 ${
this.hideExited
? 'translate-x-0.5 bg-dark-text-muted'
: 'translate-x-4 bg-dark-bg'
}"
></div>
</div>
</div>
</div>
</button>
`;
}
private renderKillAllButton(runningSessions: Session[]) {
// Only show Kill button if there are running sessions
if (runningSessions.length === 0) return '';
// Matching the same style as Show Exited button for consistency
const buttonClass =
'relative font-mono text-xs px-3 py-1.5 w-full rounded-lg border transition-all duration-200';
const stateClass = this.killingAll
? 'border-status-error bg-status-error text-dark-bg cursor-not-allowed'
: 'border-dark-border bg-dark-bg-tertiary text-status-error hover:border-status-error hover:bg-dark-bg-secondary';
return html`
<button
class="${buttonClass} ${stateClass}"
@click=${this.handleKillAll}
?disabled=${this.killingAll}
>
${
this.killingAll
? html`
<div class="flex items-center justify-center gap-2">
<div
class="w-3 h-3 border-2 border-dark-bg border-t-transparent rounded-full animate-spin"
></div>
<span>Killing...</span>
</div>
`
: `Kill All (${runningSessions.length})`
}
</button>
`;
}
private renderCompactUserMenu() {
// When no user (no-auth mode), show just a settings icon
if (!this.currentUser) {
@ -179,7 +101,6 @@ export class SidebarHeader extends HeaderBase {
class="w-full text-left px-3 py-1.5 text-xs font-mono text-dark-text hover:bg-dark-bg-secondary"
@click=${(e: Event) => {
e.stopPropagation();
console.log('🔧 Settings button clicked in sidebar header');
this.handleOpenSettings();
}}
>

View file

@ -5,12 +5,12 @@
* using the node-pty library while maintaining compatibility with tty-fwd.
*/
import type { IPty } from '@homebridge/node-pty-prebuilt-multiarch';
import * as pty from '@homebridge/node-pty-prebuilt-multiarch';
import chalk from 'chalk';
import { EventEmitter } from 'events';
import * as fs from 'fs';
import * as net from 'net';
import type { IPty } from 'node-pty';
import * as pty from 'node-pty';
import * as path from 'path';
import { v4 as uuidv4 } from 'uuid';
import type {

View file

@ -4,9 +4,9 @@
* These types match the tty-fwd format to ensure compatibility
*/
import type { IPty } from '@homebridge/node-pty-prebuilt-multiarch';
import type * as fs from 'fs';
import type * as net from 'net';
import type { IPty } from 'node-pty';
import type { SessionInfo } from '../../shared/types.js';
import type { AsciinemaWriter } from './asciinema-writer.js';

View file

@ -11,7 +11,7 @@ if (!globalThis.crypto) {
}
// Mock the native pty module before any imports
vi.mock('@homebridge/node-pty-prebuilt-multiarch', () => ({
vi.mock('node-pty', () => ({
spawn: vi.fn(() => ({
pid: 12345,
cols: 80,