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>
|
||||
);
|
||||
}
|
||||