commit e0a9c1ab4c46ecadf665dfb31dd967ce6f0019ac Author: farion1231 Date: Mon Aug 4 22:16:26 2025 +0800 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ac95d79 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +dist/ +release/ +.DS_Store +*.log +.env +.env.local \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..79624ac --- /dev/null +++ b/README.md @@ -0,0 +1,45 @@ +# Claude Code 供应商切换器 + +一个用于管理和切换 Claude Code 不同供应商配置的桌面应用。 + +## 功能特性 + +- 🔄 一键切换不同供应商(Anthropic、OpenRouter 等) +- 🔍 实时监控供应商状态和响应时间 +- ⚡ 支持添加自定义供应商 +- 🎨 简洁美观的图形界面 +- 🔒 安全存储 API 密钥 + +## 开发 + +```bash +# 安装依赖 +npm install + +# 开发模式 +npm run dev + +# 构建应用 +npm run build + +# 打包发布 +npm run dist +``` + +## 使用说明 + +1. 点击"添加供应商"添加你的 API 配置 +2. 系统会自动检测每个供应商的状态 +3. 选择要使用的供应商,点击单选按钮切换 +4. 配置会自动保存到 Claude Code 的配置文件中 + +## 技术栈 + +- Electron +- React +- TypeScript +- Vite + +## License + +MIT \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..ec8e24b --- /dev/null +++ b/package.json @@ -0,0 +1,52 @@ +{ + "name": "cc-switch", + "version": "1.0.0", + "description": "Claude Code 供应商切换工具", + "main": "dist/main.js", + "scripts": { + "dev": "concurrently \"npm run dev:main\" \"npm run dev:renderer\"", + "dev:main": "tsc -w -p tsconfig.main.json", + "dev:renderer": "vite", + "build": "npm run build:renderer && npm run build:main", + "build:main": "tsc -p tsconfig.main.json", + "build:renderer": "vite build", + "start": "electron .", + "dist": "electron-builder" + }, + "keywords": [], + "author": "", + "license": "MIT", + "devDependencies": { + "@types/node": "^20.0.0", + "@types/react": "^18.2.0", + "@types/react-dom": "^18.2.0", + "@vitejs/plugin-react": "^4.2.0", + "concurrently": "^8.2.0", + "electron": "^28.0.0", + "electron-builder": "^24.0.0", + "typescript": "^5.3.0", + "vite": "^5.0.0" + }, + "dependencies": { + "axios": "^1.6.0", + "electron-store": "^8.1.0", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "build": { + "appId": "com.ccswitch.app", + "productName": "CC Switch", + "directories": { + "output": "release" + }, + "mac": { + "category": "public.app-category.developer-tools" + }, + "win": { + "target": "nsis" + }, + "linux": { + "target": "AppImage" + } + } +} \ No newline at end of file diff --git a/src/main/index.ts b/src/main/index.ts new file mode 100644 index 0000000..4bd3d31 --- /dev/null +++ b/src/main/index.ts @@ -0,0 +1,94 @@ +import { app, BrowserWindow, ipcMain } from 'electron' +import path from 'path' +import Store from 'electron-store' +import { Provider, AppConfig } from '../shared/types' +import { checkProviderStatus, switchProvider, getClaudeCodeConfig } from './services' + +const store = new Store() + +let mainWindow: BrowserWindow | null = null + +function createWindow() { + mainWindow = new BrowserWindow({ + width: 800, + height: 600, + webPreferences: { + preload: path.join(__dirname, 'preload.js'), + contextIsolation: true, + nodeIntegration: false + }, + titleBarStyle: 'hiddenInset', + autoHideMenuBar: true + }) + + if (app.isPackaged) { + mainWindow.loadFile(path.join(__dirname, '../renderer/index.html')) + } else { + mainWindow.loadURL('http://localhost:3000') + mainWindow.webContents.openDevTools() + } + + mainWindow.on('closed', () => { + mainWindow = null + }) +} + +app.whenReady().then(() => { + createWindow() + + app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) { + createWindow() + } + }) +}) + +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + app.quit() + } +}) + +// IPC handlers +ipcMain.handle('getProviders', () => { + return store.get('providers', {}) +}) + +ipcMain.handle('getCurrentProvider', () => { + return store.get('current', '') +}) + +ipcMain.handle('addProvider', (_, provider: Provider) => { + const providers = store.get('providers', {}) + providers[provider.id] = provider + store.set('providers', providers) + return true +}) + +ipcMain.handle('deleteProvider', (_, id: string) => { + const providers = store.get('providers', {}) + delete providers[id] + store.set('providers', providers) + return true +}) + +ipcMain.handle('checkStatus', async (_, provider: Provider) => { + return await checkProviderStatus(provider) +}) + +ipcMain.handle('switchProvider', async (_, providerId: string) => { + const providers = store.get('providers', {}) + const provider = providers[providerId] + if (provider) { + const success = await switchProvider(provider) + if (success) { + store.set('current', providerId) + } + return success + } + return false +}) + +ipcMain.handle('getClaudeCodeConfigPath', () => { + return getClaudeCodeConfig().path +}) \ No newline at end of file diff --git a/src/main/preload.ts b/src/main/preload.ts new file mode 100644 index 0000000..8bbd260 --- /dev/null +++ b/src/main/preload.ts @@ -0,0 +1,12 @@ +import { contextBridge, ipcRenderer } from 'electron' +import { Provider } from '../shared/types' + +contextBridge.exposeInMainWorld('electronAPI', { + getProviders: () => ipcRenderer.invoke('getProviders'), + getCurrentProvider: () => ipcRenderer.invoke('getCurrentProvider'), + addProvider: (provider: Provider) => ipcRenderer.invoke('addProvider', provider), + deleteProvider: (id: string) => ipcRenderer.invoke('deleteProvider', id), + checkStatus: (provider: Provider) => ipcRenderer.invoke('checkStatus', provider), + switchProvider: (providerId: string) => ipcRenderer.invoke('switchProvider', providerId), + getClaudeCodeConfigPath: () => ipcRenderer.invoke('getClaudeCodeConfigPath') +}) \ No newline at end of file diff --git a/src/main/services.ts b/src/main/services.ts new file mode 100644 index 0000000..3fe24a5 --- /dev/null +++ b/src/main/services.ts @@ -0,0 +1,86 @@ +import axios from 'axios' +import fs from 'fs/promises' +import path from 'path' +import os from 'os' +import { Provider, ProviderStatus } from '../shared/types' + +export async function checkProviderStatus(provider: Provider): Promise { + const startTime = Date.now() + + try { + // 简单的健康检查请求 + const response = await axios.post( + `${provider.apiUrl}/v1/messages`, + { + model: provider.model || 'claude-3-opus-20240229', + messages: [{ role: 'user', content: 'Hi' }], + max_tokens: 1 + }, + { + headers: { + 'x-api-key': provider.apiKey, + 'anthropic-version': '2023-06-01', + 'content-type': 'application/json' + }, + timeout: 5000 + } + ) + + const responseTime = Date.now() - startTime + + return { + isOnline: true, + responseTime, + lastChecked: new Date() + } + } catch (error) { + return { + isOnline: false, + responseTime: -1, + lastChecked: new Date(), + error: error instanceof Error ? error.message : '未知错误' + } + } +} + +export function getClaudeCodeConfig() { + // Claude Code 配置文件路径 + const configDir = path.join(os.homedir(), '.claude') + const configPath = path.join(configDir, 'settings.json') + + return { path: configPath, dir: configDir } +} + +export async function switchProvider(provider: Provider): Promise { + try { + const { path: configPath, dir: configDir } = getClaudeCodeConfig() + + // 确保目录存在 + await fs.mkdir(configDir, { recursive: true }) + + // 读取现有配置 + let config: any = {} + try { + const content = await fs.readFile(configPath, 'utf-8') + config = JSON.parse(content) + } catch { + // 文件不存在或解析失败,使用空配置 + } + + // 更新配置 + config.api = { + ...config.api, + baseURL: provider.apiUrl, + apiKey: provider.apiKey, + model: provider.model + } + + // 写回配置文件 + await fs.writeFile(configPath, JSON.stringify(config, null, 2)) + + return true + } catch (error) { + console.error('切换供应商失败:', error) + return false + } +} \ No newline at end of file diff --git a/src/renderer/App.css b/src/renderer/App.css new file mode 100644 index 0000000..53c640e --- /dev/null +++ b/src/renderer/App.css @@ -0,0 +1,72 @@ +.app { + height: 100vh; + display: flex; + flex-direction: column; +} + +.app-header { + background: #2c3e50; + color: white; + padding: 1rem 2rem; + display: flex; + justify-content: space-between; + align-items: center; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.app-header h1 { + font-size: 1.5rem; + font-weight: 500; +} + +.header-actions { + display: flex; + gap: 1rem; +} + +.refresh-btn, .add-btn { + padding: 0.5rem 1rem; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 0.9rem; + transition: all 0.2s; +} + +.refresh-btn { + background: #3498db; + color: white; +} + +.refresh-btn:hover:not(:disabled) { + background: #2980b9; +} + +.refresh-btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.add-btn { + background: #27ae60; + color: white; +} + +.add-btn:hover { + background: #229954; +} + +.app-main { + flex: 1; + padding: 2rem; + overflow-y: auto; +} + +.config-path { + margin-top: 2rem; + padding: 1rem; + background: #ecf0f1; + border-radius: 4px; + font-size: 0.9rem; + color: #7f8c8d; +} \ No newline at end of file diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx new file mode 100644 index 0000000..473dd0b --- /dev/null +++ b/src/renderer/App.tsx @@ -0,0 +1,131 @@ +import React, { useState, useEffect } from 'react' +import { Provider, ProviderStatus } from '../shared/types' +import ProviderList from './components/ProviderList' +import AddProviderModal from './components/AddProviderModal' +import './App.css' + +function App() { + const [providers, setProviders] = useState>({}) + const [currentProviderId, setCurrentProviderId] = useState('') + const [statuses, setStatuses] = useState>({}) + const [isAddModalOpen, setIsAddModalOpen] = useState(false) + const [isRefreshing, setIsRefreshing] = useState(false) + const [configPath, setConfigPath] = useState('') + + // 加载供应商列表 + useEffect(() => { + loadProviders() + loadConfigPath() + }, []) + + // 定时检查状态 + useEffect(() => { + checkAllStatuses() + const interval = setInterval(checkAllStatuses, 30000) // 每30秒检查一次 + return () => clearInterval(interval) + }, [providers]) + + const loadProviders = async () => { + const loadedProviders = await window.electronAPI.getProviders() + const currentId = await window.electronAPI.getCurrentProvider() + setProviders(loadedProviders) + setCurrentProviderId(currentId) + } + + const loadConfigPath = async () => { + const path = await window.electronAPI.getClaudeCodeConfigPath() + setConfigPath(path) + } + + const checkAllStatuses = async () => { + if (Object.keys(providers).length === 0) return + + setIsRefreshing(true) + const newStatuses: Record = {} + + await Promise.all( + Object.values(providers).map(async (provider) => { + const status = await window.electronAPI.checkStatus(provider) + newStatuses[provider.id] = status + }) + ) + + setStatuses(newStatuses) + setIsRefreshing(false) + } + + const handleAddProvider = async (provider: Omit) => { + const newProvider: Provider = { + ...provider, + id: Date.now().toString() + } + await window.electronAPI.addProvider(newProvider) + await loadProviders() + setIsAddModalOpen(false) + } + + const handleDeleteProvider = async (id: string) => { + if (confirm('确定要删除这个供应商吗?')) { + await window.electronAPI.deleteProvider(id) + await loadProviders() + } + } + + const handleSwitchProvider = async (id: string) => { + const success = await window.electronAPI.switchProvider(id) + if (success) { + setCurrentProviderId(id) + alert('切换成功!') + } else { + alert('切换失败,请检查配置') + } + } + + return ( +
+
+

Claude Code 供应商切换器

+
+ + +
+
+ +
+ + + {configPath && ( +
+ 配置文件位置: {configPath} +
+ )} +
+ + {isAddModalOpen && ( + setIsAddModalOpen(false)} + /> + )} +
+ ) +} + +export default App \ No newline at end of file diff --git a/src/renderer/components/AddProviderModal.css b/src/renderer/components/AddProviderModal.css new file mode 100644 index 0000000..1bc5966 --- /dev/null +++ b/src/renderer/components/AddProviderModal.css @@ -0,0 +1,122 @@ +.modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +} + +.modal-content { + background: white; + border-radius: 8px; + padding: 2rem; + width: 90%; + max-width: 500px; + max-height: 90vh; + overflow-y: auto; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2); +} + +.modal-content h2 { + margin-bottom: 1.5rem; + color: #2c3e50; +} + +.presets { + margin-bottom: 1.5rem; + padding-bottom: 1.5rem; + border-bottom: 1px solid #ecf0f1; +} + +.presets label { + display: block; + margin-bottom: 0.5rem; + color: #555; + font-size: 0.9rem; +} + +.preset-buttons { + display: flex; + gap: 0.5rem; +} + +.preset-btn { + padding: 0.375rem 0.75rem; + border: 1px solid #3498db; + background: white; + color: #3498db; + border-radius: 4px; + cursor: pointer; + font-size: 0.85rem; + transition: all 0.2s; +} + +.preset-btn:hover { + background: #3498db; + color: white; +} + +.form-group { + margin-bottom: 1.25rem; +} + +.form-group label { + display: block; + margin-bottom: 0.5rem; + color: #555; + font-weight: 500; +} + +.form-group input { + width: 100%; + padding: 0.625rem; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 0.95rem; + transition: border-color 0.2s; +} + +.form-group input:focus { + outline: none; + border-color: #3498db; +} + +.form-actions { + display: flex; + gap: 1rem; + justify-content: flex-end; + margin-top: 2rem; +} + +.cancel-btn, +.submit-btn { + padding: 0.625rem 1.25rem; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 0.95rem; + transition: all 0.2s; +} + +.cancel-btn { + background: #ecf0f1; + color: #555; +} + +.cancel-btn:hover { + background: #bdc3c7; +} + +.submit-btn { + background: #27ae60; + color: white; +} + +.submit-btn:hover { + background: #229954; +} \ No newline at end of file diff --git a/src/renderer/components/AddProviderModal.tsx b/src/renderer/components/AddProviderModal.tsx new file mode 100644 index 0000000..2102a5d --- /dev/null +++ b/src/renderer/components/AddProviderModal.tsx @@ -0,0 +1,146 @@ +import React, { useState } from 'react' +import { Provider } from '../../shared/types' +import './AddProviderModal.css' + +interface AddProviderModalProps { + onAdd: (provider: Omit) => void + onClose: () => void +} + +const AddProviderModal: React.FC = ({ onAdd, onClose }) => { + const [formData, setFormData] = useState({ + name: '', + apiUrl: '', + apiKey: '', + model: 'claude-3-opus-20240229' + }) + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + + if (!formData.name || !formData.apiUrl || !formData.apiKey) { + alert('请填写所有必填字段') + return + } + + onAdd(formData) + } + + const handleChange = (e: React.ChangeEvent) => { + setFormData({ + ...formData, + [e.target.name]: e.target.value + }) + } + + // 预设的供应商配置 + const presets = [ + { + name: '官方 Anthropic', + apiUrl: 'https://api.anthropic.com', + model: 'claude-3-opus-20240229' + }, + { + name: 'OpenRouter', + apiUrl: 'https://openrouter.ai/api/v1', + model: 'anthropic/claude-3-opus' + } + ] + + const applyPreset = (preset: typeof presets[0]) => { + setFormData({ + ...formData, + name: preset.name, + apiUrl: preset.apiUrl, + model: preset.model + }) + } + + return ( +
+
e.stopPropagation()}> +

添加新供应商

+ +
+ +
+ {presets.map((preset, index) => ( + + ))} +
+
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+
+ ) +} + +export default AddProviderModal \ No newline at end of file diff --git a/src/renderer/components/ProviderList.css b/src/renderer/components/ProviderList.css new file mode 100644 index 0000000..1ebb102 --- /dev/null +++ b/src/renderer/components/ProviderList.css @@ -0,0 +1,127 @@ +.provider-list { + background: white; + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + padding: 1.5rem; +} + +.empty-state { + text-align: center; + padding: 3rem 1rem; + color: #7f8c8d; +} + +.empty-state p:first-child { + font-size: 1.1rem; + margin-bottom: 0.5rem; +} + +.provider-items { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.provider-item { + display: flex; + align-items: center; + padding: 1rem; + border: 2px solid #ecf0f1; + border-radius: 6px; + transition: all 0.2s; +} + +.provider-item:hover { + border-color: #3498db; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.provider-item.current { + border-color: #27ae60; + background: #f0fdf4; +} + +.provider-info { + flex: 1; + min-width: 0; +} + +.provider-name { + display: flex; + align-items: center; + gap: 0.5rem; + font-weight: 500; + margin-bottom: 0.25rem; +} + +.provider-name input[type="radio"] { + cursor: pointer; +} + +.provider-name input[type="radio"]:disabled { + cursor: not-allowed; +} + +.current-badge { + background: #27ae60; + color: white; + padding: 0.125rem 0.5rem; + border-radius: 3px; + font-size: 0.75rem; +} + +.provider-url { + color: #7f8c8d; + font-size: 0.9rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.provider-status { + display: flex; + align-items: center; + gap: 0.5rem; + margin-right: 2rem; +} + +.status-icon { + font-size: 1.1rem; +} + +.status-text { + color: #555; + font-size: 0.9rem; +} + +.response-time { + color: #3498db; + font-size: 0.85rem; + font-family: monospace; +} + +.provider-actions { + display: flex; + gap: 0.5rem; +} + +.delete-btn { + padding: 0.375rem 0.75rem; + border: 1px solid #e74c3c; + background: white; + color: #e74c3c; + border-radius: 4px; + cursor: pointer; + font-size: 0.85rem; + transition: all 0.2s; +} + +.delete-btn:hover:not(:disabled) { + background: #e74c3c; + color: white; +} + +.delete-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} \ No newline at end of file diff --git a/src/renderer/components/ProviderList.tsx b/src/renderer/components/ProviderList.tsx new file mode 100644 index 0000000..4b8ac21 --- /dev/null +++ b/src/renderer/components/ProviderList.tsx @@ -0,0 +1,97 @@ +import React from 'react' +import { Provider, ProviderStatus } from '../../shared/types' +import './ProviderList.css' + +interface ProviderListProps { + providers: Record + currentProviderId: string + statuses: Record + onSwitch: (id: string) => void + onDelete: (id: string) => void +} + +const ProviderList: React.FC = ({ + providers, + currentProviderId, + statuses, + onSwitch, + onDelete +}) => { + const formatResponseTime = (time: number) => { + if (time < 0) return '-' + return `${time}ms` + } + + const getStatusIcon = (status?: ProviderStatus) => { + if (!status) return '⏳' + return status.isOnline ? '✅' : '❌' + } + + const getStatusText = (status?: ProviderStatus) => { + if (!status) return '检查中...' + if (status.isOnline) return '正常' + return status.error || '连接失败' + } + + return ( +
+ {Object.values(providers).length === 0 ? ( +
+

还没有添加任何供应商

+

点击右上角的"添加供应商"按钮开始

+
+ ) : ( +
+ {Object.values(providers).map((provider) => { + const status = statuses[provider.id] + const isCurrent = provider.id === currentProviderId + + return ( +
+
+
+ onSwitch(provider.id)} + disabled={!status?.isOnline} + /> + {provider.name} + {isCurrent && 当前使用} +
+
{provider.apiUrl}
+
+ +
+ {getStatusIcon(status)} + {getStatusText(status)} + {status?.isOnline && ( + + {formatResponseTime(status.responseTime)} + + )} +
+ +
+ +
+
+ ) + })} +
+ )} +
+ ) +} + +export default ProviderList \ No newline at end of file diff --git a/src/renderer/index.css b/src/renderer/index.css new file mode 100644 index 0000000..a4e1d1e --- /dev/null +++ b/src/renderer/index.css @@ -0,0 +1,21 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + background-color: #f5f5f5; + color: #333; +} + +#root { + height: 100vh; + display: flex; + flex-direction: column; +} \ No newline at end of file diff --git a/src/renderer/index.html b/src/renderer/index.html new file mode 100644 index 0000000..bdf8a15 --- /dev/null +++ b/src/renderer/index.html @@ -0,0 +1,12 @@ + + + + + + Claude Code 供应商切换器 + + +
+ + + \ No newline at end of file diff --git a/src/renderer/main.tsx b/src/renderer/main.tsx new file mode 100644 index 0000000..c12f8c2 --- /dev/null +++ b/src/renderer/main.tsx @@ -0,0 +1,10 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App' +import './index.css' + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +) \ No newline at end of file diff --git a/src/shared/types.ts b/src/shared/types.ts new file mode 100644 index 0000000..57c64f4 --- /dev/null +++ b/src/shared/types.ts @@ -0,0 +1,33 @@ +export interface Provider { + id: string + name: string + apiUrl: string + apiKey: string + model?: string +} + +export interface ProviderStatus { + isOnline: boolean + responseTime: number + lastChecked: Date + error?: string +} + +export interface AppConfig { + providers: Record + current: string +} + +declare global { + interface Window { + electronAPI: { + getProviders: () => Promise> + getCurrentProvider: () => Promise + addProvider: (provider: Provider) => Promise + deleteProvider: (id: string) => Promise + checkStatus: (provider: Provider) => Promise + switchProvider: (providerId: string) => Promise + getClaudeCodeConfigPath: () => Promise + } + } +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..cacbabe --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src/renderer/**/*"], + "references": [{ "path": "./tsconfig.node.json" }] +} \ No newline at end of file diff --git a/tsconfig.main.json b/tsconfig.main.json new file mode 100644 index 0000000..5d0c13c --- /dev/null +++ b/tsconfig.main.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.node.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src/main", + "types": ["node"] + }, + "include": ["src/main/**/*", "src/shared/**/*"] +} \ No newline at end of file diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..5c3ae38 --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "CommonJS", + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "resolveJsonModule": true, + "esModuleInterop": true, + "target": "ES2020", + "strict": true, + "types": ["node"] + }, + "include": ["vite.config.ts"] +} \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..6ad4074 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import { resolve } from 'path' + +export default defineConfig({ + plugins: [react()], + root: resolve(__dirname, 'src/renderer'), + base: './', + build: { + outDir: resolve(__dirname, 'dist/renderer'), + emptyOutDir: true + }, + server: { + port: 3000 + } +}) \ No newline at end of file