refactor: remove bundled Claude Code binary support
- Remove all bundled/sidecar binary functionality - Delete build scripts for fetching and building Claude executables - Simplify binary detection to only support system installations - Update UI to remove bundled installation options - Update build configuration and documentation
This commit is contained in:
2
.github/workflows/build-test.yml
vendored
2
.github/workflows/build-test.yml
vendored
@@ -100,7 +100,7 @@ jobs:
|
||||
- name: Build frontend
|
||||
run: bun run build
|
||||
|
||||
# Build Tauri application (no bundle for faster CI)
|
||||
# Build Tauri application
|
||||
- name: Build Tauri application
|
||||
run: bun run tauri build --no-bundle -d
|
||||
env:
|
||||
|
13
README.md
13
README.md
@@ -253,9 +253,9 @@ brew install pkg-config
|
||||
bun run tauri build
|
||||
|
||||
# The built executable will be in:
|
||||
# - Linux: src-tauri/target/release/bundle/
|
||||
# - macOS: src-tauri/target/release/bundle/
|
||||
# - Windows: src-tauri/target/release/bundle/
|
||||
# - Linux: src-tauri/target/release/
|
||||
# - macOS: src-tauri/target/release/
|
||||
# - Windows: src-tauri/target/release/
|
||||
```
|
||||
|
||||
4. **Platform-Specific Build Options**
|
||||
@@ -265,11 +265,6 @@ brew install pkg-config
|
||||
bun run tauri build --debug
|
||||
```
|
||||
|
||||
**Build without bundling (creates just the executable)**
|
||||
```bash
|
||||
bun run tauri build --no-bundle
|
||||
```
|
||||
|
||||
**Universal Binary for macOS (Intel + Apple Silicon)**
|
||||
```bash
|
||||
bun run tauri build --target universal-apple-darwin
|
||||
@@ -324,7 +319,7 @@ The build process creates several artifacts:
|
||||
- `.msi` installer (Windows)
|
||||
- `.exe` installer (Windows)
|
||||
|
||||
All artifacts are located in `src-tauri/target/release/bundle/`.
|
||||
All artifacts are located in `src-tauri/target/release/`.
|
||||
|
||||
## 🛠️ Development
|
||||
|
||||
|
@@ -5,15 +5,8 @@
|
||||
"license": "AGPL-3.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"predev": "bun run build:executables:current",
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"prebuild": "bun run build:executables:current",
|
||||
"build:executables": "bun run scripts/fetch-and-build.js --version=1.0.41",
|
||||
"build:executables:current": "bun run scripts/fetch-and-build.js current --version=1.0.41",
|
||||
"build:executables:linux": "bun run scripts/fetch-and-build.js linux --version=1.0.41",
|
||||
"build:executables:macos": "bun run scripts/fetch-and-build.js macos --version=1.0.41",
|
||||
"build:executables:windows": "bun run scripts/fetch-and-build.js windows --version=1.0.41",
|
||||
"preview": "vite preview",
|
||||
"tauri": "tauri"
|
||||
},
|
||||
|
@@ -1,85 +0,0 @@
|
||||
# Build Scripts
|
||||
|
||||
This directory contains scripts for building Claude Code executables for all supported platforms.
|
||||
|
||||
## Scripts
|
||||
|
||||
### `fetch-and-build.js`
|
||||
Main build script that:
|
||||
1. Downloads the `@anthropic-ai/claude-code` package from npm
|
||||
2. Extracts and copies required files (cli.js, yoga.wasm, vendor/)
|
||||
3. Builds executables for specified platforms
|
||||
4. Cleans up temporary files
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
# Build for all platforms
|
||||
bun run scripts/fetch-and-build.js
|
||||
|
||||
# Build for specific platform
|
||||
bun run scripts/fetch-and-build.js linux
|
||||
bun run scripts/fetch-and-build.js macos
|
||||
bun run scripts/fetch-and-build.js windows
|
||||
bun run scripts/fetch-and-build.js current # Current platform only
|
||||
```
|
||||
|
||||
### `build-executables.js`
|
||||
Low-level script that builds executables from existing source files. This is called automatically by `fetch-and-build.js`.
|
||||
|
||||
### `prepare-bundle-native.js`
|
||||
Prepares the CLI source for bundling by embedding assets using Bun's native embedding features.
|
||||
|
||||
## NPM Scripts
|
||||
|
||||
The following npm scripts are available in `package.json`:
|
||||
|
||||
```bash
|
||||
# Build executables for all platforms
|
||||
npm run build:executables
|
||||
|
||||
# Build for specific platforms
|
||||
npm run build:executables:current
|
||||
npm run build:executables:linux
|
||||
npm run build:executables:macos
|
||||
npm run build:executables:windows
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
All executables are created in the `src-tauri/binaries/` directory with the following naming convention:
|
||||
|
||||
### Linux Executables
|
||||
- `claude-code-linux-x64` - Standard Linux x64 (glibc)
|
||||
- `claude-code-linux-x64-modern` - Modern CPUs (AVX2+)
|
||||
- `claude-code-linux-x64-baseline` - Older CPUs (pre-2013)
|
||||
- `claude-code-linux-arm64` - ARM64 Linux
|
||||
- `claude-code-linux-x64-musl` - Alpine Linux (musl)
|
||||
- `claude-code-linux-x64-musl-modern` - Alpine + modern CPUs
|
||||
- `claude-code-linux-x64-musl-baseline` - Alpine + older CPUs
|
||||
- `claude-code-linux-arm64-musl` - ARM64 Alpine
|
||||
|
||||
### macOS Executables
|
||||
- `claude-code-macos-x64` - Intel Mac
|
||||
- `claude-code-macos-x64-modern` - Intel Mac (modern CPUs)
|
||||
- `claude-code-macos-x64-baseline` - Intel Mac (older CPUs)
|
||||
- `claude-code-macos-arm64` - Apple Silicon Mac
|
||||
|
||||
### Windows Executables
|
||||
- `claude-code-windows-x64.exe` - Windows x64
|
||||
- `claude-code-windows-x64-modern.exe` - Windows x64 (modern CPUs)
|
||||
- `claude-code-windows-x64-baseline.exe` - Windows x64 (older CPUs)
|
||||
|
||||
## Features
|
||||
|
||||
- **Embedded Assets**: All executables include embedded yoga.wasm and ripgrep binaries
|
||||
- **Optimizations**: Built with minification and sourcemaps
|
||||
- **Cross-platform**: Supports all major operating systems and architectures
|
||||
- **CPU Variants**: Modern variants for newer CPUs (2013+), baseline for compatibility
|
||||
- **Self-contained**: No external dependencies required at runtime
|
||||
- **Tauri Integration**: Automatic sidecar binary naming for seamless Tauri integration
|
||||
|
||||
## Requirements
|
||||
|
||||
- **Bun**: Required for building (uses Bun's native compilation features)
|
||||
- **npm**: Used to download the Claude Code package
|
||||
- **tar**: For extracting the package (standard on Unix systems)
|
@@ -1,215 +0,0 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
/**
|
||||
* Build script for creating single-file executables from Claude Code package
|
||||
* Uses Bun's native embedding features with all optimizations enabled
|
||||
*
|
||||
* Output files follow Tauri sidecar triple naming convention: name-platform-architecture
|
||||
* Examples: claude-code-x86_64-apple-darwin, claude-code-aarch64-unknown-linux-gnu
|
||||
*
|
||||
* Usage:
|
||||
* bun run build-executables.js # Build all platforms
|
||||
* bun run build-executables.js linux # Build Linux executables only
|
||||
* bun run build-executables.js macos # Build macOS executables only
|
||||
* bun run build-executables.js windows # Build Windows executables only
|
||||
* bun run build-executables.js current # Build for current platform only
|
||||
*/
|
||||
|
||||
import { spawn } from 'child_process';
|
||||
import { mkdir, rm } from 'fs/promises';
|
||||
import { existsSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
// All supported targets with proper output names
|
||||
const PLATFORMS = {
|
||||
linux: [
|
||||
// Linux x64 - glibc
|
||||
{ target: 'bun-linux-x64', output: 'claude-code-x86_64-unknown-linux-gnu' },
|
||||
{ target: 'bun-linux-x64-modern', output: 'claude-code-modern-x86_64-unknown-linux-gnu' },
|
||||
{ target: 'bun-linux-x64-baseline', output: 'claude-code-baseline-x86_64-unknown-linux-gnu' },
|
||||
|
||||
// Linux ARM64 - glibc
|
||||
{ target: 'bun-linux-arm64', output: 'claude-code-aarch64-unknown-linux-gnu' },
|
||||
|
||||
// Linux x64 - musl (Alpine Linux, etc.)
|
||||
{ target: 'bun-linux-x64-musl', output: 'claude-code-x86_64-unknown-linux-musl' },
|
||||
{ target: 'bun-linux-x64-musl-modern', output: 'claude-code-modern-x86_64-unknown-linux-musl' },
|
||||
{ target: 'bun-linux-x64-musl-baseline', output: 'claude-code-baseline-x86_64-unknown-linux-musl' },
|
||||
|
||||
// Linux ARM64 - musl
|
||||
{ target: 'bun-linux-arm64-musl', output: 'claude-code-aarch64-unknown-linux-musl' }
|
||||
],
|
||||
macos: [
|
||||
// macOS x64
|
||||
{ target: 'bun-darwin-x64', output: 'claude-code-x86_64-apple-darwin' },
|
||||
{ target: 'bun-darwin-x64-modern', output: 'claude-code-modern-x86_64-apple-darwin' },
|
||||
{ target: 'bun-darwin-x64-baseline', output: 'claude-code-baseline-x86_64-apple-darwin' },
|
||||
|
||||
// macOS ARM64 (Apple Silicon)
|
||||
{ target: 'bun-darwin-arm64', output: 'claude-code-aarch64-apple-darwin' }
|
||||
],
|
||||
windows: [
|
||||
// Windows x64
|
||||
{ target: 'bun-windows-x64', output: 'claude-code-x86_64-pc-windows-msvc.exe' },
|
||||
{ target: 'bun-windows-x64-modern', output: 'claude-code-modern-x86_64-pc-windows-msvc.exe' },
|
||||
{ target: 'bun-windows-x64-baseline', output: 'claude-code-baseline-x86_64-pc-windows-msvc.exe' }
|
||||
]
|
||||
};
|
||||
|
||||
async function runCommand(command, args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log(`Running: ${command} ${args.join(' ')}`);
|
||||
const child = spawn(command, args, { stdio: 'inherit' });
|
||||
|
||||
child.on('error', reject);
|
||||
child.on('exit', (code) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`Command failed with exit code ${code}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function prepareBundle() {
|
||||
console.log('\nPreparing bundle with native Bun embedding...');
|
||||
await runCommand('bun', ['run', 'scripts/prepare-bundle-native.js']);
|
||||
}
|
||||
|
||||
async function buildExecutable(target, output) {
|
||||
console.log(`\nBuilding ${output}...`);
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
await runCommand('bun', [
|
||||
'build',
|
||||
'--compile',
|
||||
'--minify', // Optimize size
|
||||
'--sourcemap', // Embed sourcemap for debugging
|
||||
// '--bytecode', // Commented out - experimental feature that often fails
|
||||
`--target=${target}`,
|
||||
'./cli-native-bundled.js',
|
||||
`--outfile=src-tauri/binaries/${output}`
|
||||
]);
|
||||
|
||||
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
||||
console.log(`✓ Built ${output} in ${elapsed}s`);
|
||||
} catch (error) {
|
||||
// If compilation fails, throw the error
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function cleanupBundledFile() {
|
||||
const filesToClean = ['./cli-bundled.js', './cli-native-bundled.js'];
|
||||
for (const file of filesToClean) {
|
||||
if (existsSync(file)) {
|
||||
await rm(file);
|
||||
}
|
||||
}
|
||||
console.log('\n✓ Cleaned up temporary files');
|
||||
}
|
||||
|
||||
async function getCurrentPlatform() {
|
||||
const arch = process.arch === 'x64' ? 'x86_64' : 'aarch64';
|
||||
let targetTriple;
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
targetTriple = `${arch}-apple-darwin`;
|
||||
} else if (process.platform === 'linux') {
|
||||
targetTriple = `${arch}-unknown-linux-gnu`;
|
||||
} else if (process.platform === 'win32') {
|
||||
targetTriple = `${arch}-pc-windows-msvc`;
|
||||
} else {
|
||||
throw new Error(`Unsupported platform: ${process.platform}`);
|
||||
}
|
||||
|
||||
return targetTriple;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const arg = process.argv[2];
|
||||
|
||||
// Create src-tauri/binaries directory if it doesn't exist
|
||||
if (!existsSync('src-tauri/binaries')) {
|
||||
await mkdir('src-tauri/binaries', { recursive: true });
|
||||
}
|
||||
|
||||
let platformsToBuild = [];
|
||||
|
||||
if (!arg || arg === 'all') {
|
||||
// Build all platforms
|
||||
platformsToBuild = [
|
||||
...PLATFORMS.linux,
|
||||
...PLATFORMS.macos,
|
||||
...PLATFORMS.windows
|
||||
];
|
||||
} else if (arg === 'linux') {
|
||||
platformsToBuild = PLATFORMS.linux;
|
||||
} else if (arg === 'macos' || arg === 'darwin') {
|
||||
platformsToBuild = PLATFORMS.macos;
|
||||
} else if (arg === 'windows' || arg === 'win32') {
|
||||
platformsToBuild = PLATFORMS.windows;
|
||||
} else if (arg === 'current') {
|
||||
// Build only for current platform
|
||||
const currentTargetTriple = await getCurrentPlatform();
|
||||
const allPlatforms = [
|
||||
...PLATFORMS.linux,
|
||||
...PLATFORMS.macos,
|
||||
...PLATFORMS.windows
|
||||
];
|
||||
|
||||
// Find platform by matching the target triple in the output name
|
||||
const current = allPlatforms.find(p => p.output.includes(currentTargetTriple));
|
||||
if (current) {
|
||||
platformsToBuild = [current];
|
||||
} else {
|
||||
console.error(`Current platform ${currentTargetTriple} not found in build targets`);
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
console.error(`Unknown argument: ${arg}`);
|
||||
console.error('Usage: bun run build-executables.js [all|linux|macos|windows|current]');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`Building ${platformsToBuild.length} executable(s) with full optimizations...`);
|
||||
console.log('Optimizations enabled: --minify --sourcemap');
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
// Prepare the bundle once with native embedding
|
||||
await prepareBundle();
|
||||
|
||||
// Build executables sequentially to avoid resource conflicts
|
||||
let successCount = 0;
|
||||
for (const platform of platformsToBuild) {
|
||||
try {
|
||||
await buildExecutable(platform.target, platform.output);
|
||||
successCount++;
|
||||
} catch (error) {
|
||||
console.error(`Failed to build ${platform.output}:`, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
const totalElapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
||||
console.log(`\n✅ Successfully built ${successCount}/${platformsToBuild.length} executables in ${totalElapsed}s`);
|
||||
console.log('\nExecutables are available in the src-tauri/binaries/ directory');
|
||||
console.log('\nNotes:');
|
||||
console.log('- All executables include embedded assets (yoga.wasm, ripgrep binaries)');
|
||||
console.log('- File names follow Tauri sidecar triple naming convention (name-platform-architecture)');
|
||||
console.log('- Modern variants require CPUs from 2013+ (AVX2 support)');
|
||||
console.log('- Baseline variants support older CPUs (pre-2013)');
|
||||
console.log('- Musl variants are for Alpine Linux and similar distributions');
|
||||
console.log('- All executables are optimized with minification and sourcemaps');
|
||||
} finally {
|
||||
// Clean up temporary files
|
||||
await cleanupBundledFile();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
console.error('Build failed:', error);
|
||||
process.exit(1);
|
||||
});
|
@@ -1,337 +0,0 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
/**
|
||||
* Fetch Claude Code package from npm and build executables for all platforms
|
||||
*
|
||||
* This script:
|
||||
* 1. Downloads the @anthropic-ai/claude-code package from npm
|
||||
* 2. Extracts it to a temporary directory
|
||||
* 3. Runs the build-executables script to create binaries for all platforms
|
||||
* 4. Cleans up temporary files
|
||||
*
|
||||
* Usage:
|
||||
* bun run fetch-and-build.js [platform] [--version=X.X.X]
|
||||
*
|
||||
* Where platform can be: all, linux, macos, windows, current
|
||||
*
|
||||
* Version can be specified via:
|
||||
* - CLI argument: --version=1.0.41 (defaults to 1.0.41 if not specified)
|
||||
*/
|
||||
|
||||
import { spawn } from 'child_process';
|
||||
import { mkdir, rm, readdir, copyFile, access } from 'fs/promises';
|
||||
import { existsSync } from 'fs';
|
||||
import { join, resolve } from 'path';
|
||||
|
||||
/**
|
||||
* Execute a shell command and return a promise
|
||||
* @param {string} command - The command to execute
|
||||
* @param {string[]} args - Command arguments
|
||||
* @param {object} options - Spawn options
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function runCommand(command, args = [], options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log(`Running: ${command} ${args.join(' ')}`);
|
||||
const child = spawn(command, args, {
|
||||
stdio: 'inherit',
|
||||
shell: process.platform === 'win32',
|
||||
...options
|
||||
});
|
||||
|
||||
child.on('error', (error) => {
|
||||
console.error(`Failed to execute command: ${error.message}`);
|
||||
reject(error);
|
||||
});
|
||||
|
||||
child.on('exit', (code) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`Command failed with exit code ${code}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a file or directory exists
|
||||
* @param {string} path - Path to check
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async function pathExists(path) {
|
||||
try {
|
||||
await access(path);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse command line arguments to extract version and platform
|
||||
* @param {string[]} args - Command line arguments
|
||||
* @returns {object} - Parsed arguments with platform and version
|
||||
*/
|
||||
function parseArguments(args) {
|
||||
let platform = 'all';
|
||||
let version = null;
|
||||
|
||||
for (const arg of args) {
|
||||
if (arg.startsWith('--version=')) {
|
||||
version = arg.split('=')[1];
|
||||
} else if (!arg.startsWith('--')) {
|
||||
platform = arg;
|
||||
}
|
||||
}
|
||||
|
||||
return { platform, version };
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the Claude Code version to use
|
||||
* @param {string|null} cliVersion - Version from CLI argument
|
||||
* @returns {string} - The version to use
|
||||
*/
|
||||
function determineClaudeCodeVersion(cliVersion) {
|
||||
const defaultVersion = '1.0.41';
|
||||
|
||||
if (cliVersion) {
|
||||
console.log(`\n🔍 Using Claude Code version from CLI argument: ${cliVersion}`);
|
||||
return cliVersion;
|
||||
}
|
||||
|
||||
console.log(`\n🔍 Using default Claude Code version: ${defaultVersion}`);
|
||||
return defaultVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download and extract the Claude Code package from npm
|
||||
* @param {string} version - The version of the Claude Code package to download
|
||||
* @returns {Promise<string>} - Path to the extracted package directory
|
||||
*/
|
||||
async function fetchClaudeCodePackage(version) {
|
||||
console.log(`\n📦 Fetching @anthropic-ai/claude-code@${version} package from npm...`);
|
||||
|
||||
const tempDir = resolve('./temp-claude-package');
|
||||
const packageDir = join(tempDir, 'package');
|
||||
|
||||
try {
|
||||
// Clean up any existing temp directory
|
||||
if (await pathExists(tempDir)) {
|
||||
console.log('Cleaning up existing temp directory...');
|
||||
await rm(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
// Create temp directory
|
||||
await mkdir(tempDir, { recursive: true });
|
||||
|
||||
// Download the package tarball
|
||||
console.log(`Downloading package tarball for version ${version}...`);
|
||||
await runCommand('npm', ['pack', `@anthropic-ai/claude-code@${version}`], {
|
||||
cwd: tempDir
|
||||
});
|
||||
|
||||
// Find the downloaded tarball
|
||||
const files = await readdir(tempDir);
|
||||
const tarball = files.find(file => file.startsWith('anthropic-ai-claude-code-') && file.endsWith('.tgz'));
|
||||
|
||||
if (!tarball) {
|
||||
throw new Error('Failed to find downloaded tarball');
|
||||
}
|
||||
|
||||
console.log(`Found tarball: ${tarball}`);
|
||||
|
||||
// Extract the tarball
|
||||
console.log('Extracting package...');
|
||||
await runCommand('tar', ['-xzf', tarball], {
|
||||
cwd: tempDir
|
||||
});
|
||||
|
||||
// Verify extraction
|
||||
if (!(await pathExists(packageDir))) {
|
||||
throw new Error('Package extraction failed - package directory not found');
|
||||
}
|
||||
|
||||
console.log(`✓ Package extracted to: ${packageDir}`);
|
||||
return packageDir;
|
||||
|
||||
} catch (error) {
|
||||
// Clean up on error
|
||||
if (await pathExists(tempDir)) {
|
||||
await rm(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy required files from the Claude Code package to current directory
|
||||
* @param {string} packageDir - Path to the extracted package directory
|
||||
*/
|
||||
async function copyRequiredFiles(packageDir) {
|
||||
console.log('\n📋 Copying required files from Claude Code package...');
|
||||
|
||||
const filesToCopy = [
|
||||
'cli.js',
|
||||
'yoga.wasm'
|
||||
];
|
||||
|
||||
const directoriesToCopy = [
|
||||
'vendor'
|
||||
];
|
||||
|
||||
// Copy individual files
|
||||
for (const file of filesToCopy) {
|
||||
const srcPath = join(packageDir, file);
|
||||
const destPath = resolve(file);
|
||||
|
||||
if (await pathExists(srcPath)) {
|
||||
console.log(`Copying ${file}...`);
|
||||
await copyFile(srcPath, destPath);
|
||||
} else {
|
||||
console.warn(`Warning: ${file} not found in package`);
|
||||
}
|
||||
}
|
||||
|
||||
// Copy directories recursively
|
||||
for (const dir of directoriesToCopy) {
|
||||
const srcPath = join(packageDir, dir);
|
||||
const destPath = resolve(dir);
|
||||
|
||||
if (await pathExists(srcPath)) {
|
||||
console.log(`Copying ${dir}/ directory...`);
|
||||
|
||||
// Remove existing directory if it exists
|
||||
if (await pathExists(destPath)) {
|
||||
await rm(destPath, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
// Copy directory recursively using cp command
|
||||
await runCommand('cp', ['-r', srcPath, destPath]);
|
||||
} else {
|
||||
console.warn(`Warning: ${dir}/ directory not found in package`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('✓ Required files copied successfully');
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up temporary files and directories
|
||||
* @param {string} packageDir - Path to the package directory to clean up
|
||||
*/
|
||||
async function cleanup(packageDir) {
|
||||
console.log('\n🧹 Cleaning up temporary files...');
|
||||
|
||||
const tempDir = resolve('./temp-claude-package');
|
||||
|
||||
try {
|
||||
if (await pathExists(tempDir)) {
|
||||
await rm(tempDir, { recursive: true, force: true });
|
||||
console.log('✓ Temporary package directory cleaned up');
|
||||
}
|
||||
|
||||
// Clean up copied files that are no longer needed
|
||||
const filesToCleanup = [
|
||||
'./cli.js',
|
||||
'./cli-bundled.js',
|
||||
'./cli-native-bundled.js',
|
||||
'./yoga.wasm'
|
||||
];
|
||||
|
||||
for (const file of filesToCleanup) {
|
||||
if (await pathExists(file)) {
|
||||
await rm(file);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up vendor directory
|
||||
const vendorDir = './vendor';
|
||||
if (await pathExists(vendorDir)) {
|
||||
await rm(vendorDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
console.log('✓ Cleanup completed');
|
||||
} catch (error) {
|
||||
console.warn(`Warning: Cleanup failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build executables for the specified platform(s)
|
||||
* @param {string} platform - Platform to build for (all, linux, macos, windows, current)
|
||||
*/
|
||||
async function buildExecutables(platform = 'all') {
|
||||
console.log(`\n🔨 Building executables for platform: ${platform}`);
|
||||
|
||||
// Ensure src-tauri/binaries directory exists
|
||||
if (!await pathExists('./src-tauri/binaries')) {
|
||||
await mkdir('./src-tauri/binaries', { recursive: true });
|
||||
}
|
||||
|
||||
// Run the build-executables script
|
||||
const args = platform === 'all' ? [] : [platform];
|
||||
await runCommand('bun', ['run', './scripts/build-executables.js', ...args]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main execution function
|
||||
*/
|
||||
async function main() {
|
||||
// Parse command line arguments
|
||||
const args = process.argv.slice(2);
|
||||
const { platform, version: cliVersion } = parseArguments(args);
|
||||
|
||||
const validPlatforms = ['all', 'linux', 'macos', 'darwin', 'windows', 'win32', 'current'];
|
||||
|
||||
if (!validPlatforms.includes(platform)) {
|
||||
console.error(`Invalid platform: ${platform}`);
|
||||
console.error(`Valid platforms: ${validPlatforms.join(', ')}`);
|
||||
console.error('\nUsage: bun run fetch-and-build.js [platform] [--version=X.X.X]');
|
||||
console.error('Examples:');
|
||||
console.error(' bun run fetch-and-build.js');
|
||||
console.error(' bun run fetch-and-build.js linux');
|
||||
console.error(' bun run fetch-and-build.js macos --version=1.0.42');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('🚀 Starting Claude Code fetch and build process...');
|
||||
console.log(`Target platform: ${platform}`);
|
||||
|
||||
const startTime = Date.now();
|
||||
let packageDir;
|
||||
|
||||
try {
|
||||
// Step 1: Determine version to use
|
||||
const version = determineClaudeCodeVersion(cliVersion);
|
||||
|
||||
// Step 2: Fetch and extract the package
|
||||
packageDir = await fetchClaudeCodePackage(version);
|
||||
|
||||
// Step 3: Copy required files
|
||||
await copyRequiredFiles(packageDir);
|
||||
|
||||
// Step 4: Build executables
|
||||
await buildExecutables(platform);
|
||||
|
||||
const totalTime = ((Date.now() - startTime) / 1000).toFixed(1);
|
||||
console.log(`\n✅ Build process completed successfully in ${totalTime}s`);
|
||||
console.log('\n📁 Executables are available in the src-tauri/binaries/ directory');
|
||||
|
||||
} catch (error) {
|
||||
console.error(`\n❌ Build process failed: ${error.message}`);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
// Always clean up, even if there was an error
|
||||
if (packageDir) {
|
||||
await cleanup(packageDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Run the main function
|
||||
main().catch(error => {
|
||||
console.error('Unexpected error:', error);
|
||||
process.exit(1);
|
||||
});
|
@@ -1,145 +0,0 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
/**
|
||||
* Prepare the CLI for bundling using Bun's native embedding features
|
||||
* This modifies the source to use embedded files directly
|
||||
*/
|
||||
|
||||
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
// Read the original CLI file
|
||||
const cliPath = './cli.js';
|
||||
let cliContent = readFileSync(cliPath, 'utf-8');
|
||||
|
||||
console.log('Preparing CLI for native Bun embedding...');
|
||||
|
||||
// 1. Build list of embedded imports based on what files actually exist
|
||||
const embeddedImports = [];
|
||||
const embeddedFilesMapping = [];
|
||||
|
||||
// Define all possible ripgrep files
|
||||
const ripgrepFiles = [
|
||||
{ path: './vendor/ripgrep/arm64-darwin/rg', var: '__embeddedRgDarwinArm64' },
|
||||
{ path: './vendor/ripgrep/arm64-darwin/ripgrep.node', var: '__embeddedRgNodeDarwinArm64' },
|
||||
{ path: './vendor/ripgrep/arm64-linux/rg', var: '__embeddedRgLinuxArm64' },
|
||||
{ path: './vendor/ripgrep/arm64-linux/ripgrep.node', var: '__embeddedRgNodeLinuxArm64' },
|
||||
{ path: './vendor/ripgrep/x64-darwin/rg', var: '__embeddedRgDarwinX64' },
|
||||
{ path: './vendor/ripgrep/x64-darwin/ripgrep.node', var: '__embeddedRgNodeDarwinX64' },
|
||||
{ path: './vendor/ripgrep/x64-linux/rg', var: '__embeddedRgLinuxX64' },
|
||||
{ path: './vendor/ripgrep/x64-linux/ripgrep.node', var: '__embeddedRgNodeLinuxX64' },
|
||||
{ path: './vendor/ripgrep/x64-win32/rg.exe', var: '__embeddedRgWin32' },
|
||||
{ path: './vendor/ripgrep/x64-win32/ripgrep.node', var: '__embeddedRgNodeWin32' },
|
||||
];
|
||||
|
||||
// Always include yoga.wasm
|
||||
if (existsSync('./yoga.wasm')) {
|
||||
embeddedImports.push('import __embeddedYogaWasm from "./yoga.wasm" with { type: "file" };');
|
||||
embeddedFilesMapping.push(" 'yoga.wasm': __embeddedYogaWasm,");
|
||||
} else {
|
||||
console.error('Warning: yoga.wasm not found');
|
||||
}
|
||||
|
||||
// Only import ripgrep files that exist
|
||||
for (const file of ripgrepFiles) {
|
||||
if (existsSync(file.path)) {
|
||||
embeddedImports.push(`import ${file.var} from "${file.path}" with { type: "file" };`);
|
||||
const key = file.path.replace('./', '');
|
||||
embeddedFilesMapping.push(` '${key}': ${file.var},`);
|
||||
}
|
||||
}
|
||||
|
||||
const embeddedCode = `
|
||||
// Embedded files using Bun's native embedding
|
||||
${embeddedImports.join('\n')}
|
||||
|
||||
const __embeddedFiles = {
|
||||
${embeddedFilesMapping.join('\n')}
|
||||
};
|
||||
|
||||
`;
|
||||
|
||||
// Add imports after the shebang
|
||||
const shebangMatch = cliContent.match(/^#!.*\n/);
|
||||
if (shebangMatch) {
|
||||
cliContent = shebangMatch[0] + embeddedCode + cliContent.substring(shebangMatch[0].length);
|
||||
} else {
|
||||
cliContent = embeddedCode + cliContent;
|
||||
}
|
||||
|
||||
// 2. Replace yoga.wasm loading - handle top-level await properly
|
||||
// Original: var k81=await nUA(await VP9(CP9(import.meta.url).resolve("./yoga.wasm")));
|
||||
// Since this uses top-level await, we need to preserve that structure
|
||||
const yogaLoadPattern = /var k81=await nUA\(await VP9\(CP9\(import\.meta\.url\)\.resolve\("\.\/yoga\.wasm"\)\)\);/;
|
||||
// Use an IIFE to handle the async loading
|
||||
const yogaLoadReplacement = `var k81=await(async()=>{return await nUA(await Bun.file(__embeddedYogaWasm).arrayBuffer())})();`;
|
||||
|
||||
if (yogaLoadPattern.test(cliContent)) {
|
||||
cliContent = cliContent.replace(yogaLoadPattern, yogaLoadReplacement);
|
||||
console.log('✓ Replaced yoga.wasm loading with embedded version');
|
||||
} else {
|
||||
console.error('Warning: Could not find yoga.wasm loading pattern');
|
||||
// Try a more general pattern
|
||||
const generalYogaPattern = /var\s+(\w+)\s*=\s*await\s+nUA\s*\(\s*await\s+VP9\s*\([^)]+\.resolve\s*\(\s*["']\.\/yoga\.wasm["']\s*\)\s*\)\s*\)/;
|
||||
if (generalYogaPattern.test(cliContent)) {
|
||||
cliContent = cliContent.replace(generalYogaPattern, (match, varName) => {
|
||||
return `var ${varName}=await(async()=>{return await nUA(await Bun.file(__embeddedYogaWasm).arrayBuffer())})()`;
|
||||
});
|
||||
console.log('✓ Replaced yoga.wasm loading with embedded version (general pattern)');
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Replace ripgrep path resolution
|
||||
// Add check for embedded files in the ripgrep resolver
|
||||
const ripgrepPattern = /let B=Db\.resolve\(et9,"vendor","ripgrep"\);/;
|
||||
const ripgrepReplacement = `
|
||||
if(process.env.CLAUDE_CODE_BUNDLED || typeof __embeddedFiles !== 'undefined'){
|
||||
const platform = process.platform === "win32" ? "x64-win32" : \`\${process.arch}-\${process.platform}\`;
|
||||
const rgKey = \`vendor/ripgrep/\${platform}/rg\${process.platform === "win32" ? ".exe" : ""}\`;
|
||||
if(__embeddedFiles[rgKey]) return __embeddedFiles[rgKey];
|
||||
}
|
||||
let B=Db.resolve(et9,"vendor","ripgrep");`;
|
||||
|
||||
if (ripgrepPattern.test(cliContent)) {
|
||||
cliContent = cliContent.replace(ripgrepPattern, ripgrepReplacement);
|
||||
console.log('✓ Added embedded file handling for ripgrep');
|
||||
}
|
||||
|
||||
// 4. Replace ripgrep.node loading - handle the entire if-else structure
|
||||
// Look for the complete if-else pattern where B is assigned
|
||||
const ripgrepNodePattern = /if\(typeof Bun!=="undefined"&&Bun\.embeddedFiles\?\.length>0\)B="\.\/ripgrep\.node";else/;
|
||||
const ripgrepNodeReplacement = `if(typeof Bun!=="undefined"&&Bun.embeddedFiles?.length>0)B=(()=>{
|
||||
const platform = process.platform === "win32" ? "x64-win32" : \`\${process.arch}-\${process.platform}\`;
|
||||
const nodeKey = \`vendor/ripgrep/\${platform}/ripgrep.node\`;
|
||||
return __embeddedFiles[nodeKey] || "./ripgrep.node";
|
||||
})();else`;
|
||||
|
||||
if (ripgrepNodePattern.test(cliContent)) {
|
||||
cliContent = cliContent.replace(ripgrepNodePattern, ripgrepNodeReplacement);
|
||||
console.log('✓ Added embedded file handling for ripgrep.node');
|
||||
} else {
|
||||
// Fallback to simpler pattern if the exact pattern doesn't match
|
||||
const simplePattern = /B="\.\/ripgrep\.node"/;
|
||||
if (simplePattern.test(cliContent)) {
|
||||
cliContent = cliContent.replace(simplePattern, `B=(()=>{
|
||||
const platform = process.platform === "win32" ? "x64-win32" : \`\${process.arch}-\${process.platform}\`;
|
||||
const nodeKey = \`vendor/ripgrep/\${platform}/ripgrep.node\`;
|
||||
return __embeddedFiles[nodeKey] || "./ripgrep.node";
|
||||
})()`);
|
||||
console.log('✓ Added embedded file handling for ripgrep.node (fallback pattern)');
|
||||
}
|
||||
}
|
||||
|
||||
// Set bundled mode indicator
|
||||
cliContent = cliContent.replace(
|
||||
/process\.env\.CLAUDE_CODE_ENTRYPOINT="cli"/,
|
||||
'process.env.CLAUDE_CODE_ENTRYPOINT="cli";process.env.CLAUDE_CODE_BUNDLED="1"'
|
||||
);
|
||||
|
||||
// Write the modified content
|
||||
const outputPath = './cli-native-bundled.js';
|
||||
writeFileSync(outputPath, cliContent);
|
||||
|
||||
console.log(`\n✅ Created ${outputPath} ready for bundling with native embedding`);
|
||||
console.log('\nNow you can run:');
|
||||
console.log(` bun build --compile --minify ./cli-native-bundled.js --outfile dist/claude-code`);
|
@@ -14,11 +14,6 @@
|
||||
{
|
||||
"identifier": "shell:allow-execute",
|
||||
"allow": [
|
||||
{
|
||||
"name": "claude-code",
|
||||
"sidecar": true,
|
||||
"args": true
|
||||
},
|
||||
{
|
||||
"name": "claude",
|
||||
"sidecar": false,
|
||||
@@ -29,11 +24,6 @@
|
||||
{
|
||||
"identifier": "shell:allow-spawn",
|
||||
"allow": [
|
||||
{
|
||||
"name": "claude-code",
|
||||
"sidecar": true,
|
||||
"args": true
|
||||
},
|
||||
{
|
||||
"name": "claude",
|
||||
"sidecar": false,
|
||||
|
@@ -3,7 +3,7 @@ use log::{debug, error, info, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::cmp::Ordering;
|
||||
/// Shared module for detecting Claude Code binary installations
|
||||
/// Supports NVM installations, aliased paths, version-based selection, and bundled sidecars
|
||||
/// Supports NVM installations, aliased paths, and version-based selection
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use tauri::Manager;
|
||||
@@ -11,8 +11,6 @@ use tauri::Manager;
|
||||
/// Type of Claude installation
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum InstallationType {
|
||||
/// Bundled sidecar binary (preferred)
|
||||
Bundled,
|
||||
/// System-installed binary
|
||||
System,
|
||||
/// Custom path specified by user
|
||||
@@ -22,11 +20,11 @@ pub enum InstallationType {
|
||||
/// Represents a Claude installation with metadata
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ClaudeInstallation {
|
||||
/// Full path to the Claude binary (or "claude-code" for sidecar)
|
||||
/// Full path to the Claude binary
|
||||
pub path: String,
|
||||
/// Version string if available
|
||||
pub version: Option<String>,
|
||||
/// Source of discovery (e.g., "nvm", "system", "homebrew", "which", "bundled")
|
||||
/// Source of discovery (e.g., "nvm", "system", "homebrew", "which")
|
||||
pub source: String,
|
||||
/// Type of installation
|
||||
pub installation_type: InstallationType,
|
||||
@@ -50,12 +48,7 @@ pub fn find_claude_binary(app_handle: &tauri::AppHandle) -> Result<String, Strin
|
||||
) {
|
||||
info!("Found stored claude path in database: {}", stored_path);
|
||||
|
||||
// If it's a sidecar reference, return it directly
|
||||
if stored_path == "claude-code" {
|
||||
return Ok(stored_path);
|
||||
}
|
||||
|
||||
// Otherwise check if the path still exists
|
||||
// Check if the path still exists
|
||||
let path_buf = PathBuf::from(&stored_path);
|
||||
if path_buf.exists() && path_buf.is_file() {
|
||||
return Ok(stored_path);
|
||||
@@ -69,25 +62,13 @@ pub fn find_claude_binary(app_handle: &tauri::AppHandle) -> Result<String, Strin
|
||||
"SELECT value FROM app_settings WHERE key = 'claude_installation_preference'",
|
||||
[],
|
||||
|row| row.get::<_, String>(0),
|
||||
).unwrap_or_else(|_| "bundled".to_string());
|
||||
).unwrap_or_else(|_| "system".to_string());
|
||||
|
||||
info!("User preference for Claude installation: {}", preference);
|
||||
|
||||
// If user prefers bundled and it's available, use it
|
||||
if preference == "bundled" && is_sidecar_available(app_handle) {
|
||||
info!("Using bundled Claude Code sidecar per user preference");
|
||||
return Ok("claude-code".to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for bundled sidecar (if no preference or bundled preferred)
|
||||
if is_sidecar_available(app_handle) {
|
||||
info!("Found bundled Claude Code sidecar");
|
||||
return Ok("claude-code".to_string());
|
||||
}
|
||||
|
||||
// Discover all available system installations
|
||||
let installations = discover_system_installations();
|
||||
|
||||
@@ -113,67 +94,29 @@ pub fn find_claude_binary(app_handle: &tauri::AppHandle) -> Result<String, Strin
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the bundled sidecar is available
|
||||
fn is_sidecar_available(app_handle: &tauri::AppHandle) -> bool {
|
||||
// Try to create a sidecar command to test availability
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
|
||||
match app_handle.shell().sidecar("claude-code") {
|
||||
Ok(_) => {
|
||||
debug!("Bundled Claude Code sidecar is available");
|
||||
true
|
||||
}
|
||||
Err(e) => {
|
||||
debug!("Bundled Claude Code sidecar not available: {}", e);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Discovers all available Claude installations and returns them for selection
|
||||
/// This allows UI to show a version selector
|
||||
pub fn discover_claude_installations() -> Vec<ClaudeInstallation> {
|
||||
info!("Discovering all Claude installations...");
|
||||
|
||||
let mut installations = Vec::new();
|
||||
let mut installations = discover_system_installations();
|
||||
|
||||
// Always add bundled sidecar as first option if available
|
||||
// We can't easily check version for sidecar without spawning it, so we'll mark it as bundled
|
||||
installations.push(ClaudeInstallation {
|
||||
path: "claude-code".to_string(),
|
||||
version: None, // Version will be determined at runtime
|
||||
source: "bundled".to_string(),
|
||||
installation_type: InstallationType::Bundled,
|
||||
});
|
||||
|
||||
// Add system installations
|
||||
installations.extend(discover_system_installations());
|
||||
|
||||
// Sort by installation type (Bundled first), then by version (highest first), then by source preference
|
||||
// Sort by version (highest first), then by source preference
|
||||
installations.sort_by(|a, b| {
|
||||
// First sort by installation type (Bundled comes first)
|
||||
match (&a.installation_type, &b.installation_type) {
|
||||
(InstallationType::Bundled, InstallationType::Bundled) => Ordering::Equal,
|
||||
(InstallationType::Bundled, _) => Ordering::Less,
|
||||
(_, InstallationType::Bundled) => Ordering::Greater,
|
||||
_ => {
|
||||
// For non-bundled installations, sort by version then source
|
||||
match (&a.version, &b.version) {
|
||||
(Some(v1), Some(v2)) => {
|
||||
// Compare versions in descending order (newest first)
|
||||
match compare_versions(v2, v1) {
|
||||
Ordering::Equal => {
|
||||
// If versions are equal, prefer by source
|
||||
source_preference(a).cmp(&source_preference(b))
|
||||
}
|
||||
other => other,
|
||||
}
|
||||
match (&a.version, &b.version) {
|
||||
(Some(v1), Some(v2)) => {
|
||||
// Compare versions in descending order (newest first)
|
||||
match compare_versions(v2, v1) {
|
||||
Ordering::Equal => {
|
||||
// If versions are equal, prefer by source
|
||||
source_preference(a).cmp(&source_preference(b))
|
||||
}
|
||||
(Some(_), None) => Ordering::Less, // Version comes before no version
|
||||
(None, Some(_)) => Ordering::Greater,
|
||||
(None, None) => source_preference(a).cmp(&source_preference(b)),
|
||||
other => other,
|
||||
}
|
||||
}
|
||||
(Some(_), None) => Ordering::Less, // Version comes before no version
|
||||
(None, Some(_)) => Ordering::Greater,
|
||||
(None, None) => source_preference(a).cmp(&source_preference(b)),
|
||||
}
|
||||
});
|
||||
|
||||
@@ -183,7 +126,6 @@ pub fn discover_claude_installations() -> Vec<ClaudeInstallation> {
|
||||
/// Returns a preference score for installation sources (lower is better)
|
||||
fn source_preference(installation: &ClaudeInstallation) -> u8 {
|
||||
match installation.source.as_str() {
|
||||
"bundled" => 0, // Bundled sidecar has highest preference
|
||||
"which" => 1,
|
||||
"homebrew" => 2,
|
||||
"system" => 3,
|
||||
@@ -200,7 +142,7 @@ fn source_preference(installation: &ClaudeInstallation) -> u8 {
|
||||
}
|
||||
}
|
||||
|
||||
/// Discovers all Claude system installations on the system (excludes bundled sidecar)
|
||||
/// Discovers all Claude installations on the system
|
||||
fn discover_system_installations() -> Vec<ClaudeInstallation> {
|
||||
let mut installations = Vec::new();
|
||||
|
||||
|
@@ -766,37 +766,8 @@ pub async fn execute_agent(
|
||||
"--dangerously-skip-permissions".to_string(),
|
||||
];
|
||||
|
||||
// Execute based on whether we should use sidecar or system binary
|
||||
if should_use_sidecar(&claude_path) {
|
||||
spawn_agent_sidecar(app, run_id, agent_id, agent.name.clone(), args, project_path, task, execution_model, db, registry).await
|
||||
} else {
|
||||
spawn_agent_system(app, run_id, agent_id, agent.name.clone(), claude_path, args, project_path, task, execution_model, db, registry).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Determines whether to use sidecar or system binary execution for agents
|
||||
fn should_use_sidecar(claude_path: &str) -> bool {
|
||||
claude_path == "claude-code"
|
||||
}
|
||||
|
||||
/// Creates a sidecar command for agent execution
|
||||
fn create_agent_sidecar_command(
|
||||
app: &AppHandle,
|
||||
args: Vec<String>,
|
||||
project_path: &str,
|
||||
) -> Result<tauri_plugin_shell::process::Command, String> {
|
||||
let mut sidecar_cmd = app
|
||||
.shell()
|
||||
.sidecar("claude-code")
|
||||
.map_err(|e| format!("Failed to create sidecar command: {}", e))?;
|
||||
|
||||
// Add all arguments
|
||||
sidecar_cmd = sidecar_cmd.args(args);
|
||||
|
||||
// Set working directory
|
||||
sidecar_cmd = sidecar_cmd.current_dir(project_path);
|
||||
|
||||
Ok(sidecar_cmd)
|
||||
// Execute using system binary
|
||||
spawn_agent_system(app, run_id, agent_id, agent.name.clone(), claude_path, args, project_path, task, execution_model, db, registry).await
|
||||
}
|
||||
|
||||
/// Creates a system binary command for agent execution
|
||||
@@ -821,239 +792,6 @@ fn create_agent_system_command(
|
||||
}
|
||||
|
||||
/// Spawn agent using sidecar command
|
||||
async fn spawn_agent_sidecar(
|
||||
app: AppHandle,
|
||||
run_id: i64,
|
||||
_agent_id: i64,
|
||||
_agent_name: String,
|
||||
args: Vec<String>,
|
||||
project_path: String,
|
||||
_task: String,
|
||||
_execution_model: String,
|
||||
db: State<'_, AgentDb>,
|
||||
registry: State<'_, crate::process::ProcessRegistryState>,
|
||||
) -> Result<i64, String> {
|
||||
use std::sync::Mutex;
|
||||
|
||||
// Create the sidecar command
|
||||
let sidecar_cmd = create_agent_sidecar_command(&app, args, &project_path)?;
|
||||
|
||||
// Spawn the sidecar process
|
||||
let (mut rx, child) = sidecar_cmd
|
||||
.spawn()
|
||||
.map_err(|e| format!("Failed to spawn Claude sidecar: {}", e))?;
|
||||
|
||||
// Get the child PID for logging
|
||||
let pid = child.pid();
|
||||
info!("✅ Spawned Claude sidecar process with PID: {:?}", pid);
|
||||
|
||||
// Update the database with PID and status
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
{
|
||||
let conn = db.0.lock().map_err(|e| e.to_string())?;
|
||||
conn.execute(
|
||||
"UPDATE agent_runs SET status = 'running', pid = ?1, process_started_at = ?2 WHERE id = ?3",
|
||||
params![pid as i64, now, run_id],
|
||||
).map_err(|e| e.to_string())?;
|
||||
info!("📝 Updated database with running status and PID");
|
||||
}
|
||||
|
||||
// We'll extract the session ID from Claude's init message
|
||||
let session_id_holder: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
|
||||
|
||||
// Create variables we need for the spawned task
|
||||
let app_dir = app
|
||||
.path()
|
||||
.app_data_dir()
|
||||
.expect("Failed to get app data dir");
|
||||
let db_path = app_dir.join("agents.db");
|
||||
let db_path_for_stream = db_path.clone(); // Clone for the streaming task
|
||||
|
||||
// Spawn task to read events from sidecar
|
||||
let app_handle = app.clone();
|
||||
let session_id_holder_clone = session_id_holder.clone();
|
||||
let live_output = std::sync::Arc::new(Mutex::new(String::new()));
|
||||
let live_output_clone = live_output.clone();
|
||||
let registry_clone = registry.0.clone();
|
||||
let first_output = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
|
||||
let first_output_clone = first_output.clone();
|
||||
|
||||
let sidecar_task = tokio::spawn(async move {
|
||||
info!("📖 Starting to read Claude sidecar events...");
|
||||
let mut line_count = 0;
|
||||
|
||||
while let Some(event) = rx.recv().await {
|
||||
match event {
|
||||
tauri_plugin_shell::process::CommandEvent::Stdout(data) => {
|
||||
let line = String::from_utf8_lossy(&data).trim().to_string();
|
||||
if !line.is_empty() {
|
||||
line_count += 1;
|
||||
|
||||
// Log first output
|
||||
if !first_output_clone.load(std::sync::atomic::Ordering::Relaxed) {
|
||||
info!("🎉 First output received from Claude sidecar! Line: {}", line);
|
||||
first_output_clone.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
|
||||
if line_count <= 5 {
|
||||
info!("sidecar stdout[{}]: {}", line_count, line);
|
||||
} else {
|
||||
debug!("sidecar stdout[{}]: {}", line_count, line);
|
||||
}
|
||||
|
||||
// Store live output
|
||||
if let Ok(mut output) = live_output_clone.lock() {
|
||||
output.push_str(&line);
|
||||
output.push('\n');
|
||||
}
|
||||
|
||||
// Also store in process registry for cross-session access
|
||||
let _ = registry_clone.append_live_output(run_id, &line);
|
||||
|
||||
// Extract session ID from JSONL output
|
||||
if let Ok(json) = serde_json::from_str::<JsonValue>(&line) {
|
||||
// Claude Code uses "session_id" (underscore), not "sessionId"
|
||||
if json.get("type").and_then(|t| t.as_str()) == Some("system") &&
|
||||
json.get("subtype").and_then(|s| s.as_str()) == Some("init") {
|
||||
if let Some(sid) = json.get("session_id").and_then(|s| s.as_str()) {
|
||||
if let Ok(mut current_session_id) = session_id_holder_clone.lock() {
|
||||
if current_session_id.is_none() {
|
||||
*current_session_id = Some(sid.to_string());
|
||||
info!("🔑 Extracted session ID: {}", sid);
|
||||
|
||||
// Update database immediately with session ID
|
||||
if let Ok(conn) = Connection::open(&db_path_for_stream) {
|
||||
match conn.execute(
|
||||
"UPDATE agent_runs SET session_id = ?1 WHERE id = ?2",
|
||||
params![sid, run_id],
|
||||
) {
|
||||
Ok(rows) => {
|
||||
if rows > 0 {
|
||||
info!("✅ Updated agent run {} with session ID immediately", run_id);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("❌ Failed to update session ID immediately: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Emit the line to the frontend with run_id for isolation
|
||||
let _ = app_handle.emit(&format!("agent-output:{}", run_id), &line);
|
||||
// Also emit to the generic event for backward compatibility
|
||||
let _ = app_handle.emit("agent-output", &line);
|
||||
}
|
||||
}
|
||||
tauri_plugin_shell::process::CommandEvent::Stderr(data) => {
|
||||
let line = String::from_utf8_lossy(&data).trim().to_string();
|
||||
if !line.is_empty() {
|
||||
error!("sidecar stderr: {}", line);
|
||||
// Emit error lines to the frontend with run_id for isolation
|
||||
let _ = app_handle.emit(&format!("agent-error:{}", run_id), &line);
|
||||
// Also emit to the generic event for backward compatibility
|
||||
let _ = app_handle.emit("agent-error", &line);
|
||||
}
|
||||
}
|
||||
tauri_plugin_shell::process::CommandEvent::Terminated { .. } => {
|
||||
info!("📖 Claude sidecar process terminated");
|
||||
break;
|
||||
}
|
||||
tauri_plugin_shell::process::CommandEvent::Error(e) => {
|
||||
error!("🔥 Claude sidecar error: {}", e);
|
||||
break;
|
||||
}
|
||||
_ => {
|
||||
// Handle any other event types we might not know about
|
||||
debug!("Received unknown sidecar event type");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!("📖 Finished reading Claude sidecar events. Total lines: {}", line_count);
|
||||
});
|
||||
|
||||
// Monitor process status and wait for completion
|
||||
tokio::spawn(async move {
|
||||
info!("🕐 Starting sidecar process monitoring...");
|
||||
|
||||
// Wait for first output with timeout
|
||||
for i in 0..300 {
|
||||
// 30 seconds (300 * 100ms)
|
||||
if first_output.load(std::sync::atomic::Ordering::Relaxed) {
|
||||
info!("✅ Output detected after {}ms, continuing normal execution", i * 100);
|
||||
break;
|
||||
}
|
||||
|
||||
if i == 299 {
|
||||
warn!("⏰ TIMEOUT: No output from Claude sidecar after 30 seconds");
|
||||
warn!("💡 This usually means:");
|
||||
warn!(" 1. Claude sidecar is waiting for user input");
|
||||
warn!(" 2. Authentication issues (API key not found/invalid)");
|
||||
warn!(" 3. Network connectivity issues");
|
||||
warn!(" 4. Claude failed to initialize but didn't report an error");
|
||||
|
||||
// Update database with failed status
|
||||
if let Ok(conn) = Connection::open(&db_path) {
|
||||
let _ = conn.execute(
|
||||
"UPDATE agent_runs SET status = 'failed', completed_at = CURRENT_TIMESTAMP WHERE id = ?1",
|
||||
params![run_id],
|
||||
);
|
||||
}
|
||||
|
||||
let _ = app.emit("agent-complete", false);
|
||||
let _ = app.emit(&format!("agent-complete:{}", run_id), false);
|
||||
return;
|
||||
}
|
||||
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
||||
}
|
||||
|
||||
// Wait for sidecar task to complete
|
||||
info!("⏳ Waiting for sidecar reading to complete...");
|
||||
let _ = sidecar_task.await;
|
||||
|
||||
// Get the session ID that was extracted
|
||||
let extracted_session_id = if let Ok(Some(sid)) = session_id_holder.lock().map(|s| s.clone()) {
|
||||
sid
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
// Update the run record with session ID and mark as completed
|
||||
if let Ok(conn) = Connection::open(&db_path) {
|
||||
info!("🔄 Updating database with extracted session ID: {}", extracted_session_id);
|
||||
match conn.execute(
|
||||
"UPDATE agent_runs SET session_id = ?1, status = 'completed', completed_at = CURRENT_TIMESTAMP WHERE id = ?2",
|
||||
params![extracted_session_id, run_id],
|
||||
) {
|
||||
Ok(rows_affected) => {
|
||||
if rows_affected > 0 {
|
||||
info!("✅ Successfully updated agent run {} with session ID: {}", run_id, extracted_session_id);
|
||||
} else {
|
||||
warn!("⚠️ No rows affected when updating agent run {} with session ID", run_id);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("❌ Failed to update agent run {} with session ID: {}", run_id, e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
error!("❌ Failed to open database to update session ID for run {}", run_id);
|
||||
}
|
||||
|
||||
info!("✅ Claude sidecar execution monitoring complete");
|
||||
|
||||
let _ = app.emit("agent-complete", true);
|
||||
let _ = app.emit(&format!("agent-complete:{}", run_id), true);
|
||||
});
|
||||
|
||||
Ok(run_id)
|
||||
}
|
||||
|
||||
/// Spawn agent using system binary command
|
||||
async fn spawn_agent_system(
|
||||
@@ -1830,20 +1568,7 @@ pub async fn get_claude_binary_path(db: State<'_, AgentDb>) -> Result<Option<Str
|
||||
pub async fn set_claude_binary_path(db: State<'_, AgentDb>, path: String) -> Result<(), String> {
|
||||
let conn = db.0.lock().map_err(|e| e.to_string())?;
|
||||
|
||||
// Special handling for bundled sidecar reference
|
||||
if path == "claude-code" {
|
||||
// For bundled sidecar, we don't need to validate file existence
|
||||
// as it's handled by Tauri's sidecar system
|
||||
conn.execute(
|
||||
"INSERT INTO app_settings (key, value) VALUES ('claude_binary_path', ?1)
|
||||
ON CONFLICT(key) DO UPDATE SET value = ?1",
|
||||
params![path],
|
||||
)
|
||||
.map_err(|e| format!("Failed to save Claude binary path: {}", e))?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Validate that the path exists and is executable for system installations
|
||||
// Validate that the path exists and is executable
|
||||
let path_buf = std::path::PathBuf::from(&path);
|
||||
if !path_buf.exists() {
|
||||
return Err(format!("File does not exist: {}", path));
|
||||
@@ -1883,77 +1608,6 @@ pub async fn list_claude_installations(
|
||||
return Err("No Claude Code installations found on the system".to_string());
|
||||
}
|
||||
|
||||
// For bundled installations, execute the sidecar to get the actual version
|
||||
for installation in &mut installations {
|
||||
if installation.installation_type == crate::claude_binary::InstallationType::Bundled {
|
||||
// Try to get the version by executing the sidecar
|
||||
use tauri_plugin_shell::process::CommandEvent;
|
||||
|
||||
// Create a temporary directory for the sidecar to run in
|
||||
let temp_dir = std::env::temp_dir();
|
||||
|
||||
// Create sidecar command with --version flag
|
||||
let sidecar_cmd = match app
|
||||
.shell()
|
||||
.sidecar("claude-code") {
|
||||
Ok(cmd) => cmd.args(["--version"]).current_dir(&temp_dir),
|
||||
Err(e) => {
|
||||
log::warn!("Failed to create sidecar command for version check: {}", e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// Spawn the sidecar and collect output
|
||||
match sidecar_cmd.spawn() {
|
||||
Ok((mut rx, _child)) => {
|
||||
let mut stdout_output = String::new();
|
||||
let mut stderr_output = String::new();
|
||||
|
||||
// Set a timeout for version check
|
||||
let timeout = tokio::time::Duration::from_secs(5);
|
||||
let start_time = tokio::time::Instant::now();
|
||||
|
||||
while let Ok(Some(event)) = tokio::time::timeout_at(
|
||||
start_time + timeout,
|
||||
rx.recv()
|
||||
).await {
|
||||
match event {
|
||||
CommandEvent::Stdout(data) => {
|
||||
stdout_output.push_str(&String::from_utf8_lossy(&data));
|
||||
}
|
||||
CommandEvent::Stderr(data) => {
|
||||
stderr_output.push_str(&String::from_utf8_lossy(&data));
|
||||
}
|
||||
CommandEvent::Terminated { .. } => {
|
||||
break;
|
||||
}
|
||||
CommandEvent::Error(e) => {
|
||||
log::warn!("Error during sidecar version check: {}", e);
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Use regex to directly extract version pattern
|
||||
let version_regex = regex::Regex::new(r"(\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?(?:\+[a-zA-Z0-9.-]+)?)").ok();
|
||||
|
||||
if let Some(regex) = version_regex {
|
||||
if let Some(captures) = regex.captures(&stdout_output) {
|
||||
if let Some(version_match) = captures.get(1) {
|
||||
installation.version = Some(version_match.as_str().to_string());
|
||||
log::info!("Bundled sidecar version: {}", version_match.as_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("Failed to spawn sidecar for version check: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(installations)
|
||||
}
|
||||
|
||||
@@ -2012,26 +1666,6 @@ fn create_command_with_env(program: &str) -> Command {
|
||||
tokio_cmd.env("PATH", "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin");
|
||||
}
|
||||
|
||||
// BEGIN PATCH: Ensure bundled sidecar directory is in PATH when using the "claude-code" placeholder
|
||||
if program == "claude-code" {
|
||||
// Attempt to locate the sidecar binaries directory that Tauri uses during development
|
||||
// At compile-time, CARGO_MANIFEST_DIR resolves to the absolute path of the src-tauri crate.
|
||||
// The sidecar binaries live in <src-tauri>/binaries.
|
||||
#[allow(clippy::redundant_clone)]
|
||||
let sidecar_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("binaries");
|
||||
if sidecar_dir.exists() {
|
||||
if let Some(sidecar_dir_str) = sidecar_dir.to_str() {
|
||||
let current_path = std::env::var("PATH").unwrap_or_default();
|
||||
let separator = if cfg!(target_os = "windows") { ";" } else { ":" };
|
||||
if !current_path.split(separator).any(|p| p == sidecar_dir_str) {
|
||||
let new_path = format!("{}{}{}", sidecar_dir_str, separator, current_path);
|
||||
tokio_cmd.env("PATH", new_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// END PATCH
|
||||
|
||||
tokio_cmd
|
||||
}
|
||||
|
||||
|
@@ -266,30 +266,6 @@ fn create_command_with_env(program: &str) -> Command {
|
||||
tokio_cmd
|
||||
}
|
||||
|
||||
/// Determines whether to use sidecar or system binary execution
|
||||
fn should_use_sidecar(claude_path: &str) -> bool {
|
||||
claude_path == "claude-code"
|
||||
}
|
||||
|
||||
/// Creates a sidecar command with the given arguments
|
||||
fn create_sidecar_command(
|
||||
app: &AppHandle,
|
||||
args: Vec<String>,
|
||||
project_path: &str,
|
||||
) -> Result<tauri_plugin_shell::process::Command, String> {
|
||||
let mut sidecar_cmd = app
|
||||
.shell()
|
||||
.sidecar("claude-code")
|
||||
.map_err(|e| format!("Failed to create sidecar command: {}", e))?;
|
||||
|
||||
// Add all arguments
|
||||
sidecar_cmd = sidecar_cmd.args(args);
|
||||
|
||||
// Set working directory
|
||||
sidecar_cmd = sidecar_cmd.current_dir(project_path);
|
||||
|
||||
Ok(sidecar_cmd)
|
||||
}
|
||||
|
||||
/// Creates a system binary command with the given arguments
|
||||
fn create_system_command(
|
||||
@@ -578,91 +554,6 @@ pub async fn check_claude_version(app: AppHandle) -> Result<ClaudeVersionStatus,
|
||||
}
|
||||
};
|
||||
|
||||
// If the selected path is the special sidecar identifier, execute it to get version
|
||||
if claude_path == "claude-code" {
|
||||
use tauri_plugin_shell::process::CommandEvent;
|
||||
|
||||
// Create a temporary directory for the sidecar to run in
|
||||
let temp_dir = std::env::temp_dir();
|
||||
|
||||
// Create sidecar command with --version flag
|
||||
let sidecar_cmd = match app
|
||||
.shell()
|
||||
.sidecar("claude-code") {
|
||||
Ok(cmd) => cmd.args(["--version"]).current_dir(&temp_dir),
|
||||
Err(e) => {
|
||||
log::error!("Failed to create sidecar command: {}", e);
|
||||
return Ok(ClaudeVersionStatus {
|
||||
is_installed: true, // We know it exists, just couldn't create command
|
||||
version: None,
|
||||
output: format!("Using bundled Claude Code sidecar (command creation failed: {})", e),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Spawn the sidecar and collect output
|
||||
match sidecar_cmd.spawn() {
|
||||
Ok((mut rx, _child)) => {
|
||||
let mut stdout_output = String::new();
|
||||
let mut stderr_output = String::new();
|
||||
let mut exit_success = false;
|
||||
|
||||
// Collect output from the sidecar
|
||||
while let Some(event) = rx.recv().await {
|
||||
match event {
|
||||
CommandEvent::Stdout(data) => {
|
||||
let line = String::from_utf8_lossy(&data);
|
||||
stdout_output.push_str(&line);
|
||||
}
|
||||
CommandEvent::Stderr(data) => {
|
||||
let line = String::from_utf8_lossy(&data);
|
||||
stderr_output.push_str(&line);
|
||||
}
|
||||
CommandEvent::Terminated(payload) => {
|
||||
exit_success = payload.code.unwrap_or(-1) == 0;
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Use regex to directly extract version pattern (e.g., "1.0.41")
|
||||
let version_regex = regex::Regex::new(r"(\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?(?:\+[a-zA-Z0-9.-]+)?)").ok();
|
||||
|
||||
let version = if let Some(regex) = version_regex {
|
||||
regex.captures(&stdout_output)
|
||||
.and_then(|captures| captures.get(1))
|
||||
.map(|m| m.as_str().to_string())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let full_output = if stderr_output.is_empty() {
|
||||
stdout_output.clone()
|
||||
} else {
|
||||
format!("{}\n{}", stdout_output, stderr_output)
|
||||
};
|
||||
|
||||
// Check if the output matches the expected format
|
||||
let is_valid = stdout_output.contains("(Claude Code)") || stdout_output.contains("Claude Code") || version.is_some();
|
||||
|
||||
return Ok(ClaudeVersionStatus {
|
||||
is_installed: is_valid && exit_success,
|
||||
version,
|
||||
output: full_output.trim().to_string(),
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to execute sidecar: {}", e);
|
||||
return Ok(ClaudeVersionStatus {
|
||||
is_installed: true, // We know it exists, just couldn't get version
|
||||
version: None,
|
||||
output: format!("Using bundled Claude Code sidecar (version check failed: {})", e),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
use log::debug;debug!("Claude path: {}", claude_path);
|
||||
|
||||
// In production builds, we can't check the version directly
|
||||
@@ -951,12 +842,8 @@ pub async fn execute_claude_code(
|
||||
"--dangerously-skip-permissions".to_string(),
|
||||
];
|
||||
|
||||
if should_use_sidecar(&claude_path) {
|
||||
spawn_claude_sidecar(app, args, prompt, model, project_path).await
|
||||
} else {
|
||||
let cmd = create_system_command(&claude_path, args, &project_path);
|
||||
spawn_claude_process(app, cmd, prompt, model, project_path).await
|
||||
}
|
||||
let cmd = create_system_command(&claude_path, args, &project_path);
|
||||
spawn_claude_process(app, cmd, prompt, model, project_path).await
|
||||
}
|
||||
|
||||
/// Continue an existing Claude Code conversation with streaming output
|
||||
@@ -987,12 +874,8 @@ pub async fn continue_claude_code(
|
||||
"--dangerously-skip-permissions".to_string(),
|
||||
];
|
||||
|
||||
if should_use_sidecar(&claude_path) {
|
||||
spawn_claude_sidecar(app, args, prompt, model, project_path).await
|
||||
} else {
|
||||
let cmd = create_system_command(&claude_path, args, &project_path);
|
||||
spawn_claude_process(app, cmd, prompt, model, project_path).await
|
||||
}
|
||||
let cmd = create_system_command(&claude_path, args, &project_path);
|
||||
spawn_claude_process(app, cmd, prompt, model, project_path).await
|
||||
}
|
||||
|
||||
/// Resume an existing Claude Code session by ID with streaming output
|
||||
@@ -1026,12 +909,8 @@ pub async fn resume_claude_code(
|
||||
"--dangerously-skip-permissions".to_string(),
|
||||
];
|
||||
|
||||
if should_use_sidecar(&claude_path) {
|
||||
spawn_claude_sidecar(app, args, prompt, model, project_path).await
|
||||
} else {
|
||||
let cmd = create_system_command(&claude_path, args, &project_path);
|
||||
spawn_claude_process(app, cmd, prompt, model, project_path).await
|
||||
}
|
||||
let cmd = create_system_command(&claude_path, args, &project_path);
|
||||
spawn_claude_process(app, cmd, prompt, model, project_path).await
|
||||
}
|
||||
|
||||
/// Cancel the currently running Claude Code execution
|
||||
@@ -1348,144 +1227,6 @@ async fn spawn_claude_process(app: AppHandle, mut cmd: Command, prompt: String,
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Helper function to spawn Claude sidecar process and handle streaming
|
||||
async fn spawn_claude_sidecar(
|
||||
app: AppHandle,
|
||||
args: Vec<String>,
|
||||
prompt: String,
|
||||
model: String,
|
||||
project_path: String,
|
||||
) -> Result<(), String> {
|
||||
use std::sync::Mutex;
|
||||
|
||||
// Create the sidecar command
|
||||
let sidecar_cmd = create_sidecar_command(&app, args, &project_path)?;
|
||||
|
||||
// Spawn the sidecar process
|
||||
let (mut rx, child) = sidecar_cmd
|
||||
.spawn()
|
||||
.map_err(|e| format!("Failed to spawn Claude sidecar: {}", e))?;
|
||||
|
||||
// Get the child PID for logging
|
||||
let pid = child.pid();
|
||||
log::info!("Spawned Claude sidecar process with PID: {:?}", pid);
|
||||
|
||||
// We'll extract the session ID from Claude's init message
|
||||
let session_id_holder: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
|
||||
let run_id_holder: Arc<Mutex<Option<i64>>> = Arc::new(Mutex::new(None));
|
||||
|
||||
// Register with ProcessRegistry
|
||||
let registry = app.state::<crate::process::ProcessRegistryState>();
|
||||
let registry_clone = registry.0.clone();
|
||||
let project_path_clone = project_path.clone();
|
||||
let prompt_clone = prompt.clone();
|
||||
let model_clone = model.clone();
|
||||
|
||||
// Spawn task to read events from sidecar
|
||||
let app_handle = app.clone();
|
||||
let session_id_holder_clone = session_id_holder.clone();
|
||||
let run_id_holder_clone = run_id_holder.clone();
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
while let Some(event) = rx.recv().await {
|
||||
match event {
|
||||
CommandEvent::Stdout(line_bytes) => {
|
||||
let line = String::from_utf8_lossy(&line_bytes);
|
||||
let line_str = line.trim_end_matches('\n').trim_end_matches('\r');
|
||||
|
||||
if !line_str.is_empty() {
|
||||
log::debug!("Claude sidecar stdout: {}", line_str);
|
||||
|
||||
// Parse the line to check for init message with session ID
|
||||
if let Ok(msg) = serde_json::from_str::<serde_json::Value>(line_str) {
|
||||
if msg["type"] == "system" && msg["subtype"] == "init" {
|
||||
if let Some(claude_session_id) = msg["session_id"].as_str() {
|
||||
let mut session_id_guard = session_id_holder_clone.lock().unwrap();
|
||||
if session_id_guard.is_none() {
|
||||
*session_id_guard = Some(claude_session_id.to_string());
|
||||
log::info!("Extracted Claude session ID: {}", claude_session_id);
|
||||
|
||||
// Register with ProcessRegistry using Claude's session ID
|
||||
match registry_clone.register_claude_session(
|
||||
claude_session_id.to_string(),
|
||||
pid,
|
||||
project_path_clone.clone(),
|
||||
prompt_clone.clone(),
|
||||
model_clone.clone(),
|
||||
) {
|
||||
Ok(run_id) => {
|
||||
log::info!("Registered Claude sidecar session with run_id: {}", run_id);
|
||||
let mut run_id_guard = run_id_holder_clone.lock().unwrap();
|
||||
*run_id_guard = Some(run_id);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to register Claude sidecar session: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Store live output in registry if we have a run_id
|
||||
if let Some(run_id) = *run_id_holder_clone.lock().unwrap() {
|
||||
let _ = registry_clone.append_live_output(run_id, line_str);
|
||||
}
|
||||
|
||||
// Emit the line to the frontend with session isolation if we have session ID
|
||||
if let Some(ref session_id) = *session_id_holder_clone.lock().unwrap() {
|
||||
let _ = app_handle.emit(&format!("claude-output:{}", session_id), line_str);
|
||||
}
|
||||
// Also emit to the generic event for backward compatibility
|
||||
let _ = app_handle.emit("claude-output", line_str);
|
||||
}
|
||||
}
|
||||
CommandEvent::Stderr(line_bytes) => {
|
||||
let line = String::from_utf8_lossy(&line_bytes);
|
||||
let line_str = line.trim_end_matches('\n').trim_end_matches('\r');
|
||||
|
||||
if !line_str.is_empty() {
|
||||
log::error!("Claude sidecar stderr: {}", line_str);
|
||||
|
||||
// Emit error lines to the frontend with session isolation if we have session ID
|
||||
if let Some(ref session_id) = *session_id_holder_clone.lock().unwrap() {
|
||||
let _ = app_handle.emit(&format!("claude-error:{}", session_id), line_str);
|
||||
}
|
||||
// Also emit to the generic event for backward compatibility
|
||||
let _ = app_handle.emit("claude-error", line_str);
|
||||
}
|
||||
}
|
||||
CommandEvent::Terminated(payload) => {
|
||||
log::info!("Claude sidecar process terminated with payload: {:?}", payload);
|
||||
|
||||
// Add a small delay to ensure all messages are processed
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
||||
|
||||
let success = payload.code.unwrap_or(-1) == 0;
|
||||
|
||||
if let Some(ref session_id) = *session_id_holder_clone.lock().unwrap() {
|
||||
let _ = app_handle.emit(&format!("claude-complete:{}", session_id), success);
|
||||
}
|
||||
// Also emit to the generic event for backward compatibility
|
||||
let _ = app_handle.emit("claude-complete", success);
|
||||
|
||||
// Unregister from ProcessRegistry if we have a run_id
|
||||
if let Some(run_id) = *run_id_holder_clone.lock().unwrap() {
|
||||
let _ = registry_clone.unregister_process(run_id);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
_ => {
|
||||
// Handle other event types if needed
|
||||
log::debug!("Claude sidecar event: {:?}", event);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Lists files and directories in a given path
|
||||
#[tauri::command]
|
||||
|
@@ -47,19 +47,5 @@
|
||||
"shell": {
|
||||
"open": true
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.png"
|
||||
],
|
||||
"externalBin": [
|
||||
"binaries/claude-code"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@@ -6,7 +6,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { api, type ClaudeInstallation } from "@/lib/api";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { CheckCircle, Package, HardDrive, Settings } from "lucide-react";
|
||||
import { CheckCircle, HardDrive, Settings } from "lucide-react";
|
||||
|
||||
interface ClaudeVersionSelectorProps {
|
||||
/**
|
||||
@@ -37,7 +37,7 @@ interface ClaudeVersionSelectorProps {
|
||||
|
||||
/**
|
||||
* ClaudeVersionSelector component for selecting Claude Code installations
|
||||
* Supports bundled sidecar, system installations, and user preferences
|
||||
* Supports system installations and user preferences
|
||||
*
|
||||
* @example
|
||||
* <ClaudeVersionSelector
|
||||
@@ -108,8 +108,6 @@ export const ClaudeVersionSelector: React.FC<ClaudeVersionSelectorProps> = ({
|
||||
|
||||
const getInstallationIcon = (installation: ClaudeInstallation) => {
|
||||
switch (installation.installation_type) {
|
||||
case "Bundled":
|
||||
return <Package className="h-4 w-4" />;
|
||||
case "System":
|
||||
return <HardDrive className="h-4 w-4" />;
|
||||
case "Custom":
|
||||
@@ -121,8 +119,6 @@ export const ClaudeVersionSelector: React.FC<ClaudeVersionSelectorProps> = ({
|
||||
|
||||
const getInstallationTypeColor = (installation: ClaudeInstallation) => {
|
||||
switch (installation.installation_type) {
|
||||
case "Bundled":
|
||||
return "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300";
|
||||
case "System":
|
||||
return "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300";
|
||||
case "Custom":
|
||||
@@ -165,7 +161,6 @@ export const ClaudeVersionSelector: React.FC<ClaudeVersionSelectorProps> = ({
|
||||
);
|
||||
}
|
||||
|
||||
const bundledInstallations = installations.filter(i => i.installation_type === "Bundled");
|
||||
const systemInstallations = installations.filter(i => i.installation_type === "System");
|
||||
const customInstallations = installations.filter(i => i.installation_type === "Custom");
|
||||
|
||||
@@ -177,7 +172,7 @@ export const ClaudeVersionSelector: React.FC<ClaudeVersionSelectorProps> = ({
|
||||
Claude Code Installation
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Choose your preferred Claude Code installation. Bundled version is recommended for best compatibility.
|
||||
Choose your preferred Claude Code installation.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
@@ -199,28 +194,6 @@ export const ClaudeVersionSelector: React.FC<ClaudeVersionSelectorProps> = ({
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{bundledInstallations.length > 0 && (
|
||||
<>
|
||||
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground">Bundled</div>
|
||||
{bundledInstallations.map((installation) => (
|
||||
<SelectItem key={installation.path} value={installation.path}>
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
{getInstallationIcon(installation)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium">Claude Code (Bundled)</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{installation.version || "Version unknown"} • {installation.source}
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant="secondary" className={cn("text-xs", getInstallationTypeColor(installation))}>
|
||||
Recommended
|
||||
</Badge>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
{systemInstallations.length > 0 && (
|
||||
<>
|
||||
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground">System Installations</div>
|
||||
|
@@ -429,7 +429,7 @@ export const Settings: React.FC<SettingsProps> = ({
|
||||
<div>
|
||||
<Label className="text-sm font-medium mb-2 block">Claude Code Installation</Label>
|
||||
<p className="text-xs text-muted-foreground mb-4">
|
||||
Select which Claude Code installation to use. Bundled version is recommended for best compatibility.
|
||||
Select which Claude Code installation to use.
|
||||
</p>
|
||||
</div>
|
||||
<ClaudeVersionSelector
|
||||
|
@@ -99,14 +99,14 @@ export interface FileEntry {
|
||||
* Represents a Claude installation found on the system
|
||||
*/
|
||||
export interface ClaudeInstallation {
|
||||
/** Full path to the Claude binary (or "claude-code" for sidecar) */
|
||||
/** Full path to the Claude binary */
|
||||
path: string;
|
||||
/** Version string if available */
|
||||
version?: string;
|
||||
/** Source of discovery (e.g., "nvm", "system", "homebrew", "which", "bundled") */
|
||||
/** Source of discovery (e.g., "nvm", "system", "homebrew", "which") */
|
||||
source: string;
|
||||
/** Type of installation */
|
||||
installation_type: "Bundled" | "System" | "Custom";
|
||||
installation_type: "System" | "Custom";
|
||||
}
|
||||
|
||||
// Agent API types
|
||||
|
Reference in New Issue
Block a user