Files
DevPack/docs/DEVELOPMENT-PLAN.md
2026-03-05 13:43:11 +08:00

3050 lines
105 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 + templateminimal/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 | ❌ | ✅ | ✅ |
| ShellPowerShell/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. 获取 GOPATHgo 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. 按分隔符拆分 PATHWindows: ";", 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 varsGOPATH 等需要先设置)
packages ← 依赖 runtimesscoop install 可能需要 git
editors ← 依赖 runtimesVS Code 扩展可能需要语言运行时)
shells ← 依赖 packagesProfile 脚本可能引用已安装的工具)
git config ← 依赖 runtimesgit 需要先安装)
拓扑序(默认):
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 ./...` 确保不引入回归。