mirror of
https://github.com/stablyai/orca.git
synced 2026-10-19 00:03:20 +00:00
fix(macos): isolate all local package identities
This commit is contained in:
@@ -2,6 +2,9 @@
|
||||
|
||||
import { spawnSync } from 'node:child_process'
|
||||
|
||||
const isLocalMacPackage = process.argv.includes('--local-macos-package')
|
||||
const localMacAppBundleId = 'com.stablyai.orca.local'
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
runNodeScript('config/scripts/build-windows-cli-launcher.mjs')
|
||||
process.exit(0)
|
||||
@@ -12,19 +15,30 @@ if (process.platform !== 'darwin') {
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
runPnpmScript('build:computer-macos')
|
||||
runPnpmScript('build:notification-status-macos')
|
||||
runPnpmScript(
|
||||
'build:computer-macos',
|
||||
[],
|
||||
isLocalMacPackage
|
||||
? { ...process.env, ORCA_COMPUTER_MACOS_BUNDLE_ID: `${localMacAppBundleId}.computer-use` }
|
||||
: process.env
|
||||
)
|
||||
runPnpmScript(
|
||||
'build:notification-status-macos',
|
||||
isLocalMacPackage ? ['--bundle-id', localMacAppBundleId] : []
|
||||
)
|
||||
process.exit(0)
|
||||
|
||||
function runPnpmScript(scriptName) {
|
||||
function runPnpmScript(scriptName, scriptArgs = [], env = process.env) {
|
||||
const npmExecPath = process.env.npm_execpath
|
||||
const command = npmExecPath
|
||||
? process.execPath
|
||||
: process.platform === 'win32'
|
||||
? 'pnpm.cmd'
|
||||
: 'pnpm'
|
||||
const args = npmExecPath ? [npmExecPath, 'run', scriptName] : ['run', scriptName]
|
||||
const result = spawnSync(command, args, { stdio: 'inherit' })
|
||||
const args = npmExecPath
|
||||
? [npmExecPath, 'run', scriptName, ...scriptArgs]
|
||||
: ['run', scriptName, ...scriptArgs]
|
||||
const result = spawnSync(command, args, { env, stdio: 'inherit' })
|
||||
|
||||
if (result.signal) {
|
||||
process.kill(process.pid, result.signal)
|
||||
|
||||
@@ -19,6 +19,32 @@ const {
|
||||
} = require('../packaged-runtime-node-modules.cjs')
|
||||
|
||||
describe('electron-builder config', () => {
|
||||
it('changes only the non-release macOS app id', () => {
|
||||
const configPath = require.resolve('../electron-builder.config.cjs')
|
||||
const original = process.env.ORCA_MAC_RELEASE
|
||||
try {
|
||||
delete require.cache[configPath]
|
||||
delete process.env.ORCA_MAC_RELEASE
|
||||
const localConfig = require('../electron-builder.config.cjs')
|
||||
expect(localConfig.appId).toBe('com.stablyai.orca')
|
||||
expect(localConfig.mac.appId).toBe('com.stablyai.orca.local')
|
||||
|
||||
delete require.cache[configPath]
|
||||
process.env.ORCA_MAC_RELEASE = '1'
|
||||
const releaseConfig = require('../electron-builder.config.cjs')
|
||||
expect(releaseConfig.appId).toBe('com.stablyai.orca')
|
||||
expect(releaseConfig.mac.appId).toBeUndefined()
|
||||
} finally {
|
||||
if (original === undefined) {
|
||||
delete process.env.ORCA_MAC_RELEASE
|
||||
} else {
|
||||
process.env.ORCA_MAC_RELEASE = original
|
||||
}
|
||||
delete require.cache[configPath]
|
||||
require('../electron-builder.config.cjs')
|
||||
}
|
||||
})
|
||||
|
||||
it('excludes repo-only source trees from app.asar', () => {
|
||||
expect(electronBuilderConfig.files).toEqual(
|
||||
expect.arrayContaining([
|
||||
|
||||
@@ -99,7 +99,9 @@ describe('Electron runtime package contract', () => {
|
||||
expect(scripts['build:linux']).toContain('pnpm run build:desktop')
|
||||
expect(scripts['build:linux']).not.toContain('pnpm run build ')
|
||||
expect(scripts['build:linux']).not.toContain('build:computer-macos')
|
||||
expect(scripts['build:mac']).toContain('pnpm run build:computer-macos')
|
||||
expect(scripts['build:unpack']).toContain('pnpm run build:native -- --local-macos-package')
|
||||
expect(scripts['build:mac']).toContain('pnpm run build:native -- --local-macos-package')
|
||||
expect(scripts['build:mac:release']).not.toContain('com.stablyai.orca.local')
|
||||
expect(scripts['build:release']).toContain('pnpm run build:native')
|
||||
expect(scripts['build:release']).not.toContain('build:computer-macos')
|
||||
})
|
||||
|
||||
+2
-2
@@ -71,10 +71,10 @@
|
||||
"postinstall": "node config/scripts/rebuild-native-deps.mjs",
|
||||
"rebuild:electron": "node config/scripts/rebuild-native-deps.mjs",
|
||||
"rebuild:node": "pnpm rebuild node-pty",
|
||||
"build:unpack": "pnpm run build && pnpm run ensure:electron-runtime && electron-builder --config config/electron-builder.config.cjs --dir",
|
||||
"build:unpack": "pnpm run build:desktop && pnpm run build:native -- --local-macos-package && pnpm run ensure:electron-runtime && electron-builder --config config/electron-builder.config.cjs --dir",
|
||||
"build:win": "pnpm run build:desktop && pnpm run ensure:electron-runtime && electron-builder --config config/electron-builder.config.cjs --win",
|
||||
"build:icons": "bash resources/icon-source/generate.sh",
|
||||
"build:mac": "pnpm run build:desktop && pnpm run build:computer-macos && pnpm run build:notification-status-macos --bundle-id com.stablyai.orca.local && pnpm run ensure:electron-runtime && electron-builder --config config/electron-builder.config.cjs --mac",
|
||||
"build:mac": "pnpm run build:desktop && pnpm run build:native -- --local-macos-package && pnpm run ensure:electron-runtime && electron-builder --config config/electron-builder.config.cjs --mac",
|
||||
"build:mac:release": "node config/scripts/verify-macos-release-env.mjs && ORCA_MAC_RELEASE=1 pnpm run build:desktop && ORCA_MAC_RELEASE=1 pnpm run build:computer-macos && ORCA_MAC_RELEASE=1 pnpm run build:notification-status-macos && pnpm run ensure:electron-runtime && ORCA_MAC_RELEASE=1 electron-builder --config config/electron-builder.config.cjs --mac",
|
||||
"build:linux": "pnpm run build:desktop && pnpm run ensure:electron-runtime && electron-builder --config config/electron-builder.config.cjs --linux AppImage deb",
|
||||
"test:e2e": "pnpm run ensure:electron-runtime && npx playwright test --config tests/playwright.config.ts --project=electron-headless",
|
||||
|
||||
@@ -15,7 +15,9 @@ const {
|
||||
notificationCtorMock,
|
||||
notificationIsSupportedMock,
|
||||
getAllWindowsMock,
|
||||
shellOpenExternalMock
|
||||
shellOpenExternalMock,
|
||||
electronAppMock,
|
||||
execFileSyncMock
|
||||
} = vi.hoisted(() => {
|
||||
const removeHandlerMock = vi.fn()
|
||||
const handleMock = vi.fn()
|
||||
@@ -36,6 +38,12 @@ const {
|
||||
const notificationIsSupportedMock = vi.fn(() => true)
|
||||
const getAllWindowsMock = vi.fn(() => [])
|
||||
const shellOpenExternalMock = vi.fn()
|
||||
const electronAppMock = {
|
||||
focus: vi.fn(),
|
||||
getPath: vi.fn(),
|
||||
isPackaged: false
|
||||
}
|
||||
const execFileSyncMock = vi.fn()
|
||||
return {
|
||||
removeHandlerMock,
|
||||
handleMock,
|
||||
@@ -47,10 +55,14 @@ const {
|
||||
notificationCtorMock,
|
||||
notificationIsSupportedMock,
|
||||
getAllWindowsMock,
|
||||
shellOpenExternalMock
|
||||
shellOpenExternalMock,
|
||||
electronAppMock,
|
||||
execFileSyncMock
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('node:child_process', () => ({ execFileSync: execFileSyncMock }))
|
||||
|
||||
vi.mock('electron', () => ({
|
||||
ipcMain: {
|
||||
removeHandler: removeHandlerMock,
|
||||
@@ -62,9 +74,7 @@ vi.mock('electron', () => ({
|
||||
BrowserWindow: {
|
||||
getAllWindows: getAllWindowsMock
|
||||
},
|
||||
app: {
|
||||
focus: vi.fn()
|
||||
},
|
||||
app: electronAppMock,
|
||||
shell: {
|
||||
openExternal: shellOpenExternalMock
|
||||
}
|
||||
@@ -122,6 +132,10 @@ describe('registerNotificationHandlers', () => {
|
||||
getAllWindowsMock.mockReset()
|
||||
getAllWindowsMock.mockReturnValue([])
|
||||
shellOpenExternalMock.mockClear()
|
||||
electronAppMock.focus.mockClear()
|
||||
electronAppMock.getPath.mockReset()
|
||||
electronAppMock.isPackaged = false
|
||||
execFileSyncMock.mockReset()
|
||||
setTrayAttentionMock.mockClear()
|
||||
})
|
||||
|
||||
@@ -278,6 +292,55 @@ describe('registerNotificationHandlers', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('reads the packaged bundle id when launchd does not provide one', async () => {
|
||||
const originalPlatform = process.platform
|
||||
const originalDevBundleId = process.env.ORCA_DEV_MACOS_BUNDLE_ID
|
||||
const originalLaunchdBundleId = process.env.__CFBundleIdentifier
|
||||
Object.defineProperty(process, 'platform', { value: 'darwin', configurable: true })
|
||||
delete process.env.ORCA_DEV_MACOS_BUNDLE_ID
|
||||
delete process.env.__CFBundleIdentifier
|
||||
electronAppMock.isPackaged = true
|
||||
electronAppMock.getPath.mockReturnValue('/Applications/Orca.app/Contents/MacOS/Orca')
|
||||
execFileSyncMock.mockReturnValue('com.stablyai.orca.local\n')
|
||||
try {
|
||||
registerNotificationHandlers({
|
||||
getSettings: () => ({
|
||||
notifications: {
|
||||
enabled: true,
|
||||
agentTaskComplete: true,
|
||||
terminalBell: true,
|
||||
suppressWhenFocused: true
|
||||
}
|
||||
})
|
||||
} as never)
|
||||
|
||||
const handler = getOpenSystemSettingsHandler()
|
||||
handler({})
|
||||
|
||||
expect(execFileSyncMock).toHaveBeenCalledWith(
|
||||
'/usr/libexec/PlistBuddy',
|
||||
['-c', 'Print :CFBundleIdentifier', '/Applications/Orca.app/Contents/Info.plist'],
|
||||
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }
|
||||
)
|
||||
expect(shellOpenExternalMock).toHaveBeenCalledWith(
|
||||
'x-apple.systempreferences:com.apple.Notifications-Settings.extension?id=com.stablyai.orca.local'
|
||||
)
|
||||
} finally {
|
||||
Object.defineProperty(process, 'platform', { value: originalPlatform, configurable: true })
|
||||
electronAppMock.isPackaged = false
|
||||
if (originalDevBundleId === undefined) {
|
||||
delete process.env.ORCA_DEV_MACOS_BUNDLE_ID
|
||||
} else {
|
||||
process.env.ORCA_DEV_MACOS_BUNDLE_ID = originalDevBundleId
|
||||
}
|
||||
if (originalLaunchdBundleId === undefined) {
|
||||
delete process.env.__CFBundleIdentifier
|
||||
} else {
|
||||
process.env.__CFBundleIdentifier = originalLaunchdBundleId
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('opens Windows notification settings', async () => {
|
||||
const originalPlatform = process.platform
|
||||
Object.defineProperty(process, 'platform', { value: 'win32', configurable: true })
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/* eslint-disable max-lines -- Why: notification IPC keeps permission, dispatch, custom sound asset, and sound-loading handlers colocated so renderer/main contracts stay auditable. */
|
||||
import { app, BrowserWindow, Notification, ipcMain, shell } from 'electron'
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { readFile, stat } from 'node:fs/promises'
|
||||
import { extname, isAbsolute, normalize } from 'node:path'
|
||||
import { dirname, extname, isAbsolute, join, normalize } from 'node:path'
|
||||
import beepSoundPath from '../../../resources/notification-sounds/beep.mp3?asset'
|
||||
import blipSoundPath from '../../../resources/notification-sounds/blip.mp3?asset'
|
||||
import blopSoundPath from '../../../resources/notification-sounds/blop.mp3?asset'
|
||||
@@ -186,11 +187,29 @@ function probeNotificationDelivery(): Promise<NotificationDeliveryProbeResult> {
|
||||
return deliveryProbeInFlight
|
||||
}
|
||||
|
||||
function readPackagedMacBundleId(): string | null {
|
||||
if (!app.isPackaged) {
|
||||
return null
|
||||
}
|
||||
try {
|
||||
const infoPlistPath = join(dirname(app.getPath('exe')), '..', 'Info.plist')
|
||||
return (
|
||||
execFileSync('/usr/libexec/PlistBuddy', ['-c', 'Print :CFBundleIdentifier', infoPlistPath], {
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'ignore']
|
||||
}).trim() || null
|
||||
)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function getMacNotificationSettingsUrl(): string {
|
||||
// Why: local packaging uses com.stablyai.orca.local (#9756), so trust the
|
||||
// launchd-provided id before falling back to the release id.
|
||||
// Why: direct executable launches do not reliably receive launchd's private
|
||||
// bundle-id environment variable, so packaged apps read their own plist.
|
||||
const bundleId =
|
||||
process.env.ORCA_DEV_MACOS_BUNDLE_ID ??
|
||||
readPackagedMacBundleId() ??
|
||||
process.env.__CFBundleIdentifier ??
|
||||
MACOS_PACKAGED_BUNDLE_ID
|
||||
return `${MACOS_NOTIFICATION_SETTINGS_URL}?id=${encodeURIComponent(bundleId)}`
|
||||
|
||||
Reference in New Issue
Block a user