From 6ef51c7d5d59af2cdf6edae589c36c48b7623aa2 Mon Sep 17 00:00:00 2001 From: Taoyouce Date: Tue, 7 Jul 2026 16:06:12 +0000 Subject: [PATCH] =?UTF-8?q?feat(auth):=20login=20=E6=96=B0=E5=A2=9E=20--wi?= =?UTF-8?q?th-token=20=E6=A0=87=E5=87=86=E8=BE=93=E5=85=A5=E9=9D=9E?= =?UTF-8?q?=E4=BA=A4=E4=BA=92=E7=99=BB=E5=BD=95=EF=BC=88=E5=AF=B9=E6=A0=87?= =?UTF-8?q?=20gh=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + README.zh-CN.md | 1 + cmd/auth/auth.go | 25 ++++++++++++++++++++ cmd/auth/auth_test.go | 40 ++++++++++++++++++++++++++++++++ internal/i18n/locales/en-US.json | 1 + internal/i18n/locales/zh-CN.json | 1 + 6 files changed, 69 insertions(+) diff --git a/README.md b/README.md index d15d848..1c2f69a 100644 --- a/README.md +++ b/README.md @@ -852,6 +852,7 @@ To reuse the active token in scripts (e.g. raw `curl` calls against endpoints th ```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) +echo $MY_TOKEN | gitlink-cli auth login --with-token # Non-interactive login (CI, scripts) ``` Priority: `GITLINK_TOKEN` env var > keyring/file stored token. When the env var is not set, the original interactive login flow works as before. diff --git a/README.zh-CN.md b/README.zh-CN.md index f446558..2ab4af1 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -724,6 +724,7 @@ gitlink-cli auth status # 显示 "✓ Logged in via GITLINK_TOKEN environment ```bash curl -H "Authorization: Bearer $(gitlink-cli auth token)" https://www.gitlink.org.cn/api/v1/... gitlink-cli auth status --show-token # 查看原始 token(默认隐藏) +echo $MY_TOKEN | gitlink-cli auth login --with-token # 非交互登录(CI/脚本) ``` Token 优先级:`GITLINK_TOKEN` 环境变量 > keyring/文件存储的 token。不设置环境变量时完全兼容原有交互式登录。 diff --git a/cmd/auth/auth.go b/cmd/auth/auth.go index accda03..ef86ec9 100644 --- a/cmd/auth/auth.go +++ b/cmd/auth/auth.go @@ -40,11 +40,15 @@ func NewAuthCmd(translators ...*i18n.Translator) *cobra.Command { func newLoginCmd(tr *i18n.Translator) *cobra.Command { var tokenMode bool + var withToken bool cmd := &cobra.Command{ Use: "login", Short: tr.T("cmd.auth.login.short"), RunE: func(cmd *cobra.Command, args []string) error { + if withToken { + return loginWithTokenStdin(cmd.InOrStdin(), cmd.OutOrStdout(), tr) + } if tokenMode { return loginWithToken(cmd.InOrStdin(), cmd.OutOrStdout(), tr) } @@ -52,6 +56,7 @@ func newLoginCmd(tr *i18n.Translator) *cobra.Command { }, } cmd.Flags().BoolVar(&tokenMode, "token", false, tr.T("flag.auth.token")) + cmd.Flags().BoolVar(&withToken, "with-token", false, tr.T("flag.auth.with_token")) return cmd } @@ -118,6 +123,26 @@ func loginWithToken(in io.Reader, out io.Writer, tr *i18n.Translator) error { return err } +// loginWithTokenStdin reads a token from stdin without prompting, mirroring +// `gh auth login --with-token` for non-interactive use (CI, scripts): +// +// echo $TOKEN | gitlink-cli auth login --with-token +func loginWithTokenStdin(in io.Reader, out io.Writer, tr *i18n.Translator) error { + data, err := io.ReadAll(io.LimitReader(in, 4096)) + if err != nil { + return err + } + token := strings.TrimSpace(string(data)) + if token == "" { + return errors.New(tr.T("error.auth.token_empty")) + } + if err := storeToken(token); err != nil { + return errors.New(tr.Tf("error.auth.store_token_failed", i18n.Args{"message": err.Error()})) + } + _, err = fmt.Fprintln(out, tr.T("success.auth.token_saved")) + return err +} + func newLogoutCmd(tr *i18n.Translator) *cobra.Command { return &cobra.Command{ Use: "logout", diff --git a/cmd/auth/auth_test.go b/cmd/auth/auth_test.go index 09bd820..c4c3e3c 100644 --- a/cmd/auth/auth_test.go +++ b/cmd/auth/auth_test.go @@ -335,3 +335,43 @@ func findSub(cmd *cobra.Command, name string) *cobra.Command { } return nil } + +func TestLoginWithTokenStdin(t *testing.T) { + keyring.MockInit() + tempConfigDir(t) + t.Setenv("GITLINK_TOKEN", "") + _ = internalAuth.DeleteToken() + + cmd := findSub(NewAuthCmd(), "login") + if cmd == nil { + t.Fatal("login subcommand not found") + } + if err := cmd.Flags().Set("with-token", "true"); err != nil { + t.Fatal(err) + } + var buf bytes.Buffer + cmd.SetIn(strings.NewReader("stdin-token-456\n")) + cmd.SetOut(&buf) + if err := cmd.RunE(cmd, nil); err != nil { + t.Fatalf("login --with-token error: %v", err) + } + stored, err := internalAuth.LoadToken() + if err != nil || stored != "stdin-token-456" { + t.Fatalf("stored token = %q (err %v), want stdin-token-456", stored, err) + } +} + +func TestLoginWithTokenStdinEmpty(t *testing.T) { + keyring.MockInit() + tempConfigDir(t) + + cmd := findSub(NewAuthCmd(), "login") + if err := cmd.Flags().Set("with-token", "true"); err != nil { + t.Fatal(err) + } + cmd.SetIn(strings.NewReader("\n")) + cmd.SetOut(&bytes.Buffer{}) + if err := cmd.RunE(cmd, nil); err == nil { + t.Fatal("expected error for empty stdin token") + } +} diff --git a/internal/i18n/locales/en-US.json b/internal/i18n/locales/en-US.json index e9efcee..315787f 100644 --- a/internal/i18n/locales/en-US.json +++ b/internal/i18n/locales/en-US.json @@ -128,6 +128,7 @@ "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.auth.with_token": "Read the token from standard input (non-interactive)", "flag.branch.from": "Source branch or commit", "flag.branch.name": "Branch name", "flag.ci.build": "Build number", diff --git a/internal/i18n/locales/zh-CN.json b/internal/i18n/locales/zh-CN.json index 772d1d0..79c231f 100644 --- a/internal/i18n/locales/zh-CN.json +++ b/internal/i18n/locales/zh-CN.json @@ -128,6 +128,7 @@ "flag.api.query": "查询参数(key=val&key2=val2)", "flag.auth.show_token": "在状态输出中显示原始 token", "flag.auth.token": "通过粘贴已有 Token 登录", + "flag.auth.with_token": "从标准输入读取令牌(非交互,适合脚本/CI)", "flag.branch.from": "源分支或 Commit", "flag.branch.name": "分支名称", "flag.ci.build": "构建编号",