feat(desktop): add Electron desktop pet for Claude CLI
- Electron + React + TypeScript 桌面宠物外壳 - xterm.js + node-pty 直接对接 Claude CLI 进程 - 宠物 orb(80x80)可拖拽,点击展开终端面板 - CRT 复古终端主题(扫描线、文字发光) - 全局快捷键 Cmd+Shift+C 显隐 - 窗口位置自动保存 - 中华田园犬吉祥物 icon(AI 生成) - Makefile 新增 `make app` 打包命令
This commit is contained in:
@@ -32,3 +32,8 @@ build/
|
||||
/*.png
|
||||
/*.jpg
|
||||
/*.jpeg
|
||||
|
||||
# Desktop pet (Electron)
|
||||
desktop/node_modules/
|
||||
desktop/dist/
|
||||
desktop/dist-electron/
|
||||
|
||||
@@ -3,13 +3,14 @@ VENV_MYCLAUDE := $(ROOT_DIR)/.venv/bin/myclaude
|
||||
USER_LOCAL_MYCLAUDE := $(HOME)/.local/bin/myclaude
|
||||
COMP_DIR := $(HOME)/.local/bin/completions
|
||||
|
||||
.PHONY: help install uninstall _symlink-myclaude _install-completions _uninstall-completions
|
||||
.PHONY: help install uninstall app _symlink-myclaude _install-completions _uninstall-completions
|
||||
|
||||
help:
|
||||
@echo "Usage: make [target]"
|
||||
@echo ""
|
||||
@echo " install Sync venv, symlink bin, install completions"
|
||||
@echo " uninstall Remove bin and completions"
|
||||
@echo " app Build desktop pet as macOS .app"
|
||||
|
||||
install:
|
||||
@cd "$(ROOT_DIR)" && \
|
||||
@@ -37,3 +38,19 @@ uninstall: _uninstall-completions
|
||||
_uninstall-completions:
|
||||
@rm -f "$(COMP_DIR)/_myclaude" "$(COMP_DIR)/myclaude.bash"
|
||||
@echo "Removed completions"
|
||||
|
||||
app:
|
||||
@cd "$(ROOT_DIR)/desktop" && \
|
||||
if [ ! -d "node_modules" ]; then \
|
||||
echo "Installing desktop dependencies..."; \
|
||||
npm install; \
|
||||
fi && \
|
||||
echo "Rebuilding native modules for Electron..." && \
|
||||
npx electron-rebuild && \
|
||||
echo "Building and packaging..." && \
|
||||
npm run pack && \
|
||||
echo "" && \
|
||||
echo "Done. .app bundle:" && \
|
||||
ls -d "$(ROOT_DIR)/desktop/release"/*/*.app 2>/dev/null || \
|
||||
ls -d "$(ROOT_DIR)/desktop/release"/*.app 2>/dev/null || \
|
||||
echo "$(ROOT_DIR)/desktop/release/mac*/MyClaude.app"
|
||||
|
||||
@@ -20,6 +20,10 @@ myclaude/
|
||||
├── .claude/ # Claude Code 配置
|
||||
│ └── skills/ # 自定义技能
|
||||
├── .venv/ # Python 虚拟环境
|
||||
├── desktop/ # 桌面宠物(Electron + React)
|
||||
│ ├── electron/ # 主进程
|
||||
│ ├── src/ # React 前端
|
||||
│ └── package.json
|
||||
├── templates/ # 模板目录
|
||||
│ └── workspace/ # 工作区模板(CLAUDE.md, .gitignore)
|
||||
├── bin/ # Python 源码
|
||||
@@ -175,7 +179,38 @@ fi
|
||||
|
||||
Bash 将 `zsh_source` 换成 `bash_source`。保存后 `source` 该配置文件。
|
||||
|
||||
### 5. 安装 Metabot
|
||||
### 5. 安装桌面宠物(可选)
|
||||
|
||||
基于 Electron + React 的桌面宠物,常驻屏幕角落,点击展开终端聊天框。
|
||||
|
||||
**前置依赖:**
|
||||
|
||||
| 工具 | 版本要求 | 说明 |
|
||||
|------|---------|------|
|
||||
| Node.js | 20+ | JavaScript 运行时 |
|
||||
| Claude CLI | - | 已安装并登录 |
|
||||
|
||||
**安装运行:**
|
||||
|
||||
```bash
|
||||
cd ~/Documents/myBin/myclaude/desktop
|
||||
npm install
|
||||
npm run rebuild # 编译原生模块(node-pty)
|
||||
npm run dev # 开发模式
|
||||
```
|
||||
|
||||
**快捷键:**
|
||||
|
||||
- `Cmd+Shift+C`(macOS)/ `Ctrl+Shift+C`(Win/Linux):显示/隐藏宠物
|
||||
|
||||
**特性:**
|
||||
|
||||
- 会话保持 — Claude CLI 进程常驻,收起/展开不影响对话
|
||||
- 宠物外壳 — 80x80 悬浮 orb,可拖拽,呼吸动画
|
||||
- CRT 终端 — 复古终端风格,扫描线 + 文字发光效果
|
||||
- 窗口位置记忆 — 自动保存位置
|
||||
|
||||
### 6. 安装 Metabot
|
||||
|
||||
**快速安装:**
|
||||
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
"""Sync workspace symlinks from zshrc environment variables."""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from rich.console import Console
|
||||
|
||||
from bin.project_root import get_myclaude_project_root, get_workspace_root
|
||||
|
||||
console = Console()
|
||||
stderr_console = Console(stderr=True)
|
||||
|
||||
# Link name -> list of environment variable names to try, in order.
|
||||
_LINK_MAP: dict[str, list[str]] = {
|
||||
"myacademia": ["path_myacademia"],
|
||||
"myslides": ["path_myslides", "MYSLIDES_ROOT"],
|
||||
"metabot": ["METABOT_HOME"],
|
||||
"mytoolkit": ["path_mytoolkit", "MYTOOLKIT_ROOT"],
|
||||
"mywebpage": ["path_mywebpage", "MYWEBPAGE_ROOT"],
|
||||
}
|
||||
|
||||
|
||||
def _read_zsh_env(var: str, zshrc: Path) -> str | None:
|
||||
"""Source zshrc and print the value of a variable."""
|
||||
cmd = [
|
||||
"zsh",
|
||||
"-c",
|
||||
f"source '{zshrc}' >/dev/null 2>&1; echo -n ${var}",
|
||||
]
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
timeout=10,
|
||||
)
|
||||
except (OSError, subprocess.TimeoutExpired):
|
||||
return None
|
||||
value = result.stdout.strip()
|
||||
return value if value else None
|
||||
|
||||
|
||||
def _resolve_target(
|
||||
env_vars: list[str],
|
||||
zshrc: Path,
|
||||
) -> Path | None:
|
||||
"""Find the first valid target path from environment variables."""
|
||||
for var in env_vars:
|
||||
raw = _read_zsh_env(var, zshrc)
|
||||
if raw is None:
|
||||
continue
|
||||
expanded = os.path.expandvars(os.path.expanduser(raw))
|
||||
path = Path(expanded)
|
||||
if path.exists():
|
||||
return path.resolve()
|
||||
return None
|
||||
|
||||
|
||||
def _current_link_target(workspace: Path, name: str) -> Path | None:
|
||||
"""Return the target of an existing symlink, or None."""
|
||||
link = workspace / name
|
||||
if link.is_symlink():
|
||||
try:
|
||||
return Path(os.readlink(link))
|
||||
except OSError:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def _sync_link(
|
||||
workspace: Path,
|
||||
name: str,
|
||||
target: Path,
|
||||
dry_run: bool,
|
||||
force: bool,
|
||||
) -> bool:
|
||||
"""Create or update a symlink. Returns True if changed."""
|
||||
link = workspace / name
|
||||
current = _current_link_target(workspace, name)
|
||||
|
||||
if current is not None and current.resolve() == target.resolve():
|
||||
return False
|
||||
|
||||
if link.exists() and not link.is_symlink():
|
||||
if not force:
|
||||
stderr_console.print(
|
||||
f"[yellow]{name} exists but is not a symlink. "
|
||||
"Use --force to replace.[/yellow]"
|
||||
)
|
||||
return False
|
||||
if dry_run:
|
||||
console.print(
|
||||
f"[dry-run] Would remove {name} and symlink -> {target}"
|
||||
)
|
||||
return True
|
||||
if link.is_dir():
|
||||
link.rmdir()
|
||||
else:
|
||||
link.unlink()
|
||||
elif current is not None:
|
||||
if dry_run:
|
||||
console.print(
|
||||
f"[dry-run] Would update {name}: {current} -> {target}"
|
||||
)
|
||||
return True
|
||||
link.unlink()
|
||||
else:
|
||||
if dry_run:
|
||||
console.print(f"[dry-run] Would create {name} -> {target}")
|
||||
return True
|
||||
|
||||
link.symlink_to(target, target_is_directory=target.is_dir())
|
||||
return True
|
||||
|
||||
|
||||
def sync_workspace_links(
|
||||
zshrc: Path | None = None,
|
||||
dry_run: bool = False,
|
||||
force: bool = False,
|
||||
verbose: bool = True,
|
||||
) -> bool:
|
||||
"""Sync all workspace symlinks. Returns True on success."""
|
||||
workspace = get_workspace_root()
|
||||
if zshrc is None:
|
||||
zshrc = Path.home() / ".zshrc"
|
||||
|
||||
if verbose:
|
||||
console.print(f"[bold]Workspace:[/bold] {workspace}")
|
||||
console.print(f"[bold]Zshrc:[/bold] {zshrc}")
|
||||
console.print()
|
||||
|
||||
changed = 0
|
||||
skipped = 0
|
||||
broken = 0
|
||||
|
||||
# Handle myclaude separately: use project root detection, not zshrc.
|
||||
myclaude_target = get_myclaude_project_root()
|
||||
if _sync_link(workspace, "myclaude", myclaude_target, dry_run, force):
|
||||
action = "would update" if dry_run else "updated"
|
||||
if verbose:
|
||||
console.print(
|
||||
f"[green]myclaude: {action} -> {myclaude_target}[/green]"
|
||||
)
|
||||
changed += 1
|
||||
else:
|
||||
if verbose:
|
||||
console.print("[dim]myclaude: already correct[/dim]")
|
||||
|
||||
for name, env_vars in _LINK_MAP.items():
|
||||
target = _resolve_target(env_vars, zshrc)
|
||||
if target is None:
|
||||
link = workspace / name
|
||||
if link.is_symlink() and not link.exists():
|
||||
if verbose:
|
||||
stderr_console.print(
|
||||
f"[red]{name}: broken symlink (no env var found)[/red]"
|
||||
)
|
||||
broken += 1
|
||||
else:
|
||||
if verbose:
|
||||
console.print(f"[dim]{name}: skipped (no env var)[/dim]")
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
if _sync_link(workspace, name, target, dry_run, force):
|
||||
action = "would update" if dry_run else "updated"
|
||||
if verbose:
|
||||
console.print(f"[green]{name}: {action} -> {target}[/green]")
|
||||
changed += 1
|
||||
else:
|
||||
if verbose:
|
||||
console.print(f"[dim]{name}: already correct[/dim]")
|
||||
|
||||
if verbose:
|
||||
console.print()
|
||||
label = "Preview" if dry_run else "Done"
|
||||
console.print(
|
||||
f"[bold]{label}:[/bold] {changed} changed, "
|
||||
f"{skipped} skipped, {broken} broken."
|
||||
)
|
||||
|
||||
return broken == 0
|
||||
@@ -7,6 +7,7 @@ from pathlib import Path
|
||||
import click
|
||||
from rich.console import Console
|
||||
|
||||
from bin.commands.init import sync_workspace_links
|
||||
from bin.project_root import get_myclaude_project_root
|
||||
|
||||
console = Console()
|
||||
@@ -28,7 +29,7 @@ def _run_pip_editable(root: Path) -> int:
|
||||
|
||||
@click.command("update")
|
||||
def update_cmd() -> None:
|
||||
"""Reinstall myclaude from this repository (make install, or pip install -e)."""
|
||||
"""Reinstall myclaude and sync workspace symlinks."""
|
||||
root = get_myclaude_project_root()
|
||||
fallback = Path.home() / ".myclaude"
|
||||
if root == fallback:
|
||||
@@ -54,3 +55,7 @@ def update_cmd() -> None:
|
||||
stderr_console.print("[red]Update failed.[/red]")
|
||||
raise SystemExit(rc)
|
||||
console.print("[green]myclaude updated.[/green]")
|
||||
|
||||
console.print()
|
||||
console.print("[bold cyan]Syncing workspace links…[/bold cyan]")
|
||||
sync_workspace_links()
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
node_modules/
|
||||
dist/
|
||||
dist-electron/
|
||||
release/
|
||||
*.log
|
||||
.DS_Store
|
||||
@@ -0,0 +1,33 @@
|
||||
# MyClaude Pet
|
||||
|
||||
桌面宠物外壳 for Claude CLI。常驻屏幕角落,点击展开终端聊天框。
|
||||
|
||||
## 特性
|
||||
|
||||
- **会话保持** — Claude CLI 进程常驻后台,收起/展开不影响对话
|
||||
- **宠物外壳** — 80x80 悬浮 orb,可拖拽,呼吸动画
|
||||
- **CRT 终端** — 复古终端风格,扫描线 + 文字发光效果
|
||||
- **全局快捷键** — `Cmd+Shift+C`(macOS)/ `Ctrl+Shift+C`(Win/Linux)快速显示/隐藏
|
||||
- **位置记忆** — 窗口位置自动保存
|
||||
|
||||
## 开发
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run rebuild # 编译原生模块(node-pty)
|
||||
npm run dev # 启动开发环境
|
||||
```
|
||||
|
||||
## 构建
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
前端输出到 `dist/`,Electron 输出到 `dist-electron/`。
|
||||
|
||||
## 要求
|
||||
|
||||
- macOS / Windows / Linux
|
||||
- Node.js 20+
|
||||
- 已安装 [Claude CLI](https://claude.ai/download) 并登录
|
||||
@@ -0,0 +1,40 @@
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
import { app } from 'electron'
|
||||
|
||||
interface AppConfig {
|
||||
windowPosition?: { x: number; y: number }
|
||||
autoStart?: boolean
|
||||
collapsed?: boolean
|
||||
}
|
||||
|
||||
const DEFAULT_CONFIG: AppConfig = {
|
||||
autoStart: false,
|
||||
collapsed: true,
|
||||
}
|
||||
|
||||
function getConfigPath(): string {
|
||||
return path.join(app.getPath('userData'), 'config.json')
|
||||
}
|
||||
|
||||
export function loadConfig(): AppConfig {
|
||||
try {
|
||||
const configPath = getConfigPath()
|
||||
if (fs.existsSync(configPath)) {
|
||||
const data = fs.readFileSync(configPath, 'utf8')
|
||||
return { ...DEFAULT_CONFIG, ...JSON.parse(data) }
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load config:', err)
|
||||
}
|
||||
return { ...DEFAULT_CONFIG }
|
||||
}
|
||||
|
||||
export function saveConfig(config: AppConfig): void {
|
||||
try {
|
||||
const configPath = getConfigPath()
|
||||
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8')
|
||||
} catch (err) {
|
||||
console.error('Failed to save config:', err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
import { app, BrowserWindow, ipcMain, screen, globalShortcut } from 'electron'
|
||||
import * as path from 'path'
|
||||
import { PTYManager } from './pty'
|
||||
import { loadConfig, saveConfig } from './config'
|
||||
|
||||
const ptyManager = new PTYManager()
|
||||
let mainWindow: BrowserWindow | null = null
|
||||
|
||||
const COLLAPSED_SIZE = { width: 80, height: 80 }
|
||||
const EXPANDED_SIZE = { width: 900, height: 600 }
|
||||
|
||||
function createWindow() {
|
||||
const config = loadConfig()
|
||||
const display = screen.getPrimaryDisplay()
|
||||
const { width: screenWidth, height: screenHeight } = display.workAreaSize
|
||||
|
||||
// Use saved position or default to bottom-right
|
||||
let x = config.windowPosition?.x ?? screenWidth - COLLAPSED_SIZE.width - 20
|
||||
let y = config.windowPosition?.y ?? screenHeight - COLLAPSED_SIZE.height - 20
|
||||
|
||||
mainWindow = new BrowserWindow({
|
||||
width: COLLAPSED_SIZE.width,
|
||||
height: COLLAPSED_SIZE.height,
|
||||
x,
|
||||
y,
|
||||
frame: false,
|
||||
transparent: true,
|
||||
backgroundColor: '#00000000',
|
||||
alwaysOnTop: true,
|
||||
skipTaskbar: true,
|
||||
hasShadow: false,
|
||||
resizable: false,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
},
|
||||
})
|
||||
|
||||
// Dock hide on macOS
|
||||
if (app.dock) {
|
||||
app.dock.hide()
|
||||
}
|
||||
|
||||
if (process.env.VITE_DEV_SERVER_URL) {
|
||||
mainWindow.loadURL(process.env.VITE_DEV_SERVER_URL)
|
||||
} else {
|
||||
mainWindow.loadFile(path.join(__dirname, '../dist/index.html'))
|
||||
}
|
||||
|
||||
mainWindow.on('closed', () => {
|
||||
mainWindow = null
|
||||
})
|
||||
|
||||
// Save position when moved
|
||||
mainWindow.on('moved', () => {
|
||||
if (!mainWindow) return
|
||||
const [x, y] = mainWindow.getPosition()
|
||||
const config = loadConfig()
|
||||
saveConfig({ ...config, windowPosition: { x, y } })
|
||||
})
|
||||
}
|
||||
|
||||
function expandWindow() {
|
||||
if (!mainWindow) return
|
||||
const display = screen.getPrimaryDisplay()
|
||||
const { width: screenWidth, height: screenHeight } = display.workAreaSize
|
||||
|
||||
const currentPos = mainWindow.getPosition()
|
||||
let x = currentPos[0]
|
||||
let y = currentPos[1]
|
||||
|
||||
// Ensure window stays on screen
|
||||
if (x + EXPANDED_SIZE.width > screenWidth) {
|
||||
x = screenWidth - EXPANDED_SIZE.width - 20
|
||||
}
|
||||
if (y + EXPANDED_SIZE.height > screenHeight) {
|
||||
y = screenHeight - EXPANDED_SIZE.height - 20
|
||||
}
|
||||
|
||||
mainWindow.setResizable(true)
|
||||
mainWindow.setMinimumSize(EXPANDED_SIZE.width, EXPANDED_SIZE.height)
|
||||
mainWindow.setSize(EXPANDED_SIZE.width, EXPANDED_SIZE.height)
|
||||
mainWindow.setPosition(x, y)
|
||||
mainWindow.setAlwaysOnTop(true)
|
||||
mainWindow.setIgnoreMouseEvents(false)
|
||||
mainWindow.setHasShadow(true)
|
||||
|
||||
const config = loadConfig()
|
||||
saveConfig({ ...config, collapsed: false })
|
||||
}
|
||||
|
||||
function collapseWindow() {
|
||||
if (!mainWindow) return
|
||||
|
||||
const currentPos = mainWindow.getPosition()
|
||||
|
||||
mainWindow.setResizable(false)
|
||||
mainWindow.setSize(COLLAPSED_SIZE.width, COLLAPSED_SIZE.height)
|
||||
mainWindow.setPosition(currentPos[0], currentPos[1])
|
||||
mainWindow.setAlwaysOnTop(true)
|
||||
mainWindow.setIgnoreMouseEvents(true, { forward: true })
|
||||
mainWindow.setHasShadow(false)
|
||||
|
||||
const config = loadConfig()
|
||||
saveConfig({ ...config, collapsed: true, windowPosition: { x: currentPos[0], y: currentPos[1] } })
|
||||
}
|
||||
|
||||
function toggleWindow() {
|
||||
if (!mainWindow) {
|
||||
createWindow()
|
||||
return
|
||||
}
|
||||
if (mainWindow.isVisible()) {
|
||||
mainWindow.hide()
|
||||
} else {
|
||||
mainWindow.show()
|
||||
}
|
||||
}
|
||||
|
||||
app.whenReady().then(() => {
|
||||
createWindow()
|
||||
ptyManager.start('claude')
|
||||
|
||||
// Collapse after load to ensure pet mode
|
||||
mainWindow?.webContents.on('did-finish-load', () => {
|
||||
collapseWindow()
|
||||
})
|
||||
|
||||
// Register global shortcut
|
||||
const ret = globalShortcut.register('CommandOrControl+Shift+C', () => {
|
||||
if (!mainWindow) {
|
||||
createWindow()
|
||||
return
|
||||
}
|
||||
if (mainWindow.isVisible()) {
|
||||
mainWindow.hide()
|
||||
} else {
|
||||
mainWindow.show()
|
||||
mainWindow.focus()
|
||||
}
|
||||
})
|
||||
|
||||
if (!ret) {
|
||||
console.log('Global shortcut registration failed')
|
||||
}
|
||||
|
||||
// IPC handlers
|
||||
ipcMain.on('pty:write', (_event, data: string) => {
|
||||
ptyManager.write(data)
|
||||
})
|
||||
|
||||
ipcMain.on('pty:resize', (_event, cols: number, rows: number) => {
|
||||
ptyManager.resize(cols, rows)
|
||||
})
|
||||
|
||||
ipcMain.on('window:expand', () => {
|
||||
expandWindow()
|
||||
})
|
||||
|
||||
ipcMain.on('window:collapse', () => {
|
||||
collapseWindow()
|
||||
})
|
||||
|
||||
ipcMain.on('window:set-position', (_event, x: number, y: number) => {
|
||||
mainWindow?.setPosition(x, y)
|
||||
})
|
||||
|
||||
ipcMain.on('window:set-ignore-mouse', (_event, ignore: boolean) => {
|
||||
mainWindow?.setIgnoreMouseEvents(ignore, { forward: true })
|
||||
})
|
||||
|
||||
ipcMain.on('window:close', () => {
|
||||
mainWindow?.close()
|
||||
})
|
||||
|
||||
ipcMain.on('window:minimize', () => {
|
||||
mainWindow?.minimize()
|
||||
})
|
||||
|
||||
ptyManager.on('data', (data: string) => {
|
||||
mainWindow?.webContents.send('pty:data', data)
|
||||
})
|
||||
|
||||
ptyManager.on('exit', (code: number) => {
|
||||
mainWindow?.webContents.send('pty:exit', code)
|
||||
})
|
||||
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
ptyManager.kill()
|
||||
globalShortcut.unregisterAll()
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit()
|
||||
}
|
||||
})
|
||||
|
||||
app.on('before-quit', () => {
|
||||
ptyManager.kill()
|
||||
globalShortcut.unregisterAll()
|
||||
})
|
||||
|
||||
app.on('will-quit', () => {
|
||||
globalShortcut.unregisterAll()
|
||||
})
|
||||
@@ -0,0 +1,24 @@
|
||||
const { contextBridge, ipcRenderer } = require('electron')
|
||||
|
||||
const api = {
|
||||
pty: {
|
||||
write: (data) => ipcRenderer.send('pty:write', data),
|
||||
resize: (cols, rows) => ipcRenderer.send('pty:resize', cols, rows),
|
||||
onData: (callback) => {
|
||||
ipcRenderer.on('pty:data', (_event, data) => callback(data))
|
||||
},
|
||||
onExit: (callback) => {
|
||||
ipcRenderer.on('pty:exit', (_event, code) => callback(code))
|
||||
},
|
||||
},
|
||||
window: {
|
||||
expand: () => ipcRenderer.send('window:expand'),
|
||||
collapse: () => ipcRenderer.send('window:collapse'),
|
||||
setPosition: (x, y) => ipcRenderer.send('window:set-position', x, y),
|
||||
setIgnoreMouseEvents: (ignore) => ipcRenderer.send('window:set-ignore-mouse', ignore),
|
||||
close: () => ipcRenderer.send('window:close'),
|
||||
minimize: () => ipcRenderer.send('window:minimize'),
|
||||
},
|
||||
}
|
||||
|
||||
contextBridge.exposeInMainWorld('petAPI', api)
|
||||
@@ -0,0 +1,65 @@
|
||||
import * as pty from 'node-pty'
|
||||
import { execSync } from 'child_process'
|
||||
import { EventEmitter } from 'events'
|
||||
|
||||
export class PTYManager extends EventEmitter {
|
||||
private ptyProcess: pty.IPty | null = null
|
||||
|
||||
start(shell: string = 'claude', args: string[] = []) {
|
||||
const resolvedShell = this.resolveShell(shell)
|
||||
const resolvedArgs = shell === 'claude' ? args : []
|
||||
|
||||
this.ptyProcess = pty.spawn(resolvedShell, resolvedArgs, {
|
||||
name: 'xterm-color',
|
||||
cols: 80,
|
||||
rows: 24,
|
||||
cwd: process.env.HOME,
|
||||
env: process.env as { [key: string]: string },
|
||||
})
|
||||
|
||||
this.ptyProcess.onData((data: string) => {
|
||||
this.emit('data', data)
|
||||
})
|
||||
|
||||
this.ptyProcess.onExit(({ exitCode }) => {
|
||||
this.emit('exit', exitCode)
|
||||
})
|
||||
|
||||
return this.ptyProcess
|
||||
}
|
||||
|
||||
write(data: string) {
|
||||
if (this.ptyProcess) {
|
||||
this.ptyProcess.write(data)
|
||||
}
|
||||
}
|
||||
|
||||
resize(cols: number, rows: number) {
|
||||
if (this.ptyProcess) {
|
||||
this.ptyProcess.resize(cols, rows)
|
||||
}
|
||||
}
|
||||
|
||||
kill() {
|
||||
if (this.ptyProcess) {
|
||||
this.ptyProcess.kill()
|
||||
this.ptyProcess = null
|
||||
}
|
||||
}
|
||||
|
||||
private resolveShell(shell: string): string {
|
||||
if (shell === 'claude') {
|
||||
try {
|
||||
const path = execSync('which claude', { encoding: 'utf8' }).trim()
|
||||
return path
|
||||
} catch {
|
||||
try {
|
||||
return execSync('which zsh', { encoding: 'utf8' }).trim()
|
||||
} catch {
|
||||
return '/bin/zsh'
|
||||
}
|
||||
}
|
||||
}
|
||||
return shell
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>MyClaude Pet</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+5354
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"name": "myclaude-pet",
|
||||
"version": "1.0.0",
|
||||
"description": "Desktop pet wrapper for Claude CLI",
|
||||
"main": "dist-electron/main.js",
|
||||
"scripts": {
|
||||
"dev": "npx tsc -p tsconfig.electron.json && concurrently \"vite\" \"tsc -p tsconfig.electron.json --watch --preserveWatchOutput\" \"wait-on http://localhost:5173 && VITE_DEV_SERVER_URL=http://localhost:5173 npx electron dist-electron/main.js\"",
|
||||
"build": "vite build && tsc -p tsconfig.electron.json && cp electron/preload.js dist-electron/",
|
||||
"start": "npx electron dist-electron/main.js",
|
||||
"rebuild": "electron-rebuild",
|
||||
"pack": "npm run build && electron-builder --dir",
|
||||
"dist": "npm run build && electron-builder"
|
||||
},
|
||||
"build": {
|
||||
"appId": "com.apaam.myclaude-pet",
|
||||
"productName": "MyClaude",
|
||||
"directories": {
|
||||
"output": "release",
|
||||
"buildResources": "build"
|
||||
},
|
||||
"files": [
|
||||
"dist/**/*",
|
||||
"dist-electron/**/*",
|
||||
"build/**/*"
|
||||
],
|
||||
"mac": {
|
||||
"target": [
|
||||
{
|
||||
"target": "dir",
|
||||
"arch": ["arm64", "x64"]
|
||||
},
|
||||
{
|
||||
"target": "dmg",
|
||||
"arch": ["arm64", "x64"]
|
||||
}
|
||||
],
|
||||
"category": "public.app-category.productivity",
|
||||
"hardenedRuntime": true,
|
||||
"gatekeeperAssess": false
|
||||
}
|
||||
},
|
||||
"keywords": [
|
||||
"claude",
|
||||
"desktop",
|
||||
"pet"
|
||||
],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@xterm/addon-fit": "^0.11.0",
|
||||
"@xterm/xterm": "^6.0.0",
|
||||
"node-pty": "^1.1.0",
|
||||
"react": "^19.2.5",
|
||||
"react-dom": "^19.2.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron/rebuild": "^4.0.4",
|
||||
"@types/node": "^25.6.0",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
"concurrently": "^9.2.1",
|
||||
"electron": "^42.0.0",
|
||||
"electron-builder": "^26.8.1",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^6.0.3",
|
||||
"vite": "^8.0.10",
|
||||
"wait-on": "^9.0.5"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 174 KiB |
@@ -0,0 +1,27 @@
|
||||
import React, { useState, useCallback } from 'react'
|
||||
import TerminalPanel from './components/TerminalPanel'
|
||||
import PetOrb from './components/PetOrb'
|
||||
|
||||
type ViewMode = 'orb' | 'terminal'
|
||||
|
||||
const App: React.FC = () => {
|
||||
const [mode, setMode] = useState<ViewMode>('orb')
|
||||
|
||||
const handleExpand = useCallback(() => {
|
||||
setMode('terminal')
|
||||
window.petAPI?.window.expand()
|
||||
}, [])
|
||||
|
||||
const handleCollapse = useCallback(() => {
|
||||
setMode('orb')
|
||||
window.petAPI?.window.collapse()
|
||||
}, [])
|
||||
|
||||
return mode === 'terminal' ? (
|
||||
<TerminalPanel onCollapse={handleCollapse} />
|
||||
) : (
|
||||
<PetOrb onExpand={handleExpand} />
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
@@ -0,0 +1,109 @@
|
||||
import React, { useCallback, useRef, useState } from 'react'
|
||||
|
||||
interface PetOrbProps {
|
||||
onExpand: () => void
|
||||
}
|
||||
|
||||
type OrbState = 'idle' | 'thinking' | 'typing'
|
||||
|
||||
const PetOrb: React.FC<PetOrbProps> = ({ onExpand }) => {
|
||||
const [isDragging, setIsDragging] = useState(false)
|
||||
const [orbState] = useState<OrbState>('idle')
|
||||
const dragOffset = useRef({ x: 0, y: 0 })
|
||||
const hasDragged = useRef(false)
|
||||
|
||||
const getAnimation = () => {
|
||||
switch (orbState) {
|
||||
case 'thinking':
|
||||
return 'orbThink 0.8s ease-in-out infinite'
|
||||
case 'typing':
|
||||
return 'orbType 0.3s ease-in-out infinite'
|
||||
default:
|
||||
return 'orbBreathe 3s ease-in-out infinite'
|
||||
}
|
||||
}
|
||||
|
||||
const handleMouseDown = useCallback((e: React.MouseEvent) => {
|
||||
hasDragged.current = false
|
||||
setIsDragging(true)
|
||||
dragOffset.current = {
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleMouseMove = useCallback(
|
||||
(e: MouseEvent) => {
|
||||
if (!isDragging) return
|
||||
const dx = Math.abs(e.clientX - dragOffset.current.x)
|
||||
const dy = Math.abs(e.clientY - dragOffset.current.y)
|
||||
if (dx > 3 || dy > 3) {
|
||||
hasDragged.current = true
|
||||
}
|
||||
|
||||
const screenX = e.screenX - dragOffset.current.x
|
||||
const screenY = e.screenY - dragOffset.current.y
|
||||
window.petAPI?.window.setPosition(screenX, screenY)
|
||||
},
|
||||
[isDragging]
|
||||
)
|
||||
|
||||
const handleMouseUp = useCallback(() => {
|
||||
setIsDragging(false)
|
||||
window.removeEventListener('mousemove', handleMouseMove)
|
||||
window.removeEventListener('mouseup', handleMouseUp)
|
||||
|
||||
if (!hasDragged.current) {
|
||||
onExpand()
|
||||
}
|
||||
}, [handleMouseMove, onExpand])
|
||||
|
||||
const handleMouseDownWrapper = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
handleMouseDown(e)
|
||||
window.addEventListener('mousemove', handleMouseMove)
|
||||
window.addEventListener('mouseup', handleMouseUp)
|
||||
},
|
||||
[handleMouseDown, handleMouseMove, handleMouseUp]
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
cursor: isDragging ? 'grabbing' : 'grab',
|
||||
userSelect: 'none',
|
||||
}}
|
||||
onMouseDown={handleMouseDownWrapper}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: '56px',
|
||||
height: '56px',
|
||||
borderRadius: '50%',
|
||||
overflow: 'hidden',
|
||||
boxShadow: '0 4px 16px rgba(0, 0, 0, 0.3), inset 0 2px 4px rgba(255,255,255,0.2)',
|
||||
animation: getAnimation(),
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src="./icon.png"
|
||||
alt="MyClaude"
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover',
|
||||
}}
|
||||
draggable={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PetOrb
|
||||
@@ -0,0 +1,110 @@
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
import { Terminal as XTerm } from '@xterm/xterm'
|
||||
import { FitAddon } from '@xterm/addon-fit'
|
||||
import '@xterm/xterm/css/xterm.css'
|
||||
|
||||
const Terminal: React.FC = () => {
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const xtermRef = useRef<XTerm | null>(null)
|
||||
const fitAddonRef = useRef<FitAddon | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return
|
||||
|
||||
const term = new XTerm({
|
||||
fontSize: 14,
|
||||
fontFamily: '"SF Mono", Monaco, "Cascadia Code", "Roboto Mono", Consolas, "Courier New", monospace',
|
||||
theme: {
|
||||
background: '#0c0c0c',
|
||||
foreground: '#4ade80',
|
||||
cursor: '#4ade80',
|
||||
selectionBackground: '#14532d',
|
||||
black: '#0c0c0c',
|
||||
red: '#f87171',
|
||||
green: '#4ade80',
|
||||
yellow: '#facc15',
|
||||
blue: '#60a5fa',
|
||||
magenta: '#c084fc',
|
||||
cyan: '#22d3ee',
|
||||
white: '#e5e5e5',
|
||||
brightBlack: '#525252',
|
||||
brightRed: '#f87171',
|
||||
brightGreen: '#86efac',
|
||||
brightYellow: '#fde047',
|
||||
brightBlue: '#93c5fd',
|
||||
brightMagenta: '#d8b4fe',
|
||||
brightCyan: '#67e8f9',
|
||||
brightWhite: '#ffffff',
|
||||
},
|
||||
cursorBlink: true,
|
||||
scrollback: 10000,
|
||||
allowProposedApi: true,
|
||||
})
|
||||
|
||||
const fitAddon = new FitAddon()
|
||||
term.loadAddon(fitAddon)
|
||||
|
||||
term.open(containerRef.current)
|
||||
fitAddon.fit()
|
||||
|
||||
xtermRef.current = term
|
||||
fitAddonRef.current = fitAddon
|
||||
|
||||
const api = window.petAPI
|
||||
if (api) {
|
||||
api.pty.onData((data: string) => {
|
||||
term.write(data)
|
||||
})
|
||||
|
||||
api.pty.onExit((code: number) => {
|
||||
term.writeln(`\r\n\x1b[31mProcess exited with code ${code}\x1b[0m`)
|
||||
})
|
||||
|
||||
term.onData((data: string) => {
|
||||
api.pty.write(data)
|
||||
})
|
||||
|
||||
const dims = fitAddon.proposeDimensions()
|
||||
if (dims) {
|
||||
api.pty.resize(dims.cols, dims.rows)
|
||||
}
|
||||
}
|
||||
|
||||
const handleResize = () => {
|
||||
fitAddon.fit()
|
||||
const dims = fitAddon.proposeDimensions()
|
||||
if (dims && api) {
|
||||
api.pty.resize(dims.cols, dims.rows)
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('resize', handleResize)
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
handleResize()
|
||||
})
|
||||
resizeObserver.observe(containerRef.current)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
resizeObserver.disconnect()
|
||||
term.dispose()
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="crt-container crt-flicker" style={{ width: '100%', height: '100%', position: 'relative' }}>
|
||||
<div className="crt-scanline" />
|
||||
<div
|
||||
ref={containerRef}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
padding: '4px',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Terminal
|
||||
@@ -0,0 +1,96 @@
|
||||
import React from 'react'
|
||||
import Terminal from './Terminal'
|
||||
|
||||
interface TerminalPanelProps {
|
||||
onCollapse: () => void
|
||||
}
|
||||
|
||||
const TerminalPanel: React.FC<TerminalPanelProps> = ({ onCollapse }) => {
|
||||
const handleMinimize = () => {
|
||||
window.petAPI?.window.minimize()
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
onCollapse()
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
borderRadius: '12px',
|
||||
overflow: 'hidden',
|
||||
background: '#0c0c0c',
|
||||
border: '1px solid #333',
|
||||
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.6)',
|
||||
}}
|
||||
>
|
||||
<TitleBar onMinimize={handleMinimize} onClose={handleClose} />
|
||||
<div style={{ flex: 1, overflow: 'hidden', padding: '4px' }}>
|
||||
<Terminal />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface TitleBarProps {
|
||||
onMinimize: () => void
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const TitleBar: React.FC<TitleBarProps> = ({ onMinimize, onClose }) => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
height: '32px',
|
||||
background: '#1a1a1a',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '0 12px',
|
||||
WebkitAppRegion: 'drag',
|
||||
userSelect: 'none',
|
||||
borderBottom: '1px solid #333',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
color: '#888',
|
||||
fontSize: '13px',
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
myclaude
|
||||
</span>
|
||||
<div style={{ display: 'flex', gap: '8px', WebkitAppRegion: 'no-drag' }}>
|
||||
<button
|
||||
onClick={onMinimize}
|
||||
style={{
|
||||
width: '12px',
|
||||
height: '12px',
|
||||
borderRadius: '50%',
|
||||
border: 'none',
|
||||
background: '#f5bd4f',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
onClick={onClose}
|
||||
style={{
|
||||
width: '12px',
|
||||
height: '12px',
|
||||
borderRadius: '50%',
|
||||
border: 'none',
|
||||
background: '#ec6b5e',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TerminalPanel
|
||||
@@ -0,0 +1,127 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
background: transparent;
|
||||
overflow: hidden;
|
||||
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
|
||||
}
|
||||
|
||||
#root {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
/* CRT Terminal Effects */
|
||||
.crt-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.crt-container::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: repeating-linear-gradient(
|
||||
0deg,
|
||||
rgba(0, 0, 0, 0.08),
|
||||
rgba(0, 0, 0, 0.08) 1px,
|
||||
transparent 1px,
|
||||
transparent 2px
|
||||
);
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.crt-container::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: radial-gradient(
|
||||
ellipse at center,
|
||||
transparent 50%,
|
||||
rgba(0, 0, 0, 0.3) 100%
|
||||
);
|
||||
pointer-events: none;
|
||||
z-index: 11;
|
||||
}
|
||||
|
||||
/* Terminal text glow */
|
||||
.xterm-viewport {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.xterm-screen {
|
||||
text-shadow: 0 0 2px rgba(74, 222, 128, 0.3), 0 0 4px rgba(74, 222, 128, 0.1) !important;
|
||||
}
|
||||
|
||||
/* Flicker animation */
|
||||
@keyframes flicker {
|
||||
0% { opacity: 0.98; }
|
||||
5% { opacity: 0.95; }
|
||||
10% { opacity: 0.98; }
|
||||
15% { opacity: 0.96; }
|
||||
20% { opacity: 0.99; }
|
||||
50% { opacity: 0.98; }
|
||||
52% { opacity: 0.93; }
|
||||
54% { opacity: 0.98; }
|
||||
100% { opacity: 0.98; }
|
||||
}
|
||||
|
||||
.crt-flicker {
|
||||
animation: flicker 4s infinite;
|
||||
}
|
||||
|
||||
/* Scanline sweep */
|
||||
@keyframes scanline {
|
||||
0% { transform: translateY(-100%); }
|
||||
100% { transform: translateY(100vh); }
|
||||
}
|
||||
|
||||
.crt-scanline {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
transparent,
|
||||
rgba(74, 222, 128, 0.08),
|
||||
transparent
|
||||
);
|
||||
animation: scanline 8s linear infinite;
|
||||
pointer-events: none;
|
||||
z-index: 12;
|
||||
}
|
||||
|
||||
/* Pet Orb Animations */
|
||||
@keyframes orbBreathe {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.06); }
|
||||
}
|
||||
|
||||
@keyframes orbThink {
|
||||
0%, 100% { transform: scale(1) rotate(0deg); }
|
||||
25% { transform: scale(1.1) rotate(-5deg); }
|
||||
75% { transform: scale(1.1) rotate(5deg); }
|
||||
}
|
||||
|
||||
@keyframes orbType {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
25% { transform: translateY(-2px); }
|
||||
75% { transform: translateY(1px); }
|
||||
}
|
||||
|
||||
@keyframes orbGlow {
|
||||
0%, 100% { box-shadow: 0 4px 16px rgba(22, 163, 74, 0.4), inset 0 2px 4px rgba(255,255,255,0.2); }
|
||||
50% { box-shadow: 0 4px 24px rgba(74, 222, 128, 0.6), inset 0 2px 4px rgba(255,255,255,0.3); }
|
||||
}
|
||||
@@ -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(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
)
|
||||
Vendored
+22
@@ -0,0 +1,22 @@
|
||||
export {}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
petAPI: {
|
||||
pty: {
|
||||
write: (data: string) => void
|
||||
resize: (cols: number, rows: number) => void
|
||||
onData: (callback: (data: string) => void) => void
|
||||
onExit: (callback: (code: number) => void) => void
|
||||
}
|
||||
window: {
|
||||
expand: () => void
|
||||
collapse: () => void
|
||||
setPosition: (x: number, y: number) => void
|
||||
setIgnoreMouseEvents: (ignore: boolean) => void
|
||||
close: () => void
|
||||
minimize: () => void
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "node",
|
||||
"ignoreDeprecations": "6.0",
|
||||
"lib": ["ES2020"],
|
||||
"outDir": "dist-electron",
|
||||
"rootDir": "electron",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": false,
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": ["electron/**/*"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"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"]
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
base: './',
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user