Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
24 changes: 24 additions & 0 deletions internal/assist/prompt.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,14 @@ dhq signup — Create a new DeployHQ account
Projects:
dhq projects list|show|create|update|delete|star|insights
dhq projects update <permalink> --name|--permalink|--zone|--email-notify-on|--notification-email|--notify-pusher|--check-undeployed-changes|--store-artifacts
dhq projects regenerate-key <project> [--key-type ED25519|RSA] (regenerates the SSH deploy key; prints the new public key)
dhq projects undeployed-changes <project> (commits not yet deployed)
dhq projects ai-overview <project> --end-ref <rev> [--start-ref <rev>] (AI summary of changes)

Servers & Groups:
dhq servers list|show|create|update|delete|reset-host-key -p <project>
dhq servers from-global <global-server-id> -p <project> (add a global server template to a project)
dhq servers metrics <server-id> -p <project> (point-in-time metrics snapshot; beta, SSH only, use --json)
dhq server-groups list|show|create|update|delete -p <project>

Deployments:
Expand All @@ -105,6 +110,7 @@ Configuration:
dhq env-vars list|show|create|update|delete -p <project>
dhq global-env-vars list|show|create|update|delete
dhq config-files list|show|create|update|delete -p <project>
dhq config-files link-global|unlink-global <config-file-id> -p <project> (link/unlink an account-wide config file)
dhq excluded-files list|show|create|update|delete -p <project>

Build Pipeline:
Expand All @@ -114,6 +120,7 @@ dhq language-versions list -p <project> (alias: dhq lv list)

SSH & Deployment Commands:
dhq ssh-commands list|show|create|update|delete -p <project>
dhq ssh-commands link-global|unlink-global <command-id> -p <project> (link/unlink an account-wide SSH command)
dhq ssh-keys list|create|delete

Integrations & Automation:
Expand All @@ -123,12 +130,29 @@ dhq scheduled-deploys list|show|create|update|delete -p <project>

Templates:
dhq templates list|show|public|public-show|create|update|delete
dhq templates config-files|servers list|show|create|update|delete -t <template> (full CRUD)
dhq templates excluded-files|integrations|commands|build-commands|build-cache-files|server-groups list|create|update|delete -t <template> (no show)
dhq templates build-known-hosts list|create|delete -t <template> (no update)
dhq templates build-languages set <package> --version <v> -t <template>
dhq templates build-configuration -t <template> (show only)

Managed Hosting:
dhq hosted-resources list|show|sync|retry-provision
dhq managed-hosting regions|sizes
dhq beta enroll [--protocol managed_vps|static_hosting] (enroll the account in the managed-resources beta)

Account Resources:
dhq agents list|create|update|delete|revoke
dhq global-servers list|show|create|update|delete|copy-to-project
dhq invoices list (billing history; billing-manager key required)
dhq invoices download <number> (download an invoice PDF; billing-manager key required)
dhq zones list

Utilities & Info:
dhq ip-ranges (DeployHQ IP ranges/ports for firewall allowlisting)
dhq plans (subscription plans and pricing; alias: pricing)
dhq detect [path] (detect a project's framework and suggested deploy config)

Dashboard & Activity:
dhq status — Quick dashboard (deploy stats + recent activity)
dhq activity list|stats — Account-wide activity feed and deploy metrics
Expand Down
63 changes: 53 additions & 10 deletions internal/cli/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ type Context struct {
Config *config.Config
Envelope *output.Envelope
Logger *output.Logger
IsAgent bool // true when invoked by an AI agent
Version string
IsAgent bool // true when invoked by an AI agent
Version string

// Lazy-initialized API client (only when a command needs it)
client *sdk.Client
Expand Down Expand Up @@ -88,6 +88,56 @@ func (c *Context) Client() (*sdk.Client, error) {
return nil, err
}

client, err := sdk.New(account, email, apiKey, c.clientOpts(account)...)
if err != nil {
return nil, fmt.Errorf("create api client: %w", err)
}

c.client = client
return c.client, nil
}

// PublicClient returns an SDK client for the public, no-auth endpoints
// (e.g. `dhq ip-ranges`, `dhq plans`). These endpoints return 200 without
// credentials, so email/apiKey are not required — but an account is still
// needed to know which subdomain to target.
//
// The account is resolved from config (flags/env/files) and, if absent
// there, from the stored auth credentials. It is NOT cached on the Context
// (that cache is reserved for the authenticated client).
func (c *Context) PublicClient() (*sdk.Client, error) {
account, err := c.publicAccount()
if err != nil {
return nil, err
}

client, err := sdk.NewPublic(account, c.clientOpts(account)...)
if err != nil {
return nil, fmt.Errorf("create api client: %w", err)
}
return client, nil
}

// publicAccount resolves just the account subdomain for public endpoints,
// without requiring email/apiKey. A public endpoint still needs to know
// which account's subdomain to hit.
func (c *Context) publicAccount() (string, error) {
if c.Config.Account != "" {
return c.Config.Account, nil
}
// Fall back to any stored auth account, if available.
if creds, err := auth.LoadByAccount(""); err == nil && creds.Account != "" {
return creds.Account, nil
}
return "", &output.UserError{
Message: "Account not configured",
Hint: "This is a public endpoint, but it still needs an account subdomain to query.\nSet via --account flag, DEPLOYHQ_ACCOUNT env var, or 'dhq config set account <name>'",
}
}

// clientOpts builds the shared SDK options (user agent, base URL) used by
// both the authenticated and public clients.
func (c *Context) clientOpts(account string) []sdk.Option {
opts := []sdk.Option{}
agent := harness.AgentInfo{}
if c.IsAgent {
Expand All @@ -102,14 +152,7 @@ func (c *Context) Client() (*sdk.Client, error) {
if baseURL := c.Config.BaseURL(account); baseURL != "" {
opts = append(opts, sdk.WithBaseURL(baseURL))
}

client, err := sdk.New(account, email, apiKey, opts...)
if err != nil {
return nil, fmt.Errorf("create api client: %w", err)
}

c.client = client
return c.client, nil
return opts
}

// RequireProject returns the project identifier, or a UserError if not set.
Expand Down
136 changes: 129 additions & 7 deletions internal/commands/agent_metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ var commandMetadataTable = map[string]AgentMetadata{
ResourceTypes: []string{"deployment"},
},
"dhq retry": {
Idempotent: false,
Idempotent: false,
SupportsJSON: true, SafeForAutomation: true,
ResourceTypes: []string{"deployment"},
},
Expand Down Expand Up @@ -122,7 +122,7 @@ var commandMetadataTable = map[string]AgentMetadata{
},
"dhq env-vars create": {
Interactive: true, // prompts for value if --value omitted
Idempotent: false, SupportsJSON: true, SafeForAutomation: true,
Idempotent: false, SupportsJSON: true, SafeForAutomation: true,
ResourceTypes: []string{"env_var"},
},
"dhq env-vars delete": {
Expand Down Expand Up @@ -165,23 +165,23 @@ var commandMetadataTable = map[string]AgentMetadata{
Idempotent: true, SupportsJSON: false, SafeForAutomation: true,
},
"dhq init": {
Interactive: true,
Interactive: true,
SupportsJSON: false, SafeForAutomation: false,
},
"dhq hello": {
Interactive: true,
Interactive: true,
SupportsJSON: false, SafeForAutomation: false,
},
"dhq configure": {
Interactive: true,
Interactive: true,
SupportsJSON: false, SafeForAutomation: false,
},
"dhq signup": {
Interactive: true,
Interactive: true,
SupportsJSON: true, SafeForAutomation: true,
},
"dhq mcp": {
Interactive: true,
Interactive: true,
SupportsJSON: false, SafeForAutomation: false,
},

Expand Down Expand Up @@ -280,6 +280,128 @@ var commandMetadataTable = map[string]AgentMetadata{
Idempotent: true, SupportsJSON: true, SafeForAutomation: true,
ResourceTypes: []string{"template"},
},

// Hosted resources (Managed VPS + Static Hosting lifecycle)
"dhq hosted-resources list": {
Idempotent: true, SupportsJSON: true, SafeForAutomation: true,
ResourceTypes: []string{"hosted_resource"},
},
"dhq hosted-resources show": {
Idempotent: true, SupportsJSON: true, SafeForAutomation: true,
ResourceTypes: []string{"hosted_resource"},
},
"dhq hosted-resources sync": {
Idempotent: true, SupportsJSON: true, SafeForAutomation: true,
ResourceTypes: []string{"hosted_resource"},
},
"dhq hosted-resources retry-provision": {
// Only valid when the resource is in the error state; not blindly retry-safe.
Idempotent: false, SupportsJSON: true, SafeForAutomation: false,
ResourceTypes: []string{"hosted_resource"},
},

// Managed hosting catalog (read-only)
"dhq managed-hosting regions": {
Idempotent: true, SupportsJSON: true, SafeForAutomation: true,
ResourceTypes: []string{"managed_hosting_region"},
},
"dhq managed-hosting sizes": {
Idempotent: true, SupportsJSON: true, SafeForAutomation: true,
ResourceTypes: []string{"managed_hosting_size"},
},

// Template sub-resources (all take -t <template>)
"dhq templates config-files list": {Idempotent: true, SupportsJSON: true, SafeForAutomation: true, ResourceTypes: []string{"template"}},
"dhq templates config-files show": {Idempotent: true, SupportsJSON: true, SafeForAutomation: true, ResourceTypes: []string{"template"}},
"dhq templates config-files create": {SupportsJSON: true, SafeForAutomation: true, ResourceTypes: []string{"template"}},
"dhq templates config-files update": {Idempotent: true, SupportsJSON: true, SafeForAutomation: true, ResourceTypes: []string{"template"}},
"dhq templates config-files delete": {Destructive: true, RequiresConfirmation: true, SupportsJSON: true, SafeForAutomation: true, ResourceTypes: []string{"template"}},

"dhq templates excluded-files list": {Idempotent: true, SupportsJSON: true, SafeForAutomation: true, ResourceTypes: []string{"template"}},
"dhq templates excluded-files create": {SupportsJSON: true, SafeForAutomation: true, ResourceTypes: []string{"template"}},
"dhq templates excluded-files update": {Idempotent: true, SupportsJSON: true, SafeForAutomation: true, ResourceTypes: []string{"template"}},
"dhq templates excluded-files delete": {Destructive: true, RequiresConfirmation: true, SupportsJSON: true, SafeForAutomation: true, ResourceTypes: []string{"template"}},

"dhq templates integrations list": {Idempotent: true, SupportsJSON: true, SafeForAutomation: true, ResourceTypes: []string{"template"}},
"dhq templates integrations create": {SupportsJSON: true, SafeForAutomation: true, ResourceTypes: []string{"template"}},
"dhq templates integrations update": {Idempotent: true, SupportsJSON: true, SafeForAutomation: true, ResourceTypes: []string{"template"}},
"dhq templates integrations delete": {Destructive: true, RequiresConfirmation: true, SupportsJSON: true, SafeForAutomation: true, ResourceTypes: []string{"template"}},

"dhq templates commands list": {Idempotent: true, SupportsJSON: true, SafeForAutomation: true, ResourceTypes: []string{"template"}},
"dhq templates commands create": {SupportsJSON: true, SafeForAutomation: true, ResourceTypes: []string{"template"}},
"dhq templates commands update": {Idempotent: true, SupportsJSON: true, SafeForAutomation: true, ResourceTypes: []string{"template"}},
"dhq templates commands delete": {Destructive: true, RequiresConfirmation: true, SupportsJSON: true, SafeForAutomation: true, ResourceTypes: []string{"template"}},

"dhq templates build-commands list": {Idempotent: true, SupportsJSON: true, SafeForAutomation: true, ResourceTypes: []string{"template"}},
"dhq templates build-commands create": {SupportsJSON: true, SafeForAutomation: true, ResourceTypes: []string{"template"}},
"dhq templates build-commands update": {Idempotent: true, SupportsJSON: true, SafeForAutomation: true, ResourceTypes: []string{"template"}},
"dhq templates build-commands delete": {Destructive: true, RequiresConfirmation: true, SupportsJSON: true, SafeForAutomation: true, ResourceTypes: []string{"template"}},

"dhq templates build-cache-files list": {Idempotent: true, SupportsJSON: true, SafeForAutomation: true, ResourceTypes: []string{"template"}},
"dhq templates build-cache-files create": {SupportsJSON: true, SafeForAutomation: true, ResourceTypes: []string{"template"}},
"dhq templates build-cache-files update": {Idempotent: true, SupportsJSON: true, SafeForAutomation: true, ResourceTypes: []string{"template"}},
"dhq templates build-cache-files delete": {Destructive: true, RequiresConfirmation: true, SupportsJSON: true, SafeForAutomation: true, ResourceTypes: []string{"template"}},

"dhq templates build-known-hosts list": {Idempotent: true, SupportsJSON: true, SafeForAutomation: true, ResourceTypes: []string{"template"}},
"dhq templates build-known-hosts create": {SupportsJSON: true, SafeForAutomation: true, ResourceTypes: []string{"template"}},
"dhq templates build-known-hosts delete": {Destructive: true, RequiresConfirmation: true, SupportsJSON: true, SafeForAutomation: true, ResourceTypes: []string{"template"}},

"dhq templates build-languages set": {Idempotent: true, SupportsJSON: true, SafeForAutomation: true, ResourceTypes: []string{"template"}},

"dhq templates build-configuration": {Idempotent: true, SupportsJSON: true, SafeForAutomation: true, ResourceTypes: []string{"template"}},

"dhq templates servers list": {Idempotent: true, SupportsJSON: true, SafeForAutomation: true, ResourceTypes: []string{"template", "server"}},
"dhq templates servers show": {Idempotent: true, SupportsJSON: true, SafeForAutomation: true, ResourceTypes: []string{"template", "server"}},
"dhq templates servers create": {SupportsJSON: true, SafeForAutomation: true, ResourceTypes: []string{"template", "server"}},
"dhq templates servers update": {Idempotent: true, SupportsJSON: true, SafeForAutomation: true, ResourceTypes: []string{"template", "server"}},
"dhq templates servers delete": {Destructive: true, RequiresConfirmation: true, SupportsJSON: true, SafeForAutomation: true, ResourceTypes: []string{"template", "server"}},

"dhq templates server-groups list": {Idempotent: true, SupportsJSON: true, SafeForAutomation: true, ResourceTypes: []string{"template"}},
"dhq templates server-groups create": {SupportsJSON: true, SafeForAutomation: true, ResourceTypes: []string{"template"}},
"dhq templates server-groups update": {Idempotent: true, SupportsJSON: true, SafeForAutomation: true, ResourceTypes: []string{"template"}},
"dhq templates server-groups delete": {Destructive: true, RequiresConfirmation: true, SupportsJSON: true, SafeForAutomation: true, ResourceTypes: []string{"template"}},

// Phase 4 — project & server actions
"dhq projects regenerate-key": {
// Invalidates the existing deploy key — servers must be updated with the new one.
Destructive: true, RequiresConfirmation: true,
SupportsJSON: true, SafeForAutomation: false,
ResourceTypes: []string{"project"},
},
"dhq projects undeployed-changes": {
Idempotent: true, SupportsJSON: true, SafeForAutomation: true,
ResourceTypes: []string{"project"},
},
"dhq projects ai-overview": {
Idempotent: true, SupportsJSON: true, SafeForAutomation: true,
ResourceTypes: []string{"project"},
},
"dhq servers from-global": {
SupportsJSON: true, SafeForAutomation: true,
ResourceTypes: []string{"server"},
},
"dhq servers metrics": {
Idempotent: true, SupportsJSON: true, SafeForAutomation: true,
ResourceTypes: []string{"server"},
},
"dhq config-files link-global": {Idempotent: true, SupportsJSON: true, SafeForAutomation: true, ResourceTypes: []string{"config_file"}},
"dhq config-files unlink-global": {Destructive: true, Idempotent: true, SupportsJSON: true, SafeForAutomation: true, ResourceTypes: []string{"config_file"}},
"dhq ssh-commands link-global": {Idempotent: true, SupportsJSON: true, SafeForAutomation: true, ResourceTypes: []string{"ssh_command"}},
"dhq ssh-commands unlink-global": {Destructive: true, Idempotent: true, SupportsJSON: true, SafeForAutomation: true, ResourceTypes: []string{"ssh_command"}},

// Phase 7 — long-tail resources
"dhq ip-ranges": {Idempotent: true, SupportsJSON: true, SafeForAutomation: true, ResourceTypes: []string{"ip_range"}},
"dhq plans": {Idempotent: true, SupportsJSON: true, SafeForAutomation: true, ResourceTypes: []string{"package"}},
"dhq invoices list": {Idempotent: true, SupportsJSON: true, SafeForAutomation: true, ResourceTypes: []string{"invoice"}},
"dhq invoices download": {Idempotent: true, SupportsJSON: false, SafeForAutomation: true, ResourceTypes: []string{"invoice"}},
"dhq detect": {Idempotent: true, SupportsJSON: true, SafeForAutomation: true},
"dhq beta enroll": {
// Admin-gated (403 for non-admins); re-enrolling is a no-op.
Idempotent: true, RequiresConfirmation: true,
SupportsJSON: true, SafeForAutomation: true,
ResourceTypes: []string{"beta_enrollment"},
},

"dhq zones list": {
Idempotent: true, SupportsJSON: true, SafeForAutomation: true,
ResourceTypes: []string{"zone"},
Expand Down
76 changes: 76 additions & 0 deletions internal/commands/beta.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package commands

import (
"errors"
"fmt"
"net/http"

"github.com/deployhq/deployhq-cli/internal/output"
"github.com/deployhq/deployhq-cli/pkg/sdk"
"github.com/spf13/cobra"
)

func newBetaCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "beta",
Short: "Manage DeployHQ beta enrollment",
Long: `Enroll your account in DeployHQ's managed-resources beta (Static Hosting and
Managed VPS). Enrollment can only be flipped on by an account admin.`,
}
cmd.AddCommand(newBetaEnrollCmd())
return cmd
}

func newBetaEnrollCmd() *cobra.Command {
var protocol string
cmd := &cobra.Command{
Use: "enroll",
Short: "Enroll the account in the managed-resources beta",
Long: `Enroll your account in the managed-resources beta.

By default enrolls in the managed_vps protocol; pass --protocol static_hosting to
enroll in Static Hosting instead. Requires an account admin — non-admins receive a
403 with a link to enable the beta in the dashboard.`,
RunE: func(cmd *cobra.Command, args []string) error {
client, err := cliCtx.Client()
if err != nil {
return err
}
env := cliCtx.Envelope
resp, err := client.EnrollBeta(cliCtx.Background(), protocol)
if err != nil {
var apiErr *sdk.APIError
if errors.As(err, &apiErr) {
betaURL := fmt.Sprintf("https://app.deployhq.com/%s/beta_features", client.Account())
switch apiErr.StatusCode {
case http.StatusForbidden:
return &output.UserError{
Message: "Beta enrollment requires an account admin. Ask an admin to enable it at: " + betaURL,
}
case http.StatusUnprocessableEntity:
return &output.UserError{
Message: fmt.Sprintf("Unsupported protocol %q. Use managed_vps or static_hosting.", protocol),
}
}
}
return err
}
if env.WantsJSON() {
summary := "not enrolled"
if resp.Enrolled {
summary = "enrolled"
}
return env.WriteJSON(output.NewResponse(resp, summary))
}
if resp.Enrolled {
output.ColorGreen.Fprintf(env.Stderr, "Enrolled in the managed-resources beta.\n") //nolint:errcheck
} else {
env.Status("Enrollment request sent, but the account is not yet enrolled.")
}
env.Status("\nTip: dhq launch to provision Static Hosting or a Managed VPS")
return nil
},
}
cmd.Flags().StringVar(&protocol, "protocol", "managed_vps", "Protocol to enroll in: managed_vps or static_hosting")
return cmd
}
Loading
Loading