chore: wave-0 baseline (Tauri2+React shell, platform detect, fake catalog)
@@ -0,0 +1,22 @@
|
|||||||
|
# 构建产物
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
target/
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Rust
|
||||||
|
Cargo.lock
|
||||||
|
|
||||||
|
# 系统与编辑器
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
|
||||||
|
# Tauri 本地运行文件
|
||||||
|
apps/desktop/src-tauri/gen/
|
||||||
|
|
||||||
|
# 临时文件
|
||||||
|
*.tmp
|
||||||
|
*.bak
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
[workspace]
|
||||||
|
resolver = "2"
|
||||||
|
members = [
|
||||||
|
"crates/agentdock-core",
|
||||||
|
"crates/agentdock-adapter",
|
||||||
|
"crates/agentdock-platform",
|
||||||
|
"crates/agentdock-config",
|
||||||
|
"crates/agentdock-secrets",
|
||||||
|
"crates/agentdock-exec",
|
||||||
|
"crates/agentdock-diag",
|
||||||
|
"crates/agentdock-store",
|
||||||
|
"apps/desktop/src-tauri",
|
||||||
|
]
|
||||||
|
|
||||||
|
[workspace.package]
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
license = "Apache-2.0"
|
||||||
|
|
||||||
|
# 统一输出,避免各 crate 各自配置重复
|
||||||
|
[profile.dev]
|
||||||
|
debug = false
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
lto = "thin"
|
||||||
|
strip = true
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# AgentDock
|
||||||
|
|
||||||
|
跨平台、中文化、可扩展的 Agent CLI 软件管理器。帮助用户发现、安装、更新、配置、授权和排查 Agent CLI。
|
||||||
|
|
||||||
|
**本仓库当前为 Wave 0 状态**:桌面应用空壳可运行(Tauri 2 + React 19 + TypeScript + Vite),含设计 Token、
|
||||||
|
八层 Rust crate 骨架、适配器目录骨架、Windows 本机环境检测与 SQLite 空库。
|
||||||
|
|
||||||
|
## 仓库结构
|
||||||
|
|
||||||
|
```text
|
||||||
|
agentdock/
|
||||||
|
├── apps/desktop/ # Tauri 2 应用入口(React UI + src-tauri)
|
||||||
|
│ └── src/tokens/ # 设计 Token(对接视觉规范 v1.2,唯一品味依据)
|
||||||
|
├── crates/ # 本机能力层(八层)
|
||||||
|
│ ├── agentdock-core/ # 编排(发现/安装/配置/授权/诊断流水线)
|
||||||
|
│ ├── agentdock-adapter/ # 适配器 schema 加载、版本校验、执行器接口
|
||||||
|
│ ├── agentdock-platform/# Win/Linux 环境检测、PATH、发行版识别
|
||||||
|
│ ├── agentdock-config/ # 配置读写(原子写入、备份、多格式编解码)
|
||||||
|
│ ├── agentdock-secrets/ # 系统密钥库封装 + 脱敏
|
||||||
|
│ ├── agentdock-exec/ # 参数白名单、禁止 shell 拼接的进程执行
|
||||||
|
│ ├── agentdock-diag/ # 诊断规则引擎
|
||||||
|
│ └── agentdock-store/ # SQLite 空库与 schema 迁移骨架
|
||||||
|
├── adapters/ # 版本化适配器 YAML(14 + 目录索引)
|
||||||
|
├── docs/ # 架构文档副本
|
||||||
|
└── tests/ # 测试夹具与 e2e 脚本
|
||||||
|
```
|
||||||
|
|
||||||
|
## 本地开发
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 前端依赖 + 构建
|
||||||
|
cd apps/desktop
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# Rust 工作区测试(八 crate + 桌面壳)
|
||||||
|
cargo test
|
||||||
|
|
||||||
|
# 启动桌面应用(开发模式)
|
||||||
|
cd apps/desktop
|
||||||
|
npm run tauri dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Wave 0 范围
|
||||||
|
|
||||||
|
- 应用外壳:左侧竖导航六项 + 顶栏 + 六个占位页(视觉规范 v1.2 §3.0 + PRD §7)
|
||||||
|
- IPC 心跳:`detect_env` 命令(Windows 本机环境检测,架构 §5)
|
||||||
|
- `agentdock-store`:SQLite 空库 + schema 迁移骨架
|
||||||
|
- 假目录:CLI 目录页以 `adapters/catalog.yaml` 渲染 14 张卡片
|
||||||
|
|
||||||
|
后续波次见 `docs/architecture.md` §12。
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# AgentDock 适配器目录索引(v1)
|
||||||
|
# 第一批 14 个(数据取自调研底稿 agent-cli-survey-2026-08-24.md,不编造)
|
||||||
|
# 第二批 4 个(grok / amp / deepseek-dsh / plandex)暂缓,本目录不含,后续按需加入。
|
||||||
|
catalog_version: 1
|
||||||
|
tools:
|
||||||
|
- id: codex
|
||||||
|
file: tools/codex.yaml
|
||||||
|
- id: claude-code
|
||||||
|
file: tools/claude-code.yaml
|
||||||
|
- id: gemini
|
||||||
|
file: tools/gemini.yaml
|
||||||
|
- id: copilot
|
||||||
|
file: tools/copilot.yaml
|
||||||
|
- id: kimi
|
||||||
|
file: tools/kimi.yaml
|
||||||
|
- id: qwen
|
||||||
|
file: tools/qwen.yaml
|
||||||
|
- id: codebuddy
|
||||||
|
file: tools/codebuddy.yaml
|
||||||
|
- id: opencode
|
||||||
|
file: tools/opencode.yaml
|
||||||
|
- id: crush
|
||||||
|
file: tools/crush.yaml
|
||||||
|
- id: goose
|
||||||
|
file: tools/goose.yaml
|
||||||
|
- id: aider
|
||||||
|
file: tools/aider.yaml
|
||||||
|
- id: cursor
|
||||||
|
file: tools/cursor.yaml
|
||||||
|
- id: cline
|
||||||
|
file: tools/cline.yaml
|
||||||
|
- id: warp
|
||||||
|
file: tools/warp.yaml
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"$id": "https://agentdock.local/schemas/adapter.schema.json",
|
||||||
|
"title": "AgentDock Adapter",
|
||||||
|
"description": "Agent CLI 适配器定义(v1)。Wave 0 仅含占位字段;完整 schema 见架构 §3.1,随 Wave 1 落地。",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["id", "name", "name_zh", "vendor", "status"],
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "稳定 ID,如 codex",
|
||||||
|
"pattern": "^[a-z0-9][a-z0-9-]*$"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "展示名"
|
||||||
|
},
|
||||||
|
"name_zh": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "中文展示名"
|
||||||
|
},
|
||||||
|
"vendor": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "厂商"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["available", "watch"],
|
||||||
|
"description": "目录状态:available=可安装列表;watch=观察中(第二批)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# aider 适配器占位(Wave 0)
|
||||||
|
id: aider
|
||||||
|
name: Aider
|
||||||
|
name_zh: Aider
|
||||||
|
vendor: aider.chat
|
||||||
|
status: available
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# claude-code 适配器占位(Wave 0)
|
||||||
|
id: claude-code
|
||||||
|
name: Claude Code
|
||||||
|
name_zh: Claude Code
|
||||||
|
vendor: Anthropic
|
||||||
|
status: available
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# cline 适配器占位(Wave 0)
|
||||||
|
id: cline
|
||||||
|
name: Cline
|
||||||
|
name_zh: Cline
|
||||||
|
vendor: Cline
|
||||||
|
status: available
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# codebuddy 适配器占位(Wave 0)
|
||||||
|
id: codebuddy
|
||||||
|
name: CodeBuddy Code
|
||||||
|
name_zh: CodeBuddy Code
|
||||||
|
vendor: 腾讯
|
||||||
|
status: available
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# codex 适配器占位(Wave 0)
|
||||||
|
id: codex
|
||||||
|
name: Codex CLI
|
||||||
|
name_zh: Codex CLI
|
||||||
|
vendor: OpenAI
|
||||||
|
status: available
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# copilot 适配器占位(Wave 0)
|
||||||
|
id: copilot
|
||||||
|
name: Copilot CLI
|
||||||
|
name_zh: Copilot CLI
|
||||||
|
vendor: GitHub
|
||||||
|
status: available
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# crush 适配器占位(Wave 0)
|
||||||
|
id: crush
|
||||||
|
name: Crush
|
||||||
|
name_zh: Crush
|
||||||
|
vendor: Charm
|
||||||
|
status: available
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# cursor 适配器占位(Wave 0)
|
||||||
|
id: cursor
|
||||||
|
name: Cursor CLI
|
||||||
|
name_zh: Cursor CLI
|
||||||
|
vendor: Cursor
|
||||||
|
status: available
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# gemini 适配器占位(Wave 0)
|
||||||
|
id: gemini
|
||||||
|
name: Gemini CLI
|
||||||
|
name_zh: Gemini CLI
|
||||||
|
vendor: Google
|
||||||
|
status: available
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# goose 适配器占位(Wave 0)
|
||||||
|
id: goose
|
||||||
|
name: Goose
|
||||||
|
name_zh: Goose
|
||||||
|
vendor: Linux 基金会(AAIF)
|
||||||
|
status: available
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# kimi 适配器占位(Wave 0)
|
||||||
|
id: kimi
|
||||||
|
name: Kimi CLI
|
||||||
|
name_zh: Kimi CLI
|
||||||
|
vendor: 月之暗面
|
||||||
|
status: available
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# opencode 适配器占位(Wave 0)
|
||||||
|
id: opencode
|
||||||
|
name: OpenCode
|
||||||
|
name_zh: OpenCode
|
||||||
|
vendor: opencode.ai
|
||||||
|
status: available
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# qwen 适配器占位(Wave 0)
|
||||||
|
id: qwen
|
||||||
|
name: Qwen Code
|
||||||
|
name_zh: Qwen Code
|
||||||
|
vendor: 阿里巴巴
|
||||||
|
status: available
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# warp 适配器占位(Wave 0)
|
||||||
|
id: warp
|
||||||
|
name: Warp Agent CLI
|
||||||
|
name_zh: Warp Agent CLI
|
||||||
|
vendor: Warp
|
||||||
|
status: available
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>AgentDock</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"name": "agentdock-desktop",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"tauri": "tauri"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tauri-apps/api": "^2",
|
||||||
|
"lucide-react": "^0.525.0",
|
||||||
|
"react": "^19.1.0",
|
||||||
|
"react-dom": "^19.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tauri-apps/cli": "^2",
|
||||||
|
"@types/react": "^19.1.0",
|
||||||
|
"@types/react-dom": "^19.1.0",
|
||||||
|
"@vitejs/plugin-react": "^4.5.0",
|
||||||
|
"typescript": "^5.6.3",
|
||||||
|
"vite": "^6.3.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
[package]
|
||||||
|
name = "agentdock-desktop"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
description = "AgentDock 桌面壳(Tauri 2)"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "agentdock_desktop_lib"
|
||||||
|
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
tauri-build = { version = "2", features = [] }
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tauri = { version = "2", features = [] }
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
agentdock-platform = { path = "../../../crates/agentdock-platform" }
|
||||||
|
agentdock-adapter = { path = "../../../crates/agentdock-adapter" }
|
||||||
|
agentdock-store = { path = "../../../crates/agentdock-store" }
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
tauri_build::build()
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../gen/schemas/desktop-schema.json",
|
||||||
|
"identifier": "default",
|
||||||
|
"description": "默认能力:主窗口(Wave 0 仅调用自有 invoke 命令)",
|
||||||
|
"windows": ["main"],
|
||||||
|
"permissions": ["core:default"]
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 887 B |
|
After Width: | Height: | Size: 9.7 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
</adaptive-icon>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_launcher_background">#fff</color>
|
||||||
|
</resources>
|
||||||
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 622 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 973 B |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
@@ -0,0 +1,11 @@
|
|||||||
|
//! IPC 命令:假目录(架构 §2 adapters/,IPC 契约 listCatalog)
|
||||||
|
|
||||||
|
use agentdock_adapter::catalog::{self, CatalogEntry};
|
||||||
|
|
||||||
|
use crate::adapters_dir;
|
||||||
|
|
||||||
|
/// 读取 adapters/catalog.yaml 与 tools/*.yaml,返回 14 个目录条目
|
||||||
|
#[tauri::command(rename = "listCatalog")]
|
||||||
|
pub fn list_catalog() -> Result<Vec<CatalogEntry>, String> {
|
||||||
|
catalog::load_catalog(adapters_dir()).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
//! IPC 命令:本机环境检测(架构 §5,IPC 契约 detectEnv)
|
||||||
|
|
||||||
|
use agentdock_platform::detect;
|
||||||
|
use agentdock_platform::model::PlatformEnv;
|
||||||
|
|
||||||
|
/// 返回本机真实环境快照(Windows/Linux 双平台)
|
||||||
|
#[tauri::command(rename = "detectEnv")]
|
||||||
|
pub fn detect_env() -> PlatformEnv {
|
||||||
|
detect::detect_env()
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod catalog;
|
||||||
|
pub mod env;
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
mod commands;
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
/// 解析适配器目录。
|
||||||
|
/// 优先级:环境变量 AGENTDOCK_ADAPTERS_DIR > CWD 相对路径(tauri dev 时 CWD=apps/desktop)
|
||||||
|
/// > CARGO_MANIFEST_DIR 相对路径(仓库根)。
|
||||||
|
fn adapters_dir() -> PathBuf {
|
||||||
|
if let Ok(dir) = std::env::var("AGENTDOCK_ADAPTERS_DIR") {
|
||||||
|
return PathBuf::from(dir);
|
||||||
|
}
|
||||||
|
let cwd = std::env::current_dir().unwrap_or_default();
|
||||||
|
let cargo_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||||
|
let candidates = [
|
||||||
|
cwd.join("../../adapters"),
|
||||||
|
cargo_dir.join("../../../adapters"),
|
||||||
|
];
|
||||||
|
for c in candidates {
|
||||||
|
if c.join("catalog.yaml").exists() {
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cargo_dir.join("../../../adapters")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
|
pub fn run() {
|
||||||
|
tauri::Builder::default()
|
||||||
|
.setup(|_app| {
|
||||||
|
// 自测标记:`tauri dev` 启动后日志出现该行即代表壳已就绪
|
||||||
|
println!("[agentdock] window-ready");
|
||||||
|
// Wave 0:初始化 SQLite 空库(架构 §2 状态存储层)
|
||||||
|
let _ = agentdock_store::Store::open_default();
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.invoke_handler(tauri::generate_handler![
|
||||||
|
commands::env::detect_env,
|
||||||
|
commands::catalog::list_catalog,
|
||||||
|
])
|
||||||
|
.run(tauri::generate_context!())
|
||||||
|
.expect("error while running tauri application");
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||||
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
agentdock_desktop_lib::run()
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
|
"productName": "AgentDock",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"identifier": "cn.agentdock.desktop",
|
||||||
|
"build": {
|
||||||
|
"beforeDevCommand": "npm run dev",
|
||||||
|
"devUrl": "http://localhost:1420",
|
||||||
|
"beforeBuildCommand": "npm run build",
|
||||||
|
"frontendDist": "../dist"
|
||||||
|
},
|
||||||
|
"app": {
|
||||||
|
"windows": [
|
||||||
|
{
|
||||||
|
"title": "AgentDock",
|
||||||
|
"width": 1280,
|
||||||
|
"height": 800,
|
||||||
|
"minWidth": 960,
|
||||||
|
"minHeight": 600,
|
||||||
|
"center": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"security": {
|
||||||
|
"csp": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bundle": {
|
||||||
|
"active": true,
|
||||||
|
"targets": ["nsis"],
|
||||||
|
"icon": [
|
||||||
|
"icons/32x32.png",
|
||||||
|
"icons/128x128.png",
|
||||||
|
"icons/128x128@2x.png",
|
||||||
|
"icons/icon.ico"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import { Sidebar, type PageKey, PAGE_META } from "./components/Sidebar";
|
||||||
|
import { Header } from "./components/Header";
|
||||||
|
import { OverviewPage } from "./pages/Overview";
|
||||||
|
import { CatalogPage } from "./pages/Catalog";
|
||||||
|
import { MyCliPage } from "./pages/MyCli";
|
||||||
|
import { ConfigCenterPage } from "./pages/ConfigCenter";
|
||||||
|
import { BackupPage } from "./pages/Backup";
|
||||||
|
import { SettingsPage } from "./pages/Settings";
|
||||||
|
import { useEnv } from "./hooks/useEnv";
|
||||||
|
import type { PlatformEnv, RuntimeInfo } from "./ipc/types";
|
||||||
|
|
||||||
|
/** 平台相关运行时集合(Windows 不看 apt,Linux 不看 winget) */
|
||||||
|
const WIN_RUNTIMES = ["node", "npm", "python", "uv", "git", "winget"] as const;
|
||||||
|
const LINUX_RUNTIMES = ["node", "npm", "python", "uv", "git", "apt"] as const;
|
||||||
|
|
||||||
|
function envWarningCount(env: PlatformEnv | null): number {
|
||||||
|
if (!env) return 0;
|
||||||
|
const keys = env.os === "windows" ? WIN_RUNTIMES : LINUX_RUNTIMES;
|
||||||
|
let n = 0;
|
||||||
|
for (const k of keys) {
|
||||||
|
const info = env.runtimes[k] as RuntimeInfo | null | undefined;
|
||||||
|
if (info && info.status !== "installed") n++;
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
const [page, setPage] = useState<PageKey>("overview");
|
||||||
|
const { env } = useEnv();
|
||||||
|
const warningCount = envWarningCount(env);
|
||||||
|
|
||||||
|
const content = useMemo(() => {
|
||||||
|
switch (page) {
|
||||||
|
case "overview":
|
||||||
|
return <OverviewPage />;
|
||||||
|
case "catalog":
|
||||||
|
return <CatalogPage />;
|
||||||
|
case "my-cli":
|
||||||
|
return <MyCliPage />;
|
||||||
|
case "config":
|
||||||
|
return <ConfigCenterPage />;
|
||||||
|
case "backup":
|
||||||
|
return <BackupPage />;
|
||||||
|
case "settings":
|
||||||
|
return <SettingsPage />;
|
||||||
|
}
|
||||||
|
}, [page]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="app-shell">
|
||||||
|
<div className="bg-grid grid-drift" aria-hidden="true" />
|
||||||
|
<Sidebar current={page} onNavigate={setPage} />
|
||||||
|
<div className="app-main">
|
||||||
|
<Header title={PAGE_META[page].title} warningCount={warningCount} />
|
||||||
|
<main className="app-content">
|
||||||
|
{/* key=page 触发 §4.2 页面切换动效:translateY(8px)+淡入 */}
|
||||||
|
<div key={page} className="page-enter">
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
/** CLI 单色字母头像(视觉规范 §7:双字母单色,--ad-bg-2 底 + 1px --ad-border) */
|
||||||
|
export function CliMonogram({
|
||||||
|
name,
|
||||||
|
size = 32,
|
||||||
|
}: {
|
||||||
|
name: string;
|
||||||
|
size?: number;
|
||||||
|
}) {
|
||||||
|
const chars = name.replace(/\s+/g, "").slice(0, 2).toUpperCase();
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className="monogram"
|
||||||
|
style={{ width: size, height: size, fontSize: Math.round(size * 0.42) }}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
{chars}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { ChevronDown, Search } from "lucide-react";
|
||||||
|
|
||||||
|
interface HeaderProps {
|
||||||
|
title: string;
|
||||||
|
/** 环境警告条数(0 时整颗 chip 不渲染,规范 §3.0) */
|
||||||
|
warningCount?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 顶栏(视觉规范 §3.0):页面标题 + 搜索框 + 环境警告 chip */
|
||||||
|
export function Header({ title, warningCount = 0 }: HeaderProps) {
|
||||||
|
return (
|
||||||
|
<header className="header">
|
||||||
|
<h1 className="header-title">{title}</h1>
|
||||||
|
<div className="header-actions">
|
||||||
|
<div className="search-box">
|
||||||
|
<Search size={16} strokeWidth={1.5} className="search-icon" aria-hidden="true" />
|
||||||
|
<input
|
||||||
|
type="search"
|
||||||
|
placeholder="搜索 CLI…"
|
||||||
|
aria-label="搜索 CLI"
|
||||||
|
// 搜索功能属于后续波次,Wave 0 仅为占位
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{warningCount > 0 && (
|
||||||
|
<button type="button" className="env-chip">
|
||||||
|
<span className="status-dot warn breathe" aria-hidden="true" />
|
||||||
|
<span>{warningCount} 项环境警告</span>
|
||||||
|
<ChevronDown size={12} strokeWidth={1.5} aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
export type KpiTone = "primary" | "attention" | "accent" | "warning";
|
||||||
|
|
||||||
|
const TONE_COLOR: Record<KpiTone, string> = {
|
||||||
|
primary: "var(--ad-primary)",
|
||||||
|
attention: "var(--ad-attention)",
|
||||||
|
accent: "var(--ad-accent)",
|
||||||
|
warning: "var(--ad-warning)",
|
||||||
|
};
|
||||||
|
|
||||||
|
interface KpiCardProps {
|
||||||
|
label: string;
|
||||||
|
value: number;
|
||||||
|
tone: KpiTone;
|
||||||
|
/** 副标题:对象点名句式(视觉规范 §3.1.1) */
|
||||||
|
subtitle: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* KPI 卡(视觉规范 §3.1.1):
|
||||||
|
* - 径向渐变玻璃板 + 上缘高光 + --ad-shadow-panel
|
||||||
|
* - 顶部色条贯通整宽;数值为 0 时色条降灰、数字回 --ad-text-1
|
||||||
|
* - 数字随状态色贯穿
|
||||||
|
*/
|
||||||
|
export function KpiCard({ label, value, tone, subtitle }: KpiCardProps) {
|
||||||
|
const color = value > 0 ? TONE_COLOR[tone] : "var(--ad-border)";
|
||||||
|
const valueColor = value > 0 ? TONE_COLOR[tone] : "var(--ad-text-1)";
|
||||||
|
return (
|
||||||
|
<div className="kpi-card">
|
||||||
|
<div className="kpi-bar" style={{ background: color }} aria-hidden="true" />
|
||||||
|
<div className="kpi-body">
|
||||||
|
<div className="kpi-value" style={{ color: valueColor }}>
|
||||||
|
{value}
|
||||||
|
</div>
|
||||||
|
<div className="kpi-label">{label}</div>
|
||||||
|
<div className="kpi-subtitle">{subtitle}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
|
/** 内联等宽 chip(视觉规范 §2.2:文件名/版本号/命令专用) */
|
||||||
|
export function MonoChip({ children }: { children: ReactNode }) {
|
||||||
|
return <span className="mono-chip">{children}</span>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import {
|
||||||
|
Archive,
|
||||||
|
Gauge,
|
||||||
|
Library,
|
||||||
|
Settings,
|
||||||
|
SlidersHorizontal,
|
||||||
|
SquareTerminal,
|
||||||
|
} from "lucide-react";
|
||||||
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
|
||||||
|
export type PageKey =
|
||||||
|
| "overview"
|
||||||
|
| "catalog"
|
||||||
|
| "my-cli"
|
||||||
|
| "config"
|
||||||
|
| "backup"
|
||||||
|
| "settings";
|
||||||
|
|
||||||
|
export interface PageMeta {
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PAGE_META: Record<PageKey, PageMeta> = {
|
||||||
|
overview: { title: "总览" },
|
||||||
|
catalog: { title: "CLI 目录" },
|
||||||
|
"my-cli": { title: "我的 CLI" },
|
||||||
|
config: { title: "配置中心" },
|
||||||
|
backup: { title: "备份与迁移" },
|
||||||
|
settings: { title: "设置" },
|
||||||
|
};
|
||||||
|
|
||||||
|
interface NavItem {
|
||||||
|
key: PageKey;
|
||||||
|
label: string;
|
||||||
|
icon: LucideIcon;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NAV_ITEMS: NavItem[] = [
|
||||||
|
{ key: "overview", label: "总览", icon: Gauge },
|
||||||
|
{ key: "catalog", label: "CLI 目录", icon: Library },
|
||||||
|
{ key: "my-cli", label: "我的 CLI", icon: SquareTerminal },
|
||||||
|
{ key: "config", label: "配置中心", icon: SlidersHorizontal },
|
||||||
|
{ key: "backup", label: "备份与迁移", icon: Archive },
|
||||||
|
{ key: "settings", label: "设置", icon: Settings },
|
||||||
|
];
|
||||||
|
|
||||||
|
const ENGINE_VERSION = "v0.1.0";
|
||||||
|
|
||||||
|
interface SidebarProps {
|
||||||
|
current: PageKey;
|
||||||
|
onNavigate: (page: PageKey) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 左侧竖导航 + 底部引擎状态区(视觉规范 §3.0) */
|
||||||
|
export function Sidebar({ current, onNavigate }: SidebarProps) {
|
||||||
|
return (
|
||||||
|
<aside className="sidebar">
|
||||||
|
<nav className="sidebar-nav" aria-label="主导航">
|
||||||
|
{NAV_ITEMS.map(({ key, label, icon: Icon }) => (
|
||||||
|
<button
|
||||||
|
key={key}
|
||||||
|
type="button"
|
||||||
|
className={current === key ? "nav-item active" : "nav-item"}
|
||||||
|
onClick={() => onNavigate(key)}
|
||||||
|
aria-current={current === key ? "page" : undefined}
|
||||||
|
>
|
||||||
|
<Icon size={20} strokeWidth={1.5} className="nav-icon" aria-hidden="true" />
|
||||||
|
<span className="nav-label">{label}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
<div className="sidebar-engine">
|
||||||
|
<span className="status-dot ok" aria-hidden="true" />
|
||||||
|
<span className="engine-text">本地引擎 · 运行中</span>
|
||||||
|
<span className="mono-chip engine-version">{ENGINE_VERSION}</span>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
export type BadgeKind = "installed" | "update" | "auth" | "uninstalled";
|
||||||
|
|
||||||
|
const KIND_CLASS: Record<BadgeKind, string> = {
|
||||||
|
installed: "badge badge-installed",
|
||||||
|
update: "badge badge-update",
|
||||||
|
auth: "badge badge-auth",
|
||||||
|
uninstalled: "badge badge-uninstalled",
|
||||||
|
};
|
||||||
|
|
||||||
|
interface StatusBadgeProps {
|
||||||
|
kind: BadgeKind;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 状态角标(视觉规范 §3.1.4 / §3.2:已安装=青、可更新=品红+弱辉光、待授权=紫、未安装=灰) */
|
||||||
|
export function StatusBadge({ kind, label }: StatusBadgeProps) {
|
||||||
|
return <span className={KIND_CLASS[kind]}>{label}</span>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { listCatalog } from "../ipc";
|
||||||
|
import type { CatalogEntry } from "../ipc/types";
|
||||||
|
|
||||||
|
export interface UseCatalogResult {
|
||||||
|
entries: CatalogEntry[];
|
||||||
|
loading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 加载适配器目录(listCatalog) */
|
||||||
|
export function useCatalog(): UseCatalogResult {
|
||||||
|
const [entries, setEntries] = useState<CatalogEntry[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
listCatalog()
|
||||||
|
.then((list) => {
|
||||||
|
if (!cancelled) {
|
||||||
|
setEntries(list);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err: unknown) => {
|
||||||
|
if (!cancelled) {
|
||||||
|
setError(String(err));
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { entries, loading, error };
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { detectEnv } from "../ipc";
|
||||||
|
import type { PlatformEnv } from "../ipc/types";
|
||||||
|
|
||||||
|
export interface UseEnvResult {
|
||||||
|
env: PlatformEnv | null;
|
||||||
|
loading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 加载本机环境检测结果(一次性的只读快照) */
|
||||||
|
export function useEnv(): UseEnvResult {
|
||||||
|
const [env, setEnv] = useState<PlatformEnv | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
detectEnv()
|
||||||
|
.then((e) => {
|
||||||
|
if (!cancelled) {
|
||||||
|
setEnv(e);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err: unknown) => {
|
||||||
|
if (!cancelled) {
|
||||||
|
setError(String(err));
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { env, loading, error };
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
// 类型化 invoke 封装(唯一前端 → Rust 入口,架构 §2「界面层禁直接拼命令」)
|
||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
import type { CatalogEntry, PlatformEnv } from "./types";
|
||||||
|
|
||||||
|
/** 是否运行在 Tauri 环境(否则走 mock,便于纯浏览器联调 UI) */
|
||||||
|
function isTauri(): boolean {
|
||||||
|
return typeof window !== "undefined" && "__TAURI_INTERNALS__" in window;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function detectEnv(): Promise<PlatformEnv> {
|
||||||
|
if (!isTauri()) {
|
||||||
|
return mockPlatformEnv();
|
||||||
|
}
|
||||||
|
return invoke<PlatformEnv>("detectEnv");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listCatalog(): Promise<CatalogEntry[]> {
|
||||||
|
if (!isTauri()) {
|
||||||
|
return mockCatalog();
|
||||||
|
}
|
||||||
|
return invoke<CatalogEntry[]>("listCatalog");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 浏览器联调用 mock(仅当不在 Tauri 内时生效,不影响真实数据) ----
|
||||||
|
const mockCatalog = (): CatalogEntry[] => [
|
||||||
|
{ id: "codex", name: "Codex CLI", name_zh: "Codex CLI", vendor: "OpenAI", status: "available" },
|
||||||
|
{ id: "claude-code", name: "Claude Code", name_zh: "Claude Code", vendor: "Anthropic", status: "available" },
|
||||||
|
{ id: "gemini", name: "Gemini CLI", name_zh: "Gemini CLI", vendor: "Google", status: "available" },
|
||||||
|
{ id: "copilot", name: "Copilot CLI", name_zh: "Copilot CLI", vendor: "GitHub", status: "available" },
|
||||||
|
{ id: "kimi", name: "Kimi CLI", name_zh: "Kimi CLI", vendor: "月之暗面", status: "available" },
|
||||||
|
{ id: "qwen", name: "Qwen Code", name_zh: "Qwen Code", vendor: "阿里巴巴", status: "available" },
|
||||||
|
{ id: "codebuddy", name: "CodeBuddy Code", name_zh: "CodeBuddy Code", vendor: "腾讯", status: "available" },
|
||||||
|
{ id: "opencode", name: "OpenCode", name_zh: "OpenCode", vendor: "opencode.ai", status: "available" },
|
||||||
|
{ id: "crush", name: "Crush", name_zh: "Crush", vendor: "Charm", status: "available" },
|
||||||
|
{ id: "goose", name: "Goose", name_zh: "Goose", vendor: "Linux 基金会(AAIF)", status: "available" },
|
||||||
|
{ id: "aider", name: "Aider", name_zh: "Aider", vendor: "aider.chat", status: "available" },
|
||||||
|
{ id: "cursor", name: "Cursor CLI", name_zh: "Cursor CLI", vendor: "Cursor", status: "available" },
|
||||||
|
{ id: "cline", name: "Cline", name_zh: "Cline", vendor: "Cline", status: "available" },
|
||||||
|
{ id: "warp", name: "Warp Agent CLI", name_zh: "Warp Agent CLI", vendor: "Warp", status: "available" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const mockPlatformEnv = (): PlatformEnv => ({
|
||||||
|
os: "windows",
|
||||||
|
os_version: "Windows 11 24H2 (Build 26100)",
|
||||||
|
arch: "x86_64",
|
||||||
|
shells: { powershell_version: "5.1.26100.1", pwsh_version: null, bash_available: true },
|
||||||
|
runtimes: {
|
||||||
|
node: { status: "installed", version: "24.18.0", path: "C:\\Program Files\\nodejs\\node.exe" },
|
||||||
|
npm: { status: "installed", version: "11.16.0", path: "C:\\Program Files\\nodejs\\npm.cmd" },
|
||||||
|
python: { status: "installed", version: "3.14.6", path: "C:\\Python314\\python.exe" },
|
||||||
|
uv: { status: "not_in_path", version: null, path: null },
|
||||||
|
git: { status: "installed", version: "2.47.1", path: "C:\\Program Files\\Git\\cmd\\git.exe" },
|
||||||
|
winget: { status: "installed", version: "1.8.1911", path: "C:\\Users\\MoBai\\AppData\\Local\\Microsoft\\WindowsApps\\winget.exe" },
|
||||||
|
apt: { status: "not_in_path", version: null, path: null },
|
||||||
|
},
|
||||||
|
path_entries: ["C:\\Windows\\system32", "C:\\Windows", "C:\\Program Files\\nodejs"],
|
||||||
|
capabilities: { keyring: "ok", can_elevate: false },
|
||||||
|
});
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
// IPC 契约类型(对齐架构 §5 PlatformEnv / listCatalog)
|
||||||
|
// 字段命名与 Rust 侧 serde 输出一致(snake_case)。
|
||||||
|
|
||||||
|
export interface PlatformEnv {
|
||||||
|
os: string;
|
||||||
|
os_version: string;
|
||||||
|
arch: string;
|
||||||
|
shells: Shells;
|
||||||
|
runtimes: Runtimes;
|
||||||
|
path_entries: string[];
|
||||||
|
capabilities: Capabilities;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Shells {
|
||||||
|
powershell_version: string | null;
|
||||||
|
pwsh_version: string | null;
|
||||||
|
bash_available: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Runtimes {
|
||||||
|
node: RuntimeInfo | null;
|
||||||
|
npm: RuntimeInfo | null;
|
||||||
|
python: RuntimeInfo | null;
|
||||||
|
uv: RuntimeInfo | null;
|
||||||
|
git: RuntimeInfo | null;
|
||||||
|
winget: RuntimeInfo | null;
|
||||||
|
apt: RuntimeInfo | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RuntimeStatus =
|
||||||
|
| "installed"
|
||||||
|
| "not_installed"
|
||||||
|
| "not_in_path"
|
||||||
|
| "exec_failed"
|
||||||
|
| "version_unparseable";
|
||||||
|
|
||||||
|
export interface RuntimeInfo {
|
||||||
|
status: RuntimeStatus;
|
||||||
|
version: string | null;
|
||||||
|
path: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Capabilities {
|
||||||
|
keyring: string;
|
||||||
|
can_elevate: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CatalogEntry {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
name_zh: string;
|
||||||
|
vendor: string;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom/client";
|
||||||
|
import App from "./App";
|
||||||
|
|
||||||
|
// 设计 Token(视觉规范 v1.2,唯一品味依据)
|
||||||
|
import "./tokens/color.css";
|
||||||
|
import "./tokens/typography.css";
|
||||||
|
import "./tokens/space.css";
|
||||||
|
import "./tokens/motion.css";
|
||||||
|
import "./tokens/effects.css";
|
||||||
|
import "./tokens/tiers.css";
|
||||||
|
import "./styles/global.css";
|
||||||
|
|
||||||
|
/** 性能档位自动判定(架构 §8.3 / 视觉规范 §5) */
|
||||||
|
function applyFxTier(): void {
|
||||||
|
const html = document.documentElement;
|
||||||
|
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||||
|
const cores = navigator.hardwareConcurrency ?? 8;
|
||||||
|
if (reduce || cores <= 4) {
|
||||||
|
html.dataset.fxTier = "low";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
applyFxTier();
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>,
|
||||||
|
);
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { Archive } from "lucide-react";
|
||||||
|
|
||||||
|
/** 备份与迁移(PRD §7):创建备份、查看内容、恢复。Wave 0 占位。 */
|
||||||
|
export function BackupPage() {
|
||||||
|
return (
|
||||||
|
<div className="placeholder-page">
|
||||||
|
<div className="placeholder-icon">
|
||||||
|
<Archive size={40} strokeWidth={1.5} aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
<h2 className="placeholder-title">备份与迁移</h2>
|
||||||
|
<p className="placeholder-desc">
|
||||||
|
备份功能在 Wave 4 落地。备份将明确标注是否包含敏感信息,密钥永不进备份。
|
||||||
|
</p>
|
||||||
|
<button type="button" className="btn btn-secondary" disabled>
|
||||||
|
创建备份
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import { Search } from "lucide-react";
|
||||||
|
import { useCatalog } from "../hooks/useCatalog";
|
||||||
|
import { StatusBadge } from "../components/StatusBadge";
|
||||||
|
import { MonoChip } from "../components/MonoChip";
|
||||||
|
import { CliMonogram } from "../components/CliMonogram";
|
||||||
|
|
||||||
|
type StatusFilter = "all" | "installed" | "uninstalled" | "update";
|
||||||
|
type PlatformFilter = "all" | "windows" | "linux";
|
||||||
|
|
||||||
|
const STATUS_FILTERS: { key: StatusFilter; label: string }[] = [
|
||||||
|
{ key: "all", label: "全部" },
|
||||||
|
{ key: "installed", label: "已安装" },
|
||||||
|
{ key: "uninstalled", label: "未安装" },
|
||||||
|
{ key: "update", label: "可更新" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const PLATFORM_FILTERS: { key: PlatformFilter; label: string }[] = [
|
||||||
|
{ key: "all", label: "全部平台" },
|
||||||
|
{ key: "windows", label: "Windows" },
|
||||||
|
{ key: "linux", label: "Linux" },
|
||||||
|
];
|
||||||
|
|
||||||
|
/** CLI 目录页(视觉规范 §3.2 + PRD §7):搜索 + 筛选 + 14 张卡片(数据来自 catalog.yaml) */
|
||||||
|
export function CatalogPage() {
|
||||||
|
const { entries, loading, error } = useCatalog();
|
||||||
|
const [query, setQuery] = useState("");
|
||||||
|
const [status, setStatus] = useState<StatusFilter>("all");
|
||||||
|
const [platform, setPlatform] = useState<PlatformFilter>("all");
|
||||||
|
|
||||||
|
const filtered = useMemo(() => {
|
||||||
|
const q = query.trim().toLowerCase();
|
||||||
|
return entries.filter((e) => {
|
||||||
|
if (q) {
|
||||||
|
const haystack = `${e.name_zh} ${e.name} ${e.vendor} ${e.id}`.toLowerCase();
|
||||||
|
if (!haystack.includes(q)) return false;
|
||||||
|
}
|
||||||
|
// Wave 0:14 个均为未安装;已安装/可更新筛选结果为空(诚实占位)
|
||||||
|
if (status === "installed") return false;
|
||||||
|
if (status === "update") return false;
|
||||||
|
// 平台:全部 14 个均支持 Windows + Ubuntu(调研底稿结论),筛选不改变集合
|
||||||
|
if (platform === "windows" || platform === "linux") return true;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}, [entries, query, status, platform]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="catalog">
|
||||||
|
<div className="catalog-toolbar">
|
||||||
|
<div className="search-box">
|
||||||
|
<Search size={16} strokeWidth={1.5} className="search-icon" aria-hidden="true" />
|
||||||
|
<input
|
||||||
|
type="search"
|
||||||
|
placeholder="搜索 CLI…"
|
||||||
|
value={query}
|
||||||
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
|
aria-label="搜索 CLI"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="filter-group" role="group" aria-label="状态筛选">
|
||||||
|
{STATUS_FILTERS.map((f) => (
|
||||||
|
<button
|
||||||
|
key={f.key}
|
||||||
|
type="button"
|
||||||
|
className={status === f.key ? "pill active" : "pill"}
|
||||||
|
onClick={() => setStatus(f.key)}
|
||||||
|
>
|
||||||
|
{f.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="filter-group" role="group" aria-label="平台筛选">
|
||||||
|
{PLATFORM_FILTERS.map((f) => (
|
||||||
|
<button
|
||||||
|
key={f.key}
|
||||||
|
type="button"
|
||||||
|
className={platform === f.key ? "pill active" : "pill"}
|
||||||
|
onClick={() => setPlatform(f.key)}
|
||||||
|
>
|
||||||
|
{f.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <p className="panel-empty">目录加载失败:{error}</p>}
|
||||||
|
{loading && !error && <p className="panel-empty">目录加载中…</p>}
|
||||||
|
|
||||||
|
<div className="catalog-grid">
|
||||||
|
{filtered.map((entry) => (
|
||||||
|
<article className="catalog-card" key={entry.id}>
|
||||||
|
<div className="catalog-card-top">
|
||||||
|
<CliMonogram name={entry.name} size={40} />
|
||||||
|
<div className="catalog-card-head">
|
||||||
|
<div className="catalog-card-name">{entry.name_zh}</div>
|
||||||
|
<div className="catalog-card-vendor">{entry.vendor}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="catalog-card-desc">官方渠道安装 · 适配器随 Wave 1 落地</p>
|
||||||
|
<div className="catalog-card-bottom">
|
||||||
|
<StatusBadge kind="uninstalled" label="未安装" />
|
||||||
|
<span className="catalog-platforms">Windows · Linux</span>
|
||||||
|
<MonoChip>v1</MonoChip>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
))}
|
||||||
|
{!loading && filtered.length === 0 && (
|
||||||
|
<p className="panel-empty catalog-empty">没有符合条件的 CLI</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { SlidersHorizontal } from "lucide-react";
|
||||||
|
|
||||||
|
/** 配置中心(PRD §7):按 CLI 展示中文配置表单。Wave 0 占位。 */
|
||||||
|
export function ConfigCenterPage() {
|
||||||
|
return (
|
||||||
|
<div className="placeholder-page">
|
||||||
|
<div className="placeholder-icon">
|
||||||
|
<SlidersHorizontal size={40} strokeWidth={1.5} aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
<h2 className="placeholder-title">配置中心</h2>
|
||||||
|
<p className="placeholder-desc">
|
||||||
|
中文配置表单在 Wave 2 落地。敏感字段将使用密码输入与系统密钥库。
|
||||||
|
</p>
|
||||||
|
<button type="button" className="btn btn-secondary" disabled>
|
||||||
|
选择 CLI 开始配置
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { SquareTerminal } from "lucide-react";
|
||||||
|
|
||||||
|
/** 我的 CLI(PRD §7):版本 / 路径 / 授权状态。Wave 0 尚无已安装 CLI,展示空态。 */
|
||||||
|
export function MyCliPage() {
|
||||||
|
return (
|
||||||
|
<div className="placeholder-page">
|
||||||
|
<div className="placeholder-icon">
|
||||||
|
<SquareTerminal size={40} strokeWidth={1.5} aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
<h2 className="placeholder-title">暂无已安装 CLI</h2>
|
||||||
|
<p className="placeholder-desc">
|
||||||
|
安装功能在 Wave 1 开放。届时这里会展示每个 CLI 的版本、路径与授权状态。
|
||||||
|
</p>
|
||||||
|
<button type="button" className="btn btn-primary" disabled>
|
||||||
|
去 CLI 目录看看
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||