105 KiB
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 的"打包规则定义文件",控制哪些采集器启用、如何过滤数据。完整结构如下:
# ~/.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 结构体定义
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(创建):
- 接收 name + template(minimal/standard/full)
- 检查 name 合法性和唯一性
- 从
templates.go获取模板内容 - 填入用户自定义字段
- 运行
Validate()验证 - 写入
~/.devpack/profiles/{name}.yaml - 如果是第一个 Profile,自动设为默认
Load(加载):
- 拼接路径
~/.devpack/profiles/{name}.yaml - 读取文件内容
- YAML 反序列化到
Profile结构体 - 运行
Validate()验证结构完整性 - 返回
*Profile或 error
Save(保存):
- 运行
Validate()验证 - YAML 序列化
- 原子写入(先写临时文件,再 rename,避免写入中途断电损坏)
Delete(删除):
- 检查该 Profile 是否存在
- 如果是当前默认 Profile,拒绝删除(或提示用户先切换默认)
- 删除
~/.devpack/profiles/{name}.yaml
List(列出):
- 扫描
~/.devpack/profiles/目录 - 读取每个
.yaml文件的 name + description 字段 - 返回
[]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 接口中添加以下方法签名:
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:
- 复制字体文件到
C:\Windows\Fonts\(需要管理员权限) - 在注册表
HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts添加条目 - 注册表键名 = 字体显示名称 + " (TrueType)",值 = 文件名
- 调用
SendMessage(HWND_BROADCAST, WM_FONTCHANGE)通知其他程序 - 如果非管理员,自动调用
RunAsAdmin提权重试
macOS:
- 复制字体文件到
~/Library/Fonts/(用户级,无需管理员) - 无需额外注册,macOS 自动识别该目录中的字体文件
- 如需全局安装:复制到
/Library/Fonts/(需管理员权限)
Linux:
- 复制字体文件到
~/.local/share/fonts/(用户级) - 执行
fc-cache -f -v刷新字体缓存 - 验证:
fc-list | grep <font-name>确认已安装
边界情况处理:
- 字体文件不存在或格式无效(仅支持 .ttf / .otf / .woff2)→ 返回明确错误
- 字体已安装(同名文件已存在)→ 跳过并记录日志
- 权限不足 → 尝试
RunAsAdmin提权,失败则返回E5001错误 - 磁盘空间不足 → 预检查可用空间
1.4.3 RunAsAdmin 各平台实现
Windows:
// 使用 ShellExecute "runas" 动词
cmd := exec.Command("cmd", "/C", command)
cmd.SysProcAttr = &syscall.SysProcAttr{
HideWindow: true,
}
// 或者调用 ShellExecuteEx API 通过 "runas" 触发 UAC 弹窗
- 用户取消 UAC 弹窗 → 返回
ErrUserCancelled错误 - 检测是否已有管理员权限 → 如果已有则直接执行
macOS:
osascript -e 'do shell script "<command>" with administrator privileges'
- 系统弹窗要求输入密码
- 用户取消 → osascript 退出码非零
Linux:
# 优先尝试 pkexec (图形化密码弹窗)
pkexec <command>
# 回退到 sudo (终端密码输入)
sudo <command>
- 检测
pkexec是否可用,不可用则回退sudo sudo需要终端 TTY,如果没有 TTY 则报错
1.4.4 SetEnvVar 增强
Windows(永久设置):
- 打开注册表
HKCU\Environment - 设置
REG_SZ或REG_EXPAND_SZ类型的键值 - 调用
SendMessageTimeout(HWND_BROADCAST, WM_SETTINGCHANGE, 0, "Environment")广播变更 - PATH 变量特殊处理:追加而非覆盖,使用
;分隔
macOS / Linux(永久设置):
- 检测当前 Shell 类型(bash/zsh/fish)
- 向对应 profile 文件追加
export KEY="VALUE"行 - Shell profile 文件优先级:
- Bash:
~/.bashrc>~/.bash_profile>~/.profile - Zsh:
~/.zshrc - Fish:
~/.config/fish/config.fish(语法:set -gx KEY VALUE)
- Bash:
- 检查是否已有同名 export 行,有则替换而非追加
- 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.yamlwriteDefaultProfile()支持三种模板 (--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 设计:
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 设计:
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 设计:
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 结构体
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: 配置文件解析失败 E1002: Profile 格式无效 E1003: 路径不存在 E1010: Profile 名称非法 E1011: Profile 名称重复 E1012: 采集器名称未注册 E1013: enabled/disabled 冲突 E1016: Profile 版本不兼容 |
| E2xxx | 采集器错误 | E2001: 工具未安装(如 go 命令不存在)E2002: 命令执行失败(退出码非零) E2003: 输出解析错误(JSON 格式异常) E2004: 操作超时(超过设定的 timeout) E2005: 权限不足(需要管理员权限) E2006: 版本不兼容 |
| E3xxx | 打包错误 | E3001: 归档创建失败 E3002: 校验和不匹配 E3003: Manifest 格式无效 E3004: 压缩失败 E3005: Pack 文件过大 |
| E4xxx | 还原错误 | E4001: Pack 文件不存在 E4002: 平台不匹配(Windows → macOS) E4003: 冲突未解决 E4004: 安装失败 E4005: 回滚失败 E4006: 还原点创建失败 |
| E5xxx | 系统错误 | E5001: 权限不足 E5002: 磁盘空间不足 E5003: 网络连接失败 E5004: 文件系统错误 E5005: 用户取消操作 |
1.7.3 辅助构造函数
// 快速创建错误
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 错误聚合器
用于并发扫描/还原等场景,收集多个采集器的错误:
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/GOMODCACHEGOPROXY/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:
{
"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:
[
{"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 输出格式:
{
"dependencies": {
"typescript": { "version": "5.3.3" },
"ts-node": { "version": "10.9.2" },
"nodemon": { "version": "3.0.2" },
"@angular/cli": { "version": "17.1.0" }
}
}
解析逻辑:
- 反序列化为
map[string]interface{} - 遍历
dependencies字典,提取name和version - 过滤掉
npm自身(npm 随 node 安装,不单独还原) - 按名称排序
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:
{
"node_version": "20.11.0",
"npm_version": "10.2.4",
"version_manager": "fnm",
"yarn_version": "1.22.21",
"pnpm_version": null
}
global-packages.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
解析逻辑:
- 按行分割,每行格式
<publisher>.<name>@<version> - 过滤掉 Profile 中
exclude_extensions匹配的扩展 - 识别扩展类别:语言支持、主题、工具、远程开发
- 记录扩展总数和预计还原时间(约 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 模块扫描
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 格式:
{
"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:
[
{"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:
[
{"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:
{
"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:
{
"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 注册机制设计
// 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 并发扫描控制
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
{
"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 核心函数
// 创建 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 接口
type ProgressTracker interface {
SetTotal(total int) // 设置总步骤数
Increment() // 增加一步
SetMessage(message string) // 更新当前步骤描述
SetCurrent(step int, message string) // 设置当前步骤(用于多阶段)
Done() // 完成
Fail(message string) // 失败
}
3.4.2 使用示例
// 打包进度
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 数据结构
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 返回值
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() 方法:
// 可选接口,不强制实现
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:
// 命令执行器接口
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 版本检查逻辑
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 应包含以下直接依赖:
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 版本额外添加:
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 ./... 确保不引入回归。