diff --git a/desktop/src-tauri/tauri-config.test.ts b/desktop/src-tauri/tauri-config.test.ts new file mode 100644 index 00000000..8f526c2d --- /dev/null +++ b/desktop/src-tauri/tauri-config.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from 'bun:test' +import { readFileSync } from 'node:fs' +import { join } from 'node:path' + +describe('tauri security config', () => { + it('allows desktop sidecar image URLs for opener icons', () => { + const config = JSON.parse( + readFileSync(join(import.meta.dir, 'tauri.conf.json'), 'utf8'), + ) as { + app?: { + security?: { + csp?: string + } + } + } + + const csp = config.app?.security?.csp ?? '' + expect(csp).toContain('img-src') + expect(csp).toContain('http://127.0.0.1:*') + expect(csp).toContain('http://localhost:*') + }) +}) diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json index e7bf52cc..cb407f00 100644 --- a/desktop/src-tauri/tauri.conf.json +++ b/desktop/src-tauri/tauri.conf.json @@ -24,7 +24,7 @@ ], "security": { "dangerousDisableAssetCspModification": ["style-src"], - "csp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: asset: https://asset.localhost; font-src 'self' data:; connect-src 'self' ws://127.0.0.1:* http://127.0.0.1:* ws://localhost:* http://localhost:*; media-src 'self' blob:" + "csp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: asset: https://asset.localhost http://127.0.0.1:* http://localhost:*; font-src 'self' data:; connect-src 'self' ws://127.0.0.1:* http://127.0.0.1:* ws://localhost:* http://localhost:*; media-src 'self' blob:" } }, "plugins": { diff --git a/src/server/__tests__/open-target-service.test.ts b/src/server/__tests__/open-target-service.test.ts index 3815bd19..9fbc80b0 100644 --- a/src/server/__tests__/open-target-service.test.ts +++ b/src/server/__tests__/open-target-service.test.ts @@ -76,8 +76,8 @@ function createService( describe('openTargetService', () => { it('returns only detected IDE targets plus Finder on macOS', async () => { const { service } = createService('darwin', { - commands: { code: true }, paths: { + '/Applications/Visual Studio Code.app': true, '/Applications/Sublime Text.app': true, }, }) @@ -96,6 +96,21 @@ describe('openTargetService', () => { .toBe('/api/open-targets/icons/vscode') }) + it('does not treat macOS command shims as installed IDEs without the app bundle', async () => { + const { service } = createService('darwin', { + commands: { + code: true, + goland: true, + pycharm: true, + }, + }) + + const result = await service.listTargets() + + expect(result.targets.map((target) => target.id)).toEqual(['finder']) + expect(result.primaryTargetId).toBe('finder') + }) + it('falls back to Explorer when no Windows IDE is detected', async () => { const { service } = createService('win32') @@ -120,7 +135,7 @@ describe('openTargetService', () => { it('caches detection results until the TTL expires', async () => { const now = { value: 100 } - const state = createService('darwin', { + const state = createService('linux', { commands: { code: true }, now, }) @@ -153,7 +168,9 @@ describe('openTargetService', () => { const dir = await makeDir() const file = join(dir, 'note.txt') await writeFile(file, 'not a directory') - const { service } = createService('darwin', { commands: { code: true } }) + const { service } = createService('darwin', { + paths: { '/Applications/Visual Studio Code.app': true }, + }) try { await expect(service.openTarget({ targetId: 'vscode', path: file })) @@ -163,9 +180,9 @@ describe('openTargetService', () => { } }) - it('launches with argument arrays and the path as one argument', async () => { + it('launches command-first targets with argument arrays and the path as one argument', async () => { const dir = await makeDir('cc-haha open-target-') - const { service, launched } = createService('darwin', { + const { service, launched } = createService('linux', { commands: { code: true }, }) @@ -178,9 +195,10 @@ describe('openTargetService', () => { } }) - it('opens macOS app bundles through open -a when no command is present', async () => { + it('opens macOS app bundles through open -a instead of command shims', async () => { const dir = await makeDir() const { service, launched } = createService('darwin', { + commands: { cursor: true }, paths: { '/Applications/Cursor.app': true }, }) @@ -198,7 +216,7 @@ describe('openTargetService', () => { it('reports launch failures instead of returning success', async () => { const dir = await makeDir() const { service } = createService('darwin', { - commands: { code: true }, + paths: { '/Applications/Visual Studio Code.app': true }, launchResult: { code: 1, stdout: '', stderr: 'failed' }, }) diff --git a/src/server/services/openTargetService.ts b/src/server/services/openTargetService.ts index 43ca7573..dc6029b3 100644 --- a/src/server/services/openTargetService.ts +++ b/src/server/services/openTargetService.ts @@ -457,6 +457,15 @@ async function isDetected(definition: TargetDefinition, runtime: Runtime): Promi return true } + if (runtime.platform === 'darwin' && definition.appPaths?.darwin?.length) { + for (const appPath of definition.appPaths.darwin) { + if (await runtime.pathExists(appPath)) { + return true + } + } + return false + } + for (const appPath of definition.appPaths?.[runtime.platform] ?? []) { if (await runtime.pathExists(appPath)) { return true @@ -494,19 +503,17 @@ async function resolveLaunchPlan( } } - for (const command of definition.commands?.[runtime.platform] ?? []) { - if (await runtime.commandExists(command)) { - return { command, args: [targetPath] } + if (runtime.platform === 'darwin') { + for (const appPath of definition.appPaths?.darwin ?? []) { + if (await runtime.pathExists(appPath)) { + return { command: 'open', args: ['-a', appPath, targetPath] } + } } } - if (runtime.platform !== 'darwin') { - return null - } - - for (const appPath of definition.appPaths?.darwin ?? []) { - if (await runtime.pathExists(appPath)) { - return { command: 'open', args: ['-a', appPath, targetPath] } + for (const command of definition.commands?.[runtime.platform] ?? []) { + if (await runtime.commandExists(command)) { + return { command, args: [targetPath] } } }