mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
Allow packaged opener icons to load
Tauri WebView was allowed to call the desktop sidecar API, but the CSP still blocked image loads from that same local server, so extracted IDE icons degraded to the generic code glyph in packaged builds. macOS detection also trusted command shims before app bundles, which let stale JetBrains Toolbox scripts keep removed IDEs visible. Constraint: Opener icons are served by the per-run desktop sidecar on localhost or 127.0.0.1 Rejected: Bundle a fixed static icon set | the product should use detected installed application icons across platforms Rejected: Trust macOS command shims for detection | stale shims survive app removal and make the menu lie Confidence: high Scope-risk: narrow Directive: Keep macOS IDE detection anchored to real app bundles before accepting command shims Tested: bun test src/server/__tests__/open-target-service.test.ts src/server/__tests__/open-target-api.test.ts desktop/src-tauri/tauri-config.test.ts Tested: cd desktop && bun run test -- src/api/client.test.ts src/api/openTargets.test.ts src/stores/openTargetStore.test.ts src/components/layout/OpenProjectMenu.test.tsx src/components/layout/TabBar.test.tsx Tested: cd desktop && bun run lint
This commit is contained in:
parent
c03cb1f297
commit
07050bb91c
22
desktop/src-tauri/tauri-config.test.ts
Normal file
22
desktop/src-tauri/tauri-config.test.ts
Normal file
@ -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:*')
|
||||
})
|
||||
})
|
||||
@ -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": {
|
||||
|
||||
@ -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' },
|
||||
})
|
||||
|
||||
|
||||
@ -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] }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user