Skip to content

Commit 8167c3e

Browse files
committed
feat: add self-update capability and move Claude CLI management to subcommand
- Add internal/update package for GitHub Releases-based self-update - `codes update` now updates codes itself (was Claude CLI update) - `codes claude update` manages Claude CLI npm versions (moved from `codes update`) - Startup async version check with configurable mode via `codes config set auto-update notify|silent|off` - Silent mode downloads to ~/.codes/update/ staging dir, applied on next launch - Dev builds skip all update checks
1 parent 7837534 commit 8167c3e

8 files changed

Lines changed: 605 additions & 39 deletions

File tree

cmd/codes/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ func init() {
3333
rootCmd.AddCommand(commands.CompletionCmd)
3434
rootCmd.AddCommand(commands.ServeCmd)
3535
rootCmd.AddCommand(commands.RemoteCmd)
36+
rootCmd.AddCommand(commands.ClaudeCmd)
3637

3738
// 设置默认运行时行为
3839
rootCmd.Run = func(cmd *cobra.Command, args []string) {

internal/commands/cobra.go

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -92,10 +92,27 @@ var ProfileRemoveCmd = &cobra.Command{
9292
// UpdateCmd represents the update command
9393
var UpdateCmd = &cobra.Command{
9494
Use: "update",
95-
Short: "Update Claude to specific version",
96-
Long: "Update Claude CLI to a specific version",
95+
Short: "Update codes to the latest version",
96+
Long: "Check for and install the latest version of codes CLI",
9797
Run: func(cmd *cobra.Command, args []string) {
98-
RunUpdate()
98+
RunSelfUpdate()
99+
},
100+
}
101+
102+
// ClaudeCmd is the parent command for Claude CLI management.
103+
var ClaudeCmd = &cobra.Command{
104+
Use: "claude",
105+
Short: "Manage Claude CLI",
106+
Long: "Manage the Claude CLI (npm @anthropic-ai/claude-code) installation and versions",
107+
}
108+
109+
// ClaudeUpdateCmd updates the Claude CLI npm package.
110+
var ClaudeUpdateCmd = &cobra.Command{
111+
Use: "update",
112+
Short: "Update Claude CLI to specific version",
113+
Long: "Update Claude CLI (npm @anthropic-ai/claude-code) to a specific version",
114+
Run: func(cmd *cobra.Command, args []string) {
115+
RunClaudeUpdate()
99116
},
100117
}
101118

@@ -156,11 +173,11 @@ var ConfigCmd = &cobra.Command{
156173
var ConfigSetCmd = &cobra.Command{
157174
Use: "set <key> <value>",
158175
Short: "Set a configuration value",
159-
Long: "Set a configuration value (keys: default-behavior, skip-permissions, terminal)",
176+
Long: "Set a configuration value (keys: default-behavior, skip-permissions, terminal, auto-update)",
160177
Args: cobra.ExactArgs(2),
161178
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
162179
if len(args) == 0 {
163-
return []string{"default-behavior", "skip-permissions", "terminal"}, cobra.ShellCompDirectiveNoFileComp
180+
return []string{"default-behavior", "skip-permissions", "terminal", "auto-update"}, cobra.ShellCompDirectiveNoFileComp
164181
}
165182
if len(args) == 1 {
166183
switch args[0] {
@@ -173,6 +190,8 @@ var ConfigSetCmd = &cobra.Command{
173190
return []string{"auto", "wt", "powershell", "pwsh", "cmd"}, cobra.ShellCompDirectiveNoFileComp
174191
}
175192
return []string{"terminal", "iterm", "warp"}, cobra.ShellCompDirectiveNoFileComp
193+
case "auto-update":
194+
return []string{"notify", "silent", "off"}, cobra.ShellCompDirectiveNoFileComp
176195
}
177196
}
178197
return nil, cobra.ShellCompDirectiveNoFileComp
@@ -271,6 +290,9 @@ func init() {
271290
ConfigCmd.AddCommand(ConfigResetCmd)
272291
ConfigCmd.AddCommand(ConfigListCmd)
273292

293+
// Claude sub-commands
294+
ClaudeCmd.AddCommand(ClaudeUpdateCmd)
295+
274296
// Remote sub-commands
275297
RemoteAddCmd.Flags().IntP("port", "p", 0, "SSH port")
276298
RemoteAddCmd.Flags().StringP("identity", "i", "", "SSH identity file")

internal/commands/commands.go

Lines changed: 61 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"codes/internal/output"
1818
"codes/internal/remote"
1919
"codes/internal/ui"
20+
"codes/internal/update"
2021
)
2122

2223
// Version information, set via ldflags at build time.
@@ -118,7 +119,7 @@ func RunSelect() {
118119
}
119120
}
120121

121-
func RunUpdate() {
122+
func RunClaudeUpdate() {
122123
ui.ShowHeader("Claude Version Manager")
123124
ui.ShowLoading("Fetching available versions...")
124125

@@ -680,7 +681,7 @@ func RunInit(autoYes bool) {
680681
ui.ShowInfo("Checking Claude CLI installation...")
681682
if _, err := exec.LookPath("claude"); err != nil {
682683
ui.ShowError("Claude CLI not found", nil)
683-
ui.ShowWarning(" Run 'codes update' to install Claude CLI")
684+
ui.ShowWarning(" Run 'codes claude update' to install Claude CLI")
684685
allGood = false
685686
} else {
686687
ui.ShowSuccess("Claude CLI is installed")
@@ -915,7 +916,7 @@ func RunInit(autoYes bool) {
915916
ui.ShowInfo(" 1. Install Git")
916917
}
917918
if _, err := exec.LookPath("claude"); err != nil {
918-
ui.ShowInfo(" 2. Install Claude CLI: codes update")
919+
ui.ShowInfo(" 2. Install Claude CLI: codes claude update")
919920
}
920921
if _, err := os.Stat(config.ConfigPath); err != nil {
921922
ui.ShowInfo(" 3. Add a configuration: codes profile add")
@@ -924,33 +925,23 @@ func RunInit(autoYes bool) {
924925
}
925926

926927
func checkForUpdates() {
927-
// 检查codes CLI更新
928-
go func() {
929-
// 简单的版本检查逻辑
930-
// 这里可以集成GitHub API检查最新版本
931-
// 目前只是占位符
932-
// 可以通过检查GitHub releases API来获取最新版本
933-
// 例如: https://api.github.com/repos/{owner}/{repo}/releases/latest
934-
// 然后与当前版本比较,提示用户更新
935-
//
936-
// 示例实现:
937-
// resp, err := http.Get("https://api.github.com/repos/yourusername/codes/releases/latest")
938-
// if err != nil {
939-
// return
940-
// }
941-
// defer resp.Body.Close()
942-
//
943-
// var release struct {
944-
// TagName string `json:"tag_name"`
945-
// }
946-
// if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
947-
// return
948-
// }
949-
//
950-
// if release.TagName != "dev" && release.TagName != currentVersion {
951-
// ui.ShowInfo("New version %s available! Run 'codes update' to upgrade.", release.TagName)
952-
// }
953-
}()
928+
// Apply any previously staged update (synchronous)
929+
if err := update.ApplyStaged(); err != nil {
930+
ui.ShowWarning("Failed to apply staged update: %v", err)
931+
}
932+
933+
// Async version check
934+
mode := config.GetAutoUpdate()
935+
go update.AutoCheck(Version, mode)
936+
}
937+
938+
// RunSelfUpdate performs a manual codes self-update.
939+
func RunSelfUpdate() {
940+
ui.ShowHeader("codes Self-Update")
941+
if err := update.RunSelfUpdate(Version); err != nil {
942+
ui.ShowError(err.Error(), nil)
943+
os.Exit(1)
944+
}
954945
}
955946

956947
// RunStart 快速启动 Claude Code
@@ -1352,9 +1343,21 @@ func RunConfigSet(key, value string) {
13521343
RunSkipPermissionsSet(skip)
13531344
case "terminal":
13541345
RunTerminalSet(value)
1346+
case "auto-update", "autoUpdate":
1347+
v := strings.ToLower(value)
1348+
switch v {
1349+
case "notify", "silent", "off":
1350+
if err := config.SetAutoUpdate(v); err != nil {
1351+
ui.ShowError("Failed to set auto-update", err)
1352+
return
1353+
}
1354+
ui.ShowSuccess("auto-update set to: %s", v)
1355+
default:
1356+
ui.ShowError("Invalid value for auto-update. Must be 'notify', 'silent', or 'off'", nil)
1357+
}
13551358
default:
13561359
ui.ShowError(fmt.Sprintf("Unknown configuration key: %s", key), nil)
1357-
fmt.Println("Available keys: default-behavior, skip-permissions, terminal")
1360+
fmt.Println("Available keys: default-behavior, skip-permissions, terminal, auto-update")
13581361
}
13591362
}
13601363

@@ -1380,11 +1383,16 @@ func RunConfigGet(args []string) {
13801383
terminal = "terminal"
13811384
}
13821385
}
1386+
autoUpdate := cfg.AutoUpdate
1387+
if autoUpdate == "" {
1388+
autoUpdate = "notify"
1389+
}
13831390

13841391
fmt.Println("Current configuration:")
13851392
fmt.Printf(" default-behavior: %s\n", behavior)
13861393
fmt.Printf(" skip-permissions: %v\n", cfg.SkipPermissions)
13871394
fmt.Printf(" terminal: %s\n", terminal)
1395+
fmt.Printf(" auto-update: %s\n", autoUpdate)
13881396
fmt.Printf(" default: %s\n", cfg.Default)
13891397
fmt.Printf(" projects: %d configured\n", len(cfg.Projects))
13901398
return
@@ -1398,9 +1406,11 @@ func RunConfigGet(args []string) {
13981406
RunSkipPermissionsGet()
13991407
case "terminal":
14001408
RunTerminalGet()
1409+
case "auto-update", "autoUpdate":
1410+
fmt.Printf("auto-update: %s\n", config.GetAutoUpdate())
14011411
default:
14021412
ui.ShowError(fmt.Sprintf("Unknown configuration key: %s", key), nil)
1403-
fmt.Println("Available keys: default-behavior, skip-permissions, terminal")
1413+
fmt.Println("Available keys: default-behavior, skip-permissions, terminal, auto-update")
14041414
}
14051415
}
14061416

@@ -1669,6 +1679,11 @@ func RunConfigReset(args []string) {
16691679
RunDefaultBehaviorReset()
16701680
RunSkipPermissionsReset()
16711681
RunTerminalReset()
1682+
if err := config.SetAutoUpdate(""); err != nil {
1683+
ui.ShowWarning("Failed to reset auto-update: %v", err)
1684+
} else {
1685+
ui.ShowSuccess("auto-update reset to default (notify)")
1686+
}
16721687
return
16731688
}
16741689

@@ -1680,9 +1695,15 @@ func RunConfigReset(args []string) {
16801695
RunSkipPermissionsReset()
16811696
case "terminal":
16821697
RunTerminalReset()
1698+
case "auto-update", "autoUpdate":
1699+
if err := config.SetAutoUpdate(""); err != nil {
1700+
ui.ShowWarning("Failed to reset auto-update: %v", err)
1701+
} else {
1702+
ui.ShowSuccess("auto-update reset to default (notify)")
1703+
}
16831704
default:
16841705
ui.ShowError(fmt.Sprintf("Unknown configuration key: %s", key), nil)
1685-
fmt.Println("Available keys: default-behavior, skip-permissions, terminal")
1706+
fmt.Println("Available keys: default-behavior, skip-permissions, terminal, auto-update")
16861707
}
16871708
}
16881709

@@ -1693,6 +1714,7 @@ func RunConfigList(args []string) {
16931714
fmt.Println(" default-behavior Startup directory behavior (current, last, home)")
16941715
fmt.Println(" skip-permissions Global --dangerously-skip-permissions (true, false)")
16951716
fmt.Println(" terminal Terminal emulator for sessions")
1717+
fmt.Println(" auto-update Auto-update check mode (notify, silent, off)")
16961718
fmt.Println()
16971719
fmt.Println("Use 'codes config list <key>' to see available values for a key.")
16981720
return
@@ -1711,9 +1733,14 @@ func RunConfigList(args []string) {
17111733
fmt.Println(" false Disable --dangerously-skip-permissions globally (default)")
17121734
case "terminal":
17131735
RunTerminalList()
1736+
case "auto-update", "autoUpdate":
1737+
fmt.Println("Available values for auto-update:")
1738+
fmt.Println(" notify Show notification when new version is available (default)")
1739+
fmt.Println(" silent Download new version in background, apply on next launch")
1740+
fmt.Println(" off Disable automatic update checks")
17141741
default:
17151742
ui.ShowError(fmt.Sprintf("Unknown configuration key: %s", key), nil)
1716-
fmt.Println("Available keys: default-behavior, skip-permissions, terminal")
1743+
fmt.Println("Available keys: default-behavior, skip-permissions, terminal, auto-update")
17171744
}
17181745
}
17191746

internal/config/config.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ type Config struct {
2222
Terminal string `json:"terminal,omitempty"` // 终端模拟器: "terminal", "iterm", "warp", 或自定义命令
2323
Remotes []RemoteHost `json:"remotes,omitempty"` // 远程 SSH 主机
2424
ProjectsDir string `json:"projects_dir,omitempty"` // git clone 默认目标目录
25+
AutoUpdate string `json:"auto_update,omitempty"` // 自动更新模式: "notify", "silent", "off"
2526
}
2627

2728
// RemoteHost represents a remote SSH host configuration.
@@ -559,6 +560,29 @@ func SetProjectsDir(dir string) error {
559560
return SaveConfig(cfg)
560561
}
561562

563+
// GetAutoUpdate returns the configured auto-update mode.
564+
// Defaults to "notify" if not set.
565+
func GetAutoUpdate() string {
566+
cfg, err := LoadConfig()
567+
if err != nil {
568+
return "notify"
569+
}
570+
if cfg.AutoUpdate == "" {
571+
return "notify"
572+
}
573+
return cfg.AutoUpdate
574+
}
575+
576+
// SetAutoUpdate sets the auto-update mode. Valid values: "notify", "silent", "off".
577+
func SetAutoUpdate(mode string) error {
578+
cfg, err := LoadConfig()
579+
if err != nil {
580+
return err
581+
}
582+
cfg.AutoUpdate = mode
583+
return SaveConfig(cfg)
584+
}
585+
562586
// ProjectsDirOptions returns preset directory options for the projects dir setting.
563587
func ProjectsDirOptions() []string {
564588
home, err := os.UserHomeDir()

0 commit comments

Comments
 (0)