新增 CLI 自诊断命令 #132

Merged
wbtiger merged 1 commits from Mengz/gitlink-cli:mengz/doctor-command into master 2026-06-08 02:19:54 +08:00
9 changed files with 605 additions and 1 deletions

View File

@ -182,6 +182,13 @@ export GITLINK_TOKEN="your-private-token"
gitlink-cli user +me
```
If the CLI cannot detect authentication, config, or repository context correctly, run:
```bash
gitlink-cli doctor
gitlink-cli doctor --skip-network --format json
```
## Usage Examples
### Repository Operations
@ -547,6 +554,12 @@ Get-Content issue.json | gitlink-cli api POST /Gitlink/forgeplus/issues --body-s
gitlink-cli api GET /Gitlink/forgeplus/commits --query 'page=1&limit=5'
```
### Environment Diagnostics
`doctor` checks the local config file, config values, stored token or `GITLINK_TOKEN`,
repository context detection, and authenticated API connectivity. Use `--skip-network`
when running in CI or an offline environment.
## Global Parameters
| Parameter | Description | Example |

View File

@ -193,6 +193,13 @@ export GITLINK_TOKEN="your-private-token"
gitlink-cli user +me
```
如果 CLI 无法正确识别认证、配置或仓库上下文,可以运行:
```bash
gitlink-cli doctor
gitlink-cli doctor --skip-network --format json
```
## 使用示例
### 仓库操作
@ -426,6 +433,12 @@ Get-Content issue.json | gitlink-cli api POST /Gitlink/forgeplus/issues --body-s
gitlink-cli api GET /Gitlink/forgeplus/commits --query 'page=1&limit=5'
```
### 环境诊断
`doctor` 会检查本地配置文件、配置项、已保存 Token 或 `GITLINK_TOKEN`
仓库上下文识别和认证 API 连通性。在 CI 或离线环境中可以使用 `--skip-network`
跳过远端访问。
## 全局参数
| 参数 | 说明 | 示例 |

View File

@ -57,7 +57,7 @@ func TestRootCmdHasSubcommands(t *testing.T) {
for _, sub := range root.Commands() {
names[sub.Use] = true
}
for _, want := range []string{"auth", "config", "version"} {
for _, want := range []string{"auth", "config", "doctor", "version"} {
if !names[want] {
t.Fatalf("missing subcommand: %s", want)
}

319
cmd/doctor/doctor.go Normal file
View File

@ -0,0 +1,319 @@
package doctor
import (
"fmt"
"net/url"
"os"
"strings"
"github.com/spf13/cobra"
"github.com/gitlink-org/gitlink-cli/cmd/cmdutil"
internalAuth "github.com/gitlink-org/gitlink-cli/internal/auth"
internalConfig "github.com/gitlink-org/gitlink-cli/internal/config"
repoContext "github.com/gitlink-org/gitlink-cli/internal/context"
"github.com/gitlink-org/gitlink-cli/internal/i18n"
"github.com/gitlink-org/gitlink-cli/internal/output"
)
const (
statusOK = "ok"
statusWarning = "warning"
statusError = "error"
statusSkipped = "skipped"
)
var (
loadConfig = internalConfig.Load
loadToken = internalAuth.LoadToken
getCurrentUser = internalAuth.GetCurrentUser
resolveOwnerRepo = repoContext.ResolveOwnerRepo
statFile = os.Stat
lookupEnv = os.LookupEnv
)
type Report struct {
OK bool `json:"ok"`
Summary Summary `json:"summary"`
Checks []Check `json:"checks"`
Actions []string `json:"actions,omitempty"`
}
type Summary struct {
OK int `json:"ok"`
Warning int `json:"warning"`
Error int `json:"error"`
Skipped int `json:"skipped"`
Total int `json:"total"`
}
type Check struct {
Name string `json:"name"`
Status string `json:"status"`
Message string `json:"message"`
Suggestion string `json:"suggestion,omitempty"`
Details map[string]interface{} `json:"details,omitempty"`
}
func NewDoctorCmd(translators ...*i18n.Translator) *cobra.Command {
tr := i18n.Default()
if len(translators) > 0 && translators[0] != nil {
tr = translators[0]
}
var skipNetwork bool
cmd := &cobra.Command{
Use: "doctor",
Short: tr.T("cmd.doctor.short"),
Long: tr.T("cmd.doctor.long"),
RunE: func(cmd *cobra.Command, args []string) error {
report := Run(skipNetwork, tr)
return output.PrintTo(cmd.OutOrStdout(), output.SuccessEnvelope(report, nil), resolveFormat())
},
}
cmd.Flags().BoolVar(&skipNetwork, "skip-network", false, tr.T("flag.doctor.skip_network"))
return cmd
}
func Run(skipNetwork bool, tr *i18n.Translator) Report {
if tr == nil {
tr = i18n.Default()
}
checks := make([]Check, 0, 5)
cfg, cfgErr := loadConfig()
checks = append(checks, checkConfigFile(tr, cfgErr))
checks = append(checks, checkConfigValues(tr, cfg, cfgErr))
checks = append(checks, checkAuthToken(tr))
checks = append(checks, checkRepoContext(tr))
checks = append(checks, checkAuthenticatedUser(tr, skipNetwork, cfgErr))
report := Report{OK: true, Checks: checks}
seenActions := map[string]bool{}
for _, check := range checks {
report.Summary.Total++
switch check.Status {
case statusOK:
report.Summary.OK++
case statusWarning:
report.Summary.Warning++
case statusError:
report.OK = false
report.Summary.Error++
case statusSkipped:
report.Summary.Skipped++
}
if check.Suggestion != "" && !seenActions[check.Suggestion] {
report.Actions = append(report.Actions, check.Suggestion)
seenActions[check.Suggestion] = true
}
}
return report
}
func checkConfigFile(tr *i18n.Translator, cfgErr error) Check {
path := internalConfig.ConfigPath()
info, err := statFile(path)
if err != nil {
if os.IsNotExist(err) {
return Check{
Name: "config_file",
Status: statusWarning,
Message: tr.T("output.doctor.config_file.missing"),
Suggestion: "gitlink-cli config init",
Details: map[string]interface{}{"path": path},
}
}
return Check{
Name: "config_file",
Status: statusError,
Message: tr.Tf("output.doctor.config_file.unreadable", i18n.Args{"message": err.Error()}),
Suggestion: tr.T("output.doctor.suggestion.check_config_permissions"),
Details: map[string]interface{}{"path": path},
}
}
if cfgErr != nil {
return Check{
Name: "config_file",
Status: statusError,
Message: tr.Tf("output.doctor.config_file.invalid", i18n.Args{"message": cfgErr.Error()}),
Suggestion: tr.T("output.doctor.suggestion.fix_config_yaml"),
Details: map[string]interface{}{"path": path},
}
}
return Check{
Name: "config_file",
Status: statusOK,
Message: tr.T("output.doctor.config_file.ok"),
Details: map[string]interface{}{
"path": path,
"size": info.Size(),
},
}
}
func checkConfigValues(tr *i18n.Translator, cfg *internalConfig.Config, cfgErr error) Check {
if cfgErr != nil || cfg == nil {
return Check{
Name: "config_values",
Status: statusSkipped,
Message: tr.T("output.doctor.config_values.skipped"),
Suggestion: tr.T("output.doctor.suggestion.fix_config_yaml"),
}
}
details := map[string]interface{}{
"base_url": cfg.BaseURL,
"default_format": cfg.Format,
}
if err := validateBaseURL(cfg.BaseURL); err != nil {
return Check{
Name: "config_values",
Status: statusError,
Message: tr.Tf("output.doctor.config_values.bad_base_url", i18n.Args{"message": err.Error()}),
Suggestion: "gitlink-cli config set base_url https://www.gitlink.org.cn/api",
Details: details,
}
}
if !validFormat(cfg.Format) {
return Check{
Name: "config_values",
Status: statusWarning,
Message: tr.Tf("output.doctor.config_values.bad_format", i18n.Args{"format": cfg.Format}),
Suggestion: "gitlink-cli config set default_format table",
Details: details,
}
}
return Check{
Name: "config_values",
Status: statusOK,
Message: tr.T("output.doctor.config_values.ok"),
Details: details,
}
}
func checkAuthToken(tr *i18n.Translator) Check {
if token, ok := lookupEnv("GITLINK_TOKEN"); ok && strings.TrimSpace(token) != "" {
return Check{
Name: "auth_token",
Status: statusOK,
Message: tr.T("output.doctor.auth_token.env"),
Details: map[string]interface{}{"source": "env"},
}
}
token, err := loadToken()
if err != nil || strings.TrimSpace(token) == "" {
return Check{
Name: "auth_token",
Status: statusWarning,
Message: tr.T("output.doctor.auth_token.missing"),
Suggestion: "gitlink-cli auth login",
}
}
source := "token"
if strings.HasPrefix(token, "cookie:") {
source = "cookie"
}
return Check{
Name: "auth_token",
Status: statusOK,
Message: tr.T("output.doctor.auth_token.stored"),
Details: map[string]interface{}{"source": source},
}
}
func checkRepoContext(tr *i18n.Translator) Check {
owner, repo, err := resolveOwnerRepo(cmdutil.Owner, cmdutil.Repo)
if err != nil {
return Check{
Name: "repo_context",
Status: statusWarning,
Message: tr.Tf("output.doctor.repo_context.missing", i18n.Args{"message": err.Error()}),
Suggestion: tr.T("output.doctor.suggestion.pass_owner_repo"),
}
}
return Check{
Name: "repo_context",
Status: statusOK,
Message: tr.Tf("output.doctor.repo_context.ok", i18n.Args{"owner": owner, "repo": repo}),
Details: map[string]interface{}{
"owner": owner,
"repo": repo,
},
}
}
func checkAuthenticatedUser(tr *i18n.Translator, skipNetwork bool, cfgErr error) Check {
if skipNetwork {
return Check{
Name: "api_auth",
Status: statusSkipped,
Message: tr.T("output.doctor.api_auth.skipped"),
}
}
if cfgErr != nil {
return Check{
Name: "api_auth",
Status: statusSkipped,
Message: tr.T("output.doctor.api_auth.config_skipped"),
Suggestion: tr.T("output.doctor.suggestion.fix_config_yaml"),
}
}
user, err := getCurrentUser()
if err != nil {
return Check{
Name: "api_auth",
Status: statusError,
Message: tr.Tf("output.doctor.api_auth.failed", i18n.Args{"message": err.Error()}),
Suggestion: "gitlink-cli auth login",
}
}
login, _ := user["login"].(string)
if login == "" {
return Check{
Name: "api_auth",
Status: statusWarning,
Message: tr.T("output.doctor.api_auth.no_login"),
Suggestion: tr.T("output.doctor.suggestion.check_token"),
}
}
return Check{
Name: "api_auth",
Status: statusOK,
Message: tr.Tf("output.doctor.api_auth.ok", i18n.Args{"login": login}),
Details: map[string]interface{}{
"login": login,
},
}
}
func validateBaseURL(value string) error {
u, err := url.Parse(value)
if err != nil {
return err
}
if u.Scheme != "http" && u.Scheme != "https" {
return fmt.Errorf("scheme must be http or https")
}
if u.Host == "" {
return fmt.Errorf("host is required")
}
return nil
}
func validFormat(value string) bool {
switch value {
case "json", "table", "yaml":
return true
default:
return false
}
}
func resolveFormat() string {
if cmdutil.Format != "" {
return cmdutil.Format
}
return "json"
}

200
cmd/doctor/doctor_test.go Normal file
View File

@ -0,0 +1,200 @@
package doctor
import (
"bytes"
"encoding/json"
"errors"
"os"
"path/filepath"
"testing"
"github.com/gitlink-org/gitlink-cli/cmd/cmdutil"
"github.com/gitlink-org/gitlink-cli/internal/i18n"
)
func TestDoctorSkipNetworkReportsLocalChecks(t *testing.T) {
withDoctorTestState(t)
writeConfig(t, "base_url: https://www.gitlink.org.cn/api\ndefault_format: json\n")
t.Setenv("GITLINK_TOKEN", "secret-token")
resolveOwnerRepo = func(owner, repo string) (string, string, error) {
return "Gitlink", "gitlink-cli", nil
}
report := Run(true, i18n.Default())
if !report.OK {
t.Fatalf("expected report OK, got %+v", report)
}
assertCheck(t, report, "config_file", statusOK)
assertCheck(t, report, "config_values", statusOK)
assertCheck(t, report, "auth_token", statusOK)
assertCheck(t, report, "repo_context", statusOK)
assertCheck(t, report, "api_auth", statusSkipped)
if report.Summary.Total != 5 {
t.Fatalf("summary total = %d, want 5", report.Summary.Total)
}
}
func TestDoctorInvalidConfigDoesNotPanic(t *testing.T) {
withDoctorTestState(t)
writeConfig(t, "base_url: [broken\n")
resolveOwnerRepo = func(owner, repo string) (string, string, error) {
return "Gitlink", "gitlink-cli", nil
}
report := Run(true, i18n.Default())
if report.OK {
t.Fatalf("expected report not OK, got %+v", report)
}
assertCheck(t, report, "config_file", statusError)
assertCheck(t, report, "config_values", statusSkipped)
}
func TestDoctorInvalidBaseURL(t *testing.T) {
withDoctorTestState(t)
writeConfig(t, "base_url: gitlink.local/api\ndefault_format: table\n")
resolveOwnerRepo = func(owner, repo string) (string, string, error) {
return "Gitlink", "gitlink-cli", nil
}
report := Run(true, i18n.Default())
if report.OK {
t.Fatalf("expected invalid base_url to mark report not OK")
}
check := assertCheck(t, report, "config_values", statusError)
if check.Suggestion == "" {
t.Fatalf("expected config_values suggestion")
}
}
func TestDoctorMissingRepoContextIsWarning(t *testing.T) {
withDoctorTestState(t)
writeConfig(t, "base_url: https://www.gitlink.org.cn/api\ndefault_format: table\n")
resolveOwnerRepo = func(owner, repo string) (string, string, error) {
return "", "", errors.New("no origin remote")
}
report := Run(true, i18n.Default())
assertCheck(t, report, "auth_token", statusWarning)
check := assertCheck(t, report, "repo_context", statusWarning)
if check.Suggestion == "" {
t.Fatalf("expected repo_context suggestion")
}
if !report.OK {
t.Fatalf("warnings should not make report fail: %+v", report)
}
}
func TestDoctorNetworkCheckCanSucceed(t *testing.T) {
withDoctorTestState(t)
writeConfig(t, "base_url: https://www.gitlink.org.cn/api\ndefault_format: table\n")
resolveOwnerRepo = func(owner, repo string) (string, string, error) {
return "Gitlink", "gitlink-cli", nil
}
getCurrentUser = func() (map[string]interface{}, error) {
return map[string]interface{}{"login": "Mengz"}, nil
}
report := Run(false, i18n.Default())
assertCheck(t, report, "api_auth", statusOK)
if !report.OK {
t.Fatalf("expected report OK, got %+v", report)
}
}
func TestDoctorCommandPrintsJSONEnvelope(t *testing.T) {
withDoctorTestState(t)
writeConfig(t, "base_url: https://www.gitlink.org.cn/api\ndefault_format: json\n")
resolveOwnerRepo = func(owner, repo string) (string, string, error) {
return "Gitlink", "gitlink-cli", nil
}
cmd := NewDoctorCmd(i18n.Default())
cmd.SetArgs([]string{"--skip-network"})
var out bytes.Buffer
cmd.SetOut(&out)
if err := cmd.Execute(); err != nil {
t.Fatal(err)
}
var env struct {
OK bool `json:"ok"`
Data json.RawMessage `json:"data"`
}
if err := json.Unmarshal(out.Bytes(), &env); err != nil {
t.Fatalf("invalid JSON output: %v\n%s", err, out.String())
}
if !env.OK || len(env.Data) == 0 {
t.Fatalf("unexpected envelope: %+v", env)
}
}
func withDoctorTestState(t *testing.T) {
t.Helper()
oldLoadConfig := loadConfig
oldLoadToken := loadToken
oldGetCurrentUser := getCurrentUser
oldResolveOwnerRepo := resolveOwnerRepo
oldStatFile := statFile
oldLookupEnv := lookupEnv
oldFormat := cmdutil.Format
oldOwner := cmdutil.Owner
oldRepo := cmdutil.Repo
t.Setenv("GITLINK_CONFIG_DIR", t.TempDir())
t.Setenv("GITLINK_TOKEN", "")
cmdutil.Format = "json"
cmdutil.Owner = ""
cmdutil.Repo = ""
loadConfig = oldLoadConfig
loadToken = func() (string, error) { return "", os.ErrNotExist }
getCurrentUser = func() (map[string]interface{}, error) {
return nil, errors.New("unexpected network call")
}
resolveOwnerRepo = oldResolveOwnerRepo
statFile = oldStatFile
lookupEnv = func(key string) (string, bool) {
if key == "GITLINK_TOKEN" {
value := os.Getenv(key)
return value, value != ""
}
return os.LookupEnv(key)
}
t.Cleanup(func() {
loadConfig = oldLoadConfig
loadToken = oldLoadToken
getCurrentUser = oldGetCurrentUser
resolveOwnerRepo = oldResolveOwnerRepo
statFile = oldStatFile
lookupEnv = oldLookupEnv
cmdutil.Format = oldFormat
cmdutil.Owner = oldOwner
cmdutil.Repo = oldRepo
})
}
func writeConfig(t *testing.T, content string) {
t.Helper()
dir := os.Getenv("GITLINK_CONFIG_DIR")
if err := os.MkdirAll(dir, 0700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "config.yaml"), []byte(content), 0600); err != nil {
t.Fatal(err)
}
}
func assertCheck(t *testing.T, report Report, name, status string) Check {
t.Helper()
for _, check := range report.Checks {
if check.Name == name {
if check.Status != status {
t.Fatalf("%s status = %s, want %s; check=%+v", name, check.Status, status, check)
}
return check
}
}
t.Fatalf("missing check %q in %+v", name, report.Checks)
return Check{}
}

View File

@ -11,6 +11,7 @@ import (
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"
doctorCmd "github.com/gitlink-org/gitlink-cli/cmd/doctor"
internalConfig "github.com/gitlink-org/gitlink-cli/internal/config"
"github.com/gitlink-org/gitlink-cli/internal/i18n"
"github.com/gitlink-org/gitlink-cli/shortcuts"
@ -56,6 +57,7 @@ func NewRootCmd(opts RootOptions, tr *i18n.Translator) (*cobra.Command, error) {
rootCmd.AddCommand(authCmd.NewAuthCmd(tr))
rootCmd.AddCommand(apiCmd.NewAPICmd(tr))
rootCmd.AddCommand(configCmd.NewConfigCmd(tr))
rootCmd.AddCommand(doctorCmd.NewDoctorCmd(tr))
rootCmd.AddCommand(newVersionCmd(version, tr))
shortcuts.RegisterAll(rootCmd, tr)

View File

@ -0,0 +1,7 @@
# CLI 自诊断命令
新增 `gitlink-cli doctor`用于在用户遇到“无法认证、仓库识别失败、配置异常、API 请求失败”等问题时快速定位原因。命令会一次性检查配置文件是否存在且可解析、`base_url` 和 `default_format` 是否合理、本地 Token 或 `GITLINK_TOKEN` 是否可用、当前目录能否解析出 GitLink 仓库上下文,以及认证 API 是否能正常返回当前用户。
输出沿用项目已有的 `ok/data/error/meta` 结构,诊断结果包含每个检查项的状态、说明、细节和可执行修复建议,便于人类阅读,也便于 Agent 或 CI 解析。默认会验证认证 API 连通性,`--skip-network` 可在离线环境或 CI 中只做本地检查。
本次变更同时补充了中英文帮助文案、README 使用示例和单元测试。测试覆盖了正常本地检查、损坏配置文件、非法 `base_url`、仓库上下文缺失、认证 API mock 成功,以及命令 JSON envelope 输出,确保诊断命令在常见失败场景下返回结构化结果而不是直接崩溃。

View File

@ -21,6 +21,8 @@
"cmd.config.list.short": "List all configuration values",
"cmd.config.set.short": "Set a configuration value",
"cmd.config.short": "Manage gitlink-cli configuration",
"cmd.doctor.long": "Run local diagnostics for gitlink-cli configuration, authentication, repository context and API connectivity.",
"cmd.doctor.short": "Diagnose gitlink-cli environment problems",
"cmd.issue.batch_close.long": "Close filtered issues in bulk.\n\nThis command defaults to dry-run mode and only prints matching issues.\nPass --yes to execute remote close operations. Use restrictive filters and a small limit.\n\nExamples:\n gitlink-cli issue +batch-close --owner Gitlink --repo gitlink-cli --older-than-days 60 --limit 20\n gitlink-cli issue +batch-close --owner Gitlink --repo gitlink-cli --older-than-days 60 --limit 20 --yes",
"cmd.issue.batch_close.short": "Close filtered issues in bulk. Defaults to dry-run; pass --yes to execute.",
"cmd.issue.batch_label.long": "Add a label to filtered issues in bulk.\n\nThis command defaults to dry-run mode and only prints matching issues.\nPass --yes to execute remote label operations. The current implementation does not fake label writes when the API endpoint is unavailable.\n\nExamples:\n gitlink-cli issue +batch-label --owner Gitlink --repo gitlink-cli --add-label stale --older-than-days 30 --limit 50\n gitlink-cli issue +batch-label --owner Gitlink --repo gitlink-cli --add-label stale --older-than-days 30 --limit 50 --yes",
@ -101,6 +103,7 @@
"flag.comment.body": "Comment body",
"flag.debug": "Enable debug output",
"flag.description": "Description",
"flag.doctor.skip_network": "Skip authenticated API connectivity checks",
"flag.dry_run": "Preview the request without creating it",
"flag.format": "Output format: json, table, yaml (default: table)",
"flag.issue.add_label": "Label to add to each matching issue",
@ -173,6 +176,28 @@
"output.config.file": "Config file: {path}",
"output.config.not_set": "(not set)",
"output.version": "gitlink-cli {version}",
"output.doctor.api_auth.config_skipped": "API authentication check skipped because the configuration file is invalid.",
"output.doctor.api_auth.failed": "Authenticated API request failed: {message}",
"output.doctor.api_auth.no_login": "Authenticated API response did not include a login field.",
"output.doctor.api_auth.ok": "Authenticated API request succeeded as {login}.",
"output.doctor.api_auth.skipped": "Authenticated API connectivity check skipped.",
"output.doctor.auth_token.env": "GITLINK_TOKEN environment variable is set.",
"output.doctor.auth_token.missing": "No stored token or GITLINK_TOKEN environment variable was found.",
"output.doctor.auth_token.stored": "Stored credentials were found.",
"output.doctor.config_file.invalid": "Configuration file exists but cannot be parsed: {message}",
"output.doctor.config_file.missing": "Configuration file was not found; built-in defaults will be used.",
"output.doctor.config_file.ok": "Configuration file is readable.",
"output.doctor.config_file.unreadable": "Configuration file cannot be read: {message}",
"output.doctor.config_values.bad_base_url": "base_url is invalid: {message}",
"output.doctor.config_values.bad_format": "default_format is {format}, expected json, table or yaml.",
"output.doctor.config_values.ok": "Configuration values are valid.",
"output.doctor.config_values.skipped": "Configuration value checks skipped because the configuration file is invalid.",
"output.doctor.repo_context.missing": "Repository context could not be resolved: {message}",
"output.doctor.repo_context.ok": "Repository context resolved to {owner}/{repo}.",
"output.doctor.suggestion.check_config_permissions": "Check file permissions for the gitlink-cli config directory.",
"output.doctor.suggestion.check_token": "Check whether the stored token is valid, or run gitlink-cli auth login again.",
"output.doctor.suggestion.fix_config_yaml": "Fix the YAML syntax in the gitlink-cli config file.",
"output.doctor.suggestion.pass_owner_repo": "Run the command with --owner and --repo when not inside a GitLink repository.",
"prompt.auth.password": "Password: ",
"prompt.auth.token": "Paste your access token: ",
"prompt.auth.username": "Username/Email/Phone: ",

View File

@ -21,6 +21,8 @@
"cmd.config.list.short": "列出所有配置项",
"cmd.config.set.short": "设置配置项",
"cmd.config.short": "管理 gitlink-cli 配置",
"cmd.doctor.long": "诊断 gitlink-cli 的配置、认证、仓库上下文和 API 连通性问题。",
"cmd.doctor.short": "诊断 gitlink-cli 环境问题",
"cmd.issue.batch_close.long": "批量关闭筛选后的议题。\n\n该命令默认处于 dry-run 模式,只打印匹配的议题。\n传入 --yes 后执行远端关闭操作。请使用严格筛选条件和较小 limit。\n\n示例\n gitlink-cli issue +batch-close --owner Gitlink --repo gitlink-cli --older-than-days 60 --limit 20\n gitlink-cli issue +batch-close --owner Gitlink --repo gitlink-cli --older-than-days 60 --limit 20 --yes",
"cmd.issue.batch_close.short": "批量关闭筛选后的议题。默认 dry-run传入 --yes 后执行。",
"cmd.issue.batch_label.long": "给筛选后的议题批量添加标签。\n\n该命令默认处于 dry-run 模式,只打印匹配的议题。\n传入 --yes 后执行远端标签操作。当前实现不会在 API 端点不可用时伪造写入结果。\n\n示例\n gitlink-cli issue +batch-label --owner Gitlink --repo gitlink-cli --add-label stale --older-than-days 30 --limit 50\n gitlink-cli issue +batch-label --owner Gitlink --repo gitlink-cli --add-label stale --older-than-days 30 --limit 50 --yes",
@ -101,6 +103,7 @@
"flag.comment.body": "评论内容",
"flag.debug": "启用调试输出",
"flag.description": "描述",
"flag.doctor.skip_network": "跳过需要访问 GitLink 的认证连通性检查",
"flag.dry_run": "预览请求,不实际创建",
"flag.format": "输出格式json、table、yaml默认table",
"flag.issue.add_label": "要添加到每个匹配议题的标签",
@ -173,6 +176,28 @@
"output.config.file": "配置文件:{path}",
"output.config.not_set": "(未设置)",
"output.version": "gitlink-cli {version}",
"output.doctor.api_auth.config_skipped": "配置文件无效,已跳过 API 认证检查。",
"output.doctor.api_auth.failed": "认证 API 请求失败:{message}",
"output.doctor.api_auth.no_login": "认证 API 响应中缺少 login 字段。",
"output.doctor.api_auth.ok": "认证 API 请求成功,当前用户为 {login}。",
"output.doctor.api_auth.skipped": "已跳过认证 API 连通性检查。",
"output.doctor.auth_token.env": "已设置 GITLINK_TOKEN 环境变量。",
"output.doctor.auth_token.missing": "未找到已保存的 Token也未设置 GITLINK_TOKEN 环境变量。",
"output.doctor.auth_token.stored": "已找到本地保存的认证凭据。",
"output.doctor.config_file.invalid": "配置文件存在,但无法解析:{message}",
"output.doctor.config_file.missing": "未找到配置文件,将使用内置默认值。",
"output.doctor.config_file.ok": "配置文件可读取。",
"output.doctor.config_file.unreadable": "配置文件无法读取:{message}",
"output.doctor.config_values.bad_base_url": "base_url 无效:{message}",
"output.doctor.config_values.bad_format": "default_format 当前为 {format},应为 json、table 或 yaml。",
"output.doctor.config_values.ok": "配置项有效。",
"output.doctor.config_values.skipped": "配置文件无效,已跳过配置项检查。",
"output.doctor.repo_context.missing": "无法解析仓库上下文:{message}",
"output.doctor.repo_context.ok": "仓库上下文已解析为 {owner}/{repo}。",
"output.doctor.suggestion.check_config_permissions": "检查 gitlink-cli 配置目录的文件权限。",
"output.doctor.suggestion.check_token": "检查已保存的 Token 是否有效,或重新运行 gitlink-cli auth login。",
"output.doctor.suggestion.fix_config_yaml": "修复 gitlink-cli 配置文件中的 YAML 语法。",
"output.doctor.suggestion.pass_owner_repo": "不在 GitLink 仓库目录内时,请通过 --owner 和 --repo 指定仓库。",
"prompt.auth.password": "密码:",
"prompt.auth.token": "粘贴你的访问 Token",
"prompt.auth.username": "用户名/邮箱/手机号:",