Skip to content
Merged
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
41 changes: 41 additions & 0 deletions clients/api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -386,3 +386,44 @@ func (c *Client) GetDemoResource(orgID string) (*GetDemoResourceResponse, error)
}
return &resp, nil
}

// --- Environment endpoints ---

// GetApplicationEnvironment retrieves the user's current environment choice for an application
func (c *Client) GetApplicationEnvironment(applicationID string) (*GetApplicationEnvironmentResponse, error) {
path := fmt.Sprintf("/application/%s/environment", applicationID)

var resp GetApplicationEnvironmentResponse
err := c.doRequest("GET", path, nil, &resp)
if err != nil {
return nil, err
}
return &resp, nil
}

// ListApplicationEnvironments retrieves all available environments for an application
func (c *Client) ListApplicationEnvironments(applicationID string) (*ListEnvironmentsResponse, error) {
path := fmt.Sprintf("/application/%s/environments", applicationID)

var resp ListEnvironmentsResponse
err := c.doRequest("GET", path, nil, &resp)
if err != nil {
return nil, err
}
return &resp, nil
}

// SetApplicationEnvironment sets the user's environment choice for an application
func (c *Client) SetApplicationEnvironment(applicationID, environmentID string) (*SetEnvironmentChoiceResponse, error) {
path := fmt.Sprintf("/application/%s/environment", applicationID)
req := SetEnvironmentChoiceRequest{
EnvironmentID: environmentID,
}

var resp SetEnvironmentChoiceResponse
err := c.doRequest("POST", path, req, &resp)
if err != nil {
return nil, err
}
return &resp, nil
}
34 changes: 34 additions & 0 deletions clients/api/structs.go
Original file line number Diff line number Diff line change
Expand Up @@ -255,3 +255,37 @@ type GetDemoResourceResponse struct {
Error *AppErrorDetail `json:"error,omitempty"`
Resource *ResourceItem `json:"resource,omitempty"`
}

// --- Environment structs ---

// EnvironmentItem represents a single environment
type EnvironmentItem struct {
ID string `json:"id"`
Name string `json:"name"`
IsDefault bool `json:"isDefault"`
}

// GetApplicationEnvironmentResponse represents the response from GET /application/:applicationId/environment
type GetApplicationEnvironmentResponse struct {
Error *AppErrorDetail `json:"error,omitempty"`
EnvironmentID *string `json:"environmentId,omitempty"`
EnvironmentName *string `json:"environmentName,omitempty"`
}

// ListEnvironmentsResponse represents the response from GET /application/:applicationId/environments
type ListEnvironmentsResponse struct {
Error *AppErrorDetail `json:"error,omitempty"`
Environments []EnvironmentItem `json:"environments,omitempty"`
}

// SetEnvironmentChoiceRequest represents the request body for POST /application/:applicationId/environment
type SetEnvironmentChoiceRequest struct {
EnvironmentID string `json:"environmentId"`
}

// SetEnvironmentChoiceResponse represents the response from POST /application/:applicationId/environment
type SetEnvironmentChoiceResponse struct {
Error *AppErrorDetail `json:"error,omitempty"`
EnvironmentID string `json:"environmentId,omitempty"`
EnvironmentName string `json:"environmentName,omitempty"`
}
6 changes: 3 additions & 3 deletions cmd/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ var Cmd = &cobra.Command{

func init() {
// Add app subcommands
Cmd.AddCommand(cloneCmd)
Cmd.AddCommand(configureCmd)
Cmd.AddCommand(createCmd)
Cmd.AddCommand(deployCmd)
Cmd.AddCommand(infoCmd)
Cmd.AddCommand(startCmd)
Cmd.AddCommand(deployCmd)
Cmd.AddCommand(configureCmd)
Cmd.AddCommand(cloneCmd)
}
203 changes: 203 additions & 0 deletions cmd/resource/env.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
package resource

import (
"fmt"

"github.com/charmbracelet/huh"
"github.com/charmbracelet/lipgloss"
"github.com/major-technology/cli/clients/api"
"github.com/major-technology/cli/errors"
"github.com/major-technology/cli/middleware"
"github.com/major-technology/cli/singletons"
"github.com/major-technology/cli/utils"
"github.com/spf13/cobra"
)

// envCmd represents the env command
var envCmd = &cobra.Command{
Use: "env",
Short: "View and change the environment for this application",
Long: `View your current environment selection and switch between available environments.`,
PreRunE: middleware.Compose(
middleware.CheckLogin,
),
RunE: func(cobraCmd *cobra.Command, args []string) error {
return runEnv(cobraCmd)
},
}

func runEnv(cobraCmd *cobra.Command) error {
// Get application info from current directory
appInfo, err := utils.GetApplicationInfo("")
if err != nil {
return errors.WrapError("failed to identify application", err)
}

// Get the API client
apiClient := singletons.GetAPIClient()

// Fetch current environment
currentEnvResp, err := apiClient.GetApplicationEnvironment(appInfo.ApplicationID)
if err != nil {
return errors.WrapError("failed to get current environment", err)
}

// Fetch all available environments
envListResp, err := apiClient.ListApplicationEnvironments(appInfo.ApplicationID)
if err != nil {
return errors.WrapError("failed to list environments", err)
}

if len(envListResp.Environments) == 0 {
return &errors.CLIError{
Title: "No environments available",
Suggestion: "Your organization doesn't have any environments configured.",
}
}

// If only one environment, just show current and exit
if len(envListResp.Environments) == 1 {
printCurrentEnvironment(cobraCmd, currentEnvResp)
cobraCmd.Println("\nOnly one environment is available for this application.")
return nil
}

// Let user select a new environment
selectedEnv, err := selectEnvironment(cobraCmd, envListResp.Environments, currentEnvResp.EnvironmentID)
if err != nil {
return errors.WrapError("failed to select environment", err)
}

// Set the new environment
setResp, err := apiClient.SetApplicationEnvironment(appInfo.ApplicationID, selectedEnv.ID)
if err != nil {
return errors.WrapError("failed to set environment", err)
}

// Print success
printEnvironmentChanged(cobraCmd, setResp.EnvironmentName)

return nil
}

// printCurrentEnvironment displays the current environment in a styled box
func printCurrentEnvironment(cobraCmd *cobra.Command, envResp *api.GetApplicationEnvironmentResponse) {
// Styles
titleStyle := lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.Color("12")) // Blue

envNameStyle := lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.Color("10")) // Green

descStyle := lipgloss.NewStyle().
Foreground(lipgloss.Color("8")). // Gray
Italic(true)

boxStyle := lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
BorderForeground(lipgloss.Color("12")).
Padding(1, 2).
MarginTop(1).
MarginBottom(1)

// Build content
title := titleStyle.Render("Current Environment")

envName := "Not set"
if envResp.EnvironmentName != nil {
envName = *envResp.EnvironmentName
}
currentEnv := envNameStyle.Render(envName)

description := descStyle.Render(
"Switching environments changes where your application\n" +
"connects to—both locally and in the Major web app.\n" +
"Each environment can have different resources and\n" +
"configuration values.",
)

content := fmt.Sprintf("%s\n\n%s\n\n%s", title, currentEnv, description)
box := boxStyle.Render(content)

cobraCmd.Println(box)
}

// selectEnvironment prompts the user to select an environment from the list
func selectEnvironment(cobraCmd *cobra.Command, envs []api.EnvironmentItem, currentEnvID *string) (*api.EnvironmentItem, error) {
// Print explanation
descStyle := lipgloss.NewStyle().
Foreground(lipgloss.Color("8")). // Gray
Italic(true).
MarginBottom(1)

cobraCmd.Println()
cobraCmd.Println(descStyle.Render(
"Switching environments changes where your application connects to—\n" +
"both locally and in dev mode on the Major web app.",
))
cobraCmd.Println()

// Create options for huh select
options := make([]huh.Option[string], len(envs))
for i, env := range envs {
displayName := env.Name
if currentEnvID != nil && env.ID == *currentEnvID {
displayName += " ← current"
}
options[i] = huh.NewOption(displayName, env.ID)
}

var selectedID string

// Set initial value to current environment if available
if currentEnvID != nil {
selectedID = *currentEnvID
}

// Create and run the select form
form := huh.NewForm(
huh.NewGroup(
huh.NewSelect[string]().
Title("Select an environment").
Options(options...).
Value(&selectedID),
),
)

if err := form.Run(); err != nil {
return nil, errors.WrapError("failed to get selection", err)
}

// Find the selected environment
for i, env := range envs {
if env.ID == selectedID {
return &envs[i], nil
}
}

return nil, fmt.Errorf("selected environment not found")
}

// printEnvironmentChanged displays a success message after switching environments
func printEnvironmentChanged(cobraCmd *cobra.Command, newEnvName string) {
successStyle := lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.Color("10")). // Green
MarginTop(1)

envNameStyle := lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.Color("14")) // Cyan

tipStyle := lipgloss.NewStyle().
Foreground(lipgloss.Color("8")). // Gray
Italic(true).
MarginTop(1)

cobraCmd.Println(successStyle.Render("✓ Environment switched successfully!"))
cobraCmd.Println()
cobraCmd.Printf("Now using: %s\n", envNameStyle.Render(newEnvName))
cobraCmd.Println(tipStyle.Render("Run 'major app start' to use the new environment locally."))
}
5 changes: 0 additions & 5 deletions cmd/resource/manage.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,3 @@ func runManage(cobraCmd *cobra.Command) error {

return nil
}

func init() {
// Add manage subcommand
Cmd.AddCommand(manageCmd)
}
2 changes: 2 additions & 0 deletions cmd/resource/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,6 @@ var Cmd = &cobra.Command{
func init() {
// Add resource subcommands
Cmd.AddCommand(createCmd)
Cmd.AddCommand(manageCmd)
Cmd.AddCommand(envCmd)
}