diff --git a/docs/DEVELOPMENT-PLAN.md b/docs/DEVELOPMENT-PLAN.md index 2bbe7e5..2f258c5 100644 --- a/docs/DEVELOPMENT-PLAN.md +++ b/docs/DEVELOPMENT-PLAN.md @@ -28,7 +28,7 @@ ## M1: 项目基础架构(第 1-2 周) -### 任务 1.1 — 日志系统 +### 任务 1.1 — 日志系统 ✅ 已完成 **新建文件:** @@ -64,9 +64,23 @@ | `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 — 配置系统 +### 任务 1.2 — 配置系统 ✅ 已完成 **新建文件:** @@ -95,6 +109,21 @@ | `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 系统核心 @@ -120,6 +149,152 @@ strings — 字符串处理 ``` +**详细功能描述:** + +#### 1.3.1 Profile YAML 结构定义 + +Profile 是 DevPack 的"打包规则定义文件",控制哪些采集器启用、如何过滤数据。完整结构如下: + +```yaml +# ~/.devpack/profiles/standard.yaml +name: "standard" +description: "标准开发环境配置,包含常用运行时和编辑器" +version: 1 # Profile 格式版本号 + +# 采集器配置 +collectors: + enabled: # 启用的采集器列表(空=全部启用) + - go + - node + - python + - vscode + - powershell + - scoop + - git + - env + disabled: # 显式禁用的采集器 + - ssh # 敏感数据默认不采集 + - font + +# 按采集器的细粒度设置 +settings: + go: + include_tools: true # 是否采集 GOPATH/bin 下的工具 + tools_filter: # 只采集匹配的工具(glob 模式) + - "gopls" + - "dlv" + - "staticcheck" + node: + include_global_packages: true # 是否采集全局 npm 包 + exclude_packages: # 排除的包名 + - "npm" # npm 自身不需要采集 + vscode: + include_settings: true # 采集 settings.json + include_keybindings: true # 采集 keybindings.json + include_snippets: true # 采集代码片段 + exclude_extensions: # 排除的扩展 ID + - "ms-vscode.remote-*" # 远程相关扩展不迁移 + env: + include_patterns: # 要采集的环境变量名模式 + - "GOPATH" + - "GOROOT" + - "JAVA_HOME" + - "NODE_*" + - "PYTHON*" + exclude_patterns: # 排除的变量名模式 + - "*SECRET*" + - "*TOKEN*" + - "*PASSWORD*" + - "*KEY*" + scoop: + include_buckets: true # 是否采集 bucket 列表 + exclude_packages: [] # 排除的包名 + git: + include_aliases: true # 采集 git 别名 + skip_credentials: true # 跳过凭据信息 +``` + +#### 1.3.2 Profile 结构体定义 + +```go +type Profile struct { + Name string `yaml:"name"` + Description string `yaml:"description"` + Version int `yaml:"version"` + Collectors CollectorConfig `yaml:"collectors"` + Settings map[string]Setting `yaml:"settings"` // key = 采集器名称 +} + +type CollectorConfig struct { + Enabled []string `yaml:"enabled"` // 空列表 = 启用全部 + Disabled []string `yaml:"disabled"` // 显式禁用 +} + +type Setting struct { + Options map[string]interface{} `yaml:",inline"` // 各采集器自定义选项 +} +``` + +#### 1.3.3 验证规则 + +| 规则 | 说明 | 错误码 | +|---|---|---| +| `name` 非空 | 必须是 1-64 字符,仅允许 `[a-zA-Z0-9_-]` | E1010 | +| `name` 唯一 | 同一 profiles 目录下不能重名 | E1011 | +| `enabled` 合法 | 列表中的名称必须是注册过的采集器名称 | E1012 | +| `disabled` 合法 | 同上 | E1012 | +| `enabled` 与 `disabled` 不冲突 | 同一采集器不能同时出现在两个列表中 | E1013 | +| `settings` 键存在 | settings 下的键必须对应已启用的采集器 | E1014 | +| `include/exclude` 模式合法 | glob 模式可被 `filepath.Match` 解析 | E1015 | +| `version` 兼容 | Profile 版本 ≤ 当前支持的最大版本 | E1016 | + +#### 1.3.4 CRUD 操作详细流程 + +**Create(创建):** +1. 接收 name + template(minimal/standard/full) +2. 检查 name 合法性和唯一性 +3. 从 `templates.go` 获取模板内容 +4. 填入用户自定义字段 +5. 运行 `Validate()` 验证 +6. 写入 `~/.devpack/profiles/{name}.yaml` +7. 如果是第一个 Profile,自动设为默认 + +**Load(加载):** +1. 拼接路径 `~/.devpack/profiles/{name}.yaml` +2. 读取文件内容 +3. YAML 反序列化到 `Profile` 结构体 +4. 运行 `Validate()` 验证结构完整性 +5. 返回 `*Profile` 或 error + +**Save(保存):** +1. 运行 `Validate()` 验证 +2. YAML 序列化 +3. 原子写入(先写临时文件,再 rename,避免写入中途断电损坏) + +**Delete(删除):** +1. 检查该 Profile 是否存在 +2. 如果是当前默认 Profile,拒绝删除(或提示用户先切换默认) +3. 删除 `~/.devpack/profiles/{name}.yaml` + +**List(列出):** +1. 扫描 `~/.devpack/profiles/` 目录 +2. 读取每个 `.yaml` 文件的 name + description 字段 +3. 返回 `[]ProfileMeta{Name, Description, Path, ModTime}` + +#### 1.3.5 三种模板内容对比 + +| 项目 | minimal | standard | full | +|---|---|---|---| +| 运行时(Go/Node/Python) | ✅ | ✅ | ✅ | +| 包管理器(Scoop/Brew) | ✅ | ✅ | ✅ | +| 编辑器(VS Code) | ❌ | ✅ | ✅ | +| Shell(PowerShell/Bash) | ❌ | ✅ | ✅ | +| Git 配置 | ❌ | ✅ | ✅ | +| 环境变量 | ❌ | ❌ | ✅ | +| SSH/GPG 密钥 | ❌ | ❌ | ✅(加密) | +| 字体 | ❌ | ❌ | ✅ | +| 自定义脚本 | ❌ | ❌ | ✅ | + --- ### 任务 1.4 — 平台适配层完善 @@ -146,9 +321,105 @@ 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 ` 确认已安装 + +**边界情况处理:** +- 字体文件不存在或格式无效(仅支持 .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 "" with administrator privileges' +``` +- 系统弹窗要求输入密码 +- 用户取消 → osascript 退出码非零 + +**Linux:** +```bash +# 优先尝试 pkexec (图形化密码弹窗) +pkexec +# 回退到 sudo (终端密码输入) +sudo +``` +- 检测 `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:"` 追加 + --- -### 任务 1.5 — init 命令实现 +### 任务 1.5 — init 命令实现 ✅ 已完成 **需修改的现有文件:** @@ -169,6 +440,18 @@ internal/logging — 日志记录 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 工具 @@ -203,6 +486,142 @@ internal/logging — 日志记录 |---|---| | `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 — 错误处理系统 @@ -223,6 +642,93 @@ internal/logging — 日志记录 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: 配置文件解析失败
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 辅助构造函数 + +```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 周) @@ -250,11 +756,104 @@ internal/logging — 日志记录 fmt — 格式化 ``` -**实现逻辑:** -- `Scan()`: `go version` 获取版本,`go env -json` 获取环境,遍历 `GOPATH/bin` 获取安装的工具 -- `Capture()`: 将 metadata.json (版本+env) + go-tools.json (工具列表) 写入目标目录 -- `Restore()`: 检测 Go 是否安装 → 安装缺失工具 (`go install xxx@latest`) -- `Verify()`: 验证 Go 版本和工具是否存在 +**详细功能描述:** + +#### 2.1.1 IsAvailable 检测逻辑 + +``` +1. exec.LookPath("go") 检查 go 命令是否在 PATH 中 +2. 如果找到,执行 `go version` 确认可用 +3. 返回 true/false +``` + +#### 2.1.2 Scan 扫描数据结构 + +扫描结果写入 `ScanResult`,包含以下 `ScanItem` 条目: + +| ScanItem.Name | Type | 数据来源 | Properties 示例 | +|---|---|---|---| +| `Go Runtime` | binary | `go version` | `{"version": "1.22.1", "os": "windows", "arch": "amd64"}` | +| `GOPATH` | config | `go env GOPATH` | `{"path": "/Users/dev/go"}` | +| `GOROOT` | config | `go env GOROOT` | `{"path": "/usr/local/go"}` | +| `Go Env (full)` | config | `go env -json` | 完整的 go env 输出(约 30 个键值对) | +| `gopls` | binary | GOPATH/bin 扫描 | `{"module": "golang.org/x/tools/gopls", "version": "v0.15.0"}` | +| `dlv` | binary | GOPATH/bin 扫描 | `{"module": "github.com/go-delve/delve/cmd/dlv"}` | + +**go env -json 重要键值:** +- `GOPATH` / `GOROOT` / `GOBIN` / `GOCACHE` / `GOMODCACHE` +- `GOPROXY` / `GONOSUMDB` / `GONOPROXY`(代理设置对中国大陆用户尤为重要) +- `CGO_ENABLED` / `CC` / `CXX` + +#### 2.1.3 GOPATH/bin 工具检测算法 + +``` +1. 获取 GOPATH(go env GOPATH),默认 ~/go +2. 扫描 GOPATH/bin/ 目录(Windows 下为 GOPATH\bin\) +3. 对每个可执行文件: + a. 跳过名为 "go" 和 "gofmt" 的文件(运行时自带) + b. 尝试 `go version -m ` 获取 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 @` + ├── 如果指定版本安装失败 → 尝试 `@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` 网络失败 | 记录错误,继续安装下一个工具 | --- @@ -278,11 +877,101 @@ internal/logging — 日志记录 fmt — 格式化 ``` -**实现逻辑:** -- `Scan()`: `node --version`、`npm --version`、`npm list -g --json` 获取全局包 -- `Capture()`: 写入 metadata.json + global-packages.json -- `Restore()`: `npm install -g @` 逐个安装 -- `Verify()`: 验证 node 版本和全局包 +**详细功能描述:** + +#### 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 ; fnm use ` + │ └── 无版本管理器 → 提示用户手动升级,记录 UPGRADE 建议 + └── 未安装 → + ├── 有版本管理器 → `fnm install ` + └── 无版本管理器 → 提示用户安装(提供下载链接) +2. 安装全局包(串行,避免 npm 并发冲突) + ├── `npm install -g @` + ├── 安装失败 → 尝试不指定版本 `npm install -g ` + └── 记录每个包的安装结果 +``` + +#### 2.2.6 边界情况 + +| 场景 | 处理方式 | +|---|---| +| Node.js 未安装 | `IsAvailable()` 返回 false | +| npm 损坏 | `npm list -g` 执行失败 → 返回错误信息 | +| 全局包为空 | 正常记录空列表 | +| 包版本在 npm 上已被移除 | 尝试 `@latest`,仍失败则记录警告 | +| nvm 安装了多个版本 | 只采集当前活跃版本 | +| yarn/pnpm 的全局包 | 分别执行 `yarn global list --json` / `pnpm list -g --json` | --- @@ -306,11 +995,69 @@ internal/logging — 日志记录 fmt — 格式化 ``` -**实现逻辑:** -- `Scan()`: `python --version`、`pip --version`、`pip list --format=json` -- `Capture()`: 写入 metadata.json + pip-packages.json -- `Restore()`: `pip install ==` 逐个安装 -- `Verify()`: 验证安装 +**详细功能描述:** + +#### 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 ; pyenv global ` + ├── 有 conda → `conda create -n devpack python=` + └── 都没有 → 提示用户手动安装 +2. 安装 pip 包(串行,使用 --user 标志) + ├── `pip install == --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` 避免依赖地狱 | --- @@ -337,15 +1084,117 @@ internal/logging — 日志记录 fmt — 格式化 ``` -**实现逻辑:** -- `Scan()`: - - `code --version` 获取版本 - - `code --list-extensions --show-versions` 获取扩展列表 - - 读取 `%APPDATA%/Code/User/settings.json` (Windows) 或 `~/Library/Application Support/Code/User/settings.json` (macOS) - - 读取 keybindings.json、snippets 目录 -- `Capture()`: 复制 settings.json / keybindings.json / snippets/ + 写入 extensions.json -- `Restore()`: `code --install-extension @` 逐个安装扩展、复制配置文件 -- `Verify()`: 验证扩展已安装 +**详细功能描述:** + +#### 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. 按行分割,每行格式 `.@` +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 @ --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 可能覆盖还原的设置 | --- @@ -371,11 +1220,73 @@ internal/logging — 日志记录 io — 文件复制 ``` -**实现逻辑:** -- `Scan()`: PowerShell 版本、`Get-Module -ListAvailable`、Profile 路径 -- `Capture()`: 写入 metadata.json + modules.json + profile 文件内容 -- `Restore()`: `Install-Module -Name -Force` 安装模块 + 写入 Profile -- `Verify()`: 验证模块和 Profile +**详细功能描述:** + +#### 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 -RequiredVersion -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 不可用 | 尝试安装最新版本 | --- @@ -400,11 +1311,91 @@ internal/logging — 日志记录 fmt — 格式化 ``` -**实现逻辑:** -- `Scan()`: `scoop export` (JSON)、`scoop bucket list` -- `Capture()`: 写入 packages.json + buckets.json -- `Restore()`: 先 `scoop bucket add`,再 `scoop install ` -- `Verify()`: 验证包是否已安装 +**详细功能描述:** + +#### 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 []` + ├── Bucket 已存在 → 跳过 + └── URL 无效 → 记录警告,部分包可能安装失败 +3. 安装包(串行) + ├── `scoop install ` + ├── 不指定版本(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` 获取 | --- @@ -430,6 +1421,82 @@ internal/logging — 日志记录 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 ` 逐个读取 | +| include.path 指令 | 记录但不递归展开(可能引用不存在的文件) | +| 凭据管理器配置 | 跳过 `credential.*` 下的所有配置 | +| user.signingkey | 跳过(GPG 密钥不通过此采集器迁移) | + --- ### 任务 2.8 — 环境变量采集器 @@ -456,6 +1523,105 @@ internal/logging — 日志记录 internal/platform — SetEnvVar() 设置环境变量 (不同平台不同) ``` +**详细功能描述:** + +#### 2.8.1 环境变量过滤算法 + +``` +输入: os.Environ() → ["GOPATH=C:\Users\dev\go", "PATH=...", "SECRET_KEY=abc", ...] + +算法: + for each env in environ: + key, value = split(env, "=", 2) + + // 第一层: 敏感检测(硬编码规则) + if containsAny(key, ["SECRET", "TOKEN", "PASSWORD", "KEY", "CREDENTIAL", "AUTH"]): + skip(安全原因) + continue + + // 第二层: 系统变量排除(硬编码) + if key in ["PATH", "HOME", "USER", "SHELL", "TERM", "DISPLAY", + "LOGNAME", "HOSTNAME", "PWD", "OLDPWD", "SHLVL", + "LANG", "LC_*", "XDG_*", "SESSION_*", "DBUS_*", + "WINDOWID", "COLORTERM"]: + skip(系统/会话相关) + continue + + // 第三层: Profile include 模式匹配 + if profile.settings.env.include_patterns 不为空: + if 没有任何 include pattern 匹配 key: + skip + continue + + // 第四层: Profile exclude 模式匹配 + if 有任何 exclude pattern 匹配 key: + skip + continue + + // 通过所有过滤 → 采集此变量 + result.append({key, value}) +``` + +#### 2.8.2 PATH 变量特殊处理 + +PATH 变量不直接采集整个值(太长且含系统路径),而是提取开发相关的路径段: + +``` +PATH 提取规则: + 1. 按分隔符拆分 PATH(Windows: ";", Unix: ":") + 2. 过滤出开发相关路径: + - 包含 "go/bin" 或 "Go\bin" + - 包含 ".cargo/bin" + - 包含 ".npm" 或 "node_modules" + - 包含 "pyenv" 或 ".local/bin" + - 包含 "scoop" 或 "Scoop" + - 包含 "maven" 或 "gradle" + 3. 将匹配的路径段记录为 PATH_ADDITIONS + 4. 还原时追加到目标机的 PATH 中 +``` + +#### 2.8.3 Capture 输出文件 + +**variables.json:** +```json +{ + "variables": [ + {"key": "GOPATH", "value": "C:\\Users\\dev\\go"}, + {"key": "GOROOT", "value": "C:\\Program Files\\Go"}, + {"key": "JAVA_HOME", "value": "C:\\Program Files\\Java\\jdk-21"}, + {"key": "GOPROXY", "value": "https://goproxy.cn,direct"} + ], + "path_additions": [ + "C:\\Users\\dev\\go\\bin", + "C:\\Users\\dev\\.cargo\\bin", + "C:\\Users\\dev\\scoop\\shims" + ] +} +``` + +#### 2.8.4 Restore 还原流程 + +``` +1. 逐个设置环境变量 + ├── 调用 platform.SetEnvVar(key, value) + ├── 如果变量已存在且值不同 → 按冲突策略处理 + └── 记录每个变量的设置结果 +2. 处理 PATH 追加 + ├── 获取当前 PATH + ├── 检查每个 path_additions 是否已在 PATH 中 + ├── 不在的追加到 PATH 末尾 + └── 调用 platform.SetEnvVar("PATH", newPath) +``` + +#### 2.8.5 边界情况 + +| 场景 | 处理方式 | +|---|---| +| 环境变量值包含特殊字符 | 保持原样,shell 转义由 platform 层处理 | +| PATH 极长(>8000 字符) | Windows 注册表有 PATH 长度限制,记录警告 | +| 变量引用其他变量(`$HOME/go`) | 记录原始值,不展开引用 | +| Windows 系统变量 vs 用户变量 | 只操作用户级变量 (HKCU) | + --- ### 任务 2.9 — 采集器自动注册 @@ -473,6 +1639,55 @@ internal/logging — 日志记录 |---|---| | `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 命令实现 @@ -500,6 +1715,99 @@ internal/config — 路径 time — 扫描耗时 ``` +**详细功能描述:** + +#### 2.10.1 命令完整执行流程 + +``` +1. 解析命令行参数 + ├── --profile 指定 Profile(默认使用 config.yaml 中的 default_profile) + ├── --collectors 逗号分隔的采集器名(覆盖 Profile 配置) + ├── --output 输出格式: table (默认) | json | yaml + ├── --detailed 显示详细信息(每个 ScanItem 的 Properties) + └── --timeout 超时时间(默认 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 周) @@ -544,6 +1852,168 @@ internal/config — 路径 | `pkg/manifest/manifest.go` | 添加 `Validate()` 方法、`NewManifest()` 构造函数 | | `go.mod` | 添加 `github.com/google/uuid`、`github.com/schollz/progressbar/v3` | +**详细功能描述:** + +#### 3.1.1 .devpack 文件格式规范 + +`.devpack` 文件本质上是一个 `.tar.gz` 归档,内部目录结构如下: + +``` +.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// 子目录 + ├── 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: "(每行一个文件) + └── 写入 tempDir/checksum.sha256 + + 7. 打包为 tar.gz + ├── 创建 .devpack 文件 + ├── 写入 gzip → tar 数据流 + ├── 遍历临时目录,逐个添加文件/目录到 tar + └── 关闭 tar/gzip writer + + 8. 移动到 Packs 目录 + └── mv .devpack → ~/.devpack/packs/.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 命令实现 @@ -573,6 +2043,46 @@ pkg/manifest — Manifest 构建 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 命令实现 @@ -593,6 +2103,42 @@ pkg/manifest — Manifest 构建 fmt — 输出 ``` +**详细功能描述:** + +#### 3.3.1 export 流程 + +``` +devpack export [-o ] + +1. 查找 Pack + └── 在 ~/.devpack/packs/ 中查找 .devpack +2. 验证完整性 + └── 可选: 重新计算校验和确认文件未损坏 +3. 复制文件 + ├── 默认输出到当前目录 ./.devpack + ├── 大文件使用 io.Copy 带进度条 + └── 复制完成后显示文件大小和路径 +``` + +#### 3.3.2 import 流程 + +``` +devpack import [-n ] [--verify] + +1. 验证文件 + ├── 检查文件存在且可读 + ├── 检查是否为有效的 tar.gz + ├── 解析 manifest.json 确认格式正确 + └── --verify: 验证 checksum.sha256 中所有文件的校验和 +2. 检查命名冲突 + ├── 使用 manifest.name 或 --name 指定的名称 + └── 同名已存在 → 提示用户覆盖或改名 +3. 复制到 Packs 目录 + └── cp → ~/.devpack/packs/.devpack +4. 输出结果 + └── 显示 Pack 信息(来源平台、采集器数量、大小) +``` + --- ### 任务 3.4 — 进度条组件 @@ -614,6 +2160,39 @@ pkg/manifest — Manifest 构建 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 周) @@ -654,6 +2233,119 @@ pkg/manifest — Manifest 构建 sync — 并发控制 ``` +**详细功能描述:** + +#### 4.1.1 还原引擎完整流水线(10 步) + +``` +RestoreEngine.Restore(ctx, packPath, opts) 流程: + + ┌─────────────────────────────────────────────────────────────────┐ + │ 步骤 1: 解包 │ + │ ExtractArchive(packPath, tempDir) │ + │ 失败 → 返回 E3001 错误 │ + ├─────────────────────────────────────────────────────────────────┤ + │ 步骤 2: 校验 │ + │ 验证 checksum.sha256 中所有文件的 SHA-256 │ + │ 失败 → 返回 E3002 错误(文件可能被篡改) │ + ├─────────────────────────────────────────────────────────────────┤ + │ 步骤 3: 解析 Manifest │ + │ 读取 manifest.json → Manifest 结构体 │ + │ 验证 schema_version 兼容性 │ + │ 失败 → 返回 E3003 错误 │ + ├─────────────────────────────────────────────────────────────────┤ + │ 步骤 4: 平台检查 │ + │ 比较 manifest.platform.os 与当前 runtime.GOOS │ + │ 不匹配 → 除非 --force,返回 E4002 错误 │ + │ 架构不匹配 → 警告但不阻止 │ + ├─────────────────────────────────────────────────────────────────┤ + │ 步骤 5: 生成还原计划 │ + │ 对每个采集器调用 IsAvailable() + 扫描当前状态 │ + │ 与 Pack 数据对比,生成 RestoreAction 列表 │ + │ 按依赖关系拓扑排序 │ + ├─────────────────────────────────────────────────────────────────┤ + │ 步骤 6: 冲突检测 │ + │ 对每个 RestoreAction 检查目标是否已存在 │ + │ 应用 ConflictStrategy (skip/overwrite/merge/prompt/newest) │ + │ prompt 策略 → 使用 survey 交互式提问 │ + ├─────────────────────────────────────────────────────────────────┤ + │ 步骤 7: 用户确认 │ + │ 显示还原计划摘要(除非 --yes 跳过确认) │ + │ 用户选择 N → 取消还原 │ + ├─────────────────────────────────────────────────────────────────┤ + │ 步骤 8: 创建还原点(除非 --no-rollback) │ + │ 对每个将被修改的项目,快照当前状态 │ + │ 保存到 ~/.devpack/temp/restore-point-{timestamp}/ │ + ├─────────────────────────────────────────────────────────────────┤ + │ 步骤 9: 执行还原 │ + │ 按依赖序逐个调用 collector.Restore() │ + │ 使用进度条显示 "还原中 [3/7] VS Code" │ + │ 某个采集器失败: │ + │ ├── Fatal 级别 → 停止并回滚 │ + │ └── Error 级别 → 记录错误,继续下一个 │ + ├─────────────────────────────────────────────────────────────────┤ + │ 步骤 10: 验证 & 报告 │ + │ 对每个已还原的采集器调用 collector.Verify() │ + │ 生成还原报告(成功/失败/跳过/警告) │ + │ 清理临时文件 │ + └─────────────────────────────────────────────────────────────────┘ +``` + +#### 4.1.2 RestoreAction 数据结构 + +```go +type ActionType string +const ( + ActionInstall ActionType = "INSTALL" // 全新安装 + ActionUpgrade ActionType = "UPGRADE" // 版本升级 + ActionDowngrade ActionType = "DOWNGRADE" // 版本降级 + ActionMerge ActionType = "MERGE" // 合并配置 + ActionOverwrite ActionType = "OVERWRITE" // 覆盖 + ActionSkip ActionType = "SKIP" // 跳过(已存在且一致) +) + +type RestoreAction struct { + Collector string // 采集器名称 + Item string // 项目名称(如 "Go 1.22.1") + Action ActionType // 动作类型 + Details string // 说明(如 "Current: 1.21.0 → 1.22.1") + Priority int // 执行优先级(数字越小越先执行) + DependsOn []string // 依赖的其他采集器 +} +``` + +#### 4.1.3 依赖排序(拓扑排序) + +还原顺序有依赖关系,必须按拓扑序执行: + +``` +依赖图: + env vars ← 无依赖(最先执行,设置 PATH 等) + runtimes ← 依赖 env vars(GOPATH 等需要先设置) + packages ← 依赖 runtimes(scoop install 可能需要 git) + editors ← 依赖 runtimes(VS Code 扩展可能需要语言运行时) + shells ← 依赖 packages(Profile 脚本可能引用已安装的工具) + git config ← 依赖 runtimes(git 需要先安装) + +拓扑序(默认): + 1. env → 2. go/node/python → 3. scoop → 4. vscode → 5. powershell → 6. git +``` + +#### 4.1.4 还原点结构 + +``` +~/.devpack/temp/restore-point-20260303-103000/ +├── manifest.json # 还原点元数据(时间、Pack 来源) +├── vscode/ +│ └── settings.json # 备份的 settings.json +├── powershell/ +│ └── profile.ps1 # 备份的 Profile 脚本 +├── git/ +│ └── gitconfig-backup.json # 备份的 Git 配置 +└── env/ + └── variables-backup.json # 备份的环境变量值 +``` + --- ### 任务 4.2 — 干运行模式 @@ -667,6 +2359,47 @@ pkg/manifest — Manifest 构建 **所需库:** 无额外依赖,使用已有标准库 +**详细功能描述:** + +#### 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 命令实现 @@ -690,6 +2423,80 @@ internal/profile — Profile 加载 pkg/manifest — Manifest 解析 ``` +**详细功能描述:** + +#### 4.3.1 命令参数 + +``` +devpack restore [flags] + +必选参数: + pack-name Pack 名称或 .devpack 文件路径 + +可选标志: + --dry-run 预览更改(不实际执行) + --conflict 冲突处理策略 + skip — 跳过冲突项 + overwrite — 强制覆盖 + merge — 尝试合并(仅配置文件) + prompt — 逐个交互式询问 + newest — 保留更新的版本 + --no-rollback 不创建还原点 + -c, --collectors 只还原指定采集器 + --exclude 排除指定采集器 + --parallel 并行还原数(默认: 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 逻辑 @@ -707,6 +2514,65 @@ pkg/manifest — Manifest 解析 | `internal/collector/git/git_collector.go` | `git config --global ` | `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 周) @@ -733,6 +2599,65 @@ internal/logging — 日志 path/filepath — 路径 ``` +**详细功能描述:** + +#### 5.1.1 五个子命令 + +**`devpack profile create `:** +``` +--template 模板类型 (minimal/standard/full),默认 standard +--description 描述信息 + +流程: + 1. 验证 name 合法性 + 2. 检查同名 Profile 是否已存在 + 3. 从模板创建 Profile + 4. 填入用户提供的 description + 5. 保存到 ~/.devpack/profiles/.yaml + 6. 如果是唯一 Profile,自动设为默认 + 输出: "✓ Profile 'web-dev' 已创建" +``` + +**`devpack profile show `:** +``` +流程: + 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 `:** +``` +流程: + 1. 检查 Profile 是否存在 + 2. 检查是否为当前默认 → 拒绝删除,提示先切换默认 + 3. 确认提示 "确定删除 Profile 'web-dev'?[y/N]" + 4. 删除文件 + 输出: "✓ Profile 'web-dev' 已删除" +``` + +**`devpack profile use `:** +``` +流程: + 1. 检查 Profile 存在 + 2. 更新 config.yaml 的 default_profile 字段 + 3. 保存配置 + 输出: "✓ 默认 Profile 已切换到 'web-dev'" +``` + --- ### 任务 5.2 — list 命令实现 @@ -753,6 +2678,41 @@ internal/logging — 日志 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 命令实现 @@ -783,6 +2743,59 @@ internal/logging — 日志 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 — 测试覆盖率提升 @@ -810,6 +2823,48 @@ internal/logging — 日志 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 命令增强 @@ -821,6 +2876,37 @@ internal/logging — 日志 | `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 +``` + --- ## 文件总览与新增文件清单 @@ -830,66 +2916,67 @@ internal/logging — 日志 ``` internal/ ├── logging/ -│ ├── logger.go # zerolog 封装 -│ └── logger_test.go +│ ├── logger.go ✅ zerolog 封装(已完成) +│ └── logger_test.go ✅ 50 个测试(已完成) ├── config/ -│ ├── config.go # AppConfig 配置结构 -│ └── config_test.go +│ ├── config.go ✅ AppConfig 配置结构(已完成) +│ ├── config_test.go ✅ 12 个测试(已完成) +│ └── paths.go ✅ 路径管理(已完成) ├── profile/ │ ├── profile.go # Profile CRUD │ ├── profile_test.go -│ └── templates.go # 预设模板 +│ └── templates.go # 预设模板 (minimal/standard/full) ├── ui/ -│ ├── printer.go # 统一输出工具 +│ ├── printer.go # 统一输出工具 (Success/Error/Warn/Info) │ ├── table.go # 表格渲染 -│ ├── spinner.go # 加载动画 +│ ├── spinner.go # 加载动画 (Braille 点阵) │ └── progress.go # 进度条 ├── errors/ -│ ├── errors.go # 错误类型 +│ ├── errors.go # DevPackError + 错误码体系 │ └── errors_test.go ├── collector/ -│ ├── register.go # RegisterBuiltins() +│ ├── register.go # RegisterBuiltins() 集中注册 │ ├── runtime/ -│ │ ├── go_collector.go +│ │ ├── go_collector.go # Go: 版本/env/GOPATH/bin 工具 │ │ ├── go_collector_test.go -│ │ ├── node_collector.go +│ │ ├── node_collector.go # Node.js: 版本/npm全局包/nvm │ │ ├── node_collector_test.go -│ │ ├── python_collector.go +│ │ ├── python_collector.go # Python: 版本/pip包/pyenv │ │ └── python_collector_test.go │ ├── editor/ -│ │ ├── vscode_collector.go +│ │ ├── vscode_collector.go # VS Code: 扩展/settings/snippets │ │ └── vscode_collector_test.go │ ├── shell/ -│ │ ├── powershell_collector.go +│ │ ├── powershell_collector.go # PowerShell: 模块/Profile │ │ └── powershell_collector_test.go │ ├── package/ -│ │ ├── scoop_collector.go +│ │ ├── scoop_collector.go # Scoop: 包/bucket (Windows) │ │ └── scoop_collector_test.go │ ├── git/ -│ │ ├── git_collector.go +│ │ ├── git_collector.go # Git: 全局配置/别名 │ │ └── git_collector_test.go │ └── env/ -│ ├── env_collector.go +│ ├── env_collector.go # 环境变量: 过滤/PATH段提取 │ └── env_collector_test.go ├── pack/ -│ ├── engine.go # PackEngine +│ ├── engine.go # PackEngine (10步创建流程) │ ├── engine_test.go │ ├── engine_integration_test.go -│ ├── archive.go # tar.gz 归档 +│ ├── archive.go # tar.gz 归档 (安全解压) │ ├── archive_test.go │ ├── checksum.go # SHA-256 校验和 │ └── checksum_test.go ├── restore/ -│ ├── engine.go # RestoreEngine +│ ├── engine.go # RestoreEngine (10步还原流水线) │ ├── engine_test.go │ ├── engine_integration_test.go -│ ├── plan.go # 还原计划 +│ ├── plan.go # RestorePlan + 拓扑排序 │ ├── plan_test.go -│ ├── conflict.go # 冲突检测 +│ ├── conflict.go # 冲突检测 (5种策略) │ ├── conflict_test.go │ └── report.go # 还原报告 ├── diff/ -│ ├── differ.go # 差异引擎 +│ ├── differ.go # 差异引擎 (added/removed/changed) │ └── differ_test.go ├── platform/ │ └── platform_test.go # 平台层测试 @@ -948,7 +3035,7 @@ require ( ## 开发顺序建议 ``` -Week 1-2: M1 (logging → config → profile → ui → errors → init → 平台完善) +Week 1-2: M1 (logging ✅ → config ✅ → profile → ui → errors → init ✅ → 平台完善) ↓ Week 3-5: M2 (go → node → python → vscode → powershell → scoop → git → env → scan) ↓