diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index ba96b2a8..74983eff 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -23,6 +23,7 @@ - Fixed final bash tool cards to avoid rendering duplicate full-output truncation paths ([#4819](https://github.com/earendil-works/pi/issues/4819)). - Fixed bash tool truncation line counts to ignore the trailing newline as an extra output line ([#4818](https://github.com/earendil-works/pi/issues/4818)). - Fixed footer home-directory abbreviation to avoid shortening sibling paths that only share the same prefix ([#4878](https://github.com/earendil-works/pi/issues/4878)). +- Fixed macOS Bun release binaries to resolve the native clipboard sidecar so Ctrl+V image paste can load `@mariozechner/clipboard` ([#4307](https://github.com/earendil-works/pi/issues/4307)). ## [0.75.4] - 2026-05-20 diff --git a/packages/coding-agent/src/utils/clipboard-native.ts b/packages/coding-agent/src/utils/clipboard-native.ts index bcdbdfa8..f8aeb547 100644 --- a/packages/coding-agent/src/utils/clipboard-native.ts +++ b/packages/coding-agent/src/utils/clipboard-native.ts @@ -1,4 +1,6 @@ import { createRequire } from "module"; +import { dirname, join } from "path"; +import { pathToFileURL } from "url"; export type ClipboardModule = { setText: (text: string) => Promise; @@ -6,17 +8,25 @@ export type ClipboardModule = { getImageBinary: () => Promise>; }; -const require = createRequire(import.meta.url); -let clipboard: ClipboardModule | null = null; +type ClipboardRequire = (id: string) => unknown; +const moduleRequire = createRequire(import.meta.url); +const executableDirRequire = createRequire(pathToFileURL(join(dirname(process.execPath), "package.json")).href); const hasDisplay = process.platform !== "linux" || Boolean(process.env.DISPLAY || process.env.WAYLAND_DISPLAY); -if (!process.env.TERMUX_VERSION && hasDisplay) { - try { - clipboard = require("@mariozechner/clipboard") as ClipboardModule; - } catch { - clipboard = null; +export function loadClipboardNative( + requires: readonly ClipboardRequire[] = [moduleRequire, executableDirRequire], +): ClipboardModule | null { + for (const requireClipboard of requires) { + try { + return requireClipboard("@mariozechner/clipboard") as ClipboardModule; + } catch { + // Try the next resolution root. + } } + return null; } +const clipboard = !process.env.TERMUX_VERSION && hasDisplay ? loadClipboardNative() : null; + export { clipboard }; diff --git a/packages/coding-agent/test/clipboard-native.test.ts b/packages/coding-agent/test/clipboard-native.test.ts new file mode 100644 index 00000000..a9c0c201 --- /dev/null +++ b/packages/coding-agent/test/clipboard-native.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, test, vi } from "vitest"; +import { type ClipboardModule, loadClipboardNative } from "../src/utils/clipboard-native.ts"; + +type ClipboardRequire = (id: string) => unknown; + +const fakeClipboard: ClipboardModule = { + setText: async () => {}, + hasImage: () => true, + getImageBinary: async () => [1, 2, 3], +}; + +describe("loadClipboardNative", () => { + test("falls back to the next require root", () => { + const primary = vi.fn(() => { + throw new Error("missing from bundled root"); + }); + const fallback = vi.fn(() => fakeClipboard); + + expect(loadClipboardNative([primary, fallback])).toBe(fakeClipboard); + expect(primary).toHaveBeenCalledWith("@mariozechner/clipboard"); + expect(fallback).toHaveBeenCalledWith("@mariozechner/clipboard"); + }); + + test("returns null when no require root can load clipboard", () => { + const missing = vi.fn(() => { + throw new Error("missing"); + }); + + expect(loadClipboardNative([missing])).toBeNull(); + }); +}); diff --git a/scripts/build-binaries.sh b/scripts/build-binaries.sh index c1b7b868..d4ae44be 100755 --- a/scripts/build-binaries.sh +++ b/scripts/build-binaries.sh @@ -4,11 +4,14 @@ # Mirrors .github/workflows/build-binaries.yml # # Usage: -# ./scripts/build-binaries.sh [--skip-deps] [--platform ] +# ./scripts/build-binaries.sh [--skip-install] [--skip-deps] [--skip-build] [--platform ] [--out ] # # Options: +# --skip-install Skip npm ci # --skip-deps Skip installing cross-platform dependencies +# --skip-build Skip npm run build # --platform Build only for specified platform (darwin-arm64, darwin-x64, linux-x64, linux-arm64, windows-x64, windows-arm64) +# --out Output directory (default: packages/coding-agent/binaries) # # Output: # packages/coding-agent/binaries/ @@ -23,19 +26,34 @@ set -euo pipefail cd "$(dirname "$0")/.." +SKIP_INSTALL=false SKIP_DEPS=false +SKIP_BUILD=false PLATFORM="" +OUTPUT_DIR="" while [[ $# -gt 0 ]]; do case $1 in + --skip-install) + SKIP_INSTALL=true + shift + ;; --skip-deps) SKIP_DEPS=true shift ;; + --skip-build) + SKIP_BUILD=true + shift + ;; --platform) PLATFORM="$2" shift 2 ;; + --out) + OUTPUT_DIR="$2" + shift 2 + ;; *) echo "Unknown option: $1" exit 1 @@ -56,8 +74,19 @@ if [[ -n "$PLATFORM" ]]; then esac fi -echo "==> Installing dependencies..." -npm ci --ignore-scripts +if [[ -z "$OUTPUT_DIR" ]]; then + OUTPUT_DIR="packages/coding-agent/binaries" +fi +if [[ "$OUTPUT_DIR" != /* ]]; then + OUTPUT_DIR="$(pwd)/$OUTPUT_DIR" +fi + +if [[ "$SKIP_INSTALL" == "false" ]]; then + echo "==> Installing dependencies..." + npm ci --ignore-scripts +else + echo "==> Skipping npm ci (--skip-install)" +fi if [[ "$SKIP_DEPS" == "false" ]]; then echo "==> Installing cross-platform native bindings..." @@ -66,35 +95,29 @@ if [[ "$SKIP_DEPS" == "false" ]]; then # Use --force to bypass platform checks (os/cpu restrictions in package.json) # Install all in one command to avoid npm removing packages from previous installs npm install --no-save --package-lock=false --force --ignore-scripts \ - @mariozechner/clipboard-darwin-arm64@0.3.2 \ - @mariozechner/clipboard-darwin-x64@0.3.2 \ - @mariozechner/clipboard-linux-x64-gnu@0.3.2 \ - @mariozechner/clipboard-linux-arm64-gnu@0.3.2 \ - @mariozechner/clipboard-win32-x64-msvc@0.3.2 \ - @mariozechner/clipboard-win32-arm64-msvc@0.3.2 \ - @img/sharp-darwin-arm64@0.34.5 \ - @img/sharp-darwin-x64@0.34.5 \ - @img/sharp-linux-x64@0.34.5 \ - @img/sharp-linux-arm64@0.34.5 \ - @img/sharp-win32-x64@0.34.5 \ - @img/sharp-win32-arm64@0.34.5 \ - @img/sharp-libvips-darwin-arm64@1.2.4 \ - @img/sharp-libvips-darwin-x64@1.2.4 \ - @img/sharp-libvips-linux-x64@1.2.4 \ - @img/sharp-libvips-linux-arm64@1.2.4 + @mariozechner/clipboard-darwin-arm64@0.3.6 \ + @mariozechner/clipboard-darwin-x64@0.3.6 \ + @mariozechner/clipboard-linux-x64-gnu@0.3.6 \ + @mariozechner/clipboard-linux-arm64-gnu@0.3.6 \ + @mariozechner/clipboard-win32-x64-msvc@0.3.6 \ + @mariozechner/clipboard-win32-arm64-msvc@0.3.6 else echo "==> Skipping cross-platform native bindings (--skip-deps)" fi -echo "==> Building all packages..." -npm run build +if [[ "$SKIP_BUILD" == "false" ]]; then + echo "==> Building all packages..." + npm run build +else + echo "==> Skipping package build (--skip-build)" +fi echo "==> Building binaries..." cd packages/coding-agent # Clean previous builds -rm -rf binaries -mkdir -p binaries/{darwin-arm64,darwin-x64,linux-x64,linux-arm64,windows-x64,windows-arm64} +rm -rf "$OUTPUT_DIR" +mkdir -p "$OUTPUT_DIR"/{darwin-arm64,darwin-x64,linux-x64,linux-arm64,windows-x64,windows-arm64} # Determine which platforms to build if [[ -n "$PLATFORM" ]]; then @@ -106,9 +129,9 @@ fi for platform in "${PLATFORMS[@]}"; do echo "Building for $platform..." if [[ "$platform" == windows-* ]]; then - bun build --compile --target=bun-$platform ./dist/bun/cli.js --outfile binaries/$platform/pi.exe + bun build --compile --target=bun-$platform ./dist/bun/cli.js --outfile "$OUTPUT_DIR/$platform/pi.exe" else - bun build --compile --target=bun-$platform ./dist/bun/cli.js --outfile binaries/$platform/pi + bun build --compile --target=bun-$platform ./dist/bun/cli.js --outfile "$OUTPUT_DIR/$platform/pi" fi done @@ -116,17 +139,41 @@ echo "==> Creating release archives..." # Copy shared files to each platform directory for platform in "${PLATFORMS[@]}"; do - cp package.json binaries/$platform/ - cp README.md binaries/$platform/ - cp CHANGELOG.md binaries/$platform/ - cp ../../node_modules/@silvia-odwyer/photon-node/photon_rs_bg.wasm binaries/$platform/ - mkdir -p binaries/$platform/theme - cp dist/modes/interactive/theme/*.json binaries/$platform/theme/ - mkdir -p binaries/$platform/assets - cp dist/modes/interactive/assets/* binaries/$platform/assets/ - cp -r dist/core/export-html binaries/$platform/ - cp -r docs binaries/$platform/ - cp -r examples binaries/$platform/ + cp package.json "$OUTPUT_DIR/$platform/" + cp README.md "$OUTPUT_DIR/$platform/" + cp CHANGELOG.md "$OUTPUT_DIR/$platform/" + cp ../../node_modules/@silvia-odwyer/photon-node/photon_rs_bg.wasm "$OUTPUT_DIR/$platform/" + mkdir -p "$OUTPUT_DIR/$platform/theme" + cp dist/modes/interactive/theme/*.json "$OUTPUT_DIR/$platform/theme/" + mkdir -p "$OUTPUT_DIR/$platform/assets" + cp dist/modes/interactive/assets/* "$OUTPUT_DIR/$platform/assets/" + cp -r dist/core/export-html "$OUTPUT_DIR/$platform/" + cp -r docs "$OUTPUT_DIR/$platform/" + cp -r examples "$OUTPUT_DIR/$platform/" + + case "$platform" in + darwin-arm64) + clipboard_native_package="clipboard-darwin-arm64" + ;; + darwin-x64) + clipboard_native_package="clipboard-darwin-x64" + ;; + linux-x64) + clipboard_native_package="clipboard-linux-x64-gnu" + ;; + linux-arm64) + clipboard_native_package="clipboard-linux-arm64-gnu" + ;; + windows-x64) + clipboard_native_package="clipboard-win32-x64-msvc" + ;; + windows-arm64) + clipboard_native_package="clipboard-win32-arm64-msvc" + ;; + esac + mkdir -p "$OUTPUT_DIR/$platform/node_modules/@mariozechner" + cp -r ../../node_modules/@mariozechner/clipboard "$OUTPUT_DIR/$platform/node_modules/@mariozechner/" + cp -r ../../node_modules/@mariozechner/$clipboard_native_package "$OUTPUT_DIR/$platform/node_modules/@mariozechner/" # Copy Windows VT input native helper next to compiled Windows binaries. if [[ "$platform" == windows-* ]]; then @@ -135,47 +182,47 @@ for platform in "${PLATFORMS[@]}"; do else win32_arch_dir="win32-x64" fi - mkdir -p binaries/$platform/native/win32/prebuilds/$win32_arch_dir - cp ../tui/native/win32/prebuilds/$win32_arch_dir/win32-console-mode.node binaries/$platform/native/win32/prebuilds/$win32_arch_dir/ + mkdir -p "$OUTPUT_DIR/$platform/native/win32/prebuilds/$win32_arch_dir" + cp ../tui/native/win32/prebuilds/$win32_arch_dir/win32-console-mode.node "$OUTPUT_DIR/$platform/native/win32/prebuilds/$win32_arch_dir/" fi done # Create archives -cd binaries +cd "$OUTPUT_DIR" for platform in "${PLATFORMS[@]}"; do if [[ "$platform" == windows-* ]]; then # Windows (zip) echo "Creating pi-$platform.zip..." - (cd $platform && zip -r ../pi-$platform.zip .) + (cd "$platform" && zip -r ../pi-$platform.zip .) else # Unix platforms (tar.gz) - use wrapper directory for mise compatibility echo "Creating pi-$platform.tar.gz..." - mv $platform pi && tar -czf pi-$platform.tar.gz pi && mv pi $platform + mv "$platform" pi && tar -czf pi-$platform.tar.gz pi && mv pi "$platform" fi done # Extract archives for easy local testing echo "==> Extracting archives for testing..." for platform in "${PLATFORMS[@]}"; do - rm -rf $platform + rm -rf "$platform" if [[ "$platform" == windows-* ]]; then - mkdir -p $platform && (cd $platform && unzip -q ../pi-$platform.zip) + mkdir -p "$platform" && (cd "$platform" && unzip -q ../pi-$platform.zip) else - tar -xzf pi-$platform.tar.gz && mv pi $platform + tar -xzf pi-$platform.tar.gz && mv pi "$platform" fi done echo "" echo "==> Build complete!" -echo "Archives available in packages/coding-agent/binaries/" +echo "Archives available in $OUTPUT_DIR/" ls -lh *.tar.gz *.zip 2>/dev/null || true echo "" echo "Extracted directories for testing:" for platform in "${PLATFORMS[@]}"; do if [[ "$platform" == windows-* ]]; then - echo " binaries/$platform/pi.exe" + echo " $OUTPUT_DIR/$platform/pi.exe" else - echo " binaries/$platform/pi" + echo " $OUTPUT_DIR/$platform/pi" fi done diff --git a/scripts/local-release.mjs b/scripts/local-release.mjs index 782318bd..3f35e80c 100644 --- a/scripts/local-release.mjs +++ b/scripts/local-release.mjs @@ -2,7 +2,7 @@ import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; -import { dirname, isAbsolute, join, relative, resolve } from "node:path"; +import { isAbsolute, join, relative, resolve } from "node:path"; import { spawnSync } from "node:child_process"; const packages = [ @@ -131,48 +131,25 @@ function currentBinaryPlatform() { throw new Error(`Unsupported binary platform: ${process.platform} ${process.arch}`); } -function copyBinaryAssets(targetDirectory) { - const codingAgentDirectory = join(repoRoot, "packages", "coding-agent"); - const distDirectory = join(codingAgentDirectory, "dist"); - cpSync(join(codingAgentDirectory, "package.json"), join(targetDirectory, "package.json")); - cpSync(join(codingAgentDirectory, "README.md"), join(targetDirectory, "README.md")); - cpSync(join(codingAgentDirectory, "CHANGELOG.md"), join(targetDirectory, "CHANGELOG.md")); - cpSync(join(repoRoot, "node_modules", "@silvia-odwyer", "photon-node", "photon_rs_bg.wasm"), join(targetDirectory, "photon_rs_bg.wasm")); - cpSync(join(distDirectory, "modes", "interactive", "theme"), join(targetDirectory, "theme"), { recursive: true }); - cpSync(join(distDirectory, "modes", "interactive", "assets"), join(targetDirectory, "assets"), { recursive: true }); - cpSync(join(distDirectory, "core", "export-html"), join(targetDirectory, "export-html"), { recursive: true }); - cpSync(join(codingAgentDirectory, "docs"), join(targetDirectory, "docs"), { recursive: true }); - cpSync(join(codingAgentDirectory, "examples"), join(targetDirectory, "examples"), { recursive: true }); -} - -function copyWindowsConsoleModeHelper(targetDirectory, platform) { - if (!platform.startsWith("windows-")) return; - const win32Arch = platform === "windows-arm64" ? "win32-arm64" : "win32-x64"; - const relativeNativePath = join("native", "win32", "prebuilds", win32Arch); - mkdirSync(join(targetDirectory, relativeNativePath), { recursive: true }); - cpSync( - join(repoRoot, "packages", "tui", "native", "win32", "prebuilds", win32Arch, "win32-console-mode.node"), - join(targetDirectory, relativeNativePath, "win32-console-mode.node"), - ); -} - function buildBunBinaryRelease(targetDirectory, archiveDirectory) { if (!commandExists("bun")) { throw new Error("Bun is required for the local binary release build."); } const platform = currentBinaryPlatform(); - mkdirSync(targetDirectory, { recursive: true }); - const executableName = platform.startsWith("windows-") ? "pi.exe" : "pi"; - const executablePath = join(targetDirectory, executableName); - const target = `bun-${platform}`; - run("bun", ["build", "--compile", `--target=${target}`, join(repoRoot, "packages", "coding-agent", "dist", "bun", "cli.js"), "--outfile", executablePath]); - copyBinaryAssets(targetDirectory); - copyWindowsConsoleModeHelper(targetDirectory, platform); - if (platform.startsWith("windows-")) { - run("powershell", ["-NoProfile", "-Command", `Compress-Archive -Path '${join(targetDirectory, "*").replaceAll("'", "''")}' -DestinationPath '${join(archiveDirectory, `pi-${platform}.zip`).replaceAll("'", "''")}' -Force`]); - } else { - run("tar", ["-czf", join(archiveDirectory, `pi-${platform}.tar.gz`), "-C", targetDirectory, "."]); - } + const binaryBuildDirectory = join(archiveDirectory, "binary-build"); + run("./scripts/build-binaries.sh", [ + "--skip-install", + "--skip-deps", + "--skip-build", + "--platform", + platform, + "--out", + binaryBuildDirectory, + ]); + rmSync(targetDirectory, { force: true, recursive: true }); + cpSync(join(binaryBuildDirectory, platform), targetDirectory, { recursive: true }); + const archiveName = platform.startsWith("windows-") ? `pi-${platform}.zip` : `pi-${platform}.tar.gz`; + cpSync(join(binaryBuildDirectory, archiveName), join(archiveDirectory, archiveName)); return platform; }