forked from Gitlink/gitlink-cli
112 lines
3.8 KiB
Go
112 lines
3.8 KiB
Go
package api
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"net/url"
|
||
"os"
|
||
"strings"
|
||
|
||
"github.com/spf13/cobra"
|
||
|
||
"github.com/gitlink-org/gitlink-cli/cmd/cmdutil"
|
||
"github.com/gitlink-org/gitlink-cli/internal/client"
|
||
"github.com/gitlink-org/gitlink-cli/internal/output"
|
||
)
|
||
|
||
func NewAPICmd() *cobra.Command {
|
||
apiCmd := &cobra.Command{
|
||
Use: "api <METHOD> <PATH>",
|
||
Short: "Make raw API requests to GitLink",
|
||
Long: `Send arbitrary HTTP requests to the GitLink API. Authentication is injected automatically.`,
|
||
Example: ` gitlink-cli api GET /users/me
|
||
gitlink-cli api GET /projects --query 'page=1&limit=10'
|
||
gitlink-cli api POST /:owner/:repo/issues --body '{"subject":"Bug","description":"..."}'
|
||
gitlink-cli api POST /:owner/:repo/issues --body-file ./issue.json`,
|
||
Args: cobra.ExactArgs(2),
|
||
RunE: runAPI,
|
||
}
|
||
|
||
apiCmd.Flags().String("body", "", "Request body (JSON string)")
|
||
apiCmd.Flags().String("body-file", "", "Read JSON body from a file (avoids shell quoting issues)")
|
||
apiCmd.Flags().String("query", "", "Query parameters (key=val&key2=val2)")
|
||
apiCmd.Flags().StringSlice("header", nil, "Additional headers (key:value)")
|
||
|
||
return apiCmd
|
||
}
|
||
|
||
func runAPI(c *cobra.Command, args []string) error {
|
||
method := strings.ToUpper(args[0])
|
||
path := args[1]
|
||
|
||
if !strings.HasPrefix(path, "/") {
|
||
path = "/" + path
|
||
}
|
||
|
||
cli, err := client.New()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
cli.Debug = cmdutil.Debug
|
||
|
||
var body interface{}
|
||
bodyStr, _ := c.Flags().GetString("body")
|
||
bodyFile, _ := c.Flags().GetString("body-file")
|
||
if bodyStr != "" && bodyFile != "" {
|
||
return printAPIError(400, "cannot use both --body and --body-file", "请只用其中一个:--body 用于内联 JSON,--body-file 用于从文件读取")
|
||
}
|
||
if bodyFile != "" {
|
||
raw, err := os.ReadFile(bodyFile)
|
||
if err != nil {
|
||
return printAPIError(400, fmt.Sprintf("read --body-file failed: %v", err), "检查 --body-file 路径是否正确、文件是否存在且有读权限")
|
||
}
|
||
bodyStr = string(raw)
|
||
}
|
||
if bodyStr != "" {
|
||
if err := json.Unmarshal([]byte(bodyStr), &body); err != nil {
|
||
return printAPIError(400, fmt.Sprintf("invalid JSON body: %v", err), "确认 body 是合法 JSON;PowerShell 调用 .exe 时会剥离内嵌双引号,推荐改用 --body-file 从文件读取")
|
||
}
|
||
}
|
||
|
||
var query url.Values
|
||
queryStr, _ := c.Flags().GetString("query")
|
||
if queryStr != "" {
|
||
var err error
|
||
query, err = url.ParseQuery(queryStr)
|
||
if err != nil {
|
||
return printAPIError(400, fmt.Sprintf("invalid query string: %v", err), "query 应为 key=value&key2=value2 形式,注意值需要 URL 编码")
|
||
}
|
||
}
|
||
|
||
env, err := cli.Do(method, path, body, query)
|
||
if err != nil {
|
||
if apiErr, ok := err.(*client.APIError); ok {
|
||
errEnv := output.ErrorEnvelope(apiErr.Code, apiErr.Message, apiErr.Suggestion)
|
||
_ = output.Print(errEnv, resolveFormat())
|
||
return cmdutil.ErrSilent
|
||
}
|
||
// 网络错误 / DNS 失败 / 超时等非 API 错误,也按 envelope 输出保持一致
|
||
return printAPIError(503, fmt.Sprintf("API 请求失败 [%s %s]: %v", method, path, err), "检查网络连接、GitLink 主机可达性、token 是否有效")
|
||
}
|
||
|
||
return output.Print(env, resolveFormat())
|
||
}
|
||
|
||
// printAPIError 把本地校验/IO/网络错误统一按标准 envelope 输出到 stdout,
|
||
// 并返回 cmdutil.ErrSilent 让 cmd.Execute 跳过 stderr 重复打印,仅保留非零退出码。
|
||
// 设计意图:让 `api` 命令的所有错误(包括 JSON 解析、参数冲突、读文件失败、APIError、
|
||
// 网络错误)输出格式与 shortcut 一致,便于 `--format json` + jq 自动化解析。
|
||
func printAPIError(code int, message, suggestion string) error {
|
||
env := output.ErrorEnvelope(code, message, suggestion)
|
||
_ = output.Print(env, resolveFormat())
|
||
return cmdutil.ErrSilent
|
||
}
|
||
|
||
func resolveFormat() string {
|
||
f := cmdutil.Format
|
||
if f == "" {
|
||
return "json"
|
||
}
|
||
return f
|
||
}
|