From cfd62f90214f69817337bbaa390e2d91490331ec Mon Sep 17 00:00:00 2001 From: "Jinwook Jeong (Edgar)" Date: Wed, 8 Jul 2026 03:17:44 +0900 Subject: [PATCH 1/2] feat: add `ccx move` CLI subcommand for moving session project paths Expose session.MoveProject (already used by the TUI's `m` keybind) as a CLI subcommand. Supports moving the current tmux-detected session, a session by ID, or a project directory directly by path. Claude-Session: LOCAL_992a1351-22c6-490b-a7b9-3518e4a2bcdb --- internal/cli/cli.go | 54 +++++++++++++++++++++++++++++++++++++++++++++ main.go | 16 ++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/internal/cli/cli.go b/internal/cli/cli.go index 5f55c42..dba7873 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -26,6 +26,7 @@ var Commands = []struct { {"conversation", "List conversation turns from the Claude session (interactive on TTY)"}, {"info", "Show the matched Claude session metadata"}, {"sessions", "List session IDs with metadata (use --pick for TUI JSON picker)"}, + {"move", "Move a session's project path to a new location"}, {"config", "View/edit ccx config and get/set dot-path values"}, {"help", "Show available commands and usage"}, } @@ -149,6 +150,8 @@ func printHelp() { fmt.Fprintf(os.Stderr, " ccx images Interactive image picker\n") fmt.Fprintf(os.Stderr, " ccx conversation Interactive conversation picker\n") fmt.Fprintf(os.Stderr, " ccx info Show current matched session metadata\n") + fmt.Fprintf(os.Stderr, " ccx move Move current session's project path\n") + fmt.Fprintf(os.Stderr, " ccx move --from Move a project dir's sessions by path\n") fmt.Fprintf(os.Stderr, " ccx config view Print ~/.config/ccx/config.yaml\n") fmt.Fprintf(os.Stderr, " ccx config edit Open config in $EDITOR\n") fmt.Fprintf(os.Stderr, " ccx config set remote.pod_name ccx-worker\n\n") @@ -322,6 +325,57 @@ func RunSessions(claudeDir string, all bool) error { return nil } +// RunMove moves a project's session directory to newPath, taking every +// session under it along. oldDir, if set, is used directly. Otherwise +// sessionID resolves to its project dir; if that's empty too, the session +// is resolved the same way other subcommands do (tmux window / live +// registry match). +func RunMove(claudeDir, sessionID, oldDir, newPath string) error { + newPath = strings.TrimSpace(newPath) + if newPath == "" { + return fmt.Errorf("usage: ccx move [--from | --session ] ") + } + abs, err := filepath.Abs(newPath) + if err != nil { + return fmt.Errorf("resolve new path: %w", err) + } + newPath = abs + + oldPath := strings.TrimSpace(oldDir) + if oldPath != "" { + abs, err := filepath.Abs(oldPath) + if err != nil { + return fmt.Errorf("resolve old path: %w", err) + } + oldPath = abs + } else if sessionID != "" { + sess, ok := session.FindSessionByID(claudeDir, sessionID) + if !ok { + return fmt.Errorf("session %s not found", sessionID) + } + oldPath = sess.ProjectPath + } else { + _, sessID, err := findSessionFile(claudeDir) + if err != nil { + return err + } + sess, ok := session.FindSessionByID(claudeDir, sessID) + if !ok { + return fmt.Errorf("session %s not found", sessID) + } + oldPath = sess.ProjectPath + } + + if oldPath == newPath { + return fmt.Errorf("new path is the same as the current path: %s", oldPath) + } + if err := session.MoveProject(oldPath, newPath); err != nil { + return err + } + fmt.Fprintf(os.Stdout, "%s -> %s\n", oldPath, newPath) + return nil +} + func RunInfo(claudeDir string) error { _, sessID, err := findSessionFile(claudeDir) if err != nil { diff --git a/main.go b/main.go index 24d3042..983ecb7 100644 --- a/main.go +++ b/main.go @@ -200,6 +200,22 @@ func main() { os.Exit(1) } os.Exit(0) + case "move": + fs := flag.NewFlagSet("move", flag.ExitOnError) + sess := fs.String("session", "", "session ID (prefix match); defaults to the current tmux window's session") + from := fs.String("from", "", "project directory to move (moves every session under it)") + dirFlag := fs.String("dir", "", "path to Claude data directory (default: ~/.claude)") + fs.Parse(os.Args[2:]) + if fs.NArg() != 1 { + fmt.Fprintf(os.Stderr, "usage: ccx move [--from | --session ] \n") + os.Exit(1) + } + dir := resolveClaudeDir(*dirFlag) + if err := cli.RunMove(dir, *sess, *from, fs.Arg(0)); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } + os.Exit(0) case "urls", "files", "changes", "images", "conversation", "info", "help": subcmd := os.Args[1] fs := flag.NewFlagSet(subcmd, flag.ExitOnError) From bc410495ca588f6dff812186df82aae4e7eaf544 Mon Sep 17 00:00:00 2001 From: "Jinwook Jeong (Edgar)" Date: Wed, 8 Jul 2026 03:23:29 +0900 Subject: [PATCH 2/2] docs: add usage/flag help text for `ccx move` Add per-flag descriptions, a --help usage block, and clearer examples in the top-level help output. Claude-Session: LOCAL_992a1351-22c6-490b-a7b9-3518e4a2bcdb --- internal/cli/cli.go | 7 ++++--- main.go | 15 ++++++++++++--- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/internal/cli/cli.go b/internal/cli/cli.go index dba7873..b7c9df7 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -26,7 +26,7 @@ var Commands = []struct { {"conversation", "List conversation turns from the Claude session (interactive on TTY)"}, {"info", "Show the matched Claude session metadata"}, {"sessions", "List session IDs with metadata (use --pick for TUI JSON picker)"}, - {"move", "Move a session's project path to a new location"}, + {"move", "Move a session's project path to a new location (--from, --session, --help)"}, {"config", "View/edit ccx config and get/set dot-path values"}, {"help", "Show available commands and usage"}, } @@ -150,8 +150,9 @@ func printHelp() { fmt.Fprintf(os.Stderr, " ccx images Interactive image picker\n") fmt.Fprintf(os.Stderr, " ccx conversation Interactive conversation picker\n") fmt.Fprintf(os.Stderr, " ccx info Show current matched session metadata\n") - fmt.Fprintf(os.Stderr, " ccx move Move current session's project path\n") - fmt.Fprintf(os.Stderr, " ccx move --from Move a project dir's sessions by path\n") + fmt.Fprintf(os.Stderr, " ccx move Move current session's project path\n") + fmt.Fprintf(os.Stderr, " ccx move --session Move a session by ID\n") + fmt.Fprintf(os.Stderr, " ccx move --from Move a project dir by path (no session ID needed)\n") fmt.Fprintf(os.Stderr, " ccx config view Print ~/.config/ccx/config.yaml\n") fmt.Fprintf(os.Stderr, " ccx config edit Open config in $EDITOR\n") fmt.Fprintf(os.Stderr, " ccx config set remote.pod_name ccx-worker\n\n") diff --git a/main.go b/main.go index 983ecb7..f177eed 100644 --- a/main.go +++ b/main.go @@ -202,12 +202,21 @@ func main() { os.Exit(0) case "move": fs := flag.NewFlagSet("move", flag.ExitOnError) - sess := fs.String("session", "", "session ID (prefix match); defaults to the current tmux window's session") - from := fs.String("from", "", "project directory to move (moves every session under it)") + sess := fs.String("session", "", "session ID to move (prefix match); default: current tmux window's session") + from := fs.String("from", "", "project directory to move by path instead of session ID (moves every session under it)") dirFlag := fs.String("dir", "", "path to Claude data directory (default: ~/.claude)") + fs.Usage = func() { + fmt.Fprintf(os.Stderr, "Move a Claude session's project path to a new location.\n\n") + fmt.Fprintf(os.Stderr, "Usage:\n") + fmt.Fprintf(os.Stderr, " ccx move Move current tmux session's project path\n") + fmt.Fprintf(os.Stderr, " ccx move --session Move a specific session by ID\n") + fmt.Fprintf(os.Stderr, " ccx move --from Move a project dir by path (no session lookup)\n\n") + fmt.Fprintf(os.Stderr, "Flags:\n") + fs.PrintDefaults() + } fs.Parse(os.Args[2:]) if fs.NArg() != 1 { - fmt.Fprintf(os.Stderr, "usage: ccx move [--from | --session ] \n") + fs.Usage() os.Exit(1) } dir := resolveClaudeDir(*dirFlag)