commit d6914349945d95e174519905464e9bd50112b4e4 Author: ceo Date: Wed Sep 9 22:46:26 2026 +0800 初始化專案:Go 模組骨架、CLI 進入點、README、測試 - go.mod(module gitea.alterminal.com/alterminal/teai,Go 1.26) - cmd/teai/main.go:程式進入點,結束碼交由 internal/cli.Run 處理 - internal/cli:命令列架構(引數剖析、子命令分派、version 命令) - internal/cli/cli_test.go:用法輸出、version、未知命令等基本測試 - README.md:專案說明、建置與開發慣例(繁體中文) - .gitignore:建置產物、測試輸出、環境檔 - 僅使用標準庫,可離線建置;授權待維護者選定 Refs #1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dc9a3eb --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +# 建置產物 +/teai +/teai.exe +/bin/ +/dist/ + +# 測試與覆蓋率 +*.test +*.out +coverage.* + +# 環境與編輯器 +.env +.env.* +.idea/ +.vscode/ +*.swp + +# 作業系統 +.DS_Store +Thumbs.db diff --git a/README.md b/README.md new file mode 100644 index 0000000..398716f --- /dev/null +++ b/README.md @@ -0,0 +1,54 @@ +# teai + +teai 是 Gitea 的命令列(CLI)輔助工具,補足官方 `tea` CLI 缺少的功能。 +名稱取「**te**a + **AI**」:這套工具主要為了讓 AI agent 能以更完整、更可靠的 +方式操作 Gitea(查詢、回覆、審核、自動化……)。 + +- **語言:** Go(與 Gitea 本體一致) +- **目標站點:** `https://gitea.alterminal.com` +- **狀態:** 專案初始化中,功能尚未開始開發 + +## 安裝與建置 + +需要 Go 1.26 以上。 + +```sh +go build -o teai ./cmd/teai +``` + +## 使用方式 + +```sh +# 目前僅提供骨架 +./teai # 顯示說明 +./teai version # 顯示版本資訊 +``` + +離線環境可直接建置:本專案目前僅使用 Go 標準庫,沒有外部相依。 + +## 開發 + +```sh +go build ./... # 建置全部套件 +go test ./... # 跑全部測試 +go vet ./... # 靜態檢查 +``` + +套件結構: + +| 路徑 | 內容 | +| --- | --- | +| `cmd/teai/` | 程式進入點(`main`) | +| `internal/cli/` | 命令列架構(flag 剖析、子命令分派、輸出) | +| `internal/` 其他子套件 | 之後依功能新增(如 `internal/gitea`、`internal/cmds/`) | + +### 慣例 + +- 文件與溝通一律使用**繁體中文** +- 對外行為(CLI 輸出、錯誤訊息)以英文為主,方便除錯與自動化剖析 +- 變更一律走 **feature 分支 + Pull Request**,由審核者合併;不直推 `main` +- Commit 訊息與 PR 標題使用繁體中文或英文皆可,但需簡潔明確 + +## 授權 + +尚未決定(待維護者選定,例如 MIT/Apache-2.0)。 diff --git a/cmd/teai/main.go b/cmd/teai/main.go new file mode 100644 index 0000000..8e3f10e --- /dev/null +++ b/cmd/teai/main.go @@ -0,0 +1,16 @@ +// 套件 main 是 teai 的程式進入點。 +// +// teai 是 Gitea 的命令列輔助工具(tea + AI),補足官方 tea CLI 缺少的功能。 +// 實際的命令分派與執行邏輯位於 internal/cli 套件;main 僅負責把標準 +// 輸入輸出接上,並以內部錯誤碼結束行程。 +package main + +import ( + "os" + + "gitea.alterminal.com/alterminal/teai/internal/cli" +) + +func main() { + os.Exit(cli.Run(os.Stdout, os.Stderr, os.Args[1:])) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..a70f9d4 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module gitea.alterminal.com/alterminal/teai + +go 1.26 diff --git a/internal/cli/cli.go b/internal/cli/cli.go new file mode 100644 index 0000000..1117788 --- /dev/null +++ b/internal/cli/cli.go @@ -0,0 +1,114 @@ +// 套件 cli 提供 teai 的命令列架構:引數剖析、子命令分派與輸出。 +// +// 設計目標是讓之後新增子命令時只需註冊一個 command 結構, +// 不需要改動分派邏輯;也讓核心功能可以被測試(Run 接受 io.Writer)。 +package cli + +import ( + "flag" + "fmt" + "io" + "sort" +) + +// Version 是目前開發中的版本號。採用語意化版本;正式發佈前以 0 開頭。 +var Version = "0.1.0-dev" + +// ExitCode 是 Run 回傳的行程結束碼。 +type ExitCode int + +// 常見的結束碼定義。0 表示成功,其餘對應常見的命令列錯誤情境。 +const ( + ExitOK ExitCode = iota // 成功 + ExitUsage // 引數或子命令錯誤 + ExitInternal // 內部錯誤(不該發生) +) + +// command 定義一個子命令:名稱、一行說明與實作。 +type command struct { + name string + usage string + run func(env *Env, args []string) error +} + +// Env 聚集一次執行所需的輸出目標,便於測試時替換。 +type Env struct { + // Out 是一般輸出(命令結果)。 + Out io.Writer + // Err 是診斷輸出(錯誤、警告)。 + Err io.Writer +} + +// commands 是已註冊的子命令表。新增功能時在這裡註冊即可。 +var commands = map[string]*command{ + "version": { + name: "version", + usage: "顯示版本資訊", + run: runVersion, + }, +} + +// Run 剖析引數並分派到對應子命令,回傳行程結束碼。 +// +// 無引數或要求說明(-h/--help)時印出用法;未知子命令回 ExitUsage。 +func Run(stdout, stderr io.Writer, args []string) int { + env := &Env{Out: stdout, Err: stderr} + + if len(args) == 0 { + printUsage(env.Out) + return int(ExitOK) + } + + switch args[0] { + case "-h", "--help", "help": + printUsage(env.Out) + return int(ExitOK) + case "-v", "--version", "version": + return dispatch(env, "version", args[1:]) + default: + name := args[0] + if _, ok := commands[name]; !ok { + fmt.Fprintf(stderr, "teai: unknown command %q\n\n", name) + printUsage(stderr) + return int(ExitUsage) + } + return dispatch(env, name, args[1:]) + } +} + +// dispatch 執行已註冊的子命令,把錯誤轉成結束碼並輸出。 +func dispatch(env *Env, name string, args []string) int { + cmd := commands[name] + fs := flag.NewFlagSet("teai "+name, flag.ContinueOnError) + fs.SetOutput(env.Err) + if err := fs.Parse(args); err != nil { + return int(ExitUsage) + } + if err := cmd.run(env, fs.Args()); err != nil { + fmt.Fprintf(env.Err, "teai %s: %v\n", name, err) + return int(ExitInternal) + } + return int(ExitOK) +} + +// runVersion 輸出版本資訊。 +func runVersion(env *Env, args []string) error { + fmt.Fprintf(env.Out, "teai version %s\n", Version) + return nil +} + +// printUsage 印出用法與已註冊的子命令清單(依名稱排序)。 +func printUsage(w io.Writer) { + fmt.Fprintf(w, "teai — Gitea CLI 輔助工具(tea + AI)\n\n") + fmt.Fprintf(w, "用法:\n teai [命令] [參數]\n\n命令:\n") + names := make([]string, 0, len(commands)) + for name := range commands { + names = append(names, name) + } + sort.Strings(names) + for _, name := range names { + fmt.Fprintf(w, " %-10s %s\n", name, commands[name].usage) + } + fmt.Fprintf(w, "\n說明:\n -h, --help 顯示說明\n -v, --version 顯示版本\n") + fmt.Fprintf(w, "\n更多資訊:https://gitea.alterminal.com/alterminal/teai\n") +} diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go new file mode 100644 index 0000000..200aded --- /dev/null +++ b/internal/cli/cli_test.go @@ -0,0 +1,60 @@ +// cli_test.go 驗證命令列架構的基本行為:用法輸出、子命令分派、 +// 版本輸出與未知命令的錯誤處理。 +package cli + +import ( + "bytes" + "strings" + "testing" +) + +// run 是測試輔助:以 buffer 收集輸出並執行 Run。 +func run(args ...string) (stdout, stderr string, code int) { + var out, errb bytes.Buffer + code = Run(&out, &errb, args) + return out.String(), errb.String(), code +} + +func TestRunNoArgsShowsUsage(t *testing.T) { + stdout, _, code := run() + if code != 0 { + t.Errorf("無引數應回 0,得到 %d", code) + } + if !strings.Contains(stdout, "teai") || !strings.Contains(stdout, "命令") { + t.Errorf("無引數應印出用法說明,得到:\n%s", stdout) + } +} + +func TestRunHelpFlags(t *testing.T) { + for _, flagArg := range []string{"-h", "--help", "help"} { + stdout, _, code := run(flagArg) + if code != 0 { + t.Errorf("%s 應回 0,得到 %d", flagArg, code) + } + if !strings.Contains(stdout, "用法") { + t.Errorf("%s 應印出用法,得到:\n%s", flagArg, stdout) + } + } +} + +func TestRunVersion(t *testing.T) { + for _, arg := range []string{"version", "-v", "--version"} { + stdout, _, code := run(arg) + if code != 0 { + t.Errorf("%s 應回 0,得到 %d", arg, code) + } + if !strings.Contains(stdout, "teai version ") { + t.Errorf("%s 應印出版本,得到:%q", arg, stdout) + } + } +} + +func TestRunUnknownCommand(t *testing.T) { + _, stderr, code := run("no-such-command") + if code != int(ExitUsage) { + t.Errorf("未知命令應回 ExitUsage(%d),得到 %d", int(ExitUsage), code) + } + if !strings.Contains(stderr, "unknown command") { + t.Errorf("未知命令應在 stderr 說明,得到:%q", stderr) + } +}