mirror of
https://github.com/stablyai/orca.git
synced 2026-11-13 09:27:29 +00:00
* Give terminal agents a lasting Orca Agent ID Terminal agents now get an Orca ID (orca_session_id:<agent>_<uuid>) derived from their own conversation id on the host that runs them, so it stays the same across worktree sleep/wake, app restart and a hand-run resume in any pane. The terminal pane menu gets Copy Agent ID beside Copy Session ID, and worktree ps --json agent rows carry orcaSessionId. Messaging a terminal agent by that ID is refused with a message naming its pane until terminal delivery lands. * Name terminal agents at row admission and fix review round 1 - Perf: a row's Agent ID is resolved once, when its provider key changes at admission (legacy rows via the status adapter's prepareRow hook, tmux rows at canonical commit) and carried on the row as runtime-only agentIdentity. Snapshots, publications and internal notifications only copy it; no key serialization, cache lookup or database access per walk. Drops the pane FIFO cache. - Refusal: the identity service keeps a reverse index id -> rows, kept exact by the adapter's rowChanged hook; a terminal refusal reads only matching panes and names only confirmed running ones (no resume seeds, exited or unconfirmed restored rows). - Retry: a read-only identity file is lookup-only and never mints; any store failure backs off all SQLite work for 30 s; a Copy click retries once. - Menu: Copy Agent ID shows for a live pane only when its host stamped an ID, and for a sleeping pane only when this app owns it on macOS/Linux. - SSH: a sleeping record keeps its connection; a foreign (non-WSL) one resolves no ID. - Renderer keeps an existing ID only for the same agent, owner and provider key including the Pi transcript locator. - tmux-selected terminal rows carry their ID at every snapshot seam and in Copy resolution; structured chat rows stay excluded. * Keep Agent ID work out of notifications and reach Copy wherever a host can answer - Internal status/provider notifications walk rows without Agent ID work: canonical (tmux-selected) rows are decorated only for outputs that carry the id (snapshots, live emit, ps, Copy). The tmux publish body is now publishTmuxSelectedStatus so tests drive the production path. - A row this host can name but has not yet (failed or pending link) is published with orcaSessionId: null; older hosts omit the field. The renderer offers Copy Agent ID on a live row whenever the field is present, and the click retries through terminal.agentId. - Hosts advertise terminal.agent-id.v1. A sleeping pane offers Copy Agent ID when its owner is this app on macOS/Linux, or a remote owner that advertises the capability on a known non-Windows platform, and its record names no foreign connection. - "Running in pane" places require that this runtime observed the row; a saved status, Done included, is history. * Guard the pane menu subscription and stop offering Copy where it can never succeed - A settled read-only miss (no link in a newer build's identity file) no longer publishes orcaSessionId; only a resolved id or a pending failed link does, so Copy Agent ID is offered only where a click can succeed. - Render contract for the per-pane menu selection: another pane's or host's status change re-renders no unaffected pane; a pane's own stamp flips its availability. * Move terminal agent identity SQL into a worker thread Main's blocking-SQLite ratchet forbids the synchronous identity store, so (per the decider ruling) agent-identities.db is now owned by one lazy worker-thread entry per hook-server owner, desktop and orcad alike. - The schema, open/version handling, mint/link transaction and read-only behaviour move unchanged into agent-identity-worker-entry.ts; main keeps only the key type, path and an async client over WorkerThreadRequestQueue. - Admission stays synchronous: a new key is published pending (null), one request per exact key is shared by every row and click, and the committed answer re-stamps the current rows still pending on that key (legacy and tmux-selected) as identity-only replays. Replies are fenced by store generation, key and current subscription; failures back off 30 s and a Copy click retries once. - terminal.agentId and the terminal-ID refusal are async: Copy waits for the pending lookup and revalidates the pane; send/task routing resolves the terminal recipient (awaiting the store's has() on an index miss) before any synchronous routing or effects. - The renderer treats the host's row naming a conversation as authoritative for its id, so a settled miss clears a pending null, while ID-less local updates keep it. - Packaged for desktop and orcad; registered as a worker entry, no baseline growth. * Own identity demand per key, publish settled ids as projections, recheck authority Implements the second decider ruling for the identity worker: - One owner-level demand table replaces the per-key promise backlog and the settled-row scan: each exact key a current row or caller needs has one record (identity, places, waiters); the pump sends the worker one operation at a time, only for keys still needed, so a healthy burst completes and detached keys are never sent. Operation deadlines start at dispatch; a caller's wait expiring never drops a row's demand. - One failure gate for lookups and membership: after a failure nothing is sent for 30 s except one coalesced Copy attempt; a single owner wake retries while rows still wait, and cancels with the last demand or close. - The retirement barrier outlives a client: a replaced worker must exit before another starts; a different store path is a different authority. - Settlement re-stamps the stored row and publishes a projection only: window readers get the row without its hook event (no agent-state record) and session.tabs is woken by a row touch; no enriched subscriber, plugin event or freshness heartbeat runs again. - Copy and terminal membership check the owner generation after their waits. A known chat is decided by the chat store alone; send, peer mail, reply and assignee admission resolve the terminal recipient before reading the facts they act on, recheck the sender after the wait, and group send reselects its audience and mailboxes after each wait before writing. * Scope identity answers to their store authority, expire Copy grants with callers, index provider aliases - One synchronized authority token before Copy/reverse fast paths, compared after their awaits; answers carry their authority and an ended store turns rows, tmux subjects and the reverse index pending together. Status path changes sync the authority outside admission. - A failed operation reschedules the pump so a queued click gets its attempt; grants belong to live waiters and a click joining a running attempt earns none. - Provider-native ids resolve through a commit-maintained alias index at the record store. - Group fan-out no longer classifies authoritative candidates; @idle rereads statuses with current candidates before the final synchronous selection. - Hook-server unit fixtures use an in-memory identity client; a real worker load/stop failure check covers the worker boundary. * Pick a shared provider alias's owner among its owners only The alias index keeps the store's listing order (readable, then unreadable) as derived ranks, updated from the draft's appended keys on commit, so a duplicate alias no longer walks the history per lookup. * Ask a remote pane's host for its Agent ID by the host's own pane key Copy Agent ID on a web-terminal surface sent the client's mirrored pane key, which the execution host does not know, so it answered null. * Keep focus where the user moved, recover a missed identity re-stamp, ack peer-mail replays first Copy Agent ID refocuses its pane only when focus is unclaimed or still in the pane; a carried pending row takes its key's settled answer; an already-delivered peer-mail retry is acknowledged before recipient preparation.
930 lines
38 KiB
JavaScript
930 lines
38 KiB
JavaScript
const { chmodSync, existsSync, readdirSync, readFileSync, writeFileSync } = require('node:fs')
|
||
const { execFileSync } = require('node:child_process')
|
||
const { join, resolve } = require('node:path')
|
||
const {
|
||
MACOS_FOLDER_USAGE_DESCRIPTIONS,
|
||
applyMacHelperFolderUsageDescriptions
|
||
} = require('./scripts/desktop-distribution/macos/macos-folder-usage-descriptions.cjs')
|
||
const electronBuilderNativeRebuild = require('./scripts/desktop-distribution/runtime-verification/electron-builder-native-rebuild.cjs')
|
||
const {
|
||
assertPackagedDaemonEntryExists,
|
||
verifyPackagedDaemonEntryBoots,
|
||
verifyPackagedMacTerminalHostBoots
|
||
} = require('./scripts/desktop-distribution/runtime-verification/verify-packaged-daemon-entry.cjs')
|
||
const {
|
||
buildMacTerminalHost,
|
||
macTerminalHostSignIgnore,
|
||
verifyMacTerminalHost
|
||
} = require('./scripts/macos/terminal-host-bundle/macos-terminal-host-bundle.cjs')
|
||
const {
|
||
assertPackagedNativeVariantsInstalled,
|
||
assertProcessHostOutputBuilt,
|
||
createPackagedRuntimeNodeModuleResources,
|
||
normalizeElectronArchitecture,
|
||
prunePackagedRuntimeNodeModules,
|
||
readPackage,
|
||
verifyPackagedMainRuntimeDeps
|
||
} = require('./packaged-runtime-node-modules.cjs')
|
||
const { copyPackagedTranscribeCpp } = require('./packaged-transcribe-cpp.cjs')
|
||
const {
|
||
assertNoWindowsExecutablesInRuntimeNodeModules
|
||
} = require('./packaged-windows-only-runtime-files.cjs')
|
||
const {
|
||
verifyLinuxGlibcFloor
|
||
} = require('./scripts/native-build/linux/verify-linux-glibc-floor.cjs')
|
||
const {
|
||
verifyPackagedWindowsAgentHook
|
||
} = require('./scripts/desktop-distribution/runtime-verification/verify-packaged-windows-agent-hook.cjs')
|
||
const {
|
||
ensureBundledWaylandClipboard,
|
||
finalizePackagedWaylandClipboard
|
||
} = require('./wayland-clipboard-resources.cjs')
|
||
const {
|
||
writeMacBuildCompatibility
|
||
} = require('./scripts/desktop-distribution/macos/mac-build-compatibility.cjs')
|
||
const {
|
||
MOBILE_WEB_BUNDLE_DIR,
|
||
assertMobileWebBundleBuilt
|
||
} = require('./scripts/desktop-distribution/runtime-verification/verify-packaged-mobile-web-bundle.cjs')
|
||
const {
|
||
verifyPackagedPluginResources,
|
||
verifyPackagedPluginTypings
|
||
} = require('./scripts/desktop-distribution/runtime-verification/verify-packaged-plugin-resources.cjs')
|
||
const {
|
||
assertPackagedServeSelectionEntry,
|
||
assertPackagedWebClient
|
||
} = require('./scripts/desktop-distribution/runtime-verification/verify-packaged-serve-payload.cjs')
|
||
const {
|
||
assertBundledRipgrepInstalled,
|
||
bundledRipgrepExtraResources,
|
||
bundledRipgrepMacSignIgnore,
|
||
finalizePackagedRipgrep
|
||
} = require('./bundled-ripgrep-resources.cjs')
|
||
const {
|
||
verifyPackagedWindowsNodePty
|
||
} = require('./scripts/desktop-distribution/runtime-verification/verify-packaged-node-pty-job-ownership.cjs')
|
||
const {
|
||
assertOrcadTemplateBuilt,
|
||
finalizePackagedOrcadTemplate,
|
||
orcadTemplateExtraResource,
|
||
orcadTemplateNodeModulesExtraResource,
|
||
orcadTemplateMacSignIgnore
|
||
} = require('./scripts/orcad-distribution/template/packaged-orcad-template.cjs')
|
||
const {
|
||
verifySkillsCliRuntime
|
||
} = require('./scripts/runtime-verification/skills-cli/verify-skills-cli-runtime.cjs')
|
||
const { assertNotDarkBuild } = require('./dark-build-switches.cjs')
|
||
const {
|
||
verifyStaticAppImagePackage
|
||
} = require('./scripts/desktop-distribution/appimage/static-appimage-package-contract.cjs')
|
||
const {
|
||
appImageNodeRuntimeFpmArgs,
|
||
placeAppImageNodeRuntime,
|
||
verifyAppImageNodeRuntimeServesPty
|
||
} = require('./scripts/desktop-distribution/linux/appimage-node-runtime.cjs')
|
||
const {
|
||
signWindowsUninstallerViaSignPath
|
||
} = require('./scripts/desktop-distribution/windows/windows-uninstaller-signing.cjs')
|
||
const {
|
||
buildMacShipItScheduling
|
||
} = require('./scripts/desktop-distribution/macos/macos-shipit-scheduling.cjs')
|
||
|
||
// Dev channels need release bundle ID/signature/notarization so Squirrel.Mac accepts updates and macOS retains app identity.
|
||
const isMacHourly = process.env.ORCA_MAC_HOURLY === '1'
|
||
const isMacDaily = process.env.ORCA_MAC_DAILY === '1'
|
||
const isMacAdhoc = process.env.ORCA_MAC_ADHOC === '1'
|
||
// Keep Windows dev flags separate: mac flags enable hardened runtime/notarization/global signing, which would demand an unavailable Windows cert.
|
||
const isWinHourly = process.env.ORCA_WIN_HOURLY === '1'
|
||
const isWinDaily = process.env.ORCA_WIN_DAILY === '1'
|
||
const isWinAdhoc = process.env.ORCA_WIN_ADHOC === '1'
|
||
const isWinDevChannel = isWinHourly || isWinDaily || isWinAdhoc
|
||
const isMacRelease = process.env.ORCA_MAC_RELEASE === '1' || isMacHourly || isMacDaily || isMacAdhoc
|
||
const isLinuxArm64Release = process.env.ORCA_LINUX_ARM64_RELEASE === '1'
|
||
const localBuildVersion =
|
||
isMacRelease || isWinDevChannel ? undefined : process.env.ORCA_LOCAL_BUILD_VERSION
|
||
const isHourlyChannel = isMacHourly || isWinHourly
|
||
const isDailyChannel = isMacDaily || isWinDaily
|
||
const isAdhocChannel = isMacAdhoc || isWinAdhoc
|
||
const devChannelBuildVersion = isHourlyChannel
|
||
? process.env.ORCA_HOURLY_BUILD_VERSION
|
||
: isDailyChannel
|
||
? process.env.ORCA_DAILY_BUILD_VERSION
|
||
: isAdhocChannel
|
||
? process.env.ORCA_ADHOC_BUILD_VERSION
|
||
: undefined
|
||
// Separate dev-channel repos: 24 hourly tags/day would evict stable/RC from the ten-entry release feed; isolate adhoc/daily from hourly subscribers too.
|
||
const devChannelRepo = isHourlyChannel
|
||
? 'orca-hourly'
|
||
: isDailyChannel
|
||
? 'orca-daily'
|
||
: isAdhocChannel
|
||
? 'orca-adhoc'
|
||
: null
|
||
const appId = 'com.stablyai.orca'
|
||
const featureWallResources = {
|
||
from: 'resources/onboarding/feature-wall',
|
||
to: 'onboarding/feature-wall'
|
||
}
|
||
// Freshness checks need immutable build identity, not skill bytes or runtime network access.
|
||
const skillFreshnessResources = {
|
||
from: 'resources/skills',
|
||
to: 'skills'
|
||
}
|
||
// The bundled agent-browser binary finds its version-matched catalog via the nearest skills/ above it (Resources/).
|
||
const agentBrowserSkillResources = [
|
||
{ from: 'node_modules/agent-browser/skills/agent-browser', to: 'skills/agent-browser' },
|
||
{ from: 'node_modules/agent-browser/skill-data', to: 'skill-data' }
|
||
]
|
||
// Native agents load plugins by path and cannot read inside app.asar.
|
||
const nativeChatVisualsResource = {
|
||
from: 'resources/native-chat-visuals',
|
||
to: 'native-chat-visuals'
|
||
}
|
||
// SSH deploy resolves process.resourcesPath/relay; extraResources keeps bundles on real disk.
|
||
const relayExtraResource = {
|
||
from: 'out/relay',
|
||
to: 'relay'
|
||
}
|
||
// Bootstrap verifies/publishes immutable plugin bytes from ordinary directories.
|
||
const bundledPluginResources = {
|
||
from: 'resources/plugins/launch',
|
||
to: 'plugins/launch'
|
||
}
|
||
// electron-builder always drops `*.d.ts` from app files, so `orca plugin types` reads these copies.
|
||
const pluginAuthorTypingsResources = {
|
||
from: 'resources/plugins',
|
||
to: 'plugins',
|
||
filter: ['declarative/*.d.ts', 'types/*.d.ts']
|
||
}
|
||
// Packaged main/CLI/SSH/speech paths lack pnpm links; copy runtime closure so bare requires cannot fall through to checkout dependencies.
|
||
// Ship only the 166KB emoji dataset used by worktree naming, not all 49MB of emojibase-data.
|
||
const emojiShortcodeDatasetResource = {
|
||
from: 'node_modules/emojibase-data/en/shortcodes/emojibase.json',
|
||
to: 'node_modules/emojibase-data/en/shortcodes/emojibase.json'
|
||
}
|
||
const commonExtraResources = [
|
||
relayExtraResource,
|
||
orcadTemplateExtraResource,
|
||
orcadTemplateNodeModulesExtraResource,
|
||
...bundledRipgrepExtraResources,
|
||
bundledPluginResources,
|
||
pluginAuthorTypingsResources,
|
||
skillFreshnessResources,
|
||
...agentBrowserSkillResources,
|
||
nativeChatVisualsResource,
|
||
emojiShortcodeDatasetResource
|
||
]
|
||
// electron-builder replaces these defaults when `depends` is configured; retain
|
||
// Electron's loader requirements alongside Orca's headless-host dependencies.
|
||
const debElectronRuntimeDependencies = [
|
||
'libgtk-3-0',
|
||
'libnotify4',
|
||
'libnss3',
|
||
'libxss1',
|
||
'libxtst6',
|
||
'xdg-utils',
|
||
'libatspi2.0-0',
|
||
'libuuid1',
|
||
'libsecret-1-0',
|
||
// Electron links these but electron-builder's defaults omit them; t64 is Ubuntu 24.04+/Debian 13.
|
||
'libasound2t64 | libasound2',
|
||
'libgbm1'
|
||
]
|
||
const rpmElectronRuntimeDependencies = [
|
||
'gtk3',
|
||
'libnotify',
|
||
'nss',
|
||
'libXScrnSaver',
|
||
'(libXtst or libXtst6)',
|
||
'xdg-utils',
|
||
'at-spi2-core',
|
||
'(libuuid or libuuid1)',
|
||
'alsa-lib'
|
||
]
|
||
|
||
// Mirror OS document extensions: CJS packager cannot import TS; keep startup/os-opened-documents.ts and NSIS hooks aligned.
|
||
const MARKDOWN_FILE_EXTENSIONS = ['md', 'markdown', 'mdx']
|
||
const TABULAR_FILE_EXTENSIONS = ['csv', 'tsv']
|
||
|
||
// Config loads without unused Windows addons; beforePack enforces target natives. Only @vscode/windows-process-tree has os:win32; workspace links prove no binary exists.
|
||
const windowsRuntimeResources = existsSync(
|
||
join(__dirname, '..', 'node_modules', '@vscode', 'windows-process-tree', 'package.json')
|
||
)
|
||
? createPackagedRuntimeNodeModuleResources('win32')
|
||
: []
|
||
|
||
// Shared with the terminal host helper so its purpose strings cannot drift from the app's.
|
||
const macUsageDescriptions = {
|
||
NSAppleEventsUsageDescription:
|
||
'Orca allows terminal-launched developer tools to automate local apps when you request it.',
|
||
NSBluetoothAlwaysUsageDescription:
|
||
'Orca allows terminal-launched developer tools to access Bluetooth devices when you request it.',
|
||
NSBluetoothPeripheralUsageDescription:
|
||
'Orca allows terminal-launched developer tools to access Bluetooth devices when you request it.',
|
||
NSCalendarsUsageDescription:
|
||
'Orca allows terminal-launched developer tools to access your calendars when you request it.',
|
||
NSCalendarsFullAccessUsageDescription:
|
||
'Orca allows terminal-launched developer tools to access your calendars when you request it.',
|
||
NSCameraUsageDescription: "Application requests access to the device's camera.",
|
||
NSContactsUsageDescription:
|
||
'Orca allows terminal-launched developer tools to access your contacts when you request it.',
|
||
NSLocationUsageDescription:
|
||
'Orca allows terminal-launched developer tools to access location when you request it.',
|
||
NSLocalNetworkUsageDescription:
|
||
'Orca allows terminal-launched developer tools to discover and connect to local development servers when you request it.',
|
||
NSMicrophoneUsageDescription: "Application requests access to the device's microphone.",
|
||
NSRemindersUsageDescription:
|
||
'Orca allows terminal-launched developer tools to access your reminders when you request it.',
|
||
NSRemindersFullAccessUsageDescription:
|
||
'Orca allows terminal-launched developer tools to access your reminders when you request it.',
|
||
NSAudioCaptureUsageDescription:
|
||
'Orca allows terminal-launched developer tools to capture desktop audio when you request it.',
|
||
NSBonjourServices: ['_http._tcp', '_https._tcp'],
|
||
...MACOS_FOLDER_USAGE_DESCRIPTIONS
|
||
}
|
||
// Developer ID team that signs every release-channel mac build.
|
||
const ORCA_MAC_TEAM_ID = '6CX3WHS9HZ'
|
||
|
||
/** @type {import('electron-builder').Configuration} */
|
||
module.exports = {
|
||
appId,
|
||
productName: 'Orca',
|
||
protocols: [{ name: 'Orca', schemes: ['orca'] }],
|
||
toolsets: { appimage: '1.0.3' },
|
||
...(devChannelBuildVersion
|
||
? { extraMetadata: { version: devChannelBuildVersion } }
|
||
: localBuildVersion
|
||
? { extraMetadata: { version: localBuildVersion } }
|
||
: {}),
|
||
directories: {
|
||
buildResources: 'resources/build'
|
||
},
|
||
files: [
|
||
'!**/.vscode/*',
|
||
// Repo inputs already bundled/copied through extraResources would bloat app.asar.
|
||
'!src{,/**/*}',
|
||
'!out/orcad{,/**/*}',
|
||
// Never in app.asar: the template ships via orcadTemplateExtraResource; prebuilds are build inputs.
|
||
'!out/orcad-*{,/**/*}',
|
||
'!out/.orcad-*{,/**/*}',
|
||
// Why: the pinned Node a local orcad build references (~120 MB) and its download cache.
|
||
'!out/runtimes{,/**/*}',
|
||
'!out/node-runtime-cache{,/**/*}',
|
||
'!config{,/**/*}',
|
||
// Cloud services deploy separately; their sources are not desktop runtime resources.
|
||
'!cloud{,/**/*}',
|
||
// Release archives and other build staging are not runtime resources.
|
||
'!.build{,/**/*}',
|
||
// Worktree-local Vite dependency caches are development inputs.
|
||
'!.cache/vite{,.lock}{,/**/*}',
|
||
'!docs{,/**/*}',
|
||
'!mobile{,/**/*}',
|
||
'!native{,/**/*}',
|
||
'!skills{,/**/*}',
|
||
// Authoring guides/stubs compile into runtime artifacts; source trees only duplicate content.
|
||
'!skill-guides{,/**/*}',
|
||
'!skill-stubs{,/**/*}',
|
||
'!tests{,/**/*}',
|
||
// Examples have no runtime consumer and include adversarial hostile-panel; ship only resources/plugins/launch.
|
||
'!examples{,/**/*}',
|
||
// Exclude gitignored local screenshot evidence defensively so packaging cannot pick up stray captures.
|
||
'!pr-evidence{,/**/*}',
|
||
// Local build logs and rollback copies are never application resources.
|
||
'!notes{,/**/*}',
|
||
// Local tooling directories are not runtime inputs and may contain symlink loops.
|
||
'!{.claude,.grok,.agents,.codex}{,/**/*}',
|
||
'!Casks{,/**/*}',
|
||
'!{AGENTS.md,CLAUDE.md,DEVELOPING.md,bundle-size-progress.md,ORCHESTRATION_IMPLEMENTATION_CHECKLIST.md,ORCHESTRATION_STRUCTURED_OUTPUT_DESIGN.md}',
|
||
'!out/**/*.test.js',
|
||
// Why: main builds with sourcemap:'hidden' so release CI can publish maps
|
||
// for decoding minified crash traces. The app never loads them (no
|
||
// sourceMappingURL is emitted), and packing them would add ~34MB to app.asar.
|
||
'!out/**/*.map',
|
||
// Why: Vite's manifest is only used to project the paired web client.
|
||
'!out/renderer/.vite{,/**/*}',
|
||
// The installed browser serves its allowlisted assets from the unpacked renderer.
|
||
'!out/web{,/**/*}',
|
||
// Local dev Electron.app caches (~270MB/branch) must not enter packaged builds.
|
||
'!out/electron-dev{,/**/*}',
|
||
// extraResources already ships relay; duplicating it in asar added 14MB and let script-heuristic quarantine gut the whole archive (#20966/#20972).
|
||
'!out/relay{,/**/*}',
|
||
'!electron.vite.config.{js,ts,mjs,cjs}',
|
||
'!{.oxlintrc.json,.oxfmtrc.json,components.json,opencode.json,orca.yaml,pnpm-workspace.yaml,vite.web.config.ts}',
|
||
'!{.eslintcache,eslint.config.mjs,.prettierignore,.prettierrc.yaml,CHANGELOG.md,README.md}',
|
||
'!{.env,.env.*,.npmrc,pnpm-lock.yaml}',
|
||
'!tsconfig.json',
|
||
// Feature media is read from process.resourcesPath via extraResources; exclude duplicate sources.
|
||
'!resources/onboarding/feature-wall/**',
|
||
'!resources/skills/**',
|
||
'!resources/native-chat-visuals/**',
|
||
// extraResources already ships plugin bytes; exclude source duplicates.
|
||
'!resources/plugins/launch/**',
|
||
// afterPack copies the target's speech engine to resources/node_modules; asar would duplicate it and ship other variants.
|
||
'!node_modules/{transcribe-cpp,@transcribe-cpp,koffi,@koromix}{,/**/*}',
|
||
// CLI shim belongs beside resources/bin/orca.exe; unpacking source creates a second .cmd without its executable (#7351).
|
||
'!resources/win32{,/**/*}'
|
||
],
|
||
// Node-mode CLI/daemon/foreign SQLite reader/serve entries cannot read asar; unpack their shared/main dependencies too.
|
||
// Keep out/package.json’s CommonJS boundary and remote CLI WebSocket/E2EE deps. Electron-only workers may stay packed.
|
||
// Renderer/mobile-web assets need real paths: swept temporary asar extracts are not recreated (#27976).
|
||
asarUnpack: [
|
||
'out/package.json',
|
||
'out/cli/**',
|
||
'out/shared/**',
|
||
'out/main/agent-hooks/**',
|
||
'out/main/antigravity/**',
|
||
'out/main/claude/**',
|
||
'out/main/codex/**',
|
||
// The CLI loads the socket-dir check; tsc-emitted output also needs its mode constants beside it.
|
||
'out/main/daemon/endpoint/publication/daemon-{socket-endpoint-path,private-file-modes}.js',
|
||
'out/main/copilot/**',
|
||
'out/main/cursor/**',
|
||
'out/main/droid/**',
|
||
'out/main/gemini/**',
|
||
'out/main/gitlab/projects/project-ref-parser.js',
|
||
'out/main/grok/**',
|
||
'out/main/hermes/**',
|
||
'out/main/filesystem/asar-transparent-fs.js',
|
||
'out/main/filesystem/macos-tree-clone.js',
|
||
'out/main/orca-profiles/profile-index-store.js',
|
||
'out/main/persistence/profile-state/**',
|
||
'out/main/plugins/plugin-artifact-validation.js',
|
||
'out/main/plugins/plugin-content-hash.js',
|
||
'out/main/plugins/plugin-catalog-release-entry.js',
|
||
'out/main/environment-doctor/environment-doctor.js',
|
||
'out/main/environment-doctor/environment-doctor-host.js',
|
||
// `orca plugin test` and `orca plugin check` load the runtime, kit and hooks in Node.
|
||
'resources/plugins/declarative/*.mjs',
|
||
'out/main/skills/bundled-skill-install/bundled-skill-local-host.js',
|
||
'out/main/skills/bundled-skill-install/bundled-skill-installation.js',
|
||
'out/main/startup/network-compatibility/http1-compatibility-marker.js',
|
||
'out/main/daemon-entry.js',
|
||
'out/main/session-scanner-service-entry.js',
|
||
'out/main/wsl-transcript-fs-process-entry.js',
|
||
'out/main/foreign-sqlite-reader-entry.js',
|
||
'out/main/agent-identity-worker-entry.js',
|
||
'out/main/session-search-worker-entry.js',
|
||
'out/main/plugin-host-entry.js',
|
||
'out/main/browser-extension-native-host-supervisor-entry.js',
|
||
'out/main/computer-sidecar.js',
|
||
'out/main/parcel-watcher-process-entry.js',
|
||
'out/main/orcad/orcad-local-serve-selection-entry.js',
|
||
'out/main/orcad/instance-lock/orcad-stale-lock-reclaim.js',
|
||
'out/main/orcad/profile-state/orcad-host-identity-reset.js',
|
||
'out/main/chunks/**',
|
||
'out/renderer/**',
|
||
'out/mobile-web/**',
|
||
'resources/**',
|
||
'node_modules/ws/**',
|
||
'node_modules/tweetnacl/**',
|
||
'node_modules/zod/**',
|
||
'node_modules/yaml/**'
|
||
],
|
||
artifactBuildCompleted: ({ file, arch }) => {
|
||
if (file.endsWith('.AppImage')) {
|
||
verifyStaticAppImagePackage(file, arch)
|
||
}
|
||
},
|
||
// electron-builder calls this with the context alone. The second parameter is the bundle root,
|
||
// so a test can point the guard at a scratch bundle instead of needing the repo's out/ built.
|
||
beforePack: (
|
||
context,
|
||
mobileWebBundleDir = MOBILE_WEB_BUNDLE_DIR,
|
||
ensureWaylandClipboard = ensureBundledWaylandClipboard,
|
||
orcadTemplateDir = join(__dirname, '..', orcadTemplateExtraResource.from)
|
||
) => {
|
||
// The template is the server every SSH host gets, so a dark test template must never ship.
|
||
assertNotDarkBuild([join(__dirname, '..', 'out', 'main'), mobileWebBundleDir, orcadTemplateDir])
|
||
assertPackagedNativeVariantsInstalled(context.electronPlatformName, context.arch)
|
||
assertBundledRipgrepInstalled()
|
||
assertProcessHostOutputBuilt()
|
||
assertOrcadTemplateBuilt()
|
||
assertMobileWebBundleBuilt(mobileWebBundleDir)
|
||
if (context.electronPlatformName === 'linux') {
|
||
ensureWaylandClipboard(context.arch)
|
||
}
|
||
},
|
||
afterPack: async (context) => {
|
||
const resourcesDir =
|
||
context.electronPlatformName === 'darwin'
|
||
? join(
|
||
context.appOutDir,
|
||
`${context.packager.appInfo.productFilename}.app`,
|
||
'Contents',
|
||
'Resources'
|
||
)
|
||
: join(context.appOutDir, 'resources')
|
||
if (!existsSync(resourcesDir)) {
|
||
throw new Error(`Missing packaged resources directory: ${resourcesDir}`)
|
||
}
|
||
// FpmTarget replaces this with deb/rpm while building those artifacts from the shared app tree.
|
||
if (context.electronPlatformName === 'linux') {
|
||
writeFileSync(join(resourcesDir, 'package-type'), 'AppImage')
|
||
}
|
||
if (context.electronPlatformName === 'darwin') {
|
||
applyMacHelperFolderUsageDescriptions(
|
||
join(context.appOutDir, `${context.packager.appInfo.productFilename}.app`),
|
||
context.packager.appInfo.productFilename
|
||
)
|
||
const architecture = macSliceArch(context.arch, 'local-build compatibility')
|
||
const schedulingAddon = buildMacShipItScheduling(resourcesDir, architecture)
|
||
await signMacStandaloneHelper(schedulingAddon, 'ShipIt scheduling', context.packager)
|
||
const version = context.packager.appInfo.version
|
||
let commit = process.env.ORCA_BUILD_COMMIT || process.env.GITHUB_SHA || 'unknown'
|
||
if (commit === 'unknown') {
|
||
try {
|
||
commit = execFileSync('git', ['rev-parse', '--short=12', 'HEAD'], {
|
||
encoding: 'utf8'
|
||
}).trim()
|
||
} catch {
|
||
// Source archives can still produce a signed build with an explicit version.
|
||
}
|
||
}
|
||
writeMacBuildCompatibility(resourcesDir, { version, commit, architecture })
|
||
}
|
||
stampPackagedCliVersion(resourcesDir, context.packager.appInfo.version)
|
||
// Before the prune, which drops the copied packages' type declarations too.
|
||
copyPackagedTranscribeCpp(
|
||
readPackage,
|
||
resourcesDir,
|
||
context.electronPlatformName,
|
||
normalizeElectronArchitecture(context.arch)
|
||
)
|
||
prunePackagedRuntimeNodeModules(resourcesDir, context.electronPlatformName, context.arch)
|
||
assertNoWindowsExecutablesInRuntimeNodeModules(resourcesDir, context.electronPlatformName)
|
||
// Gate native glibc floor after pruning foreign CPU variants; GLIBC_2.34 once broke Ubuntu 20.04 startup (#9902).
|
||
let appImageNodeRuntime = null
|
||
if (context.electronPlatformName === 'linux') {
|
||
const targetArch = { 1: 'x64', 3: 'arm64' }[context.arch]
|
||
// Before the floor gate, so the bundled Node is held to the same glibc floor.
|
||
appImageNodeRuntime = await placeAppImageNodeRuntime(resourcesDir, targetArch)
|
||
// Why the arch is passed: symbol-version checks pass happily on a wrong-architecture binary,
|
||
// so a cross-built slice could ship the host's pty.node and only fail at runtime.
|
||
finalizePackagedWaylandClipboard(resourcesDir)
|
||
verifyLinuxGlibcFloor(context.appOutDir, { targetArch })
|
||
}
|
||
verifyPackagedMainRuntimeDeps(resourcesDir)
|
||
// Smoke only host-runnable slices: daemon JS loads target-arch node-pty. Arch enum ia32=0, x64=1, armv7l=2, arm64=3, universal=4.
|
||
const archEnumByNodeArch = { ia32: 0, x64: 1, armv7l: 2, arm64: 3 }
|
||
const hostArchEnum = archEnumByNodeArch[process.arch]
|
||
const canExecuteTargetArch = context.arch === hostArchEnum || context.arch === 4
|
||
if (context.electronPlatformName === 'win32') {
|
||
verifyPackagedWindowsNodePty(resourcesDir, context.arch, { canExecuteTargetArch })
|
||
verifyPackagedWindowsAgentHook(resourcesDir, context.arch)
|
||
}
|
||
verifySkillsCliRuntime(join(resourcesDir, 'app.asar.unpacked', 'out'), resourcesDir, {
|
||
executeCommands: canExecuteTargetArch
|
||
})
|
||
if (!canExecuteTargetArch) {
|
||
console.log(
|
||
`[verify-skills-cli-runtime] skipped command probes on cross-arch slice (target ${context.arch}, host ${process.arch})`
|
||
)
|
||
}
|
||
if (canExecuteTargetArch) {
|
||
verifyPackagedDaemonEntryBoots(resourcesDir)
|
||
if (appImageNodeRuntime) {
|
||
await verifyAppImageNodeRuntimeServesPty(resourcesDir, appImageNodeRuntime)
|
||
}
|
||
} else {
|
||
// Cross-arch smoke cannot run, but missing unpacked entry is always a packaging error.
|
||
assertPackagedDaemonEntryExists(resourcesDir)
|
||
console.log(
|
||
`[verify-packaged-daemon-entry] skipped boot on cross-arch slice (target ${context.arch}, host ${process.arch})`
|
||
)
|
||
}
|
||
// Validate real electron-builder output so broken extraResources mappings fail before shipping.
|
||
verifyPackagedPluginResources(resourcesDir)
|
||
verifyPackagedPluginTypings(resourcesDir)
|
||
assertPackagedWebClient(resourcesDir)
|
||
assertPackagedServeSelectionEntry(resourcesDir)
|
||
finalizePackagedRipgrep(resourcesDir)
|
||
for (const filename of readdirSync(resourcesDir)) {
|
||
if (!filename.startsWith('agent-browser-')) {
|
||
continue
|
||
}
|
||
// Upstream executable bits vary (notably darwin-x64); restore them so packaged execFile works.
|
||
chmodSync(join(resourcesDir, filename), 0o755)
|
||
// The orcad template hashes this exact file next, so app signing must not rewrite it later.
|
||
if (context.electronPlatformName === 'darwin') {
|
||
await signMacStandaloneHelper(
|
||
join(resourcesDir, filename),
|
||
filename,
|
||
context.packager,
|
||
resolve(__dirname, '../resources/build/entitlements.mac.plist')
|
||
)
|
||
}
|
||
}
|
||
await finalizePackagedOrcadTemplate(resourcesDir, {
|
||
platform: context.electronPlatformName,
|
||
signMacBinary: (path) =>
|
||
signMacStandaloneHelper(path, 'orcad template binary', context.packager)
|
||
})
|
||
chmodUnixCliLaunchers(resourcesDir, context.electronPlatformName)
|
||
if (context.electronPlatformName === 'darwin') {
|
||
await signMacComputerUseHelper(join(resourcesDir, 'Orca Computer Use.app'), context.packager)
|
||
await signMacStandaloneHelper(
|
||
join(resourcesDir, '..', 'MacOS', 'orca-notification-status'),
|
||
'orca-notification-status',
|
||
context.packager
|
||
)
|
||
await signMacStandaloneHelper(
|
||
join(resourcesDir, '..', 'MacOS', 'orca-keyboard-layout'),
|
||
'orca-keyboard-layout',
|
||
context.packager
|
||
)
|
||
await signMacStandaloneHelper(
|
||
join(resourcesDir, '..', 'MacOS', 'orca-process-footprint'),
|
||
'orca-process-footprint',
|
||
context.packager
|
||
)
|
||
await packageMacTerminalHost(join(resourcesDir, '..', '..'), context)
|
||
}
|
||
},
|
||
win: {
|
||
executableName: 'Orca',
|
||
// SignPath signs Windows installers after packaging, so set release publisherName explicitly.
|
||
// Unsigned dev channels omit it or updater signature checks reject dev updates and return to stable.
|
||
// Sign hook signs nothing: it relays transient NSIS uninstaller files to CI SignPath; unset relay env leaves local/dev builds inert.
|
||
signtoolOptions: {
|
||
sign: signWindowsUninstallerViaSignPath,
|
||
...(isWinDevChannel ? {} : { publisherName: 'SignPath Foundation' })
|
||
},
|
||
...(isWinDevChannel ? { verifyUpdateCodeSignature: false } : {}),
|
||
extraResources: [
|
||
...commonExtraResources,
|
||
...windowsRuntimeResources,
|
||
{
|
||
from: 'resources/win32/bin/orca.cmd',
|
||
to: 'bin/orca.cmd'
|
||
},
|
||
{
|
||
from: 'native/windows-cli-launcher/.build/orca.exe',
|
||
to: 'bin/orca.exe'
|
||
},
|
||
{
|
||
from: 'native/windows-cli-launcher/.build/orca-administrator-restart.exe',
|
||
to: 'bin/orca-administrator-restart.exe'
|
||
},
|
||
// Why resources/bin: SignPath signs every unsigned PE under win-unpacked, and the hook
|
||
// installer copies this signed file into ~/.orca/agent-hooks (#20913).
|
||
{
|
||
from: 'native/windows-agent-hook/.build/orca-agent-hook.exe',
|
||
to: 'bin/orca-agent-hook.exe'
|
||
},
|
||
{
|
||
from: 'node_modules/agent-browser/bin/agent-browser-win32-x64.exe',
|
||
to: 'agent-browser-win32-x64.exe'
|
||
},
|
||
{
|
||
from: 'native/computer-use-windows/runtime.ps1',
|
||
to: 'computer-use-windows/runtime.ps1'
|
||
},
|
||
featureWallResources
|
||
]
|
||
},
|
||
nsis: {
|
||
artifactName: 'orca-windows-setup.${ext}',
|
||
shortcutName: '${productName}',
|
||
uninstallDisplayName: '${productName}',
|
||
createDesktopShortcut: 'always',
|
||
// One NSIS include hosts update-guarded daemon cleanup and additive markdown Open With registration.
|
||
// Avoid fileAssociations: it would steal default .md ownership; see the include’s header.
|
||
include: resolve(__dirname, 'nsis', 'orca-installer-hooks.nsh')
|
||
},
|
||
mac: {
|
||
// Alternate rank preserves Finder’s default; one entry per extension because app-builder globs the value as a string.
|
||
fileAssociations: [
|
||
...MARKDOWN_FILE_EXTENSIONS.map((ext) => ({
|
||
ext,
|
||
name: 'Markdown Document',
|
||
description: 'Markdown Document',
|
||
role: 'Editor',
|
||
rank: 'Alternate'
|
||
})),
|
||
...TABULAR_FILE_EXTENSIONS.map((ext) => ({
|
||
ext,
|
||
name: `${ext.toUpperCase()} Document`,
|
||
description: `${ext.toUpperCase()} Document`,
|
||
role: 'Editor',
|
||
rank: 'Alternate'
|
||
}))
|
||
],
|
||
// Set for ONNX Runtime (#22690, #39135), now gone; transcribe-cpp needs 11.0, Electron 12.0. Kept until
|
||
// Kelvin decides. The update-manifest gate reads this from Info.plist.
|
||
minimumSystemVersion: '13.4',
|
||
icon: 'resources/build/icon.icns',
|
||
entitlements: 'resources/build/entitlements.mac.plist',
|
||
entitlementsInherit: 'resources/build/entitlements.mac.plist',
|
||
...(isMacRelease
|
||
? { sign: resolve(__dirname, './scripts/desktop-distribution/macos/sign-mac-app.mjs') }
|
||
: {}),
|
||
signIgnore: [
|
||
...bundledRipgrepMacSignIgnore,
|
||
...orcadTemplateMacSignIgnore,
|
||
// afterPack signs it before the orcad template hashes it.
|
||
'/Contents/Resources/agent-browser-darwin-[^/]+$',
|
||
...macTerminalHostSignIgnore
|
||
],
|
||
extendInfo: macUsageDescriptions,
|
||
// Local macOS builds need no release credentials; hardening/notarization stay on explicit release paths.
|
||
hardenedRuntime: isMacRelease,
|
||
// Dev notarization preserves TCC identifier/team identity across cdhash changes; unticketed hourly updates otherwise lose folder grants with EPERM and no prompt.
|
||
notarize: isMacRelease,
|
||
extraResources: [
|
||
...commonExtraResources,
|
||
...createPackagedRuntimeNodeModuleResources('darwin'),
|
||
{
|
||
from: 'resources/darwin/bin/orca',
|
||
to: 'bin/orca'
|
||
},
|
||
{
|
||
from: 'node_modules/agent-browser/bin/agent-browser-darwin-${arch}',
|
||
to: 'agent-browser-darwin-${arch}'
|
||
},
|
||
{
|
||
from: 'native/computer-use-macos/.build/release/Orca Computer Use.app',
|
||
to: 'Orca Computer Use.app'
|
||
},
|
||
featureWallResources
|
||
],
|
||
// macOS 26 UNUserNotificationCenter aborts helpers launched from Resources; execute from Contents/MacOS (#7929).
|
||
extraFiles: [
|
||
{
|
||
from: 'native/notification-status-macos/.build/release/orca-notification-status',
|
||
to: 'MacOS/orca-notification-status'
|
||
},
|
||
{
|
||
from: 'native/keyboard-layout-macos/.build/release/orca-keyboard-layout',
|
||
to: 'MacOS/orca-keyboard-layout'
|
||
},
|
||
{
|
||
from: 'native/process-footprint-macos/.build/release/orca-process-footprint',
|
||
to: 'MacOS/orca-process-footprint'
|
||
}
|
||
],
|
||
target: [
|
||
{
|
||
target: 'dmg',
|
||
arch: ['x64', 'arm64']
|
||
},
|
||
{
|
||
target: 'zip',
|
||
arch: ['x64', 'arm64']
|
||
}
|
||
]
|
||
},
|
||
// Why: release builds should fail if signing is unavailable instead of
|
||
// silently downgrading to ad-hoc artifacts that look shippable in CI logs.
|
||
forceCodeSigning: isMacRelease,
|
||
dmg: {
|
||
artifactName: 'orca-macos-${arch}.${ext}'
|
||
},
|
||
linux: {
|
||
// shared-mime-info already maps md/markdown: advertise handler only, leaving mimeapps.list defaults intact.
|
||
// Omit mdx because Ubuntu 24.04 maps it to application/x-genesis-32x-rom; claiming it would need a glob override.
|
||
mimeTypes: ['text/markdown', 'text/csv', 'text/tab-separated-values'],
|
||
// Avoid GNOME Orca’s orca package and /usr/bin/orca names.
|
||
executableName: 'orca-ide',
|
||
// Use icns to generate hicolor sizes; some Linux docks ignore a lone 1024px PNG.
|
||
icon: 'resources/build/icon.icns',
|
||
desktop: {
|
||
entry: {
|
||
// Why: Electron reports WM_CLASS=orca for the visible Linux window;
|
||
// GNOME docks need an exact match to group it with orca-ide.desktop.
|
||
StartupWMClass: 'orca'
|
||
}
|
||
},
|
||
extraResources: [
|
||
...commonExtraResources,
|
||
...createPackagedRuntimeNodeModuleResources('linux'),
|
||
{
|
||
from: 'native/wayland-clipboard/.build/${arch}/orca-wayland-clipboard',
|
||
to: 'bin/orca-wayland-clipboard'
|
||
},
|
||
{
|
||
from: 'resources/linux/bin/orca-ide',
|
||
to: 'bin/orca-ide'
|
||
},
|
||
{
|
||
from: 'node_modules/agent-browser/bin/agent-browser-linux-${arch}',
|
||
to: 'agent-browser-linux-${arch}'
|
||
},
|
||
{
|
||
from: 'native/computer-use-linux/runtime.py',
|
||
to: 'computer-use-linux/runtime.py'
|
||
},
|
||
featureWallResources
|
||
],
|
||
// Keep local artifacts aligned with the release pipeline.
|
||
target: ['AppImage', 'deb', 'rpm'],
|
||
maintainer: 'stablyai',
|
||
category: 'Utility'
|
||
},
|
||
appImage: {
|
||
artifactName: isLinuxArm64Release ? 'orca-linux-arm64.${ext}' : 'orca-linux.${ext}'
|
||
},
|
||
deb: {
|
||
packageName: 'orca-ide',
|
||
artifactName: 'orca-ide_${version}_${arch}.${ext}',
|
||
// Headless Chromium still needs display service; orca serve starts packaged Xvfb when available (ensure-virtual-display.ts).
|
||
depends: [
|
||
...debElectronRuntimeDependencies,
|
||
'python3',
|
||
'python3-gi',
|
||
'gir1.2-atspi-2.0',
|
||
'at-spi2-core',
|
||
'xdotool',
|
||
'xclip',
|
||
'xvfb'
|
||
],
|
||
// Install CLI onto PATH for headless hosts where GUI-triggered CliInstaller can never run.
|
||
afterInstall: 'resources/linux/packaging/after-install.sh',
|
||
afterRemove: 'resources/linux/packaging/after-remove.sh',
|
||
// Why: only an AppImage's files can vanish under a running daemon (see appimage-node-runtime.cjs).
|
||
fpm: appImageNodeRuntimeFpmArgs
|
||
},
|
||
rpm: {
|
||
packageName: 'orca-ide',
|
||
artifactName: 'orca-ide-${version}.${arch}.${ext}',
|
||
// Why: see deb depends. RPM distros ship Xvfb as xorg-x11-server-Xvfb (there
|
||
// is no `xvfb` package), so the name differs from the deb here.
|
||
depends: [
|
||
...rpmElectronRuntimeDependencies,
|
||
'python3',
|
||
'python3-gobject',
|
||
'xdotool',
|
||
'xclip',
|
||
'xorg-x11-server-Xvfb'
|
||
],
|
||
// Why: same headless CLI-on-PATH registration as deb; rpm runs these via fpm.
|
||
afterInstall: 'resources/linux/packaging/after-install.sh',
|
||
afterRemove: 'resources/linux/packaging/after-remove.sh',
|
||
// Why: only an AppImage's files can vanish under a running daemon (see appimage-node-runtime.cjs).
|
||
fpm: appImageNodeRuntimeFpmArgs
|
||
},
|
||
beforeBuild: electronBuilderNativeRebuild,
|
||
// Dual-arch builds need target node-pty rebuilds or Intel Macs get arm64 binaries and posix_spawnp failure.
|
||
// beforeBuild performs targeted rebuild then returns false to skip optional cpu-features.
|
||
npmRebuild: true,
|
||
publish: {
|
||
provider: 'github',
|
||
owner: 'stablyai',
|
||
repo: devChannelRepo ?? 'orca',
|
||
// Keep main-repo releases draft until release-cut verifies all assets; early --publish always would expose missing Windows installers.
|
||
releaseType: devChannelRepo ? 'prerelease' : 'draft'
|
||
}
|
||
}
|
||
|
||
// Stamp the effective channel version where node-mode CLI code can read it.
|
||
function stampPackagedCliVersion(resourcesDir, version) {
|
||
const packageJsonPath = join(resourcesDir, 'app.asar.unpacked', 'out', 'package.json')
|
||
if (!existsSync(packageJsonPath)) {
|
||
throw new Error(`Missing unpacked CLI package boundary: ${packageJsonPath}`)
|
||
}
|
||
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'))
|
||
writeFileSync(packageJsonPath, `${JSON.stringify({ ...packageJson, version }, null, 2)}\n`)
|
||
}
|
||
|
||
function chmodUnixCliLaunchers(resourcesDir, electronPlatformName) {
|
||
if (electronPlatformName === 'win32') {
|
||
return
|
||
}
|
||
for (const launcherName of ['orca', 'orca-ide']) {
|
||
const launcherPath = join(resourcesDir, 'bin', launcherName)
|
||
if (!existsSync(launcherPath)) {
|
||
continue
|
||
}
|
||
// Public shell commands need executable extraResources modes despite source/packager drift.
|
||
chmodSync(launcherPath, 0o755)
|
||
}
|
||
}
|
||
|
||
async function signMacComputerUseHelper(helperAppPath, packager) {
|
||
if (!existsSync(helperAppPath)) {
|
||
if (isMacRelease) {
|
||
throw new Error(`Missing Orca Computer Use helper app at ${helperAppPath}`)
|
||
}
|
||
return
|
||
}
|
||
const codeSigningInfo =
|
||
isMacRelease && process.env.CSC_LINK && packager?.codeSigningInfo?.value
|
||
? await packager.codeSigningInfo.value
|
||
: null
|
||
const identity =
|
||
process.env.ORCA_COMPUTER_MACOS_SIGN_IDENTITY ??
|
||
process.env.CSC_NAME ??
|
||
findInstalledMacSigningIdentity(codeSigningInfo?.keychainFile) ??
|
||
(isMacRelease ? null : '-')
|
||
if (!identity) {
|
||
throw new Error('Missing signing identity for Orca Computer Use helper app')
|
||
}
|
||
// Why: TCC grants attach to this nested app's code identity. Sign it before
|
||
// the outer Orca.app is sealed so production builds preserve that identity.
|
||
execFileSync('codesign', codesignArgs(identity, helperAppPath), { stdio: 'inherit' })
|
||
execFileSync('codesign', ['--verify', '--deep', '--strict', helperAppPath], {
|
||
stdio: 'inherit'
|
||
})
|
||
}
|
||
|
||
// electron-builder `Arch` enum: x64=1, arm64=3. A universal (4) slice has no single arch.
|
||
function macSliceArch(archEnum, purpose) {
|
||
const arch = { 1: 'x64', 3: 'arm64' }[archEnum]
|
||
if (!arch) {
|
||
throw new Error(`Unsupported ${purpose} architecture: ${archEnum}`)
|
||
}
|
||
return arch
|
||
}
|
||
|
||
// Why after the node_modules prune: the helper copies this slice's node-pty.
|
||
async function packageMacTerminalHost(appPath, context) {
|
||
const arch = macSliceArch(context.arch, 'macOS terminal host')
|
||
await buildMacTerminalHost({
|
||
appPath,
|
||
arch,
|
||
iconPath: resolve(__dirname, '../resources/build/icon.icns'),
|
||
entitlementsPath: resolve(__dirname, '../resources/build/entitlements.mac.plist'),
|
||
usageDescriptions: macUsageDescriptions,
|
||
signCode: (path, { entitlements }) =>
|
||
signMacStandaloneHelper(path, 'terminal host', context.packager, entitlements)
|
||
})
|
||
// Only a local build may skip a slice this host cannot run; a shipped slice must serve a PTY.
|
||
await verifyPackagedMacTerminalHostBoots(appPath, {
|
||
arch,
|
||
required: isMacRelease || process.env.CI === 'true'
|
||
})
|
||
await verifyMacTerminalHost({
|
||
appPath,
|
||
arch,
|
||
usageDescriptions: macUsageDescriptions,
|
||
expectedTeamId: isMacRelease ? ORCA_MAC_TEAM_ID : null
|
||
})
|
||
}
|
||
|
||
async function signMacStandaloneHelper(helperPath, helperName, packager, entitlements) {
|
||
if (!existsSync(helperPath)) {
|
||
if (isMacRelease) {
|
||
throw new Error(`Missing ${helperName} helper at ${helperPath}`)
|
||
}
|
||
return
|
||
}
|
||
const codeSigningInfo =
|
||
isMacRelease && process.env.CSC_LINK && packager?.codeSigningInfo?.value
|
||
? await packager.codeSigningInfo.value
|
||
: null
|
||
const identity =
|
||
process.env.CSC_NAME ??
|
||
findInstalledMacSigningIdentity(codeSigningInfo?.keychainFile) ??
|
||
(isMacRelease ? null : '-')
|
||
if (!identity) {
|
||
throw new Error(`Missing signing identity for ${helperName} helper`)
|
||
}
|
||
// Why: nested executables must be signed before the outer app bundle is sealed.
|
||
const args = ['--force', '--sign', identity]
|
||
if (isMacRelease) {
|
||
args.push('--options', 'runtime', '--timestamp')
|
||
}
|
||
if (entitlements) {
|
||
args.push('--entitlements', entitlements)
|
||
}
|
||
args.push(helperPath)
|
||
execFileSync('codesign', args, { stdio: 'inherit' })
|
||
execFileSync('codesign', ['--verify', '--strict', helperPath], { stdio: 'inherit' })
|
||
}
|
||
|
||
function codesignArgs(identity, targetPath) {
|
||
const args = ['--force', '--deep', '--sign', identity]
|
||
if (isMacRelease) {
|
||
args.push(
|
||
'--options',
|
||
'runtime',
|
||
'--timestamp',
|
||
'--entitlements',
|
||
resolve(__dirname, '../resources/build/entitlements.computer-use.mac.plist')
|
||
)
|
||
}
|
||
args.push(targetPath)
|
||
return args
|
||
}
|
||
|
||
function findInstalledMacSigningIdentity(keychainFile) {
|
||
try {
|
||
const output = execFileSync(
|
||
'security',
|
||
['find-identity', '-v', '-p', 'codesigning', ...(keychainFile ? [keychainFile] : [])],
|
||
{
|
||
encoding: 'utf8'
|
||
}
|
||
)
|
||
const releaseMatch =
|
||
output.match(/"([^"]*Developer ID Application:[^"]+)"/) ??
|
||
output.match(/"([^"]*Apple Distribution:[^"]+)"/)
|
||
if (releaseMatch?.[1]) {
|
||
return releaseMatch[1]
|
||
}
|
||
if (!isMacRelease) {
|
||
return output.match(/"([^"]*Apple Development:[^"]+)"/)?.[1] ?? null
|
||
}
|
||
} catch {}
|
||
return null
|
||
}
|