You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

339 lines
11 KiB

/// <reference types="vitest/config" />
import fs from 'node:fs'
import path from 'node:path'
import { exec } from 'node:child_process'
import os from 'node:os'
import { defineConfig, loadEnv, type Plugin } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
import babel from '@babel/core'
import addDataLocator from './plugins/add-data-locator.cjs'
function getIdeCandidates(): string[] {
const isWindows = os.platform() === 'win32'
const isMac = os.platform() === 'darwin'
const home = os.homedir()
const candidates: string[] = []
if (isMac) {
const macPaths = [
'/Applications/Antigravity IDE.app/Contents/Resources/app/bin/antigravity-ide',
path.join(home, 'Applications/Antigravity IDE.app/Contents/Resources/app/bin/antigravity-ide'),
path.join(home, '.gemini/antigravity/bin/antigravity-ide'),
'/Applications/Cursor.app/Contents/Resources/app/bin/cursor',
path.join(home, 'Applications/Cursor.app/Contents/Resources/app/bin/cursor'),
'/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code',
path.join(home, 'Applications/Visual Studio Code.app/Contents/Resources/app/bin/code'),
path.join(home, '.gemini/antigravity/bin/code'),
'/Applications/Visual Studio Code - Insiders.app/Contents/Resources/app/bin/code-insiders',
path.join(home, 'Applications/Visual Studio Code - Insiders.app/Contents/Resources/app/bin/code-insiders'),
'/usr/local/bin/antigravity-ide',
'/usr/local/bin/cursor',
'/usr/local/bin/code',
'/opt/homebrew/bin/antigravity-ide',
'/opt/homebrew/bin/cursor',
'/opt/homebrew/bin/code',
]
for (const p of macPaths) {
if (fs.existsSync(p) && !candidates.includes(p)) {
candidates.push(p)
}
}
candidates.push('antigravity-ide', 'cursor', 'code')
} else if (isWindows) {
const localAppData = process.env.LOCALAPPDATA || ''
const programFiles = process.env.ProgramFiles || 'C:\\Program Files'
const winPaths = [
path.join(localAppData, 'Programs', 'Antigravity IDE', 'bin', 'antigravity-ide.cmd'),
path.join(localAppData, 'Programs', 'cursor', 'resources', 'app', 'bin', 'cursor.cmd'),
path.join(localAppData, 'Programs', 'Microsoft VS Code', 'bin', 'code.cmd'),
path.join(programFiles, 'Microsoft VS Code', 'bin', 'code.cmd'),
'antigravity-ide.cmd',
'cursor.cmd',
'code.cmd',
]
for (const p of winPaths) {
if (p.includes(path.sep)) {
if (fs.existsSync(p) && !candidates.includes(p)) candidates.push(p)
} else if (!candidates.includes(p)) {
candidates.push(p)
}
}
} else {
// Linux
const linuxPaths = [
'/usr/local/bin/antigravity-ide',
'/usr/bin/antigravity-ide',
'/usr/local/bin/cursor',
'/usr/bin/cursor',
'/usr/local/bin/code',
'/usr/bin/code',
'/snap/bin/code',
'antigravity-ide',
'cursor',
'code',
]
for (const p of linuxPaths) {
if (p.startsWith('/') ? fs.existsSync(p) : true) {
if (!candidates.includes(p)) candidates.push(p)
}
}
}
return candidates.length > 0 ? candidates : ['antigravity-ide', 'code']
}
function openViaCli(locator: string, callback: (err: Error | null, bin?: string) => void) {
const isWindows = os.platform() === 'win32'
const home = os.homedir()
const extraPaths = isWindows
? []
: [
path.join(home, '.gemini/antigravity/bin'),
path.join(home, 'Library/Application Support/Antigravity/bin'),
'/usr/local/bin',
'/opt/homebrew/bin',
'/opt/homebrew/sbin',
'/usr/bin',
'/bin',
]
const augmentedPath = [...extraPaths, process.env.PATH || ''].filter(Boolean).join(isWindows ? ';' : ':')
const env = { ...process.env, PATH: augmentedPath }
const candidates = getIdeCandidates()
let index = 0
let lastError: Error | null = null
function tryNext() {
if (index >= candidates.length) {
return callback(lastError || new Error('No IDE command succeeded to open locator'))
}
const bin = candidates[index++]
const quotedBin = bin.includes(' ') ? `"${bin}"` : bin
const cmd = `${quotedBin} -r -g "${locator}"`
exec(cmd, { env }, (error) => {
if (error) {
lastError = error
tryNext()
} else {
callback(null, bin)
}
})
}
tryNext()
}
function openInIde(locator: string, callback: (err: Error | null, bin?: string) => void) {
const isWindows = os.platform() === 'win32'
const isMac = os.platform() === 'darwin'
const home = os.homedir()
// Clean and prepare URI
const cleanPath = locator.replace(/\\/g, '/').replace(/^\/+/, '')
const encodedTargetUri = encodeURI(`/${cleanPath}`)
if (isMac) {
// 1. Direct macOS protocol URL scheme with targeted app:
// Uses macOS LaunchServices directly without spawning a secondary Electron CLI process.
// Instant (<100ms) with zero dock bounce and no delay.
const ideAppPaths = [
'/Applications/Antigravity IDE.app',
path.join(home, 'Applications/Antigravity IDE.app'),
'/Applications/Antigravity.app',
path.join(home, 'Applications/Antigravity.app'),
'/Applications/Cursor.app',
path.join(home, 'Applications/Cursor.app'),
'/Applications/Visual Studio Code.app',
path.join(home, 'Applications/Visual Studio Code.app'),
]
const foundApp = ideAppPaths.find((p) => fs.existsSync(p))
let scheme = 'antigravity-ide'
if (foundApp?.includes('Cursor')) {
scheme = 'cursor'
} else if (foundApp?.includes('Visual Studio Code')) {
scheme = 'vscode'
} else if (foundApp && !foundApp.includes('Antigravity IDE') && foundApp.includes('Antigravity')) {
scheme = 'antigravity'
}
const url = `${scheme}://file${encodedTargetUri}`
const cmd = foundApp ? `open -a "${foundApp}" "${url}"` : `open "${url}"`
exec(cmd, (error) => {
if (!error) {
if (foundApp?.includes('Antigravity IDE') || !foundApp) {
exec(`osascript -e 'tell application id "com.google.antigravity-ide" to activate' 2>/dev/null`, () => {})
} else if (foundApp.includes('Antigravity')) {
exec(`osascript -e 'tell application id "com.google.antigravity" to activate' 2>/dev/null`, () => {})
} else if (foundApp.includes('Cursor')) {
exec(`osascript -e 'tell application "Cursor" to activate' 2>/dev/null`, () => {})
} else if (foundApp.includes('Visual Studio Code')) {
exec(`osascript -e 'tell application "Visual Studio Code" to activate' 2>/dev/null`, () => {})
}
return callback(null, foundApp || scheme)
}
// Fallback to CLI if open command failed
openViaCli(locator, callback)
})
return
}
if (isWindows) {
const cmd = `start "" /MAX "antigravity-ide://file${encodedTargetUri}" || start "" /MAX "cursor://file${encodedTargetUri}" || start "" /MAX "vscode://file${encodedTargetUri}"`
exec(cmd, (error) => {
if (!error) {
return callback(null, 'antigravity-ide://file')
}
openViaCli(locator, callback)
})
return
}
// Linux
const cmd = `xdg-open "antigravity-ide://file${encodedTargetUri}" || xdg-open "vscode://file${encodedTargetUri}"`
exec(cmd, (error) => {
if (!error) {
return callback(null, 'antigravity-ide://file')
}
openViaCli(locator, callback)
})
}
function openInIdePlugin(): Plugin {
return {
name: 'open-in-ide-plugin',
apply: 'serve',
configureServer(server) {
server.middlewares.use((req, res, next) => {
const url = req.url?.split('?')[0]
if (url === '/api/open-in-ide' && req.method === 'POST') {
let body = ''
req.on('data', (chunk) => {
body += chunk
})
req.on('end', () => {
try {
const { locator } = JSON.parse(body)
if (!locator) {
res.statusCode = 400
res.setHeader('Content-Type', 'application/json')
res.end(JSON.stringify({ message: 'Locator is required' }))
return
}
openInIde(locator, (error, bin) => {
if (error) {
console.error(`[open-in-ide] Error opening "${locator}": ${error.message}`)
res.statusCode = 500
res.setHeader('Content-Type', 'application/json')
res.end(
JSON.stringify({
message: 'Failed to open file in IDE',
error: error.message,
})
)
return
}
res.statusCode = 200
res.setHeader('Content-Type', 'application/json')
res.end(JSON.stringify({ success: true, bin }))
})
} catch (err) {
console.error('[open-in-ide] Error parsing body:', err, 'raw body:', body)
res.statusCode = 500
res.setHeader('Content-Type', 'application/json')
res.end(JSON.stringify({ message: 'Internal server error', error: String(err), rawBody: body }))
}
})
return
}
if (
(url === '/api/remote-logs' ||
url === '/api/remote-network' ||
url === '/api/dev-storage') &&
req.method === 'POST'
) {
res.statusCode = 200
res.setHeader('Content-Type', 'application/json')
res.end(JSON.stringify({ ok: true }))
return
}
next()
})
},
}
}
function addDataLocatorPlugin(): Plugin {
return {
name: 'add-data-locator-plugin',
enforce: 'pre',
apply: 'serve',
transform(code, id) {
const cleanId = id.split('?')[0]
if (cleanId.includes('node_modules') || !/\.[jt]sx$/.test(cleanId)) {
return null
}
try {
const result = babel.transformSync(code, {
filename: cleanId,
configFile: false,
babelrc: false,
parserOpts: { plugins: ['jsx', 'typescript'] },
plugins: [addDataLocator],
})
if (result?.code) {
return { code: result.code, map: result.map }
}
} catch (err) {
console.error('[add-data-locator-plugin] Transform error:', err)
}
return null
},
}
}
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '')
/* مقصد بک‌اند برای پراکسیِ dev — همهٔ '/api/*' بدون CORS به این‌جا می‌رود. */
const apiTarget = env.VITE_API_TARGET ?? 'http://localhost:8001'
return {
plugins: [
openInIdePlugin(),
addDataLocatorPlugin(),
react(),
tailwindcss(),
],
resolve: {
alias: { '@': path.resolve(__dirname, './src') },
},
server: {
proxy: {
'/api': {
target: apiTarget,
changeOrigin: true,
},
'/media': {
target: apiTarget,
changeOrigin: true,
},
'/static': {
target: apiTarget,
changeOrigin: true,
},
},
},
test: {
environment: 'node',
include: ['src/**/*.test.{ts,tsx}'],
},
}
})