Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
066727f
chore: ignore local env, test fixtures, and dev docs
ashishmax31 Aug 5, 2026
9c4ffba
fix: anchor repo-root gitignore entries
ashishmax31 Aug 5, 2026
ef6cb1e
fix: migrate client to projects API, resolve default project via /use…
ashishmax31 Aug 5, 2026
daec29c
feat: env-var auth (STACKDOME_TOKEN/URL), 130 on abort, TTY-guarded p…
ashishmax31 Aug 5, 2026
edb5fa4
fix: report status from the converged release, treat Superseded as te…
ashishmax31 Aug 5, 2026
d2e0724
fix: persist credentials obtained while STACKDOME_TOKEN is set, keep …
ashishmax31 Aug 5, 2026
ca2cbf4
feat: transparent token refresh on 401
ashishmax31 Aug 5, 2026
2c8a747
feat: add token command for scoped API token management
ashishmax31 Aug 5, 2026
12d706f
fix: retry after refresh even when saving credentials fails
ashishmax31 Aug 5, 2026
ec600b7
feat: declarative apply deploy, release commands, release-event --wait
ashishmax31 Aug 5, 2026
0eafd37
feat: postgres addon commands, build log streaming, volume create
ashishmax31 Aug 5, 2026
023ef1f
fix: create a release after apply, exit 130 on interrupted wait
ashishmax31 Aug 5, 2026
e6deab9
feat: whoami, unified -s stack-name flags, json output sweep
ashishmax31 Aug 5, 2026
78542c9
fix: structured output for status --watch, redact tokens in config view
ashishmax31 Aug 5, 2026
b9e3520
fix: log stream timeout, status release choice, usage exit codes, str…
ashishmax31 Aug 5, 2026
23d4fe7
fix: resolve project via org endpoint and never discard a fresh login…
ashishmax31 Aug 5, 2026
38f2ff1
refactor: use hub's canonical stackfile package instead of the fork
ashishmax31 Aug 5, 2026
50947cb
fix: resolve truncated build and release IDs by prefix
ashishmax31 Aug 5, 2026
655105d
fix: env-supplied scope, stack ref resolution, and output cosmetics
ashishmax31 Aug 5, 2026
76f06b9
fix: tighten SSE end detection, heal legacy current_stack, drop dead …
ashishmax31 Aug 5, 2026
cb52589
fix: refresh the session on the server's 403-with-token-reason
ashishmax31 Aug 5, 2026
7771a41
fix: derive build start/duration from status conditions
ashishmax31 Aug 5, 2026
58a6ca1
fix: use local RFC3339 for build start, de-flake elapsed test
ashishmax31 Aug 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
bin/
.vscode/stackdome
.env
/stackdome
/stackfile.yaml
/docs/superpowers/
HANDOVER.md
352 changes: 352 additions & 0 deletions cmd/stackdome/addon.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,352 @@
package main

import (
"fmt"
"os"

openapi "github.com/Stackdome/stackdome/pkg/api/openapi"
"github.com/spf13/cobra"
"github.com/stackdome/cli/internal/cmdutil"
clierrors "github.com/stackdome/cli/internal/errors"
"github.com/stackdome/cli/internal/output"
)

func newAddonCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "addon",
Short: "Manage addons",
}
cmd.AddCommand(newPostgresCmd())
return cmd
}

func newPostgresCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "postgres",
Short: "Manage PostgreSQL addons",
}

cmd.AddCommand(newPostgresCreateCmd())
cmd.AddCommand(newPostgresListCmd())
cmd.AddCommand(newPostgresInfoCmd())
cmd.AddCommand(newPostgresDeleteCmd())
cmd.AddCommand(newPostgresCredentialsCmd())
cmd.AddCommand(newPostgresBackupCmd())
cmd.AddCommand(newPostgresBackupsCmd())
return cmd
}

func newPostgresCreateCmd() *cobra.Command {
var (
flagDatabase string
flagSuperuser bool
flagVersion int32
flagInstances int32
flagStorage string
)

cmd := &cobra.Command{
Use: "create <name>",
Short: "Create a PostgreSQL addon",
Args: cobra.ExactArgs(1),
RunE: cmdutil.WithContext(cmdutil.RequireAuth(func(ctx *cmdutil.CommandContext, cmd *cobra.Command, args []string) error {
name := args[0]
database := flagDatabase
if database == "" {
database = name
}

addon := openapi.PostgresAddon{
Name: name,
Spec: openapi.PostgresAddonSpec{
Version: openapi.PostgresVersion{Major: flagVersion},
Instances: openapi.PostgresInstances{Count: flagInstances},
Storage: openapi.PostgresStorage{Size: flagStorage},
Databases: []openapi.PostgresDatabase{{Name: database}},
},
}
if flagSuperuser {
addon.Spec.Configuration = &openapi.PostgresConfiguration{EnableSuperuserAccess: &flagSuperuser}
}

created, err := ctx.Client.CreatePostgresAddon(cmd.Context(), addon)
if err != nil {
return err
}

if !ctx.Formatter.IsTable() {
return ctx.Formatter.PrintStructured(created)
}

fmt.Fprintf(os.Stderr, "Postgres addon %q created.\n", created.Name)
return nil
})),
}

cmd.Flags().StringVar(&flagDatabase, "database", "", "Initial database name (defaults to the addon name)")
cmd.Flags().BoolVar(&flagSuperuser, "superuser", false, "Enable superuser access")
cmd.Flags().Int32Var(&flagVersion, "version", 16, "PostgreSQL major version (13-17)")
cmd.Flags().Int32Var(&flagInstances, "instances", 1, "Number of instances (1-5)")
cmd.Flags().StringVar(&flagStorage, "storage", "10Gi", "Storage size")
return cmd
}

func newPostgresListCmd() *cobra.Command {
return &cobra.Command{
Use: "list",
Short: "List PostgreSQL addons",
RunE: cmdutil.WithContext(cmdutil.RequireAuth(func(ctx *cmdutil.CommandContext, cmd *cobra.Command, args []string) error {
addons, err := ctx.Client.ListPostgresAddons(cmd.Context())
if err != nil {
return err
}

if !ctx.Formatter.IsTable() {
return ctx.Formatter.PrintStructured(addons)
}

if len(addons) == 0 {
fmt.Fprintln(os.Stderr, "No postgres addons found.")
return nil
}

tbl := ctx.Formatter.NewTable("NAME", "VERSION", "INSTANCES", "STORAGE", "STATE", "CREATED")
for _, a := range addons {
tbl.AddRow(
a.Name,
fmt.Sprintf("%d", a.Spec.Version.Major),
fmt.Sprintf("%d", a.Spec.Instances.Count),
a.Spec.Storage.Size,
addonState(a),
addonCreated(a),
)
}
tbl.Render()
return nil
})),
}
}

func newPostgresInfoCmd() *cobra.Command {
return &cobra.Command{
Use: "info <name>",
Short: "Show PostgreSQL addon details",
Args: cobra.ExactArgs(1),
RunE: cmdutil.WithContext(cmdutil.RequireAuth(func(ctx *cmdutil.CommandContext, cmd *cobra.Command, args []string) error {
addon, err := findPostgresAddon(ctx, cmd, args[0])
if err != nil {
return err
}

if !ctx.Formatter.IsTable() {
return ctx.Formatter.PrintStructured(addon)
}

fmt.Printf("Name: %s\n", addon.Name)
fmt.Printf("Version: %d\n", addon.Spec.Version.Major)
fmt.Printf("Instances: %d\n", addon.Spec.Instances.Count)
fmt.Printf("Storage: %s\n", addon.Spec.Storage.Size)
fmt.Printf("State: %s\n", addonState(*addon))

if addon.Status != nil && addon.Status.ConnectionInfo != nil {
info := addon.Status.ConnectionInfo
if info.Host != nil {
fmt.Printf("Host: %s\n", *info.Host)
}
if info.Port != nil {
fmt.Printf("Port: %d\n", *info.Port)
}
if len(info.Databases) > 0 {
fmt.Println("\nDatabases:")
for _, db := range info.Databases {
fmt.Printf(" %s\n", db.GetName())
}
}
}
return nil
})),
}
}

func newPostgresDeleteCmd() *cobra.Command {
var flagYes bool

cmd := &cobra.Command{
Use: "delete <name>",
Short: "Delete a PostgreSQL addon",
Args: cobra.ExactArgs(1),
RunE: cmdutil.WithContext(cmdutil.RequireAuth(func(ctx *cmdutil.CommandContext, cmd *cobra.Command, args []string) error {
addon, err := findPostgresAddon(ctx, cmd, args[0])
if err != nil {
return err
}

if _, err := cmdutil.Confirm(ctx.Formatter, fmt.Sprintf("Delete postgres addon %q and all its data?", args[0]), flagYes); err != nil {
return err
}

if err := ctx.Client.DeletePostgresAddon(cmd.Context(), *addon.Id); err != nil {
return err
}

fmt.Fprintf(os.Stderr, "Postgres addon %q deleted.\n", args[0])
return nil
})),
}

cmd.Flags().BoolVarP(&flagYes, "yes", "y", false, "Skip confirmation")
return cmd
}

func newPostgresCredentialsCmd() *cobra.Command {
var flagSuperuser bool

cmd := &cobra.Command{
Use: "credentials <name> <database>",
Short: "Get just-in-time credentials for a database",
Args: cobra.ExactArgs(2),
RunE: cmdutil.WithContext(cmdutil.RequireAuth(func(ctx *cmdutil.CommandContext, cmd *cobra.Command, args []string) error {
addon, err := findPostgresAddon(ctx, cmd, args[0])
if err != nil {
return err
}

creds, err := ctx.Client.GetPostgresCredentials(cmd.Context(), *addon.Id, args[1], flagSuperuser)
if err != nil {
return err
}

if !ctx.Formatter.IsTable() {
return ctx.Formatter.PrintStructured(creds)
}

for _, field := range []struct {
label string
value *string
}{
{"Host", creds.Host},
{"Database", creds.Database},
{"Username", creds.Username},
{"Password", creds.Password},
{"SSL Mode", creds.SslMode},
{"URL", creds.ConnectionString},
} {
if field.value != nil && *field.value != "" {
fmt.Printf("%s: %s\n", field.label, *field.value)
}
}
if creds.Port != nil {
fmt.Printf("Port: %d\n", *creds.Port)
}
return nil
})),
}

cmd.Flags().BoolVar(&flagSuperuser, "superuser", false, "Request superuser credentials")
return cmd
}

func newPostgresBackupCmd() *cobra.Command {
var flagDescription string

cmd := &cobra.Command{
Use: "backup <name>",
Short: "Trigger an immediate backup",
Args: cobra.ExactArgs(1),
RunE: cmdutil.WithContext(cmdutil.RequireAuth(func(ctx *cmdutil.CommandContext, cmd *cobra.Command, args []string) error {
addon, err := findPostgresAddon(ctx, cmd, args[0])
if err != nil {
return err
}

resp, err := ctx.Client.BackupPostgresAddon(cmd.Context(), *addon.Id, flagDescription)
if err != nil {
return err
}

if !ctx.Formatter.IsTable() {
return ctx.Formatter.PrintStructured(resp)
}

fmt.Fprintf(os.Stderr, "Backup triggered for %q (%s).\n", args[0], resp.GetBackupId())
return nil
})),
}

cmd.Flags().StringVar(&flagDescription, "description", "", "Description for this backup")
return cmd
}

func newPostgresBackupsCmd() *cobra.Command {
return &cobra.Command{
Use: "backups <name>",
Short: "List backups of a PostgreSQL addon",
Args: cobra.ExactArgs(1),
RunE: cmdutil.WithContext(cmdutil.RequireAuth(func(ctx *cmdutil.CommandContext, cmd *cobra.Command, args []string) error {
addon, err := findPostgresAddon(ctx, cmd, args[0])
if err != nil {
return err
}

backups, err := ctx.Client.ListPostgresBackups(cmd.Context(), *addon.Id)
if err != nil {
return err
}

if !ctx.Formatter.IsTable() {
return ctx.Formatter.PrintStructured(backups)
}

if len(backups) == 0 {
fmt.Fprintln(os.Stderr, "No backups found.")
return nil
}

tbl := ctx.Formatter.NewTable("ID", "NAME", "TYPE", "PHASE", "STARTED")
for _, b := range backups {
started := "-"
if b.StartedAt != nil {
started = output.TimeAgo(*b.StartedAt)
}
tbl.AddRow(shortID(b.GetId()), b.GetName(), b.GetType(), b.GetPhase(), started)
}
tbl.Render()
return nil
})),
}
}

func findPostgresAddon(ctx *cmdutil.CommandContext, cmd *cobra.Command, name string) (*openapi.PostgresAddon, error) {
addon, err := ctx.Client.FindPostgresAddonByName(cmd.Context(), name)
if err != nil {
return nil, err
}
if addon == nil || addon.Id == nil {
return nil, clierrors.NotFoundError("Postgres addon", name)
}
return addon, nil
}

func addonState(a openapi.PostgresAddon) string {
if a.Status == nil || a.Status.State == nil {
return "Unknown"
}
state := *a.Status.State
switch state {
case "Ready", "Running":
return output.Green(state)
case "Failed", "Error":
return output.Red(state)
case "Pending", "Provisioning":
return output.Yellow(state)
default:
return state
}
}

func addonCreated(a openapi.PostgresAddon) string {
if a.CreatedAt == nil {
return "-"
}
return output.TimeAgo(*a.CreatedAt)
}
Loading