Compare commits
1 Commits
34a5512009
...
docs/readm
| Author | SHA1 | Date | |
|---|---|---|---|
| 7ed99d7347 |
15
README.md
15
README.md
@@ -1,6 +1,8 @@
|
|||||||
# HarborForge.Cli
|
# HarborForge.Cli
|
||||||
|
|
||||||
`HarborForge.Cli` is the Go-based `hf` binary for HarborForge.
|
`HarborForge.Cli` is the Go-based `hf` command-line client for HarborForge.
|
||||||
|
|
||||||
|
Part of the [HarborForge](../README.md) platform. `hf` is a thin, scriptable client over the `HarborForge.Backend` REST API (default `http://127.0.0.1:8000`). It is permission-aware (command visibility derives from the caller's backend permissions) and supports both automatic secret resolution (padded-cell mode) and explicit `--token` auth (manual mode).
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
@@ -205,14 +207,15 @@ There is not yet a finer-grained exit-code taxonomy; callers should currently tr
|
|||||||
- Top-level and group/leaf help rendering (`--help` / `--help-brief`)
|
- Top-level and group/leaf help rendering (`--help` / `--help-brief`)
|
||||||
- Permission-aware command visibility via `/auth/me/permissions`
|
- Permission-aware command visibility via `/auth/me/permissions`
|
||||||
- Detailed leaf help text for all commands, with padded-cell/manual auth flag differences
|
- Detailed leaf help text for all commands, with padded-cell/manual auth flag differences
|
||||||
- Nested help coverage for `config`, `monitor server`, and `monitor api-key` subtrees
|
- Nested help coverage for `config`, `monitor server`, `monitor api-key`, and `proposal essential` subtrees
|
||||||
- `(not permitted)` rendering for unauthorized commands
|
- `(not permitted)` rendering for unauthorized commands
|
||||||
|
|
||||||
**Core commands:**
|
**Core commands:**
|
||||||
- `hf version`, `hf health`, `hf config` (show / `--url` / `--acc-mgr-token`)
|
- `hf version`, `hf health`, `hf config` (show / `--url` / `--acc-mgr-token`)
|
||||||
|
- `hf update-discord-id <username> [discord-id]` — top-level convenience command
|
||||||
|
|
||||||
**Resource commands (all implemented with list/get/create/update/delete + special actions):**
|
**Resource commands (all implemented with list/get/create/update/delete + special actions):**
|
||||||
- `hf user` — create, list, get, update, activate, deactivate, delete
|
- `hf user` — create, list, get, update, activate, deactivate, delete, reset-apikey
|
||||||
- `hf role` — list, get, create, update, delete, set-permissions, add-permissions, remove-permissions
|
- `hf role` — list, get, create, update, delete, set-permissions, add-permissions, remove-permissions
|
||||||
- `hf permission` — list
|
- `hf permission` — list
|
||||||
- `hf project` — list, get, create, update, delete, members, add-member, remove-member
|
- `hf project` — list, get, create, update, delete, members, add-member, remove-member
|
||||||
@@ -220,7 +223,9 @@ There is not yet a finer-grained exit-code taxonomy; callers should currently tr
|
|||||||
- `hf task` — list, get, create, update, transition, take, delete, search
|
- `hf task` — list, get, create, update, transition, take, delete, search
|
||||||
- `hf meeting` — list, get, create, update, attend, delete
|
- `hf meeting` — list, get, create, update, attend, delete
|
||||||
- `hf support` — list, get, create, update, take, transition, delete
|
- `hf support` — list, get, create, update, take, transition, delete
|
||||||
- `hf propose` — list, get, create, update, accept, reject, reopen
|
- `hf proposal` (alias: `hf propose`) — list, get, create, update, accept, reject, reopen
|
||||||
|
- `hf proposal essential` — list, create, update, delete
|
||||||
|
- `hf calendar` — schedule, show, edit, cancel, date-list, plan-schedule, plan-list, plan-edit, plan-cancel
|
||||||
- `hf comment` — add, list
|
- `hf comment` — add, list
|
||||||
- `hf worklog` — add, list
|
- `hf worklog` — add, list
|
||||||
- `hf monitor` — overview, server (list/get/create/delete), api-key (generate/revoke)
|
- `hf monitor` — overview, server (list/get/create/delete), api-key (generate/revoke)
|
||||||
@@ -229,4 +234,4 @@ There is not yet a finer-grained exit-code taxonomy; callers should currently tr
|
|||||||
|
|
||||||
- Backend code-based endpoint support (some commands still use id-based API routes)
|
- Backend code-based endpoint support (some commands still use id-based API routes)
|
||||||
- Release automation beyond local `make release` packaging (checksums / archives / CI publishing)
|
- Release automation beyond local `make release` packaging (checksums / archives / CI publishing)
|
||||||
- Integration tests
|
- Broader test coverage (unit tests exist for the calendar and proposal commands; end-to-end coverage is still partial)
|
||||||
|
|||||||
@@ -224,12 +224,6 @@ func handleGroup(group help.Group, args []string) {
|
|||||||
case "monitor":
|
case "monitor":
|
||||||
handleMonitorCommand(sub.Name, remaining)
|
handleMonitorCommand(sub.Name, remaining)
|
||||||
return
|
return
|
||||||
case "schedule-type":
|
|
||||||
handleScheduleTypeCommand(sub.Name, remaining)
|
|
||||||
return
|
|
||||||
case "assign-schedule-type":
|
|
||||||
handleAssignScheduleType(remaining)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(args) > 0 && args[0] == "update-discord-id" {
|
if len(args) > 0 && args[0] == "update-discord-id" {
|
||||||
@@ -1144,47 +1138,3 @@ func handleMonitorAPIKeyCommand(args []string, tokenFlag string) {
|
|||||||
output.Errorf("unknown monitor api-key subcommand: %s", subCmd)
|
output.Errorf("unknown monitor api-key subcommand: %s", subCmd)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleScheduleTypeCommand(subCmd string, args []string) {
|
|
||||||
tokenFlag := ""
|
|
||||||
var filtered []string
|
|
||||||
for i := 0; i < len(args); i++ {
|
|
||||||
switch args[i] {
|
|
||||||
case "--token":
|
|
||||||
if i+1 < len(args) {
|
|
||||||
i++
|
|
||||||
tokenFlag = args[i]
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
filtered = append(filtered, args[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch subCmd {
|
|
||||||
case "list":
|
|
||||||
commands.RunScheduleTypeList(tokenFlag)
|
|
||||||
case "create":
|
|
||||||
commands.RunScheduleTypeCreate(filtered, tokenFlag)
|
|
||||||
case "delete":
|
|
||||||
commands.RunScheduleTypeDelete(filtered, tokenFlag)
|
|
||||||
default:
|
|
||||||
output.Errorf("hf schedule-type %s is not implemented yet", subCmd)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleAssignScheduleType(args []string) {
|
|
||||||
tokenFlag := ""
|
|
||||||
var filtered []string
|
|
||||||
for i := 0; i < len(args); i++ {
|
|
||||||
switch args[i] {
|
|
||||||
case "--token":
|
|
||||||
if i+1 < len(args) {
|
|
||||||
i++
|
|
||||||
tokenFlag = args[i]
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
filtered = append(filtered, args[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
commands.RunAssignScheduleType(filtered, tokenFlag)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,165 +0,0 @@
|
|||||||
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"
|
|
||||||
)
|
|
||||||
|
|
||||||
type scheduleTypeResponse struct {
|
|
||||||
ID int `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
WorkFrom int `json:"work_from"`
|
|
||||||
WorkTo int `json:"work_to"`
|
|
||||||
EntertainmentFrom int `json:"entertainment_from"`
|
|
||||||
EntertainmentTo int `json:"entertainment_to"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// RunScheduleTypeList implements `hf schedule-type list`.
|
|
||||||
func RunScheduleTypeList(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("/schedule-types/")
|
|
||||||
if err != nil {
|
|
||||||
output.Errorf("failed to list schedule types: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var types []scheduleTypeResponse
|
|
||||||
if err := json.Unmarshal(data, &types); err != nil {
|
|
||||||
output.Errorf("invalid response: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if output.JSONMode {
|
|
||||||
output.PrintJSON(types)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(types) == 0 {
|
|
||||||
fmt.Println("No schedule types defined.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("%-4s %-20s %-12s %-12s\n", "ID", "Name", "Work", "Entertainment")
|
|
||||||
fmt.Printf("%-4s %-20s %-12s %-12s\n", "----", "--------------------", "------------", "------------")
|
|
||||||
for _, t := range types {
|
|
||||||
fmt.Printf("%-4d %-20s %02d:00-%02d:00 %02d:00-%02d:00\n",
|
|
||||||
t.ID, t.Name, t.WorkFrom, t.WorkTo, t.EntertainmentFrom, t.EntertainmentTo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RunScheduleTypeCreate implements `hf schedule-type create <name> --work <from>-<to> --entertainment <from>-<to>`.
|
|
||||||
func RunScheduleTypeCreate(args []string, tokenFlag string) {
|
|
||||||
token := ResolveToken(tokenFlag)
|
|
||||||
|
|
||||||
if len(args) < 1 {
|
|
||||||
output.Error("usage: hf schedule-type create <name> --work <from>-<to> --entertainment <from>-<to>")
|
|
||||||
}
|
|
||||||
|
|
||||||
name := args[0]
|
|
||||||
workFrom, workTo, entFrom, entTo := -1, -1, -1, -1
|
|
||||||
|
|
||||||
for i := 1; i < len(args); i++ {
|
|
||||||
switch args[i] {
|
|
||||||
case "--work":
|
|
||||||
if i+1 < len(args) {
|
|
||||||
i++
|
|
||||||
fmt.Sscanf(args[i], "%d-%d", &workFrom, &workTo)
|
|
||||||
}
|
|
||||||
case "--entertainment":
|
|
||||||
if i+1 < len(args) {
|
|
||||||
i++
|
|
||||||
fmt.Sscanf(args[i], "%d-%d", &entFrom, &entTo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if workFrom < 0 || workTo < 0 || entFrom < 0 || entTo < 0 {
|
|
||||||
output.Error("usage: hf schedule-type create <name> --work <from>-<to> --entertainment <from>-<to>\n e.g.: hf schedule-type create standard --work 8-18 --entertainment 19-23")
|
|
||||||
}
|
|
||||||
|
|
||||||
body := map[string]any{
|
|
||||||
"name": name,
|
|
||||||
"work_from": workFrom,
|
|
||||||
"work_to": workTo,
|
|
||||||
"entertainment_from": entFrom,
|
|
||||||
"entertainment_to": entTo,
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg, err := config.Load()
|
|
||||||
if err != nil {
|
|
||||||
output.Errorf("config error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonBody, _ := json.Marshal(body)
|
|
||||||
c := client.New(cfg.BaseURL, token)
|
|
||||||
data, err := c.Post("/schedule-types/", bytes.NewReader(jsonBody))
|
|
||||||
if err != nil {
|
|
||||||
output.Errorf("failed to create schedule type: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var resp scheduleTypeResponse
|
|
||||||
json.Unmarshal(data, &resp)
|
|
||||||
fmt.Printf("Created schedule type: %s (id=%d, work=%02d:00-%02d:00, entertainment=%02d:00-%02d:00)\n",
|
|
||||||
resp.Name, resp.ID, resp.WorkFrom, resp.WorkTo, resp.EntertainmentFrom, resp.EntertainmentTo)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RunScheduleTypeDelete implements `hf schedule-type delete <id>`.
|
|
||||||
func RunScheduleTypeDelete(args []string, tokenFlag string) {
|
|
||||||
token := ResolveToken(tokenFlag)
|
|
||||||
|
|
||||||
if len(args) < 1 {
|
|
||||||
output.Error("usage: hf schedule-type delete <id>")
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg, err := config.Load()
|
|
||||||
if err != nil {
|
|
||||||
output.Errorf("config error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
c := client.New(cfg.BaseURL, token)
|
|
||||||
_, err = c.Delete("/schedule-types/" + args[0])
|
|
||||||
if err != nil {
|
|
||||||
output.Errorf("failed to delete schedule type: %v", err)
|
|
||||||
}
|
|
||||||
fmt.Printf("Deleted schedule type %s\n", args[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
// RunAssignScheduleType implements `hf assign-schedule-type <agent-id> <schedule-type-name>`.
|
|
||||||
func RunAssignScheduleType(args []string, tokenFlag string) {
|
|
||||||
token := ResolveToken(tokenFlag)
|
|
||||||
|
|
||||||
if len(args) < 2 {
|
|
||||||
output.Error("usage: hf assign-schedule-type <agent-id> <schedule-type-name>")
|
|
||||||
}
|
|
||||||
|
|
||||||
agentID := args[0]
|
|
||||||
scheduleName := args[1]
|
|
||||||
|
|
||||||
body := map[string]string{
|
|
||||||
"schedule_type_name": scheduleName,
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg, err := config.Load()
|
|
||||||
if err != nil {
|
|
||||||
output.Errorf("config error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonBody, _ := json.Marshal(body)
|
|
||||||
c := client.New(cfg.BaseURL, token)
|
|
||||||
data, err := c.Put("/schedule-types/agent/"+agentID+"/assign", bytes.NewReader(jsonBody))
|
|
||||||
if err != nil {
|
|
||||||
output.Errorf("failed to assign schedule type: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var resp map[string]any
|
|
||||||
json.Unmarshal(data, &resp)
|
|
||||||
fmt.Printf("Assigned schedule type '%s' to agent '%s'\n", scheduleName, agentID)
|
|
||||||
}
|
|
||||||
@@ -181,16 +181,6 @@ func CommandSurface() []Group {
|
|||||||
{Name: "api-key", Description: "Manage monitor API keys", Permitted: has(perms, "monitor.manage")},
|
{Name: "api-key", Description: "Manage monitor API keys", Permitted: has(perms, "monitor.manage")},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
Name: "schedule-type",
|
|
||||||
Description: "Manage work/entertainment schedule types",
|
|
||||||
SubCommands: []Command{
|
|
||||||
{Name: "list", Description: "List schedule types", Permitted: has(perms, "schedule_type.read")},
|
|
||||||
{Name: "create", Description: "Create a schedule type", Permitted: has(perms, "schedule_type.manage")},
|
|
||||||
{Name: "delete", Description: "Delete a schedule type", Permitted: has(perms, "schedule_type.manage")},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{Name: "assign-schedule-type", Description: "Assign a schedule type to an agent: assign-schedule-type <agent-id> <type-name>", Permitted: has(perms, "schedule_type.manage")},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range groups {
|
for i := range groups {
|
||||||
|
|||||||
Reference in New Issue
Block a user