diff --git a/.github/workflows/release-standalone.yml b/.github/workflows/release-standalone.yml new file mode 100644 index 0000000000..36dc585970 --- /dev/null +++ b/.github/workflows/release-standalone.yml @@ -0,0 +1,179 @@ +name: Release Standalone CLI + +on: + push: + tags: + - 'v*.*.*' + workflow_dispatch: + inputs: + version: + description: 'Version to release (e.g., v3.32.6)' + required: true + type: string + +permissions: + contents: write + +jobs: + build: + name: Build ${{ matrix.platform }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + include: + - os: macos-13 + platform: darwin-x64 + arch: x64 + - os: macos-14 + platform: darwin-arm64 + arch: arm64 + - os: ubuntu-latest + platform: linux-x64 + arch: x64 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: '1.24' + cache-dependency-path: cli/go.sum + + - name: Install dependencies + run: npm ci + + - name: Install webview dependencies + run: cd webview-ui && npm ci + + - name: Download Node.js binaries + run: npm run download-node + + - name: Build CLI binaries + run: npm run compile-cli + + - name: Build standalone CLI package + run: npm run compile-standalone-cli + env: + NODE_ENV: production + + - name: Get version + id: version + run: | + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + echo "version=${{ inputs.version }}" >> $GITHUB_OUTPUT + else + echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT + fi + + - name: Rename package + run: | + cd dist-standalone + mv standalone-cli.zip cline-${{ steps.version.outputs.version }}-${{ matrix.platform }}.tar.gz + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: cline-${{ matrix.platform }} + path: dist-standalone/cline-${{ steps.version.outputs.version }}-${{ matrix.platform }}.tar.gz + retention-days: 1 + + release: + name: Create Release + needs: build + runs-on: ubuntu-latest + environment: publish + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + + - name: Get version + id: version + run: | + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + echo "version=${{ inputs.version }}" >> $GITHUB_OUTPUT + else + echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT + fi + + - name: Display structure + run: ls -R artifacts/ + + - name: Create Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ steps.version.outputs.version }} + name: Cline CLI ${{ steps.version.outputs.version }} + draft: false + prerelease: false + generate_release_notes: true + files: | + artifacts/cline-darwin-x64/cline-${{ steps.version.outputs.version }}-darwin-x64.tar.gz + artifacts/cline-darwin-arm64/cline-${{ steps.version.outputs.version }}-darwin-arm64.tar.gz + artifacts/cline-linux-x64/cline-${{ steps.version.outputs.version }}-linux-x64.tar.gz + body: | + ## Installation + + Install Cline CLI with a single command: + + ```bash + curl -fsSL https://raw.githubusercontent.com/cline/cline/main/scripts/install.sh | bash + ``` + + ### Platform-Specific Downloads + + - **macOS (Intel)**: `cline-${{ steps.version.outputs.version }}-darwin-x64.tar.gz` + - **macOS (Apple Silicon)**: `cline-${{ steps.version.outputs.version }}-darwin-arm64.tar.gz` + - **Linux (x64)**: `cline-${{ steps.version.outputs.version }}-linux-x64.tar.gz` + + ### Manual Installation + + 1. Download the appropriate package for your platform + 2. Extract: `tar -xzf cline-*.tar.gz` + 3. Move to installation directory: `mv cline-* ~/.cline` + 4. Add to PATH: `export PATH="$HOME/.cline/bin:$PATH"` + + ### What's Included + + - ✅ Node.js v22.15.0 (bundled) + - ✅ Cline CLI binary + - ✅ Cline Host bridge + - ✅ Cline Core (TypeScript compiled) + - ✅ All dependencies + + ### Getting Started + + ```bash + # Verify installation + cline version + + # Sign in + cline auth login + + # Get help + cline --help + ``` + + ### Documentation + + - [Installation Guide](https://docs.cline.bot/getting-started/installing-cline) + - [CLI Documentation](https://docs.cline.bot/exploring-clines-tools/cline-tools-guide) + - [GitHub Repository](https://github.com/cline/cline) + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }} + ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }} + CLINE_ENVIRONMENT: production diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a34cea6cc5..068e8ef537 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -187,8 +187,20 @@ jobs: if: steps.webview-cache.outputs.cache-hit != 'true' run: cd webview-ui && npm ci - - name: Compile standalone - run: npm run compile-standalone + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: '1.24' + cache-dependency-path: cli/go.sum + + - name: Download Node.js binaries + run: npm run download-node + + - name: Build CLI binaries + run: npm run compile-cli + + - name: Compile standalone CLI + run: npm run compile-standalone-cli - name: Install testing platform dependencies if: steps.testing-platform-cache.outputs.cache-hit != 'true' @@ -244,11 +256,14 @@ jobs: - name: Download test platform integration core coverage artifact uses: actions/download-artifact@v4 + continue-on-error: true + id: download-integration-coverage with: name: test-platform-integration-core-coverage path: integration-core-coverage-reports - name: Upload core integration tests coverage to Qlty + if: steps.download-integration-coverage.outcome == 'success' uses: qltysh/qlty-action/coverage@v2 with: token: ${{ secrets.QLTY_COVERAGE_TOKEN }} diff --git a/cli/pkg/cli/global/cline-clients.go b/cli/pkg/cli/global/cline-clients.go index 4515844379..4d3f207a3c 100644 --- a/cli/pkg/cli/global/cline-clients.go +++ b/cli/pkg/cli/global/cline-clients.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "os/exec" + "path" "time" "github.com/cline/cli/pkg/common" @@ -211,8 +212,16 @@ func (c *ClineClients) EnsureInstanceAtAddress(ctx context.Context, address stri func startClineHost(hostPort, corePort int) (*exec.Cmd, error) { fmt.Printf("Starting cline-host on port %d\n", hostPort) + // Get the directory where the cline binary is located + execPath, err := os.Executable() + if err != nil { + return nil, fmt.Errorf("failed to get executable path: %w", err) + } + binDir := path.Dir(execPath) + clineHostPath := path.Join(binDir, "cline-host") + // Start the cline-host process - cmd := exec.Command("./cli/bin/cline-host", + cmd := exec.Command(clineHostPath, "--verbose", "--port", fmt.Sprintf("%d", hostPort)) @@ -227,6 +236,16 @@ func startClineHost(hostPort, corePort int) (*exec.Cmd, error) { func startClineCore(corePort, hostPort int) (*exec.Cmd, error) { fmt.Printf("Starting cline-core on port %d (with hostbridge on %d)\n", corePort, hostPort) + // Get paths relative to the cline binary location + execPath, err := os.Executable() + if err != nil { + return nil, fmt.Errorf("failed to get executable path: %w", err) + } + binDir := path.Dir(execPath) + installDir := path.Dir(binDir) + nodePath := path.Join(binDir, "node") + clineCorePath := path.Join(installDir, "cline-core.js") + // Create port-tagged log file in OS temp directory with full address logFileName := fmt.Sprintf("cline-core-debug-localhost-%d.log", corePort) logFilePath := fmt.Sprintf("%s/%s", os.TempDir(), logFileName) @@ -235,28 +254,29 @@ func startClineCore(corePort, hostPort int) (*exec.Cmd, error) { return nil, fmt.Errorf("failed to create log file: %w", err) } - // Start the cline-core process with --config flag instead of CLINE_DIR env var - args := []string{"cline-core.js", + // Start the cline-core process with --config flag + args := []string{clineCorePath, "--port", fmt.Sprintf("%d", corePort), "--host-bridge-port", fmt.Sprintf("%d", hostPort), "--config", Config.ConfigPath} - fmt.Printf("DEBUG: Starting cline-core with command: node %v\n", args) - fmt.Printf("DEBUG: Working directory: ./dist-standalone\n") + fmt.Printf("DEBUG: Starting cline-core with command: %s %v\n", nodePath, args) + fmt.Printf("DEBUG: Working directory: %s\n", installDir) fmt.Printf("DEBUG: Config path: %s\n", Config.ConfigPath) - cmd := exec.Command("node", args...) + cmd := exec.Command(nodePath, args...) - // Set working directory to dist-standalone (relative to project root) - cmd.Dir = "./dist-standalone" + // Set working directory to installation root + cmd.Dir = installDir // Redirect stdout and stderr to log file cmd.Stdout = logFile cmd.Stderr = logFile - // Set environment variables (removed CLINE_DIR) + // Set environment variables with NODE_PATH for node_modules env := os.Environ() env = append(env, + fmt.Sprintf("NODE_PATH=%s", path.Join(installDir, "node_modules")), "GRPC_TRACE=all", "GRPC_VERBOSITY=DEBUG", "NODE_ENV=development", diff --git a/package.json b/package.json index 2738579580..35abba5576 100644 --- a/package.json +++ b/package.json @@ -295,9 +295,13 @@ "vscode:prepublish": "npm run package", "compile": "npm run check-types && npm run lint && node esbuild.mjs", "compile-standalone": "npm run check-types && npm run lint && node esbuild.mjs --standalone", + "compile-standalone-cli": "npm run check-types && npm run lint && node esbuild.mjs --standalone", "compile-cli": "scripts/build-cli.sh", + "download-node": "node scripts/download-node.mjs", + "test:install": "bash scripts/test-install.sh", "dev:cli:watch": "node scripts/dev-cli-watch.mjs", "postcompile-standalone": "node scripts/package-standalone.mjs", + "postcompile-standalone-cli": "node scripts/package-standalone.mjs --target=cli", "watch": "npm-run-all -p watch:*", "watch:esbuild": "node esbuild.mjs --watch", "watch:tsc": "tsc --noEmit --watch --project tsconfig.json", diff --git a/scripts/download-node.mjs b/scripts/download-node.mjs new file mode 100755 index 0000000000..fc8b3a84df --- /dev/null +++ b/scripts/download-node.mjs @@ -0,0 +1,187 @@ +#!/usr/bin/env node + +/** + * Download Node.js binaries for all target platforms + * This script downloads official Node.js binaries from nodejs.org + * and extracts them to dist-standalone/node-binaries/ + */ + +import fs from "fs" +import https from "https" +import path from "path" +import { pipeline } from "stream/promises" +import tar from "tar" +import { createGunzip } from "zlib" + +const NODE_VERSION = "22.15.0" +const OUTPUT_DIR = "dist-standalone/node-binaries" + +// Platform configurations +const PLATFORMS = [ + { + name: "darwin-x64", + nodeArch: "darwin-x64", + url: `https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-darwin-x64.tar.gz`, + }, + { + name: "darwin-arm64", + nodeArch: "darwin-arm64", + url: `https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-darwin-arm64.tar.gz`, + }, + { + name: "linux-x64", + nodeArch: "linux-x64", + url: `https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.gz`, + }, +] + +/** + * Download a file from a URL + */ +async function downloadFile(url, destPath) { + return new Promise((resolve, reject) => { + console.log(` Downloading: ${url}`) + const file = fs.createWriteStream(destPath) + + https + .get(url, (response) => { + if (response.statusCode === 302 || response.statusCode === 301) { + // Handle redirect + return downloadFile(response.headers.location, destPath).then(resolve).catch(reject) + } + + if (response.statusCode !== 200) { + reject(new Error(`Failed to download: ${response.statusCode} ${response.statusMessage}`)) + return + } + + response.pipe(file) + + file.on("finish", () => { + file.close() + resolve() + }) + }) + .on("error", (err) => { + fs.unlink(destPath, () => {}) // Delete the file on error + reject(err) + }) + + file.on("error", (err) => { + fs.unlink(destPath, () => {}) // Delete the file on error + reject(err) + }) + }) +} + +/** + * Extract a tar.gz file + */ +async function extractTarGz(tarPath, destDir) { + console.log(` Extracting to: ${destDir}`) + + return pipeline( + fs.createReadStream(tarPath), + createGunzip(), + tar.extract({ + cwd: destDir, + strip: 1, // Remove the top-level directory from the archive + }), + ) +} + +/** + * Download and extract Node.js for a specific platform + */ +async function downloadNodeForPlatform(platform) { + console.log(`\n📦 Processing ${platform.name}...`) + + const platformDir = path.join(OUTPUT_DIR, platform.name) + const tarPath = path.join(OUTPUT_DIR, `node-${platform.name}.tar.gz`) + + // Create output directory + fs.mkdirSync(platformDir, { recursive: true }) + + try { + // Download + await downloadFile(platform.url, tarPath) + console.log(` ✓ Downloaded`) + + // Extract + await extractTarGz(tarPath, platformDir) + console.log(` ✓ Extracted`) + + // Verify the binary exists + const binaryPath = path.join(platformDir, "bin", "node") + if (!fs.existsSync(binaryPath)) { + throw new Error(`Binary not found at ${binaryPath}`) + } + + // Make binary executable + fs.chmodSync(binaryPath, 0o755) + console.log(` ✓ Binary ready: ${binaryPath}`) + + // Clean up tar file + fs.unlinkSync(tarPath) + console.log(` ✓ Cleaned up`) + + return true + } catch (error) { + console.error(` ✗ Failed: ${error.message}`) + throw error + } +} + +/** + * Main function + */ +async function main() { + console.log("🚀 Node.js Binary Downloader") + console.log(` Version: ${NODE_VERSION}`) + console.log(` Output: ${OUTPUT_DIR}`) + + // Create output directory + fs.mkdirSync(OUTPUT_DIR, { recursive: true }) + + // Download for all platforms + const results = [] + for (const platform of PLATFORMS) { + try { + await downloadNodeForPlatform(platform) + results.push({ platform: platform.name, success: true }) + } catch (error) { + results.push({ platform: platform.name, success: false, error: error.message }) + } + } + + // Print summary + console.log("\n" + "=".repeat(50)) + console.log("📊 Summary:") + console.log("=".repeat(50)) + + let successCount = 0 + for (const result of results) { + const status = result.success ? "✅" : "❌" + console.log(`${status} ${result.platform}`) + if (result.success) { + successCount++ + } else { + console.log(` Error: ${result.error}`) + } + } + + console.log("=".repeat(50)) + console.log(`✓ ${successCount}/${PLATFORMS.length} platforms successful`) + + if (successCount < PLATFORMS.length) { + process.exit(1) + } + + console.log("\n✅ All Node.js binaries downloaded successfully!") +} + +// Run the script +main().catch((error) => { + console.error("\n❌ Fatal error:", error) + process.exit(1) +}) diff --git a/scripts/install.sh b/scripts/install.sh new file mode 100755 index 0000000000..61f1431806 --- /dev/null +++ b/scripts/install.sh @@ -0,0 +1,266 @@ +#!/bin/bash +# Cline Installation Script +# Usage: curl -fsSL https://raw.githubusercontent.com/cline/cline/main/scripts/install.sh | bash + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Configuration +INSTALL_DIR="${CLINE_INSTALL_DIR:-$HOME/.cline/cli}" +GITHUB_REPO="cline/cline" +RELEASE_TAG="${CLINE_VERSION:-latest}" + +# Detect OS and architecture +detect_platform() { + local os=$(uname -s | tr '[:upper:]' '[:lower:]') + local arch=$(uname -m) + + case "$os" in + darwin) + case "$arch" in + x86_64) echo "darwin-x64" ;; + arm64) echo "darwin-arm64" ;; + *) echo "unsupported" ;; + esac + ;; + linux) + case "$arch" in + x86_64) echo "linux-x64" ;; + *) echo "unsupported" ;; + esac + ;; + *) + echo "unsupported" + ;; + esac +} + +# Print colored message +print_message() { + local color=$1 + shift + echo -e "${color}$@${NC}" +} + +# Print error and exit +error_exit() { + print_message "$RED" "Error: $1" + exit 1 +} + +# Check if command exists +command_exists() { + command -v "$1" >/dev/null 2>&1 +} + +# Check prerequisites +check_prerequisites() { + print_message "$BLUE" "Checking prerequisites..." + + if ! command_exists curl; then + error_exit "curl is required but not installed. Please install curl and try again." + fi + + if ! command_exists tar; then + error_exit "tar is required but not installed. Please install tar and try again." + fi + + print_message "$GREEN" "✓ Prerequisites satisfied" +} + +# Get download URL for the release +get_download_url() { + local platform=$1 + local api_url + + if [ "$RELEASE_TAG" = "latest" ]; then + api_url="https://api.github.com/repos/$GITHUB_REPO/releases/latest" + else + api_url="https://api.github.com/repos/$GITHUB_REPO/releases/tags/$RELEASE_TAG" + fi + + print_message "$BLUE" "Fetching release information..." >&2 + + local release_data=$(curl -fsSL "$api_url") + local download_url=$(echo "$release_data" | grep -o "\"browser_download_url\": \"[^\"]*${platform}[^\"]*\"" | head -1 | cut -d'"' -f4) + + if [ -z "$download_url" ]; then + error_exit "Could not find download URL for platform: $platform" + fi + + echo "$download_url" +} + +# Download and extract Cline +install_cline() { + local platform=$1 + local download_url=$2 + + print_message "$BLUE" "Installing Cline to $INSTALL_DIR..." + + # Create temporary directory + local tmp_dir=$(mktemp -d) + trap "rm -rf $tmp_dir" EXIT + + # Download package + print_message "$BLUE" "Downloading Cline..." + local package_file="$tmp_dir/cline.tar.gz" + + if ! curl -fsSL -o "$package_file" "$download_url"; then + error_exit "Failed to download Cline package" + fi + + # Remove existing installation + if [ -d "$INSTALL_DIR" ]; then + print_message "$YELLOW" "Removing existing installation..." + rm -rf "$INSTALL_DIR" + fi + + # Create installation directory + mkdir -p "$INSTALL_DIR" + + # Extract package + print_message "$BLUE" "Extracting package..." + if ! tar -xzf "$package_file" -C "$INSTALL_DIR" --strip-components=0; then + error_exit "Failed to extract package" + fi + + # Make binaries executable + chmod +x "$INSTALL_DIR/bin/"* + + # Copy platform-specific native modules to node_modules + if [ -d "$INSTALL_DIR/binaries/$platform/node_modules" ]; then + print_message "$BLUE" "Installing platform-specific native modules..." + if ! cp -r "$INSTALL_DIR/binaries/$platform/node_modules/"* "$INSTALL_DIR/node_modules/"; then + error_exit "Failed to install platform-specific native modules" + fi + print_message "$GREEN" "✓ Native modules installed" + fi + print_message "$GREEN" "✓ Cline installed successfully" +} + +# Configure PATH +configure_path() { + local bin_dir="$INSTALL_DIR/bin" + local shell_rc="" + + # Detect shell configuration file + if [ -n "$BASH_VERSION" ]; then + if [ -f "$HOME/.bashrc" ]; then + shell_rc="$HOME/.bashrc" + elif [ -f "$HOME/.bash_profile" ]; then + shell_rc="$HOME/.bash_profile" + fi + elif [ -n "$ZSH_VERSION" ]; then + shell_rc="$HOME/.zshrc" + fi + + if [ -z "$shell_rc" ]; then + print_message "$YELLOW" "⚠ Could not detect shell configuration file" + print_message "$YELLOW" "Please manually add the following to your shell configuration:" + print_message "$YELLOW" " export PATH=\"$bin_dir:\$PATH\"" + return + fi + + # Check if PATH is already configured + if grep -q "CLINE_INSTALL_DIR" "$shell_rc" 2>/dev/null; then + print_message "$GREEN" "✓ PATH already configured in $shell_rc" + return + fi + + # Add to PATH + print_message "$BLUE" "Configuring PATH in $shell_rc..." + cat >> "$shell_rc" << EOF + +# Cline CLI +export PATH="$bin_dir:\$PATH" +EOF + + print_message "$GREEN" "✓ PATH configured in $shell_rc" + print_message "$YELLOW" "⚠ Please restart your shell or run: source $shell_rc" +} + +# Verify installation +verify_installation() { + print_message "$BLUE" "Verifying installation..." + + local cline_bin="$INSTALL_DIR/bin/cline" + + if [ ! -f "$cline_bin" ]; then + error_exit "Installation verification failed: cline binary not found" + fi + + if [ ! -x "$cline_bin" ]; then + error_exit "Installation verification failed: cline binary not executable" + fi + + # Check version (the binary now handles service management internally) + local version_output=$("$cline_bin" version 2>&1 || true) + if [ -z "$version_output" ]; then + error_exit "Installation verification failed: could not get version" + fi + + print_message "$GREEN" "✓ Installation verified" +} + +# Print success message +print_success() { + echo "" + print_message "$GREEN" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + print_message "$GREEN" " Cline installed successfully! 🎉" + print_message "$GREEN" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "" + print_message "$BLUE" "Installation directory: $INSTALL_DIR" + echo "" + print_message "$YELLOW" "To get started:" + print_message "$YELLOW" " 1. Restart your shell or run: source ~/.zshrc (or ~/.bashrc)" + print_message "$YELLOW" " 2. Run: cline --help" + print_message "$YELLOW" " 3. Sign in: cline auth login" + echo "" + print_message "$BLUE" "Documentation: https://docs.cline.bot" + echo "" +} + +# Main installation flow +main() { + echo "" + print_message "$BLUE" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + print_message "$BLUE" " Cline Installation Script" + print_message "$BLUE" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "" + + # Detect platform + local platform=$(detect_platform) + if [ "$platform" = "unsupported" ]; then + error_exit "Unsupported platform: $(uname -s) $(uname -m)" + fi + print_message "$GREEN" "✓ Detected platform: $platform" + + # Check prerequisites + check_prerequisites + + # Get download URL + local download_url=$(get_download_url "$platform") + print_message "$GREEN" "✓ Found release: $download_url" + + # Install Cline + install_cline "$platform" "$download_url" + + # Configure PATH + configure_path + + # Verify installation + verify_installation + + # Print success message + print_success +} + +# Run main function +main "$@" diff --git a/scripts/package-standalone.mjs b/scripts/package-standalone.mjs index 20873ad460..78c636b456 100755 --- a/scripts/package-standalone.mjs +++ b/scripts/package-standalone.mjs @@ -13,6 +13,8 @@ import { rmrf } from "./file-utils.mjs" const BUILD_DIR = "dist-standalone" const BINARIES_DIR = `${BUILD_DIR}/binaries` const RUNTIME_DEPS_DIR = "standalone/runtime-files" +const NODE_BINARIES_DIR = `${BUILD_DIR}/node-binaries` +const CLI_BINARIES_DIR = "cli/bin" const IS_DEBUG_BUILD = process.env.IS_DEBUG_BUILD === "true" // This should match the node version packaged with the JetBrains plugin. @@ -28,15 +30,56 @@ const SUPPORTED_BINARY_MODULES = ["better-sqlite3"] const UNIVERSAL_BUILD = !process.argv.includes("-s") const IS_VERBOSE = process.argv.includes("-v") || process.argv.includes("--verbose") +// Parse --target flag (e.g., --target=cli) +// Default behavior is JetBrains build (no binaries) +// Use --target=cli for standalone CLI build (with binaries) +const targetArg = process.argv.find((arg) => arg.startsWith("--target=")) +const BUILD_TARGET = targetArg ? targetArg.split("=")[1] : "jetbrains" +const IS_CLI_BUILD = BUILD_TARGET === "cli" + +// Detect current platform +function getCurrentPlatform() { + const platform = os.platform() + const arch = os.arch() + + if (platform === "darwin") { + return arch === "arm64" ? "darwin-arm64" : "darwin-x64" + } else if (platform === "linux") { + return "linux-x64" + } else if (platform === "win32") { + return "win-x64" + } + throw new Error(`Unsupported platform: ${platform}-${arch}`) +} + async function main() { + console.log(`🚀 Building Cline ${IS_CLI_BUILD ? "Standalone CLI" : "JetBrains"} Package\n`) + + // Step 1: Install Node.js dependencies await installNodeDependencies() + + // Step 2: Copy Node.js binary (only for CLI builds) + // Step 3: Copy CLI binaries (only for CLI builds) + // Step 4: Create VERSION file (only for CLI builds) + if (IS_CLI_BUILD) { + await copyNodeBinary() + await copyCliBinaries() + await createVersionFile() + } + + // Step 6: Package platform-specific binary modules if (UNIVERSAL_BUILD) { - console.log("Building universal package for all platforms...") + console.log("\nBuilding universal package for all platforms...") await packageAllBinaryDeps() } else { - console.log(`Building package for ${os.platform()}-${os.arch()}...`) + console.log(`\nBuilding package for ${os.platform()}-${os.arch()}...`) } + + // Step 7: Create final package + console.log("\n📦 Creating final package...") await zipDistribution() + + console.log("\n✅ Build complete!") } async function installNodeDependencies() { @@ -54,6 +97,94 @@ async function installNodeDependencies() { fs.renameSync(`${BUILD_DIR}/vscode`, `${BUILD_DIR}/node_modules/vscode`) } +/** + * Copy Node.js binary for the current platform + */ +async function copyNodeBinary() { + const currentPlatform = getCurrentPlatform() + const nodeBinarySource = path.join(NODE_BINARIES_DIR, currentPlatform, "bin", "node") + const nodeBinaryDest = path.join(BUILD_DIR, "bin", "node") + + console.log(`Copying Node.js binary for ${currentPlatform}...`) + + // Check if Node.js binaries exist + if (!fs.existsSync(nodeBinarySource)) { + console.error(`Error: Node.js binary not found at ${nodeBinarySource}`) + console.error(`Please run: npm run download-node`) + process.exit(1) + } + + // Create bin directory + fs.mkdirSync(path.join(BUILD_DIR, "bin"), { recursive: true }) + + // Copy Node.js binary + await cpr(nodeBinarySource, nodeBinaryDest) + + // Make it executable + fs.chmodSync(nodeBinaryDest, 0o755) + + console.log(`✓ Node.js binary copied to ${nodeBinaryDest}`) +} + +/** + * Copy CLI binaries (cline and cline-host) + * The Go binary is named 'cline' and includes service management + */ +async function copyCliBinaries() { + console.log("Copying CLI binaries...") + + const binaries = [ + { source: "cline", dest: "cline" }, + { source: "cline-host", dest: "cline-host" }, + ] + const binDir = path.join(BUILD_DIR, "bin") + + // Create bin directory + fs.mkdirSync(binDir, { recursive: true }) + + for (const { source, dest } of binaries) { + const sourcePath = path.join(CLI_BINARIES_DIR, source) + const destPath = path.join(binDir, dest) + + // Check if binary exists + if (!fs.existsSync(sourcePath)) { + console.error(`Error: CLI binary not found at ${sourcePath}`) + console.error(`Please run: npm run compile-cli`) + process.exit(1) + } + + // Copy binary + await cpr(sourcePath, destPath) + + // Make it executable + fs.chmodSync(destPath, 0o755) + + console.log(`✓ ${source} copied to ${destPath}`) + } +} + +/** + * Create a VERSION file with build metadata + */ +async function createVersionFile() { + const packageJson = JSON.parse(fs.readFileSync("package.json", "utf8")) + const version = packageJson.version + const platform = getCurrentPlatform() + const buildDate = new Date().toISOString() + + const versionInfo = { + version, + platform, + buildDate, + nodeVersion: TARGET_NODE_VERSION, + } + + const versionPath = path.join(BUILD_DIR, "VERSION.txt") + fs.writeFileSync(versionPath, JSON.stringify(versionInfo, null, 2)) + + console.log(`✓ VERSION file created: ${version} (${platform})`) +} + /** * Downloads prebuilt binaries for each platform for the modules that include binaries. It uses `npx prebuild-install` * to download the binary. @@ -106,8 +237,10 @@ async function packageAllBinaryDeps() { } async function zipDistribution() { - // Zip the build directory (excluding any pre-existing output zip). - const zipPath = path.join(BUILD_DIR, "standalone.zip") + // Use different filename for CLI builds + // Default (JetBrains) = standalone.zip, CLI = standalone-cli.zip + const zipFilename = IS_CLI_BUILD ? "standalone-cli.zip" : "standalone.zip" + const zipPath = path.join(BUILD_DIR, zipFilename) const output = fs.createWriteStream(zipPath) const startTime = Date.now() const archive = archiver("zip", { zlib: { level: 6 } }) @@ -125,15 +258,33 @@ async function zipDistribution() { }) archive.pipe(output) + + // Build ignore lists for build directory and extension directory + const ignorePatterns = ["standalone.zip", "standalone-cli.zip"] + const extensionIgnores = ["dist/**"] + + // For JetBrains (default) builds, exclude binaries from both directories + if (!IS_CLI_BUILD) { + // JetBrains provides their own Node.js, so exclude all binaries + ignorePatterns.push( + "bin/**", // Exclude entire bin directory + "node-binaries/**", // Exclude all platform-specific Node.js binaries + ) + extensionIgnores.push( + "cli/bin/**", // Exclude CLI binaries from extension + "node-binaries/**", // Exclude node-binaries from extension + ) + console.log("JetBrains build: Excluding Node.js and CLI binaries (JetBrains provides its own Node.js)") + } + // Add all the files from the standalone build dir. archive.glob("**/*", { cwd: BUILD_DIR, - ignore: ["standalone.zip"], + ignore: ignorePatterns, }) // Exclude the same files as the VCE vscode extension packager. - // Also ignore the dist directory, the build directory for the extension. - const isIgnored = createIsIgnored(["dist/**"]) + const isIgnored = createIsIgnored(extensionIgnores) // Add the whole cline directory under "extension", except the for the ignored files. archive.directory(process.cwd(), "extension", (entry) => { diff --git a/scripts/test-install.sh b/scripts/test-install.sh new file mode 100755 index 0000000000..4fad53c1c7 --- /dev/null +++ b/scripts/test-install.sh @@ -0,0 +1,112 @@ +#!/bin/bash +# Test script for install.sh +# This validates the install script without actually running it + +set -e + +echo "Testing install.sh script..." +echo "" + +# Test 1: Script syntax +echo "Test 1: Script Syntax Verification" +if bash -n scripts/install.sh; then + echo " ✅ PASS: Script syntax is valid" +else + echo " ❌ FAIL: Script has syntax errors" + exit 1 +fi +echo "" + +# Test 2: Check for required functions +echo "Test 2: Required Functions Check" +required_functions=( + "detect_platform" + "print_message" + "error_exit" + "command_exists" + "check_prerequisites" + "get_download_url" + "install_cline" + "configure_path" + "verify_installation" + "print_success" + "main" +) + +for func in "${required_functions[@]}"; do + if grep -q "^$func()" scripts/install.sh || grep -q "^${func} ()" scripts/install.sh; then + echo " ✅ PASS: Function '$func' exists" + else + echo " ❌ FAIL: Function '$func' not found" + exit 1 + fi +done +echo "" + +# Test 3: Check for required variables +echo "Test 3: Required Variables Check" +required_vars=( + "INSTALL_DIR" + "GITHUB_REPO" + "RELEASE_TAG" +) + +for var in "${required_vars[@]}"; do + if grep -q "$var=" scripts/install.sh; then + echo " ✅ PASS: Variable '$var' is defined" + else + echo " ❌ FAIL: Variable '$var' not found" + exit 1 + fi +done +echo "" + +# Test 4: Check for platform support +echo "Test 4: Platform Support Check" +platforms=("darwin-x64" "darwin-arm64" "linux-x64") +for platform in "${platforms[@]}"; do + if grep -q "$platform" scripts/install.sh; then + echo " ✅ PASS: Platform '$platform' supported" + else + echo " ❌ FAIL: Platform '$platform' not found" + exit 1 + fi +done +echo "" + +# Test 5: Check for error handling +echo "Test 5: Error Handling Check" +if grep -q "error_exit" scripts/install.sh && grep -q "set -e" scripts/install.sh; then + echo " ✅ PASS: Error handling present" +else + echo " ❌ FAIL: Error handling missing" + exit 1 +fi +echo "" + +# Test 6: Check for PATH configuration +echo "Test 6: PATH Configuration Check" +if grep -q "export PATH=" scripts/install.sh; then + echo " ✅ PASS: PATH configuration present" +else + echo " ❌ FAIL: PATH configuration missing" + exit 1 +fi +echo "" + +# Test 7: Check for verification step +echo "Test 7: Installation Verification Check" +if grep -q "verify_installation" scripts/install.sh; then + echo " ✅ PASS: Installation verification present" +else + echo " ❌ FAIL: Installation verification missing" + exit 1 +fi +echo "" + +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "All tests passed! ✅" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "" +echo "The install script is ready for use:" +echo " curl -fsSL https://raw.githubusercontent.com/cline/cline/main/scripts/install.sh | bash"