From 87290396981b1baafc465a23cc64c81f3e869f6b Mon Sep 17 00:00:00 2001 From: matt Date: Sat, 14 Feb 2026 12:12:44 +0900 Subject: [PATCH] feat(mac): add ARM64 and x64 distribution scripts and update README - Introduced new distribution scripts for macOS targeting ARM64 and x64 architectures. - Updated README to clarify download instructions for macOS Apple Silicon and Intel users. - Enhanced CI workflow to support building for both architectures, improving release process. --- .github/workflows/release.yml | 20 ++++-- README.md | 6 +- package.json | 2 + .../infrastructure/SshConnectionManager.ts | 70 +++++++++++++++++-- 4 files changed, 86 insertions(+), 12 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 99a50a38..7e7a0a4b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -49,7 +49,17 @@ jobs: release-mac: needs: build - runs-on: macos-14 + strategy: + fail-fast: false + matrix: + include: + - arch: arm64 + runner: macos-14 + dist_command: pnpm dist:mac:arm64 + - arch: x64 + runner: macos-13 + dist_command: pnpm dist:mac:x64 + runs-on: ${{ matrix.runner }} steps: - name: Checkout @@ -83,16 +93,16 @@ jobs: VERSION="${GITHUB_REF#refs/tags/v}" pnpm pkg set version="$VERSION" - - name: Build app (macOS) + - name: Build app (macOS ${{ matrix.arch }}) run: pnpm build - - name: Verify packaged inputs (macOS) + - name: Verify packaged inputs (macOS ${{ matrix.arch }}) run: | test -f dist-electron/main/index.cjs test -f dist-electron/preload/index.js test -f out/renderer/index.html - - name: Package & Release (macOS) + - name: Package & Release (macOS ${{ matrix.arch }}) env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} CSC_LINK: ${{ secrets.CSC_LINK }} @@ -100,7 +110,7 @@ jobs: APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} - run: pnpm dist:mac + run: ${{ matrix.dist_command }} release-win: needs: build diff --git a/README.md b/README.md index a54324fa..aa533757 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,8 @@ | Platform | Download | Notes | |----------|----------|-------| -| **macOS** (Apple Silicon) | [`.dmg`](https://github.com/matt1398/claude-devtools/releases/latest) | Drag to Applications. On first launch: right-click → Open | +| **macOS** (Apple Silicon) | [`.dmg`](https://github.com/matt1398/claude-devtools/releases/latest) | Download the `arm64` asset. Drag to Applications. On first launch: right-click → Open | +| **macOS** (Intel) | [`.dmg`](https://github.com/matt1398/claude-devtools/releases/latest) | Download the `x64` asset. Drag to Applications. On first launch: right-click → Open | | **Windows** | [`.exe`](https://github.com/matt1398/claude-devtools/releases/latest) | Standard installer. May trigger SmartScreen — click "More info" → "Run anyway" | The app reads session logs from `~/.claude/` — the data is already on your machine. No setup, no API keys, no login. @@ -201,7 +202,8 @@ The app auto-discovers your Claude Code projects from `~/.claude/`. #### Build for Distribution ```bash -pnpm dist:mac # macOS (.dmg) +pnpm dist:mac:arm64 # macOS Apple Silicon (.dmg) +pnpm dist:mac:x64 # macOS Intel (.dmg) pnpm dist:win # Windows (.exe) pnpm dist # Both platforms ``` diff --git a/package.json b/package.json index 265141f6..d9088cda 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,8 @@ "build": "electron-vite build", "dist": "electron-builder --mac --win --linux", "dist:mac": "electron-builder --mac --publish always", + "dist:mac:arm64": "electron-builder --mac --arm64 --publish always", + "dist:mac:x64": "electron-builder --mac --x64 --publish always", "dist:win": "electron-builder --win --publish always", "dist:linux": "electron-builder --linux --publish always", "preview": "electron-vite preview", diff --git a/src/main/services/infrastructure/SshConnectionManager.ts b/src/main/services/infrastructure/SshConnectionManager.ts index 37c62cef..dcf40890 100644 --- a/src/main/services/infrastructure/SshConnectionManager.ts +++ b/src/main/services/infrastructure/SshConnectionManager.ts @@ -435,25 +435,85 @@ export class SshConnectionManager extends EventEmitter { } private async resolveRemoteProjectsPath(username: string): Promise { - // Try to resolve the remote home directory - // SFTP doesn't have a direct "get home dir" call, so we try common paths + // Prefer remote $HOME when available, then fall back to common paths. + const remoteHome = await this.resolveRemoteHomeDirectory(); const candidates = [ + ...(remoteHome ? [path.posix.join(remoteHome, '.claude', 'projects')] : []), `/home/${username}/.claude/projects`, `/Users/${username}/.claude/projects`, `/root/.claude/projects`, ]; - for (const candidate of candidates) { + for (const candidate of [...new Set(candidates)]) { if (await this.provider.exists(candidate)) { return candidate; } } - // Fallback: try to read from environment via realpath of ~ - // Default to Linux convention + // Fallback to inferred home-based path when we could resolve $HOME. + if (remoteHome) { + return path.posix.join(remoteHome, '.claude', 'projects'); + } + + // Final fallback: Linux convention. return `/home/${username}/.claude/projects`; } + /** + * Resolve remote user's home directory by querying `$HOME` over SSH. + */ + private async resolveRemoteHomeDirectory(): Promise { + if (!this.client) { + return null; + } + + try { + const home = await this.execRemoteCommand('printf %s "$HOME"'); + const normalized = home.trim(); + return normalized.startsWith('/') ? normalized : null; + } catch { + return null; + } + } + + /** + * Execute a command on the connected SSH host and return stdout. + */ + private async execRemoteCommand(command: string): Promise { + const client = this.client; + if (!client) { + throw new Error('SSH client is not connected'); + } + + return new Promise((resolve, reject) => { + client.exec(command, (err, stream) => { + if (err) { + reject(err); + return; + } + + let stdout = ''; + let stderr = ''; + + stream.on('data', (chunk: Buffer | string) => { + stdout += chunk.toString(); + }); + + stream.stderr.on('data', (chunk: Buffer | string) => { + stderr += chunk.toString(); + }); + + stream.on('close', (code: number | null) => { + if (code === 0) { + resolve(stdout); + return; + } + reject(new Error(stderr.trim() || `Remote command failed with exit code ${code}`)); + }); + }); + }); + } + private handleDisconnect(): void { if (this.state === 'disconnected') return;