From b8e92895f4a70220a54b620ff6c868e2debaed8f Mon Sep 17 00:00:00 2001 From: Taoyouce Date: Tue, 7 Jul 2026 15:33:39 +0000 Subject: [PATCH] =?UTF-8?q?feat(auth):=20=E6=96=B0=E5=A2=9E=20auth=20token?= =?UTF-8?q?=20=E5=AD=90=E5=91=BD=E4=BB=A4=E4=B8=8E=20auth=20status=20--sho?= =?UTF-8?q?w-token=EF=BC=88=E5=AF=B9=E6=A0=87=20gh=20auth=20token=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 7 +++ README.zh-CN.md | 7 +++ cmd/auth/auth.go | 40 ++++++++++++- cmd/auth/auth_test.go | 96 +++++++++++++++++++++++++++++++- internal/i18n/locales/en-US.json | 5 ++ internal/i18n/locales/zh-CN.json | 5 ++ 6 files changed, 158 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e5e4318..d15d848 100644 --- a/README.md +++ b/README.md @@ -847,6 +847,13 @@ gitlink-cli repo +list # Ready to use gitlink-cli auth status # Shows "✓ Logged in via GITLINK_TOKEN environment variable" ``` +To reuse the active token in scripts (e.g. raw `curl` calls against endpoints the CLI does not wrap yet): + +```bash +curl -H "Authorization: Bearer $(gitlink-cli auth token)" https://www.gitlink.org.cn/api/v1/... +gitlink-cli auth status --show-token # Inspect the raw token (hidden by default) +``` + Priority: `GITLINK_TOKEN` env var > keyring/file stored token. When the env var is not set, the original interactive login flow works as before. ### Q: What if npm installs successfully but `gitlink-cli` reports a missing binary? diff --git a/README.zh-CN.md b/README.zh-CN.md index 6a8879d..f446558 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -719,6 +719,13 @@ gitlink-cli repo +list # 直接可用 gitlink-cli auth status # 显示 "✓ Logged in via GITLINK_TOKEN environment variable" ``` +脚本中复用当前生效的 token(例如直接 `curl` CLI 尚未封装的端点): + +```bash +curl -H "Authorization: Bearer $(gitlink-cli auth token)" https://www.gitlink.org.cn/api/v1/... +gitlink-cli auth status --show-token # 查看原始 token(默认隐藏) +``` + Token 优先级:`GITLINK_TOKEN` 环境变量 > keyring/文件存储的 token。不设置环境变量时完全兼容原有交互式登录。 ### Q: npm 安装成功但 `gitlink-cli` 提示缺少二进制怎么办? diff --git a/cmd/auth/auth.go b/cmd/auth/auth.go index 25653d7..accda03 100644 --- a/cmd/auth/auth.go +++ b/cmd/auth/auth.go @@ -34,6 +34,7 @@ func NewAuthCmd(translators ...*i18n.Translator) *cobra.Command { cmd.AddCommand(newLoginCmd(tr)) cmd.AddCommand(newLogoutCmd(tr)) cmd.AddCommand(newStatusCmd(tr)) + cmd.AddCommand(newTokenCmd(tr)) return cmd } @@ -132,7 +133,9 @@ func newLogoutCmd(tr *i18n.Translator) *cobra.Command { } func newStatusCmd(tr *i18n.Translator) *cobra.Command { - return &cobra.Command{ + var showToken bool + + cmd := &cobra.Command{ Use: "status", Short: tr.T("cmd.auth.status.short"), RunE: func(cmd *cobra.Command, args []string) error { @@ -142,9 +145,19 @@ func newStatusCmd(tr *i18n.Translator) *cobra.Command { if _, err := fmt.Fprintln(out, tr.Tf("success.auth.logged_in_via_env", i18n.Args{"env": envTokenVar})); err != nil { return err } + if showToken { + if _, err := fmt.Fprintln(out, tr.Tf("output.auth.token_value", i18n.Args{"token": envToken})); err != nil { + return err + } + } } token, err := loadToken() + if err == nil && token != "" && showToken { + if _, err := fmt.Fprintln(out, tr.Tf("output.auth.token_value", i18n.Args{"token": token})); err != nil { + return err + } + } if err != nil || token == "" { if os.Getenv(envTokenVar) == "" { if _, err := fmt.Fprintln(out, tr.T("warning.auth.not_logged_in")); err != nil { @@ -180,4 +193,29 @@ func newStatusCmd(tr *i18n.Translator) *cobra.Command { return err }, } + cmd.Flags().BoolVar(&showToken, "show-token", false, tr.T("flag.auth.show_token")) + return cmd +} + +// newTokenCmd prints the active token to stdout for scripting, mirroring +// `gh auth token`. Resolution order matches API calls: GITLINK_TOKEN env +// var first, then the stored keyring/file token. +func newTokenCmd(tr *i18n.Translator) *cobra.Command { + return &cobra.Command{ + Use: "token", + Short: tr.T("cmd.auth.token.short"), + Long: tr.T("cmd.auth.token.long"), + RunE: func(cmd *cobra.Command, args []string) error { + token := os.Getenv(envTokenVar) + if token == "" { + stored, err := loadToken() + if err != nil || stored == "" { + return errors.New(tr.T("error.auth.no_token")) + } + token = stored + } + _, err := fmt.Fprintln(cmd.OutOrStdout(), token) + return err + }, + } } diff --git a/cmd/auth/auth_test.go b/cmd/auth/auth_test.go index d09602b..09bd820 100644 --- a/cmd/auth/auth_test.go +++ b/cmd/auth/auth_test.go @@ -1,9 +1,11 @@ package auth import ( + "bytes" "errors" "os" "path/filepath" + "strings" "testing" "github.com/spf13/cobra" @@ -35,7 +37,7 @@ func TestNewAuthCmd(t *testing.T) { } expectedSubs := map[string]bool{ - "login": false, "logout": false, "status": false, + "login": false, "logout": false, "status": false, "token": false, } for _, sub := range cmd.Commands() { if _, ok := expectedSubs[sub.Use]; !ok { @@ -130,6 +132,98 @@ func TestStatusCmdStoredTokenButLoadFails(t *testing.T) { } } +func TestTokenCmdEnvToken(t *testing.T) { + keyring.MockInitWithError(errors.New("keychain unavailable")) + tempConfigDir(t) + t.Setenv("GITLINK_TOKEN", "env-token-123") + _ = internalAuth.DeleteToken() + + cmd := findSub(NewAuthCmd(), "token") + if cmd == nil { + t.Fatal("token subcommand not found") + } + var buf bytes.Buffer + cmd.SetOut(&buf) + if err := cmd.RunE(cmd, nil); err != nil { + t.Fatalf("token error: %v", err) + } + if got := strings.TrimSpace(buf.String()); got != "env-token-123" { + t.Fatalf("token output = %q, want env-token-123", got) + } +} + +func TestTokenCmdStoredToken(t *testing.T) { + keyring.MockInitWithError(errors.New("keychain unavailable")) + dir := tempConfigDir(t) + t.Setenv("GITLINK_TOKEN", "") + + os.MkdirAll(dir, 0700) + os.WriteFile(filepath.Join(dir, "credentials"), []byte("stored-token-456"), 0600) + + cmd := findSub(NewAuthCmd(), "token") + var buf bytes.Buffer + cmd.SetOut(&buf) + if err := cmd.RunE(cmd, nil); err != nil { + t.Fatalf("token error: %v", err) + } + if got := strings.TrimSpace(buf.String()); got != "stored-token-456" { + t.Fatalf("token output = %q, want stored-token-456", got) + } +} + +func TestTokenCmdNoToken(t *testing.T) { + keyring.MockInitWithError(errors.New("keychain unavailable")) + tempConfigDir(t) + t.Setenv("GITLINK_TOKEN", "") + _ = internalAuth.DeleteToken() + + cmd := findSub(NewAuthCmd(), "token") + var buf bytes.Buffer + cmd.SetOut(&buf) + if err := cmd.RunE(cmd, nil); err == nil { + t.Fatal("expected error when no token is available") + } +} + +func TestStatusCmdShowToken(t *testing.T) { + keyring.MockInitWithError(errors.New("keychain unavailable")) + dir := tempConfigDir(t) + t.Setenv("GITLINK_TOKEN", "") + + os.MkdirAll(dir, 0700) + os.WriteFile(filepath.Join(dir, "credentials"), []byte("stored-token-789"), 0600) + + cmd := findSub(NewAuthCmd(), "status") + var buf bytes.Buffer + cmd.SetOut(&buf) + cmd.Flags().Set("show-token", "true") + if err := cmd.RunE(cmd, nil); err != nil { + t.Fatalf("status error: %v", err) + } + if !strings.Contains(buf.String(), "stored-token-789") { + t.Fatalf("status --show-token output missing token: %q", buf.String()) + } +} + +func TestStatusCmdHidesTokenByDefault(t *testing.T) { + keyring.MockInitWithError(errors.New("keychain unavailable")) + dir := tempConfigDir(t) + t.Setenv("GITLINK_TOKEN", "") + + os.MkdirAll(dir, 0700) + os.WriteFile(filepath.Join(dir, "credentials"), []byte("stored-token-789"), 0600) + + cmd := findSub(NewAuthCmd(), "status") + var buf bytes.Buffer + cmd.SetOut(&buf) + if err := cmd.RunE(cmd, nil); err != nil { + t.Fatalf("status error: %v", err) + } + if strings.Contains(buf.String(), "stored-token-789") { + t.Fatalf("status leaked token without --show-token: %q", buf.String()) + } +} + func TestLogoutCmdNoStoredToken(t *testing.T) { keyring.MockInitWithError(errors.New("keychain unavailable")) tempConfigDir(t) diff --git a/internal/i18n/locales/en-US.json b/internal/i18n/locales/en-US.json index 0739395..e9efcee 100644 --- a/internal/i18n/locales/en-US.json +++ b/internal/i18n/locales/en-US.json @@ -5,6 +5,8 @@ "cmd.auth.logout.short": "Logout from GitLink", "cmd.auth.short": "Authentication commands", "cmd.auth.status.short": "Show authentication status", + "cmd.auth.token.long": "Print the token that gitlink-cli would use for API calls, for use in scripts (e.g. `curl -H \"Authorization: Bearer $(gitlink-cli auth token)\"`). Resolution order: GITLINK_TOKEN environment variable, then the stored token.", + "cmd.auth.token.short": "Print the active authentication token", "cmd.branch.create.short": "Create a branch", "cmd.branch.delete.short": "Delete a branch", "cmd.branch.list.short": "List branches", @@ -107,6 +109,7 @@ "cmd.webhook.view.short": "View webhook details", "error.auth.delete_token_failed": "failed to delete token: {message}", "error.auth.login_failed": "login failed: {message}", + "error.auth.no_token": "no token found: run `gitlink-cli auth login` or set GITLINK_TOKEN", "error.auth.store_token_failed": "failed to store token: {message}", "error.auth.token_empty": "token cannot be empty", "error.config.save_failed": "failed to save config: {message}", @@ -123,6 +126,7 @@ "flag.api.body_stdin": "Read request body JSON from stdin", "flag.api.header": "Additional headers (key:value)", "flag.api.query": "Query parameters (key=val&key2=val2)", + "flag.auth.show_token": "Display the raw token in the status output", "flag.auth.token": "Login by pasting an existing token", "flag.branch.from": "Source branch or commit", "flag.branch.name": "Branch name", @@ -231,6 +235,7 @@ "flag.webhook.url": "Webhook target URL", "output.auth.env_hint": " Or set {env} environment variable", "output.auth.login_hint": " Run: gitlink-cli auth login", + "output.auth.token_value": " Token: {token}", "output.config.file": "Config file: {path}", "output.config.not_set": "(not set)", "output.doctor.api_auth.config_skipped": "API authentication check skipped because the configuration file is invalid.", diff --git a/internal/i18n/locales/zh-CN.json b/internal/i18n/locales/zh-CN.json index 2e6fc4d..772d1d0 100644 --- a/internal/i18n/locales/zh-CN.json +++ b/internal/i18n/locales/zh-CN.json @@ -5,6 +5,8 @@ "cmd.auth.logout.short": "退出 GitLink 登录", "cmd.auth.short": "认证命令", "cmd.auth.status.short": "显示认证状态", + "cmd.auth.token.long": "输出 gitlink-cli 调用 API 时实际使用的 token,便于脚本使用(如 `curl -H \"Authorization: Bearer $(gitlink-cli auth token)\"`)。解析顺序:GITLINK_TOKEN 环境变量优先,其次是已保存的 token。", + "cmd.auth.token.short": "输出当前生效的认证 token", "cmd.branch.create.short": "创建分支", "cmd.branch.delete.short": "删除分支", "cmd.branch.list.short": "列出分支", @@ -107,6 +109,7 @@ "cmd.webhook.view.short": "查看 Webhook 详情", "error.auth.delete_token_failed": "删除 Token 失败:{message}", "error.auth.login_failed": "登录失败:{message}", + "error.auth.no_token": "未找到 token:请运行 `gitlink-cli auth login` 或设置 GITLINK_TOKEN", "error.auth.store_token_failed": "保存 Token 失败:{message}", "error.auth.token_empty": "Token 不能为空", "error.config.save_failed": "保存配置失败:{message}", @@ -123,6 +126,7 @@ "flag.api.body_stdin": "从标准输入读取 JSON 请求体", "flag.api.header": "附加请求头(key:value)", "flag.api.query": "查询参数(key=val&key2=val2)", + "flag.auth.show_token": "在状态输出中显示原始 token", "flag.auth.token": "通过粘贴已有 Token 登录", "flag.branch.from": "源分支或 Commit", "flag.branch.name": "分支名称", @@ -231,6 +235,7 @@ "flag.webhook.url": "Webhook 目标 URL", "output.auth.env_hint": " 或设置 {env} 环境变量", "output.auth.login_hint": " 运行:gitlink-cli auth login", + "output.auth.token_value": " Token: {token}", "output.config.file": "配置文件:{path}", "output.config.not_set": "(未设置)", "output.doctor.api_auth.config_skipped": "配置文件无效,已跳过 API 认证检查。",