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