From b8e92895f4a70220a54b620ff6c868e2debaed8f Mon Sep 17 00:00:00 2001 From: Taoyouce Date: Tue, 7 Jul 2026 15:33:39 +0000 Subject: [PATCH 1/2] =?UTF-8?q?feat(auth):=20=E6=96=B0=E5=A2=9E=20auth=20t?= =?UTF-8?q?oken=20=E5=AD=90=E5=91=BD=E4=BB=A4=E4=B8=8E=20auth=20status=20-?= =?UTF-8?q?-show-token=EF=BC=88=E5=AF=B9=E6=A0=87=20gh=20auth=20token?= =?UTF-8?q?=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 认证检查。", From 6ef51c7d5d59af2cdf6edae589c36c48b7623aa2 Mon Sep 17 00:00:00 2001 From: Taoyouce Date: Tue, 7 Jul 2026 16:06:12 +0000 Subject: [PATCH 2/2] =?UTF-8?q?feat(auth):=20login=20=E6=96=B0=E5=A2=9E=20?= =?UTF-8?q?--with-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": "构建编号",