From ca787271b3f6f19abc5ee223a749ad5d751f1a36 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sun, 8 Mar 2026 17:06:05 +0700 Subject: [PATCH] Support per-arch desktop builds and prune runtime artifacts --- .github/workflows/desktop-release.yml | 13 +- .../components/desktop-updates-section.tsx | 239 +++--------------- .../desktop/scripts/build-managed-runtime.mjs | 108 ++++++++ packages/website/src/routes/index.tsx | 2 +- 4 files changed, 153 insertions(+), 209 deletions(-) diff --git a/.github/workflows/desktop-release.yml b/.github/workflows/desktop-release.yml index efb874660..43691e3d9 100644 --- a/.github/workflows/desktop-release.yml +++ b/.github/workflows/desktop-release.yml @@ -21,10 +21,17 @@ env: jobs: publish-macos: + strategy: + matrix: + include: + - runner: macos-14 + rust_target: aarch64-apple-darwin + - runner: macos-13 + rust_target: x86_64-apple-darwin permissions: contents: write packages: read - runs-on: macos-latest + runs-on: ${{ matrix.runner }} steps: - uses: actions/checkout@v4 @@ -43,7 +50,7 @@ jobs: - name: Install Rust stable uses: dtolnay/rust-toolchain@stable with: - targets: aarch64-apple-darwin,x86_64-apple-darwin + targets: ${{ matrix.rust_target }} - name: Install JS dependencies run: npm ci @@ -111,7 +118,7 @@ jobs: releaseBody: See the assets to download and install this version. releaseDraft: false prerelease: false - args: --target universal-apple-darwin + args: --target ${{ matrix.rust_target }} publish-linux: permissions: diff --git a/packages/app/src/desktop/components/desktop-updates-section.tsx b/packages/app/src/desktop/components/desktop-updates-section.tsx index 68d5a43d9..a2ca81a84 100644 --- a/packages/app/src/desktop/components/desktop-updates-section.tsx +++ b/packages/app/src/desktop/components/desktop-updates-section.tsx @@ -1,12 +1,14 @@ import { useCallback, useEffect, useState } from "react"; -import { ActivityIndicator, Alert, Image, Text, View } from "react-native"; +import { ActivityIndicator, Alert, Image, Pressable, Text, View } from "react-native"; import * as Clipboard from "expo-clipboard"; import * as QRCode from "qrcode"; import { useFocusEffect } from "@react-navigation/native"; import { StyleSheet } from "react-native-unistyles"; -import { AdaptiveModalSheet, AdaptiveTextInput } from "@/components/adaptive-modal-sheet"; +import { ArrowUpRight } from "lucide-react-native"; +import { AdaptiveModalSheet } from "@/components/adaptive-modal-sheet"; import { Button } from "@/components/ui/button"; import { confirmDialog } from "@/utils/confirm-dialog"; +import { openExternalUrl } from "@/utils/open-external-url"; import { formatVersionWithPrefix, isVersionMismatch, @@ -19,7 +21,6 @@ import { restartManagedDaemon, shouldUseManagedDesktopDaemon, uninstallManagedCliShim, - updateManagedDaemonTcpSettings, type ManagedDaemonLogs, type ManagedPairingOffer, type ManagedDaemonStatus, @@ -36,11 +37,9 @@ export function LocalDaemonSection({ appVersion }: LocalDaemonSectionProps) { const [statusError, setStatusError] = useState(null); const [isRestartingDaemon, setIsRestartingDaemon] = useState(false); const [isInstallingCli, setIsInstallingCli] = useState(false); - const [isSavingTcpSettings, setIsSavingTcpSettings] = useState(false); const [statusMessage, setStatusMessage] = useState(null); const [cliStatusMessage, setCliStatusMessage] = useState(null); const [managedLogs, setManagedLogs] = useState(null); - const [isTcpModalOpen, setIsTcpModalOpen] = useState(false); const [isLogsModalOpen, setIsLogsModalOpen] = useState(false); const [isPairingModalOpen, setIsPairingModalOpen] = useState(false); const [isCliInstallModalOpen, setIsCliInstallModalOpen] = useState(false); @@ -50,8 +49,6 @@ export function LocalDaemonSection({ appVersion }: LocalDaemonSectionProps) { null ); const [pairingStatusMessage, setPairingStatusMessage] = useState(null); - const [tcpHostInput, setTcpHostInput] = useState(DEFAULT_TCP_HOST); - const [tcpPortInput, setTcpPortInput] = useState(String(DEFAULT_TCP_PORT)); const loadManagedStatus = useCallback(() => { if (!showSection) { @@ -62,9 +59,6 @@ export function LocalDaemonSection({ appVersion }: LocalDaemonSectionProps) { setManagedStatus(status); setManagedLogs(logs); setStatusError(null); - const [tcpHost, tcpPort] = splitTcpListen(status.tcpListen); - setTcpHostInput(tcpHost); - setTcpPortInput(tcpPort); }) .catch((error) => { const message = error instanceof Error ? error.message : String(error); @@ -85,12 +79,7 @@ export function LocalDaemonSection({ appVersion }: LocalDaemonSectionProps) { const localDaemonVersionText = formatVersionWithPrefix(managedStatus?.runtimeVersion ?? null); const daemonVersionMismatch = isVersionMismatch(appVersion, managedStatus?.runtimeVersion ?? null); const daemonVersionHint = - statusError ?? - (managedStatus?.daemonRunning - ? managedStatus.transportType === "tcp" - ? `Running on ${managedStatus.transportPath}.` - : "Running." - : "Not running."); + statusError ?? (managedStatus?.daemonRunning ? "Running." : "Not running."); const handleUpdateLocalDaemon = useCallback(() => { if (!showSection) { @@ -246,101 +235,23 @@ export function LocalDaemonSection({ appVersion }: LocalDaemonSectionProps) { }); }, [pairingOffer?.url]); - const handleOpenTcpModal = useCallback(() => { - if (!managedStatus) { - return; - } - const [tcpHost, tcpPort] = splitTcpListen(managedStatus.tcpListen); - setTcpHostInput(tcpHost); - setTcpPortInput(tcpPort); - setIsTcpModalOpen(true); - }, [managedStatus]); - - const handleDisableTcp = useCallback(() => { - if (isSavingTcpSettings) { - return; - } - setIsSavingTcpSettings(true); - setStatusMessage(null); - void updateManagedDaemonTcpSettings({ - enabled: false, - host: tcpHostInput.trim() || DEFAULT_TCP_HOST, - port: parseTcpPort(tcpPortInput) ?? DEFAULT_TCP_PORT, - }) - .then((status) => { - setManagedStatus(status); - setStatusMessage("Network access disabled."); - return loadManagedStatus(); - }) - .catch((error) => { - const message = error instanceof Error ? error.message : String(error); - setStatusMessage(`Failed to update: ${message}`); - }) - .finally(() => { - setIsSavingTcpSettings(false); - }); - }, [isSavingTcpSettings, loadManagedStatus, tcpHostInput, tcpPortInput]); - - const handleSaveTcp = useCallback(() => { - if (isSavingTcpSettings) { - return; - } - const host = tcpHostInput.trim(); - const port = parseTcpPort(tcpPortInput); - if (!host) { - Alert.alert("Host required", "Enter a TCP bind host."); - return; - } - if (port == null || port <= 0) { - Alert.alert("Port required", "Enter a valid TCP port."); - return; - } - if (port === 6767) { - Alert.alert("Port unavailable", "Port 6767 is reserved. Choose a different port."); - return; - } - - void confirmDialog({ - title: "Enable network access", - message: - "This makes the daemon reachable on your local network. Relay connections will continue to work.", - confirmLabel: "Enable", - cancelLabel: "Cancel", - }) - .then((confirmed) => { - if (!confirmed) { - return; - } - setIsSavingTcpSettings(true); - setStatusMessage(null); - void updateManagedDaemonTcpSettings({ enabled: true, host, port }) - .then((status) => { - setManagedStatus(status); - setIsTcpModalOpen(false); - setStatusMessage(`Network access enabled on ${host}:${port}.`); - return loadManagedStatus(); - }) - .catch((error) => { - const message = error instanceof Error ? error.message : String(error); - setStatusMessage(`Failed to update: ${message}`); - }) - .finally(() => { - setIsSavingTcpSettings(false); - }); - }) - .catch((error) => { - console.error("[Settings] Failed to open TCP confirmation", error); - Alert.alert("Error", "Unable to open the confirmation dialog."); - }); - }, [isSavingTcpSettings, loadManagedStatus, tcpHostInput, tcpPortInput]); - if (!showSection) { return null; } return ( - Built-in daemon + + Built-in daemon + void openExternalUrl(ADVANCED_DAEMON_SETTINGS_URL)} + style={styles.sectionLink} + > + Advanced settings + + + @@ -352,9 +263,7 @@ export function LocalDaemonSection({ appVersion }: LocalDaemonSectionProps) { Restart daemon - - Restarts the built-in daemon. Your data and external connections are not affected. - + Restarts the built-in daemon. {statusMessage ? ( {statusMessage} ) : null} @@ -425,39 +334,6 @@ export function LocalDaemonSection({ appVersion }: LocalDaemonSectionProps) { Pair device - - - Network access - - Allow other apps or devices on your network to connect directly. - - - {managedStatus?.tcpEnabled - ? `Enabled on ${managedStatus.tcpListen ?? managedStatus.transportPath}` - : "Disabled"} - - - - {managedStatus?.tcpEnabled ? ( - - ) : null} - - - {daemonVersionMismatch ? ( @@ -469,44 +345,6 @@ export function LocalDaemonSection({ appVersion }: LocalDaemonSectionProps) { ) : null} - setIsTcpModalOpen(false)} - title="Network access" - > - - - Expose the daemon on a specific address and port so other apps can connect directly. - Port 6767 is reserved and cannot be used here. - - - - - - - - - - setIsCliInstallModalOpen(false)} @@ -571,21 +409,7 @@ export function LocalDaemonSection({ appVersion }: LocalDaemonSectionProps) { ); } -const DEFAULT_TCP_HOST = "127.0.0.1"; -const DEFAULT_TCP_PORT = 7771; - -function splitTcpListen(value: string | null): [string, string] { - if (!value) { - return [DEFAULT_TCP_HOST, String(DEFAULT_TCP_PORT)]; - } - const [host, port] = value.split(":"); - return [host || DEFAULT_TCP_HOST, port || String(DEFAULT_TCP_PORT)]; -} - -function parseTcpPort(value: string): number | null { - const parsed = Number.parseInt(value.trim(), 10); - return Number.isFinite(parsed) ? parsed : null; -} +const ADVANCED_DAEMON_SETTINGS_URL = "https://paseo.sh/docs/configuration"; function PairingOfferDialogContent(input: { isLoading: boolean; @@ -690,12 +514,26 @@ const styles = StyleSheet.create((theme) => ({ section: { marginBottom: theme.spacing[6], }, + sectionHeader: { + alignItems: "center", + flexDirection: "row", + justifyContent: "space-between", + marginBottom: theme.spacing[3], + marginLeft: theme.spacing[1], + }, sectionTitle: { color: theme.colors.foregroundMuted, fontSize: theme.fontSize.xs, fontWeight: theme.fontWeight.normal, - marginBottom: theme.spacing[3], - marginLeft: theme.spacing[1], + }, + sectionLink: { + alignItems: "center", + flexDirection: "row", + gap: theme.spacing[1], + }, + sectionLinkText: { + color: theme.colors.foregroundMuted, + fontSize: theme.fontSize.xs, }, card: { backgroundColor: theme.colors.surface2, @@ -807,15 +645,6 @@ const styles = StyleSheet.create((theme) => ({ backgroundColor: theme.colors.surface0, padding: theme.spacing[3], }, - input: { - borderWidth: 1, - borderColor: theme.colors.border, - borderRadius: theme.borderRadius.md, - color: theme.colors.foreground, - backgroundColor: theme.colors.surface1, - paddingHorizontal: theme.spacing[3], - paddingVertical: theme.spacing[3], - }, modalActions: { flexDirection: "row", justifyContent: "flex-end", diff --git a/packages/desktop/scripts/build-managed-runtime.mjs b/packages/desktop/scripts/build-managed-runtime.mjs index db8632933..7e359cf99 100644 --- a/packages/desktop/scripts/build-managed-runtime.mjs +++ b/packages/desktop/scripts/build-managed-runtime.mjs @@ -20,6 +20,21 @@ const workspaces = [ { name: "@getpaseo/cli", root: path.join(repoRoot, "packages", "cli") }, ]; +const ripgrepPlatformDirMap = { + darwin: { + arm64: "arm64-darwin", + x64: "x64-darwin", + }, + linux: { + arm64: "arm64-linux", + x64: "x64-linux", + }, + win32: { + arm64: "arm64-win32", + x64: "x64-win32", + }, +}; + async function rmSafe(target) { await fs.rm(target, { recursive: true, force: true }); } @@ -50,6 +65,12 @@ async function pathExists(target) { } } +async function removeIfExists(target) { + if (await pathExists(target)) { + await rmSafe(target); + } +} + async function readToolVersions() { const raw = await fs.readFile(path.join(repoRoot, ".tool-versions"), "utf8"); const entries = new Map(); @@ -284,6 +305,92 @@ async function writeRuntimePackageJson(runtimeRoot) { ); } +async function pruneChildrenExcept(parent, keepNames) { + if (!(await pathExists(parent))) { + return; + } + const entries = await fs.readdir(parent); + await Promise.all( + entries + .filter((entry) => !keepNames.has(entry)) + .map((entry) => rmSafe(path.join(parent, entry))) + ); +} + +async function pruneNodeDistribution(runtimeRoot) { + const nodeRoot = path.join(runtimeRoot, "node"); + await Promise.all([ + removeIfExists(path.join(nodeRoot, "include")), + removeIfExists(path.join(nodeRoot, "share")), + removeIfExists(path.join(nodeRoot, "CHANGELOG.md")), + ]); + + const nodeGlobalModulesRoot = path.join(nodeRoot, "lib", "node_modules"); + if (await pathExists(nodeGlobalModulesRoot)) { + const keepNames = new Set(["npm"]); + await pruneChildrenExcept(nodeGlobalModulesRoot, keepNames); + } +} + +async function pruneOnnxRuntime(runtimeRoot) { + const onnxRoot = path.join(runtimeRoot, "node_modules", "onnxruntime-node", "bin", "napi-v6"); + if (!(await pathExists(onnxRoot))) { + return; + } + if (process.platform === "darwin") { + await removeIfExists(path.join(onnxRoot, "linux")); + await removeIfExists(path.join(onnxRoot, "win32")); + await pruneChildrenExcept(path.join(onnxRoot, "darwin"), new Set([process.arch])); + return; + } + if (process.platform === "linux") { + await removeIfExists(path.join(onnxRoot, "darwin")); + await removeIfExists(path.join(onnxRoot, "win32")); + await pruneChildrenExcept(path.join(onnxRoot, "linux"), new Set([process.arch])); + return; + } + if (process.platform === "win32") { + await removeIfExists(path.join(onnxRoot, "darwin")); + await removeIfExists(path.join(onnxRoot, "linux")); + await pruneChildrenExcept(path.join(onnxRoot, "win32"), new Set([process.arch])); + return; + } +} + +async function pruneNodePty(runtimeRoot) { + const prebuildsRoot = path.join(runtimeRoot, "node_modules", "node-pty", "prebuilds"); + const keepName = `${process.platform}-${process.arch}`; + await pruneChildrenExcept(prebuildsRoot, new Set([keepName])); + + if (process.platform !== "win32") { + await removeIfExists(path.join(runtimeRoot, "node_modules", "node-pty", "third_party")); + } +} + +async function pruneClaudeAgentSdk(runtimeRoot) { + const ripgrepRoot = path.join( + runtimeRoot, + "node_modules", + "@anthropic-ai", + "claude-agent-sdk", + "vendor", + "ripgrep" + ); + const keepName = ripgrepPlatformDirMap[process.platform]?.[process.arch]; + if (keepName) { + await pruneChildrenExcept(ripgrepRoot, new Set(["COPYING", keepName])); + } +} + +async function pruneManagedRuntime(runtimeRoot) { + await Promise.all([ + pruneNodeDistribution(runtimeRoot), + pruneOnnxRuntime(runtimeRoot), + pruneNodePty(runtimeRoot), + pruneClaudeAgentSdk(runtimeRoot), + ]); +} + async function main() { await ensureWorkspaceBuilds(); @@ -322,6 +429,7 @@ async function main() { path.join(runtimeRoot, "node"), tarballs.map((entry) => entry.path) ); + await pruneManagedRuntime(runtimeRoot); const nodeRelativePath = process.platform === "win32" ? path.join("node", "node.exe") diff --git a/packages/website/src/routes/index.tsx b/packages/website/src/routes/index.tsx index d97cdf1e0..45c9718a7 100644 --- a/packages/website/src/routes/index.tsx +++ b/packages/website/src/routes/index.tsx @@ -5,7 +5,7 @@ import websitePackage from '../../package.json' import '~/styles.css' const desktopVersion = websitePackage.version -const macDownloadHref = `https://github.com/getpaseo/paseo/releases/download/v${desktopVersion}/Paseo_${desktopVersion}_universal.dmg` +const macDownloadHref = `https://github.com/getpaseo/paseo/releases/download/v${desktopVersion}/Paseo_${desktopVersion}_aarch64.dmg` export const Route = createFileRoute('/')({ head: () => ({