feat(auth): login 新增 --with-token 标准输入非交互登录(对标 gh)

This commit is contained in:
Taoyouce 2026-07-07 16:06:12 +00:00
parent b8e92895f4
commit 6ef51c7d5d
6 changed files with 69 additions and 0 deletions

View File

@ -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.

View File

@ -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。不设置环境变量时完全兼容原有交互式登录。

View File

@ -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",

View File

@ -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")
}
}

View File

@ -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",

View File

@ -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": "构建编号",