diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9fef6d0..99f30c2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -88,8 +88,9 @@ jobs: run: pnpm run build - name: Build release packages - env: - CSC_IDENTITY_AUTO_DISCOVERY: 'false' + # No signing certificate yet: mac.identity "-" in package.json forces + # ad-hoc signing so Apple Silicon builds launch. Leaving auto-discovery + # at its default avoids short-circuiting that explicit ad-hoc request. run: ${{ matrix.build_command }} shell: bash diff --git a/README.md b/README.md index c0c1ae0..8628cf1 100644 --- a/README.md +++ b/README.md @@ -67,10 +67,17 @@ Stay on top of the conversation with live ASR: Power Interview desktop client is supported on: - Windows 10/11 (x64 installer build) -- macOS (Apple Silicon and Intel release artifacts) +- macOS 14.4+ (Apple Silicon and Intel release artifacts) - 14.4 is the floor for system-audio loopback capture Release binaries are published on the [GitHub Releases](https://github.com/PowerInterviewAI/client-app/releases) page. +### Unsigned builds + +Release artifacts are not yet code-signed, so the OS will warn on first launch: + +- **macOS**: the app is ad-hoc signed (so it runs on Apple Silicon) but not notarized. If you see "app is damaged" or it is blocked, run `xattr -cr "/Applications/Power Interview AI.app"`, or right-click the app and choose Open. +- **Windows**: SmartScreen shows "Windows protected your PC". Click "More info" then "Run anyway". + ## Architecture Power Interview follows a **client-server architecture**. diff --git a/SPEC.md b/SPEC.md index d8a69de..e22ecf7 100644 --- a/SPEC.md +++ b/SPEC.md @@ -76,7 +76,7 @@ electron-updater publishes to GitHub Releases under `PowerInterviewAI/client` (c ## Platform Support - Windows 10/11 x64 (NSIS installer) -- macOS Apple Silicon and Intel (DMG + ZIP) +- macOS 14.4+ Apple Silicon and Intel (DMG + ZIP) - 14.4 is required for system-audio loopback capture ## Project Structure diff --git a/build/icon.png b/build/icon.png new file mode 100644 index 0000000..224c0fd Binary files /dev/null and b/build/icon.png differ diff --git a/package.json b/package.json index 94bf17d..3db3069 100644 --- a/package.json +++ b/package.json @@ -112,9 +112,27 @@ }, "mac": { "target": [ - "dmg", - "zip" + { + "target": "dmg", + "arch": [ + "arm64", + "x64" + ] + }, + { + "target": "zip", + "arch": [ + "arm64", + "x64" + ] + } ], + "icon": "build/icon.png", + "category": "public.app-category.productivity", + "minimumSystemVersion": "14.4.0", + "identity": "-", + "hardenedRuntime": false, + "notarize": false, "entitlements": "build/entitlements.mac.plist", "entitlementsInherit": "build/entitlements.mac.inherit.plist", "extendInfo": { diff --git a/src/main/ipc/permissions.ts b/src/main/ipc/permissions.ts index 237305d..ff80d20 100644 --- a/src/main/ipc/permissions.ts +++ b/src/main/ipc/permissions.ts @@ -1,4 +1,4 @@ -import { ipcMain, shell, systemPreferences } from 'electron'; +import { app, ipcMain, shell, systemPreferences } from 'electron'; export function registerPermissionHandlers(): void { ipcMain.handle('permissions:check-all', () => { @@ -25,4 +25,11 @@ export function registerPermissionHandlers(): void { }; if (urls[pane]) await shell.openExternal(urls[pane]).catch(() => {}); }); + + // macOS only applies a freshly-granted Screen Recording permission after the + // app is relaunched, so the first capture otherwise fails silently. + ipcMain.handle('permissions:relaunch', () => { + app.relaunch(); + app.exit(0); + }); } diff --git a/src/main/preload.cts b/src/main/preload.cts index 01a7a47..a0697bc 100644 --- a/src/main/preload.cts +++ b/src/main/preload.cts @@ -150,6 +150,7 @@ const electronApi = { requestMicrophone: () => ipcRenderer.invoke('permissions:request-microphone'), openSettings: (pane: 'microphone' | 'screen') => ipcRenderer.invoke('permissions:open-settings', pane), + relaunch: () => ipcRenderer.invoke('permissions:relaunch'), }, openExternal: (url: string) => ipcRenderer.invoke('external:open', url), diff --git a/src/renderer/components/custom/permission-gate-dialog.tsx b/src/renderer/components/custom/permission-gate-dialog.tsx index 19a707b..bf87cf3 100644 --- a/src/renderer/components/custom/permission-gate-dialog.tsx +++ b/src/renderer/components/custom/permission-gate-dialog.tsx @@ -42,6 +42,10 @@ export default function PermissionGateDialog({ getElectron()?.permissions.openSettings(pane); }; + const relaunch = () => { + getElectron()?.permissions.relaunch(); + }; + const handleProceed = () => { onOpenChange(false); onProceed(); @@ -92,13 +96,18 @@ export default function PermissionGateDialog({ ? 'Access granted' : status.screen === 'not-determined' ? 'Will be requested when recording starts' - : 'Enable in System Settings, then click Check Again' + : 'Enable in System Settings, then restart the app to apply' } action={ status.screen === 'denied' || status.screen === 'restricted' ? ( - +