internal/dailyops + cli:日常操作命令 issues/pulls/labels/milestones/releases/repos/api(#8)

- internal/gitea:用戶端支援寫入動詞(DoJSON/DoRaw,共用 do 核心)
- internal/dailyops:七組日常操作的 API 層(issues/pulls/labels/milestones/releases/repos/api)
- internal/cli:子命令接線;寫入類一律 --yes 閘門(未確認不發請求、exit 2)
- teai pulls 相容 #6 工作流用法(無子命令);有子命令走日常操作
- 修正 dispatch:命令旗標(如 --has-work、--comments)不再被誤判為未知全域選項
- teai api 支援 path 內含查詢字串
- 單元測試(httptest 假伺服器)+真實站點交叉驗證(讀取命令輸出對照 Gitea API;寫入回路 create→comment→close 於 alex/.profile 驗證後清理)
This commit is contained in:
2026-09-10 09:19:03 +08:00
parent 5583a00bb5
commit 7badd9e46d
7 changed files with 1721 additions and 15 deletions
+92 -2
View File
@@ -12,6 +12,7 @@ package cli
import (
"errors"
"flag"
"fmt"
"io"
"sort"
@@ -144,6 +145,78 @@ var globalFlags = map[string]bool{
"--timeout": true,
}
// extractGlobals 從 args 中「抽取」全域選項(可出現在命令之後、與命令
// 旗標交錯),其餘引數依原順序保留給子命令剖析。未知的選項一律留給
// 子命令處理(可能是命令旗標,如 --has-work),不在這裡報錯。
func extractGlobals(g *Globals, args []string) ([]string, error) {
rest := make([]string, 0, len(args))
applyGlobal := func(name, value string) error {
switch name {
case "--url":
if strings.TrimSpace(value) == "" {
return &ErrUsage{Msg: "--url must not be empty"}
}
g.URL = value
case "--token":
g.Token = value
case "--config":
if strings.TrimSpace(value) == "" {
return &ErrUsage{Msg: "--config must not be empty"}
}
g.ConfigPath = value
case "--output":
f, err := gitea.ParseFormat(value)
if err != nil {
return &ErrUsage{Msg: err.Error()}
}
g.Output = f
case "--timeout":
d, err := time.ParseDuration(value)
if err != nil || d <= 0 {
return &ErrUsage{Msg: fmt.Sprintf("invalid --timeout %q (want e.g. 30s)", value)}
}
g.Timeout = d
}
return nil
}
i := 0
for i < len(args) {
arg := args[i]
if arg == "--" {
rest = append(rest, args[i+1:]...)
return rest, nil
}
if !strings.HasPrefix(arg, "--") {
rest = append(rest, arg)
i++
continue
}
name, inline, hasInline := strings.Cut(arg, "=")
_, ok := globalFlags[name]
if !ok {
// 不是全域選項:留給子命令(可能是 --has-work 這類命令旗標)。
rest = append(rest, arg)
i++
continue
}
var value string
if hasInline {
value = inline
} else {
if i+1 >= len(args) {
return nil, &ErrUsage{Msg: fmt.Sprintf("global option %q requires a value", name)}
}
i++
value = args[i]
}
if err := applyGlobal(name, value); err != nil {
return nil, err
}
i++
}
return rest, nil
}
// parseGlobals 從 args 前端取走全域選項,回傳剩餘引數(命令與其參數)。
// 遇到第一個非選項引數即停;不認得的選項或缺少值都回 ErrUsage。
func parseGlobals(g *Globals, args []string) ([]string, error) {
@@ -214,11 +287,12 @@ func parseGlobals(g *Globals, args []string) ([]string, error) {
// dispatch 執行已註冊的子命令,把錯誤轉成結束碼並輸出。
// 旗標剖析交由各命令自行處理(不同命令有不同旗標,如 pulls --mine);
// 全域選項允許出現在命令之後(README 範例:teai next --output table),
// 先從 args 剝離併入 Globals 再分派。
// dispatch 先從 args 剝離全域選項併入 Globals,其餘(含命令自己的旗標,
// 如 --has-work)原樣交給命令,不會誤判為未知全域選項。
func dispatch(env *Env, name string, args []string) int {
cmd := commands[name]
g := env.Globals
rest, err := parseGlobals(&g, args)
rest, err := extractGlobals(&g, args)
if err != nil {
fmt.Fprintf(env.Err, "teai: %v\n\n", err)
printUsage(env.Err)
@@ -232,6 +306,22 @@ func dispatch(env *Env, name string, args []string) int {
return int(ExitOK)
}
// errHelp 表示使用者要求該命令的說明(-h/--help);FlagSet 已自行印出
// 用法,靜默成功結束即可。
var errHelp = errors.New("help requested")
// parseFlags 剖析命令旗標:-h/--help 回 errHelp(由 exitCodeFor 視為成功),
// 其他剖析錯誤回 ErrUsage。
func parseFlags(fs *flag.FlagSet, args []string) error {
if err := fs.Parse(args); err != nil {
if errors.Is(err, flag.ErrHelp) {
return errHelp
}
return &ErrUsage{Msg: err.Error()}
}
return nil
}
// exitCodeFor 把命令錯誤映射到結束碼:用法錯誤 → 2,API 錯誤 → 3,
// 其他(內部)→ 1。
func exitCodeFor(err error) ExitCode {