feat(cli): v2 — full API coverage, profiles, --json/--yes, containr up

Rewrite the CLI as a thin client over the authenticated API so agents
and scripts reach every resource the UI does (ROADMAP2 phase A2).

- shared Client with bearer auth, error-envelope decoding, and stable
  exit codes: 0 ok / 1 error / 2 auth / 3 not-found / 4 validation /
  5 unreachable
- named profiles in ~/.containr.yaml (current_profile + profiles.*);
  resolution order --flag > env > profile > localhost default;
  CONTAINR_TOKEN / CONTAINR_API_URL / CONTAINR_PROFILE env support
- --json prints raw API responses; --yes skips confirmations;
  destructive commands refuse non-interactively without it
- auth login verifies the token against /user/profile before saving,
  interactive prompt via term.ReadPassword
- commands for every resource: tokens, projects, services, deploy,
  deployments (logs/rollback), logs --follow (SSE), exec, variables
  (set/unset merge with secret masking preserved), databases
  (backup/restore/download), cron, templates, nodes + agent tokens,
  scaling, ha, security, gateway, admin, notifications
- containr up: git remote + branch → create-or-reuse service → deploy

Verified live: login/status/tokens/projects/services, up creating a
service + deployment, non-TTY confirmation refusal, scope-capped admin
rejection (exit 2), revoke.

Authored By: TDvorak <[email protected]>
This commit is contained in:
Tomas Dvorak committed 2026-10-08 15:19:31 +02:00
1 parent 17e429b86b
commit e51b20b2d4
26 files changed
+2798 -350

No files matched your search

@@ -0,0 +1,65 @@
package commands
import (
"github.com/spf13/cobra"
)
// NotificationsCmd lists and manages notifications.
var NotificationsCmd = &cobra.Command{
Use: "notifications",
Aliases: []string{"notifs"},
Short: "Manage notifications",
}
var notifListCmd = &cobra.Command{
Use: "list",
Short: "List notifications",
RunE: func(cmd *cobra.Command, args []string) error {
c, err := client()
if err != nil {
return err
}
data, err := c.Do("GET", "/notifications", nil)
if err != nil {
return err
}
items, err := unwrapList(data, "notifications")
if err != nil {
return err
}
rows := make([][]string, 0, len(items))
for _, n := range items {
rows = append(rows, []string{
str(n, "id"), str(n, "type"), str(n, "title"), str(n, "read"), relTime(n, "created_at"),
})
}
printRows(data, []string{"ID", "TYPE", "TITLE", "READ", "AGE"}, rows)
return nil
},
}
var notifReadCmd = &cobra.Command{
Use: "read <id>|all",
Short: "Mark a notification (or all) as read",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
c, err := client()
if err != nil {
return err
}
path := "/notifications/" + args[0] + "/read"
if args[0] == "all" {
path = "/notifications/read-all"
}
data, err := c.Do("POST", path, map[string]interface{}{})
if err != nil {
return err
}
PrintRaw(data)
return nil
},
}
func init() {
NotificationsCmd.AddCommand(notifListCmd, notifReadCmd)
}