feat:新增 organizations 指令(#45)

tea organizations 對等的日常操作:
- organizations list:GET /user/orgs,輸出 name/full_name/website/
  location/description/visibility(與 teai orgs 工作流命令同一
  端點,但輸出完整欄位)
- organizations create:POST /orgs,--name 必填,可選 --full-name/
  --description/--website/--location/--visibility;寫入需 --yes
- organizations delete:DELETE /orgs/{org},組織名為位置參數
  (tea 相容);寫入需 --yes

驗收測試涵蓋端點、payload、--yes 閘門(未確認不發請求)與
值域檢查;並以本機建置對 Gitea 實測 list 輸出欄位與 tea 一致。
This commit is contained in:
ceo
2026-09-13 19:32:34 +08:00
parent d45a8243c8
commit 8d446ed62b
4 changed files with 370 additions and 3 deletions
+133 -1
View File
@@ -1,5 +1,5 @@
// dailyops_commands.go — 日常操作命令的 CLI 接線(#8):issues、pulls、
// labels、milestones、releases、repos、api(tea 對等指令)。
// labels、milestones、releases、repos、organizations、api(tea 對等指令)。
//
// 規則:
// - 寫入類命令(create/close/comment/merge…)一律要求 --yes,
@@ -33,6 +33,7 @@ func init() {
register(&command{name: "milestones", usage: "里程碑(跨倉庫總覽;list/create 管理單倉庫)", run: runMilestones})
register(&command{name: "releases", usage: "發佈清單(list)", run: runReleases})
register(&command{name: "repos", usage: "倉庫查詢(list)", run: runRepos})
register(&command{name: "organizations", usage: "組織管理(list/create/delete;tea organizations 對等)", run: runOrganizations})
register(&command{name: "api", usage: "任意 API 呼叫(--method/--data;寫入動詞需 --yes)", run: runAPIDirect})
}
@@ -1097,3 +1098,134 @@ func runAPIDirect(env *Env, args []string) error {
}
return err
}
// ---- organizations(#45,tea organizations 對等)----
// orgListFields 是 organizations 清單的欄位定義。
func orgListFields() []gitea.Field {
return []gitea.Field{
{Name: "name"}, {Name: "full_name"}, {Name: "website"},
{Name: "location"}, {Name: "description"}, {Name: "visibility"},
}
}
// runOrganizations:teai organizations [list|create|delete] …。
// tea 對等:organizations list 列出自己可存取的組織;create/delete 是
// 寫入操作,比照慣例要求 --yes。
func runOrganizations(env *Env, args []string) error {
if len(args) == 0 {
return &ErrUsage{Msg: "organizations 需要子命令:list | create | delete"}
}
sub, rest := args[0], args[1:]
switch sub {
case "list":
return runOrganizationsList(env, rest)
case "create":
return runOrganizationsCreate(env, rest)
case "delete":
return runOrganizationsDelete(env, rest)
default:
return &ErrUsage{Msg: fmt.Sprintf("organizations 未知子命令 %q(可用:list | create | delete)", sub)}
}
}
// runOrganizationsList:teai organizations list。
// GET /user/orgs(與 teai orgs 工作流命令同一端點,但輸出完整欄位)。
func runOrganizationsList(env *Env, args []string) error {
fs := flag.NewFlagSet("teai organizations list", flag.ContinueOnError)
fs.SetOutput(env.Err)
if err := parseFlags(fs, args); err != nil {
return err
}
if fs.NArg() != 0 {
return &ErrUsage{Msg: "organizations list 不接受位置參數"}
}
ops, err := newOpsClient(env)
if err != nil {
return err
}
orgs, err := ops.ListMyOrganizations(context.Background())
if err != nil {
return err
}
p := printer(env)
rows := make([]gitea.Values, 0, len(orgs))
for i := range orgs {
org := &orgs[i]
rows = append(rows, gitea.Values{
org.Account(), org.FullName, org.Website,
org.Location, org.Description, org.Visibility,
})
}
return p.PrintList(orgListFields(), rows)
}
// runOrganizationsCreate:teai organizations create --name … [--full-name …]
// [--description …] [--website …] [--location …] [--visibility public|limited|private] --yes。
// tea 以位置參數給組織名;teai 慣例用 --name(語義等價,命名與其他 create 一致)。
func runOrganizationsCreate(env *Env, args []string) error {
fs := flag.NewFlagSet("teai organizations create", flag.ContinueOnError)
fs.SetOutput(env.Err)
name := fs.String("name", "", "組織帳號名(必填,同 tea 的位置參數)")
fullName := fs.String("full-name", "", "顯示名稱")
desc := fs.String("description", "", "說明")
website := fs.String("website", "", "網站")
location := fs.String("location", "", "所在地")
visibility := fs.String("visibility", "", "可見度:public|limited|private(預設服務端決定)")
yes := fs.Bool("yes", false, "確認寫入")
if err := parseFlags(fs, args); err != nil {
return err
}
if strings.TrimSpace(*name) == "" {
return &ErrUsage{Msg: "organizations create 需要 --name"}
}
switch *visibility {
case "", "public", "limited", "private":
default:
return &ErrUsage{Msg: "organizations create 的 --visibility 需為 public|limited|private"}
}
if err := requireYes(*yes, "organizations create"); err != nil {
return err
}
ops, err := newOpsClient(env)
if err != nil {
return err
}
org, err := ops.CreateOrganization(context.Background(), *name, *fullName, *desc, *website, *location, *visibility)
if err != nil {
return err
}
return printer(env).PrintOne(orgListFields(), gitea.Values{
org.Account(), org.FullName, org.Website,
org.Location, org.Description, org.Visibility,
})
}
// runOrganizationsDelete:teai organizations delete <name> --yes。
// tea 相容:組織名作為位置參數。
func runOrganizationsDelete(env *Env, args []string) error {
fs := flag.NewFlagSet("teai organizations delete", flag.ContinueOnError)
fs.SetOutput(env.Err)
yes := fs.Bool("yes", false, "確認寫入")
if err := parseFlags(fs, args); err != nil {
return err
}
if fs.NArg() != 1 {
return &ErrUsage{Msg: "organizations delete 需要恰好一個組織名"}
}
orgName := strings.TrimSpace(fs.Arg(0))
if orgName == "" {
return &ErrUsage{Msg: "organizations delete 需要組織名"}
}
if err := requireYes(*yes, "organizations delete"); err != nil {
return err
}
ops, err := newOpsClient(env)
if err != nil {
return err
}
if err := ops.DeleteOrganization(context.Background(), orgName); err != nil {
return err
}
return printer(env).PrintOne([]gitea.Field{{Name: "name"}, {Name: "state"}}, gitea.Values{orgName, "deleted"})
}