diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 41f5883a..1ab97a28 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -254,7 +254,7 @@ jobs: run: ${{ matrix.dist_command }} --publish never - name: Validate packaged bundle (macOS ${{ matrix.arch }}) - run: node ./scripts/electron-builder/verifyBundle.cjs "release/mac-${{ matrix.arch }}/Agent-Teams-UI.app" darwin ${{ matrix.arch }} + run: node ./scripts/electron-builder/verifyBundle.cjs "release/mac-${{ matrix.arch }}/Agent Teams UI.app" darwin ${{ matrix.arch }} - name: Upload assets to release if: startsWith(github.ref, 'refs/tags/v') diff --git a/package.json b/package.json index d3f74fd9..b61c4d8d 100644 --- a/package.json +++ b/package.json @@ -32,12 +32,12 @@ "team:smoke-changes-real-data": "tsx scripts/team-changes-real-data-smoke.ts", "prebuild": "tsx scripts/fetch-pricing-data.ts && pnpm --filter agent-teams-controller build && pnpm --filter agent-teams-mcp build", "build": "node --max-old-space-size=8192 ./node_modules/electron-vite/bin/electron-vite.js build", - "dist": "electron-builder --mac --win --linux", - "dist:mac": "electron-builder --mac", - "dist:mac:arm64": "electron-builder --mac --arm64", - "dist:mac:x64": "electron-builder --mac --x64", - "dist:win": "electron-builder --win", - "dist:linux": "electron-builder --linux", + "dist": "node ./scripts/electron-builder/dist.mjs --mac --win --linux", + "dist:mac": "node ./scripts/electron-builder/dist.mjs --mac", + "dist:mac:arm64": "node ./scripts/electron-builder/dist.mjs --mac --arm64", + "dist:mac:x64": "node ./scripts/electron-builder/dist.mjs --mac --x64", + "dist:win": "node ./scripts/electron-builder/dist.mjs --win", + "dist:linux": "node ./scripts/electron-builder/dist.mjs --linux", "preview": "electron-vite preview", "typecheck": "tsc --noEmit", "typecheck:workspace": "pnpm typecheck && pnpm --filter agent-teams-mcp typecheck && pnpm --filter agent-teams-mcp typecheck:test", @@ -230,7 +230,7 @@ }, "build": { "appId": "com.agent-teams.app", - "productName": "Agent-Teams-UI", + "productName": "Agent Teams UI", "directories": { "output": "release" }, @@ -305,10 +305,7 @@ "pacman" ], "icon": "resources/icons/png", - "category": "Development", - "desktop": { - "Name": "Agent Teams UI" - } + "category": "Development" }, "appImage": { "artifactName": "Agent.Teams.AI-${version}.${ext}" diff --git a/scripts/electron-builder/dist.mjs b/scripts/electron-builder/dist.mjs new file mode 100644 index 00000000..e3c72615 --- /dev/null +++ b/scripts/electron-builder/dist.mjs @@ -0,0 +1,97 @@ +#!/usr/bin/env node +import { spawn } from 'node:child_process'; +import { createRequire } from 'node:module'; +import { pathToFileURL } from 'node:url'; + +const require = createRequire(import.meta.url); + +const PLATFORM_FLAGS = new Map([ + ['--mac', 'mac'], + ['-m', 'mac'], + ['--win', 'win'], + ['-w', 'win'], + ['--linux', 'linux'], + ['-l', 'linux'], +]); + +const PLATFORM_ARGS = { + mac: '--mac', + win: '--win', + linux: '--linux', +}; + +const LINUX_PACKAGE_NAME_OVERRIDES = [ + '--config.productName=Agent-Teams-UI', + '--config.linux.desktop.entry.Name=Agent Teams UI', +]; + +export function buildElectronBuilderInvocations(argv) { + const targets = []; + const sharedArgs = []; + + for (const arg of argv) { + const target = PLATFORM_FLAGS.get(arg); + if (target) { + if (!targets.includes(target)) { + targets.push(target); + } + continue; + } + sharedArgs.push(arg); + } + + if (targets.length === 0) { + return [{ args: sharedArgs }]; + } + + return targets.map((target) => ({ + args: [ + PLATFORM_ARGS[target], + ...sharedArgs, + ...(target === 'linux' ? LINUX_PACKAGE_NAME_OVERRIDES : []), + ], + })); +} + +async function runElectronBuilder(args) { + const cliPath = require.resolve('electron-builder/cli.js'); + await new Promise((resolve, reject) => { + const child = spawn(process.execPath, [cliPath, ...args], { + stdio: 'inherit', + env: process.env, + }); + + child.on('error', reject); + child.on('exit', (code, signal) => { + if (code === 0) { + resolve(); + return; + } + reject(new Error(`electron-builder failed with ${signal ?? `exit code ${code}`}`)); + }); + }); +} + +async function main(argv) { + const invocations = buildElectronBuilderInvocations(argv); + + if (process.env.ELECTRON_BUILDER_DIST_DRY_RUN === '1') { + console.log( + JSON.stringify( + invocations.map((invocation) => invocation.args), + null, + 2 + ) + ); + return; + } + + for (const invocation of invocations) { + await runElectronBuilder(invocation.args); + } +} + +const entryPointUrl = process.argv[1] ? pathToFileURL(process.argv[1]).href : null; +if (entryPointUrl === import.meta.url) { + await main(process.argv.slice(2)); +} diff --git a/src/main/index.ts b/src/main/index.ts index ba782746..4c914fc7 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -107,10 +107,6 @@ import { app, BrowserWindow, ipcMain } from 'electron'; import { existsSync } from 'fs'; import { join } from 'path'; -// productName uses hyphens to avoid spaces in the Linux install path (/opt/Agent-Teams-UI/). -// Restore the human-readable display name for macOS menus and Windows system dialogs. -app.setName('Agent Teams UI'); - import { cleanupEditorState, setEditorMainWindow } from './ipc/editor'; import { initializeIpcHandlers, removeIpcHandlers } from './ipc/handlers'; import { registerRendererLogHandlers } from './ipc/rendererLogs'; diff --git a/test/main/build/electronBuilderDistScript.test.ts b/test/main/build/electronBuilderDistScript.test.ts new file mode 100644 index 00000000..dcb80d48 --- /dev/null +++ b/test/main/build/electronBuilderDistScript.test.ts @@ -0,0 +1,52 @@ +// @vitest-environment node +import { pathToFileURL } from 'node:url'; + +import { describe, expect, it } from 'vitest'; + +const scriptUrl = pathToFileURL(`${process.cwd()}/scripts/electron-builder/dist.mjs`).href; + +describe('electron-builder dist wrapper', () => { + it('splits multi-platform builds so Linux-only package name overrides do not affect macOS or Windows', async () => { + const { buildElectronBuilderInvocations } = await import(scriptUrl); + + expect( + buildElectronBuilderInvocations(['--mac', '--win', '--linux', '--publish', 'never']) + ).toEqual([ + { args: ['--mac', '--publish', 'never'] }, + { args: ['--win', '--publish', 'never'] }, + { + args: [ + '--linux', + '--publish', + 'never', + '--config.productName=Agent-Teams-UI', + '--config.linux.desktop.entry.Name=Agent Teams UI', + ], + }, + ]); + }); + + it('adds the filesystem-safe package name override to Linux-only builds', async () => { + const { buildElectronBuilderInvocations } = await import(scriptUrl); + + expect(buildElectronBuilderInvocations(['--linux', '--publish', 'never'])).toEqual([ + { + args: [ + '--linux', + '--publish', + 'never', + '--config.productName=Agent-Teams-UI', + '--config.linux.desktop.entry.Name=Agent Teams UI', + ], + }, + ]); + }); + + it('leaves macOS arch-specific builds unchanged', async () => { + const { buildElectronBuilderInvocations } = await import(scriptUrl); + + expect(buildElectronBuilderInvocations(['--mac', '--arm64', '--publish', 'never'])).toEqual([ + { args: ['--mac', '--arm64', '--publish', 'never'] }, + ]); + }); +}); diff --git a/test/main/services/team/ClaudeBinaryResolver.test.ts b/test/main/services/team/ClaudeBinaryResolver.test.ts index 810a90eb..2378db8e 100644 --- a/test/main/services/team/ClaudeBinaryResolver.test.ts +++ b/test/main/services/team/ClaudeBinaryResolver.test.ts @@ -72,7 +72,7 @@ describe('ClaudeBinaryResolver', () => { }); process.cwd = vi.fn(() => workspaceRoot); Object.defineProperty(process, 'resourcesPath', { - value: '/Applications/Agent-Teams-UI.app/Contents/Resources', + value: '/Applications/Agent Teams UI.app/Contents/Resources', configurable: true, writable: true, }); @@ -200,7 +200,7 @@ describe('ClaudeBinaryResolver', () => { it('prefers the bundled runtime binary for packaged agent_teams_orchestrator builds', async () => { const expectedBinary = path.join( - '/Applications/Agent-Teams-UI.app/Contents/Resources', + '/Applications/Agent Teams UI.app/Contents/Resources', 'runtime', 'claude-multimodel' );