chore: wave-0 baseline (Tauri2+React shell, platform detect, fake catalog)

This commit is contained in:
AgentDock 施工员
2026-08-25 00:21:13 +08:00
commit 296843215f
137 changed files with 6696 additions and 0 deletions
+22
View File
@@ -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
+26
View File
@@ -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
+51
View File
@@ -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。
+33
View File
@@ -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
+33
View File
@@ -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=观察中(第二批)"
}
}
}
+6
View File
@@ -0,0 +1,6 @@
# aider 适配器占位(Wave 0
id: aider
name: Aider
name_zh: Aider
vendor: aider.chat
status: available
+6
View File
@@ -0,0 +1,6 @@
# claude-code 适配器占位(Wave 0
id: claude-code
name: Claude Code
name_zh: Claude Code
vendor: Anthropic
status: available
+6
View File
@@ -0,0 +1,6 @@
# cline 适配器占位(Wave 0
id: cline
name: Cline
name_zh: Cline
vendor: Cline
status: available
+6
View File
@@ -0,0 +1,6 @@
# codebuddy 适配器占位(Wave 0
id: codebuddy
name: CodeBuddy Code
name_zh: CodeBuddy Code
vendor: 腾讯
status: available
+6
View File
@@ -0,0 +1,6 @@
# codex 适配器占位(Wave 0
id: codex
name: Codex CLI
name_zh: Codex CLI
vendor: OpenAI
status: available
+6
View File
@@ -0,0 +1,6 @@
# copilot 适配器占位(Wave 0
id: copilot
name: Copilot CLI
name_zh: Copilot CLI
vendor: GitHub
status: available
+6
View File
@@ -0,0 +1,6 @@
# crush 适配器占位(Wave 0
id: crush
name: Crush
name_zh: Crush
vendor: Charm
status: available
+6
View File
@@ -0,0 +1,6 @@
# cursor 适配器占位(Wave 0
id: cursor
name: Cursor CLI
name_zh: Cursor CLI
vendor: Cursor
status: available
+6
View File
@@ -0,0 +1,6 @@
# gemini 适配器占位(Wave 0
id: gemini
name: Gemini CLI
name_zh: Gemini CLI
vendor: Google
status: available
+6
View File
@@ -0,0 +1,6 @@
# goose 适配器占位(Wave 0
id: goose
name: Goose
name_zh: Goose
vendor: Linux 基金会(AAIF
status: available
+6
View File
@@ -0,0 +1,6 @@
# kimi 适配器占位(Wave 0
id: kimi
name: Kimi CLI
name_zh: Kimi CLI
vendor: 月之暗面
status: available
+6
View File
@@ -0,0 +1,6 @@
# opencode 适配器占位(Wave 0
id: opencode
name: OpenCode
name_zh: OpenCode
vendor: opencode.ai
status: available
+6
View File
@@ -0,0 +1,6 @@
# qwen 适配器占位(Wave 0
id: qwen
name: Qwen Code
name_zh: Qwen Code
vendor: 阿里巴巴
status: available
+6
View File
@@ -0,0 +1,6 @@
# warp 适配器占位(Wave 0
id: warp
name: Warp Agent CLI
name_zh: Warp Agent CLI
vendor: Warp
status: available
+12
View File
@@ -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>
+2137
View File
File diff suppressed because it is too large Load Diff
+26
View File
@@ -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"
}
}
+21
View File
@@ -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" }
+3
View File
@@ -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"]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 887 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

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>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

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>
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 622 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 973 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

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;
+42
View File
@@ -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");
}
+6
View File
@@ -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()
}
+37
View File
@@ -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"
]
}
}
+65
View File
@@ -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 不看 aptLinux 不看 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>
);
}
+34
View File
@@ -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>
);
}
+39
View File
@@ -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>
);
}
+6
View File
@@ -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>;
}
+79
View File
@@ -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>;
}
+38
View File
@@ -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 };
}
+38
View File
@@ -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 };
}
+58
View File
@@ -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 },
});
+54
View File
@@ -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;
}
+30
View File
@@ -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>,
);
+19
View File
@@ -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>
);
}
+113
View File
@@ -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>
);
}
+19
View File
@@ -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>
);
}
+19
View File
@@ -0,0 +1,19 @@
import { SquareTerminal } from "lucide-react";
/** 我的 CLIPRD §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>
);
}

Some files were not shown because too many files have changed in this diff Show More