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 }