package commands import ( "bytes" "encoding/json" "fmt" "git.hangman-lab.top/zhi/HarborForge.Cli/internal/client" "git.hangman-lab.top/zhi/HarborForge.Cli/internal/config" "git.hangman-lab.top/zhi/HarborForge.Cli/internal/output" ) // projectResponse matches the backend ProjectResponse schema. type projectResponse struct { ID int `json:"id"` Code string `json:"code"` Name string `json:"name"` Description *string `json:"description"` Repo *string `json:"repo"` Owner string `json:"owner"` CreatedAt string `json:"created_at"` } // projectMemberResponse matches the backend ProjectMemberResponse schema. type projectMemberResponse struct { Username string `json:"username"` RoleName string `json:"role_name"` } // RunProjectList implements `hf project list`. func RunProjectList(args []string, tokenFlag string) { token := ResolveToken(tokenFlag) // Build query params query := "" for i := 0; i < len(args); i++ { switch args[i] { case "--owner": if i+1 >= len(args) { output.Error("--owner requires a value") } i++ query = appendQuery(query, "owner", args[i]) case "--order-by": if i+1 >= len(args) { output.Error("--order-by requires a value") } i++ query = appendQuery(query, "order_by", args[i]) default: output.Errorf("unknown flag: %s", args[i]) } } cfg, err := config.Load() if err != nil { output.Errorf("config error: %v", err) } c := client.New(cfg.BaseURL, token) path := "/projects" if query != "" { path += "?" + query } data, err := c.Get(path) if err != nil { output.Errorf("failed to list projects: %v", err) } if output.JSONMode { var raw json.RawMessage if err := json.Unmarshal(data, &raw); err != nil { output.Errorf("invalid JSON response: %v", err) } output.PrintJSON(raw) return } var projects []projectResponse if err := json.Unmarshal(data, &projects); err != nil { output.Errorf("cannot parse project list: %v", err) } headers := []string{"CODE", "NAME", "OWNER", "DESCRIPTION"} var rows [][]string for _, p := range projects { desc := "" if p.Description != nil { desc = *p.Description if len(desc) > 50 { desc = desc[:47] + "..." } } rows = append(rows, []string{p.Code, p.Name, p.Owner, desc}) } output.PrintTable(headers, rows) } // RunProjectGet implements `hf project get `. func RunProjectGet(projectCode, tokenFlag string) { token := ResolveToken(tokenFlag) cfg, err := config.Load() if err != nil { output.Errorf("config error: %v", err) } c := client.New(cfg.BaseURL, token) data, err := c.Get("/projects/" + projectCode) if err != nil { output.Errorf("failed to get project: %v", err) } if output.JSONMode { var raw json.RawMessage if err := json.Unmarshal(data, &raw); err != nil { output.Errorf("invalid JSON response: %v", err) } output.PrintJSON(raw) return } var p projectResponse if err := json.Unmarshal(data, &p); err != nil { output.Errorf("cannot parse project: %v", err) } desc := "" if p.Description != nil { desc = *p.Description } repo := "" if p.Repo != nil { repo = *p.Repo } output.PrintKeyValue( "code", p.Code, "name", p.Name, "description", desc, "repo", repo, "owner", p.Owner, "created", p.CreatedAt, ) } // RunProjectCreate implements `hf project create`. func RunProjectCreate(args []string, tokenFlag string) { token := ResolveToken(tokenFlag) name, desc, repo := "", "", "" for i := 0; i < len(args); i++ { switch args[i] { case "--name": if i+1 >= len(args) { output.Error("--name requires a value") } i++ name = args[i] case "--desc": if i+1 >= len(args) { output.Error("--desc requires a value") } i++ desc = args[i] case "--repo": if i+1 >= len(args) { output.Error("--repo requires a value") } i++ repo = args[i] default: output.Errorf("unknown flag: %s", args[i]) } } if name == "" { output.Error("usage: hf project create --name ") } payload := map[string]interface{}{ "name": name, } if desc != "" { payload["description"] = desc } if repo != "" { payload["repo"] = repo } body, err := json.Marshal(payload) if err != nil { output.Errorf("cannot marshal payload: %v", err) } cfg, err := config.Load() if err != nil { output.Errorf("config error: %v", err) } c := client.New(cfg.BaseURL, token) data, err := c.Post("/projects", bytes.NewReader(body)) if err != nil { output.Errorf("failed to create project: %v", err) } if output.JSONMode { var raw json.RawMessage if err := json.Unmarshal(data, &raw); err != nil { output.Errorf("invalid JSON response: %v", err) } output.PrintJSON(raw) return } var p projectResponse if err := json.Unmarshal(data, &p); err != nil { fmt.Printf("project created: %s\n", name) return } fmt.Printf("project created: %s (code: %s)\n", p.Name, p.Code) } // RunProjectUpdate implements `hf project update `. func RunProjectUpdate(projectCode string, args []string, tokenFlag string) { token := ResolveToken(tokenFlag) payload := make(map[string]interface{}) for i := 0; i < len(args); i++ { switch args[i] { case "--name": if i+1 >= len(args) { output.Error("--name requires a value") } i++ payload["name"] = args[i] case "--desc": if i+1 >= len(args) { output.Error("--desc requires a value") } i++ payload["description"] = args[i] case "--repo": if i+1 >= len(args) { output.Error("--repo requires a value") } i++ payload["repo"] = args[i] default: output.Errorf("unknown flag: %s", args[i]) } } if len(payload) == 0 { output.Error("nothing to update — provide at least one flag") } body, err := json.Marshal(payload) if err != nil { output.Errorf("cannot marshal payload: %v", err) } cfg, err := config.Load() if err != nil { output.Errorf("config error: %v", err) } c := client.New(cfg.BaseURL, token) _, err = c.Patch("/projects/"+projectCode, bytes.NewReader(body)) if err != nil { output.Errorf("failed to update project: %v", err) } fmt.Printf("project updated: %s\n", projectCode) } // RunProjectDelete implements `hf project delete `. func RunProjectDelete(projectCode, tokenFlag string) { token := ResolveToken(tokenFlag) cfg, err := config.Load() if err != nil { output.Errorf("config error: %v", err) } c := client.New(cfg.BaseURL, token) _, err = c.Delete("/projects/" + projectCode) if err != nil { output.Errorf("failed to delete project: %v", err) } fmt.Printf("project deleted: %s\n", projectCode) } // RunProjectMembers implements `hf project members `. func RunProjectMembers(projectCode, tokenFlag string) { token := ResolveToken(tokenFlag) cfg, err := config.Load() if err != nil { output.Errorf("config error: %v", err) } c := client.New(cfg.BaseURL, token) data, err := c.Get("/projects/" + projectCode + "/members") if err != nil { output.Errorf("failed to list project members: %v", err) } if output.JSONMode { var raw json.RawMessage if err := json.Unmarshal(data, &raw); err != nil { output.Errorf("invalid JSON response: %v", err) } output.PrintJSON(raw) return } var members []projectMemberResponse if err := json.Unmarshal(data, &members); err != nil { output.Errorf("cannot parse member list: %v", err) } headers := []string{"USERNAME", "ROLE"} var rows [][]string for _, m := range members { rows = append(rows, []string{m.Username, m.RoleName}) } output.PrintTable(headers, rows) } // RunProjectAddMember implements `hf project add-member --user --role `. func RunProjectAddMember(projectCode string, args []string, tokenFlag string) { token := ResolveToken(tokenFlag) username, roleName := "", "" for i := 0; i < len(args); i++ { switch args[i] { case "--user": if i+1 >= len(args) { output.Error("--user requires a value") } i++ username = args[i] case "--role": if i+1 >= len(args) { output.Error("--role requires a value") } i++ roleName = args[i] default: output.Errorf("unknown flag: %s", args[i]) } } if username == "" || roleName == "" { output.Error("usage: hf project add-member --user --role ") } payload := map[string]interface{}{ "username": username, "role_name": roleName, } body, err := json.Marshal(payload) if err != nil { output.Errorf("cannot marshal payload: %v", err) } cfg, err := config.Load() if err != nil { output.Errorf("config error: %v", err) } c := client.New(cfg.BaseURL, token) _, err = c.Post("/projects/"+projectCode+"/members", bytes.NewReader(body)) if err != nil { output.Errorf("failed to add member: %v", err) } fmt.Printf("member %s added to project %s with role %s\n", username, projectCode, roleName) } // RunProjectRemoveMember implements `hf project remove-member --user `. func RunProjectRemoveMember(projectCode string, args []string, tokenFlag string) { token := ResolveToken(tokenFlag) username := "" for i := 0; i < len(args); i++ { switch args[i] { case "--user": if i+1 >= len(args) { output.Error("--user requires a value") } i++ username = args[i] default: output.Errorf("unknown flag: %s", args[i]) } } if username == "" { output.Error("usage: hf project remove-member --user ") } cfg, err := config.Load() if err != nil { output.Errorf("config error: %v", err) } c := client.New(cfg.BaseURL, token) _, err = c.Delete("/projects/" + projectCode + "/members/" + username) if err != nil { output.Errorf("failed to remove member: %v", err) } fmt.Printf("member %s removed from project %s\n", username, projectCode) } // appendQuery is a helper for building query strings. func appendQuery(existing, key, value string) string { if existing == "" { return key + "=" + value } return existing + "&" + key + "=" + value }