Files
mopac-glpi-go/internal/cli/commands.go
T

382 lines
11 KiB
Go

package cli
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"sort"
"strconv"
"strings"
"git.knownelement.com/ukrrs/mopac-glpi-go/glpi"
)
func marshalIndent(v any) ([]byte, error) { return json.MarshalIndent(v, "", " ") }
// --- whoami -----------------------------------------------------------------
// cmdWhoami lists the session's profiles. It NEVER switches the active
// profile (agent mode's MGLPI_PROFILE_ID is deliberately not applied).
// Active-profile detection comes from GetActiveProfile; when that
// endpoint is missing or permission-blocked (404/403), whoami degrades
// gracefully to all-inactive rather than erroring — other failures
// (5xx, unreachable) still surface as API errors.
func cmdWhoami(args []string, stdout, stderr io.Writer) int {
fs, cfgPath, out := newFlags("whoami", stderr)
pos, err := parseArgs(fs, args)
if err != nil || len(pos) != 0 {
return usageErr(stderr, "whoami: usage: mglpi whoami")
}
c, _, err := client(*cfgPath, 0)
if err != nil {
return usageErr(stderr, "%v", err)
}
profs, err := c.GetMyProfiles(context.Background())
if err != nil {
return apiErr(stderr, err)
}
activeID, aerr := c.GetActiveProfile(context.Background())
degraded := aerr != nil && (errors.Is(aerr, glpi.ErrNotFound) || errors.Is(aerr, glpi.ErrAuth))
if aerr != nil && !degraded {
return apiErr(stderr, aerr)
}
if !degraded {
for i := range profs {
profs[i].IsActive = profs[i].ID == activeID
}
}
if *out == "json" {
emitJSON(stdout, map[string]any{"profiles": profs})
return 0
}
for _, p := range profs {
line := fmt.Sprintf("%-6d %s", p.ID, p.Name)
if p.IsActive {
line += " [active]"
}
fmt.Fprintln(stdout, line)
}
return 0
}
// --- change -------------------------------------------------------------------
func cmdChange(args []string, stdout, stderr io.Writer) int {
if len(args) == 0 {
return usageErr(stderr, "change: expected list|show|create|transition|followup (see mglpi help)")
}
sub, rest := args[0], args[1:]
switch sub {
case "list":
return cmdChangeList(rest, stdout, stderr)
case "show":
return cmdChangeShow(rest, stdout, stderr)
case "create":
return cmdChangeCreate(rest, stdout, stderr)
case "transition":
return cmdChangeTransition(rest, stdout, stderr)
case "followup":
return cmdChangeFollowup(rest, stdout, stderr)
default:
return usageErr(stderr, "change: unknown subcommand %q", sub)
}
}
// withClient is the common preamble: resolve config + profile, switch
// profile when one is set (an API op -> exit 2 on failure), and hand
// the client over.
func withClient(stderr io.Writer, cfgPath string, profileFlag int, fn func(c *glpi.Client, ctx context.Context) int) int {
c, profile, err := client(cfgPath, profileFlag)
if err != nil {
return usageErr(stderr, "%v", err)
}
ctx := context.Background()
if profile > 0 {
if err := c.ChangeActiveProfile(ctx, profile); err != nil {
return apiErr(stderr, err)
}
}
return fn(c, ctx)
}
func cmdChangeList(args []string, stdout, stderr io.Writer) int {
fs, cfgPath, out := newFlags("change list", stderr)
status := fs.String("status", "", "filter by status name or numeric id (e.g. new, solved, 1)")
pos, err := parseArgs(fs, args)
if err != nil || len(pos) != 0 {
return usageErr(stderr, "change list: usage: mglpi change list [--status N|NAME]")
}
want := 0
if *status != "" {
if id, ok := glpi.StatusID(*status); ok {
want = id
} else {
n, err := strconv.Atoi(*status)
if err != nil || n <= 0 {
return usageErr(stderr, "change list: --status must be a status name or positive number, got %q", *status)
}
want = n
}
}
return withClient(stderr, *cfgPath, 0, func(c *glpi.Client, ctx context.Context) int {
rows, err := c.ListChanges(ctx, want)
if err != nil {
return apiErr(stderr, err)
}
if *out == "json" {
emitJSON(stdout, map[string]any{"changes": rows})
return 0
}
for _, r := range rows {
fmt.Fprintf(stdout, "%-6d %-12s %s\n", r.ID, glpi.StatusName(r.Status), r.Name)
}
return 0
})
}
func cmdChangeShow(args []string, stdout, stderr io.Writer) int {
fs, cfgPath, out := newFlags("change show", stderr)
pos, err := parseArgs(fs, args)
if err != nil || len(pos) != 1 {
return usageErr(stderr, "change show: usage: mglpi change show ID")
}
id, err := strconv.Atoi(pos[0])
if err != nil {
return usageErr(stderr, "change show: ID must be numeric, got %q", pos[0])
}
return withClient(stderr, *cfgPath, 0, func(c *glpi.Client, ctx context.Context) int {
ch, err := c.GetChange(ctx, id)
if err != nil {
return apiErr(stderr, err)
}
if *out == "json" {
emitJSON(stdout, map[string]any{"change": ch})
return 0
}
printChange(stdout, ch)
return 0
})
}
func printChange(w io.Writer, ch *glpi.Change) {
fmt.Fprintf(w, "#%d %s\n", ch.ID, ch.Name)
fmt.Fprintf(w, "Status: %-12s Urgency: %d Impact: %d\n", glpi.StatusName(ch.Status), ch.Urgency, ch.Impact)
fmt.Fprintf(w, "Created: %s Updated: %s\n", ch.Date, ch.DateMod)
if ch.Content != "" {
fmt.Fprintln(w)
fmt.Fprintln(w, ch.Content)
}
}
func cmdChangeCreate(args []string, stdout, stderr io.Writer) int {
fs, cfgPath, out := newFlags("change create", stderr)
title := fs.String("title", "", "change title (required)")
content := fs.String("content", "-", "content body from FILE, or - for stdin")
urgency := fs.Int("urgency", 3, "urgency 1..5 (3 = medium)")
impact := fs.Int("impact", 3, "impact 1..5 (3 = medium)")
profile := fs.Int("profile", 0, "agent profile id to switch to after InitSession (e.g. 5 = Hotliner)")
pos, err := parseArgs(fs, args)
if err != nil || len(pos) != 0 || *title == "" {
return usageErr(stderr, "change create: usage: mglpi change create --title T [--urgency N] [--impact N] [--profile N] [--content FILE|-]")
}
if *urgency < 1 || *urgency > 5 || *impact < 1 || *impact > 5 {
return usageErr(stderr, "change create: --urgency/--impact must be 1..5")
}
body := ""
if *content != "" {
text, err := readBody(*content)
if err != nil {
return usageErr(stderr, "%v", err)
}
body = text
}
return withClient(stderr, *cfgPath, *profile, func(c *glpi.Client, ctx context.Context) int {
id, err := c.CreateChange(ctx, *title, body, *urgency, *impact)
if err != nil {
return apiErr(stderr, err)
}
if *out == "json" {
emitJSON(stdout, map[string]any{"change": map[string]any{"id": id}})
return 0
}
fmt.Fprintf(stdout, "created change #%d\n", id)
return 0
})
}
func cmdChangeTransition(args []string, stdout, stderr io.Writer) int {
fs, cfgPath, out := newFlags("change transition", stderr)
pos, err := parseArgs(fs, args)
if err != nil || len(pos) != 2 {
return usageErr(stderr, "change transition: usage: mglpi change transition ID STATUS")
}
id, err := strconv.Atoi(pos[0])
if err != nil {
return usageErr(stderr, "change transition: ID must be numeric, got %q", pos[0])
}
status, ok := glpi.StatusID(pos[1])
if !ok {
status, err = strconv.Atoi(pos[1])
if err != nil || status <= 0 {
return usageErr(stderr, "change transition: STATUS must be a status name or positive number, got %q", pos[1])
}
}
return withClient(stderr, *cfgPath, 0, func(c *glpi.Client, ctx context.Context) int {
if err := c.TransitionChange(ctx, id, status); err != nil {
return apiErr(stderr, err)
}
if *out == "json" {
ch, err := c.GetChange(ctx, id)
if err != nil {
return apiErr(stderr, err)
}
emitJSON(stdout, map[string]any{"change": ch})
return 0
}
fmt.Fprintf(stdout, "updated change #%d (status %s)\n", id, glpi.StatusName(status))
return 0
})
}
func cmdChangeFollowup(args []string, stdout, stderr io.Writer) int {
fs, cfgPath, out := newFlags("change followup", stderr)
content := fs.String("content", "-", "followup body from FILE, or - for stdin")
profile := fs.Int("profile", 0, "agent profile id to switch to after InitSession")
pos, err := parseArgs(fs, args)
if err != nil || len(pos) != 1 {
return usageErr(stderr, "change followup: usage: mglpi change followup ID [--content FILE|-]")
}
id, err := strconv.Atoi(pos[0])
if err != nil {
return usageErr(stderr, "change followup: ID must be numeric, got %q", pos[0])
}
body := ""
if *content != "" {
text, err := readBody(*content)
if err != nil {
return usageErr(stderr, "%v", err)
}
body = text
}
if strings.TrimSpace(body) == "" {
return usageErr(stderr, "change followup: content is required (stdin or --content FILE)")
}
return withClient(stderr, *cfgPath, *profile, func(c *glpi.Client, ctx context.Context) int {
if err := c.AddFollowup(ctx, id, body); err != nil {
return apiErr(stderr, err)
}
if *out == "json" {
emitJSON(stdout, map[string]any{"followup": map[string]any{"items_id": id}})
return 0
}
fmt.Fprintf(stdout, "added followup to change #%d\n", id)
return 0
})
}
// readBody loads a body from a file, or from stdin when path is "-".
func readBody(path string) (string, error) {
if path == "-" {
b, err := io.ReadAll(os.Stdin)
if err != nil {
return "", fmt.Errorf("content: cannot read stdin: %w", err)
}
return string(b), nil
}
b, err := os.ReadFile(path)
if err != nil {
return "", fmt.Errorf("content: cannot read %s", path)
}
return string(b), nil
}
// --- ci ----------------------------------------------------------------------
func cmdCI(args []string, stdout, stderr io.Writer) int {
if len(args) == 0 {
return usageErr(stderr, "ci: expected search|show (see mglpi help)")
}
sub, rest := args[0], args[1:]
switch sub {
case "search":
return cmdCISearch(rest, stdout, stderr)
case "show":
return cmdCIShow(rest, stdout, stderr)
default:
return usageErr(stderr, "ci: unknown subcommand %q", sub)
}
}
func cmdCISearch(args []string, stdout, stderr io.Writer) int {
fs, cfgPath, out := newFlags("ci search", stderr)
pos, err := parseArgs(fs, args)
if err != nil || len(pos) != 2 {
return usageErr(stderr, "ci search: usage: mglpi ci search TYPE TERM")
}
return withClient(stderr, *cfgPath, 0, func(c *glpi.Client, ctx context.Context) int {
rows, err := c.SearchCI(ctx, pos[0], pos[1])
if err != nil {
return apiErr(stderr, err)
}
type result struct {
ID int `json:"id"`
Name string `json:"name"`
}
results := make([]result, 0, len(rows))
for _, r := range rows {
results = append(results, result{ID: r.ID, Name: r.Name})
}
if *out == "json" {
emitJSON(stdout, map[string]any{"results": results})
return 0
}
for _, r := range results {
fmt.Fprintf(stdout, "%-6d %s\n", r.ID, r.Name)
}
return 0
})
}
func cmdCIShow(args []string, stdout, stderr io.Writer) int {
fs, cfgPath, out := newFlags("ci show", stderr)
pos, err := parseArgs(fs, args)
if err != nil || len(pos) != 2 {
return usageErr(stderr, "ci show: usage: mglpi ci show TYPE ID")
}
id, err := strconv.Atoi(pos[1])
if err != nil {
return usageErr(stderr, "ci show: ID must be numeric, got %q", pos[1])
}
return withClient(stderr, *cfgPath, 0, func(c *glpi.Client, ctx context.Context) int {
obj, err := c.GetItem(ctx, pos[0], id)
if err != nil {
return apiErr(stderr, err)
}
if *out == "json" {
emitJSON(stdout, obj)
return 0
}
fmt.Fprintf(stdout, "%s #%d %s\n", pos[0], id, obj["name"])
for _, k := range sortedKeys(obj) {
if k == "id" || k == "name" {
continue
}
fmt.Fprintf(stdout, " %s: %v\n", k, obj[k])
}
return 0
})
}
// sortedKeys orders an object's keys for stable text output.
func sortedKeys(m map[string]any) []string {
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
return keys
}