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
130 changes: 118 additions & 12 deletions cmd/asobi/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,18 @@ func main() {
cmdLogout()
case "whoami":
cmdWhoami()
case "create":
cmdCreate()
case "deploy":
cmdDeploy()
case "stop":
cmdStop()
case "start":
cmdStart()
case "delete":
cmdDelete()
case "envs":
cmdEnvs()
case "destroy":
cmdDestroy()
case "env":
Expand All @@ -52,18 +62,19 @@ func usage() {
fmt.Println(`asobi - Asobi game backend CLI

Usage:
asobi login Login via browser (device-code flow)
asobi logout Clear stored credentials
asobi whoami Show current credential info
asobi deploy <dir> Deploy Lua scripts to the engine
asobi deploy --ephemeral Create a fresh ephemeral env (1h TTL) + deploy
asobi destroy <env_id> Delete an environment and revoke its keys
asobi env list List environments for the current game
asobi env list --ephemeral List only ephemeral environments
asobi health Check engine health
asobi config set <k> <v> Set config (url, api_key, saas_url)
asobi config show Show current config
asobi help Show this help
asobi login Login via browser (device-code flow)
asobi logout Clear stored credentials
asobi whoami Show current credential info
asobi create <name> [--size xs|s|m|l] Create an environment
asobi deploy <name> [dir] Deploy Lua scripts to an environment
asobi stop <name> Stop an environment
asobi start <name> Start an environment
asobi delete <name> Delete an environment
asobi envs List your environments
asobi health Check engine health
asobi config set <k> <v> Set config (url, api_key, saas_url)
asobi config show Show current config
asobi help Show this help

Login options:
--saas-url <url> SaaS URL (default: ` + defaultSaasURL + `)
Expand Down Expand Up @@ -450,3 +461,98 @@ func cmdEnvList() {
fmt.Printf("%-40s %-20s %-10s %-10s %s\n", e.ID, e.Name, e.Status, eph, e.ExpiresAt)
}
}

// --- New environment commands ---

func cmdCreate() {
if len(os.Args) < 3 {
fatal("usage: asobi create <name> [--size xs|s|m|l]")
}
name := os.Args[2]
size := "xs"
for i := 3; i < len(os.Args); i++ {
if os.Args[i] == "--size" && i+1 < len(os.Args) {
size = os.Args[i+1]
i++
}
}

creds := mustLoadCreds()
result, err := auth.CreateEnv(creds, name, size)
if err != nil {
fatal("create: %v", err)
}
fmt.Printf("Environment created: %s (size: %s)\n", name, size)
if env, ok := result["environment"].(map[string]interface{}); ok {
if id, ok := env["id"].(string); ok {
fmt.Printf(" id: %s\n", id)
}
}
}

func cmdStop() {
if len(os.Args) < 3 {
fatal("usage: asobi stop <name>")
}
creds := mustLoadCreds()
if err := auth.EnvAction(creds, os.Args[2], "stop"); err != nil {
fatal("stop: %v", err)
}
fmt.Printf("Environment %s stopping\n", os.Args[2])
}

func cmdStart() {
if len(os.Args) < 3 {
fatal("usage: asobi start <name>")
}
creds := mustLoadCreds()
if err := auth.EnvAction(creds, os.Args[2], "start"); err != nil {
fatal("start: %v", err)
}
fmt.Printf("Environment %s starting\n", os.Args[2])
}

func cmdDelete() {
if len(os.Args) < 3 {
fatal("usage: asobi delete <name>")
}
creds := mustLoadCreds()
if err := auth.DeleteEnv(creds, os.Args[2]); err != nil {
fatal("delete: %v", err)
}
fmt.Printf("Environment %s deleted\n", os.Args[2])
}

func cmdEnvs() {
creds := mustLoadCreds()
envs, err := auth.ListEnvs2(creds)
if err != nil {
fatal("list: %v", err)
}
if len(envs) == 0 {
fmt.Println("No environments. Create one with: asobi create <name>")
return
}
fmt.Printf("%-20s %-6s %-15s %s\n", "NAME", "SIZE", "STATUS", "ENDPOINT")
for _, e := range envs {
name, _ := e["name"].(string)
size, _ := e["size"].(string)
if size == "" {
size, _ = e["resource_tier"].(string)
}
status, _ := e["provisioning_status"].(string)
endpoint, _ := e["endpoint_url"].(string)
if endpoint == "" {
endpoint = "-"
}
fmt.Printf("%-20s %-6s %-15s %s\n", name, strings.ToUpper(size), status, endpoint)
}
}

func mustLoadCreds() *auth.Credentials {
creds, err := auth.LoadCredentials()
if err != nil {
fatal("not logged in. Run: asobi login")
}
return creds
}
151 changes: 151 additions & 0 deletions internal/auth/saas.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,3 +237,154 @@ func ListEnvs(creds *Credentials, ephemeralOnly bool) ([]Environment, error) {
}
return result.Environments, nil
}

// CreateEnv creates a named environment with a given size.
func CreateEnv(creds *Credentials, name, size string) (map[string]interface{}, error) {
body, _ := json.Marshal(map[string]string{"name": name, "size": size})
req, err := http.NewRequest("POST", creds.SaasURL+"/internal/cli/envs/create", bytes.NewReader(body))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+creds.AccessToken)
resp, err := httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
data, _ := io.ReadAll(resp.Body)
if resp.StatusCode == 401 {
refreshed, err := RefreshAccessToken(creds)
if err != nil {
return nil, err
}
creds.AccessToken = refreshed
_ = SaveCredentials(creds)
return CreateEnv(creds, name, size)
}
var result map[string]interface{}
_ = json.Unmarshal(data, &result)
if resp.StatusCode >= 400 {
return nil, fmt.Errorf("create failed (%d): %s", resp.StatusCode, data)
}
return result, nil
}

// DeployBundle uploads a zip bundle to a named environment.
func DeployBundle(creds *Credentials, name string, bundle []byte) (map[string]interface{}, error) {
req, err := http.NewRequest("POST", creds.SaasURL+"/internal/cli/envs/"+name+"/deploy", bytes.NewReader(bundle))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/zip")
req.Header.Set("Authorization", "Bearer "+creds.AccessToken)
resp, err := httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
data, _ := io.ReadAll(resp.Body)
if resp.StatusCode == 401 {
refreshed, err := RefreshAccessToken(creds)
if err != nil {
return nil, err
}
creds.AccessToken = refreshed
_ = SaveCredentials(creds)
return DeployBundle(creds, name, bundle)
}
var result map[string]interface{}
_ = json.Unmarshal(data, &result)
if resp.StatusCode >= 400 {
return nil, fmt.Errorf("deploy failed (%d): %s", resp.StatusCode, data)
}
return result, nil
}

// EnvAction performs stop/start on a named environment.
func EnvAction(creds *Credentials, name, action string) error {
req, err := http.NewRequest("POST", creds.SaasURL+"/internal/cli/envs/"+name+"/"+action, nil)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+creds.AccessToken)
resp, err := httpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode == 401 {
refreshed, err := RefreshAccessToken(creds)
if err != nil {
return err
}
creds.AccessToken = refreshed
_ = SaveCredentials(creds)
return EnvAction(creds, name, action)
}
if resp.StatusCode >= 400 {
data, _ := io.ReadAll(resp.Body)
return fmt.Errorf("%s failed (%d): %s", action, resp.StatusCode, data)
}
return nil
}

// DeleteEnv deletes a named environment.
func DeleteEnv(creds *Credentials, name string) error {
req, err := http.NewRequest("DELETE", creds.SaasURL+"/internal/cli/envs/"+name, nil)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+creds.AccessToken)
resp, err := httpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode == 401 {
refreshed, err := RefreshAccessToken(creds)
if err != nil {
return err
}
creds.AccessToken = refreshed
_ = SaveCredentials(creds)
return DeleteEnv(creds, name)
}
if resp.StatusCode >= 400 {
data, _ := io.ReadAll(resp.Body)
return fmt.Errorf("delete failed (%d): %s", resp.StatusCode, data)
}
return nil
}

// ListEnvs2 returns environments using the new flat model.
func ListEnvs2(creds *Credentials) ([]map[string]interface{}, error) {
req, err := http.NewRequest("GET", creds.SaasURL+"/internal/cli/envs", nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+creds.AccessToken)
resp, err := httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
data, _ := io.ReadAll(resp.Body)
if resp.StatusCode == 401 {
refreshed, err := RefreshAccessToken(creds)
if err != nil {
return nil, err
}
creds.AccessToken = refreshed
_ = SaveCredentials(creds)
return ListEnvs2(creds)
}
var result struct {
Environments []map[string]interface{} `json:"environments"`
}
_ = json.Unmarshal(data, &result)
if resp.StatusCode >= 400 {
return nil, fmt.Errorf("list failed (%d): %s", resp.StatusCode, data)
}
return result.Environments, nil
}
Loading