Skip to content

Commit 2a38235

Browse files
committed
chore(cloud-claude): clean up Mutagen-facing leftovers
Align the CLI and doctor output with the new in-process hot sync path so users are no longer sent back to obsolete Mutagen flows. Keep the legacy mount-mode value as a parsing alias while switching user-facing wording and conflict reporting to hot sync semantics. Made-with: Cursor
1 parent a9a2bcb commit 2a38235

12 files changed

Lines changed: 53 additions & 70 deletions

File tree

cmd/cloud-claude/main.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ func main() {
8888
// runRoot 内会手动解析并从 args 中剥离,避免透传给远端 claude。
8989
// 这里注册仅用于 --help 显示与 cobra Mark Hidden 等元数据。
9090
rootCmd.PersistentFlags().String("mount-mode", "auto",
91-
"文件映射模式: auto|full|mutagen-only|sshfs-only")
91+
"文件映射模式: auto|full|hot-only|sshfs-only(兼容 legacy: mutagen-only)")
9292

9393
rootCmd.AddCommand(initCmd, envCmd, sshCmd, newSyncCmd(), newSessionsCmd(), newExplainCmd(), newDoctorCmd())
9494

@@ -280,7 +280,7 @@ func runRoot(cmd *cobra.Command, args []string) error {
280280

281281
mode, err := cloudclaude.ParseMode(mountMode)
282282
if err != nil {
283-
fmt.Fprintln(os.Stderr, "错误: --mount-mode 必须是 auto / full / mutagen-only / sshfs-only 之一")
283+
fmt.Fprintln(os.Stderr, "错误: --mount-mode 必须是 auto / full / hot-only / sshfs-only 之一(兼容 legacy: mutagen-only)")
284284
os.Exit(exitConfigError)
285285
}
286286

cmd/cloud-claude/sync.go

Lines changed: 16 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,6 @@ package main
22

33
import (
44
"fmt"
5-
"os"
6-
"os/exec"
7-
"path/filepath"
85

96
"github.com/spf13/cobra"
107

@@ -13,21 +10,22 @@ import (
1310

1411
// newSyncCmd 构造 cloud-claude sync 父命令树。
1512
//
16-
// Phase 31 Plan 03 仅注册 sync conflicts 子命令(CONTEXT D-28 — 最小可行版本);
17-
// sync resolve / sync resume 等 wrapper 由 v3.1 落地(OOS-A2 不暴露 5 种冲突模式)。
13+
// 自研 hot sync 当前只暴露 conflicts 查询:
14+
// 读取 ~/.cloud-claude/last-session.json 的 conflict_count,提示用户最近一次会话
15+
// 是否出现自动 resolved 的双向冲突。逐文件清单暂未持久化。
1816
func newSyncCmd() *cobra.Command {
1917
cmd := &cobra.Command{
2018
Use: "sync",
21-
Short: "Mutagen 同步管理(v3.0 三层文件映射)",
22-
Long: "查看本地 Mutagen 客户端管理的 cloud-claude 同步会话与冲突文件清单。\n注:当前仅实现 sync conflicts 子命令;sync resolve / sync resume 留 v3.1。",
19+
Short: "热同步相关工具",
20+
Long: "查看 cloud-claude 自研热同步的最近一次冲突计数等状态。\n当前仅实现 sync conflicts 子命令;逐文件冲突清单留后续版本补齐。",
2321
SilenceUsage: true,
2422
SilenceErrors: true,
2523
}
2624

2725
conflictsCmd := &cobra.Command{
2826
Use: "conflicts",
29-
Short: "查看当前 Mutagen 同步会话的冲突文件清单",
30-
Long: "调用本地 Mutagen 客户端 sync list --long 渲染所有 cloud-claude 创建的 sync session 的冲突文件(path / alpha / beta / mtime)。",
27+
Short: "查看最近一次热同步冲突计数",
28+
Long: "读取 ~/.cloud-claude/last-session.json 中的 conflict_count。\n当前自研热同步会在运行时实时提示冲突,但尚未持久化逐文件清单。",
3129
SilenceUsage: true,
3230
SilenceErrors: true,
3331
RunE: runSyncConflicts,
@@ -36,35 +34,18 @@ func newSyncCmd() *cobra.Command {
3634
return cmd
3735
}
3836

39-
// runSyncConflicts 执行 ~/.cloud-claude/bin/mutagen sync list --long 并把结果直接打到
40-
// stdout / stderr。本地命令出错时返回中文错误,由 cobra runE 错误处理流程 stderr 打印。
41-
//
42-
// 行为约束:
43-
// - 不发起 SSH 连接(本子命令只查本地 mutagen daemon 状态,不需要联网)
44-
// - ExtractMutagenBinary 幂等保证 ~/.cloud-claude/bin/mutagen 存在
45-
// - MUTAGEN_DATA_DIRECTORY 环境变量隔离与主流程一致(CONTEXT D-05)
37+
// runSyncConflicts 仅读取本地上次会话快照,不发起 SSH 连接。
38+
// 当前实现不再依赖 Mutagen daemon,也不维护逐文件冲突清单。
4639
func runSyncConflicts(cmd *cobra.Command, args []string) error {
47-
home, err := os.UserHomeDir()
40+
snap, err := cloudclaude.LoadLastSession()
4841
if err != nil {
49-
return fmt.Errorf("无法获取用户主目录: %w", err)
50-
}
51-
52-
binPath := filepath.Join(home, ".cloud-claude", "bin", "mutagen")
53-
if err := cloudclaude.ExtractMutagenBinary(binPath); err != nil {
54-
return fmt.Errorf("无法准备 Mutagen 二进制: %w", err)
42+
return fmt.Errorf("无法读取最近一次会话快照: %w", err)
5543
}
56-
57-
env := append(os.Environ(),
58-
"MUTAGEN_DATA_DIRECTORY="+filepath.Join(home, ".cloud-claude", "mutagen"),
59-
)
60-
61-
// sync list --long 输出含每个 session 的冲突详细信息(path / alpha / beta / mtime)
62-
c := exec.Command(binPath, "sync", "list", "--long")
63-
c.Env = env
64-
c.Stdout = os.Stdout
65-
c.Stderr = os.Stderr
66-
if err := c.Run(); err != nil {
67-
return fmt.Errorf("查询 Mutagen 冲突清单失败: %w", err)
44+
if snap.ConflictCount <= 0 {
45+
fmt.Println("最近一次会话未记录热同步冲突。")
46+
return nil
6847
}
48+
fmt.Printf("最近一次会话记录到 %d 个热同步冲突。\n", snap.ConflictCount)
49+
fmt.Println("当前版本仅持久化冲突计数;逐文件冲突会在运行 cloud-claude 时实时提示。")
6950
return nil
7051
}

internal/cloudclaude/doctor/doctor.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -225,8 +225,6 @@ func RunDoctor(ctx context.Context, opts Options) (*Report, error) {
225225
// 6) mount 维度
226226
if want("mount") {
227227
ensureRemote()
228-
report.Checks = append(report.Checks, runWithTimeout(ctx, "mount", "mutagen_version_match", timeout,
229-
func(c context.Context) Check { return checkMutagenVersionMatch(c, remoteRunner) }))
230228
report.Checks = append(report.Checks, runWithTimeout(ctx, "mount", "mergerfs_branches", timeout,
231229
func(c context.Context) Check { return checkMergerfsBranches(c, remoteRunner) }))
232230
report.Checks = append(report.Checks, runWithTimeout(ctx, "mount", "sshfs_mountpoint", timeout,
@@ -241,7 +239,6 @@ func RunDoctor(ctx context.Context, opts Options) (*Report, error) {
241239
report.Checks = append(report.Checks, checkLocalDisk(ctx))
242240
report.Checks = append(report.Checks, runWithTimeout(ctx, "disk", "container_disk", timeout,
243241
func(c context.Context) Check { return checkContainerDisk(c, remoteRunner) }))
244-
report.Checks = append(report.Checks, checkMutagenDataSize(ctx))
245242
}
246243

247244
// 8) 聚合 Summary

internal/cloudclaude/doctor/render.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ func renderDowngradeBanner(banner *DowngradeBanner) string {
8585
fmt.Fprintf(&b, " 角色: %s\n", banner.ClientRole)
8686
}
8787
if banner.ConflictCount > 0 {
88-
fmt.Fprintf(&b, " Mutagen 冲突: %d 个\n", banner.ConflictCount)
88+
fmt.Fprintf(&b, " 热同步冲突: %d 个\n", banner.ConflictCount)
8989
}
9090
if banner.ReconnectCount > 0 {
9191
fmt.Fprintf(&b, " 重连次数: %d\n", banner.ReconnectCount)

internal/cloudclaude/doctor/render_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,15 @@ func TestDowngradeBannerRendersChain(t *testing.T) {
1515
IntendedMode: "full",
1616
ActualMode: "sshfs-only",
1717
DowngradeChain: []DowngradeStep{
18-
{From: "full", To: "mutagen-only", ReasonCode: "MOUNT_MUTAGEN_VERSION_SKEW", ReasonMessage: "version skew"},
19-
{From: "mutagen-only", To: "sshfs-only", ReasonCode: "MOUNT_AUTO_DOWNGRADED", ReasonMessage: "daemon died"},
18+
{From: "full", To: "hot-only", ReasonCode: "MOUNT_MERGERFS_FAILED", ReasonMessage: "merge failed"},
19+
{From: "hot-only", To: "sshfs-only", ReasonCode: "MOUNT_AUTO_DOWNGRADED", ReasonMessage: "hot sync failed"},
2020
},
2121
}
2222
out := renderDowngradeBanner(banner)
2323
for _, want := range []string{
24-
"[降级] full → mutagen-only",
25-
"[降级] mutagen-only → sshfs-only",
26-
"MOUNT_MUTAGEN_VERSION_SKEW",
24+
"[降级] full → hot-only",
25+
"[降级] hot-only → sshfs-only",
26+
"MOUNT_MERGERFS_FAILED",
2727
"MOUNT_AUTO_DOWNGRADED",
2828
"意图=full",
2929
"实际=sshfs-only",

internal/cloudclaude/errcodes/explanations.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,9 @@ func init() {
111111
修复路径:观察 stderr 提示的下游错误码([CODE] 后面的部分)针对修复;运行 cloud-claude doctor mount 看完整健康度;如要锁定模式可用 --mount-mode flag 强制。
112112
关联文档:Phase 31 RESEARCH §6 / Phase 31 PITFALLS M13(禁止静默降级)`)
113113

114-
registerExplanation(MOUNT_FORCE_MODE_FAILED, `触发场景:用户通过 --mount-mode={full,mutagen-only,sshfs-only} 强制指定模式,但该模式启动失败。强制模式下 cloud-claude 不会自动降级。
115-
根本原因:用户显式指定模式意味着接受失败即报错的语义(M13 防御静默降级)。常见触发为 mutagen-only 模式下 daemon 不可用、sshfs-only 模式下 fuse 不可用。
116-
复现方式:cloud-claude --mount-mode=mutagen-only 但本机 mutagen daemon 起不来 → SeverityFatal 阻断。
114+
registerExplanation(MOUNT_FORCE_MODE_FAILED, `触发场景:用户通过 --mount-mode={full,hot-only,sshfs-only} 强制指定模式,但该模式启动失败。强制模式下 cloud-claude 不会自动降级。
115+
根本原因:用户显式指定模式意味着接受失败即报错的语义(M13 防御静默降级)。常见触发为 hot-only 模式下热同步初始化失败、sshfs-only 模式下 fuse 不可用。
116+
复现方式:cloud-claude --mount-mode=hot-only 但远端同路径目录不可写 → SeverityFatal 阻断。
117117
修复路径:移除 --mount-mode flag 让自动降级生效,或针对错误信息中的具体下游 code 修复;需要锁定模式时建议先运行 cloud-claude doctor mount 看可用层。
118118
关联文档:Phase 31 PLAN <errcode_registry> / Phase 31 PITFALLS M13`)
119119

internal/cloudclaude/exitcodes.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,5 @@ const (
2525
// 6-8:v3.0 Phase 31 新增(OAuth + mount force)
2626
ExitOAuthNotFound = 6 // /home/claude/.claude/.credentials.json 不存在或解析失败(D-22 第 3 条)
2727
ExitOAuthExpired = 7 // expiresAt < now(D-22 第 3 条)
28-
ExitMountForceFailed = 8 // --mount-mode=full|mutagen-only|sshfs-only 任一档失败(D-15 / errcodes.MOUNT_FORCE_MODE_FAILED
28+
ExitMountForceFailed = 8 // --mount-mode=full|hot-only|sshfs-only 任一档失败(兼容 legacy: mutagen-only
2929
)

internal/cloudclaude/ignore.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,17 @@ func (m *IgnoreMatcher) IsIgnoredRel(rel string, isDir bool) bool {
183183
return ignored
184184
}
185185

186+
// isHardcodedSkipDir 是热同步/体积统计共用的目录白名单。
187+
// 这些目录通常是依赖缓存或构建产物,不应进入热同步,也应在本地扫描时直接 SkipDir。
188+
func isHardcodedSkipDir(name string) bool {
189+
switch name {
190+
case "node_modules", "target", "dist", ".venv", "__pycache__",
191+
".next", "build", ".cache", ".git":
192+
return true
193+
}
194+
return false
195+
}
196+
186197
// compileIgnoreRule 把一行 gitignore pattern 编译成 ignoreRule。
187198
// 返回 ok=false 表示该行是空、注释或无法编译,调用方应跳过。
188199
func compileIgnoreRule(raw string) (ignoreRule, bool) {

internal/cloudclaude/integration_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ func TestIntegration_F2B_KillMutagenAgent_DowngradesIn2s(t *testing.T) {
178178
}
179179
if !strings.Contains(stderr, string(errcodes.MOUNT_AUTO_DOWNGRADED)) &&
180180
!strings.Contains(stderr, "[sshfs-only]") &&
181-
!strings.Contains(stderr, "[mutagen-only]") {
181+
!strings.Contains(stderr, "[hot-only]") {
182182
t.Errorf("期望降级 banner,stderr: %s", stderr)
183183
}
184184
}

internal/cloudclaude/last_session_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ func Test_WriteLastSession_Schema(t *testing.T) {
1616

1717
snap := LastSessionSnapshot{
1818
IntendedMode: "auto",
19-
ActualMode: "mutagen-only",
19+
ActualMode: "hot-only",
2020
DowngradeChain: []DowngradeStep{
21-
{From: "full", To: "mutagen-only", ReasonCode: "MOUNT_MERGERFS_FAILED", ReasonMessage: "mergerfs 失败"},
21+
{From: "full", To: "hot-only", ReasonCode: "MOUNT_MERGERFS_FAILED", ReasonMessage: "mergerfs 失败"},
2222
},
2323
ConflictCount: 3,
2424
ClaudeAccountID: "acc_xyz",
@@ -44,7 +44,7 @@ func Test_WriteLastSession_Schema(t *testing.T) {
4444
if v, _ := got["schema_version"].(float64); v != 1 {
4545
t.Errorf("schema_version = %v, want 1", got["schema_version"])
4646
}
47-
if got["actual_mode"] != "mutagen-only" {
47+
if got["actual_mode"] != "hot-only" {
4848
t.Errorf("actual_mode = %v", got["actual_mode"])
4949
}
5050
if got["intended_mode"] != "auto" {
@@ -68,7 +68,7 @@ func Test_WriteLastSession_Schema(t *testing.T) {
6868
if !ok {
6969
t.Fatal("downgrade_chain[0] 不是 object")
7070
}
71-
if step["from"] != "full" || step["to"] != "mutagen-only" {
71+
if step["from"] != "full" || step["to"] != "hot-only" {
7272
t.Errorf("downgrade_chain[0] from/to 异常: %v", step)
7373
}
7474
if step["reason_code"] != "MOUNT_MERGERFS_FAILED" {

0 commit comments

Comments
 (0)