From dec10afaebc958ba39712e81c52bf1acb88db1b1 Mon Sep 17 00:00:00 2001 From: Mengz <2567587994@qq.com> Date: Sat, 6 Jun 2026 18:11:45 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=20CLI=20=E8=87=AA?= =?UTF-8?q?=E8=AF=8A=E6=96=AD=E5=91=BD=E4=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 13 ++ README.zh-CN.md | 13 ++ cmd/cmd_test.go | 2 +- cmd/doctor/doctor.go | 319 +++++++++++++++++++++++++++++++ cmd/doctor/doctor_test.go | 200 +++++++++++++++++++ cmd/root.go | 2 + doc/changes/doctor-command.md | 7 + internal/i18n/locales/en-US.json | 25 +++ internal/i18n/locales/zh-CN.json | 25 +++ 9 files changed, 605 insertions(+), 1 deletion(-) create mode 100644 cmd/doctor/doctor.go create mode 100644 cmd/doctor/doctor_test.go create mode 100644 doc/changes/doctor-command.md diff --git a/README.md b/README.md index 797e6c7..299d387 100644 --- a/README.md +++ b/README.md @@ -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 | diff --git a/README.zh-CN.md b/README.zh-CN.md index 88c6119..a727d59 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -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` +跳过远端访问。 + ## 全局参数 | 参数 | 说明 | 示例 | diff --git a/cmd/cmd_test.go b/cmd/cmd_test.go index 04f114a..12499db 100644 --- a/cmd/cmd_test.go +++ b/cmd/cmd_test.go @@ -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) } diff --git a/cmd/doctor/doctor.go b/cmd/doctor/doctor.go new file mode 100644 index 0000000..b301fb4 --- /dev/null +++ b/cmd/doctor/doctor.go @@ -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" +} diff --git a/cmd/doctor/doctor_test.go b/cmd/doctor/doctor_test.go new file mode 100644 index 0000000..2f1e85a --- /dev/null +++ b/cmd/doctor/doctor_test.go @@ -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{} +} diff --git a/cmd/root.go b/cmd/root.go index 26278d8..75f8532 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -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) diff --git a/doc/changes/doctor-command.md b/doc/changes/doctor-command.md new file mode 100644 index 0000000..a01590e --- /dev/null +++ b/doc/changes/doctor-command.md @@ -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 输出,确保诊断命令在常见失败场景下返回结构化结果而不是直接崩溃。 diff --git a/internal/i18n/locales/en-US.json b/internal/i18n/locales/en-US.json index d9f3c34..a25b430 100644 --- a/internal/i18n/locales/en-US.json +++ b/internal/i18n/locales/en-US.json @@ -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: ", diff --git a/internal/i18n/locales/zh-CN.json b/internal/i18n/locales/zh-CN.json index 848e605..8594ddd 100644 --- a/internal/i18n/locales/zh-CN.json +++ b/internal/i18n/locales/zh-CN.json @@ -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": "用户名/邮箱/手机号:",