diff --git a/cmd/alias/alias.go b/cmd/alias/alias.go new file mode 100644 index 0000000..059b052 --- /dev/null +++ b/cmd/alias/alias.go @@ -0,0 +1,119 @@ +package alias + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + "gopkg.in/yaml.v3" + + "github.com/gitlink-org/gitlink-cli/internal/config" +) + +// AliasConfig represents the aliases section of the CLI config. +type AliasConfig struct { + Aliases map[string]string `yaml:"aliases,omitempty"` +} + +// NewAliasCmd creates the alias command with subcommands. +func NewAliasCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "alias", + Short: "管理命令别名(把长命令变短)", + Long: `管理 gitlink-cli 的命令别名。 + +别名允许你为常用命令创建简短的名称,例如: + gitlink-cli alias +set rl "repo +list" + 之后可以使用: gitlink-cli rl + +别名存储在 ~/.config/gitlink-cli/aliases.yaml 中。`, + Example: ` gitlink-cli alias +list + gitlink-cli alias +set rl "repo +list" + gitlink-cli alias +set ri "repo +info --owner Gitlink --repo gitlink-cli" + gitlink-cli alias +delete rl`, + } + + cmd.AddCommand( + &cobra.Command{ + Use: "+list", + Short: "列出所有已定义的别名", + Long: "列出所有已定义的命令别名。如果没有任何别名,会给出创建提示。", + RunE: func(cmd *cobra.Command, args []string) error { + aliases, _ := loadAliases() + if len(aliases) == 0 { + fmt.Println("(未定义任何别名)") + fmt.Println("使用 alias +set <名称> <命令> 来创建别名") + return nil + } + for k, v := range aliases { + fmt.Printf(" %-15s → %s\n", k, v) + } + return nil + }, + }, + &cobra.Command{ + Use: "+set ", + Short: "设置别名", + Long: "为一条命令设置别名。如果别名已存在,会被覆盖。", + Args: cobra.ExactArgs(2), + Example: ` gitlink-cli alias +set rl "repo +list" + gitlink-cli alias +set ri "repo +info"`, + RunE: func(cmd *cobra.Command, args []string) error { + aliases, _ := loadAliases() + aliases[args[0]] = args[1] + if err := saveAliases(aliases); err != nil { + return err + } + fmt.Printf("别名已设置: %s → %s\n", args[0], args[1]) + return nil + }, + }, + &cobra.Command{ + Use: "+delete ", + Short: "删除别名", + Long: "删除一个已定义的命令别名。", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + aliases, _ := loadAliases() + if _, ok := aliases[args[0]]; !ok { + return fmt.Errorf("别名 %s 不存在", args[0]) + } + delete(aliases, args[0]) + if err := saveAliases(aliases); err != nil { + return err + } + fmt.Printf("别名已删除: %s\n", args[0]) + return nil + }, + }, + ) + return cmd +} + +func aliasesPath() string { + return config.ConfigDir() + "/aliases.yaml" +} + +func loadAliases() (map[string]string, error) { + data, err := os.ReadFile(aliasesPath()) + if err != nil { + return make(map[string]string), nil + } + var ac AliasConfig + if err := yaml.Unmarshal(data, &ac); err != nil { + return make(map[string]string), nil + } + if ac.Aliases == nil { + ac.Aliases = make(map[string]string) + } + return ac.Aliases, nil +} + +func saveAliases(a map[string]string) error { + data, err := yaml.Marshal(AliasConfig{Aliases: a}) + if err != nil { + return err + } + os.MkdirAll(config.ConfigDir(), 0700) + return os.WriteFile(aliasesPath(), data, 0600) +} diff --git a/cmd/browse/browse.go b/cmd/browse/browse.go new file mode 100644 index 0000000..58400a2 --- /dev/null +++ b/cmd/browse/browse.go @@ -0,0 +1,58 @@ +package browse + +import ( + "fmt" + "os/exec" + "runtime" + + "github.com/spf13/cobra" + + "github.com/gitlink-org/gitlink-cli/internal/context" +) + +// NewBrowseCmd creates the browse command for opening GitLink pages in a browser. +func NewBrowseCmd() *cobra.Command { + return &cobra.Command{ + Use: "browse [resource]", + Short: "在浏览器中打开 GitLink 页面", + Long: `打开当前仓库(或指定资源)的 GitLink 页面。 + +如果不带参数,打开当前仓库主页。 +资源格式: issues/42, pulls/42, wiki + +浏览器打开命令: + - macOS: open + - Windows: start + - Linux: xdg-open`, + Example: ` gitlink-cli browse + gitlink-cli browse issues/42 + gitlink-cli browse pulls/128 + gitlink-cli browse wiki`, + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + owner, repo, err := context.ResolveOwnerRepo("", "") + if err != nil { + return fmt.Errorf("无法推断仓库信息: %w", err) + } + url := fmt.Sprintf("https://gitlink.org.cn/%s/%s", owner, repo) + if len(args) > 0 { + url += "/" + args[0] + } + fmt.Printf("正在打开: %s\n", url) + return openBrowser(url) + }, + } +} + +func openBrowser(url string) error { + var cmd *exec.Cmd + switch runtime.GOOS { + case "darwin": + cmd = exec.Command("open", url) + case "windows": + cmd = exec.Command("cmd", "/c", "start", url) + default: + cmd = exec.Command("xdg-open", url) + } + return cmd.Start() +} diff --git a/cmd/root.go b/cmd/root.go index 26278d8..1e83fb5 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -7,7 +7,10 @@ import ( "github.com/spf13/cobra" + aliasCmd "github.com/gitlink-org/gitlink-cli/cmd/alias" apiCmd "github.com/gitlink-org/gitlink-cli/cmd/api" + browseCmd "github.com/gitlink-org/gitlink-cli/cmd/browse" + statusCmd "github.com/gitlink-org/gitlink-cli/cmd/status" authCmd "github.com/gitlink-org/gitlink-cli/cmd/auth" "github.com/gitlink-org/gitlink-cli/cmd/cmdutil" configCmd "github.com/gitlink-org/gitlink-cli/cmd/config" @@ -57,6 +60,9 @@ func NewRootCmd(opts RootOptions, tr *i18n.Translator) (*cobra.Command, error) { rootCmd.AddCommand(apiCmd.NewAPICmd(tr)) rootCmd.AddCommand(configCmd.NewConfigCmd(tr)) rootCmd.AddCommand(newVersionCmd(version, tr)) + rootCmd.AddCommand(aliasCmd.NewAliasCmd()) + rootCmd.AddCommand(browseCmd.NewBrowseCmd()) + rootCmd.AddCommand(statusCmd.NewStatusCmd()) shortcuts.RegisterAll(rootCmd, tr) diff --git a/cmd/status/status.go b/cmd/status/status.go new file mode 100644 index 0000000..be4b8b2 --- /dev/null +++ b/cmd/status/status.go @@ -0,0 +1,68 @@ +package status + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + + "github.com/gitlink-org/gitlink-cli/internal/auth" + "github.com/gitlink-org/gitlink-cli/internal/config" + "github.com/gitlink-org/gitlink-cli/internal/context" +) + +// NewStatusCmd creates the status command that displays login state and context. +func NewStatusCmd() *cobra.Command { + return &cobra.Command{ + Use: "status", + Short: "显示当前登录状态和上下文信息", + Long: `显示 gitlink-cli 的当前状态,包括: + - 认证状态(是否已登录、Token 来源) + - API 地址 + - 当前目录 + - 自动推断的仓库信息`, + Example: ` gitlink-cli status`, + RunE: func(cmd *cobra.Command, args []string) error { + cfg, _ := config.Load() + token, _ := auth.LoadToken() + if token == "" { + token = os.Getenv("GITLINK_TOKEN") + } + cwd, _ := os.Getwd() + + fmt.Println("GitLink CLI 状态") + fmt.Println("───────────────") + + // 认证状态 + if token != "" { + fmt.Println(" 认证状态: 已登录") + fmt.Printf(" Token 来源: %s\n", tokenSource(token)) + } else { + fmt.Println(" 认证状态: 未登录(运行 gitlink-cli auth login)") + } + + // API 地址 + fmt.Printf(" API 地址: %s\n", cfg.BaseURL) + + // 当前目录 + fmt.Printf(" 当前目录: %s\n", cwd) + + // 推断的仓库 + owner, repo, err := context.ResolveOwnerRepo("", "") + if err == nil { + fmt.Printf(" 推断仓库: %s/%s\n", owner, repo) + } else { + fmt.Println(" 推断仓库: (不在 Git 仓库中)") + } + + return nil + }, + } +} + +func tokenSource(token string) string { + if token == os.Getenv("GITLINK_TOKEN") { + return "环境变量 GITLINK_TOKEN" + } + return "keyring / 配置文件" +}