/// 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}'], }, } })