3050 lines
105 KiB
Markdown
3050 lines
105 KiB
Markdown
# DevPack 后续开发详细计划
|
||
|
||
> 基于 ROADMAP.md 路线图,细化到每个文件及其所需第三方库 / 标准库
|
||
> 更新日期:2026-03-03
|
||
|
||
---
|
||
|
||
## 依赖库总览
|
||
|
||
下表列出后续开发中需要引入的**所有第三方库**(go get 命令):
|
||
|
||
| 库 | Import Path | 用途 | 引入阶段 |
|
||
|---|---|---|---|
|
||
| zerolog | `github.com/rs/zerolog` | 结构化日志 | M1 |
|
||
| go-yaml | `gopkg.in/yaml.v3` | Profile YAML 解析 | M1 |
|
||
| uuid | `github.com/google/uuid` | Pack ID 生成 | M1 |
|
||
| color | `github.com/fatih/color` | 终端彩色输出 | M1 |
|
||
| tablewriter | `github.com/olekukonenko/tablewriter` | 终端表格渲染 | M1 |
|
||
| progressbar | `github.com/schollz/progressbar/v3` | 终端进度条 | M3 |
|
||
| survey | `github.com/AlecAivazis/survey/v2` | 交互式终端提示 | M4 |
|
||
| testify | `github.com/stretchr/testify` | 测试断言与 Mock | M1 |
|
||
| archiver | `github.com/mholt/archiver/v4` | tar.gz 高级归档(可选,也可用标准库) | M3 |
|
||
| x/crypto | `golang.org/x/crypto` | Argon2id 密钥派生 | v0.3.0 |
|
||
|
||
> 注意:`cobra` 和 `viper` 已在 go.mod 中,无需再添加。
|
||
|
||
---
|
||
|
||
## M1: 项目基础架构(第 1-2 周)
|
||
|
||
### 任务 1.1 — 日志系统 ✅ 已完成
|
||
|
||
**新建文件:**
|
||
|
||
| 文件 | 说明 |
|
||
|---|---|
|
||
| `internal/logging/logger.go` | 全局 Logger 封装、日志级别控制、文件 + 控制台双输出 |
|
||
| `internal/logging/logger_test.go` | 日志系统单元测试 |
|
||
|
||
**所需库:**
|
||
|
||
```
|
||
第三方库:
|
||
github.com/rs/zerolog — 核心日志引擎
|
||
|
||
标准库:
|
||
os — 日志文件创建
|
||
io — MultiWriter (控制台+文件)
|
||
time — 时间戳格式
|
||
fmt — 格式化
|
||
path/filepath — 日志文件路径
|
||
```
|
||
|
||
**实现要点:**
|
||
- 导出全局 `Logger` 实例
|
||
- 支持 `--verbose` / `--quiet` / `--log-level` 全局标志
|
||
- 支持 `--log-file` 输出到文件
|
||
- 集成到 `cmd/devpack/commands/root.go` 的 `PersistentPreRun`
|
||
|
||
**需修改的现有文件:**
|
||
|
||
| 文件 | 改动 |
|
||
|---|---|
|
||
| `cmd/devpack/commands/root.go` | 在 `PersistentPreRunE` 中初始化 Logger,传递 verbose/quiet/log-level/log-file 标志 |
|
||
| `go.mod` | 添加 `github.com/rs/zerolog` 依赖 |
|
||
|
||
**✅ 实际完成情况:**
|
||
|
||
已实现完整的日志系统,核心结构如下:
|
||
|
||
- `Logger` 结构体:封装 `zerolog.Logger`,支持控制台彩色输出 + JSON 文件输出双通道
|
||
- `Options` 配置结构体:`Level`、`LogFile`、`Verbose`、`Quiet`、`NoColor`
|
||
- 全局单例模式:`Init(opts Options)` 初始化 + `G()` 获取全局实例
|
||
- 完整日志级别:`Trace/Debug/Info/Warn/Error/Fatal` 及 `Xxxf` 格式化变体
|
||
- 结构化日志方法:`WithField(key, val)`、`WithError(err)`、`WithDuration(d)`
|
||
- `Close()` 方法:关闭日志文件句柄,释放资源
|
||
- 已通过 `root.go` 的 `PersistentPreRunE` 集成 CLI 标志
|
||
- 默认日志文件:`~/.devpack/logs/devpack.log`
|
||
- 测试覆盖:50 个单元测试全部通过
|
||
|
||
---
|
||
|
||
### 任务 1.2 — 配置系统 ✅ 已完成
|
||
|
||
**新建文件:**
|
||
|
||
| 文件 | 说明 |
|
||
|---|---|
|
||
| `internal/config/config.go` | 配置结构体定义 (AppConfig)、加载/保存/默认值逻辑 |
|
||
| `internal/config/config_test.go` | 配置系统单元测试 |
|
||
|
||
**所需库:**
|
||
|
||
```
|
||
第三方库:
|
||
github.com/spf13/viper — 配置文件读取(已有)
|
||
gopkg.in/yaml.v3 — YAML 序列化/反序列化
|
||
|
||
标准库:
|
||
os — 文件操作
|
||
path/filepath — 路径拼接
|
||
fmt — 格式化
|
||
```
|
||
|
||
**需修改的现有文件:**
|
||
|
||
| 文件 | 改动 |
|
||
|---|---|
|
||
| `internal/config/paths.go` | 增加 `ConfigFilePath()` 方法返回 config.yaml 路径 |
|
||
| `go.mod` | 添加 `gopkg.in/yaml.v3` 依赖 |
|
||
|
||
**✅ 实际完成情况:**
|
||
|
||
已实现完整的配置管理系统:
|
||
|
||
- `AppConfig` 结构体:包含 `LogLevel`、`LogFile`、`DefaultProfile` 及嵌套 `RestoreConfig`(`ConflictStrategy`、`CreateRestorePoint`、`DryRunFirst`)
|
||
- 所有字段使用 `yaml` 标签(而非 `mapstructure`)
|
||
- `DefaultAppConfig()` 提供合理默认值
|
||
- `LoadConfig(path)` 文件不存在时返回默认配置而非报错
|
||
- `SaveConfig(path, cfg)` 写入 YAML 格式的配置文件
|
||
- `Paths` 结构体:管理 `~/.devpack/` 下的目录结构(Config、Profiles、Packs、Logs、Temp)
|
||
- `DefaultPaths()` 使用 `os.UserHomeDir()` 解析家目录
|
||
- `EnsureDirs()` 递归创建所有必要目录
|
||
- `ConfigFilePath()` 支持 viper 覆盖
|
||
- 测试覆盖:12 个单元测试全部通过
|
||
|
||
---
|
||
|
||
### 任务 1.3 — Profile 系统核心
|
||
|
||
**新建文件:**
|
||
|
||
| 文件 | 说明 |
|
||
|---|---|
|
||
| `internal/profile/profile.go` | Profile 结构体、加载/保存/验证/列出/默认模板 |
|
||
| `internal/profile/profile_test.go` | Profile 单元测试 |
|
||
| `internal/profile/templates.go` | 预设模板: minimal / standard / full |
|
||
|
||
**所需库:**
|
||
|
||
```
|
||
第三方库:
|
||
gopkg.in/yaml.v3 — Profile YAML 解析与生成
|
||
|
||
标准库:
|
||
os — 文件操作
|
||
path/filepath — Profile 文件路径
|
||
fmt — 错误信息
|
||
strings — 字符串处理
|
||
```
|
||
|
||
**详细功能描述:**
|
||
|
||
#### 1.3.1 Profile YAML 结构定义
|
||
|
||
Profile 是 DevPack 的"打包规则定义文件",控制哪些采集器启用、如何过滤数据。完整结构如下:
|
||
|
||
```yaml
|
||
# ~/.devpack/profiles/standard.yaml
|
||
name: "standard"
|
||
description: "标准开发环境配置,包含常用运行时和编辑器"
|
||
version: 1 # Profile 格式版本号
|
||
|
||
# 采集器配置
|
||
collectors:
|
||
enabled: # 启用的采集器列表(空=全部启用)
|
||
- go
|
||
- node
|
||
- python
|
||
- vscode
|
||
- powershell
|
||
- scoop
|
||
- git
|
||
- env
|
||
disabled: # 显式禁用的采集器
|
||
- ssh # 敏感数据默认不采集
|
||
- font
|
||
|
||
# 按采集器的细粒度设置
|
||
settings:
|
||
go:
|
||
include_tools: true # 是否采集 GOPATH/bin 下的工具
|
||
tools_filter: # 只采集匹配的工具(glob 模式)
|
||
- "gopls"
|
||
- "dlv"
|
||
- "staticcheck"
|
||
node:
|
||
include_global_packages: true # 是否采集全局 npm 包
|
||
exclude_packages: # 排除的包名
|
||
- "npm" # npm 自身不需要采集
|
||
vscode:
|
||
include_settings: true # 采集 settings.json
|
||
include_keybindings: true # 采集 keybindings.json
|
||
include_snippets: true # 采集代码片段
|
||
exclude_extensions: # 排除的扩展 ID
|
||
- "ms-vscode.remote-*" # 远程相关扩展不迁移
|
||
env:
|
||
include_patterns: # 要采集的环境变量名模式
|
||
- "GOPATH"
|
||
- "GOROOT"
|
||
- "JAVA_HOME"
|
||
- "NODE_*"
|
||
- "PYTHON*"
|
||
exclude_patterns: # 排除的变量名模式
|
||
- "*SECRET*"
|
||
- "*TOKEN*"
|
||
- "*PASSWORD*"
|
||
- "*KEY*"
|
||
scoop:
|
||
include_buckets: true # 是否采集 bucket 列表
|
||
exclude_packages: [] # 排除的包名
|
||
git:
|
||
include_aliases: true # 采集 git 别名
|
||
skip_credentials: true # 跳过凭据信息
|
||
```
|
||
|
||
#### 1.3.2 Profile 结构体定义
|
||
|
||
```go
|
||
type Profile struct {
|
||
Name string `yaml:"name"`
|
||
Description string `yaml:"description"`
|
||
Version int `yaml:"version"`
|
||
Collectors CollectorConfig `yaml:"collectors"`
|
||
Settings map[string]Setting `yaml:"settings"` // key = 采集器名称
|
||
}
|
||
|
||
type CollectorConfig struct {
|
||
Enabled []string `yaml:"enabled"` // 空列表 = 启用全部
|
||
Disabled []string `yaml:"disabled"` // 显式禁用
|
||
}
|
||
|
||
type Setting struct {
|
||
Options map[string]interface{} `yaml:",inline"` // 各采集器自定义选项
|
||
}
|
||
```
|
||
|
||
#### 1.3.3 验证规则
|
||
|
||
| 规则 | 说明 | 错误码 |
|
||
|---|---|---|
|
||
| `name` 非空 | 必须是 1-64 字符,仅允许 `[a-zA-Z0-9_-]` | E1010 |
|
||
| `name` 唯一 | 同一 profiles 目录下不能重名 | E1011 |
|
||
| `enabled` 合法 | 列表中的名称必须是注册过的采集器名称 | E1012 |
|
||
| `disabled` 合法 | 同上 | E1012 |
|
||
| `enabled` 与 `disabled` 不冲突 | 同一采集器不能同时出现在两个列表中 | E1013 |
|
||
| `settings` 键存在 | settings 下的键必须对应已启用的采集器 | E1014 |
|
||
| `include/exclude` 模式合法 | glob 模式可被 `filepath.Match` 解析 | E1015 |
|
||
| `version` 兼容 | Profile 版本 ≤ 当前支持的最大版本 | E1016 |
|
||
|
||
#### 1.3.4 CRUD 操作详细流程
|
||
|
||
**Create(创建):**
|
||
1. 接收 name + template(minimal/standard/full)
|
||
2. 检查 name 合法性和唯一性
|
||
3. 从 `templates.go` 获取模板内容
|
||
4. 填入用户自定义字段
|
||
5. 运行 `Validate()` 验证
|
||
6. 写入 `~/.devpack/profiles/{name}.yaml`
|
||
7. 如果是第一个 Profile,自动设为默认
|
||
|
||
**Load(加载):**
|
||
1. 拼接路径 `~/.devpack/profiles/{name}.yaml`
|
||
2. 读取文件内容
|
||
3. YAML 反序列化到 `Profile` 结构体
|
||
4. 运行 `Validate()` 验证结构完整性
|
||
5. 返回 `*Profile` 或 error
|
||
|
||
**Save(保存):**
|
||
1. 运行 `Validate()` 验证
|
||
2. YAML 序列化
|
||
3. 原子写入(先写临时文件,再 rename,避免写入中途断电损坏)
|
||
|
||
**Delete(删除):**
|
||
1. 检查该 Profile 是否存在
|
||
2. 如果是当前默认 Profile,拒绝删除(或提示用户先切换默认)
|
||
3. 删除 `~/.devpack/profiles/{name}.yaml`
|
||
|
||
**List(列出):**
|
||
1. 扫描 `~/.devpack/profiles/` 目录
|
||
2. 读取每个 `.yaml` 文件的 name + description 字段
|
||
3. 返回 `[]ProfileMeta{Name, Description, Path, ModTime}`
|
||
|
||
#### 1.3.5 三种模板内容对比
|
||
|
||
| 项目 | minimal | standard | full |
|
||
|---|---|---|---|
|
||
| 运行时(Go/Node/Python) | ✅ | ✅ | ✅ |
|
||
| 包管理器(Scoop/Brew) | ✅ | ✅ | ✅ |
|
||
| 编辑器(VS Code) | ❌ | ✅ | ✅ |
|
||
| Shell(PowerShell/Bash) | ❌ | ✅ | ✅ |
|
||
| Git 配置 | ❌ | ✅ | ✅ |
|
||
| 环境变量 | ❌ | ❌ | ✅ |
|
||
| SSH/GPG 密钥 | ❌ | ❌ | ✅(加密) |
|
||
| 字体 | ❌ | ❌ | ✅ |
|
||
| 自定义脚本 | ❌ | ❌ | ✅ |
|
||
|
||
---
|
||
|
||
### 任务 1.4 — 平台适配层完善
|
||
|
||
**需修改的现有文件:**
|
||
|
||
| 文件 | 改动 | 所需库 |
|
||
|---|---|---|
|
||
| `internal/platform/platform.go` | 添加 `InstallFont()` 和 `RunAsAdmin()` 方法签名 | `os/exec` |
|
||
| `internal/platform/windows.go` | 实现 `InstallFont()` (注册表写入)、`RunAsAdmin()` (runas 动词)、完善 `SetEnvVar` (使用 `golang.org/x/sys/windows/registry`) | `os/exec`, `golang.org/x/sys/windows/registry` |
|
||
| `internal/platform/darwin.go` | 实现 `InstallFont()` (~/Library/Fonts)、`RunAsAdmin()` (osascript) | `os/exec`, `os` |
|
||
| `internal/platform/linux.go` | 实现 `InstallFont()` (~/.local/share/fonts)、`RunAsAdmin()` (pkexec/sudo) | `os/exec`, `os` |
|
||
|
||
**新建文件:**
|
||
|
||
| 文件 | 说明 |
|
||
|---|---|
|
||
| `internal/platform/platform_test.go` | 平台层单元测试 (使用 build tags 分平台) |
|
||
|
||
**额外库(仅 Windows 构建时需要):**
|
||
|
||
```
|
||
第三方库:
|
||
golang.org/x/sys/windows/registry — Windows 注册表操作 (设置环境变量、安装字体)
|
||
```
|
||
|
||
**详细功能描述:**
|
||
|
||
#### 1.4.1 Platform 接口扩展
|
||
|
||
在现有 `Platform` 接口中添加以下方法签名:
|
||
|
||
```go
|
||
type Platform interface {
|
||
// 现有方法
|
||
Name() string // 返回平台名称 "windows"/"darwin"/"linux"
|
||
SetEnvVar(key, value string) error
|
||
GetEnvVar(key string) string
|
||
|
||
// 新增方法
|
||
InstallFont(fontPath string) error // 安装字体到系统
|
||
RunAsAdmin(cmd string, args ...string) error // 以管理员权限执行命令
|
||
HomeDir() string // 返回用户家目录
|
||
IsAdmin() bool // 检查当前是否具有管理员权限
|
||
CommandExists(name string) bool // 检查命令是否可用
|
||
}
|
||
```
|
||
|
||
#### 1.4.2 InstallFont 各平台实现
|
||
|
||
**Windows:**
|
||
1. 复制字体文件到 `C:\Windows\Fonts\`(需要管理员权限)
|
||
2. 在注册表 `HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts` 添加条目
|
||
3. 注册表键名 = 字体显示名称 + " (TrueType)",值 = 文件名
|
||
4. 调用 `SendMessage(HWND_BROADCAST, WM_FONTCHANGE)` 通知其他程序
|
||
5. 如果非管理员,自动调用 `RunAsAdmin` 提权重试
|
||
|
||
**macOS:**
|
||
1. 复制字体文件到 `~/Library/Fonts/`(用户级,无需管理员)
|
||
2. 无需额外注册,macOS 自动识别该目录中的字体文件
|
||
3. 如需全局安装:复制到 `/Library/Fonts/`(需管理员权限)
|
||
|
||
**Linux:**
|
||
1. 复制字体文件到 `~/.local/share/fonts/`(用户级)
|
||
2. 执行 `fc-cache -f -v` 刷新字体缓存
|
||
3. 验证:`fc-list | grep <font-name>` 确认已安装
|
||
|
||
**边界情况处理:**
|
||
- 字体文件不存在或格式无效(仅支持 .ttf / .otf / .woff2)→ 返回明确错误
|
||
- 字体已安装(同名文件已存在)→ 跳过并记录日志
|
||
- 权限不足 → 尝试 `RunAsAdmin` 提权,失败则返回 `E5001` 错误
|
||
- 磁盘空间不足 → 预检查可用空间
|
||
|
||
#### 1.4.3 RunAsAdmin 各平台实现
|
||
|
||
**Windows:**
|
||
```go
|
||
// 使用 ShellExecute "runas" 动词
|
||
cmd := exec.Command("cmd", "/C", command)
|
||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||
HideWindow: true,
|
||
}
|
||
// 或者调用 ShellExecuteEx API 通过 "runas" 触发 UAC 弹窗
|
||
```
|
||
- 用户取消 UAC 弹窗 → 返回 `ErrUserCancelled` 错误
|
||
- 检测是否已有管理员权限 → 如果已有则直接执行
|
||
|
||
**macOS:**
|
||
```bash
|
||
osascript -e 'do shell script "<command>" with administrator privileges'
|
||
```
|
||
- 系统弹窗要求输入密码
|
||
- 用户取消 → osascript 退出码非零
|
||
|
||
**Linux:**
|
||
```bash
|
||
# 优先尝试 pkexec (图形化密码弹窗)
|
||
pkexec <command>
|
||
# 回退到 sudo (终端密码输入)
|
||
sudo <command>
|
||
```
|
||
- 检测 `pkexec` 是否可用,不可用则回退 `sudo`
|
||
- `sudo` 需要终端 TTY,如果没有 TTY 则报错
|
||
|
||
#### 1.4.4 SetEnvVar 增强
|
||
|
||
**Windows(永久设置):**
|
||
1. 打开注册表 `HKCU\Environment`
|
||
2. 设置 `REG_SZ` 或 `REG_EXPAND_SZ` 类型的键值
|
||
3. 调用 `SendMessageTimeout(HWND_BROADCAST, WM_SETTINGCHANGE, 0, "Environment")` 广播变更
|
||
4. PATH 变量特殊处理:追加而非覆盖,使用 `;` 分隔
|
||
|
||
**macOS / Linux(永久设置):**
|
||
1. 检测当前 Shell 类型(bash/zsh/fish)
|
||
2. 向对应 profile 文件追加 `export KEY="VALUE"` 行
|
||
3. Shell profile 文件优先级:
|
||
- Bash: `~/.bashrc` > `~/.bash_profile` > `~/.profile`
|
||
- Zsh: `~/.zshrc`
|
||
- Fish: `~/.config/fish/config.fish` (语法: `set -gx KEY VALUE`)
|
||
4. 检查是否已有同名 export 行,有则替换而非追加
|
||
5. PATH 变量:使用 `export PATH="$PATH:<value>"` 追加
|
||
|
||
---
|
||
|
||
### 任务 1.5 — init 命令实现 ✅ 已完成
|
||
|
||
**需修改的现有文件:**
|
||
|
||
| 文件 | 改动 | 所需库 |
|
||
|---|---|---|
|
||
| `cmd/devpack/commands/init.go` | 实现完整的 init 逻辑:创建目录、写入默认 config.yaml、创建默认 Profile、支持 `--template` / `--profile` / `--force` | `internal/config`, `internal/profile`, `internal/logging` |
|
||
|
||
**依赖的内部包:**
|
||
|
||
```
|
||
internal/config — DefaultPaths(), EnsureDirs()
|
||
internal/profile — 创建默认/模板 Profile
|
||
internal/logging — 日志记录
|
||
|
||
标准库:
|
||
os — 文件/目录创建
|
||
fmt — 用户输出
|
||
path/filepath — 路径
|
||
```
|
||
|
||
**✅ 实际完成情况:**
|
||
|
||
已实现完整的 init 命令:
|
||
|
||
- 使用 `config.DefaultPaths()` 获取目录结构,`EnsureDirs()` 创建所有目录
|
||
- 检测 `config.yaml` 是否已存在,已存在则提示 "已初始化过"
|
||
- 支持 `--force` 强制覆盖重新初始化
|
||
- `writeDefaultConfig()` 生成包含合理默认值的 config.yaml
|
||
- `writeDefaultProfile()` 支持三种模板 (`--template minimal|standard|full`)
|
||
- 所有路径使用 `filepath.Join` 拼接(解决 Windows `\` 问题)
|
||
- 目录位于用户家目录 `~/.devpack/`(通过 `os.UserHomeDir()` 解析)
|
||
|
||
---
|
||
|
||
### 任务 1.6 — 终端 UI 工具
|
||
|
||
**新建文件:**
|
||
|
||
| 文件 | 说明 |
|
||
|---|---|
|
||
| `internal/ui/printer.go` | 统一输出工具:Success/Error/Warn/Info 打印、Verbose 判断 |
|
||
| `internal/ui/table.go` | 表格渲染封装 (基于 tablewriter) |
|
||
| `internal/ui/spinner.go` | 加载动画封装 (用于扫描等操作) |
|
||
|
||
**所需库:**
|
||
|
||
```
|
||
第三方库:
|
||
github.com/fatih/color — 终端颜色 (绿色✓、红色✗、黄色⚠ 等)
|
||
github.com/olekukonenko/tablewriter — 终端表格
|
||
|
||
标准库:
|
||
fmt — 格式化输出
|
||
os — Stdout/Stderr
|
||
io — Writer 接口
|
||
strings — 字符串处理
|
||
time — 动画定时
|
||
sync — Spinner 并发控制
|
||
```
|
||
|
||
**需修改的现有文件:**
|
||
|
||
| 文件 | 改动 |
|
||
|---|---|
|
||
| `go.mod` | 添加 `github.com/fatih/color`、`github.com/olekukonenko/tablewriter` |
|
||
|
||
**详细功能描述:**
|
||
|
||
#### 1.6.1 Printer — 统一控制台输出
|
||
|
||
所有用户可见的输出都通过 Printer 统一管理,确保风格一致且受 `--quiet` / `--no-color` 标志控制。
|
||
|
||
**API 设计:**
|
||
|
||
```go
|
||
type Printer struct {
|
||
stdout io.Writer
|
||
stderr io.Writer
|
||
quiet bool // --quiet 模式:抑制 Info/Debug 输出
|
||
noColor bool // --no-color 模式:去除 ANSI 颜色码
|
||
verbose bool // --verbose 模式:显示 Debug 级别输出
|
||
}
|
||
|
||
func NewPrinter(opts PrinterOptions) *Printer
|
||
|
||
// 带图标的输出方法
|
||
func (p *Printer) Success(format string, a ...interface{}) // ✓ 绿色
|
||
func (p *Printer) Error(format string, a ...interface{}) // ✗ 红色 → stderr
|
||
func (p *Printer) Warn(format string, a ...interface{}) // ⚠ 黄色
|
||
func (p *Printer) Info(format string, a ...interface{}) // ℹ 蓝色(quiet 模式下被抑制)
|
||
func (p *Printer) Debug(format string, a ...interface{}) // • 灰色(仅 verbose 模式显示)
|
||
func (p *Printer) Plain(format string, a ...interface{}) // 无图标,纯文本输出
|
||
|
||
// 辅助方法
|
||
func (p *Printer) Newline() // 输出空行
|
||
func (p *Printer) Divider() // 输出 ──────── 分隔线
|
||
```
|
||
|
||
**输出示例:**
|
||
|
||
```
|
||
✓ 扫描完成,发现 7 个采集器
|
||
ℹ Go Runtime — 1.22.1 (3 tools)
|
||
ℹ Node.js — 20.11.0 (12 global packages)
|
||
⚠ Python — 未安装
|
||
✗ SSH Keys — 跳过 (需要 --encrypt 标志)
|
||
```
|
||
|
||
**输出路由规则:**
|
||
| 方法 | 输出目标 | quiet 模式 | verbose 要求 |
|
||
|---|---|---|---|
|
||
| Success | stdout | 正常输出 | 否 |
|
||
| Error | stderr | 正常输出 | 否 |
|
||
| Warn | stderr | 正常输出 | 否 |
|
||
| Info | stdout | 被抑制 | 否 |
|
||
| Debug | stdout | 被抑制 | 是 |
|
||
| Plain | stdout | 被抑制 | 否 |
|
||
|
||
#### 1.6.2 Table — 终端表格渲染
|
||
|
||
封装 `tablewriter` 库,提供简洁的表格构建 API,适配 DevPack 的各种数据展示需求。
|
||
|
||
**API 设计:**
|
||
|
||
```go
|
||
type Table struct {
|
||
headers []string
|
||
rows [][]string
|
||
options TableOptions
|
||
}
|
||
|
||
type TableOptions struct {
|
||
MaxColumnWidth int // 单列最大宽度,超出截断并加 "..."
|
||
MinColumnWidth int // 单列最小宽度
|
||
Border bool // 是否显示边框
|
||
HeaderColor bool // 表头是否着色(粗体+蓝色)
|
||
Alignment []int // 每列对齐方式 (LEFT/CENTER/RIGHT)
|
||
AutoWrap bool // 长文本是否自动换行
|
||
}
|
||
|
||
func NewTable(headers ...string) *Table
|
||
func (t *Table) SetOptions(opts TableOptions) *Table
|
||
func (t *Table) AddRow(values ...string) *Table
|
||
func (t *Table) AddRows(rows [][]string) *Table
|
||
func (t *Table) Render() string // 返回渲染后的字符串
|
||
func (t *Table) RenderTo(w io.Writer) // 直接输出到 Writer
|
||
```
|
||
|
||
**输出效果示例:**
|
||
|
||
```
|
||
┌────────────────────┬──────────┬─────────────┬──────────┐
|
||
│ Collector │ Category │ Items │ Size │
|
||
├────────────────────┼──────────┼─────────────┼──────────┤
|
||
│ Go Runtime │ runtime │ 3 tools │ 2.1 MB │
|
||
│ Node.js Runtime │ runtime │ 12 packages │ 1.5 MB │
|
||
│ VS Code │ editor │ 35 ext │ 4.2 MB │
|
||
│ PowerShell │ shell │ 5 modules │ 0.3 MB │
|
||
└────────────────────┴──────────┴─────────────┴──────────┘
|
||
```
|
||
|
||
#### 1.6.3 Spinner — 加载动画
|
||
|
||
用于扫描、打包、还原等耗时操作的等待提示。
|
||
|
||
**API 设计:**
|
||
|
||
```go
|
||
type Spinner struct {
|
||
message string
|
||
frames []rune // 动画帧序列
|
||
mu sync.Mutex
|
||
active bool
|
||
done chan struct{}
|
||
}
|
||
|
||
func NewSpinner(message string) *Spinner
|
||
func (s *Spinner) Start() // 启动动画(后台 goroutine)
|
||
func (s *Spinner) Update(message string) // 更新显示消息
|
||
func (s *Spinner) Stop() // 停止动画
|
||
func (s *Spinner) StopWithSuccess(message string) // 停止并显示 ✓ 成功消息
|
||
func (s *Spinner) StopWithError(message string) // 停止并显示 ✗ 失败消息
|
||
```
|
||
|
||
**行为规范:**
|
||
|
||
- 动画帧:`⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏`(Braille 点阵),每 100ms 切换一帧
|
||
- 线程安全:`Start/Stop/Update` 通过 `sync.Mutex` 保护
|
||
- 非 TTY 检测:CI 环境(无交互式终端)下不显示动画,只打印一次消息
|
||
- `--quiet` 模式:完全不显示 spinner
|
||
- `--no-color` 模式:不显示动画帧,只显示文本
|
||
|
||
**输出效果:**
|
||
|
||
```
|
||
⠹ 正在扫描环境...
|
||
↓ (100ms 后)
|
||
⠼ 正在扫描环境...
|
||
↓ (完成后)
|
||
✓ 扫描完成 (耗时 2.3s)
|
||
```
|
||
|
||
---
|
||
|
||
### 任务 1.7 — 错误处理系统
|
||
|
||
**新建文件:**
|
||
|
||
| 文件 | 说明 |
|
||
|---|---|
|
||
| `internal/errors/errors.go` | DevPackError 结构体、ErrorLevel 枚举、CollectorError、格式化输出 |
|
||
| `internal/errors/errors_test.go` | 错误处理单元测试 |
|
||
|
||
**所需库:**
|
||
|
||
```
|
||
标准库:
|
||
fmt — 错误格式化
|
||
errors — errors.Is / errors.As 兼容
|
||
strings — 消息拼接
|
||
```
|
||
|
||
**详细功能描述:**
|
||
|
||
#### 1.7.1 DevPackError 结构体
|
||
|
||
```go
|
||
type ErrorLevel int
|
||
|
||
const (
|
||
LevelWarning ErrorLevel = iota // 可继续执行的问题
|
||
LevelError // 需要处理但可恢复的错误
|
||
LevelFatal // 不可恢复的致命错误
|
||
)
|
||
|
||
type DevPackError struct {
|
||
Code string // 错误码,如 "E2001"
|
||
Message string // 用户可读的中文错误消息
|
||
Cause error // 被包装的原始错误
|
||
Level ErrorLevel // 错误级别
|
||
Suggestion string // 建议的修复操作
|
||
Context map[string]string // 附加上下文信息
|
||
}
|
||
|
||
// 实现 error 接口
|
||
func (e *DevPackError) Error() string
|
||
// 实现 errors.Unwrap
|
||
func (e *DevPackError) Unwrap() error
|
||
// 格式化输出(面向用户的友好格式)
|
||
func (e *DevPackError) Format() string
|
||
```
|
||
|
||
#### 1.7.2 错误码体系
|
||
|
||
| 范围 | 类别 | 示例 |
|
||
|---|---|---|
|
||
| **E1xxx** | 配置错误 | E1001: 配置文件解析失败<br>E1002: Profile 格式无效<br>E1003: 路径不存在<br>E1010: Profile 名称非法<br>E1011: Profile 名称重复<br>E1012: 采集器名称未注册<br>E1013: enabled/disabled 冲突<br>E1016: Profile 版本不兼容 |
|
||
| **E2xxx** | 采集器错误 | E2001: 工具未安装(如 `go` 命令不存在)<br>E2002: 命令执行失败(退出码非零)<br>E2003: 输出解析错误(JSON 格式异常)<br>E2004: 操作超时(超过设定的 timeout)<br>E2005: 权限不足(需要管理员权限)<br>E2006: 版本不兼容 |
|
||
| **E3xxx** | 打包错误 | E3001: 归档创建失败<br>E3002: 校验和不匹配<br>E3003: Manifest 格式无效<br>E3004: 压缩失败<br>E3005: Pack 文件过大 |
|
||
| **E4xxx** | 还原错误 | E4001: Pack 文件不存在<br>E4002: 平台不匹配(Windows → macOS)<br>E4003: 冲突未解决<br>E4004: 安装失败<br>E4005: 回滚失败<br>E4006: 还原点创建失败 |
|
||
| **E5xxx** | 系统错误 | E5001: 权限不足<br>E5002: 磁盘空间不足<br>E5003: 网络连接失败<br>E5004: 文件系统错误<br>E5005: 用户取消操作 |
|
||
|
||
#### 1.7.3 辅助构造函数
|
||
|
||
```go
|
||
// 快速创建错误
|
||
func New(code, message string) *DevPackError
|
||
func Newf(code, format string, args ...interface{}) *DevPackError
|
||
func Wrap(cause error, code, message string) *DevPackError
|
||
|
||
// 特化构造函数(常用场景)
|
||
func CollectorNotFound(name string) *DevPackError
|
||
func CommandFailed(cmd string, err error) *DevPackError
|
||
func ParseError(format string, cause error) *DevPackError
|
||
func TimeoutError(operation string, duration time.Duration) *DevPackError
|
||
func PlatformMismatch(source, target string) *DevPackError
|
||
```
|
||
|
||
#### 1.7.4 用户可见的错误输出格式
|
||
|
||
```
|
||
✗ E2001: Go 运行时未安装
|
||
详情: exec: "go": executable file not found in %PATH%
|
||
建议: 请先安装 Go — https://go.dev/dl/
|
||
```
|
||
|
||
```
|
||
✗ E4002: 平台不匹配
|
||
详情: Pack 来源平台 "darwin/arm64",当前平台 "windows/amd64"
|
||
建议: DevPack 目前不支持跨操作系统还原,请在相同 OS 类型的机器上使用
|
||
```
|
||
|
||
#### 1.7.5 错误聚合器
|
||
|
||
用于并发扫描/还原等场景,收集多个采集器的错误:
|
||
|
||
```go
|
||
type ErrorCollector struct {
|
||
errors []*DevPackError
|
||
mu sync.Mutex
|
||
}
|
||
|
||
func (ec *ErrorCollector) Add(err *DevPackError)
|
||
func (ec *ErrorCollector) HasErrors() bool
|
||
func (ec *ErrorCollector) HasFatal() bool // 是否包含致命错误
|
||
func (ec *ErrorCollector) Errors() []*DevPackError
|
||
func (ec *ErrorCollector) Summary() string // "3 个错误,2 个警告"
|
||
```
|
||
|
||
---
|
||
|
||
## M2: 核心采集器(第 3-5 周)
|
||
|
||
### 任务 2.1 — Go 运行时采集器
|
||
|
||
**新建文件:**
|
||
|
||
| 文件 | 说明 |
|
||
|---|---|
|
||
| `internal/collector/runtime/go_collector.go` | Go 采集器: 扫描版本、GOPATH、GOROOT、go env、GOPATH/bin 下的工具 |
|
||
| `internal/collector/runtime/go_collector_test.go` | Go 采集器单元测试 |
|
||
|
||
**所需库:**
|
||
|
||
```
|
||
标准库:
|
||
os/exec — 执行 `go version`、`go env -json`、`go install`
|
||
encoding/json — 解析 `go env -json` 输出
|
||
context — 命令超时控制
|
||
path/filepath — GOPATH/bin 扫描
|
||
os — 文件遍历
|
||
strings — 输出解析
|
||
runtime — 获取当前 Go 信息
|
||
fmt — 格式化
|
||
```
|
||
|
||
**详细功能描述:**
|
||
|
||
#### 2.1.1 IsAvailable 检测逻辑
|
||
|
||
```
|
||
1. exec.LookPath("go") 检查 go 命令是否在 PATH 中
|
||
2. 如果找到,执行 `go version` 确认可用
|
||
3. 返回 true/false
|
||
```
|
||
|
||
#### 2.1.2 Scan 扫描数据结构
|
||
|
||
扫描结果写入 `ScanResult`,包含以下 `ScanItem` 条目:
|
||
|
||
| ScanItem.Name | Type | 数据来源 | Properties 示例 |
|
||
|---|---|---|---|
|
||
| `Go Runtime` | binary | `go version` | `{"version": "1.22.1", "os": "windows", "arch": "amd64"}` |
|
||
| `GOPATH` | config | `go env GOPATH` | `{"path": "/Users/dev/go"}` |
|
||
| `GOROOT` | config | `go env GOROOT` | `{"path": "/usr/local/go"}` |
|
||
| `Go Env (full)` | config | `go env -json` | 完整的 go env 输出(约 30 个键值对) |
|
||
| `gopls` | binary | GOPATH/bin 扫描 | `{"module": "golang.org/x/tools/gopls", "version": "v0.15.0"}` |
|
||
| `dlv` | binary | GOPATH/bin 扫描 | `{"module": "github.com/go-delve/delve/cmd/dlv"}` |
|
||
|
||
**go env -json 重要键值:**
|
||
- `GOPATH` / `GOROOT` / `GOBIN` / `GOCACHE` / `GOMODCACHE`
|
||
- `GOPROXY` / `GONOSUMDB` / `GONOPROXY`(代理设置对中国大陆用户尤为重要)
|
||
- `CGO_ENABLED` / `CC` / `CXX`
|
||
|
||
#### 2.1.3 GOPATH/bin 工具检测算法
|
||
|
||
```
|
||
1. 获取 GOPATH(go env GOPATH),默认 ~/go
|
||
2. 扫描 GOPATH/bin/ 目录(Windows 下为 GOPATH\bin\)
|
||
3. 对每个可执行文件:
|
||
a. 跳过名为 "go" 和 "gofmt" 的文件(运行时自带)
|
||
b. 尝试 `go version -m <binary>` 获取 module 路径和版本
|
||
c. 如果成功,记录 {name, module, version}
|
||
d. 如果失败(非 Go 编译的二进制),记录 {name, unknown}
|
||
4. 将工具列表按名称字母排序
|
||
```
|
||
|
||
#### 2.1.4 Capture 输出文件
|
||
|
||
在目标目录下生成两个文件:
|
||
|
||
**metadata.json:**
|
||
```json
|
||
{
|
||
"version": "1.22.1",
|
||
"os": "windows",
|
||
"arch": "amd64",
|
||
"env": {
|
||
"GOPATH": "C:\\Users\\dev\\go",
|
||
"GOROOT": "C:\\Program Files\\Go",
|
||
"GOPROXY": "https://goproxy.cn,direct",
|
||
"CGO_ENABLED": "1"
|
||
}
|
||
}
|
||
```
|
||
|
||
**tools.json:**
|
||
```json
|
||
[
|
||
{"name": "gopls", "module": "golang.org/x/tools/gopls", "version": "v0.15.3"},
|
||
{"name": "dlv", "module": "github.com/go-delve/delve/cmd/dlv", "version": "v1.22.1"},
|
||
{"name": "staticcheck", "module": "honnef.co/go/tools/cmd/staticcheck", "version": "v0.4.7"}
|
||
]
|
||
```
|
||
|
||
#### 2.1.5 Restore 还原流程
|
||
|
||
```
|
||
1. 检测当前 Go 版本
|
||
├── 未安装 → 记录 action=INSTALL
|
||
├── 版本相同 → 记录 action=SKIP
|
||
└── 版本不同 → 记录 action=UPGRADE(提示用户手动升级或使用包管理器)
|
||
2. 还原环境变量
|
||
├── 设置 GOPROXY(尤其是自定义代理)
|
||
├── 设置 GONOSUMDB / GONOPROXY(如有)
|
||
└── 不设置 GOPATH/GOROOT(使用目标机默认值)
|
||
3. 安装工具(并行,最多 4 个 goroutine)
|
||
├── 对每个 tool: `go install <module>@<version>`
|
||
├── 如果指定版本安装失败 → 尝试 `@latest`
|
||
└── 记录成功/失败状态
|
||
4. 生成还原报告
|
||
```
|
||
|
||
#### 2.1.6 边界情况
|
||
|
||
| 场景 | 处理方式 |
|
||
|---|---|
|
||
| Go 未安装 | `IsAvailable()` 返回 false,跳过此采集器 |
|
||
| GOPATH 未设置 | 使用默认值 `~/go` |
|
||
| GOPATH/bin 目录不存在 | 工具列表为空,不报错 |
|
||
| `go env -json` 执行超时 | 返回超时错误,使用默认的 30 秒超时 |
|
||
| GOPATH/bin 中有非 Go 二进制 | 记录为 `{type: "unknown"}`,不纳入还原范围 |
|
||
| 目标机 Go 版本低于源机 | 生成 UPGRADE 建议但不强制升级 |
|
||
| `go install` 网络失败 | 记录错误,继续安装下一个工具 |
|
||
|
||
---
|
||
|
||
### 任务 2.2 — Node.js 运行时采集器
|
||
|
||
**新建文件:**
|
||
|
||
| 文件 | 说明 |
|
||
|---|---|
|
||
| `internal/collector/runtime/node_collector.go` | Node.js 采集器: 扫描 node/npm/nvm 版本、全局包 |
|
||
| `internal/collector/runtime/node_collector_test.go` | 单元测试 |
|
||
|
||
**所需库:**
|
||
|
||
```
|
||
标准库:
|
||
os/exec — 执行 `node --version`、`npm --version`、`npm list -g --json`
|
||
encoding/json — 解析 npm list JSON
|
||
context — 超时
|
||
strings — 版本解析
|
||
fmt — 格式化
|
||
```
|
||
|
||
**详细功能描述:**
|
||
|
||
#### 2.2.1 扫描范围
|
||
|
||
| 检测项 | 命令 | 说明 |
|
||
|---|---|---|
|
||
| Node.js 版本 | `node --version` | 输出如 `v20.11.0` |
|
||
| npm 版本 | `npm --version` | 输出如 `10.2.4` |
|
||
| 全局包列表 | `npm list -g --json --depth=0` | JSON 格式的全局安装包 |
|
||
| yarn 版本 | `yarn --version` | 可选检测 |
|
||
| pnpm 版本 | `pnpm --version` | 可选检测 |
|
||
| nvm/fnm 检测 | `nvm version` / `fnm current` | 检测版本管理器 |
|
||
|
||
#### 2.2.2 npm 全局包 JSON 解析
|
||
|
||
`npm list -g --json --depth=0` 输出格式:
|
||
```json
|
||
{
|
||
"dependencies": {
|
||
"typescript": { "version": "5.3.3" },
|
||
"ts-node": { "version": "10.9.2" },
|
||
"nodemon": { "version": "3.0.2" },
|
||
"@angular/cli": { "version": "17.1.0" }
|
||
}
|
||
}
|
||
```
|
||
|
||
解析逻辑:
|
||
1. 反序列化为 `map[string]interface{}`
|
||
2. 遍历 `dependencies` 字典,提取 `name` 和 `version`
|
||
3. 过滤掉 `npm` 自身(npm 随 node 安装,不单独还原)
|
||
4. 按名称排序
|
||
|
||
#### 2.2.3 版本管理器检测
|
||
|
||
按优先级检测以下版本管理器:
|
||
|
||
| 管理器 | 检测命令 | 环境变量 |
|
||
|---|---|---|
|
||
| nvm (Windows) | `nvm version` | `NVM_HOME` |
|
||
| nvm (Unix) | `nvm --version` (shell function) | `NVM_DIR` |
|
||
| fnm | `fnm --version` | `FNM_DIR` |
|
||
| Volta | `volta --version` | `VOLTA_HOME` |
|
||
|
||
如果检测到版本管理器,还原时优先通过版本管理器安装 Node.js,而非直接下载安装包。
|
||
|
||
#### 2.2.4 Capture 输出文件
|
||
|
||
**metadata.json:**
|
||
```json
|
||
{
|
||
"node_version": "20.11.0",
|
||
"npm_version": "10.2.4",
|
||
"version_manager": "fnm",
|
||
"yarn_version": "1.22.21",
|
||
"pnpm_version": null
|
||
}
|
||
```
|
||
|
||
**global-packages.json:**
|
||
```json
|
||
[
|
||
{"name": "typescript", "version": "5.3.3"},
|
||
{"name": "ts-node", "version": "10.9.2"},
|
||
{"name": "nodemon", "version": "3.0.2"}
|
||
]
|
||
```
|
||
|
||
#### 2.2.5 Restore 还原策略
|
||
|
||
```
|
||
1. 检测 Node.js 是否已安装
|
||
├── 已安装且版本匹配 → SKIP
|
||
├── 已安装但版本不同 →
|
||
│ ├── 有版本管理器 → `fnm install <ver>; fnm use <ver>`
|
||
│ └── 无版本管理器 → 提示用户手动升级,记录 UPGRADE 建议
|
||
└── 未安装 →
|
||
├── 有版本管理器 → `fnm install <ver>`
|
||
└── 无版本管理器 → 提示用户安装(提供下载链接)
|
||
2. 安装全局包(串行,避免 npm 并发冲突)
|
||
├── `npm install -g <name>@<version>`
|
||
├── 安装失败 → 尝试不指定版本 `npm install -g <name>`
|
||
└── 记录每个包的安装结果
|
||
```
|
||
|
||
#### 2.2.6 边界情况
|
||
|
||
| 场景 | 处理方式 |
|
||
|---|---|
|
||
| Node.js 未安装 | `IsAvailable()` 返回 false |
|
||
| npm 损坏 | `npm list -g` 执行失败 → 返回错误信息 |
|
||
| 全局包为空 | 正常记录空列表 |
|
||
| 包版本在 npm 上已被移除 | 尝试 `@latest`,仍失败则记录警告 |
|
||
| nvm 安装了多个版本 | 只采集当前活跃版本 |
|
||
| yarn/pnpm 的全局包 | 分别执行 `yarn global list --json` / `pnpm list -g --json` |
|
||
|
||
---
|
||
|
||
### 任务 2.3 — Python 运行时采集器
|
||
|
||
**新建文件:**
|
||
|
||
| 文件 | 说明 |
|
||
|---|---|
|
||
| `internal/collector/runtime/python_collector.go` | Python 采集器: 版本、pip 包、virtualenv 配置 |
|
||
| `internal/collector/runtime/python_collector_test.go` | 单元测试 |
|
||
|
||
**所需库:**
|
||
|
||
```
|
||
标准库:
|
||
os/exec — 执行 `python --version`、`pip list --format=json`
|
||
encoding/json — 解析 pip list JSON
|
||
context — 超时
|
||
strings — 解析
|
||
fmt — 格式化
|
||
```
|
||
|
||
**详细功能描述:**
|
||
|
||
#### 2.3.1 Python 命令检测顺序
|
||
|
||
不同系统中 Python 的命令名不一致,按以下优先级尝试:
|
||
|
||
| 优先级 | 命令 | 说明 |
|
||
|---|---|---|
|
||
| 1 | `python3` | Linux/macOS 首选 |
|
||
| 2 | `python` | Windows 首选,或 Linux 上的别名 |
|
||
| 3 | `py -3` | Windows Python Launcher |
|
||
| 4 | `pyenv which python` | 通过 pyenv 管理的 Python |
|
||
| 5 | `conda info --json` | Conda 环境 |
|
||
|
||
#### 2.3.2 扫描范围
|
||
|
||
| 检测项 | 命令 | 说明 |
|
||
|---|---|---|
|
||
| Python 版本 | `python --version` | 输出如 `Python 3.12.2` |
|
||
| pip 版本 | `pip --version` | 输出如 `pip 24.0 from ...` |
|
||
| 全局 pip 包 | `pip list --format=json --user` | 用户级安装的包 |
|
||
| pyenv 检测 | `pyenv versions` | 检测 pyenv 及已安装版本 |
|
||
| conda 检测 | `conda info --json` | 检测 conda 环境 |
|
||
| virtualenv 检测 | 检查 `VIRTUAL_ENV` 环境变量 | 不采集虚拟环境内的包 |
|
||
|
||
#### 2.3.3 pip 包过滤规则
|
||
|
||
`pip list --format=json` 输出所有已安装包(可能 200+ 个),需要过滤:
|
||
|
||
```
|
||
默认排除:
|
||
- pip, setuptools, wheel(安装器自带)
|
||
- pkg_resources, _distutils_hack(内部工具包)
|
||
- 所有以 "_" 开头的包
|
||
|
||
Profile 过滤:
|
||
- settings.python.include_packages: [glob 模式]
|
||
- settings.python.exclude_packages: [glob 模式]
|
||
```
|
||
|
||
#### 2.3.4 Restore 还原策略
|
||
|
||
```
|
||
1. 检测 Python 是否已安装
|
||
├── 有 pyenv → `pyenv install <ver>; pyenv global <ver>`
|
||
├── 有 conda → `conda create -n devpack python=<ver>`
|
||
└── 都没有 → 提示用户手动安装
|
||
2. 安装 pip 包(串行,使用 --user 标志)
|
||
├── `pip install <name>==<version> --user`
|
||
├── 版本安装失败 → 尝试不指定版本
|
||
└── 某些包需要编译(如 numpy)→ 超时设为 5 分钟
|
||
```
|
||
|
||
#### 2.3.5 边界情况
|
||
|
||
| 场景 | 处理方式 |
|
||
|---|---|
|
||
| Python 2 和 3 共存 | 仅采集 Python 3 |
|
||
| 系统 Python(只读) | `pip list --user` 只列出用户安装的包 |
|
||
| pip 未安装 | 记录警告,跳过包列表采集 |
|
||
| conda 环境 | 使用 `conda list --json` 替代 pip list |
|
||
| 虚拟环境激活状态 | 检测 `VIRTUAL_ENV`,提醒用户虚拟环境内的包不被采集 |
|
||
| 包依赖冲突 | 还原时使用 `--no-deps` 避免依赖地狱 |
|
||
|
||
---
|
||
|
||
### 任务 2.4 — VS Code 编辑器采集器
|
||
|
||
**新建文件:**
|
||
|
||
| 文件 | 说明 |
|
||
|---|---|
|
||
| `internal/collector/editor/vscode_collector.go` | VS Code 采集器: 版本、扩展列表、settings.json、keybindings.json、代码片段 |
|
||
| `internal/collector/editor/vscode_collector_test.go` | 单元测试 |
|
||
|
||
**所需库:**
|
||
|
||
```
|
||
标准库:
|
||
os/exec — 执行 `code --version`、`code --list-extensions --show-versions`、`code --install-extension`
|
||
encoding/json — 解析/生成 settings.json
|
||
os — 读取 settings.json、keybindings.json 文件
|
||
path/filepath — VS Code 配置文件路径 (不同平台不同)
|
||
context — 超时
|
||
strings — 扩展列表解析
|
||
io — 文件复制
|
||
fmt — 格式化
|
||
```
|
||
|
||
**详细功能描述:**
|
||
|
||
#### 2.4.1 配置文件路径(各平台)
|
||
|
||
| 平台 | 用户配置目录 |
|
||
|---|---|
|
||
| Windows | `%APPDATA%\Code\User\` |
|
||
| macOS | `~/Library/Application Support/Code/User/` |
|
||
| Linux | `~/.config/Code/User/` |
|
||
|
||
Insiders 版本路径:将 `Code` 替换为 `Code - Insiders`
|
||
|
||
#### 2.4.2 扫描内容清单
|
||
|
||
| 采集项 | 方法 | 文件/命令 |
|
||
|---|---|---|
|
||
| VS Code 版本 | `code --version` | 输出三行:版本号/commit hash/架构 |
|
||
| 扩展列表 | `code --list-extensions --show-versions` | 每行 `publisher.name@version` |
|
||
| settings.json | 文件读取 | 用户设置(不含默认设置) |
|
||
| keybindings.json | 文件读取 | 自定义快捷键 |
|
||
| 代码片段 | 目录读取 | `snippets/` 目录下所有 `.json` 文件 |
|
||
|
||
#### 2.4.3 扩展列表处理
|
||
|
||
`code --list-extensions --show-versions` 输出示例:
|
||
```
|
||
ms-python.python@2024.0.1
|
||
golang.go@0.41.0
|
||
esbenp.prettier-vscode@10.1.0
|
||
```
|
||
|
||
解析逻辑:
|
||
1. 按行分割,每行格式 `<publisher>.<name>@<version>`
|
||
2. 过滤掉 Profile 中 `exclude_extensions` 匹配的扩展
|
||
3. 识别扩展类别:语言支持、主题、工具、远程开发
|
||
4. 记录扩展总数和预计还原时间(约 2 秒/个扩展)
|
||
|
||
#### 2.4.4 settings.json 处理策略
|
||
|
||
settings.json 可能包含机器特定路径(如 `terminal.integrated.defaultProfile`),需要智能处理:
|
||
|
||
**Capture(采集)时:**
|
||
- 完整复制 settings.json
|
||
- 标记机器特定设置(含绝对路径的设置项)
|
||
- 记录文件大小和设置项数量
|
||
|
||
**Restore(还原)时的合并策略:**
|
||
|
||
| 策略 | 说明 | 适用场景 |
|
||
|---|---|---|
|
||
| `overwrite` | 完全覆盖目标 settings.json | 新机器(无个性化设置) |
|
||
| `merge` | 深度合并:源设置覆盖同名键,保留目标独有键 | 已有部分设置 |
|
||
| `skip` | 不触碰 settings.json | 用户不想改变设置 |
|
||
| `prompt` | 列出冲突的键,让用户逐个选择 | 交互式还原 |
|
||
|
||
深度合并算法:
|
||
```
|
||
对于每个键 K:
|
||
if K 只在源中存在 → 添加到目标
|
||
if K 只在目标中存在 → 保留不变
|
||
if K 两者都有:
|
||
if 值相同 → 保留
|
||
if 值不同:
|
||
if 值是 object → 递归合并
|
||
if 值是其他类型 → 使用源的值(优先还原)
|
||
```
|
||
|
||
#### 2.4.5 Capture 输出文件
|
||
|
||
```
|
||
vscode/
|
||
├── extensions.json # 扩展列表
|
||
├── settings.json # 用户设置(原样复制)
|
||
├── keybindings.json # 快捷键(原样复制)
|
||
└── snippets/ # 代码片段目录
|
||
├── go.json
|
||
├── python.json
|
||
└── markdown.json
|
||
```
|
||
|
||
#### 2.4.6 Restore 还原流程
|
||
|
||
```
|
||
1. 检测 VS Code 是否已安装
|
||
├── 命令 `code --version` 可执行 → 已安装
|
||
└── 不可执行 → 提示用户安装,跳过此采集器
|
||
2. 安装扩展(串行,每个约 2-10 秒)
|
||
├── `code --install-extension <publisher.name>@<version> --force`
|
||
├── 安装失败 → 尝试不指定版本
|
||
├── 使用进度条显示 "安装扩展 [15/35]"
|
||
└── 记录每个扩展的安装结果
|
||
3. 还原配置文件
|
||
├── 根据冲突策略处理 settings.json
|
||
├── 复制 keybindings.json(如果目标不存在直接创建,否则按策略处理)
|
||
└── 复制 snippets/ 目录下所有文件
|
||
4. 验证
|
||
└── `code --list-extensions --show-versions` 检查扩展是否安装成功
|
||
```
|
||
|
||
#### 2.4.7 边界情况
|
||
|
||
| 场景 | 处理方式 |
|
||
|---|---|
|
||
| VS Code 未安装 | `IsAvailable()` 返回 false |
|
||
| `code` 不在 PATH 中 | 检查常见安装路径(Program Files、/Applications 等) |
|
||
| Insiders 版本 | 检测 `code-insiders` 命令,使用 Insiders 路径 |
|
||
| settings.json 不存在 | 正常处理,跳过设置采集 |
|
||
| settings.json 有语法错误 | 原样复制,不尝试解析 |
|
||
| 扩展依赖其他扩展 | VS Code 自动处理依赖,无需额外逻辑 |
|
||
| 扩展已被弃用或下架 | 安装失败后记录警告 |
|
||
| 同步设置已启用 | 提醒用户 Settings Sync 可能覆盖还原的设置 |
|
||
|
||
---
|
||
|
||
### 任务 2.5 — PowerShell Shell 采集器
|
||
|
||
**新建文件:**
|
||
|
||
| 文件 | 说明 |
|
||
|---|---|
|
||
| `internal/collector/shell/powershell_collector.go` | PowerShell 采集器: 版本、已安装模块、Profile 脚本 |
|
||
| `internal/collector/shell/powershell_collector_test.go` | 单元测试 |
|
||
|
||
**所需库:**
|
||
|
||
```
|
||
标准库:
|
||
os/exec — 执行 `pwsh -Command "Get-Module -ListAvailable | ConvertTo-Json"`
|
||
encoding/json — 解析 PowerShell JSON 输出
|
||
os — 读取 $PROFILE 文件
|
||
path/filepath — Profile 路径
|
||
context — 超时
|
||
strings — 解析
|
||
io — 文件复制
|
||
```
|
||
|
||
**详细功能描述:**
|
||
|
||
#### 2.5.1 PowerShell 版本检测
|
||
|
||
优先检测 PowerShell 7+(`pwsh`),回退到 Windows PowerShell 5.1(`powershell`):
|
||
|
||
| 命令 | 版本 | 说明 |
|
||
|---|---|---|
|
||
| `pwsh --version` | 7.x | 跨平台的 PowerShell Core |
|
||
| `powershell -Command "$PSVersionTable.PSVersion.ToString()"` | 5.1 | Windows 内置 |
|
||
|
||
#### 2.5.2 Profile 文件路径
|
||
|
||
PowerShell 有四个 Profile 文件,按加载顺序:
|
||
|
||
| Profile 变量 | Windows 路径示例 | 说明 |
|
||
|---|---|---|
|
||
| `$PROFILE.AllUsersAllHosts` | `C:\Windows\System32\WindowsPowerShell\v1.0\profile.ps1` | 所有用户所有主机 |
|
||
| `$PROFILE.AllUsersCurrentHost` | `...Microsoft.PowerShell_profile.ps1` | 所有用户当前主机 |
|
||
| `$PROFILE.CurrentUserAllHosts` | `~\Documents\PowerShell\profile.ps1` | 当前用户所有主机 |
|
||
| `$PROFILE.CurrentUserCurrentHost` | `~\Documents\PowerShell\Microsoft.PowerShell_profile.ps1` | 当前用户当前主机 |
|
||
|
||
**采集策略:** 只采集 `CurrentUserAllHosts` 和 `CurrentUserCurrentHost` 两个用户级 Profile。
|
||
|
||
#### 2.5.3 模块扫描
|
||
|
||
```powershell
|
||
Get-Module -ListAvailable |
|
||
Where-Object { $_.ModuleBase -like "$env:USERPROFILE*" } |
|
||
Select-Object Name, Version, ModuleBase |
|
||
ConvertTo-Json
|
||
```
|
||
|
||
过滤逻辑:只采集用户级安装的模块(路径在用户目录下),排除系统预装模块。
|
||
|
||
常见用户安装模块示例:
|
||
- `posh-git` — Git 集成
|
||
- `oh-my-posh` — 终端美化
|
||
- `PSReadLine` — 命令行编辑增强
|
||
- `Terminal-Icons` — 文件图标
|
||
- `z` — 目录快速跳转
|
||
|
||
#### 2.5.4 Restore 还原流程
|
||
|
||
```
|
||
1. 检测 PowerShell 版本
|
||
└── 如果源机使用 pwsh 7.x 但目标机只有 5.1 → 提示安装 PowerShell 7
|
||
2. 安装模块
|
||
├── `Install-Module -Name <name> -RequiredVersion <ver> -Force -Scope CurrentUser`
|
||
├── 如果 PSGallery 仓库未注册 → 先 `Register-PSRepository`
|
||
├── 如果受 ExecutionPolicy 限制 → 提示修改执行策略
|
||
└── 记录安装结果
|
||
3. 还原 Profile 脚本
|
||
├── 备份现有 Profile(如果存在)为 `.bak`
|
||
├── 写入采集的 Profile 内容
|
||
└── 提示用户重新打开终端生效
|
||
```
|
||
|
||
#### 2.5.5 边界情况
|
||
|
||
| 场景 | 处理方式 |
|
||
|---|---|
|
||
| 仅有 Windows PowerShell 5.1 | 使用 `powershell` 命令 |
|
||
| ExecutionPolicy = Restricted | 提示用户执行 `Set-ExecutionPolicy RemoteSigned -Scope CurrentUser` |
|
||
| PSGallery 不受信任 | 自动 `Set-PSRepository -Name PSGallery -InstallationPolicy Trusted` |
|
||
| Profile 文件不存在 | 正常处理,跳过 Profile 采集 |
|
||
| 模块版本在 PSGallery 不可用 | 尝试安装最新版本 |
|
||
|
||
---
|
||
|
||
### 任务 2.6 — Scoop 包管理器采集器 (Windows)
|
||
|
||
**新建文件:**
|
||
|
||
| 文件 | 说明 |
|
||
|---|---|
|
||
| `internal/collector/package/scoop_collector.go` | Scoop 采集器: 已安装包、bucket 列表 |
|
||
| `internal/collector/package/scoop_collector_test.go` | 单元测试 |
|
||
|
||
**所需库:**
|
||
|
||
```
|
||
标准库:
|
||
os/exec — 执行 `scoop list`、`scoop bucket list`、`scoop install`
|
||
encoding/json — 解析 scoop export JSON
|
||
context — 超时
|
||
strings — 输出解析
|
||
os — Scoop 目录检测
|
||
fmt — 格式化
|
||
```
|
||
|
||
**详细功能描述:**
|
||
|
||
#### 2.6.1 Scoop 检测
|
||
|
||
```
|
||
1. exec.LookPath("scoop") 检查命令
|
||
2. 或检查 %USERPROFILE%\scoop\shims\ 目录是否存在
|
||
3. 或检查 %SCOOP% 环境变量
|
||
```
|
||
|
||
#### 2.6.2 扫描数据格式
|
||
|
||
**Bucket 列表(`scoop bucket list`):**
|
||
```
|
||
main
|
||
extras
|
||
versions
|
||
nerd-fonts
|
||
java
|
||
```
|
||
|
||
**包列表(`scoop export`)JSON 格式:**
|
||
```json
|
||
{
|
||
"apps": [
|
||
{"Name": "7zip", "Version": "23.01", "Source": "main"},
|
||
{"Name": "git", "Version": "2.43.0", "Source": "main"},
|
||
{"Name": "vscode", "Version": "1.85.1", "Source": "extras"},
|
||
{"Name": "firacode-nf", "Version": "3.1.1", "Source": "nerd-fonts"}
|
||
],
|
||
"buckets": [
|
||
{"Name": "main", "Source": "https://github.com/ScoopInstaller/Main"},
|
||
{"Name": "extras", "Source": "https://github.com/ScoopInstaller/Extras"},
|
||
{"Name": "nerd-fonts", "Source": "https://github.com/matthewjberger/scoop-nerd-fonts"}
|
||
]
|
||
}
|
||
```
|
||
|
||
#### 2.6.3 Restore 还原流程
|
||
|
||
```
|
||
1. 检测 Scoop 是否安装
|
||
└── 未安装 → 提示用户安装 (irm get.scoop.sh | iex),跳过此采集器
|
||
2. 添加 Bucket(串行,避免冲突)
|
||
├── `scoop bucket add <name> [<url>]`
|
||
├── Bucket 已存在 → 跳过
|
||
└── URL 无效 → 记录警告,部分包可能安装失败
|
||
3. 安装包(串行)
|
||
├── `scoop install <name>`
|
||
├── 不指定版本(Scoop 总是安装最新版本,版本管理通过 scoop reset 处理)
|
||
├── 安装失败 → 记录错误并继续
|
||
└── 使用进度条 "安装 Scoop 包 [15/42]"
|
||
4. 验证
|
||
└── `scoop list` 检查包是否安装
|
||
```
|
||
|
||
#### 2.6.4 Capture 输出文件
|
||
|
||
**packages.json:**
|
||
```json
|
||
[
|
||
{"name": "7zip", "version": "23.01", "bucket": "main"},
|
||
{"name": "git", "version": "2.43.0", "bucket": "main"},
|
||
{"name": "vscode", "version": "1.85.1", "bucket": "extras"}
|
||
]
|
||
```
|
||
|
||
**buckets.json:**
|
||
```json
|
||
[
|
||
{"name": "main", "url": "https://github.com/ScoopInstaller/Main"},
|
||
{"name": "extras", "url": "https://github.com/ScoopInstaller/Extras"}
|
||
]
|
||
```
|
||
|
||
#### 2.6.5 边界情况
|
||
|
||
| 场景 | 处理方式 |
|
||
|---|---|
|
||
| Scoop 未安装 | `IsAvailable()` 返回 false(仅 Windows) |
|
||
| 非 Windows 平台 | 不注册此采集器 |
|
||
| 自定义 Bucket URL 失效 | 记录警告,尝试跳过该 Bucket 的包 |
|
||
| 包被 hold(锁定版本) | 记录 hold 状态,还原时提示 |
|
||
| 包依赖其他包 | Scoop 自动处理依赖 |
|
||
| Scoop 目录为自定义路径 | 从 `%SCOOP%` 或 `scoop prefix scoop` 获取 |
|
||
|
||
---
|
||
|
||
### 任务 2.7 — Git 配置采集器
|
||
|
||
**新建文件:**
|
||
|
||
| 文件 | 说明 |
|
||
|---|---|
|
||
| `internal/collector/git/git_collector.go` | Git 采集器: 全局配置、别名 |
|
||
| `internal/collector/git/git_collector_test.go` | 单元测试 |
|
||
|
||
**所需库:**
|
||
|
||
```
|
||
标准库:
|
||
os/exec — 执行 `git config --global --list`、`git config --global <key> <value>`
|
||
context — 超时
|
||
strings — key=value 解析
|
||
bufio — 逐行读取 .gitconfig
|
||
os — 读取 ~/.gitconfig 文件
|
||
path/filepath — 路径
|
||
fmt — 格式化
|
||
```
|
||
|
||
**详细功能描述:**
|
||
|
||
#### 2.7.1 扫描配置分区
|
||
|
||
`git config --global --list` 输出所有全局配置,按分区(section)分组采集:
|
||
|
||
| 分区 | 采集 | 说明 |
|
||
|---|---|---|
|
||
| `user.*` | ✅ | user.name, user.email |
|
||
| `alias.*` | ✅ | 所有 git 别名 |
|
||
| `core.*` | ✅ | core.editor, core.autocrlf, core.eol |
|
||
| `push.*` | ✅ | push.default, push.autoSetupRemote |
|
||
| `pull.*` | ✅ | pull.rebase |
|
||
| `diff.*` | ✅ | diff.tool |
|
||
| `merge.*` | ✅ | merge.tool, merge.conflictstyle |
|
||
| `init.*` | ✅ | init.defaultBranch |
|
||
| `credential.*` | ❌ | 跳过凭据信息(安全敏感) |
|
||
| `http.*` | ⚠️ | 保留 proxy 设置,跳过 sslVerify=false |
|
||
| `url.*` | ⚠️ | 保留 insteadOf 重写规则 |
|
||
|
||
#### 2.7.2 Capture 输出文件
|
||
|
||
**gitconfig.json:**
|
||
```json
|
||
{
|
||
"user": {
|
||
"name": "开发者",
|
||
"email": "dev@example.com"
|
||
},
|
||
"alias": {
|
||
"st": "status",
|
||
"co": "checkout",
|
||
"br": "branch",
|
||
"lg": "log --oneline --graph --all"
|
||
},
|
||
"core": {
|
||
"editor": "code --wait",
|
||
"autocrlf": "true"
|
||
},
|
||
"push": {
|
||
"default": "current",
|
||
"autoSetupRemote": "true"
|
||
},
|
||
"init": {
|
||
"defaultBranch": "main"
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 2.7.3 Restore 还原流程
|
||
|
||
```
|
||
1. 检测 Git 是否安装
|
||
└── `git --version` 可执行
|
||
2. 逐条设置 Git 配置
|
||
├── `git config --global user.name "开发者"`
|
||
├── `git config --global alias.st "status"`
|
||
├── ...
|
||
└── 使用冲突策略决定是否覆盖已有配置
|
||
3. 注意事项
|
||
├── user.email / user.name 如果目标机已有 → 默认 SKIP(用户可能有不同身份)
|
||
├── alias → 默认 MERGE(追加不存在的别名)
|
||
└── core/push/pull → 默认 OVERWRITE
|
||
```
|
||
|
||
#### 2.7.4 边界情况
|
||
|
||
| 场景 | 处理方式 |
|
||
|---|---|
|
||
| Git 未安装 | `IsAvailable()` 返回 false |
|
||
| ~/.gitconfig 不存在 | 正常返回空配置 |
|
||
| 配置值包含特殊字符 | 使用 `git config --global --get <key>` 逐个读取 |
|
||
| include.path 指令 | 记录但不递归展开(可能引用不存在的文件) |
|
||
| 凭据管理器配置 | 跳过 `credential.*` 下的所有配置 |
|
||
| user.signingkey | 跳过(GPG 密钥不通过此采集器迁移) |
|
||
|
||
---
|
||
|
||
### 任务 2.8 — 环境变量采集器
|
||
|
||
**新建文件:**
|
||
|
||
| 文件 | 说明 |
|
||
|---|---|
|
||
| `internal/collector/env/env_collector.go` | 环境变量采集器: 按 Profile 中定义的 include/exclude pattern 过滤 |
|
||
| `internal/collector/env/env_collector_test.go` | 单元测试 |
|
||
|
||
**所需库:**
|
||
|
||
```
|
||
标准库:
|
||
os — os.Environ() 获取所有环境变量
|
||
strings — key=value 拆分、pattern 匹配
|
||
path/filepath — filepath.Match 用于 glob 匹配
|
||
context — 超时
|
||
fmt — 格式化
|
||
encoding/json — 序列化
|
||
|
||
内部依赖:
|
||
internal/platform — SetEnvVar() 设置环境变量 (不同平台不同)
|
||
```
|
||
|
||
**详细功能描述:**
|
||
|
||
#### 2.8.1 环境变量过滤算法
|
||
|
||
```
|
||
输入: os.Environ() → ["GOPATH=C:\Users\dev\go", "PATH=...", "SECRET_KEY=abc", ...]
|
||
|
||
算法:
|
||
for each env in environ:
|
||
key, value = split(env, "=", 2)
|
||
|
||
// 第一层: 敏感检测(硬编码规则)
|
||
if containsAny(key, ["SECRET", "TOKEN", "PASSWORD", "KEY", "CREDENTIAL", "AUTH"]):
|
||
skip(安全原因)
|
||
continue
|
||
|
||
// 第二层: 系统变量排除(硬编码)
|
||
if key in ["PATH", "HOME", "USER", "SHELL", "TERM", "DISPLAY",
|
||
"LOGNAME", "HOSTNAME", "PWD", "OLDPWD", "SHLVL",
|
||
"LANG", "LC_*", "XDG_*", "SESSION_*", "DBUS_*",
|
||
"WINDOWID", "COLORTERM"]:
|
||
skip(系统/会话相关)
|
||
continue
|
||
|
||
// 第三层: Profile include 模式匹配
|
||
if profile.settings.env.include_patterns 不为空:
|
||
if 没有任何 include pattern 匹配 key:
|
||
skip
|
||
continue
|
||
|
||
// 第四层: Profile exclude 模式匹配
|
||
if 有任何 exclude pattern 匹配 key:
|
||
skip
|
||
continue
|
||
|
||
// 通过所有过滤 → 采集此变量
|
||
result.append({key, value})
|
||
```
|
||
|
||
#### 2.8.2 PATH 变量特殊处理
|
||
|
||
PATH 变量不直接采集整个值(太长且含系统路径),而是提取开发相关的路径段:
|
||
|
||
```
|
||
PATH 提取规则:
|
||
1. 按分隔符拆分 PATH(Windows: ";", Unix: ":")
|
||
2. 过滤出开发相关路径:
|
||
- 包含 "go/bin" 或 "Go\bin"
|
||
- 包含 ".cargo/bin"
|
||
- 包含 ".npm" 或 "node_modules"
|
||
- 包含 "pyenv" 或 ".local/bin"
|
||
- 包含 "scoop" 或 "Scoop"
|
||
- 包含 "maven" 或 "gradle"
|
||
3. 将匹配的路径段记录为 PATH_ADDITIONS
|
||
4. 还原时追加到目标机的 PATH 中
|
||
```
|
||
|
||
#### 2.8.3 Capture 输出文件
|
||
|
||
**variables.json:**
|
||
```json
|
||
{
|
||
"variables": [
|
||
{"key": "GOPATH", "value": "C:\\Users\\dev\\go"},
|
||
{"key": "GOROOT", "value": "C:\\Program Files\\Go"},
|
||
{"key": "JAVA_HOME", "value": "C:\\Program Files\\Java\\jdk-21"},
|
||
{"key": "GOPROXY", "value": "https://goproxy.cn,direct"}
|
||
],
|
||
"path_additions": [
|
||
"C:\\Users\\dev\\go\\bin",
|
||
"C:\\Users\\dev\\.cargo\\bin",
|
||
"C:\\Users\\dev\\scoop\\shims"
|
||
]
|
||
}
|
||
```
|
||
|
||
#### 2.8.4 Restore 还原流程
|
||
|
||
```
|
||
1. 逐个设置环境变量
|
||
├── 调用 platform.SetEnvVar(key, value)
|
||
├── 如果变量已存在且值不同 → 按冲突策略处理
|
||
└── 记录每个变量的设置结果
|
||
2. 处理 PATH 追加
|
||
├── 获取当前 PATH
|
||
├── 检查每个 path_additions 是否已在 PATH 中
|
||
├── 不在的追加到 PATH 末尾
|
||
└── 调用 platform.SetEnvVar("PATH", newPath)
|
||
```
|
||
|
||
#### 2.8.5 边界情况
|
||
|
||
| 场景 | 处理方式 |
|
||
|---|---|
|
||
| 环境变量值包含特殊字符 | 保持原样,shell 转义由 platform 层处理 |
|
||
| PATH 极长(>8000 字符) | Windows 注册表有 PATH 长度限制,记录警告 |
|
||
| 变量引用其他变量(`$HOME/go`) | 记录原始值,不展开引用 |
|
||
| Windows 系统变量 vs 用户变量 | 只操作用户级变量 (HKCU) |
|
||
|
||
---
|
||
|
||
### 任务 2.9 — 采集器自动注册
|
||
|
||
**需修改的现有文件:**
|
||
|
||
| 文件 | 改动 | 所需库 |
|
||
|---|---|---|
|
||
| `internal/collector/registry.go` | 无需修改,现有注册机制已可用 | — |
|
||
| `cmd/devpack/main.go` | 添加 `registerCollectors()` 函数,创建 Registry 实例并注册所有内置采集器 | 所有 `internal/collector/*` 子包 |
|
||
|
||
**新建文件:**
|
||
|
||
| 文件 | 说明 |
|
||
|---|---|
|
||
| `internal/collector/register.go` | `RegisterBuiltins(registry)` — 一次性注册所有内置采集器,集中管理 |
|
||
|
||
**详细功能描述:**
|
||
|
||
#### 2.9.1 注册机制设计
|
||
|
||
```go
|
||
// internal/collector/register.go
|
||
func RegisterBuiltins(r *Registry) {
|
||
// 运行时采集器
|
||
r.Register(runtime.NewGoCollector())
|
||
r.Register(runtime.NewNodeCollector())
|
||
r.Register(runtime.NewPythonCollector())
|
||
|
||
// 编辑器采集器
|
||
r.Register(editor.NewVSCodeCollector())
|
||
|
||
// Shell 采集器
|
||
r.Register(shell.NewPowerShellCollector())
|
||
|
||
// 包管理器采集器(平台相关)
|
||
if goruntime.GOOS == "windows" {
|
||
r.Register(pkg.NewScoopCollector())
|
||
}
|
||
// if goruntime.GOOS == "darwin" {
|
||
// r.Register(pkg.NewBrewCollector()) // 未来扩展
|
||
// }
|
||
|
||
// 配置采集器
|
||
r.Register(git.NewGitCollector())
|
||
r.Register(env.NewEnvCollector())
|
||
}
|
||
```
|
||
|
||
#### 2.9.2 注册顺序与分类
|
||
|
||
注册顺序决定了默认的扫描和显示顺序:
|
||
|
||
| 顺序 | 分类 | 采集器 | 说明 |
|
||
|---|---|---|---|
|
||
| 1 | `runtime` | Go, Node.js, Python | 运行时环境优先 |
|
||
| 2 | `editor` | VS Code | 编辑器配置 |
|
||
| 3 | `shell` | PowerShell | Shell 配置 |
|
||
| 4 | `package` | Scoop | 包管理器 |
|
||
| 5 | `config` | Git, Env | 通用配置 |
|
||
|
||
#### 2.9.3 可用性过滤
|
||
|
||
`Registry.Available()` 方法对所有注册的采集器调用 `IsAvailable()`,只返回当前环境可用的采集器。
|
||
例如在未安装 Python 的机器上,Python 采集器不会出现在可用列表中。
|
||
|
||
---
|
||
|
||
### 任务 2.10 — scan 命令实现
|
||
|
||
**需修改的现有文件:**
|
||
|
||
| 文件 | 改动 | 所需库 |
|
||
|---|---|---|
|
||
| `cmd/devpack/commands/scan.go` | 实现完整扫描逻辑:解析 flags → 加载 Profile → 选择采集器 → 并发扫描 → 格式化输出 (table/json/yaml) | `internal/collector`, `internal/profile`, `internal/ui`, `internal/logging` |
|
||
|
||
**依赖的内部包:**
|
||
|
||
```
|
||
internal/collector — Registry.ScanAll() / ListByCategory()
|
||
internal/profile — 加载 Profile 确定启用的采集器
|
||
internal/ui — 表格渲染、颜色输出
|
||
internal/logging — 日志
|
||
internal/config — 路径
|
||
|
||
标准库:
|
||
encoding/json — --output json
|
||
context — 超时控制
|
||
fmt — 输出
|
||
strings — 逗号分隔解析
|
||
time — 扫描耗时
|
||
```
|
||
|
||
**详细功能描述:**
|
||
|
||
#### 2.10.1 命令完整执行流程
|
||
|
||
```
|
||
1. 解析命令行参数
|
||
├── --profile <name> 指定 Profile(默认使用 config.yaml 中的 default_profile)
|
||
├── --collectors <list> 逗号分隔的采集器名(覆盖 Profile 配置)
|
||
├── --output <format> 输出格式: table (默认) | json | yaml
|
||
├── --detailed 显示详细信息(每个 ScanItem 的 Properties)
|
||
└── --timeout <duration> 超时时间(默认 30s)
|
||
|
||
2. 初始化采集器
|
||
├── 创建 Registry 实例
|
||
├── RegisterBuiltins(registry)
|
||
└── 获取可用采集器列表
|
||
|
||
3. 应用 Profile 过滤
|
||
├── 加载 Profile
|
||
├── 按 enabled/disabled 列表过滤
|
||
└── 如果指定了 --collectors,以其为准
|
||
|
||
4. 并发扫描
|
||
├── 启动 Spinner "正在扫描环境..."
|
||
├── 为每个采集器创建 goroutine
|
||
├── 每个 goroutine: ctx, cancel := context.WithTimeout(ctx, timeout)
|
||
├── 收集 []ScanResult 到 channel
|
||
└── 等待全部完成或超时
|
||
|
||
5. 输出结果
|
||
├── table: 使用 ui.Table 渲染,按 Category 分组
|
||
├── json: json.MarshalIndent 输出
|
||
├── yaml: yaml.Marshal 输出
|
||
└── 底部显示汇总: "发现 X 个采集器,Y 个项目,耗时 Z 秒"
|
||
```
|
||
|
||
#### 2.10.2 并发扫描控制
|
||
|
||
```go
|
||
type scanJob struct {
|
||
collector Collector
|
||
result *ScanResult
|
||
err error
|
||
duration time.Duration
|
||
}
|
||
|
||
// 使用带缓冲的 channel 控制并发
|
||
results := make(chan scanJob, len(collectors))
|
||
sem := make(chan struct{}, maxConcurrency) // 最大并发数 = 4
|
||
|
||
for _, c := range collectors {
|
||
go func(col Collector) {
|
||
sem <- struct{}{} // 获取信号量
|
||
defer func() { <-sem }() // 释放信号量
|
||
|
||
start := time.Now()
|
||
ctx, cancel := context.WithTimeout(parentCtx, timeout)
|
||
defer cancel()
|
||
|
||
result, err := col.Scan(ctx, opts)
|
||
results <- scanJob{col, result, err, time.Since(start)}
|
||
}(c)
|
||
}
|
||
```
|
||
|
||
#### 2.10.3 输出格式示例
|
||
|
||
**Table 格式(默认):**
|
||
```
|
||
🔍 环境扫描结果
|
||
|
||
运行时:
|
||
✓ Go Runtime 1.22.1 3 tools 2.1 MB
|
||
✓ Node.js Runtime 20.11.0 12 packages 1.5 MB
|
||
⚠ Python Runtime 未安装 — —
|
||
|
||
编辑器:
|
||
✓ VS Code 1.85.1 35 extensions 4.2 MB
|
||
|
||
Shell:
|
||
✓ PowerShell 7.4.1 5 modules 0.3 MB
|
||
|
||
包管理器:
|
||
✓ Scoop installed 42 packages 0.1 MB
|
||
|
||
配置:
|
||
✓ Git 2.43.0 12 aliases 0.01 MB
|
||
✓ Environment Vars — 8 variables 0.001 MB
|
||
|
||
───────────────────────────────────
|
||
合计: 7 采集器 | 115 项目 | 8.2 MB | 耗时 2.3s
|
||
```
|
||
|
||
---
|
||
|
||
## M3: 打包引擎(第 6-7 周)
|
||
|
||
### 任务 3.1 — Pack 引擎核心
|
||
|
||
**新建文件:**
|
||
|
||
| 文件 | 说明 |
|
||
|---|---|
|
||
| `internal/pack/engine.go` | PackEngine 结构体:创建 Pack 的核心流程控制 |
|
||
| `internal/pack/engine_test.go` | Pack 引擎单元测试 |
|
||
| `internal/pack/archive.go` | tar.gz 打包/解包实现 |
|
||
| `internal/pack/archive_test.go` | 归档单元测试 |
|
||
| `internal/pack/checksum.go` | SHA-256 校验和计算与验证 |
|
||
| `internal/pack/checksum_test.go` | 校验和单元测试 |
|
||
|
||
**所需库:**
|
||
|
||
```
|
||
第三方库:
|
||
github.com/google/uuid — 生成 Pack UUID
|
||
|
||
标准库:
|
||
archive/tar — tar 格式写入/读取
|
||
compress/gzip — gzip 压缩/解压
|
||
crypto/sha256 — SHA-256 校验和
|
||
encoding/hex — 校验和 hex 编码
|
||
encoding/json — manifest.json 写入
|
||
io — 数据流复制
|
||
os — 文件操作
|
||
path/filepath — 路径遍历
|
||
time — 时间戳
|
||
fmt — 格式化
|
||
strings — 路径处理
|
||
```
|
||
|
||
**需修改的现有文件:**
|
||
|
||
| 文件 | 改动 |
|
||
|---|---|
|
||
| `pkg/manifest/manifest.go` | 添加 `Validate()` 方法、`NewManifest()` 构造函数 |
|
||
| `go.mod` | 添加 `github.com/google/uuid`、`github.com/schollz/progressbar/v3` |
|
||
|
||
**详细功能描述:**
|
||
|
||
#### 3.1.1 .devpack 文件格式规范
|
||
|
||
`.devpack` 文件本质上是一个 `.tar.gz` 归档,内部目录结构如下:
|
||
|
||
```
|
||
<pack-name>.devpack (tar.gz)
|
||
│
|
||
├── manifest.json # Pack 元数据清单(必须)
|
||
├── checksum.sha256 # 所有文件的 SHA-256 校验和(必须)
|
||
├── profile.yaml # 打包时使用的 Profile 副本(必须)
|
||
│
|
||
└── collectors/ # 各采集器的数据目录
|
||
├── go/
|
||
│ ├── metadata.json # Go 版本、go env 等元数据
|
||
│ └── tools.json # GOPATH/bin 工具列表
|
||
├── node/
|
||
│ ├── metadata.json # Node.js/npm 版本
|
||
│ └── global-packages.json
|
||
├── vscode/
|
||
│ ├── extensions.json # 扩展列表
|
||
│ ├── settings.json # 用户设置
|
||
│ ├── keybindings.json # 快捷键绑定
|
||
│ └── snippets/ # 代码片段目录
|
||
│ ├── go.json
|
||
│ └── python.json
|
||
├── powershell/
|
||
│ ├── metadata.json # PowerShell 版本
|
||
│ ├── modules.json # 已安装模块列表
|
||
│ └── profile.ps1 # Profile 脚本内容
|
||
├── scoop/
|
||
│ ├── packages.json # 已安装包列表
|
||
│ └── buckets.json # Bucket 列表及 URL
|
||
├── git/
|
||
│ └── gitconfig.json # Git 全局配置(JSON 格式)
|
||
└── env/
|
||
└── variables.json # 环境变量列表 + PATH 追加段
|
||
```
|
||
|
||
#### 3.1.2 manifest.json 完整 Schema
|
||
|
||
```json
|
||
{
|
||
"schema_version": 1,
|
||
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
||
"name": "my-dev-env",
|
||
"description": "我的开发环境 - 2026年3月",
|
||
"version": "1.0.0",
|
||
"created_at": "2026-03-03T10:30:00+08:00",
|
||
"created_by": "Administrator@DESKTOP-ABC123",
|
||
"devpack_version": "0.1.0",
|
||
|
||
"platform": {
|
||
"os": "windows",
|
||
"arch": "amd64",
|
||
"os_version": "10.0.22631",
|
||
"hostname": "DESKTOP-ABC123"
|
||
},
|
||
|
||
"collectors": [
|
||
{
|
||
"name": "go",
|
||
"display_name": "Go Runtime",
|
||
"category": "runtime",
|
||
"item_count": 5,
|
||
"size_bytes": 2100000,
|
||
"files": ["collectors/go/metadata.json", "collectors/go/tools.json"]
|
||
}
|
||
],
|
||
|
||
"summary": {
|
||
"total_collectors": 7,
|
||
"total_items": 115,
|
||
"total_size_bytes": 8200000
|
||
},
|
||
|
||
"checksum": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||
}
|
||
```
|
||
|
||
#### 3.1.3 PackEngine 创建流程
|
||
|
||
```
|
||
PackEngine.Create(ctx, opts) 流程:
|
||
|
||
1. 验证参数
|
||
├── Pack 名称合法(非空、无特殊字符)
|
||
├── Profile 存在且有效
|
||
└── 输出路径可写
|
||
|
||
2. 创建临时工作目录
|
||
└── os.MkdirTemp("", "devpack-capture-*")
|
||
|
||
3. 并发执行 Capture(信号量控制最大并发=4)
|
||
├── 为每个启用的采集器创建 collectors/<name>/ 子目录
|
||
├── collector.Capture(ctx, targetDir, captureOpts)
|
||
├── 记录每个采集器的状态(成功/失败/跳过)
|
||
└── 收集文件大小和项目数量
|
||
|
||
4. 复制 Profile 到临时目录
|
||
└── cp profile.yaml → tempDir/profile.yaml
|
||
|
||
5. 生成 manifest.json
|
||
├── 填充所有元数据字段
|
||
├── 计算 summary(总采集器数、总项目数、总大小)
|
||
└── 写入 tempDir/manifest.json
|
||
|
||
6. 计算校验和
|
||
├── 遍历 tempDir 下所有文件
|
||
├── 对每个文件计算 SHA-256
|
||
├── 格式: "sha256:<hex> <relative-path>"(每行一个文件)
|
||
└── 写入 tempDir/checksum.sha256
|
||
|
||
7. 打包为 tar.gz
|
||
├── 创建 <pack-name>.devpack 文件
|
||
├── 写入 gzip → tar 数据流
|
||
├── 遍历临时目录,逐个添加文件/目录到 tar
|
||
└── 关闭 tar/gzip writer
|
||
|
||
8. 移动到 Packs 目录
|
||
└── mv <pack-name>.devpack → ~/.devpack/packs/<pack-name>.devpack
|
||
|
||
9. 清理临时目录
|
||
└── os.RemoveAll(tempDir)
|
||
|
||
10. 返回 PackResult
|
||
└── {Name, Path, Size, CollectorCount, ItemCount, Duration}
|
||
```
|
||
|
||
#### 3.1.4 checksum.sha256 文件格式
|
||
|
||
```
|
||
sha256:a1b2c3d4e5f6... manifest.json
|
||
sha256:f6e5d4c3b2a1... profile.yaml
|
||
sha256:1234567890ab... collectors/go/metadata.json
|
||
sha256:abcdef012345... collectors/go/tools.json
|
||
sha256:fedcba987654... collectors/vscode/extensions.json
|
||
sha256:098765432abc... collectors/vscode/settings.json
|
||
...
|
||
```
|
||
|
||
校验算法:逐行读取,对每个文件重新计算 SHA-256 并与记录值对比。
|
||
|
||
#### 3.1.5 archive.go 核心函数
|
||
|
||
```go
|
||
// 创建 tar.gz 归档
|
||
func CreateArchive(sourceDir, outputPath string) error
|
||
|
||
// 解压 tar.gz 归档
|
||
func ExtractArchive(archivePath, outputDir string) error
|
||
|
||
// 遍历目录树,逐个添加到 tar writer
|
||
func addToTar(tw *tar.Writer, basePath, relativePath string) error
|
||
```
|
||
|
||
**安全处理:**
|
||
- 解压时检查路径穿越攻击(`../` 开头的路径)
|
||
- 限制单个文件最大大小(默认 100MB)
|
||
- 限制总文件数(默认 10000 个)
|
||
|
||
---
|
||
|
||
### 任务 3.2 — capture 命令实现
|
||
|
||
**需修改的现有文件:**
|
||
|
||
| 文件 | 改动 | 所需库 |
|
||
|---|---|---|
|
||
| `cmd/devpack/commands/capture.go` | 实现完整捕获流程:解析 flags → 加载 Profile → 创建临时目录 → 并发运行 Capture() → 生成 Manifest → 打包 → 输出结果 | `internal/pack`, `internal/collector`, `internal/profile`, `internal/ui`, `internal/logging` |
|
||
|
||
**依赖的内部包:**
|
||
|
||
```
|
||
internal/pack — PackEngine.CreatePack()
|
||
internal/collector — Registry + 各采集器
|
||
internal/profile — 加载 Profile
|
||
internal/ui — 进度条、表格
|
||
internal/logging — 日志
|
||
internal/config — 路径 (Packs 目录)
|
||
pkg/manifest — Manifest 构建
|
||
|
||
标准库:
|
||
os — 临时目录、文件
|
||
path/filepath — 路径
|
||
context — 超时
|
||
fmt — 输出
|
||
time — 时间戳
|
||
```
|
||
|
||
**详细功能描述:**
|
||
|
||
#### 3.2.1 命令执行流程
|
||
|
||
```
|
||
devpack capture [name] [flags]
|
||
|
||
1. 解析参数
|
||
├── name: Pack 名称(可选,默认 "env-{date}")
|
||
├── --profile: 使用的 Profile
|
||
├── --description: Pack 描述信息
|
||
├── --collectors: 逗号分隔覆盖 Profile
|
||
├── --exclude: 排除的采集器
|
||
├── --encrypt: 启用加密(v0.3.0+)
|
||
└── --output: 输出路径(默认 ~/.devpack/packs/)
|
||
|
||
2. 预扫描(快速检测哪些采集器可用)
|
||
├── 对每个启用的采集器调用 IsAvailable()
|
||
├── 不可用的自动标记为 SKIP
|
||
└── 输出计划摘要
|
||
|
||
3. 用户确认
|
||
└── "即将采集 7 个采集器的数据,是否继续?[Y/n]"
|
||
|
||
4. 执行采集
|
||
├── 启动进度条 "采集中 [0/7]"
|
||
├── 调用 PackEngine.Create(ctx, opts)
|
||
├── 每完成一个采集器更新进度
|
||
└── 失败的采集器记录警告但不中断
|
||
|
||
5. 输出结果
|
||
└── 显示 Pack 信息(名称、路径、大小、采集器数量)
|
||
```
|
||
|
||
#### 3.2.2 自动命名规则
|
||
|
||
如果用户不指定 Pack 名称,自动生成:
|
||
- 格式:`env-{YYYYMMDD}`(如 `env-20260303`)
|
||
- 同名冲突:追加序号 `env-20260303-2`
|
||
|
||
---
|
||
|
||
### 任务 3.3 — export / import 命令实现
|
||
|
||
**需修改的现有文件:**
|
||
|
||
| 文件 | 改动 | 所需库 |
|
||
|---|---|---|
|
||
| `cmd/devpack/commands/export.go` | export: 从内部 Packs 目录复制 .devpack 到指定路径; import: 从外部路径复制 .devpack 到 Packs 目录并验证 | `internal/pack`, `internal/config`, `internal/ui` |
|
||
|
||
**所需库:**
|
||
|
||
```
|
||
标准库:
|
||
os — 文件复制
|
||
io — 数据流
|
||
path/filepath — 路径
|
||
fmt — 输出
|
||
```
|
||
|
||
**详细功能描述:**
|
||
|
||
#### 3.3.1 export 流程
|
||
|
||
```
|
||
devpack export <pack-name> [-o <output-path>]
|
||
|
||
1. 查找 Pack
|
||
└── 在 ~/.devpack/packs/ 中查找 <pack-name>.devpack
|
||
2. 验证完整性
|
||
└── 可选: 重新计算校验和确认文件未损坏
|
||
3. 复制文件
|
||
├── 默认输出到当前目录 ./<pack-name>.devpack
|
||
├── 大文件使用 io.Copy 带进度条
|
||
└── 复制完成后显示文件大小和路径
|
||
```
|
||
|
||
#### 3.3.2 import 流程
|
||
|
||
```
|
||
devpack import <file-path> [-n <name>] [--verify]
|
||
|
||
1. 验证文件
|
||
├── 检查文件存在且可读
|
||
├── 检查是否为有效的 tar.gz
|
||
├── 解析 manifest.json 确认格式正确
|
||
└── --verify: 验证 checksum.sha256 中所有文件的校验和
|
||
2. 检查命名冲突
|
||
├── 使用 manifest.name 或 --name 指定的名称
|
||
└── 同名已存在 → 提示用户覆盖或改名
|
||
3. 复制到 Packs 目录
|
||
└── cp <file-path> → ~/.devpack/packs/<name>.devpack
|
||
4. 输出结果
|
||
└── 显示 Pack 信息(来源平台、采集器数量、大小)
|
||
```
|
||
|
||
---
|
||
|
||
### 任务 3.4 — 进度条组件
|
||
|
||
**新建文件或修改:**
|
||
|
||
| 文件 | 说明 |
|
||
|---|---|
|
||
| `internal/ui/progress.go` | 基于 progressbar 库的进度条封装、适配打包/还原等长时操作 |
|
||
|
||
**所需库:**
|
||
|
||
```
|
||
第三方库:
|
||
github.com/schollz/progressbar/v3 — 进度条渲染
|
||
|
||
标准库:
|
||
os — Stderr 输出
|
||
fmt — 格式化
|
||
```
|
||
|
||
**详细功能描述:**
|
||
|
||
#### 3.4.1 ProgressTracker 接口
|
||
|
||
```go
|
||
type ProgressTracker interface {
|
||
SetTotal(total int) // 设置总步骤数
|
||
Increment() // 增加一步
|
||
SetMessage(message string) // 更新当前步骤描述
|
||
SetCurrent(step int, message string) // 设置当前步骤(用于多阶段)
|
||
Done() // 完成
|
||
Fail(message string) // 失败
|
||
}
|
||
```
|
||
|
||
#### 3.4.2 使用示例
|
||
|
||
```go
|
||
// 打包进度
|
||
progress := ui.NewProgress(len(collectors))
|
||
for i, c := range collectors {
|
||
progress.SetCurrent(i+1, fmt.Sprintf("采集 %s", c.DisplayName()))
|
||
err := c.Capture(ctx, dir, opts)
|
||
progress.Increment()
|
||
}
|
||
progress.Done()
|
||
```
|
||
|
||
**输出效果:**
|
||
```
|
||
采集中 [████████████░░░░░░░░] 60% | 4/7 | 采集 PowerShell
|
||
```
|
||
|
||
---
|
||
|
||
## M4: 还原引擎(第 8-10 周)
|
||
|
||
### 任务 4.1 — 还原引擎核心
|
||
|
||
**新建文件:**
|
||
|
||
| 文件 | 说明 |
|
||
|---|---|
|
||
| `internal/restore/engine.go` | RestoreEngine: 解包 → 校验 → 解析 Manifest → 平台检查 → 冲突检测 → 执行还原 → 验证 → 报告 |
|
||
| `internal/restore/engine_test.go` | 还原引擎单元测试 |
|
||
| `internal/restore/plan.go` | RestorePlan: 还原计划生成、依赖排序 (拓扑排序) |
|
||
| `internal/restore/plan_test.go` | 还原计划单元测试 |
|
||
| `internal/restore/conflict.go` | 冲突检测器: 版本对比、已存在检测、策略应用 |
|
||
| `internal/restore/conflict_test.go` | 冲突检测单元测试 |
|
||
| `internal/restore/report.go` | 还原报告: 成功/失败/跳过/警告 的汇总和输出 |
|
||
|
||
**所需库:**
|
||
|
||
```
|
||
第三方库:
|
||
github.com/AlecAivazis/survey/v2 — 冲突时交互式提示 (ConflictPrompt 策略)
|
||
|
||
标准库:
|
||
archive/tar — 解包
|
||
compress/gzip — 解压
|
||
crypto/sha256 — 校验和验证
|
||
encoding/hex — hex 解码
|
||
encoding/json — Manifest 解析
|
||
os — 文件操作
|
||
path/filepath — 路径
|
||
context — 超时
|
||
fmt — 格式化
|
||
sort — 依赖排序
|
||
strings — 版本对比
|
||
time — 时间
|
||
sync — 并发控制
|
||
```
|
||
|
||
**详细功能描述:**
|
||
|
||
#### 4.1.1 还原引擎完整流水线(10 步)
|
||
|
||
```
|
||
RestoreEngine.Restore(ctx, packPath, opts) 流程:
|
||
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ 步骤 1: 解包 │
|
||
│ ExtractArchive(packPath, tempDir) │
|
||
│ 失败 → 返回 E3001 错误 │
|
||
├─────────────────────────────────────────────────────────────────┤
|
||
│ 步骤 2: 校验 │
|
||
│ 验证 checksum.sha256 中所有文件的 SHA-256 │
|
||
│ 失败 → 返回 E3002 错误(文件可能被篡改) │
|
||
├─────────────────────────────────────────────────────────────────┤
|
||
│ 步骤 3: 解析 Manifest │
|
||
│ 读取 manifest.json → Manifest 结构体 │
|
||
│ 验证 schema_version 兼容性 │
|
||
│ 失败 → 返回 E3003 错误 │
|
||
├─────────────────────────────────────────────────────────────────┤
|
||
│ 步骤 4: 平台检查 │
|
||
│ 比较 manifest.platform.os 与当前 runtime.GOOS │
|
||
│ 不匹配 → 除非 --force,返回 E4002 错误 │
|
||
│ 架构不匹配 → 警告但不阻止 │
|
||
├─────────────────────────────────────────────────────────────────┤
|
||
│ 步骤 5: 生成还原计划 │
|
||
│ 对每个采集器调用 IsAvailable() + 扫描当前状态 │
|
||
│ 与 Pack 数据对比,生成 RestoreAction 列表 │
|
||
│ 按依赖关系拓扑排序 │
|
||
├─────────────────────────────────────────────────────────────────┤
|
||
│ 步骤 6: 冲突检测 │
|
||
│ 对每个 RestoreAction 检查目标是否已存在 │
|
||
│ 应用 ConflictStrategy (skip/overwrite/merge/prompt/newest) │
|
||
│ prompt 策略 → 使用 survey 交互式提问 │
|
||
├─────────────────────────────────────────────────────────────────┤
|
||
│ 步骤 7: 用户确认 │
|
||
│ 显示还原计划摘要(除非 --yes 跳过确认) │
|
||
│ 用户选择 N → 取消还原 │
|
||
├─────────────────────────────────────────────────────────────────┤
|
||
│ 步骤 8: 创建还原点(除非 --no-rollback) │
|
||
│ 对每个将被修改的项目,快照当前状态 │
|
||
│ 保存到 ~/.devpack/temp/restore-point-{timestamp}/ │
|
||
├─────────────────────────────────────────────────────────────────┤
|
||
│ 步骤 9: 执行还原 │
|
||
│ 按依赖序逐个调用 collector.Restore() │
|
||
│ 使用进度条显示 "还原中 [3/7] VS Code" │
|
||
│ 某个采集器失败: │
|
||
│ ├── Fatal 级别 → 停止并回滚 │
|
||
│ └── Error 级别 → 记录错误,继续下一个 │
|
||
├─────────────────────────────────────────────────────────────────┤
|
||
│ 步骤 10: 验证 & 报告 │
|
||
│ 对每个已还原的采集器调用 collector.Verify() │
|
||
│ 生成还原报告(成功/失败/跳过/警告) │
|
||
│ 清理临时文件 │
|
||
└─────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
#### 4.1.2 RestoreAction 数据结构
|
||
|
||
```go
|
||
type ActionType string
|
||
const (
|
||
ActionInstall ActionType = "INSTALL" // 全新安装
|
||
ActionUpgrade ActionType = "UPGRADE" // 版本升级
|
||
ActionDowngrade ActionType = "DOWNGRADE" // 版本降级
|
||
ActionMerge ActionType = "MERGE" // 合并配置
|
||
ActionOverwrite ActionType = "OVERWRITE" // 覆盖
|
||
ActionSkip ActionType = "SKIP" // 跳过(已存在且一致)
|
||
)
|
||
|
||
type RestoreAction struct {
|
||
Collector string // 采集器名称
|
||
Item string // 项目名称(如 "Go 1.22.1")
|
||
Action ActionType // 动作类型
|
||
Details string // 说明(如 "Current: 1.21.0 → 1.22.1")
|
||
Priority int // 执行优先级(数字越小越先执行)
|
||
DependsOn []string // 依赖的其他采集器
|
||
}
|
||
```
|
||
|
||
#### 4.1.3 依赖排序(拓扑排序)
|
||
|
||
还原顺序有依赖关系,必须按拓扑序执行:
|
||
|
||
```
|
||
依赖图:
|
||
env vars ← 无依赖(最先执行,设置 PATH 等)
|
||
runtimes ← 依赖 env vars(GOPATH 等需要先设置)
|
||
packages ← 依赖 runtimes(scoop install 可能需要 git)
|
||
editors ← 依赖 runtimes(VS Code 扩展可能需要语言运行时)
|
||
shells ← 依赖 packages(Profile 脚本可能引用已安装的工具)
|
||
git config ← 依赖 runtimes(git 需要先安装)
|
||
|
||
拓扑序(默认):
|
||
1. env → 2. go/node/python → 3. scoop → 4. vscode → 5. powershell → 6. git
|
||
```
|
||
|
||
#### 4.1.4 还原点结构
|
||
|
||
```
|
||
~/.devpack/temp/restore-point-20260303-103000/
|
||
├── manifest.json # 还原点元数据(时间、Pack 来源)
|
||
├── vscode/
|
||
│ └── settings.json # 备份的 settings.json
|
||
├── powershell/
|
||
│ └── profile.ps1 # 备份的 Profile 脚本
|
||
├── git/
|
||
│ └── gitconfig-backup.json # 备份的 Git 配置
|
||
└── env/
|
||
└── variables-backup.json # 备份的环境变量值
|
||
```
|
||
|
||
---
|
||
|
||
### 任务 4.2 — 干运行模式
|
||
|
||
**涉及文件:**
|
||
|
||
| 文件 | 改动 |
|
||
|---|---|
|
||
| `internal/restore/engine.go` | 在 `Restore()` 中根据 `DryRun` 标志只模拟操作、输出计划但不执行 |
|
||
| `internal/restore/report.go` | DryRun 专用报告格式 |
|
||
|
||
**所需库:** 无额外依赖,使用已有标准库
|
||
|
||
**详细功能描述:**
|
||
|
||
#### 4.2.1 DryRun 执行范围
|
||
|
||
DryRun 模式执行流水线的步骤 1-6(解包、校验、解析、平台检查、生成计划、冲突检测),但**不执行**步骤 7-10(确认、还原点、还原、验证)。
|
||
|
||
#### 4.2.2 DryRun 输出格式
|
||
|
||
```
|
||
🔍 Dry Run — "my-env" 还原计划
|
||
|
||
来源: DESKTOP-ABC123 (Windows 11, amd64)
|
||
目标: LAPTOP-XYZ789 (Windows 11, amd64) ✓ 兼容
|
||
|
||
┌─────────────────────┬─────────────┬──────────────────────────────────────┐
|
||
│ 项目 │ 动作 │ 详情 │
|
||
├─────────────────────┼─────────────┼──────────────────────────────────────┤
|
||
│ GOPATH │ SET │ C:\Users\dev\go │
|
||
│ GOPROXY │ SET │ https://goproxy.cn,direct │
|
||
│ Go 1.22.1 │ INSTALL │ 从 golang.org 下载 │
|
||
│ gopls │ INSTALL │ go install golang.org/x/tools/gopls │
|
||
│ Node.js 20.11.0 │ SKIP │ 已安装 (20.11.0) │
|
||
│ npm 全局包 (12) │ INSTALL 8 │ 4 个已存在 │
|
||
│ Python 3.12.2 │ UPGRADE │ 当前: 3.11.7 → 3.12.2 │
|
||
│ VS Code 扩展 (35) │ INSTALL 12 │ 23 个已存在 │
|
||
│ VS Code settings │ MERGE │ 3 个设置项冲突 │
|
||
│ PowerShell 模块 (5) │ INSTALL 3 │ 2 个已存在 │
|
||
│ PowerShell Profile │ OVERWRITE │ 备份为 profile.bak │
|
||
│ Scoop 包 (42) │ INSTALL 8 │ 34 个已存在 │
|
||
│ Git aliases (12) │ MERGE │ 添加 5 个新别名 │
|
||
│ Git user.name │ SKIP │ 保留目标机的用户名 │
|
||
└─────────────────────┴─────────────┴──────────────────────────────────────┘
|
||
|
||
汇总: 安装 31 | 升级 1 | 合并 2 | 覆盖 1 | 跳过 3
|
||
预计耗时: 约 5 分钟(取决于网络速度)
|
||
预计磁盘空间: 约 150 MB
|
||
|
||
⚠ 这是干运行模式,未执行任何更改。
|
||
运行 devpack restore my-env 以实际执行。
|
||
```
|
||
|
||
---
|
||
|
||
### 任务 4.3 — restore 命令实现
|
||
|
||
**需修改的现有文件:**
|
||
|
||
| 文件 | 改动 | 所需库 |
|
||
|---|---|---|
|
||
| `cmd/devpack/commands/restore.go` | 实现完整还原流程:解析 flags → 加载 Pack → 调用 RestoreEngine → 处理冲突 → 输出报告 | `internal/restore`, `internal/pack`, `internal/ui`, `internal/logging`, `internal/collector` |
|
||
|
||
**依赖的内部包:**
|
||
|
||
```
|
||
internal/restore — RestoreEngine
|
||
internal/pack — Pack 解包、校验和验证
|
||
internal/collector — 各采集器 Restore() / Verify()
|
||
internal/ui — 进度条、表格、确认提示
|
||
internal/logging — 日志
|
||
internal/config — 路径
|
||
internal/profile — Profile 加载
|
||
pkg/manifest — Manifest 解析
|
||
```
|
||
|
||
**详细功能描述:**
|
||
|
||
#### 4.3.1 命令参数
|
||
|
||
```
|
||
devpack restore <pack-name> [flags]
|
||
|
||
必选参数:
|
||
pack-name Pack 名称或 .devpack 文件路径
|
||
|
||
可选标志:
|
||
--dry-run 预览更改(不实际执行)
|
||
--conflict <strategy> 冲突处理策略
|
||
skip — 跳过冲突项
|
||
overwrite — 强制覆盖
|
||
merge — 尝试合并(仅配置文件)
|
||
prompt — 逐个交互式询问
|
||
newest — 保留更新的版本
|
||
--no-rollback 不创建还原点
|
||
-c, --collectors 只还原指定采集器
|
||
--exclude 排除指定采集器
|
||
--parallel <n> 并行还原数(默认: 1,串行执行更安全)
|
||
-y, --yes 跳过确认提示
|
||
--force 忽略平台不匹配警告
|
||
--password 解密密码(加密 Pack 用)
|
||
```
|
||
|
||
#### 4.3.2 交互式冲突解决(prompt 策略)
|
||
|
||
使用 `survey` 库逐个询问冲突项:
|
||
|
||
```
|
||
⚠ 冲突: VS Code settings.json
|
||
|
||
当前值 (本机):
|
||
"editor.fontSize": 14
|
||
"editor.tabSize": 4
|
||
|
||
Pack 中的值:
|
||
"editor.fontSize": 16
|
||
"editor.tabSize": 2
|
||
|
||
? 选择处理方式: (Use arrow keys)
|
||
> 跳过 (保留本机)
|
||
覆盖 (使用 Pack 中的)
|
||
合并 (深度合并)
|
||
查看完整差异
|
||
```
|
||
|
||
#### 4.3.3 还原报告
|
||
|
||
```
|
||
✅ 还原完成 — "my-env"
|
||
|
||
┌─────────────────────┬──────────┬──────────────────────────┬──────────┐
|
||
│ 采集器 │ 状态 │ 详情 │ 耗时 │
|
||
├─────────────────────┼──────────┼──────────────────────────┼──────────┤
|
||
│ Environment Vars │ ✓ 成功 │ 设置 4 个变量 │ 0.5s │
|
||
│ Go Runtime │ ✓ 成功 │ 安装 3 个工具 │ 45.2s │
|
||
│ Node.js Runtime │ ⊘ 跳过 │ 版本一致 │ — │
|
||
│ Python Runtime │ ⚠ 部分 │ 2/8 包安装失败 │ 120.3s │
|
||
│ VS Code │ ✓ 成功 │ 安装 12 个扩展 │ 30.1s │
|
||
│ PowerShell │ ✓ 成功 │ 安装 3 个模块 │ 15.7s │
|
||
│ Scoop │ ✓ 成功 │ 安装 8 个包 │ 180.5s │
|
||
│ Git Config │ ✓ 成功 │ 添加 5 个别名 │ 0.3s │
|
||
└─────────────────────┴──────────┴──────────────────────────┴──────────┘
|
||
|
||
成功: 6 | 跳过: 1 | 部分: 1 | 失败: 0
|
||
总耗时: 6 分 32 秒
|
||
|
||
⚠ Python: numpy 和 scipy 安装失败(需要 C 编译器)
|
||
建议: pip install numpy scipy --only-binary=:all:
|
||
```
|
||
|
||
---
|
||
|
||
### 任务 4.4 — 各采集器的 Restore 逻辑
|
||
|
||
此阶段需**回到每个采集器文件**实现完整的 `Restore()` 和 `Verify()` 方法:
|
||
|
||
| 文件 | Restore 逻辑 | 所需命令 |
|
||
|---|---|---|
|
||
| `internal/collector/runtime/go_collector.go` | `go install <tool>@latest` 安装工具 | `os/exec` |
|
||
| `internal/collector/runtime/node_collector.go` | `npm install -g <pkg>@<ver>` 安装全局包 | `os/exec` |
|
||
| `internal/collector/runtime/python_collector.go` | `pip install <pkg>==<ver>` 安装包 | `os/exec` |
|
||
| `internal/collector/editor/vscode_collector.go` | `code --install-extension <id>` + 复制配置文件 | `os/exec`, `io`, `os` |
|
||
| `internal/collector/shell/powershell_collector.go` | `Install-Module -Name <mod>` + 写入 Profile | `os/exec`, `os` |
|
||
| `internal/collector/package/scoop_collector.go` | `scoop bucket add` + `scoop install` | `os/exec` |
|
||
| `internal/collector/git/git_collector.go` | `git config --global <key> <value>` | `os/exec` |
|
||
| `internal/collector/env/env_collector.go` | 调用 `platform.SetEnvVar()` | `internal/platform` |
|
||
|
||
**详细功能描述:**
|
||
|
||
#### 4.4.1 每个采集器的 Restore 返回值
|
||
|
||
```go
|
||
type RestoreResult struct {
|
||
Collector string // 采集器名称
|
||
Status RestoreStatus // Success / Partial / Skipped / Failed
|
||
Items []ItemResult // 每个子项的结果
|
||
Duration time.Duration // 总耗时
|
||
Error error // 整体错误(如有)
|
||
}
|
||
|
||
type ItemResult struct {
|
||
Name string // 如 "gopls", "typescript"
|
||
Action ActionType // INSTALL / SKIP / etc.
|
||
Status string // "ok" / "failed" / "skipped"
|
||
Message string // 补充信息
|
||
Duration time.Duration // 此项耗时
|
||
}
|
||
```
|
||
|
||
#### 4.4.2 Verify 验证逻辑(通用模式)
|
||
|
||
每个采集器的 `Verify()` 方法检查还原后的实际状态是否符合预期:
|
||
|
||
| 采集器 | 验证方法 | 预期结果 |
|
||
|---|---|---|
|
||
| Go | `go version` + 检查 GOPATH/bin | 版本正确,工具存在 |
|
||
| Node.js | `node --version` + `npm list -g --json` | 版本正确,全局包存在 |
|
||
| Python | `python --version` + `pip list --format=json` | 版本正确,包存在 |
|
||
| VS Code | `code --list-extensions --show-versions` | 扩展已安装 |
|
||
| PowerShell | `Get-Module -ListAvailable` | 模块已安装 |
|
||
| Scoop | `scoop list` | 包已安装 |
|
||
| Git | `git config --global --list` | 配置项存在 |
|
||
| Env | `os.Getenv(key)` | 变量值正确 |
|
||
|
||
#### 4.4.3 回滚支持
|
||
|
||
每个采集器可选实现 `Rollback()` 方法:
|
||
|
||
```go
|
||
// 可选接口,不强制实现
|
||
type Rollbackable interface {
|
||
Rollback(ctx context.Context, restorePointDir string) error
|
||
}
|
||
```
|
||
|
||
支持回滚的采集器:
|
||
- VS Code: 恢复备份的 settings.json / keybindings.json
|
||
- PowerShell: 恢复备份的 Profile 脚本
|
||
- Git: 恢复备份的 .gitconfig
|
||
- Env: 恢复备份的环境变量值
|
||
|
||
不支持回滚的采集器(只能手动卸载):
|
||
- Go tools: `go install` 安装的工具无自动卸载
|
||
- Node.js packages: 需要 `npm uninstall -g`
|
||
- Scoop packages: 需要 `scoop uninstall`
|
||
|
||
---
|
||
|
||
## M5: 配置与打磨(第 11-12 周)
|
||
|
||
### 任务 5.1 — profile 命令实现
|
||
|
||
**需修改的现有文件:**
|
||
|
||
| 文件 | 改动 | 所需库 |
|
||
|---|---|---|
|
||
| `cmd/devpack/commands/profile.go` | 实现 create/show/list/delete/use 五个子命令的完整逻辑 | `internal/profile`, `internal/config`, `internal/ui`, `internal/logging` |
|
||
|
||
**依赖的内部包:**
|
||
|
||
```
|
||
internal/profile — Profile CRUD 操作
|
||
internal/config — 路径
|
||
internal/ui — 表格、颜色输出
|
||
internal/logging — 日志
|
||
|
||
标准库:
|
||
fmt — 输出
|
||
os — 文件操作
|
||
path/filepath — 路径
|
||
```
|
||
|
||
**详细功能描述:**
|
||
|
||
#### 5.1.1 五个子命令
|
||
|
||
**`devpack profile create <name>`:**
|
||
```
|
||
--template <type> 模板类型 (minimal/standard/full),默认 standard
|
||
--description <desc> 描述信息
|
||
|
||
流程:
|
||
1. 验证 name 合法性
|
||
2. 检查同名 Profile 是否已存在
|
||
3. 从模板创建 Profile
|
||
4. 填入用户提供的 description
|
||
5. 保存到 ~/.devpack/profiles/<name>.yaml
|
||
6. 如果是唯一 Profile,自动设为默认
|
||
输出: "✓ Profile 'web-dev' 已创建"
|
||
```
|
||
|
||
**`devpack profile show <name>`:**
|
||
```
|
||
流程:
|
||
1. 加载 Profile
|
||
2. 输出 YAML 内容(带语法高亮:键=蓝色,值=白色,注释=灰色)
|
||
```
|
||
|
||
**`devpack profile list`:**
|
||
```
|
||
输出格式:
|
||
┌────────────┬────────────────────────┬──────────┬──────────────┐
|
||
│ Name │ Description │ Collectors │ Modified │
|
||
├────────────┼────────────────────────┼──────────┼──────────────┤
|
||
│ * standard │ 标准开发环境配置 │ 8 │ 2026-03-03 │
|
||
│ minimal │ 最小环境(仅运行时+包) │ 3 │ 2026-03-01 │
|
||
│ full │ 完整环境(含加密数据) │ 12 │ 2026-03-02 │
|
||
└────────────┴────────────────────────┴──────────┴──────────────┘
|
||
|
||
* 表示当前默认 Profile
|
||
```
|
||
|
||
**`devpack profile delete <name>`:**
|
||
```
|
||
流程:
|
||
1. 检查 Profile 是否存在
|
||
2. 检查是否为当前默认 → 拒绝删除,提示先切换默认
|
||
3. 确认提示 "确定删除 Profile 'web-dev'?[y/N]"
|
||
4. 删除文件
|
||
输出: "✓ Profile 'web-dev' 已删除"
|
||
```
|
||
|
||
**`devpack profile use <name>`:**
|
||
```
|
||
流程:
|
||
1. 检查 Profile 存在
|
||
2. 更新 config.yaml 的 default_profile 字段
|
||
3. 保存配置
|
||
输出: "✓ 默认 Profile 已切换到 'web-dev'"
|
||
```
|
||
|
||
---
|
||
|
||
### 任务 5.2 — list 命令实现
|
||
|
||
**需修改的现有文件:**
|
||
|
||
| 文件 | 改动 | 所需库 |
|
||
|---|---|---|
|
||
| `cmd/devpack/commands/list.go` | 实现 packs/profiles/collectors 列表展示,支持 `--format` 标志 | `internal/pack`, `internal/profile`, `internal/collector`, `internal/ui` |
|
||
|
||
**所需标准库:**
|
||
|
||
```
|
||
encoding/json — --format json 输出
|
||
fmt — 格式化
|
||
path/filepath — 扫描 Packs 目录
|
||
os — 读取目录
|
||
time — Pack 创建时间
|
||
```
|
||
|
||
**详细功能描述:**
|
||
|
||
#### 5.2.1 三个子命令
|
||
|
||
**`devpack list packs`:**
|
||
```
|
||
┌──────────────┬──────────────┬──────────┬──────────┬────────────────┐
|
||
│ Name │ Created │ Size │ Items │ Platform │
|
||
├──────────────┼──────────────┼──────────┼──────────┼────────────────┤
|
||
│ my-env │ 2026-03-03 │ 8.2 MB │ 115 │ windows/amd64 │
|
||
│ work-env │ 2026-03-01 │ 5.1 MB │ 67 │ windows/amd64 │
|
||
│ home-setup │ 2026-02-28 │ 12.3 MB │ 180 │ windows/amd64 │
|
||
└──────────────┴──────────────┴──────────┴──────────┴────────────────┘
|
||
3 个 Pack
|
||
```
|
||
|
||
**`devpack list profiles`:** 同 `devpack profile list`
|
||
|
||
**`devpack list collectors`:**
|
||
```
|
||
┌────────────────┬──────────┬──────────────────────────────┬──────────┐
|
||
│ Name │ Category │ Description │ Status │
|
||
├────────────────┼──────────┼──────────────────────────────┼──────────┤
|
||
│ go │ runtime │ Go 运行时和工具 │ ✓ 可用 │
|
||
│ node │ runtime │ Node.js 和全局 npm 包 │ ✓ 可用 │
|
||
│ python │ runtime │ Python 和 pip 包 │ ✗ 不可用 │
|
||
│ vscode │ editor │ VS Code 扩展和设置 │ ✓ 可用 │
|
||
│ powershell │ shell │ PowerShell 模块和 Profile │ ✓ 可用 │
|
||
│ scoop │ package │ Scoop 包管理器 │ ✓ 可用 │
|
||
│ git │ config │ Git 全局配置 │ ✓ 可用 │
|
||
│ env │ config │ 环境变量 │ ✓ 可用 │
|
||
└────────────────┴──────────┴──────────────────────────────┴──────────┘
|
||
7/8 个采集器可用
|
||
```
|
||
|
||
---
|
||
|
||
### 任务 5.3 — diff 命令实现
|
||
|
||
**需修改的现有文件:**
|
||
|
||
| 文件 | 改动 | 所需库 |
|
||
|---|---|---|
|
||
| `cmd/devpack/commands/diff.go` | 实现环境对比逻辑:Pack vs 当前环境 / Pack vs Pack | `internal/pack`, `internal/collector`, `internal/ui` |
|
||
|
||
**新建文件:**
|
||
|
||
| 文件 | 说明 |
|
||
|---|---|
|
||
| `internal/diff/differ.go` | 差异计算引擎:逐项对比、输出 added/removed/changed |
|
||
| `internal/diff/differ_test.go` | 差异引擎单元测试 |
|
||
|
||
**所需库:**
|
||
|
||
```
|
||
标准库:
|
||
encoding/json — 数据加载
|
||
fmt — 输出
|
||
strings — 版本对比
|
||
sort — 排序
|
||
|
||
内部依赖:
|
||
internal/ui — 彩色差异输出 (红色-/绿色+/黄色~)
|
||
```
|
||
|
||
**详细功能描述:**
|
||
|
||
#### 5.3.1 差异计算算法
|
||
|
||
```
|
||
输入: sourceItems []ScanItem, targetItems []ScanItem
|
||
|
||
算法:
|
||
1. 建立两个 Map: sourceMap[name] = item, targetMap[name] = item
|
||
2. 遍历 sourceMap:
|
||
if name 不在 targetMap → DiffType = REMOVED (红色 -)
|
||
if name 在 targetMap:
|
||
if version 相同 → DiffType = SAME (不显示或灰色 =)
|
||
if version 不同 → DiffType = CHANGED (黄色 ~)
|
||
3. 遍历 targetMap:
|
||
if name 不在 sourceMap → DiffType = ADDED (绿色 +)
|
||
4. 按 DiffType 分组排序
|
||
|
||
输出:
|
||
[]DiffItem{Name, Type, SourceVersion, TargetVersion}
|
||
```
|
||
|
||
#### 5.3.2 版本比较
|
||
|
||
版本比较支持语义化版本(SemVer):
|
||
- `1.2.3` < `1.3.0` < `2.0.0`
|
||
- 非 SemVer 格式(如日期版本 `20240101`)→ 字符串比较
|
||
- 版本带前缀 `v`(如 `v1.22.1`)→ 去掉 `v` 后比较
|
||
|
||
#### 5.3.3 diff 输出格式
|
||
|
||
```
|
||
📊 环境对比: "my-env" vs 当前环境
|
||
|
||
运行时:
|
||
= Go Runtime 1.22.1 → 1.22.1 (一致)
|
||
~ Node.js Runtime 20.11.0 → 20.12.0 (已升级)
|
||
- Python Runtime 3.12.2 → 未安装 (缺失)
|
||
|
||
VS Code 扩展:
|
||
+ ms-vscode.live-server — → 0.4.13 (新增)
|
||
- ms-azuretools.vscode-docker 0.39.0 → — (缺失)
|
||
~ golang.go 0.40.0 → 0.41.0 (已升级)
|
||
= ms-python.python 2024.0.1 (一致)
|
||
|
||
Scoop 包:
|
||
+ neovim — → 0.9.5 (新增)
|
||
- telegram 4.14.0 → — (缺失)
|
||
|
||
汇总:
|
||
一致: 85 | 已变更: 8 | 新增: 12 | 缺失: 10
|
||
```
|
||
|
||
---
|
||
|
||
### 任务 5.4 — 测试覆盖率提升
|
||
|
||
**新建文件:**
|
||
|
||
| 文件 | 说明 |
|
||
|---|---|
|
||
| `internal/pack/engine_integration_test.go` | Pack 引擎集成测试:完整的打包→解包流程 |
|
||
| `internal/restore/engine_integration_test.go` | 还原引擎集成测试 |
|
||
| `test/e2e/capture_restore_test.go` | 端到端测试:capture → export → import → restore |
|
||
|
||
**所需库:**
|
||
|
||
```
|
||
第三方库:
|
||
github.com/stretchr/testify/assert — 断言
|
||
github.com/stretchr/testify/require — 必要条件断言
|
||
github.com/stretchr/testify/mock — Mock (模拟外部命令)
|
||
|
||
标准库:
|
||
testing — Go 测试框架
|
||
os — 临时目录
|
||
path/filepath — 路径
|
||
io/fs — 文件遍历
|
||
```
|
||
|
||
**详细功能描述:**
|
||
|
||
#### 5.4.1 测试策略
|
||
|
||
| 测试层级 | 覆盖目标 | 方法 |
|
||
|---|---|---|
|
||
| **单元测试** | 每个模块的核心逻辑 | Mock 外部命令(os/exec) |
|
||
| **集成测试** | 模块间交互 | 使用 mock collector,真实 Pack 引擎 |
|
||
| **端到端测试** | 完整 capture → restore 流程 | 使用测试用 mock collector |
|
||
|
||
#### 5.4.2 Mock 外部命令策略
|
||
|
||
通过接口抽象 `os/exec` 调用,测试时注入 mock:
|
||
|
||
```go
|
||
// 命令执行器接口
|
||
type CommandRunner interface {
|
||
Run(ctx context.Context, name string, args ...string) (stdout string, stderr string, err error)
|
||
}
|
||
|
||
// 真实实现
|
||
type ExecRunner struct{}
|
||
|
||
// Mock 实现
|
||
type MockRunner struct {
|
||
responses map[string]mockResponse // key = "go version", value = {stdout, stderr, err}
|
||
}
|
||
```
|
||
|
||
#### 5.4.3 目标覆盖率
|
||
|
||
| 包 | 目标覆盖率 | 说明 |
|
||
|---|---|---|
|
||
| `internal/logging` | > 90% | ✅ 已达成(50 个测试) |
|
||
| `internal/config` | > 90% | ✅ 已达成(12 个测试) |
|
||
| `internal/profile` | > 80% | CRUD + 验证 |
|
||
| `internal/collector/*` | > 70% | 主要测试解析逻辑,外部命令通过 Mock |
|
||
| `internal/pack` | > 80% | 归档/校验和核心逻辑 |
|
||
| `internal/restore` | > 80% | 计划生成/冲突检测 |
|
||
| `internal/diff` | > 85% | 差异计算算法 |
|
||
| `internal/ui` | > 60% | UI 组件难以完全测试 |
|
||
|
||
---
|
||
|
||
### 任务 5.5 — version 命令增强
|
||
|
||
**需修改的现有文件:**
|
||
|
||
| 文件 | 改动 | 所需库 |
|
||
|---|---|---|
|
||
| `cmd/devpack/commands/version.go` | 添加 `--check-update` 标志,从 GitHub API 检查最新版本 | `net/http`, `encoding/json` |
|
||
| `pkg/version/version.go` | 添加 `CheckLatest()` 方法 | `net/http`, `encoding/json`, `fmt` |
|
||
|
||
**详细功能描述:**
|
||
|
||
#### 5.5.1 版本检查逻辑
|
||
|
||
```go
|
||
func CheckLatest() (*UpdateInfo, error) {
|
||
// 1. GET https://api.github.com/repos/user/devpack/releases/latest
|
||
// 2. 解析 JSON 响应,提取 tag_name
|
||
// 3. 比较当前版本和最新版本(语义化版本对比)
|
||
// 4. 返回 UpdateInfo{Available bool, CurrentVersion, LatestVersion, ReleaseURL}
|
||
}
|
||
```
|
||
|
||
#### 5.5.2 输出示例
|
||
|
||
```
|
||
devpack version v0.1.0 (commit: abc1234, built: 2026-03-03T10:00:00Z)
|
||
|
||
✓ 已是最新版本
|
||
```
|
||
|
||
或:
|
||
|
||
```
|
||
devpack version v0.1.0 (commit: abc1234, built: 2026-03-03T10:00:00Z)
|
||
|
||
⚠ 有新版本可用: v0.2.0
|
||
下载: https://github.com/user/devpack/releases/tag/v0.2.0
|
||
更新: scoop update devpack
|
||
```
|
||
|
||
---
|
||
|
||
## 文件总览与新增文件清单
|
||
|
||
### 新建文件列表 (按模块分组)
|
||
|
||
```
|
||
internal/
|
||
├── logging/
|
||
│ ├── logger.go ✅ zerolog 封装(已完成)
|
||
│ └── logger_test.go ✅ 50 个测试(已完成)
|
||
├── config/
|
||
│ ├── config.go ✅ AppConfig 配置结构(已完成)
|
||
│ ├── config_test.go ✅ 12 个测试(已完成)
|
||
│ └── paths.go ✅ 路径管理(已完成)
|
||
├── profile/
|
||
│ ├── profile.go # Profile CRUD
|
||
│ ├── profile_test.go
|
||
│ └── templates.go # 预设模板 (minimal/standard/full)
|
||
├── ui/
|
||
│ ├── printer.go # 统一输出工具 (Success/Error/Warn/Info)
|
||
│ ├── table.go # 表格渲染
|
||
│ ├── spinner.go # 加载动画 (Braille 点阵)
|
||
│ └── progress.go # 进度条
|
||
├── errors/
|
||
│ ├── errors.go # DevPackError + 错误码体系
|
||
│ └── errors_test.go
|
||
├── collector/
|
||
│ ├── register.go # RegisterBuiltins() 集中注册
|
||
│ ├── runtime/
|
||
│ │ ├── go_collector.go # Go: 版本/env/GOPATH/bin 工具
|
||
│ │ ├── go_collector_test.go
|
||
│ │ ├── node_collector.go # Node.js: 版本/npm全局包/nvm
|
||
│ │ ├── node_collector_test.go
|
||
│ │ ├── python_collector.go # Python: 版本/pip包/pyenv
|
||
│ │ └── python_collector_test.go
|
||
│ ├── editor/
|
||
│ │ ├── vscode_collector.go # VS Code: 扩展/settings/snippets
|
||
│ │ └── vscode_collector_test.go
|
||
│ ├── shell/
|
||
│ │ ├── powershell_collector.go # PowerShell: 模块/Profile
|
||
│ │ └── powershell_collector_test.go
|
||
│ ├── package/
|
||
│ │ ├── scoop_collector.go # Scoop: 包/bucket (Windows)
|
||
│ │ └── scoop_collector_test.go
|
||
│ ├── git/
|
||
│ │ ├── git_collector.go # Git: 全局配置/别名
|
||
│ │ └── git_collector_test.go
|
||
│ └── env/
|
||
│ ├── env_collector.go # 环境变量: 过滤/PATH段提取
|
||
│ └── env_collector_test.go
|
||
├── pack/
|
||
│ ├── engine.go # PackEngine (10步创建流程)
|
||
│ ├── engine_test.go
|
||
│ ├── engine_integration_test.go
|
||
│ ├── archive.go # tar.gz 归档 (安全解压)
|
||
│ ├── archive_test.go
|
||
│ ├── checksum.go # SHA-256 校验和
|
||
│ └── checksum_test.go
|
||
├── restore/
|
||
│ ├── engine.go # RestoreEngine (10步还原流水线)
|
||
│ ├── engine_test.go
|
||
│ ├── engine_integration_test.go
|
||
│ ├── plan.go # RestorePlan + 拓扑排序
|
||
│ ├── plan_test.go
|
||
│ ├── conflict.go # 冲突检测 (5种策略)
|
||
│ ├── conflict_test.go
|
||
│ └── report.go # 还原报告
|
||
├── diff/
|
||
│ ├── differ.go # 差异引擎 (added/removed/changed)
|
||
│ └── differ_test.go
|
||
├── platform/
|
||
│ └── platform_test.go # 平台层测试
|
||
test/
|
||
└── e2e/
|
||
└── capture_restore_test.go # 端到端测试
|
||
```
|
||
|
||
**新增文件总计:约 45 个**
|
||
|
||
---
|
||
|
||
## go.mod 最终依赖
|
||
|
||
完成 MVP 后,`go.mod` 应包含以下直接依赖:
|
||
|
||
```go
|
||
require (
|
||
// CLI 框架 (已有)
|
||
github.com/spf13/cobra v1.8.0
|
||
github.com/spf13/viper v1.18.2
|
||
|
||
// 日志
|
||
github.com/rs/zerolog v1.32.0
|
||
|
||
// 终端 UI
|
||
github.com/fatih/color v1.16.0
|
||
github.com/olekukonenko/tablewriter v0.0.5
|
||
github.com/schollz/progressbar/v3 v3.14.2
|
||
|
||
// 交互式提示
|
||
github.com/AlecAivazis/survey/v2 v2.3.7
|
||
|
||
// 数据格式
|
||
gopkg.in/yaml.v3 v3.0.1
|
||
|
||
// 工具
|
||
github.com/google/uuid v1.6.0
|
||
|
||
// 测试
|
||
github.com/stretchr/testify v1.9.0
|
||
)
|
||
```
|
||
|
||
v0.3.0 版本额外添加:
|
||
|
||
```go
|
||
require (
|
||
golang.org/x/crypto v0.21.0 // Argon2id 密钥派生
|
||
golang.org/x/sys v0.18.0 // Windows 注册表操作
|
||
)
|
||
```
|
||
|
||
---
|
||
|
||
## 开发顺序建议
|
||
|
||
```
|
||
Week 1-2: M1 (logging ✅ → config ✅ → profile → ui → errors → init ✅ → 平台完善)
|
||
↓
|
||
Week 3-5: M2 (go → node → python → vscode → powershell → scoop → git → env → scan)
|
||
↓
|
||
Week 6-7: M3 (archive → checksum → pack engine → capture → export/import)
|
||
↓
|
||
Week 8-10: M4 (plan → conflict → restore engine → 各采集器 restore → restore cmd)
|
||
↓
|
||
Week 11-12: M5 (profile cmd → list → diff → tests → 打磨)
|
||
```
|
||
|
||
每完成一个模块就跑 `go test ./...` 确保不引入回归。
|